強迫症的模組化

發表於2016-07-05

在ES2015釋出後,JavaScript最終也有了一個標準的模組化方案,而同時從webpack開始,也帶來了一波“一切皆模組”的潮流。整個2015-2016的前端發展中,除去在UI層不斷的努力和突破外,幾乎每一件事都和模組化脫不開關係。

本文也試圖從幾個方面簡單地說一下模組化,並分析一些在模組化實施中產生的誤區。

模組的本質

模組化顧名思義,指的必然是將程式拆分為多個“模組”,使用模組間通訊的方式進行互動。這和其它對程式邏輯進行拆分的手段是一樣的,無論是拆成類、拆成物件還是拆成啥,其目的無非是隔離內部的實現以及宣告對外的介面。

事實上自遠古時代起,JavaScript就在努力進行實現封裝的嘗試,無論是通過IIFE避免全域性變數,還是基於Namespace進行管理,以至後期的AMD、CJS社群模組化方案,直到現在的ES Module標準模組化,無不以上為目的。

在模組化的思想裡,一個模組應該是:

  1. 需要一系列的依賴。
  2. 涉及一個內部的實現。
  3. 提供一系列的介面。

一個模組的介面既然是它的結構表達,就如同一個類的屬性和方法一樣,應該是一個在同版本內穩定的簽名。在實際的版本管理中,對於模組應該嚴格遵守semver等版本號規則,當模組對外宣告的介面有所變化時,通過不同層級的版本號的變化以示區分。

從ES Module開始

在ES2015釋出後,ES Module也事實上成為了JavaScript的模組標準。從語法上來看,ES Module與以往社群主導的AMD、CJS等都有不同,主要體現在:

  1. importexport是語法靜態的,即通過語法的解析就可以得到,而不需要實際執行。
  2. import的物件是export,而不是模組本身,在粒度上比AMD和CJS要更細化。
  3. export的是一個引用(或者說Binding),而不是一個值。

ES Module的這些限制大大增加了語言的靜態性,從而也收穫了不少的優勢:

  1. 可以通過語法分析確立模組間的關係,從而為構建等後續工作提供基礎。
  2. 可以應用Tree Shaking等手段進行程式碼的消除。眾所周知JavaScript因為其動態性,導致Code Elimination非常難做,而靜態化的依賴宣告以及依賴粒度為export則很大程度上簡化了這一過程。
  3. 使用引用作為export有效解決不少情況下迴圈引用的問題。

從這一分析中可以看到,語言層面的靜態性是對除程式設計以外的各個工作環節都有很大的幫助的,作為長久以來一直基於JavaScript的弱型別、動態性進行程式設計的工程師,也應該借這次機會重新認識和審視一下型別和靜態性的優勢。隨著應用程式的複雜度的提升,我們的設計應該更傾向於更多使用強型別和靜態性支撐程式的強壯性和可維護性。

全模組化設計

當前另一個流行的概念叫“一切皆模組”,由前端構建工具webpack所引領。在這一概念下,開發者傾向於認為模組化不僅僅應用於JavaScript,任何的資源都可以被抽象為模組,並通過統一的方式(webpack中使用require)進行管理。

然而,在我看來,當前的前端遠沒有達到所謂的“全模組化設計”,更多隻是浮於表面跟隨webpack實現的一種表現而已。

在當前使用webpack的場景中,工程師會在JavaScript中使用require('css!./style.css')宣告對一個樣式的依賴,會使用require('image!./logo.png')宣告對一個圖片的依賴。然後他們會說“我們將樣式和圖片都作為模組來管理了,所以我們是一切皆模組的忠實履行者”。

但是,事實上大家都忽略了幾個很重要的因素。

模組與資源的區別

首先,我們並沒有遵循一個統一的模組結構。如果以ES Module作為模組結構的標準,我們的JavaScript可以很容易地做到宣告依賴和介面,並封裝一個內部實現,但是對於其它型別的資源,我們是否有考慮過作為模組時,它是怎麼樣的?

這就很像我們對RESTful的實施,玩了好多年最後也就是URL長得規則一點,用了用PUTDELETE,而沒有把RESTful的思想應用到系統的設計中去,把好好的一篇博士論文硬是玩成了表面上的無聊玩意,估計是Roy Thomas Fielding博士萬萬想不到的。

舉一個例子,當我們建立一個style.css檔案時,我們在心裡想的依舊是“建立了一個樣式檔案”,而不是“建立了一個樣式模組”,從而我們忽略了對一系列問題的思考:

  1. 這個模組的依賴是什麼?
  2. 這個模組對外的介面(匯出)是什麼?
  3. 模組的依賴通過怎麼樣的語法進行宣告?
  4. 模組的依賴和介面如何支援靜態(非執行時)的分析?
  5. 當前樣式的語法是否足夠我進行模組的宣告,而不是簡單地編寫樣式?

將這些問題落地細化,就可能轉為更實際的內容(css本身語法太弱,此處以less為例):

  1. JavaScript的依賴是到export級別的,因此一個.less依賴另一個.less檔案並不合適,需要細化至selectormixinfunction級別。
  2. 因此一個樣式模組的介面(匯出)應該是一系列的selectormixinfunction,而不是一段文字甚至啥也沒有。
  3. less的語法是不足以宣告這麼細粒度的依賴的,事實上根本不存在一個擴充套件css的語言支援這個,我們需要自己動手。

所以,如果是全模組化的設計的話,我們大概就會再去實現一個CSS超集語言了吧……

如果擴充套件開來,還會有很多有趣的問題,比如:

  1. 一個.ico型別的模組,它的匯出是不是應該是不同尺寸的影像?
  2. 一個模組型別的模組,匯出其實應該是多個Block而不是一個渲染函式?
  3. 作為入口模組的HTML,應該如何宣告對其它模組的依賴?

其中第3個問題尤為有趣,因為在WHATWG的標準裡,一個HTML引入一個JavaScript的模組是這樣的:

可以看到,這裡其實是引入了整個模組,而不是模組的一部分export,這是不符合ES對模組間依賴的定義的。也就是說,其實在WHATWG的標準裡,根本沒把HTML與JavaScript之間的關係作為模組間的關係來考慮,那麼我們在實際實施全模組化設計的時候,又要做怎麼樣的擴充套件才能配合真正的全模組化設計呢?

模組型別的誤區

在實際的模組化實施過程中,另一個很大的誤區就是使用資源型別來決定模組型別。比如只要是.js檔案,一概當作普通的ES Module處理,只要是.less檔案,一概使用css外掛處理。

但實際上,在全模組化設計中,根本不存在“資源”這樣的一個概念,字尾只是URL和檔案系統上的表達,與模組的真正含義並不相關。

以一個實際的場景為例:

在系統中存在一些變數,這些變數在開發環境、測試環境和線上環境有不同的值。

系統建立了一個變數模組,對應的檔案為variables.js。該模組使用多個export提供不同的變數。

到此為止,我們就很容易犯一個錯誤,我們會這樣去引用這個模組:

並不是說這樣會讓系統掛掉,但這絕對不是一個合理的設計。因為在實際的設計中,這個模組的作用是“變數”,而非“程式”,所以它在使用的時候,也不應該被當作程式來引用,正確的方法應該是:

雖然可能只是細微的變化,雖然要為此多寫一個Loader Plugin,雖然可能在實際執行時根本沒有變化,但這意味著我們確實是以模組為粒度進行設計,而不是單純地作檔案-模組的對映。

當然要說優勢的話也不是沒有,在這樣的設計下,我們可以建立variables-dev.jsvariables-qa.jsvariables-online.js等多套變數,通過var這個Loader Plugin控制具體載入的變數集,而不需要使用類似TextReplacer之類基於檔案的工具進行處理,讓整個執行和構建過程也得以靠近模組化。

遠端服務

最後一個相對極端的情況是,我們將前端的所有資源認為模組後,卻忽略了其它遠端服務,而在全模組化的設計中,根本不需要區分前端後端遠端本地,一切均可以是一個模組。

所以我們在程式碼中經常出現的:

其實並不是“那麼的模組化”,不如嘗試變成這樣:

配合模組化的靜態依賴分析等特點,將整個系統全部作為模組來管理後,我們甚至可以基於此對Web API進行管理,比如移除已經不再使用的介面(其實就是Tree Shaking)等工作變得非常簡便。

當然這對於整個系統的設計挑戰是非常大的,即便真的理解這是一個很好的模組化方向,也大概沒什麼工程會照此執行吧。

總結

本文旨在簡單介紹模組化的概念後,重點提出在日常模組化實施中應當被注意的幾個點,包括:

  1. 從ES Module的靜態性出發,重新審視強型別和靜態性對構建應用程式的重要性,在實際設計和實現中更多地利用這些概念提升健壯性和可維護性。
  2. 在全模組化的設計中,將資源的概念摒棄,真正從模組的角度思考每一件事物。
  3. 用模組的型別決定模組的引入,而不是模組對應的檔案或資源的型別。
  4. 除前端自有的資源外,其它外部的服務同樣可用模組化的方式進行定義。

相關文章