熟悉而陌生的模塊化(全面剖析 CommonJs 和 ES6Module)

前言

💬 「 來了嗎 」
💬 「 來了,來了 」
javascript

🏃🏃🏃🏃🏃🏃🏃🏃🏃🏃💨html

各位看官姥爺好,今天是 2019 年 12 月 31 日了,2019 年的最後一天了。前端

立刻 2020 年了,先在這祝各位看官姥爺 新年快樂!!!vue

先放鞭炮,請各位 🙉🙉🙉java

💥💥💥💥💥💥💥
🎉🎉🎉node

好:👏👏👏
react


21 世紀 20 年代了,還傻傻分不清模塊化嗎?
面試官問你對模塊化的理解,內心明白着,殊不知道該怎麼回答?
面試官問你 AMD、CMD、UMD、CommonJs 一臉蒙圈?
CommonJs 和 ES6 Module 的區別又是什麼呢?
webpack

彆着急,你想知道的不想知道的,你知道的不知道的,你知不知道的,本文都(bu yi ding)有。🙈🙈🙈es6

請注意: 本文篇幅有點長,若有興趣,結合代碼食用更佳,還請跟隨文章代碼敲一敲。web

耐得住寂寞,才能守得住繁華。

請知悉: 本文主要內容是用來分析 CommonJs 規範ES6Moudle 兩個模塊化方式的,對於其餘的模塊化方式本文未作分析。我的筆記,還請批評。

在本文,你可以收穫到:


正文開始

在現代前端開發中,我想聽到最多的應該是工程化、模塊化、組件化這幾個概念了吧。
或許你不能流暢的描述什麼是工程化、模塊化、組件化。
可是,你必定用到過。

你確定用到過以下指令:

npm run serve | dev
npm run build
npm run lint
...
複製代碼

你也確定用到過以下語法:

const http = require("http");
import { log } from "@/utils";
...
複製代碼

你也確定用過以下結構:

<a-button type="primary">Primary</a-button>
// 或者
<el-button type="primary">主要按鈕</el-button>
...
複製代碼

吶,這些都是你平常用到,再熟悉不過的開發方式了對吧。

工程化

目前來講,隨着瀏覽器的發展、網絡的發展、前端生態圈的發展...
總之時代在進步,人們的需求不斷增長,有需求就有業務。
如今,web 業務日益複雜化和多元化,縱觀市場上的項目,都已經再也不是過去的拼個頁面 + 搞幾個 jQuery 插件就能完成的了。前端開發已經由 webPage 模式爲主轉變爲以 webApp 模式爲主了。運行在 web 端的 app,可見其複雜度。

綜上所述,咱們開發一個前端項目再也不是從畫頁面,幾個頁面互相跳轉一下的時代。
咱們要將項目看作一個工程,從大局出發,一個項目要使用哪些工具,要使用哪些技術,哪些部分是複用的,要如何高效的抽離,如何優化性能,如何加載資源,如何使開發更規範,如何使後期維護更高效等等。

轉換一下,所謂前端工程化是否是就是咱們平常開發中使用的 模塊化、組件化、規範化、自動化的集合體?
前端工程化是否是前端質的變化呢?

而對於平常開發中使用的 webpack、vue、angular、react、ant-design、element-ui... 你不能說它們就是前端工程化,它們只是實現前端工程化的方式而已。

咱們要作的是前端工程師,而不是前端頁面師。

是否是有點跑題了呢,本文主要目的是說模塊化的啊,我以爲工程化仍是有必要放在模塊化以前提一下的

模塊化

咱們已經意識到了前端的 web 程序愈來愈複雜,也默轉潛移的身處於前端工程化的潮流中,是否有種 「初聞不知曲中意,再聽已經是曲中人」 的意思了呢。

"模塊化" 又是什麼呢?

對於工程化來講,它是工程化的下游分支;
對於 JavaScript 來講,它是一種代碼的組織方式;
對於程序來講,它是一種清晰的、易於維護、高效的開發方式。

在沒有模塊化以前

你有沒有見過這樣的代碼

在很長的一段時間裏,前端只能經過一系列的 <script> 標籤來維護咱們的代碼關係,可是一旦咱們的項目複雜度提升的時候,這種簡陋的代碼組織方式即是如噩夢般使得咱們的代碼變得混亂不堪。
而且,這種方式將多個 js 文件一股腦的引入到頁面,其實都是在一個全局執行環境下,很容易形成變量污染的問題

亦或者這樣的代碼

一個 js 文件裏 3000 行代碼,一段代碼這粘貼一塊,那粘貼一塊,維護這 3000 行代碼,難度可想而知....

我想從上面的代碼不難看出以往開發的痛點在哪裏了。

模塊化後

咱們既然知道痛點在哪裏,就應該從痛點出發,去解決問題。
咱們思考下,若是要解決上面的幾個問題,咱們要怎們作?

  • 首先須要解決多個腳本引入的依賴關係問題
    • 有沒有哪一種方式可以明確的看到某個腳本依賴哪些腳本?
    • 而且不用在一股腦的在頁面中如此引用,太混亂了。
  • 其次須要解決多個腳本都在一個全局執行環境中,變量都混在一塊兒。
    • 有沒有什麼方式可以使腳本之間獨立運行,互不影響?
  • 而後,某個腳本因爲業務複雜,不能都寫在一個文件裏面
    • 能不能一個腳本實現的業務拆成多個文件,分開管理?

有問題,也就會有解決問題的方式 -- 模塊化就爲此而生

概念:

  • 將一個複雜的程序依據必定的規則(規範)封裝成幾個塊(文件),並進行組合在一塊兒
  • 塊的內部數據與實現是私有的,只是向外部暴露一些接口(方法)與外部其它模塊通訊

其實模塊化就是將一個複雜的系統分解成多個獨立的模塊的代碼組織方式;

不少人以爲模塊化開發的工程意義是複用,其實應該是模塊化開發的最大價值應該是分治。無論你未來是否要複用某段代碼,你都有充分的理由將其分治爲一個模塊。

模塊化好處:

  • 避免命名衝突(減小命名空間污染)
  • 更好的分離,按需加載
  • 更高複用性
  • 高可維護性

說明

好,說了這麼多可算把模塊化的概念說完了。

更多的還有關於模塊化的進化過程:

全局 function 模式 => namespace 模式 => IIFE 模式 => IIFE 模式加強 : 引入依賴

具體的幾種模塊化的規範:

  • IIFE
  • AMD
  • CMD
  • CommonJS
  • UMD
  • ES6 Modules

就再也不逐一分析,重點仍是放到 CommonJsES6 Modules 中,由於這兩個是目前用的最多的。

CommonJs

1. 概述

隨着 Javasript 應用進軍服務器端,業界急需一種標準的模塊化解決方案,因而,CommonJS 應運而生。這是一種被普遍使用的 Javascript 模塊化規範,你們最熟悉的 Node.js 應用中就是採用這個規範。

在 Node.js 模塊系統中,每一個文件都被視爲一個獨立的模塊。模塊有本身的做用域,一個模塊內部全部的變量、函數、類 都是私有的,模塊之間不能直接訪問模塊內部。
在服務器端,模塊的加載是運行時同步加載的;在瀏覽器端,模塊須要提早編譯打包處理。

2. 基本語法

  • 暴露模塊:

    • exports.xxx = value
    • module.exports = value
  • 導入模塊:

    • require(xxx)
      • 若是是第三方模塊,xxx 爲模塊名;
      • 若是是自定義模塊,xxx 爲模塊文件路徑;

3. 特色

  • 全部代碼都運行在模塊做用域,不會污染全局做用域。
  • 模塊能夠屢次加載,可是隻會在第一次加載時運行一次,而後運行結果就被緩存了,之後再加載,就直接讀取緩存結果。要想讓模塊再次運行,必須清除緩存。
  • 模塊加載的順序,按照其在代碼中出現的順序。

4. 分析

好,咱們大體瞭解了下 CommonJs,如今讓咱們逐步分析

4.1 module 對象

已知在 node 中,每一個文件都是一個獨立的模塊,那麼,這個 「模塊」 究竟是什麼呢?
nodejs 官網告訴咱們:在每一個模塊中都有一個名爲 module 的自由變量是對錶示當前模塊的對象的引用

如今,新建一個 app.js 文件,在裏面嘗試打印下 module

console.log(module);

node app.js
複製代碼

順利的話,你應該能夠看到相似的輸出,沒錯,module 是一個可訪問的對象,
而這個對象,就是表明了當前文件(模塊)的引用;
如今知道 commonjs 中的 "模塊" 到底是什麼了吧。

module 對象的屬性
  • id: 模塊的標識符。 一般是徹底解析後的文件名。

  • exports: module.exports 對象由 Module 系統建立

    exports 屬性是重中之重,這個屬性是對外的接口,在外部加載模塊時,其實加載的是這個模塊的 module.exports 屬性。咱們在暴露屬性時,也是經過將屬性掛載到 module.exports 上面進行暴露操做的。

  • parent: 標識最早引用該模塊的模塊。

  • filename: 模塊的徹底解析後的文件名。

  • loaded: 模塊是否已經加載完成,或正在加載中。

  • children: 被該模塊引用的模塊對象。

  • paths: 模塊的搜索路徑。

4.2 暴露模塊

在暴露模塊時,咱們有兩種方式來將屬性暴露出去:module.exports 和 exports

exports 是一個對於 module.exports 的更簡短的引用形式。
exports 變量是在模塊的文件級做用域內可用的,且在模塊執行以前賦值給 module.exports。

實際上:
一個模塊最終暴露的是 module 整個對象,而在加載時,加載的是 module 對象的 exports 屬性;
module.exports 始終做爲一個模塊的輸出接口,以供外部訪問內部的變量。

在一個模塊做用域中,還有一個 exports 屬性,與 module.exports 屬性是同一個引用,指向同一個數據,使用 exports.xxx 的方式,能夠將對應的屬性掛載到 module.exports 屬性上,從而達到暴露屬性的目的,以下所示:

// module1.js

person = {
  name: "Jack",
  age: 18,
  sex: "man"
};

function pointPerson() {
  console.log("point person at module1.js:", person);
}

exports.person = person;
module.exports.pointPerson = pointPerson;

console.log("module.exports === exports:", module.exports === exports); // true
console.log("module1:", module);
複製代碼

能夠看到 exports 與 module.exports 是同一個引用;

不管使用 exports 仍是使用 module.exports 都掛載到了 module 下面,也會在未來暴露出去;

若是咱們將 module.exports 或者 exports 的引用改變了呢?

  1. 咱們先將 exports 的引用改變:
// module1.js

person = {
  name: "Jack",
  age: 18,
  sex: "man"
};

function pointPerson() {
  console.log("point person at module1.js:", person);
}

exports.person = person;
module.exports.pointPerson = pointPerson;

console.log("module.exports === exports:", module.exports === exports); // true
// console.log('module1:', module);

exports = { index: 1 };
exports.a = "aaa";
console.log("module.exports === exports:", module.exports === exports); // false

console.log("module1:", module);
複製代碼

承接未更改 exports 引用,對比發現,exports 再也不和 moudle.exports 全等,給 exports 添加的屬性也沒有被添加到 module.exports 上面;

因爲module.exports 始終做爲一個模塊的輸出接口,當 exports 與 module.exports 發生斷鏈後,再往 exports 上面添加屬性,將再也不被暴露出去;

  1. 嘗試改變 module.exports 的引用:
// module1.js

module.exports = {
  count: 123
};

console.log("module.exports === exports:", module.exports === exports); // false
console.log("module1:", module);
複製代碼

一樣的,exports 與 module.exports 再也不全等。在這一步的狀況下,exports 還指向舊的 moudle.exports 指向的對象,並未自動的隨着 module.exports 的改變而改變;

區別於改變 exports 的引用,直接改變 module.exports 的引用是真實有效的;最終暴露出去的接口,始終取決於 module.exports 的指向。

若是要改變 module.exports 的引用,大可將 exports 的引用改爲同一個引用,以下:

// module1.js

module.exports = exports = {
  count: 123
};

console.log("module.exports === exports:", module.exports === exports); // true
複製代碼

4.3 加載模塊

在 node 中,加載模塊使用的是 require 方法,這個方法被內置於 node 模塊做用域中,和 module 及 exports 同樣,能夠直接拿來使用。

require 在 node 官方給出的解釋是這樣的:

用於引入模塊、JSON、或本地文件。 能夠從 node_modules 引入模塊。 可使用相對路徑(例如 ./、 ./foo、 ./bar/baz、 ../foo)引入本地模塊或 JSON 文件,路徑會根據 __dirname 定義的目錄名或當前工做目錄進行處理。

// 引入本地模塊:
const myLocalModule = require("./path/myLocalModule");

// 引入 JSON 文件:
const jsonData = require("./path/filename.json");

// 引入 node_modules 模塊或 Node.js 內置模塊:
const crypto = require("crypto");
複製代碼

require 同步的讀入並執行一個 js 文件,並返回這個 js 模塊的 module.exports 屬性,若是 js 文件並無 exports 任何接口,那麼它的 module.exports 就是一個空對象,require 返回的也將是這個空對象

require 還能夠引入 json 文件,返回值就是 json 文件內的 json 數據

上代碼:

// module2.js
console.log("module2.js start:", new Date().getTime());
const person = {
  name: "Jack",
  age: 18,
  sex: "man"
};

exports.person = person;
// 暴露出去了一個 quote 變量,使它引用了當前模塊的 module.exports 屬性
exports.quote = module.exports;
console.log("module2.js end:", new Date().getTime());

// app.js
console.log("app.js start:", new Date().getTime());
const m2 = require("./modules/module2");

console.log("m2:", m2);
console.log("m2.quote:", m2.quote);
console.log("m2.quote === m2:", m2.quote === m2); // true
console.log("app.js end:", new Date().getTime());
複製代碼

result:

解析加載流程

1)在 app.js 開始部分咱們打印了一個開始的時間戳,最開始輸出的也是這一個
2)代碼遇到 require,讀入並執行 module2.js
3)在 module2.js 中打印了 module2 開始的時間戳
4)在 module2 中,暴露了一個 person 對象,及一個 module.exports 的引用
5)module2.js 執行完,輸出最後一條語句的時間戳
6)執行流返回 app.js,定義的 m2 變量接收 require() 調用的返回值
7)輸出 m2 及 m2.quote 屬性
8)發現 m2 和 m2.quote 是相等呢,那麼是否是能夠證實 require 返回的就是 模塊下的 module.exports 屬性呢
9)最後輸出 app.js 執行完畢的時間戳,其實咱們在上面就已經能看出,在執行完 module2.js 後才返回繼續執行 app.js,是否是就已經證實了 require() 是同步讀入並執行的呢。

加載緩存

既然 require 會執行 js 文件,若是我屢次加載同一模塊,是否會執行屢次這個 js 文件呢。
咱們來試一下:

// module2.js 依舊保持上一狀態

// app.js
const m2 = require("./modules/module2");
console.log("第一次加載 module2", new Date().getTime());

const m3 = require("./modules/module2");
console.log("第二次加載 module2", new Date().getTime());

const m4 = require("./modules/module2");
console.log("第三次加載 module2", new Date().getTime());
複製代碼

result:

咦?不是說 require 會讀入並執行 js 文件嗎?
怎麼就執行了一次 module2.js 呢

實際上:

在第一次使用 require 加載模塊後,這個被加載的模塊的 module 屬性(對應前面的 module 對象),就被緩存了起來;

在緩存後,require 就會返回這個緩存中的模塊的 module.exports 屬性(是否驗證了module.exports 始終做爲一個模塊的輸出接口這一說法);

若是後續還有 require 加載相同的模塊(好比 module2),那麼 require 將不會再從新讀入且執行那個模塊,而是直接將緩存中的對應的模塊的 module.exports 屬性返回;

對於當前模塊來講,不管加載多少次,不管使用什麼變量(m2/m3/m4)去接收 require 的返回值,都只不過是引用緩存中的模塊對象的 exports 屬性而已;

驗證:

// app.js

const m2 = require("./modules/module2");
const m3 = require("./modules/module2");
const m4 = require("./modules/module2");

console.log(m2 === m3); // true
console.log(m2 === m4); // true
複製代碼
requer.cache

在 app.js 中嘗試打印 require.cache

// app.js

const m2 = require("./modules/module2");
const m3 = require("./modules/module2");
const m4 = require("./modules/module2");
console.log(require.cache);
複製代碼

result:

雖然咱們不可以明確的知道這個到底輸出的是個什麼東西,不過看這個結構也能猜個七七八八了

咱們能夠將 require.cache 看作一個對象,緩存對象;
看這個對象的格式,key 應該是某些模塊的完整路徑及模塊名字,value 應該是對應模塊的module 對象; 第一個屬性是當前模塊的 module 對象
其餘的屬性應該是加載的其餘模塊的 module 對象
不難看出,require 按照規則,返回的就是這個對象下的某個字段(某個module 對象)下的 exports 屬性;

咱們能夠看下代碼:

// app.js

const m2 = require("./modules/module2");
const m3 = require("./modules/module2");
const m4 = require("./modules/module2");

const name = require.resolve("./modules/module2");
const moduleCache = require.cache[name];

console.log(m2 === moduleCache.exports); // true
console.log(m3 === moduleCache.exports); // true
console.log(m4 === moduleCache.exports); // true
複製代碼

當咱們直接獲取 require.cache 對象中的 module2 的屬性後,將它的 exports 屬性,與 require 加載的模塊比較,發現就是全等的;
這樣一來,是否是就明白了這個緩存的機制了呢。
若是想要讓模塊文件再次執行,那就在加載模塊前清除掉緩存就能夠了(刪除掉 require.cache 中表明模塊的屬性便可);

function clearCache(path) {
  path = require.resolve(path);
  delete require.cache[path];
}
複製代碼

在 app 模塊中,大概就像下圖這樣:

【注意:】 若是你能弄明白這個加載的緩存機制,也就可以明白爲何 commonjs 中加載的模塊爲何不能實時響應模塊內部數據的變化了。 由於模塊加載的是被加載模塊的一個緩存副本,並不能實時的響應模塊內部的數據的變化

小結

1)在 node 中,每一個文件都是獨立的模塊;
2)在每一個模塊中,都有一個名爲 module 的自由變量,用來表示當前模塊的引用;
3)module 對象下面的 exports 屬性是最終引用的關鍵屬性
4)暴露模塊有兩種方式,module.exports && exports

  • exports 是 module.exports 的簡寫
  • 它們兩個在最初時指向同一個地址
  • 改變其中任意一個,都會使 exports 和 moudle.exports 斷鏈
  • 最終暴露出去的接口,徹底取決於 module.exports 屬性

5)加載模塊使用 require 方法;
6)加載模塊時會將被加載模塊的 module 對象緩存在當前模塊中的 require.cache 中;
7)正式由於加載的緩存機制,加載事後的模塊不能實時獲取模塊內部的數據。

ES6Module

個人媽耶,光一個 CommonJs 剖析就寫了這麼多,有點出乎意料,有點蒙圈。我須要整理一下思緒,再整理 es6 的 module。

太多了,累死了 😨😨😨 估計也沒人會認真看到這,先溜了,我要去跨年啦,明年再繼續寫。。。

新年快樂!新年快樂!新年快樂!

參考文章

前端模塊化詳解(完整版)
前端模塊化的前世此生
淺談前端工程化
NodeJs - Module
ES6 - Module

相關文章
相關標籤/搜索