• dva实用的学习笔记


    本文改自CSDN博主「黄大琪琪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

    原文链接:https://blog.csdn.net/weixin_38398698/article/details/93387757

    什么是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)

          

    • 输入url渲染对应的组件,该组件通过dispatch去出发action里面的函数,如果是同步的就去进入model的ruducer去修改state,如果是异步比如fetch获取数据就会被effect拦截通过server交互获取数据进而修改state,同样state通过connect将model、状态数据与组件相连

    简单快速的dva项目

    步骤:

    1. npm install dva-cli -g
    2. dva new dva-quickstart
    • 这会创建 dva-quickstart 目录,包含项目初始化目录和文件,并提供开发服务器、构建脚本、数据 mock 服务、代理服务器等功能。
    • npm start启动开发服务器

    使用 antd

    通过 npm 安装 antd 和 babel-plugin-import 。babel-plugin-import 是用来按需加载 antd 的脚本和样式的,详见 repo 。

    $ npm install antd babel-plugin-import --save
    

    编辑 .webpackrc,使 babel-plugin-import 插件生效。

    {
    +  "extraBabelPlugins": [
    +    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
    +  ]
    }

    注:dva-cli 基于 roadhog 实现 build 和 dev,更多 .webpackrc 的配置详见 roadhog#配置

    定义路由

    我们要写个应用来先显示产品列表。首先第一步是创建路由,路由可以想象成是组成应用的不同页面。

    新建 route component routes/Products.js,内容如下:

    import React from 'react';
    
    const Products = (props) => (
      <h2>List of Products</h2>
    );
    
    export default Products;

    添加路由信息到路由表,编辑 router.js :

    import Products from './routes/Products';
    ...
    <Route path="/products" exact component={Products} />

    然后在浏览器里打开 http://localhost:8000/#/products ,你应该能看到前面定义的 <h2> 标签。

    编写 UI Component

    随着应用的发展,你会需要在多个页面分享 UI 元素 (或在一个页面使用多次),在 dva 里你可以把这部分抽成 component 。

    我们来编写一个 ProductList component,这样就能在不同的地方显示产品列表了。

    新建 components/ProductList.js 文件:

    import React from 'react';
    import PropTypes from 'prop-types';
    import { Table, Popconfirm, Button } from 'antd';
    
    const ProductList = ({ onDelete, products }) => {
      const columns = [{
        title: 'Name',
        dataIndex: 'name',
      }, {
        title: 'Actions',
        render: (text, record) => {
          return (
            <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}>
              <Button>Delete</Button>
            </Popconfirm>
          );
        },
      }];
      return (
        <Table
          dataSource={products}
          columns={columns}
        />
      );
    };
    
    ProductList.propTypes = {
      onDelete: PropTypes.func.isRequired,
      products: PropTypes.array.isRequired,
    };
    
    export default ProductList;

    定义 Model

    完成 UI 后,现在开始处理数据和逻辑。

    dva 通过 model 的概念把一个领域的模型管理起来,包含同步更新 state 的 reducers,处理异步逻辑的 effects,订阅数据源的 subscriptions 。

    新建 model models/products.js :

    export default {
      namespace: 'products',
      state: [],
      reducers: {
        'delete'(state, { payload: id }) {
          return state.filter(item => item.id !== id);
        },
      },
    };

    这个 model 里:

    • namespace 表示在全局 state 上的 key
    • state 是初始值,在这里是空数组
    • reducers 等同于 redux 里的 reducer,接收 action,同步更新 state

    然后别忘记在 index.js 里载入他:

    // 3. Model
    app.model(require('./models/products').default);

    connect 起来

    到这里,我们已经单独完成了 model 和 component,那么他们如何串联起来呢?

    dva 提供了 connect 方法。如果你熟悉 redux,这个 connect 就是 react-redux 的 connect 。

    编辑 routes/Products.js,替换为以下内容:

    import React from 'react';
    import { connect } from 'dva';
    import ProductList from '../components/ProductList';
    
    const Products = ({ dispatch, products }) => {
      function handleDelete(id) {
        dispatch({
          type: 'products/delete',
          payload: id,
        });
      }
      return (
        <div>
          <h2>List of Products</h2>
          <ProductList onDelete={handleDelete} products={products} />
        </div>
      );
    };
    
    // export default Products;
    export default connect(({ products }) => ({
      products,
    }))(Products);

    我们还需要一些初始数据让这个应用 run 起来。编辑 index.js

    const app = dva({
       initialState: {
         products: [
           { name: 'dva', id: 1 },
           { name: 'antd', id: 2 },
         ],
       },
     });

    index.js(入口文件)

    app = dva(opts)

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

    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转化为browserHistory

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

    同样可以配置hooks以及注册其他插件

    import createLoading from 'dva-loading';
    ...
    app.use(createLoading(opts));
    app.model

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

    • namespace

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

    import { connect } from 'dva'
    ...
    export default connect(({app})=>{return {...app}})(Top)
    • state
      为状态值的初始值,优先级要低于app.dva({})
    const app = dva({
      initialState: { count: 1 },
    });
    app.model({
      namespace: 'count',
      state: 0,
    });
    • 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 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。

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

    纯函数的好处:将函数抽离出来,与业务不耦合,更有利于单元测试 无副作用(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 方法,用于取消数据订阅。

     

    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的动态扩展

    • 注意到dva中的每个model,实际上都是普通的JavaScript对象,可以利用object.assign进行覆盖使用
    • 通过工厂函数来生成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' } });
    • 可以借助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);
    }
  • 相关阅读:
    真不容易...终于我也有了个js的语法高亮
    持久层相关概念
    测试语法高亮显示
    BSTR简介和内部结构
    Debugging JavaScript in Your Applications
    Google Analytics 跟踪代码迁移手册
    我对事件驱动的理解
    imagettftext 可调整字间距输出
    投票机的实现及相关技术
    js光标定位到文本末尾
  • 原文地址:https://www.cnblogs.com/passkey/p/12918097.html
Copyright © 2020-2023  润新知