[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解
Posted GoldenaArcher
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解相关的知识,希望对你有一定的参考价值。
[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解
看完这篇教程,你应该可以:
- 使用 React 脚手架新建一个项目
- 了解 React 的项目结构
- 编写 React 代码
- 使用 React 渲染一个静态页面
原生的项目是之前使用 html/CSS 完成的学成在线页面,视屏展示在这里:
学成网首页 - 静态页面展示
本篇主旨就是使用 React 去重构整个静态页面,达成一样的实现效果。
另外,虽然字数有2w5,但是很大一部分是代码。
准备工作
工欲善其事,必先利其器。
在开始写代码之前,请确认一下必须的工具是否安装完毕了。
安装必备工具/库
nodejs
nodejs 的安装还是非常简单的,直接去官网上下载对应平台的安装包即可。
安装完毕后查看 nodejs 是否安装成功:
# 查看node版本
$ node -v
$ v14.17.0
# 查看npm版本
$ npm -v
$ 6.14.13
React 脚手架
React 官方提供的脚手架,可以直接初始化一个可以运行的 React 项目,并且不需要手动配置。对于学习项目来说,是再合适不过的工具了。
具体安装方法如下,在终端中输入下面的命令:
$ pushd D:\\front\\react
# 假设你想到D盘下,front文件夹中的react文件夹里去新建项目
$ npx create-react-app my-app # 会在当前目录下新建一个名为 my-app 的文件夹
$ cd my-app # 进入文件夹,里面所有的东西都已经配置好了,可以直接启动项目
$ npm start # 开始项目
这时候项目应该就能启动了,能看到一个初始化的的页面,上面会有一个不断旋转的 React Logo。
需要的 node 依赖包
目前不会涉及数据的处理,因此只需要一个包:react-router-dom。
先在命令行按 ctrl + c
停止运行,随后输入安装依赖包。等待安装完成后,重新开启项目:
# 安装依赖包
$ npm install --save react-router-dom
# 安装完成后,重新开启项目
$ npm start
分析需求
首先分析一下业务需求,根据 PSD/视频 得知,这个项目必须要有四个页面:
-
首页
-
总课程页面
渲染了所有的课程的页面
-
子课程页面
渲染单独一个课程的页面
-
职业规划
这么一来,先搭建基础的框架,新建 2 个文件夹,1 个教 components ,1 个叫 containers,其中包含 4 个文件夹,每个文件夹分别对应上面的页面。这相当于是约定俗称的一件事情,大部分的项目都会将组件结合起来的页面放入 containers 之中交由路由去渲染,components 则负责对应页面的组件。
随后,再看看有没有什么模块是可以被重复使用的。
这些页面上大部分的模块都是比较具有唯一性的,会在页面中复用,但是不会跨页面复用。这一部分的内容就放到 components 文件夹中去实现。最后再加上一个专门管理路由的文件夹。
乍一看,而会被跨页面复用的,只有下面四个模块:
- header
- footer
- banner
- course-item
所以,新建一个 common 文件夹放会被重复调用的内容,目前的项目结构就是这样的:
初始化项目
结构搭好了,现在就开始往里面填充内容了。
搭建框架
这一块的目的是先清理一下初始化的代码,并且改为当前项目所需要的的实现。
根目录
因为在这一步还没有实现 Routes 组件所以会引起报错。但是不要紧,下面马上就将 Routes 怎么实现了。
index.js
清理掉其他不打算用的部分,引入 app,也就是主程序
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
App.js
引入 Routes,使得页面可以按照 url 被访问到
import './App.css';
import Routes from './router/routes';
function App() {
return (
<div className="App">
<Routes />
</div>
);
}
export default App;
components
先创建 4 个空的文件夹/容器,之后再开始实现具体 UI
- home
- careerPath
- courses
- course
containers
先加一个测试用的字符串,判断路由是否成功,每一个容器下的代码,除了 div 中包含的字符串不同之外,其他结构完全一致
home/index.js
import React from 'react';
const Home = () => {
return <div>home</div>;
};
export default Home;
careerPath/index.js
import React from 'react';
const CareerPath = () => {
return <div>career-path</div>;
};
export default CareerPath;
courses/index.js
import React from 'react';
const Courses = () => {
return <div>courses</div>;
};
export default Courses;
-
course/index.js
import React from 'react'; const Course = () => { return <div>course</div>; }; export default Course;
router
加入路由,使得 url 能够与对应的页面组件进行联动。
-
Switch 是 react-router-dom 内部封装好的一个组件,会从被 Switch 包裹中的页面选取第一个匹配的组件进行渲染。
-
exact 代表的是 url 必须与当前页面传来的 url 完全一致,这时候才会导入当前页面。
对于首页和所有的课程列表页面来说,这一块是必须的。毕竟所有的 url 都是主页的分支。
例如说 CSDN 博客的 url 是
https://blog.csdn.net/
,打开某一篇文章后的 url 是https://blog.csdn.net/articles/details/文章id
,如果不做精确配对, index 页面又在第一个的前提下,那么只能访问到首页。所有课程的组件用 exact 的原理是一样的
接下来,开始具体的实现:
routePaths.js
作为常量保存所有的路由地址,这一部分单独拉出来是因为通过引用的方式调用地址,以后只要修改一处地方,其他的引用就会被自动修改。预防手动修改造成的人为失误。
export const INDEX = '/';
export const CAREER_PATH = '/career-path';
export const COURSES = '/courses';
export const COURSE = '/courses/:id';
routes.js
匹配路 url 与对应的组件
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
import * as routePaths from './routerPaths';
import CareerPath from '../containers/careerPath';
import Home from '../containers/home';
import Courses from '../containers/courses';
import Course from '../containers/course';
const routes = () => {
return (
<Router>
<Switch>
<Route path={routePaths.INDEX} exact component={Home} />
<Route path={routePaths.CAREER_PATH} component={CareerPath} />
<Route path={routePaths.COURSES} exact component={Courses} />
<Route path={routePaths.COURSE} component={Course} />
</Switch>
</Router>
);
};
export default routes;
以上代码全都实现完毕后,就能够根据 4 个路由去访问静态页面:
common
common 的结构是这样的:
- banner
- course-item
- footer
- header
- renderWithHeaderFooter
renderWithHeaderFooter
考虑到每个页面都会有一个 Header 和一个 Footer,所以封装了一个高阶组件出来,接收传来的 content,返回一个
header
content
footer
这样结构的组件,可以有效地减少四处复制黏贴的问题,也可以有效地减少代码量。
import React from 'react';
import Header from '../header/index';
export default function HeaderFooterHOC(WrappedComp) {
class HOC extends React.Component {
render() {
return (
<>
<Header />
<WrappedComp />
<Footer />
</>
);
}
}
return HOC;
}
Header
在搭结构的过程中,现在只是放一个占位符而已,具体实现下一个模块
import React from 'react';
import '../../index.css';
import './header.css';
const index = () => {
return (
<div className="header flex">
<div className="container">Header Area</div>
</div>
);
};
export default index;
Footer 同理
import React from 'react';
import '../../index.css';
import './footer.css';
const Footer = () => {
return (
<div className="footer">
<div className="container">Footer Area</div>
</div>
);
};
export default Footer;
这时候页面的基础结构就是这样的:
实现结构
介于篇幅的关系,这里做的是通用组件 Header 和 Footer
实现 Header
填充 header 的内容,先把原本的页面结构复制黏贴下来,并且修改一下图片引用。
修改图片引用指的是,本来的图片地址是一个目录结构,如 ../../img/some-img.png
,但是因为 React 会使用 WebPack 对项目进行打包,分割图片与 javascript,所以这样会找不到图片的地址。
模块内引用的方法是使用 import from
语句去进行正确的导入,WebPack 会根据 import from
去寻找打包后正确的路径。
为了能够正确的引入图片,我这里在根目录下面新建了一个 asset 文件去存放图片,其结构如下
|- src
| |- asset
| | |- img
| | | |- header
| | | | |- 一干图片文件
初步修改后的代码如下:
import React from 'react';
import '../../index.css';
import './header.css';
import pic from '../../asset/img/header/logo.png';
import faSearch from '../../asset/img/header/fa-search.png';
import ld from '../../asset/img/header/ld.png';
import profile from '../../asset/img/header/pic.png';
const index = () => {
return (
<div className="header flex">
<div className="logo">
<img src={pic} alt="logo" />
</div>
<div className="container flex">
<ul class="menu flex">
<li class="homepage active">
<a href="./index.html">首页</a>
</li>
<li class="courses">
<a href="./all-courses.html">课程</a>
</li>
<li class="career-planning">
<a href="./career-planning.html">职业规划</a>
</li>
</ul>
<div class="search-bar">
<input type="text" name="" id="" placeholder="输入关键字" />
<input type="button" value="" style={{ background: { faSearch } }} />
</div>
<div class="user flex">
<div class="user-center">个人中心</div>
<div class="alert">
<a href="#">
<img src={ld} alt="" />{' '}
</a>
</div>
<div class="profile-img">
<img src={profile} alt="profile-image" />
</div>
<div class="username">qq-leishui</div>
</div>
</div>
</div>
);
};
export default index;
渲染后的结果:
写到这里应该就已经有人意识到,为什么明明写的是 JavaScript,语法看起来和 HTML 这么像。
这就是 React 封装的语法糖,用类似 HTML 的结构去渲染页面。这也是我觉得 React 上手其实还挺快的原因。
那可能又有人在想,既然直接写 HTML 也可以工作,为什么还要拆分这么多组件?
这就以 Header 左上角的 Logo 为例,我突然发现这个 Logo 会同时在 Header 和 Footer 中被用到,所以临时将其抽离出来,做成单独的一个组件让 Header 和 Footer 去用。
实现 Logo 的逻辑剥离
Logo 的内容其实很简单,就是名为 logo 的 div,拆出来其实只有七八行代码:
import pic from '../../asset/img/header/logo.png';
import React from 'react';
const Logo = () => {
return (
<div className="logo">
<img src={pic} alt="logo" />
</div>
);
};
export default Logo;
修改 Header,在 Header 中引用 Logo:
import React from 'react';
import '../../index.css';
import './header.css';
import faSearch from '../../asset/img/header/fa-search.png';
import ld from '../../asset/img/header/ld.png';
import profile from '../../asset/img/header/pic.png';
import Logo from '../logo';
const index = () => {
return (
<div className="header flex">
<Logo />
{/* 后面代码省略 */}
</div>
);
};
export default index;
修改 Footer,在 Footer 中引入 Logo。
注,以下代码是不完全实现,Footer 的完整实现在后文。
import React from 'react';
import '../../index.css';
import './footer.css';
import Logo from '../logo';
const Footer = () => {
return (
<div>
<Logo />
</div>
);
};
这时候就能看到复用的好处了吧,只需要导入已经写好的组件,就可以实现复用的效果,而不是自己再复制黏贴一遍。
同样,如果哪一天的需求是修改 Logo 的图片了,也只需要在 Logo 组件之中修改即可,而不用满世界的到处去寻找所有图片的引用,减少人工错误。
实现 menu 的逻辑剥离
同样的,也将 menu 抽离出来单独做一个组件。
为了减少手动的复制黏贴,我这里新建了一个对象,保存的是所有 menu 中子项的中文名,以及其对应的 url 地址。url 是直接引用在路由中封装好的字符串:
import React from 'react';
import * as routePaths from '../../../router/routerPaths';
import { Link } from 'react-router-dom';
const LINKS = [
{ name: '首页', url: routePaths.INDEX },
{ name: '课程', url: routePaths.COURSE },
{ name: '职业规划', url: routePaths.CAREER_PATH },
];
const Nav = () => {
return (
<ul className="menu flex">
{LINKS.map((link) => (
<li>
<Link to={link.url}>{link.name}</Link>
</li>
))}
</ul>
);
};
export default Nav;
这里贴一下原生的 HTML 与现在的 JSX 的代码对比:
<ul class="menu flex"> <li class="homepage active"> <a href="./index.html">首页</a> </li> <li class="courses"> <a href="./all-courses.html">课程</a> </li> <li class="career-planning"> <a href="./career-planning.html">职业规划</a> </li> </ul> | import React from 'react'; import * as routePaths from '../../routerrouterPaths'; import { Link } from 'react-router-dom'; |
const LINKS = [
{ name: ‘首页’, url: routePaths.INDEX },
{ name: ‘课程’, url: routePaths.COURSE },
{ name: ‘职业规划’, url: routePaths.CAREER_PATH },
];
const Nav = () => {
return (
<ul className=“menu flex”>
{LINKS.map((link) => (
<li>
<Link to={link.url}>{liname}</Link>
</li>
))}
</ul>
);
};
export default Nav;
使用 JSX 的优势在于,可以动态的接收数据。
动态接收数据指的是,假设所有的内容不是手写出来的,而是存储在某些地方,那么对于开发来说就没有办法一行行写死代码——毕竟连多少数据量都不知道。
这时候的 Header
import React from 'react';
import '../../index.css';
import './header.css';
import faSearch from '../../asset/img/header/fa-search.png';
import ld from '../../asset/img/header/ld.png';
import profile from '../../asset/img/header/pic.png';
import Logo from '../logo';
import Nav from './Nav/Nav';
const index = () => {
return (
<div className="header flex">
<Logo />
<div className="container flex">
<Nav />
{/* 后面代码省略 */}
</div>
</div>
);
};
export default index;
这时候从 Header 页面也能一眼看出来,这个页面分成了若干模块:
- Logo
- Nav
- 以及其他被 div 嵌套,一眼看不出来有多少个的模块
使用 JSX 和直接使用原生 HTML 的对比,已经慢慢变得明显了。
注:写于组件实现之后:写完后我发现 menu 其实就是 nav,所以在后面修改警告的时候也对其进行了一些修正。详情可以看最后的完整实例部分。
实现 search 的逻辑剥离
按照这个模式继续修改,抽出 search 组件
import React from 'react';
import faSearch from '../../../asset/img/header/fa-search.png';
const SearchBar = () => {
return (
<div class="search-bar">
<input type="text" name="" id="" placeholder="输入关键字" />
<input
type="button"
value=""
style={{ background: `url({ faSearch })` }}
/>
</以上是关于[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解的主要内容,如果未能解决你的问题,请参考以下文章
[项目实战,源码完整]手把手教你怎么封装功能,React 重写学成在线 IV
[项目实战,源码完整]手把手教你怎么封装组件,React 重写学成在线 III
为了验证某些事,我实现了一个toy微前端框架万字长文,请君一览
为了验证某些事,我实现了一个toy微前端框架万字长文,请君一览