浅析dva

什么是dva

  • dva 首先是一个基于 redux 和 redux-saga 的数据流方案,而后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,因此也能够理解为一个轻量级的应用框架。
  • 学过React的童鞋都知道它的技术栈真的不少,因此每当你使用React的时候都须要引入不少的模块,那么dva就是把这些用到的模块集成在一块儿,造成必定的架构规范。把react经常须要咱们必须写的须要用到的引用、代码都集成在了一块儿,好比一些依赖、必写的一些ReactDOM.render、引入saga、redux控制台工具、provider包裹等都省去不写,大大提升咱们的开发效率
  • 增长了一个 Subscriptions, 用于收集其余来源的 action, eg: 键盘操做、滚动条、websocket、路由等

  • 在react-redux上开发的dva+在redux-saga基础上开发的dva-core+在webpack基础上开发的roadhog进行打包启动服务
  • 数据流向(基于redux,因此同react-redux)
    image
    • 输入url渲染对应的组件,该组件经过dispatch去出发action里面的函数,若是是同步的就去进入model的ruducer去修改state,若是是异步好比fetch获取数据就会被effect拦截经过server交互获取数据进而修改state,一样state经过connect将model、状态数据与组件相连

简单快速的dva项目

步骤:css

  1. npm install dva-cli -g
  2. dva new dva-quickstart
  • 目录结构
    在这里插入图片描述

src

在这里插入图片描述

index.js(入口文件)

在这里插入图片描述

app = dva(opts)

建立应用,返回 dva 实例。(注:dva 支持多实例)。node

const app = dva({
  history, // 指定给路由用的 history,默认是 hashHistory
  initialState,  // 指定初始数据,优先级高于 model 中的 state
  onError, // effect 执行错误或 subscription 经过 done 主动抛错时触发,可用于管理全局出错状态。
  onAction, // 在 action 被 dispatch 时触发
  onStateChange, // state 改变时触发,可用于同步 state 到 localStorage,服务器端等
  onReducer, // 封装 reducer 执行。好比借助 redux-undo 实现 redo/undo
  onEffect, // 封装 effect
  onHmr, // 热替换相关
  extraReducers, // 指定额外的 reducer,好比 redux-form 须要指定额外的 form reducer
  extraEnhancers, // 指定额外的 StoreEnhancer ,好比结合 redux-persist 的使用
});
复制代码

这里能够对如下的hook进行option配置 这里能够将hashhistory转化为browserHistoryreact

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
});
复制代码
app.use(hooks)

一样能够配置hooks以及注册其余插件webpack

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
复制代码
app.model

在普通的react-redux+redux-saga的项目中,咱们首先会建4个文件夹,分别是actions,reducer,saga,组件,还有获取请求数据的services文件夹,一样在入口文件那要引入不少中间件、provider、connect等去将这几个文件夹联系起来,在这里的model如下就将这些集成在了一块儿,大大减少了开发工做量。 web

在这里插入图片描述

  • namespace model 的命名空间,同时也是他在全局 state 上的属性,只能用字符串,不支持经过 . 的方式建立多层命名空间。至关于这个model的key 在组件里面,经过connect+这个key将想要引入的model加入数据库

    import { connect } from 'dva'
    复制代码

在这里插入图片描述

  • state 为状态值的初始值,优先级要低于app.dva({})
const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
});
复制代码

此时为1express

  • reducer Action 处理器,处理同步动做,用来算出最新的 State,同redux中的reducer dva对redux作了一层封装,它会把modal里面的 reducers函数, 进行一次key的遍历,每一个key为一个reducer,固然它加上命名空间,action type对应的reducer、effect
    在这里插入图片描述
  • effect Action 处理器,处理异步动做,基于 Redux-saga 实现。Effect 指的是反作用。根据函数式编程,计算之外的操做都属于 Effect,典型的就是 I/O 操做、数据库读写。以 key/value 格式定义 effect。用于处理异步操做和业务逻辑,不直接修改 state。由 action 触发,能够触发 action,能够和服务器交互,能够获取全局 state 的数据等等 经过generate yield以及saga里面的经常使用call、put、takeEvery、takeLatest、take
  • call 进行触发异步操做
  • put 至关于dispatch 触发reducer改变state
['setQuery']: [function*() {}, { type: 'takeEvery'}],
复制代码
- takeEvery监听action的每次变化执行(默认)
- takeLatest监听action最近一次的变化
- take监听一次action留着,后面执行动做
复制代码
  • 为何要把同步和异步的分开呢 须要注意的是 Reducer 必须是纯函数,因此一样的输入必然获得一样的输出,它们不该该产生任何反作用。而且,每一次的计算都应该使用immutable data,这种特性简单理解就是每次操做都是返回一个全新的数据(独立,纯净),因此热重载和时间旅行这些功能才可以使用。

Effect 被称为反作用,在咱们的应用中,最多见的就是异步操做。它来自于函数编程的概念,之因此叫反作用是由于它使得咱们的函数变得不纯,一样的输入不必定得到一样的输出。npm

dva 为了控制反作用的操做,底层引入了redux-sagas作异步流程控制,因为采用了generator的相关概念,因此将异步转成同步写法,从而将effects转为纯函数。至于为何咱们这么纠结于 纯函数,若是你想了解更多能够阅读Mostly adequate guide to FP,或者它的中文译本JS函数式编程指南。编程

纯函数的好处:将函数抽离出来,与业务不耦合json

更有利于单元测试
	无反作用(side-effect),不会修改做用域外的值,使代码好调试
	执行顺序不会对系统形成影响
	剥离出业务逻辑,好复用
复制代码
  • action跑哪去了? action在组件的dispath中触发,dva对redux作了一层封装,它会把modal里面的 reducers函数, 进行一次key的遍历,每一个key为一个reducer,固然它加上命名空间,action type对应的reducer、effect
const { dispatch } = this.props;
        dispatch({ 
            type: 'app/updateState' ,
            payload: {
                opacityTop: 'none',//控制top的透明度
                hiddenDivDisplay: 'none',//控制隐藏头部的display
                footerDisplay: 'none'//控制footer的display
            }
        });
复制代码
  • subscriptions 以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,而后根据须要 dispatch 相应的 action。在 app.start() 时被执行,数据源能够是当前的时间、服务器的 websocket 链接、keyboard 输入、geolocation 变化、history 路由变化等等。

    格式为 ({ dispatch, history }, done) => unlistenFunction。
      注意:若是要使用 app.unmodel(),subscription 必须返回 unlisten 方法,用于取消数据订阅。
    复制代码

在这里插入图片描述
在这里插入图片描述

app.router

在这里插入图片描述
直接将路由引入或在多页应用中只将组件引入

app.router(require('./router'));
app.router(() => <App />);
复制代码
app.start

启动应用。selector 可选,若是没有 selector 参数,会返回一个返回 JSX 元素的函数。 selector为根元素

app.start('#root');
复制代码

mock---.roadhogrc.mock.js

roadhog server 支持 mock 功能,相似 dora-plugin-proxy,在 .roadhogrc.mock.js 中进行配置,支持基于 require 动态分析的实时刷新,支持 ES6 语法,以及友好的出错提示。在配置文件进行一下(node语法)配置,就能够经过简单的fetch请求获取到数据。

.roadhogrc.mock.js
export default {
  // 支持值为 Object 和 Array
  'GET /api/users': { users: [1,2] },
 
  // GET POST 可省略
  '/api/users/1': { id: 1 },
 
  // 支持自定义函数,API 参考 express@4
  'POST /api/users/create': (req, res) => { res.end('OK'); },
 
  // Forward 到另外一个服务器
  'GET /assets/*': 'https://assets.online/',
 
  // Forward 到另外一个服务器,并指定子路径
  // 请求 /someDir/0.0.50/index.css 会被代理到 https://g.alicdn.com/tb-page/taobao-home, 实际返回 https://g.alicdn.com/tb-page/taobao-home/0.0.50/index.css
  'GET /someDir/(.*)': 'https://g.alicdn.com/tb-page/taobao-home',
};
复制代码

若为多接口应用,则在mock文件夹下利用mockjs进行数据模拟,再在配置文件里,进行文件遍历引入

mock->user.js
const qs = require('qs');
const mockjs = require('mockjs');  //导入mock.js的模块

const Random = mockjs.Random;  //导入mock.js的随机数

// 数据持久化   保存在global的全局变量中
let tableListData = {};

if (!global.tableListData) {
  const data = mockjs.mock({
    'data|100': [{
      'id|+1': 1,
      'name': () => {
        return Random.cname();
      },
      'mobile': /1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}/,
    }],
    page: {
      total: 100,
      current: 1,
    },
  });
  tableListData = data;
  global.tableListData = tableListData;
} else {
  tableListData = global.tableListData;
}

module.exports = {
  //post请求  /api/users/ 是拦截的地址   方法内部接受 request response对象
  'GET /users' (req, res) {
    setTimeout(() => {
      res.json({      //将请求json格式返回
        success: true,
        data,
        page: '123',
      });
    }, 200);
  },

.roadhogrc.mock.js

const mock = {}
require('fs').readdirSync(require('path').join(__dirname + '/mock')).forEach(function(file) {
    Object.assign(mock, require('./mock/' + file))
})
module.exports = mock
复制代码

.webpackrc

格式为 JSON,容许注释,布尔类型的配置项默认值均为 false,支持经过 webpack.config.js 以编码的方式进行配置,但不推荐,由于 roadhog 自己的 major 或 minor 升级可能会引发兼容问题。

在这里插入图片描述

  • entry:设置入口文件
  • disableCSSModules:设置是否css模块化
  • publicPath:
  • outputPublic:
  • extraBabelPlugins 配置额外的 babel plugin。babel plugin 只能添加,不容许覆盖和删除。好比,同时使用 antd, dva 时,一般须要这么配:
"extraBabelPlugins": [
      "transform-runtime",
      "dva-hmr",
      ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": "css" }]
    ]
复制代码
  • proxy 配置代理,详见 webpack-dev-server#proxy。若是要代理请求到其余服务器,能够这样配:
"proxy": {
  "/api": {
    "target": "http://jsonplaceholder.typicode.com/",
    "changeOrigin": true,
    "pathRewrite": { "^/api" : "" }
  }
}
复制代码
  • multipage 配置是否多页应用。多页应用会自动提取公共部分为 common.js 和 common.css 。

  • define 配置 webpack 的 DefinePlugin 插件,define 的值会自动作 JSON.stringify 处理。

  • env 针对特定的环境进行配置。server 的环境变量是 development,build 的环境变量是 production。防止生产环境冗余。

"extraBabelPlugins": ["transform-runtime"],
"env": {
  "development": {
    "extraBabelPlugins": ["dva-hmr"]
  }
}
复制代码
  • theme 配置主题,其实是配 less 的 modifyVars。支持 Object 和文件路径两种方式的配置。结合antd设置全局样式。
"theme": {
  "@primary-color": "#1DA57A"
}
/
"theme": "./node_modules/abc/theme-config.js"
复制代码

段位升级

dva/dynamic(懒加载)

在router.js中使用,动态加载model和component app: dva 实例,加载 models 时须要 models: 返回 Promise 数组的函数,Promise 返回 dva model component:返回 Promise 的函数,Promise 返回 React Component

在这里插入图片描述
在这里插入图片描述

css 模块化

在roadhog中引入他们本身封装的af-webpack,这里面用css-loader以及加上.webpackrc的配置对css进行模块化,将css结果js的一层封装,给classname后面加上随机的hash,使得classname不会冲突,若要全局的就加上:global便可

在这里插入图片描述

用model共享全局信息

若是当前应用中加载了不止一个model,在其中一个的effect里面作select操做,是能够获取另一个中的state的:

*foo(action, { select }) {
  const { a, b } = yield select();
}
复制代码

model的动态扩展

  1. 注意到dva中的每一个model,实际上都是普通的JavaScript对象,能够利用object.assign进行覆盖使用
  2. 经过工厂函数来生成model
function createModel(options) {
  const { namespace, param } = options;
  return {
    namespace: `demo${namespace}`,
    states: {},
    reducers: {},
    effects: {
      *foo() {
        // 这里能够根据param来肯定下面这个call的参数
        yield call()
      }
    }
  };
}

const modelA = createModel({ namespace: 'A', param: { type: 'A' } });
const modelB = createModel({ namespace: 'A', param: { type: 'B' } });
复制代码
  1. 能够借助dva社区的dva-model-extend库来作这件事

多任务调度

  • 任务的并行执行
const [result1, result2]  = yield all([
  call(service1, param1),
  call(service2, param2)
])
复制代码
  • 任务的竞争
const { data, timeout } = yield race({
  data: call(service, 'some data'),
  timeout: call(delay, 1000)
});

if (data)
  put({type: 'DATA_RECEIVED', data});
else
  put({type: 'TIMEOUT_ERROR'});
复制代码

跨model的通讯

若是这里是要在组件里面作某些事情,怎么办? 将resolve传给model

new Promise((resolve, reject) => {
  dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } });
})
.then((data) => {
  console.log(`after a long time, ${data} returns`);
});
复制代码

在model进行跨model通讯

try {
  const result = yield call(service1);
  yield put({ type: 'service1Success', payload: result });
  resolve(result);
}
catch (error) {
  yield put({ type: 'service1Fail', error });
  reject(ex);
}
复制代码

源码浅析

roadhog

roadhog主要是依赖于他们本身封装的af-webpack

获取webpackrc的配置以及校验

在getUserConfig的文件夹下,直接经过json内容去获取配置

在这里插入图片描述
在config下进行每一项的校验
在这里插入图片描述
在index,js中调用watch.js的方法去监听咱们的配置文件,而监听文件夹用的是chokidar这个包
在这里插入图片描述

css模块化

在这里插入图片描述
在这里插入图片描述
经过咱们的配置文件的配置,以及对环境对判断,动态给classname的后面加上hash值

dva

在这里插入图片描述
经过对其package.json的研究能够看出,这个下面只是对fetch、redux、router进行封装

dva/dynamic

在这里插入图片描述

  • 首先经过传入的model以及component利用promise.all进行加载,先判断是否有model,没有model就直接将component传出去,有model的话,就在此动态加载model(registerModel)
    在这里插入图片描述
    这里利用app.model进行注册,固然咱们也能够利用这个方法去扩展卸载的方法app.unmodel
  • 那组件传到哪了呢?
    在这里插入图片描述
    在这里看到其实咱们的dynamic里面还能够传一个参数就是默认加载的组件,为LoadingComponent,利用该默认组件的生命周期的加载,去控制咱们传入的component的设置,赋值到AsyncComponent,
  • 那为何有this.state.AsyncComponent = AsyncComponent;这种写法 防止渲染速度太快,致使默认组件尚未挂在上,直接渲染async组件
  • 当async为null的时候,就只渲染默认组件,从这能够看出component为必填选项
index.js

在这里插入图片描述
从这就能够看出在这里将provider、render等在此编写
在这里插入图片描述
而这些都是在app.start中完成的,而且router必须在start前注册
在这里插入图片描述
一样,在dva({})初始化的时候,将中间件注册并返回了一个app的对象
在这里插入图片描述
利用react-router-redux的routerReducer进行action的路由跳转,并将routing能够返回给组件使用

dva-core

在这里插入图片描述
经过package.json就知道这里是对redux-saga进行封装

model

在这里插入图片描述

  • 首先在checkModel中进行5个api的校验
  • 在prefixNamespace中对每一个model加上key的前缀,以即可以将其看成action的type去dispatch
    在这里插入图片描述
  • 在createStore中将中间件以及saga进行注册
    在这里插入图片描述
  • 在getSaga getReducer中将reducer、effect功能实现
    在这里插入图片描述
  • 在这里也是返回了一个app的对象实现start具体功能以及use
    在这里插入图片描述
    c从卸载的函数里面就能够看到他是把model也都存在一个store里面,经过dispatch去触发删除model
  • subscription
    在这里插入图片描述
    Object.prototype.hasOwnProperty.call(subs, key) 仍是使用原型方法判断 key 是否是 subs 的自有属性 若是是自由属性,那么拿到属性对应的值(是一个 function) 调用该 function,传入 dispatch 和 history 属性。history 就是通过 redux-router 强化过的 history,而 dispatch,也就是 prefixedDispatch(app._store.dispatch, model)
  • prefixedDispatch: 就是给dispatch方法加上namespace的前缀
    在这里插入图片描述
相关文章
相关标签/搜索