本系列分三部曲:《框架實現》 《框架使用》 與 《跳出框架看哲學》,這三篇是我對數據流階段性的總結,正好補充以前過期的文章。前端
本篇是 《框架使用》。node
如今咱們團隊也在從新思考數據流的價值,在業務不斷髮展,業務場景增多時,一個固定的數據流方案可能難以覆蓋全部場景,在全部業務裏都用得爽。特別在前端數據層很薄的場景下,在數據流治理上花功夫反卻是本末倒置。react
業務場景一般很複雜,可是對技術的探索每每只追求理想狀況下的效果,因此不少人草草閱讀完別人的經驗,給本身業務操刀時,會聽到一些反對的聲音,而實際效果也差強人意。git
因此在閱讀文章以前,應該先認識到數據流只是項目中很是微小的一環,並且每一個具體方案都很看場景,就算用對了路子,帶來的提效也不必定很明顯。github
2017 年 Redux 依然是主流,可能到 18 年仍是。你們吐槽歸吐槽,最終活仍是得幹,Redux 仍是得用,就算分析出 js 天生不適合函數式,也依然一條路走到黑,由於誰也不知道將來會如何發展,redux 生態雖然用得繁瑣,但普適性強,忍一忍,生活也能繼續過。typescript
Dob 和 Mobx 相似,也只是數據流中響應式方案的一個分支,思考也是比較理想化的,所以可能也擺脫不了中看不中用的命運,誰叫業務場景那麼多呢。redux
不過相對而言,應該算是接地氣一些,它既沒有要求純函數式和分離反作用,也沒有 cyclejs 那麼抽象,只要入門的面向對象,就能夠用好。後端
使用 redux 時,不少時候是傻傻分不清要不要將結構化數據拍平,再分別訂閱,或者分不清訂閱後數據處理應該放在組件上仍是全局。這是由於 redux 破壞了 react 分形設計,在 最近的一次討論記錄 有說到。而許多基於 redux 的分形方案都是 「僞」 分形的,偷偷利用 replaceReducer
作一些動態 reducer 註冊,再綁定到全局。安全
討論理想數據流方案比較痛苦,並且引言裏說到,不少業務場景下收益也不大,因此能夠考慮結合工程化思惟解決,將組件類型區分開,分爲普通組件與業務組件,普通組件不使用數據流,業務組件綁定全局數據流,能夠避免糾結。數據結構
使用 Mobx 時,文檔告訴咱們它具備依賴追蹤、監聽等許多能力,但沒有好的實踐例子作指導,看完了 todoMvc 以爲學完了 90%,在項目中實踐後發現無從下手。
所謂最佳實踐,是基於某種約定或約束,讓代碼可讀性、可維護性更好的方案。約定是活的,不遵照也沒事,約束是死的,不遵照就沒法運行。約束大部分由框架提供,好比開啓嚴格模式後,禁止在 Action 外修改變量。然而糾結最多的地方仍是在約定上,我在寫 dob 框架先後,總結出了一套使用約定,可能僅對這種響應式數據流管用。
使用數據流,第一要作的事情就是管理數據,要解決 Store 放在哪,怎麼放的問題。其實還有個前置條件:要不要用 Store 的問題。
首先,最簡單的組件確定不須要用數據流。那麼組件複雜時,若是數據流自己具備分形功能,那麼可用可不用。所謂具備分形功能的數據流,是貼着 react 分形功能,將其包裝成任具備分形能力的組件:
import { combineStores, observable, inject, observe } from 'dob'
import { Connect } from 'dob-react'
@observable
class Store { name = 123 }
class Action {
@inject(Store) store: Store
changeName = () => { this.store.name = 456 }
}
const stores = combineStores({ Store, Action })
@Connect(stores)
class App extends React.Component<typeof stores, any> {
render() {
return <div onClick={this.props.Action.changeName}>{this.props.Store.name}</div>
}
}
ReactDOM.render(<App /> , document.getElementById('react-dom'))
複製代碼
dob 就是這樣的框架,上面例子中,點擊文字能夠觸發刷新,即使根 dom 節點沒有 Provider
。這意味着這個組件不論放到任何環境,均可以獨立運行,成爲任何項目中的一部分。這種組件雖然用了數據流,可是和普通 React 組件徹底無區別,能夠放心使用。
若是是僞分形的數據流,可能在 ReactDOM.render
須要特定的 Provider
配合纔可以使用,那麼這個組件就不具有可遷移能力。若是別人不幸安裝了這種組件,就須要在項目根目錄安裝一個全家桶。
問:雖然數據流+組件具有徹底分形能力,但若此組件對 props 有響應式要求,那仍是有對該數據流框架的隱形依賴。
答:是的,若是組件要求接收的 props 是 observable
化的,以便在其變化時自動 rerender,那當某個環境傳遞了普通 props,這個組件的部分功能將失效。其實 props 屬於 react 的通用鏈接橋樑,所以組件只應該依賴普通對象的 props,內部能夠再對其 observable
化,以具有完備的可遷移能力。
React 雖然能夠徹底模塊化,但實際項目中模塊必定分爲通用組件與業務組件,頁面模塊也能夠看成業務組件。複雜的網站由數據驅動比較好,既然是數據驅動,那麼能夠將業務組件與數據的鏈接移到頂層管理,通常經過頁面頂層包裹 Provider
實現:
import { combineStores, observable, inject, observe } from 'dob'
import { Connect } from 'dob-react'
@observable
class Store { name = 123 }
class Action {
@inject(Store) store: Store
changeName = () => { this.store.name = 456 }
}
const stores = combineStores({ Store, Action })
ReactDOM.render(
<Provider {...store}>
<App />
</Provider>
, document.getElementById('react-dom'))
複製代碼
本質上只是改變了 Store 定義的位置,而組件使用方式依然不變:
@Connect
class App extends React.Component<typeof stores, any> {
render() {
return <div onClick={this.props.Action.changeName}>{this.props.Store.name}</div>
}
}
複製代碼
有一個區別是 @Connect
不須要帶參數了,由於若是全局註冊了 Provider
,會默認透傳到 Connect
中。與分形相反,這種設計會致使組件沒法遷移到其餘項目單獨運行,但好處是能夠在本項目中任意移動。
分形的組件對結構強依賴,只要給定須要的 props 就能夠完成功能,而全局數據流的組件幾乎能夠徹底不依賴結構,全部 props 都從全局 store 獲取。
其實說到這裏,能夠發現這兩點是難以合二爲一的,咱們能夠預先將組件分爲業務耦合與非業務耦合兩種,讓業務耦合的組件依賴全局數據流,讓非業務耦合組件保持分形能力。
若是有更好的 Store 管理方式,能夠在個人 github 和 知乎 深刻聊聊。
對於 Mvvm 思想的庫,Connect 概念不只僅在於注入數據(與 redux 不一樣),還會監聽數據的變化觸發 rerender。那麼每一個組件須要 Connect 嗎?
從數據流功能來講,沒有用到數據流的組件固然不須要 Connect,但業務組件保持着將來不肯定性(業務不肯定),因此保持每一個業務組件的 Connect 便於後期維護。
並且 Connect 可能還會作其餘優化工做,好比 dob 的 Connect 不只會注入數據,完成組件自動 render,還會保證組件的 PureRender
,若是對 dob 原理感興趣,能夠閱讀 精讀《dob - 框架實現》。
其實個議題只是很是微小的點,不過現實就是諷刺的,不少時候多會糾結在這種小點子上,因此單獨花費篇幅說幾句。
Store 扁平化有很大緣由是 js 對 immutable 支持力度不夠,致使對深層數據修改很是麻煩致使的,雖然 immutable.js
這類庫能夠經過字符串快速操做,但這種使用方式必然會被不斷髮展的前端浪潮所淹沒,咱們不可能看到 js 標準推薦咱們使用字符串訪問對象屬性。
經過字符串訪問對象屬性,和 lodash 的 _.get
相似,不過對於安全訪問屬性,也已經有 proposal-optional-chaining 的提案在語法層面解決,一樣 immutable 的便捷操做也須要一種標準方式完成。實際上不用等待另外一個提案,利用 js 現有能力就能夠模擬原生 immutable 支持的效果。
dob-redux 能夠經過相似 this.store.articles.push(article)
的 mutable 寫法,實現與 react-redux
的對接,內部天然作掉了相似 immutable.set
的事情,感興趣能夠讀讀個人這篇文章:Redux 使用可變數據結構,介紹了這個黑魔法的實現原理。
有點扯遠了,那麼數據流扁平化本質解決的是數據格式規範問題。好比 normalizr 就是一種標準數據規範的推動,不少時候咱們都將冗餘、或者錯誤歸類的數據存入 Store,那維護性天然比較差,Redux 推崇的應當是正確的數據格式化,而不是一昧追求扁平化。
對於前端數據流很薄的場景,也不是隨便處理數據就完事了。還有許多事可作,好比使用 node 微服務對後端數據標準化、封裝一些標準格式處理組件,把很薄的數據作成零厚度,業務代碼能夠對簡單的數據流徹底無感知等等。
Redux 天然而然用 action 隔離了反作用與異步,那在只有 action 的 Mvvm 開發模式中,異步須要如何隔離?Mvvm 真的完美解決了 Redux 避而遠之的異步問題嗎?
在使用 dob 框架時,異步後賦值須要很是當心:
@Action async getUserInfo() {
const userInfo = await fetchUser()
this.store.user.data = userInfo // 嚴格模式下將會報錯,由於脫離了 Action 做用域。
}
複製代碼
緣由是 await
只是僞裝用同步寫異步,當一個 await
開始時,當前函數的棧已經退出,所以後續代碼都不在一個 Action
中,因此通常的解法是顯示申明 Action
的顯示申明大法:
@Action async getUserInfo() {
const userInfo = await fetchUser()
Action(() => {
this.store.user.data = userInfo
})
}
複製代碼
這說明了異步須要小心!Redux 將異步隔離到 Reducer
以外很正確,只要涉及到數據流變化的操做是同步的,外面 Action
怎麼千奇百怪,Reducer
均可以高枕無憂。
其實 redux 的作法與下面代碼相似:
@Action async getUserInfo() { // 類 redux action
const userInfo = await fetchUser()
this.setUserInfo(userInfo)
}
@Action async setUserInfo(userInfo) { // 類 redux reduer
this.store.user.data = userInfo
}
複製代碼
因此這是 dob 中對異步的另外一種處理方法,稱做隔離大法吧。因此在響應式框架中,顯示申明大法與隔離大法均可以解決異步問題,代碼也顯得更加靈活。
響應式框架的另外一個好處在於能夠自動觸發,好比自動觸發請求、自動觸發操做等等。
好比咱們但願當請求參數改變時,能夠自動重發,通常的,在 react 中須要這麼申明:
componentWillMount() {
this.fetch({ url: this.props.url, userName: this.props.userName })
}
componentWillReceiveProps(nextProps) {
if (
nextProps.url !== this.props.url ||
nextProps.userName !== this.props.userName
) {
this.fetch({ url: nextProps.url, userName: nextProps.userName })
}
}
複製代碼
在 dob 這類框架中,如下代碼的功能是等價的:
import { observe } from 'dob'
componentWillMount() {
this.signal = observe(() => {
this.fetch({ url: this.props.url, userName: this.props.userName })
})
}
複製代碼
其神奇地方在於,observe
回調函數內用到的變量(observable 後的變量)改變時,會從新執行此回調函數。而 componentWillReceiveProps
內作的判斷,實際上是利用 react 的生命週期手工監聽變量是否改變,若是改變了就觸發請求函數,然而這一系列操做均可以讓 observe
函數代勞。
observe
有點像更自動化的 addEventListener
:
document.addEventListener('someThingChanged', this.fetch)
複製代碼
因此組件銷燬時不要忘了取消監聽:
this.signal.unobserve()
複製代碼
最近咱們團隊也在探索如何更方便的利用這一特性,正在考慮實現一個自動請求庫,若是有好的建議,也很是歡迎一塊兒交流。
若是你在使用 redux,能夠參考 你所不知道的 Typescript 與 Redux 類型優化 優化 typescript 下 redux 類型的推導,若是使用 dob 或 mobx 之類的框架,類型推導就更簡單了:
import { combineStores, Connect } from 'dob'
const stores = combineStores({ Store, Action })
@Connect
class Component extends React.PureComponent<typeof stores, any> {
render() {
this.props.Store // 幾行代碼便得到了完整類型支持
}
}
複製代碼
這都得益於響應式數據流是基於面向對象方式操做,能夠天然的推導出類型。
複雜的數據流必然存在 Store 與 Action 之間相互引用,比較推薦依賴注入的方式解決,這也是 dob 推崇的良好實踐之一。
固然依賴注入不能濫用,好比不要存在循環依賴,雖然手握靈活的語法,但在下手寫代碼以前,須要對數據流有一套較爲完整的規劃,好比簡單的用戶、文章、評論場景,咱們能夠這麼設計數據流:
分別創建 UserStore
ArticleStore
ReplyStore
:
import { inject } from 'dob'
class UserStore {
users
}
class ReplyStore {
@inject(UserStore) userStore: UserStore
replys // each.user
}
class ArticleStore {
@inject(UserStore) userStore: UserStore
@inject(ReplyStore) replyStore: ReplyStore
articles // each.replys each.user
}
複製代碼
每一個評論都涉及到用戶信息,因此 ReplyStore
注入了 UserStore
,每一個文章都包含做者與評論信息,因此 ArticleStore
注入了 UserStore
與 ReplyStore
,能夠看出 Store 之間依賴關係應當是樹形,而不是環形。
最終 Action 對 Store 的操做也是經過注入來完成,而因爲 Store 之間已經注入完了,Action 能夠只操做對應的 Store,必要的時候再注入額外 Store,並且也不會存在循環依賴:
class UserAction {
@inject(UserStore) userStore: UserStore
}
class ReplyAction {
@inject(ReplyStore) replyStore: ReplyStore
}
class ArticleAction {
@inject(ArticleStore) articleStore: ArticleStore
}
複製代碼
最後,不建議在局部 Store 注入全局 Store,或者局部 Action 注入全局 Store,由於這會破壞局部數據流的分形特色,切記保證非業務組件的獨立性,把全局綁定交給業務組件處理。
比較優雅的方式,是編寫類級別的裝飾器,統一捕獲 Action 的異常並拋出:
const errorCatch = (errorHandler?: (error?: Error) => void) => (target: any) => {
Object.getOwnPropertyNames(target.prototype).forEach(key => {
const func = target.prototype[key]
target.prototype[key] = async (...args: any[]) => {
try {
await func.apply(this, args)
} catch (error) {
errorHandler && errorHandler(error)
}
}
})
return target
}
const myErrorCatch = errorCatch(error => {
// 上報異常信息 error
})
@myErrorCatch
class ArticleAction {
@inject(ArticleStore) articleStore: ArticleStore
}
複製代碼
當任意步驟觸發異常,await 以後的代碼將中止執行,並將異常上報到前端監控平臺,好比咱們內部的 clue 系統。關於異常處理更多信息,能夠訪問我較早的一篇文章:Callback Promise Generator Async-Await 和異常處理的演進。
準確區分出業務與非業務組件、寫代碼前先設計數據流的依賴關係、異步時注意分離,就能夠解決絕大部分業務場景的問題,實在遇到特殊狀況可使用 observe
監聽數據變化,由此能夠拓展出好比請求自動重發的功能,運用得當能夠解決餘下比較棘手的特殊需求。
雖然數據流只是項目中很是微小的一環,但若是想讓整個項目保持良好的可維護性,須要把各個環節作精緻。
這篇文章寫於 2017 年最後一天,祝你們元旦快樂!
若是你想參與討論,請點擊這裏,每週都有新的主題,每週五發布。