介绍
一款javascript前端框架,把用户界面抽象成一个个的组件,按需组合成页面,官网,与其他框架的共同点是,都采用虚拟dom,和数据驱动
一个js库用于构建用户界面
react 关注的是用户界面
react 不是完整的mvc框架 V
React 起源 : React起源于Facebook 的内部项目,因为该公司对市场上所有的jsMVC框架都不满意,就决定自己写一套,用来架设Instagram的网站,做出来以后,发现很好用 ,就在2013.5开源了。
| angularJs | reactJs | vueJs | angularTs | |
|---|---|---|---|---|
| 控制器 | √ | - | - | 弱化 |
| 过滤器 | √ | - | √ | √ |
| 指令 | √ 重 | - | √ | √ |
| 模板语法 | √ | - | √ | √ |
| 服务 | √ | - | - | √ |
| 组件 | - | √ | √ | √ |
| jsx | - | √ | 加入 | - |
react 提出了 vdom jsx 手写vdom 性能提升
ES6 语法
- let const 都是块级作用域
- 箭头函数
function foo(){
this.bar = 1;
this.f = (a) => a + this.bar;
}
//等价于
function foo(){
this.bar = 1;
this.f = (function(a){
return a + this.bar
}).bind(this);
}
- 模板字符串 可以拼变量 换行
- 解构赋值
//结构数组
//解构对象
- rest参数 获取函数的多余参数 形式为...变量名) ,rest参数之后不能再有其他参数
- 扩展运算符 是三个点(...),它将一个数组转为用逗号分隔的参数 序列,类似于rest参数的逆运
- class
- import export 模块化
react 高性能的两个体现
- 虚拟dom
- react fiber 算法 之前是diff diff 更新过程是同步的,可能会导致性能问题,只要一个加载或更新过程开始,中途不会中断,因为js单线程的特点,如果组件树很大的时候,每个同步任务耗时间太长,会出现卡顿。
ReactFiber:方法是分片,把一个耗时很长的任务分成很多小片,每一个小片运行时间都很短,虽然总时间依然很长,但是每个小片在运行完之后,都会给其他任务一个执行的机会,这样唯一的县城就不会被独占,其他任务仍然有机会运行。
React特点和优势
环境搭建
官方脚手架
安装 yarn
//查询当前镜像
yarn config get registry
//设置为淘宝镜像
yarn config set registry https://registry.npm.taobao.org/
//设置为官方镜像
//yarn config set registry https://registry.yarnpkg.com
yarn global add create-react-app
或
npm install create-react-app -g //非安装包安装的yarn 推荐
创建 react项目
create-react-app 目录 | npx create-react-app 目录 | npm init react-app 目录
yarn eject 解构出所有的配置文件 可选
yarn start | npm start 开发
yarn build | npm run build 打包
//调试 需要安装给chrome浏览器一个插件 react-dev-tools
环境解析
- react: 核心包,解析组件,识别jsx 演示
- react-dom: 编译 -> 浏览器 演示
- react-scripts: react的项目环境配置
- manifest.json 生成一个网页的桌面快捷方式时,会以这个文件中的内容作为图标和文字的显示内容
- registerServiceWorker.js支持离线访问,所以用起来和原生app的体验很接近,只有打包生成线上版本的react项目时,registerServiceWorker.js才会有效。服务器必须采用https协议
- 对Internet Explorer 9,10和11的支持需要polyfill。
环境配置
npm run eject | yarn eject
报git错误时:
git add . -> git commit -m 'init' -> yarn eject
报缺少babel 包: 安装一下
//修改端口
//修改script/start.js
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || 3001;
//去除eslint 警告
//config/webpack.config.js
//注释关于eslint的导入和rules规则
第三方脚手架
yomen/dva/umi
webpack手动搭建
资源限制
-
本地资源导入(import) 不可以导入src之外的包
-
相对 路径指向src,绝对路径 指向了 public目录
-
前景图片, 相对 和 绝对路径 都指向了 public目录
基础结构
import React from 'react'
import ReactDom from 'react-dom'
import App from './app'
//将组件们挂在到页面上
ReactDom.render(
<>
<App />
</>,
document.getElementById('root'),
()=>{
console.log('成功后的回调函数')
}
)
react优点
- 虚拟DOM
- 组件化
- 单向数据流
- jsx
virtual dom虚拟DOM概念
它并不直接对DOM进行操作,引入了一个叫做virtual-dom的概念,安插在javascript逻辑和实际的DOM之间,好处是减少DOM操作,减少DOM操作的目的是提高浏览器的渲染性能。 虚拟dom就中小型项目而言,的确从表象上看不出太多的优势,因为它解决的是底层的dom渲染,IO开销问题。但是想想facebook的体量,不难猜出react的诞生是为了解决更复杂更大型的项目开发和管理的。 实际上React和Vue其实也在操作DOM,只是比较高效地在操作DOM而已,虚拟DOM其实最终也会映射到真实DOM,虽然虚拟DOM只会将变化的部分更新到真实DOM,但实际上直接操作DOM也可以通过某些方式去优化,那么: 1、操作data,不直接操作DOM有什么好处? 更少的代码做更多的事。 2、操作data会给DOM操作带来什么不好的地方吗? 不会,但是不是所有功能“使用操作data”都可以代替的。 3、会不会比直接操作DOM存在什么难度? 不会有难度,但是思维需要有一些转变。
JSX
jsx是一个 JavaScript 的语法扩展,可以理解为js的一个新的数据类型,类XML(JSON前身)语法,出现在js当中,文件为xx.js|xx.jsx
var b= <strong>强壮</strong>
JSX语法只是 React.createElement (component, props, ...children)的语法糖,所有的JSX 语法最终都会被转换成对这个方法的调用
语法要求
- 标签要闭合
- 元素必须要有一个顶层元素
- 变量首字母大写代表组件,小写对应是js数据类型
- 属性,小驼峰命名
<xx tabIndex="2">
JSX 是一个 JavaScript 语法扩展。它类似于模板语言,但它具有 JavaScript 的全部能力。JSX 最终会被编译为 React.createElement() 函数调用,返回称为 “React 元素” 的普通 JavaScript 对象
React.createElement(type,props,children)
import {Component, createElement} from 'react';
class App extends Component {
state = {
bl: false
}
render() {
return (
createElement(
////三个参数,分别是,元素名称,属性(对象),内容
' h1',
{
className: 'app',
style: {
fontSize: '100ppx',
color: 'green'
}
},
'hello world'
)
)
}
}
类
es6
class Person2223{
constructor(name){
this.name=name||'alex' //实例属性创建,赋值
}
show(){//实例方法
console.log('show',this.name);
}
}
Person2223.VERSION='1.2.3';//静态属性|类属性
//子类
class Worker123 extends Person2223{
constructor(name,job){
super(name);//类如果有继承 super就要出现
this.job=job||'卖烧饼';
}
show2(){
console.log(this.job,this.name);
}
}
es6+
//es7 类
class Person123{
name='alex'; //实例属性 放在类内部,设置默认值
age; //没有默认值的实例属性
static VER='1.11.1'; //类属性 静态属性
constructor(name,age){
this.name=name;
this.age=age||20; //构造器里面可以初始化实例属性
}
show(){//方法
console.log(this.name,this.age,this.show);//访问实例属性
}
static show2(){//静态|类 方法定义
console.log(this.name)
}
}
class Workerr321 extends Person123{
job; //实例属性
static SUM=100;
constructor(name,age,job){
super(name,age);//调用父类 影响父类传入到当前的实例属性
this.job=job||'卖闲鱼'; //构造器初始化
// this.address='外滩18号';//实例属性,要实现声明
}
showJob(){
console.log(this.job);
}
}
state状态
/*this.state.age+=1 不能直接修改 */
/* 1、 修改state 只传要修改的 key和value
* this.setState({age:20})
* 可以修改state
* setState 是异步,多个setState 会合并成一个
* 不可以拿上一次setState的结果,作为下一次的参考
* 因为两次修改的是同一个state 后修改的会覆盖先修改的
* this.setState({age:this.state.age+1});
* this.setState({age:this.state.age+1});
* 这时age 只会加一
* */
/* 2 、 修改state 传一个回调函数
* 可以用回调函数做参数,说明setState是个异步函数
* this.setState({age:19})
* this.setState((asyncState,asyncProps)=>{
* //asyncState此实例的state asyncProps 父组件传过来的参数
* console.log(this.state.age)//同步结果
* console.log(asyncState.age)//拿到异步结果
* return{
* age:asyncState.age +5
* }
*})
* */
/* 3、修改state 传一个对象,在对象里修改state 和一个回调函数 推荐的使用方式 */
/*this.setState({
age:this.state.age + 1
},()=>{
console.log(this.state.age);// 异步结果
})*/
/* 4、 修改state
* 第一个参数为函数时,state参数可以读取最新的state
* 因为setState是异步函数,当连续修改时,有可能state直接为最后一次修改的值,
* 而当第一个参数是函数,并且函数的参数是state时,这时的state是每次修改的最新值
* */
/*this.setState((state)=>{
return {
age:this.state.age +8
}},()=>{
console.log(this.state.age)
})
*/
组件
react组件:类组件和函数式组件和api组件(React.createClass)
创建组件
//es6
import React from 'react';
class 组件名 extends React.Component{
state={} 实例属性 组件状态
static msg; 类属性
constrctor(props){ //需要在构造时,修改组件的状态时,constrctor才会出现
super(props) //类如果有继承 super就要出现
需要在组件构造器内处理传递过来的props时,props参数就出现
this.state={ // 本地状态
}
}
render(){
return jsx|null //jsx~~要渲染 null不渲染
}
方法1(){} 自定义的方法
static 方法2(){}
}
//es5
var React = require('react');
let 组件名 = React.createClass({
getInitialState:function(){ //组件状态
return {
数据:值
}
}
/**/
render:function(){
return jsx
}
});
使用组件
<App/>
<Header></Header>
嵌套组件
渲染(描画)页面
import ReactDom from 'react-dom';
var RactDom = require('react-dom');
ReactDom.render(jsx,插入点,回调)
React 组件名首字母必须大写 html元素小写
类组件 函数式组件
props
传递属性
<组件名 属性名=值 属性名2=值2 .. />
propName="字符" propName={js数据类型}
使用属性
{this.props.属性名}
this 代表的是组件本身
对象无法直接通过{对象}展示
类型检查
import propsTypes from 'prop-types'
//默认值:
组件.defaultProps={propName:值,xx:oo}
//类型约定:
组件.propTypes={propsName:propsTypes库.类型名,xx:类型}
//propsTypes.array/bool/func/number/object/string
//必传参数
propName: propsTypes库.类型名.isRequired
组件无论是使用函数声明还是通过 class 声明,都决不能修改自身的 props
事件
- React 事件的命名采用小驼峰式(camelCase),而不是纯小写。
- 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
- 类组件,事件函数内部this会丢失
事件绑定
<JSX元素 onClick={this.实例方法|函数体}
修正this
onClick={this.方法.bind(this,值)}
onClick={()=>this.方法()}
构造器: this.方法=this.方法.bind(this) √
this.方法=()=>{箭头函数定义方法} √√
事件对象
实例方法(ev) ev 代理事件对象 ev.target 返回虚拟Vdom √
冒泡
阻止: ev.stopPropagation()
默认行为
阻止: ev.preventDefault()
组件状态
state|数据|私有状态|本地状态
定义
//es6+
//实例属性: state
class App{state:{}}
//es6:构造器 this.state
class App extends React.Component{
constructor(){
this.state={}
}
}
//ES5:
React.createClass({
getInitialState:function(){
return {
状态名:值,xx:oo
}
}
})
获取
//渲染
{this.state.proname}
//获取
this.state.proname
修改状态
//修改
this.setState(对象) //浅合并state
this.setState((asyncState,prevProps)=>{
//一般是用于在setState之前做一些操作
//this.state==同步结果
//asyncState==异步结果
return {
sname:value
}
})
this.setState({
sname:value
}, () => {
//一般是用于在setState之后做一些操作
//this.state == 修改之后的state
})
setState是异步的
setState 用法
setState的第二个参数是个函数,用来立即读取修改后的值
//可以用回调函数做参数,说明setState是个异步函数
//第二个参数是函数,可以立即读取修改后的新值
this.setState({
keyword:value
},()=>{
console.log(this.state.keyword)
})
//第一个参数为函数时,state参数可以读取最新的state
//因为setState是异步函数,当连续修改时,有可能state直接为最后一次修改的值,
//而当第一个参数是函数,并且函数的参数是state时,这时的state是每次修改的最新值
this.setState((state)=>{
return {
keyword:value
}},()=>{
console.log(this.state.keyword)
})
状态vs属性
列表渲染
//对象 数组 string 数字
this.props|state.属性名.map(function(val,index){
return jsx
})
条件渲染
//表达式渲染
this.state|props.proname ? jsx1 : jsx2
this.state|props.proname && jsx
//render里面写语句
render(){
let el=null;
if(this.state|props.proname){
el=jsx1
}else{
el=jsx2
}
return el
}
//渲染写成实例方法
renderFn(参数){
...参数 做判断
return el
}
render(){
return {this.renderFn(条件)}
}
refs
需要抓取dom元素与第三方 DOM 库集成,触发命令式动画,管理焦点,文本选择或媒体播放
用法
refs用法 有4种
//1、 string refs
<jsx元素 ref="名字"...
this.refs.名字
//2. 实例化
this.firstRef = React.createRef() //发生在构造器
<jsx ref={this.firstRef} />
this.firstRef 访问 -》 {current:dom}
// 3. callback refs 回调 √
<jsx ref={el => this.定义一个实例属性 = el}
this.定义一个实例属性 //后期用作访问jsx元素
// 4. 转发 refs
//当组件挂载时,将 DOM el元素传递给 ref 的回调
//当组件卸载时,则会传递 null。
//ref 回调会在 componentDidMount 和 componentDidUpdate 生命周期之前调用
受控元素
表单的value受控,受数据控制
value={this.state.数据名} //model->view
onChange={this.方法} //view->model
处理多个输入元素
可以为每个元素添加一个 name 属性(通常和数据名一致),处理函数根据 event.target.name 的值来选择要做什么
<input name="inputUserName"
<input name="inputContent"
this.setState({[ev.target.name]:ev.target.value})
双向绑定
非受控元素
要编写一个非受控组件,而不是为每个状态更新都编写数据处理函数,你可以 使用 ref 来从 DOM 节点中获取表单数据
<input type="text" ref="xx" />
默认值
表单元素上的 value 将会覆盖 DOM 节点中的值,在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新,指定一个 defaultValue 属性,而不是 value
留言板
样式
css
引用
<jsx className="类名 类名2" className={返回字符}
<jsx style={{key:value,key:value}}
//style的属性值,可以不给单位,默认px 子属性小驼峰
定义
-
index.html : 引入 link/style 公共样式 不优化 第三方样式
-
index.jsx: import './css/xx.css' 是全局 公共样式 会优化
-
组件.jsx import './css/xx.css' 全局 公共样式 会优化
选择器冲突解决方案
-
命名空间 BEM
-
模块化
import 变量 from './css/xx.module.css'
<jsx className={变量.类名|id}
//配置1
//webpack配置 "style-loader!css-loader?modules" | module:true
//问题:所有css都需要模块化使用
//配置2
//改名xx.css -> xx.module.css
//需要模块化的才修改,不影响其他非模块化css写法
scss
安装: node-sass
/*定义scss*/
$bg-color: #399;
.box{
background: $bg-color;
}
//引入
import 'xx/xx.scss'
//使用
<jsx className="box"
//模块化
import style form xx.module.scss
<xx className={style.box}
引入scss全局变量
-
局部scss文件内部: @import './全局.scss'
-
webpack配置一次,局部scss内部直接使用
//1. 安装插件 : sass-resources-loader
//2. 配置修改webpack.config.js
{
test:sassRegex,
...
use: [
{loader:'style-loader'},
{loader:'css-loader'},
{loader:'sass-loader'},
{
loader: 'sass-resources-loader',
options:{
resources:'./src/xx/全局主题.scss'
}
}
]
}
注意: loader:'css-loader?modules' ?modules 模块化时需要添加 resources 指向作用域在项目环境下
组件拆分规则
组件拆分目标:为了复用
组件如何拆:单一原则
状态应该给谁(状态提升)
-
尽量给顶层组件(状态提升),->props->子组件
-
可以从 props(属性) 得到,那么它可能不应该在 state(状态) 中
-
方法-》操作数据(数据|状态在哪,方法就应该在哪)
-
props取名从组件本身的角度来命名, 而不是它被使用的上下文环境
动画
tansition
transition: .5s ease all;
进度条
AntMotion
官网,是一款蚂蚁金服的动画组件库,支持单元素,css、进出场动画、及文字动画
组件内部的 一级元素&& 做动画 一级元素要有key,根据编号依次做动画,无key不动画,路由离场动画无效 包裹路由组件无效(一级元素&& 进退场)
生命周期
实例化 -> 更新期 -> 销毁时
es5版
实例化
- 取得默认属性(getDefaultProps) 外部传入的props
- 初始状态(getInitailState) state状态
- 即将挂载 componentWillMount
- 描画VDOM render
- 挂载完毕 componentDidMount
次新版
实例化
-
取得默认属性,初始状态在constructor中完成
运行一次,可读数据,同步修改state,可以访问到props
-
即将挂载 componentWillMount
-
描画VDOM render
-
挂载完毕 componentDidMount
使用ref,使用setState,读取数据
更新期
-
props改变 componentWillReceiveProps(nextProps) 初始化render时不执行 这里调用更新状态是安全的,并不会触发额外的render调用,nextProps 更新后 this.props更新前
-
是否更新 shouldComponentUpdate
指视图 return true/false
-
即将更新 componentWillUpdate
-
描画dom render
不要在这里修改数据
-
描画结束 componentDidUpdate
销毁时
componentWillUnmount即将卸载,可以做一些组件相关的清理工作,例如取消计时器、网络请求等
所有子挂载完,才标志着父挂载完,父更新子更新,子更新父不更新
新版
脑图,挂载前、更新前、props更新前统一用getDerivedStateFromProps代替,并添加了返回快照钩子getSnapshotBeforeUpdate
返回快照:发生在render完了,但还没有去编译真实dom之前,返回dom的快照
实例化
-
渲染前 static getDerivedStateFromProps(nextProps,nextState) {}
无法访问this nextProps,nextState是更新后的 必须返回 一个对象,用来更新state 或者 返回 null不更新 必须要初始化state 场景:state 的值在任何时候都取决于 props时 state根据props变化
-
渲染中 render
必须return jsx|string|number|null 不会直接与浏览器交互:不要操作DOM|和数据
-
挂载后 componentDidMount 访问真实dom 访问ref 做业务
更新期 render 之前通过this的拿到的是上一次的数据 通过参数可以拿到 修改后的数据 之后通过this 拿到的是修改之后的数据
-
渲染前 static getDerivedStateFromProps(nextProps, nextState) 是一个静态属性 不能操作this 可以修改状态 return {}
-
是否渲染 shouldComponentUpdate(nextProps, nextState)
是否更新,必须返回true/false 首次渲染或使用 forceUpdate() 时不会调用该方法 nextProps,nextState更新后的,this.props,this.state 更新前的 return false 只阻止当前组件渲染
-
渲染中 render
-
dom快照 getSnapshotBeforeUpdate(prevProps, prevState) 在渲染之前拿到真实dom
组件能在发生更改之前从 DOM 中捕获一些信息(dom渲染前的状态) 返回的 值|null 会给 componentDidUpdate prevProps, prevState 更新前 this.props,this.state更新后
-
更新后 componentDidUpdate(prevProps, prevState,snopshot) snopshot 快照 真实dom
this.props.更新后的 snopshot 是 getSnapshotBeforeUpdate构造的返回值
抓取到的是渲染后的dom状态,通过snopshot拿到dom渲染前的状态
销毁时
即将卸载 componentWillUnmount
REACT 生命周期 钩子
//就生命周期钩子(有顺序,按顺序)
constructor:做状态的初始化
componentWillMount:页面渲染前
render:diff算法,dom渲染
componentDidMount:页面渲染后
componentWillReceiveProps(props):当属性发生改变时会调用,首次不会调用,调用后会执行render函数,注意仅仅是执行了setState,但是值并未发生改变也会发生更新,这就要靠shouldComponentUpdate来优化了
shouldComponentUpdate(nextProps,nextState){
if(this.props.keyword===nextProps.keyword){//nextProps是更新后的属性,this.props是更新前的属性,他俩比较判断是否更新了
return false
}
else{
return true
}
}
//可以用PureComponent替换这个钩子,达到同样的效果
componentWillUpdate
componentDidUpdate
componentWillUnmount
//ReactDom.unmountComponentAtNode(document.getElementById('root')) react中卸载元素
```

被废弃的钩子

static getDerivedStateFromProps(props,state){ return {}//返回的值会与state做merge }
//注意: //子组件的componentDidMount先与父组件的componentDidmount调用,切两者总是挨着调用的 //this.forceUpdate() 强制刷新
getSnapshotBeforeUpdate(prevProps,prevState)//在虚拟dom比较之后,真实dom渲染之前执行 componentDidUpdate(prevProps,prevState,snapshot)//snapshot是getSnapshotBeforeUpdate返回的值
## 数据交互
### fetch
js原生api,是promise的语法糖,用法如下
```jsx
fetch(url+get数据,{配置})
.then((res)=>{})
.catch((err)=>{})
fetch 暂不支持对象
//配置
//method:'POST' 默认get
//headers:{"Content-type":"application/x-www-form-urlencoded"},
//body:'a=1&b=2'|URLSearchParams 非地址栏数据可以携带在body里 axios里携带在data里 原生暂不支持对象
//注意: body数据为字符时,需要携带请求头
//async + await 用法
res.ok : true/false 成功/失败 res.status: 状态码 res.body : 数据 数据流(stream) res.text() : 转换 文本(string),过程异步,return res.text() res.json() : 转 对象
jsonp
fetch不带jsonp请求 需要依赖第三库yarn add fetch-jsonp --save
import fetchJsonp from 'fetch-jsonp'
fetchJsonp(url+数据,{配置}).then((res)=>{}).catch(err=>{})
//是个promise 返回promise 数据是个流
//res.json() -> 流转换数据 是异步
timeout: 延时 5000 配置 jsonpCallback: 回调函数key callback jsonpCallbackFunction: null
百度下拉(函数节流、事件、setState异步)
axios
同vue
umi-request
客户端代理
正向代理隐藏真实客户端,反向代理隐藏真实服务端,正向代理实现翻墙,反向代理实现跨域,客户端代理指的就是代码写在客户端,不过实现的是跨域
方案1
//配置: package.json
"proxy":"https://uncle9.top"
//组件
/api/xx ...
问题: 只能代理一个服务器
浏览器这款软件 有杀伤限定 也就是同源策略
方案2
利用客户端代理中间件(http-proxy-middleware)完成, 官网给了新的使用方式,在src下新建文件setupProxy.js加下面代码,无需单独应用,webpack会自动引入文件。
// src/ 创建 setupProxy.js
//verion < 1.0
const proxy = require('http-proxy-middleware'); //需要安装中间件
module.exports = function(app) {
app.use(
proxy("/api", {
target: 'https://uncle9.top',
changeOrigin: true
})
);
app.use(
proxy("/v2", {
target: "https://api.douban.com",
changeOrigin: true
})
);
};
//组件: /api/xx ... | /v2/...
//verion > 1.0
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use('/api', createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
}));
app.use('/api2', createProxyMiddleware({
target: 'http://vareyoung.top',
changeOrigin: true,
pathRewrite: { //路径替换
'^/api2': '/api', // axios 访问/api2 == target + /api
}
}));
};
方案3
配置create-react-app环境下的webpack
// config/webpackDevServer.js
proxy: {
'/api2': {
target: 'http://vareyoung.top', // 后台服务地址以及端口号
ws: true, // websoket 服务
changeOrigin: true, //是否跨域
pathRewrite: { '^/api2': '/api' }
}
}
React路由传参的三种方式
params
1、跳转
声明式跳转
<Link to={ ' /detail/ ' + ' 1 ' } >1</Link>
编程式跳转
this.props.history.push('/detail/'+'1')
2、展示区
<Route path=' /detail/:id ' component={Detail}></Route>
3、子组件
可以通过this.props.metch.params.id拿到传过来的id
query 必须由配置query那个页面跳转过来,参数才能被传递
1、跳转
声明式跳转
<Link to={ { path : ' /detail ' , query : { name : 'detail' }} } >1</Link>
编程式跳转
this.props.history.push({ path : ' /detail ' , query : { name : 'detail' }})
2、子组件
可以通过this.props.location.query.name拿到传过来的name
state state使用方式和query差不多,只是state参数是加密的,query是公开的,显示在地址栏
1、跳转
声明式跳转
<Link to={ { path : ' /detail ' , state : { name : 'detail' }} } >1</Link>
编程式跳转
this.props.history.push({ path : ' /detail ' , state : { name : 'detail' }})
2、子组件
可以通过this.props.location.state.name拿到传过来的name
mock
JSON-Server 是一个 Node 模块,运行 Express 服务器,你可以指定一个 json 文件作为 api 的数据源。
安装json-server
npm install -g json-server
启动 json-server
json-server可以直接把一个json文件托管成一个具备全RESTful风格的API,并支持跨域、jsonp、路由订制、数据快照保存等功能的 web 服务器。
db.json文件的内容:
{
"course": [
{
"id": 1000,
"course_name": "马连白米且",
"autor": "袁明",
"college": "金并即总变史",
"category_Id": 2
},
{
"id": 1001,
"course_name": "公拉农题队始果动",
"autor": "高丽",
"college": "先了队叫及便",
"category_Id": 2
}
}
}
例如以下命令,把db.json文件托管成一个 web 服务。
$ json-server --watch --port 53000 db.json
输出类似以下内容,说明启动成功。
\{^_^}/ hi!
Loading db.json
Done
Resources
http://localhost:53000/course
Home
http://localhost:53000
Type s + enter at any time to create a snapshot of the database
Watching...
此时,你可以打开你的浏览器,然后输入:http://localhost:53000/course
json-server 的相关启动参数
- 语法:
json-server [options] <source> - 选项列表:
| 参数 | 简写 | 默认值 | 说明 |
|---|---|---|---|
| --config | -c | 指定配置文件 | [默认值: "json-server.json"] |
| --port | -p | 设置端口 [默认值: 3000] | Number |
| --host | -H | 设置域 [默认值: "0.0.0.0"] | String |
| --watch | -w | Watch file(s) | 是否监听 |
| --routes | -r | 指定自定义路由 | |
| --middlewares | -m | 指定中间件 files | [数组] |
| --static | -s | Set static files directory | 静态目录,类比:express的静态目录 |
| --readonly | --ro | Allow only GET requests [布尔] | |
| --nocors | --nc | Disable Cross-Origin Resource Sharing [布尔] | |
| --no | gzip | , --ng Disable GZIP Content-Encoding [布尔] | |
| --snapshots | -S | Set snapshots directory [默认值: "."] | |
| --delay | -d | Add delay to responses (ms) | |
| --id | -i | Set database id property (e.g. _id) [默认值: "id"] | |
| --foreignKeySuffix | -- | fks Set foreign key suffix (e.g. _id as in post_id) | [默认值: "Id"] |
| --help | -h | 显示帮助信息 | [布尔] |
| --version | -v | 显示版本号 | [布尔] |
- source可以是json文件或者js文件。实例:
json-server --watch -c ./jsonserver.json
json-server --watch db.js 命令行里面要的db是个函数
json-server db.json
json-server --watch -port 8888 db.json
动态生成模拟数据
启动json-server的命令:json-server --watch db.js 是把一个js文件返回的数据托管成web服务。
app.js配合mockjs库可以很方便的进行生成模拟数据。
// 用mockjs模拟生成数据
var Mock = require('mockjs');
module.exports = () => {
// 使用 Mock
var data = Mock.mock({
'course|227': [
{
// 属性 id 是一个自增数,起始值为 1,每次增 1
'id|+1': 1000,
course_name: '@ctitle(5,10)',
autor: '@cname',
college: '@ctitle(6)',
'category_Id|1-6': 1
}
],
'course_category|6': [
{
"id|+1": 1,
"pid": -1,
cName: '@ctitle(4)'
}
]
});
// 返回的data会作为json-server的数据
return data;
};
路由
默认的路由
json-server为提供了GET,POST, PUT, PATCH ,DELETE等请求的API,分别对应数据中的所有类型的实体。
# 获取所有的课程信息
GET /course
# 获取id=1001的课程信息
GET /course/1001
# 添加课程信息,请求body中必须包含course的属性数据,json-server自动保存。
POST /course
# 修改课程,请求body中必须包含course的属性数据
PUT /course/1
PATCH /course/1
# 删除课程信息
DELETE /course/1
# 获取具体课程信息id=1001
GET /course/1001
自定义路由
当然你可以自定义路由:
$ json-server --watch --routes route.json db.json
route.json文件
{
"/api/*": "/$1", // /api/course <==> /course
"/:resource/:id/show": "/:resource/:id",
"/posts/:category": "/posts?category=:category",
"/articles\\?id=:id": "/posts/:id"
}
自定义配置文件
通过命令行配置路由、数据文件、监控等会让命令变的很长,而且容易敲错,可以把命令写到npm的scripts中,但是依然配置不方便。
json-server允许我们把所有的配置放到一个配置文件中,这个配置文件默认json-server.json;
例如:
{
"port": 53000,
"watch": true,
"static": "./public",
"read-only": false,
"no-cors": false,
"no-gzip": false,
"routes": "route.json"
}
使用配置文件启动json-server:
# 默认使用:json-server.json配置文件
$ json-server db.js
$ json-server db.json
# 指定配置文件
$ json-server --watch -c jserver.json db.json
过滤查询
查询数据,可以额外提供
GET /posts?title=json-server&author=typicode
GET /posts?id=1&id=2
# 可以用 . 访问更深层的属性。
GET /comments?author.name=typicode
还可以使用一些判断条件作为过滤查询的辅助。
GET /posts?views_gte=10&views_lte=20
可以用的拼接条件为:
_gte: 大于等于_lte: 小于等于_ne: 不等于_like: 包含
GET /posts?id_ne=1
GET /posts?id_lte=100
GET /posts?title_like=server
分页查询
默认后台处理分页参数为: _page 第几页, _limit一页多少条。
GET /posts?_page=7
GET /posts?_page=7&_limit=20
默认一页10条。
后台会返回总条数,总条数的数据在响应头:X-Total-Count中。
排序
- 参数:
_sort设定排序的字段 - 参数:
_order设定排序的方式(默认升序)
GET /posts?_sort=views&_order=asc
GET /posts/1/comments?_sort=votes&_order=asc
支持多个字段排序:
GET /posts?_sort=user,views&_order=desc,asc
任意切片数据
GET /posts?_start=20&_end=30
GET /posts/1/comments?_start=20&_end=30
GET /posts/1/comments?_start=20&_limit=10
全文检索
可以通过q参数进行全文检索,例如:GET /posts?q=internet
实体关联
关联子实体
包含children的对象, 添加_embed
GET /posts?_embed=comments
GET /posts/1?_embed=comments
关联父实体
包含 parent 的对象, 添加_expand
GET /comments?_expand=post
GET /comments/1?_expand=post
其他高级用法
json-server本身就是依赖express开发而来,可以进行深度定制。细节就不展开,具体详情请参考官网。
const jsonServer = require('json-server');//在node里面使用json-server包
const db = require('./db.js');//引入mockjs配置模块
const path = require('path');
const Mock = require('mockjs');
let mock='/mock';//定义路由根别名
//创建服务器
const server = jsonServer.create();//创建jsonserver 服务对象
//配置jsonserver服务器 中间件
server.use(jsonServer.defaults({
static:path.join(__dirname, '/public'),//静态资源托管
}));
server.use(jsonServer.bodyParser);//抓取body数据使用json-server中间件
//响应
server.use((request, res, next) => {//可选 统一修改请求方式
// console.log(1)
// request.method = 'GET';
next();
});
//登录注册校验
let mr = Mock.Random;//提取mock的随机对象
server.get(mock+'/login', (req, res) => {
// console.log(req.query, req.body);//抓取提交过来的query和body
let username=req.query.username;
let password=req.query.password;
(username === 'aa' && password === 'aa123')?
res.jsonp({
"err": 0,
"msg": "登录成功",
"data": {
"follow": mr.integer(1,5),
"fans": mr.integer(1,5),
"nikename": mr.cname(),
"icon": mr.image('20x20',mr.color(),mr.cword(1)),
"time": mr.integer(13,13)
}
}) :
res.jsonp({
"err": 1,
"msg": "登录失败",
})
});
server.post(mock+'/reg', (req, res) => {
let username=req.body.username;
(username !== 'aa') ?
res.jsonp({
"err": 0,
"msg": "注册成功",
"data": {
"follow": mr.integer(0,0),
"fans": mr.integer(0,0),
"nikename": mr.cname(),
"icon": mr.image('20x20',mr.color(),mr.cword(1)),
"time": mr.integer(13,13)
}
}) :
res.jsonp({
"err": 1,
"msg": "注册失败",
})
});
//响应mock接口 自定义返回结构 定义mock接口别名
const router = jsonServer.router(db);//创建路由对象 db为mock接口路由配置 db==object
router.render = (req, res) => {//自定义返回结构
let len = Object.keys(res.locals.data).length; //判断数据是不是空数组和空对象
// console.log(len);
setTimeout(()=>{//模拟服务器延时
res.jsonp({
err: len !== 0 ? 0 : 1,
msg: len !== 0 ? '成功' : '失败',
data: res.locals.data
})
},1000)
// res.jsonp(res.locals.data)
};
server.use(jsonServer.rewriter({//路由自定义别名
[mock+"/*"]: "/$1",
// "/product\\?dataName=:dataName": "/:dataName",
// "/banner\\?dataName=:dataName": "/:dataName",
// "/detail\\?dataName=:dataName&id=:id": "/:dataName/:id",
// "/product/del\\?dataName=:dataName&id=:id": "/:dataName/:id",
// "/product/add\\?dataName=:dataName": "/:dataName",
// "/product/check\\?dataName=:dataName&id=:id": "/:dataName/:id"
}));
server.use(router);//路由响应
//开启jsonserver服务
server.listen(3333, () => {
console.log('mock server is running')
});
路由
官网 中文
| vue-router | react-router | |
|---|---|---|
| 配置 | 分离式(统一位置配置) | 嵌套式(路由配置在组件内部) |
| 匹配 | 排他性(只有一个路由被渲染) | 包容性(多路由渲染) |
| 形态 | 静态路由 | 动态路由 |
理念
遵循Just Component的 API 设计理念 万物皆组件,路由规则位于布局和 UI 本身之间
安装
React Router被拆分成三个包:react-router,react-router-dom和react-router-native。react-router提供核心的路由组件与函数。其余两个则提供运行环境(即浏览器与react-native)所需的特定组件
yarn add react-router-dom --save
提供组件
| 组件 | 作用 |
|---|---|
| BrowserRouter | 约定模式 为 history,使用 HTML5 提供的 history API 来保持 UI 和 URL 的同步 |
| HashRouter | 约定模式 为 hash,使用 URL 的 hash (例如:window.location.hash) 来保持 UI 和URL 的同步 |
| NavLink | 声明式跳转 还可以约定 路由激活状态 |
| Link | 声明式跳转 ~~ push 无激活状态 |
| Redirect | 重定向 ~~ replace |
| Route | 匹配、展示 |
| Switch | 排他性匹配 |
| Prompt | 后置守卫 |
| withRouter | 把不是通过路由切换过来的组件中,将 history、location、match 三个对象传入props对象上 |
结构
- BrowserRouter|HashRouter
- 根组件(App)
- NavLink|Link
- Route
- Redirect
- 子组件
- NavLink|Link
- Route
- ...
- 子组件
- 根组件(App)
BrowserRouter
| 属性 | 类型 | 作用 |
|---|---|---|
| basename | string | 所有位置的基本URL。如果您的应用是从服务器上的子目录提供的,则需要将其设置为子目录。格式正确的基本名称应以斜杠开头,但不能以斜杠结尾 |
| getUserConfirmation | Function | 用于确认导航的功能。默认使用window.confirm。 |
| forceRefresh | boolean | 是否调整时强制刷新,模拟旧式服务器渲染 |
Route
| 属性 | 类型 | 作用 |
|---|---|---|
| path | string object | 路由匹配路径。没有path属性的Route 总是会 匹配 |
| exact | boolean | 为true时,要求全路径匹配(/home)。路由默认为“包含”的(/和/home都匹配),这意味着多个 Route 可以同时进行匹配和渲染 |
| component | Function ReactElement | 在地址匹配的时候React的组件才会被渲染,route props也会随着一起被渲染 |
| render | Function | 内联渲染和包装组件,要求要返回目标组件的调用 |
Link
| 属性 | 类型 | 作用 |
|---|---|---|
| to | string | {pathname,search,hash} | 要跳转的路径或地址 |
| replace | boolean | 是否替换历史记录 |
NavLink
| 属性 | 类型 | 作用 |
|---|---|---|
| to | string object | 要跳转的路径或地址 |
| replace | boolean | 是否替换历史记录 |
| activeClassName | string | 当元素被选中时,设置选中样式,默认值为 active |
| activeStyle | object | 当元素被选中时,设置选中样式 |
| exact | boolean | 严格匹配 |
Switch
该组件用来渲染匹配地址的第一个Route或者Redirect,仅渲染一个路由,排他性路由,默认全匹配(场景:侧边栏和面包屑,引导选项卡等
| 属性 | 类型 | 作用 |
|---|---|---|
| location | string object | |
| children | node |
Redirect
该组件用来渲染匹配地址的第一个Route或者Redirect,仅渲染一个路由,排他性路由,默认全匹配(场景:侧边栏和面包屑,引导选项卡等
| 属性 | 类型 | 作用 |
|---|---|---|
| from | string | 来自 |
| to | string object | 去向 |
| push | boolean | 添加历史记录 |
| exact | boolean | 严格匹配 |
| sensitive | boolean | 区分大小写 |
404
<Route component={Error}/> 总是会匹配
参数数据传递
let {history,location,match}=props
<Link to={match.url+'/001'}/>
<Link to={`${match.url}/002?a=1&b=2`}/>
<Link to={{pathname:match.url+'/003',search:'?a=11&b=12',hash:'#a1'}}
<Route path={match.path+'/:aid'} component={Detail}
url - (浏览器 URL 中的实际路径) URL 匹配的部分。 用于构建嵌套的 path - (路由编写的路径) 用于匹配路径模式。用于构建嵌套的
接收
//接参数:
{match.params.aid}
//接数据
{location.search}
//接地址:
{location.pathname}
无法从v4+ 中获取 URL 的查询字符串了。因为没有关于如何处理复杂查询字符串的标准。所以,作者让开发者去选择如何处理查询字符串。推荐qs库|query-string
编程式跳转
history.push('/user?a=1&b=2')
history.push({pathname:'/user',search:'?a=11&b=22'})
history.replace({pathname:'/user',search:'?a=111&b=222'})
history.go(-1)
非路由跳转组件
不是所有组件会通过路由跳转,也需要抓取路由上下文时,解决方案
- 通过路由跳转
- 通过属性传递
- 通过withRouter包装
import {withRouter} from 'react-router-dom'
class 组件 extends Component{}
export default withRouter(组件)
前置授权路由
需要自定义路由,具体为,自定义一个组件,代替Route,其内部根据条件返回一个Route 组件指向目标组件,或者Route的render函数内部判断加载目标,最后组件树关系为:switch>自定义组件>Route>目标组件
<Auth path="/goods" component={Goods} />
<Auth path="/user" component={User} />
export default class Auth extends React.Component{
state={
hasSendAuth:false,
auth:false,
data:{}
};
async componentDidMount(){
let res = await axios({url:'/data/user.json'})
console.log('数据回来了')
this.setState({
auth:res.data.auth,
hasSendAuth:true,
data:res.data.data
})
}
render(){
// console.log('渲染了',this.props) //包含了path,component的一个传入
let {component:Component} = this.props;//目标组件
if (!this.state.hasSendAuth) return null;
return <Route render={props=>(//...props 目标组件需要用到的路由信息
this.state.auth ?
<Component {...props} data={this.state.data} /> :// 数据预载
<Redirect to="/login" />
)}/>
}
}
后置守卫
// reg.jsx
import { Prompt } from 'react-router-dom'
<Prompt
when={this.state.isBlocking}
message={location=>...}
/>
message: 后面可以跟简单的提示语,也可以跟函数,函数是有默认参数的。 when: when的属性值为true时防止跳转;
项目
技术栈选型
前端
create-react-app
react-router-dom
axios
redux/react-redux/react-think
mockjs/json-server
后端
nodejs
express
mongodb
bcrypt
jsonwebtoken
multer
环境规划
|-config CRA配置
|-scirpts CRA配置
|-pubilc
|- data
|- 静态数据
|-index.html 浏览器入口
|-node_modules
|-mock 数据模拟
|-db.js
|-server.js
|-src
|-library 公司内部库
|-jquery.js
|-swiper.js
|-utils 工具包
|-date.js / fillzero.js/...
|-layouts 布局
|- App/Header/Footer
|-components 应用内部基础通用组件、木偶组件
|- swiper、input、loading
|- cell、uc-nav
|- button
|-pages 智能组件 页面
|- Home / Follow / Column / User
|- Detail / Login / Reg / Error
|-guard
守卫组件
|- assets
|- img
|- css、sass
|- font
|- store
|- state/reducer/asyncAction
|- plugins
|- axios / ....
Index.js
组件开发
准备工作
移动端(设置视口,设置字体比例,基础样式normal,base)
资源引入
- index.html引入 不优化
- index.js 引入 优化
- 组件 引入 优化
资源指向
相对路径 以src为根静态资源,绝对路径 以public为根动态资源, jsx前景图片默认都指向public, jsx里面行间样式链接图片资源指向了pubic,
布局方案
- 切图,需要设计稿,用户端开发时用到
- UI库,管理端开发时用到,常用的UI库(elementUI/ant.design)
- 模板移植,老项目重构时用到
数据交互
客户端代理
module.exports = function(app) {
app.use('/api', createProxyMiddleware({
target: 'http://localhost:3001',
changeOrigin: true,
}));
app.use('/api2', createProxyMiddleware({
target: 'http://vareyoung.top',
changeOrigin: true,
pathRewrite: { //路径替换
'^/api2': '/api', // axios 访问/api2 == target + /api
}
}));
};
拦截器axios
import React from 'react';
import axios from 'axios';
import {BrowserRouter as Router} from 'react-router-dom'
import {baseLocalUrl} from '../server'
import qs from 'qs'
// 添加一个请求的拦截
axios.interceptors.request.use((config) => {
//1抓取本地token,携带在请求头里
let user = window.localStorage.getItem('user');
user = user ? qs.parse(user) : '';
config.headers={'token': user.token}
//显示loading...
return config;//2返回请求
}, function(error) {
// 请求错误时做点事
return Promise.reject(error);
});
//添加一个响应拦截
axios.interceptors.response.use(function(response) {
console.log('响应拦截',response);
let router=new Router();
//token过期: 返回值2,当前路由不是login时跳转
if (response.data.err === 2 && !router.history.location.pathname.includes('/login')) {
console.log('token 失败 跳转到login',router);
window.location.href=baseLocalUrl+'/login?path='+router.history.location.pathname
/*router.history.push({ //hash 模式可以,history模式有问题
pathname: '/login',
search: "path="+router.history.location.pathname
})*/
}
return response;
}, function(error) {
return Promise.reject(error);
});
React.axios = axios;//axios绑到对象包上
React.Component.prototype.axios = axios; // axios绑定到Component类的原型 组件|this.axios
window.axios = axios; //× 希望全局使用axios , 使用webpack 来配置
export default axios;
拦截器umi-request
import React from 'react';
import {BrowserRouter as Router} from 'react-router-dom'
import request,{ extend } from 'umi-request';
import qs from 'qs'
// request拦截器, 改变url 或 options.
request.interceptors.request.use((url, options) => {
//1抓取本地token,携带在请求头里
let user = window.localStorage.getItem('user');
user = user ? qs.parse(user) : '';
options.headers={'token': user.token}
return (
{
url,
options
}
);
});
// 提前对响应做异常处理
request.interceptors.response.use(async (response) => {
const codeMaps = {
200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。',
202: '一个请求已经进入后台排队(异步任务)。',
204: '删除数据成功。',
400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
401: '用户没有权限(令牌、用户名、密码错误)。',
403: '用户得到授权,但是访问是被禁止的。',
404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
406: '请求的格式不可得。',
410: '请求的资源被永久删除,且不会再得到的。',
422: '当创建一个对象时,发生一个验证错误。',
500: '服务器发生错误,请检查服务器。',
502: '网关错误。',
503: '服务不可用,服务器暂时过载或维护。',
504: '网关超时。',
};
console.log(codeMaps[response.status]);
const data = await response.clone().json();//克隆响应对象做解析处理
let router=new Router();
//token过期: 返回值2,当前路由不是login时跳转
if (data.err === 2 && !router.history.location.pathname.includes('/login')) {
console.log('token 失败 跳转到login',router);
window.location.href=baseLocalUrl+'/login?path='+router.history.location.pathname
/*router.history.push({ //hash 模式可以,history模式有问题
pathname: '/login',
search: "path="+router.history.location.pathname
})*/
}
return response;
});
React.request = request;//request绑到对象包上
React.Component.prototype.request = request; // request绑定到Component类的原型 组件|this.request
window.request = request; //× 希望全局使用request , 使用webpack 来配置
export default request;
登录
//更新同步localStrage
window.localStorage.setItem('user',qs.stingify(...))
//跳转到之前
history.push({
pathname:qs.parse(
this.props.location.search,{
ignoreQueryPrefix:true
}).path
})
列表、详情
home-> cell/swiper -> detail 拿到id dataName
<jsx dangerouslySetInnerHTML={{__html:HTML字符的数据}}></jsx>危险数据的信任和转换
全局方法过滤
|-common|utils
date.js
fillzero.js
...
index.js
import date/fillzero ..
export {
date,fillzero
}
公共数据
//路由检测: pathname的变化
static getDerivedStateFromProps(nextProps,nextState){
let path = nextProps.location.pathname;
if (/home|follow|column/.test(path)){
return {bNav:true,bFoot:true}
}
if (/detail|login|reg/.test(path)){
return {bNav:false,bFoot:false}
}
if (/user/.test(path)){
return {bNav:false,bFoot:true}
}
return null;
}
//loading数据
//订阅发布库
//订阅发布库: App订阅, 组件求数据时发布 | 拦截器发布
pubsub-js
安装
yarn add pubsub-js -S
订阅
token = PubSub.subscribe('事件名称', 函数(msg,data));
//msg == 事件名称
//data == 传入的数据
发布
PubSub.publish('事件名称', '数据')
取消订阅
PubSub.unsubscribe(token); //取消指定订阅
PubSub.clearAllSubscriptions(); //取消所有订阅 不推荐使用
先订阅,再发布
部署
react的项目打包(dist),拷贝到空node项目环境(public)下,利用node做后端代理,访问json-server服务器的数据(mock),再一同拷贝到购买的云服务器上,阿里云的服务器类型选择centos
| 前端 | 代理端 | 服务端 |
|---|---|---|
| react | node | json-server + mock |
| 在node目录的public/template下 | 提供静态请求 | 提供api和库的动态请求 |
node做代理
方案1
// node项目环境 下安装 http-proxy-middleware 中间件
npm i http-proxy-middleware --save
// app.js
const { createProxyMiddleware } = require('http-proxy-middleware');
//因为 bodyParser 导致的代理转发带有 body 数据的 post 请求会失败,代理中加上把解析后的 body 数据再转回来即可
var restream = function(proxyReq, req) {
if (req.body) {
let bodyData = JSON.stringify(req.body);
// incase if content-type is application/x-www-form-urlencoded -> we need to change to application/json
proxyReq.setHeader('Content-Type','application/json');
proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
// stream the content
proxyReq.write(bodyData);
}
}
//响应mock请求,交由中间件转发
app.use('/mock', createProxyMiddleware({
target: 'http://localhost:3333',
changeOrigin: true,
secure: false,
onProxyReq: restream
}));
方案2
// node项目环境 下安装 express-http-proxy 中间件
npm i express-http-proxy --save
// app.js
const proxy = require('express-http-proxy');
//配置
let opts = {
preserveHostHdr: true,
reqAsBuffer: true,
//转发之前触发该方法
proxyReqPathResolver: function(req, res) {
//这个代理会把匹配到的url(下面的 ‘/api’等)去掉,转发过去直接404,这里手动加回来,
req.url = req.baseUrl+req.url;
return require('url').parse(req.url).path;
},
}
//响应mock请求,交由中间件转发
app.use('/mock',proxy('http://localhost:3333',opts));
json-server服务器:三目有问题,压缩分号 ***
阿里云部署
简洁型部署
买服务器(机器)
-
选择云服务器ECS、centos系统,学生特惠地址
-
支付宝-》注册-》实名认证填写身份证的信息-》ecs
-
重设密码初始化磁盘:ecs服务器->控制台
使用finalShell连接服务器
-
安装 finalShell
-
启动 finalShell-》新建会话-》SSH链接->主机:公网IP-》端口 : 22-》用户名:root-》密码: 登录密码
給服务器安装环境
//安装node 在 finalShell里面
curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
yum install -y nodejs
检测: node -v
上传代码
-
react 打包:
yarn build-> build目录 -
创建空的node环境:
express -e . -
build里面的文件 copy -> node 的 public下面
-
把node项目 -》 拖拽到 finalSheel/usr/local/创建目录/
-
//让阿里云支持node里面的3000端口 找到控制台->安全组-》配置规则-》添加规则-》端口范围(3000/3000),授权对象(0.0.0.0/0) -
finalShell 里面-> cd /usr/local/你的目录 -> npm start测试: 浏览器输入: http://公网IP:3000
高要求部署
买服务器(机器)
-
成人特惠地址,认准云服务器ECS/centos系统
-
支付宝-》注册-》实名认证填写身份证的信息-》ecs
-
手动停止服务器 ----> 初始化磁盘 ---> 重设密码(登录密码)
使用finalShell连接服务器
-
安装 finalShell
-
启动 finalShell-》新建会话-》主机:公网IP-》端口 : 22-》用户名:root-》密码: 登录密码
給服务器安装环境
curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
sudo yum install -y nodejs
检测: node -v
上传代码
- react 打包:
npm run build-> build - 本地测试生产环境(css有时打包后有出错)
npm i serve -g
serve -s dist -l 8080
问题 : 生产环境下 不能访问 3001
原因 : 生产环境下客户端代理是无效的,部署后的代码需要在服务端做代理
解决: 服务器端 安装ngnix 来完成代理
- 拷贝 build -> node的public下面 + 本地测试(启动node服务)
- 整合好的node 拖到 finalShell 下面(不拽node_modules)
npm i
npm start
給服务器安装json-server服务
//1 copy react下面的mock 到服务器其他目录
|-app.js
|-db.js
|-public
//2 安装依赖
npm init
npm i
//3 开一个3333安全组(防火墙)
问题汇总
关闭finalShell ,服务断了
//安装pm2, nodejs服务器管理器
npm i pm2 -g
//启动服务器:
pm2 start 启动文件.js
//浏览器访问项目即可
http://公网IP:node端口
//如果想停掉服务器:
pm2 stop all
可以有多个app?使用一个实例?
分析:app指向不同端口就好了 解决:app指向不同端口,安全组里添加多个端口,pm2 进入到对应服务器位置,逐个启动,如果端口重复,先启用的应用会占用端口
不想要端口可以?
分析:使用http协议默认的80端口,使用https协议默认端口443 解决: 修改本地的端口号指向80,安全组添加80
不使用ip,使用网址?
分析: 是一个IP和域名关联的过程
解决: 必须得用于一个已经备过案的域名(未备案不可使用一级域名和省略端口),域名购买地址
备案: 特惠专区-》域名与网站->域名新手多重礼(实名,备案15工作日)
域名解析:域名-》解析-》添加记录->记录值(ip)
www:解析后的域名为www.aliyun.com。
@:直接解析主域名 aliyun.com。
二级域名:如:abc.aliyun.com,填写abc。
不备案有什么影响
小程序上线时不能部署,但不影响学习 没有域名不便于宣传,解决:做成二维码 无法使用https安全协议访问
启用https访问
流程:SSL证书->获取https免费证书->配置(node服务器使用https模块响应)
下载: 证书通过后->下载 other类型的 xx.key/xx.pem 下载到-> bin/www
配置node:
var https = require('https');
const fs = require('fs');
const port=443;
app.set('port', port);
const options = {
key: fs.readFileSync('./bin/1826016_uncle9.top.key'),//指向key
cert: fs.readFileSync('./bin/1826016_uncle9.top.pem'),
};
var server = https.createServer(options,app);//查看nodejs.cn>https模块|或已完成的node项目
安全组规则:添加443 ,443是https的默认端口
在阿里云配置apache+mysql+php
历史记录模式路由,强刷找不到
现象:客户端路由服务找/todos/42时,服务器会找/todos/42的接口(没有这个子服务接口) 解决:服务器路由优先,找不到时,返回vue的前端index.html,交还给客户端路由
// node项目 app.js
app.use(function(err, req, res, next) {
...
if(req.url.includes('/api')){//webApi接口错误
res.send({
err:1,
msg:'不存在的接口名'
})
}else if(req.url.includes('/admin')){//服务端Api接口错误
res.render('error');
}else{//交还给客户端判断
res.sendFile(path.join(__dirname, 'public','template', 'index.html'));
}
});
也可以通过中间件 connect-history-api-fallback 实现
无状态组件
是个函数,不能访问this对象,也就不存在state、实例方法、钩子、也不需要,只能访问props,无需实例化,渲染性能高,适用场景:展示,纯渲染的地方,别名:UI组件,哑组件,函数式组件,无状态组件,木偶组件
const 组件名=(props)=>(jsx)
const 组件名=props=>jsx
const 组件名=(props)=>{
let xx=props.xx
return jsx
}
组件通讯
父子
//单项数据流
<Child 属性=数据/>
this.props.属性
子父
//反向数据流
<Child 属性=父方法/>
this.props.属性(子数据)
中间人
<ChildA 属性=父方法/>
<ChildB 属性=接受的a数据/>
所有 React 组件都必须是纯函数,并禁止修改其自身 props
纯函数不会试图改变它们的输入,并且对于同样的输入,始终可以得到相同的结果,React 组件都必须是纯函数,并禁止修改其自身 props
转发 refs
Forwarding refs,将 ref 通过组件传递给其子节点的技术。它对于可复用组件库和高阶组件(HOC)等情况非常有用
this.inputRef = React.createRef()//构造器
<子组件 ref={this.inputRef} />
//子组件是个函数时
const 子组件 = React.forwardRef((props, ref) => (
...
<input type="text" ref={ref}/>)
...
);
context组件上下文
Context 旨在共享一个组件树内可被视为 “全局” 的数据,达到越级传递,场景:当前经过身份验证的用户,主题或首选语言,包括管理当前的 locale,theme,或者一些缓存数据
老api
//顶层组件 类属性 组件属性 定义子上下文类型
static childContextTypes={
msg: propTypes.string,
setMsg : propTypes.func
};
getChildContext(){//返回上下文对象
return {
msg:this.state.msg,
setMsg:this.setMsg
}
}
//下层组件 类属性 组件属性 接受上下文
static contextTypes = {
msg: propTypes.string,
setMsg: propTypes.func
};
//使用
this.context.msg | this.context.setMsg(数据)
新api
//Context
import {createContext} from 'react'
const Context = createContext(默认值);//默认值可以不给
export default Context
//祖先组件 Context.Provider包裹组件并且传递属性值
import Context from './Context';
class 祖先组件 extends Component {
state = {
count: 60
};
render() {
const { count } = this.state;
return (
<Context.Provider value={count}>
...
<中间件层组件 />
...
</Context.Provider>
);
}
}
//后代组件 Context.Consumer来接收值,Consumer里面不能直接渲染其他组件,而是要声明一个函数。函数的参数就是context的值
import Context from './Context';
export default class Leaf extends Component {
render() {
return (
<Context.Consumer>
{
value => {
return (
<div className="leaf">
{value}
</div>
)
}
}
</Context.Consumer>
)
}
}
//封装Context.Provider
import React,{Component} from "react";
import Context from './Context'
export default class Provider extends Component {
state={
count:10
...
};
increment=(val=1,ev)=>this.setState({count:this.state.count+val})
decrement=(val=1,ev)=>this.setState({count:this.state.count-val})
...
render(){
return (
<Context.Provider value={
{
count: this.state.count,
increment: this.increment,
decrement: this.decrement
}
}>
{this.props.children}
</Context.Provider>
)
}
}
//使用封装
<Provider>
<App/>
</Provider>
订阅发布
pub/sub模式、 消息通知、观察者模式、yarn add pubsub-js -D
- 订阅: token=pubsub.subscribe('消息名',回调函数('消息名',数据))
- 发布: pubsub.publish('消息名',数据)
- 清除指定订阅:pubsub.unsubscribe(token|'消息名'|回调函数名);
- 清除所有:pubsub.unsubscribeAll()
路由
let {history,location,match}=props
import {widthRoute}='react-router-dom'
web存储
localStrage、cookie
状态管理
后面学习
高阶组件 HOC
又叫Higher-Order Components,是一个函数能够接受一个组件并返回一个新的组件。组件是将props转化成UI,然而高阶组件将一个组价转化成另外一个组件,例如Redux的connect
就是一个函数接受一个组件作为参数,经过一系列加工后,最后返回一个新的组件,withUser函数就是一个高阶组件,它返回了一个新的组件,这个组件具有了它提供的获取用户信息的功能。
const withRouter = WrappedComponent => {
.... 抓取到history,location,match
return props => <WrappedComponent history={history} {...props} />;
//return 要求是个类或者函数
};
const Swiper = props => (
<div class="user-container">
<p>My name is {props.history}!</p>
</div>
);
export default withRouter(Swiper);
渲染属性(Render Props)
render prop 是一个用于告知组件需要渲染什么内容的函数,
class Mouse extends React.Component{
mouseOver = () => {console.log('over')};
mouseOut = () => {console.log('out')};
render(){
return (
<div onMouseOver={this.mouseOver} onMouseOut={this.mouseOut}>
{this.props.render()}
</div>
)
}
}
//
<Mouse render={()=>{
return (
<>
<h3>标题</h3>
<p>段落</p>
<p>段落</p>
<p>段落</p>
</>
)
}}/>
状态管理
- 思想:flux
- 实现:vuex redux
redux
可以同一个地方查询状态,改变状态,传播状态,用在中大项目,组件状态需要共享,在任何地方都可以拿到,组件需要改变全局状态,一个组件需要改变另外一个组件的状态,创建store实例,其他组件导入并共享这个store实例
redux成员
| 成员 | 作用 | 类型 |
|---|---|---|
| createStore | 创建store实例 | 函数 |
| combineReducers | 合并多个reducer | 函数 |
| applyMiddleware | 安装中间件,改装增强redux | 函数 |
store成员
| 成员 | 作用 | 类型 |
|---|---|---|
| subscribe | 订阅state变化 | 函数 |
| dispatch | 发送action 给 reducer | 函数 |
| getState | 获取一次state的值 | 函数 |
| replaceReducer | 一般在 Webpack Code-Splitting 按需加载的时候用 | 函数 |
数据流动
| component(views) | action | reducer | state | component(views) |
|---|---|---|---|---|
| 展示state | 转发的动作,异步业务 | 同步业务处理逻辑, 修改state,并且返回state | 状态收集 | |
| store.dispatch---》 | -------------》 | 《--subscribe | ||
| 《--getState |
操作流程
import {createStore} from 'redux'
//生成默认state
let defaultState={}
//创建reducer
const reducer = (state=defaultState,action)=>{
let {type,payload}=action
swtich type
case XXXXX
更新copy后的state Object.assign(空,老,新)
default:
return state
}
//创建store对象
store = createStore(reducer,state)
export default store;
//组件内部更新,状态获取state
import store from '...'
store.dispatch({type:xxx,payload:ooo}) //发送action给reducer type是必传参数
store.subscribe(回调) //订阅 state 更新state时触发
store.getState() //获取状态,执行一次
提取并定义 Action Creators
let nextTodoId = 0;
export const addTodo = text => ({
type: "ADD_TODO",
id: nextTodoId++,
text
});
export const removeTodo = id => ({
type: "REMOVE_TODO",
id
});
export const checkNav = bl => ({
type: "CHECK_NAV",
bl
});
//处理异步
const updateHome = (collectionName) => dispatch => { //dispatch接受函数 需要thunk中间件
return axios.get({api:collectionName}).then(
res=> {
dispatch({type:'UPDATE_HOME',payload:res.data.data});
return res//有回执
}
)
};
//安装中间件改装 redux
import {createStore,applyMiddleware,combineReducers} from 'redux'
import thunk from 'redux-thunk'
let store = createStore(rootReducer,rootState,applyMiddleware(thunk));
//组件内部
dispatch(checkNav(!bNav))
dispatch(addTodo('呵呵哒'))
combineReducers提取reducer
当应用逻辑逐渐复杂的时候,我们就要考虑将巨大的 Reducer 函数拆分成一个个独立的单元,这在算法中被称为 ”分而治之“,Reducers 在 Redux 中实际上是用来处理 Store 中存储的 State 中的某个部分,一个 Reducer 和 State 对象树中的某个属性一一对应,一个 Reducer 负责处理 State 中对应的那个属性
// src/plugins/redux
import {createStore,applyMiddleware,combineReducers} from 'redux'
import thunk from 'redux-thunk'
import todos from '../store/reducers/todos'
import bNav from '../store/reducers/nav'
let rootReducer=combineReducers({bNav,todos});
let store = createStore(rootReducer,applyMiddleware(thunk));
export default store;
// src/store/reducers/todos
let initState=[]
const todos = (todos=initState, action) => {
switch (action.type) {
case "ADD_TODO": {
return [
...todos,
{
id: action.id,
text: action.text,
completed: false
}
]
}
case "REMOVE_TODO": {
const { id } = action;
todos.map((item,index) => item.id ===id && todos.splice(index, 1));
return [...todos]
}
case "CHECK_TODO": {
const { id } = action;
todos.map((item,index) => item.id ===id && (todos[index].completed=!todos[index].completed));
return [...todos]
}
default:
return todos;
}
};
export default todos;
// src/store/reducers/bNav
const bNav = (bNav=false, action) => {
switch (action.type) {
case "CHECK_NAV": {
const { bl } = action;
return bl
}
default:
return bNav;
}
};
export default bNav;
state数据不写在构造器内订阅,可以写在主入口文件 订阅reactdom的更新
let render = ()=>{
ReactDOM.render(
<App/>,
document.getElementById('root')
)
};
render();
store.subscribe(render);
react-redux
基于redux思想,专门为react使用redux而生,把组件拆分为容器组件, UI组件,所有的 UI 组件都由用户提供,容器组件则是由 React-Redux 自动生成。也就是说,用户负责视觉层,状态管理则是全部交给它
UI组件
- 只负责 UI 的呈现,不带有任何业务逻辑
- 没有状态(即不使用this.state这个变量)
- 所有数据都由参数(this.props)提供
- 不使用任何 Redux 的 API
容器组件
- 负责管理数据和业务逻辑,不负责 UI 的呈现
- 带有内部状态
- 使用 Redux 的 API
最佳实现
//主入口
import {Provider} from react-redux
import store from './plugins/redux'
<Provider store={redux打造的store}>
<容器组件/>
</Provider>
//Creators改装 把异步actins内部有关,api请求的通用部分封装出来的一个过程
//api
const get = ({api,_page=1,_limit=10,id=null}) => (
axios({
url: id ? `/mock/${api}/${id}` : `/mock/${api}`,
params: {_page,_limit}
})
);
//actionsCreators
const clearHome={type: 'CLEAR_HOME'};//dispatch接受对象 默认
const updateHome = () => dispatch => { //dispatch接受函数 需要thunk中间件
return get({api:'home'}).then(
res=> {
dispatch({type:'UPDATE_HOME',payload:res.data.data});
return res//有回执
}
)
};
const updateBANNER=()=>async dispatch => {
let res = await get({api:'banner'});
dispatch({type:'UPDATE_BANNER',payload:res.data.data})
};
export {clearHome,updateHome,updateBANNER}
//UI组件
const Home = ({home, banner,dispatch}) => {
useEffect(() => {
dispatch(clearHome);
dispatch(updateHome()).then(data => 收取回执)
dispatch(updateBANNER())
}, []);
return (
<div className="Home">
<Swiper data={banner}/>
{
home.map(item => (
<Cell key={item.id} item={item} dataName="home"/>
))
}
</div>
)
};
//容器组件 dispatch方法 默认传递给UI组件
export default connect(
state=>({banner:state.banner, home:state.home})
)(Home)
redux-devtools使用
import {createStore,combineReducers,applyMiddleware,compose} from 'redux';
//compose 增强器
import thunk from 'redux-thunk'
let rootReducer = combineReducers({banner, column, detail, follow, home, user});
//使用redux-devtools
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
let store = createStore(rootReducer,composeEnhancers(applyMiddleware(thunk)));//安装了中间件,改装了redux
export default store;
token加入redux 做持久化处理
思路1:登录种cookie同步redux,axios拦截器只读redux,为了速度,index主入口读取cookie同步redux为了强刷做好准备,跳转有axios完成, 问题是组件会渲染,再去跳转
思路2:全局守卫,redux里面准备一条数据,axios和其他组件都去修改他 , BaseLayout界面根据这条数据响应式渲染, 其他组件或者拦截器,无需跳转
//BaseLayout.jsx
export const connect(user=> state.user)(function Auth({user:{err}, ...rest}){
if (err==1) { return <Login {...rest} /> }
if (err==3) { return <Reg /> }
return <default {...rest}/>
})
思想3: 前置路由守卫(部分路由独享),axios和其他组件负责跳转
片段
为一个组件返回多个元素。 可以让你将子元素列表添加到一个分组中,并且不会在DOM中增加额外节点
<React.Fragment key="bmw"></..>
<></>
异步组件
把静态导入的组件,变成一个可以返回promise的函数,函数内部在路由跳转时,去异步加载目标组件,关键字import(),create-react-app 环境 webpack自动分片打包
//import 语法
import ("./ChildB.js").then(
ChildB=>console.log(ChildB)
)
//方式1
const Child = asyncComponent(()=>import("./Child"))
export default function asyncComponent(importComponent) {
class AsyncComponent extends Component {
constructor(props) {
super(props);
this.state = {
component: null
};
}
async componentDidMount() {
const { default: component } = await importComponent();
this.setState({
component: component
});
}
render() {
const C = this.state.component;
return C ? <C {...this.props} /> : null;
}
}
return AsyncComponent;
}
//方式2
import Loadable from 'react-loadable';
const Loading = () => <div>Loading...</div>;
const Home = Loadable({
loader: () => import('./routes/Home'),
loading: Loading,
loading:()=>{return null}
});
PureComponent
- 使用PureCompoent是因为它是一个更具性能的Component的版本
- 性能的提高还伴随着一些附加的条件
- 提供了具有浅比较的shouldComponentUpdate方法
- 当props或者state改变时,PureComponent将对props和state进行浅比较
- Component的shouldComponentUpdate构造被调用默认重渲,PureCompoent不一定
- 不能再重写shouldComponentUpdate
- 不渲染的情况: 父组件中改变对象,子组件比较的是引用是否相同,
- 不要在
render方法中创建一个新的函数,对象或者是数组 - 场景:组件收到的props和定义的state是基本类型时***
单页滚动条
路由切换,每次切换到页面顶部
static getDerivedStateFromProps(nextProps){//props改变时
if(this.props.location !== nextProps.location){//当前地址不等于目标地址
window.scrollTo(0,0);//滚动到顶部
}
}
页面切换出去再切换回来后怎样保持之前的滚动位置
//sTop = 模块内部变量 | 类属性
componentDidMount(){
window.scrollTo(0,sTop)
}
componentWillUnmount(){
sTop = document.documentElement.scrollTop
}
UI库
Ant Design
antd 是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。
特性
- 🌈 提炼自企业级中后台产品的交互语言和视觉风格。
- 📦 开箱即用的高质量 React 组件。
- 🛡 使用 TypeScript 开发,提供完整的类型定义文件。
- ⚙️ 全链路开发和设计工具体系。
- 🌍 数十个国际化语言支持。
- 🎨 深入每个细节的主题定制能力。
安装
yarn add antd --save
按需引入
yarn add babel-plugin-import --save
webpack loader配置,找到babel-loader 按需引入配置
+ options 项目
"plugins": [
["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }],
// `style: true` 会加载 less 文件 pc
]
使用组件
import {LocaleProvider, DatePicker,Button } from 'antd';
修改文案
// 方案1 V3
import zhCN from 'antd/lib/locale-provider/zh_CN';
import moment from 'moment';
import 'moment/locale/zh-cn';
moment.locale('zh-cn');
//组件需要被 包裹
<LocaleProvider locale={zhCN}>
<App/>
</LocaleProvider>
//方案2 V4
import zhCN from 'antd/es/locale/zh_CN';
return (
<ConfigProvider locale={zhCN}>
<App />
</ConfigProvider>
);
栗子***
antd-mobile
安装
yarn add antd-mobile --save
按需引入
yarn add babel-plugin-import --save
//webpack loader配置,找到babel-loader 按需引入配置
// + options 项目
"plugins": [
["import", { libraryName: "antd-mobile", style: "css" }]
// `style: true` 会加载 less 文件 touch pc端配置和touch端配置不可并存
]
使用组件
import { DatePickerView } from 'antd-mobile'; //直接使用组件 文案是中文
//import 'antd-mobile/lib/DatePickerView/style/css'; 手动
栗子
//Tabbar组件
//TabBar>TabBar.Item + 数据(title,key,path,icon,selectedIcon)
//路由:
history.push(this.state.tabs[index].path)
//监听:
static getDerivedStateFromProps(nextProps,nextState) {}
location.pathname.indexOf(item.path)
setState->selectedTab:item.key
//home
Flex 组件
Flex>Flex.Item style={{flex:0.6}} 约定比例
WhiteSpace 上下留白
Carousel 走马灯
Link>img
Grid 宫格
Tabs 标签页
//category
分段器手写 + Route
//follow/column
PullToRefresh 拉动刷新
List 列表
List.Item history.push(编程式跳转)
List.Item.Brief
//detail
NavBar导航
箭头样式 写入base.css 覆盖默认,同类共用
WingBlank 两侧留白
Flex>Flex.Item
//shopcart
SwipeAction 滑动操作
List>SwipeAction>List.Item>Stepper步进器
//user
卡片 card
Card>Card.Header|Body>Badge 徽标
Card>Card.Header|Body>Grid 宫格
NoticeBar 通告栏
//登录|注册
InputItem 文本输入
Button 按钮
行间样式修改 覆盖样式
mobx
一款可以与redux媲美的数据流方案,Flux思想单向数据流方案,以 Redux 为代表,Reactive响应式数据流方案,以 Mobx 为代表
-
单向数据流实现:redux + react-redux + react-thunk
-
响应式数据流实现:mobx + mobx-react
MobX 的理念是通过观察者模式对数据做出追踪处理,在对可观察属性作出变更或者引用的时候,触发其依赖的监听函数,整体的store注入机制采用react提供的context来进行传递
适用场景可以是react vue angular mpvue 小程序 taro
装饰器Decorator
是个函数,用来装饰类或者类成员 ,是Object.defineProperty的语法糖
//给对象添加或修改属性
Object.defineProperty(target, prop, desc)
//target 需要定义属性的当前对象
//prop 当前需要定义的属性名 类型:字符
| desc | 默认值 | 说明 |
|---|---|---|
| configurable | false | 描述属性是否可以被删除,默认为 false |
| enumerable | false | 描述属性是否可以被for...in或Object.keys枚举,默认为 false |
| writable | false | 描述属性是否可以修改,默认为 false |
| get | undefined | 当访问属性时触发该方法,默认为undefined |
| set | undefined | 当属性被修改时触发该方法,默认为undefined |
| value | undefined | 属性值,默认为undefined |
//定义装饰器
function 装饰器名 (target,prop,descriptor){
descriptor.writable=false;//writable属性是否可以写入
return descriptor;
}
//使用定时器
@装饰器名 类
@装饰器名 类的实例属性|静态属性
@装饰器名 类的实例方法|静态方法
//使用场景
mobx / angluarTs / vueTs / reactTs / java ...
配置
cra脚手架 不支持装饰器语法,需要小配一下
yarn add @babel/plugin-proposal-decorators --save
package.json
babel: {
"presets":...
+
"plugins": [
["@babel/plugin-proposal-decorators", { "legacy": true }],
]
....
}
vscode编辑配置
vscode->设置->搜索设置输入:experimentalDecorators->勾上
//webstrom 无需设置
mobx成员
observable action 装饰类和其成员
@observable 装饰store类的成员,为被观察者 @action 实例方法, 处理实例属性,修改状态,不推荐组件内部改
mobx-react成员
inject observer Provider
Provider,顶层提供store的服务
<Provider store={store}></Provider>
inject,注入Provider提供的store到该组件的props中,组件内部使用,inject 是一个高阶组件 高阶组件返回的是组件,作用在包装组件
export default inject('store')(react函数式组件)
@inject 是装饰器,装饰的是类本身和类成员
@inject('store') class 类组件
observer,设置当前组件为观察者,一旦检测到store中被监测者发生变化就会进行视图的强制刷新
@observer class 类组件
const 组件=observer((store)=>{jsx})
构建
程序主入口
import {Provider} from 'mobx-react'
import store from './store';
<Provider store={store}>所有</.>
store
// src/store/index
import User from './user'
...
class Store {
constructor(){
this.user = new User(this);//传递this防止this丢失
.... 可以把组件的数据交给一个一个类来处理,有的模块管理的感觉
}
}
export default new Store();
// src/store/user
import { observable, action } from 'mobx'
import axios from "axios";
class User {
//被观测者
@observable user= window.localStorage.getItem('1909_newsapp') ?
JSON.parse(window.localStorage.getItem('1909_newsapp')) :
{
err:1,
msg:'未登录',
data:{}
};
constructor(store){
this.store=store;
}
//处理被观测者数据
@action check = async ({api,method='get',username,password}) => {
return axios({
url:`/api/${api}`,
method,
params: method === 'get' ? {username, password}: null,
data: method === 'post' ? {username, password}: null,
}).then(
res=>{
this.user = res.data;
window.localStorage.setItem('xxx',JSON.stringify(res.data));
return res
}
)
};
}
export default User;
//组件注入 被做一个观察者
import {inject, observer} from "mobx-react";
@inject('store')
@observer
export default class Home extends React.Component{
constructor(props){
super(props);
props.store.goods.update({
...
})
}
render(){
let {goods:{home,banner}}=this.props.store;
return(
...
)
}
}
//面对函数式组件
const react函数式组件=observer((store)=>{jsx})
export default inject('store')(react函数式组件)
hooks 钩子
Hook 使你在非 class 的情况下可以使用更多的 React 特性,React为什么要搞一个Hooks,想要复用一个有状态的组件太麻烦了!我们都知道react都核心思想就是,将一个页面拆成一堆独立的,可复用的组件,并且用自上而下的单向数据流的形式将这些组件串联起来。但假如你在大型的工作项目中用react,你会发现你的项目中实际上很多react组件冗长且难以复用。尤其是那些写成class的组件,它们本身包含了状态(state),所以复用这类组件就变得很麻烦,那之前,官方推荐怎么解决这个问题呢?答案是:渲染属性(Render Props)和高阶组件(Higher-Order Components),hooks为共享状态逻辑提供更好的原生途径,使你在无需修改组件结构的情况下复用状态逻辑
版本支持上,16.7.0-alpha 开始支持 16.8.0 第一个正式版
使用规则
- Hook可让您在不编写类的情况下使用状态和其他React功能
- 只能在顶层调用Hooks 。不要在循环,条件或嵌套函数中调用Hook
- 只能在functional component或者自定义钩子中使用Hooks
- 钩子在类内部不起作用,没有计划从React中删除类
useState 状态
import { useState } from 'react';
const [状态属性, 状态方法] = useState(状态属性的初始值);
const [count, setCount] = useState(0);
//使用状态
{状态属性} //返回 状态值
//修改状态
setCount(新值)
可以自由命名,状态变量可以不只一个state变量了
useEffect 生命周期
每当 React更新之后,就会触发 useEffect,在第一次 render 和每次 update 后触发,不用再去考虑“挂载”还是“更新”。React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
import { useEffect } from 'react';
useEffect(()=>{
//didMount || didUpdate
return ()=>{willUnmount}
},[])
[] == didMount ,不传递==didUpdate
[state|props] == 指定的state或者props变化时
每一个state|props可以拥有一个effect(关注点分离),按照 effect 声明的顺序依次调用
return 函数,在需要清除副作用时使用
useRef 元素引用
返回一个可变的ref对象,current属性初始化为传递的参数initialValue
let refContainer = useRef(initialValue) // ~~ React.createRef(init)
<JSX ref={refContainer} ...
refContainer.current.dom操作
自定义钩子 useXxxXxx
- 重用不同组件之间的常见有状态业务逻辑。
- 但每次使用自定义钩子时,其中的所有状态和效果都是完全隔离的
- 我必须以“ use” 开头命名我的自定义Hook
- 自定义Hook是一个JavaScript函数,其名称以“ use” 开头,可以调用其他Hook
function useList(initList) {
//使用系统和自定义钩子
let [list, setList] = useState(initList);
//业务
function add(item) {
alert('add')
setList([...list, item])
}
function del(index) {
let arr = [...list];
arr.splice(index, 1);
setList(arr);
}
function check(index, key,value) {
alert('check')
let arr = [...list];
arr[index][key] = value;
setList(arr);
}
// return [list, add, del, check]
return {list, add, del, check}
}
//上面的业务,可以被购物结算和留言列表多个组件复用
useContext
不使用组件嵌套就可以订阅 React 的 Context,useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 MyContext.Consumer
const ThemeContext = React.createContext(themes.light);
function App() {
return (
<ThemeContext.Provider value={themes.dark}>
<Xxx />
</ThemeContext.Provider>
);
}
function Xxx(props) {
return (
<div>
<Ooo />
</div>
);
}
function Ooo() {
const theme = useContext(ThemeContext);
return (
<button style={{ background: theme.background, color: theme.foreground }}>
...
</button>
);
}