日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 前端技术 > HTML >内容正文

HTML

antd 嵌套子表格_大型前端项目架构优化探索之路腾讯文档表格

發布時間:2024/10/8 HTML 73 豆豆
生活随笔 收集整理的這篇文章主要介紹了 antd 嵌套子表格_大型前端项目架构优化探索之路腾讯文档表格 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

騰訊文檔表格是一個非常復雜的業務,它實現了傳統 excel 的大部分核心功能,包括函數計算、條件格式、圖表、智能分列等;除此之外還支持高效的多人協同編輯;它的代碼量百萬級別,啟動也流程多達十幾步。在前端領域,能達到這種規模的項目應該還是比較少的。

騰訊文檔表格業務不僅代碼規模龐大,模塊間依賴關系也很復雜;由于業務的特殊性導致模塊間天然有較強的依賴關系;比如改動一個單元格的內容,可能觸發包括函數計算、圖表渲染等多個模塊的邏輯執行。

由于大部分前端項目相對較小,我們日常開發中可能意識不到大量模塊耦合帶來的問題;往往是直接在代碼里硬編碼依賴關系;比如我使用 A、B、C 三個模塊完成一個功能,只需要手動 new 三個模塊就行了,看起來很簡單。

logic(){ const a = new A(); const b = new B(); const c = new C(); a.do(); b.do(); c.do();}

項目的規模比較小,代碼量很少時,我們的代碼或許整體上看起來還很干凈。就像一個小型機房,只有幾交換機,每臺交換機用網線互相連接;即便我們不對它們進行額外的管理,也不會顯得混亂。

但如果我們管理的是一個大型數據中心呢,還是不對機器之間的連接進行額外的治理,會是怎么樣?對于大型的項目而言,隨著功能模塊的增加,整個代碼庫看起來就像這個數據中心一樣:

針對這種混亂的局面,如果讓我們排查兩個機器之間的連接問題是非常困難的。對于大型項目,我們往往會進行一些初步的梳理。

1. 初步梳理

大型項目的模塊過多時,我們往往會考慮根據功能進行分層;就比如在線表格項目中,我們把模塊分為渲染層、數據層、網絡層、以及 feature 層,當然還包括 worker 里的模塊:

然后對每一個層級,都新建一個單獨的目錄:

+ src + dataLayer subModule ... + netLayer + renderLayer + feature + worker

對模塊進行分層后,再引入全局變量?globalApp,將各個模塊分層次掛載后,

const globalApp = { dataLayer: { subModule... }, netLayer: { subModule... }, renderLayer: { subModule... }, feature: { subModule... }, worker: { subModule... }}

就可以直接通過全局變量引用模塊,就像 node 里的?global超級對象一樣:

logic(){ // 調用數據層子模塊 globalApp.dataLayer.a.doSomething(); // 調用網絡層子模塊 globalApp.netLayer.b.doSomething(); // 調用渲染層子模塊 globalApp.renderLayer.c.doSomething();}

這樣看,模塊的組織貌似還算有條理,很多大型項目都止步于此;在線表格業務在很長一段時間也是這么做的。

但是通過全局?globalApp引用子模塊,隱藏了實際的依賴關系,本來應該直接依賴 subModule,現在變成間接通過 globalApp 依賴了 subModule 了;在不了解系統的全貌的情況下,在改模塊代碼、寫模塊單測時都變得更困難了。我們必須認真的讀代碼邏輯,才知道這段代碼具體依賴于哪個子模塊。除此之外,有些模塊是異步模塊,通過全局 APP 調用異步模塊時,異步模塊可能還沒初始化好,很容易導致時序問題。

2. 依賴注入/控制反轉

為了解決這些問題,我們引入了依賴注入框架來管理依賴。依賴注入的思想在軟件設計領域已經非常成熟,但受限于前端項目規模,可能不少前端開發還沒有在前端項目中實際使用過。

可以這樣理解依賴注入/控制反轉:一個模塊本來需要接受各種參數來構造一個對象,現在只接受一個參數——已經實例化的對象。對模塊來說對對象的『依賴』是注入進來的,而和它的構造方式解耦了。而“構造它”這個『控制』操作也交給了第三方框架,也就是控制反轉。

引入依賴注入框架后,模塊間沒有直接聯系。模塊就像一個個獨立的零部件,放在一個容器里等待裝配。一段代碼聲明了它需要哪些類型(接口)的模塊。然后由框架將模塊裝配起來實現功能。可以將我們聲明的依賴關系理解為一份配置,模塊容器根據這份配置,為我們裝配出各個模塊的實際關系,形成一個系統功能。

通過這種思路,我們即避免了模塊直接強耦合,也解決了手動管理復雜依賴的困境。為了便于理解架構的演進,接下來我們來簡要了解一下依賴注入框架的技術細節。

(1) 依賴聲明與解析

首先我們將所有的模塊放入一個列表中(借鑒至 vscode),每個模塊都有一個 ID 和構造器:

模塊列表 = [ { IA, A }, { IB, B },];

然后使用 Typescript 的參數裝飾器聲明依賴的關系:

class X{ construction(@IA a, @IB b){}}

參數裝飾器為構造函數添加一個元數據屬性,用來保存依賴關系:

X.$$DEPS = [IA, IB]

通過構造函數元數據中保存的依賴關系,查找模塊列表就能解析出一個模塊的依賴關系圖:

根據模塊間聲明的依賴關系,進行多次查表,我們就能解析出整個系統的依賴關系圖。而依賴圖解析完成后,按深度優先遍歷的順序實例化,就能保證初始化時序的正確性。

通過聲明式依賴,保證了依賴關系的清晰;框架負責注入依賴實例,保證了模塊的解耦,提高可測性和可維護性;框架按依賴圖初始化,避免了時序問題。

(2) 延遲初始化模塊實例

當模塊所依賴的對象實例都是由框架注入,我們還可以做一些有趣的優化:延遲初始化。對于騰訊文檔在線表格業務來說,無論有無編輯權限,大部用戶打開表格都是為了查看表格,而不會進行編輯。如果大部分用戶不編輯表格,我們可以利用依賴注入框架將與編輯功能有關的模塊,統一延遲到真正使用時再初始化。比如在線表格業務中的 undoredo 棧,在用戶沒編輯時就不會初始化,從而達到一定的內存治理的效果。

延遲初始化是如何實現的呢?undoredo 模塊的實例是由依賴注入框架提供的,我們可以先注入一個 Proxy 占位:

然后通過 Proxy 攔截實例的屬性查找、函數調用,這樣就能做到在真正獲取屬性實例或調用實例的方法時再初始化實例。

const undeoProxy = new Proxy(Object.create(null), { get(target, key) { // 沒有實例時,先創建實例 if (!this.instance) { this.instance = di.create(Ctor); } // 返回已經緩存的實例 return this.instance[key]; },});

這套延遲初始化方案,對模塊本身是透明的,完全由依賴注入框架通過參數配置有選擇的開啟。

(3) 異步依賴管理

在線表格代碼量在百萬級別,模塊有上千個;而要在單個頁面中加載一百萬行代碼,我們必須對模塊進行異步分批加載。上文提到的依賴注入思路只能支持同步模塊,顯然無法滿足我們的業務需求。舉一個我們業務場景中實際的例子:頁面在執行到某個生命周期階段時,需要加載插件系統,而插件系統會加載 workbench 模塊,workbench 上的文件導出模塊可能在用戶正在點擊時才觸發加載。

針對這種多層嵌套的異步依賴關系,我們該如何有效的管理呢?為此框架提供了一個通用 loader,通過 loader 就能將一個同步模塊包裝成異步模塊。loader 負責兩件事:

  • 加載異步模塊

  • 解析異步模塊的依賴并初始化

  • 假設一個系統中,C 模塊異步的依賴于 D 模塊,那我們只需要聲明 C 對 dLoader 的依賴:

    class C { constructor(@Id d: dLoader){} //C模塊聲明對dLoader的依賴 test(){ const d = dLoader.getInstance(); //加載并初始化D模塊 }}

    則依賴關系圖如下:

    在調用?dLoader.getInstance時會觸發對 D 模塊的加載,同時解析 D 模塊本身的依賴關系,然后再初始化 D 模塊。假設 D 模塊本身又依賴于 E 模塊,則解析出的 D 模塊依賴關系圖如下:

    以 X 為根節點的依賴圖包含了 dLoader,但對 D 的依賴圖無感知;只有 dLoader 知道 D 模塊的依賴圖;可以通過 dLoader 作為橋梁,將兩部分依賴圖鏈接起來,形成包含同步模塊和異步模塊的復雜的依賴關系圖:

    通過引入 Loader,我們屏蔽了同步和異步模塊的實現細節;一個模塊是同步還是異步,只需要在聲明依賴和注冊模塊時稍有差異。多個 Loader 就可以組裝成任意多層嵌套的異步依賴關系圖;通過支持同步和異步模塊的隨意組合,我們就能駕馭真實的復雜場景,而如果要硬編碼維護這樣復雜的多層嵌套異步依賴關系,是非常容易出問題的。

    至此,我們已經探索出一條有效的模塊依賴關系治理之路。但架構的優化不會止步于此,業務對架構又提出了新的訴求。

    3. 模塊分層

    什么是模塊分層呢?對在線表格來說,我們將系統分為幾個層級:

    Core 層提供核心的能力;只讀層調用 Core 層的能力實現只讀功能;相應的,可編輯層調用只讀層和 Core 層的能力,實現可編輯功能

    分層的好處是:理想情況下,我們可以隨意的替換掉外層,來提供不同的產品能力;而要隨意的替換外層,必須保證外層模塊不影響內層模塊的功能,這就需要做到:

    外層模塊單向依賴于內層模塊,內層模塊不能依賴外層模塊

    因為內層模塊如果依賴于外層模塊,替換外層就會破壞內層的功能。保證模塊分層單向依賴后,假設一個第三方的 APP 只需要在線表格的只讀功能,那么我們只需要拿掉可編輯層,封裝一個 SDK 提供給第三方 APP 就行了。

    為了應對這種訴求,框架也支持多層容器,可以將模塊放在不同層級的容器中。比如模塊 C 放在可編輯層容器,模塊 B 放在只讀層容器,模塊 A 放在 core 層容器。

    外層容器通過 parent 指向內層容器。

    editableCollection.parent = readonlyCollection;readonlyCollection.parent = coreCollection;

    而在依賴解析時,遞歸查找父容器

    function inCollection(collection, id) { if (colleciton.has(id)) { return true; } else if (colleciton.parent) { //遞歸的查詢入容器 return inCollection(colleciton.parent); } return false;}

    通過這種單向的遞歸依賴解析,我們就做到了層級間模塊單向可見:

    inCollection(editableCollection, A) === true;inCollection(coreCollection, B) === false;

    預先將模塊放入不同層級的容器中,對于違背單向依賴規則的模塊則拋出異常

    比如我們的 A 模塊是負責圖表繪制的核心模塊,而 B 模塊是負責錯誤上報的非核心模塊(與具體的日志平臺耦合)。理論上來說,為了保證 A 模塊的高可用性,A 模塊不應該依賴于特殊的業務模塊,即錯誤上報模塊 B。而在一個代碼庫中的模塊,如何防止這種情況出現呢?傳統的方法是?CodeReview,而最好的辦法就是利用容器分層單向依賴關系,在運行時拋出異常,強制業務開發不能這么使用。

    4. 模塊生命周期管理

    模塊的生命周期包括創建、銷毀、清理。大部分其他架構只考慮根據依賴關系創建模塊,極少考慮到模塊的清理、銷毀、以及銷毀后的重建。接下來我們繼續探索一下我們的框架是如何管理模塊的銷毀和清理的。

    為什么需要管理模塊的生命周期呢?對應我們的要業務來說:比如刪除子表時,對應子表的實例就應該可靠的銷毀,否則會有內存泄露的問題。

    (1)模塊實例的銷毀

    模塊的銷毀就是直接刪除模塊的實例。我們首先考慮模塊的銷毀難在哪里?假設一個模塊依賴圖,包括了復雜的同步、異步依賴組成的關系網。

    我們要保證銷毀部分模塊后,還能重建整個依賴關系網,系統功能還是正常的。比如標紅的 D 和 K 模塊,它們都是要被銷毀的模塊,但是它們都被其他模塊引用了。模塊被引用,如何可靠的銷毀呢?

    一種解決思路是:對于需要銷毀的模塊,我們生成一個 Scoped Wrapper,其他模塊通過 Wrapper 引用該模塊,保證模塊的真實實例沒有被外部直接引用,這樣就能安全的銷毀掉模塊實例。銷毀后,其他模塊通過 Wrapper 調用該模塊的方法時,會觸發通過 DI 框架重新初始化該模塊,從而達到安全的重新初始化。

    這樣就做到了:

    只銷毀掉依賴關系圖中的部分模塊的實例,同時保留了其他模塊的實例。

    (2)模塊實例的清理

    模塊實例的清理往往是為了清理實例的狀態,這些狀態包括:設置的 timer、pending 狀態的 Promise、插入的 DOM 節點、自定義的事件等。

    假設我們有 a、b、c、d、e、f、g 六個模塊實例需要清理。

    一種常見方法是:各個模塊是平級關系,全局拋出一個事件(dispose),各個模塊都監聽這個事件,執行自己的清理工作,各人自掃門前雪。

    onDispose(()=>{ a.dispose();});...onDispose(()=>{ f.dispose();});

    這種方式對業務模塊入侵較大,每個實現模塊的都必須監聽一個全局事件;此外,清理工作分散在各個模塊中,很容易因為疏忽導致泄露。

    另外一種方案是:各個模塊實現統一的清理接口,負責自身狀態的清理;同時把自己的清理接口的調用委托給父節點,形成一個清理樹:

    這樣只需要根節點調用一次清理方法,就能完成整個子樹的清理工作。這種方案使得清理工作更加結構化,但需要我們手動編碼維護這種清理樹;框架提供了基類,只需要簡單的調用就能維護這棵清樹。

    無論框架提供多么高的便利性,還是需要我們手動編碼組織起這塊清理樹。如果某些節點沒有將自身清理工作委托給父節點,就會導致子樹的泄露。

    為此,框架提供了一種簡單的開發階段的泄露檢查機制,判斷發生泄露時,會觸發異常;進而提醒開發關注,而通過異常堆棧就可以快速定位到具體那里的代碼存在泄露問題。結構化清理機制支持 trace 日志,可以清晰的看出銷毀樹的層級,方便開發調試。

    dispose IInstantiationService dispose -> IC dispose -> IInstantiationService dispose -> IB dispose -> IA dispose -> Ainstance

    (3)模塊重用的綜合效果

    我們希望在切換不同的表格時,能夠復用 webview 和已經加載的模塊,而不是每次都重新加載整個頁面。可以將頁面理解為一個容器,同一個容器,清理狀態,就能承載不同的表格數據。 容器化重用的核心就是銷毀和清理模塊。

    清理 container 的狀態就是指:清理模塊實例的狀態、銷毀模塊實例、并重用模塊

    通過架構對模塊全生命周期的管理,我們可以對模塊進行可靠的復用,從而達到非常好的優化效果,在模塊復用的情況下進行切換表格,可以快速展示表格內容。

    總結一下業務驅動的架構探索之路:

    • 管理復雜的依賴關系

    • 支持異步依賴關系管理

    • 支持模塊分層單向依賴

    • 支持模塊全生命周期管理

    架構的探索最終都反應在依賴注入框架的演進上。我們也已經將這套依賴注入框架部分核心能力從業務中抽離出來,形成 npm 包,目前團隊中的其他項目也在嘗試接入中。

    關于AlloyTeam

    AlloyTeam 是國內影響力最大的前端團隊之一,核心成員來自前 WebQQ 前端團隊。AlloyTeam負責過WebQQ、QQ群、興趣部落、騰訊文檔等大型Web項目,積累了許多豐富寶貴的Web開發經驗。這里技術氛圍好,領導nice、錢景好,無論你是身經百戰的資深工程師,還是即將從學校步入社會的新人,只要你熱愛挑戰,希望前端技術和我們飛速提高,這里將是最適合你的地方。加入我們,請將簡歷發送至 alloyteam@qq.com,或直接在公眾號留言~期待您的回復?

    總結

    以上是生活随笔為你收集整理的antd 嵌套子表格_大型前端项目架构优化探索之路腾讯文档表格的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。