用mobx構建大型項目的最佳實踐

下一篇:

用mobx構建大型項目的最佳實踐(2)css

mobx是一款基於觀察者模式的響應式數據管理框架,相對於redux來講是後起之秀。前端

有一種觀點認爲mobx不適合構建大型項目,這源於mobx過於靈活的特色。靈活即意味着隨意,這在開發日益複雜的大型項目是致命的弱點。redux則否則,它的惟一數據源、reducer純函數、只能經過dispatch修改狀態等幾個特性保證了代碼書寫格式的高度統一。node

本文不會討論mobx的使用細節,只會在充分利用mobx優點的基礎上,對開發格式進行統一,保證開發大型項目的可維護性。react

mobx的優點極其優秀,面向對象編程、響應式編程、mutable的數據處理方式、精準更新組件的能力,這裏不過多討論。webpack

mobx劣勢

  • 0、數據可隨處定義。能夠定義在組件內,來替代state的做用;也能夠定義在單獨的store
  • 一、用戶交互邏輯能夠寫在組件聲明的方法內,也能夠寫在store聲明的方法內。
  • 二、用戶交互每每涉及多個store的數據處理,store間可能造成交叉引用的網狀結構。
  • 三、store每每按頁面和模塊劃分,散落在各處,很差統一管理。
  • 四、store實例化的時機和方式不可控。
  • 五、當單例store由於業務變動須要支持多實例時,改造難度極大
  • 六、對服務端渲染不友好。node端在讀取數據填充頁面時,還須要把數據存儲到頁面,供前端加載時從數據恢復到storeredxucreateStore自然支持從initialState恢復數據的能力)

面對以上的種種問題,大部分人都會持有mobx不適合大型項目的觀點。web

解決方案

在筆者用mobx+react作了諸多中大型的前端項目以後,對這些劣勢深惡痛絕,也逐漸摸索出了一些方案來解決上述的問題。編程

一、分層

爲了解決數據定義,數據共享以及邏輯代碼如何防止等問題,首先對項目結構進行分層。redux

  • 項目按照頁面進行分割
  • 頁面按照 storesactionsviews分爲三層
  • stores定義頁面內各個數據模型及數據的操做方法,各個store之間互相獨立
  • views層做爲視圖層,接收stores注入的數據負責渲染
  • actions層處理交互邏輯,引用各個store方法調用更新數據,又mobx自動觸發視圖刷新

以上是一個典型的mvc分層結構,這種方式很大程度上解決了問題點0、一、2。api

二、惟一數據源

經過第一步的改造,項目的可維護性可謂上升一個臺階。promise

可是頁面的storeaction須要手動實例化並手動注入到每一個頁面組件,着實是一個負擔。而且store實例化自由,管理起來較爲混亂。並未解決三、四、5的問題。

因此須要開發一個狀態管理庫,主要實現以下功能

  • storeaction的自動查找加載。storeaction分頁面放置,經過某種機制進行查找
  • 查找到的全部storeaction自動實例化,並造成全局惟一數據源
  • store提供配置單例或多實例的配置項,減小因需求變動致使的代碼改造工做量
  • 按需實例化store。好比訪問頁面A,只需實例化A頁面依賴的store
查找機制

storeaction的查找方式簡單介紹兩種,一種是經過webpack提供的require.context動態的引入特定目錄下的storeaction模塊,第二種是經過裝飾器模式進行加載。 僞代碼以下

//webpack 
    require.context('./',true,/^(.+\/)*stores\/(.+)\.(t|j)sx?$/i)
    
    //裝飾器
    @store({
        path:'pageA.storeA', //在全局store中的訪問路徑
        type:'singleton'|'multi' // 聲明單例仍是多實例
    })
    class StoreA{
        
    }
    
    // store裝飾器的實現
    let store = (config) => target => {
      target['__storeType'] = config.type //保存
      App['__stores'] = App['__stores'] || [] //App爲狀態管理類
      App['__stores'].push({ target, path: config.path})
      return target;
    }
複製代碼

拿到全部store的信息以後,就能夠在管理類裏對storesactions進行處理,組裝全局惟一的rootStore了,action處理也是同樣。

按需實例化

若是爲了追求性能,能夠考慮實現這麼一個特性。實現方式能夠用訪問器屬性,在訪問到store屬性時,再進行動態的實例化。僞代碼以下

Object.defineProperty(rootAction, 'storeA', {
          configurable: true,
          enumerable: true,
          get() {
            StoreA['__instance'] = StoreA['__instance'] || new StoreA()
            return StoreA['__instance']
          },
          set() {
            throw Error("can not set store")
          }
        })
複製代碼

經過這麼一個狀態管理庫,咱們解決了三、四、5,對於問題6 服務端渲染,也能夠經過簡單的處理對rootStore進行恢復。

三、開發體驗優化

(1)path自動聲明

上面的裝飾器@store須要手動指定storerootStore中所處的節點,能不能經過store文件所在的目錄名、文件名、store類名等信息直接映射到對應的結構呢?

答案是能夠的,只須要編寫一個babel轉換插件,在編譯時對文件的抽象語法樹進行分析替換,自動填充@storepath屬性就行了。(筆者項目用的是ts,提供了一個ts transformer完成一樣的功能)

(2)腳手架
  • 因爲頁面結構保持了高度統一,不管是store文件、action文件,或是jsxcss文件,都有或多或少的樣板代碼。爲了開發流程的自動化,能夠開發腳手架工具,自動生成頁面骨架。一是爲了提高開發效率,二能夠規範開發流程。
  • 若是項目中用到ts的話,這種全局自動加載造成的store會丟失類型信息。因此須要自動的生成一份類型聲明文件(.d.ts)幫助有更好的開發體驗。

四、開發規範限制

最後一個話題,如何更嚴格的規範代碼的書寫方式。

即便咱們限定了業務邏輯只能在action內處理,但終歸是口頭約定。老成員總有圖便利把邏輯寫到view層的時候,新成員剛加入時的代碼更可能如此。

因此咱們須要提供一種機制來保證只能在action內調用store的方法進行邏輯處理,而在action外的store調用都無效,並在開發環境給以警告。

這個問題若是你認爲很簡單,多是由於你還沒理解到這個的關鍵點在哪。下面經過例子來討論解決方案。

//聲明一個store
    class StoreA{
        age = null;
        
        setAge(age){
            this.age = age;
        }
    }
    
    //聲明一個action
    class ActionA{
        //調用store方法
        setAge(age){
            this.storeA.setAge(age); //有效
        }
    }
    
    //組件內
    storeA.setAge(age)  //無效
複製代碼

對於上述場景,處理方法比較簡單。只須要

  • 聲明一個變量flag
  • 在實例化storeaction時對實例的方法分別進行包裝
  • action的方法調用前設置flagtrue,執行action的方法,而後設置flagfalse
  • 這樣store的方法若是在action內調用時訪問到的flagtrue,在其餘地方訪問到的flagfalse
  • store方法的包裝比較簡單,判斷flag,爲true執行數據操做,爲false進行友好提示

通過上述幾步,就完成了同步場景的限制處理。

但實際的項目中大量的存在異步操做,若是action以下所示,會如何呢?

class ActionA{
        //調用store方法
        async setAge(age){
            await saveAge(url); //接口調用
            this.storeA.setAge(age); //有效
        }
    }
複製代碼

這時storeA.setAge雖然處於action內,但訪問到的flag倒是false,方案失效了。

對同步操做的處理如此簡單,異步操做倒是一個巨大的難題。如今的課題能夠抽象爲以下描述

如何實如今同一個方法內的調用(包括同步操做, setTimeout、promise、rAF、各類事件等異步操做的回調內...)都能訪問到同一個上下文(true),而在這個方法外訪問到的是另外一個(false複製代碼

心裏隱隱約約有一個答案,若是在action調用時保存這個上下文,並在各類異步的回調裏再取出這個上下文便可實現功能。但這是一個可怕的事情,意味着須要咱們去代理全部的異步調用,換句話說咱們須要覆蓋原生的方法來作這麼一件事情!

這彷佛是很難去實現的,直到我發現了zone.js

zone.js

簡單介紹一下,zone.jsangular框架的核心組件,angular利用zone.js監聽全部(可能致使數據變化)的異步事件。

這跨度有點大,怎麼又扯到了angular

不要緊,從新介紹一下。zone.js描述了JavaScript執行過程的上下文,能夠在異步任務之間進行持久性傳遞。

重點就是這句話,我翻譯一下,zonejs能保持同一個方法內的調用(不管同步仍是異步的)都能訪問到同一個上下文對象。這不正好解決了咱們的問題嗎?

如今利用zonejs來解決咱們以前的問題。代碼以下

//這裏並無闡述zone.js如何使用,若是看過zonejs文檔應該很容易理解下面的代碼所作的事情
    const zone = Zone.root.fork({
      name: '__mobx__zone'
    });
    
    //包裝action的setAge方法,使得action內的方法調用訪問到Zone.current都爲zone
    let oldFn = ActionA.setAge
    ActionA.setAge = (...args) => {
      return zone.run(oldFn, context, args)
    }
    
    //包裝store的方法,判斷Zone.current是否爲zone,若是在action以外調用則爲Zone.root
     let oldFn = StoreA.setAge
    StoreA.setAge = (...args) => {
      if(Zone.current === zone){
        return oldFn.apply(context,args)
      }else{
          //在action外調用store方法觸發警告
          console.error('invalid call')
      }
    }
    
    //以上的包裝方法均在內部處理,不暴露在業務代碼中
複製代碼

利用zone.js能夠很容易的實現咱們想要的功能,經過粗略的源碼瀏覽發現zone.js正是暴力的代理了原生的api

經過上述幾步處理,咱們就能夠愉快的拿mobx進行大型項目的構建和持續迭代了。

結尾

本文並未涉及過多的代碼細節,對於mobx如何使用也並未闡述。本文着重去解決在使用mobx過程當中可能引起的問題,而且在規範成員的代碼風格方面作了嘗試,使得在用mobx進行項目的開發時能最大限度的保證代碼格式的統一,下降項目的維護成本。 關於如何開發和維護一個大型項目是一個很大的話題,應該在約定或者強制某些規範的基礎上,再根據所處的業務場景進行特定的設計纔可能作好。

相關文章
相關標籤/搜索