node 模塊化
JS 誕生的時候,僅僅是為了實現(xiàn)網(wǎng)頁表單的本地校驗和簡單的 dom 操作處理。所以并沒有模塊化的規(guī)范設(shè)計邢隧。
項目小的時候痕慢,我們可以通過命名空間、局部作用域抑堡、自執(zhí)行函數(shù)等手段實現(xiàn)變量不沖突摆出。但是到了大一點的項目,各種組件首妖,各種第三方插件和各種 js 腳步融合的時候偎漫,就會發(fā)現(xiàn)這些技巧遠(yuǎn)遠(yuǎn)不夠。
模塊化的演變
為什么要有 JS 模塊化呢有缆?在瀏覽器中象踊,頂層作用域的變量是全局的,所以項目稍微復(fù)雜點棚壁,如果引用的 js 非常多的時候杯矩,很容易造成命名沖突,然后造成很大意想不到的結(jié)果灌曙。
為了避免全局污染菊碟,JS 前輩們想了很多辦法,也就是前端的模塊化的演變過程在刺,可以參考我的視頻:前端模塊化演變
模塊化演變過程:
-
對象封裝
- 所有的方法和屬性封裝到一個對象中
- 所有的訪問通過對象來訪問逆害,只污染一個對象头镊,盡量避免污染其他。
var module = {
star : 0,
f1 : function ()
//...
},
f2 : function (){
//...
}
};
module.f1();
module.star = 1;
-
命名空間(對象封裝的變種或者叫做升級)
- 理論意義上減少了變量沖突
- 缺點 1:暴露了模塊中所有的成員魄幕,內(nèi)部狀態(tài)可以被外部改寫相艇,不安全
- 缺點 2:命名空間會越來越長
var Shop = {}; // 頂層命名空間 Shop.User = {}; // 電商的用戶模塊 Shop.User.UserList = {}; //用戶列表頁面模塊。 Shop.User.UserList.length = 19; // 用戶一共有19個纯陨。
-
私有空間
- 私有空間的變量和函數(shù)不會影響全局作用域
- 公開公有方法坛芽,隱藏私有屬性
// => 給單個文件里面定義的局部變量都 變成 局部作用域里面的變量。 // 第二個嘗試: // a.js (function() { var a = 9; })(); // b.js (function() { var a = 'ssss'; })();
-
模塊的維護(hù)和擴展
- 開閉原則
- 可維護(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ā)時代活喊。
什么是 JS 模塊化
JS 模塊化就是指 JS 代碼分成不同的模塊,模塊內(nèi)部定義變量作用域只屬于模塊內(nèi)部量愧,模塊之間變量命名不會相互沖突钾菊。各個模塊相互獨立,而且又可以通過某種方式相互引用協(xié)作偎肃。
模塊化的標(biāo)準(zhǔn)
目前前端流行的幾個模塊化標(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)前整個網(wǎng)頁的入口代碼暖哨。那么其他需要引用的 JS 文件呢赌朋?
第四步: 引用其他模塊的文件
主模塊依賴于其他模塊,這時就要使用 AMD 規(guī)范定義的的 require()函數(shù)篇裁。
// main.js
require(['moduleA', 'moduleB', 'moduleC'], function(moduleA, moduleB, moduleC) {
// some code here
});
require()函數(shù)接受兩個參數(shù)沛慢。第一個參數(shù)是一個數(shù)組,表示所依賴的模塊达布,上例就是['moduleA', 'moduleB', 'moduleC']团甲,即主模塊依賴這三個模塊;第二個參數(shù)是一個回調(diào)函數(shù)黍聂,當(dāng)前面指定的模塊都加載成功后躺苦,它將被調(diào)用身腻。加載的模塊會以參數(shù)形式傳入該函數(shù),從而在回調(diào)函數(shù)內(nèi)部就可以使用這些模塊匹厘。
require()異步加載 moduleA嘀趟,moduleB 和 moduleC,瀏覽器不會失去響應(yīng)愈诚;它指定的回調(diào)函數(shù)她按,只有前面的模塊都加載成功后,才會運行炕柔,解決了依賴性的問題酌泰。
實際應(yīng)用例子:
require(['jquery', 'underscore', 'backbone'], function($, _, Backbone) {
// some code here
});
如果依賴的 JS 文件跟我們的 require.js 不在相同的目錄,那么需要我們單獨設(shè)置一下路徑映射關(guān)系匕累。
require.config({
paths: {
underscore: 'lib/underscore.min',
backbone: 'lib/backbone.min'
}
});
第五步:如何自定義 AMD 模塊(可選)
自定義的模塊還依賴其他模塊陵刹,那么 define()函數(shù)的第一個參數(shù),必須是一個數(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 有一個簡單的模塊加載系統(tǒng),遵循的是 CommonJS 的規(guī)范慷妙。 在 Node.js 中僻焚,文件和模塊是一一對應(yīng)的(每個文件被視為一個獨立的模塊)。
Node 在加載 JS 文件的時候膝擂,自動給 JS 文件包裝上定義模塊的頭部和尾部虑啤。
// nodejs 會自動給我們的js文件添加頭部,見下行
(function(exports, require, module, __filename, __dirname) {
// 這里是你自己寫的js代碼文件
}); // 自定添加上尾部
見 NodeJs 的源碼截圖:
Node會自動給js文件模塊傳遞的5個參數(shù)架馋,每個模塊內(nèi)的代碼都可以直接用狞山。而且您也看到了,我們的代碼都會被包裝到一個函數(shù)中叉寂,所以我們的代碼的作用域都是在這個包裝的函數(shù)內(nèi)萍启,這點跟瀏覽器的window全局作用域是不同的。
模塊內(nèi)的參數(shù)說明:
- __dirname: 當(dāng)前模塊的文件夾名稱
- __filename: 當(dāng)前模塊的文件名稱---解析后的絕對路徑。
- module: 當(dāng)前模塊的引用勘纯,通過此對象可以控制當(dāng)前模塊對外的行為和屬性等局服。
- require:是一個函數(shù),幫助引入其他模塊.
- exports:這是一個對于 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)前模塊時媒惕,可以直接通過模塊對象訪問到 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源代碼編譯的時候編譯進(jìn)了二進(jìn)制執(zhí)行文件谴垫,加載的速度最快章母。開發(fā)人員自定義的js文件是動態(tài)加載的,加載速度比原生模塊慢翩剪,這個只是在第一次加載有區(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)沒有以 '/'府蔗、'./' 或 '../' 開頭來表示文件時晋控,這個模塊必須是一個核心模塊或加載自 node_modules 目錄。如果給定的路徑不存在姓赤,則 require() 會拋出一個 code 屬性為 'MODULE_NOT_FOUND' 的 Error赡译。 - 如果加載目錄,又分三種情況:
- 第一種方式是在根目錄下創(chuàng)建一個 package.json 文件不铆,并指定一個 main 模塊蝌焚。 例子,package.json 文件類似:
{
"name" : "some-library",
"main" : "./lib/some-library.js"
}
如果這是在 ./some-library 目錄中誓斥,則 require('./some-library') 會試圖加載 ./some-library/lib/some-library.js只洒。不存在也會報錯。
- 如果目錄里沒有 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 到一個已經(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 是一個核心模塊,
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 是一個文件聂薪,加載 X 作為 JavaScript 文本家乘。結(jié)束
2. 如果 X.js 是一個文件,加載 X.js 作為 JavaScript 文本藏澳。結(jié)束
3. 如果 X.json 是一個文件仁锯,解析 X.json 成一個 JavaScript 對象。結(jié)束
4. 如果 X.node 是一個文件翔悠,加載 X.node 作為二進(jìn)制插件业崖。結(jié)束
加載目錄(X)
1. 如果 X/package.json 是一個文件,
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é):
我們自己加載模塊的時候双炕,盡量的寫全點,盡量不要讓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' ] }
在每個模塊中站超,module 的自由變量是一個指向表示當(dāng)前模塊的對象的引用。 為了方便乖酬,module.exports 也可以通過全局模塊的 exports 對象訪問死相。
module.exports 與 exports區(qū)別,看Node中的源碼就知道了咬像。
// 模塊的構(gòu)造函數(shù)
function Module(id, parent) {
this.id = id;
this.exports = {}; // 模塊實例的exports屬性初始化O蔽场K簟!module.exports === exports
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
注意:
exports
是module.exports
的一個引用钮惠,就好比在每一個模塊定義最開始的地方寫了這么一句代碼:var exports = module.exports
要注意的一點就是: 最終模塊會把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)出跟以上都有點區(qū)別辩昆。不過肯定是未來的統(tǒng)一的模型阅酪。node目前版本位置并沒有es6的模塊api支持的很好,只是在實驗階段汁针。不過我們可以借助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ī)范