前端模塊化發展簡史

前端發展日新月異,短短不過 10 年已經從原始走向現代,甚至引領潮流。網站逐漸變成了互聯網應用程序,代碼量飛速增長,爲了支撐這種需求和變化,同時兼顧代碼質量、降低開發成本,接入模塊化勢在必行。伴隨這一變化的是相對應的構建工具的快速成長,或是爲了優化、或是爲了轉義,都離不開這類工具。

所謂溫故而知新,本篇回顧總結下前端模塊化的發展歷程及輔助工具。在回顧中可以更清晰的看到當前我們用的方案所處的位置,爲什麼會發展到這一步,目前模塊化方案帶來的優勢等。

1. 沒有模塊化的日子

最開始 JavaScript 承擔的任務量並不多,表單驗證基本上就是他的全部,最多就是簡短的前端交互,這個時期 JavaScript 組織結構非常凌亂,大部分都是後端哥哥們順手代勞,那時候還沒有“前端”這一職位。 一般都是寫到一個文件或者直接寫到 jsp、asp 的後端模板頁面上就完事了。這個階段沒啥可說的,跳過吧。。。

2. 傳統模塊化

隨着 ajax 的流行,前端能做的東西一夜之間暴漲,代碼量飛速增加,單文件維護代碼已經太沉重,於是拆之,進而引入模塊化,將負責不同功能的代碼拆分成小粒度的模塊,方便維護。

這裏要說的模塊化是拋開現在你所熟知的 require,amd,seajs 等,不借助任何的模式和工具,由 JavaScript 直接完成的代碼結構化。JavaScript 天生沒有模塊化的概念(直到 ES6), 而不像後端語言源生自帶模塊功能, 比如 Java 的 import、C++的 include、Node 的 require(下文說到),所以需要通過其他的方式來實現模塊化。

應用模塊化開發的主要目的是爲了複用代碼、代碼結構清晰、便於維護等,比如在開發過程中,我們往往會將一些重複用到的代碼提取出來,封裝到一個 function 裏,然後在需要的地方調用,那麼這可以看做是一種模塊化。

我們看一段代碼 例 – 1:

function show(element) { // 展示一個元素 }
function close(element) { // 隱藏一個元素 }

上面的代碼非常直觀,就是要顯示、隱藏一個 dom 元素,往往這種方法需要大範圍多次調用,一般我們可能會放到 util.js 這樣的文件裏,這是第一步。接下來在業務代碼中引用,例 – 2

<body>
    <script src="lib/utils.js"></script>
    <script src="lib/page-1.js"></script>
    <script src="lib/page-2.js"></script>
</body>

2.1 存在的問題

以現在的經驗來看,上面的寫法會帶來非常明顯的問題,當然也是在這個模塊化引入階段逐步暴露的。

  • 全局變量衝突風險:如果編寫 page-1 的同學不知道 utils 裏面有一個 show/close 方法,然後他自個也寫了一個,同時還添加了額外邏輯,自然就覆蓋了原來的方法,那麼 page-2 同學在不知道的情況下調用了這方法,自然會發生錯誤.
  • 人工維護依賴關係:因爲存在依賴關係,所以必須先加載 util,然後才能加載 page-1/2,這裏的例子非常簡單,但在實際項目場景了,這樣的依賴會非常多且複雜,維護非常困難,很難建立清晰的依賴關係。想想當時高大上的校內網,那種程度的頁面得需要多少的模塊去支撐。後續項目迭代往往會帶來意料之外的問題.

2.2 嘗試解決問題

針對問題 1,可以做下面這些改進:

2.2.1 代碼提取

把這些方法放到一個 object 裏面對外輸出,例 - 3

var utils = {
    _name: ‘baotong.wang’,
    show: function(element) {},
    close: function(element) {}
}

但這樣依然不能避免我們的 utils 被覆蓋的可能性,孱弱的英語積累讓我們想不出什麼更高級的詞來命名 utils,var fuzhufangfa = {}?。。。

不過這種寫法同時還帶來了暴露內部變量的問題,外部可以訪問到 _name。

2.2.2 命名空間

然後部分開發者引入了命名空間,這個東西牛逼了,例 – 4:

var com.company.departure.team.utils = {}

代碼模塊通過嚴格的命名規則做了規範,可以按照實際情況具體到部門、team、類庫。如果一個公司在代碼規範上做了這樣的約束,基本上可以避免變量名衝突的問題,但同時帶來的需要輸入過多單詞的負擔,目前還沒有哪個 IDE 能支持 JavaScript 像 Java 一樣可以一路點點點下去,這些都是需要打出來的。當然我們也可以不設計成這麼複雜的命名空間,var Company.ProjectName.Module = {}; 同時結合局部變量減少輸入的長度。

2.2.3 閉包封裝

爲了解決封裝內部變量的問題,就該有請立即執行的函數登場了,這也是我們接觸的最多的一種模塊化方式,公司內部有點年紀的項目多少都能看到這樣的寫法,結合命名空間如下,例 – 5:

(function() { 
   var Company = Company || {};
   Company.Base = Company.Base || {};

   var _name = ‘baotong.wang’

   function show () {}
   function close () {}

   Company.Base.Util = {
     show: show,
     close: close
   }
})();

上述寫法通過一個立即執行的函數表達式,賦予了模塊的獨立作用域,同時通過全局變量配置了我們的 module,從而達到模塊化的目的。基本上到這一步,問題 1 就解決了。

2.2.4 關於依賴關係

針對問題 2,代碼的組織依賴關係,這塊我不是很瞭解,向司徒求證了一下。大概情況如下。

當時業界也是有不少方案的,比如百度的 Tangram 與 Qwrap,查了下他們的 github 地址,最後一次更新是在五年前。它解決依賴關係的方式是在類庫中什麼依賴,類似 depend=[“com.qunar.dujia.lib”, “”, …],然後通過配套的工具去解析。

同時也有一些後端大牛爲了解決前端工程化的問題發明創造了各種方案,但當時的氛圍並沒有現在這麼重視前端,前端從業人員的水平也沒現在高;同時後端哥哥們往往對前端問題、痛點了解的不深入,所以開發出來的方案很難推廣。比如搞 Java 和搞 Ruby 的後端做的方案基本不太會一樣。 用司徒的話來講叫生不逢時。

2.3 這個時代的工具

2.3.1 代碼合併

例 – 2 中的代碼引用方式相信肯定存在於一些站點上。雖然不會帶來功能問題,但是卻帶來了很多不必要的 http 請求,特別是複雜頁面需要引用很多獨立 JavaScript 的時候,從而延長了頁面的 ready 時間。所以這裏需要合對文件進行合併處理,將可以合併的業務代碼連接到一個文件裏。

需要注意的是,合併並不是所有的文件合併爲一個爲好,比如公共文件 jquery 文件、功能公共方法可以單獨引用,利用瀏覽器的緩存機制,減少多頁面情況下總的下載量。如果站點一共就是一個 SPA,合併爲一個爲好。

2.3.2 代碼混淆壓縮

另外一個就是代碼壓縮,現在的同學對這個肯定非常熟悉了,但是即便現在找一個你熟悉的網站看一下,也不敢說一定做到了這一步。

走到這有了這兩步,網站看起來就挺像那麼回事了。

2.3.3 代表性工具

YUI compressor,出自雅虎,在那個時期雅虎可以說是網站優化的風向標,同樣出自雅虎的前端優化 34 條(數量不同版本不一樣)在業界也是鼎鼎大名,爲前端做出了很大貢獻。

這個時候適合模塊化的通用工具並未出現,相信有實力的大公司都有內部的一條工具去做類似的事情,這裏個人所知有限,沒啥發言權,歡迎大家交流討論。

3. Node 來了

2009 年,node 的發佈給前端同學帶來了無限可能,npm 生態的逐漸成熟給了我們更多選擇,以往需要通過其他語言工具執行的編譯過程也可以由前端一手接管。同時 node 也帶來了 commonJS,給前端的模塊化提供了新的思路,我們這裏首先關注 node 實現的 commonJS 規範。

3.1 commonJS 概述

作爲後端語言,沒有模塊化加載機制是運轉不起來的,node 選擇實現了 commonJS 作爲它的模塊加載方案,整體非常簡單。注:commonJS 並不是 node 發明的,他只是按照該規範做了一套實現。

3.2 npm 生態

npm 生態讓 node 有了自己的模塊倉庫,各種類庫的不斷支持讓我們也有了更多選擇。commonJS 一開始就提供了對 npm module 的支持,在路徑查找的時候內部配置了對 node_modules 文件夾的查找支持。

3.3 說說 node

對前端來說幸運的是 node 的設計者 Ryan Dahl 選擇了 JavaScript 作爲他的支持語言,這也說明了 JavaScript 事件驅動的魅力所在。大批後端的加入豐富了作爲一門後端語言的各種基本功能。

對於前端同學來說 node 有着天然的親和力,讓我們多了一個全新施展本領的領域;同時對於懂後端的同學來說視乎可以大幹一場了。目前我們用的最多的有兩部分,node 佈置站點、數據接口集成維護,這個和本篇沒啥關係,不展開說;另外一部分就是利用 node 開發工作工具,提高前端的工作效率,社區裏解析 commonJS 的、構建工程工具不斷噴湧而出, 具有代表性的有 grunt、gulp、browserify,webpack,前端模塊化可以更進一步。

4. 模塊化方案

4.1 commonJS

簡單概括下 commonJS 的幾個概念,還是非常簡單的

  • 每個文件是一個模塊,有自己的作用域。這裏面定義到函數、變量、類都是私有的,對其他文件不可見;
  • 每個模塊內部,module 變量代表當前模塊,它是一個對象;
  • module 的 exports 屬性(即 module.exports)是對外的接口;加載某個模塊,其實是加載該模塊的 module.exports 屬性如果文件中沒有 exports 屬性,那麼外部引用不到任何東西;
  • 使用 require 關鍵字加載對應的文件,也就是模塊;
  • require 命令的基本功能是,讀入並執行一個 JavaScript 文件,然後返回該模塊的 exports 對象,如果沒有發現該模塊,報錯。

這裏對上面的代碼做了模塊化的改進,文件內部設置的對外輸出,下面的寫法在 node 環境中源生支持。

// 設置文件輸出
module.exports = { 
func: function() {},
field: "string"
}
// 添加單個 export
module.exports.show = function() {}

//這裏引入幾個模塊、文件
require("modulepath"); 
var Base = require("../base.js");
var page = require("./file.js");

page.show();

4.2 其它模式

除了 commonJS,當前留下的模塊化模式還有以 requireJS 爲代表的 AMD 和以 seaJS 爲代表 CMD, 在去哪兒網內部始終是 commonJS 佔據主流,我個人也是喜歡 commonJS 更多一些,requireJS 可以做到在瀏覽器端執行動態異步模塊加載,僅從首次代碼下載量的角度講,這種方案更好一些,但我們完全有其他辦法在 commonJS 模式下解決這有個問題,所以本篇主要介紹 commonJS 和編譯工具支持的一個思路

4.3 代碼改造

基於 commonJS,回過頭來再看下例 -5 中的代碼應該怎麼改造。

  • 首先,那層閉包可以不用加了,你沒看到有誰在 node 裏面加這個東西吧,這層其實還是需要有點,但是我們交給工具自動幫我們加上。
  • 其次,我們需要在模塊內部寫上對外輸出的內容,module.exports = *;
  • 然後,在業務代碼中添加對模塊的引用,var module = require(“modulepath”), 有了這個之後就能引用 module export 出來的功能了
  • 最後,通過打包工具的編譯,解析 commonJS,分析入口文件得到最終輸出。

最終得到的代碼如下:

// module.js
var _name = 'baotong.wang';

function show() { alert(_name); }
function close() {}

module.exports = {
    show,
    close
}

// page.js
var module = require('./module.js');
module.show();

4.4 瀏覽器端支持

commonJS 是服務器端的模塊化方案,瀏覽器端是不支持的,單是 require 就沒有,所以就需要輔助工具來替我們完成 commonJS 代碼向瀏覽器代碼的轉換。

社區成熟的解析類庫有 browserify,能夠完美解析 commonJS;因爲公司內部業務的特點需要,browserify 並不能滿足實際需求,因此去哪兒網內部先後推出了 fekit、ykit 兩款針對 commonJS 的前端工具,來執行代碼的編譯。前者是自己實現的一套解析 commonJS 的工具集,對一些規範的實現不是很規範,同時面向的是內部的 module 倉庫,導致和主流 npm 環境脫節,於是有了 ykit;後者是基於 webpack 和公司業務特點封裝的一個工具集,核心打包交給了 webpack,同時做了部分優化,具體前面發過一篇文章,介紹過實現機制。

在這我說下 fekit 的編譯過程,介紹下這個工具處理 commonJS 的一般思路。

4.5 fekit 編譯過程

fekit 是一個基於 node 的命令行工具集,在支持 commonJS 的過程中也做了一些修改和擴展,比如支持在 css 文件中通過 require 加載文件,做到和 JS 文件一樣;增加對內部 module 倉庫的支持,下圖介紹了一次 pack 的具體執行過程。下面的流程適合模塊解析相關的部分,其他業務構建部分在這裏跳過。

module 處理模板代碼

;(function(__context) {
    var module = {        
        id : "{{md5Key}}" ,        
        filename : "{{fileName}}" ,        
        exports : {}
    };    

   if( !__context.____MODULES ) { 
        __context.____MODULES = {}; 
    } 

   var r = (function( exports , module , global ) { 

       //----------原始文件代碼----------
        {source}        
       //----------原始文件代碼----------

    })( module.exports , module , __context );

    __context.____MODULES[ "{{md5Key}}" ] = module.exports;

})(this);

在業務代碼中的 require 會變成,即通過一個 object 拿到 module.exports。 var module = context.__MODULES[“md5Key”]; 以上就是對一個 commonJS 文件的解析過程了。

4.6 ES6 的模塊化方案

ES6 中給出了 import export 這樣的方案,目前爲止我們都是通過 babel 將 ES6 代碼轉爲 ES5,import 轉爲了 require,export 轉爲了 module.exports,即 commonJS。

他的實現原理和 commonJS 這種引用即引用整個類不一樣,它是用啥就引用啥,export 輸出的也不是一個類,這裏往下說就比較多了,阮一峯老師的 ES6 教程對這塊也有比較詳細的說明。限於篇幅,本篇不針對這個展開來說了。

5. 總結

本篇簡單回顧了模塊化的發展歷程,介紹了以往存在的問題。然後到現代模塊化方案的時候,講解了 commonJS,同時介紹瞭解析 commonJS 的一種方法。通過一個例子串連,講述了模塊化帶來的改變。部分知識點沒展開來說,大家有興趣可以深入學習一下。同時感謝司徒指點,希望本篇能對大家有所幫助,best regards。

本文來自「Qunar 技術沙龍」,作者王寶同,2014 年加入 Qunar,在旅遊度假事業部擔任前端工程師。擅長代碼結構設計與優化,喜歡研究構建工具,折騰 Node。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章