「Node-js 系列」深入淺出 Node 模塊化開發——CommonJS 規範

前言


本文將爲大家透徹的介紹關於 Node 的模塊化——CommonJS 的一切。

看完本文可以掌握,以下幾個方面:

一. 什麼是模塊化?

在很多開發的情況下,我們都知道要使用模塊化開發,那爲什麼要使用它呢?

而事實上,模塊化開發最終的目的是將程序劃分成一個個小的結構

上面說提到的結構,就是模塊

按照這種結構劃分開發程序的過程,就是模塊化開發的過程;

二. JavaScript 設計缺陷

在網頁開發的早期,由於 JavaScript 僅僅作爲一種腳本語言,只能做一些簡單的表單驗證或動畫實現等,它還是具有很多的缺陷問題的,比如:

但隨着前端和 JavaScript 的快速發展,JavaScript 代碼變得越來越複雜了;

所以,模塊化已經是 JavaScript 一個非常迫切的需求:

到此,我們明白了爲什麼要用模塊化開發?

那如果沒有模塊化會帶來什麼問題呢?

三. 沒有模塊化的問題

當我們在公司面對一個大型的前端項目時,通常是多人開發的,會把不同的業務邏輯分步在多個文件夾當中。

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進行了支持和實現,所以它具備以下幾個特點;

無疑,模塊化的核心是導出導入,Node 中對其進行了實現:

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變量

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

其中要注意的點:

bar = exports

3.4 從內存角度分析 bar 和 exports 是同一個對象

在 Node 中,有一個特殊的全局對象,其實exports就是其中之一。

如果在文件內,不再使用exports.xxx的形式導出某個變量的話,其實exports就是一個空對象。

模塊之間的引用關係

爲了進一步論證,barexports是同一個對象:

我們加入定時器看看

所以綜上所述,Node中實現CommonJS規範的本質就是對象的引用賦值(淺拷貝本質)。

exports對象的引用賦值bar對象上。

CommonJS 規範的本質就是對象的引用賦值

3.5 module.exports 又是什麼?

但是 Node 中我們經常使用module.exports導出東西,也會遇到這樣的面試題:

module.exportsexports有什麼關係或者區別呢?

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.exports

4.1 真正導出的是 module.exports

以下是通過維基百科對 CommonJS 規範的解析:

但是,爲什麼 exports 也可以導出呢?

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.exportsexports是都是相同的引用地址,改變了其中一個的屬性,另一個也會跟着改變。

注意:真正導出的模塊內容的核心其實是 module.exports,只是爲了實現 CommonJS 的規範,剛好 module.exports 對 exports 對象使用的是同一個引用而已

圖解 module.exports 和 exports 聯繫

區別:有以下兩點

那麼如果,代碼這樣修改了:

圖解 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中導入的結果。

五. CommonJS 的加載過程

CommonJS 模塊加載 js 文件的過程是運行時加載的,並且是同步的:

const flag = true;

if (flag) {
  const foo = require('./foo');
  console.log("等require函數執行完畢後,再輸出這句代碼");
}

CommonJS 通過 module.exports 導出的是一個對象:

六. CommonJS 規範的本質

CommonJS 規範的本質就是對象的引用賦值

後續文章

《JavaScript 模塊化——ES Module》

在下一篇文章中,

感謝大家💛

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 歡迎聯繫我噢,微信「TH0000666」。

  3. 關注公衆號「前端 Sharing」,持續爲你推送精選好文。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/bfGYDWCVjmVqgYQldJ2FXw