JavaScript模塊化(ES Module/CommonJS/AMD/CMD)

1模塊化歷史

1.1前言

參照前端模塊化開發(fā)的價(jià)值

1.2無模塊化

每次說到JavaScript都會(huì)想到Brendan Eich花了十來天就發(fā)明了它,那就是JS的鴻蒙時(shí)期,混沌初開。
就像當(dāng)年在校初學(xué)前端時(shí)寫的代碼,沒有那么多的套路,就是從上往下碼代碼,沒有想著去聲明函數(shù)神馬的,甚至多少代碼都寫在一個(gè)JS里。現(xiàn)在想來真是慘不忍睹。雖然本人入坑前端距那個(gè)鴻蒙時(shí)代實(shí)在久遠(yuǎn),但是據(jù)各種典籍記載,那時(shí)候的代碼風(fēng)格就差不多這樣子,從上往下一直堆著就好了。

var a = 0;
if (xxx) {
  // 省略100L
}
document.getElementById('id').onclick = function(event) {
  // 省略若干行
}
......

1.3模塊化冒泡

每個(gè)行業(yè)都有梗,現(xiàn)在和同事聊天有時(shí)候還會(huì)吐槽十幾年前的老網(wǎng)站,真是有幸見過。前輩的聊天更有意思了,當(dāng)年的登錄居然是寫死在前端代碼里,就像這樣子

if (username === 'xxxxx' && password === 'xxxxxx') {
  // 登錄成功
}

是不是覺得很無語。當(dāng)年的前端都是靜態(tài)頁面,沒有現(xiàn)在這樣子通過ajax和后端交互神馬的,內(nèi)容更是豐富多彩,更新及時(shí)。
前端代碼愈發(fā)龐大,那么自然而然會(huì)暴露很多問題。
無非就倆個(gè):

  • 命名沖突
  • 文件依賴

1.3.1命名沖突解決

  1. java風(fēng)格的namespace,這個(gè)很好理解,在此不贅訴,缺點(diǎn)的話,自行想象,不堪
  2. 自執(zhí)行函數(shù)(內(nèi)部變量不可見不被污染)
// jQuery式的匿名自執(zhí)行函數(shù)
// 缺點(diǎn)是增添了全局變量、依賴需要提前提供
(function(root) {
    root.jQuery = window.$ = jQuery; // 掛載到window之上
})(window)
// 普通自執(zhí)行函數(shù)
// 缺點(diǎn)就是暴露了全局變量,而且隨著模塊的增加,全局變量會(huì)很多
module = function() {
    function module() {

    }
    return module;
}()
  1. YUI3的沙箱機(jī)制,這個(gè)表示不曉得。

1.3.2文件依賴解決

這個(gè)沒有解決方法,乖乖自行保證順序和不缺漏吧

<script src="https://cdn.bootcss.com/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdn.bootcss.com/backbone.js/1.3.3/backbone-min.js"></script>

1.4CommonJS

1.4.1前言

隨著前端的發(fā)展,到node.js被創(chuàng),js可以用來寫server端代碼。
做過后端的同學(xué)肯定知道沒有模塊化怎么能忍呢?
我們通過上節(jié)所得,我們可以得出以下幾點(diǎn)需待解決

  • 模塊代碼安全,不可被污染也不可污染別人,沙箱呀
  • 把模塊接口暴露出去(得優(yōu)雅呀,不能增添全局變量)
  • 這個(gè)依賴順序管理

1.4.2發(fā)展

這個(gè)還真沒有經(jīng)歷了解過。度娘一番,幸好看到seajs下的一個(gè)issues。
大致就是大牛很牛,推出了Modules/1.0規(guī)范。
之后為了推廣到瀏覽器端,大牛產(chǎn)生分歧,分為三大流派:

  • Modules/1.x 流派(Modules/Transport
    通過工具轉(zhuǎn)換現(xiàn)有的CommonJS)
  • Modules/Async 流派(自立門派)
  • Modules/2.0 流(Modules/Wrappings
    對(duì)1.0的升級(jí))

這里說下為什么不能用在瀏覽器

  • 服務(wù)端代碼在硬盤,加載模塊時(shí)間幾乎忽略不計(jì)。瀏覽器端就不成了。
  • 模塊引用未被function,所以暴露在了全局之下。

1.4.3番外(AMD、CMD)

AMD是 RequireJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。
CMD是 SeaJS 在推廣過程中對(duì)模塊定義的規(guī)范化產(chǎn)出。

1.5ES Module

這個(gè)是ECMA搞得一套。和之前的區(qū)別在于人家是官方的,根正苗紅,上文的是社區(qū)搞得,野生。

2 ES Module/CommonJS/AMD/CMD差異

2.1 ES Module與CommonJS的差異

編譯時(shí)和運(yùn)行時(shí)

首先說下編譯時(shí)和運(yùn)行時(shí)。JavaScript有倆種聲明方法(聲明變量和聲明方法)。var/const/let和function
編譯時(shí),對(duì)于聲明變量會(huì)在內(nèi)存中開辟一塊內(nèi)存空間并指向變量名,且指向變量名,賦值為undefined。對(duì)于函數(shù)聲明會(huì)一樣的開啟空間。不過賦值為聲明的函數(shù)體。PS:無論順序如何,都會(huì)先聲明變量
運(yùn)行時(shí),執(zhí)行變量初始化之類的。

// 源碼
var a = 3;
function f() {
    return 'f';
}

// 編譯時(shí)
var a = undefined;
var f = function() {
    return 'f';
}
// 運(yùn)行時(shí)
a = 3;

CommonJS模塊是對(duì)象,是運(yùn)行時(shí)加載,運(yùn)行時(shí)才把模塊掛載在exports之上(加載整個(gè)模塊的所有),加載模塊其實(shí)就是查找對(duì)象屬性。
ES Module不是對(duì)象,是使用export顯示指定輸出,再通過import輸入。此法為編譯時(shí)加載,編譯時(shí)遇到import就會(huì)生成一個(gè)只讀引用。等到運(yùn)行時(shí)就會(huì)根據(jù)此引用去被加載的模塊取值。所以不會(huì)加載模塊所有方法,僅取所需。

  • CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。
  • CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。
    詳情參見

2.2CommonJS與AMD/CMD的差異

AMD/CMD是CommonJS在瀏覽器端的解決方案。CommonJS是同步加載(代碼在本地,加載時(shí)間基本等于硬盤讀取時(shí)間)。AMD/CMD是異步加載(瀏覽器必須這么干,代碼在服務(wù)端)

2.3AMD與CMD的差異

  • AMD是提前執(zhí)行(RequireJS2.0開始支持延遲執(zhí)行,不過只是支持寫法,實(shí)際上還是會(huì)提前執(zhí)行),CMD是延遲執(zhí)行
  • AMD推薦依賴前置,CMD推薦依賴就近

3 用法

3.1 CommonJS

// 導(dǎo)出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
// 就是在此對(duì)象上掛屬性
// commonjs
module.exports.add = function add(params) {
    return ++params;
}
exports.sub = function sub(params) {
    return --params;
}

// 加載模塊使用require('xxx')。相對(duì)、絕對(duì)路徑均可。默認(rèn)引用js,可以不寫.js后綴
// index.js
var common = require('./commonjs');
console.log(common.sub(1));
console.log(common.add(1));

3.2 AMD/RequireJS

  • 定義模塊:define(id?, dependencies?, factory)
  • 加載模塊:require([module], factory)
// a.js
// 依賴有三個(gè)默認(rèn)的,即"require", "exports", "module"。順序個(gè)數(shù)均可視情況
// 如果忽略則factory默認(rèn)此三個(gè)傳入?yún)?shù)
// id一般是不傳的,默認(rèn)是文件名
define(["b", "require", "exports"], function(b, require, exports) {
    console.log("a.js執(zhí)行");
    console.log(b);
// 暴露api可以使用exports、module.exports、return
    exports.a = function() {
        return require("b");
    }
})
// b.js
define(function() {
    console.log('b.js執(zhí)行');
    console.log(require);
    console.log(exports);
    console.log(module);
    return 'b';
})
// index.js
// 支持Modules/Wrappings寫法,注意dependencies得是空的,且factory參數(shù)不可空
define(function(require, exports, module) {
    console.log('index.js執(zhí)行');
    var a = require('a');
    var b = require('b');
})
// index.js
require(['a', 'b'], function(a, b) {
    console.log('index.js執(zhí)行');
})

3.3 CMD/SeaJS

SeaJS平時(shí)沒有到,不過了解了下,豐富用法看CMD定義規(guī)范。

  • 定義模塊:define(factory)
// a.js
// require, exports, module參數(shù)順序不可亂
// 暴露api方法可以使用exports、module.exports、return
// 與requirejs不同的是,若是未暴露,則返回{},requirejs返回undefined
define(function(require, exports, module) {
    console.log('a.js執(zhí)行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// b.js
// 
define(function(require, module, exports) {
    console.log('b.js執(zhí)行');
    console.log(require);
    console.log(exports);
    console.log(module);
})
// index.js
define(function(require) {
    var a = require('a');
    var b = require('b');
    console.log(a);
    console.log(b);
})

定義模塊無需列依賴,它會(huì)調(diào)用factory的toString方法對(duì)其進(jìn)行正則匹配以此分析依賴。預(yù)先下載,延遲執(zhí)行

3.4 ES Module

輸出/export

// 報(bào)錯(cuò)1
export 1;
// 報(bào)錯(cuò)2
const m = 1;
export m;

// 接口名與模塊內(nèi)部變量之間,建立了一一對(duì)應(yīng)的關(guān)系
// 寫法1
export const m = 1;
// 寫法2
const m = 1;
export { m };
// 寫法3
const m = 1;
export { m as module };

PS:這個(gè)有點(diǎn)不是很明白,大致理解就是不能直接導(dǎo)出變量,但是可以導(dǎo)出聲明(函數(shù)、變量聲明)。這里的接口理解是export之后的變量,它和變量建立了映射關(guān)系??偟亩?,export之后只能接聲明或者語句

輸入/import

基本用法

// 類似于對(duì)象解構(gòu)
// module.js
export const m = 1;
// index.js
// 注意,這里的m得和被加載的模塊輸出的接口名對(duì)應(yīng)
import { m } from './module';
// 若是想為輸入的變量取名
import { m as m1 }  './module';
// 值得注意的是,import是編譯階段,所以不能動(dòng)態(tài)加載,比如下面寫法是錯(cuò)誤的。因?yàn)?a' + 'b'在運(yùn)行階段才能取到值,運(yùn)行階段在編譯階段之后
import { 'a' + 'b' } from './module';
// 若是只是想運(yùn)行被加載的模塊,如下
// 值得注意的是,即使加載兩次也只是運(yùn)行一次
import './module';
// 整體加載
import * as module from './module';

PS:CommonJS和ES Module是可以寫一起的,但是最好不要。畢竟一個(gè)是編譯階段一個(gè)是運(yùn)行階段。就在項(xiàng)目中入過坑,自行體會(huì)。

賦值

首先輸入的模塊變量是不可重新賦值的,它只是個(gè)可讀引用,不過卻可以改寫屬性

// 單例
// module.js
export const a = {};
// module2.js
export { a } from './module';
import { a as a1 } from './module';
import { a } from './module2';
a1.e = 3;
console.log(a1) // { e: 3 }
console.log(a) // { e: 3 }

輸出/export default

// module.js
// 其實(shí)export default就是export { xxx as default }
const m = 1;
export default m;
===
export { m as default }
// index.js
// 對(duì)應(yīng)的輸入也得相應(yīng)變化
import module from './module';
===
import { default as module } from './module';

還記得之前export小結(jié)處的倆報(bào)錯(cuò)么?如下寫法正確,因?yàn)樘峁┝薲efault接口

// 寫法1
export default 1;
// 寫法2
const m = 1;
export default m;
// 錯(cuò)誤寫法
export default const m = 1;

PS:export default只能一次

復(fù)合寫法

可用于模塊間繼承。比如在a模塊寫下如下,那么a模塊不就有了./module的方法了

export { a } from './module';
export { a as a1 } from './module';
export * from './module';

動(dòng)態(tài)加載/import()

因?yàn)榫幾g時(shí)加載,所以不能動(dòng)態(tài)加載模塊。不過幸好有import()方法。
這家伙返回值是一個(gè)Promise對(duì)象,所以then、catch你開心就好

// 普通寫法
import('./module').then(({ a }) => {})
// async、await
const { a } = await import('./module');

4 番外自實(shí)現(xiàn)

var MyModules = (function(){
    var modules = [];
    function define(name, deps, cb) {
        deps.forEach(function(dep, i) {
            deps[i] = modules[dep];
        });
        modules[name] = cb.apply(cb, deps);
    }
    function get(name) {
        return modules[name];
    }
    return {
        define: define,
        get: get
    };
})();
MyModules.define('add', [], function() {
    return function(a, b) {
            return a + b;
        };
})
MyModules.define('foo', ['add'], function(add) {
    var a = 3;
    var b = 4;
    return {
        doSomething: function() {
            return add(a, b) + a;;
        }
    };
})
var add = MyModules.get('add');
var foo = MyModules.get('foo');
console.log(add(1, 2));
console.log(foo.doSomething());
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容