45.CommonJS詳解

什么是模塊化?

到底什么是模塊化、模塊化開發(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ī)范:AMDCMD、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):

  • exportsmodule.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.exportsexports有什么關(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ī)則是怎么樣的呢?

情況一: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’)

image.png

  • 會(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;
image.png

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

image.png

  • 這個(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

第二步:定義HTML的script標(biāo)簽引入require.js和定義入口文件:*

  • data-main屬性的作用是在加載完src的文件后會(huì)加載執(zhí)行該文件

require.js的使用

<script src="./lib/require.js" data-main="./index.js"></script>
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>
    <!-- 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

第二步:引入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,
  };
});

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容