Dva/Umi/Mobx入门教程

798 阅读34分钟

Dva

一、介绍

1、什么是 dva

React 应用级框架,将 React-Router + Redux + Redux-saga 三个 React 工具库包装在一起,简化了 API,让开发 React 应用更加方便和快捷

简单理解:dva = React-Router + Redux + Redux-saga

2、dva 文档

dvajs.com/guide/#%E7%…

二、环境搭建和使用

1、环境搭建

通过 npm 安装 dva-cli 并确保版本是 0.9.1 或以上。

$ npm install dva-cli -g    
$ dva -v                    //查看下是否安装成功,显示 dva 的版本号
dva-cli version 0.9.1

2、创建项目

$ dva new dva-1    //dva-1 为你创建项目的名称

安装成功后,cd 进入 dva-1 目录下,通过 npm start 和 yarn start 启动项目

如果启动报错的话,可以先执行 npm i 或者 yarn

3、使用 antd

在进入到项目目录下后,输入如下命令:

$ npm install antd babel-plugin-import --save

通过 npm 安装 antd 和 babel-plugin-importbabel-plugin-import 是用来按需加载 antd 的脚本和样式的。

注意!!!!!

请在全局目录下找到 .webpackrc 文件,输入以下代码,使 babel-plugin-import 插件生效。

{
+  "extraBabelPlugins": [
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+  ]
}

dva-cli 基于 roadhog 实现 build 和 dev,更多 .webpackrc 的配置详见 roadhog#配置

三、全局架构

.
├── mock    // mock数据文件夹
├── node_modules // 第三方的依赖
├── public  // 存放公共public文件的文件夹
├── src  // 最重要的文件夹,编写代码都在这个文件夹下
│   ├── assets // 可以放图片等静态资源
│   ├── components // 就是react中的公共组件
│   ├── models // dva最重要的文件夹,所有的数据交互及逻辑都写在这里
│   ├── routes // 就是react中的页面组件,类似于pages
│   ├── services // 放请求借口方法的文件夹
│   ├── utils // 自己的工具方法可以放在这边
│   ├── index.css // 入口文件样式
│   ├── index.ejs // ejs模板引擎
│   ├── index.js // 入口文件
│   └── router.js // 项目的路由文件
├── .eslintrc // 配置代码规范
├── .editorconfig // 用于跨不同的编辑器和IDE为多个开发人员维护一致的编码风格的配置文件
├── .gitignore // git上传时忽略的文件
├── .webpackrc.js // 项目的配置文件,配置接口转发,css_module等都在这边。
├── .roadhogrc.mock.js // mock的入口文件
└── package.json // 项目的依赖

1、index.js(重点)

import dva from 'dva';

// 1、创建 dva 实例
const app = dva();

// 2、装载插件 (可选)
app.use(require('dva-loading')());

// 3、注册 Model
app.model(require('./models/example'));

// 4、配置路由
app.router(require('./router'));

// 5、启动应用
app.start('#root');

通过上面的代码块,应该就可以很清楚了了解到 Dva 的5个 API

如果还不清楚,没关系,下面我一一讲解:

(1)、创建 dva 实例

用于创建应用,返回 dva 实例,dva 支持多实例,下面包含全部的可配属性::

const app = dva({
     history,
     initialState,
     onError,
     onAction,
     onStateChange,
     onReducer,
     onEffect,
     onHmr,
     extraReducers,
     extraEnhancers,
});

配置路由模式:

// 1. Initialize
const app = dva({ 
    // history:require("history").createBrowserHistory() //使用Browser模式的路由
    history:require("history").createHashHistory() //使用Hash模式的路由,默认值,不写也可以
});

(2)、装载插件

// 2、装载插件 (可选) 
app.use(require('dva-loading')());

需要任何样式的插件以上面的形式编写代码即可。

如果不需要任何插件,这段代码都可以直接省略。

上面引用的插件是:页面还未加载完毕时显示的 loading 图标,加上了上面那行插件代码,你就不要每个页面都写 showloading 和 hideloading 了。

(3)、注册 Model

// 3. Model
//app.model(require('./models/命名空间名').default);,一般来说命名空间名和文件名相同
app.model(require('./models/maizuo').default);
// app.model(require('./models/aaa').default);
// app.model(require('./models/bbb').default);

你每在models文件夹下创建出来的一个 model文件 都需要来全局 index.js 来注册一下,这样 model 层才能用。

(4)、配置路由

// 4、配置路由 
app.router(require('./router'));

细心的小伙伴会发现在 index.js 同级目录下有一个 router.js,这里的配置路由就是配置这个页面的东西。下面第2小点马上就讲解,这里只是告诉大家,如何引用配置好的路由。

(5)、启动应用

// 5、启动应用 
app.start('#root');

启动应用不解释。

2、router.js

Snipaste_2022-07-17_17-52-05.png

打开 router.js 你就看到如下的代码:

import React from "react";
import { Router, Route, Switch, Redirect, withRouter } from "dva/router";
import App from "./routes/App";
import Film from "./routes/Film";
import Cinema from "./routes/Cinema";
import Center from "./routes/Center";
import Detail from "./routes/Detail";
import Login from "./routes/Login";

function RouterConfig({ history }) {
  return (
    <Router history={history}>
      <Switch>
        <Route path="/login" component={Login} />
        <Route path="/" render={() => (
            <App>
              <Switch>
                <Route path="/film" component={Film} />
                <Route path="/cinema" component={Cinema} />
                <Route path="/center" render={() =>
                    localStorage.getItem("token") ? (
                      <Center />
                    ) : (
                      <Redirect to="/login" />
                    )
                  }
                />
                <Route path="/detail/:myid" component={Detail} />

                <Redirect from="/" to="/film"></Redirect>
              </Switch>
            </App>
          )}
        />
      </Switch>
    </Router>
  );
}

export default RouterConfig;

App.js

import { connect } from "dva";
import React, { Component } from "react";
import Tabbar from "../components/Tabbar";

class App extends Component {
  componentDidMount() {
    // console.log(this.props)
  }

  render() {
    return (
      <div>
          //接收App组件下传递过来的孩子组件
        {this.props.children} 
          //<Tabbar />即下面三个导航菜单组件
        {this.props.isShow && <Tabbar />}
      </div>
    );
  }
}

export default connect((state) => {
  console.log(111, state);
  return {
    a: 1,
    isShow: state.maizuo.isShow,
  };
})(App);

3、components 包

一般为我们创建出来的公共组件。

4、routes 包

这里我们可以理解为 pages。你所要显示出来的页面都写在这个下面。

5、services 包

为后台调用服务端接口的包,不做解释。

6、utils 包

这个包可以用来存放一些公共方法。需要使用时,导入 js,直接使用方法即可。

6、models 包

models 包用来存放 所有的 model 文件。

一个完整的 model 文件的架构:


export default {

  namespace: 'example',

  state: {},

  subscriptions: {
    setup({ dispatch, history }) {  // eslint-disable-line
    },
  },

  effects: {
    *fetch({ payload }, { call, put }) {  // eslint-disable-line
      yield put({ type: 'save' });
    },
  },

  reducers: {
    save(state, action) {
      return { ...state, ...action.payload };
    },
  },

};

好了。马上进入我们的 Model 文件结构专题。

四、Model 包下文件架构(重点)

首先要在index.js中引入,一般来说命名空间名和文件名相同 app.model(require('./models/命名空间名').default);

import { getCinemaListService } from "../services/maizuo"

export default {

    namespace: 'maizuo',
  
    state: {
        isShow:true,
        list:[]
    },

    reducers:{
        hide(prevState,action){
            console.log(222, action); //{type: 'maizuo/hide', payload: xxx}
            return {...prevState,isShow:false}
        },
        show(prevState,action){
            return {...prevState,isShow:true}
        },
        changeCinemaList(prevState,{payload}){
            return {...prevState,list:payload}
        }
    },

    subscriptions: {
        setup({ dispatch, history }) {  // eslint-disable-line
            console.log("初始化")
        }
    },

    //异步- redux-saga
    effects:{
        *getCinemaList(action,{call,put}){
            // console.log(obj)
            var res = yield call(getCinemaListService)
            console.log(res.data.data.cinemas)
            yield put({
                type:"changeCinemaList",
                payload:res.data.data.cinemas
            })
        }
    }
}

1、namespace

官网解释:当前 Model 的名称。整个应用的 State是由多个 Model 的 State 以 namespace 为 key 合成的。

官网解释很绕,通俗的说:就是给每个 Model 一个命名用作 key 的标识。一般命名方式等于 Model js 的 文件名,举个例子:

Model 文件 maizuo.js 里 namespace 值可以设为:maizuo

2、state

该 Model 当前的状态。每个 Model 的数据都保存在这里,这里的数据通过 Route 视图层的 this.props,直接显示在页面上。

这里的数据一改变,页面上的数据也将自动改变。

3、reducers

用来处理同步操作。如果不需要调接口时候,我们前台传递的 action可以直接调用 reducers 里的方法。

上面说的同步是同步什么呢?同步该 Model 里面的 state 的数据。

打开项目中 models/example.js。找到 reducers,我将他复制在下方,来讲解一下怎么创建 reducers 下的方法。

  reducers:{
        hide(prevState,action){
            console.log(222, action); //{type: 'maizuo/hide', payload: xxx}
            return {...prevState,isShow:false}
        },
        show(prevState,action){
            return {...prevState,isShow:true}
        },
        changeCinemaList(prevState,{payload}){
            return {...prevState,list:payload}
        }
    },

(1)、save

  • hide:为一个普通方法的命名,可自行取名;
  • show:为一个普通方法的命名,可自行取名;

(2)、state

state:为当前 Model 下的所有 state 值,可以 console.log(state) 看一下输出就知道了。

(3)、action

action:当前台页面需要进行数据操作时,就会创建一个 action,action 存放了传递过来需要对当前 state 进行改变的数据。

(4)、payload

payload:就是 action 里传递过来的数据。可以 console.log(action.payload) 看一下输出就知道了。

(5)、return

return:返回的是新的 state。等于舍弃了旧的 state,重新 return 一个新的 state 作为当前 Model 的 state

一般情况下,我们要解开旧的 state,将它重新赋值给新的 state...state 为 ES6 语法。

将操作完成得数据累加到 return 中。

同名的数据会覆盖,所以不用担心旧的 state 值会影响到新设置的值。

不同名的数据会追加。

(6)、调用

import { connect } from 'dva'
import React, { Component } from 'react'

class Detail extends Component {
    componentDidMount() {
        console.log(`接受上个页面传来的id,利用此id取数据`,this.props.match.params.myid)

        // console.log(this.props)

        this.props.dispatch({
            type:"maizuo/hide", //type为对应模块的对应方法名
            payload: 111,
        })
    }

    componentWillUnmount(){
        this.props.dispatch({
            type:"maizuo/show"
        })
    }
    
    render() {
        return (
            <div>
                Detail
            </div>
        )
    }
}

export default connect()(Detail)

4、effects

用来处理异步操作。如果需要调取接口的话,前台页面就需要调用 effects 里的方法。

将数据取出来,在传递给 reducers 里的方法进行数据操作和同步 state

来看看例子:

import { querySomething } from '@/services/api';

*query({ payload }, { call, put, select }) {
    const data = yield call(querySomething, payload);
    console.log(data)
    yield put({
    type: 'save1',
    payload: { name: data.text },
    });
},

(1)、*

*:这个 * 符号,可能小伙伴们不熟悉,简单点,不管它,只要记住每个 effects 里方法前面都加上 * 即可。

稍微解释一下:

这表明它是一个异步函数,里面可以使用 yield 等待其他异步函数执行结果。

(2)、query

query:方法名,自定义命名。不多解释。

(3)、payload

payload:当前台页面需要进行数据操作时,就会创建一个 action,action 存放了传递过来需要对当前 state 进行改变的数据。

payload 就是存放在 action 里面的数据。可以 console.log(payload) 看输出的效果。

(4)、call

call:与后台服务端接口进行交互。

第一个传参:后台服务器接口对应的名称。第二个参数:入参。

同行的 data 为出参。可以 console.log(data) 看输出的效果。

(5)、put

put:用来发出事件,即 action。一般调用 reducers 下的方法进行同步数据。

type:该 Model 层里 reducers 下的方法名。

payload:参数的传递。

如此一来。我们就将服务端查出来的数据,传递给 reducers 进行同步数据的操作了。

(6)、select

select:如果我们需要调用到其他 Model 层里面的 state值。那么我们就需要用 select 来进行操作。

const homeName = yield select(state => state.home);

这样我们就可以取到名为 home 的 Model 层里面的 state 数据了。

5、subscription

Model 中的 subscription 相当于一个监听器,可以监听路由的变化、鼠标、键盘、服务器连接变化等。

这样再其中可以根据不同的变化作出相应的处理。

如果你只是入门级别的选手,其实不需要用到这个东西,我自己也没用过,放个例子你们看看。

subscriptions: {
    setup:({ dispatch, history }) {
        window.onresize = () => {   //这里表示的当浏览器的页面的大小变化时就会触发里面的dispatch方法,这里的save就是reducers中的方法名
        dispatch (type:"save")  
      }
    },
    onClick ({dispatch}) {
      document.addEventListener('click',() => {   //这里表示当鼠标点击时就会触发里面的dispatch命令,这里的save就是reducers中的方法名
        dispatch (type:"save")
    })
}

五、connect 连接 Model 和 Route 页面下的数据

  1. dva 有提供 connect 方法。只要在每个 Routes 页面导入下面的代码即可。
import { connect } from 'dva';

如果细心的小伙伴已经发现了,Routes 下的页面定义的都是状态组件,而不是用 class ~ extends React.Components,这样的好处是:组件不被实例化,整体渲染性得到了提升。

对于组件:

  1. 我们在最后导出时使用 connect 进行与 Models 的连接。
export default connect(({maizuo}) => ({maizuo}))(App);
或:
export default connect((state) => {
  //console.log(111, state);
  return {
    isShow: state.maizuo.isShow,
  };
})(App);

解释一下 maizuo:

maizuo 为 Model 层里面的 namespace。只要补上上面的代码就可以了。是不是很快~

  1. 从props中取出数据
import { connect } from "dva";
import React, { Component } from "react";
import Tabbar from "../components/Tabbar";

class App extends Component {
  componentDidMount() {
    // console.log(this.props)
  }

  render() {
    return (
      <div>
        {this.props.children}
        {this.props.isShow && <Tabbar />}
      </div>
    );
  }
}

export default connect((state) => {
  console.log(111, state);
  return {
    a: 1,
    isShow: state.maizuo.isShow,
  };
})(App);

六、初始化数据 和 Model 数据比对

1、初始化数据

这里指的是全局 index.js 里的 initialState

这里存放的是一个个以 model 下的 namespace 命名的对象;如果你随意命名的化页面是找不到你存放在这里的数据的。

2、Model -> state

这里也是用来存放数据对象的。

两者的对比是:initialState 的优先级会高于 model => state,默认是 {},所以页面初始化时,读取到的数据是 initialState

七、数据显示和操作的流程

接下来我将用最简单的步骤从无到有的演示一遍 dva 的写法和数据传输的流向。

不要看有那么多的步骤,其实每一步都很简短,很简单。

1、编写 Route 页面

class 的写法就是 class ~ extends React.Component{}

这里我将用组件的形式演示一遍。

import React from 'react';

const Example = ({dispatch,全局 `index.js` 里你需要的参数对象名称}) => {
    return (<div></div>)
}

export default Example;

这就是一个最简单的页面。

2、编写 Model 层代码

export default {

  namespace: 'example',

  state: {},

  effects: {
    *fetch({ payload }, { call, put }) {  // eslint-disable-line
      yield put({ type: 'save',payload:data });
    },
  },

  reducers: {
    save(state, action) {
      return { ...state, ...action.payload };
    },
  },
};

也是最简单的 Model 的格式,有任何不懂得地方请直接参考第四大点

3、编写 初始化数据

在全局 index.js 里 修改下面这段代码:

const app = dva({
    initialState: {
        example: {
            name:'nameText'
        }
    }
})

app.model(require('./models/example').default);     //还要记得补上这句话。在 index.js 里载入它。

4、修改路由配置

import Count from './routes/Example';

<Route path="/example" exact component={Example} />

5、使用 connect 连接

在 Route -> example.js 页面上使用 connect

修改代码:

import { connect } from 'dva';

export default connect(({ example }) => ({ example }))(Example);

如此一来,在页面上通过 this.props 即可获取到 example 里得数据。

6、前台调用 Model 层方法

如果需要于后台交互,那么就需要将入参传递到后台的 Model 层进行服务器的交互。

这里距需要讲解一下 dispatch了。

dispatch:是一个用于触发 action(这里可以直接理解为:调用后台的 model 里的方法) 的函数。只是触发 Model 里的函数而已,并没有对数据进行操作。

可以类比为一个引路人。

来看一下前台怎么使用 dispatch 的。

const { dispatch } = this.props;    //在 dva 中,可以通过 `this.props` 直接取得 `dispatch`

dispatch ({
    type:'example/fetch',           //指定哪个 model 层里面的哪个方法
    payload:{name:'exampleNew'},    //需要传递到 model 层里面的参数。payload 为固定用法(我自己的理解)。
})

至此,我们就已经在页面上触发了 model 层里面的某个方法,并且把参数一起传递过去了。

type:如果你不需要调用异步的话可以直接 example/save 调用 reducer 下的方法。

7、数据在 Model 中的流向

下面这些文字若有任何不懂的地方请直接参考上面的内容。

如果你上一步是调用 异步(Effects) 里的方法的话

那么你可以 console.log(payload) 下,看看数据是否有传递过来。

如果需要调用 服务端接口就使用 const data = yield call(接口名,参数名);,然后 console.log(data) 看看数据有没有查询出来。

接着调用 yield put({ type:'save',payload:data }) 调用 将参数传递到 reducer 下的方法进行同步。

来到 reducers 的方法下,进行数据操作,操作完成后用 return 将数据返回给 state

八、稍复杂概念

1、多次调用

在一个 effect 中,可以使用多个 put 来分别调用 reducer 更新状态(state)。

在一个 effect 中,可以存在多个 call 操作。

2、多任务调度

  • 并行。若干任务之间不存在依赖关系,并且后续操作对他们的结果无依赖。
  • 竞争。只有一个完成,就进入下一个环节。
  • 子任务。若干任务,并行执行,全部做完之后,才能进入下一个环节。

(1)、并行

const [ result1,result2 ] = yield [
    call(service1,param1),
    call(service2,param2),
]

(2)、竞争

const { result1,result2 } = yield race({
    result1:call(service1,param1),
    result2:call(service2,param2),
})

(3)、子任务

可以直接参照 上一点 多次调用 的方法。

九、 Mock

mock/api.js:

export default{
    'GET /users' : {name:"kerwin",age:100,location:"dalian"},

    'POST /users/login' :(req,res)=>{
    //req:接收的数据,res:返回的数据
        console.log(req.body)

        if(req.body.username==="kerwin" && req.body.password==="123") {
            res.send({
                ok:1
            })
        }else{
            res.send({
                ok:0
            })
        }
        
    }
}

.roadhogrc.mock.js:

const mockobj = require("./mock/api");
export default {
  ...mockobj,
};

访问:

http://localhost:8000/users
或:
GET:
    componentDidMount() {
        request("/users").then(res=>{
            console.log(res.data)
        })
    } 
    
 POST: 
        request("/users/login",{
            method:"POST",
            body:JSON.stringify({
                username:this.username.current.value,
                password:this.password.current.value
            }),
            headers:{
                "Content-Type":"application/json"
            }
        }).then(res=>{
            console.log(res.data)

            if(res.data.ok){
                localStorage.setItem("token","dwadw23232")
                this.props.history.push("/center")
            }else{
                alert("用户名密码不匹配")
            }
        })
{

"name""kerwin",

"age"100,

"location""dalian"

}

Umi - 2.x

一、 构建项目

1、 node环境

node版本 >= 8.0.0

2、 全局安装umi

npm install -g umi

建议使用yarn安装

// 全局安装yarn
npm install -g yarn

// 使用yarn安装umi
yarn global add umi

3、 构建umi项目

mkdir myapp && cd myapp
yarn create umi

二、目录结构

├── config/
    ├── config.js                  // umi 配置,同 .umirc.js,二选一
├── dist/                          // 默认的 build 输出目录
├── mock/                          // mock 文件所在目录,基于 express
├── public/                        // 全局相对路径文件
└── src/                           // 源码目录,可选
    ├── assets/                    // 静态文件
    ├── components/                // 全局共用组件
    ├── layouts/index.js           // 约定式路由时的全局布局文件
    ├── models/                    // 全局models文件,存放全局共用数据store
    ├── pages/                     // 页面目录,里面的文件即路由
        ├── .umi/                  // dev 临时目录,需添加到 .gitignore
        ├── .umi-production/       // build 临时目录,会自动删除
        ├── index/                 // 首页模块
        ├── manager/               // 管理端模块
            ├── components/        // 管理端-局部公共组件
            ├── models/            // 管理端-局部models,存放manager的store
            ├── services/          // 管理端-局部services,存放manager的接口
            ├── index.js           // 业务组件index
            ├── page.js            // 业务组件page
            ├── _layout.js         // 局部入口文件
        ├── 404.js                 // 404 页面
    ├── services/                  // 全局services文件,存放全局公共接口
    ├── utils/                     // 全局工具类
    ├── global.css                 // 约定的全局样式文件,自动引入,也可以用 global.less
    ├── global.js                  // 约定的全局Js文件,自动引入,可以在这里加入 polyfill
    ├── app.js                     // 运行时配置文件
├── .umirc.js                      // umi 配置,同 config/config.js,二选一
├── .env                           // 环境变量
└── package.json

1、 配置文件

umi 允许在 .umirc.js 或 config/config.js (二选一,.umirc.js 优先)中进行配置,支持 ES6 语法。本文使用config/config.js

export default {
  base: '/web/',  //部署到非根目录时才需配置
  targets: { //配置浏览器最低版本,比如兼容ie11
   ie: 11
  },
  hash: true,  //开启打包文件的hash值后缀
  treeShaking: true, //去除那些引用的但却没有使用的代码
  plugins: [
    [
      'umi-plugin-react',
      {
        antd: true, //启用后自动配置 babel-plugin-import,实现antd的css按需加载
        dynamicImport: { //实现路由级的动态加载
          webpackChunkName: true //实现有意义的异步文件名,不用理解,true就对了
          loadingComponent: './components/PageLoading/index',//指定加载时的加载组件的路径
          level: 3, //指定按需加载的路由等级,1代表只按需加载1级路由,3代表按需加载1,2,3级路由
        },
        dva: {
          dynamicImport: true, //是否启用按需加载
          hmr: true //是否启用 dva 的 热更新
        },
        locale:{ //语言配置
            //default:'en-US', // 默认加载为英语
            default:'zh-CN', // 默认加载为中文
        }
        //通过 webpack 的 dll 插件预打包一份 dll 文件来达到二次启动提速的目的--开发环境下的提速
        dll: {
          exclude: [], //需要二次加载的
          include: ['dva', 'dva/router', 'dva/saga', 'dva/fetch', 'antd/es'] //不需要二次加载的
        },
        //约定式路由时才需引用,用于忽略指定文件夹中自动生成的路由
        routes: {
          exclude: [
            /components//,
            /model.(j|t)sx?$/,
            /components.(j|t)sx?$/,
            /service.(j|t)sx?$/,
            /models//,
            /services//
          ],
        },
      }
    ]
  ],
  //配置式路由时,路由文件由此引用(往下会讲到)
  routes: routes,
  //代理请求
  proxy: {
    "/api": {
      "target": "http://jsonplaceholder.typicode.com/",
      "changeOrigin": true,
      "pathRewrite": { "^/api" : "" }
    }
  },
  alias: {'@': resolve(__dirname, '../src'),} //别名,umirc.js为'src'
};

2、 pages

+ pages
  + manager
    + components/
    + models/
    + services/
    - index.js
    - _layout.js

业务目录中,标配局部公用组件components,局部共用数据models,局部共用接口services,_layout.js入口文件需要则引入。

三、 路由

1、约定式路由

  • umi约定,在pages目录下的.js/.jsx会自动生成路由,除开在配置文件plugins/routes中被exclude的目录或文件,文件的路径即路由。
  • src/layout/为全局layout,默认全局入口文件,配置式路由下无效。pages/下任何文件下的_layout.js即当前文件夹路由下的入口文件,必须先经过_layout.js才能进入当前文件夹路由

2、配置式路由

在配置文件 .umirc.(ts|js) 或者 config/config.(ts|js)中引入:

export default {
  routes: [
    {
      path: '/',
      component: '../layouts/index',
      routes: [
        { path: '/user', redirect: '/user/login' },//redirect,避免只渲染_layout.js
        { path: '/user/login', component: './user/login' },
        {
          path: '/manager', component: '../pages/management/_layout.js',
          routes: [
            { path: '/manager/system', component: '../pages/management/manager/system', Routes: ['./routes/PrivateRoute.js'] }
        }
      ],
    },
  ],
};

3、权限路由

路由通过Routes属性来实现权限路由,如上文:/manager/system路由。创建一个./routes/PrivateRoute.js 权限文件,props里会传入/manager/system的路由信息。

export default (props) => {
  return (
    <div>
      <div>PrivateRoute (routes/PrivateRoute.js)</div>
      { props.children }
    </div>
  );
}

4、跳转路由

link方式

import Link from 'umi/link';
  <Link to="/list">Go to list page</Link>

router方式

import router from 'umi/router';
  router.push({
    pathname: '/list',
    query: {
      a: 'b',
    },
  });

四、 models层

models/index.js,不能空文件。由于umi/dva是基于redux,redux-saga的数据流方案,dva的connect即redux的connect,dva的model即redux-saga简化版,更易用。

import * as services from '../services';
{
  namespace: 'system',   //models命名空间,需全局唯一
  state: {               //models存储的数据store
    dataList: []
  },                     
  reducers: {
    save(state, { payload }) {          //更新store,用新数据合并state的旧数据
      return { ...state, ...payload };
    }
  },
  effects: {
    * testFunc({ payload: params }, { call, put, select }) {   //dispatch请求的方法
      const { dataList } = yield select(state => state.system); //获取models中的state
      const { data } = yield call(services.testFunc, params);  //call,请求services里面的接口以及传参,可继续往后面加参数,跟JavaScript的call一样
      if (data && data.code == 0) {
        const data_ = data.data.content;
        yield put({ //put,必须发出action save,此action被reducer监听,从而达到更新state数据的目的
          type: 'save',                                        
          payload: {
            dataList: data_ || []
          }
        });
        return data_;   //返回response,可选
      }                                                        
    },
  },
  subscriptions: {      //订阅,在app.start()即启动项目时被执行
    setup({ dispatch, history }) {
      return history.listen(({ pathname, query }) => {
        // 进入 '/manager/system' 路由,会发起一个名叫 'save' 的 effect
        if (pathname === '/manager/system') {
                    //do sth... dispatch({ type: 'save', payload: query });
                }
      })
    }
  }
}

注:call会阻塞,在call方法调用结束之前,call方法之后的语句是无法执行的,使得以同步的方式执行异步,如需无阻塞,则要引入fork,用yield fork代替yield call。

五、 services层

请求后台接口的方法,返回一个promise对象。

import request from '@utils/request';

export function rightSave(values) {
  return request(`/authirty/right/save`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify(values)
  });
}

六、components层

1、dispatch

与effect中的put方法相似,必须触发action。 在model的subscription参数中使用。 在组件中,由connect过的组件中,组件的props中才可以获取到dispatch。

import { connect } from 'dva'; 
export default connect()(System);

接口请求 方式一(推荐): 使用dispatch调用models里面effects/reducer声明的方法

this.props.dispatch({
  type: 'system/testFunc',   //type,命名空间/effects方法名
  payload: params,           //payload,参数
}).then(res => {})

//直接赋值更新state
this.props.dispatch({
  type: 'system/save',  //type,命名空间/reducer方法名
  payload: {
    rightDetail: {a:1}
  },                    //payload,参数
}).then(res => {})

//请求全局model
this.props.dispatch({
  type: 'global/getOrganizationList',  //全局namespace/全局effects方法名
  payload: params,                     //payload,参数
}).then(res => {})

方式二:dispatch带callback回调函数作为第三个参数

//组件中
this.props.dispatch({
  type: 'system/testFunc',
  payload: params,
  callback: (res) => {
    if (!!res) {
      //do sth
    }
  }
});

//model中
*testFunc({ payload, callback }, { call, put }){
  const { data } = yield call(services.rightSave, payload);
  if (data && data.code == 0) {
    !!callback && && callback(data);
  }
}

方式三(少用):如果组件中不需要用到model存store时,直接引入services请求接口

//组件中
//使用new promise请求接口,service返回promise函数,待接口异步请求完才执行.then
import * as service from '../services'; 
new Promise((resolve) => {
  const ret = service.rightSave(param);
  resolve(ret);
}).then((ret) => {
  if (ret && ret.code == 200) {
    //do sth
  }
});

// 或者
// 现在的ajax框架返回的就是一个promise的话,可以直接方法名跟上.then()即可
import { rightSave } from '../services';
rightSave().then(res => {
  let data = res.data;
  if (data && data.code == 200) {
    //do sth
  }
});

2、connect

方式一:mapStateToProps,class继承组件和函数组件都能用这种

接口请求完后,组件中引入connect,获取models的数据放入当前组件props中

import { connect } from 'dva'; 
function mapStateToProps(state) { //state是项目所有的models
  const { selectList } = state.system; //获取namespace命名空间为system的models数据state
  const { organizationList } = state.global; //全局获取namespace命名空间为global的models数据state
  return {
    selectList,
    organizationList
  };
}
export default connect(mapStateToProps)(System);   //connect组件
//或者直接解构
export default connect(({ system, global: { organizationList } }) => ({ ...system, organizationList }))(System);

方式二:es6注解方式引入,只能用于class继承组件

@connect(({ system, global: {organizationList} }) => ({ 
  ...system,
  organizationList
}))
class System extends React.Component{render(){return {<></>}}}

七、 withRouter

withRouter的作用: 未经路由跳转的组件,如子组件想拿到路由的相关信息location、history、match等时可用,经路由跳转的页面则默认已有路由信息。

FAQ: url 变化了,但页面组件不刷新,是什么原因? layouts/index.js 里如果用了 connect 传数据,需要用 umi/withRouter 高阶一下

import withRouter from 'umi/withRouter';
export default withRouter(connect(mapStateToProps)(LayoutComponent));

全局 layout 使用 connect 后路由切换后没有刷新? 需用 withRouter 包一下导出的 react 组件,注意顺序。

import withRouter from 'umi/withRouter';
export default withRouter(connect()(Layout));

八、 mock

export default {
  // 支持值为 Object 和 Array
  'GET /api/users': { users: [1, 2] },
  // 支持自定义函数,API 参考 express@4
  'POST /api/users/create': (req, res) => { res.end('OK'); },
}

如请求接口/api/list,数据返回{ users: [1, 2] }

Umi - 3.x

一、 构建项目

1、环境准备

首先得有 node,并确保 node 版本是 10.13 或以上。

推荐使用 yarn 管理 npm 依赖

本项目使用的版本为 node v14.17.5 yarn 1.22.15

2、脚手架

先找个地方建个空目录。

$ mkdir myapp && cd myapp

通过官方工具创建项目

yarn create @umijs/umi-app

安装依赖 yarn

启动项目 yarn start

3、官方文档

v3.umijs.org/zh-CN

二、目录结构

一个基础的 Umi 项目大致是这样的,

.
├── package.json //包含插件和插件集
├── .umirc.ts //配置文件,包含 umi 内置功能和插件的配置
├── .env //环境变量
├── dist //执行 `umi build` 后,产物默认会存放在这里
├── mock //存储 mock 文件,此目录下所有 js 和 ts 文件会被解析为 mock 文件
├── public //此目录下所有文件会被 copy 到输出路径
└── src
    ├── .umi //临时文件目录,比如入口文件、路由等,都会被临时生成到这里。**不要提交 .umi 目录到 git 仓库,他们会在 umi dev 和 umi build 时被删除并重新生成
    ├── layouts/index.tsx   //路由的全局布局文件,配置子路由,通常在需要为多个路径增加 layout 组件时使用
    ├── pages  //所有路由组件存放在这里
        ├── index.less
        └── index.tsx
    └── app.ts //运行时配置文件,可以在这里扩展运行时的能力,比如修改路由、修改 render 方法等

三、路由

1、配置路由

在配置文件中通过 routes 进行配置,格式为路由信息的数组。

比如:

export default {
  routes: [
    { exact: true, path: '/', component: 'index' },
    { exact: true, path: '/user', component: 'user' },
  ],
}

2、约定路由

  • 除配置式路由外,Umi 也支持约定式路由。约定式路由也叫文件路由,就是不需要手写配置,文件系统即路由,通过目录和文件及其命名分析出路由配置。
  • 如果没有 routes 配置,Umi 会进入约定式路由模式,然后分析 src/pages 目录拿到路由配置。
  • umi 会根据 pages 目录自动生成路由配置,但需要注释.umirc.js中routes相关,否则自动配置不生效。

(1)、重定向

  • 约定路由没法在routes中进行重定向
//新建pages/index.js,该文件是根路径,在根路径中重定向到film路径

import React from 'react';
import {Redirect} from 'umi'

export default () => {
    return (
        <Redirect to="/film"/>
    );
}

(2)、嵌套路由

有时候不好用,重启一下

Umi 里约定目录下有 _layout.tsx 时会生成嵌套路由,以 _layout.tsx 为该目录的 layout。layout 文件需要返回一个 React 组件,并通过 props.children 渲染子组件。

比如以下目录结构,

.
└── pages
    └── users
        ├── _layout.tsx
        ├── index.tsx
        └── list.tsx

会生成路由,

[
  { exact: false, path: '/users', component: '@/pages/users/_layout',
    routes: [
      { exact: true, path: '/users', component: '@/pages/users/index' },
      { exact: true, path: '/users/list', component: '@/pages/users/list' },
    ]
  }
]

例:

Comingsoon组件与Nowplaying组件共享轮播图布局: Snipaste_2022-07-18_16-03-34.png 目录结构: Snipaste_2022-07-18_16-04-16.png _layout.tsx:

import React from 'react'
import {Redirect,useLocation} from 'umi'
export default function Film(props:any) {
    const location = useLocation()
    // console.log(location)
    // 二级重定向
    if(location.pathname==="/film" || location.pathname==="/film/"){
        return <Redirect to="/film/nowplaying"/>
    }
    return (
        <div>
            <div style={{height:'200px',background:"yellow"}}>大轮播</div>
            {props.children}
            {/* 在此处插入子组件 */}
        </div>
    )
}

Comingsoon.tsx:

export default function Comingsoon() {
  return <div>Comingsoon</div>;
}

Nowplaying.tsx:

export default function Nowplaying(props: any) {
  return <div>Nowplaying</div>;
}

(3)、全局 layout

有时候不好用,重启一下

约定 src/layouts/index.tsx 为全局路由。返回一个 React 组件,并通过 props.children 渲染子组件。

比如以下目录结构,

.
└── src
    ├── layouts
    │   └── index.tsx
    └── pages
        ├── index.tsx
        └── users.tsx

会生成路由,

[
  { exact: false, path: '/', component: '@/layouts/index',
    routes: [
      { exact: true, path: '/', component: '@/pages/index' },
      { exact: true, path: '/users', component: '@/pages/users' },
    ],
  },
]

为整个应用下方添加一个选项卡:

Snipaste_2022-07-18_16-27-49.png src/index.tsx:

import React from 'react'
import {NavLink} from 'umi'
import './index.less'
export default function IndexLayout(props:any) {
      //不需要选项卡的情况
    if(props.location.pathname==="/city" || props.location.pathname.includes("/detail")){
        return <div>{props.children}</div>
    }
  
    return (
        <div>
            {props.children}
        
            <ul>
                <li>
                    <NavLink to="/film" activeClassName="active">film</NavLink>
                </li>
                <li>
                    <NavLink to="/cinema" activeClassName="active">cinema</NavLink>
                </li>
                <li>
                    <NavLink to="/center" activeClassName="active">center</NavLink>
                </li>
            </ul>
        </div>
    )
}

(4)、动态路由

约定 [] 包裹的文件或文件夹为动态路由。

比如:

  • src/pages/users/[id].tsx 会成为 /users/:id
  • src/pages/users/[id]/settings.tsx 会成为 /users/:id/settings

举个完整的例子,比如以下文件结构,

.
  └── pages
    └── [post]
      ├── index.tsx
      └── comments.tsx
    └── users
      └── [id].tsx
    └── index.tsx

会生成路由配置,

[
  { exact: true, path: '/', component: '@/pages/index' },
  { exact: true, path: '/users/:id', component: '@/pages/users/[id]' },
  { exact: true, path: '/:post/', component: '@/pages/[post]/index' },
  {
    exact: true,
    path: '/:post/comments',
    component: '@/pages/[post]/comments',
  },
];

例:

pages/film/Nowplaying.tsx:

import { useEffect, useState } from 'react';
import { useHistory } from 'umi';
export default function Nowplaying(props: any) {
  const [list, setlist] = useState([]);
  const history = useHistory();
  useEffect(() => {
    fetch(
      'https://m.maizuo.com/gateway?cityId=110100&pageNum=1&pageSize=10&type=1&k=7383801',
      {
        headers: {
          'X-Client-Info':
            '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
          'X-Host': 'mall.film-ticket.film.list',
        },
      },
    )
      .then((res) => res.json())
      .then((res) => {
        console.log(res.data.films);
        setlist(res.data.films);
      });
  }, []);
  return (
    <div>
      {list.map((item: any) => (
        <li
          key={item.filmId}
          onClick={() => {
            // console.log(history)
            history.push(`/detail/${item.filmId}`);
          }}
        >
          {item.name}
        </li>
      ))}
    </div>
  );
}

pages/detail/[zgcid].tsx:

import React from 'react';
import { useParams } from 'umi';
interface IParams {
  zgcid: string;
}
export default function Detail(props: any) {
  //console.log(props); 可以看到从params获取参数
  const params = useParams<IParams>();
 // console.log(params.zgcid);
  return <div>{params.zgcid}</div>;
}

四、集成Dva

1、介绍

包含以下功能,

  • 内置 dva,默认版本是 ^2.6.0-beta.20,如果项目中有依赖,会优先使用项目中依赖的版本。
  • 约定式的 model 组织方式,不用手动注册 model
  • 文件名即 namespace,model 内如果没有声明 namespace,会以文件名作为 namespace
  • 内置 dva-loading,直接 connect loading 字段使用即可
  • 支持 immer,通过配置 immer 开启

2、约定式的 model 组织方式

符合以下规则的文件会被认为是 model 文件,

  • src/models 下的文件
  • src/pages 下,子目录中 models 目录下的文件
  • src/pages 下,所有 model.ts 文件(不区分任何字母大小写)

3、同步数据

src/models/CityModel:

export default {
    namespace:"city", //命名空间,
    state :{
        cityName:"北京",
        cityId:"110100"
    },

    reducers:{
        changeCity(prevState:any,action:any){
            console.log(action)

            return {
                ...prevState,
                cityName:action.payload.cityName,
                cityId:action.payload.cityId
            }
        }
    }
}

pages/Cinema.tsx:

import React from 'react';
import { NavBar } from 'antd-mobile';
import { SearchOutline } from 'antd-mobile-icons';
import { connect } from 'dva';

function Cinema(props: any) {
  // console.log(props)
  return (
    <div>
      <NavBar
        onBack={() => {
          props.history.push(`/city`);
        }}
        back={props.cityName}
        backArrow={false}
        right={<SearchOutline />}
      >
        标题
      </NavBar>
      Cinema
    </div>
  );
}

export default connect((state: any) => {
  // console.log(state);
  return {
    cityName: state.city.cityName,
  };
})(Cinema);

pages/City.tsx:

import { useEffect, useState } from 'react';
import { IndexBar, List } from 'antd-mobile';
import { useHistory } from 'umi';
import { connect } from 'dva';

function City(props: any) {
  const history = useHistory();
  const [list, setlist] = useState<any>([]);

  const filterCity = (cities: any) => {
    // console.log(cities);
    const letterArr: Array<string> = [];
    const newlist = [];
    for (var i = 65; i < 91; i++) {
      letterArr.push(String.fromCharCode(i));
    }
    for (var m in letterArr) {
      var cityitems: any = cities.filter(
        (item: any) =>
          item.pinyin.substring(0, 1).toUpperCase() === letterArr[m],
      );
      cityitems.length &&
        newlist.push({
          title: letterArr[m],
          items: cityitems,
        });
    }
    // console.log(newlist)
    return newlist;
  };

  useEffect(() => {
    fetch('https://m.maizuo.com/gateway?k=2145459', {
      headers: {
        'X-Client-Info':
          '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529","bc":"110100"}',
        'X-Host': 'mall.film-ticket.city.list',
      },
    })
      .then((res) => res.json())
      .then((res) => {
        setlist(filterCity(res.data.cities));
      });
  }, []);

  const changeCity = (item: any) => {
    console.log(item.name, item.cityId);

    // 修改store state中的状态
    props.dispatch({
      type: 'city/changeCity', //注意要带着命名空间
      payload: {
        cityName: item.name,
        cityId: item.cityId,
      },
    });
    history.push('/cinema');
  };
  return (
    <div style={{ height: window.innerHeight }}>
      <IndexBar>
        {list.map((item: any) => {
          const { title, items } = item;
          return (
            <IndexBar.Panel index={title} title={title} key={title}>
              <List>
                {items.map((item: any, index: number) => (
                  <List.Item key={index} onClick={() => changeCity(item)}>
                    {item.name}
                  </List.Item>
                ))}
              </List>
            </IndexBar.Panel>
          );
        })}
      </IndexBar>
    </div>
  );
}

export default connect(() => ({}))(City);

4、异步数据

src/models/CinemaModel:


export default {
    namespace: "cinema",
    state: {
        list: []
    },

    reducers:{
        clearList(prevState:any,action:any){
            return {
                ...prevState,
                list:[]
            }
        },
        changeList(prevState:any,action:any){
            return {
                ...prevState,
                list:action.payload
            }
        }
    },
    
    effects: {
        *getList(action: any, obj: any):any {
            // console.log("getList",action,obj)
            const { put, call } = obj
            var res = yield call(getListForCinema,action.payload.cityId)
            // console.log(res)

            yield put({
                type:"changeList",
                payload:res
            })
        }
    }
}


async function getListForCinema(cityId:any) {
    // console.log(cityId)
    var res = await fetch(`https://m.maizuo.com/gateway?cityId=${cityId}&ticketFlag=1&k=6412143`, {
        headers: {
            'X-Client-Info': '{"a":"3000","ch":"1002","v":"5.2.0","e":"16395416565231270166529"}',
            'X-Host': 'mall.film-ticket.cinema.list'
        }
    }).then(res=>res.json())

    // console.log(res)
    return res.data.cinemas
}

pages/Cinema.tsx:

import { useEffect } from 'react';
import { NavBar, DotLoading } from 'antd-mobile';
import { SearchOutline } from 'antd-mobile-icons';
import { connect } from 'dva';

function Cinema(props: any) {
  // console.log(props)
  useEffect(() => {
    if (props.list.length === 0) {
      //取数据
      props.dispatch({
        type: 'cinema/getList',
        payload: {
          cityId: props.cityId,
        },
      });
    } else {
      console.log('缓存');
    }
  }, []);
  return (
    <div>
      <NavBar
        onBack={() => {
          //清空cinema list,
          props.dispatch({
            type: 'cinema/clearList',
          });

          props.history.push(`/city`);
        }}
        back={props.cityName}
        backArrow={false}
        right={<SearchOutline />}
      >
        标题
      </NavBar>

      {props.loading && (
        <div style={{ fontSize: 14, textAlign: 'center' }}>
          <DotLoading />
        </div>
      )}
      <ul>
        {props.list.map((item: any) => (
          <li key={item.cinemaId}>{item.name}</li>
        ))}
      </ul>
    </div>
  );
}

export default connect((state: any) => {
  console.log('state', state);

  return {
    loading: state.loading.global,
    cityName: state.city.cityName,
    cityId: state.city.cityId,
    list: state.cinema.list,
  };
})(Cinema);

5、加载中

dva-loading插件,直接 connect loading 字段使用即可。

在state.loading中有个global属性,具体用法查看上面代码,它会等待你的effect副作用函数结束自动变为false。

www.programminghunter.com/article/106…

Mobx

Snipaste_2022-07-19_10-51-08.png

「首先从左往右看,事件触发了 Actions,Actions 作为唯一修改 State 的方式,修改了 State,State 的修改更新了计算值 Computed,计算值的改变引起了 Reactions 的改变,导致了 UI 的改变,Reactions 可以经过事件调用 Actions。」

一、 Mobx介绍

「Mobx 是简单、可扩展的状态管理,React 和 MobX 是一对强力组合。React 通过提供机制把应用状态转换为可渲染组件树并对其进行渲染。而 MobX 提供机制来存储和更新应用状态供 React 使用。」

  1. Mobx是一个功能强大,上手非常容易的状态管理工具。
  2. Mobx背后的哲学很简单: 任何源自应用状态的东西都应该自动地获得。
  3. Mobx利用getter和setter来收集组件的数据依赖关系,从而在数据发生变化的时候精确知道哪些组件需要重绘,在界面的规模变大的时候,往往会有很多细粒度更新。

二、 Mobx与redux的区别

  • Mobx写法上更偏向于OOP
  • 对一份数据直接进行修改操作,不需要始终返回一个新的数据
  • 并非单一store,可以多store。
  • Redux默认以JavaScript原生对象形式存储数据,而Mobx使用可观察对象.

三、 Mobx的安装与相关依赖

npm i mobx@5

四、MobX 常用 API 介绍

1、 设置可观察数据(observable)

(1)、 (@)observable

observable 是一种让数据的变化可以被观察的方法,底层是通过把该属性转化成 getter / setter 来实现的。

observable 值可以是 JS基本数据类型、引用类型、普通对象、类实例、数组和映射。

(2)、observable 使用

  • 对于JS基本数据类型Number/String/Boolean), 使用observable.box()方法设置:
const num = observable.box(99)
const str = observable.box('leo')
const bool = observable.box(true)

// 获取原始值  get()
console.log(num.get(),str.get(),bool.get())   // 99 "leo" true

// 修改原始值  set(params)
num.set(100);
str.set('pingan');
bool.set(false);
console.log(num.get(),str.get(),bool.get())  // 100 "pingan" false
  • 对于数组对象类型,使用 observable() 方法设置:
const list = observable([1, 2, 4]);
list[2] = 3;
list.push(5) // 可以调用数组方法
console.log(list[0], list[1], list[2], list[3]) // 1 2 3 5

const obj = observable({a: '11', b: '22'})
console.log(obj.a, obj.b) // 11 22
obj.a = "leo";
console.log(obj.a, obj.b) // leo 22

需要注意的是:应该避免下标越界去访问数组中的值,这样的数据将不会被 MobX 所监视:

const list = observable([1, 2, 4]);
// 错误
console.log(list[9]) // undefined

因此在实际开发中,需要注意数组长度的判断。

  • 对于映射(Map)类型,使用 observable.map() 方法设置:
const map = observable.map({ key: "value"});
map.set("key", "new value");
console.log(map.has('key'))  // true
console.log(map.get('key'))  // value
map.delete("key");
console.log(map.has('key'))  // false

(3)、@observable 使用

MobX 也提供使用装饰器 @observable 来将其转换成可观察的,可以使用在实例的字段和属性上。

import {observable} from "mobx";

class Leo {
    @observable arr = [1];
    @observable obj = {};
    @observable map = new Map();
    @observable str = 'leo';
    @observable num = 100;
    @observable bool = false;
}
let leo = new Leo()
console.log(leo.arr[0]) // 1

相比于前面使用 observable.box() 方法对JS原始类型Number/String/Boolean)进行定义,装饰器 @observable 则可以直接定义这些类型。

原因是装饰器 @observable 更进一步封装了 observable.box()

2、(@)computed

计算值(computed values)是可以根据现有的状态或其它计算值进行组合计算的值。可以使实际可修改的状态尽可能的小。

此外计算值还是高度优化过的,所以尽可能的多使用它们。

可以简单理解为:它是相关状态变化时自动更新的值,可以将多个可观察数据合并成一个可观察数据,并且只有在被使用时才会自动更新

(1)、知识点:使用方式

  • 使用方式1:声明式创建
import {observable, computed} from "mobx";

class Money {
    @observable price = 0;
    @observable amount = 2;

    constructor(price = 1) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount;
    }
}
let m = new Money()

console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
  • 使用方式2:使用 decorate 引入
import {decorate, observable, computed} from "mobx";

class Money {
    price = 0;
    amount = 2;
    constructor(price = 1) {
        this.price = price;
    }

    get total() {
        return this.price * this.amount;
    }
}
decorate(Money, {
    price: observable,
    amount: observable,
    total: computed
})

let m = new Money()

console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
  • 使用方式3:使用 observable.object 创建

observable.objectextendObservable 都会自动将 getter 属性推导成计算属性,所以下面这样就足够了:

import {observable} from "mobx";

const Money = observable.object({
    price: 0,
    amount: 1,
    get total() {
        return this.price * this.amount
    }
})

console.log(Money.total) // 0
Money.price = 10;
console.log(Money.total) // 10
  • 注意点

如果任何影响计算值的值发生变化了,计算值将根据状态自动进行变化。

如果前一个计算中使用的数据没有更改,计算属性将不会重新运行。 如果某个其它计算属性或 reaction 未使用该计算属性,也不会重新运行。 在这种情况下,它将被暂停。

(2)、知识点:computed 的 setter

computedsetter 不能用来改变计算属性的值,而是用来它里面的成员,来使得 computed 发生变化。

这里我们使用 computed 的第一种声明方式为例,其他几种方式实现起来类似:

import {observable, computed} from "mobx";

class Money {
    @observable price = 0;
    @observable amount = 2;

    constructor(price = 1) {
        this.price = price;
    }

    @computed get total() {
        return this.price * this.amount;
    }

    set total(n){
        this.price = n + 1
    }
}

let m = new Money()

console.log(m.total) // 2
m.price = 10;
console.log(m.total) // 20
m.total = 6;
console.log(m.total) // 14

从上面实现方式可以看出,set total 方法中接收一个参数 n 作为 price 的新值,我们调用 m.total 后设置了新的 price,于是 m.total 的值也随之发生改变。

注意:
一定在 geeter 之后定义 setter,一些 typescript 版本会认为声明了两个名称相同的属性。

(3)、知识点:computed(expression) 函数

一般可以通过下面两种方法观察变化,并获取计算值:

  • 方法1: 将 computed 作为函数调用,在返回的对象使用 .get() 来获取计算的当前值。
  • 方法2: 使用 observe(callback) 来观察值的改变,其计算后的值在 .newValue 上。
import {observable, computed} from "mobx";

let leo = observable.box('hello');
let upperCaseName = computed(() => leo.get().toUpperCase())
let disposer = upperCaseName.observe(change => console.log(change.newValue))
leo.set('pingan')

更详细的 computed 参数可以查看文档:《Computed 选项》

(4)、知识点:错误处理

计算值在计算期间抛出异常,则此异常会被捕获,并在读取其值的时候抛出异常

抛出异常不会中断跟踪,所有计算值可以从异常中恢复。

import {observable, computed} from "mobx";
let x = observable.box(10)
let y = observable.box(2)
let div = computed(() => {
    if(y.get() === 0) throw new Error('y 为0了')
    return x.get() / y.get()
})

div.get() // 5
y.set(0)  // ok
div.get() // 报错,y 为0了

y.set(5)
div.get() // 恢复正常,返回 2

(5)、小结

用法:

  • computed(() => expression)
  • computed(() => expression, (newValue) => void)
  • computed(() => expression, options)
  • @computed({equals: compareFn}) get classProperty() { return expression; }
  • @computed get classProperty() { return expression; }

还有各种选项可以控制 computed 的行为。包括:

  • equals: (value, value) => boolean 用来重载默认检测规则的比较函数。 内置比较器有:comparer.identity, comparer.default, comparer.structural
  • requiresReaction: boolean 在重新计算衍生属性之前,等待追踪的 observables 值发生变化;
  • get: () => value) 重载计算属性的 getter
  • set: (value) => void 重载计算属性的 setter
  • keepAlive: boolean 设置为 true 以自动保持计算值活动,而不是在没有观察者时暂停;

3、 autorun

(1)、概念

autorun 直译就是自动运行的意思,那么我们要知道这两个问题:

  • 自动运行什么?

即:自动运行传入 autorun 的参数函数。

import { observable, autorun } from 'mobx'
class Store {
    @observable str = 'leo';
    @observable num = 123;
}

let store = new Store()
autorun(() => {
    console.log(`${store.str}--${store.num}`)
})
// leo--123

可以看出 autorun 自动被运行一次,并输出 leo--123 的值,显然这还不是自动运行。

  • 怎么触发自动运行?

当修改 autorun 中任意一个可观察数据即可触发自动运行。

// 紧接上部分代码

store.str = 'pingan'

// leo--123
// pingan--123

现在可以看到控制台输出这两个日志,证明 autorun 已经被执行两次。

(2)、知识点:观察 computed 的数据

import { observable, autorun } from 'mobx'
class Store {
    @observable str = 'leo';
    @observable num = 123;

    @computed get all(){
        return `${store.str}--${store.num}`
    }
}

let store = new Store()
autorun(() => {
    console.log(store.all)
})
store.str = 'pingan'

// leo--123
// pingan--123

可以看出,这样将 computed 的值在 autorun 中进行观察,也是可以达到一样的效果,这也是我们实际开发中常用到的。

(3)、知识点:computed 与 autorun 区别

相同点:

都是响应式调用的表达式;

不同点:

  • @computed 用于响应式的产生一个可以被其他 observer 使用的
  • autorun 不产生新的值,而是达到一个效果(如:打印日志,发起网络请求等命令式的副作用);
  • @computed中,如果一个计算值不再被观察了,MobX 可以自动地将其垃圾回收,而 autorun 中的值必须要手动清理才行。

(4)、小结

autorun 默认会执行一次,以获取哪些可观察数据被引用。

autorun 的作用是在可观察数据被修改之后自动去执行依赖可观察数据的行为,这个行为一直就是传入 autorun 的函数。

4、when

接收两个函数参数,第一个函数必须根据可观察数据来返回一个布尔值,当该布尔值为 true 时,才会去执行第二个函数,并且只会执行一次。

import { observable, when } from 'mobx'
class Leo {
    @observable str = 'leo';
    @observable num = 123;
    @observable bool = false;
}

let leo = new Leo()
when(() => leo.bool, () => {
    console.log('这是true')
})
leo.bool = true
// 这是true

可以看出当 leo.bool 设置成 true 以后,when 的第二个方法便执行了。

(1)、注意

  1. 第一个参数,必须是根据可观察数据来返回的布尔值,而不是普通变量的布尔值。
  2. 如果第一个参数默认值为 true,则 when 函数会默认执行一次。

5、 reaction

接收两个函数参数,第一个函数引用可观察数据,并返回一个可观察数据,作为第二个函数的参数。

reaction 第一次渲染的时候,会先执行一次第一个函数,这样 MobX 就会知道哪些可观察数据被引用了。随后在这些数据被修改的时候,执行第二个函数。

import { observable, reaction } from 'mobx'
class Leo {
    @observable str = 'leo';
    @observable num = 123;
    @observable bool = false;
}

let leo = new Leo()
reaction(() => [leo.str, leo.num], arr => {
    console.log(arr)
})
leo.str = 'pingan'
leo.num = 122
// ["pingan", 122]
// ["pingan", 122]

这里我们依次修改 leo.strleo.num 两个变量,会发现 reaction 方法被执行两次,在控制台输出两次结果 ["pingan", 122] ,因为可观察数据 strnum 分别被修改了一次。

实际使用场景:

当我们没有获取到数据的时候,没有必要去执行存缓存逻辑,当第一次获取到数据以后,就执行存缓存的逻辑。

6、 区别

  • computed 可以将多个可观察数据组合成一个可观察数据;
  • autorun 可以自动追踪所引用的可观察数据,并在数据发生变化时自动触发;
  • when 可以设置自动触发变化的时机,是 autorun 的一个变种情况;
  • reaction 可以通过分离可观察数据声明,以副作用的方式对 autorun 做出改进;

它们各有特点,互为补充,都能在合适场景中发挥重要作用。

7、 (@)action

在上一部分内容中,我们了解到,对可观察的数据做出反应的时候,需要我们手动修改可观察数据的值。这种修改是通过直接向变量赋值来实现的,虽然简单易懂,但是这样会带来一个较为严重的副作用,就是每次的修改都会触发 autorun 或者 reaction 运行一次。多数情况下,这种高频的触发是完全没有必要的。

比如用户对视图的一次点击操作需要很多修改 N 个状态变量,但是视图的更新只需要一次就够了。

为了优化这个问题, MobX 引入了 actionaction 是修改任何状态的行为,使用 action 的好处是能将多次修改可观察状态合并成一次,从而减少触发 autorun 或者 reaction 的次数。

可以理解成批量操作,即一次动作中包含多次修改可观察状态,此时只会在动作结束后,做一次性重新计算和反应。

action 也有两种使用方法,这里以 decorate 方式来介绍。

import { observable, computed, reaction, action} from 'mobx'

class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action bar(){
        this.string = 'pingan'
        this.number = 100
    }
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
    console.log(arr)
})
store.bar() // ["pingan", 100]

当我们连续去修改 store.stringstore.number 两个变量后,再运行 store.bar() 会发现,控制台值输出一次 ["pingan", 100] ,这就说明 reaction 只被执行一次。

(1)、知识点:action.bound

另外 action 还有一种特殊使用方法:action.bound,常常用来作为一个 callback 的方法参数,并且执行效果也是一样:

import { observable, computed, reaction, action} from 'mobx'

class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action.bound bar(){
        this.string = 'pingan'
        this.number = 100
    }
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
    console.log(arr)
})
let bar = store.bar;
function foo(fun){
    fun()
}
foo(bar) //["pingan", 100]

(2)、知识点:runInAction(name?, thunk)

runInAction 是个简单的工具函数,它接收代码块并在(异步的)动作中执行。这对于即时创建和执行动作非常有用,例如在异步过程中。runInAction(f)action(f)() 的语法糖。

import { observable, computed, reaction, action} from 'mobx'
class Store {
    @observable string = 'leo';
    @observable number = 123;
    @action.bound bar(){
        this.string = 'pingan'
        this.number = 100
    }
}
let store = new Store()
reaction(() => [store.string, store.number], arr => {
    console.log(arr)
})
runInAction(() => {
    store.string = 'pingan'
    store.number = 100
})//["pingan", 100]

五、Mobx的使用

1、 observable 和 autorun

import React, { Component } from "react";
import { observable, autorun } from "mobx";

//对于普通类型数据的监听,使用observable.box转化成可观察对象
var observableNumber = observable.box(10);
var observableName = observable.box("kerwin");

// autorun第一次执行, 之后每次被监听的值(observableNumber)发生改变也会执行
// 注意:只关心跟自己相关的值的改变,在这个autorun中observableName变化了不会执行
autorun(() => {
  console.log("number改变了", observableNumber.get());
});

autorun(() => {
  console.log("name改变了", observableName.get());
});

setTimeout(() => {
  observableNumber.set(20);
  // observableName.set("xiaoming")
}, 1000);

setTimeout(() => {
  // observableNumber.set(20)
  observableName.set("xiaoming");
}, 2000);

//观察对象,通过map
var myobj1 = observable.map({
  name: "kerwin",
  age: 100,
});

autorun(() => {
  // 第一次调用,每次myobj1的name属性发生变化就会被调用,age属性发生变化不会调用
  console.log("对象的name属性改变了", myobj1.get("name"));
});

setTimeout(() => {
  myobj1.set("name", "xiaoming");
}, 3000);

//观察对象,不通过map(推荐)
var myobj2 = observable({
  name: "kerwin",
  age: 100,
});

autorun(() => {
  console.log("对象的name属性改变了", myobj2.name);
});

setTimeout(() => {
  myobj2.name = "xiaoming";
}, 3000);

export default class App extends Component {
  render() {
    return <div>app</div>;
  }
}

实例:

src/mobx/store.js:

  //定义数据
import {observable} from 'mobx'

const store = observable( {
    isTabbarShow:true,
    list:[],
    cityName:"北京"
})

export default store

src/views/Detail.js:

import React, { useEffect } from "react";
import store from "../mobx/store";
export default function Detail(props) {
  useEffect(() => {
      //在此直接修改mobx/store中的数据
      //这样会导致一个问题:如果在很多页面对值进行操作,难以追踪
    store.isTabbarShow = false;
    return () => {
      store.isTabbarShow = true;
    };
  }, []);

  return <div>deteail</div>;
}

src/App.js:

import React, { Component } from "react";
import MRouter from "./router/IndexRouter";
import Tabbar from "./components/Tabbar";
import "./views/css/App.css";
import { autorun } from "mobx";
import store from "./mobx/store";
export default class App extends Component {

  state = {
    isShow: false,
  };

  componentDidMount() {
     //获取数据,根据数据状态判断是否展示<Tabbar></Tabbar>组件
     //监听函数
    autorun(() => {
    //一旦值发生改变就会立即调用
      console.log(store.isTabbarShow);

      this.setState({
        isShow: store.isTabbarShow,
      });
    });
  }

  render() {
    return (
      <div>
        {/* 其他的内容 */}
        <MRouter>{this.state.isShow && <Tabbar></Tabbar>}</MRouter>
      </div>
    );
  }
}

2、 action

Mobx提供的约定及模版代码很少,代码编写很自由,如果不做一些约定,比较容易导致团队代码风格不统一,而且在各个组件中直接对值进行修改,难以追踪代码位置。

(1)、解决方法:严格模式

使用configure开启严格模式

src/mobx/store.js:

import { observable, configure, action } from "mobx";

// 开启严格模式
configure({
  enforceActions: "always",
});

const store = observable(
  {
    isTabbarShow: true,
    list: [],
    cityName: "北京",
    changeShow() {
      this.isTabbarShow = true;
    },
    changeHide() {
      this.isTabbarShow = false;
    },
  },
  {
    changeHide: action,
    changeShow: action, //标记两个方法是action,专门修改可观测的value
  }
);

export default store;

src/views/Detail.js:

import React, { useEffect } from "react";
import store from "../mobx/store";

export default function Detail(props) {
  console.log(props.match.params.myid, "利用id去后端拿数据。");

  useEffect(() => {
  //不在组件中直接修改,而是调用action方法去修改store的值
    // store.isTabbarShow = false,
    store.changeHide();

    return () => {
      // store.isTabbarShow = true
      store.changeShow();
    };
  }, []);

  return <div>deteail</div>;
}

(2)、@装饰器

  1. 首先将vscode配置为支持装饰器的语法: Snipaste_2022-07-19_14-20-42.png
  2. 支持装饰器
npm i @babel/core @babel/plugin-proposal-decorators @babel/preset-env
  1. 在根目录新建.babelrc
{
    "presets": [
        "@babel/preset-env"
    ],
    "plugins": [
        [
            "@babel/plugin-proposal-decorators",
            {
                "legacy": true
            }
        ]
    ]
}
  1. 在根目录新建config-overrides.js
const path = require("path");
const { override, addDecoratorsLegacy } = require("customize-cra");

function resolve(dir) {
  return path.join(__dirname, dir);
}

const customize = () => (config, env) => {
  config.resolve.alias["@"] = resolve("src");
  if (env === "production") {
    config.externals = {
      react: "React",
      "react-dom": "ReactDOM",
    };
  }

  return config;
};

module.exports = override(addDecoratorsLegacy(), customize());
  1. 安装依赖
npm i customize-cra react-app-rewired
  1. 修改package.json
"scripts": {
    "start": "react-app-rewired start", 
    "build": "react-app-rewired build", 
    "test":  "react-app-rewired test", 
    "eject": "react-app-rewired eject"
  },
  1. 使用

src/mobx/store.js:

import { observable, configure, action } from "mobx";

// 开启严格模式
configure({
  enforceActions: "always",
});

// const store = observable( {
//     isTabbarShow:true,
//     list:[],
//     cityName:"北京",
//     changeShow(){
//         this.isTabbarShow = true
//     },
//     changeHide(){
//         this.isTabbarShow = false
//     }
// },{
//     changeHide:action,
//     changeShow:action //标记两个方法是action,专门修改可观测的value
// })

class Store {
  @observable isTabbarShow = true;
  @observable list = [];

  @action changeShow() {
    this.isTabbarShow = true;
  }

  @action changeHide() {
    this.isTabbarShow = false;
  }
}
const store = new Store();

export default store;

3、 runInAction(异步)

src/mobx/store.js:

import { observable, configure, action, runInAction } from "mobx";
import axios from "axios";

configure({
  enforceActions: "always",
});

class Store {
  @observable isTabbarShow = true;
  @observable list = [];

  @action changeShow() {
    this.isTabbarShow = true;
  }

  @action changeHide() {
    this.isTabbarShow = false;
  }

  @action async getList() {
    var list = await axios({
      url: "https://m.maizuo.com/gateway?cityId=110100&ticketFlag=1&k=7406159",
      method: "get",
      headers: {
        "X-Client-Info":
          '{"a":"3000","ch":"1002","v":"5.0.4","e":"16395416565231270166529","bc":"110100"}',

        "X-Host": "mall.film-ticket.cinema.list",
      },
    });
    // .then((res) => {
    //   runInAction(() => {
    //     this.list = res.data.data.cinemas;
    //   });
    // });
    // console.log("list", list);
    runInAction(() => {
      this.list = list.data.data.cinemas;
    });
  }
}
const store = new Store();

export default store;

src/Cinemas.js:

import { autorun } from "mobx";
import React, { useState, useEffect } from "react";
import store from "../mobx/store";
export default function Cinemas(props) {
  const [cinemaList, setCinemaList] = useState([]);

  useEffect(() => {
    // 进行异步请求
    if (store.list.length === 0) {
      store.getList();
    }
    //监听,初始时调用一次,一旦监听的数据发生变化就会再次调用
    var unsubscribe = autorun(() => {
      console.log(store.list, store.isTabbarShow);
      setCinemaList(store.list);
    });

    return () => {
      //记得取消订阅
      unsubscribe();
    };
  }, []);

  return (
    <div>
      {cinemaList.map((item) => (
        <dl key={item.cinemaId} style={{ padding: "10px" }}>
          <dt>{item.name}</dt>
          <dd style={{ fontSize: "12px", color: "gray" }}>{item.address}</dd>
        </dl>
      ))}
    </div>
  );
}

六、 mobx-react的使用

不在需要写监听函数了

1、安装

npm i mobx-react@5
yarn add mobx-react@5

2、类组件

index.js:

import React from "react";
import ReactDOM from "react-dom/client";
import App from "./04-router/App";

import { Provider } from "mobx-react";
import store from "./04-router/mobx/store";

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

App.js(这里可以是任何需要用到store的组件):

//引入store 
@inject("store") 
@observer
//直接从this.props中获取
import React, { Component } from "react";
import MRouter from "./router/IndexRouter";
import Tabbar from "./components/Tabbar";
import "./views/css/App.css";
import { inject, observer } from "mobx-react";

//构建一个 父组件 -高阶组件mobx-react
//引入store
@inject("store")
@observer
class App extends Component {
  // state = {
  //     isShow:false
  // }

  componentDidMount() {
    // autorun(()=>{
    //     console.log(store.isTabbarShow)
    //     this.setState({
    //         isShow:store.isTabbarShow
    //     })
    // })
    // console.log(this.props.store.isTabbarShow);
  }

  render() {
    return (
      <div>
        {/* 其他的内容 */}
        <MRouter>
         {/* {this.state.isShow && <Tabbar></Tabbar>} */}
        {this.props.store.isTabbarShow && <Tabbar></Tabbar>}
        </MRouter>
      </div>
    );
  }
}

export default App;

3、 函数组件

(1)、Mobx

src/Cinemas.js:

import { autorun } from "mobx";
import React, { useState, useEffect } from "react";
import store from "../mobx/store";
export default function Cinemas(props) {
  const [cinemaList, setCinemaList] = useState([]);

  useEffect(() => {
    // 进行异步请求
    if (store.list.length === 0) {
      store.getList();
    }
    //监听,初始时调用一次,一旦监听的数据发生变化就会再次调用
    var unsubscribe = autorun(() => {
      console.log(store.list, store.isTabbarShow);
      setCinemaList(store.list);
    });

    return () => {
      //记得取消订阅
      unsubscribe();
    };
  }, []);

  return (
    <div>
      {cinemaList.map((item) => (
        <dl key={item.cinemaId} style={{ padding: "10px" }}>
          <dt>{item.name}</dt>
          <dd style={{ fontSize: "12px", color: "gray" }}>{item.address}</dd>
        </dl>
      ))}
    </div>
  );
}

(2)、Mobx-react

src/Cinemas.js:

// import { autorun } from 'mobx'
import { Observer } from "mobx-react";
import React, { useEffect } from "react";
import store from "../mobx/store";

export default function Cinemas(props) {

  useEffect(() => {
    if (store.list.length === 0) {
      store.getList();
    }
    //不在需要订阅和取消订阅
  }, []);
   //使用<Observer></Observer>组件进行包裹并且用箭头函数返回
  return (
    <div>
      <Observer>
        {() => {
          return store.list.map((item) => (
            <dl key={item.cinemaId} style={{ padding: "10px" }}>
              <dt>{item.name}</dt>
              <dd style={{ fontSize: "12px", color: "gray" }}>
                {item.address}
              </dd>
            </dl>
          ));
        }}
      </Observer>
    </div>
  );
}

与类组件不同的是它的store仍然是从mobx文件下的store获取,而类组件的store是Provider提供的