前端数据流方案Dva

一、Dva简单介绍

基于 reduxredux-saga 的数据流方案.

实际上通俗点的讲法,就是集成了react的一些库,包括reactreact-domreact-router-domconnected-react-routerreduxredux-saga

组件传值:
  • 父传子

  • 子传父

  • 兄弟组件传值(约定最小公约父节点)

一些容易混淆的基本概念的区别
  • create-react-app 内置了webpack配置的脚手架
  • roadhog 相当于可配置的create-react-app
  • umi = roadhog + 路由系统
  • dva 管理数据流工具

二、Dva数据流向

PPrerEAKbIoDZYr

数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明

基本结构
import dva from 'dva'

const app = dva({
  //指定给路由用的 history,默认是 hashHistory
  history, 
  //初始化Model中state状态,比Model优先高, 默认 {}
  initialState, 
  // 对应功能钩子函数 
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
})
// 执行插件
app.use() 
// 存储纯函数 修改数据的方法对象
app.model( required('..model/example.js').defaut ) // 
// 路由配置文件,可对路由表以JavaScript对象的形式去配置, 如不要路由可直接返回组件,
app.router( requried('../router').default)
// 挂载启动
app.start('element');


  1. model结构

    app.model({
      namespace: 'count',
      state: {
        current: 0,
      },
      reducers: { // getReducer
        add(state) {
          const newCurrent = state.current + 1;
          return { ...state,
            current: newCurrent,
          };
        },
      },
      effects: {  // react-saga 实现
        *add(action, { call, put }) {
          yield call(delay, 1000);
          yield put({ type: 'add' });
          // yield 的作用:xxxxxxxx
        },
      },
      subscriptions: {
        keyboardWatcher({ dispatch }) {
          key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
        },
      },
    });
    

    *reducer:纯函数,有固定输入输出,主要目的是修改自身state

    *effect:一些外部服务,请求

    *subscriptions:内部定义的函数都会被被执行 ,执行之后作为监听来处理事务

三、图解

图解一: React表示法

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

  • 多个Component之间发生交互,状态(数据)维护在Component的最小公约父节点上,即<App/>
图解二: Redux 表示法

1528436134375-4c15f63d-72f1-4c73-94a6-55b220d2547c

  • React 只负责页面渲染,而不负责页面逻辑

  • 我们把页面逻辑单独抽取出来,就是我们需要的reducer,加上页面的state数据,基本上就是store的框架了

  • 通过dispatch派发函数的过程是可以被拦截的,所以我们可以在中间加不同的Middleware实现自定义功能.比如:

  • 耦合度更低, 复用度更高, 扩展性更好

图解三: 加入Saga

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

  • 点击创建Todo的按钮, 发起一个type == addTodoaction
  • saga 拦截这个 action, 发起 http 请求, 如果请求成功, 则继续向 reducer发一个 type == addTodoSuccaction, 提示创建成功, 反之则发送 type == addTodoFailaction 即可
图解四: Dva表示法

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

  1. storesaga 统一为一个 model 的概念, 写在一个 js 文件里面
  2. 增加了一个 Subscriptions, 用于收集其他来源的action, eg: 键盘操作
  3. model 写法很简约, 类似于 DSL 或者 RoR, coding 快得飞起✈️
    1. 约定大于配置 ,比如: namespace 作为 key
    2. RoR一切皆为对象
    3. 设计的目的: 简化元素,降低难度,让你不用管他怎么实现的,我们按照默认的这个规则去写就可以的

四、源码解读

看源码 – 不同步骤做了什么事情, 简化版

https://github.com/dvajs/dva/blob/45dfa782c4e6f2854fe6e5c6f34fee1b0d6ef151/packages/dva/src/index.js

  1. model

    function model(model) {
        let prefixModel = prefixNamespace(model);
        app._models.push(prefixModel);
      }
    
    /**
     * 把reducer 对象的属性名 加上 `namespace`
     * @param {*} m
     */
    function prefixNamespace(m) {
      let reducers = m.reducers;
      m.reducer = Object.keys(reducers).reduce((memo, key) => {
        let newKey = `${m.namespace}/${key}`;
        memo[newKey] = reducers[key];
        return memo;
      }, {});
      return m;
    }
    
    
  2. router

     function router(routerConfig) {
        app._router = routerConfig;
      }
    
  3. start

    function start(container) {
        let reducers = getReducers(app);
        app._store = createStore(reducers);
        ReactDOM.render(
          <Provider store={app._store}>{app._router()}</Provider>,
          document.querySelector(container)
        );
      }
      
      //getReducers
    
    • getReducer
    /**
     * 将所有的model 的reducer 以namespace 为key 整合成新的reducers
     * {
     * namespace1:function(state,action){},
     * namespace2:function(state,action){}
     * }
     * @param {*} app
     */
    function getReducers(app) {
      let reducers = {}; //用来合并,会传递给combineReducers
      for (const model of app._models) {
        // 这里的state 是这个 model 对应的分状态
        reducers[model.namespace] = function (state = model.state || {}, action) {
          let model_reducers = model["reducers"] || {}; // 拿到一个model 的所有reducer
          let reducer = model_reducers[action.type]; // model_reducers['counter/add'] =>  model_reducers['add']
          if (reducer) {
            return reducer(state, action);
          }
          return state;
        };
      }
      
      return combineReducers(reducers);
    }
    
    • effects
    //rootSaga
    import createSagaMiddleware from "redux-saga";
    import * as sagaEffects from "redux-saga/effects";
    import { applyMiddleware } from "redux";
    
    let sagaMiddleware = createSagaMiddleware(); // 新建middleware
    app._store = createStore(reducers, applyMiddleware(sagaMiddleware)); //插入middleware
    function* rootSaga() {
      const { takeEvery } = sagaEffects;
      for (const model of app._models) {
        const effects = model.effects;
        for (const key in effects) {
          //遍历effects
          yield takeEvery(`${model.namespace}/${key}`, function* (action) {
            console.log("执行了saga");
            yield effects[key](action, sagaEffects);
          });
        }
      }
    }
    sagaMiddleware.run(rootSaga);
    
    • subscriptions
    // subscriptions 监听函数全部跑一遍
    for (const model of app._models) {
      if (model.subscriptions) {
        for (const key in model.subscriptions) {
          //遍历跑一遍 subscriptions
          let sub = model.subscriptions[key];
          sub({ history, dispatch: app._store.dispatch });
        }
      }
    }
    
  4. provider 和 connect

    参考 https://juejin.cn/post/6844903505191239694

    provider 组件将数据与视图联系了起来,生成 React 元素呈现给使用者

    //provider
    // 使用 querySelector 获得 dom
    if (isString(container)) {
      container = document.querySelector(container);
      invariant(
        container,
        `[app.start] container ${container} not found`,
      );
    }
    
    // 其他代码
    
    // 实例化 store
    oldAppStart.call(app); 
    const store = app._store;
    
    // export _getProvider for HMR
    // ref: https://github.com/dvajs/dva/issues/469
    app._getProvider = getProvider.bind(null, store, app);
    
    // If has container, render; else, return react component
    // 如果有真实的 dom 对象就把 react 拍进去
    if (container) {
      render(container, store, app, app._router);
      // 热加载在这里
      app._plugin.apply('onHmr')(render.bind(null, container, store, app));
    } else {
      // 否则就生成一个 react ,供外界调用
      return getProvider(store, this, this._router);
    }
      
     // 使用高阶组件包裹组件
    function getProvider(store, app, router) {
      return extraProps => (
        <Provider store={store}>
          { router({ app, history: app._history, ...extraProps }) }
        </Provider>
      );
    }
    
    // 真正的 react 在这里
    function render(container, store, app, router) {
      const ReactDOM = require('react-dom');  // eslint-disable-line
      ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
    }
    
    //connect
    export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
      return function wrapWithConnect(WrappedComponent) {
     class Connect extends Component {
       constructor(props, context) {
         // 从祖先Component处获得store
         this.store = props.store || context.store
         this.stateProps = computeStateProps(this.store, props)
         this.dispatchProps = computeDispatchProps(this.store, props)
         this.state = { storeState: null }
         // 对stateProps、dispatchProps、parentProps进行合并
         // 合并在一起得到nextState,作为props传给真正的Component
         this.updateState()
       }
       shouldComponentUpdate(nextProps, nextState) {
         // 进行判断,当数据发生改变时,Component重新渲染
         if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
           this.updateState(nextProps)
             return true
           }
         }
         componentDidMount() {
           // 改变Component的state
           this.store.subscribe(() = {
             this.setState({
               storeState: this.store.getState()
             })
           })
         }
         render() {
           // 生成包裹组件Connect
           return (
             <WrappedComponent {...this.nextState} />
           )
         }
       }
       Connect.contextTypes = {
         store: storeShape
       }
       return Connect;
     }
      }
    
### dva-loading

​```javascript
import createLoading from "dva-loading";

app.use(createLoading()); // 使用插件

五、hooks

https://dvajs.com/API.html#app-use-hooks

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
});
  1. onError((err, dispatch) => {})

effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态

注意:subscription 并没有加 try…catch,所以有错误时需通过第二个参数 done 主动抛错

例子:

app.model({
  subscriptions: {
    setup({ dispatch }, done) {
      done(e)
    },
  },
})
  1. onAction(fn | fn[])

在action被dispatch时触发,用于注册 redux 中间件。支持函数或函数数组格式

例如我们要通过 redux-logger 打印日志:

import createLogger from 'redux-logger';
const app = dva({
  onAction: createLogger(opts),
})
  1. onStateChange(fn)

state 改变时触发,可用于同步 state 到 localStorage,服务器端等

  1. onReducer(fn)

封装 reducer 执行,比如借助 redux-undo 实现 redo/undo :

import undoable from 'redux-undo';
const app = dva({
  onReducer: reducer => {
    return (state, action) => {
      const undoOpts = {};
      const newState = undoable(reducer, undoOpts)(state, action);
      // 由于 dva 同步了 routing 数据,所以需要把这部分还原
      return { ...newState, routing: newState.present.routing };
    },
  },
})
  1. onEffect(fn)

封装 effect 执行。比如 dva-loading 基于此实现了自动处理 loading 状态

6、 onHmr(fn)

热更新相关,目前用于 babel-plugin-dva-hmr

六、 补充

yeild 具体业务:如 提交报名/创建用户 --1.检查用户名、邮箱、手机号合法 2.存储如数据库

const [result1, result2]  = yield all([
  call(service1, param1),
  call(service2, param2)
])

yeild 与yeild* ,前者是并行,后者是顺序执行。(适用在all的情况下)

yield* 操作符来组合多个 Sagas,使得它们保持顺序。 这让你可以一种简单的程序风格来排列你的 宏观任务(macro-tasks)

function* playLevelOne() { ... }

function* playLevelTwo() { ... }

function* playLevelThree() { ... }

function* game() {
  const score1 = yield* playLevelOne()
  yield put(showScore(score1))

  const score2 = yield* playLevelTwo()
  yield put(showScore(score2))

  const score3 = yield* playLevelThree()
  yield put(showScore(score3))
}

注意,使用 yield* 将导致该 Javascript 运行环境 漫延 至整个序列。 由此产生的迭代器(来自 game())将 yield 所有来自于嵌套迭代器里的值。一个更强大的替代方案是使用更通用的中间件组合机制。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值