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学习总结 #39

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

dva学习总结 #39

SunShinewyf opened this issue Mar 3, 2018 · 0 comments

Comments

@SunShinewyf
Copy link
Owner

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():
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