
框架總覽
?? 前言
?? 模塊化的理解
- ?? 什么是模塊
- ?? 模塊化的進(jìn)化過程
- ?? 模塊化的好處
- ?? 引入多個(gè)<script>后出現(xiàn)出現(xiàn)問題
?? 模塊化規(guī)范
- ?? CommonJS 模塊規(guī)范
- ?? AMD 規(guī)范
- ?? CMD 規(guī)范
- ?? Es6模塊化
- ?? 總結(jié)
?? Node.js模塊分類
?? nodejs模塊使用
- ?? 創(chuàng)建 & 導(dǎo)出模塊
- ?? 引入模塊
?? require的加載機(jī)制
- ?? 路徑分析
- ?? 文件定位
- ?? 編譯執(zhí)行
?? 模塊循環(huán)引用問題
?? 站在巨人肩上
1. 前言
隨著公司618,雙11大促的到來。erp搭建的活動(dòng)頁面邏輯和交互越來越復(fù)雜。此時(shí)在JS方面就會(huì)考慮使用模塊化規(guī)范去管理。一來便于開發(fā)梳理調(diào)理。二來也可以多人協(xié)作,最后配合bable可以使用很多ES6、ES7的新功能??梢哉f,公司目前很多復(fù)雜的頁面非常需要模塊化開發(fā)。
本文內(nèi)容主要有理解模塊化,為什么要模塊化,模塊化的優(yōu)缺點(diǎn)以及模塊化規(guī)范,介紹下開發(fā)中最流行的CommonJS, AMD, ES6、CMD規(guī)范。并介紹node中的模塊引入機(jī)制。本文試圖站在小白的角度,用通俗易懂的筆調(diào)介紹這些枯燥無味的概念,希望諸君閱讀后,對(duì)模塊化編程有個(gè)全新的認(rèn)識(shí)和理解!如有錯(cuò)誤請(qǐng)多多包涵。
1. 模塊化的理解
1.1.什么是模塊?
- 將一個(gè)復(fù)雜的程序依據(jù)一定的規(guī)則(規(guī)范)封裝成幾個(gè)塊(文件), 并進(jìn)行組合在一起
- 塊的內(nèi)部數(shù)據(jù)與實(shí)現(xiàn)是私有的, 只是向外部暴露一些接口(方法)與外部其它模塊通信
2.2.模塊化的進(jìn)化過程
-
全局function模式 : 將不同的功能封裝成不同的全局函數(shù)
- 編碼: 將不同的功能封裝成不同的全局函數(shù)
- 問題: 污染全局命名空間, 容易引起命名沖突或數(shù)據(jù)不安全,而且模塊成員之間看不出直接關(guān)系
function m1(){
//...
}
function m2(){
//...
}
-
namespace模式 : 簡(jiǎn)單對(duì)象封裝
- 作用: 減少了全局變量,解決命名沖突
- 問題: 數(shù)據(jù)不安全(外部可以直接修改模塊內(nèi)部的數(shù)據(jù))
let myModule = {
data: 'www.baidu.com',
foo() {
console.log(`foo() ${this.data}`)
},
bar() {
console.log(`bar() ${this.data}`)
}
}
myModule.data = 'other data' //能直接修改模塊內(nèi)部的數(shù)據(jù)
myModule.foo() // foo() other data
這樣的寫法會(huì)暴露所有模塊成員,內(nèi)部狀態(tài)可以被外部改寫。
-
IIFE模式:匿名函數(shù)自調(diào)用(閉包)
- 作用: 數(shù)據(jù)是私有的, 外部只能通過暴露的方法操作
- 編碼: 將數(shù)據(jù)和行為封裝到一個(gè)函數(shù)內(nèi)部, 通過給window添加屬性來向外暴露接口
- 問題: 如果當(dāng)前這個(gè)模塊依賴另一個(gè)模塊怎么辦?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
myModule.bar()
console.log(myModule.data) //undefined 不能訪問模塊內(nèi)部數(shù)據(jù)
myModule.data = 'xxxx' //不是修改的模塊內(nèi)部的data
myModule.foo() //沒有改變
</script>
// module.js文件
(function(window) {
let data = 'www.baidu.com'
//操作數(shù)據(jù)的函數(shù)
function foo() {
//用于暴露有函數(shù)
console.log(`foo() ${data}`)
}
function bar() {
//用于暴露有函數(shù)
console.log(`bar() ${data}`)
otherFun() //內(nèi)部調(diào)用
}
function otherFun() {
//內(nèi)部私有的函數(shù)
console.log('otherFun()')
}
//暴露行為
window.myModule = { foo, bar } //ES6寫法
})(window)
最后得到的結(jié)果:

- IIFE模式增強(qiáng) : 引入依賴
這就是現(xiàn)代模塊實(shí)現(xiàn)的基石
// module.js文件
(function(window, $) {
let data = 'www.baidu.com'
//操作數(shù)據(jù)的函數(shù)
function foo() {
//用于暴露有函數(shù)
console.log(`foo() ${data}`)
$('body').css('background', 'red')
}
function bar() {
//用于暴露有函數(shù)
console.log(`bar() ${data}`)
otherFun() //內(nèi)部調(diào)用
}
function otherFun() {
//內(nèi)部私有的函數(shù)
console.log('otherFun()')
}
//暴露行為
window.myModule = { foo, bar }
})(window, jQuery)
// index.html文件
<!-- 引入的js必須有一定順序 -->
<script type="text/javascript" src="jquery-1.10.1.js"></script>
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
myModule.foo()
</script>
上例子通過jquery方法將頁面的背景顏色改成紅色,所以必須先引入jQuery庫,就把這個(gè)庫當(dāng)作參數(shù)傳入。這樣做除了保證模塊的獨(dú)立性,還使得模塊之間的依賴關(guān)系變得明顯??梢詤⒖脊镜腶ctivity2018.js.
2.3 模塊化的好處
- 避免命名沖突(減少命名空間污染)
- 更好的分離, 按需加載
- 更高復(fù)用性
- 高可維護(hù)性
2.4 引入多個(gè)<script>后出現(xiàn)出現(xiàn)問題
- 請(qǐng)求過多
首先我們要依賴多個(gè)模塊,那樣就會(huì)發(fā)送多個(gè)請(qǐng)求,導(dǎo)致請(qǐng)求過多
- 依賴模糊
我們不知道他們的具體依賴關(guān)系是什么,也就是說很容易因?yàn)椴涣私馑麄冎g的依賴關(guān)系導(dǎo)致加載先后順序出錯(cuò)。
- 難以維護(hù)
以上兩種原因就導(dǎo)致了很難維護(hù),很可能出現(xiàn)牽一發(fā)而動(dòng)全身的情況導(dǎo)致項(xiàng)目出現(xiàn)嚴(yán)重的問題。
模塊化固然有多個(gè)好處,然而一個(gè)頁面需要引入多個(gè)js文件,就會(huì)出現(xiàn)以上這些問題。而這些問題可以通過模塊化規(guī)范來解決,下面介紹開發(fā)中最流行的commonjs, AMD, ES6, CMD規(guī)范。
3. 模塊化規(guī)范
3.1 CommonJS規(guī)范

(1)概述
Node 應(yīng)用由模塊組成,采用 CommonJS 模塊規(guī)范。每個(gè)文件就是一個(gè)模塊,有自己的作用域。在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對(duì)其他文件不可見。commonJS用同步的方式加載模塊。在服務(wù)端,模塊文件都存在本地磁盤,讀取非常快,所以這樣做不會(huì)有問題。但是在瀏覽器端,限于網(wǎng)絡(luò)原因,更合理的方案是使用異步加載。
(2)特點(diǎn)
所有代碼都運(yùn)行在模塊作用域,不會(huì)污染全局作用域。
模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。
(3)基本語法
暴露模塊:module.exports = value或exports.xxx = value
引入模塊:require(xxx),如果是第三方模塊,xxx為模塊名;如果是自定義模塊,xxx為模塊文件路徑
CommonJS規(guī)范規(guī)定,每個(gè)模塊內(nèi)部,module變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,它的exports屬性(即module.exports)是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
上面代碼通過module.exports輸出變量x和函數(shù)addX。
var example = require('./example.js');//如果參數(shù)字符串以“./”開頭,則表示加載的是一個(gè)位于相對(duì)路徑
console.log(example.x); // 5
console.log(example.addX(1)); // 6
require命令用于加載模塊文件。require命令的基本功能是,讀入并執(zhí)行一個(gè)JavaScript文件,然后返回該模塊的module.exports對(duì)象。如果沒有發(fā)現(xiàn)指定模塊,會(huì)報(bào)錯(cuò)。
Node.js 借鑒了 CommonJS 規(guī)范的設(shè)計(jì),特別是 CommonJS 的 Modules 規(guī)范,實(shí)現(xiàn)了一套模塊系統(tǒng),同時(shí) NPM 實(shí)現(xiàn)了 CommonJS 的 Packages 規(guī)范,模塊和包組成了 Node 應(yīng)用開發(fā)的基礎(chǔ)。
3.2 AMD 規(guī)范
CommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。AMD規(guī)范則是非同步加載模塊,允許指定回調(diào)函數(shù)。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。但是,如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,這時(shí)就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。此外用AMD規(guī)范進(jìn)行頁面開發(fā)需要用到對(duì)應(yīng)的庫函數(shù),也就是大名鼎鼎RequireJS,實(shí)際上AMD 是 RequireJS 在推廣過程中對(duì)模塊定義的規(guī)范化的產(chǎn)出.
(1)AMD規(guī)范基本語法
定義暴露模塊:
//定義沒有依賴的模塊
define(function(){
return 模塊
})
//定義有依賴的模塊
define(['module1', 'module2'], function(m1, m2){
return 模塊
})
引入使用模塊:
require(['module1', 'module2'], function(m1, m2){
使用m1/m2
})
(2)requireJS主要解決兩個(gè)問題
多個(gè)js文件可能有依賴關(guān)系,被依賴的文件需要早于依賴它的文件加載到瀏覽器
js加載的時(shí)候?yàn)g覽器會(huì)停止頁面渲染,加載文件越多,頁面失去響應(yīng)時(shí)間越長(zhǎng)
(3) 未使用AMD規(guī)范與使用require.js
通過比較兩者的實(shí)現(xiàn)方法,來說明使用AMD規(guī)范的好處。
- 未使用AMD規(guī)范
// dataService.js文件
(function (window) {
let msg = 'www.baidu.com'
function getMsg() {
return msg.toUpperCase()
}
window.dataService = {getMsg}
})(window)
// alerter.js文件
(function (window, dataService) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>
最后得到如下結(jié)果:

- 使用require.js
RequireJS是一個(gè)工具庫,主要用于客戶端的模塊管理。它的模塊管理遵守AMD規(guī)范,RequireJS的基本思想是,通過define方法,將代碼定義為模塊;通過require方法,實(shí)現(xiàn)代碼的模塊加載。
接下來介紹AMD規(guī)范在瀏覽器實(shí)現(xiàn)的步驟:
①下載require.js, 并引入
- 官網(wǎng):
http://www.requirejs.cn/ - github :
https://github.com/requirejs/requirejs
然后將require.js導(dǎo)入項(xiàng)目: js/libs/require.js
②創(chuàng)建項(xiàng)目結(jié)構(gòu)
|-js
|-libs
|-require.js
|-modules
|-alerter.js
|-dataService.js
|-main.js
|-index.html
③定義require.js的模塊代碼
// dataService.js文件
// 定義沒有依賴的模塊
define(function() {
let msg = 'www.baidu.com'
function getMsg() {
return msg.toUpperCase()
}
return { getMsg } // 暴露模塊
})
//alerter.js文件
// 定義有依賴的模塊
define(['dataService'], function(dataService) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
// 暴露模塊
return { showMsg }
})
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路徑 出發(fā)點(diǎn)在根目錄下
paths: {
//映射: 模塊標(biāo)識(shí)名: 路徑
alerter: './modules/alerter', //此處不能寫成alerter.js,會(huì)報(bào)錯(cuò)
dataService: './modules/dataService'
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()
// index.html文件
<!DOCTYPE html>
<html>
<head>
<title>Modular Demo</title>
</head>
<body>
<!-- 引入require.js并指定js主文件的入口 -->
<script data-main="js/main" src="js/libs/require.js"></script>
</body>
</html>
④頁面引入require.js模塊:
在index.html引入 <script data-main="js/main" src="js/libs/require.js"></script>
此外在項(xiàng)目中如何引入第三方庫?只需在上面代碼的基礎(chǔ)稍作修改:
// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
let name = 'Tom'
function showMsg() {
alert(dataService.getMsg() + ', ' + name)
}
$('body').css('background', 'green')
// 暴露模塊
return { showMsg }
})
// main.js文件
(function() {
require.config({
baseUrl: 'js/', //基本路徑 出發(fā)點(diǎn)在根目錄下
paths: {
//自定義模塊
alerter: './modules/alerter', //此處不能寫成alerter.js,會(huì)報(bào)錯(cuò)
dataService: './modules/dataService',
// 第三方庫模塊
jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會(huì)報(bào)錯(cuò)
}
})
require(['alerter'], function(alerter) {
alerter.showMsg()
})
})()
上例是在alerter.js文件中引入jQuery第三方庫,main.js文件也要有相應(yīng)的路徑配置。
小結(jié):require()函數(shù)在加載依賴的函數(shù)的時(shí)候是異步加載的,這樣瀏覽器不會(huì)失去響應(yīng),它指定的回調(diào)函數(shù),只有依賴的模塊都加載成功后,才會(huì)運(yùn)行,解決了依賴性的問題。
3.3 CMD 規(guī)范
CMD 即Common Module Definition通用模塊定義,CMD是另一種js模塊化方案,它與AMD很類似,不同點(diǎn)在于:AMD 推崇依賴前置、提前執(zhí)行,CMD推崇依賴就近、延遲執(zhí)行。此規(guī)范其實(shí)是在sea.js推廣過程中產(chǎn)生的。
/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面聲明并初始化了要用到的所有模塊
a.doSomething();
if (false) {
// 即便沒用到某個(gè)模塊 b,但 b 還是提前執(zhí)行了
b.doSomething()
}
});
/** CMD寫法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要時(shí)申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
(1)CMD規(guī)范基本語法
定義暴露模塊:
//定義沒有依賴的模塊
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定義有依賴的模塊
define(function(require, exports, module){
//引入依賴模塊(同步)
var module2 = require('./module2')
//引入依賴模塊(異步)
require.async('./module3', function (m3) {
})
//暴露模塊
exports.xxx = value
})
引入使用模塊:
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
(2)sea.js簡(jiǎn)單使用教程
①下載sea.js, 并引入
- 官網(wǎng): http://seajs.org/
- github : https://github.com/seajs/seajs
然后將sea.js導(dǎo)入項(xiàng)目: js/libs/sea.js
②創(chuàng)建項(xiàng)目結(jié)構(gòu)
|-js
|-libs
|-sea.js
|-modules
|-module1.js
|-module2.js
|-module3.js
|-module4.js
|-main.js
|-index.html
③定義sea.js的模塊代碼
// module1.js文件
define(function (require, exports, module) {
//內(nèi)部變量數(shù)據(jù)
var data = 'atguigu.com'
//內(nèi)部函數(shù)
function show() {
console.log('module1 show() ' + data)
}
//向外暴露
exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
module.exports = {
msg: 'I Will Back'
}
})
// module3.js文件
define(function(require, exports, module) {
const API_KEY = 'abc123'
exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
//引入依賴模塊(同步)
var module2 = require('./module2')
function show() {
console.log('module4 show() ' + module2.msg)
}
exports.show = show
//引入依賴模塊(異步)
require.async('./module3', function (m3) {
console.log('異步引入依賴模塊3 ' + m3.API_KEY)
})
})
// main.js文件
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})
④在index.html中引入
<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/modules/main')
</script>
最后得到結(jié)果如下:
module1 show() atguigu.com
module4 show() I Will Back
異步引入依賴模塊3
abc123
Es6模塊化
在之前的javascript中是沒有模塊化概念的。如果要進(jìn)行模塊化操作,需要引入第三方的類庫。隨著技術(shù)的發(fā)展,前后端分離,前端的業(yè)務(wù)變的越來越復(fù)雜化。直至ES6帶來了模塊化,才讓javascript第一次支持了module。ES6的模塊化分為導(dǎo)出(export)與導(dǎo)入(import)兩個(gè)模塊。
export的用法
在ES6中每一個(gè)模塊即是一個(gè)文件,在文件中定義的變量,函數(shù),對(duì)象在外部是無法獲取的。如果你希望外部可以讀取模塊當(dāng)中的內(nèi)容,就必須使用export來對(duì)其進(jìn)行暴露(輸出)。先來看個(gè)例子,來對(duì)一個(gè)變量進(jìn)行模塊化。我們先來創(chuàng)建一個(gè)test.js文件,來對(duì)這一個(gè)變量進(jìn)行輸出:
export let myName="laowang";
然后可以創(chuàng)建一個(gè)index.js文件,以import的形式將這個(gè)變量進(jìn)行引入:
import {myName} from "./test.js";
console.log(myName);//laowang
如果要輸出多個(gè)變量可以將這些變量包裝成對(duì)象進(jìn)行模塊化輸出:
let myName="laowang";
let myAge=90;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"歲了"
}
export {
myName,
myAge,
myfn
}
/******************************接收的代碼調(diào)整為**********************/
import {myfn,myAge,myName} from "./test.js";
console.log(myfn());//我是laowang!今年90歲了
console.log(myAge);//90
console.log(myName);//laowang
如果你不想暴露模塊當(dāng)中的變量名字,可以通過as來進(jìn)行操作:
let myName="laowang";
let myAge=90;
let myfn=function(){
return "我是"+myName+"!今年"+myAge+"歲了"
}
export {
myName as name,
myAge as age,
myfn as fn
}
/******************************接收的代碼調(diào)整為**********************/
import {fn,age,name} from "./test.js";
console.log(fn());//我是laowang!今年90歲了
console.log(age);//90
console.log(name);//laowang
默認(rèn)導(dǎo)出(default export)
一個(gè)模塊只能有一個(gè)默認(rèn)導(dǎo)出,對(duì)于默認(rèn)導(dǎo)出,導(dǎo)入的名稱可以和導(dǎo)出的名稱不一致。
/******************************導(dǎo)出**********************/
export default function(){
return "默認(rèn)導(dǎo)出一個(gè)方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意這里默認(rèn)導(dǎo)出不需要用{}。名稱隨意起
console.log(myFn());//默認(rèn)導(dǎo)出一個(gè)方法
export和default export的區(qū)別
1.exports default 后面跟一個(gè)具體的值, export 后面跟變量申明語句。
本質(zhì)上,export default value就是輸出一個(gè)叫做default的變量。default是被value賦值的,正是因?yàn)閑xport default命令其實(shí)只是輸出一個(gè)叫做default的變量,所以它后面不能跟變量聲明語句。require引入default的值,并為其起一個(gè)名字。接下來,我們可以實(shí)踐下。
首先看看 export 的執(zhí)行情況:
export let test1 = 'test1';
import {test1} from "./index";
console.log(test1);
輸出// test1
接下來,我們來看看export default 的執(zhí)行情況:
export default let test1 = 'test1'
import test1 from "./index";
console.log(test1);
// 報(bào)錯(cuò)。
2.使用export, import 需要加大括號(hào)(* 除外), export default 則不需要
3.export default 在一個(gè)模塊里只能有一個(gè),但是export可以有多個(gè)
首先看看 export 的執(zhí)行情況:
let test1 = 'test1'
let test2 = 'test2'
export {
test1,
test2
}
import {test1, test2} from "./index";
console.log(test1, test2);
上述代碼的執(zhí)行結(jié)果如下:
// test1
// test2
接下來看看export default 的執(zhí)行情況
let test1 = 'test1'
let test2 = 'test2'
export default test1
export default test2
import test1, test2 from "./index";
console.log(test1, test2);
復(fù)制代碼上述代碼的執(zhí)行結(jié)果如下:
// 報(bào)錯(cuò)
4.通過export導(dǎo)出的屬性或者方法可以修改,通過export default 導(dǎo)出的基本類型不可修改
首先看看 export 的執(zhí)行情況:
let test1 = 'test1'
let test2 = {
a: '1'
}
export {
test1,
test2
}
test1 = 'test1 modify'
test2.a = '1 modify'
import {test1, test2} from "./index";
console.log(test1, test2);
上述代碼的執(zhí)行結(jié)果如下:
test1 modify {a: "1 modify"}
接下來看看export default 的執(zhí)行情況
let test1 = 'test1'
export default test1
test1 = 'test1 modify'
import test1 from "./index";
console.log(test1);
上述代碼的執(zhí)行結(jié)果如下:
// test1
上述代碼證明export導(dǎo)出的屬性或者方法可以修改,無論是基本類型,還是引用類型。
let test1 = {
a: 'test1'
}
export default test1
test1.a = 'test1 modify'
import test1 from "./index";
console.log(test1);
上述代碼的執(zhí)行結(jié)果如下:
{a: "test1 modify"}
export default value,相當(dāng)于default = value。 import的時(shí)候可以將值賦值給任意一個(gè)變量,a/b/c等都行。所以當(dāng)value是基本數(shù)據(jù)類型的時(shí)候。value修改并不會(huì)引起require引入的值的修改。因?yàn)閑xport default也是一種
export,所以all實(shí)質(zhì)上是{a,default}因?yàn)閑xport導(dǎo)出的是一個(gè)變量。所以可以修改。
ES6 模塊與 CommonJS 模塊的差異
它們有兩個(gè)重大差異:
① CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。
② CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。
第二個(gè)差異是因?yàn)?CommonJS 加載的是一個(gè)對(duì)象(即module.exports屬性),該對(duì)象只有在腳本運(yùn)行完才會(huì)生成。而 ES6 模塊不是對(duì)象,它的對(duì)外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會(huì)生成
到此,前端模塊化講完。接下來講node模塊。

4. Node.js模塊分類
前文說, 在 Node.js 中, 每個(gè)文件就被視為一個(gè)模塊. 這個(gè)文件可能是 JavaScript 編寫的文件、JSON 或者用 C/C++ 編譯的二進(jìn)制文件.
模塊可以分成三類:

- 『 核心模塊 』: Node.js 自帶的原生模塊. 比如,
http,fs,url. 其中分為 C/C++ 編寫的和 JavaScript 編寫的兩部分. C/C++ 模塊存放在 Node.js 源代碼目錄的src/目錄下. JavaScript 模塊存放在lib/目錄下. 核心模塊在Node源碼編譯成可執(zhí)行文件時(shí)存為二進(jìn)制文件,直接加載在內(nèi)存中,所以不用文件定位和編譯執(zhí)行。 - 『 文件模塊 』: 開發(fā)人員在本地寫的模塊. 加載時(shí)通過相對(duì)路徑, 絕對(duì)路徑來定位模塊所在位置.在運(yùn)行時(shí)動(dòng)態(tài)加載,包括了上述完整的路徑分析、文件定位、編譯執(zhí)行這些過程
- 『 第三方模塊 』: 別人編寫的模塊, 通過包管理工具, 比如 npm, yarn, 可以將其從網(wǎng)絡(luò)上引入到本地項(xiàng)目, 供己使用.
5.nodejs模塊使用
在了解了什么是模塊之后, 讓我們來看看如何在 Node.js 中實(shí)際應(yīng)用模塊機(jī)制. 在使用上, 可以很簡(jiǎn)單的分為三個(gè)步驟: 創(chuàng)建, 導(dǎo)出, 引入.。先創(chuàng)建一個(gè)模塊, 然后導(dǎo)出功能或數(shù)據(jù), 模塊之間可以互相引入導(dǎo)出的內(nèi)容.
Node.js 提供了exports 和 require 兩個(gè)對(duì)象,其中exports用于導(dǎo)出模塊,require 用于從外部引入另一個(gè)模塊, 即獲取模塊的 exports 對(duì)象.
5.1創(chuàng)建 & 導(dǎo)出模塊
先讓我們來看看如何創(chuàng)建并把模塊的內(nèi)容導(dǎo)出. 在Node.js中, 一個(gè)文件就是一個(gè)模塊. 創(chuàng)建模塊的方法就是創(chuàng)建一個(gè)文件.
通過 exports對(duì)象來指定一個(gè)模塊的導(dǎo)出內(nèi)容.
示例:
// 文件名: nameModule.js
var name = 'Garrik';
exports.setName = function(newName) {
name = newName;
}
exports.getName = function() {
return name;
}
在以上示例中, nameModule.js 文件通過 exports 對(duì)象將 setName 和 getName 作為模塊的訪問接口. 其他的模塊可以引入導(dǎo)出的 exports 對(duì)象, 直接訪問 exports 對(duì)象的成員函數(shù).
5.2引入模塊
在 Node.js 中, 通過 require 函數(shù)來引入外界模塊導(dǎo)出的內(nèi)容. require 函數(shù)接受一個(gè)字符串作為路徑參數(shù), 函數(shù)根據(jù)這個(gè)字符串參數(shù)來進(jìn)行模塊查找. 找到后會(huì)返回目標(biāo)模塊導(dǎo)出的 exports 對(duì)象.
示例:
// 文件名: showNameModule.js
var nameModule = require('./nameModule.js');
console.log(nameModule.getName());
// 顯示: Garrik
nameModule.setName('Xiang');
console.log(nameModule.getName());
// 顯示: Xiang
上面示例中, 通過require引入了當(dāng)前目錄下nameModule.js導(dǎo)出的 exports對(duì)象, 并讓一個(gè)本地變量指向引入模塊的 exports 對(duì)象. 之后在 showNameModule.js 文件中就可以使用getName 和 setName 這兩個(gè)方法了.
6. require的加載機(jī)制

上述模塊規(guī)范看起來十分簡(jiǎn)單,只有module、exports和require,但 Node 是如何實(shí)現(xiàn)的呢?
需要經(jīng)歷路徑分析(模塊的完整路徑)、文件定位(文件擴(kuò)展名或目錄)、編譯執(zhí)行三個(gè)步驟。
6.1 路徑分析
回顧require()接收 模塊標(biāo)識(shí) 作為參數(shù)來引入模塊,Node 就是基于這個(gè)標(biāo)識(shí)符進(jìn)行路徑分析。不同的標(biāo)識(shí)符采用的分析方式是不同的,主要分為以下幾類:
-
Node 提供的核心模塊,如 http、fs、path
核心模塊在 Node 源碼編譯時(shí)存為二進(jìn)制執(zhí)行文件,在 Node 啟動(dòng)時(shí)直接加載到內(nèi)存中,因?yàn)椴恍枰窂椒治龊臀募ㄎ?,所以加載速度很快,而且也不用后續(xù)的文件定位和編譯執(zhí)行。
如果想加載與核心模塊同名的自定義模塊,如自定義 http 模塊,那必須選用不同標(biāo)志符或改用路徑方式。
.、..形式的文件模塊
以.、..或/開始的標(biāo)識(shí)符都會(huì)當(dāng)成文件模塊處理,Node 會(huì)將require('./untils.js')中的路徑作為參數(shù)獲取模塊可能出現(xiàn)的位置,并以數(shù)組的形式返回文件所在的父級(jí),比如[E:/moudles]
- 自定義文件模塊,即非路徑形式的文件模塊
自定義文件模塊是特殊的文件模塊,在路徑查找時(shí) Node 會(huì)從所在的父級(jí)開始逐級(jí)查找該模塊路徑中的node_modules路徑,直到根目錄,生成一個(gè)可能路徑的數(shù)組。并將這個(gè)數(shù)組返回。
模塊路徑查找策略示例如下:
// Module._resolveLookupPaths代碼相對(duì)復(fù)雜,這里簡(jiǎn)單起見只展示一些其執(zhí)行結(jié)果
tt.js文件目錄d/wedoctor
node tt.js
console.log(module.constructor._resolveLookupPaths('fs', module, true))
console.log(module.constructor._resolveLookupPaths('/hello', module, true))
console.log(module.constructor._resolveLookupPaths('../../hello', module, true))
console.log(module.constructor._resolveLookupPaths('hello', module, true))
// 1.加載核心模塊的時(shí)候,返回 null
// 2.加載絕對(duì)路徑的時(shí)候,返回
[ 'D:\\wedoctor\\node_modules',
'D:\\node_modules',
'C:\\Users\\小韓\\.node_modules',
'C:\\Users\\小韓\\.node_libraries',
'D:\\tools\\nodejs\\lib\\node' ]
// 由于是絕對(duì)路徑,所以在_findPath方法中會(huì)被清空
// 3.加載相對(duì)路徑的時(shí)候,返回
[ 'D:\\wedoctor' ]
// 4.加載自定義模塊的時(shí)候,返回
[ 'D:\\wedoctor\\node_modules',
'D:\\node_modules',
'C:\\Users\\小韓\\.node_modules',
'C:\\Users\\小韓\\.node_libraries',
'D:\\tools\\nodejs\\lib\\node' ]
//上面的數(shù)組,就是模塊所有可能的路徑?;旧鲜牵瑥漠?dāng)前路徑開始一級(jí)級(jí)向上尋找 node_modules 子目錄。
路徑分析只是獲取文件可能出現(xiàn)的位置,將可能出現(xiàn)位置組成的數(shù)組返回,其中文件模塊返回require引用文件的父級(jí)組成的數(shù)組:[modu.parent],第三方模塊返回沿當(dāng)前路徑向上逐級(jí)查找node_modules目錄直到根目錄組成的數(shù)組
paths
6.2 文件定位
模塊路徑分析完成后緊接著的步驟是文件定位。文件定位分為以下幾個(gè)步驟:
1.從 path數(shù)組中取出第一個(gè)目錄作為查找基準(zhǔn)。比如
root/src。Node 會(huì)將require()中的路徑./untils.js和當(dāng)前查找基準(zhǔn)合并。成為真實(shí)路徑root/src/untils.js。從目錄中查找該文件,如果存在,就結(jié)束查找。為索引,然后編譯執(zhí)行。如果不存在,就向上進(jìn)行下一條查找作。如果省略后綴名,將跳過第一步執(zhí)行第二步。2.通過添加.js .json .node后綴查找,如果存在文件就結(jié)束查找。如果不存在,則進(jìn)行下一條。
- 將require的參數(shù)作為一個(gè)包進(jìn)行查找,讀取目錄下的package.json文件,取得main(入口文件)指定的文件。如果沒有則進(jìn)行下一步
- 如果沒有pakage.json或者main屬性指定的文件名錯(cuò)誤,那 Node 會(huì)將 index 當(dāng)做默認(rèn)文件名,依次查找 index.js、index.json、index.node
- 如果仍沒找到,則取出module path數(shù)組中的下一個(gè)目錄作為基準(zhǔn)查找,循環(huán)1-4步驟,直到module path中的最后一個(gè)值。
- 如果找到,返回合并的絕對(duì)路徑。作為下一步要用的索引值。如果仍沒找到就會(huì)拋出異常。
-
整個(gè)流程如下圖:
image.png
整個(gè)查找過程類似原型鏈的查找和作用域的查找,但node對(duì)路徑查找實(shí)現(xiàn)了緩存機(jī)制,所以不會(huì)很耗性能。
6.3 編譯執(zhí)行
Node 中每個(gè)模塊都是一個(gè)對(duì)象,在具體定位到文件后,Node 會(huì)新建該模塊對(duì)象,然后根據(jù)路徑載入并編譯。不同的文件擴(kuò)展名載入方法為:
- .js 文件: 通過 fs 模塊同步讀取后編譯執(zhí)行
- .json 文件: 通過 fs 模塊同步讀取后,用JSON.parse()解析并返回結(jié)果
- .node 文件: 這是用 C/C++ 寫的擴(kuò)展文件,通過process.dlopen()方法加載最后編譯生成的二進(jìn)制文件執(zhí)行即可。
- 其他擴(kuò)展名: 都被當(dāng)做 js 文件載入
載入成功后 Node 會(huì)調(diào)用具體的編譯方式將文件執(zhí)行后返回給調(diào)用者。對(duì)于 .json 文件的編譯最簡(jiǎn)單,JSON.parse()解析得到對(duì)象后直接賦值給模塊對(duì)象的exports,而 .node 文件是C/C++編譯生成的,Node 直接調(diào)用process.dlopen()載入執(zhí)行就可以,下面重點(diǎn)介紹 .js 文件的編譯:
1. 包裝(Wrapping)
在 Node API 文檔中每個(gè)模塊有 module、exports 、 require __filename、__dirname這些變量,但是在模塊中或者全局作用域中沒有定義這些變量,那它們是怎么產(chǎn)生的呢?
事實(shí)上在編譯過程中,通過fs.readFileSync讀取js文件,把js內(nèi)容拼接到一個(gè)大大的閉包中。每個(gè)文件都是一個(gè)模塊,有自己的作用域。將exports,require,module,__dirname,__filename五大參數(shù)傳入。這樣模塊就可以使用它們了。例如一個(gè) JS 文件會(huì)被封裝成如下:
(function (exports, require, module, __filename, __dirname) {
var math = require('math')
export.add = function(){ //... }
})
- 2.執(zhí)行(Evaluation):
傳入?yún)?shù),并執(zhí)行包裝得到的函數(shù)。
- 3.緩存(Caching):
函數(shù)執(zhí)行完畢后,最后將運(yùn)行函數(shù)得到的結(jié)果放入module.exports并返回。編譯并執(zhí)行成功的模塊會(huì)將文件絕對(duì)路徑作為索引,將module的做為值組成一個(gè)對(duì)象緩存起來。例如下面代碼,會(huì)將sayHi和sayHaHa函數(shù)放入module.exports中并作為require()的返回值返回。最后緩存一個(gè)key,value的對(duì)象。
// untils.js
console.log('untils');
exports.sayHi = function() {
console.log('Hi');
}
module.exports.sayHaHa = function() {
console.log('haha');
}
// 緩存的值
{ 'E:\\node test\\index.js':
Module {
id: '.',
exports: {},
parent: null,
filename: 'E:\\node test\\index.js',
loaded: false,
children: [ [Module] ],
paths: [ 'E:\\node test\\node_modules', 'E:\\node_modules' ] },
'E:\\node test\\extra\\src\\untils.js':
Module {
id: 'E:\\node test\\extra\\src\\untils.js',
exports: { sayHi: [Function], sayHaHa: [Function] },
parent:
Module {
id: '.',
exports: {},
parent: null,
filename: 'E:\\node test\\index.js',
loaded: false,
children: [Array],
paths: [Array] },
filename: 'E:\\node test\\extra\\src\\untils.js',
loaded: true,
children: [],
paths:
[ 'E:\\node test\\extra\\src\\node_modules',
'E:\\node test\\extra\\node_modules',
'E:\\node test\\node_modules',
'E:\\node_modules' ] } }
至此,module、exports 和 require的流程就介紹完了。
注意: 路徑分析時(shí)優(yōu)先查找緩存,提高二次引入的性能。所以在第二次使用使用該模塊時(shí)不會(huì)執(zhí)行模塊中的js。只會(huì)將函數(shù)運(yùn)行的結(jié)果引入。例如:
// untils.js
console.log('untils');
module.exports = function() {
console.log('haha');
}
//index.js
const untils1 = require(./src/untils.js);
const untils2 = require(./src/untils.js);
untils1();
untils2();
// 打印結(jié)果
untils
haha
haha
7.node模塊間的循環(huán)引用
話不多少,直接上源碼吧:
modA.js:
module.exports.test = 'A';
const modB = require('./modB.js');
console.log( 'modA:', modB.test);
module.exports.test = 'AA';</pre>
modB.js:
module.exports.test = 'B';
const modA = require('./modA.js');
console.log( 'modB:', modA.test);
module.exports.test = 'BB';</pre>
main.js
const modA = require('./modA');
運(yùn)行結(jié)果如下:

剛開始學(xué)習(xí)和閱讀上述代碼,是有點(diǎn)覺得暈暈乎乎,如果A與B存在相互依賴、相互引用關(guān)系,不就形成了一個(gè)閉環(huán)或者說死循環(huán)?那程序怎么會(huì)繼續(xù)解析呢?很顯然,運(yùn)行結(jié)果告訴我們,nodejs引擎有自己的一套處理循環(huán)引用的機(jī)制。下面我們根據(jù)上述運(yùn)行結(jié)果,來推演了兩個(gè)module模塊的執(zhí)行順序,以了解nodejs打破閉環(huán)的機(jī)制。

過程分解:
①執(zhí)行modA第一行,輸出一個(gè)test接口
②執(zhí)行modA第二行,要引入modB此時(shí)斷點(diǎn)產(chǎn)生了,即開始執(zhí)行modB里的代碼, 程序開始走"breakpoint-out"路線
③執(zhí)行modB第一行
④執(zhí)行modB第二行,要因?yàn)閙odA,此步驟為打破閉環(huán)的關(guān)鍵,此時(shí)將A里斷點(diǎn)之前的執(zhí)行結(jié)果輸出給modB,如圖里的藍(lán)色虛線框標(biāo)識(shí)的部分,此時(shí)在modB中打印modA.test,打印'A'
⑤繼續(xù)執(zhí)行modB第三行
⑥繼續(xù)執(zhí)行modB第四行,對(duì)外輸出test接口('BB'),此后,modB執(zhí)行完畢,主程序返回至斷點(diǎn)處(modA中在②步驟產(chǎn)生的斷點(diǎn)),將modB的執(zhí)行結(jié)果保存在'modB' const變量中。
⑦執(zhí)行modA的第三行
⑧執(zhí)行modA的第四行,打印'modB'對(duì)象里的test接口,根據(jù)中指向結(jié)果可知,'modB'返回的test接口為'BB',因此,打印'BB',程序結(jié)束。
如果main.js調(diào)用的是'modB.js',分析過程完全一致,打印的結(jié)果將是'B, AA'。
根據(jù)上述分析可知,nodejs中的模塊互相引用形成的“閉環(huán)”其實(shí)是用“斷點(diǎn)”這一方式打開的,以斷點(diǎn)為出口去執(zhí)行其他模塊,也以斷點(diǎn)為入口進(jìn)行返回,之后繼續(xù)執(zhí)行斷點(diǎn)之后的代碼。
——學(xué)無止境,保持好奇。may stars guide your way.
8.站在巨人肩上
詳解Node模塊加載機(jī)制
nodejs模塊加載機(jī)制
模塊機(jī)制
node前后端模塊規(guī)范與模塊加載原理
