Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dva 学习总结 #40

Open
SunShinewyf opened this issue Mar 3, 2018 · 0 comments
Open

dva 学习总结 #40

SunShinewyf opened this issue Mar 3, 2018 · 0 comments

Comments

@SunShinewyf
Copy link
Owner

SunShinewyf commented Mar 3, 2018

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 数据流动的一张图,如下所示:

输入图片说明

如图所示:用户在浏览器中访问某个 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])

injectModel()

其实 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(监听->执行) 的工作形式。其中该函数传入的 resolve,reject 是 createPromiseMiddleware.js 这个文件生成的。之所以有这个文件,主要是提供一种机制,提供给某些需要 effect 返回 resolve, reject 等方法的场景。

  • subscriptions 分支调用了同级目录下 subscription.js 中的 run(), run() 的逻辑是把所有的 listener 遍历执行一遍并返回不同分类(是函数或不是函数)的集合。

start()

这个函数用来启动整个应用, 其中 dva 中的 start() 主要是根据传入的 container 容器来渲染页面,核心代码如下:

  if (container) {
      render(container, store, app, app._router);
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      return getProvider(store, this, this._router);
    }

如果传入的参数是 DomElement 或者 DomQueryString,那么直接启动应用,渲染页面,否则就返回一个 <Provider /> (React Component)
除此之外,dva-core 中的 start(), 则是将 dva 的所涉及的一些概念全部整合到一个 store 的对象中,并执行一些赋值操作,具体源码移步这里

create()

这是 dva-core 中唯一暴露的一个函数,里面包含了上面介绍的三个函数,并且还夹杂了一些其他的逻辑。比如插件的使用,关于插件,它的主要逻辑是放在了 dva-core/Plugin.js 这个文件里面,这个文件提供了一个插件管理类,提供了 apply(), get(), use() 成员方法,这个类主要对钩子函数进行了一些处理,并且限制了钩子函数的几个可选项:

const hooks = [
  'onError',
  'onStateChange',
  'onAction',
  'onHmr',
  'onReducer',
  'onEffect',
  'extraReducers',
  'extraEnhancers',
];

关于 hooks 的概念,可以移步这里进行查阅

总结

以上只是笔者在这几天的学习中总结的一些技术要点,由于时间比较仓促,所以有些地方可能总结得有点问题,如有错误,欢迎指正~

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant