• Ant Design Pro(React/dva/antd)


    Ant Design Pro 是一个企业级中后台前端/设计解决方案。本地环境需要安装 node 和 git,技术栈基于 ES2015+、React、dva、g2 和 antd。

    参考:https://dvajs.com/

    https://github.com/ant-design/ant-design-pro/blob/master/README.zh-CN.md

    https://pro.ant.design/docs/getting-started-cn

    1、预备知识

    1)Redux 是 JavaScript 状态容器,提供可预测化的状态管理;Redux 除了和 React 一起用外,还支持其它界面库。

    connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):连接 React 组件与 Redux store。

    [mapStateToProps(state, [ownProps]): stateProps] (Function): 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。

    • 函数将被调用两次。第一次是设置参数,第二次是组件与 Redux store 连接:connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)

    • connect 函数不会修改传入的 React 组件,返回的是一个新的已与 Redux store 连接的组件,而且你应该使用这个新组件。

    • mapStateToProps 函数接收整个 Redux store 的 state 作为 props,然后返回一个传入到组件 props 的对象。

    注入 dispatch 和 todos

    function mapStateToProps(state) {
      return { todos: state.todos }
    }
    export default connect(mapStateToProps)(TodoApp)
    
    // 注入 dispatch 和全局 state
    export default connect(state => state)(TodoApp)
    // 不要这样做!这会导致每次 action 都触发整个 TodoApp 重新渲染
    // 最好在多个组件上使用 connect(),每个组件只监听它所关联的部分 state

    Action 是把数据从应用(这里之所以不叫 view 是因为这些数据有可能是服务器响应,用户输入或其它非 view 的数据 )传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。

    Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作

    2)redux-saga 是一个 redux 中间件,意味着这个线程可以通过正常的 redux action 从主应用程序启动,暂停和取消,它能访问完整的 redux state,也可以 dispatch redux action。

    redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试。通过这样的方式,这些异步的流程看起来就像是标准同步的 Javascript 代码。

    effects: {
      *create({ payload: values }, { call, put }) {
        yield call(usersService.create, values);
        yield put({ type: 'reload' });
      },
      *reload(action, { put, select }) {
        const page = yield select(state => state.users.page);
        yield put({ type: 'fetch', payload: { page } });
      },
    }

    call(fn, ...args)

    创建一个 Effect 描述信息,用来命令 middleware 以参数 args 调用函数 fn 。

    • fn: Function - 一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数。
    • args: Array<any> - 传递给 fn 的参数数组。

    put(action)

    创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。

    select(selector, ...args)

    创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器。

    • selector: Function - 一个 (state, ...args) => args 的函数。它接受当前 state 和一些可选参数,并返回当前 Store state 上的一部分数据。

    2、dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。

    dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念。dva 帮你自动化了Redux 架构一些繁琐的步骤,比如redux store 的创建,中间件的配置,路由的初始化等等,只需写几行代码就可以实现上述步骤。 

    1)使用 antd

    通过 npm 安装 antd 和 babel-plugin-import ,babel-plugin-import 是用来按需加载 antd 的脚本和样式的;编辑 .webpackrc,使 babel-plugin-import 插件生效。

    // .webpackrc.js
    extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]]

    2)dva应用

    // src/index.js 入口js
    import dva from 'dva';
    import browserHistory from 'history/createBrowserHistory';
    import createLoading from 'dva-loading';
    
    // 1. Initialize
    const app = dva({
        history: browserHistory(),
    });
    // 2. Plugins
    app.use(createLoading());
    // 3. Model
    app.model(require('./models/global').default);
    app.model(require('./models/menu').default);
    // 4. Router
    app.router(require('./router').default);
    // 5. Start
    app.start('#root'); // 启动应用

    app = dva(opts)-》创建应用,返回 dva 实例。(注:dva 支持多实例)

    opts 包含:

    • history:指定给路由用的 history,默认是 hashHistory

    2)定义路由

    app.router(({ history, app }) => RouterConfig)

    注册路由表,推荐把路由信息抽成一个单独的文件,这样结合 babel-plugin-dva-hmr 可实现路由和组件的热加载(只更新页面修改的部分,不会刷新整个页面)。

    // .webpackrc.js
    env: {
      development: {
        extraBabelPlugins: ['dva-hmr'],
      },
    },

    3)定义 Model(处理数据和逻辑)

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

    import * as usersService from '../services/users';
    
    export default {
      namespace: 'users',
      state: {
        list: [],
        total: null,
        page: null,
      },
      reducers: {
        save(state, { payload: { data: list, total, page } }) {
          return { ...state, list, total, page };
        },
      },
      effects: {
        *fetch({ payload: { page = 1 } }, { call, put }) {
          const { data, headers } = yield call(usersService.fetch, { page });
          yield put({
            type: 'save',
            payload: {
              data,
              total: parseInt(headers['x-total-count'], 10),
              page: parseInt(page, 10),
            },
          });
        },
        *remove({ payload: id }, { call, put }) {
          yield call(usersService.remove, id);
          yield put({ type: 'reload' });
        },*reload(action, { put, select }) {
          const page = yield select(state => state.users.page);
          yield put({ type: 'fetch', payload: { page } });
        },
      },
      subscriptions: {
        setup({ dispatch, history }) {
          return history.listen(({ pathname, query }) => {
            if (pathname === '/users') {
              dispatch({ type: 'fetch', payload: query });
            }
          });
        },
      },
    };

    namespace:model 的命名空间,同时也是他在全局 state 上的属性

    state:初始值

    reducers:以 key/value 格式定义 reducer。用于处理同步操作,唯一可以修改 state 的地方。由 action 触发

    effects:以 key/value 格式定义 effect。用于处理异步操作和业务逻辑,不直接修改 state。由 action 触发,可以触发 action,可以和服务器交互,可以获取全局 state 的数据等等。

    subscriptions:以 key/value 格式定义 subscription。subscription 是订阅,用于订阅一个数据源,然后根据需要 dispatch 相应的 action。在 app.start() 时被执行,数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。

    app.model(model)-》注册 model

    4)编写UI Component并connect起来

    import React from 'react';
    import { connect } from 'dva';
    import { Table, Pagination, Popconfirm, Button } from 'antd';
    import { routerRedux } from 'dva/router';
    import styles from './Users.css';
    import { PAGE_SIZE } from '../../../../constants';
    import UserModal from './UserModal';
    
    function Users({ dispatch, list: dataSource, loading, total, page: current }) {
      function deleteHandler(id) {
        dispatch({
          type: 'users/remove',
          payload: id,
        });
      }
    
      function pageChangeHandler(page) {
        dispatch(
          routerRedux.push({
            pathname: '/users',
            query: { page },
          })
        );
      }
    
      const columns = [
        {
          title: 'Username',
          dataIndex: 'username',
          key: 'username',
          render: text => <a href="">{text}</a>,
        },
        {
          title: 'Street',
          dataIndex: 'address.street',
          key: 'street',
        },
        {
          title: 'Website',
          dataIndex: 'website',
          key: 'website',
        },
        {
          title: 'Operation',
          key: 'operation',
          render: (text, record) => (
            <span className={styles.operation}>
              <Popconfirm title="Confirm to delete?" onConfirm={deleteHandler.bind(null, record.id)}>
                <a href="">Delete</a>
              </Popconfirm>
            </span>
          ),
        },
      ];
    
      return (
        <div className={styles.normal}>
          <div>
            <Table
              columns={columns}
              dataSource={dataSource}
              loading={loading}
              rowKey={record => record.id}
              pagination={false}
            />
            <Pagination
              className="ant-table-pagination"
              total={total}
              current={current}
              pageSize={PAGE_SIZE}
              onChange={pageChangeHandler}
            />
          </div>
        </div>
      );
    }
    
    function mapStateToProps(state) {
      const { list, total, page } = state.users;
      return {
        loading: state.loading.models.users,
        list,
        total,
        page,
      };
    }
    export default connect(mapStateToProps)(Users);

    5)相关概念

    dva 提供了 connect 方法,这个 connect 就是 react-redux 的 connect 。 connect 方法返回的也是一个 React 组件,通常称为容器组件。因为它是原始 UI 组件的容器,即在外面包了一层 State。connect 方法传入的第一个参数是 mapStateToProps 函数,mapStateToProps 函数会返回一个对象,用于建立 State 到 Props 的映射关系

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

    Model 对象的属性

    • namespace: 当前 Model 的名称。整个应用的 State,由多个小的 Model 的 State 以 namespace 为 key 合成
    • state: 该 Model 当前的状态。数据保存在这里,直接决定了视图层的输出
    • reducers: Action 处理器,处理同步动作,用来算出最新的 State
    • effects:Action 处理器,处理异步动作

    Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects

    dispatch({
      type: 'user/add', // 如果在 model 外调用,需要添加 namespace
      payload: {}, // 需要传递的信息
    });

    Reducer函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。在 dva 中,reducers 聚合积累的结果是当前 model 的 state 对象。通过 actions 中传入的值,与当前 reducers 中的值进行运算获得新的值(也就是新的 state)。

    state: {
      list: [],
      total: null,
      page: null,
    },
    reducers: {
      save(state, { payload: { data: list, total, page } }) {
        return { ...state, list, total, page };
      },
    }

    Effect:Action 处理器,处理异步动作,基于 Redux-saga 实现。Effect 指的是副作用。根据函数式编程,计算以外的操作都属于 Effect,典型的就是 I/O 操作、数据库读写。

    dva 提供多个 effect 函数内部的处理函数,比较常用的是 call 和 put

    • call:执行异步函数
    • put:发出一个 Action,类似于 dispatch
    effects: {
      *create({ payload: values }, { call, put }) {
        yield call(usersService.create, values);
        yield put({ type: 'reload' });
      },
      *reload(action, { put, select }) {
        const page = yield select(state => state.users.page);
        yield put({ type: 'fetch', payload: { page } });
      },
    }

    Router:这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。

    dva 实例提供了 router 方法来控制路由,使用的是react-router。

    在组件设计方法中,我们提到过 Container Components,在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。

    所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件。

    组件设计

    React 应用是由一个个独立的 Component 组成的,我们在拆分 Component 的过程中要尽量让每个 Component 专注做自己的事。

    一般来说,我们的组件有两种设计:Container Component、Presentational Component

    • Container Component

    Container Component 一般指的是具有监听数据行为的组件,一般来说它们的职责是绑定相关联的 model 数据,以数据容器的角色包含其它子组件。

    • Presentational Component

    它不会关联订阅 model 上的数据,而所需数据的传递则是通过 props 传递到组件内部。

    对组件分类,主要有两个好处:让项目的数据处理更加集中;让组件高内聚低耦合,更加聚焦;

    试想如果每个组件都去订阅数据 model,那么一方面组件本身跟 model 耦合太多,另一方面代码过于零散,到处都在操作数据,会带来后期维护的烦恼。

    除了写法上订阅数据的区别以外,在设计思路上两个组件也有很大不同。 Presentational Component是独立的纯粹的,可以参考 ant.design UI组件的React实现 ,每个组件跟业务数据并没有耦合关系,只是完成自己独立的任务,需要的数据通过 props 传递进来,需要操作的行为通过接口暴露出去。 而 Container Component 更像是状态管理器,它表现为一个容器,订阅子组件需要的数据,组织子组件的交互逻辑和展示。

    3、其它

    1)roadhog-》和 webpack 相似的库,起的是 webpack 自动打包和热更替的作用

    roadhog 是一个 cli 工具,提供 dev、 build 和 test 三个命令,分别用于本地调试、构建和测试,并且提供了特别易用的 mock 功能。在体验上,保持了和 create-react-app一致(如 redbox 显示出错信息、HMR、ESLint 出错提示等等),并且提供了 JSON 格式的配置方式。如果 create-react-app 的默认配置不能满足需求,而他又不提供定制的功能,于是基于他实现了一个可配置版。所以如果既要 create-react-app 的优雅体验,又想定制配置,那么可以试试 roadhog

    ## Install globally or locally 
    $ npm i roadhog -g
    
    ## Local development 
    $ roadhog dev
    
    ## Build 
    $ roadhog build
     
    ## Test 
    $ roadhog test

    roadhog dev支持mock, 在.roadhogrc.mock.js里配置

    export default {
      // Support type as Object and Array
      'GET /api/users': { users: [1,2] },
      // Method like GET or POST can be omitted(省略)
      '/api/users/1': { id: 1 },
      // Support for custom functions, the API is the same as express@4
      'POST /api/users/create': (req, res) => { res.end('OK'); },
    };

    roadhog的webpack部分是基于af-webpack的实现。在项目根目录创建 .webpackrc进行配置,格式是JSON。

    2)react-router-redux和dva

    redux 是状态管理的库,router 是(唯一)控制页面跳转的库。两者都很美好,但是不美好的是两者无法协同工作。换句话说,当路由变化以后,store 无法感知到。于是便有了 react-router-redux

    react-router-redux 是 redux 的一个中间件,主要作用是:加强了React Router库中history这个实例,以允许将history中接受到的变化反应到state中去。

    从代码上讲,主要是监听了 history 的变化。dva 在此基础上又进行了一层代理,把代理后的对象当作初始值传递给了 dva-core,方便其在 model 的 subscriptions 中监听 router 变化

    3)dva/fetch-》异步请求库,输出 isomorphic-fetch 的接口。

    4)dva-loading

    dva 有一个管理 effects 执行的 hook,并基于此封装了 dva-loading 插件。通过这个插件,我们可以不必一遍遍地写 showLoading 和 hideLoading,当发起请求时,插件会自动设置数据里的 loading 状态为 true 或 false 。然后我们在渲染 components 时绑定并根据这个数据进行渲染。

    // 1、注册 dva-loading 插件
    import dva from 'dva';
    import createLoading from 'dva-loading';
    const app = dva();
    app.use(createLoading());
    
    // 2、从store中获取loading状态
    import React from 'react';
    import { connect } from 'dva';
    import { Table } from 'antd';
    
    function Users({ dispatch, list: dataSource, loading }) {
      const columns = [
        {
          title: 'Username',
          dataIndex: 'username',
          key: 'username',
          render: text => <a href="">{text}</a>,
        },
        {
          title: 'Street',
          dataIndex: 'address.street',
          key: 'street',
        },
        {
          title: 'Website',
          dataIndex: 'website',
          key: 'website',
        }
      ];
    
      return (
        <div className={styles.normal}>
          <Table
            columns={columns}
            dataSource={dataSource}
            loading={loading}
            rowKey={record => record.id}
            pagination={false}
          />
        </div>
      );
    }
    
    function mapStateToProps(state) {
      const { list } = state.users;
      return {
        loading: state.loading.models.users,
        list,
      };
    }
    export default connect(mapStateToProps)(Users);

    2、项目积累

    1)React 中常见模式是为一个组件返回多个元素。为了包裹多个元素我们写过很多的 div 和 span,进行不必要的嵌套,无形中增加了浏览器的渲染压力。

    react15版以前,render 函数的返回必须有一个根节点,否则报错,为满足这一原则我会使用一个没有任何样式的 div 包裹一下。

    import React from 'react';
    export default function () {
        return (
            <div>
                <div>一步 01</div>
                <div>一步 02</div>
                <div>一步 03</div>
            </div>
        );
    }

    react 16版开始, render支持返回数组,这一特性已经可以减少不必要节点嵌套

    import React from 'react';
    export default function () {
        return [
            <div>一步 01</div>,
            <div>一步 02</div>,
            <div>一步 03</div>
        ];
    }

    而且,React 16为我们提供了Fragment。Fragment与Vue.js的<template>功能类似,可做不可见的包裹元素

    import React from 'react';
    export default function () {
        return (
            <React.Fragment>
                <div>一步 01</div>
                <div>一步 02</div>
                <div>一步 03</div>
            </React.Fragment>
        );
    }

    参考:https://segmentfault.com/a/1190000013220508

    附录:es6

    1)Generator 函数

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

    形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态

    Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

    function* helloWorldGenerator() {
      yield 'hello';
      yield 'world';
      return 'ending';
    }
    var hw = helloWorldGenerator();

    上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

    然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象—遍历器对象。

    下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

    hw.next() // { value: 'hello', done: false }
    hw.next() // { value: 'world', done: false }
    hw.next() // { value: 'ending', done: true }
    hw.next() // { value: undefined, done: true }

    遍历器对象的next方法的运行逻辑如下。

    (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

    (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

    (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

    (4)如果该函数没有return语句,则返回的对象的value属性值为undefined

    总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

    2)Generator 函数的异步应用

    ES6 诞生以前,异步编程的方法,大概有四种:回调函数、事件监听、发布/订阅、Promise 对象。Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

  • 相关阅读:
    【http】使用浏览器Cache和http状态码304实现的客户端缓存
    delegate与模式
    用Delegate绕开频繁反射的又一个简单高效的方法
    直接调用、委托与反射调用的性能区别
    Lambda表达式的非Linq用法
    泛型+反射+特性=以静制动
    绕开频繁反射
    不要用错单例模式
    活用接口——反例:MultiKeyDictionary
    jQuery框架总体分析
  • 原文地址:https://www.cnblogs.com/colorful-coco/p/9454315.html
Copyright © 2020-2023  润新知