Ant Design Pro(React/dva/antd)

Ant Design Pro 是一個企業級中後臺前端/設計解決方案。本地環境須要安裝 node 和 git,技術棧基於 ES2015+、React、dva、g2 和 antd。javascript

參考:https://dvajs.com/css

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

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

一、預備知識node

1)Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理;Redux 除了和 React 一塊兒用外,還支持其它界面庫。react

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options]):鏈接 React 組件與 Redux store。webpack

[mapStateToProps(state, [ownProps]): stateProps] (Function): 若是定義該參數,組件將會監聽 Redux store 的變化。任什麼時候候,只要 Redux store 發生改變,mapStateToProps 函數就會被調用。該回調函數必須返回一個純對象,這個對象會與組件的 props 合併。git

  • 函數將被調用兩次。第一次是設置參數,第二次是組件與 Redux store 鏈接:connect(mapStateToProps, mapDispatchToProps, mergeProps)(MyComponent)es6

  • connect 函數不會修改傳入的 React 組件,返回的是一個新的已與 Redux store 鏈接的組件,並且你應該使用這個新組件。github

  • 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 上的一部分數據。

 

二、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 更像是狀態管理器,它表現爲一個容器,訂閱子組件須要的數據,組織子組件的交互邏輯和展現。

 

三、其它

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 時綁定並根據這個數據進行渲染。

// 一、註冊 dva-loading 插件
import dva from 'dva';
import createLoading from 'dva-loading';
const app = dva();
app.use(createLoading()); // 二、從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);

 

二、項目積累

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>
    );
}

參考:http://www.javashuo.com/article/p-tpcakqgb-dt.html

 

附錄: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 異步編程帶入了一個全新的階段。

相關文章
相關標籤/搜索