dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此也能夠理解爲一個輕量級的應用框架。html
通過一段時間的自學或培訓,你們應該都能理解 redux 的概念,並承認這種數據流的控制可讓應用更可控,以及讓邏輯更清晰。但隨之而來一般會有這樣的疑問:概念太多,而且 reducer, saga, action 都是分離的(分文件)。前端
基於 Redux 的基本概念。包括:react
其餘概念git
import dva from 'dva';
const App = () => <div>Hello dva</div>;
// 建立應用
const app = dva();
// 註冊視圖
app.router(() => <App />);
// 啓動應用
app.start('#root');
複製代碼
// 建立應用
const app = dva();
app.use(createLoading()) // 使用插件
// 註冊 Model
app.model({
namespace: 'count',
state: 0,
reducers: {
add(state) { return state + 1 },
},
effects: {
*addAfter1Second(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'add' });
},
},
});
// 註冊視圖
app.router(() => <ConnectedApp />);
// 啓動應用
app.start('#root');
複製代碼
dva 作了三件比較重要的事情:github
// dva/src/index.js
export default function (opts = {}) {
// 1. 使用 connect-react-router 和 history 初始化 router 和 history
// 經過添加 redux 的中間件 react-redux-router,強化了 history 對象的功能
const history = opts.history || createHashHistory();
const createOpts = {
initialReducer: {
router: connectRouter(history),
},
setupMiddlewares(middlewares) {
return [routerMiddleware(history), ...middlewares];
},
setupApp(app) {
app._history = patchHistory(history);
},
};
// 2. 調用 dva-core 裏的 create 方法 ,函數內實例化一個 app 對象。
const app = create(opts, createOpts);
const oldAppStart = app.start;
// 3. 用自定義的 router 和 start 方法代理
app.router = router;
app.start = start;
return app;
// 3.1 綁定用戶傳遞的 router 到 app._router
function router(router) {
invariant(
isFunction(router),
`[app.router] router should be function, but got ${typeof router}`,
);
app._router = router;
}
// 3.2 調用 dva-core 的 start 方法,並渲染視圖
function start(container) {
// 對 container 作一系列檢查,並根據 container 找到對應的DOM節點
if (!app._store) {
oldAppStart.call(app);
}
const store = app._store;
// 爲HMR暴露_getProvider接口
// ref: https://github.com/dvajs/dva/issues/469
app._getProvider = getProvider.bind(null, store, app);
// 渲染視圖
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 => (
<Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>
);
return DvaRoot;
}
function render(container, store, app, router) {
const ReactDOM = require('react-dom'); // eslint-disable-line
ReactDOM.render(React.createElement(getProvider(store, app, router)), container);
}
複製代碼
咱們同時能夠發現 app 是經過create(opts, createOpts)進行初始化的,其中opts是暴露給使用者的配置,createOpts是暴露給開發者的配置,真實的create方法在dva-core中實現web
dva-core 則完成了核心功能:編程
經過 create 方法完成 app 實例的構造,並暴露use、model和start三個接口json
經過 start 方法完成redux
dva-core createapi
做用: 完成 app 實例的構造,並暴露use、model和start三個接口
// 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;
}
複製代碼
dva-core start
做用:
function start() {
const sagaMiddleware = createSagaMiddleware();
const promiseMiddleware = createPromiseMiddleware(app);
app._getSaga = getSaga.bind(null);
const sagas = [];
const reducers = { ...initialReducer };
for (const m of app._models) {
// 把每一個 model 合併爲一個reducer,key 是 namespace 的值,value 是 reducer 函數
reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);
if (m.effects) {
// 收集每一個 effects 到 sagas 數組
sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));
}
}
// 初始化 Store
app._store = createStore({
reducers: createReducer(),
initialState: hooksAndOpts.initialState || {},
plugin,
createOpts,
sagaMiddleware,
promiseMiddleware,
});
const store = app._store;
// Extend store
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {};
// Execute listeners when state is changed
const listeners = plugin.get('onStateChange');
for (const listener of listeners) {
store.subscribe(() => {
listener(store.getState());
});
}
// Run sagas, 調用 Redux-Saga 的 createSagaMiddleware 建立 saga中間件,調用中間件的 run 方法全部收集起來的異步方法
// run方法監聽每個反作用action,當action發生的時候,執行對應的 saga
sagas.forEach(sagaMiddleware.run);
// Setup app
setupApp(app);
// 運行 subscriptions
const unlisteners = {};
for (const model of this._models) {
if (model.subscriptions) {
unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);
}
}
// 暴露三個 Model 相關的接口,Setup app.model and app.unmodel
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);
/**
* Create global reducer for redux.
*
* @returns {Object}
*/
function createReducer() {
return reducerEnhancer(
combineReducers({
...reducers,
...extraReducers,
...(app._store ? app._store.asyncReducers : {}),
}),
);
}
}
}
複製代碼
在前面的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。
按照 React 官方指導意見, 若是多個 Component 之間要發生交互, 那麼狀態(即: 數據)就維護在這些 Component 的最小公約父節點上, 也便是
以及 自己不維持任何 state, 徹底由父節點 傳入 props 以決定其展示, 是一個純函數的存在形式, 即: Pure Component
與上圖相比, 幾個明顯的改進點:
這樣一來, 各個部分各司其職, 耦合度更低, 複用度更高, 擴展性更好。
由於咱們可使用 Middleware 攔截 action, 這樣一來異步的網絡操做也就很方便了, 作成一個 Middleware 就好了, 這裏使用 redux-saga 這個類庫, 舉個栗子:
有了前面三步的鋪墊, Dva 的出現也就水到渠成了, 正如 Dva 官網所言, Dva 是基於 React + Redux + Saga 的最佳實踐, 對於提高編碼體驗有三點貢獻:
約定大於配置
app.model({
namespace: 'count',
state: {
record: 0,
current: 0,
},
reducers: {
add(state) {
const newCurrent = state.current + 1;
return { ...state,
record: newCurrent > state.record ? newCurrent : state.record,
current: newCurrent,
};
},
minus(state) {
return { ...state, current: state.current - 1};
},
},
effects: {
*add(action, { call, put }) {
yield call(delay, 1000);
yield put({ type: 'minus' });
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });
},
},
});
複製代碼
Dva 的 api 參考了 choo,概念來自於 elm。
We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and then casually be the best choice around. Real casually.
We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. Choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.
We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.