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 對象的屬性
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
。
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 數據
,以數據容器的角色包含其它子組件。
它不會關聯訂閱 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
表達式(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 異步編程帶入了一個全新的階段。