Javascript模塊化的演進歷程

ES2015 在2015年6月正式發佈,官方終於引入了對於模塊的原生支持,現在 JS 的模塊化開發很是的方便、天然,但這個新規範僅僅持續了3年。就在7年前,JS 的模塊化還停留在運行時的支持;13年前,經過後端模版定義、註釋定義模塊依賴。對於經歷過的人來講,歷史的模塊化方式還停留在腦海中,久久不能忘懷。javascript

爲何須要模塊化

模塊,是爲了完成某一功能所需的程序或者子程序。模塊是系統中「職責單一」且「可替換」的部分。所謂的模塊化就是指把系統代碼分爲一系列職責單一且可替換的模塊。模塊化開發是指如何開發新的模塊和複用已有的模塊來實現應用的功能。前端

那麼,爲何須要模塊化呢?主要是如下幾個方面的緣由:java

  • 傳統的網頁開發正在轉變成 Web Apps 開發
  • 代碼複雜度在逐步增高,隨着 Web 能力的加強,愈來愈多的業務邏輯和交互都放在 Web 層實現
  • 開發者想要分離的JS文件/模塊,便於後續代碼的維護性
  • 部署時但願把代碼優化成幾個 HTTP 請求

模塊化的演進歷程

直接定義依賴(1999)

最早嘗試將模塊化引入到 JS 中是在1999年,稱做「直接定義依賴」。這種方式實現模塊化簡單粗暴 —— 經過全局方法定義、引用模塊。Dojo 就是使用這種方式進行模塊組織的,使用dojo.provide 定義一個模塊,dojo.require 來調用在其它地方定義的模塊。git

咱們在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中,每個文件便是一個模塊。github

namespace模式(2002)

最先,咱們這麼寫代碼:typescript

function foo() {

}

function bar() {

}
複製代碼

不少變量和函數會直接在 global 全局做用域下面聲明,很容易產生命名衝突。因而,命名空間模式被提出。好比:npm

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社區,每週定時推送優質文章。

相關文章
相關標籤/搜索