什么是模塊化?
到底什么是模塊化、模塊化開發(fā)呢?
- 事實(shí)上模塊化開發(fā)最終的目的是將程序劃分成一個(gè)個(gè)小的結(jié)構(gòu);
- 這個(gè)結(jié)構(gòu)中編寫屬于自己的邏輯代碼,有自己的作用域,不會(huì)影響到其他的結(jié)構(gòu);
- 這個(gè)結(jié)構(gòu)可以將自己希望暴露的變量、函數(shù)、對(duì)象等導(dǎo)出給其結(jié)構(gòu)使用;
- 也可以通過某種方式,導(dǎo)入另外結(jié)構(gòu)中的變量、函數(shù)、對(duì)象等;
上面說提到的結(jié)構(gòu),就是模塊;按照這種結(jié)構(gòu)劃分開發(fā)程序的過程,就是模塊化開發(fā)的過程;
無論你多么喜歡JavaScript,以及它現(xiàn)在發(fā)展的有多好,它都有很多的缺陷:
- 比如var定義的變量作用域問題;
- 比如JavaScript的面向?qū)ο蟛⒉荒芟癯R?guī)面向?qū)ο笳Z言一樣使用class;
- 比如JavaScript沒有模塊化的問題;
Brendan Eich本人也多次承認(rèn)過JavaScript設(shè)計(jì)之初的缺陷,但是隨著JavaScript的發(fā)展以及標(biāo)準(zhǔn)化,存在的缺陷問題基
本都得到了完善。
無論是web、移動(dòng)端、小程序端、服務(wù)器端、桌面應(yīng)用都被廣泛的使用;
模塊化的歷史
在網(wǎng)頁開發(fā)的早期,Brendan Eich開發(fā)JavaScript僅僅作為一種腳本語言,做一些簡(jiǎn)單的表單驗(yàn)證或動(dòng)畫實(shí)現(xiàn)等,那個(gè)時(shí)候代碼還是很少的:
- 這個(gè)時(shí)候我們只需要講JavaScript代碼寫到<script>標(biāo)簽中即可;
- 并沒有必要放到多個(gè)文件中來編寫;甚至流行:通常來說 JavaScript 程序的長(zhǎng)度只有一行。
但是隨著前端和JavaScript的快速發(fā)展,JavaScript代碼變得越來越復(fù)雜了:
- ajax的出現(xiàn),前后端開發(fā)分離,意味著后端返回?cái)?shù)據(jù)后,我們需要通過JavaScript進(jìn)行前端頁面的渲染;
- SPA的出現(xiàn),前端頁面變得更加復(fù)雜:包括前端路由、狀態(tài)管理等等一系列復(fù)雜的需求需要通過JavaScript來實(shí)現(xiàn);
- 包括Node的實(shí)現(xiàn),JavaScript編寫復(fù)雜的后端程序,沒有模塊化是致命的硬傷;
所以,模塊化已經(jīng)是JavaScript一個(gè)非常迫切的需求:
- 但是JavaScript本身,直到ES6(2015)才推出了自己的模塊化方案;
- 在此之前,為了讓JavaScript支持模塊化,涌現(xiàn)出了很多不同的模塊化規(guī)范:
AMD、CMD、CommonJS等;
沒有模塊化帶來的問題
早期沒有模塊化帶來了很多的問題:比如命名沖突的問題
當(dāng)然,我們有辦法可以解決上面的問題:立即函數(shù)調(diào)用表達(dá)式(IIFE)
-
IIFE (Immediately Invoked Function Expression)
image.png
./why/index.js
var moduleA = (function () {
var name = "why";
var isFlag = true;
return {
name,
isFlag,
};
})();
./coby/index.js
var moduleB = (function () {
var name = "coby";
var age = 40;
var isFlag = false;
return {
name,
age,
isFlag,
};
})();
./index.js
if (moduleA.isFlag) {
console.log("name is", moduleA.name);
}
if (moduleB.isFlag) {
console.log("name is", moduleB.name);
}
./index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./why/index.js"></script>
<script src="./coby/index.js"></script>
<script src="./index.js"></script>
</body>
</html>
但是,我們其實(shí)帶來了新的問題:
- 第一,我必須記得每一個(gè)模塊中返回對(duì)象的命名,才能在其他模塊使用過程中正確的使用;
- 第二,代碼寫起來混亂不堪,每個(gè)文件中的代碼都需要包裹在一個(gè)匿名函數(shù)中來編寫;
- 第三,在沒有合適的規(guī)范情況下,每個(gè)人、每個(gè)公司都可能會(huì)任意命名、甚至出現(xiàn)模塊名稱相同的情況;
所以,我們會(huì)發(fā)現(xiàn),雖然實(shí)現(xiàn)了模塊化,但是我們的實(shí)現(xiàn)過于簡(jiǎn)單,并且是沒有規(guī)范的。
- 我們需要制定一定的規(guī)范來約束每個(gè)人都按照這個(gè)規(guī)范去編寫模塊化的代碼;
- 這個(gè)規(guī)范中應(yīng)該包括核心功能:模塊本身可以導(dǎo)出暴露的屬性,模塊又可以導(dǎo)入自己需要的屬性;
- JavaScript社區(qū)為了解決上面的問題,涌現(xiàn)出一系列好用的規(guī)范,接下來我們就學(xué)習(xí)具有代表性的一些規(guī)范。
CommonJS規(guī)范和Node關(guān)系
我們需要知道CommonJS是一個(gè)規(guī)范,最初提出來是在瀏覽器以外的地方使用,并且當(dāng)時(shí)被命名為ServerJS,后來為了體現(xiàn)它的廣泛性,修改為CommonJS,平時(shí)我們也會(huì)簡(jiǎn)稱為CJS。
- Node是CommonJS在服務(wù)器端一個(gè)具有代表性的實(shí)現(xiàn);
- Browserify是CommonJS在瀏覽器中的一種實(shí)現(xiàn);
- webpack打包工具具備對(duì)CommonJS的支持和轉(zhuǎn)換;
所以,Node中對(duì)CommonJS進(jìn)行了支持和實(shí)現(xiàn),讓我們?cè)陂_發(fā)node的過程中可以方便的進(jìn)行模塊化開發(fā):
- 在Node中每一個(gè)js文件都是一個(gè)單獨(dú)的模塊;
- 這個(gè)模塊中包括CommonJS規(guī)范的核心變量:
exports、module.exports、require; - 我們可以使用這些變量來方便的進(jìn)行模塊化開發(fā);
前面我們提到過模塊化的核心是導(dǎo)出和導(dǎo)入,Node中對(duì)其進(jìn)行了實(shí)現(xiàn):
-
exports和module.exports可以負(fù)責(zé)對(duì)模塊中的內(nèi)容進(jìn)行導(dǎo)出; -
require函數(shù)可以幫助我們導(dǎo)入其他模塊(自定義模塊、系統(tǒng)模塊、第三方庫模塊)中的內(nèi)容
CommonJS的基本使用
./foo.js
const name = "why";
const age = 18;
const height = 1.88;
//導(dǎo)出方式一:
module.exports = {
name,
age,
height,
};
//導(dǎo)出方式二:
// exports.name = name;
// exports.age = age;
// exports.height = height;
./index.js
const { name, age, height } = require("./foo.js");
console.log(name, age, height); //why 18 1.88
exports導(dǎo)出
注意:exports是一個(gè)對(duì)象,我們可以在這個(gè)對(duì)象中添加很多個(gè)屬性,添加的屬性會(huì)導(dǎo)出;
exports.name = "why";
exports.age = 18;
exports.height = 1.88;
另外一個(gè)文件main.js中可以導(dǎo)入:
const bar = require("./bar");
上面這行完成了什么操作呢?理解下面這句話,Node中的模塊化一目了然
- 意味著main中的bar變量等于exports對(duì)象;
- 也就是require通過各種查找方式,最終找到了exports這個(gè)對(duì)象;
- 并且將這個(gè)exports對(duì)象賦值給了bar變量;
- bar變量就是exports對(duì)象了;
module.exports
但是Node中我們經(jīng)常導(dǎo)出東西的時(shí)候,又是通過module.exports導(dǎo)出的:
-
module.exports和exports有什么關(guān)系或者區(qū)別呢?
我們追根溯源,通過維基百科中對(duì)CommonJS規(guī)范的解析: - CommonJS中是沒有module.exports的概念的;
- 但是為了實(shí)現(xiàn)模塊的導(dǎo)出,Node中使用的是Module的類,每一個(gè)模塊都是Module的一個(gè)實(shí)例,也就是
module; - 所以在Node中真正用于導(dǎo)出的其實(shí)根本不是exports,而是module.exports;
- 因?yàn)閙odule才是導(dǎo)出的真正實(shí)現(xiàn)者;
但是,為什么exports也可以導(dǎo)出呢?
- 這是因?yàn)閙odule對(duì)象的exports屬性是exports對(duì)象的一個(gè)引用;
- 也就是說
module.exports = exports;
原理:
node中的每一個(gè)文件都是一個(gè)模塊,每個(gè)模塊都有私有的module.export和exports變量和require函數(shù)
在源碼中進(jìn)行了如下操作:
module.exports = {}
exports = modules.exports
也就是說,默認(rèn)modules.exports和exports指向同一個(gè)引用地址,
但是實(shí)際上require(x)方法導(dǎo)出的時(shí)候,是找到匹配x的文件,導(dǎo)出當(dāng)前文件中的modules.exports對(duì)象
源碼大致如下:
function require(id) {
return modules.exports;
}
require查找規(guī)則
我們現(xiàn)在已經(jīng)知道,require是一個(gè)函數(shù),可以幫助我們引入一個(gè)文件(模塊)中導(dǎo)出的對(duì)象。
那么,require的查找規(guī)則是怎么樣的呢?
-
https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_all_together
這里我總結(jié)比較常見的查找規(guī)則:導(dǎo)入格式如下:require(X)
情況一:X是一個(gè)Node核心模塊,比如path、http
- 直接返回核心模塊,并且停止查找
情況二:X是以 ./ 或 ../ 或 /(根目錄)開頭的
第一步:將X當(dāng)做一個(gè)文件在對(duì)應(yīng)的目錄下查找;
- 1.如果有后綴名,按照后綴名的格式查找對(duì)應(yīng)的文件
- 2.如果沒有后綴名,會(huì)按照如下順序:
- 1> 直接查找文件X
- 2> 查找X.js文件
- 3> 查找X.json文件
- 4> 查找X.node文件
第二步:沒有找到對(duì)應(yīng)的文件,將X作為一個(gè)目錄
- 查找目錄下面的index文件
- 1> 查找X/index.js文件
- 2> 查找X/index.json文件
- 3> 查找X/index.node文件
如果沒有找到,那么報(bào)錯(cuò):not found
情況三:直接是一個(gè)X(沒有路徑),并且X不是一個(gè)核心模塊
/Users/coderwhy/Desktop/Node/TestCode/04_learn_node/05_javascript-module/02_commonjs/main.js中編寫 require('why’)

- 會(huì)先在當(dāng)前文件坐在目錄下的node_modules目錄下尋找,如果沒有找到
- 在去上級(jí)目錄下的node_modules目錄下尋找,如果依然沒找到
- 繼續(xù)去上上級(jí)目錄下的node_modules目錄下尋找...
- 直至找到根目錄,依然沒有找到,則報(bào)錯(cuò)
如果上面的路徑中都沒有找到,那么報(bào)錯(cuò):not found
模塊的加載過程
結(jié)論一:模塊在被第一次引入時(shí),模塊中的js代碼會(huì)被運(yùn)行一次
結(jié)論二:模塊被多次引入時(shí),會(huì)緩存,最終只加載(運(yùn)行)一次
- 為什么只會(huì)加載運(yùn)行一次呢?
- 這是因?yàn)槊總€(gè)模塊對(duì)象module都有一個(gè)屬性:loaded。
- 為false表示還沒有加載,為true表示已經(jīng)加載;
./index.js
在index.js文件中多次引用bar.js
const bar = require("./bar");
require("./bar");
require("./bar");
require("./bar");
require("./bar");
require("./bar");
console.log(bar.name, bar.age);
./bar.js
console.log(module.loaded)
exports.name = "why";
exports.age = 18;

結(jié)論三:如果有循環(huán)引入,那么加載順序是什么?
如果出現(xiàn)右圖模塊的引用關(guān)系,那么加載順序是什么呢?

- 這個(gè)其實(shí)是一種數(shù)據(jù)結(jié)構(gòu):圖結(jié)構(gòu);
- 圖結(jié)構(gòu)在遍歷的過程中,有
深度優(yōu)先搜索(DFS, depth first search)和廣度優(yōu)先搜索(BFS, breadth first search); -
Node采用的是
深度優(yōu)先算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS規(guī)范缺點(diǎn)
CommonJS加載模塊是同步的:
- 同步的意味著只有等到對(duì)應(yīng)的模塊加載完畢,當(dāng)前模塊中的內(nèi)容才能被運(yùn)行;
- 這個(gè)在服務(wù)器不會(huì)有什么問題,因?yàn)榉?wù)器加載的js文件都是本地文件,加載速度非??欤?/li>
如果將它應(yīng)用于瀏覽器呢?
- 瀏覽器加載js文件需要先從服務(wù)器將文件下載下來,之后再加載運(yùn)行;
- 那么采用同步的就意味著后續(xù)的js代碼都無法正常運(yùn)行,即使是一些簡(jiǎn)單的DOM操作;
所以在瀏覽器中,我們通常不使用CommonJS規(guī)范:
- 當(dāng)然在webpack中使用CommonJS是另外一回事;
- 因?yàn)樗鼤?huì)將我們的代碼轉(zhuǎn)成瀏覽器可以直接執(zhí)行的代碼;
在早期為了可以在瀏覽器中使用模塊化,通常會(huì)采用AMD或CMD:
- 但是目前一方面現(xiàn)代的瀏覽器已經(jīng)支持ES Modules,另一方面借助于webpack等工具可以實(shí)現(xiàn)對(duì)CommonJS或者ES Module代碼的轉(zhuǎn)換;
- AMD和CMD已經(jīng)使用非常少了,所以這里我們進(jìn)行簡(jiǎn)單的演練;
AMD規(guī)范
AMD主要是應(yīng)用于瀏覽器的一種模塊化規(guī)范:
- AMD是Asynchronous Module Definition(異步模塊定義)的縮寫;
- 它采用的是異步加載模塊;
- 事實(shí)上AMD的規(guī)范還要早于CommonJS,但是CommonJS目前依然在被使用,而AMD使用的較少了;
我們提到過,規(guī)范只是定義代碼的應(yīng)該如何去編寫,只有有了具體的實(shí)現(xiàn)才能被應(yīng)用:
- AMD實(shí)現(xiàn)的比較常用的庫是require.js和curl.js;
require.js的使用
第一步:下載require.js
- 下載地址:https://github.com/requirejs/requirejs
- 找到其中的require.js文件;
第二步:定義HTML的script標(biāo)簽引入require.js和定義入口文件:*
- data-main屬性的作用是在加載完src的文件后會(huì)加載執(zhí)行該文件
require.js的使用
<script src="./lib/require.js" data-main="./index.js"></script>

./index..html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<!-- data-main屬性的作用是在加載完src的文件后會(huì)加載執(zhí)行該文件 -->
<script src="./lib/require.js" data-main="./src/index.js"></script>
</body>
</html>
./src/index.js
//配置模塊及其路徑
require.config({
baseUrl: "./src", //默認(rèn)為當(dāng)前文件所在目錄
paths: {
foo: "./modules/foo",
bar: "./modules/bar",
},
});
//使用模塊
require(["foo", "bar"], function (foo) {
console.log(foo.name, foo.age);
});
./src/modules/foo.js
//定義模塊
define(function () {
const name = "why";
const age = 18;
//導(dǎo)出模塊中的變量
return {
name,
age,
};
});
./src/modules/bar.js
//定義模塊,并且在當(dāng)前模塊中導(dǎo)入foo模塊,并使用foo模塊中導(dǎo)出的變量
define(["foo"], function (foo) {
console.log("bar.js", foo.name, foo.age);
});
//寫法二:
// define(function (foo) {
// require(["foo"], function (foo) {
// console.log("bar.js", foo.name, foo.age);
// });
// });
CMD規(guī)范
CMD規(guī)范也是應(yīng)用于瀏覽器的一種模塊化規(guī)范:
- CMD 是Common Module Definition(通用模塊定義)的縮寫;
- 它也采用了異步加載模塊,但是它將CommonJS的優(yōu)點(diǎn)吸收了過來;
- 但是目前CMD使用也非常少了;
CMD也有自己比較優(yōu)秀的實(shí)現(xiàn)方案: - SeaJS
第一步:下載SeaJS
- 下載地址:https://github.com/seajs/seajs
- 找到dist文件夾下的sea.js
第二步:引入sea.js和使用主入口文件
-
seajs是指定主入口文件的
image.png
./index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="./lib/sea.js" ></script>
<script>
seajs.use('./src/index.js')
</script>
</body>
</html>
./src/index.js
//定義模塊
define(function(require, exports, module) {
//導(dǎo)入foo模塊
const foo = require('./modules/foo')
})
./src/modules/foo.js
//定義模塊
define(function (require, exports, module) {
//導(dǎo)入bar模塊
const bar = require("./bar");
console.log(bar.name, bar.age);
});
./src/modules/bar.js
//定義模塊
define(function (require, exports, module) {
const name = "why";
const age = 18;
//導(dǎo)出變量
module.exports = {
name,
age,
};
});

