[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解

    • 准备工作
      • 安装必备工具/库
        • nodejs
        • React 脚手架
        • 需要的 node 依赖包
      • 分析需求
    • 初始化项目
      • 搭建框架
        • 根目录
          • index.js
          • App.js
        • components
        • containers
          • home/index.js
          • careerPath/index.js
          • courses/index.js
        • router
          • routePaths.js
          • routes.js
        • common
          • renderWithHeaderFooter
          • Header
          • Footer 同理
      • 实现结构
        • 实现 Header
          • 实现 Logo 的逻辑剥离
          • 实现 menu 的逻辑剥离
          • 实现 search 的逻辑剥离
          • 实现 user-profile 的逻辑剥离
          • Header 的最终呈现效果
        • 实现 Footer
          • 实现 Copyright 的逻辑剥离
          • 实现 footerlinks 的逻辑剥离
            • 拆分常量 FOOTER_LINKS
            • 实现 footerlink 的逻辑剥离
        • 清除 warnings
      • 修改样式
        • 修改 index 部分样式
        • 给 Logo 添加样式
        • 给 Header 添加样式
          • 修改 Logo 部分样式
          • 修改 nav 部分样式
          • 修改 search 部分样式
          • 修改 user-profile 部分样式
        • 给 Footer 添加样式
          • 修改 copyright 部分样式
          • 修改 footerlinks 部分样式
    • 完整代码
      • 根目录完整代码
        • app.js 完整代码
        • index.css 完整代码
        • index.js 完整代码
        • .env
      • asset
      • common 完整代码
        • footer 完整代码
        • footer/index.js 完整代码
        • footer.css 完整代码
        • copyright 完整代码
        • FooterLinks 完整代码
          • FooterLinks/index.js 完整代码
          • FooterLink 完整代码
        • header 完整代码
          • nav 完整代码
          • searchBar 完整代码
          • userProfile 完整代码
          • header/index.js 完整代码
          • header.css 完整代码
        • logo 完整代码
        • renderWithHeaderFooter 完整代码
      • components 完整代码
      • constants 完整代码
        • footerLinks 完整代码
        • navLinks 完整代码
        • routerPaths 完整代码
      • containers 部分完整代码
      • router 完整代码

看完这篇教程,你应该可以:

  • 使用 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 文件夹放会被重复调用的内容,目前的项目结构就是这样的:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第1张图片

初始化项目

结构搭好了,现在就开始往里面填充内容了。

搭建框架

这一块的目的是先清理一下初始化的代码,并且改为当前项目所需要的的实现。

根目录

因为在这一步还没有实现 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 个路由去访问静态页面:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第2张图片 [万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第3张图片 [万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第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;

这时候页面的基础结构就是这样的:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第5张图片

实现结构

介于篇幅的关系,这里做的是通用组件 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 的代码对比:

      
    
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 (


    {LINKS.map((link) => (

  • {liname}

  • ))}

);
};

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 })` }}
      />
    </div>
  );
};

export default SearchBar;
实现 user-profile 的逻辑剥离
import React from 'react';
import ld from '../../../asset/img/header/ld.png';
import profile from '../../../asset/img/header/pic.png';

const UserProfile = () => {
     
  return (
    <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>
  );
};

export default UserProfile;
Header 的最终呈现效果

清理过后的 Header 其实也很短,并且一眼能够看出包含了几个组件:

import React from 'react';
import '../../index.css';
import './header.css';

import Logo from '../logo';
import Nav from './nav/Nav';
import SearchBar from './searchBar';
import UserProfile from './userProfile';

const index = () => {
     
  return (
    <div className="header flex">
      <Logo />
      <Nav />
      <SearchBar />
      <UserProfile />
    </div>
  );
};

export default index;

至此,Header 的结构实现就完成了

实现 Footer

Header 写完了,本期的任务就剩下 Footer 了。

老规矩,先把 Footer 的 HTML 复制黏贴进来:

import React from 'react';
import '../../index.css';
import './footer.css';

const Footer = () => {
     
  return (
    <div class="footer">
      <div class="container flex">
        <div class="copyright">
          <img src="./img/logo.png" alt="logo" />
          <p>
            学成在线致力于普及中国最好的教育。它与中国一流大学和机构合作,提供在线课程。
            <br />© 2017XTGG Inc. 保留所有权利。-ICP11111111</p>
          <a href="#" class="app">
            下载APP
          </a>
        </div>
        <div class="links flex">
          <dl>
            <dt>关于学成网</dt>
            <dd>
              <a href="#">关于</a>
            </dd>
            <dd>
              <a href="#">团队管理</a>
            </dd>
            <dd>
              <a href="#">工作机会</a>
            </dd>
            <dd>
              <a href="#">客户服务</a>
            </dd>
            <dd>
              <a href="#">帮助</a>
            </dd>
          </dl>
          <dl>
            <dt>关于学成网</dt>
            <dd>
              <a href="#">关于</a>
            </dd>
            <dd>
              <a href="#">团队管理</a>
            </dd>
            <dd>
              <a href="#">工作机会</a>
            </dd>
            <dd>
              <a href="#">客户服务</a>
            </dd>
            <dd>
              <a href="#">帮助</a>
            </dd>
          </dl>
          <dl>
            <dt>关于学成网</dt>
            <dd>
              <a href="#">关于</a>
            </dd>
            <dd>
              <a href="#">团队管理</a>
            </dd>
            <dd>
              <a href="#">工作机会</a>
            </dd>
            <dd>
              <a href="#">客户服务</a>
            </dd>
            <dd>
              <a href="#">帮助</a>
            </dd>
          </dl>
        </div>
      </div>
    </div>
  );
};

export default Footer;

代码已经复制黏贴进来了,那么继续拆分组件

实现 Copyright 的逻辑剥离
import React from 'react';
import Logo from '../../logo';

const Copyright = () => {
     
  return (
    <div className="copyright">
      <Logo />
      <p>
        学成在线致力于普及中国最好的教育。它与中国一流大学和机构合作,提供在线课程。
        <br />© 2017XTGG Inc. 保留所有权利。-ICP11111111</p>
      <a href="./" className="app">
        下载APP
      </a>
    </div>
  );
};

export default Copyright;
实现 footerlinks 的逻辑剥离

这里我拆了两个组件出来,对于这种简单的业务来说,拆一个问题也不大。

footerlinks 是 footer 中所有 dl 组成的数组

具体实现为:

import React from 'react';
import FOOTER_LINKS from '../../../constant/footerLinks';
import FooterLink from './FooterLink';

const FooterLinks = () => {
     
  const footerLinkArray = [];
  for (const link in FOOTER_LINKS) {
     
    footerLinkArray.push(
      <FooterLink subLinks={
     FOOTER_LINKS[link]} key={
     link} />
    );
  }
  return <div className="links flex">{
     footerLinkArray}</div>;
};

export default FooterLinks;
拆分常量 FOOTER_LINKS

另外,我将上面的一串 dl > dt +dd 的部分也单独抽离出来,放在了一个常量之中。

数据结构为:

import * as routePaths from './routerPaths';

const FOOTER_LINKS = {
     
  about: {
     
    key: '关于学成网',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '关于',
      },
      {
     
        url: routePaths.INDEX,
        name: '团队管理',
      },
      {
     
        url: routePaths.INDEX,
        name: '工作机会',
      },
      {
     
        url: routePaths.INDEX,
        name: '客户服务',
      },
      {
     
        url: routePaths.INDEX,
        name: '帮助',
      },
    ],
  },
  userGuide: {
     
    key: '新手指南',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '如何注册',
      },
      {
     
        url: routePaths.INDEX,
        name: '如何选课',
      },
      {
     
        url: routePaths.INDEX,
        name: '如何拿到毕业证',
      },
      {
     
        url: routePaths.INDEX,
        name: '学分是什么',
      },
      {
     
        url: routePaths.INDEX,
        name: '考试未通过怎么办',
      },
    ],
  },
  parterner: {
     
    key: '合作伙伴',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '合作机构',
      },
      {
     
        url: routePaths.INDEX,
        name: '合作导师',
      },
    ],
  },
};

export default FOOTER_LINKS;
实现 footerlink 的逻辑剥离

这里是单独的一个 link 的业务实现,也就是一个 dl>dt+dd 的逻辑

import React from 'react';

const FooterLink = (props) => {
     
  const key = props?.subLinks?.key;
  const options = props?.subLinks?.options;
  return (
    <dl>
      <dt>{
     key}</dt>
      {
     options?.map((opt) => (
        <dd {
     opt.name}>
          <a href={
     opt.url}>{
     opt.name}</a>
        </dd>
      ))}
    </dl>
  );
};

export default FooterLink;

清除 warnings

在结构完成之后,先打开浏览器看一下效果,并且根据下面的控制台修改一下 warnings。

没有 CSS 的效果是这样的:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第6张图片

确实不大好看,不过这点会在 CSS 中被修正。

另外,开始清 warnings 之后才发现,自己粗心的地方还蛮多的……

主要是两个方向:

  1. 之前所有的 class 属于 HTML 的标记,是 JSX 中的保留词。为了防止出错,JSX 中的对应名称为 className,所以需要将所有的 class 修改为 className

  2. 没有在循环中加入 key,这个警告是在 nav 组件中

    循环体里加 key 是 React 所具有的特性之一,属于性能优化类。不改虽然不影响渲染,在数据量大的情况下会影响性能。

注:最后完整版放的代码都是经过修改的

修改样式

修改 index 部分样式

这一部分主要是增加一些全局会使用的 CSS,包括 a 标签、li 标签的一些排版问题,目前实现的 CSS 如下:

body {
     
  margin: 0;
  width: 1980px;
  background-color: #f3f5f7;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
    'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
     
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

li {
     
  list-style: none;
}

input {
     
  outline: none;
}

a {
     
  text-decoration: none;
  color: #050505;
}
a:hover {
     
  color: #00a4ff;
}
dd {
     
  margin: 0;
}

.flex {
     
  display: flex;
}
.relative {
     
  position: relative;
}
.absolute {
     
  position: absolute;
}
.container {
     
  width: 1200px;
  margin: auto;
  background-color: transparent;
}
.flex-center {
     
  align-items: center;
}
.float-left {
     
  float: left;
}
.float-right {
     
  float: right;
}

给 Logo 添加样式

鉴于 Logo 已经拆分出去了,所以就为它添加单独的 css 文件了。很短,只是确定了 Logo 的高度:

.logo {
     
  width: 390px;
}

给 Header 添加样式

这个部分调整的还是蛮快的,所有的 CSS 修改都在 header.css 文件之中。当然,最终在实现 CSS 的时候会发现,其实有一些的 HTML 部分也可以做相应的细微调整,以达到比较好的代码复用效果。

修改 Header 主体部分的 CSS,主要是调整了高度和 margin:

.header {
     
  height: 42px;
  margin: 30px auto;
}
修改 Logo 部分样式

logo 的图片有向右浮动的效果,这是只有 header 中的 logo 有,footer 中的 logo 没有的,因此加到这里:

.header .logo img {
     
  float: right;
}
修改 nav 部分样式

给 nav 部分添加左右间距,同时也给 nav 中的每个 li 设置内外边距,增强展现效果:

.nav {
     
  margin-right: 300px;
  font-size: 18px;
  text-align: center;
}
.nav li {
     
  margin-left: 65px;
  padding: 0 8px;
}

修改完后的结果:

修改 search 部分样式

JSX 中的修改是在引入的时候做的修改,主要是 style 这里:

style={
     {
      backgroundImage: `url(${
       faSearch})` }}

这是属于 JSX 的行内元素的写入方法,毕竟 JSX 是 JavaScript 的语法糖,所以其中 style 接受的参数必须要是一个 JavaScript 的对象。同样的,CSS 属性 background-image 被修改为了对应的驼峰格式:backgroundImage

url 部分使用的是 ES6 后的新属性,也就是模板字符串,优势在于拼接变量方便一些。

.search-bar input[type='text'] {
     
  box-sizing: border-box;
  width: 360px;
  height: 42px;
  border: 1px solid #00a4ff;
  border-right: 0;
  background-color: #f3f5f7;
  color: #bfbfbf;
  font-size: 14px;
  padding-left: 15px;
}
.search-bar input[type='button'] {
     
  margin-right: 32px;
  float: right;
  width: 50px;
  height: 42px;
  border: 0;
}

CSS 实现的部分有以下几个功能:修改样式字体、内外间距,以及将按钮和输入框放在同一行(通过 float 实现)

修改 user-profile 部分样式

主要也是调整了字体大小、内外间距,以及图片透明度和实现圆边框功能

.user {
     
  font-size: 14px;
}
.user-center {
     
  margin-right: 32px;
}
.alert {
     
  width: 14px;
}
.alert img {
     
  opacity: 0.6;
}
.alert img,
.user-profile img {
     
  transform: translateY(3px);
}
.user-profile {
     
  margin-left: 30px;
  margin-right: 6px;
}
.user-profile img {
     
  border-radius: 50%;
}

页面实现后:

给 Footer 添加样式

这里主要就是对 copyright 和 footerlinks 的布局进行修改。使用的是 flex,又想拟态 float-left 和 float-right 的效果,就是用了 justify-content: space-between;

.footer {
     
  height: 100px;
}
.footer .container {
     
  justify-content: space-between;
}
修改 copyright 部分样式

其实写到这里就会发现,CSS 大部分的样式调整的东西都是大同小异的。

.copyright p {
     
  font-size: 12px;
  color: #666;
  margin: 20px 0 15px 0;
}
.download-app {
     
  display: inline-block;
  width: 118px;
  height: 33px;
  text-align: center;
  line-height: 33px;
  border: 1px solid #00a4ff;
  font-style: 16px;
  color: #00a4ff;
}
修改 footerlinks 部分样式
.footer-links dl {
     
  margin-left: 80px;
  padding: 0 30px;
  color: #333;
}
.footer-links dt {
     
  font-size: 16px;
  margin-bottom: 5px;
}
.footer-links dd a {
     
  font-size: 12px;
  color: #333;
}

这时候的效果:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第7张图片

到这里,教程部分就结束了,下面放一下完整的实现部分。

完整代码

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第8张图片

主要的结构就是这样的,接下来会按照模块的顺序贴完整代码

根目录完整代码

app.css 其实现在没东西,所以就不放了

app.js 完整代码

import Routes from './router/routes';

function App() {
     
  return (
    <div className="App">
      <Routes />
    </div>
  );
}

export default App;

index.css 完整代码

body {
     
  margin: 0;
  width: 1980px;
  background-color: #f3f5f7;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto',
    'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans',
    'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
     
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}

li {
     
  list-style: none;
}

input {
     
  outline: none;
}

a {
     
  text-decoration: none;
  color: #050505;
}
a:hover {
     
  color: #00a4ff;
}
dd {
     
  margin: 0;
}

.flex {
     
  display: flex;
}
.relative {
     
  position: relative;
}
.absolute {
     
  position: absolute;
}
.container {
     
  width: 1200px;
  margin: auto;
  background-color: transparent;
}
.flex-center {
     
  align-items: center;
}
.float-left {
     
  float: left;
}
.float-right {
     
  float: right;
}

index.js 完整代码

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')
);

.env

这是负责热部署,在 React v17 会出现热部署失效的功能,加上这个再重新启动一下项目就好了。

FAST_REFRESH=false

asset

就一些图片,目前只有 header 要用的(logo 图片没有抽出去)

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第9张图片

common 完整代码

其中,目前 banner 和 course-item 是空的,就不加进去了

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第10张图片

footer 完整代码

footer 模块中的完整代码,默认是 index.js 文件

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第11张图片

footer/index.js 完整代码

import React from 'react';
import './footer.css';
import Copyright from './copyright';
import FooterLinks from './FooterLinks';

const Footer = () => {
     
  return (
    <footer className="footer">
      <div className="container flex">
        <Copyright />
        <FooterLinks />
      </div>
    </footer>
  );
};

export default Footer;

footer.css 完整代码

.footer {
     
  height: 100px;
}
.footer .container {
     
  justify-content: space-between;
}
/* copyright */
.copyright p {
     
  font-size: 12px;
  color: #666;
  margin: 20px 0 15px 0;
}
.download-app {
     
  display: inline-block;
  width: 118px;
  height: 33px;
  text-align: center;
  line-height: 33px;
  border: 1px solid #00a4ff;
  font-style: 16px;
  color: #00a4ff;
}
/* copyright */
/* footer-links */
.footer-links dl {
     
  margin-left: 80px;
  padding: 0 30px;
  color: #333;
}
.footer-links dt {
     
  font-size: 16px;
  margin-bottom: 5px;
}
.footer-links dd a {
     
  font-size: 12px;
  color: #333;
}
/* footer-links */

copyright 完整代码

import React from 'react';
import Logo from '../../logo';

const Copyright = () => {
     
  return (
    <div className="copyright">
      <Logo />
      <p>
        学成在线致力于普及中国最好的教育。它与中国一流大学和机构合作,提供在线课程。
        <br />© 2017XTGG Inc. 保留所有权利。-ICP11111111</p>
      <a href="./" className="download-app">
        下载APP
      </a>
    </div>
  );
};

export default Copyright;

FooterLinks 完整代码

FooterLinks/index.js 完整代码
import React from 'react';
import FOOTER_LINKS from '../../../constants/footerLinks';
import FooterLink from './FooterLink';

const FooterLinks = () => {
     
  const footerLinkArray = [];
  for (const link in FOOTER_LINKS) {
     
    footerLinkArray.push(
      <FooterLink subLinks={
     FOOTER_LINKS[link]} key={
     link} />
    );
  }
  return <div className="footer-links flex">{
     footerLinkArray}</div>;
};

export default FooterLinks;
FooterLink 完整代码
import React from 'react';

const FooterLink = (props) => {
     
  const key = props?.subLinks?.key;
  const options = props?.subLinks?.options;
  return (
    <dl>
      <dt>{
     key}</dt>
      {
     options?.map((opt) => (
        <dd key={
     opt.name}>
          <a href={
     opt.url}>{
     opt.name}</a>
        </dd>
      ))}
    </dl>
  );
};

export default FooterLink;

header 完整代码

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第12张图片
nav 完整代码
import React from 'react';
import NAV_LINKS from '../../../constants/navLinks';
import {
      Link } from 'react-router-dom';

const Nav = () => {
     
  return (
    <nav className="nav">
      <ul className="flex">
        {
     NAV_LINKS.map((link) => (
          <li key={
     link.name}>
            <Link to={
     link.url}>{
     link.name}</Link>
          </li>
        ))}
      </ul>
    </nav>
  );
};

export default Nav;
searchBar 完整代码
import React from 'react';
import faSearch from '../../../asset/img/header/fa-search.png';

const SearchBar = () => {
     
  return (
    <div className="search-bar">
      <input type="text" name="" id="" placeholder="输入关键字" />
      <input
        type="button"
        value=""
        style={
     {
      backgroundImage: `url(${
       faSearch})` }}
      />
    </div>
  );
};

export default SearchBar;
userProfile 完整代码
import React from 'react';
import ld from '../../../asset/img/header/ld.png';
import profile from '../../../asset/img/header/pic.png';

const UserProvile = () => {
     
  return (
    <div className="user flex flex-center">
      <div className="user-center">个人中心</div>
      <div className="alert">
        <a href="./">
          <img src={
     ld} alt="" />{
     ' '}
        </a>
      </div>
      <div className="user-profile">
        <img src={
     profile} alt="user-profile" />
      </div>
      <div className="username">qq-leishui</div>
    </div>
  );
};

export default UserProvile;
header/index.js 完整代码
import React from 'react';
import './header.css';

import Logo from '../logo';
import Nav from './nav';
import SearchBar from './searchBar';
import UserProvile from './userProfile';

const index = () => {
     
  return (
    <header className="header flex flex-center">
      <Logo />
      <Nav />
      <SearchBar />
      <UserProvile />
    </header>
  );
};

export default index;
header.css 完整代码
.header {
     
  height: 42px;
  margin: 30px auto;
}
/* logo */
.header .logo img {
     
  float: right;
}
/* logo */
/* nav */
.nav {
     
  margin-right: 300px;
  font-size: 18px;
  text-align: center;
}
.nav li {
     
  margin-left: 65px;
  padding: 0 8px;
}
/* nav */
/* search bar */
.search-bar input[type='text'] {
     
  box-sizing: border-box;
  width: 360px;
  height: 42px;
  border: 1px solid #00a4ff;
  border-right: 0;
  background-color: #f3f5f7;
  color: #bfbfbf;
  font-size: 14px;
  padding-left: 15px;
}
.search-bar input[type='button'] {
     
  margin-right: 32px;
  float: right;
  width: 50px;
  height: 42px;
  border: 0;
}
/* search bar */
/* user profile */
.user {
     
  font-size: 14px;
}
.user-center {
     
  margin-right: 32px;
}
.alert {
     
  width: 14px;
}
.alert img {
     
  opacity: 0.6;
}
.alert img,
.user-profile img {
     
  transform: translateY(3px);
}
.user-profile {
     
  margin-left: 30px;
  margin-right: 6px;
}
.user-profile img {
     
  border-radius: 50%;
}
/* user profile */

logo 完整代码

比较短,就不分了,直接贴 CSS 和 JSX

import pic from '../../asset/img/header/logo.png';
import React from 'react';
import './logo.css';

const Logo = () => {
     
  return (
    <div className="logo">
      <img src={
     pic} alt="logo" />
    </div>
  );
};

export default Logo;
.logo {
     
  width: 390px;
}

renderWithHeaderFooter 完整代码

import React from 'react';
import Footer from '../footer';
import Header from '../header/index';

export default function HeaderFooterHOC(WrappedComp) {
     
  class HOC extends React.Component {
     
    render() {
     
      return (
        <>
          <Header />
          <WrappedComp />
          <Footer />
        </>
      );
    }
  }
  return HOC;
}

components 完整代码

其实目前只有一个 placeholder:

import React from 'react';

const Home = () => {
     
  return <div>Home</div>;
};

export default Home;

constants 完整代码

将一些常量/假数据抽离出来了:

[万字长文]使用 React 重写学成在线前端项目 I 代码完整可运行,步骤有详解_第13张图片

footerLinks 完整代码

import * as routePaths from './routerPaths';

const FOOTER_LINKS = {
     
  about: {
     
    key: '关于学成网',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '关于',
      },
      {
     
        url: routePaths.INDEX,
        name: '团队管理',
      },
      {
     
        url: routePaths.INDEX,
        name: '工作机会',
      },
      {
     
        url: routePaths.INDEX,
        name: '客户服务',
      },
      {
     
        url: routePaths.INDEX,
        name: '帮助',
      },
    ],
  },
  userGuide: {
     
    key: '新手指南',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '如何注册',
      },
      {
     
        url: routePaths.INDEX,
        name: '如何选课',
      },
      {
     
        url: routePaths.INDEX,
        name: '如何拿到毕业证',
      },
      {
     
        url: routePaths.INDEX,
        name: '学分是什么',
      },
      {
     
        url: routePaths.INDEX,
        name: '考试未通过怎么办',
      },
    ],
  },
  parterner: {
     
    key: '合作伙伴',
    options: [
      {
     
        url: routePaths.INDEX,
        name: '合作机构',
      },
      {
     
        url: routePaths.INDEX,
        name: '合作导师',
      },
    ],
  },
};

export default FOOTER_LINKS;

navLinks 完整代码

import * as routePaths from './routerPaths';

const NAV_LINKS = [
  {
      name: '首页', url: routePaths.INDEX },
  {
      name: '课程', url: routePaths.COURSES },
  {
      name: '职业规划', url: routePaths.CAREER_PATH },
];

export default NAV_LINKS;

routerPaths 完整代码

export const INDEX = '/';
export const CAREER_PATH = '/career-path';
export const COURSES = '/courses';
export const COURSE = '/courses/:id';

containers 部分完整代码

这个和最上面放的是完全一样(没有动过),所以只放一个案例在这了:

import React from 'react';
import HeaderFooterHOC from '../../common/renderWithHeaderFooter';
import Home from '../../components/home/Home';

const HomeIndex = () => {
     
  return (
    <div>
      <Home />
    </div>
  );
};

export default HeaderFooterHOC(HomeIndex);

router 完整代码

import {
      BrowserRouter as Router, Switch, Route } from 'react-router-dom';

import * as routePaths from '../constants/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} exact component={
     CareerPath} />
        <Route path={
     routePaths.COURSES} exact component={
     Courses} />
        <Route path={
     routePaths.COURSE} exact component={
     Course} />
      </Switch>
    </Router>
  );
};

export default routes;

你可能感兴趣的:(项目,新星计划,前端,reactjs,javascript,hooks)