SunShinewyf/issue-blog

dva学习总结

Closed this issue · 0 comments

dva 初探

前言: 最近正在学习 dva ,整理出一些学习笔记,笔者默认阅读此文的读者有一定的react , redux , redux-saga 基础,如果没有,可先自行了解这些技术,本文不再赘述。

什么是 dva

dva是基于现有应用框架(redux+react-router+redux-saga等)封装的一个框架(不是库),基本上没有引入新概念,也没有创建新语法,对于熟悉前言中涉及的技术栈的童鞋来说会非常容易上手。详细介绍可移步dva介绍

为什么会有 dva

在处理复杂异步请求的业务中,一开始我们是使用 redux-thunk + async/await 结合使用,比如在异步登录的逻辑中,使用 redux-thunk 处理如下:

// action/auth.js

import request from 'axios';
import { loadUserData } from './user';

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

这种处理之后,组件调用的是dispatch(action creator),此时的 action 被赋予了太多的逻辑功能,不再是一个 pure action 。为了保持 action 的简洁性,继而引入 redux-saga ,它提供了一个 saga 文件用来存放异步逻辑,引入 redux-saga 之后,上面的验证用户登录逻辑就变成如下:

// sagas/index.js
import { take, call, put } from 'redux-saga/effects'
import Api from '...'

export function* login(user, pass) {
  try {
    const data = yield call(Api.authorize, user, pass)
    yield put({type: 'LOGIN_SUCCESS', data.uid})
  } catch(error) {
    yield put({type: 'LOGIN_ERROR', error})
  }
}

使用 redux-saga 之后,action 又回归其纯粹性。并且将异步操作全部抽离在 sagas 中一层进行处理,这样方便我们进行多种异步处理操作。
redux-saga 虽然在处理较为复杂的异步逻辑时提供了比较好的解决方案,但是当业务变复杂时,随着模块的逐渐增加,由于项目通常要分 reducer, action, saga, component 等等,所以项目中的文件个数也会变得很多,如下:

    + src
      + actions
        - user.js
        - detail.js
      + reducers
        - user.js
        - detail.js
      + sagas 
        - user.js
        - detail.js
      + components

这样在项目开发过程中,就需要不断地切换文件目录,大大影响开发效率。于是 dva 应运而生,dva 的主要解决的项目开发中的痛点:

  • reducer, saga, action 之间来回切换的开发成本
  • saga 创建麻烦
  • 主文件中的入口逻辑变得很复杂

上面的例子使用 dva 来实现如下:

// models/login.js

import Api from '...'

export default {
  namespace: 'login',
  state: {
    user: null
  },
  effects: {
	 *login(){
	   const data = yield call(Api.authorize, user, pass)
       yield put({type: 'LOGIN_SUCCESS', data.uid})
	 },	
  },
  reducers: {}
}

其中,reducers 可以看成是同步的请求逻辑,effects 可以看成是异步的请求逻辑,所有的逻辑都放在了 models 目录下的文件中,省去了文件之间的切换成本,让开发人员可以专注于业务逻辑。
具体可以参考支付宝前端应用架构的发展和选择

dva 的相关知识点

dva中只有5个 API,8个新的概念,其中所有的 API 如下:

  • app = dva(Opts) 创建应用,返回 dva 实例
  • app.use(Hooks) 配置 hooks 或者注册插件
  • app.model(ModelObject) 注册 model
  • app.router(Function) 注册路由表
  • app.start([HTMLElement], opts) 启动应用

具体的使用可以移步这里

8个概念如下所示:

  • State 表示应用的所有数据层,其中全局的 state 由所有 model的 state 组成
  • Action 表示应用的所有事件,包括同步的和异步的,格式如下:
{
  type: String,
  payload: Any?,
  error? Error,
}

调用的时候有如下两种方式:

  • dispatch(Action);
  • dispatch({ type: 'todos/add', payload: 'todo content' });
  • Model 用于将数据相关的逻辑进行聚合
    • Reducer 和 redux 中的 reducer 概念相同,接受 state,action 作为参数,返回新的state
    • Effect 用来处理异步逻辑,使用 generator实现
    • Subscription 表示订阅,用于订阅一个数据源,然后按需 dispatch action。
  • Router 路由的配置信息
  • RouteComponent 表示 Router 里匹配路径的 Component,通常会绑定 model 的数据

dva 的使用

如何基于 dva 开发一个项目,dva 的作者给出了一个一步步开发 dva 项目的教程, 笔者仿照该教程,并且基于 dva2.0, 做出了一个 demo,该 demo 类似于 dva中的范例,只是初步体验一下 dva 的开发。

深入 dva

借用描述 dva 数据流动的一张图,如下所示:

images

如图所示:用户在浏览器中访问某个 URL,由此渲染一个页面,该页面可能包含多个 Components, 当用户在页面进行操作的时候,由此 dispatch 某个 action,同步的 action 逻辑放在 Reducer 中,异步的 action 逻辑存放在 Effect 中。通过 model 中的数据处理,将新的 state 传入页面中,从而触发页面数据的更新。

dva 源码解读

这次的解读主要是针对 dva@2.1 和 dva-core@1.1。

首先是 dva 中的入口文件所暴露出来的方法,主要是const app = dva();这行代码的作用,返回一个 app实例。该方法如下:

export default function (opts = {}) {
  const history = opts.history || createHashHistory();  //history默认是HashHistory
  const createOpts = {
    initialReducer: {
      routing,
    },
    setupMiddlewares(middlewares) {
      return [
        routerMiddleware(history),
        ...middlewares,
      ];
    },
    setupApp(app) {
      app._history = patchHistory(history);
    },
  };

  const app = core.create(opts, createOpts);
  const oldAppStart = app.start;
  app.router = router;
  app.start = start;
  return app;
}
// 此处略去一些方法的定义

这个函数很简单,主要是调用了 dva-core 里面的 create 方法,并且返回了一个包含如下方法的 app 对像:

  var app = {
    _models: [(0, _prefixNamespace2.default)((0, _extends3.default)({}, dvaModel))],
    _store: null,
    _plugin: plugin,
    use: plugin.use.bind(plugin),
    model: model,
    start: start
  };

对 app 的初始化定义在 dva-core/lib/index.js 文件中。在这个文件中,实现了 app 对象的所有方法。接下来一个一个进行分析:

  • model()
    这个方法比较简单,只是将传进来的 model push 进 _models 这个属性中。这就意味着每次我们注册 model 时,只能单个进行传递,不能以数组的形式进行传递,例如:
app.model(Model1); app.model(Model2);
//而不是
app.model([Model1,Model2])

其实 app.model 在调用 app.start 之后会变成 injectModel(), 它的源码如下:

  function injectModel(createReducer, onError, unlisteners, m) {
    model(m);

    var store = app._store;
    if (m.reducers) {
      store.asyncReducers[m.namespace] = (0, _getReducer2.default)(m.reducers, m.state);
      store.replaceReducer(createReducer(store.asyncReducers));
    }
    if (m.effects) {
      store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect')));
    }
    if (m.subscriptions) {
      unlisteners[m.namespace] = (0, _subscription.run)(m.subscriptions, m, app, onError);
    }
  }

这个函数里面调用了上面的 model() ,除此之外,该函数还将 model 定义的 reducers,effects, subscriptions 进行分别处理。

  • reducers 分支 是调用 redux 的原生 api 对 model 中的 reducers 进行处理
  • effects 分支是调用 redux-saga 中的 sagaMiddleware.run() 来执行管理一部 action,在这之前,先调用了 app._getSaga()方法:
export default function getSaga(resolve, reject, effects, model, onError, onEffect) {
  return function *() {
    for (const key in effects) {
      if (Object.prototype.hasOwnProperty.call(effects, key)) {
        const watcher = getWatcher(resolve, reject, key, effects[key], model, onError, onEffect);
        const task = yield sagaEffects.fork(watcher);
        yield sagaEffects.fork(function *() {
          yield sagaEffects.take(`${model.namespace}/@@CANCEL_EFFECTS`);
          yield sagaEffects.cancel(task);
        });
      }
    }
  };
}

这个方法主要实现了 saga 那一套的watch/worker(监听->执行) 的工作形式。

  • subscriptions 分支调用了同级目录下 subscription.js 中的 run():