本文同步在我的博客shymean.com上,歡迎關注react
dva是基於react的二次封裝,在工做中常常用到,所以有必要了解一下其中的實現。本文將從源碼層面分析dva是如何將redux
、redux-saga
、react-router
等進行封裝的。git
本文使用的dva版本爲2.6.0-beta.20
,參考github
下面是一個簡單的dva應用json
// 初始化應用
const app = dva({
onError(error) {
console.log(error)
}
})
app.use(createLoading()) // 使用插件
app.model(model) // 註冊model
app.router(App) // 註冊路由
app.start('#app') // 渲染應用
複製代碼
咱們從這幾個api開始,瞭解dva是如何封裝react應用的。redux
整個dva項目使用lerna管理的,在每一個package的package.json中找到模塊對應的入口文件,而後查看對應源碼。api
// dva/src/index.js
export default function(opts = {}) {
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
const app = create(opts, createOpts);
const oldAppStart = app.start;
app.router = router;
app.start = start;
return app;
// 爲app暴露的router接口,主要是更新app._router
function router(router) {
app._router = router;
}
// 爲app暴露的start接口,主要渲染整個應用
function start(container) {
// 根據container找到對應的DOM節點
if (!app._store) oldAppStart.call(app); // 執行本來的app.start
const store = app._store;
// 爲HMR暴露_getProvider接口
app._getProvider = getProvider.bind(null, store, app);
// If has container, render; else, return react component
if (container) {
render(container, store, app, app._router);
app._plugin.apply('onHmr')(render.bind(null, container, store, app));
} else {
return getProvider(store, this, this._router);
}
}
}
// 渲染邏輯
function getProvider(store, app, router) {
const DvaRoot = extraProps => (
// app.router傳入的組件在此處被渲染,並傳入app,history等prop
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require('react-dom');
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
複製代碼
能夠看見app是經過create(opts, createOpts)
進行初始化的,其中opts
是暴露給使用者的配置,createOpts
是暴露給開發者的配置,真實的create
方法在dva-core
中實現。數組
下面是create
方法的大體實現promise
// dva-core/src/index.js
const dvaModel = {
namespace: '@@dva',
state: 0,
reducers: {
UPDATE(state) {
return state + 1;
},
},
};
export function create(hooksAndOpts = {}, createOpts = {}) {
const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中構造了createOpts對象
const plugin = new Plugin(); // dva-core中的插件機制,每一個實例化的dva對象都包含一個plugin對象
plugin.use(filterHooks(hooksAndOpts)); // 將dva(opts)構造參數opts上與hooks相關的屬性轉換成一個插件
const app = {
_models: [prefixNamespace({ ...dvaModel })],
_store: null,
_plugin: plugin,
use: plugin.use.bind(plugin), // 暴露的use方法,方便編寫自定義插件
model, // 暴露的model方法,用於註冊model
start, // 本來的start方法,在應用渲染到DOM節點時經過oldStart調用
};
return app;
}
複製代碼
能夠看見上面代碼主要暴露了use
、model
和start
這三個接口。react-router
dva實現高度依賴內置的Plugin系統,爲了便於理解後續代碼,咱們暫時跳出start方法,去探究Plugin原理。app
dva的插件時一個包含下面部分屬性的JS對象,每一個屬性對應一個函數或配置項
// plugin對象只暴露了下面這幾個屬性,顧名思義,以on開頭的方法會在整個應用的某些時刻觸發,相似於生命週期鉤子函數
const hooks = [
'onError',
'onStateChange',
'onAction',
'onHmr',
'onReducer',
'onEffect',
'extraReducers',
'extraEnhancers',
'_handleActions',
];
複製代碼
一個大概的插件結構相似於
const testPlugin = {
onStateChange(newState){
console.log(newState)
}
// ...其餘須要監聽的hook
}
app.use(testPlugin)
複製代碼
來看看在start初始化時構造的Plugin管理器
// Plugin管理對象
class Plugin {
constructor() {
this._handleActions = null;
// 每一個鉤子對應的處理函數默認都是空數組
this.hooks = hooks.reduce((memo, key) => {
memo[key] = [];
return memo;
}, {});
}
// 接收一個plugin對象
use(plugin) {
const { hooks } = this;
for (const key in plugin) {
if (Object.prototype.hasOwnProperty.call(plugin, key)) {
// ...檢測use的plugin上的key,並處理特定形式的key
if (key === '_handleActions') {
this._handleActions = plugin[key]; // 若是定義了插件的_handleActions方法,會覆蓋Plugin對象自己的_handleActions
} else if (key === 'extraEnhancers') {
hooks[key] = plugin[key]; // 若是定義了插件的extraEnhancers方法,會覆蓋this.hooks自己的extraEnhancers
} else {
hooks[key].push(plugin[key]); // 其餘key對應的方法會追加到hooks對應key數組中
}
}
}
}
// 返回一個經過key執行hooks上註冊的全部方法的函數
apply(key, defaultHandler) {
const { hooks } = this;
const fns = hooks[key];
// 遍歷fns並以此執行
return (...args) => {};
}
// 根據key找到hooks上對應的方法
get(key) {
const { hooks } = this;
if (key === 'extraReducers') {
// 將hooks[key]都合併到一個對象中,如將[{ x: 1 }, { y: 2 }]轉換爲{x:1,y:2}的對象
return getExtraReducers(hooks[key]);
} else if (key === 'onReducer') {
// 返回一個接受reducer並依次運行hooks.onReducer的函數,每次循環都會將前一個hooks.onReducer元素方法返回值做爲新的reducer
return getOnReducer(hooks[key]);
} else {
return hooks[key];
}
}
}
function getOnReducer(hook) {
return function(reducer) {
for (const reducerEnhancer of hook) {
reducer = reducerEnhancer(reducer);
}
return reducer;
};
}
複製代碼
能夠看見,整個插件系統的實現仍是比較簡單的,主要暴露了use
註冊插件、get
根據key獲取插件等接口。
app.use = plugin.use.bind(plugin)
複製代碼
回到前面章節,繼續查看start方法的執行流程,咱們能夠看見完成的store構建流程
// oldStart方法
function start() {
// 全局錯誤處理函數,若是某個插件註冊了onError鉤子,將在此處經過plugin.apply('onError')調用
const onError = (err, extension) => {};
// 遍歷app._models,收集saga
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// 建立 app._store,具體參看下面章節的createStore
// app._store = craeteStore()...
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
// 獲取全部監聽了onStateChange的插件,經過store.subscribe註冊到redux中
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// 運行全部sagas
sagas.forEach(sagaMiddleware.run);
setupApp(app);
// 運行全部model的subscriptions方法
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// 在執行start時註冊model、unmodel和replaceModel三個接口,具體參看下面章節
// ...
}
複製代碼
可見在整個start方法中
app._models
,收集並運行sagas
,app._store
,使用store.subscribe
註冊全部插件的onStateChange
回調model.subscriptions
app.model
、app.unmodel
、app.replaceModel
三個接口createStore
方法是封裝的redux createStore
,傳入了一些中間件和reducers
const sagaMiddleware = createSagaMiddleware(); // 初始化saga中間件
const promiseMiddleware = createPromiseMiddleware(app);
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
function createStore({ reducers, initialState, plugin, sagaMiddleware, promiseMiddleware, createOpts: { setupMiddlewares = returnSelf }, }) {
// extra enhancers
const extraEnhancers = plugin.get('extraEnhancers');
const extraMiddlewares = plugin.get('onAction');
const middlewares = setupMiddlewares([
promiseMiddleware,
sagaMiddleware,
...flatten(extraMiddlewares),
]);
const enhancers = [applyMiddleware(...middlewares), ...extraEnhancers];
// redux.createStore
return createStore(reducers, initialState, compose(...enhancers));
}
複製代碼
其中的reducers
參數由createReducer
方法構建,該方法返回一個加強版的reducer
const reducerEnhancer = plugin.get('onReducer'); // 在插件系統中提到,傳入onReducer時實際返回全部插件組合後的onReducer方法
const extraReducers = plugin.get('extraReducers'); // 傳入extraReducers實際返回的是全部插件合併後的reducerObj對象
function createReducer() {
return reducerEnhancer(
// redux.combineReducers
combineReducers({
...reducers, // start Opts參數上的默認reducer
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
複製代碼
其中promiseMiddleware
中間件由createPromiseMiddleware
建立,是一個當isEffect(action.type)
時返回Promise的中間件,並在本來的action上掛載該Promise的{__dva_resolve: resolve, __dva_reject: reject}
export default function createPromiseMiddleware(app) {
return () => next => action => {
const { type } = action;
// isEffect的實現爲:從app._models中找到namespace與action.type.split('/')[0]相同的model,而後根據model.effects[type]是否存在判斷當前action是否須要返回Promise
if (isEffect(type)) {
return new Promise((resolve, reject) => {
next({
__dva_resolve: resolve,
__dva_reject: reject,
...action,
});
});
} else {
return next(action);
}
};
}
複製代碼
一個model大概是下面的結構
{
namespace: 'index_model',
state: {
text: 'hello'
},
effects: {
* asyncGetInfo({payload = {}}, {call, put}) {
}
},
reducers: {
updateData: (state, {payload}) => {
return {...state, ...payload}
}
}
}
複製代碼
在create
方法中,app.model
方法實現以下,其本質是
function model(m) {
// 將model上的reducers和effects各個key處理爲`${namespace}${NAMESPACE_SEP}${key}`形式
const prefixedModel = prefixNamespace({ ...m });
app._models.push(prefixedModel);
return prefixedModel;
}
複製代碼
在執行start方法後,會覆蓋app.model方法爲injectModel
,並新增unmodel
和replaceModel
app.model = injectModel.bind(app, createReducer, onError, unlisteners);
app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);
app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);
// 動態添加model,這樣就能夠實現model與route組件相似的異步加載方案
function injectModel(createReducer, onError, unlisteners, m) {
m = model(m);
const store = app._store;
store.asyncReducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
// 從新調用createReducer
store.replaceReducer(createReducer());
if (m.effects) {
store.runSaga(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
// 更新unlisteners方便後面移除
if (m.subscriptions) {
unlisteners[m.namespace] = runSubscription(m.subscriptions, m, app, onError);
}
}
// 根據namespace移除對應model,須要移除store.asyncReducers並調用store.replaceReducer(createReducer())從新生成reducer
function unmodel(createReducer, reducers, unlisteners, namespace) {}
// 找到namespace相同的model並替換,若是不存在,則直接添加
function replaceModel(createReducer, reducers, unlisteners, onError, m) {}
複製代碼
經過此處瞭解到,dva中的model分爲兩類
在前面的dva.start
方法中咱們看到了createOpts
,並瞭解到在dva-core
的start
中的不一樣時機調用了對應方法
import * as routerRedux from 'connected-react-router';
const { connectRouter, routerMiddleware } = routerRedux;
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
複製代碼
其中initialReducer
和setupMiddlewares
在初始化store時調用,而後才調用setupApp
能夠看見針對router相關的reducer
和中間件配置,其中connectRouter
和routerMiddleware
均使用了connected-react-router
這個庫,其主要思路是:把路由跳轉也當作了一種特殊的action。
首先來看看connectRouter
方法的實現,該方法會返回關於router的reducer
// connected-react-router/src/reducer.js
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
const createConnectRouter = (structure) => {
const { fromJS, merge } = structure // 複製和合並JS對象的兩個工具方法
const createRouterReducer = (history) => {
const initialRouterState = fromJS({
location: injectQuery(history.location),
action: history.action,
})
return (state = initialRouterState, { type, payload } = {}) => {
if (type === LOCATION_CHANGE) {
const { location, action, isFirstRendering } = payload
// 首次渲染時不更新state
return isFirstRendering
? state
: merge(state, { location: fromJS(injectQuery(location)), action })
}
return state
}
}
return createRouterReducer
}
複製代碼
而後是routerMiddleware
,該方法返回關於router的中間件
export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
const routerMiddleware = (history) => store => next => action => {
if (action.type !== CALL_HISTORY_METHOD) {
return next(action)
}
// 當action爲路由跳轉時,調用history方法而不是執行next
const { payload: { method, args } } = action
history[method](...args)
}
複製代碼
最後回到dva中,查看patchHistory方法
function patchHistory(history) {
// 劫持了history.listen方法
const oldListen = history.listen;
history.listen = callback => {
const cbStr = callback.toString();
// 當使用了dva.routerRedux.ConnectedRouter路由組件時,其構造函數會執行history.listen,此時isConnectedRouterHandler將爲true
const isConnectedRouterHandler =
(callback.name === 'handleLocationChange' && cbStr.indexOf('onLocationChanged') > -1) ||
(cbStr.indexOf('.inTimeTravelling') > -1 && cbStr.indexOf('arguments[2]') > -1);
// 在app.start以後的其餘地方如model.subscriptions中使用history時,會接收到location和action兩個參數
callback(history.location, history.action);
return oldListen.call(history, (...args) => {
if (isConnectedRouterHandler) {
callback(...args);
} else {
// Delay all listeners besides ConnectedRouter
setTimeout(() => {
callback(...args);
});
}
});
};
return history;
}
複製代碼
能夠看見,dva對router並未作過多封裝,只是經過connected-react-router
提供了一個reducer和一箇中間件,並將該庫暴露爲routerRedux
。
從源碼能夠看見,dva主要是對redux
和redux-saga
進行封裝,簡化並對外暴露了幾個有限接口。除此以外,還內置了react-router
和其餘一些庫,所以也能夠看作是一個輕量級應用框架
dva
主要對外提供了相關的apidva-core
對redux
和redux-saga
進行封裝,並實現了一個簡單的插件系統學習dva源碼的一個好處可讓你去了解整個React的生態,並學會使用一種還不錯的開發方案。瞭解dva的封裝實現只是學習的第一步,要想編寫高效可維護的代碼,還須要深刻redux
、redux-saga
、react-router
等庫的使用和實現。