React一

556 阅读38分钟

介绍

一款javascript前端框架,把用户界面抽象成一个个的组件,按需组合成页面,官网,与其他框架的共同点是,都采用虚拟dom,和数据驱动

一个js库用于构建用户界面

react 关注的是用户界面

react 不是完整的mvc框架 V

React 起源 : React起源于Facebook 的内部项目,因为该公司对市场上所有的jsMVC框架都不满意,就决定自己写一套,用来架设Instagram的网站,做出来以后,发现很好用 ,就在2013.5开源了。

angularJsreactJsvueJsangularTs
控制器--弱化
过滤器-
指令√ 重-
模板语法-
服务--
组件-
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

安装 create-react-app

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版

实例化

  1. 取得默认属性(getDefaultProps) 外部传入的props
  2. 初始状态(getInitailState) state状态
  3. 即将挂载 componentWillMount
  4. 描画VDOM render
  5. 挂载完毕 componentDidMount

次新版

实例化

  1. 取得默认属性,初始状态在constructor中完成

    运行一次,可读数据,同步修改state,可以访问到props

  2. 即将挂载 componentWillMount

  3. 描画VDOM render

  4. 挂载完毕 componentDidMount

    使用ref,使用setState,读取数据

更新期

  1. props改变 componentWillReceiveProps(nextProps) 初始化render时不执行 这里调用更新状态是安全的,并不会触发额外的render调用,nextProps 更新后 this.props更新前

  2. 是否更新 shouldComponentUpdate

    指视图 return true/false

  3. 即将更新 componentWillUpdate

  4. 描画dom render

    不要在这里修改数据

  5. 描画结束 componentDidUpdate

销毁时

componentWillUnmount即将卸载,可以做一些组件相关的清理工作,例如取消计时器、网络请求等

所有子挂载完,才标志着父挂载完,父更新子更新,子更新父不更新

新版

脑图,挂载前、更新前、props更新前统一用getDerivedStateFromProps代替,并添加了返回快照钩子getSnapshotBeforeUpdate

返回快照:发生在render完了,但还没有去编译真实dom之前,返回dom的快照

实例化

  1. 渲染前 static getDerivedStateFromProps(nextProps,nextState) {}

    无法访问this nextProps,nextState是更新后的 必须返回 一个对象,用来更新state 或者 返回 null不更新 必须要初始化state 场景:state 的值在任何时候都取决于 props时 state根据props变化

  2. 渲染中 render

    必须return jsx|string|number|null 不会直接与浏览器交互:不要操作DOM|和数据

  3. 挂载后 componentDidMount 访问真实dom 访问ref 做业务

更新期 render 之前通过this的拿到的是上一次的数据 通过参数可以拿到 修改后的数据 之后通过this 拿到的是修改之后的数据

  1. 渲染前 static getDerivedStateFromProps(nextProps, nextState) 是一个静态属性 不能操作this 可以修改状态 return {}

  2. 是否渲染 shouldComponentUpdate(nextProps, nextState)

    是否更新,必须返回true/false 首次渲染或使用 forceUpdate() 时不会调用该方法 nextProps,nextState更新后的,this.props,this.state 更新前的 return false 只阻止当前组件渲染

  3. 渲染中 render

  4. dom快照 getSnapshotBeforeUpdate(prevProps, prevState) 在渲染之前拿到真实dom

    组件能在发生更改之前从 DOM 中捕获一些信息(dom渲染前的状态) 返回的 值|null 会给 componentDidUpdate prevProps, prevState 更新前 this.props,this.state更新后

    事例

  5. 更新后 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中卸载元素
    ```

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/28/17394324b08a398e~tplv-t2oaga2asx-image.image)
    



被废弃的钩子

![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2020/7/28/173943297d27d9d1~tplv-t2oaga2asx-image.image)

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-wWatch file(s)是否监听
--routes-r指定自定义路由
--middlewares-m指定中间件 files[数组]
--static-sSet static files directory静态目录,类比:express的静态目录
--readonly--roAllow only GET requests [布尔]
--nocors--ncDisable Cross-Origin Resource Sharing [布尔]
--nogzip, --ng Disable GZIP Content-Encoding [布尔]
--snapshots-SSet snapshots directory [默认值: "."]
--delay-dAdd delay to responses (ms)
--id-iSet 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-routerreact-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
          • ...

BrowserRouter

属性类型作用
basenamestring所有位置的基本URL。如果您的应用是从服务器上的子目录提供的,则需要将其设置为子目录。格式正确的基本名称应以斜杠开头,但不能以斜杠结尾
getUserConfirmationFunction用于确认导航的功能。默认使用window.confirm
forceRefreshboolean是否调整时强制刷新,模拟旧式服务器渲染

Route

属性类型作用
pathstring object路由匹配路径。没有path属性的Route 总是会 匹配
exactboolean为true时,要求全路径匹配(/home)。路由默认为“包含”的(/和/home都匹配),这意味着多个 Route 可以同时进行匹配和渲染
componentFunction ReactElement在地址匹配的时候React的组件才会被渲染,route props也会随着一起被渲染
renderFunction内联渲染和包装组件,要求要返回目标组件的调用

Link

属性类型作用
tostring | {pathname,search,hash}要跳转的路径或地址
replaceboolean是否替换历史记录

NavLink

属性类型作用
tostring object要跳转的路径或地址
replaceboolean是否替换历史记录
activeClassNamestring当元素被选中时,设置选中样式,默认值为 active
activeStyleobject当元素被选中时,设置选中样式
exactboolean严格匹配

Switch

该组件用来渲染匹配地址的第一个Route或者Redirect,仅渲染一个路由,排他性路由,默认全匹配(场景:侧边栏和面包屑,引导选项卡等

属性类型作用
locationstring object
childrennode

Redirect

该组件用来渲染匹配地址的第一个Route或者Redirect,仅渲染一个路由,排他性路由,默认全匹配(场景:侧边栏和面包屑,引导选项卡等

属性类型作用
fromstring来自
tostring object去向
pushboolean添加历史记录
exactboolean严格匹配
sensitiveboolean区分大小写

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)

非路由跳转组件

不是所有组件会通过路由跳转,也需要抓取路由上下文时,解决方案

  1. 通过路由跳转
  2. 通过属性传递
  3. 通过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

前端代理端服务端
reactnodejson-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

pm2使用

可以有多个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模块响应)

获取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)actionreducerstatecomponent(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默认值说明
configurablefalse描述属性是否可以被删除,默认为 false
enumerablefalse描述属性是否可以被for...in或Object.keys枚举,默认为 false
writablefalse描述属性是否可以修改,默认为 false
getundefined当访问属性时触发该方法,默认为undefined
setundefined当属性被修改时触发该方法,默认为undefined
valueundefined属性值,默认为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>
  );
}