「Node-js 系列」深入淺出 Node 模塊化開發——CommonJS 規範
前言
本文將爲大家透徹的介紹關於 Node 的模塊化——CommonJS 的一切。
看完本文可以掌握,以下幾個方面:
-
什麼是模塊化,以及沒有模塊化會帶來哪些問題,是如何解決的;
-
JavaScript 的設計缺陷;
-
CommonJS 規範;
-
它的規範特性;
-
如何配合 Node 完成模塊化開發;
-
exports 如何導出的;
-
module.exports
是如何導出變量的,值類型和引用類型導出間的差異; -
從內存角度深度分析
module.exports
和exports
又有怎樣的區別和聯繫; -
require 的細節,以及模塊的加載執行順序;
-
CommonJS 的加載過程;
-
CommonJS 規範的本質;
一. 什麼是模塊化?
在很多開發的情況下,我們都知道要使用模塊化開發,那爲什麼要使用它呢?
而事實上,模塊化開發
最終的目的是將程序劃分成一個個小的結構
;
-
在這個結構中編寫屬於
自己的邏輯代碼
,有自己的作用域
,不會影響到其他的結構; -
這個結構可以將自己希望暴露的
變量
、函數
、對象
等導出給其結構使用; -
也可以通過某種方式,導入另外結構中的
變量
、函數
、對象
等;
上面說提到的結構
,就是模塊
;
按照這種結構
劃分開發程序的過程,就是模塊化開發
的過程;
二. JavaScript 設計缺陷
在網頁開發的早期,由於 JavaScript 僅僅作爲一種腳本語言
,只能做一些簡單的表單驗證或動畫實現等,它還是具有很多的缺陷問題的,比如:
-
var 定義的變量作用域問題;
-
JavaScript 的面向對象並不能像常規面嚮對象語言一樣使用 class;
-
在早期 JavaScript 並沒有模塊化的問題,所以也就沒有對應的模塊化解決方案;
但隨着前端和 JavaScript 的快速發展,JavaScript 代碼變得越來越複雜了;
-
ajax 的出現,
前後端開發分離
,意味着後端返回數據後,我們需要通過JavaScript進行前端頁面的渲染
; -
SPA 的出現,前端頁面變得更加複雜:包括
前端路由
、狀態管理
等等一系列複雜的需求需要通過 JavaScript 來實現; -
包括 Node 的實現,JavaScript 編寫
複雜的後端程序
,沒有模塊化是致命的硬傷;
所以,模塊化已經是 JavaScript 一個非常迫切的需求:
-
但是 JavaScript 本身,直到
ES6
(2015)才推出了自己的模塊化方案; -
在此之前,爲了讓 JavaScript 支持模塊化,湧現出了很多不同的模塊化規範:
AMD、CMD、CommonJS
等;
到此,我們明白了爲什麼要用模塊化開發?
那如果沒有模塊化會帶來什麼問題呢?
三. 沒有模塊化的問題
當我們在公司面對一個大型的前端項目時,通常是多人開發的,會把不同的業務邏輯分步在多個文件夾當中。
2.1 沒有模塊化給項目帶來的弊端
-
假設有兩個人,分別是小豪和小紅在開發一個項目
-
項目的目錄結構是這樣的
小豪開發的bar.js
文件
var name = "小豪";
console.log("bar.js----", name);
小豪開發的baz.js
文件
console.log("baz.js----", name);
小紅開發的foo.js
文件
var name = "小紅";
console.log("foo.js----", name);
引用路徑如下:
<body>
<script src="./bar.js"></script>
<script src="./foo.js"></script>
<script src="./baz.js"></script>
</body>
最後當我去執行的時候,卻發現執行結果:
當我們看到這個結果,有的小夥伴可能就會驚訝,baz.js
文件不是小豪寫的麼?爲什麼會輸出小紅的名字呢?
究其原因,我們才發現,其實JavaScript
是沒有模塊化的概念(至少到現在爲止還沒有用到 ES6 規範),換句話說就是每個.js
文件並不是一個獨立的模塊,沒有自己的作用域
,所以在.js
文件中定義的變量,都是可以被其他的地方共享的,所以小豪開發的baz.js
裏面的 name,其實訪問的是小紅重新聲明的。
但是共享也有一點不好就是,項目的其他協作人員也可以隨意的改變它們,顯然這不是我們想要的。
2.2 IIFE 解決早期的模塊化問題
所以,隨着前端的發展,模塊化變得必不可少,那麼在早期是如何解決的呢?
在早期,因爲函數是有自己的作用域,所以可以採用立即函數調用表達式(IIFE),也就是自執行函數,把要供外界使用的變量作爲函數的返回結果。
小豪——bar.js
var moduleBar = (function () {
var name = "小豪";
var age = "18";
console.log("bar.js----", name, age);
return {
name,
age,
};
})();
小豪——baz.js
console.log("baz.js----", moduleBar.name);
console.log("baz.js----", moduleBar.age);
小紅——foo.js
(function () {
var name = "小紅";
var age = 20;
console.log("foo.js----", name, age);
})();
來看一下,解決之後的輸出結果,原調用順序不變;
但是,這又帶來了新的問題:
-
我必須記得每一個
模塊中返回對象的命名
,才能在其他模塊使用過程中正確的使用; -
代碼寫起來
雜亂無章
,每個文件中的代碼都需要包裹在一個匿名函數中來編寫; -
在
沒有合適的規範
情況下,每個人、每個公司都可能會任意命名、甚至出現模塊名稱相同的情況;
所以現在急需一個統一的規範,來解決這些缺陷問題,就此CommonJS規範
問世了。
三. Node 模塊化開發——CommonJS 規範
3.1 CommonJS 規範特性
CommonJS 是一個規範,最初提出來是在瀏覽器以外的地方使用,並且當時被命名爲 ServerJS,後來爲了體現它的廣泛性,修改爲 CommonJS 規範。
-
Node 是 CommonJS 在服務器端一個具有代表性的實現;
-
Browserify 是 CommonJS 在瀏覽器中的一種實現;
-
webpack 打包工具具備對 CommonJS 的支持和轉換;
正是因爲 Node 中對CommonJS
進行了支持和實現,所以它具備以下幾個特點;
-
在 Node 中
每一個js文件都是一個單獨的模塊
; -
該模塊中,包含
CommonJS規範的核心變量
: exports、module.exports、require; -
使用核心變量,進行
模塊化
開發;
無疑,模塊化的核心是導出和導入,Node 中對其進行了實現:
-
exports 和 module.exports 可以負責
對模塊中的內容進行導出
; -
require 函數可以幫助我們
導入其他模塊(自定義模塊、系統模塊、第三方庫模塊)中的內容
;
3.2 CommonJS 配合 Node 模塊化開發
假設現在有兩個文件:
bar.js
const name = "時光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
main.js
console.log(name);
console.log(age);
執行 node main.js 之後,會看到
這是因爲在當前main.js
模塊內,沒有發現name
這個變量;
這點與我們前面看到的明顯不同,因爲 Node 中每個 js 文件都是一個單獨的模塊。
那麼如果要在別的文件內訪問bar.js
變量
-
bar.js
需要導出自己想要暴露的變量、函數、對象等等; -
main.js
從bar.js
引入想用的變量、函數、對象等等;
3.3 exports 導出
exports 是一個對象,我們可以在這個對象中添加很多個屬性,添加的屬性會導出。
bar.js
文件導出:
const name = "時光屋小豪";
const age = 18;
function sayHello(name) {
console.log("hello" + name);
}
exports.name = name;
exports.age = age;
exports.sayHello = sayHello;
main.js
文件導入:
const bar = require('./bar');
console.log(bar.name); // 時光屋小豪
console.log(bar.age); // 18
其中要注意的點:
- main.js 中的
bar
變量等於exports
對象;
bar = exports
-
所以我們通過
bar.xxx
來使用導出文件內的變量,比如 name,age; -
require
其實是一個函數
,返回值是一個對象,值爲 “導出文件” 的exports
對象;
3.4 從內存角度分析 bar 和 exports 是同一個對象
在 Node 中,有一個特殊的全局對象,其實exports
就是其中之一。
如果在文件內,不再使用exports.xxx
的形式導出某個變量的話,其實exports
就是一個空對象。
模塊之間的引用關係
-
當我們在
main.js
中 require 導入的時候,它會去自動查找特殊的全局對象exports
,並且把require
函數的執行結果賦值給bar
; -
bar
和exports
指向同一個引用(引用地址相同); -
如果發現
exports
上有變量,則會放到bar
對象上,正因爲這樣我們才能從bar
上讀取想用的變量;
爲了進一步論證,bar
和exports
是同一個對象:
我們加入定時器看看
所以綜上所述,Node
中實現CommonJS規範
的本質就是對象的引用賦值
(淺拷貝本質)。
把exports
對象的引用賦值bar
對象上。
CommonJS 規範的本質就是對象的引用賦值
3.5 module.exports 又是什麼?
但是 Node 中我們經常使用module.exports
導出東西,也會遇到這樣的面試題:
module.exports
和exports
有什麼關係或者區別呢?
3.6 require 細節
require 本質就是一個函數,可以幫助我們引入一個文件(模塊)中導入的對象。
require 的查找規則 https://nodejs.org/dist/latest-v14.x/docs/api/modules.html#modules_all_together
3.7 require 模塊的加載順序
結論一: 模塊在被第一次引入時,模塊中的 js 代碼會被運行一次
// aaa.js
const name = 'coderwhy';
console.log("Hello aaa");
setTimeout(() => {
console.log("setTimeout");
}, 1000);
// main.js
const aaa = require('./aaa');
aaa.js 中的代碼在引入時會被運行一次
結論二:模塊被多次引入時,會緩存,最終只加載(運行)一次
// main.js
const aaa = require('./aaa');
const bbb = require('./bbb');
/// aaa.js
const ccc = require("./ccc");
// bbb.js
const ccc = require("./ccc");
// ccc.js
console.log('ccc被加載');
ccc 中的代碼只會運行一次。
爲什麼只會加載運行一次呢?
-
每個模塊對象 module 都有一個屬性:loaded;
-
爲 false 表示還沒有加載;
-
爲 true 表示已經加載;
結論三:如果有循環引入,那麼加載順序是什麼?
如果出現下面模塊的引用關係,那麼加載順序是什麼呢?
-
這個其實是一種數據結構:圖結構;
-
圖結構在遍歷的過程中,有深度優先搜索(DFS, depth first search)和廣度優先搜索(BFS, breadth first search);
-
Node 採用的是深度優先算法:main -> aaa -> ccc -> ddd -> eee ->bbb;
多個模塊的引入關係
四. module.exports
4.1 真正導出的是 module.exports
以下是通過維基百科對 CommonJS 規範的解析:
-
CommonJS 中是沒有 module.exports 的概念的;
-
但是爲了實現模塊的導出,Node 中使用的是
Module
的類,每一個模塊都是Module
的一個實例module
; -
所以在 Node 中真正用於導出的其實根本不是
exports
,而是module.exports
; -
exports
只是module
上的一個對象
但是,爲什麼 exports 也可以導出呢?
-
這是因爲
module
對象的exports
屬性是exports
對象的一個引用; -
等價於
module.exports = exports = main中的bar
(CommonJS 內部封裝);
4.2 module.exports 和 exports 有什麼關係或者區別呢?
聯繫:module.exports = exports
進一步論證 module.exports = exports
// bar.js
const name = "時光屋小豪";
exports.name = name;
setTimeout(() => {
module.exports.name = "哈哈哈";
console.log("bar.js中1s之後", exports.name);
}, 1000);
// main.js
const bar = require("./bar");
console.log("main.js", bar.name);
setTimeout((_) => {
console.log("main.js中1s之後", bar.name);
}, 2000);
在上面代碼中,只要在bar.js
中修改exports
對象裏的屬性,導出的結果都會變,因爲即使真正導出的是 module.exports
,而module.exports
和exports
是都是相同的引用地址,改變了其中一個的屬性,另一個也會跟着改變。
注意:真正導出的模塊內容的核心其實是 module.exports,只是爲了實現 CommonJS 的規範,剛好 module.exports 對 exports 對象使用的是同一個引用而已
圖解 module.exports 和 exports 聯繫
區別:有以下兩點
那麼如果,代碼這樣修改了:
-
module.exports
也就和exports
沒有任何關係了; -
無論
exports
怎麼改,都不會影響最終的導出結果; -
因爲
module.exports = { xxx }
這樣的形式,會在堆內存中新開闢出一塊內存空間,會生成一個新的對象,用它取代之前的exports
對象的導出 -
那麼也就意味着
require
導入的對象是新的對象;
圖解 module.exports 和 exports 的區別
講完它們兩個的區別,來看下面這兩個例子,看看自己是否真正掌握了module.exports
的用法
4.3 關於 module.exports 的練習題
練習 1:導出的變量爲值類型
// bar.js
let name = "時光屋小豪";
setTimeout(() => {
name = "123123";
}, 1000);
module.exports = {
name: name,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
// main.js
const bar = require("./bar");
console.log("main.js", bar.name); // main.js 時光屋小豪
setTimeout(() => {
console.log("main.js中2s後", bar.name); // main.js中2s後 時光屋小豪
}, 2000);
練習 2:導出的變量爲引用類型
// bar.js
let info = {
name: "時光屋小豪",
};
setTimeout(() => {
info.name = "123123";
}, 1000);
module.exports = {
info: info,
age: "20",
sayHello: function (name) {
console.log("你好" + name);
},
};
// main.js
const bar = require("./bar");
console.log("main.js", bar.info.name); // main.js 時光屋小豪
setTimeout(() => {
console.log("main.js中2s後", bar.info.name); // main.js中2s後 123123
}, 2000);
從main.js
輸出結果來看,定時器修改的name
變量的結果,並沒有影響main.js
中導入的結果。
-
因爲 name 爲值類型,基本類型,一旦定義之後,就把其屬性值,放到了
module.exports
的內存裏(練 1) -
因爲 info 爲引用類型,所以
module.exports
裏存放的是 info 的引用地址,所以由定時器更改的變量,會影響main.js
導入的結果(練 2)
五. CommonJS 的加載過程
CommonJS 模塊加載 js 文件的過程是運行時加載的,並且是同步的:
-
運行時加載意味着是 js 引擎在執行 js 代碼的過程中加載模塊;
-
同步的就意味着一個文件沒有加載結束之前,後面的代碼都不會執行;
const flag = true;
if (flag) {
const foo = require('./foo');
console.log("等require函數執行完畢後,再輸出這句代碼");
}
CommonJS 通過 module.exports 導出的是一個對象:
-
導出的是一個對象意味着可以將這個對象的引用在其他模塊中賦值給其他變量;
-
但是最終他們指向的都是同一個對象,那麼一個變量修改了對象的屬性,所有的地方都會被修改;
六. CommonJS 規範的本質
CommonJS 規範的本質就是對象的引用賦值
後續文章
《JavaScript 模塊化——ES Module》
在下一篇文章中,
-
會重點講解 ES Module 規範的一切;
-
及 CommonJS 和 ES Module 是如何交互的;
-
類比 CommonJS 和 ES Module 優缺點,如何完美的回答這道面試題;
感謝大家💛
如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
-
點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
-
歡迎聯繫我噢,微信「TH0000666」。
-
關注公衆號「前端 Sharing」,持續爲你推送精選好文。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/bfGYDWCVjmVqgYQldJ2FXw