umi+Dva学习总结

6,734 阅读18分钟

写在前面

距离我从20年7月份开始接触到ES6,以及umi技术栈到现在已经有大半年时间了。

一开始为了完成老师的项目有目的的去学着怎么用umi中的react+antd写出一个jsx页面,用dva去实现model处理数据流和逻辑,连MVVM架构是什么都是完全不清楚,当初只会跟着语雀上的项目依葫芦画瓢,dva实现的model是怎么绑定到jsx组件上的,组件又是怎么获取到model中的state的,这些东西完全不懂。

慢慢的经过了几个项目的磨练,让我对react有了新的认识,而且理解了为什么React+Redux可以实现MVVM模式,懂了各个model的数据是怎么来的,存在哪里,组件又是怎么去拿到这些数据的。但是对于dva还是有些云里雾里,只是知道怎么去用,但是要为什么要这么写,这么写的目的是什么呢,还是不知道,最搞笑的就是每次写一个model都是复制前面的model的代码再修改。

这段时间为了准备实习面试,疯狂的学习各个官方文档中的知识,各个社区中的相关帖子,看了很多关于dva的相关知识,终于让我看到了那么一丝曙光,也斗胆写篇文章用浅显的语言记录自己对这套技术栈里各个点的理解,算是总结,也算是对自己的一种考验吧,看看是不是真的理解了。这也是在掘金里写下的第一篇文章,以后迷茫的时候也可以回来看看。

如果有写的不好的地方,也欢迎各位大佬的指正,我也不敢保证自己的理解也一定正确 T _ T,也希望能在这里学到更多慢慢进步!

1. React

React是什么呢?看看官网对React的描述:

React是一个用于构建用户界面的JavaScript库。

React的特点:

声明式: React 使创建交互式 UI 变得轻而易举。为你应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。以声明式编写 UI,可以让你的代码更加可靠,且方便调试。

组件化:创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。组件逻辑使用 JavaScript 编写而非模板,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。

一次学习,随处编写:无论你现在正在使用什么技术栈,你都可以随时引入 React 来开发新特性,而不需要重写现有代码。

对于第三点,因为目前我只进行过PC端的web前端开发,也只接触到umi+dva这一套技术栈,所以对这个理解并不是十分深刻。

React是一个JavaScript库,它的核心作用在于能够让我们在js中可以创建最后要在页面上显示的dom节点,这个在react中被称为组件,最简单的组件比如

const element = <h1>Hello, world!</h1>;

这就可以被称为一个组件了,这种写法被称为jsx。这样写的好处是什么呢,想像一下假如现在有一个div里面要渲染十个h1标题,标题文字分别是1到10,如果用原始的html模板写,怎么写呢,是不是必须老老实实写10个h1,或者还有个办法,在html最后写个srcipt,div渲染好之后,选择到这个div,然后用for循环创建h1,再插入到div中循环10次。在jsx中会怎么写呢(umi)。

import React;

const Index = (props) => {
    const arr = [];
    for(let i = 1;i<=10;i++){
        arr.push(i)
    }
    ...

    return(
        <div>   
        {
            arr.map(item => (
                return(<h1>{item}</h1>)
            ))
        }
        </div>
    )
};

export default Index;

我觉的这个就是React最重要的思想,把html元素当成一个个类,只不过这个类可以用来渲染元素到页面上罢了,就相当于把 面向对象 的思想引入到了前端编程里,这真的太重要了。每一个大组件都是一个类,它会有自己的props,自己的state,当props和state发生改变时,react会根据实际使用情况来更新这个组件。

我们在把一个个的组件封装成类之后,实际上React就很明了了,它会有一部分代码用来负责元素渲染,也就是renturn里的jsx代码,然后会有一部分代码来处理组件内部的数据也就是props和state的逻辑,也就是这个类里其他代码。

插个嘴,刚开始学习React的时候都是用Class来写组件,不过看antd的代码看了比较多以后,发现用箭头函数的方法确实更加简单明了,代码可读性也更好。中间经过了一段class和箭头函数混着用的时期。能用箭头函数来替代类的话,更多的是因为React hook的出现吧。特别是useState,这个钩子就让没有this的箭头函数也可以设置state。其实除了useState和useEffect两个钩子其他的用的也好少,基本没用过==。

说了这么多,其实就是想表达,当可以用js代码来直接处理html元素时,和页面组件的交互,控制组件的更新都会更方便,用面向对象的思想来封装组件后,分离页面各个部分,各自隔离处理各自的内部逻辑也让代码可维护性,可读性都更强。

但是有个关键的一点前面没有说得很明白,怎么样写代码可以让组件实现动态局部更新呢,在React里的话,我的理解就是将state和props作为某个关键子组件的控制点。这个控制点有点抽象,举个例子,刚刚上面的代码里,arr这个数组就是一个关键点,如果你将arr作为Index的state的活,当arr更新的时候,Index就会根据arr的改变局部更新。

这里提一嘴==,像数组啊,对象啊这样的引用类型,我们可能认为添加一个元素删除一个元素修改一个元素就是更新arr了,实际上js中引用元素的改变,必须是内存地址的改变,也就是说它判断你这是不是一个新的引用元素只会看地址不会看值,所以将arr作为state控制组件的话,想要触发组件的更新必须每次都slice一份更新后的,并赋给arr。

这样的控制点还有很多啊,比如antd组件里的表格Table有一个属性dataSource就是表格上展示的数据,将state中的某个obj赋给这个dataSource的话,这个obj改变表格就就会重新渲染表格里的数据;再比如style里的display,可以这么写style = dp === 0 ? 'block' : 'none',就可以用dp的值来控制这个元素存在还是消失了,这个dp必须得是state或者props嗷。

如果单页应用复杂起来了,内部的state越来越多,处理逻辑也特别多的时候,react处理起来就难免有些力不从心了,因为在实际页面里,state实在太多太多了,引用Redux官网的一句话

这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。

所以Redux产生的动机就出现了, 为了有效的管理单页面上所有state的状态变化。

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。

2. Redux

其实大家可以看看Redux的官方文档,第一次看可能完全不知所云,但是可以过一遍,在以后的开发中不时的思索一下,过段时间可能就理解很多东西了。我原来看学长给我的umi技术栈文档的时候,开始也是举步维艰完全不知道在说啥,但是过了大半年又翻出来看了几遍,收获也挺多的。

Redux三大原则:

单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

State 是只读的: 唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

使用纯函数来执行修改: 为了描述 action 如何改变 state tree ,你需要编写 reducers。

这里面涉及到了蛮多新概念,慢慢一个个看。

大家想象一下,现在有一个应用,它有很多的页面,有页面当然就会有组件,有组件就要有state来控制,但是现在有的页面state也太多太复杂了,这个应用要是放着不管的话,各个页面在各自的代码里处理各自的state的话,不仅代码冗长,而且有可能这个页面的state会涉及到另一个页面的某个组件的属性怎么办呢。

所以这应用想了个办法,我建个仓库吧,这个仓库有很多房间,把所有的state塞到各自的房间里,哪个页面需要哪些state就从仓库这里拿,这就是三大原则里的单一数据源,所有的state都会放到一个store里面统一管理。

但是这样还不行啊,我仅仅是把state 这些数据抽离了出来,但是state的改变逻辑还没管呢,要是页面拿到个state想怎么改怎么改那怎么行,所以应用又想了个办法,它把所有房间都上了把锁,不允许直接对state进行修改,你只能按我规定的逻辑来改。这就是第二个原则,State是只读的,想要修改State只能触发action。action是什么后面细说。

好了,现在锁是锁上了,那想改怎么改呢。那就给你一把钥匙,你得用这把钥匙打开房间门才能修改state,这个钥匙就是reducers,这也就是Redux的第三个原则,使用纯函数来修改state。

通过这三大原则,Redux对所有state实行了单向数据流的控制:把所有state存到仓库里,state只出不进,只能由仓库流向页面,不能由页面来添加state进仓库。也就是说,应用一开始就在仓库里定义好了各个房间里的state,至于页面,只能拿着用,不能塞state到它的房间里去了。

现在大家看一下一个示例,转载自www.yuque.com/flying.ni/t…

示例背景是最常见的 Web 类示例之一: TodoList = Todo list + Add todo button

这是一个React构造的页面的图解,页面顶层组件叫App,下面有一个TodoList,和一个增加按钮:

1528436560812-2586a0b5-7a6a-4a07-895c-f822fa85d5de.png

再看使用redux之后的图解:

1528436134375-4c15f63d-72f1-4c73-94a6-55b220d2547c.png 发现区别了吗,App的state被抽离出来了,放到Store的一个房间里去了,这里没给这个房间起名字了因为就一个,这个房间的钥匙在下面reducers里,只有调用reducers里的函数才能修改state,怎么调用呢,就是页面里组件连向仓库的的蓝色方块Dispatch。

Dispatch接受一个参数,这个参数就是上文没有解释的action,action是一个对象,有两个属性第一个是type,对于要使用的reducers里的某个reducer名字,第二个参数就是要传过去的数据。

而connect是什么呢,可以先简单的认为是绑定组件和仓库里的一些state。

Redux的作用现在清晰了起来,它就是一个所有页面state的管理工具。它抽离了所有页面的state和state的修改逻辑,并控制数据只能单向流动。

但是现在又有问题来了,reducers现在里面还都是同步函数,但是我们知道,许多时候页面的数据是需要从后端获取的,那reducers里如果有异步函数呢,比如现在add如果是一个异步函数,它需要post数据到后端服务器再根据结果更新TodoList,那按照现在的处理逻辑,还没等后台返回结果,这个reducer就已经更新state了,那肯定会造成不必要的麻烦啊。这时候就需要引入redux-saga了!

3. Redux-Saga

前面提到了,saga实际上就是为了解决redux中存在的异步reducer问题的,前面没说,redux的dispatch向store发送action去触发reducer的这个过程是可以被拦截下来的,那自然就可以在这个过程里添加各种中间件,来实现各种自定义功能,而saga实际上就是实现了处理异步操作的功能。

Saga是怎么去实现的呢,sagas被实现为Generator函数,不了解的话可以看看阮一峰的ES6教程。每一个saga都是一个generator函数,它会yield对象到redux-saga中间件里,被yield的对象一般都是可以被中间件执行的指令,当中间件取得一个 yield 后的 Promise,middleware 会暂停 Saga,直到 Promise 完成。 了解过generator函数就知道,这个函数内部是可以分段执行的,但是需要用迭代器的next()方法控制,saga相当于实现了一个这样的功能,执行到第一个yield时,等待yield后面的Promise(一般是异步请求如http请求)resolve后,得到返回数据,再恢复saga的执行,也就是执行这个yield后面的代码,通常是请求reducer更新state这样。

我们看一下加入Redux-Saga后的图解:

1528436167824-7fa834ea-aa6c-4f9f-bab5-b8c5312bcf7e.png

我们给被saga yield的对象起个名字,就叫effect,再给房间加一个Effects箱子,把effect放进去里面。

4. Redux-Router

这个我也没看很多,我的理解的话就是一句话,当应用进入到特定的页面时,触发特定的行为。Dva里的subscription就是参考的Redux-Router。

比如现在有一个需求,登陆完成进入主界面也就是进入路由为/main的页面时,要求能立即触发一个action发送http请求获取用户数据展现在主页某个组件,这个功能就可以通过subscription来实现。

然后其他一些比如键盘操作啊之类的也可以触发action,subscription就是干这个的。

5. Dva

现在Redux,Redux—saga,Redux-Router或多或少都理了一遍,终于可以来说Dva了。

Dva是啥,你可以把它理解为Redux + Redux-Saga + Redux-Router。神说,我需要一个state的管理系统,于是Redux出现了;神说,我需要一个能出来异步操作的中间件,于是Redux-Saga出现了;神说,我还想处理一下路由改变引起的数据流,于是Redux-Router出现了;神说我全都要,于是Dva出现了。(写完我自己都笑了

我们回想一下,我们前面说了,存储state的大仓库store里面,有很多房间,每个房间都有对应的页面。这个房间里有state了,有reducer了,Redux-Saga给他添加了effect进去,Redux-Router为它添加了subscription进去,我们再完善一下房间。各个页面的房间总得有个自己名字把,而且两两之间肯定不能一样是不,不然怎么区分,所以我们添加个namespace属性进去,作为房间的唯一标识。最后,再给这些房间换个名字吧,老叫'房间'显得不专业,就叫model吧。

这就是Dva的整个框架了,Dva让原来的redux框架完善丰满了起来,加入saga和router后也能处理更加复杂的事件了。再来看一下使用Dva之后的图解:

1528436195004-cd3800f2-f13d-40ba-bb1f-4efba99cfe0d.png

再看看一个经典的model是什么样子的:

app.model({
  namespace: 'count',
  state: {
    record: 0,
    current: 0,
  },
  reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({
            type: 'add',
          });
        }
      });
    },
  },
  },
});

Dva官网的图解里说了这样一段话:

有了前面的三步铺垫, Dva 的出现也就水到渠成了, 正如 Dva 官网所言, Dva 是基于 React + Redux + Saga 的最佳实践沉淀, 做了 3 件很重要的事情, 大大提升了编码体验:

  1. 把 store 及 saga 统一为一个 model 的概念, 写在一个 js 文件里面

  2. 增加了一个 Subscriptions, 用于收集其他来源的 action.

  3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起

到现在我也终于能理解个大概意思了T_T.

6. Umi + Dva

umi是啥呢,学长给我的文档上是这样写的:

Umi是一个React应用框架,集成路由管理、前端插件系统、项目资源打包、语法编译等的功能为一体,帮助开发者快速创建一套完整体系的React项目。使用这个脚手架可以很方便的创建React项目。

对于目前的我来说,umi就是能让我很方便的使用 React + antd + Dva + 其他一些可以安装的js库 的一个脚手架,开发体验确实爽的飞起。

在umi中使用dva更加简单,完全0 api了,现在把上面的model放到umi中来写,然后绑定一下页面,看如何实现。

安装umi的过程就略了,官网有很详细的介绍。做好准备工作后,在pages文件夹下新建两个文件,一个叫index.jsx,一个叫model.js,为什么要叫model.ts呢,因为umi规定了只有在src/models/下的文件或者pages文件夹下的叫model.js(或者ts,才会被当成model。

在index.jsx里写下如下代码:

import React from 'react;

const Index = (props) => {

    return(
    <>
    ...
    </>
    )
}

export default Index;

这样就是一个最基础的页面编写了,它的state在哪呢,别急,我们来写model.js了。

const Model = {
  namespace: 'count',
  state: {
    record: 0,
    current: 0,
  },
  reducers: {
    add(state) {
      const newCurrent = state.current + 1;
      return { ...state,
        record: newCurrent > state.record ? newCurrent : state.record,
        current: newCurrent,
      };
    },
    minus(state) {
      return { ...state, current: state.current - 1};
    },
  },
  effects: {
    *add(action, { call, put }) {
      yield call(delay, 1000);
      yield put({ type: 'minus' });
    },
  },
  subscriptions: {
    keyboardWatcher({ dispatch }) {
      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
    },
    setup({ dispatch, history }) {
      history.listen(({ pathname }) => {
        if (pathname === '/') {
          dispatch({
            type: 'add',
          });
        }
      });
    },
  },
}

这样一个基础的model也写好了,怎么绑定到一起呢,再修改一下index:

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

const Index = (props) => {
    const { Count, dispatch } = props; 

    return(
    <>
    ...
    </>
    )
}

const mapStateToProps = (props) => {
    const { count } = props;
    return {
        Count: count,
    };
};

export default connect(mapStateToProps)(Index);

这样就绑定好了。当初我这里看了很久都没有看懂,直到过了一遍redux和dva的官方文档。看不懂是因为没有仓库这个概念,不知道count是哪里来的。前面说过了,所有的房间都在一个仓库里,自然,所有model里定义的state也都是存放在一起的,它们被放在一个全局store里面,一个model的state在这个全局store里的名字就是这个model的namespace。

去哪找这个store呢,我们引入了umi的connect方法,connect后面有两个括号,表示将数据和页面绑定到一起。第一个括号就是用来获取数据的,我们定义一个函数,将这个函数放到第一个括号里后,这个的props就是这个store了,你就可以从store里把count提取出来。如果你多定义几个model,你就会发现,所有model的namespace都可以到这个store里找到,代表每个model的state,所以这又解决了一个很重要的问题,两个页面之间的通信问题,如果两个页面都绑定了一个state,那这个state的数据就可以被他们共享。

第二个括号里就是放组件名的,组件名放到第二个括号里以后,你就可以在组件的props里找到第一个括号里那个函数返回的数据!在将这个数据作为组件渲染里的一些关键点,就实现了state的绑定。当然,分发任务的dispatch也可以在props里找到了!

读到这,大概也就知道MVVM是怎么实现的了。

最后再一遍umi中dva的数据流图吧:

pHTYrKJxQHPyJGAYOzMu.png

结语

参考了很多官方的文档,特别是dva官网的社区里的Dva图解,真的给了我很大的启发。以上都是一些个人的见解,说的不对的欢迎指正呀!

一路学习下来,真的感觉要弄明白一个东西怎么用的,一定一定要去了解一下它出现的原因,像ES6里的Promise,不就是为了解决回调地狱吗,所以知道这个是链式调用的。然后就是多看看优秀的代码吧,自己写的时候真的是一坨屎,啥也不会,后来看了其他研究生学长的代码,看了antd官方的模板代码,受益匪浅只能说。

继续学习,继续进步。