ES6系列之模組載入方案
前言
本篇我們重點介紹以下四種模組載入規範:
- AMD
- CMD
- CommonJS
- ES6 模組
最後再延伸講下 Babel 的編譯和 webpack 的打包原理。
require.js
在瞭解 AMD 規範之前,我們先來看看 require.js 的使用方式。
專案目錄為:
* project/
* index.html
* vender/
* main.js
* require.js
* add.js
* square.js
* multiply.js
index.html
的內容如下:
<!DOCTYPE html>
<html>
<head>
<title>require.js</title>
</head>
<body>
<h1>Content</h1>
<script data-main="vender/main" src="vender/require.js"></script>
</body>
</html>
data-main="vender/main"
表示主模組是 vender
下的 main.js
。
main.js
的配置如下:
// main.js
require([`./add`, `./square`], function(addModule, squareModule) {
console.log(addModule.add(1, 1))
console.log(squareModule.square(3))
});
require 的第一個參數列示依賴的模組的路徑,第二個參數列示此模組的內容。
由此可以看出,主模組
依賴 add 模組
和 square 模組
。
我們看下 add 模組
即 add.js
的內容:
// add.js
define(function() {
console.log(`載入了 add 模組`);
var add = function(x, y) {
return x + y;
};
return {
add: add
};
});
requirejs
為全域性新增了 define
函式,你只要按照這種約定的方式書寫這個模組即可。
那如果依賴的模組又依賴了其他模組呢?
我們來看看主模組
依賴的 square 模組
, square 模組
的作用是求出一個數字的平方,比如輸入 3 就返回 9,該模組依賴一個乘法模組
,該乘法模組即 multiply.js
的程式碼如下:
// multiply.js
define(function() {
console.log(`載入了 multiply 模組`)
var multiply = function(x, y) {
return x * y;
};
return {
multiply: multiply
};
});
而 square 模組
就要用到 multiply 模組
,其實寫法跟 main.js 新增依賴模組一樣:
// square.js
define([`./multiply`], function(multiplyModule) {
console.log(`載入了 square 模組`)
return {
square: function(num) {
return multiplyModule.multiply(num, num)
}
};
});
require.js 會自動分析依賴關係,將需要載入的模組正確載入。
requirejs 專案 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/requirejs
而如果我們在瀏覽器中開啟 index.html
,列印的順序為:
載入了 add 模組
載入了 multiply 模組
載入了 square 模組
2
9
AMD
在上節,我們說了這樣一句話:
requirejs
為全域性新增了define
函式,你只要按照這種約定的方式書寫這個模組即可。
那這個約定的書寫方式是指什麼呢?
指的便是 The Asynchronous Module Definition (AMD) 規範。
所以其實 AMD 是 RequireJS 在推廣過程中對模組定義的規範化產出。
你去看 AMD 規範) 的內容,其主要內容就是定義了 define 函式該如何書寫,只要你按照這個規範書寫模組和依賴,require.js 就能正確的進行解析。
sea.js
在國內,經常與 AMD 被一起提起的還有 CMD,CMD 又是什麼呢?我們從 sea.js
的使用開始說起。
檔案目錄與 requirejs 專案目錄相同:
* project/
* index.html
* vender/
* main.js
* require.js
* add.js
* square.js
* multiply.js
index.html
的內容如下:
<!DOCTYPE html>
<html>
<head>
<title>sea.js</title>
</head>
<body>
<h1>Content</h1>
<script src="vender/sea.js"></script>
<script>
// 在頁面中載入主模組
seajs.use("./vender/main");
</script>
</body>
</html>
main.js 的內容如下:
// main.js
define(function(require, exports, module) {
var addModule = require(`./add`);
console.log(addModule.add(1, 1))
var squareModule = require(`./square`);
console.log(squareModule.square(3))
});
add.js 的內容如下:
// add.js
define(function(require, exports, module) {
console.log(`載入了 add 模組`)
var add = function(x, y) {
return x + y;
};
module.exports = {
add: add
};
});
square.js 的內容如下:
define(function(require, exports, module) {
console.log(`載入了 square 模組`)
var multiplyModule = require(`./multiply`);
module.exports = {
square: function(num) {
return multiplyModule.multiply(num, num)
}
};
});
multiply.js 的內容如下:
define(function(require, exports, module) {
console.log(`載入了 multiply 模組`)
var multiply = function(x, y) {
return x * y;
};
module.exports = {
multiply: multiply
};
});
跟第一個例子是同樣的依賴結構,即 main 依賴 add 和 square,square 又依賴 multiply。
seajs 專案 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/seajs
而如果我們在瀏覽器中開啟 index.html
,列印的順序為:
載入了 add 模組
2
載入了 square 模組
載入了 multiply 模組
9
CMD
與 AMD 一樣,CMD 其實就是 SeaJS 在推廣過程中對模組定義的規範化產出。
你去看 CMD 規範的內容,主要內容就是描述該如何定義模組,如何引入模組,如何匯出模組,只要你按照這個規範書寫程式碼,sea.js 就能正確的進行解析。
AMD 與 CMD 的區別
從 sea.js 和 require.js 的例子可以看出:
1.CMD 推崇依賴就近,AMD 推崇依賴前置。看兩個專案中的 main.js:
// require.js 例子中的 main.js
// 依賴必須一開始就寫好
require([`./add`, `./square`], function(addModule, squareModule) {
console.log(addModule.add(1, 1))
console.log(squareModule.square(3))
});
// sea.js 例子中的 main.js
define(function(require, exports, module) {
var addModule = require(`./add`);
console.log(addModule.add(1, 1))
// 依賴可以就近書寫
var squareModule = require(`./square`);
console.log(squareModule.square(3))
});
2.對於依賴的模組,AMD 是提前執行,CMD 是延遲執行。看兩個專案中的列印順序:
// require.js
載入了 add 模組
載入了 multiply 模組
載入了 square 模組
2
9
// sea.js
載入了 add 模組
2
載入了 square 模組
載入了 multiply 模組
9
AMD 是將需要使用的模組先載入完再執行程式碼,而 CMD 是在 require 的時候才去載入模組檔案,載入完再接著執行。
感謝
感謝 require.js 和 sea.js 在推動 JavaScript 模組化發展方面做出的貢獻。
CommonJS
AMD 和 CMD 都是用於瀏覽器端的模組規範,而在伺服器端比如 node,採用的則是 CommonJS 規範。
匯出模組的方式:
var add = function(x, y) {
return x + y;
};
module.exports.add = add;
引入模組的方式:
var add = require(`./add.js`);
console.log(add.add(1, 1));
我們將之前的例子改成 CommonJS 規範:
// main.js
var add = require(`./add.js`);
console.log(add.add(1, 1))
var square = require(`./square.js`);
console.log(square.square(3));
// add.js
console.log(`載入了 add 模組`)
var add = function(x, y) {
return x + y;
};
module.exports.add = add;
// multiply.js
console.log(`載入了 multiply 模組`)
var multiply = function(x, y) {
return x * y;
};
module.exports.multiply = multiply;
// square.js
console.log(`載入了 square 模組`)
var multiply = require(`./multiply.js`);
var square = function(num) {
return multiply.multiply(num, num);
};
module.exports.square = square;
CommonJS 專案 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/commonJS
如果我們執行 node main.js
,列印的順序為:
載入了 add 模組
2
載入了 square 模組
載入了 multiply 模組
9
跟 sea.js 的執行結果一致,也是在 require 的時候才去載入模組檔案,載入完再接著執行。
CommonJS 與 AMD
引用阮一峰老師的《JavaScript 標準參考教程(alpha)》:
CommonJS 規範載入模組是同步的,也就是說,只有載入完成,才能執行後面的操作。
AMD規範則是非同步載入模組,允許指定回撥函式。
由於 Node.js 主要用於伺服器程式設計,模組檔案一般都已經存在於本地硬碟,所以載入起來比較快,不用考慮非同步載入的方式,所以 CommonJS 規範比較適用。
但是,如果是瀏覽器環境,要從伺服器端載入模組,這時就必須採用非同步模式,因此瀏覽器端一般採用 AMD 規範。
ES6
ECMAScript2015 規定了新的模組載入方案。
匯出模組的方式:
var firstName = `Michael`;
var lastName = `Jackson`;
var year = 1958;
export {firstName, lastName, year};
引入模組的方式:
import {firstName, lastName, year} from `./profile`;
我們再將上面的例子改成 ES6 規範:
目錄結構與 requirejs 和 seajs 目錄結構一致。
<!DOCTYPE html>
<html>
<head>
<title>ES6</title>
</head>
<body>
<h1>Content</h1>
<script src="vender/main.js" type="module"></script>
</body>
</html>
注意!瀏覽器載入 ES6 模組,也使用 <script>
標籤,但是要加入 type="module"
屬性。
// main.js
import {add} from `./add.js`;
console.log(add(1, 1))
import {square} from `./square.js`;
console.log(square(3));
// add.js
console.log(`載入了 add 模組`)
var add = function(x, y) {
return x + y;
};
export {add}
// multiply.js
console.log(`載入了 multiply 模組`)
var multiply = function(x, y) {
return x * y;
};
export {multiply}
// square.js
console.log(`載入了 square 模組`)
import {multiply} from `./multiply.js`;
var square = function(num) {
return multiply(num, num);
};
export {square}
ES6-Module 專案 Demo 地址:https://github.com/mqyqingfeng/Blog/tree/master/demos/ES6/module/ES6
值得注意的,在 Chrome 中,如果直接開啟,會報跨域錯誤,必須開啟伺服器,保證檔案同源才可以有效果。
為了驗證這個效果你可以:
cnpm install http-server -g
然後進入該目錄,執行
http-server
在瀏覽器開啟 http://localhost:8080/
即可檢視效果。
列印的順序為:
載入了 add 模組
載入了 multiply 模組
載入了 square 模組
2
9
跟 require.js 的執行結果是一致的,也就是將需要使用的模組先載入完再執行程式碼。
ES6 與 CommonJS
引用阮一峰老師的 《ECMAScript 6 入門》:
它們有兩個重大差異。
- CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
- CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。
第二個差異可以從兩個專案的列印結果看出,導致這種差別的原因是:
因為 CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。
重點解釋第一個差異。
CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
舉個例子:
// 輸出模組 counter.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模組 main.js
var mod = require(`./counter`);
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
counter.js 模組載入以後,它的內部變化就影響不到輸出的 mod.counter 了。這是因為 mod.counter 是一個原始型別的值,會被快取。
但是如果修改 counter 為一個引用型別的話:
// 輸出模組 counter.js
var counter = {
value: 3
};
function incCounter() {
counter.value++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模組 main.js
var mod = require(`./counter.js`);
console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4
value 是會發生改變的。不過也可以說這是 “值的拷貝”,只是對於引用型別而言,值指的其實是引用。
而如果我們將這個例子改成 ES6:
// counter.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from `./counter`;
console.log(counter); // 3
incCounter();
console.log(counter); // 4
這是因為
ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令 import,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的 import 有點像 Unix 系統的“符號連線”,原始值變了,import 載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。
Babel
鑑於瀏覽器支援度的問題,如果要使用 ES6 的語法,一般都會藉助 Babel,可對於 import 和 export 而言,只借助 Babel 就可以嗎?
讓我們看看 Babel 是怎麼編譯 import 和 export 語法的。
// ES6
var firstName = `Michael`;
var lastName = `Jackson`;
var year = 1958;
export {firstName, lastName, year};
// Babel 編譯後
`use strict`;
Object.defineProperty(exports, "__esModule", {
value: true
});
var firstName = `Michael`;
var lastName = `Jackson`;
var year = 1958;
exports.firstName = firstName;
exports.lastName = lastName;
exports.year = year;
是不是感覺有那麼一點奇怪?編譯後的語法更像是 CommonJS 規範,再看 import 的編譯結果:
// ES6
import {firstName, lastName, year} from `./profile`;
// Babel 編譯後
`use strict`;
var _profile = require(`./profile`);
你會發現 Babel 只是把 ES6 模組語法轉為 CommonJS 模組語法,然而瀏覽器是不支援這種模組語法的,所以直接跑在瀏覽器會報錯的,如果想要在瀏覽器中執行,還是需要使用打包工具將程式碼打包。
webpack
Babel 將 ES6 模組轉為 CommonJS 後, webpack 又是怎麼做的打包的呢?它該如何將這些檔案打包在一起,從而能保證正確的處理依賴,以及能在瀏覽器中執行呢?
首先為什麼瀏覽器中不支援 CommonJS 語法呢?
這是因為瀏覽器環境中並沒有 module、 exports、 require 等環境變數。
換句話說,webpack 打包後的檔案之所以在瀏覽器中能執行,就是靠模擬了這些變數的行為。
那怎麼模擬呢?
我們以 CommonJS 專案中的 square.js 為例,它依賴了 multiply 模組:
console.log(`載入了 square 模組`)
var multiply = require(`./multiply.js`);
var square = function(num) {
return multiply.multiply(num, num);
};
module.exports.square = square;
webpack 會將其包裹一層,注入這些變數:
function(module, exports, require) {
console.log(`載入了 square 模組`);
var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
}
那 webpack 又會將 CommonJS 專案的程式碼打包成什麼樣呢?我寫了一個精簡的例子,你可以直接複製到瀏覽器中檢視效果:
// 自執行函式
(function(modules) {
// 用於儲存已經載入過的模組
var installedModules = {};
function require(moduleName) {
if (installedModules[moduleName]) {
return installedModules[moduleName].exports;
}
var module = installedModules[moduleName] = {
exports: {}
};
modules[moduleName](module, module.exports, require);
return module.exports;
}
// 載入主模組
return require("main");
})({
"main": function(module, exports, require) {
var addModule = require("./add");
console.log(addModule.add(1, 1))
var squareModule = require("./square");
console.log(squareModule.square(3));
},
"./add": function(module, exports, require) {
console.log(`載入了 add 模組`);
module.exports = {
add: function(x, y) {
return x + y;
}
};
},
"./square": function(module, exports, require) {
console.log(`載入了 square 模組`);
var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
},
"./multiply": function(module, exports, require) {
console.log(`載入了 multiply 模組`);
module.exports = {
multiply: function(x, y) {
return x * y;
}
};
}
})
最終的執行結果為:
載入了 add 模組
2
載入了 square 模組
載入了 multiply 模組
9
參考
ES6 系列
ES6 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。
如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。
相關文章
- ES6 系列之模組載入方案
- ES6模組(Module)載入知識總結(一)
- Angular入門到精通系列教程(11)- 模組(NgModule),延遲載入模組Angular
- 關於前端模組化 CommonJS、AMD、CMD、ES6中模組載入前端JS
- js模組化之自定義模組(頁面模組化載入)JS
- 前端模組化之迴圈載入前端
- 前端微服務化解決方案3 - 模組載入器前端微服務
- ES6之路之模組詳解
- web系列之模組化——AMD、CMD、CommonJS、ES6 整理&&比較WebJS
- ES6 系列之模板字串字串
- 模組載入器
- swiper 模組載入
- 深入探究ES6之模組系統
- es6快速入門 系列 - async
- es6 快速入門 系列 —— promisePromise
- CommonJS和ES6模組迴圈載入處理的區別JS
- 入門到放棄node系列之網路模組(二)
- es6模組化的匯入匯出
- JavaScript 模組化程式設計之載入器原理JavaScript程式設計
- swoole 模組的載入
- JavaScript 模組載入特性JavaScript
- Webpack模組載入器Web
- php載入memcache模組PHP
- 【JVM】JVM系列之類載入機制(四)JVM
- VUE系列之效能最佳化--懶載入Vue
- ES6 module載入機制
- JVM系列之類載入流程-自定義類載入器JVM
- ES6模組
- ES6 系列之箭頭函式函式
- ES6模組化之export和import的用法ExportImport
- ABP - 模組載入機制
- Helloworld 驅動模組載入
- ES6系列之我們來聊聊PromisePromise
- ES6 系列之我們來聊聊 PromisePromise
- ES6 - 模組化
- ES6模組化
- 前端模組化:CommonJS,AMD,CMD,ES6(轉載)前端JS
- 【Dubbo原始碼閱讀系列】之 Dubbo XML 配置載入原始碼XML