node 模塊化
JS 誕生的時(shí)候,僅僅是為了實(shí)現(xiàn)網(wǎng)頁表單的本地校驗(yàn)和簡單的 dom 操作處理。所以并沒有模塊化的規(guī)范設(shè)計(jì)。
項(xiàng)目小的時(shí)候,我們可以通過命名空間、局部作用域、自執(zhí)行函數(shù)等手段實(shí)現(xiàn)變量不沖突。但是到了大一點(diǎn)的項(xiàng)目,各種組件,各種第三方插件和各種 js 腳步融合的時(shí)候,就會發(fā)現(xiàn)這些技巧遠(yuǎn)遠(yuǎn)不夠。
模塊化的演變
為什么要有 JS 模塊化呢?在瀏覽器中,頂層作用域的變量是全局的,所以項(xiàng)目稍微復(fù)雜點(diǎn),如果引用的 js 非常多的時(shí)候,很容易造成命名沖突,然后造成很大意想不到的結(jié)果。
為了避免全局污染,JS 前輩們想了很多辦法,也就是前端的模塊化的演變過程,可以參考我的視頻:前端模塊化演變
模塊化演變過程:
-
對象封裝
- 所有的方法和屬性封裝到一個(gè)對象中
- 所有的訪問通過對象來訪問,只污染一個(gè)對象,盡量避免污染其他。
var module = {
star : 0,
f1 : function ()
//...
},
f2 : function (){
//...
}
};
module.f1();
module.star = 1;
-
命名空間(對象封裝的變種或者叫做升級)
- 理論意義上減少了變量沖突
- 缺點(diǎn) 1:暴露了模塊中所有的成員,內(nèi)部狀態(tài)可以被外部改寫,不安全
- 缺點(diǎn) 2:命名空間會越來越長
var Shop = {}; // 頂層命名空間 Shop.User = {}; // 電商的用戶模塊 Shop.User.UserList = {}; //用戶列表頁面模塊。 Shop.User.UserList.length = 19; // 用戶一共有19個(gè)。 -
私有空間
- 私有空間的變量和函數(shù)不會影響全局作用域
- 公開公有方法,隱藏私有屬性
// => 給單個(gè)文件里面定義的局部變量都 變成 局部作用域里面的變量。 // 第二個(gè)嘗試: // a.js (function() { var a = 9; })(); // b.js (function() { var a = 'ssss'; })(); -
模塊的維護(hù)和擴(kuò)展
- 開閉原則
- 可維護(hù)性好
// laoma.core.js
(function(laoma, d1, d2) {
laoma.Btn = {
getVal: function() {
console.log('val');
},
setVal: function(str) {
console.log('setvale');
}
};
})(window.laoma || {}, depend1, depend2);
// laoma.animate.js
// 動畫組件
(function(laoma, d1, d2) {
laoma.animate = {};
})(window.laoma || {}, depend1, depend2);
// laoma.form.js
// 表單組件
(function(laoma, d1, d2) {
laoma.form = {};
})(window.laoma || {}, depend1, depend2);
- 圍觀jQuery的結(jié)構(gòu)
(function(window, undefined) {
var jQuery = function() {}
// ...
window.jQuery = window.$ = jQuery;
})(window);
后續(xù)的演變就是,出現(xiàn)了 AMD、CMD、CommonJS 等模塊化標(biāo)準(zhǔn),然后前端模塊化進(jìn)入大爆發(fā)時(shí)代。
什么是 JS 模塊化
JS 模塊化就是指 JS 代碼分成不同的模塊,模塊內(nèi)部定義變量作用域只屬于模塊內(nèi)部,模塊之間變量命名不會相互沖突。各個(gè)模塊相互獨(dú)立,而且又可以通過某種方式相互引用協(xié)作。
模塊化的標(biāo)準(zhǔn)
目前前端流行的幾個(gè)模塊化標(biāo)準(zhǔn):CommonJs標(biāo)準(zhǔn)(node 的方案)、AMD、CMD、ES6 模塊方案。
未來的趨勢肯定是 ES6 的標(biāo)準(zhǔn)方案會逐漸統(tǒng)一。但是 AMD、CMD 標(biāo)準(zhǔn)跟 CommonJs 的標(biāo)準(zhǔn)相差不大,需要我們都研究一下。
requirejs 入門
requirejs 的使用:
第一步:requirejs 下載
第二步: 把 requirejs 直接引入到 html
<script src="js/require.js"></script>
第三步: 設(shè)置當(dāng)前頁面的 js 入口文件
<script src="js/require.js" data-main="js/main"></script>
data-main 屬性的作用是,指定網(wǎng)頁程序的主模塊。意思是當(dāng)前整個(gè)網(wǎng)頁的入口代碼。那么其他需要引用的 JS 文件呢?
第四步: 引用其他模塊的文件
主模塊依賴于其他模塊,這時(shí)就要使用 AMD 規(guī)范定義的的 require()函數(shù)。
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC) {
// some code here
});
require()函數(shù)接受兩個(gè)參數(shù)。第一個(gè)參數(shù)是一個(gè)數(shù)組,表示所依賴的模塊,上例就是['moduleA', 'moduleB', 'moduleC'],即主模塊依賴這三個(gè)模塊;第二個(gè)參數(shù)是一個(gè)回調(diào)函數(shù),當(dāng)前面指定的模塊都加載成功后,它將被調(diào)用。加載的模塊會以參數(shù)形式傳入該函數(shù),從而在回調(diào)函數(shù)內(nèi)部就可以使用這些模塊。
require()異步加載 moduleA,moduleB 和 moduleC,瀏覽器不會失去響應(yīng);它指定的回調(diào)函數(shù),只有前面的模塊都加載成功后,才會運(yùn)行,解決了依賴性的問題。
實(shí)際應(yīng)用例子:
require(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
// some code here
});
如果依賴的 JS 文件跟我們的 require.js 不在相同的目錄,那么需要我們單獨(dú)設(shè)置一下路徑映射關(guān)系。
require.config({
paths: {
underscore: 'lib/underscore.min',
backbone: 'lib/backbone.min'
}
});
第五步:如何自定義 AMD 模塊(可選)
自定義的模塊還依賴其他模塊,那么 define()函數(shù)的第一個(gè)參數(shù),必須是一個(gè)數(shù)組,指明該模塊的依賴性
define(['myLib'], function(myLib) {
function foo() {
myLib.doSomething();
}
return {
foo: foo
};
});
CMD 與 Sea.js
[Sea.js]在推廣過程中逐漸形成了 CMD 的模塊定義標(biāo)準(zhǔn)。具體詳情請參考。
跟 AMD 比較類似,而且兼容 CommonJS 的模塊寫法。
CMD 推崇的是:依賴就近依賴,AMD 則默認(rèn)約束模塊一開始就聲明相關(guān)依賴。其他定義方式及模塊相關(guān)的變量都很相似。
由于 Sea.js 官方文檔很詳細(xì),在此就不再贅述。如何使用請參考官網(wǎng)。
Node 的模塊化
Node.js 有一個(gè)簡單的模塊加載系統(tǒng),遵循的是 CommonJS 的規(guī)范。 在 Node.js 中,文件和模塊是一一對應(yīng)的(每個(gè)文件被視為一個(gè)獨(dú)立的模塊)。
Node 在加載 JS 文件的時(shí)候,自動給 JS 文件包裝上定義模塊的頭部和尾部。
// nodejs 會自動給我們的js文件添加頭部,見下行
(function(exports, require, module, __filename, __dirname) {
// 這里是你自己寫的js代碼文件
}); // 自定添加上尾部
見 NodeJs 的源碼截圖:

Node會自動給js文件模塊傳遞的5個(gè)參數(shù),每個(gè)模塊內(nèi)的代碼都可以直接用。而且您也看到了,我們的代碼都會被包裝到一個(gè)函數(shù)中,所以我們的代碼的作用域都是在這個(gè)包裝的函數(shù)內(nèi),這點(diǎn)跟瀏覽器的window全局作用域是不同的。
模塊內(nèi)的參數(shù)說明:
- __dirname: 當(dāng)前模塊的文件夾名稱
- __filename: 當(dāng)前模塊的文件名稱---解析后的絕對路徑。
- module: 當(dāng)前模塊的引用,通過此對象可以控制當(dāng)前模塊對外的行為和屬性等。
- require:是一個(gè)函數(shù),幫助引入其他模塊.
- exports:這是一個(gè)對于 module.exports 的更簡短的引用形式,也就是當(dāng)前模塊對外輸出的引用。
如何加載模塊
在模塊內(nèi),我們可以通過require函數(shù)(此函數(shù)由nodejs自動傳入,在模塊內(nèi)可以直接用)來加載js文件模塊、node內(nèi)置模塊等。require函數(shù)需要傳入要加載的模塊的名字或者是文件名或者目錄。
/*
假設(shè)開發(fā)目錄下有文件:
.
├── circle.js
└── main.js
*/
// circle.js
exports.pi = 3.1415926; // 其他模塊引用當(dāng)前模塊時(shí),可以直接通過模塊對象訪問到 pi屬性。
// 主文件main.js:
const circle = require('./circle.js'); // 加載circle.js文件的module.export 賦值給circle
console.log(circle.pi); // => 3.1415926
解釋:
require加載文件circle.js后,此文件被node拼裝成模塊的代碼,然后執(zhí)行文件里面的js代碼,并把模塊內(nèi)的module.exports做為模塊的對外接口返回給引用者。
// circle.js 包裝后的代碼就是
// nodejs 會自動給我們的js文件添加頭部
(function(exports, require, module, __filename, __dirname) {
exports.pi = 3.1415926;
// exports === modeule.exports
}); // 自定添加上尾部
// 主文件main.js:
const circle = require('./circle.js');
circle => circle.js中的module.exports
加載策略
Node.js的模塊分為兩類,一類為原生(核心)模塊,一類為文件模塊。
模塊在第一次加載后會被緩存。 這也意味著如果每次調(diào)用 require('foo') 都解析到同一文件,則返回相同的對象。
Node.js提供了一些底層的核心模塊,它們定義在 Node.js 源代碼的 lib/ 目錄下。這些原生模塊在Node.js源代碼編譯的時(shí)候編譯進(jìn)了二進(jìn)制執(zhí)行文件,加載的速度最快。開發(fā)人員自定義的js文件是動態(tài)加載的,加載速度比原生模塊慢,這個(gè)只是在第一次加載有區(qū)別,模塊加載完后都會被緩存,后續(xù)使用就不會被再次加載。
require() 總是會優(yōu)先加載核心模塊。 例如,require('http') 始終返回內(nèi)置的 HTTP 模塊,即使有同名文件。
文件模塊中,又分為3類模塊。這三類文件模塊以后綴來區(qū)分,Node.js會根據(jù)后綴名來決定加載方法。
- .js。通過fs模塊同步讀取js文件并編譯執(zhí)行。
- .node。通過C/C++進(jìn)行編寫的Addon。通過dlopen方法進(jìn)行加載。
- .json。讀取文件,調(diào)用JSON.parse解析加載。
參考源碼:

模塊加載邏輯
require方法接受以下幾種參數(shù)的傳遞:
- http、fs、path等,原生模塊。
- ./mod或../mod,相對路徑的文件模塊。
- /pathtomodule/mod,絕對路徑的文件模塊。
- mod,非原生模塊的文件模塊。
文件加載的邏輯還是比較復(fù)雜的,而且考慮很多種情況。
- require加載文件模塊,直接找對應(yīng)完整文件名最快,如果不給文件后綴名,node會自動嘗試添加
js\json\mod等后綴進(jìn)行嘗試。當(dāng)沒有以 '/'、'./' 或 '../' 開頭來表示文件時(shí),這個(gè)模塊必須是一個(gè)核心模塊或加載自 node_modules 目錄。如果給定的路徑不存在,則 require() 會拋出一個(gè) code 屬性為 'MODULE_NOT_FOUND' 的 Error。 - 如果加載目錄,又分三種情況:
- 第一種方式是在根目錄下創(chuàng)建一個(gè) package.json 文件,并指定一個(gè) main 模塊。 例子,package.json 文件類似:
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
如果這是在 ./some-library 目錄中,則 require('./some-library') 會試圖加載 ./some-library/lib/some-library.js。不存在也會報(bào)錯。
- 如果目錄里沒有 package.json 文件,則 Node.js 就會試圖加載目錄下的 index.js 或 index.node 文件。 例如,如果上面的例子中沒有 package.json 文件,則 require('./some-library') 會試圖加載:
./some-library/index.js
./some-library/index.node
- 其他的情況,則從 node_modules 目錄加載。 Node.js 會從當(dāng)前模塊的父目錄開始,嘗試從它的 /node_modules 目錄里加載模塊。 Node.js 不會附加 node_modules 到一個(gè)已經(jīng)以 node_modules 結(jié)尾的路徑上。
如果還是沒有找到,則移動到再上一層父目錄,直到文件系統(tǒng)的根目錄。
例子,如果在 '/home/ry/projects/foo.js' 文件里調(diào)用了 require('bar.js'),則 Node.js 會按以下順序查找:
/home/ry/projects/node_modules/bar.js
/home/ry/node_modules/bar.js
/home/node_modules/bar.js
/node_modules/bar.js
這使得程序本地化它們的依賴,避免它們產(chǎn)生沖突。
可以通過module.paths打印當(dāng)前node尋找模塊要搜索的所有路徑。
綜上邏輯,看官網(wǎng)的加載邏輯偽代碼:
從 Y 路徑的模塊 require(X)
1. 如果 X 是一個(gè)核心模塊,
a. 返回核心模塊
b. 結(jié)束
2. 如果 X 是以 '/' 開頭
a. 設(shè) Y 為文件系統(tǒng)根目錄
3. 如果 X 是以 './' 或 '/' 或 '../' 開頭
a. 加載文件(Y + X)
b. 加載目錄(Y + X)
4. 加載Node模塊(X, dirname(Y))
5. 拋出 "未找到"
加載文件(X)
1. 如果 X 是一個(gè)文件,加載 X 作為 JavaScript 文本。結(jié)束
2. 如果 X.js 是一個(gè)文件,加載 X.js 作為 JavaScript 文本。結(jié)束
3. 如果 X.json 是一個(gè)文件,解析 X.json 成一個(gè) JavaScript 對象。結(jié)束
4. 如果 X.node 是一個(gè)文件,加載 X.node 作為二進(jìn)制插件。結(jié)束
加載目錄(X)
1. 如果 X/package.json 是一個(gè)文件,
a. 解析 X/package.json,查找 "main" 字段
b. let M = X + (json main 字段)
c. 加載文件(M)
d. 加載索引(M)
2. 加載索引(X)
加載Node模塊(X, START)
1. let DIRS=NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. 加載文件(DIR/X)
b. 加載目錄(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = []
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS
總結(jié):
我們自己加載模塊的時(shí)候,盡量的寫全點(diǎn),盡量不要讓node去推斷,引用文件模塊直接把文件名寫全,文件
module 對象
如果想查看當(dāng)前模塊,可以直接使用console直接打印一下module對象。
console.dir(module);
// 打印結(jié)果:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/02console.js',
loaded: false,
children: [],
paths:
[ '/Users/flydragon/Desktop/work/gitdata/nodedemos/demos/node_modules',
'/Users/flydragon/Desktop/work/gitdata/nodedemos/node_modules',
'/Users/flydragon/Desktop/work/gitdata/node_modules',
'/Users/flydragon/Desktop/work/node_modules',
'/Users/flydragon/Desktop/node_modules',
'/Users/flydragon/node_modules',
'/Users/node_modules',
'/node_modules' ] }
在每個(gè)模塊中,module 的自由變量是一個(gè)指向表示當(dāng)前模塊的對象的引用。 為了方便,module.exports 也可以通過全局模塊的 exports 對象訪問。
module.exports 與 exports區(qū)別,看Node中的源碼就知道了。
// 模塊的構(gòu)造函數(shù)
function Module(id, parent) {
this.id = id;
this.exports = {}; // 模塊實(shí)例的exports屬性初始化!?。odule.exports === exports
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
注意:
exports是module.exports的一個(gè)引用,就好比在每一個(gè)模塊定義最開始的地方寫了這么一句代碼:var exports = module.exports要注意的一點(diǎn)就是: 最終模塊會把module.exports作為對外的接口。所以,module.exports的引用地址發(fā)生了改變,在改變之前通過exports屬性設(shè)置的都會被遺棄。
module的其他屬性:
| 屬性 | 類型 | 屬性說明 |
|---|---|---|
| module.filename | string | 模塊的完全解析后的文件名 |
| module.id | string | 模塊的標(biāo)識符。 通常是完全解析后的文件名。 |
| module.loaded | boolean | 模塊是否已經(jīng)加載完成,或正在加載 |
| module.parent | object | 最先引用該模塊的模塊。 |
| module.paths | string | 模塊的搜索路徑。 |
| module.children | object | 被該模塊引用的模塊對象。 |
詳情請參考:中文Node文檔
es6的模塊
es6的模塊引入和導(dǎo)出跟以上都有點(diǎn)區(qū)別。不過肯定是未來的統(tǒng)一的模型。node目前版本位置并沒有es6的模塊api支持的很好,只是在實(shí)驗(yàn)階段。不過我們可以借助babel來轉(zhuǎn)換我們的js代碼,可以放心的使用。
由于這塊內(nèi)容,請直接參考阮一峰老師的es6入門
總結(jié)
從客戶端到服務(wù)端我們都搞定了js的模塊化,也就是說讓js走向了工程化,大型應(yīng)用的基礎(chǔ)被奠定了。當(dāng)然,目前業(yè)界模塊化已經(jīng)走入深水區(qū),尤其是webpack已經(jīng)可以讓前端的大部分資源都模塊化使用。
我們已經(jīng)搞定了,自己書寫模塊,已經(jīng)引用核心模塊、自己寫的模塊,那么怎么引用第三方模塊,怎么使用package文件,好吧提前透露一下:npm解密(下一節(jié))
參考:
- NodeJs 官網(wǎng)文檔
- MDN 文檔
- Javascript 模塊化編程(二):AMD 規(guī)范
- Javascript 模塊化編程(三):require.js 的用法
- CMD 模塊定義規(guī)范