问题背景

在进行前端开发时,除了完成UI界面之外,还要关注代码逻辑,更要关注数据流。数据是页面的灵魂,因此数据管理就变得很重要了。

页面动态化主要是依靠state中的数据动态变化,但当页面变得庞大的时候,state的变化往往牵一发动全身,因此需要纯净的UI组件,除了渲染逻辑,不杂糅其他(如网络请求),所有组件降级为无状态组件,仅仅依靠props渲染,专注UI渲染

(注:这里说的组件主要是指 page 下面的页面组件,对于 component 下的组件本身就应该是比较通用的组件,更应该仅仅依赖 props 渲染,它们也不应该有 model,数据应该通过在页面组件中通过 props 传递过去)。

本篇博客分享一下React项目中的Dvajs实践。

Dva概述

Dva没有提出新的框架,是在原本的React+Redux基础上进行封装,简化了开发流程,解决了直接使用 Redux 时的 “配置繁琐、样板代码多、工具链整合复杂” 等问题。

Redux要手动配置storeactionreducer,并且若要进行异步请求,要借助中间件redux-thunkredux-saga,代码分散在多个文件中(如 actions.jsreducers.jssagas.js)。

而Dva引入了Model的概念,将数据state、同步操作reducer,异步操作effects、订阅subscriptions,集中在一个model文件中,简化了模板代码

贴上一段官方的model模板代码

import { queryUsers, queryUser } from '../../services/user';

export default {
  state: {
    user: {},
  },

  effects: {
    *queryUser({ payload }, { call, put }) {
      const { data } = yield call(queryUser, payload);
      yield put({ type: 'queryUserSuccess', payload: data });
    },
  },

  reducers: {
    queryUserSuccess(state, { payload }) {
      return {
        ...state,
        user: payload,
      };
    },
  },

  test(state) {
    console.log('test');
    return state;
  },
};
  • Model 中的 state 对应 Redux 的 store 状态;
  • reducers 对应 Redux 的 reducer(纯函数,用于同步更新状态);
  • dva 内部会自动创建 Redux 的 store,并将所有 Model 的 reducers 合并为根 reducer。
  • Redux 本身仅支持同步状态更新,异步操作(如接口请求)需要依赖中间件。dva 内置了 redux-saga(处理复杂异步流程的中间件),并通过 Model 中的 effects 简化了 saga 的使用。

项目实践

项目用的是Taro框架,与umi不同的是,Taro并不内置dva支持,因此需要自己对dva进行封装。

基础依赖配置(package.json)

dva是基于redux的,因此也需要配置redux

{
  "dva-core": "*",
  "dva-loading": "^3.0.22",
  "react-redux": "^7.2.0",
  "redux": "^4.0.0"
}

Dva封装

引入工具 → 创建实例 → 配置插件 → 注册模型 → 启动应用 → 暴露接口

import { create } from 'dva-core'; //dva的核心组件
import createLoading from 'dva-loading'; // 用于监听和管理effects的异步操作

function createApp(opt){
  app = create(opt);
  app.use(createLoading({}));  // ✅ 配置 loading 插件
  if(!global.registered){ //每次只注册一次,防止重复注册导致的状态混乱
    opt.models.forEach(model => app.model(model));  // ✅ 注册所有 models
  }
  global.registered = true;
  app.start(); // 启动dva应用
  store = app._store;    
  // 将store、dispatch挂载到app上,减少对底层的依赖,符合“封装底层细节”
  app.getStore = () => store;
  dispatch = store.dispatch;
  app.dispatch = dispatch;
  return app;
}

这有个小细节:项目是热重载的,但由于在创建实例注册模型的时候加了一个判断,因此若在开发过程中添加了model,则需要重新运行项目,否则model不会生效。

应用入口配置

要在Taro中配置dva,肯定需要在应用程序初始化的时候进行挂载,因此要在src/app.js入口文件中进行配置。

import { Component } from 'react'
import { Provider } from 'react-redux'
import 'taro-ui/dist/style/index.scss'
import models from './models'
import dva from './utils/dva';
import './app.less'
import Taro from "@tarojs/taro";

const dvaApp = dva.createApp({
  onError(err) {
    // 在这里进行错误处理
    console.log(err);
  },
  initialState: {},
  models
});
const store = dvaApp.getStore();

class App extends Component {

  componentDidMount() {
    ...
  }
  componentDidShow() { }
  componentDidHide() { }
  componentDidCatchError() { }

  render() {
    return (
      <Provider store={store}>
        {/* 你的页面组件 */}
      </Provider>
    )
  }
}

export default App

展示一下models文件的内容:将项目中的所有model集中在这个文件中进行记录和管理,没有在这个文件中引用的model不会生效

import globalModel from "./global";

export default [
  globalModel,
];

至此前期配置已全部完成,下面给出项目中的使用示例

使用示例

组件与model连接

import {connect} from "react-redux";

// 单个 model 连接
@connect(({VerificationModel})=>({
  VerificationModel
}))

// 多个 model 连接
@connect(({WSAWmsWorkModel, WSAWorkOrderModel, WSAStatOfWorkerModel}) => ({
  WSAWmsWorkModel,
  WSAWorkOrderModel,
  WSAStatOfWorkerModel,
}))

组件中使用dispatch


this.props.dispatch({
  type: 'WSA_WmsWorkerOrderPoolModel/getPoolList',
})


this.props.dispatch({
  type: 'VerificationModel/save',
  payload: { username: 'newName' }
})

model中的异步数据流

// 1. 组件触发 action
this.props.dispatch({
  type: 'WSA_WmsWorkerOrderPoolModel/getPoolList',
})

// 2. Effect 处理异步请求
*getPoolList({payload},{call, put}){
  const response = yield call(PoolService.getWSAPoolList, payload);
  if(code===RESPONSE_OBJ.SUCCESS){
    yield put({
      type: 'save',
      payload: { poolList: data }
    })
  }
}

// 3. Reducer 更新 state
save(state, {payload}) {
  return {
    ...state,
    ...payload
  }
}

// 4. 组件自动重新渲染

跨model通信

在model的方法中调用其他方法通过put进行,若调用的是当前model的方法function1,则type:function1,若调用的是modelA的方法function2,则type:modelA/function1

// 在 VerificationModel 中调用其他 model
yield put({
  type: 'dispatchContainerModel/changeRole',
  payload: {
    roleName: 'worker',
    pagePath: 'pages/WSA_Index/index',
  }
})

知识点Tips

关于effect和reducer

我们可以注意到,在model中有两类函数,一种是effect,一种是reducer

effect主要处理复杂逻辑以及异步操作(网络请求等),在执行逻辑上面,调用effect函数会调用redux-saga中间件进行异步操作处理,最终通过put来调用reducer中的函数进行state更新。而reducer则直接调用函数。

在参数方面也可以观察到一些不同,在这里就不展开了,感兴趣的朋友可以查阅官网资料。

项目结构

只展示了本文中出现的文件的目录位置

.
├── app.js
├── src
│   ├── utils // 工具函数目录,存放一些公用的函数或工具类
│   │   ├── dva.js
│   ├── models
│   │   ├── global.js // 页面的全局数据存放在这
│   │   └── index.js //用来记录model,若没在里面注册,则不会生效,原因可见dva.js的代码
│   │   └── xxxmodel.js // 某页面对应的model
│   ├── pages
│   │   ├── index.less
│   │   └── index.js
│   ├── services // 封装网络请求
│   │   └── api.js
├── node_modules
│   └── .cache
│       ├── bundler-webpack
│       ├── mfsu
│       └── mfsu-deps

数据获取

Model中数据的更新也会引起页面的更新,那么如果我想在component中获取数据,或者在Model中获取数据,该如何写呢?

// 获取本model的数据
// src/models/WSA_WmsWorkOrderModel.js
*getWorkOrderList({payload,callback},{call, put, select}){
  // 获取本 model 的 state
  const screenTime = yield select(state => state.WSAWmsWorkOrderModel.screenTime);
  console.log(screenTime)

  // ...
}


// 获取其他 model 的 state
*getUserData({payload}, {call, put, select}){
  // 获取 VerificationModel 的 state
  const userInfo = yield select(state => state.VerificationModel.userId);
  
  // 获取 globalModel 的 state  
  const selectedIndex = yield select(state => state.globalModel.selectedIndex);
  
  // 获取多个 model 的 state
  const { VerificationModel, globalModel } = yield select(state => ({
    VerificationModel: state.VerificationModel,
    globalModel: state.globalModel
  }));
}

// 在组件中获取数据
class WSA_Workbench extends Component {
  render() {
    // 通过 this.props 访问连接的 model state
    const { WSAWmsWorkModel, WSAWorkOrderModel } = this.props;
    
    // 解构获取具体的数据
    const { workOrder } = WSAWmsWorkModel;
    const { statusCountArray } = WSAWorkOrderModel;
    
    return (
      <View>
        {/* 使用 state 数据 */}
        <Text>{workOrder.length} 个工单</Text>
      </View>
    );
  }
}

参考网址

dva (umijs.org)

Taro 如何与 Dva 集成?-JavaScript中文网-JavaScript教程资源分享门户

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐