本篇咱們重點介紹如下四種模塊加載規範:javascript
最後再延伸講下 Babel 的編譯和 webpack 的打包原理。html
在瞭解 AMD 規範以前,咱們先來看看 require.js 的使用方式。java
項目目錄爲:node
* project/ * index.html * vender/ * main.js * require.js * add.js * square.js * multiply.js
index.html
的內容以下:webpack
<!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
。git
main.js
的配置以下:es6
// main.js require(['./add', './square'], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3)) });
require 的第一個參數表示依賴的模塊的路徑,第二個參數表示此模塊的內容。github
由此能夠看出,主模塊
依賴 add 模塊
和 square 模塊
。web
咱們看下 add 模塊
即 add.js
的內容:npm
// 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
在上節,咱們說了這樣一句話:
requirejs
爲全局添加了define
函數,你只要按照這種約定的方式書寫這個模塊便可。
那這個約定的書寫方式是指什麼呢?
指的即是 The Asynchronous Module Definition (AMD) 規範。
因此其實 AMD 是 RequireJS 在推廣過程當中對模塊定義的規範化產出。
你去看 AMD 規範) 的內容,其主要內容就是定義了 define 函數該如何書寫,只要你按照這個規範書寫模塊和依賴,require.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
與 AMD 同樣,CMD 其實就是 SeaJS 在推廣過程當中對模塊定義的規範化產出。
你去看 CMD 規範的內容,主要內容就是描述該如何定義模塊,如何引入模塊,如何導出模塊,只要你按照這個規範書寫代碼,sea.js 就能正確的進行解析。
從 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 模塊化發展方面作出的貢獻。
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 的時候纔去加載模塊文件,加載完再接着執行。
引用阮一峯老師的《JavaScript 標準參考教程(alpha)》:
CommonJS 規範加載模塊是同步的,也就是說,只有加載完成,才能執行後面的操做。AMD規範則是非同步加載模塊,容許指定回調函數。
因爲 Node.js 主要用於服務器編程,模塊文件通常都已經存在於本地硬盤,因此加載起來比較快,不用考慮非同步加載的方式,因此 CommonJS 規範比較適用。
可是,若是是瀏覽器環境,要從服務器端加載模塊,這時就必須採用非同步模式,所以瀏覽器端通常採用 AMD 規範。
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 的執行結果是一致的,也就是將須要使用的模塊先加載完再執行代碼。
引用阮一峯老師的 《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 模塊是動態引用,而且不會緩存值,模塊裏面的變量綁定其所在的模塊。
鑑於瀏覽器支持度的問題,若是要使用 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 模塊語法,然而瀏覽器是不支持這種模塊語法的,因此直接跑在瀏覽器會報錯的,若是想要在瀏覽器中運行,仍是須要使用打包工具將代碼打包。
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 系列目錄地址:https://github.com/mqyqingfeng/Blog
ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級做用域、標籤模板、箭頭函數、Symbol、Set、Map 以及 Promise 的模擬實現、模塊加載方案、異步處理等內容。
若是有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。若是喜歡或者有所啓發,歡迎 star,對做者也是一種鼓勵。