一。目標定位
想解決什麼問題?打算怎麼做?
簡言之:dva 想提供一個基於業界 react&redux 最佳實踐的業務框架,以解決用裸 redux 全家桶作為前端資料層帶來的種種問題
- 編輯成本高,需要在 reducer, saga, action 之間來回切換
- 不便於組織業務模型(或者叫 domain model)。比如我們寫了一個 userlist 之後,要寫一個 productlist,需要複製很多檔案。
- saga 書寫太複雜,每監聽一個 action 都需要走 fork -> watcher -> worker 的流程
- redux entry 書寫麻煩,要完成 store 建立,中間件配置,路由初始化,Provider 的 store 的綁定,saga 的初始化
例如:
+ src
+ sagas
- user.js
+ reducers
- user.js
+ actions
- user.js
+ service
- user.js
二。核心實現
怎麼做了?
依賴關係
dva
react
react-dom
dva-core
redux
redux-saga
history
react-redux
react-router-redux
實現思路
他最核心的是提供了 app.model 方法,用於把 reducer, initialState, action, saga 封裝到一起
const model = {
// 用作頂層 state key,以及 action 字首
namespace
// module 級初始 state
state
// 訂閱其它資料源,如 router change,window resize, key down/up...
subscriptions
// redux-saga 裡的 sagas
effects
// redux 裡的 reducer
reducers
};
dva-core 實際所作的主要工作是從 model 配置得到 reducers,worker sagas, states 後,遮蔽接下來的一系列繁瑣工作:
-
接 redux(組合 state,組合 reducer)
-
接 redux-saga(完成 redux-saga 的 fork -> watcher -> worker,並做好錯誤捕獲)
除了 core 裡最重要的兩部分外,dva 還做了一些事情:
-
內建 react-router-redux, history 負責路由管理
-
粘上 react-redux 的 connect,isomorphic-fetch 等常用的東西
-
subscriptions 錦上添花,給監聽場外因素的程式碼提供一個容身之處
-
和 react 連線起來(用 store 連線 react 和 redux,靠 redux 中間件機制把 redux-saga 拉進來一起玩)
到這裡差不多封裝好了,那麼,下面開一些口子增加一點靈活性:
-
遞出一堆鉤子(effect/reducer/action/state 級 hook),讓內部狀態可讀
-
提供全域性錯誤處理方式,解決非同步錯誤不可控的痛點
-
增強 model 管理(允許動態增刪 model)
猜測整個實現過程是這樣:
-
配置化
在技術上實現固化,把靈活性限制起來,讓業務寫法更統一,滿足工程化的需要
-
面向通用場景擴充套件
只開必要的口子,放出能滿足大多數業務場景需要的最小靈活性集合
-
面向特定需要增強
應對業務呼聲,考慮是否放出/提供更多一些的靈活性,在靈活性與工程化(可控程度)之間權衡取捨
三。設計理念
遵從什麼思想,想要怎麼樣?
借鑑自 elm 和 choo,包括 elm 的 subscription 和 choo 的設計理念
elm 的 subscription
通過訂閱一些訊息來從其它資料源取資料,比如 websocket connection of server, keyboard input, geolocation change, history router change 等等
例如:
subscriptions: {
setupHistory ({ dispatch, history }) {
history.listen((location) => {
dispatch({
type: 'updateState',
payload: {
locationPathname: location.pathname,
locationQuery: queryString.parse(location.search),
},
})
})
},
setup ({ dispatch }) {
dispatch({ type: 'query' })
let tid
window.onresize = () => {
clearTimeout(tid)
tid = setTimeout(() => {
dispatch({ type: 'changeNavbar' })
}, 300)
}
}
}
提供這種機制來接入其它資料源,並集中到 model 裡統一管理
choo 的設計理念
choo 的理念是儘量精簡,儘量降低選擇/切換成本:
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.
大意是說框架不應該發展成堡壘,應該隨時可用可不用(低成本切換),API 及設計應該保持最小化,不要丟給使用者一坨「知識」,這樣你好他(同事)也好
P.S. 當然,這段話拿到哪裡都是對的,至於 dva 甚至 choo 自身有沒有做到就不好說了(從 choo 的實現上沒看出來有什麼拆除堡壘的有效措施)
在 API 設計上,dva-core 差不多保持最小化了:
-
一份 model 僅 4 個配置項
-
API 屈指可數
-
hook 差不多都是必須的(onHmr 與 extraReducers 是後來面向特定需要的增強)
不過話說回來,dva-core 實際做的只把 redux 和 redux-saga 通過 model 配置整合起來,並增強一些控制(錯誤處理等),引入的唯一外來概念是 subscription,還掛在 model 上,即便用力設計 API,也複雜不到哪去
四。優缺點
有什麼缺點,帶來的收益是什麼?
優點:
-
框架限制有利於工程化,磚塊一樣的程式碼最好了
-
簡化繁瑣的樣板程式碼(boilerplate code),儀式一樣的 action/reducer/saga/api...
-
解決多檔案導致關注點分散的問題,邏輯分離是好事,但檔案隔離就有點難受了
缺點:
-
限制了靈活性(比如 combineReducers 問題)
-
性能負擔(getSaga 部分的實現,看著就不快,做了不少額外的事情來達到控制的目的)
五。實現技巧
外置引數檢查
invariant 是原始碼出現最多的基本套路:
function start(container) {
// 允許 container 是字串,然後用 querySelector 找元素
if (isString(container)) {
container = document.querySelector(container);
invariant(
container,
`[app.start] container ${container} not found`,
);
}
// 並且是 HTMLElement
invariant(
!container || isHTMLElement(container),
`[app.start] container should be HTMLElement`,
);
// 路由必須提前註冊
invariant(
app._router,
`[app.start] router must be registered before app.start()`,
);
oldAppStart.call(app);
//...
}
invariant 用來保證強條件(不滿足條件直接 throw,生產環境也 throw),warning 用來保證弱條件(開發環境 log error 並 [無干擾 throw](/articles/redux 原始碼解讀/#articleHeader6),生產環境不 throw,換成空函式)
invariant 無差別 throw 可以用,但 warning 不建議使用,因為含 warning 的 release 程式碼不如編譯替換乾淨(還會執行空函式)
另一個技巧是包一層函式,在外面做引數檢查,比如示例中的:
function start(container) {
//... 引數檢查
oldAppStart.call(app);
}
這樣做的好處是把引數檢查拿出去了,可讀性會更好一些,但有多一層函式呼叫的性能開銷,而且不如 if-else 控制度高(只能通過 throw 阻斷後續流程)
切面 Hook
先看這部分原始碼:
// 把每一個 effect 都包一遍,為了實現 effect 級的控制
const sagaWithOnEffect = applyOnEffect(onEffect, sagaWithCatch, model, key);
function applyOnEffect(fns, effect, model, key) {
for (const fn of fns) {
effect = fn(effect, sagaEffects, model, key);
}
return effect;
}
然後用法是這樣的(傳入的 onEffect Hook):
function onEffect(effect, { put }, model, actionType) {
const { namespace } = model;
return function*(...args) {
yield put({ type: SHOW, payload: { namespace, actionType } });
yield effect(...args);
yield put({ type: HIDE, payload: { namespace, actionType } });
};
}
(摘自 dva-loading
這不就是環繞增強(AOP 裡的 Around Advice)嗎?
圍繞一個連線點的增強,如方法呼叫。這是最強大的一種增強類型。環繞增強可以在方法呼叫前後完成自定義的行為。它也負責選擇是繼續執行連線點,還是直接返回它們自己的返回值或者丟擲異常來結束執行
(摘自 AOP(Aspect-Oriented Programming))
這裡的實際作用是 onEffect 把 saga 包一層,把 saga 的執行權交出去,允許外部(通過 onEfect hook)注入邏輯。把自己交給 hook,不是什麼了不起的技巧,但用法上很有意思,利用 iterator 可展開的特性,實現了裝飾者的效果(交出去一個 saga,拿回來一個增強過的 saga,型別沒變不影響流程)
暫無評論,快來發表你的看法吧