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

初始化项目

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

搭建框架

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

根目录

因为在这一步还没有实现 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="输入关键字" /><inputtype="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 的效果是这样的:

确实不大好看,不过这点会在 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;
}

这时候的效果:

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

完整代码

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

根目录完整代码

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 图片没有抽出去)

common 完整代码

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

footer 完整代码

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

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 完整代码

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="输入关键字" /><inputtype="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 完整代码

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

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;

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

  1. 学成在线--0.项目概述

    文章目录 0.前言 1.功能架构 2.技术架构 3.技术栈 4.开发步骤 0.前言 当前市场的在线教育模式多种多样,包括:B2C.C2C.B2B2C等业务模式,学成在线采用B2B2C业务模式,即向 企 ...

  2. 学成在线案例素材及代码

    头部标签 顶部一行案例分五个盒子: 顶部居中header大盒子,学成在线图片logo盒子,导航栏nav盒子, 搜索栏search盒子,用户user盒子 <div class="head ...

  3. 【前端】学成在线网页项目练习制作

    目录 html代码 css代码 Hello我是Leo,最近开始学习前端,学习的差不多了开始做简单的项目. 这是我学习前端路上的一个练习,跟着billbill黑马程序员pink老师做的,语法有不规范的地 ...

  4. 学成在线-第16天-讲义- Spring Security Oauth2 JWT RSA加解密

    学成在线-第16天-讲义- Spring Security Oauth2 JWT 1 用户认证需求分析 1.1 用户认证与授权 ​ 截至目前,项目已经完成了在线学习功能,用户通过在线学习页面点播视频进 ...

  5. 学成在线--认证授权模块

    完整版请移步至我的个人博客查看:https://cyborg2077.github.io/ 学成在线–项目环境搭建 学成在线–内容管理模块 学成在线–媒资管理模块 学成在线–课程发布模块 学成在线–认 ...

  6. 2023年最新黑马程序员Java微服务项目--学成在线

    正式上线Java微服务项目<学成在线> 项目对程序员的重要性 不用播妞多说了吧 更重要的是 这次是完整!实战!企业级!项目! 划重点:全新发布!正式上线! <学成在线>项目以在 ...

  7. 学成在线 职业规划 页面

    学成在线 职业规划 页面 为什么这么赶呢,主要是PS白嫖期快过了1/3了,总得赶在试用期之前做完嘛-- 之前的进度 第一版跟着视频做的,也写了步骤分解,详情可以看这个:学成在线 制作分解版. 完整的页 ...

  8. 微服务实战项目-学成在线-课程发布模块

    学成在线-课程发布模块 1 模块需求分析 1.1 模块介绍 课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后学习者在网站可以搜索到课程,然后查看课程的详细信息,进一步选课.支付.在 ...

  9. 学成在线项目开发技巧整理---第一部分

    学成在线项目开发技巧整理---第一部分 1.数据字典 2.http-client远程测试插件,可以保存测试数据 3.三种跨域解决 4.具有多层级数据查询思路 5.Mybaits分页插件原理 6.根据文 ...

最新文章

  1. 学以致用七---Centos7.2+python3.6.2+django2.1.1 --搭建一个网站(补充)
  2. windows导出文件名列表
  3. VMware配置linux网络步骤
  4. 20个常用的Python小技巧
  5. 【ThinkPHP】实例化模型的方法
  6. 华为哪个是鸿蒙,华为系统是鸿蒙还是安卓?有什么区别
  7. 打着改造传统市场的旗号玩垄断
  8. 阿里巴巴的26款超神Java开源项目!建议收藏~
  9. AVG开发过程记录(3) —— 半透明的实现
  10. 《大数据技术原理与应用》林子雨 期末复习重点(总结)
  11. android 工程师级别划分及学习路线
  12. [转载]《博客园精华集》设计模式 (164-284)
  13. 打工人息息相关的个税计算
  14. java计算机毕业设计体育用品购物系统源码+数据库+系统+lw文档+mybatis+运行部署
  15. 删除 linux的ln文件夹,linux下添加链接与删除链接(ln命令的用法)
  16. Vite+Vue3+TypeScript
  17. 利润中心的设置与组织架构 绩效考核 财务核算
  18. win7 打开防火墙端口
  19. 在计算机领域cda,什么是CDA
  20. arm mali 天梯图_ARM发飙!最强移动CPU/GPU来了:一图懂

热门文章

  1. linux常用指令词典
  2. php7新特性ppt,2019新版PPT,不知道这7个新功能,怎么做好幻灯片?
  3. python实现视频音频同步
  4. 【Java应用】使用Java实现机器学习算法:聚类、分类、预测
  5. 华为南研所2015年面试经历总结
  6. Java中的线程状态
  7. fillpolygon
  8. 可盈可乐研究院 | 2019:区块链+跨境支付”新旧”势力的新一轮角力
  9. 【Linux学习笔记】27:环境变量中的语系变量
  10. vue双向绑定数据更新会频繁触发render更新