React实战全栈后台
React实战全栈后台
项目预览:http://lesliewaong.top/react-admin-client/
准备
项目描述
- 此项目为一个前后台分离的后台管理的 SPA(单页面应用), 包括前端 PC 应用和后端应用
- 包括用户管理 / 商品分类管理 / 商品管理 / 权限管理等功能模块
- 前端: 使用 React 全家桶(函数式组件、react-router V6) + Antd(v4) + Axios + ES6 + Webpack 等技术
- 后端: 使用 Node + Express + Mongodb 等技术
- 采用模块化、组件化、工程化的模式开发
技术选型
项目结构
1 | public ---- 静态资源文件夹 |
前端路由
1 | >pages -------- 路由组件 |
接口
npm/yarn 常用命令
1 | ## 设置淘宝镜像 |
git 常用基本命令
1 | git config --global user.name "username" //配置用户名 |
应用开发详解
开启项目开发
使用create-react-app
(脚手架)搭建项目
create-react-app
是react 官方提供的用于搭建基于react
+webpack
+es6
项目的脚手架
1 | # 全局下载工具 |
编码测试与打包发布项目
1 | # 编码, 自动编译打包刷新(live-reload), 查看效果 |
git管理项目
1 | 1). 创建远程仓库 |
项目源码基本目录设计
1 | src ---- 源码文件夹 |
引入antd
antd
是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。
craco
一个对 create-react-app 进行自定义配置的社区解决方案
下载安装
yarn add antd
安装 antdyarn add @craco/craco
安装 cracoyarn add craco-less
下载 less ,支持 less 格式文件
package.json 中修改属性
1 | "scripts": { |
根目录创建 craco.config.js 文件
1 | const CracoLessPlugin = require('craco-less'); |
引入 less 文件
在 src 目录下的 index.js 文件中
1 | import 'antd/dist/antd.less' |
某个组件中使用antd组件
1 | import {Button} from 'antd' |
引入路由
yarn add react-router-dom
Login组件(不与后台交互)
静态组件
前台表单验证与数据收集
运行server 端项目
该项目是一个前后台分离的项目: 前台应用与后台应用
后台应用负责处理前台应用提交的请求, 并给前台应用返回json 数据
前台应用负责展现数据, 与用户交互, 与后台应用交互
运行后台应用
- 确保启动
mongodb
服务 - 启动服务器应用:
npm start
使用postman工具测试接口
- postman 是用来测试API 接口的工具
- postman 可以看作活接口文档
前后台交互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.exports
、require
方法用于加载模块。
前端: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
登录界面
气泡背景原理:
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)可以是0
到1
之间的数字,或者百分比,数字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 | import React, { Component } from 'react' |
函数式组件
1 | import React from 'react' |
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 | /** |
v6 无法直接访问history实例,将useHistory更改为 useNavigate(兼容性和体验)。
useNavigate返回一个函数用来实现编程式导航。
传入数值进行前进或后退,类似于5.x中的 history.go()方法 navigate(-1)
1 | import React, { useEffect, useState } from 'react' |
v5:withRouter高阶组件:包装非路由组件, 返回一个新的组件,新的组件向非路由组件传递3个属性: history/location/match
v6:非路由组件使用useLocation
也可得到location
对象,withRouter
不再使用。useLocation().pathname
得到当前路径名。
1 | const location =useLocation(); |
useRoutes
钩子是一个路由API,它允许你使用JavaScript对象而不是React元素来声明和组合路由。
1 | function App() { |
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 | function App() { |
路由参数传递
param参数
- 在
Route组件
中的path属性
中定义路径参数 - 在组件内通过
useParams
hook访问路径参数
1 | // 定义参数 |
个人理解:param参数适合传递一个参数时使用。
useMatch()
作用:返回当前匹配信息,对标5.x中的路由组件的
match
属性。示例代码:
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 | <LinkButton onClick={() => navigate('addupdate?name=tom&age=18', { state: product })} > |
个人理解:state参数适合传js对象。
search
参数一般使用useSearchParams()
进行获取和修改。
- 作用:用于读取和修改当前位置的 URL 中的查询字符串。
- 返回一个包含两个值的数组,内容分别为:当前的seaech参数、更新search的函数。
1 | const [search,setSearch] = useSearchParams() |
个人理解:search参数适合传可能会被修改的参数。
Antd v3升级成v4
去除 Form.create
v4 的 Form 不再需要通过 Form.create()
创建上下文。Form 组件现在自带数据域,因而 getFieldDecorator
也不再需要,直接写入 Form.Item 即可:
1 | // antd v3 |
改成:
1 | // antd v4 |
由于移除了 Form.create()
,原本的 onFieldsChange
等方法移入 Form 中,通过 fields
对 Form 进行控制。
表单控制调整
Form 自带表单控制实体,如需要调用 form 方法,可以通过 Form.useForm()
创建 Form 实体进行操作:
1 | // antd v3 |
改成:
1 | // antd v4 |
对于 class component,也可以通过 ref
获得实体:
1 | // antd v4 |
由于 Form.Item 内置字段绑定,如果需要不带样式的表单绑定,可以使用 noStyle
属性移除额外样式:
1 | // antd v3 |
改成:
1 | // antd v4 |
字段联动调整
新版 Form 采用增量更新方式,仅会更新需要更新的字段。因而如果有字段关联更新,或者跟随整个表单更新而更新。可以使用 dependencies
或 shouldUpdate
。
onFinish 替代 onSubmit
对于表单校验,过去版本需要通过监听 onSubmit
事件手工触发 validateFields
。新版直接使用 onFinish
事件,该事件仅当校验通过后才会执行:
1 | // antd v3 |
改成:
1 | // antd v4 |
scrollToField 替代 validateFieldsAndScroll
新版推荐使用 onFinish
进行校验后提交操作,因而 validateFieldsAndScroll
拆成更独立的 scrollToField
方法:
1 | // antd v3 |
改成:
1 | // antd v4 |
初始化调整
此外,我们将 initialValue
从字段中移到 Form 中。以避免同名字段设置 initialValue
的冲突问题:
1 | // antd v3 |
改成:
1 | // antd v4 |
在 v3 版本中,修改未操作的字段 initialValue
会同步更新字段值,这是一个 BUG。但是由于被长期作为一个 feature 使用,因而我们一直没有修复。在 v4 中,该 BUG 已被修复。initialValue
只有在初始化以及重置表单时生效。
嵌套字段路径使用数组
过去版本我们通过 .
代表嵌套路径(诸如 user.name
来代表 { user: { name: '' } }
)。然而在一些后台系统中,变量名中也会带上 .
。这造成用户需要额外的代码进行转化,因而新版中,嵌套路径通过数组来表示以避免错误的处理行为(如 ['user', 'name']
)。
也因此,诸如 getFieldsError
等方法返回的路径总是数组形式以便于用户处理:
1 | form.getFieldsError(); |
嵌套字段定义由:
1 | // antd v3 |
改至:
1 | // antd v4 |
相似的,setFieldsValue
由:
1 | // antd v3 |
改至:
1 | // antd v4 |
validateFields 不再支持 callback
validateFields
会返回 Promise 对象,因而你可以通过 async/await
或者 then/catch
来执行对应的错误处理。不再需要判断 errors
是否为空:
1 | // antd v3 |
改成:
1 | // antd v4 |
数据可视化
- 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 | “homepage”: “https://Lesliewaong.github.io/react-admin-client” |
- 开始打包
1 | npm run build |
- 之后我们就把项目传到github上面去
1 | git add . |
- 在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 | import { useEffect } from 'react' |
后续可能的改进:
在 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页面展现