[万字长文]使用 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微前端框架万字长文,请君一览

为了验证某些事,我实现了一个toy微前端框架万字长文,请君一览

SQL注入详解(全网最全,万字长文)