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

前言

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

看完本文能夠掌握,如下幾個方面:前端

  • 什麼是模塊化,以及沒有模塊化會帶來哪些問題,是如何解決的;
  • JavaScript的設計缺陷;
  • CommonJS規範;
    • 它的規範特性;
    • 如何配合Node完成模塊化開發;
    • exports如何導出的;
    • module.exports是如何導出變量的,值類型和引用類型導出間的差別;
    • 從內存角度深度分析module.exportsexports又有怎樣的區別和聯繫;
    • require的細節,以及模塊的加載執行順序;
  • CommonJS的加載過程;
  • CommonJS規範的本質;

PS:本篇文章爲「Node.js系列」的第二篇,完全搞懂JS和Node中的模塊化機制;

以後會保持每週1~2篇的Node.js文章,歡迎你們和我一塊兒學習大前端進階系列。node

一.什麼是模塊化?

在不少開發的狀況下,咱們都知道要使用模塊化開發,那爲何要使用它呢?webpack

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

  • 在這個結構中編寫屬於本身的邏輯代碼有本身的做用域,不會影響到其餘的結構;
  • 這個結構能夠將本身但願暴露的變量函數對象等導出給其結構使用;
  • 也能夠經過某種方式,導入另外結構中的變量函數對象等;

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

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

二.JavaScript設計缺陷

在網頁開發的早期,因爲JavaScript僅僅做爲一種腳本語言,只能作一些簡單的表單驗證或動畫實現等,它仍是具備不少的缺陷問題的,好比:算法

  • var定義的變量做用域問題;
  • JavaScript的面向對象並不能像常規面嚮對象語言同樣使用class;
  • 在早期JavaScript並無模塊化的問題,因此也就沒有對應的模塊化解決方案;

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

  • ajax的出現,先後端開發分離,意味着後端返回數據後,咱們須要經過JavaScript進行前端頁面的渲染
  • SPA的出現,前端頁面變得更加複雜:包括前端路由狀態管理等等一系列複雜的需求須要經過JavaScript來實現;
  • 包括Node的實現,JavaScript編寫複雜的後端程序,沒有模塊化是致命的硬傷;

因此,模塊化已是JavaScript一個很是迫切的需求:後端

  • 可是JavaScript自己,直到ES6(2015)才推出了本身的模塊化方案;

  • 在此以前,爲了讓JavaScript支持模塊化,涌現出了不少不一樣的模塊化規範:AMD、CMD、CommonJS等;

到此,咱們明白了爲何要用模塊化開發?

那若是沒有模塊化會帶來什麼問題呢?

三.沒有模塊化的問題

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

3.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,其實訪問的是小紅從新聲明的。

可是共享也有一點很差就是,項目的其餘協做人員也能夠隨意的改變它們,顯然這不是咱們想要的。

3.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規範

4.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函數能夠幫助咱們導入其餘模塊(自定義模塊、系統模塊、第三方庫模塊)中的內容

4.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.jsbar.js引入想用的變量、函數、對象等等;

4.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對象;

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

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

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

模塊之間的引用關係

  • 當咱們在main.js中require導入的時候,它會去自動查找特殊的全局對象exports,而且把require函數的執行結果賦值給bar
  • barexports指向同一個引用(引用地址相同);
  • 若是發現exports上有變量,則會放到bar對象上,正由於這樣咱們才能從bar上讀取想用的變量;

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

咱們加入定時器看看

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

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

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

4.5 module.exports又是什麼?

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

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

4.6 require細節

require本質就是一個函數,能夠幫助咱們引入一個文件(模塊)中導入的對象。

require的查找規則nodejs.org/dist/latest…

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

5.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內部封裝);

5.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沒有任何關係了;

    • 不管exports怎麼改,都不會影響最終的導出結果;
  • 由於module.exports = { xxx }這樣的形式,會在堆內存中新開闢出一塊內存空間,會生成一個新的對象,用它取代以前的exports對象的導出

    • 那麼也就意味着require導入的對象是新的對象;

圖解module.exports和exports的區別

講完它們兩個的區別,來看下面這兩個例子,看看本身是否真正掌握了module.exports的用法

5.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優缺點,如何完美的回答這道面試題;

感謝你們💙

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)
  2. 關注公衆號「前端時光屋」,持續爲你推送精選好文
  3. 以爲不錯的話,也能夠閱讀時光屋小豪近期梳理的文章(感謝掘友的鼓勵與支持🌹🌹🌹)
相關文章
相關標籤/搜索