React笔记 && Taro笔记

React基础

1 组件通讯

1.1 props

子组件

import React from "react";
import PropTypes from "prop-types";
import { Button } from "antd";

export default class EightteenChildOne extends React.Component {
  static propTypes = { //propTypes校验传入类型,详情在技巧11
    name: PropTypes.string
  };

  click = () => {
    // 通过触发方法由子组件调用父组件方法并传参
    this.props.eightteenChildOneToFather("这是 props 改变父元素的值");
  };

  render() {
    return (
      <div>
        {/*this.props.name由父组件传递过来*/}
        <div>这是通过 props 传入的值{this.props.name}</div>
        <Button type="primary" onClick={this.click}>
          点击改变父元素值
        </Button>
      </div>
    );
  }
}

父组件

{/*父组件定义eightteenChildOneToFather监听子组件方法调用*/}
<EightteenChildOne name={'props 传入的 name 值'} eightteenChildOneToFather={(mode)=>this.eightteenChildOneToFather(mode)}></EightteenChildOne> 

// 或者
<EightteenChildOne name={'props 传入的 name 值'} eightteenChildOneToFather={this.eightteenChildOneToFather.bind(this)}></EightteenChildOne> 

props 传多个值时:
传统写法

const {dataOne,dataTwo,dataThree} = this.state
<Com dataOne={dataOne} dataTwo={dataTwo} dataThree={dataThree}>

升级写法

<Com {...{dataOne,dataTwo,dataThree}}>

1.2 props 升级版

原理:子组件里面利用 props 获取父组件方法直接调用,从而改变父组件的值
注意: 此方法和 props 大同小异,都是 props 的应用,所以在源码中没有举例

调用父组件方法改变该值

// 父组件
state = {
  count: {}
}
changeParentState = obj => {
    this.setState(obj);
}
// 子组件
onClick = () => {
    this.props.changeParentState({ count: 2 });
}

1.3 Provider,Consumer和Context

1.Context在 16.x 之前是定义一个全局的对象,类似 vue 的 eventBus,如果组件要使用到该值直接通过this.context获取

//根组件
class MessageList extends React.Component {
  getChildContext() {
    return {color: "purple",text: "item text"};
  }

  render() {
    const {messages} = this.props || {}
    const children = messages && messages.map((message) =>
      <Message text={message.text} />
    );
    return <div>{children}</div>;
  }
}

MessageList.childContextTypes = {
  color: React.PropTypes.string
  text: React.PropTypes.string
};

//中间组件
class Message extends React.Component {
  render() {
    return (
      <div>
        <MessageItem />
        <Button>Delete</Button>
      </div>
    );
  }
}

//孙组件(接收组件)
class MessageItem extends React.Component {
  render() {
    return (
      <div>
        {this.context.text}
      </div>
    );
  }
}

MessageItem.contextTypes = {
  text: React.PropTypes.string //React.PropTypes在 15.5 版本被废弃,看项目实际的 React 版本
};

class Button extends React.Component {
  render() {
    return (
      <button style={{background: this.context.color}}>
        {this.props.children}
      </button>
    );
  }
}

Button.contextTypes = {
  color: React.PropTypes.string
};

2.16.x 之后的Context使用了Provider和Customer模式,在顶层的Provider中传入value,在子孙级的Consumer中获取该值,并且能够传递函数,用来修改context 声明一个全局的 context 定义,context.js

import React from 'react'
let { Consumer, Provider } = React.createContext();//创建 context 并暴露Consumer和Provider模式
export {
    Consumer,
    Provider
}

父组件导入

// 导入 Provider
import {Provider} from "../../utils/context"

<Provider value={name}>
  <div style={{border:'1px solid red',width:'30%',margin:'50px auto',textAlign:'center'}}>
    <p>父组件定义的值:{name}</p>
    <EightteenChildTwo></EightteenChildTwo>
  </div>
</Provider>

子组件

// 导入Consumer
import { Consumer } from "../../utils/context"
function Son(props) {
  return (
    //Consumer容器,可以拿到上文传递下来的name属性,并可以展示对应的值
    <Consumer>
      {name => (
        <div
          style={{
            border: "1px solid blue",
            width: "60%",
            margin: "20px auto",
            textAlign: "center"
          }}
        >
        // 在 Consumer 中可以直接通过 name 获取父组件的值
          <p>子组件。获取父组件的值:{name}</p>
        </div>
      )}
    </Consumer>
  );
}
export default Son;

1.4 EventEmitter

EventEmiter 传送门 使用 events 插件定义一个全局的事件机制

1.5 路由传参

1.params

<Route path='/path/:name' component={Path}/>
<link to="/path/2">xxx</Link>
this.props.history.push({pathname:"/path/" + name});
读取参数用:this.props.match.params.name

2.query

<Route path='/query' component={Query}/>
<Link to={{ pathname : '/query' , query : { name : 'sunny' }}}>
this.props.history.push({pathname:"/query",query: { name : 'sunny' }});
读取参数用: this.props.location.query.name

3.state

<Route path='/sort ' component={Sort}/>
<Link to={{ pathname : '/sort ' , state : { name : 'sunny' }}}> 
this.props.history.push({pathname:"/sort ",state : { name : 'sunny' }});
读取参数用: this.props.location.query.state 

4.search

<Route path='/web/search ' component={Search}/>
<link to="web/search?id=12121212">xxx</Link>
this.props.history.push({pathname:`/web/search?id ${row.id}`});
读取参数用: this.props.location.search

这个在 react-router-dom: v4.2.2有 bug,传参跳转页面会空白,刷新才会加载出来

5.优缺点

1.params在HashRouter和BrowserRouter路由中刷新页面参数都不会丢失
2.state在BrowserRouter中刷新页面参数不会丢失,在HashRouter路由中刷新页面会丢失
3.query:在HashRouter和BrowserRouter路由中刷新页面参数都会丢失
4.query和 state 可以传对象

1.6 onRef

原理:onRef 通讯原理就是通过 props 的事件机制将组件的 this(组件实例)当做参数传到父组件,父组件就可以操作子组件的 state 和方法

EightteenChildFour.jsx

export default class EightteenChildFour extends React.Component {
  state={
      name:'这是组件EightteenChildFour的name 值'
    placeholder: '请输入'
  }

  componentDidMount(){
    this.props.onRef(this) // 将EightteenChildFour传递给父组件
    console.log(this)
    // this.props.onRef()方法
  }

  click = () => {
    this.setState({name:'这是组件click 方法改变EightteenChildFour改变的name 值'})
  };

	handleChange = (e) => {
    console.log(e.target.value)
    this.setState({
      name: '这是组件EightteenChildFour的Input 值'+ e.target.value
    })
  }

  render() {
    return (
      <div>
        <div>{this.state.name}</div>
        <Input type="text" onChange={this.handleChange} placeholder={this.state.placeholder} />
        <Button type="primary" onClick={this.click}>
          点击改变组件EightteenChildFour的name 值
        </Button>
      </div>
    );
  }
}

eighteen.jsx

state = {
  eightteenChildFour: '',
}
{/*触发handleClickEightteenChildFourFunc方法调用EightteenChildFour 子组件click方法*/}
<Button onClick={this.handleClickEightteenChildFourFunc}>EightteenChildFour</Button>

<EightteenChildFour onRef={this.eightteenChildFourRef}></EightteenChildFour>

handleClickEightteenChildFourFunc = () => {
  console.log(this.state.eightteenChildFour)
  this.state.eightteenChildFour.click()
  /*this.state.eightteenChildFour.setState({
    name: '123'
  })*/
}

eightteenChildFourRef = (ref)=>{
  console.log('eightteenChildFour的Ref值为')
  // 获取的 ref 里面包括整个组件实例
  console.log(ref)
  this.setState({
    eightteenChildFour: ref
  })
  // 调用子组件方法
  ref.click()
}

1.7 ref

原理:就是通过 React 的 ref 属性获取到整个子组件实例,再进行操作

EightteenChildFive.jsx

// 常用的组件定义方法
export default class EightteenChildFive extends React.Component {
  state={
      name:'这是组件EightteenChildFive的name 值'
  }

  click = () => {
    this.setState({name:'这是组件click 方法改变EightteenChildFive改变的name 值'})
  };

  render() {
    return (
      <div>
        <div>{this.state.name}</div>
        <Button type="primary" onClick={this.click}>
          点击改变组件EightteenChildFive的name 值
        </Button>
      </div>
    );
  }
}

eighteen.jsx

// 钩子获取实例
componentDidMount(){
    console.log('eightteenChildFive的Ref值为')
      // 获取的 ref 里面包括整个组件实例,同样可以拿到子组件的实例
    console.log(this.refs["eightteenChildFiveRef"])
  }

// 组件定义 ref 属性
<EightteenChildFive ref="eightteenChildFiveRef"></EightteenChildFive>

1.8 redux

redux 是一个独立的事件通讯插件,这里就不做过多的叙述 请戳传送门:

1.9 MobX

MobX 也是一个独立的事件通讯插件,这里就不做过多的叙述
请戳传送门:

1.10 flux

flux 也是一个独立的事件通讯插件,这里就不做过多的叙述
请戳传送门:

1.11 hooks

1.hooks 是利用 userReducer 和 context 实现通讯,下面模拟实现一个简单的 redux
2.核心文件分为 action,reducer,types
action.js

import * as Types from './types';

export const onChangeCount = count => ({
    type: Types.EXAMPLE_TEST,
    count: count + 1
})

reducer.js

import * as Types from "./types";
export const defaultState = {
  count: 0
};
export default (state, action) => {
  switch (action.type) {
    case Types.EXAMPLE_TEST:
      return {
        ...state,
        count: action.count
      };
    default: {
      return state;
    }
  }
};

types.js

export const EXAMPLE_TEST = 'EXAMPLE_TEST';

eightteen.jsx

export const ExampleContext = React.createContext(null);//创建createContext上下文

// 定义组件
function ReducerCom() {
  const [exampleState, exampleDispatch] = useReducer(example, defaultState);

  return (
    <ExampleContext.Provider
      value={{ exampleState, dispatch: exampleDispatch }}
    >
      <EightteenChildThree></EightteenChildThree>
    </ExampleContext.Provider>
  );
}

EightteenChildThree.jsx // 组件

import React, {  useEffect, useContext } from 'react';
import {Button} from 'antd'

import {onChangeCount} from '../../pages/TwoTen/store/action';
import { ExampleContext } from '../../pages/TwoTen/eighteen';

const Example = () => {

    const exampleContext = useContext(ExampleContext);

    useEffect(() => { // 监听变化
        console.log('变化执行啦')
    }, [exampleContext.exampleState.count]);

    return (
        <div>
            <p>值为{exampleContext.exampleState.count}</p>
            <Button onClick={() => exampleContext.dispatch(onChangeCount(exampleContext.exampleState.count))}>点击加 1</Button>
        </div>
    )
}

export default Example;

3.hooks其实就是对原有React 的 API 进行了封装,暴露比较方便使用的钩子;

4.钩子有:

钩子名 作用
useState 初始化和设置状态
useEffect componentDidMount,componentDidUpdate和componentWillUnmount和结合体,所以可以监听useState定义值的变化
useContext 定义一个全局的对象,类似 context
useReducer 可以增强函数提供类似 Redux 的功能
useCallback 记忆作用,共有两个参数,第一个参数为一个匿名函数,就是我们想要创建的函数体。第二参数为一个数组,里面的每一项是用来判断是否需要重新创建函数体的变量,如果传入的变量值保持不变,返回记忆结果。如果任何一项改变,则返回新的结果
useMemo 作用和传入参数与 useCallback 一致,useCallback返回函数,useDemo 返回值
useRef 获取 ref 属性对应的 dom
useImperativeMethods 自定义使用ref时公开给父组件的实例值
useMutationEffect 作用与useEffect相同,但在更新兄弟组件之前,它在React执行其DOM改变的同一阶段同步触发
useLayoutEffect 作用与useEffect相同,但在所有DOM改变后同步触发

5.useImperativeMethods

function FancyInput(props, ref) {
  const inputRef = useRef();
  useImperativeMethods(ref, () => ({
    focus: () => {
      inputRef.current.focus();
    }
  }));
  return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);

1.12 slot

slot 就是将父组件的标签传给子组件,类似vue 的 v-slot
场景:有些组件只是共用部分dom 逻辑,里面有部分逻辑是独立的

// 父组件文件
import SlotChild from 'SlotChild'

<SlotChild
slot={<div>这是父组件的 slot</div>}>
</SlotChild>

// 子组件
子组件直接获取 this.props.slot 就可获取到内容

1.13 对比

方法 优点 缺点
props 不需要引入外部插件 兄弟组件通讯需要建立共同父级组件,麻烦
props 升级版 不需要引入外部插件,子传父,不需要在父组件用方法接收 同 props
Provider,Consumer和Context 不需要引入外部插件,跨多级组件或者兄弟组件通讯利器 状态数据状态追踪麻烦
EventEmitter 可支持兄弟,父子组件通讯 要引入外部插件
路由传参 可支持兄弟组件传值,页面简单数据传递非常方便 父子组件通讯无能为力
onRef 可以在获取整个子组件实例,使用简单 兄弟组件通讯麻烦,官方不建议使用
ref 同 onRef 同 onRef
redux 建立了全局的状态管理器,兄弟父子通讯都可解决 引入了外部插件
mobx 建立了全局的状态管理器,兄弟父子通讯都可解决 引入了外部插件
flux 建立了全局的状态管理器,兄弟父子通讯都可解决 引入了外部插件
hooks 16.x 新的属性,可支持兄弟,父子组件通讯 需要结合 context 一起使用
slot 支持父向子传标签

redux , mobx和flux对比

方法 介绍
redux 1.核心模块:Action,Reducer,Store;2. Store 和更改逻辑是分开的;3. 只有一个 Store;4. 带有分层 reducer 的单一 Store;5. 没有调度器的概念;6. 容器组件是有联系的;7. 状态是不可改变的;8.更多的是遵循函数式编程思想
mobx 1.核心模块:Action,Reducer,Derivation;2.有多个 store;3.设计更多偏向于面向对象编程和响应式编程,通常将状态包装成可观察对象,一旦状态对象变更,就能自动获得更新
flux 1.核心模块:Store,ReduceStore,Container;2.有多个 store;

2.require.context()

这个是 webpack 的 api,这个在 vue 技巧中有介绍,因为 Vue 和 React 工程都是基于 webpack打包,所以在 react 也可以使用

const path = require('path')
const files = require.context('@/components/home', false, /\.vue$/)
const modules = {}
files.keys().forEach(key => {
  const name = path.basename(key, '.vue')
  modules[name] = files(key).default || files(key)
})

3.Decorator

定义:decorator是ES7的一个新特性,可以修改class的属性

import React from 'react'
import Test from '../../utils/decorators'

@Test
//只要Decorator后面是Class,默认就已经把Class当成参数隐形传进Decorator了。
class TwentyNine extends React.Component{
    componentDidMount(){
        console.log(this,'decorator.js') // 这里的this是类的一个实例
        console.log(this.testable)
    }
    render(){
        return (
            <div>这是技巧23</div>
        )
    }
}

export default TwentyNine

decorators.js

function testable(target) {
  console.log(target)
  target.isTestable = true;
  target.prototype.getDate = ()=>{
    console.log( new Date() )
  }
}

export default testable

很多中间件,像 redux 里面就封装了Decorator的使用

4.使用 if...else

场景:有些时候需要根据不同状态值页面显示不同内容

import React from "react";

export default class Four extends React.Component {
  state = {
    count: 1
  };
  render() {
    let info
    if(this.state.count===0){
      info=(
        <span>这是数量为 0 显示</span>
      )
    } else if(this.state.count===1){
      info=(
        <span>这是数量为 1 显示</span>
      )
    }
    return (
      <div>
        {info}
      </div>
    );
  }
}

5.state 值改变的五种方式

方式 1

let {count} = this.state
this.setState({count:2})

方式 2:callBack

this.setState(({count})=>({count:count+2}))

方式 3:接收 state 和 props 参数

this.setState((state, props) => {
    return { count: state.count + props.step };
});

方式 4:hooks

const [count, setCount] = useState(0)
// 设置值
setCount(count+2)

方式 5:state 值改变后调用

this.setState(
    {count:3},()=>{
        //得到结果做某种事
    }
)

6.监听states 变化

1.16.x 之前使用componentWillReceiveProps

componentWillReceiveProps (nextProps){
  if(this.props.visible !== nextProps.visible){
      //props 值改变做的事
  }
}

注意:有些时候componentWillReceiveProps在 props 值未变化也会触发,因为在生命周期的第一次render后不会被调用,但是会在之后的每次render中被调用 = 当父组件再次传送props

2.16.x 之后使用getDerivedStateFromProps,16.x 以后componentWillReveiveProps也未移除

export default class Six extends React.Component {
  state = {
    countOne:1,
    changeFlag:''
  };
  clickOne(){
    let {countOne} = this.state
    this.setState({countOne:countOne+1})
  };
  static getDerivedStateFromProps (nextProps){
    console.log('变化执行')
    return{
      changeFlag:'state 值变化执行'
    }
  }
  render() {
    const {countOne,changeFlag} = this.state
    return (
      <div>
        <div>
         <Button type="primary" onClick={this.clickOne.bind(this)}>点击加 1</Button><span>countOne 值为{countOne}</span>
        <div>{changeFlag}</div>
        </div>
      </div>
    );
  }
}

7.组件定义方法

方式 1:ES5 的Function 定义

function FunCom(props){
  return <div>这是Function 定义的组件</div>
}
ReactDOM.render(<FunCom name="Sebastian" />, mountNode)

// 在 hooks 未出来之前,这个是定义无状态组件的方法,现在有了 hooks 也可以处理状态

方式 2: ES5的 createClass 定义

const CreateClassCom = React.createClass({
  render: function() {
  return <div>这是React.createClass定义的组件</div>
  }
});

方式 3:ES6 的 extends

class Com extends React.Component {
  render(){
    return(<div>这是React.Component定义的组件</div>)
  }
}

调用

export default class Seven extends React.Component {
  render() {
    return (
      <div>
        <FunCom></FunCom>
        <Com></Com>
      </div>
    );
  }
}

区别: ES5的 createClass是利用function模拟class的写法做出来的es6; 通过es6新增class的属性创建的组件此组件创建简单.

8.通过 ref 属性获取 component

方式 1:也是最早的用法,通过 this.refs[属性名获取] 也可以作用到组件上,从而拿到组件实例

class RefOne extends React.Component{
  componentDidMount() {
    this.refs['box'].innerHTML='这是 div 盒子,通过 ref 获取'
  }
  render(){
    return(
      <div ref="box"></div>
    )
  }
}

方式 2:回调函数,在dom节点或组件上挂载函数,函数的入参是dom节点或组件实例,达到的效果与字符串形式是一样的,都是获取其引用

class RefTwo extends React.Component{
  componentDidMount() {
    this.input.value='这是输入框默认值';
    this.input.focus();
  }
  render(){
    return(
      <input ref={comp => { this.input = comp; }}/>
    )
  }
}

方式 3:React.createRef() React 16.3版本后,使用此方法来创建ref。将其赋值给一个变量,通过ref挂载在dom节点或组件上,该ref的current属性,将能拿到dom节点或组件的实例

class RefThree extends React.Component{
  constructor(props){
    super(props);
    this.myRef=React.createRef();
  }
  componentDidMount(){
    console.log(this.myRef.current);
  }
  render(){
    return <input ref={this.myRef}/>
  }
}

方式 4:React.forwardRef
React 16.3版本后提供的,可以用来创建子组件,以传递ref

class RefFour extends React.Component{
  constructor(props){
    super(props);
    this.myFourRef=React.forwardRef();
  }
  componentDidMount(){
    console.log(this.myFourRef.current);
  }
  render(){
    return <Child ref={this.myFourRef}/>
  }
}

子组件通过React.forwardRef来创建,可以将ref传递到内部的节点或组件,进而实现跨层级的引用。forwardRef在高阶组件中可以获取到原始组件的实例.这个功能在技巧 18 会着重讲

9.static 使用

场景:声明静态方法的关键字,静态方法是指即使没有组件实例也可以直接调用

export default class Nine extends React.Component {
  static update(data) {
    console.log('静态方法调用执行啦')
  }
  render() {
    return (
      <div>
        这是 static 关键字技能
      </div>
    );
  }
}

Nine.update('2')

注意: 1.ES6的class,我们定义一个组件的时候通常是定义了一个类,而static则是创建了一个属于这个类的属性或者方法
2.组件则是这个类的一个实例,component的props和state是属于这个实例的,所以实例还未创建
3.所以static并不是react定义的,而加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,所以也是无法访问到 this
4.getDerivedStateFromProps也是通过静态方法监听值,详情请见技巧 6

10.constructor和super

回顾:
1.谈这两个属性之前,先回顾一下ES6 函数定义方法
2.每一个使用class方式定义的类默认都有一个constructor函数, 这个函数是构造函数的主函数, 该函数体内部的this指向生成的实例
3.super关键字用于访问和调用一个对象的父对象上的函数

export default class Ten extends React.Component {
  constructor() { // class 的主函数
    super() // React.Component.prototype.constructor.call(this),其实就是拿到父类的属性和方法
    this.state = {
      arr:[]
    }
  }  
  render() {
    return (
      <div>
        这是技巧 10
      </div>
    );
  }
}

11.PropTypes

场景:检测传入子组件的数据类型
类型检查PropTypes自React v15.5起已弃用,请使用prop-types
方式 1:旧的写法

class PropTypeOne extends React.Component {
  render() {
    return (
      <div>
        <div>{this.props.email}</div>
        <div>{this.props.name}</div>
      </div>
    );
  }
}

PropTypeOne.propTypes = {
  name: PropTypes.string, //值可为array,bool,func,number,object,symbol
  email: function(props, propName, componentName) { //自定义校验
    if (
      !/^([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/.test(
        props[propName]
      )
    ) {
      return new Error(
        "组件" + componentName + "里的属性" + propName + "不符合邮箱的格式"
      );
    }
  },
};

方法 2:利用 ES7 的静态属性关键字 static

class PropTypeTwo extends React.Component {
  static propTypes = {
      name:PropTypes.string
  };
  render() {
    return (
      <div>
        <div>{this.props.name}</div>
      </div>
    );
  }
}

12.使用类字段声明语法

场景:可以在不使用构造函数的情况下初始化本地状态,并通过使用箭头函数声明类方法,而无需额外对它们进行绑定

class Counter extends Component {
  state = { value: 0 };

  handleIncrement = () => {
    this.setState(prevState => ({
      value: prevState.value + 1
    }));
  };

  handleDecrement = () => {
    this.setState(prevState => ({
      value: prevState.value - 1
    }));
  };

  render() {
    return (
      <div>
        {this.state.value}

        <button onClick={this.handleIncrement}>+</button>
        <button onClick={this.handleDecrement}>-</button>
      </div>
    )
  }
}

13.异步组件

1.场景:路由切换,如果同步加载多个页面路由会导致缓慢

2.核心 API:
loader:需要加载的组件
loading:未加载出来的页面展示组件
delay:延迟加载时间
timeout:超时时间

3.使用方法:
安装 react-loadable ,babel插件安装 syntax-dynamic-import. react-loadable是通过webpack的异步import实现的

const Loading = () => {
  return <div>loading</div>;
};

const LoadableComponent = Loadable({
  loader: () => import("../../components/TwoTen/thirteen"),
  loading: Loading
});

export default class Thirteen extends React.Component {
  render() {
    return <LoadableComponent></LoadableComponent>;
  }
}

4.Loadable.Map()
并行加载多个资源的高阶组件

14.动态组件

场景:做一个 tab 切换时就会涉及到组件动态加载
实质上是利用三元表达式判断组件是否显示

class FourteenChildOne extends React.Component {
    render() {
        return <div>这是动态组件 1</div>;
    }
}

class FourteenChildTwo extends React.Component {
    render() {
        return <div>这是动态组件 2</div>;
    }
}

export default class Fourteen extends React.Component {
  state={
      oneShowFlag:true
  }
  tab=()=>{
      this.setState({oneShowFlag:!this.state.oneShowFlag})
  }
  render() {
    const {oneShowFlag} = this.state
    return (<div>
        <Button type="primary" onClick={this.tab}>显示组件{this.state.oneShowFlag?2:1}</Button>
        {this.state.oneShowFlag?<FourteenChildOne></FourteenChildOne>:<FourteenChildTwo></FourteenChildTwo>}
    </div>);
  }
}

如果是单个组件是否显示可以用短路运算

oneShowFlag&&<FourteenChildOne></FourteenChildOne>

15.递归组件

场景:tree组件
利用React.Fragment或者 div 包裹循环

class Item extends React.Component {
  render() {
    const list = this.props.children || [];
    return (
      <div className="item">
        {list.map((item, index) => {
          return (
            <React.Fragment key={index}>
              <h3>{item.name}</h3>
              {// 当该节点还有children时,则递归调用本身
              item.children && item.children.length ? (
                <Item>{item.children}</Item>
              ) : null}
            </React.Fragment>
          );
        })}
      </div>
    );
  }
}

16.受控组件和不受控组件

受控组件:组件的状态通过React 的状态值 state 或者 props 控制

class Controll extends React.Component {
  constructor() {
    super();
    this.state = { value: "这是受控组件默认值" };
  }
  render() {
    return <div>{this.state.value}</div>;
  }
}

不受控组件:组件不被 React的状态值控制,通过 dom 的特性或者React 的ref 来控制

class NoControll extends React.Component {
  render() {
    return <div>{this.props.value}</div>;
  }
}

导入代码:

export default class Sixteen extends React.Component {
  componentDidMount() {
    console.log("ref 获取的不受控组件值为", this.refs["noControll"]);
  }
  render() {
    return (
      <div>
        <Controll></Controll>
        <NoControll
          value={"这是不受控组件传入值"}
          ref="noControll"
        ></NoControll>
      </div>
    );
  }
}

17.高阶组件(HOC)

17.1 定义

1.就是类似高阶函数的定义,将组件作为参数或者返回一个组件的组件;
2.作用:
抽取重复代码,实现组件复用,常见场景,页面复用;
条件渲染,控制组件的渲染逻辑(渲染劫持),常见场景,权限控制;
捕获/劫持被处理组件的生命周期,常见场景,组件渲染性能追踪、日志打点

17.2 实现方法

1.属性代理

import React,{Component} from 'react';

const Seventeen = WrappedComponent =>
  class extends React.Component {
    render() {
      const props = {
        ...this.props,
        name: "这是高阶组件"
      };
      return <WrappedComponent {...props} />;
    }
  };

class WrappedComponent extends React.Component {
  state={
     baseName:'这是基础组件' 
  }
  render() {
    const {baseName} = this.state
    const {name} = this.props
    return <div>
        <div>基础组件值为{baseName}</div>
        <div>通过高阶组件属性代理的得到的值为{name}</div>
    </div>
  }
}

export default Seventeen(WrappedComponent)

2.反向继承
原理就是利用 super 改变改组件的 this 方向,继而就可以在该组件处理容器组件的一些值

  const Seventeen = (WrappedComponent)=>{
    return class extends WrappedComponent {
        componentDidMount() {
            this.setState({baseName:'这是通过反向继承修改后的基础组件名称'})
        }
        render(){
            return super.render();
        }
    }
}

class WrappedComponent extends React.Component {
  state={
     baseName:'这是基础组件' 
  }
  render() {
    const {baseName} = this.state
    return <div>
        <div>基础组件值为{baseName}</div>
    </div>
  }
}

export default Seventeen(WrappedComponent);

18.元素是否显示

一般用三元表达式

 flag?<div>显示内容</div>:''
 flag&&<div>显示内容</div>

19.Dialog 组件创建

Dialog 应该是用的比较多的组件,下面有三种不同的创建方法 方式 1:通过 state 控制组件是否显示

 class NineteenChildOne extends React.Component {
  render() {
    const Dialog = () => <div>这是弹层1</div>;

    return this.props.dialogOneFlag && <Dialog />;
  }
}

方式 2:通过ReactDom.render创建弹层-挂载根节点外层
通过原生的createElement,appendChild, removeChild和react 的ReactDOM.render,ReactDOM.unmountComponentAtNode来控制元素的显示和隐藏

NineteenChild.jsx

import ReactDOM from "react-dom";

class Dialog {
  constructor(name) {
    this.div = document.createElement("div");
    this.div.style.width = "200px";
    this.div.style.height = "200px";
    this.div.style.backgroundColor = "green";
    this.div.style.position = "absolute";
    this.div.style.top = "200px";
    this.div.style.left = "400px";
    this.div.id = "dialog-box";
  }
  show(children) {
    // 销毁
    const dom = document.querySelector("#dialog-box");
    if(!dom){ //兼容多次点击
      // 显示
      document.body.appendChild(this.div);
      ReactDOM.render(children, this.div);
    }
  }
  destroy() {
    // 销毁
    const dom = document.querySelector("#dialog-box");
    if(dom){//兼容多次点击
      ReactDOM.unmountComponentAtNode(this.div);
      dom.parentNode.removeChild(dom);
    }
  }
}
export default {
  show: function(children) {
    new Dialog().show(children);
  },
  hide: function() {
    new Dialog().destroy();
  }
};

nineteen.jsx

twoSubmit=()=>{
    Dialog.show('这是弹层2')
  }

  twoCancel=()=>{
    Dialog.hide()
  }

20.React.memo

作用:当类组件的输入属性相同时,可以使用 pureComponent 或 shouldComponentUpdate 来避免组件的渲染。现在,你可以通过把函数组件包装在 React.memo 中来实现相同的功能

import React from "react";

function areEqual(prevProps, nextProps) {
  /*
  如果把 nextProps 传入 render 方法的返回结果与
  将 prevProps 传入 render 方法的返回结果一致则返回 true,
  否则返回 false
  */
  if (prevProps.val === nextProps.val) {
    return true;
  } else {
    return false;
  }
}

// React.memo()两个参数,第一个是纯函数,第二个是比较函数
export default React.memo(function twentyChild(props) {
  console.log("MemoSon rendered : " + Date.now());
  return <div>{props.val}</div>;
}, areEqual);

21.React.PureComponent

作用:
1.React.PureComponent 和 React.Component类似,都是定义一个组件类。
2.不同是React.Component没有实现shouldComponentUpdate(),而 React.PureComponent通过props和state的浅比较实现了。
3.React.PureComponent是作用在类中,而React.memo是作用在函数中。
4.如果组件的props和state相同时,render的内容也一致,那么就可以使用React.PureComponent了,这样可以提高组件的性能

class TwentyOneChild extends React.PureComponent{  //组件直接继承React.PureComponent
  render() {
    return <div>{this.props.name}</div>
  }
}

export default class TwentyOne extends React.Component{
    render(){
        return (
            <div>
              <TwentyOneChild name={'这是React.PureComponent的使用方法'}></TwentyOneChild>
            </div>
        )
    }
}

22.React.Component

作用:是基于ES6 class的React组件,React允许定义一个class或者function作为组件,那么定义一个组件类,就需要继承React.Component

export default class TwentyTwo extends React.Component{ //组件定义方法
    render(){
        return (
            <div>这是技巧22</div>
        )
    }
}

23.在 JSX 打印 falsy 值

定义:
1.falsy 值 (虚值) 是在 Boolean 上下文中认定为 false 的值;
2.值有 0,"",'',``,null,undefined,NaN

export default class TwentyThree extends React.Component{
    state={myVariable:null}
    render(){
        return (
            <div>{String(this.state.myVariable)}</div>
        )
    }
}

虚值如果直接展示,会发生隐式转换,为 false,所以页面不显示

24.ReactDOM.createPortal

作用:组件的render函数返回的元素会被挂载在它的父级组件上,createPortal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案

import React from "react";
import ReactDOM from "react-dom";
import {Button} from "antd"

const modalRoot = document.body;

class Modal extends React.Component {
  constructor(props) {
    super(props);
    this.el = document.createElement("div");
    this.el.style.width = "200px";
    this.el.style.height = "200px";
    this.el.style.backgroundColor = "green";
    this.el.style.position = "absolute";
    this.el.style.top = "200px";
    this.el.style.left = "400px";
  }

  componentDidMount() {
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    modalRoot.removeChild(this.el);
  }

  render() {
    return ReactDOM.createPortal(this.props.children, this.el);
  }
}

function Child() {
  return (
    <div className="modal">
      这个是通过ReactDOM.createPortal创建的内容
    </div>
  );
}

export default class TwentyFour extends React.Component {
  constructor(props) {
    super(props);
    this.state = { clicks: 0 };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    this.setState(prevState => ({
      clicks: prevState.clicks + 1
    }));
  }

  render() {
    return (
      <div>
          <Button onClick={this.handleClick}>点击加1</Button>
        <p>点击次数: {this.state.clicks}</p>
        <Modal>
          <Child />
        </Modal>
      </div>
    );
  }
}

这样元素就追加到指定的元素下面啦

25.在 React 使用innerHTML

场景:有些后台返回是 html 格式字段,就需要用到 innerHTML 属性

export default class TwentyFive extends React.Component {
  render() {
    return (
      <div dangerouslySetInnerHTML={{ __html: "<span>这是渲染的 HTML 内容</span>" }}></div>
    );
  }
}

26.React.createElement

语法:
React.createElement( type, [props], [...children] )

源码:

export default class TwentySix extends React.Component {
  render() {
    return (
      <div>
        {React.createElement(
          "div",
          { id: "one", className: "two" },
          React.createElement("span", { id: "spanOne" }, "这是第一个 span 标签"),
          React.createElement("br"),
          React.createElement("span", { id: "spanTwo" }, "这是第二个 span 标签")
        )}
      </div>
    );
  }
}

原理:实质上 JSX 的 dom 最后转化为 js 都是React.createElement

// jsx 语法
<div id='one' class='two'>
    <span id="spanOne">this is spanOne</span>
    <span id="spanTwo">this is spanTwo</span>
</div>

// 转化为 js
React.createElement(
  "div",
 { id: "one", class: "two" },
 React.createElement( "span", { id: "spanOne" }, "this is spanOne"), 
 React.createElement("span", { id: "spanTwo" }, "this is spanTwo")
);

27.React.cloneElement

语法:

React.cloneElement(
  element,
  [props],
  [...children]
)

作用:这个方法的作用是复制组件,给组件传值或者添加属性
核心代码

React.Children.map(children, child => {
  return React.cloneElement(child, {
    count: _this.state.count
  });
});

28.React.Fragment

作用:React.Fragment可以让你聚合一个子元素列表,并且不在DOM中增加额外节点
核心代码

render() {
    const { info } = this.state;
    return (
      <div>
        {info.map((item, index) => {
          return (
            <React.Fragment key={index}>
              <div>{item.name}</div>
              <div>{item.age}</div>
            </React.Fragment>
          );
        })}
      </div>
    );
  }

29.循环元素

内部没有封装像 vue 里面 v-for 的指令,而是通过 map 遍历

{arr.map((item,index)=>{
  return(
    <div key={item.id}>
      <span>{item.name}</span>
      <span>{item.age}</span>
    </div>
  )
})}

30.给 DOM 设置和获取自定义属性

作用:有些要通过自定义属性传值

export default class Thirty extends React.Component {
  click = e => {
    console.log(e.target.getAttribute("data-row"));
  };

  render() {
    return (
      <div>
        <div data-row={"属性1"} data-col={"属性 2"} onClick={this.click}>
          点击获取属性
        </div>
      </div>
    );
  }
}

31.绑定事件

场景:交互就会涉及到事件点击,然后点击选中值传参也是一个很常见场景

import React from "react";
import { Button } from 'antd'

export default class Three extends React.Component {
  state = {
    flag: true,
    flagOne: 1
  };
  click(data1,data2){
    console.log('data1 值为',data1)
    console.log('data2 值为',data2)
  }
  render() {
    return (
      <div>
        <Button type="primary" onClick={this.click.bind(this,'参数 1','参数 2')}>点击事件</Button>
      </div>
    );
  }
}

使用方法在源码 routes.js 有详细使用

32.React-Router

32.1 V3和 V4的区别

1.V3或者说V早期版本是把router 和 layout components 分开;
2.V4是集中式 router,通过 Route 嵌套,实现 Layout 和 page 嵌套,Layout 和 page 组件 是作为 router 的一部分;
3.在V3 中的 routing 规则是 exclusive,意思就是最终只获取一个 route;
4.V4 中的 routes 默认是 inclusive 的,这就意味着多个; 可以同时匹配和呈现.如果只想匹配一个路由,可以使用Switch,在 中只有一个 会被渲染,同时可以再在每个路由添加exact,做到精准匹配 Redirect,浏览器重定向,当多有都不匹配的时候,进行匹配

32.2 使用

import { HashRouter as Router, Switch  } from "react-router-dom";

class App extends React.Component{
    render(){
        const authPath = '/login' // 默认未登录的时候返回的页面,可以自行设置
        let authed = this.props.state.authed || localStorage.getItem('authed') // 如果登陆之后可以利用redux修改该值
        return (
            <Router>
                <Switch>
                    {renderRoutes(routes, authed, authPath)}
                </Switch>
            </Router>
        )
    }
}

V4是通过 Route 嵌套,实现 Layout 和 page 嵌套,Switch切换路由的作用

33.样式引入方法

方式 1:import 导入

import './App.css';

方式 2:内联方式

import React from 'react';

const Header = () => {

    const heading = '头部组件'

    return(
        <div style={{backgroundColor:'orange'}}>
            <h1>{heading}</h1>
        </div>
    )
}

或者
import React from 'react';

const footerStyle = {
    width: '100%',
    backgroundColor: 'green',
    padding: '50px',
    font: '30px',
    color: 'white',
    fontWeight: 'bold'
}

export const Footer = () => {
    return(
        <div style={footerStyle}>
            底部组件
        </div>
    )
}

34.动态绑定 className

原理:通过三元表达式控制 className 值

render(){
  const flag=true
  return (
    <div className={flag?"active":"no-active"}>这是技巧 34</div>
  )
}

组件类

组件类,详细分的话有三种类,第一类说白了就是我平时用于继承的基类组件Component,PureComponent,还有就是react提供的内置的组件,比如Fragment,StrictMode,另一部分就是高阶组件forwardRef,memo等。

comp.jpg

Component

Componentclass组件的根基。类组件一切始于Component。对于React.Component使用,我们没有什么好讲的。我们这里重点研究一下reactComponent做了些什么。

react/src/ReactBaseClasses.js
function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject;
  this.updater = updater || ReactNoopUpdateQueue;
}

这就是Component函数,其中updater对象上保存着更新组件的方法。

我们声明的类组件是什么时候以何种形式被实例化的呢?

react-reconciler/src/ReactFiberClassComponent.js

constructClassInstance

function constructClassInstance(
    workInProgress,
    ctor,
    props
){
   const instance = new ctor(props, context);
    instance.updater = {
        isMounted,
        enqueueSetState(){
            /* setState 触发这里面的逻辑 */
        },
        enqueueReplaceState(){},
        enqueueForceUpdate(){
            /* forceUpdate 触发这里的逻辑 */
        }
    }
}

对于Componentreact 处理逻辑还是很简单的,实例化我们类组件,然后赋值updater对象,负责组件的更新。然后在组件各个阶段,执行类组件的render函数,和对应的生命周期函数就可以了。

⭐️PureComponent

PureComponentComponent用法,差不多一样,唯一不同的是,纯组件PureComponent会浅比较,propsstate是否相同,来决定是否重新渲染组件。所以一般用于性能调优,减少render次数。

什么叫做浅比较,我这里举个列子:

class Index extends React.PureComponent{
    constructor(props){
        super(props)
        this.state={
           data:{
              name:'alien',
              age:28
           }
        }
    }
    handerClick= () =>{
        const { data } = this.state
        data.age++
        this.setState({ data })
    }
    render(){
        const { data } = this.state
        return <div className="box" >
        <div className="show" >
            <div> 你的姓名是: { data.name } </div>
            <div> 年龄: { data.age  }</div>
            <button onClick={ this.handerClick } >age++</button>
        </div>
    </div>
    }
}

pureComponent.gif

点击按钮,没有任何反应,因为PureComponent会比较两次data对象,都指向同一个data,没有发生改变,所以不更新视图。

解决这个问题很简单,只需要在handerClick事件中这么写:

this.setState({ data:{...data} })

浅拷贝就能根本解决问题。

memo

React.memoPureComponent作用类似,可以用作性能优化,React.memo 是高阶组件,函数组件和类组件都可以使用, 和区别PureComponentReact.memo只能对props的情况确定是否渲染,而PureComponent是针对propsstate

React.memo 接受两个参数,第一个参数原始组件本身,第二个参数,可以根据一次更新中props是否相同决定原始组件是否重新渲染。是一个返回布尔值,true 证明组件无须重新渲染,false证明组件需要重新渲染,这个和类组件中的shouldComponentUpdate()正好相反 。

React.memo: 第二个参数 返回 true 组件不渲染 , 返回 false 组件重新渲染。 shouldComponentUpdate: 返回 true 组件渲染 , 返回 false 组件不渲染。

接下来我们做一个场景,控制组件在仅此一个props数字变量,一定范围渲染。

例子🌰:

控制 props 中的 number

  • 1 只有 number 更改,组件渲染。
  • 2 只有 number 小于 5 ,组件渲染。
function TextMemo(props){
    console.log('子组件渲染')
    if(props)
    return <div>hello,world</div> 
}

const controlIsRender = (pre,next)=>{
   if(pre.number === next.number  ){ // number 不改变 ,不渲染组件
       return true 
   }else if(pre.number !== next.number && next.number > 5 ) { // number 改变 ,但值大于5 , 不渲染组件
       return true
   }else { // 否则渲染组件
       return false
   }
}

const NewTexMemo = memo(TextMemo,controlIsRender)
class Index extends React.Component{
    constructor(props){
        super(props)
        this.state={
            number:1,
            num:1
        }
    }
    render(){
        const { num , number }  = this.state
        return <div>
            <div>
                改变num:当前值 { num }  
                <button onClick={ ()=>this.setState({ num:num + 1 }) } >num++</button>
                <button onClick={ ()=>this.setState({ num:num - 1 }) } >num--</button>  
            </div>
            <div>
                改变number: 当前值 { number } 
                <button onClick={ ()=>this.setState({ number:number + 1 }) } > number ++</button>
                <button onClick={ ()=>this.setState({ number:number - 1 }) } > number -- </button>  
            </div>
            <NewTexMemo num={ num } number={number}  />
        </div>
    }
}

memo.gif

完美达到了效果,React.memo一定程度上,可以等价于组件外部使用shouldComponentUpdate ,用于拦截新老props,确定组件是否更新。

forwardRef

官网对forwardRef的概念和用法很笼统,也没有给定一个具体的案例。很多同学不知道 forwardRef具体怎么用,下面我结合具体例子给大家讲解forwardRef应用场景。

1 转发引入Ref

这个场景实际很简单,比如父组件想获取孙组件,某一个dom元素。这种隔代ref获取引用,就需要forwardRef来助力。

function Son (props){
    const { grandRef } = props
    return <div>
        <div> i am alien </div>
        <span ref={grandRef} >这个是想要获取元素</span>
    </div>
}

class Father extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return <div>
            <Son grandRef={this.props.grandRef}  />
        </div>
    }
}

const NewFather = React.forwardRef((props,ref)=><Father grandRef={ref}  {...props} />  )

class GrandFather extends React.Component{
    constructor(props){
        super(props)
    }
    node = null 
    componentDidMount(){
        console.log(this.node)
    }
    render(){
        return <div>
            <NewFather ref={(node)=> this.node = node } />
        </div>
    }
}

效果

forwaedRef.jpg

react不允许ref通过props传递,因为组件上已经有 ref 这个属性,在组件调和过程中,已经被特殊处理,forwardRef出现就是解决这个问题,把ref转发到自定义的forwardRef定义的属性上,让ref,可以通过props传递。

2 高阶组件转发Ref

一文吃透hoc文章中讲到,由于属性代理的hoc,被包裹一层,所以如果是类组件,是通过ref拿不到原始组件的实例的,不过我们可以通过forWardRef转发ref

function HOC(Component){
  class Wrap extends React.Component{
     render(){
        const { forwardedRef ,...otherprops  } = this.props
        return <Component ref={forwardedRef}  {...otherprops}  />
     }
  }
  return  React.forwardRef((props,ref)=> <Wrap forwardedRef={ref} {...props} /> ) 
}
class Index extends React.Component{
  componentDidMount(){
      console.log(666)
  }
  render(){
    return <div>hello,world</div>
  }
}
const HocIndex =  HOC(Index,true)
export default ()=>{
  const node = useRef(null)
  useEffect(()=>{
     /* 就可以跨层级,捕获到 Index 组件的实例了 */ 
    console.log(node.current.componentDidMount)
  },[])
  return <div><HocIndex ref={node}  /></div>
}

如上,解决了高阶组件引入Ref的问题。

lazy

React.lazy 和 Suspense 技术还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,我们推荐 Loadable Components 这个库

React.lazySuspense配合一起用,能够有动态加载组件的效果。React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise ,该 Promise 需要 resolve 一个 default exportReact 组件。

我们模拟一个动态加载的场景。

父组件

import Test from './comTest'
const LazyComponent =  React.lazy(()=> new Promise((resolve)=>{
      setTimeout(()=>{
          resolve({
              default: ()=> <Test />
          })
      },2000)
}))
class index extends React.Component{   
    render(){
        return <div className="context_box"  style={ { marginTop :'50px' } }   >
           <React.Suspense fallback={ <div className="icon" ><SyncOutlined  spin  /></div> } >
               <LazyComponent />
           </React.Suspense>
        </div>
    }
}

我们用setTimeout来模拟import异步引入效果。

Test

class Test extends React.Component{
    constructor(props){
        super(props)
    }
    componentDidMount(){
        console.log('--componentDidMount--')
    }
    render(){
        return <div>
            <img src={alien}  className="alien" />
        </div>
    }
}

效果

lazy.gif

Suspense

何为Suspense, Suspense 让组件“等待”某个异步操作,直到该异步操作结束即可渲染。

用于数据获取的 Suspense 是一个新特性,你可以使用 `` 以声明的方式来“等待”任何内容,包括数据。本文重点介绍它在数据获取的用例,它也可以用于等待图像、脚本或其他异步的操作。

上面讲到高阶组件lazy时候,已经用 lazy + Suspense模式,构建了异步渲染组件。我们看一下官网文档中的案例:

const ProfilePage = React.lazy(() => import('./ProfilePage')); // 懒加载
<Suspense fallback={<Spinner />}>
  <ProfilePage />
</Suspense>

Fragment

react不允许一个组件返回多个节点元素,比如说如下情况

render(){
    return <li> 🍎🍎🍎 </li>
           <li> 🍌🍌🍌 </li>
           <li> 🍇🍇🍇 </li>
}

如果我们想解决这个情况,很简单,只需要在外层套一个容器元素。

render(){
    return <div>
           <li> 🍎🍎🍎 </li>
           <li> 🍌🍌🍌 </li>
           <li> 🍇🍇🍇 </li>
    </div>
}

但是我们不期望,增加额外的dom节点,所以react提供Fragment碎片概念,能够让一个组件返回多个元素。 所以我们可以这么写

<React.Fragment>
    <li> 🍎🍎🍎 </li>
    <li> 🍌🍌🍌 </li>
    <li> 🍇🍇🍇 </li>
</React.Fragment>

还可以简写成:

<>
    <li> 🍎🍎🍎 </li>
    <li> 🍌🍌🍌 </li>
    <li> 🍇🍇🍇 </li>
</>

Fragment区别是,Fragment可以支持key属性。<>不支持key属性。

温馨提示。我们通过map遍历后的元素,react底层会处理,默认在外部嵌套一个``。

比如:

{
   [1,2,3].map(item=><span key={item.id} >{ item.name }</span>)
}

react底层处理之后,等价于:

<Fragment>
   <span></span>
   <span></span>
   <span></span>
</Fragment>

Profiler

Profiler这个api一般用于开发阶段,性能检测,检测一次react组件渲染用时,性能开销。

Profiler 需要两个参数:

第一个参数:是 id,用于表识唯一性的Profiler

第二个参数:onRender回调函数,用于渲染完成,接受渲染参数。

实践:

const index = () => {
  const callback = (...arg) => console.log(arg)
  return <div >
    <div >
      <Profiler id="root" onRender={ callback }  >
        <Router  >
          <Meuns/>
          <KeepaliveRouterSwitch withoutRoute >
              { renderRoutes(menusList) }
          </KeepaliveRouterSwitch>
        </Router>
      </Profiler> 
    </div>
  </div>
}

结果

Profiler.jpg

onRender

  • 0 -id: root -> Profiler 树的 id

  • 1 -phase: mount -> mount 挂载 , update 渲染了。

  • 2 -actualDuration: 6.685000262223184 -> 更新 committed 花费的渲染时间。

  • 3 -baseDuration: 4.430000321008265 -> 渲染整颗子树需要的时间

  • 4 -startTime : 689.7299999836832 -> 本次更新开始渲染的时间

  • 5 -commitTime : 698.5799999674782 -> 本次更新committed 的时间

  • 6 -interactions: set{} -> 本次更新的 interactions 的集合

尽管 Profiler 是一个轻量级组件,我们依然应该在需要时才去使用它。对一个应用来说,每添加一些都会给 CPU 和内存带来一些负担。

StrictMode

StrictMode见名知意,严格模式,用于检测react项目中的潜在的问题,。与 Fragment 一样, StrictMode 不会渲染任何可见的 UI 。它为其后代元素触发额外的检查和警告。

严格模式检查仅在开发模式下运行;它们不会影响生产构建。

StrictMode目前有助于:

  • ①识别不安全的生命周期。
  • ②关于使用过时字符串 ref API 的警告
  • ③关于使用废弃的 findDOMNode 方法的警告
  • ④检测意外的副作用
  • ⑤检测过时的 context API

实践:识别不安全的生命周期

对于不安全的生命周期,指的是UNSAFE_componentWillMountUNSAFE_componentWillReceiveProps , UNSAFE_componentWillUpdate

外层开启严格模式:

<React.StrictMode> 
    <Router  >
        <Meuns/>
        <KeepaliveRouterSwitch withoutRoute >
            { renderRoutes(menusList) }
        </KeepaliveRouterSwitch>
    </Router>
</React.StrictMode>

我们在内层组件中,使用不安全的生命周期:

class Index extends React.Component{    
    UNSAFE_componentWillReceiveProps(){
    }
    render(){      
        return <div className="box" />   
    }
}

结果

strictMode.jpg

工具类

接下来我们一起来探究一下react工具类函数的用法。

utils.jpg

createElement

一提到createElement,就不由得和JSX联系一起。我们写的jsx,最终会被 babel,用createElement编译成react元素形式。我写一个组件,我们看一下会被编译成什么样子,

如果我们在render里面这么写:

render(){
    return <div className="box" >
        <div className="item"  >生命周期</div>
        <Text  mes="hello,world"  />
        <React.Fragment> Flagment </React.Fragment>
        { /*  */ }
        text文本
    </div>
}

会被编译成这样:

render() {
    return React.createElement("div", { className: "box" },
            React.createElement("div", { className: "item" }, "\u751F\u547D\u5468\u671F"),
            React.createElement(Text, { mes: "hello,world" }),
            React.createElement(React.Fragment, null, " Flagment "),
            "text\u6587\u672C");
    }

当然我们可以不用jsx模式,而是直接通过createElement进行开发。

createElement模型:

React.createElement(
  type,
  [props],
  [...children]
)

createElement参数:

第一个参数: 如果是组件类型,会传入组件,如果是dom元素类型,传入div或者span之类的字符串。

第二个参数: 第二个参数为一个对象,在dom类型中为属性,在组件类型中为props

**其他参数: **依次为children,根据顺序排列。

createElement做了些什么?

经过createElement处理,最终会形成 $$typeof = Symbol(react.element)对象。对象上保存了该react.element的信息。

cloneElement

可能有的同学还傻傻的分不清楚cloneElementcreateElement区别和作用。

createElement把我们写的jsx,变成element对象; 而cloneElement的作用是以 element 元素为样板克隆并返回新的 React 元素。返回元素的 props 是将新的 props 与原始元素的 props 浅层合并后的结果。

那么cloneElement感觉在我们实际业务组件中,可能没什么用,但是在一些开源项目,或者是公共插槽组件中用处还是蛮大的,比如说,我们可以在组件中,劫持children element,然后通过cloneElement克隆element,混入props。经典的案例就是 react-router中的Swtich组件,通过这种方式,来匹配唯一的 Route并加以渲染。

我们设置一个场景,在组件中,去劫持children,然后给children赋能一些额外的props:

function FatherComponent({ children }){
    const newChildren = React.cloneElement(children, { age: 18})
    return <div> { newChildren } </div>
}

function SonComponent(props){
    console.log(props)
    return <div>hello,world</div>
}

class Index extends React.Component{    
    render(){      
        return <div className="box" >
            <FatherComponent>
                <SonComponent name="alien"  />
            </FatherComponent>
        </div>   
    }
}

打印:

cloneElment.jpg

完美达到了效果!

createContext

createContext用于创建一个Context对象,createContext对象中,包括用于传递 Context 对象值 valueProvider,和接受value变化订阅的Consumer

const MyContext = React.createContext(defaultValue)

createContext接受一个参数defaultValue,如果Consumer上一级一直没有Provider,则会应用defaultValue作为value只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

我们来模拟一个 Context.ProviderContext.Consumer的例子:

function ComponentB(){
    /* 用 Consumer 订阅, 来自 Provider 中 value 的改变  */
    return <MyContext.Consumer>
        { (value) => <ComponentA  {...value} /> }
    </MyContext.Consumer>
}

function ComponentA(props){
    const { name , mes } = props
    return <div> 
            <div> 姓名: { name }  </div>
            <div> 想对大家说: { mes }  </div>
         </div>
}

function index(){
    const [ value , ] = React.useState({
        name:'alien',
        mes:'let us learn React '
    })
    return <div style={{ marginTop:'50px' }} >
        <MyContext.Provider value={value}  >
          <ComponentB />
    </MyContext.Provider>
    </div>
}

打印结果:

createContent.jpg

ProviderConsumer的良好的特性,可以做数据的Consumer一方面传递value,另一方面可以订阅value的改变。

Provider还有一个特性可以层层传递value,这种特性在react-redux中表现的淋漓尽致。

createFactory

React.createFactory(type)

返回用于生成指定类型 React 元素的函数。类型参数既可以是标签名字符串(像是 'div' 或 'span'),也可以是 React 组件 类型 ( class 组件或函数组件),或是 React fragment 类型。

使用:

 const Text = React.createFactory(()=><div>hello,world</div>) 
function Index(){  
    return <div style={{ marginTop:'50px'  }} >
        <Text/>
    </div>
}

效果

createFactory.jpg

报出警告,这个api将要被废弃,我们这里就不多讲了,如果想要达到同样的效果,请用React.createElement

createRef

createRef可以创建一个 ref 元素,附加在react元素上。

用法:

class Index extends React.Component{
    constructor(props){
        super(props)
        this.node = React.createRef()
    }
    componentDidMount(){
        console.log(this.node)
    }
    render(){
        return <div ref={this.node} > my name is alien </div>
    }
}

个人觉得createRef这个方法,很鸡肋,我们完全可以class类组件中这么写,来捕获ref

class Index extends React.Component{
    node = null
    componentDidMount(){
        console.log(this.node)
    }
    render(){
        return <div ref={(node)=> this.node } > my name is alien </div>
    }
}

或者在function组件中这么写:

function Index(){
    const node = React.useRef(null)
    useEffect(()=>{
        console.log(node.current)
    },[])
    return <div ref={node} >  my name is alien </div>
}

isValidElement

这个方法可以用来检测是否为react element元素,接受待验证对象,返回true或者false。这个api可能对于业务组件的开发,作用不大,因为对于组件内部状态,都是已知的,我们根本就不需要去验证,是否是react element 元素。 但是,对于一起公共组件或是开源库,isValidElement就很有作用了。

实践

我们做一个场景,验证容器组件的所有子组件,过滤到非react element类型。

没有用isValidElement验证之前:

const Text = () => <div>hello,world</div> 
class WrapComponent extends React.Component{
    constructor(props){
        super(props)
    }
    render(){
        return this.props.children
    }
}
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            <Text/>
            <div> my name is alien </div>
            Let's learn react together!
        </WrapComponent>
    </div>
}

过滤之前的效果

isValidElement.jpg

我们用isValidElement进行react element验证:

class WrapComponent extends React.Component{
    constructor(props){
        super(props)
        this.newChidren = this.props.children.filter(item => React.isValidElement(item) )
    }
    render(){
        return this.newChidren
    }
}

过滤之后效果

isValidElement111.jpg

过滤掉了非react elementLet's learn react together!

Children.map

接下来的五个api都是和react.Chidren相关的,我们来分别介绍一下,我们先来看看官网的描述,React.Children 提供了用于处理 this.props.children 不透明数据结构的实用方法。

有的同学会问遍历 children用数组方法,mapforEach 不就可以了吗? 请我们注意一下不透明数据结构,什么叫做不透明结构?

我们先看一下透明的结构:

class Text extends React.Component{
    render(){
        return <div>hello,world</div>
    }
}
function WrapComponent(props){
    console.log(props.children)
    return props.children
}
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            <Text/>
            <Text/>
            <Text/>
            <span>hello,world</span>
        </WrapComponent>
    </div>
}

打印

chidrenmap.jpg

但是我们把Index结构改变一下:

function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            { new Array(3).fill(0).map(()=><Text/>) }
            <span>hello,world</span>
        </WrapComponent>
    </div>
}

打印

chidrenmap2.jpg

这个数据结构,我们不能正常的遍历了,即使遍历也不能遍历,每一个子元素。此时就需要 react.Chidren 来帮忙了。

但是我们把WrapComponent组件用react.Chidren处理children:

function WrapComponent(props){
    const newChildren = React.Children.map(props.children,(item)=>item)
    console.log(newChildren)
    return newChildren
} 

此时就能正常遍历了,达到了预期效果。

C71364B2-25E8-4F7D-A26D-50CA36AF4E33.jpg

注意 如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历。

Children.forEach

Children.forEachChildren.map 用法类似,Children.map可以返回新的数组,Children.forEach仅停留在遍历阶段。

我们将上面的WrapComponent方法,用Children.forEach改一下。

function WrapComponent(props){
    React.Children.forEach(props.children,(item)=>console.log(item))
    return props.children
}   

Children.count

children 中的组件总数量,等同于通过 mapforEach 调用回调函数的次数。对于更复杂的结果,Children.count可以返回同一级别子组件的数量。

我们还是把上述例子进行改造:

function WrapComponent(props){
    const childrenCount =  React.Children.count(props.children)
    console.log(childrenCount,'childrenCount')
    return props.children
}   
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            { new Array(3).fill(0).map((item,index) => new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
            <span>hello,world</span>
        </WrapComponent>
    </div>
}

效果:

chidrencunt.jpg

Children.toArray

Children.toArray返回,props.children扁平化后结果。

function WrapComponent(props){
    const newChidrenArray =  React.Children.toArray(props.children)
    console.log(newChidrenArray,'newChidrenArray')
    return newChidrenArray
}   
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            { new Array(3).fill(0).map((item,index)=>new Array(2).fill(1).map((item,index1)=><Text key={index+index1} />)) }
            <span>hello,world</span>
        </WrapComponent>
    </div>
}

效果:

chuldeanarrgy.jpg

newChidrenArray ,就是扁平化的数组结构。React.Children.toArray() 在拉平展开子节点列表时,更改 key 值以保留嵌套数组的语义。也就是说, toArray 会为返回数组中的每个 key 添加前缀,以使得每个元素 key 的范围都限定在此函数入参数组的对象内。

Children.only

验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误。

不唯一

function WrapComponent(props){
    console.log(React.Children.only(props.children))
    return props.children
}   
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
            { new Array(3).fill(0).map((item,index)=><Text key={index} />) }
            <span>hello,world</span>
        </WrapComponent>
    </div>
}

效果

falseonly.jpg

唯一

function WrapComponent(props){
    console.log(React.Children.only(props.children))
    return props.children
}   
function Index(){
    return <div style={{ marginTop:'50px' }} >
        <WrapComponent>
           <Text/>
        </WrapComponent>
    </div>
}

效果

only.jpg

React.Children.only() 不接受 React.Children.map() 的返回值,因为它是一个数组而并不是 React 元素。

react-hooks

对于react-hooks,我已经写了三部曲,介绍了react-hooks使用,自定义hooks,以及react-hooks原理,感兴趣的同学可以去看看,文章末尾有链接,对于常用的api,我这里参考了react-hooks如何使用那篇文章。并做了相应精简化和一些内容的补充。

hooks.jpg

useState

useState可以弥补函数组件没有state的缺陷。useState可以接受一个初识值,也可以是一个函数actionaction返回值作为新的state。返回一个数组,第一个值为state读取值,第二个值为改变statedispatchAction函数。

我们看一个例子:

const DemoState = (props) => {
   /* number为此时state读取值 ,setNumber为派发更新的函数 */
   let [number, setNumber] = useState(0) /* 0为初始值 */
   return (<div>
       <span>{ number }</span>
       <button onClick={ ()=> {
         setNumber(number+1) /* 写法一 */
         setNumber(number=>number + 1 ) /* 写法二 */
         console.log(number) /* 这里的number是不能够即时改变的  */
       } } >num++</button>
   </div>)
}

useEffect

useEffect可以弥补函数组件没有生命周期的缺点。我们可以在useEffect第一个参数回调函数中,做一些请求数据,事件监听等操作,第二个参数作为dep依赖项,当依赖项发生变化,重新执行第一个函数。

useEffect可以用作数据交互。

/* 模拟数据交互 */
function getUserInfo(a){
    return new Promise((resolve)=>{
        setTimeout(()=>{ 
           resolve({
               name:a,
               age:16,
           }) 
        },500)
    })
}
const DemoEffect = ({ a }) => {
    const [ userMessage , setUserMessage ] :any= useState({})
    const div= useRef()
    const [number, setNumber] = useState(0)
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    /* useEffect使用 ,这里如果不加限制 ,会是函数重复执行,陷入死循环*/
    useEffect(()=>{
        /* 请求数据 */
       getUserInfo(a).then(res=>{
           setUserMessage(res)
       })
       /* 操作dom  */
       console.log(div.current) /* div */
       /* 事件监听等 */
        window.addEventListener('resize', handleResize)
    /* 只有当props->a和state->number改变的时候 ,useEffect副作用函数重新执行 ,如果此时数组为空[],证明函数只有在初始化的时候执行一次相当于componentDidMount */
    },[ a ,number ])
    return (<div ref={div} >
        <span>{ userMessage.name }</span>
        <span>{ userMessage.age }</span>
        <div onClick={ ()=> setNumber(1) } >{ number }</div>
    </div>)
}

useEffect可以用作事件监听,还有一些基于dom的操作。,别忘了在useEffect第一个参数回调函数,返一个函数用于清除事件监听等操作。

const DemoEffect = ({ a }) => {
    /* 模拟事件监听处理函数 */
    const handleResize =()=>{}
    useEffect(()=>{
       /* 定时器 延时器等 */
       const timer = setInterval(()=>console.log(666),1000)
       /* 事件监听 */
       window.addEventListener('resize', handleResize)
       /* 此函数用于清除副作用 */
       return function(){
           clearInterval(timer) 
           window.removeEventListener('resize', handleResize)
       }
    },[ a ])
    return (<div  >
    </div>)
}

useMemo

useMemo接受两个参数,第一个参数是一个函数,返回值用于产生保存值。 第二个参数是一个数组,作为dep依赖项,数组里面的依赖项发生变化,重新执行第一个函数,产生新的值

应用场景: 1 缓存一些值,避免重新执行上下文

const number = useMemo(()=>{
    /** ....大量的逻辑运算 **/
   return number
},[ props.number ]) // 只有 props.number 改变的时候,重新计算number的值。

2 减少不必要的dom循环

/* 用 useMemo包裹的list可以限定当且仅当list改变的时候才更新此list,这样就可以避免selectList重新循环 */
 {useMemo(() => (
      <div>{
          selectList.map((i, v) => (
              <span
                  className={style.listSpan}
                  key={v} >
                  {i.patentName} 
              </span>
          ))}
      </div>
), [selectList])}

3 减少子组件渲染

/* 只有当props中,list列表改变的时候,子组件才渲染 */
const  goodListChild = useMemo(()=> <GoodList list={ props.list } /> ,[ props.list ])

useCallback

useMemouseCallback 接收的参数都是一样,都是在其依赖项发生变化后才执行,都是返回缓存的值,区别在于 useMemo 返回的是函数运行的结果, useCallback 返回的是函数。 返回的callback可以作为props回调函数传递给子组件。

/* 用react.memo */
const DemoChildren = React.memo((props)=>{
   /* 只有初始化的时候打印了 子组件更新 */
    console.log('子组件更新')
   useEffect(()=>{
       props.getInfo('子组件')
   },[])
   return <div>子组件</div>
})
const DemoUseCallback=({ id })=>{
    const [number, setNumber] = useState(1)
    /* 此时usecallback的第一参数 (sonName)=>{ console.log(sonName) }
     经过处理赋值给 getInfo */
    const getInfo  = useCallback((sonName)=>{
          console.log(sonName)
    },[id])
    return <div>
        {/* 点击按钮触发父组件更新 ,但是子组件没有更新 */}
        <button onClick={ ()=>setNumber(number+1) } >增加</button>
        <DemoChildren getInfo={getInfo} />
    </div>
}

useRef

useRef的作用:

  • 一 是可以用来获取dom元素,或者class组件实例 。
  • react-hooks原理文章中讲过,创建useRef时候,会创建一个原始对象,只要函数组件不被销毁,原始对象就会一直存在,那么我们可以利用这个特性,来通过useRef保存一些数据。
const DemoUseRef = ()=>{
    const dom= useRef(null)
    const handerSubmit = ()=>{
        /*  <div >表单组件</div>  dom 节点 */
        console.log(dom.current)
    }
    return <div>
        {/* ref 标记当前dom节点 */}
        <div ref={dom} >表单组件</div>
        <button onClick={()=>handerSubmit()} >提交</button> 
    </div>
}

useLayoutEffect

useEffect执行顺序: 组件更新挂载完成 -> 浏览器 dom 绘制完成 -> 执行 useEffect 回调。 useLayoutEffect 执行顺序: 组件更新挂载完成 -> 执行 useLayoutEffect 回调-> 浏览器dom绘制完成。

所以说 useLayoutEffect 代码可能会阻塞浏览器的绘制 。我们写的 effectuseLayoutEffectreact在底层会被分别打上PassiveEffectHookLayout,在commit阶段区分出,在什么时机执行。

const DemoUseLayoutEffect = () => {
    const target = useRef()
    useLayoutEffect(() => {
        /*我们需要在dom绘制之前,移动dom到制定位置*/
        const { x ,y } = getPositon() /* 获取要移动的 x,y坐标 */
        animate(target.current,{ x,y })
    }, []);
    return (
        <div >
            <span ref={ target } className="animate"></span>
        </div>
    )
}

useReducer

react-hooks原理那篇文章中讲解到,useState底层就是一个简单版的useReducer

useReducer 接受的第一个参数是一个函数,我们可以认为它就是一个 reducer , reducer 的参数就是常规 reducer 里面的 stateaction ,返回改变后的 state , useReducer 第二个参数为 state 的初始值 返回一个数组,数组的第一项就是更新之后 state 的值 ,第二个参数是派发更新的 dispatch 函数。

我们来看一下useReducer如何使用:

const DemoUseReducer = ()=>{
    /* number为更新后的state值,  dispatchNumbner 为当前的派发函数 */
   const [ number , dispatchNumbner ] = useReducer((state,action)=>{
       const { payload , name  } = action
       /* return的值为新的state */
       switch(name){
           case 'add':
               return state + 1
           case 'sub':
               return state - 1 
           case 'reset':
             return payload       
       }
       return state
   },0)
   return <div>
      当前值:{ number }
      { /* 派发更新 */ }
      <button onClick={()=>dispatchNumbner({ name:'add' })} >增加</button>
      <button onClick={()=>dispatchNumbner({ name:'sub' })} >减少</button>
      <button onClick={()=>dispatchNumbner({ name:'reset' ,payload:666 })} >赋值</button>
      { /* 把dispatch 和 state 传递给子组件  */ }
      <MyChildren  dispatch={ dispatchNumbner } State={{ number }} />
   </div>
}

useContext

我们可以使用 useContext ,来获取父级组件传递过来的 context 值,这个当前值就是最近的父级组件 Provider 设置的 value 值,useContext 参数一般是由 createContext 方式引入 ,也可以父级上下文 context 传递 ( 参数为 context )。useContext 可以代替 context.Consumer 来获取 Provider 中保存的 value

/* 用useContext方式 */
const DemoContext = ()=> {
    const value:any = useContext(Context)
    /* my name is alien */
return <div> my name is { value.name }</div>
}
/* 用Context.Consumer 方式 */
const DemoContext1 = ()=>{
    return <Context.Consumer>
         {/*  my name is alien  */}
        { (value)=> <div> my name is { value.name }</div> }
    </Context.Consumer>
}

export default ()=>{
    return <div>
        <Context.Provider value={{ name:'alien' , age:18 }} >
            <DemoContext />
            <DemoContext1 />
        </Context.Provider>
    </div>
}

useImperativeHandle

useImperativeHandle 可以配合 forwardRef 自定义暴露给父组件的实例值。这个很有用,我们知道,对于子组件,如果是class类组件,我们可以通过ref获取类组件的实例,但是在子组件是函数组件的情况,如果我们不能直接通过ref的,那么此时useImperativeHandleforwardRef配合就能达到效果。

useImperativeHandle接受三个参数:

  • 第一个参数ref: 接受 forWardRef 传递过来的 ref
  • 第二个参数 createHandle :处理函数,返回值作为暴露给父组件的ref对象。
  • 第三个参数 deps:依赖项 deps,依赖项更改形成新的ref对象。

我们来模拟给场景,用useImperativeHandle,使得父组件能让子组件中的input自动赋值并聚焦。

function Son (props,ref) {
    console.log(props)
    const inputRef = useRef(null)
    const [ inputValue , setInputValue ] = useState('')
    useImperativeHandle(ref,()=>{
       const handleRefs = {
           /* 声明方法用于聚焦input框 */
           onFocus(){
              inputRef.current.focus()
           },
           /* 声明方法用于改变input的值 */
           onChangeValue(value){
               setInputValue(value)
           }
       }
       return handleRefs
    },[])
    return <div>
        <input
            placeholder="请输入内容"
            ref={inputRef}
            value={inputValue}
        />
    </div>
}

const ForwarSon = forwardRef(Son)

class Index extends React.Component{
    inputRef = null
    handerClick(){
       const { onFocus , onChangeValue } =this.cur
       onFocus()
       onChangeValue('let us learn React!')
    }
    render(){
        return <div style={{ marginTop:'50px' }} >
            <ForwarSon ref={node => (this.inputRef = node)} />
            <button onClick={this.handerClick.bind(this)} >操控子组件</button>
        </div>
    }
}

useImperativeHandle.gif

useDebugValue

useDebugValue 可用于在 React 开发者工具中显示自定义 hook 的标签。这个hooks目的就是检查自定义hooks

function useFriendStatus(friendID) {
  const [isOnline, setIsOnline] = useState(null);
  // ...
  // 在开发者工具中的这个 Hook 旁边显示标签
  // e.g. "FriendStatus: Online"
  useDebugValue(isOnline ? 'Online' : 'Offline');

  return isOnline;
}

我们不推荐你向每个自定义 Hook 添加 debug 值。当它作为共享库的一部分时才最有价值。在某些情况下,格式化值的显示可能是一项开销很大的操作。除非需要检查 Hook,否则没有必要这么做。因此,useDebugValue 接受一个格式化函数作为可选的第二个参数。该函数只有在 Hook 被检查时才会被调用。它接受 debug 值作为参数,并且会返回一个格式化的显示值。

useTransition

useTransition允许延时由state改变而带来的视图渲染。避免不必要的渲染。它还允许组件将速度较慢的数据获取更新推迟到随后渲染,以便能够立即渲染更重要的更新。

const TIMEOUT_MS = { timeoutMs: 2000 }
const [startTransition, isPending] = useTransition(TIMEOUT_MS)
  • useTransition 接受一个对象, timeoutMs代码需要延时的时间。
  • 返回一个数组。第一个参数: 是一个接受回调的函数。我们用它来告诉 React 需要推迟的 state第二个参数: 一个布尔值。表示是否正在等待,过度状态的完成(延时state的更新)。

下面我们引入官网的列子,来了解useTransition的使用。

const SUSPENSE_CONFIG = { timeoutMs: 2000 };

function App() {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition(SUSPENSE_CONFIG);
  return (
    <>
      <button
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId));
          });
        }}
      >
        Next
      </button>
      {isPending ? " 加载中..." : null}
      <Suspense fallback={<Spinner />}>
        <ProfilePage resource={resource} />
      </Suspense>
    </>
  );
}

在这段代码中,我们使用 startTransition 包装了我们的数据获取。这使我们可以立即开始获取用户资料的数据,同时推迟下一个用户资料页面以及其关联的 Spinner 的渲染 2 秒钟( timeoutMs 中显示的时间)。

Taro使用笔记

CLI 工具安装

# 使用 npm 安装 CLI
$ npm install -g @tarojs/cli

# OR 使用 yarn 安装 CLI
$ yarn global add @tarojs/cli

# OR 安装了 cnpm,使用 cnpm 安装 CLI
$ cnpm install -g @tarojs/cli

如果安装过程出现sass相关的安装错误

npm install -g mirror-config-china

项目初始化

使用命令创建模板项目:

taro init myApp

npm 5.2+ 也可在不全局安装的情况下使用 npx 创建模板项目:

npx @tarojs/cli init myApp

编译

使用 Taro 的 build 命令可以把 Taro 代码编译成不同端的代码,然后在对应的开发工具中查看效果。

Taro 编译分为 devbuild 模式:

  • dev 模式(增加 --watch 参数) 将会监听文件修改。
  • build 模式(去掉 --watch 参数) 将不会监听文件修改,并会对代码进行压缩打包。
  • dev 模式生成的文件较大,设置环境变量 NODE_ENVproduction 可以开启压缩,方便预览,但编译速度会下降。

微信小程序

# yarn
$ yarn dev:weapp
$ yarn build:weapp

# npm script
$ npm run dev:weapp
$ npm run build:weapp

# 仅限全局安装
$ taro build --type weapp --watch
$ taro build --type weapp

# npx 用户也可以使用
$ npx taro build --type weapp --watch
$ npx taro build --type weapp

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type weapp --watch # Windows
$ NODE_ENV=production taro build --type weapp --watch # Mac

下载并打开微信开发者工具,然后选择项目根目录进行预览。

百度小程序

# yarn
$ yarn dev:swan
$ yarn build:swan

# npm script
$ npm run dev:swan
$ npm run build:swan

# 仅限全局安装
$ taro build --type swan --watch
$ taro build --type swan

# npx 用户也可以使用
$ npx taro build --type swan --watch
$ npx taro build --type swan

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type swan --watch # Windows
$ NODE_ENV=production taro build --type swan --watch # Mac

支付宝小程序

# yarn
$ yarn dev:alipay
$ yarn build:alipay

# npm script
$ npm run dev:alipay
$ npm run build:alipay

# 仅限全局安装
$ taro build --type alipay --watch
$ taro build --type alipay

# npx 用户也可以使用
$ npx taro build --type alipay --watch
$ npx taro build --type alipay

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type alipay --watch # Windows
$ NODE_ENV=production taro build --type alipay --watch # Mac

字节跳动小程序

# yarn
$ yarn dev:tt
$ yarn build:tt

# npm script
$ npm run dev:tt
$ npm run build:tt

# 仅限全局安装
$ taro build --type tt --watch
$ taro build --type tt

# npx 用户也可以使用
$ npx taro build --type tt --watch
$ npx taro build --type tt

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type tt --watch # Windows
$ NODE_ENV=production taro build --type tt --watch # Mac

QQ 小程序

# yarn
$ yarn dev:qq
$ yarn build:qq

# npm script
$ npm run dev:qq
$ npm run build:qq

# 仅限全局安装
$ taro build --type qq --watch
$ taro build --type qq

# npx 用户也可以使用
$ npx taro build --type qq --watch
$ npx taro build --type qq

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type qq --watch # Windows
$ NODE_ENV=production taro build --type qq --watch # Mac

京东小程序

# yarn
$ yarn dev:jd
$ yarn build:jd

# npm script
$ npm run dev:jd
$ npm run build:jd

# 仅限全局安装
$ taro build --type jd --watch
$ taro build --type jd

# npx 用户也可以使用
$ npx taro build --type jd --watch
$ npx taro build --type jd

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type jd --watch # Windows
$ NODE_ENV=production taro build --type jd --watch # Mac

企业微信小程序

yarn add @tarojs/plugin-platform-weapp-qy

Taro 项目配置

config = {
  // ...
  plugins: [
    '@tarojs/plugin-platform-weapp-qy'
  ]
}
# yarn
$ yarn dev:qywx
$ yarn build:qywx

# npm script
$ npm run dev:qywx
$ npm run build:qywx

# 仅限全局安装
$ taro build --type qywx --watch
$ taro build --type qywx

# npx 用户也可以使用
$ npx taro build --type qywx --watch
$ npx taro build --type qywx

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type qywx --watch # Windows
$ NODE_ENV=production taro build --type qywx --watch # Mac

钉钉小程序

Taro v3.1+ 开始支持

Taro v3.3.8+: 请使用 @tarojs/plugin-platform-alipay-dd 插件的 ~0.1.0 版本

Taro v3.1 & v3.2: 请使用 @tarojs/plugin-platform-alipay-dd 插件的 ~0.0.5 版本
yarn add @tarojs/plugin-platform-alipay-dd

Taro 项目配置

config = {
  // ...
  plugins: [
    '@tarojs/plugin-platform-alipay-dd'
  ]
}
# yarn
$ yarn dev:dd
$ yarn build:dd

# npm script
$ npm run dev:dd
$ npm run build:dd

# 仅限全局安装
$ taro build --type dd --watch
$ taro build --type dd

# npx 用户也可以使用
$ npx taro build --type dd --watch
$ npx taro build --type dd

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type dd --watch # Windows
$ NODE_ENV=production taro build --type dd --watch # Mac

支付宝 IOT 小程序

安装插件

yarn add @tarojs/plugin-platform-alipay-iot

Taro 项目配置

config = {
  // ...
  plugins: [
    '@tarojs/plugin-platform-alipay-iot'
  ]
}
# yarn
$ yarn dev:iot
$ yarn build:iot

# npm script
$ npm run dev:iot
$ npm run build:iot

# 仅限全局安装
$ taro build --type iot --watch
$ taro build --type iot

# npx 用户也可以使用
$ npx taro build --type iot --watch
$ npx taro build --type iot

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type iot --watch # Windows
$ NODE_ENV=production taro build --type iot --watch # Mac

飞书小程序

yarn add @tarojs/plugin-platform-lark

Taro 项目配置

config = {
  // ...
  plugins: [
    '@tarojs/plugin-platform-lark'
  ]
}

编译

# yarn
$ yarn dev:lark
$ yarn build:lark

# npm script
$ npm run dev:lark
$ npm run build:lark

# 仅限全局安装
$ taro build --type lark --watch
$ taro build --type lark

# npx 用户也可以使用
$ npx taro build --type lark --watch
$ npx taro build --type lark

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type lark --watch # Windows
$ NODE_ENV=production taro build --type lark --watch # Mac

快手小程序

Taro v3.1+ 开始支持

Taro v3.3+: 请使用 taro-plugin-platform-kwai 插件的 1.0 或以上版本

Taro v3.2: 请使用 taro-plugin-platform-ks 插件的 1.2.x 版本

Taro v3.1: 请使用 taro-plugin-platform-ks 插件的 1.0.x 版本

插件安装

yarn add @tarojs/plugin-platform-kwai

Taro 项目配置

config = {
  // ...
  plugins: [
    '@tarojs/plugin-platform-kwai'
  ]
}

编译

# yarn
$ yarn dev:kwai
$ yarn build:kwai

# npm script
$ npm run dev:kwai
$ npm run build:kwai

# 仅限全局安装
$ taro build --type kwai --watch
$ taro build --type kwai

# npx 用户也可以使用
$ npx taro build --type kwai --watch
$ npx taro build --type kwai

# watch 同时开启压缩
$ set NODE_ENV=production && taro build --type kwai --watch # Windows
$ NODE_ENV=production taro build --type kwai --watch # Mac

h5

# yarn
$ yarn dev:h5
$ yarn build:h5

# npm script
$ npm run dev:h5
$ npm run build:h5

# 仅限全局安装
$ taro build --type h5 --watch
$ taro build --type h5

# npx 用户也可以使用
$ npx taro build --type h5 --watch
$ npx taro build --type h5

React native

React Native 端开发流程

由浅入深的实战《教程》

如果发现不一致的情况可以使用 Taro 升级命令 taro update self [版本号] 和 taro update project [版本号]来分别将 CLI 和项目依赖升级到指定版本; 或者也可以手动安装相应版本 CLI,修改 package.json 依赖版本号,然后重装依赖来解决。

# 使用Taro 升级命令更新CLI版本到最新版本
$ taro update self [版本号]
# 使用Taro 升级命令更新CLI版本到指定版本
$ taro update self
# 使用Taro 升级命令将项目依赖升级到与@tarojs/cli一致的版本
$ taro update project 
# 使用Taro 升级命令将项目依赖升级到指定版本
$ taro update project [版本号]

更多资源 https://github.com/NervJS/awesome-taro

https://taro-docs.jd.com/taro/docs/composition

项目目录结构

├── dist                        编译结果目录
|
├── config                      项目编译配置目录
|   ├── index.js                默认配置
|   ├── dev.js                  开发环境配置
|   └── prod.js                 生产环境配置
|
├── src                         源码目录
|   ├── pages                   页面文件目录
|   |   └── index               index 页面目录
|   |       ├── index.js        index 页面逻辑
|   |       ├── index.css       index 页面样式
|   |       └── index.config.js index 页面配置
|   |
|   ├── app.js                  项目入口文件
|   ├── app.css                 项目总通用样式
|   └── app.config.js           项目入口配置
|
├── project.config.json         微信小程序项目配置 project.config.json
├── project.tt.json             字节跳动小程序项目配置 project.config.json
├── project.swan.json           百度小程序项目配置 project.swan.json
├── project.qq.json             QQ 小程序项目配置 project.config.json
|
├── babel.config.js             Babel 配置
├── tsconfig.json               TypeScript 配置
├── .eslintrc                   ESLint 配置
|
└── package.json

编译配置

└── config                      项目编译配置目录
    ├── index.js                默认配置
    ├── dev.js                  开发环境配置
    └── prod.js                 生产环境配置

用于配置 Taro 项目的编译行为、修改 Webpack 配置等,详情请参考编译配置编译配置详情

page

└── src                         源码目录
    └── pages                   页面文件目录
        └── index               index 页面目录
            ├── index.js        index 页面逻辑
            ├── index.css       index 页面样式
            └── index.config.js index 页面配置
  1. 页面配置

page.config.js 对应小程序规范的页面配置文件 page.json,优势在于它是 JS 文件可以编写逻辑。配置以微信小程序的页面配置为规范。详情请参考页面配置

  1. 页面样式

页面的样式文件可以通过 ES6 规范的 import 进行引入。

pages/index/index.js

import './index.css';

Copy

  1. 页面路由

页面路由与小程序规范一致,需要在小程序全局配置 app.config.js 中进行配置。

项目配置

└──project.config.json         微信小程序项目配置 project.config.json

各类小程序平台均有自己的项目配置文件,Taro 支持对它们进行适配,详情请参考项目配置

babel配置

└── babel.config.js             Babel 配置

请参考 Babel 配置

Eslint 配置

└── .eslintrc                   ESLint 配置

ESLint 配置请参考 Github

编译配置

编译配置存放于项目根目录下的 config 目录中,包含三个文件:

  • index.js 是通用配置
  • dev.js 是项目预览时的配置
  • prod.js 是项目打包时的配置

详细的编译配置文档请查阅:编译配置详情

默认配置

const config = {
  // 项目名称
  projectName: 'Awesome Next',
  // 项目创建日期
  date: '2020-6-2',
  // 设计稿尺寸
  designWidth: 750,
  // 设计稿尺寸换算规则
  deviceRatio: {
    640: 2.34 / 2,
    750: 1,
    828: 1.81 / 2
  },
  // 项目源码目录
  sourceRoot: 'src',
  // 项目产出目录
  outputRoot: 'dist',
  // Taro 插件配置
  plugins: [],
  // 全局变量设置
  defineConstants: {},
  // 文件 copy 配置
  copy: {
    patterns: [
    ],
    options: {
    }
  },
  // 框架,react,nerv,vue, vue3 等
  framework: 'react',
  // 小程序端专用配置
  mini: {
    postcss: {
      autoprefixer: {
        enable: true
      },
      // 小程序端样式引用本地资源内联配置
      url: {
        enable: true,
        config: {
          limit: 10240
        }
      },
      cssModules: {
        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
        config: {
          namingPattern: 'module', // 转换模式,取值为 global/module
          generateScopedName: '[name]__[local]___[hash:base64:5]'
        }
      }
    },
    // 自定义 Webpack 配置
    webpackChain (chain, webpack) {}
  },
  // H5 端专用配置
  h5: {
    publicPath: '/',
    staticDirectory: 'static',
    postcss: {
      autoprefixer: {
        enable: true
      },
      cssModules: {
        enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
        config: {
          namingPattern: 'module', // 转换模式,取值为 global/module
          generateScopedName: '[name]__[local]___[hash:base64:5]'
        }
      }
    },
    // 自定义 Webpack 配置
    webpackChain (chain, webpack) {},
    devServer: {}
  }
};

module.exports = function(merge) {
  if (process.env.NODE_ENV === 'development') {
    return merge({}, config, require('./dev'));
  }
  return merge({}, config, require('./prod'));
};

设计稿及尺寸单位

在 Taro 中尺寸单位建议使用 px百分比 %,Taro 默认会对所有单位进行转换。在 Taro 中书写尺寸按照 1:1 的关系来进行书写,即从设计稿上量的长度 100px,那么尺寸书写就是 100px,当转成微信小程序的时候,尺寸将默认转换为 100rpx,当转成 H5 时将默认转换为以 rem 为单位的值。

如果你希望部分 px 单位不被转换成 rpx 或者 rem ,最简单的做法就是在 px 单位中增加一个大写字母,例如 Px 或者 PX 这样,则会被转换插件忽略。

结合过往的开发经验,Taro 默认以 750px 作为换算尺寸标准,如果设计稿不是以 750px 为标准,则需要在项目配置 config/index.js 中进行设置,例如设计稿尺寸是 640px,则需要修改项目配置 config/index.js 中的 designWidth 配置为 640

/config/index.js

const config = {
  projectName: 'myProject',
  date: '2018-4-18',
  designWidth: 640,
  ....
}

目前 Taro 支持 750640828 三种尺寸设计稿,他们的换算规则如下:

const DEVICE_RATIO = {
  '640': 2.34 / 2,
  '750': 1,
  '828': 1.81 / 2
}

建议使用 Taro 时,设计稿以 iPhone 6 750px 作为设计尺寸标准。

如果你的设计稿是 375 ,不在以上三种之中,那么你需要把 designWidth 配置为 375,同时在 DEVICE_RATIO 中添加换算规则如下:

const DEVICE_RATIO = {
  '640': 2.34 / 2,
  '750': 1,
  '828': 1.81 / 2,
  '375': 2 / 1
}

api

在编译时,Taro 会帮你对样式做尺寸转换操作,但是如果是在 JS 中书写了行内样式,那么编译时就无法做替换了,针对这种情况,Taro 提供了 API Taro.pxTransform 来做运行时的尺寸转换。

Taro.pxTransform(10) // 小程序:rpx,H5:rem

配置

默认配置会对所有的 px 单位进行转换,有大写字母的 PxPX 则会被忽略。

参数默认值如下:

{
  onePxTransform: true, // 设置 1px 是否需要被转换(https://taro-docs.jd.com/taro/docs/size#onepxtransform-boolean)
  unitPrecision: 5, // REM 单位允许的小数位。(https://taro-docs.jd.com/taro/docs/size#unitprecision-number)
  propList: ['*'], // 允许转换的属性。(https://taro-docs.jd.com/taro/docs/size#proplist-array)
  selectorBlackList: [], // 黑名单里的选择器将会被忽略。(https://taro-docs.jd.com/taro/docs/size#selectorblacklist)
  replace: true, // 直接替换而不是追加一条进行覆盖。(https://taro-docs.jd.com/taro/docs/size#replace-boolean)
  mediaQuery: false, // 允许媒体查询里的 px 单位转换 (https://taro-docs.jd.com/taro/docs/size#mediaquery-boolean)
  minPixelValue: 0 // 设置一个可被转换的最小 px 值(https://taro-docs.jd.com/taro/docs/size#minpixelvalue-number)
}

/config/index.js

{
  h5: {
    publicPath: '/',
    staticDirectory: 'static',
    postcss: {
      autoprefixer: {
        enable: true
      },
      pxtransform: {
        enable: true,
        config: {
          selectorBlackList: ['body']
        }
      }
    }
  },
  mini: {
    // ...
    postcss: {
      pxtransform: {
        enable: true,
        config: {
          selectorBlackList: ['body']
        }
      }
    }
  }
}

CSS 编译时忽略(过滤)

当前忽略单个属性的最简单的方法,就是 px 单位使用大写字母

 /* `px` is converted to `rem` */
.convert {
  font-size: 16px; // converted to 1rem
}

 /* `Px` or `PX` is ignored by `postcss-pxtorem` but still accepted by browsers */
.ignore {
  border: 1Px solid; // ignored
  border-width: 2PX; // ignored
}

全局配置

根目录下的 app.config.js 文件用来对小程序进行全局配置,配置项遵循微信小程序规范,并且对所有平台进行统一。

注意:

  1. app.config.js 里 require 或 import 引用的 js 文件目前没有经过 Babel 编译语法
  2. 多端差异化逻辑可以使用 process.env.TARO_ENV 变量作条件判断来实现。
  3. app.config.js 不支持多端文件的形式,如 app.weapp.js 这样是不起作用的。

通用配置项

在 H5、React Native、所有小程序均支持的配置。

属性 类型 必填 描述
pages String Array 页面路径列表
window Object 全局的默认窗口表现
tabBar Object 底部 tab 栏的表现
subPackages Object Array 分包结构配置

pages

如开发目录为:

├── app.js
├── app.json
├── app.wxss
├── pages
│   │── index
│   │   ├── index.wxml
│   │   ├── index.js
│   │   ├── index.json
│   │   └── index.wxss
│   └── logs
│       ├── logs.wxml
│       └── logs.js
└── utils

app.config.js

export default {
  pages: [
    'pages/index/index',
    'pages/logs/logs'
  ]
}

window

用于设置小程序的状态栏、导航条、标题、窗口背景色,其配置项如下。

属性 类型 默认值 描述
navigationBarBackgroundColor HexColor(十六进制颜色值) #000000 导航栏背景颜色,如 #000000
navigationBarTextStyle String white 导航栏标题颜色,仅支持 black / white
navigationBarTitleText String 导航栏标题文字内容
navigationStyle String default 导航栏样式,仅支持以下值:default 默认样式;custom 自定义导航栏,只保留右上角胶囊按钮
backgroundColor String 窗口的背景色
backgroundTextStyle String dark 下拉 loading 的样式,仅支持 dark / light
backgroundColorTop String #ffffff 顶部窗口的背景色,仅 iOS 支持
backgroundColorBottom String #ffffff 底部窗口的背景色,仅 iOS 支持
enablePullDownRefresh boolean false 是否开启当前页面的下拉刷新。
onReachBottomDistance Number 50 页面上拉触底事件触发时距页面底部距离,单位为 px
pageOrientation String portrait 屏幕旋转设置,支持 auto / portrait / landscape 详见 响应显示区域变化

各端支持程度如下#

属性 微信 百度 字节跳动 支付宝 H5 RN
navigationBarBackgroundColor ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
navigationBarTextStyle ✔️ ✔️ ✔️ ✔️ ✔️
navigationBarTitleText ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
navigationStyle ✔️(微信客户端 6.6.0) ✔️(百度 App 版本 11.1.0) ✔️
backgroundColor ✔️ ✔️ ✔️
backgroundTextStyle ✔️ ✔️ ✔️
backgroundColorTop ✔️(微信客户端 6.5.16) ✔️
backgroundColorBottom ✔️(微信客户端 6.5.16) ✔️
enablePullDownRefresh ✔️ ✔️ ✔️ ✔️
onReachBottomDistance ✔️ ✔️ ✔️
pageOrientation ✔️ 2.4.0 (auto) / 2.5.0 (landscape)

app.config.js

export default {
  pages: [
    'pages/index/index',
    'pages/logs/logs'
  ],
  window: {
    navigationBarBackgroundColor: '#ffffff',
    navigationBarTextStyle: 'black',
    navigationBarTitleText: '微信接口功能演示',
    backgroundColor: '#eeeeee',
    backgroundTextStyle: 'light'
  }
}

tabBar

如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

其配置项如下其配置项如下

属性 类型 必填 默认值 描述
color HexColor(十六进制颜色值) tab 上的文字默认颜色,仅支持十六进制颜色
selectedColor HexColor(十六进制颜色值) tab 上的文字选中时的颜色,仅支持十六进制颜色
backgroundColor HexColor(十六进制颜色值) tab 的背景色,仅支持十六进制颜色
borderStyle String black tabbar 上边框的颜色, 仅支持 black / white
list Array tab 的列表,详见 list 属性说明,最少 2 个、最多 5 个 tab
position String bottom tabBar的位置,仅支持 bottom / top
custom Boolean false 自定义 tabBar

其中 list 接受一个数组,只能配置最少 2 个、最多 5 个 tab。tab 按数组的顺序排序,每个项都是一个对象,其属性值如下:

属性 类型 必填 描述
pagePath String 页面路径,必须在 pages 中先定义
text String tab 上按钮文字
iconPath String 图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px,不支持网络图片。 当 position 为 top 时,不显示 icon。
selectedIconPath String 选中时的图片路径,icon 大小限制为40kb,建议尺寸为 81px * 81px,不支持网络图片。 当 position 为 top 时,不显示 icon。

各端支持程度如下#

属性 微信 百度 字节跳动 支付宝 H5 RN
color ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
selectedColor ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
backgroundColor ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
borderStyle ✔️ ✔️ ✔️ ✔️ ✔️
list ✔️ ✔️ ✔️ ✔️ ✔️ ✔️
position ✔️ ✔️
custom ✔️(基础库 2.5.0 以上)

subPackages#

H5 和 RN 会把 subPackages 合入 pages

启用分包加载时,声明项目分包结构

小程序端特有属性#

只在部分小程序中支持的属性,H5、RN 均不支持。

属性 类型 描述
networkTimeout Object 网络超时时间
debug Boolean 是否开启 debug 模式,默认关闭
permission Object 小程序接口权限相关设置
requiredBackgroundModes String Array 需要在后台使用的能力,如「音乐播放」
preloadRule Object 分包预下载规则
entryPagePath String 小程序默认启动首页
workers String Worker 代码放置的目录
navigateToMiniProgramAppIdList String Array 需要跳转的小程序列表,详见 wx.navigateToMiniProgram

networkTimeout#

各类网络请求的超时时间,单位均为毫秒。

属性 类型 必填 默认值 描述
request Number 60000 Taro.request 的超时时间,单位:毫秒
connectSocket Number 60000 Taro.connectSocket 的超时时间,单位:毫秒
uploadFile Number 60000 Taro.uploadFile 的超时时间,单位:毫秒
downloadFile Number 60000 Taro.downloadFile 的超时时间,单位:毫秒

permission

小程序接口权限相关设置。字段类型为 Object,结构为:

属性 类型 必填 默认值 描述
scope.userLocation PermissionObject 位置相关权限声明

PermissionObject 结构:

属性 类型 必填 默认值 描述
desc string 小程序获取权限时展示的接口用途说明。最长 30 个字符

app.config.js

export default {
  pages: [
    'pages/index/index',
    'pages/logs/logs'
  ],
  permission: {
    'scope.userLocation': {
      desc: '你的位置信息将用于小程序位置接口的效果展示'
    }
  }
}

图片

requiredBackgroundModes

申明需要后台运行的能力,类型为数组。目前支持以下项目:

  • audio: 后台音乐播放
  • location: 后台定位

app.config.js

export default {
  "pages": ["pages/index/index"],
  "requiredBackgroundModes": ["audio", "location"]
}

微信小程序特有属性

只在微信小程序中支持的属性。

属性 类型 描述
functionalPages Boolean 是否启用插件功能页,默认关闭
plugins Object 使用到的插件
resizable Boolean iPad 小程序是否支持屏幕旋转,默认关闭
usingComponents Object 全局自定义组件配置
sitemapLocation String 指明 sitemap.json 的位置
style String 指定使用升级后的weui样式
useExtendedLib Object 指定需要引用的扩展库
entranceDeclare Object 微信消息用小程序打开
darkmode boolean 小程序支持 DarkMode
themeLocation String 指明 theme.json 的位置
lazyCodeLoading String 配置自定义组件代码按需注入
singlePage Object 单页模式相关配置

百度小程序特有属性

只在百度小程序中支持的属性。

属性 类型 描述
routes Array Object 小程序自定义路由相关设置
dynamicLib Object 引入动态库
export default {
  "pages": [
      "pages/home/home",
      "pages/list/list",
      "pages/detail/detail"
  ],
  "subPackage": [
      {
          "root": "packageA",
          "pages": [
              "pages/home/home",
              "pages/list/list",
              "pages/detail/detail"
          ]
      }
  ],
  "routes": [
      {
          // 投放入口,scheme中的path
          "path": "home", 
          // 真实的物理存储路径
          "page": "pages/home/home" 
      },
      {
          "path": "list",
          "page": "pages/list/list"
      },
      {
          "path": "foo/bar",
          "page": "pages/list/list"
      }
  ]
}

QQ小程序特有属性

只在 QQ 小程序中支持的属性。

属性 类型 描述
groupIdList String Object 需要打开群资料卡的群号列表

路由功能

Taro 遵循微信小程序的路由规范。只需要修改全局配置的 pages 属性,配置为 Taro 应用中每个页面的路径即可。

路由跳转

可以通过 Taro 提供的 API 来跳转到目的页面。路由 API 的详细用法请查看 API 文档的 导航 章节。

// 跳转到目的页面,打开新页面
Taro.navigateTo({
  url: '/pages/page/path/name'
})

// 跳转到目的页面,在当前页面打开
Taro.redirectTo({
  url: '/pages/page/path/name'
})

路由传参

可以通过在所有跳转的 url 后面添加查询字符串参数进行跳转传参,例如:

// 传入参数 id=2&type=test
Taro.navigateTo({
  url: '/pages/page/path/name?id=2&type=test'
})

获取

import React, { Component } from 'react'
import { View } from '@tarojs/components'

class Index extends Component {
  // 建议在页面初始化时把 getCurrentInstance() 的结果保存下来供后面使用,
  // 而不是频繁地调用此 API
  $instance = getCurrentInstance()

  componentDidMount () {
    // 获取路由参数
    console.log($instance.router.params) // 输出 { id: 2, type: 'test' }
  }

  render () {
    return (
      <View className='index' />
    )
  }
}

export default Index

Taro 案例分析

搭建redux环境

安装

npm install redux react-redux redux-thunk redux-logger

创建文件src/store/index.js

import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import rootReducer from '../reducers'

const composeEnhancers =
  typeof window === 'object' &&
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({
      // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize...
    }) : compose

const middlewares = [
  thunkMiddleware
]

if (process.env.NODE_ENV === 'development') {
  middlewares.push(require('redux-logger').createLogger())
}

const enhancer = composeEnhancers(
  applyMiddleware(...middlewares),
  // other store enhancers if any
)

export default function configStore () {
  const store = createStore(rootReducer, enhancer)
  return store
}

创建src/reducers/index.js

import { combineReducers } from 'redux'
import home from './home'

export default combineReducers({
  home,
})

创建src/reducers/home.js

import {
  HOME_INFO, HOME_SEARCH_COUNT, HOME_RECOMMEND, HOME_PIN
} from '@constants/home'

const INITIAL_STATE = {
  homeInfo: {},
  searchCount: 0,
  pin: [],
  recommend: []
}

export default function home(state = INITIAL_STATE, action) {
  switch(action.type) {
    case HOME_INFO: {
      return {
        ...state,
        homeInfo: action.payload
      }
    }
    case HOME_SEARCH_COUNT: {
      return {
        ...state,
        searchCount: action.payload.count
      }
    }
    case HOME_PIN: {
      // 每3个分成一组
      const pin = []
      action.payload.forEach((item, index) => {
        const groupIndex = parseInt(index / 3)
        if (!pin[groupIndex]) {
          pin[groupIndex] = []
        }
        pin[groupIndex].push(item)
      })
      return { ...state, pin }
    }
    case HOME_RECOMMEND: {
      return {
        ...state,
        recommend: state.recommend.concat(action.payload.rcmdItemList)
      }
    }
    default:
      return state
  }
}

创建src/constants/home.js

export const HOME_INFO = 'HOME_INFO'
export const HOME_SEARCH_COUNT = 'HOME_SEARCH_COUNT'
export const HOME_PIN = 'HOME_PIN'
export const HOME_RECOMMEND = 'HOME_RECOMMEND'

修改src/app.js

import { Component } from 'react'
import './app.scss'
import {Provider} from "react-redux";
import configStore from './store'

const store = configStore()

class App extends Component {

  componentDidMount () {}

  componentDidShow () {}

  componentDidHide () {}

  componentDidCatchError () {}

  // this.props.children 是将要会渲染的页面
  render () {
    return (
      <Provider store={store}>
        {this.props.children}
      </Provider>
    )
    // return this.props.children
  }
}

export default App

举个简单的例子

首先创建src/constants/test.js

test.js

export const type = {
  INCREMENT: 'INCREMENT',
  DECREMENT: 'DECREMENT'
}

创建action

import {type} from "../constants/test";

export function add(){
  return {
    type: type.INCREMENT
  }
}
export function remove(){
  return {
    type: type.DECREMENT
  }
}

创建reducer

import {type} from '../constants/test'

const test = (state={count: 0}, action) => {
  switch (action.type) {
    case type.INCREMENT:
      return {...state,count: state.count + 1}
    case type.DECREMENT:
      return {...state,count: state.count - 1}
    default:
      return state;
  }
}

export default test;
import { combineReducers } from 'redux'
import test from './test'

export default combineReducers({
  test
})

页面中进行测试

import React,{Component} from 'react';
import {Button, View} from "@tarojs/components";
import {connect} from "react-redux";
import  * as actions from "../../../../actions/test";

@connect(state => state.test, {...actions})
class Test extends Component{
  state = {
    count: 0
  }

  handleAdd = () => {
    this.props.add()
  }

  handleRemove = () => {
    this.props.remove()
  }

  render(){
    const { count } = this.props
    return (
      <View>
        {count}
        <Button onClick={this.handleAdd}>+</Button>
        <Button onClick={this.handleRemove}>-</Button>
      </View>
    )
  }
}

export default Test;

搭建基于redux环境的接口请求

创建src/actions/home.js

import {
  HOME_INFO, HOME_SEARCH_COUNT, HOME_RECOMMEND, HOME_PIN
} from '@constants/home'
import {
  API_HOME, API_HOME_SEARCH_COUNT, API_HOME_RECOMMEND, API_HOME_PIN
} from '@constants/api'
import { createAction } from '@utils/redux'

/**
 * 首页数据
 * @param {*} payload
 */
export const dispatchHome = payload => createAction({
  url: API_HOME,
  type: HOME_INFO,
  payload
})

/**
 * 商品总数
 * @param {*} payload
 */
export const dispatchSearchCount = payload => createAction({
  url: API_HOME_SEARCH_COUNT,
  type: HOME_SEARCH_COUNT,
  payload
})

/**
 * 拼团
 * @param {*} payload
 */
export const dispatchPin = payload => createAction({
  url: API_HOME_PIN,
  type: HOME_PIN,
  payload
})

/**
 * 推荐商品
 * @param {*} payload
 */
export const dispatchRecommend = payload => createAction({
  url: API_HOME_RECOMMEND,
  type: HOME_RECOMMEND,
  payload
})

创建src/constants/api.js

/**
 * NOTE HOST、HOST_M 是在 config 中通过 defineConstants 配置的
 * 只所以不在代码中直接引用,是因为 eslint 会报 no-undef 的错误,因此用如下方式处理
 */
/* eslint-disable */
export const host = HOST
export const hostM = HOST_M
/* eslint-enable */

// pic
export const CDN = 'https://yanxuan.nosdn.127.net'

// home
export const API_HOME = `${host}/xhr/index/index.json`
export const API_HOME_SEARCH_COUNT = `${host}/xhr/search/displayBar.json`
export const API_HOME_PIN = `${hostM}/pin/min/item/recommend.json`
export const API_HOME_RECOMMEND = `${host}/xhr/rcmd/index.json`

创建src/utils/redux.js

/**
 * 适当封装 Redux,简化调用
 */
/* eslint-disable import/prefer-default-export */
import fetch from './request'

export function createAction(options) {
  const { url, payload, method, fetchOptions, cb, type } = options
  return (dispatch) => {
    return fetch({ url, payload, method, ...fetchOptions }).then((res) => {
      dispatch({ type, payload: cb ? cb(res) : res })
      return res
    })
  }
}

创建src/utils/request.js

import Taro from '@tarojs/taro'
import { API_USER, API_USER_LOGIN } from '@constants/api'

const CODE_SUCCESS = '200'
const CODE_AUTH_EXPIRED = '600'

function getStorage(key) {
  return Taro.getStorage({ key }).then(res => res.data).catch(() => '')
}

function updateStorage(data = {}) {
  return Promise.all([
    Taro.setStorage({ key: 'token', data: data['3rdSession'] || '' }),
    Taro.setStorage({ key: 'uid', data: data['uid'] || ''})
  ])
}

/**
 * 简易封装网络请求
 * // NOTE 需要注意 RN 不支持 *StorageSync,此处用 async/await 解决
 * @param {*} options
 */
export default async function fetch(options) {
  const { url, payload, method = 'GET', showToast = true, autoLogin = true } = options
  const token = await getStorage('token')
  const header = token ? { 'WX-PIN-SESSION': token, 'X-WX-3RD-Session': token } : {}
  if (method === 'POST') {
    header['content-type'] = 'application/json'
  }

  return Taro.request({
    url,
    method,
    data: payload,
    header
  }).then(async (res) => {
    const { code, data } = res.data
    if (code !== CODE_SUCCESS) {
      if (code === CODE_AUTH_EXPIRED) {
        await updateStorage({})
      }
      return Promise.reject(res.data)
    }

    if (url === API_USER_LOGIN) {
      await updateStorage(data)
    }

    // XXX 用户信息需展示 uid,但是 uid 是登录接口就返回的,比较蛋疼,暂时糅合在 fetch 中解决
    if (url === API_USER) {
      const uid = await getStorage('uid')
      return { ...data, uid }
    }

    return data
  }).catch((err) => {
    const defaultMsg = err.code === CODE_AUTH_EXPIRED ? '登录失效' : '请求异常'
    if (showToast) {
      Taro.showToast({
        title: err && err.errorMsg || defaultMsg,
        icon: 'none'
      })
    }

    if (err.code === CODE_AUTH_EXPIRED && autoLogin) {
      Taro.navigateTo({
        url: '/pages/user-login/user-login'
      })
    }

    return Promise.reject({ message: defaultMsg, ...err })
  })
}

首先在页面中引入action

import * as actions from '@actions/home'

同时引入connect

import {connect} from "react-redux"

使用注解方式进行使用

@connect(state => state.home, { ...actions })

调用方法

this.props.dispatchHome().then(() => {
	this.setState({ loaded: true })
})

Redux基础知识

1 Redux 是什么

  • redux是的诞生是为了给 React 应用提供「可预测化的状态管理」机制。
  • Redux会将整个应用状态(其实也就是数据)存储到到一个地方,称为store
  • 这个store里面保存一棵状态树(state tree)
  • 组件改变state的唯一方法是通过调用store的dispatch(派遣)方法,触发一个action,这个action被对应的reducer处理,于是state完成更新
  • 组件可以派发(dispatch)行为(action)给store,而不是直接通知其它组件
  • 其它组件可以通过订阅store中的状态(state)来刷新自己的视图
  • Redux 专注于状态管理,和react解耦
  • 单一状态,单项数据流
  • 核心概念:store, state, action, reducer

原理图

Redux工作流

Redux工作流

创建reducer

  • 可以使用单独的一个reducer,也可以将多个reducer合并为一个reducer,即:combineReducers()
  • action发出命令后将state放入reucer加工函数中,返回新的state,对state进行加工处理

创建action

  • 用户是接触不到state的,只能有view触发,所以,这个action可以理解为指令,需要发出多少动作就有多少指令
  • action是一个对象,必须有一个叫type的参数,定义action类型

创建的store,使用createStore方法

  • store 可以理解为有多个加工机器的总工厂
  • 提供subscribe,dispatch,getState这些方法。

举例:

赵政委的主要功能

  • 老赵有一个保险箱(store),所有人的状态,在那里都有记录(state)
  • 需要改变的时候,需要告诉专员(dispatch)要干什么(action)
  • 处理变化的人(reducer)拿到state和action,生成新的state

走马上任

  • 首先通过reducer新建store,随时通过store.getState获取状态
  • 需要状态变更,store.dispatch(action)来修改状态
  • Reducer函数接受state和action,返回新的state,可以用store.subscribe监听每次的修改

官方案例:

import { createStore } from 'redux';
/**
 * 这是一个 reducer,形式为 (state, action) => state 的纯函数。
 * 描述了 action 如何把 state 转变成下一个 state。
 *
 * state 的形式取决于你,可以是基本类型、数组、对象、
 * 甚至是 Immutable.js 生成的数据结构。惟一的要点是
 * 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
 *
 * 下面例子使用 `switch` 语句和字符串来做判断,但你可以写帮助类(helper)
 * 根据不同的约定(如方法映射)来判断,只要适用你的项目即可。
 */
function counter(state = 0, action) {
  switch (action.type) {
  case 'INCREMENT':
    return state + 1;
  case 'DECREMENT':
    return state - 1;
  default:
    return state;
  }
}
// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);
// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
  console.log(store.getState())
);
// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

2 Redux如何和React一起使用

手动连接,老赵怎么管理独立团呢

  • 把store.dispatch 方法传递给组件,内部可以调用修改状态
  • Subscribe订阅render函数,每次修改都重新渲染
  • Redux相关内容,移到单独的文件index.redux.js单独管理

在src目录下分别redux/action/ redux/reducer/ redux/store/

在 action 目录下新建 index.js

/**
 * Action类型:用户事件操作
 * @type {{INCREMENT: string, DECREMENT: string}}
 */
export const type = {
    INCREMENT: 'INCREMENT',
    DECREMENT: 'DECREMENT'
}
export function add(){
    return {
        type: type.INCREMENT
    }
}
export function remove(){
    return {
        type: type.DECREMENT
    }
}

在 reducer 目录下新建 index.js

/*
 * Reducer: 数据处理
 */
 import { type } from "./../action";
 const counter = (state=0, action) => {
     switch (action.type) {
       case type.INCREMENT:
           return state + 1
       case type.DECREMENT:
           return state - 1
       default:
           return state;
     }
 }
 export default counter;

在 store 目录下新建 index.js

/*
 *  引入createStore 保存数据源
 */
import { createStore } from "redux";
// 引入所有的reducer
import reducer from "./../reducer";
export default ()=>createStore(reducer)

修改src根目录下的index.js

import configureStore from './redux/store/index';
const store = configureStore();
function render(){
    ReactDOM.render(<App store={store}/>, document.getElementById('root'));
}
render()
store.subscribe(render)

修改App.js 文件

import React from 'react';
import './App.css';
import {Button, Flex, WingBlank} from 'antd-mobile';
import {add, remove} from './redux/action'
class App extends React.Component{
    state = {
        count: 0
    }
    static defaultProps = {
        num: 1
    }
    add = () => {
        this.setState({
            count: this.state.count+1
        })
    }
    remove = () => {
        this.setState({
            count: this.state.count-1
        })
    }
    render(){
        const store = this.props.store
        const num = store.getState()
        return (
            <div>
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={this.add}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{this.state.count}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={this.remove}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(add())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(remove())}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
            </div>
        );
    }
}
export default App;

3 处理异步、调试工具、更优雅的和React结合

  • Redux处理异步,需要redux-thunk插件
  • Npm install redux-devtools-extension并且开启
  • 使用react-redux优雅的链接react和redux

处理异步

Redux 默认只处理同步,异步任务需要react-thunk中间件

  • 安装及使用redux-thunk插件
    • yarn add redux-thunk --save
    • 使用applyMiddleware开启thunk中间件
    • Action可以返回函数,使用dispatch提交action

示例:

修改 src/redux/store/index.js

/*
 *  引入createStore 保存数据源
 */
import {createStore, applyMiddleware} from "redux";
import thunk from 'redux-thunk';
// 引入所有的reducer
import counter from "./../reducer";
export default () => createStore(counter, applyMiddleware(thunk))

同时在 src/redux/action/index.js 代码中新增

export function addAsync() {
    return dispatch => {
        setTimeout(() => {
            dispatch(add())
        }, 2000)
    }
}

在 App.js 代码中新增

import {add, remove, addAsync} from './redux/action'
<Flex.Item><Button type="primary" onClick={() => store.dispatch(addAsync())}>
                            +
                        </Button></Flex.Item>

4 调试工具

Chrome 搜索 redux 安装

  • 新建store的时候判断window.devToolsExtension
  • 使用compose结合thunk和window.devToolsExtension
  • 调试窗口的redux选项卡,实时看到state

5 使用react-redux

  • npm install react-redux --save
  • 忘记subscribe,记住reducer, action 和 dispatch即可
  • React-redux提供Provider和connect俩个接口来链接

React-redux 具体使用

  • Provider 组件在应用最外层,传入store即可,只用一次
  • Connect 负责从外部获取组件需要的参数
  • Connect 可以用装饰器的方式来写

示例:

修改 src/index.js 添加项目根组件

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import configureStore from './redux/store/index';
import { Provider } from "react-redux"; // 添加<Provider />项目根组件
import App from './App';
import * as serviceWorker from './serviceWorker';
const store = configureStore();
/*function render(){
    ReactDOM.render(<App store={store}/>, document.getElementById('root'));
}
render()
store.subscribe(render)*/
ReactDOM.render(
    (<Provider store={store}>
        <App/>
    </Provider>),
    document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

修改 src/App.js 文件 引入 react-redux connect

import React from 'react';
import './App.css';
import {connect} from 'react-redux';
import {Button, Flex, WingBlank} from 'antd-mobile';
import {add, remove, addAsync} from './redux/action'
class App extends React.Component{
    state = {
        count: 0
    }
    static defaultProps = {
        num: 1
    }
    add = () => {
        this.setState({
            count: this.state.count+1
        })
    }
    remove = () => {
        this.setState({
            count: this.state.count-1
        })
    }
    render(){
        /*const store = this.props.store
        const num = store.getState()*/
        const num = this.props.num;
        const addAsync = this.props.addAsync;
        const remove = this.props.remove;
        return (
            <div>
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={this.add}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{this.state.count}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={this.remove}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
                {/*<WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(addAsync())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(add())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(remove())}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>*/}
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => addAsync()}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => remove()}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
            </div>
        );
    }
}
//将state.num 绑定到 props 的num
const mapStateToProps = state => {
    console.log(state);
    return {
        num: state
    };
};
const actionCreators = {add, remove, addAsync}
export default connect(mapStateToProps,actionCreators)(App);

其中的代码修改处

  /*const store = this.props.store
        const num = store.getState()*/
        const num = this.props.num;
        const addAsync = this.props.addAsync;
        const remove = this.props.remove;
{/*<WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(addAsync())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(add())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(remove())}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>*/}
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => addAsync()}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => remove()}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>

最后将state.num 绑定到 props 的num

//将state.num 绑定到 props 的num
const mapStateToProps = state => {
    console.log(state);
    return {
        num: state
    };
};
const actionCreators = {add, remove, addAsync}
export default connect(mapStateToProps,actionCreators)(App);

6 使用装饰器优化connect代码

  • Npm run eject 弹出个性化配置
  • Npm install babel-plugin-transform-decorators-legacy插件
  • package.json 里 babel加上plugins配置

yarn add babel-plugin-transform-decorators-legacy --save-dev

babel-plugin-transform-decorators-legacy

进而可以使用注解的方式

示例:

安装plugins

{
 "plugins": [
      ["@babel/plugin-proposal-decorators", { "legacy": true }],
      [
        "import",
        {
          "libraryName": "antd-mobile",
          "style": "css"
        }
      ]
    ]
}

修改 src/App.js 代码

import React from 'react';
import './App.css';
import {connect} from 'react-redux';
import {Button, Flex, WingBlank} from 'antd-mobile';
import {add, remove, addAsync} from './redux/action'
//将state.num 绑定到 props 的num
/*const mapStateToProps = state => {
    console.log(state);
    return {
        num: state
    };
};
const actionCreators = {add, remove, addAsync}
@connect(
    mapStateToProps,actionCreators
)*/
@connect(
    state=>({num: state}),
    {add, remove, addAsync}
)
class App extends React.Component{
    state = {
        count: 0
    }
    static defaultProps = {
        num: 1
    }
    add = () => {
        this.setState({
            count: this.state.count+1
        })
    }
    remove = () => {
        this.setState({
            count: this.state.count-1
        })
    }
    render(){
        /*const store = this.props.store
        const num = store.getState()*/
        const num = this.props.num;
        const addAsync = this.props.addAsync;
        const remove = this.props.remove;
        return (
            <div>
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={this.add}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{this.state.count}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={this.remove}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
                {/*<WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(addAsync())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(add())}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => store.dispatch(remove())}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>*/}
                <WingBlank>
                    <Flex>
                        <Flex.Item><Button type="primary" onClick={() => addAsync()}>
                            +
                        </Button></Flex.Item>
                        <Flex.Item style={{textAlign: 'center'}}>{num}</Flex.Item>
                        <Flex.Item><Button type="primary" onClick={() => remove()}>
                            -
                        </Button></Flex.Item>
                    </Flex>
                </WingBlank>
            </div>
        );
    }
}
// export default connect(mapStateToProps,actionCreators)(App);
export default App;

taro + taro-ui + dva

我们引入dva

npm i dva-core dva-loading --save

新建dva.js

import { create } from 'dva-core';
import { createLogger } from 'redux-logger';
import createLoading from 'dva-loading';
 
let app;
let store;
let dispatch;
 
function createApp(opt) {
  // redux日志
  // opt.onAction = [createLogger()];
  app = create(opt);
  app.use(createLoading({}));
 
  if (!global.registered) opt.models.forEach(model => app.model(model));
  global.registered = true;
  app.start();
 
  store = app._store;
  app.getStore = () => store;
 
  dispatch = store.dispatch;
 
  app.dispatch = dispatch;
  return app;
}
 
export default {
  createApp,
  getDispatch() {
    return app.dispatch;
  }
}

并在入口文件导入

import dva from './utils/dva'
const dvaApp = dva.createApp({
  initialState: {},
  models: models,
});
const store = dvaApp.getStore();

封装下request网络请求

import Taro from '@tarojs/taro';
import { baseUrl, noConsole } from '../config';
 
export default (options = { method: 'GET', data: {} }) => {
  if (!noConsole) {
    console.log(`${new Date().toLocaleString()}【 M=${options.url} 】P=${JSON.stringify(options.data)}`);
  }
  return Taro.request({
    url: baseUrl + options.url,
    data: options.data,
    headers: {
      'Content-Type': 'application/json',
    },
    method: options.method.toUpperCase(),
  }).then((res) => {
    const { statusCode, data } = res;
    if (statusCode >= 200 && statusCode < 300) {
      if (!noConsole) {
        console.log(`${new Date().toLocaleString()}【 M=${options.url} 】【接口响应:】`,res.data);
      }
      if (data.status !== 'ok') {
        Taro.showToast({
          title: `${res.data.error.message}~` || res.data.error.code,
          icon: 'none',
          mask: true,
        });
      }
      return data;
    } else {
      throw new Error(`网络请求错误,状态码${statusCode}`);
    }
  })
}
posted @ 2022-04-07 08:45  journeyIT  阅读(527)  评论(0编辑  收藏  举报