開發與維運

低成本可複用前端框架——Linke

業務背景

目前團隊內的開發模式多是面向組件的,UI層和邏輯層均強耦合在一起,由於業務的差異性,往往很難完全複用。

  • 閒魚前端業務處在高速發展不斷嘗試的階段,如何能更快更穩定地完成需求,更好的支撐業務發展絕對是一個值得探索的問題。
  • 在接手一個複雜的老業務代碼時,經過較多人的修改,往往可維護性較差,有時只想修改某個小地方卻需要較大的理解成本,所以用一套統一的組件開發規範在長期維護中顯得格外重要。
  • 閒魚技術體系經歷了從weex、rax0.x到現在rax1.x的變更,中間有過一些前端資產的積累,但是由於遷移的成本後期都不再維護,如何用更小的成本讓業務層平穩過渡到新的技術體系?

對於以上的問題我們希望能用框架一併解決,對於該框架的目標主要包括:

  • 提高代碼可複用性
  • 規範代碼,降低長期維護成本
  • 降低業務層與技術體系的關聯

思路

關於提效,其中比較重要的是相同的代碼不要重複寫,做更細的區分和提取,提高可複用的顆粒度。另一方面是解決現有開發下比較影響開發效率的問題。

組件的分層

所以我們將面向組件的開發模式分為UI層View和邏輯層Store,以Interface進行隔離和耦合。
O1CN012gVJDO1ilWLsqz0G7_!!6000000004453-2-tps-668-566.png

圖一:組件構成
在UI層無需關心狀態的流轉,只負責展示和交互方法的調用,DOM相關的動畫交互等行為邏輯也會放到該層中。
O1CN015V035g1jCXiiVf8XT_!!6000000004512-2-tps-815-340.png
圖二:組件分工

在確認了分層的邏輯後自然就引入了Interface,主要分為兩部分:一部分是IProps,申明該組件所需的Props,在使用者調用該組件時進行對應的提示和約束;另一部分則負責連接Store和View,其中包括狀態state和交互方法;見下面的Interface示例:

export interface IMultiScrollerProps {
  tabs: string[];
  onTabChange?(i: number): void;
}

export interface IMultiScroller extends IBase {
  readonly tabIndex: number;
  readonly tabSource: ITabItem[];
  readonly children: any[];

  onSwiperChange(i: number): void;
}

總結一下:所有的state和交互方法都在store中管理,供View消費;View中只負責和dom相關的邏輯操作,View和store的職責分界線就是View和store分別單獨使用時其交互和效果都能保持不變;以此實現View和store分別能有更多的複用。

狀態管理

現有的業務開發中基本所有的需求都是基於hooks的狀態管理,主要存在以下問題:

  • 對於較複雜的組件hooks在多次迭代後的維護成本會非常高;

    • 有時候,你的useEffect依賴某個函數的不可變性,這個函數的不可變性又依賴於另一個函數的不可變性,這樣便形成了一條依賴鏈。一旦這條依賴鏈的某個節點意外地被改變了,那麼useEffect就被意外地觸發了後面的情況就會變得不可控。
  • 異步陷阱

    • 狀態的修改是異步的

useState返回的修改函數是異步的,並不會直接生效,所以此時讀取該值獲取到的是舊值。要在下次重繪才能獲取新值。不要試圖在更改狀態之後立馬獲取狀態。

const [value, setValue] = useState(0);
setValue(100);
console.log(value); // <- 0
  • timeout指向的是舊值

timeout指向的是舊值,即使在外部已經重新設置,由於閉包所有在setTimeout中獲取到的都是之前的值。

const [value, setValue] = useState(0);
window.setTimeout(() => {
  console.log('setAnotherValue', value) // <- 0
}, 1000);
setValue(100);
  • 何時使用useCallback/useMemo等對於新手來說存在一定的門檻。

關於 Hook 中的閉包:
useEffect、useMemo、useCallback都是自帶閉包的。也就是說,每一次組件的渲染,其都會捕獲當前組件函數上下文中的狀態(state、props)。所以每一次這三種 Hook 的執行,反映的也都是當時的狀態,無法獲取最新的狀態。對於這種情況,應該使用 ref 來訪問。

對於狀態管理react體系中最受歡迎的應是redux與mobx
O1CN017iUcSC1jlLoxLkcqA_!!6000000004588-2-tps-960-1013.png
圖三:Redux Flow

redux的特點從上圖可以總結得到下面的三大原則:

  • 單一數據源
  • state 是隻讀的
  • 使用純函數來執行修改

但是redux的問題也是十分明顯的:開發者需要寫更多附加的樣板代碼,並且留下更多需要我們維護的代碼。

與 Redux 相似的,另一個狀態管理方案是 MobX:
O1CN01RXiBaY1uRA8bF83xA_!!6000000006033-2-tps-1280-439.png
圖四:Mobx Flow

相比 Redux 的強規則約定,MobX 更簡單靈活,核心原理是通過 action 觸發 state 的變化,進而觸發 state 的衍生對象(Computed value & Reactions)。開發者只需要定義需要 Observe 的數據和由此衍生的數據(Computed value)或者操作 (Reactions),剩下的更新就交給 MobX 去做就可以了。一句話總結就是:

任何源自應用狀態的東西都應該自動地獲得。

分析閒魚的業務特色並不存在5個以上同學同時維護一個項目的超大型需求,強約定的redux對我們來說收益有限,而MobX 確實比 Redux 上手更容易些,並且不需要寫很多樣板代碼,可以提供更高效的選擇。

實現

我們給框架取名:Linke,來自switch的遊戲塞爾達,希望它能像林克一樣點亮一個個神廟。
基於上面的分析思路結合實際業務中的技術體系(Rax)最後我們設計了下面的研發體系:
UI部分也就是View還是沿用原有的Rax,UI用到的狀態也直接在View中管理。
業務邏輯部分也就是Store用Mobx的能力解決上面提到的現有hooks開發遇到的問題,兩者沒有強關聯。
Linke做為中間耦合層對他們進行約束和橋接。
O1CN01QZKwIz1ZEhQbUQCW0_!!6000000003163-2-tps-1524-886.png
圖五:基於Linke的研發體系

API

為保證開發者最低的學習成本,Linke在設計時儘可能地減少API,最終只有一個方法和4個Store內置方法,詳見:

observer(baseComponent, Store)

保證組件能響應store中的可觀察對象(observable)變更,即store更新,組件視圖響應式更新

Store內置方法

  • 成員方法 - $$set: 所有狀態變化必須通過$$set來完成,與微信的setData()類似
  • 成員方法 - $$setProps:處理外部傳入的組件props,View初始化或者props發生變化時調用
  • 成員方法 - $$didMount:提供View的生命週期,View被插入DOM時調用
  • 成員方法 - $$unMount:提供View的生命週期,View被移除DOM時調用

可以看出Store內置方法中除了$$set其他三個都是生命週期方法,其調用順序為:$$setProps -> $$didMount -> $$unMount

demo

Interface.ts

O1CN01IWWQRb1XnOC7CctNM_!!6000000002968-2-tps-659-289.png

index.tsx

O1CN01H4t5tp1I20zIxSWLj_!!6000000000834-2-tps-545-386.png

store.ts
O1CN01oByjRk1Q9MGcBvHLP_!!6000000001933-2-tps-664-876.png

上面就是一個完整的組件demo。

對比

現在的組件開發模塊模式如下圖六所示,以組件為單位所有的邏輯是耦合在一起的,相互之間沒有分界,即便是相同的樣式也很難實現複用。
無論是在代碼理解還是二次開發上都存在較大的成本和不穩定性風險。
O1CN01XH7HE61cZG3LIc8kd_!!6000000003614-2-tps-802-644.png
圖六:原組件的開發模式

基於Linke的組件開發模式如下圖所示:
O1CN01Qk0pnK1lfW0Su8Vnd_!!6000000004846-2-tps-908-674.png
圖七:基於Linke的組件開發模式

View和Store相對獨立沒有強耦合性,這樣的好處顯而易見:
• 通過閱讀Interface就能知道Store/View的基本邏輯,減少理解成本
• 數據邏輯和View邏輯分別在Store和View中管理,真正實現各司其職,減少維護成本。
• 最重要的一點是通過分離讓Store和View分別實現了複用,組合不同的Store/View生成不同的組件
O1CN01IaT1c727ieGHkhovg_!!6000000007831-2-tps-1500-596.png
圖八:Store分別和不同的View組合

O1CN01GIgZsq1pUIlzgiP6U_!!6000000005363-2-tps-1500-587.png
圖九:不同的Store和同一個View組合

應用

目前Linke已經應用在了閒魚前端各個新項目中,包括2個線上項目和3個正在開發的項目收益明顯,什麼功能的代碼在什麼位置一目瞭然配合Interface中的註釋大大減少了接手項目的理解成本。
通用基礎組件和業務組件都在有序的抽離中,同時隨著View/Store庫的不斷豐富,可以複用的物料資源增加,不同業務和同一業務不同場景中可以複用的View/Store越來越多,在一定程度上大大減少開發成本提高效率。

下一步

目前新財年除了現有的H5業務外,最大的特點是會對各個小程序做一些流量探索,比如淘系輕應用、微信小程序、支付寶輕應用等,這些應用的特點是與端內的H5業務及其相似,但是會有各自的細微差異。所以我們也在探索基於Linke對此類業務場景的提效。

Leave a Reply

Your email address will not be published. Required fields are marked *