React实战全栈后台

项目预览:http://lesliewaong.top/react-admin-client/

准备

项目描述

  1. 此项目为一个前后台分离后台管理的 SPA(单页面应用), 包括前端 PC 应用和后端应用
  2. 包括用户管理 / 商品分类管理 / 商品管理 / 权限管理等功能模块
  3. 前端: 使用 React 全家桶(函数式组件、react-router V6) + Antd(v4) + Axios + ES6 + Webpack 等技术
  4. 后端: 使用 Node + Express + Mongodb 等技术
  5. 采用模块化组件化工程化的模式开发

技术选型

oDk4ZF.png

项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public ---- 静态资源文件夹
css ------ 和风天气图标字体
favicon.icon ------ 网站页签图标
index.html -------- 主页面
logo192.png ------- logo图
logo512.png ------- logo图
manifest.json ----- 应用加壳的配置文件
robots.txt -------- 爬虫协议文件
src ---- 源码文件夹
>api -------- ajax相关
>assets -------- 公用资源
>components -------- 非路由组件
>config -------- 配置
>hooks -------- 自定义hook
>pages -------- 路由组件
>utils -------- 工具模块
App.css -------- App组件的样式
App.js --------- App组件
index.js ------- 入口文件

前端路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>pages -------- 路由组件
/login --- 登录
/ ----主页面
index --- 主路由 home
/category --- 分类管理
/product --- 商品管理
index --- 主路由
/detail --- 详情
/addupdate --- 添加/更新
/user --- 用户管理
/role --- 角色管理
/charts-bar --- 柱状图
/charts-pie --- 饼图
/charts-line --- 线图
* --- 404

接口

I0hMlt.jpg

npm/yarn 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
## 设置淘宝镜像
npm config set registry https://registry.npm.taobao.org
yarn config set registry https://registry.npm.taobao.org
## 初始化项目:
yarn init -y
npm init -y
## 下载项目的所有声明的依赖:
yarn
npm install
## 下载指定的运行时依赖包:
yarn add webpack@3.2.1
npm install webpack@3.2.1 -S
## 下载指定的开发时依赖:
yarn add webpack@3.2.1 -D
npm install webpack@3.2.1 -D
## 全局下载指定包:
yarn global add webpack
npm install webpack -g
## 删除依赖包:
yarn remove webpack
npm remove webpack -S
yarn global remove webpack
npm remove webpack -g
## 运行项目中配置的script:
yarn run xxx
npm run xxx
## 查看某个包的信息:
yarn info xxx
npm info xxx

git 常用基本命令

1
2
3
4
5
6
7
8
9
10
11
12
git config --global user.name "username" //配置用户名
git config --global user.email "xx@gmail.com" //配置邮箱
git init //初始化生成一个本地仓库
git add . //添加到暂存区
git commit –m "注释语句" //提交到本地仓库
git remote add origin url //关联到远程仓库
git push origin master //推送本地master分支到远程master分支
git checkout -b dev //创建一个开发分支并切换到新分支
git push ogigin dev //推送本地dev 分支到远程dev 分支
git pull origin dev //从远程dev 分支拉取到本地dev 分支
git clone url //将远程仓库克隆下载到本地
git checkout -b dev origin/dev // 克隆仓库后切换到dev 分支

应用开发详解

开启项目开发

使用create-react-app(脚手架)搭建项目

create-react-app 是react 官方提供的用于搭建基于react+webpack+es6 项目的脚手架

1
2
3
4
5
# 全局下载工具
npm install -g create-react-app
# 下载模板项目
create-react-app react-admin-client
cd react-admin-client

编码测试与打包发布项目

1
2
3
4
5
6
7
8
# 编码, 自动编译打包刷新(live-reload), 查看效果
npm start
# 不一定3000
访问: localhost:3000
# 打包发布
npm run build
npm install -g serve
serve build

git管理项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1). 创建远程仓库
2). 创建本地仓库
a. 配置.gitignore
b. git init
c. git add .
d. git commit -m "init"
3). 将本地仓库推送到远程仓库
git remote add origin url
git push origin master
4). 在本地创建dev分支, 并推送到远程
git checkout -b dev
git push origin dev
5). 如果本地有修改
git add .
git commit -m "xxx"
git push origin dev
6). 新的同事: 克隆仓库
git clone url
git checkout -b dev origin/dev
git pull origin dev
7). 如果远程修改
git pull origin dev

项目源码基本目录设计

1
2
3
4
5
6
7
8
9
10
src ---- 源码文件夹
>api -------- ajax相关
>assets -------- 公用资源
>components -------- 非路由组件
>config -------- 配置
>hooks -------- 自定义hook
>pages -------- 路由组件
>utils -------- 工具模块
App.js --------- App组件
index.js ------- 入口文件

引入antd

antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。

craco一个对 create-react-app 进行自定义配置的社区解决方案

下载安装

yarn add antd 安装 antd
yarn add @craco/craco 安装 craco
yarn add craco-less 下载 less ,支持 less 格式文件

package.json 中修改属性

1
2
3
4
5
6
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject"
},

根目录创建 craco.config.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const CracoLessPlugin = require('craco-less');

module.exports = {
plugins: [
{
plugin: CracoLessPlugin,
options: {
lessLoaderOptions: {
lessOptions: {
modifyVars: { '@primary-color': '#1DA57A' },
javascriptEnabled: true,
},
},
},
},
],
};

引入 less 文件

在 src 目录下的 index.js 文件中

1
import 'antd/dist/antd.less'

某个组件中使用antd组件

1
2
3
4
5
6
7
8
import {Button} from 'antd'
render(){
return(
<div>
<Button>点击一下</Button>
</div>
)
}

引入路由

yarn add react-router-dom

Login组件(不与后台交互)

静态组件

前台表单验证与数据收集

运行server 端项目

该项目是一个前后台分离的项目: 前台应用与后台应用

后台应用负责处理前台应用提交的请求, 并给前台应用返回json 数据

前台应用负责展现数据, 与用户交互, 与后台应用交互

运行后台应用

  1. 确保启动mongodb服务
  2. 启动服务器应用: npm start

使用postman工具测试接口

  1. postman 是用来测试API 接口的工具
  1. postman 可以看作活接口文档

I6q0EQ.png

前后台交互ajax

yarn add axios

Promise+ async + await

配置代理

package.json

1
"proxy": "http://localhost:5000"

请求测试: login.jsx

Login 组件(完成登陆功能)

维持登陆与自动登陆

yarn add store

store.js 是一个兼容所有浏览器的 LocalStorage 包装器,不需要借助 Cookie 或者 Flash。store.js 会根据浏览器自动选择使用 localStorage、globalStorage 或者 userData 来实现本地存储功能。

Admin 组件(搭建整体结构)

LeftNav组件 Header组件 Admin组件

Admin 的子路由

注意v6的更改

LeftNav组件

defaultOpenKeys={[openKey]}目前使用useMemo实现,后续再优化

Header 组件

Home 界面

分类管理

yarn add react-draft-wysiwyg draftjs-to-html draft-js

使用说明

模块化、组件化、工程化

模块化

后端:CommonJS :module.exportsrequire方法用于加载模块。

前端:ES6 模块化语法:export 和 import;

工程化

webpack最热门的前端资源模块化管理和打包工具、

create-react-app 脚手架初始化react 项目开发、

ESLint 插件化的 JavaScript 代码检测工具

组件化

其中以React的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。

理解:用来实现局部功能效果的代码和资源的集合(html/css/js/img等等)

为什么要用组件:一个界面的功能复杂

作用:复用编码,简化项目编码,提高运行效率

当应用是以多组件的方式实现,这个应用就是组件化的应用。

后端数据

Node + Express + Mongodb等技术。

Node.js 使用 JavaScript 语言开发服务器端应用;Express是基于 Node.js平台,快速、开放、极简的 Web 开发框架;MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。MongoDB 是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。

MySQL 是一个开源的关系数据库,其中的数据存于表中,数据中的某些属性可以跟其他表建立关系。

MongoDB 也是开源的,但它属于文档型数据库。因此,它没有记录的概念,它的数据模不固定,所以它是一种动态灵活的数据库,可以插入大量数据。

在选定最佳数据库之前,特定的业务需求和项目的优先事项应当是清晰确定的,正如前文提到的,在处理大量数据方面,MongoDB 比 MySQL 更胜一筹。另外,在云计算服务和需求频繁变化的项目上,MongoDB 也是如此。

相反,MySQL 中数据结构和模式是固定的,因此保证了数据一致性和可靠性。使用 MySQL 还有一个好处,就是由于它支持基于 ACID 准则的事务操作,数据安全性更高。所以对于看重这些因素的项目来说,MySQL 是最合适的。

MongoDB 的安装与配置

下载:https://www.mongodb.com/try/download/ops-manager

配置path: 将mongodb下的bin目录配置到path环境变量中

添加数据

使用mongoDB Compass

连接服务器:mongodb://localhost/server_db2

可以手动输入,也可以导入json

天气API

和风天气(不需要跨域处理)

get请求

开发文档

将和风天气图标安装在你的项目中,包括SVG图标、图标字体等等。

1
npm i qweather-icons

在public下的HTML引入<link rel="stylesheet" href="%PUBLIC_URL%/css/qweather-icons.css">,注意还要导入一个font文件,在对应位置即可使用图标字体显示天气图标。picUrl即请求到的图标的代码。

1
<i className={"qi-"+picUrl+"-fill"} style={{color:'black',fontSize:'17px',margin:'0 15px'}}/>

less

less是一门css的预处理语言

  • less是一个css的增强版,通过less可以编写更少的代码实现更强大的样式,添加了许多的新特性:像对变量的支持、对mixin的支持… …
  • less的语法大体上和css语法一致,但是less中增添了许多对css的扩展,所以浏览器无法直接执行less代码,要执行必须向将less转换为css,然后再由浏览器执行

特点

  • 结构写的更加清晰
  • 变量 @变量名
  • & 表示外层的父元素 &::after &:hover
  • :extend() 对当前选择器扩展指定选择器的样式(选择器分组)

界面

布局

Flex absolute float

登录界面

image-20220313161250827

气泡背景原理:

background可以设置多个,默认地,每个背景图像在水平和垂直方向上重复。因此,通过设置多个背景图像的位置和大小产生重叠效果。

气泡:

radial-gradient+rgba

开始和结束的透明度都设为0。

通过两个确定位置的固定色值(有一定透明度的白色),实现渐变效果。

radial-gradient(rgba(255,255,255,0) 0, rgba(255,255,255,.15) 30%, rgba(255,255,255,.3) 32%, rgba(255,255,255,0) 33%)

边框

box-shadow 内外阴影效果。

颜色

transparent 关键字表示一个完全透明的颜色,即该颜色看上去将是背景色。从技术上说,它是带有阿尔法通道为最小值的黑色,是 rgba(0,0,0,0) 的简写。

RGB

颜色可以使用红-绿-蓝(red-green-blue (RGB))模式的两种方式被定义。

RGB颜色可以通过以#为前缀的十六进制字符函数rgb()rgba())标记表示。

在CSS 颜色标准 4 中,rgba()rgb()的别称。在实行第4级标准的浏览器中,它们接受相同的参数,作用效果也相同。

rgb[a](R, G, B[, A])

rgb[a](R G B[ / A]) CSS 颜色级别 4 支持用空格分开的值。

R(红)、G(绿)、B (蓝)可以是数字或百分比,255相当于100%。A(alpha)可以是01之间的数字,或者百分比,数字1相当于100%(完全不透明)。

HSL

颜色也可以使用 hsl() 函数符被定义为色相-饱和度-亮度(Hue-saturation-lightness)模式。HSL 相比 RGB 的优点是更加直观:你可以估算你想要的颜色,然后微调。它也更易于创建相称的颜色集合。(通过保持相同的色相并改变亮度/暗度和饱和度)。

HSL 颜色通过功能hsl()hsla()符号表示。

从 CSS 颜色级别 4 开始,hsla()hsl()在实现 4 级标准的浏览器中,它们接受相同的参数并以相同的方式运行。

hsl[a](H, S, L[, A]) hsl[a](H S L[ / A])

H(hue) 是度数deg,S(饱和度)和L(亮度)是百分比。

背景毛玻璃

backdrop-filter: blur(5px);

该属性可以让你为一个元素后面区域添加图形效果(如模糊或颜色偏移)。 因为它适用于元素背后的所有元素,为了看到效果,必须使元素或其背景至少部分透明。

头像旋转

animation+transform: rotate

居中布局

Flex

flex + justify-content + align-items

margin

margin: 0 auto

图片与文字在一行内的居中问题

父元素开启Flex并设置align-items: center,图片可以正常居中,但文字不行,文字(h1中)会自动添加一个下边距,应该是文字与图片对齐方式不同导致的。

一种方法是文字里添加margin-bottom:0;

另一种方法是图片和文字采用不同方法居中,父元素开启Flex使它们都在一行,但不设置align-items: center,给图片设置align-self:center;使其垂直居中,文字设置line-height值等于父元素的height

类式组件与函数式组件

因为React-Router v6正式版的更新,类式路由组件很多功能不易完成,比如无法直接操作history。另外React-Router官方推荐使用函数式组件。

类式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import React, { Component } from 'react'
import { BrowserRouter as Router,Routes, Route } from 'react-router-dom'
import Login from './pages/login'
import Admin from './pages/admin'
/**
* 根组件
*/

export default class App extends Component {
// 自定义方法:赋值语句的形式+箭头函数
render() {
//render是放在哪里的?—— MyComponent的原型对象上,供实例使用。
//render中的this是谁?—— MyComponent的实例对象 <=> MyComponent组件实例对象。
console.log('render中的this:',this);
return (
<Router>
{/* React-Router v6新特性 <Switch>重命名为<Routes></Routes>
component重命名为element
*/}
<Routes>
<Route path='/login' element={<Login/>}></Route>
<Route path='/' element={<Admin/>}></Route>
</Routes>
</Router>

)
}
}

函数式组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
import Login from './pages/login'
import Admin from './pages/admin'
/**
* 根组件(函数式)
*/
export default function App() {
console.log(this); //此处的this是undefined,因为babel编译后开启了严格模式
return (
<Router>
{/* React-Router v6新特性 <Switch>重命名为<Routes></Routes>
component重命名为element
*/}
<Routes>
<Route path='/login' element={<Login />}></Route>
<Route path='/' element={<Admin />}></Route>
</Routes>
</Router>

)
}

React-Router v6 新特性

SPA的理解

  • 单页Web应用(single page web application,SPA)。
  • 整个应用只有一个完整的页面
  • 点击页面中的链接不会刷新页面,只会做页面的局部更新。
  • 数据都需要通过ajax请求获取, 并在前端异步展现。

基础使用

React Router v6 大量使用React hooks。官方明确推荐函数式组件了。

Routes是以前 Switch 组件的升级版,它包括相对路由和链接、自动路由排名、嵌套路由和布局等功能。

component重命名为element

v6的<Routes>元素下的所有<Route path>和<Link to>值都是自动相对于它们的父路由渲染的,而且忽略当前URL中的尾部斜杠。头部斜杠代变绝对路径。

中小型项目嵌套路由可集中显示,在需要显示的地方使用<Outlet />作为占位符。

分别显示,具有后代路由(在其他组件中定义)的路由在其路径中使用尾随*

“默认子路由”:index(不写path)表示索引路由共享父路径。这就是重点——它没有路径。

“未找到”路由:path='*'表示路径都不匹配时。具有最弱的优先级。

v6 提供了 Navigate 组件,以前版本中的Redirect组件也消失了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/**
* 根组件(函数式)
*/
export default function App() {
return (
<Routes>
<Route path='login' element={<Login />}></Route>
<Route path='/' element={<Admin />}>
<Route index element={<Home />} />
<Route path='category' element={<Category />} />
<Route path='product' element={<Product />}>
<Route index element={<ProductHome />} />
<Route path='detail' element={<ProductDetail />} />
<Route path='addupdate' element={<ProductAddUpdate />} />
</Route>
<Route path='user' element={<User />} />
<Route path='role' element={<Role />} />
<Route path="charts-bar" element={<Bar />} />
<Route path="charts-pie" element={<Pie />} />
<Route path="charts-line" element={<Line />} />
</Route>
{/* 以上路径都不匹配时 */}
<Route path='*' element={<NotFound />} />
</Routes>

)
}
/**
* 后台管理的路由组件
* */
export default function Admin() {
const user = memoryUtils.user
// 如果内存没有存储user ==> 当前没有登陆
if (!user || !user._id) {
// 自动跳转到登陆(在render()中) Navigate替代Redirect
return <Navigate to='/login' />
}
return (
...
<Outlet />{/* 占位符 */}
...

)

}

v6 无法直接访问history实例,将useHistory更改为 useNavigate(兼容性和体验)。

useNavigate返回一个函数用来实现编程式导航。

传入数值进行前进或后退,类似于5.x中的 history.go()方法 navigate(-1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React, { useEffect, useState } from 'react'
import { useNavigate } from "react-router-dom";

/**
* 登录的路由组件(函数式组件)
* 类式路由组件在onFinish中拿不到history
* */
export default function Login() {
const navigate = useNavigate();


// 提交表单且数据验证成功后回调事件
const onFinish = async values => {
...
// 跳转到管理界面 (不需要再回退回到登陆)
navigate('/', { replace: true })
} else {
...
}

}

return (
...
)

}

v5:withRouter高阶组件:包装非路由组件, 返回一个新的组件,新的组件向非路由组件传递3个属性: history/location/match

v6:非路由组件使用useLocation也可得到location对象,withRouter不再使用。useLocation().pathname得到当前路径名。

1
2
3
4
5
6
7
8
9
10
11
const location =useLocation();
const path = location.pathname;

// location对象
{
hash: ""
key: "hvfbi1fy"
pathname: "/role"
search: ""
state: null
}

useRoutes钩子是一个路由API,它允许你使用JavaScript对象而不是React元素来声明和组合路由。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
let element = useRoutes([
// 这些与您提供给 <Route> 的props相同
{ path: "/", element: <Home /> },
{ path: "dashboard", element: <Dashboard /> },
{
path: "invoices",
element: <Invoices />,
// 嵌套路由使用 children 属性,这也与 <Route> 相同
children: [
{ path: ":id", element: <Invoice /> },
{ path: "sent", element: <SentInvoices /> },
],
},
// Not found routes work as you'd expect
{ path: "*", element: <NotFound /> },
]);

// 返回的元素将呈现整个元素层次结构及其所需的所有适当上下文
return element;
}

Link

标签体内容也是一种特殊的标签属性。

在 v5 中,不以 / 开头的 <Link to> 值是不明确的; 这取决于当前的 URL 是什么。 例如,如果当前 URL 是 /users,则 v5 <Link to="me"> 将呈现 <a href="/me">。 但是,如果当前 URL 有一个结尾斜杠,例如 /users/,则相同的 <Link to="me"> 将呈现 <a href="/users/me">。 这使得很难预测链接的行为方式,因此在 v5 中,我们建议您从根 URL(使用 match.url)构建链接,而不是使用相对的 <Link to> 值。

React Router v6 修复了这种歧义。 在 v6 中,<Link to="me"> 将始终呈现相同的 <a href>,而不管当前的 URL。

例如,在 <Route path="users"> 中呈现的 <Link to="me"> 将始终呈现指向 /users/me 的链接,无论当前 URL 是否具有尾部斜杠。

当您想“向上”链接回父路由时,请在 <Link to> 值中使用前导 .. 段,类似于您在 <a href> 中所做的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
function App() {
return (
<Routes>
<Route path="users" element={<Users />}>
<Route path=":id" element={<UserProfile />} />
</Route>
</Routes>
);
}

function Users() {
return (
<div>
<h2>
{/* This links to /users - the current route */}
<Link to=".">Users</Link>
</h2>

<ul>
{users.map(user => (
<li>
{/* This links to /users/:id - the child route */}
<Link to={user.id}>{user.name}</Link>
</li>
))}
</ul>
</div>
);
}

function UserProfile() {
return (
<div>
<h2>
{/* This links to /users - the parent route */}
<Link to="..">All Users</Link>
</h2>

<h2>
{/* This links to /users/:id - the current route */}
<Link to=".">User Profile</Link>
</h2>

<h2>
{/* This links to /users/mj - a "sibling" route */}
<Link to="../mj">MJ</Link>
</h2>
</div>
);
}

路由参数传递

param参数
  • Route组件中的path属性中定义路径参数
  • 在组件内通过useParams hook访问路径参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 定义参数
<BrowserRouter>
<Routes>
<Route path='/foo/:id' element={Foo} />
</Routes>
</BrowserRouter>
// 传递参数
<LinkButton onClick={() => navigate(`addupdate/${product.id}`)} >
修改
</LinkButton>
// 提取参数
import { useParams } from 'react-router-dom';
export default function Foo(){
const params = useParams();
return (
<div>
<h1>{params.id}</h1>
</div>
)
}

个人理解:param参数适合传递一个参数时使用。

useMatch()

  1. 作用:返回当前匹配信息,对标5.x中的路由组件的match属性。

  2. 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <Route path="/login/:page/:pageSize" element={<Login />}/>
    <NavLink to="/login/1/10">登录</NavLink>

    export default function Login() {
    const match = useMatch('/login/:x/:y')
    console.log(match) //输出match对象
    //match对象内容如下:
    /*
    {
    params: {x: '1', y: '10'}
    pathname: "/LoGin/1/10"
    pathnameBase: "/LoGin/1/10"
    pattern: {
    path: '/login/:x/:y',
    caseSensitive: false,
    end: false
    }
    }
    */
    return (
    <div>
    <h1>Login</h1>
    </div>
    )
    }
search参数&sate参数

Location:这是一个 React Router 特定的对象,它基于内置浏览器的 window.location 对象。 它代表“用户在哪里”。 它主要是 URL 的对象表示,但比这更多。

location state:与 URL 中未编码的位置保持一致的值。 很像哈希或搜索参数(在 URL 中编码的数据),但不可见地存储在浏览器的内存中。

您可以通过两种方式设置location state<Link>上或navigate上,在下一路由组件中你可以用useLocation来访问它。

useLocation()既可以获得state参数,也可以获得search参数。

1
2
3
4
5
6
7
8
<LinkButton onClick={() => navigate('addupdate?name=tom&age=18', { state: product })} >
修改
</LinkButton>

const isUpdate = location.state
const search = location.search
console.log(search) // ?name=tom&age=18
console.log(isUpdate) // {status: 1, imgs: Array(2), _id: '5e12b97de31bb727e4b0e349', name: '联想ThinkPad 翼4809', desc: '年度重量级新品,X390、T490全新登场 更加轻薄机身设计9', …}

个人理解:state参数适合传js对象。

search参数一般使用useSearchParams()进行获取和修改。

  1. 作用:用于读取和修改当前位置的 URL 中的查询字符串。
  2. 返回一个包含两个值的数组,内容分别为:当前的seaech参数、更新search的函数。
1
2
3
const [search,setSearch] = useSearchParams()
const age = search.get('age')
const name = search.get('name')

个人理解:search参数适合传可能会被修改的参数。

Antd v3升级成v4

去除 Form.create

v4 的 Form 不再需要通过 Form.create() 创建上下文。Form 组件现在自带数据域,因而 getFieldDecorator 也不再需要,直接写入 Form.Item 即可:

1
2
3
4
5
6
7
8
9
10
11
12
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);

const WrappedDemo = Form.create()(Demo);

改成:

1
2
3
4
5
6
7
8
// antd v4
const Demo = () => (
<Form>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);

由于移除了 Form.create(),原本的 onFieldsChange 等方法移入 Form 中,通过 fields 对 Form 进行控制。

表单控制调整

Form 自带表单控制实体,如需要调用 form 方法,可以通过 Form.useForm() 创建 Form 实体进行操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// antd v3
const Demo = ({ form: { setFieldsValue } }) => {
React.useEffect(() => {
setFieldsValue({
username: 'Bamboo',
});
}, []);

return (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};

const WrappedDemo = Form.create()(Demo);

改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// antd v4
const Demo = () => {
const [form] = Form.useForm();

React.useEffect(() => {
form.setFieldsValue({
username: 'Bamboo',
});
}, []);

return (
<Form form={form}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};

对于 class component,也可以通过 ref 获得实体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// antd v4
class Demo extends React.Component {
formRef = React.createRef();

componentDidMount() {
this.formRef.current.setFieldsValue({
username: 'Bamboo',
});
}

render() {
return (
<Form ref={this.formRef}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
}
}

由于 Form.Item 内置字段绑定,如果需要不带样式的表单绑定,可以使用 noStyle 属性移除额外样式:

1
2
3
4
5
6
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => {
return <Form>{getFieldDecorator('username')(<Input />)}</Form>;
};

const WrappedDemo = Form.create()(Demo);

改成:

1
2
3
4
5
6
7
8
9
10
// antd v4
const Demo = () => {
return (
<Form>
<Form.Item name="username" noStyle>
<Input />
</Form.Item>
</Form>
);
};

字段联动调整

新版 Form 采用增量更新方式,仅会更新需要更新的字段。因而如果有字段关联更新,或者跟随整个表单更新而更新。可以使用 dependenciesshouldUpdate

onFinish 替代 onSubmit

对于表单校验,过去版本需要通过监听 onSubmit 事件手工触发 validateFields。新版直接使用 onFinish 事件,该事件仅当校验通过后才会执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// antd v3
const Demo = ({ form: { getFieldDecorator, validateFields } }) => {
const onSubmit = e => {
e.preventDefault();
validateFields((err, values) => {
if (!err) {
console.log('Received values of form: ', values);
}
});
};

return (
<Form onSubmit={onSubmit}>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
})(<Input />)}
</Form.Item>
</Form>
);
};

const WrappedDemo = Form.create()(Demo);

改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// antd v4
const Demo = () => {
const onFinish = values => {
console.log('Received values of form: ', values);
};

return (
<Form onFinish={onFinish}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);
};

scrollToField 替代 validateFieldsAndScroll

新版推荐使用 onFinish 进行校验后提交操作,因而 validateFieldsAndScroll 拆成更独立的 scrollToField 方法:

1
2
3
4
5
6
// antd v3
onSubmit = () => {
form.validateFieldsAndScroll((error, values) => {
// Your logic
});
};

改成:

1
2
3
4
// antd v4
onFinishFailed = ({ errorFields }) => {
form.scrollToField(errorFields[0].name);
};

初始化调整

此外,我们将 initialValue 从字段中移到 Form 中。以避免同名字段设置 initialValue 的冲突问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
// antd v3
const Demo = ({ form: { getFieldDecorator } }) => (
<Form>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true }],
initialValue: 'Bamboo',
})(<Input />)}
</Form.Item>
</Form>
);

const WrappedDemo = Form.create()(Demo);

改成:

1
2
3
4
5
6
7
8
// antd v4
const Demo = () => (
<Form initialValues={{ username: 'Bamboo' }}>
<Form.Item name="username" rules={[{ required: true }]}>
<Input />
</Form.Item>
</Form>
);

在 v3 版本中,修改未操作的字段 initialValue 会同步更新字段值,这是一个 BUG。但是由于被长期作为一个 feature 使用,因而我们一直没有修复。在 v4 中,该 BUG 已被修复。initialValue 只有在初始化以及重置表单时生效。

嵌套字段路径使用数组

过去版本我们通过 . 代表嵌套路径(诸如 user.name 来代表 { user: { name: '' } })。然而在一些后台系统中,变量名中也会带上 .。这造成用户需要额外的代码进行转化,因而新版中,嵌套路径通过数组来表示以避免错误的处理行为(如 ['user', 'name'])。

也因此,诸如 getFieldsError 等方法返回的路径总是数组形式以便于用户处理:

1
2
3
4
5
6
7
8
form.getFieldsError();

/*
[
{ name: ['user', 'name'], errors: [] },
{ name: ['user', 'age'], errors: ['Some error message'] },
]
*/

嵌套字段定义由:

1
2
// antd v3
<Form.Item label="Firstname">{getFieldDecorator('user.0.firstname', {})(<Input />)}</Form.Item>

改至:

1
2
3
4
// antd v4
<Form.Item name={['user', 0, 'firstname']} label="Firstname">
<Input />
</Form.Item>

相似的,setFieldsValue 由:

1
2
3
4
// antd v3
this.formRef.current.setFieldsValue({
'user.0.firstname': 'John',
});

改至:

1
2
3
4
5
6
7
8
// antd v4
this.formRef.current.setFieldsValue({
user: [
{
firstname: 'John',
},
],
});

validateFields 不再支持 callback

validateFields 会返回 Promise 对象,因而你可以通过 async/await 或者 then/catch 来执行对应的错误处理。不再需要判断 errors 是否为空:

1
2
3
4
5
6
// antd v3
validateFields((err, value) => {
if (!err) {
// Do something with value
}
});

改成:

1
2
3
4
// antd v4
validateFields().then(values => {
// Do something with value
});

数据可视化

  • echarts echarts-for-react
  • bizcharts

功能实现

登录数据持久化

默认使用最高级管理员登录,该管理员名称和密码为默认值,且拥有最高权限。通过向后端发送用户名和密码,后端返回用户信息,其中密码在后端使用md5加密格式返回,然后将用户信息保存在内存和localStorage中(store.js),每次打开该网站时就从localStorage中读取数据到内存,实现登录数据持久化。

输入校验

正则

/^[a-zA-Z0-9_]+$/用户名必须是英文,数字和下划线组成

/^[a-zA-Z0-9_]+$/.test(value)

跨域和Ajax

父子通信

props

  • 【父组件】给【子组件】传递数据:通过props传递

  • 【子组件】给【父组件】传递数据:通过props传递,要求父提前给子传递一个函数

    该项目中将useState的更新函数传给子组件,将数据作为函数参数传回父组件用来更新状态

  • 【子组件】给【父组件】传递函数/方法

    • useRef: 用于获取元素的原生DOM或者获取自定义组件所暴露出来的ref方法(父组件可以通过ref获取子组件,并调用相对应子组件中的方法)
    • useImperativeHandle:在函数式组件中,用于定义暴露给父组件的ref方法。
    • React.forwardRef: 将ref父类的ref作为参数传入函数式组件中,本身props只带有children这个参数,这样可以让子类转发父类的ref,当父类把ref挂在到子组件上时,子组件外部通过forwrardRef包裹,可以直接将父组件创建的ref挂在到子组件的某个dom元素上。

权限管理

仅仅是通过登录用户修改前端页面的展示内容,可以展示的内容在后端的修改不区分用户

发布

因为是静态站点,所以完全是客户端部分,那么在使用路由的时候如果不在意美观,尽可能地使用Hash路由,这样跳转会没有任何问题。因为browserRouter依赖于服务端支持,使用它的话访问会出现404 not Found.

由于最开时搭建项目没有考虑发布问题,而且v6版本的’/‘代变绝对路径,输入https://Lesliewaong.github.io/react-admin-client会自动跳转至https://Lesliewaong.github.io/,于是使用HashRouter来代替BrowserRouter进行发布。

BrowserRouter

这一种很自然,比如 / 对应 Home页/about 对应 About 页,但是这样的设计需要服务器端渲染,因为用户可能直接访问任何一个 URL,服务器端必须能对 /的访问返回 HTML,也要对 /about的访问返回 HTML。BrowserRouter支持这种URL。

①基于history模式:页面跳转原理是使用了HTML5为浏览器全局的history对象新增了两个API,包括 history.pushState、history.replaceState;和vue router的history模式实现一致
②更加优雅: 直接拼接路径;如:www.abc.com/xx
后端需做请求处理: 切换路由后,请求接口路径会发生变化,后端需要配合,做处理

HashRouter

这一种看起来不自然,但是实现更简单。

只有一个路径 /,通过 URL 后面的 # 部分来决定路由/#/ 对应 Home 页,/#/about 对应 About 页。

因为URL中#之后的部分是不会发送给服务器的,所以,无论哪个 URL,最后都是访问服务器的 / 路径,服务器也只需要返回同样一份 HTML就可以,然后由浏览器端解析#后的部分,完成浏览器端渲染。HashRouter支持这种URL。

基于hash模式:页面跳转原理是使用了location.hash、location.replace;和vue router的hash模式实现一致。
比较丑:在域名后,先拼接/#,再拼接路径;也就是利用锚点,实现路由的跳转;如:www.abc.com/#/xx

使用场景

HashRouter项目部署在内网:如To B的项目、本公司业务人员用的项目等等。

BrowserRouter项目部署在公网:如To C的项目、面向大众的项目,url路径美观点当然更好,但后端需要做处理。

GitHub Pages发布静态页面

  • 利用脚手架创建react 项目
1
create-react-app react-admin-client
  • 我们在github上面新建一个远程仓库,名字也叫react-admin-client。记录远程仓库的地址。

  • 修改并完善项目,达到你想要的那种程度,疯狂敲代码

  • 在package.json配置文件中加一句:

1
2
3
4
“homepage”: “https://Lesliewaong.github.io/react-admin-client” 

'react-admin-client '是 项目名称,跟远程仓库名字一样,'Lesliewaong'对应你的github用户名
homepage不配置的话或者配置有误,访问链接的时候就会报错404
  • 开始打包
1
2
npm run build
// 打包完成之后会生成一个build文件夹,内面都是静态资源
  • 之后我们就把项目传到github上面去
1
2
3
4
5
6
7
git add .
git commit -m '一些描述'
git remote add origin https://github.com/xxx(之前我们保存的地址)
// 一般提交代码之前我们都应该先拉下代码,避免冲突
git pull origin master
git push origin master
// 成功之后代码就会提交到master上面去,此时仓库中有相应的代码
  • 在github展示的是静态的html文件,我们上传的react项目是不能预览的,此时我们可以把打包好的build文件夹上传到另一个分支
1
git subtree push --prefix=build origin gh-pages
  • 执行上述操作之后,仓库多了个gh-pages分支

  • 进入仓库,点击Setting,找到GitHub Pages这一块,修改Source选择刚刚的gh-pages,然后保存

https://Lesliewaong.github.io/react-admin-client就是你项目预览的地址。点击链接就可以预览你的项目啦。

项目优化

自定义hook实现搜索防抖

Input event.target.value 获取输入值

useState 更新此时的搜索状态

自定义 hook useDebounce

使用useEffect时,如果直接将debounce放入包裹防抖函数,由于useEffect的特性导致每次都创建了新的定时器,定时器无法按规定清除,于是借助useEffect自定义useDebounce ,将定时器放入useEffect中,并在return中将定时器清除,React 会在组件卸载的时候执行清除操作。effect 在每次渲染的时候都会执行。React 会在执行当前 effect 之前对上一个effect进行清除。

1
2
3
4
5
6
7
8
9
10
11
import { useEffect } from 'react'
function useDebounce(fn, delay, dep=[]) {
useEffect(()=>{
let timer;
timer = setTimeout(fn, delay);
return ()=>{clearTimeout(timer);} // 这里用到useEffect清除的能力 类似于componentWillUnmount
// eslint-disable-next-line
}, [...dep]
)
}
export default useDebounce

后续可能的改进:

在 effect 内部去声明它所需要的函数,能容易的看出那个 effect 依赖了组件作用域中的哪些值,更安全。

其他发现:

依赖的值可以设置多个,只要有一个更新,就会执行effect。

useEffect(() => updateStatus(productStatus), [productStatus,productId])

day01

1. 项目开发准备

1). 描述项目
2). 技术选型 
3). API接口/接口文档/测试接口

2. 启动项目开发

1). 使用react脚手架创建项目
2). 开发环境运行: npm start
3). 生产环境打包运行: npm run build   serve build

3. git管理项目

1). 创建远程仓库
2). 创建本地仓库
    a. 配置.gitignore
    b. git init
    c. git add .
    d. git commit -m "init"
3). 将本地仓库推送到远程仓库
    git remote add origin url
    git push origin master
4). 在本地创建dev分支, 并推送到远程
    git checkout -b dev
    git push origin dev
5). 如果本地有修改
    git add .
    git commit -m "xxx"
    git push origin dev
6). 新的同事: 克隆仓库
    git clone url
    git checkout -b dev origin/dev
    git pull origin dev
7). 如果远程修改
    git pull origin dev

4. 创建项目的基本结构

api: ajax请求的模块
components: 非路由组件
pages: 路由组件
App.js: 应用的根组件
index.js: 入口js

5 引入antd

下载antd的包
按需打包: 只打包import引入组件的js/css
    下载工具包
    config-overrides.js
    package.json
自定义主题
    下载工具包
    config-overrides.js
使用antd的组件
    根据antd的文档编写

6. 引入路由

下载包: react-router-dom
拆分应用路由:
  Login: 登陆
  Admin: 后台管理界面
注册路由:
  <BrowserRouter>
  <Switch>
  <Route path='' component={}/>

7. Login的静态组件

1). 自定义了一部分样式布局
2). 使用antd的组件实现登陆表单界面
  Form  / Form.Item
  Input
  Icon
  Button

8. 收集表单数据和表单的前台验证

1). form对象
    如何让包含<Form>的组件得到form对象?  WrapLoginForm = Form.create()(LoginForm)
    WrapLoginForm是LoginForm的父组件, 它给LoginForm传入form属性
    用到了高阶函数和高阶组件的技术
2). 操作表单数据
    form.getFieldDecorator('标识名称', {initialValue: 初始值, rules: []})(<Input/>)包装表单项组件标签
    form.getFieldsValue(): 得到包含所有输入数据的对象
    form.getFieldValue(id): 根据标识得到对应字段输入的数据

3). 前台表单验证
    a. 声明式实时表单验证:
        form.getFieldDecorator('标识名称', {rules: [{min: 4, message: '错误提示信息'}]})(<Input/>)
    b. 自定义表单验证
        form.getFieldDecorator('标识名称', {rules: [{validator: this.validatePwd}]})(<Input/>)
        validatePwd = (rule, value, callback) => {
          if(有问题) callback('错误提示信息') else callack()
        } 
    c. 点击提示时统一验证
        form.validateFields((error, values) => {
          if(!error) {通过了验证, 发送ajax请求}
        })

9. 高阶函数与高阶组件

1. 高阶函数
    1). 一类特别的函数
        a. 接受函数类型的参数
        b. 返回值是函数
    2). 常见
        a. 定时器: setTimeout()/setInterval()
        b. Promise: Promise(() => {}) then(value => {}, reason => {})
        c. 数组遍历相关的方法: forEach()/filter()/map()/reduce()/find()/findIndex()
        d. 函数对象的bind()
        e. Form.create()() / getFieldDecorator()()
    3). 高阶函数更新动态, 更加具有扩展性

2. 高阶组件
    1). 本质就是一个函数
    2). 接收一个组件(被包装组件), 返回一个新的组件(包装组件), 包装组件会向被包装组件传入特定属性
    3). 作用: 扩展组件的功能
    
3. 高阶组件与高阶函数的关系
    高阶组件是特别的高阶函数
    接收一个组件函数, 返回是一个新的组件函数

day02

1. 后台应用

启动后台应用: mongodb服务必须启动
使用postman测试接口(根据接口文档):
    访问测试: post请求的参数在body中设置
    保存测试接口
    导出/导入所有测试接口

2. 编写ajax代码

1). ajax请求函数模块: api/ajax.js
    封装axios + Promise
    函数的返回值是promise对象  ===> 后面用上async/await
    自己创建Promise
      1. 内部统一处理请求异常: 外部的调用都不用使用try..catch来处理请求异常
      2. 异步返回是响应数据(而不是响应对象): 外部的调用异步得到的就直接是数据了(response --> response.data)
2). 接口请求函数模块: api/index.js
    根据接口文档编写(一定要具备这个能力)
    接口请求函数: 使用ajax(), 返回值promise对象
3). 解决ajax跨域请求问题(开发时)
    办法: 配置代理  ==> 只能解决开发环境
    编码: package.json: proxy: "http://localhost:5000"
4). 对代理的理解
    1). 是什么?
        具有特定功能的程序
    2). 运行在哪?
        前台应用端
        只能在开发时使用
    3). 作用?
        解决开发时的ajax请求跨域问题
        a. 监视并拦截请求(3000)
        b. 转发请求(4000)
    4). 配置代理
        告诉代理服务器一些信息: 比如转发的目标地址
        开发环境: 前端工程师
        生产环境: 后端工程师
5). async和await
    a. 作用?
       简化promise对象的使用: 不用再使用then()来指定成功/失败的回调函数
       以同步编码(没有回调函数了)方式实现异步流程
    b. 哪里写await?
        在返回promise的表达式左侧写await: 不想要promise, 想要promise异步执行的成功的value数据
    c. 哪里写async?
        await所在函数(最近的)定义的左侧写async

3. 实现登陆(包含自动登陆)

login.jsx
    1). 调用登陆的接口请求
    2). 如果失败, 显示错误提示信息
    3). 如果成功了:
        保存user到local/内存中
        跳转到admin
    4). 如果内存中的user有值, 自动跳转到admin
src/index.js
    读取local中user到内存中保存
admin.jsx
    判断如果内存中没有user(_id没有值), 自动跳转到login
storageUtils.js
    包含使用localStorage来保存user相关操作的工具模块
    使用第三库store
        简化编码
        兼容不同的浏览器
memoryUtils.js
    用来在内存中保存数据(user)的工具类

4. 搭建admin的整体界面结构

1). 整体布局使用antd的Layout组件
2). 拆分组件
    LeftNav: 左侧导航
    Header: 右侧头部
3). 子路由
    定义路由组件
    注册路由

5. LeftNav组件

1). 使用antd的组件
    Menu / Item / SubMenu

2). 使用react-router
    withRouter(): 包装非路由组件, 给其传入history/location/match属性
    history: push()/replace()/goBack()
    location: pathname属性
    match: params属性

3). componentWillMount与componentDidMount的比较
    componentWillMount: 在第一次render()前调用一次, 为第一次render()准备数据(同步)
    componentDidMount: 在第一次render()之后调用一次, 启动异步任务, 后面异步更新状态重新render

4). 根据动态生成Item和SubMenu的数组
    map() + 递归: 多级菜单列表
    reduce() + 递归: 多级菜单列表

5). 2个问题?
    刷新时如何选中对应的菜单项?
        selectedKey是当前请求的path
    刷新子菜单路径时, 自动打开子菜单列表?
        openKey是 一级列表项的某个子菜单项是当前对应的菜单项

day03

1. Header组件

1). 界面静态布局
    三角形效果
2). 获取登陆用户的名称显示
    MemoryUtils
3). 当前时间
    循环定时器, 每隔1s更新当前时间状态
    格式化指定时间: dateUtils
4). 天气预报
    使用jsonp库发jsonp请求百度天气预报接口
    对jsonp请求的理解
5). 当前导航项的标题
    得到当前请求的路由path: withRouter()包装非路由组件
    根据path在menuList中遍历查找对应的item的title
6). 退出登陆
    Modal组件显示提示
    清除保存的user
    跳转到login
7). 抽取通用的类链接按钮组件
    通过...透传所有接收的属性: <Button {...props} />    <LinkButton>xxxx</LinkButton>
    组件标签的所有子节点都会成为组件的children属性

2. jsonp解决ajax跨域的原理

1). jsonp只能解决GET类型的ajax请求跨域问题
2). jsonp请求不是ajax请求, 而是一般的get请求
3). 基本原理
    浏览器端:
        动态生成<script>来请求后台接口(src就是接口的url)
        定义好用于接收响应数据的函数(fn), 并将函数名通过请求参数提交给后台(如: callback=fn)
    服务器端:
        接收到请求处理产生结果数据后, 返回一个函数调用的js代码, 并将结果数据作为实参传入函数调用
    浏览器端:
        收到响应自动执行函数调用的js代码, 也就执行了提前定义好的回调函数, 并得到了需要的结果数据

day04: Category组件

1. 使用antd组件构建分类列表界面

Card
Table
Button
Icon

2. 相关接口请求函数

获取一级/二级分类列表
添加分类
更新分类

3. 异步显示一级分类列表

设计一级分类列表的状态: categorys
异步获取一级分类列表: componentDidMount(){}
更新状态, 显示

4. 显示二级分类列表

设计状态: subCategorys / parentId / parentName
显示二级分类列表: 根据parentId状态值, 异步获取分类列表
setState()的问题
    setState()更新状态是异步更新的, 直接读取状态值还是旧的状态值
    setState({}, [callback]), 回调函数是在状态更新且界面更新之后执行, 可以在此获取最新的状态

5. 更新分类

1). 界面
    antd组件: Modal, Form, Input
    显示/隐藏: showStatus状态为2/0
    
2). 功能
    父组(Category)件得到子组件(AddForm)的数据(form)
    调用更新分类的接口
    重新获取分类列表

day05

1. 添加分类

1). 界面
    antd组件: Modal, Form, Select, Input
    显示/隐藏: showStatus状态为1/0
    
2). 功能
    父组(Category)件得到子组件(AddForm)的数据(form)
    调用添加分类的接口
    重新获取分类列表

2. Product整体路由

1). 配置子路由: 
    ProductHome / ProductDetail / ProductAddUpdate
    <Route> / <Switch> / <Redirect>

2). 匹配路由的逻辑:
    默认: 逐层匹配   <Route path='/product' component={ProductHome}/>
    exact属性: 完全匹配

3. 分页实现技术(2种)

1). 前台分页
    请求获取数据: 一次获取所有数据, 翻页时不需要再发请求
    请求接口: 
        不需要指定请求参数: 页码(pageNum)和每页数量(pageSize)
        响应数据: 所有数据的数组

2). 基于后台的分页
    请求获取数据: 每次只获取当前页的数据, 翻页时要发请求
    请求接口: 
        需要指定请求参数: 页码(pageNum)和每页数量(pageSize)
        响应数据: 当前页数据的数组 + 总记录数(total)

3). 如何选择?
    基本根据数据多少来选择

4. ProductHome组件

1). 分页显示
   界面: <Card> / <Table> / Select / Icon / Input / Button
   状态: products / total
   接口请求函数需要的数据: pageNum, pageSize
   异步获取第一页数据显示
       调用分页的接口请求函数, 获取到当前页的products和总记录数total
       更新状态: products / total
   翻页:
       绑定翻页的监听, 监听回调需要得到pageNum
       异步获取指定页码的数据显示  
 
2). 搜索分页
   接口请求函数需要的数据: 
       pageSize: 每页的条目数
       pageNum: 当前请求第几页 (从1开始)
       productDesc / productName: searchName 根据商品描述/名称搜索
   状态:  searchType / searchName  / 在用户操作时实时收集数据
   异步搜索显示分页列表
       如果searchName有值, 调用搜索的接口请求函数获取数据并更新状态
       
3). 更新商品的状态
   初始显示: 根据product的status属性来显示  status = 1/2
   点击切换:
       绑定点击监听
       异步请求更新状态

4). 进入详情界面
   history.push('/product/detail', {product})

5). 进入添加界面
    history.push('/product/addupdate')

5. ProductDetail组件

1). 读取商品数据: this.props.location.state.product
2). 显示商品信息: <Card> / List 
3). 异步显示商品所属分类的名称
    pCategoryId==0 : 异步获取categoryId的分类名称
    pCategoryId!=0: 异步获取 pCategoryId/categoryId的分类名称
4). Promise.all([promise1, promise2])
    返回值是promise
    异步得到的是所有promsie的结果的数组
    特点: 一次发多个请求, 只有当所有请求都成功, 才成功, 并得到成功的数据,一旦有一个失败, 整个都失败

day06

1. ProductAddUpdate

1). 基本界面
    Card / Form / Input / TextArea / Button
    FormItem的label标题和layout
    
2). 分类的级联列表
    Cascader的基本使用
    异步获取一级分类列表, 生成一级分类options
    如果当前是更新二级分类的商品, 异步获取对应的二级分类列表, 生成二级分类options, 并添加为对应option的children
    async函数返回值是一个新promise对象, promise的结果和值由async函数的结果决定
    当选择某个一级分类项时, 异步获取对应的二级分类列表, 生成二级分类options, 并添加为当前option的children

3). 表单数据收集与表单验证

2. PicturesWall

1). antd组件
    Upload / Modal / Icon
    根据示例DEMO改造编写
2). 上传图片
    在<Upload>上配置接口的path和请求参数名
    监视文件状态的改变: 上传中 / 上传完成/ 删除
    在上传成功时, 保存好相关信息: name / url
    为父组件提供获取已上传图片文件名数组的方法
3). 删除图片
    当文件状态变为删除时, 调用删除图片的接口删除上传到后台的图片
4). 父组件调用子组件对象的方法: 使用ref技术
    1. 创建ref容器: thi.pw = React.createRef()
    2. 将ref容器交给需要获取的标签元素: <PicturesWall ref={this.pw} />  // 自动将将标签对象添加为pw对象的current属性
    3. 通过ref容器读取标签元素: this.pw.current

day07

1. RichTextEditor

1). 使用基于react的富文本编程器插件库: react-draft-wysiwyg
2). 参考库的DEMO和API文档编写
3). 如果还有不确定的, 百度搜索, 指定相对准确的关键字

2. 完成商品添加与修改功能

1). 收集输入数据
    通过form收集: name/desc/price/pCategoryId/categoryId
    通过ref收集: imgs/detail
    如果是更新收集: _id
    将收集数据封装成product对象
2). 更新商品
    定义添加和更新的接口请求函数
    调用接口请求函数, 如果成功并返回商品列表界面

3. 角色管理

1). 角色前台分页显示
2). 添加角色
3). 给指定角色授权
    界面: Tree
    状态: checkedKeys, 根据传入的role的menus进行初始化
    勾选某个Node时, 更新checkedKeys
    点击OK时: 通过ref读取到子组件中的checkedKeys作为要更新product新的menus
            发请求更新product
    解决默认勾选不正常的bug: 利用组件的componentWillReceiveProps()

day08

1. setState()的使用

1). setState(updater, [callback]),
    updater为返回stateChange对象的函数: (state, props) => stateChange
    接收的state和props被保证为最新的
2). setState(stateChange, [callback])
    stateChange为对象,
    callback是可选的回调函数, 在状态更新且界面更新后才执行
3). 总结:
    对象方式是函数方式的简写方式
        如果新状态不依赖于原状态 ===> 使用对象方式
        如果新状态依赖于原状态 ===> 使用函数方式
    如果需要在setState()后获取最新的状态数据, 在第二个callback函数中读取

2. setState()的异步与同步

1). setState()更新状态是异步还是同步的?
    a. 执行setState()的位置?
        在react控制的回调函数中: 生命周期勾子 / react事件监听回调
        非react控制的异步回调函数中: 定时器回调 / 原生事件监听回调 / promise回调 /...
    b. 异步 OR 同步?
        react相关回调中: 异步
        其它异步回调中: 同步

2). 关于异步的setState()
    a. 多次调用, 如何处理?
        setState({}): 合并更新一次状态, 只调用一次render()更新界面 ---状态更新和界面更新都合并了
        setState(fn): 更新多次状态, 但只调用一次render()更新界面  ---状态更新没有合并, 但界面更新合并了
    b. 如何得到异步更新后的状态数据?
        在setState()的callback回调函数中

3. Component与PureComponent

1). Component存在的问题?
    a. 父组件重新render(), 当前组件也会重新执行render(), 即使没有任何变化
    b. 当前组件setState(), 重新执行render(), 即使state没有任何变化
  
2). 解决Component存在的问题
    a. 原因: 组件的shouldcomponentUpdate()默认返回true, 即使数据没有变化render()都会重新执行
    b. 办法1: 重写shouldComponentUpdate(), 判断如果数据有变化返回true, 否则返回false
    c. 办法2: 使用PureComponent代替Component
    d. 说明: 一般都使用PureComponent来优化组件性能
  
3). PureComponent的基本原理
    a. 重写实现shouldComponentUpdate()
    b. 对组件的新/旧state和props中的数据进行浅比较, 如果都没有变化, 返回false, 否则返回true
    c. 一旦componentShouldUpdate()返回false不再执行用于更新的render()
  
4). 面试题:
    组件的哪个生命周期勾子能实现组件优化?
    PureComponent的原理?
    区别Component与PureComponent?

4. 用户管理

1). 显示用户分页列表
2). 添加用户
3). 修改用户
4). 删除用户

5. 导航菜单权限控制

1). 基本思路(依赖于后台): 
    角色: 包含所拥有权限的所有菜单项key的数组: menus=[key1, key2, key3]
    用户: 包含所属角色的ID: role_id
    当前登陆用户: user中已经包含了所属role对象
    遍历显示菜单项时: 判断只有当有对应的权限才显示
2). 判断是否有权限的条件?
    a. 如果当前用户是admin
    b. 如果当前item是公开的
    c. 当前用户有此item的权限: key有没有menus中
    d. 如果当前用户有此item的某个子item的权限

day09

1. redux理解

什么?: redux是专门做状态管理的独立第3方库, 不是react插件, 但一般都用在react项目中
作用?: 对应用中状态进行集中式的管理(写/读)
开发: 与react-redux, redux-thunk等插件配合使用

2. redux相关API

redux中包含: createStore(), applyMiddleware(), combineReducers()
store对象: getState(), dispatch(), subscribe()
react-redux: 
    <Provider store={store}>: 向所有的容器组件提供store
    connect(
        state => ({xxx: state.xxx}),
        {actionCreator1, actionCreator2}
    )(UI组件): 
        产生的就是容器组件, 负责向UI组件传递标签属性, 
        一般属性值从state中获取, 函数属性内部会执行dispatch分发action

3. redux核心概念(3个)

action: 
    默认是对象(同步action), {type: 'xxx', data: value}, 需要通过对应的actionCreator产生, 
    它的值也可以是函数(异步action), 需要引入redux-thunk才可以
reducer
    根据老的state和指定的action, 返回一个新的state
    不能修改老的state
store
    redux最核心的管理对象
    内部管理着: state和reducer
    提供方法: getState(), dispatch(action), subscribe(listener)

4. redux工作流程

5. 使用redux及相关库编码

需要引入的库: 
    redux
    react-redux
    redux-thunk
    redux-devtools-extension(这个只在开发时需要)
redux文件夹: 
    action-types.js
    actions.js
    reducers.js
    store.js
组件分2类: 
    ui组件(components): 不使用redux相关API
    容器组件(containers): 通过connect()()生成的组件

day10

1. 在项目中搭建redux整套环境

1). store.js
2). reducer.js
3). actions.js
4). action-types.js
5). index.js
6). 在需要与redux进行状态数据通信(读/写)的UI组件包装生成容器组件

2. 通过redux管理头部标题headTitle数据

1). action-types.js
2). actoins.js
3). reducer.js
4). 相关组件: 
    left-nav.js
    header.js

3. 通过redux管理登陆用户信息user数据

1). action-types.js
2). actoin.js
3). reducer.js
4). 相关组件: 
    login.js
    admin.js
    left-nav.js
    header.js
    role.js

4. 自定义redux库

1). redux库向外暴露下面几个函数
    createStore(): 接收的参数为reducer函数, 返回为store对象
    combineReducers(): 接收包含n个reducer方法的对象, 返回一个新的reducer函数
    applyMiddleware() // 暂不实现

2). store对象的内部结构
    getState(): 返回值为内部保存的state数据
    dispatch(): 参数为action对象
    subscribe(): 参数为监听内部state更新的回调函数

3). combineReducers函数:
    返回的总reducer函数内部会根据总的state和指定的action, 
    调用每个reducer函数得到对应的新的state, 并封装成一个新的总state对象返回

5. 自定义react-redux库

1). react-redux向外暴露了2个API
    a. Provider组件类
    b. connect函数

2). Provider组件
    接收store属性
    通过context将store暴露给所有的容器子组件
    Provider原样渲染其所有标签子节点
    
3). connect函数
    接收2个参数: mapStateToProps和mapDispatchToProps
    connect()执行的返回值为一个高阶组件: 包装UI组件, 返回一个新的容器组件
    mapStateToProps: 
        为一个函数, 返回包含n个一般属性对象, 
        容器组件中调用得到对象后, 初始化为容器组件的初始状态, 并指定为UI组件标签的一般属性
    mapDispatchToProps:
        如果为函数, 调用得到包含n个dispatch方法的对象
        如果为对象, 遍历封装成包含n个dispatch方法的对象
        将包含n个dispatch方法的对象分别作为函数属性传入UI组件
    通过store绑定state变化的监听, 在回调函数中根据store中最新的state数据更新容器组件状态, 从而更新UI组件

day11

1. 数据可视化

1). echarts(百度) ==> echarts-for-react
2). g2(阿里) ==> bizCharts
3). d3(国外)

2. 前台404界面

<Redirect from='/' to='/home' exact/>
<Route component={NotFound}/>

3. 打包应用运行

1). 解决生产环境ajax跨域问题
    使用nginx的反向代理解决(一般由后台配置)
    CORS: 允许浏览器端跨域
2). BrowserRouter模式刷新404的问题
    a. 问题: 刷新某个路由路径时, 会出现404的错误
    b. 原因: 项目根路径后的path路径会被当作后台路由路径, 去请求对应的后台路由, 但没有
    c. 解决: 使用自定义中间件去读取返回index页面展现