新聞中心
一、背景
作為前端開發(fā),模塊化我們已經(jīng)耳熟能詳,我們平時(shí)接觸到的 ES6 的 import,nodejs中的require他們有啥區(qū)別?

成都地區(qū)優(yōu)秀IDC服務(wù)器托管提供商(成都創(chuàng)新互聯(lián)).為客戶提供專業(yè)的服務(wù)器主機(jī)托管,四川各地服務(wù)器托管,服務(wù)器主機(jī)托管、多線服務(wù)器托管.托管咨詢專線:13518219792
我們也聽過CommonJS、CMD、AMD、ES6模塊系統(tǒng),這些都有什么聯(lián)系呢?
本文將對這些問題進(jìn)行歸納總結(jié),可以對模塊化有個(gè)清晰的認(rèn)識。
二、為何需要模塊化?
1. 起源
最開始 js 是沒有模塊化的概念的,就是普通的腳本語言放到 script 標(biāo)簽里,做些簡單的校驗(yàn),代碼量比較少。
隨著ajax的出現(xiàn),前端可以請求數(shù)據(jù)了,做的事情更多了,邏輯越來越復(fù)雜,就會出現(xiàn)很多問題。
1.1 全局變量沖突
因?yàn)榇蠹业拇a都在一個(gè)作用域,不同人定義的變量名可能重復(fù),導(dǎo)致覆蓋。
var num = 1; // 一個(gè)人聲明了
...
var num = 2; // 其他人又聲明了
1.2 依賴關(guān)系管理麻煩
比如我們引入了3個(gè)js文件,他們直接相互依賴,我們需要按照依賴關(guān)系從上到下排序。
如果文件有十多個(gè),我們需要理清楚依賴關(guān)系再手動按順序引入,會導(dǎo)致后續(xù)代碼更加難以維護(hù)。
2. 早期解決方案
針對前面說的問題,其實(shí)也有一些響應(yīng)的解決方案。
2.1 命名空間
命名空間是將一組實(shí)體、變量、函數(shù)、對象封裝在一個(gè)空間的行為。這里展現(xiàn)了模塊化思想雛形,通過簡單的命名空間進(jìn)行「塊兒」的切分,體現(xiàn)了分離和內(nèi)聚的思想。著名案例 「YUI2」。
// 示例:
const car = {
name: '小汽車',
start: () => {
console.log('start')
},
stop: () => {
console.log('stop')
}
}
上面示例可以發(fā)現(xiàn)可能存在問題,比如我們修改了car的name,會導(dǎo)致原有的name被更改
car.name = '測試'
console.log(car) // {name: '111', start: ?, stop: ?}
2.2 閉包
再次提升模塊化的解決方案,利用閉包使污染的問題得到解決,更加純粹的內(nèi)聚
moduleA = function() {
var name = '小汽車';
return {
start: function (c){
return name + '啟動';
};
}
}()
上面示例中function內(nèi)部的變量就對全局隱藏了,達(dá)到了封裝的目的。但是模塊名稱暴露在全局,還是存在命名沖突的問題。
下面這個(gè)基于 IIFE 和閉包實(shí)現(xiàn)的效果:
// moduleA.js
(function(global) {
var name = '小汽車';
function start() {};
global.moduleA = { name, start };
})(window)
上面表達(dá)式中的變量 name 不能直接從外部訪問。
綜上,所以模塊化解決的問題有哪些:
- 解決命名污染,全局污染,變量沖突等問題
- 內(nèi)聚私有,變量不能被外面訪問到
- 怎么引入其它模塊,怎樣暴露出接口給其它模塊
- 引入其他模塊可能存在循環(huán)引用的問題
三、主流模塊化解決方案
1. CommonJS
可以點(diǎn)擊 CommonJS規(guī)范查看相關(guān)介紹。
1)每個(gè)文件就是一個(gè)模塊,有自己的作用域。在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對其他文件不可見。
2)CommonJS規(guī)范規(guī)定,每個(gè)模塊內(nèi)部,module變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對象,它的exports屬性(即module.exports)是對外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。
3)require方法用于加載模塊。
1.1 加載模塊
var example = require('./example.js');
var config = require('config.js');
var http = require('http');
1.2 對外暴露模塊
module.exports.example = function () {
...
}
module.exports = function(x){
console.log(x)
}
1.3 Node.js的模塊化
說到CommonJS 我們要提一下 Node.js,Node.js的出現(xiàn)讓我們可以用JavaScript來寫服務(wù)端代碼,而 Node 應(yīng)用由模塊組成,采用的是 CommonJS 模塊規(guī)范,當(dāng)然并非完全按照CommonJS來,它進(jìn)行了取舍,增加了一些自身的特性。
1)Node內(nèi)部提供一個(gè)Module構(gòu)建函數(shù)。所有模塊都是Module的實(shí)例,每個(gè)模塊內(nèi)部,都有一個(gè)module對象,代表當(dāng)前模塊。包含以下屬性:
- module.id 模塊的識別符,通常是帶有絕對路徑的模塊文件名。
- module.filename 模塊的文件名,帶有絕對路徑。
- module.loaded 返回一個(gè)布爾值,表示模塊是否已經(jīng)完成加載。
- module.parent 返回一個(gè)對象,表示調(diào)用該模塊的模塊。
- module.children 返回一個(gè)數(shù)組,表示該模塊要用到的其他模塊。
- module.exports 表示模塊對外輸出的值。
2)Node使用CommonJS模塊規(guī)范,內(nèi)置的require命令用于加載模塊文件。
3)第一次加載某個(gè)模塊時(shí),Node會緩存該模塊。以后再加載該模塊,就直接從緩存取出該模塊的module.exports屬性。所有緩存的模塊保存在require.cache之中。
// a.js
var name = 'Lucy'
exports.name = name
// b.js
var a = require('a.js')
console.log(a.name) // "Lucy"
a.name = "hello";
var b = require('./a.js')
console.log(b.name) // "hello"
上面第一次加載以后修改了name值,第二次加載的時(shí)候打印的name是上次修改的,證明是從緩存中讀取的。
想刪除模塊的緩存可以這樣:
delete require.cache[moduleName];
4)CommonJS模塊的加載機(jī)制是,輸入的是被輸出的值的拷貝。也就是說,一旦輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。請看下面這個(gè)例子。
// a.js
var counter = 3
exports.counter = counter
exports.addCounter = function(a){
counter++
}
// b.js
var a = require('a.js')
console.log(a.counter) // 3
a.addCounter()
console.log(a.age) // 3
這個(gè)例子說明a.js模塊加載以后,模塊內(nèi)部的變化就影響不到a.counter了。這是因?yàn)閍.counter是一個(gè)原始類型的值,會被緩存。除非寫成一個(gè)函數(shù),才能得到內(nèi)部變動后的值。
2.前端模塊化
前面所說的CommonJS規(guī)范,都是基于node來說的,所以CommonJS都是針對服務(wù)端的實(shí)現(xiàn)。為什么呢?
因?yàn)镃ommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。
如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,用CommonJS需要等模塊下載完并運(yùn)行后才能使用,將阻塞后面代碼的執(zhí)行,這時(shí)就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范,解決異步加載的問題。
2.1 AMD(Asynchronous Module Definition)和 RequireJS
AMD是異步加載模塊規(guī)范。
RequireJS是一個(gè)工具庫。主要用于客戶端的模塊管理。它可以讓客戶端的代碼分成一個(gè)個(gè)模塊,實(shí)現(xiàn)異步或動態(tài)加載,從而提高代碼的性能和可維護(hù)性。它的模塊管理遵守AMD規(guī)范。
2.1.1 模塊定義
1)獨(dú)立模塊(不需要依賴任何其他模塊)
//獨(dú)立模塊定義
define({
method1: function() {}
method2: function() {}
});
//或者
define(function(){
return {
method1: function() {},
method2: function() {},
}
});
2)非獨(dú)立模塊(需要依賴其他模塊)
define(['module1', 'module2'], function(m1, m2){
return {
method: function() {
m1.methodA();
m2.methodB();
}
};
});
define方法:
- 第一個(gè)參數(shù)是一個(gè)數(shù)組,它的成員是當(dāng)前模塊所依賴的模塊
- 第二個(gè)參數(shù)是一個(gè)函數(shù),當(dāng)前面數(shù)組的所有成員加載成功后,它將被調(diào)用。它的參數(shù)與數(shù)組的成員一一對應(yīng),這個(gè)函數(shù)必須返回一個(gè)對象,供其他模塊調(diào)用
2.1.2 模塊調(diào)用
require方法用于調(diào)用模塊。它的參數(shù)與define方法類似。
require(['a', 'b'], function ( a, b ) {
a.doSomething();
});
define和require這兩個(gè)定義模塊、調(diào)用模塊的方法,合稱為AMD模式。它的模塊定義的方法非常清晰,不會污染全局環(huán)境,能夠清楚地顯示依賴關(guān)系。
2.1.3 require.js的config方法
require方法本身也是一個(gè)對象,它帶有一個(gè)config方法,用來配置require.js運(yùn)行參數(shù)。
require.config({
paths: {
jquery: [
'//cdnjs.cloudflare.com/ajax/libs/jquery/2.0.0/jquery.min.js',
'lib/jquery'
]
}
});
參數(shù)對象包含:
- paths 指定各個(gè)模塊的位置
- baseUrl 指定本地模塊位置的基準(zhǔn)目
- shim 用來幫助require.js加載非AMD規(guī)范的庫。
2.1.3 CommonJS 和AMD的對比
- CommonJS一般用于服務(wù)端比如node,AMD一般用于瀏覽器環(huán)境,并且允許非同步加載模塊,可以根據(jù)需要動態(tài)加載模塊
- CommonJS和AMD都是運(yùn)行時(shí)加載
2.1.4 運(yùn)行時(shí)加載
簡單來講,就是CommonJS和AMD都只能在運(yùn)行時(shí)才能確定一些東西,所以是運(yùn)行時(shí)加載。比如下面的例子:
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
上面代碼其實(shí)是整體加載了fs模塊,生成了一個(gè)_fs 的對象,然后從這個(gè)對象上讀取三個(gè)方法。因?yàn)橹挥羞\(yùn)行時(shí)才能得到這個(gè)對象,所以成為運(yùn)行時(shí)加載。
下面是AMD的例子:
// AMD
define('a', function () {
console.log('a 加載')
return {
run: function () { console.log('a 執(zhí)行') }
}
})
define('b', function () {
console.log('b 加載')
return {
run: function () { console.log('b 執(zhí)行') }
}
})
//運(yùn)行
require(['a', 'b'], function (a, b) {
console.log('main 執(zhí)行')
a.run()
b.run()
})
// 運(yùn)行結(jié)果:
// a 加載
// b 加載
// main 執(zhí)行
// a 執(zhí)行
// b 執(zhí)行
我們可以看到執(zhí)行的時(shí)候,a和b先加載,后面才從main開始執(zhí)行。所以require一個(gè)模塊的時(shí)候,模塊會先被加載,并返回一個(gè)對象,并且這個(gè)對象是整體加載的,也就是常說的 依賴前置。
2.2 CMD(Common Module Definition) 和 SeaJS
在 Sea.js 中,所有 JavaScript 模塊都遵循 CMD(Common Module Definition) 模塊定義規(guī)范。
Sea.js和 RequireJS 區(qū)別在哪里呢?這里有個(gè)官方給出的區(qū)別。
RequireJS 遵循 AMD(異步模塊定義)規(guī)范,Sea.js 遵循 CMD (通用模塊定義)規(guī)范。規(guī)范的不同,導(dǎo)致了兩者 API 不同。Sea.js 更貼近 CommonJS Modules/1.1 和 Node Modules 規(guī)范。
這里對AMD和CMD做個(gè)簡單對比:
- AMD 定義模塊時(shí),指定所有的依賴,依賴模塊加載后會執(zhí)行回調(diào)并通過參數(shù)傳到這回調(diào)方法中:
define(['module1', 'module2'], function(m1, m2) {
...
});
- CMD規(guī)范中一個(gè)模塊就是一個(gè)文件,模塊更接近于Node對CommonJS規(guī)范的定義:
define(factory); // factory 可以是一個(gè)函數(shù),也可以是一個(gè)對象或字符串。
factory 為函數(shù)時(shí),表示是模塊的構(gòu)造方法。執(zhí)行該構(gòu)造方法,可以得到模塊向外 提供的接口。factory 方法在執(zhí)行時(shí),默認(rèn)會傳入三個(gè)參數(shù):require、exports 和 module:
define(function(require, exports, module) {
// 模塊代碼
});
其中,require 是一個(gè)方法,接受 模塊標(biāo)識 作為唯一參數(shù),用來獲取其他模塊提供的接口。需要依賴模塊時(shí),隨時(shí)調(diào)用require( )引入即可
define(function(require, exports) {
// 獲取模塊 a 的接口
var a = require('./a');
// 調(diào)用模塊 a 的方法
a.doSomething();
});
下面演示一下CMD的執(zhí)行
define('a', function (require, exports, module) {
console.log('a 加載')
exports.run = function () { console.log('a 執(zhí)行') }
})
define('b', function (require, exports, module) {
console.log('b 加載')
exports.run = function () { console.log('b 執(zhí)行') }
})
define('main', function (require, exports, module) {
console.log('main 執(zhí)行')
var a = require('a')
a.run()
var b = require('b')
b.run()
})
// main 執(zhí)行
// a 加載
// a 執(zhí)行
// b 加載
// b 執(zhí)行
看到執(zhí)行結(jié)果,會在真正需要使用(依賴)模塊時(shí)才執(zhí)行該模塊,感覺這好像和我們認(rèn)知的一樣,畢竟我也是這么想的執(zhí)行順序,但是看前面AMD的執(zhí)行結(jié)果,是先把a(bǔ)和b都加載以后,才開始執(zhí)行main的。所以相較于AMD的依賴前置、提前執(zhí)行,CMD則推崇依賴就近、延遲執(zhí)行。
2.3 UMD(Universal Module Definition) 通用模塊規(guī)范
可以看到其實(shí)兼容模式是將幾種常見模塊定義方式都做了兼容處理。
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(require('lodash')) // node, commonJS
: typeof define === 'function' && define.amd
? define(['lodash'], factory) // amd cmd
: (
global = typeof globalThis !== 'undefined'
? globalThis
: global || self, factory(global.lodash)
);
}(this, (function (lodash) { 'use strict';
...
})));
2.4 ES6 模塊
模塊功能主要由兩個(gè)命令構(gòu)成:export和import。export命令用于規(guī)定模塊的對外接口,import命令用于輸入其他模塊提供的功能。
2.4.1 模塊導(dǎo)出
一個(gè)模塊就是一個(gè)獨(dú)立的文件。該文件內(nèi)部的所有變量,外部無法獲取。如果你希望外部能夠讀取模塊內(nèi)部的某個(gè)變量(函數(shù)或類),就必須使用export關(guān)鍵字輸出該變量(函數(shù)或類)。
1) 導(dǎo)出變量 和 函數(shù)
// a.js
// 導(dǎo)出變量
export var name = 'Michael';
export var year = 2010;
// 或者
// 也可以這樣導(dǎo)出
var name = 'Michael';
export { name, year };
復(fù)制代碼
// 導(dǎo)出函數(shù)
export function multiply(x, y) {
return x * y;
};
2) as的使用
通常情況下,export輸出的變量就是本來的名字,但是可以使用as關(guān)鍵字重命名。
function v1() { ... }
function v2() { ... }
export {
v1 as streamV1,
v2 as streamV2,
v2 as streamLatestVersion
};
2.4.2 模塊引入
1) 使用export命令定義了模塊的對外接口以后,其他 JS 文件就可以通過import命令加載這個(gè)模塊。
// 一般用法
import { name, year} from './a.js';
// as 用法
import { name as userName } from './a.js';
注意:
import命令具有提升效果,會提升到整個(gè)模塊的頭部,首先執(zhí)行。
下面的代碼不會報(bào)錯(cuò),因?yàn)閕mport的執(zhí)行早于foo的調(diào)用。這種行為的本質(zhì)是,import命令是編譯階段執(zhí)行的(后面對比CommonJs時(shí)會講到),在代碼運(yùn)行之前。
foo();
import { foo } from 'my_module';
2)整體模塊加載
//user.js
export name = 'lili';
export age = 18;
//逐一加載
import { age, name } from './user.js';
//整體加載
import * as user from './user.js';
console.log(user.name);
console.log(user.age);
3)export default 命令
export default命令用于指定模塊的默認(rèn)輸出。顯然,一個(gè)模塊只能有一個(gè)默認(rèn)輸出,因此export default命令只能使用一次。所以,import命令后面才不用加大括號,因?yàn)橹豢赡芪ㄒ粚?yīng)export default命令。
export default function foo() { // 輸出
// ...
}
import foo from 'foo'; // 輸入
注意: 正是因?yàn)閑xport default命令其實(shí)只是輸出一個(gè)叫做default的變量,所以它后面不能跟變量聲明語句。
// 正確
var a = 1;
export default a;
// 錯(cuò)誤
// `export default a`的含義是將變量`a`的值賦給變量`default`。
// 所以,這種寫法會報(bào)錯(cuò)。
export default var a = 1;
2.4.3 ES6模塊、CommonJS和AMD模塊區(qū)別
1) 編譯時(shí)加載 和 運(yùn)行時(shí)加載
ES6 模塊的設(shè)計(jì)思想是盡量的靜態(tài)化,使得編譯時(shí)就能確定模塊的依賴關(guān)系,以及輸入和輸出的變量。所以ES6是編譯時(shí)加載。CommonJS 和 AMD 模塊,都只能在運(yùn)行時(shí)確定這些東西。比如,CommonJS 模塊就是對象,輸入時(shí)必須查找對象屬性。
// CommonJS模塊
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// -----------------
// ES6模塊
import { stat, exists, readFile } from 'fs';
CommonJS和ES6模塊加載區(qū)別:
- CommonJS 實(shí)質(zhì)是整體加載fs模塊(即加載fs的所有方法),生成一個(gè)對象(_fs),然后再從這個(gè)對象上面讀取 3 個(gè)方法。這種加載稱為“運(yùn)行時(shí)加載”,因?yàn)橹挥羞\(yùn)行時(shí)才能得到這個(gè)對象,導(dǎo)致完全沒辦法在編譯時(shí)做“靜態(tài)優(yōu)化”。
- ES6模塊 實(shí)質(zhì)是從fs模塊加載 3 個(gè)方法,其他方法不加載。這種加載稱為“編譯時(shí)加載 ”或者靜態(tài)加載,即 ES6 可以在編譯時(shí)就完成模塊加載,效率要比 CommonJS 模塊的加載方式高。
2) 值拷貝 和 引用拷貝
- 前面 1.3 Node.js模塊化提到了 CommonJS是值拷貝,模塊加載完并輸出一個(gè)值,模塊內(nèi)部的變化就影響不到這個(gè)值。因?yàn)檫@個(gè)值是一個(gè)原始類型的值,會被緩存。
- ES6 模塊的運(yùn)行機(jī)制與 CommonJS 不一樣。JS 引擎對腳本靜態(tài)分析的時(shí)候,遇到模塊加載命令import,就會生成一個(gè)只讀引用。等到腳本真正執(zhí)行時(shí),再根據(jù)這個(gè)只讀引用,到被加載的那個(gè)模塊里面去取值。換句話說,ES6 的import有點(diǎn)像 Unix 系統(tǒng)的“符號連接”,原始值變了,import加載的值也會跟著變。因此,ES6 模塊是動態(tài)引用,并且不會緩存值,模塊里面的變量綁定其所在的模塊。
// a.js
export let counter = 3;
export function addCounter() {
counter++;
}
// b.js
import { counter, addCounter } from './a';
console.log(counter); // 3
addCounter();
console.log(counter); // 4
ES6 模塊輸入的變量counter是活的,完全反應(yīng)其所在模塊a.js內(nèi)部的變化。
本文題目:前端模塊化知識梳理
文章出自:http://fisionsoft.com.cn/article/djppodh.html


咨詢
建站咨詢
