Ant Design Pro 是一個企業級中后台前端/設計解決方案。本地環境需要安裝 node 和 git,技術棧基於 ES2015+、React、dva、g2 和 antd。
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 一般指的是具有監聽數據行為的組件,一般來說它們的職責是綁定相關聯的 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表達式(hello和world),即該函數有三個狀態: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方法,就會返回一個有着value和done兩個屬性的對象。value屬性表示當前的內部狀態的值,是yield表達式后面那個表達式的值;done屬性是一個布爾值,表示是否遍歷結束。另外需要注意,yield表達式只能用在 Generator 函數里面,用在其他地方都會報錯。
2)Generator 函數的異步應用
ES6 誕生以前,異步編程的方法,大概有四種:回調函數、事件監聽、發布/訂閱、Promise 對象。Generator 函數將 JavaScript 異步編程帶入了一個全新的階段。
