Javascript模組化的演進歷程

騰訊IVWEB團隊發表於2018-07-19

ES2015 在2015年6月正式釋出,官方終於引入了對於模組的原生支援,如今 JS 的模組化開發非常的方便、自然,但這個新規範僅僅持續了3年。就在7年前,JS 的模組化還停留在執行時的支援;13年前,通過後端模版定義、註釋定義模組依賴。對於經歷過的人來說,歷史的模組化方式還停留在腦海中,久久不能忘懷。

為什麼需要模組化

模組,是為了完成某一功能所需的程式或者子程式。模組是系統中“職責單一”且“可替換”的部分。所謂的模組化就是指把系統程式碼分為一系列職責單一且可替換的模組。模組化開發是指如何開發新的模組和複用已有的模組來實現應用的功能。

那麼,為什麼需要模組化呢?主要是以下幾個方面的原因:

  • 傳統的網頁開發正在轉變成 Web Apps 開發
  • 程式碼複雜度在逐步增高,隨著 Web 能力的增強,越來越多的業務邏輯和互動都放在 Web 層實現
  • 開發者想要分離的JS檔案/模組,便於後續程式碼的維護性
  • 部署時希望把程式碼優化成幾個 HTTP 請求

模組化的演進歷程

直接定義依賴(1999)

最先嚐試將模組化引入到 JS 中是在1999年,稱作“直接定義依賴”。這種方式實現模組化簡單粗暴 —— 通過全域性方法定義、引用模組。Dojo 就是使用這種方式進行模組組織的,使用dojo.provide 定義一個模組,dojo.require 來呼叫在其它地方定義的模組。

我們在dojo 1.6中修改下示例:

// greeting.js 檔案
dojo.provide("app.greeting");

app.greeting.helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

app.greeting.sayHello = function (lang) {
    return app.greeting.helloInLang[lang];
};

// hello.js 檔案
dojo.provide("app.hello");

dojo.require('app.greeting');

app.hello = function(x) {
    document.write(app.greeting.sayHello('es'));
};
複製程式碼

直接定義依賴的方式和commonjs非常類似,區別是它可以在任何檔案中定義模組,模組不和檔案進行關聯。而在commonjs中,每一個檔案即是一個模組。

namespace模式(2002)

最早,我們這麼寫程式碼:

function foo() {

}

function bar() {

}
複製程式碼

很多變數和函式會直接在 global 全域性作用域下面宣告,很容易產生命名衝突。於是,名稱空間模式被提出。比如:


var app = {};

app.foo = function() {

}

app.bar = function() {

}

app.foo();
複製程式碼

匿名閉包 IIFE 模式(2003)

儘管 namespace 模式一定程度上解決了 global 名稱空間上的變數汙染問題,但是它沒辦法解決程式碼和資料隔離的問題。

解決這個問題的先驅是:匿名閉包 IIFE 模式。它最核心的思想是對資料和程式碼進行封裝,並通過提供外部方法來對它們進行訪問。一個基本的例子如下:


var greeting = (function () {
    var module = {};

    var helloInLang = {
        en: 'Hello world!',
        es: '¡Hola mundo!',
        ru: 'Привет мир!'
    };

    module.getHello = function (lang) {
        return helloInLang[lang];
    };

    module.writeHello = function (lang) {
        document.write(module.getHello(lang))
    };
    
    return module;
}());

複製程式碼

模板依賴定義(2006)

模板依賴定義是接下來的一個用於解決模組化問題的方案,它需要配合後端的模板語法一起使用,通過後端語法聚合 js 檔案,從而實現依賴載入。

它最開始被應用到 Prototype 1.4 庫中。05年的時候,Sam Stephenson 開始開發 Prototype 庫,Prototype 當時是作為 Ruby on rails 的一個組成部分。由於 Sam 平時使用 Ruby 很多,這也難怪他會選擇使用 Ruby 裡的 erb 模板作為 JS 模組的依賴管理了。

模板依賴定義的具體做法是:對於某個 JS 檔案而言,如果它依賴其它 JS 檔案,則在這個檔案的頭部通過特殊的標籤語法去指定以來。這些標籤語法可以通過後端模板(erb, jinjia, smarty)去解析,和特殊的構建工具如 borshik 去識別。只得一提的是,這種模式只能作用於預編譯階段。

下面是一個使用了 borshik 的例子:


// app.tmp.js 檔案

/*borschik:include:../lib/main.js*/

/*borschik:include:../lib/helloInLang.js*/

/*borschik:include:../lib/writeHello.js*/

// main.js 檔案
var app = {};

// helloInLang.js 檔案
app.helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

// writeHello.js 檔案
app.writeHello = function (lang) {
    document.write(app.helloInLang[lang]);
};

複製程式碼

註釋定義依賴(2006)

註釋定義依賴和1999年的直接定義依賴的做法非常類似,不同的是,註釋定義依賴是以檔案為單位定義模組了。模組之間的依賴關係通過註釋語法進行定義。

一個應用如果想用這種方式,可以通過預編譯的方式(Mootools),或者在執行期動態的解析下載下來的程式碼(LazyJS)。

// helloInLang.js 檔案
var helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

// sayHello.js 檔案

/*! lazy require scripts/app/helloInLang.js */

function sayHello(lang) {
    return helloInLang[lang];
}

// hello.js 檔案

/*! lazy require scripts/app/sayHello.js */

document.write(sayHello('en'));
複製程式碼

依賴注入(2009)

2004年,Martin Fowler為了描述Java裡的元件之間的通訊問題,提出了依賴注入概念。

五年之後,前Sun和Adobe員工Miško Hevery(JAVA程式設計師)開始為他的創業公司設計一個 JS 框架,這個框架主要使用依賴注入的思想卻解決元件之間的通訊問題。後來的結局大家都知道,他的專案被 Google 收購,Angular 成為最流行的 JS 框架之一。

// greeting.js 檔案
angular.module('greeter', [])
    .value('greeting', {
        helloInLang: {
            en: 'Hello world!',
            es: '¡Hola mundo!',
            ru: 'Привет мир!'
        },

        sayHello: function(lang) {
            return this.helloInLang[lang];
        }
    });

// app.js 檔案
angular.module('app', ['greeter'])
    .controller('GreetingController', ['$scope', 'greeting', function($scope, greeting) {
        $scope.phrase = greeting.sayHello('en');
    }]);
複製程式碼

CommonJS模式 (2009)

09年 CommonJS(或者稱作 CJS)規範推出,並且最後在 Node.js 中被實現。

// greeting.js 檔案
var helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

var sayHello = function (lang) {
    return helloInLang[lang];
}

module.exports.sayHello = sayHello;

// hello.js 檔案
var sayHello = require('./lib/greeting').sayHello;
var phrase = sayHello('en');

console.log(phrase);
複製程式碼

這裡我們發現,為了實現CommonJS規範,我們需要兩個新的入口 -- requiremodule,它們提供載入模組和對外界暴露介面的能力。

值得注意的是,無論是 require 還是 module,它們都是語言的關鍵字。在 Node.js 中,由於是輔助功能,我們可以使用它。在將Node.js中的模組傳送給 V8 之前,會通過下面的函式進行包裹:

(function (exports, require, module, __filename, __dirname) {
    // ...
    // 模組的程式碼在這裡
    // ...
});
複製程式碼

就目前來看,CommonJS規範是最常見的模組格式規範。你不僅可以在 Node.js 中使用它,而且可以藉助 Browserfiy 或 Webpack 在瀏覽器中使用。

AMD 模式(2009)

CommonJS的工作在全力的推進,同時,關於模組的非同步載入的討論也越來越多。主要是希望解決 Web 應用的動態載入依賴,相比於 CommonJS,體積更小。

// lib/greeting.js 檔案
define(function() {
    var helloInLang = {
        en: 'Hello world!',
        es: '¡Hola mundo!',
        ru: 'Привет мир!'
    };

    return {
        sayHello: function (lang) {
            return helloInLang[lang];
        }
    };
});

// hello.js 檔案
define(['./lib/greeting'], function(greeting) {
    var phrase = greeting.sayHello('en');
    document.write(phrase);
});
複製程式碼

雖然 AMD 的模式很適合瀏覽器端的開發,但是隨著 npm 包管理的機制越來越流行,這種方式可能會逐步的被淘汰掉。

ES2015 Modules(2015)

ES2015 modules(簡稱 ES modules)就是我們目前所使用的模組化方案,它已經在Node.js 9裡原生支援,可以通過啟動加上flag --experimental-modules使用,不需要依賴 babel 等工具。目前還沒有被瀏覽器實現,前端的專案可以使用 babel 或 typescript 提前體驗。

// lib/greeting.js 檔案
const helloInLang = {
    en: 'Hello world!',
    es: '¡Hola mundo!',
    ru: 'Привет мир!'
};

export const greeting = {
    sayHello: function (lang) {
        return helloInLang[lang];
    }
};

// hello.js 檔案
import { greeting } from "./lib/greeting";
const phrase = greeting.sayHello("en");
document.write(phrase);
複製程式碼

總結

JS 模組化的演進歷程讓人感慨,感謝 TC 39 對 ES modules 的支援,讓 JS 的模組化程式終於可以告一段落了,希望幾年後所有的主流瀏覽器可以原生支援 ES modules。

JS 模組化的演進另一方面也說明了 Web 的能力在不斷增強,Web 應用日趨複雜。相信未來 JS 的能力進一步提升,我們的開發效率也會更加高效。


《IVWEB 技術週刊》 震撼上線了,關注公眾號:IVWEB社群,每週定時推送優質文章。

相關文章