[万字长文]使用 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 文件夹放会被重复调用的内容,目前的项目结构就是这样的:
初始化项目
结构搭好了,现在就开始往里面填充内容了。
搭建框架
这一块的目的是先清理一下初始化的代码,并且改为当前项目所需要的的实现。
根目录
因为在这一步还没有实现 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 />© 2017年 XTGG Inc. 保留所有权利。-沪ICP备11111111号</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 />© 2017年 XTGG Inc. 保留所有权利。-沪ICP备11111111号</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 之后才发现,自己粗心的地方还蛮多的……
主要是两个方向:
之前所有的 class 属于 HTML 的标记,是 JSX 中的保留词。为了防止出错,JSX 中的对应名称为 className,所以需要将所有的 class 修改为 className
没有在循环中加入 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 />© 2017年 XTGG Inc. 保留所有权利。-沪ICP备11111111号</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 代码完整可运行,步骤有详解相关推荐
- 学成在线--0.项目概述
文章目录 0.前言 1.功能架构 2.技术架构 3.技术栈 4.开发步骤 0.前言 当前市场的在线教育模式多种多样,包括:B2C.C2C.B2B2C等业务模式,学成在线采用B2B2C业务模式,即向 企 ...
- 学成在线案例素材及代码
头部标签 顶部一行案例分五个盒子: 顶部居中header大盒子,学成在线图片logo盒子,导航栏nav盒子, 搜索栏search盒子,用户user盒子 <div class="head ...
- 【前端】学成在线网页项目练习制作
目录 html代码 css代码 Hello我是Leo,最近开始学习前端,学习的差不多了开始做简单的项目. 这是我学习前端路上的一个练习,跟着billbill黑马程序员pink老师做的,语法有不规范的地 ...
- 学成在线-第16天-讲义- Spring Security Oauth2 JWT RSA加解密
学成在线-第16天-讲义- Spring Security Oauth2 JWT 1 用户认证需求分析 1.1 用户认证与授权 截至目前,项目已经完成了在线学习功能,用户通过在线学习页面点播视频进 ...
- 学成在线--认证授权模块
完整版请移步至我的个人博客查看:https://cyborg2077.github.io/ 学成在线–项目环境搭建 学成在线–内容管理模块 学成在线–媒资管理模块 学成在线–课程发布模块 学成在线–认 ...
- 2023年最新黑马程序员Java微服务项目--学成在线
正式上线Java微服务项目<学成在线> 项目对程序员的重要性 不用播妞多说了吧 更重要的是 这次是完整!实战!企业级!项目! 划重点:全新发布!正式上线! <学成在线>项目以在 ...
- 学成在线 职业规划 页面
学成在线 职业规划 页面 为什么这么赶呢,主要是PS白嫖期快过了1/3了,总得赶在试用期之前做完嘛-- 之前的进度 第一版跟着视频做的,也写了步骤分解,详情可以看这个:学成在线 制作分解版. 完整的页 ...
- 微服务实战项目-学成在线-课程发布模块
学成在线-课程发布模块 1 模块需求分析 1.1 模块介绍 课程信息编辑完毕即可发布课程,发布课程相当于一个确认操作,课程发布后学习者在网站可以搜索到课程,然后查看课程的详细信息,进一步选课.支付.在 ...
- 学成在线项目开发技巧整理---第一部分
学成在线项目开发技巧整理---第一部分 1.数据字典 2.http-client远程测试插件,可以保存测试数据 3.三种跨域解决 4.具有多层级数据查询思路 5.Mybaits分页插件原理 6.根据文 ...
最新文章
- 学以致用七---Centos7.2+python3.6.2+django2.1.1 --搭建一个网站(补充)
- windows导出文件名列表
- VMware配置linux网络步骤
- 20个常用的Python小技巧
- 【ThinkPHP】实例化模型的方法
- 华为哪个是鸿蒙,华为系统是鸿蒙还是安卓?有什么区别
- 打着改造传统市场的旗号玩垄断
- 阿里巴巴的26款超神Java开源项目!建议收藏~
- AVG开发过程记录(3) —— 半透明的实现
- 《大数据技术原理与应用》林子雨 期末复习重点(总结)
- android 工程师级别划分及学习路线
- [转载]《博客园精华集》设计模式 (164-284)
- 打工人息息相关的个税计算
- java计算机毕业设计体育用品购物系统源码+数据库+系统+lw文档+mybatis+运行部署
- 删除 linux的ln文件夹,linux下添加链接与删除链接(ln命令的用法)
- Vite+Vue3+TypeScript
- 利润中心的设置与组织架构 绩效考核 财务核算
- win7 打开防火墙端口
- 在计算机领域cda,什么是CDA
- arm mali 天梯图_ARM发飙!最强移动CPU/GPU来了:一图懂