從前端模塊化深入解析node.js的模塊加載機(jī)制

node模塊加載機(jī)制.png

框架總覽

?? 前言
?? 模塊化的理解

  • ?? 什么是模塊
  • ?? 模塊化的進(jìn)化過程
  • ?? 模塊化的好處
  • ?? 引入多個(gè)<script>后出現(xiàn)出現(xiàn)問題

?? 模塊化規(guī)范

  • ?? CommonJS 模塊規(guī)范
  • ?? AMD 規(guī)范
  • ?? CMD 規(guī)范
  • ?? Es6模塊化
  • ?? 總結(jié)

?? Node.js模塊分類
?? nodejs模塊使用

  • ?? 創(chuàng)建 & 導(dǎo)出模塊
  • ?? 引入模塊

?? require的加載機(jī)制

  • ?? 路徑分析
  • ?? 文件定位
  • ?? 編譯執(zhí)行

?? 模塊循環(huán)引用問題
?? 站在巨人肩上


1. 前言

隨著公司618,雙11大促的到來。erp搭建的活動(dòng)頁面邏輯和交互越來越復(fù)雜。此時(shí)在JS方面就會(huì)考慮使用模塊化規(guī)范去管理。一來便于開發(fā)梳理調(diào)理。二來也可以多人協(xié)作,最后配合bable可以使用很多ES6、ES7的新功能??梢哉f,公司目前很多復(fù)雜的頁面非常需要模塊化開發(fā)。
本文內(nèi)容主要有理解模塊化,為什么要模塊化,模塊化的優(yōu)缺點(diǎn)以及模塊化規(guī)范,介紹下開發(fā)中最流行的CommonJS, AMD, ES6、CMD規(guī)范。并介紹node中的模塊引入機(jī)制。本文試圖站在小白的角度,用通俗易懂的筆調(diào)介紹這些枯燥無味的概念,希望諸君閱讀后,對(duì)模塊化編程有個(gè)全新的認(rèn)識(shí)和理解!如有錯(cuò)誤請(qǐng)多多包涵。


1. 模塊化的理解

1.1.什么是模塊?

  • 將一個(gè)復(fù)雜的程序依據(jù)一定的規(guī)則(規(guī)范)封裝成幾個(gè)塊(文件), 并進(jìn)行組合在一起
  • 塊的內(nèi)部數(shù)據(jù)與實(shí)現(xiàn)是私有的, 只是向外部暴露一些接口(方法)與外部其它模塊通信

2.2.模塊化的進(jìn)化過程

  • 全局function模式 : 將不同的功能封裝成不同的全局函數(shù)

    • 編碼: 將不同的功能封裝成不同的全局函數(shù)
    • 問題: 污染全局命名空間, 容易引起命名沖突或數(shù)據(jù)不安全,而且模塊成員之間看不出直接關(guān)系
function m1(){
  //...
}
function m2(){
  //...
}
  • namespace模式 : 簡(jiǎn)單對(duì)象封裝

    • 作用: 減少了全局變量,解決命名沖突
    • 問題: 數(shù)據(jù)不安全(外部可以直接修改模塊內(nèi)部的數(shù)據(jù))
let myModule = {
  data: 'www.baidu.com',
  foo() {
    console.log(`foo() ${this.data}`)
  },
  bar() {
    console.log(`bar() ${this.data}`)
  }
}
myModule.data = 'other data' //能直接修改模塊內(nèi)部的數(shù)據(jù)
myModule.foo() // foo() other data

這樣的寫法會(huì)暴露所有模塊成員,內(nèi)部狀態(tài)可以被外部改寫。

  • IIFE模式:匿名函數(shù)自調(diào)用(閉包)

    • 作用: 數(shù)據(jù)是私有的, 外部只能通過暴露的方法操作
    • 編碼: 將數(shù)據(jù)和行為封裝到一個(gè)函數(shù)內(nèi)部, 通過給window添加屬性來向外暴露接口
    • 問題: 如果當(dāng)前這個(gè)模塊依賴另一個(gè)模塊怎么辦?
// index.html文件
<script type="text/javascript" src="module.js"></script>
<script type="text/javascript">
    myModule.foo()
    myModule.bar()
    console.log(myModule.data) //undefined 不能訪問模塊內(nèi)部數(shù)據(jù)
    myModule.data = 'xxxx' //不是修改的模塊內(nèi)部的data
    myModule.foo() //沒有改變
</script>
// module.js文件
(function(window) {
  let data = 'www.baidu.com'
  //操作數(shù)據(jù)的函數(shù)
  function foo() {
    //用于暴露有函數(shù)
    console.log(`foo() ${data}`)
  }
  function bar() {
    //用于暴露有函數(shù)
    console.log(`bar() ${data}`)
    otherFun() //內(nèi)部調(diào)用
  }
  function otherFun() {
    //內(nèi)部私有的函數(shù)
    console.log('otherFun()')
  }
  //暴露行為
  window.myModule = { foo, bar } //ES6寫法
})(window)

最后得到的結(jié)果:

image.png
  • IIFE模式增強(qiáng) : 引入依賴

這就是現(xiàn)代模塊實(shí)現(xiàn)的基石

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作數(shù)據(jù)的函數(shù)
  function foo() {
    //用于暴露有函數(shù)
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函數(shù)
    console.log(`bar() ${data}`)
    otherFun() //內(nèi)部調(diào)用
  }
  function otherFun() {
    //內(nèi)部私有的函數(shù)
    console.log('otherFun()')
  }
  //暴露行為
  window.myModule = { foo, bar }
})(window, jQuery)
 // index.html文件
  <!-- 引入的js必須有一定順序 -->
  <script type="text/javascript" src="jquery-1.10.1.js"></script>
  <script type="text/javascript" src="module.js"></script>
  <script type="text/javascript">
    myModule.foo()
  </script>

上例子通過jquery方法將頁面的背景顏色改成紅色,所以必須先引入jQuery庫,就把這個(gè)庫當(dāng)作參數(shù)傳入。這樣做除了保證模塊的獨(dú)立性,還使得模塊之間的依賴關(guān)系變得明顯??梢詤⒖脊镜腶ctivity2018.js.

2.3 模塊化的好處

  • 避免命名沖突(減少命名空間污染)
  • 更好的分離, 按需加載
  • 更高復(fù)用性
  • 高可維護(hù)性

2.4 引入多個(gè)<script>后出現(xiàn)出現(xiàn)問題

  • 請(qǐng)求過多

首先我們要依賴多個(gè)模塊,那樣就會(huì)發(fā)送多個(gè)請(qǐng)求,導(dǎo)致請(qǐng)求過多

  • 依賴模糊

我們不知道他們的具體依賴關(guān)系是什么,也就是說很容易因?yàn)椴涣私馑麄冎g的依賴關(guān)系導(dǎo)致加載先后順序出錯(cuò)。

  • 難以維護(hù)

以上兩種原因就導(dǎo)致了很難維護(hù),很可能出現(xiàn)牽一發(fā)而動(dòng)全身的情況導(dǎo)致項(xiàng)目出現(xiàn)嚴(yán)重的問題。
模塊化固然有多個(gè)好處,然而一個(gè)頁面需要引入多個(gè)js文件,就會(huì)出現(xiàn)以上這些問題。而這些問題可以通過模塊化規(guī)范來解決,下面介紹開發(fā)中最流行的commonjs, AMD, ES6, CMD規(guī)范。


3. 模塊化規(guī)范

3.1 CommonJS規(guī)范

image.png
(1)概述

Node 應(yīng)用由模塊組成,采用 CommonJS 模塊規(guī)范。每個(gè)文件就是一個(gè)模塊,有自己的作用域。在一個(gè)文件里面定義的變量、函數(shù)、類,都是私有的,對(duì)其他文件不可見。commonJS用同步的方式加載模塊。在服務(wù)端,模塊文件都存在本地磁盤,讀取非常快,所以這樣做不會(huì)有問題。但是在瀏覽器端,限于網(wǎng)絡(luò)原因,更合理的方案是使用異步加載。

(2)特點(diǎn)

所有代碼都運(yùn)行在模塊作用域,不會(huì)污染全局作用域。
模塊可以多次加載,但是只會(huì)在第一次加載時(shí)運(yùn)行一次,然后運(yùn)行結(jié)果就被緩存了,以后再加載,就直接讀取緩存結(jié)果。要想讓模塊再次運(yùn)行,必須清除緩存。
模塊加載的順序,按照其在代碼中出現(xiàn)的順序。

(3)基本語法

暴露模塊:module.exports = valueexports.xxx = value
引入模塊:require(xxx),如果是第三方模塊,xxx為模塊名;如果是自定義模塊,xxx為模塊文件路徑

CommonJS規(guī)范規(guī)定,每個(gè)模塊內(nèi)部,module變量代表當(dāng)前模塊。這個(gè)變量是一個(gè)對(duì)象,它的exports屬性(即module.exports)是對(duì)外的接口。加載某個(gè)模塊,其實(shí)是加載該模塊的module.exports屬性。

// example.js
var x = 5;
var addX = function (value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;

上面代碼通過module.exports輸出變量x和函數(shù)addX。

var example = require('./example.js');//如果參數(shù)字符串以“./”開頭,則表示加載的是一個(gè)位于相對(duì)路徑
console.log(example.x); // 5
console.log(example.addX(1)); // 6

require命令用于加載模塊文件。require命令的基本功能是,讀入并執(zhí)行一個(gè)JavaScript文件,然后返回該模塊的module.exports對(duì)象。如果沒有發(fā)現(xiàn)指定模塊,會(huì)報(bào)錯(cuò)。

Node.js 借鑒了 CommonJS 規(guī)范的設(shè)計(jì),特別是 CommonJS 的 Modules 規(guī)范,實(shí)現(xiàn)了一套模塊系統(tǒng),同時(shí) NPM 實(shí)現(xiàn)了 CommonJS 的 Packages 規(guī)范,模塊和包組成了 Node 應(yīng)用開發(fā)的基礎(chǔ)。


3.2 AMD 規(guī)范

CommonJS規(guī)范加載模塊是同步的,也就是說,只有加載完成,才能執(zhí)行后面的操作。AMD規(guī)范則是非同步加載模塊,允許指定回調(diào)函數(shù)。由于Node.js主要用于服務(wù)器編程,模塊文件一般都已經(jīng)存在于本地硬盤,所以加載起來比較快,不用考慮非同步加載的方式,所以CommonJS規(guī)范比較適用。但是,如果是瀏覽器環(huán)境,要從服務(wù)器端加載模塊,這時(shí)就必須采用非同步模式,因此瀏覽器端一般采用AMD規(guī)范。此外用AMD規(guī)范進(jìn)行頁面開發(fā)需要用到對(duì)應(yīng)的庫函數(shù),也就是大名鼎鼎RequireJS,實(shí)際上AMD 是 RequireJS 在推廣過程中對(duì)模塊定義的規(guī)范化的產(chǎn)出.

(1)AMD規(guī)范基本語法

定義暴露模塊:

//定義沒有依賴的模塊
define(function(){
   return 模塊
})
//定義有依賴的模塊
define(['module1', 'module2'], function(m1, m2){
   return 模塊
})

引入使用模塊:

require(['module1', 'module2'], function(m1, m2){
   使用m1/m2
})

(2)requireJS主要解決兩個(gè)問題

  • 多個(gè)js文件可能有依賴關(guān)系,被依賴的文件需要早于依賴它的文件加載到瀏覽器

  • js加載的時(shí)候?yàn)g覽器會(huì)停止頁面渲染,加載文件越多,頁面失去響應(yīng)時(shí)間越長(zhǎng)

(3) 未使用AMD規(guī)范與使用require.js

通過比較兩者的實(shí)現(xiàn)方法,來說明使用AMD規(guī)范的好處。

  • 未使用AMD規(guī)范
// dataService.js文件
(function (window) {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  window.dataService = {getMsg}
})(window)
 // alerter.js文件
(function (window, dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  window.alerter = {showMsg}
})(window, dataService)
// main.js文件
(function (alerter) {
  alerter.showMsg()
})(alerter)
// index.html文件
<div><h1>Modular Demo 1: 未使用AMD(require.js)</h1></div>
<script type="text/javascript" src="js/modules/dataService.js"></script>
<script type="text/javascript" src="js/modules/alerter.js"></script>
<script type="text/javascript" src="js/main.js"></script>

最后得到如下結(jié)果:


image.png
  • 使用require.js

RequireJS是一個(gè)工具庫,主要用于客戶端的模塊管理。它的模塊管理遵守AMD規(guī)范,RequireJS的基本思想是,通過define方法,將代碼定義為模塊;通過require方法,實(shí)現(xiàn)代碼的模塊加載。
接下來介紹AMD規(guī)范在瀏覽器實(shí)現(xiàn)的步驟:

①下載require.js, 并引入

  • 官網(wǎng): http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后將require.js導(dǎo)入項(xiàng)目: js/libs/require.js

②創(chuàng)建項(xiàng)目結(jié)構(gòu)

|-js
  |-libs
    |-require.js
  |-modules
    |-alerter.js
    |-dataService.js
  |-main.js
|-index.html

③定義require.js的模塊代碼

// dataService.js文件 
// 定義沒有依賴的模塊
define(function() {
  let msg = 'www.baidu.com'
  function getMsg() {
    return msg.toUpperCase()
  }
  return { getMsg } // 暴露模塊
})
//alerter.js文件
// 定義有依賴的模塊
define(['dataService'], function(dataService) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  // 暴露模塊
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路徑 出發(fā)點(diǎn)在根目錄下
    paths: {
      //映射: 模塊標(biāo)識(shí)名: 路徑
      alerter: './modules/alerter', //此處不能寫成alerter.js,會(huì)報(bào)錯(cuò)
      dataService: './modules/dataService'
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()
// index.html文件
<!DOCTYPE html>
<html>
  <head>
    <title>Modular Demo</title>
  </head>
  <body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="js/main" src="js/libs/require.js"></script>
  </body>
</html>

④頁面引入require.js模塊:

在index.html引入 <script data-main="js/main" src="js/libs/require.js"></script>

此外在項(xiàng)目中如何引入第三方庫?只需在上面代碼的基礎(chǔ)稍作修改:

// alerter.js文件
define(['dataService', 'jquery'], function(dataService, $) {
  let name = 'Tom'
  function showMsg() {
    alert(dataService.getMsg() + ', ' + name)
  }
  $('body').css('background', 'green')
  // 暴露模塊
  return { showMsg }
})
// main.js文件
(function() {
  require.config({
    baseUrl: 'js/', //基本路徑 出發(fā)點(diǎn)在根目錄下
    paths: {
      //自定義模塊
      alerter: './modules/alerter', //此處不能寫成alerter.js,會(huì)報(bào)錯(cuò)
      dataService: './modules/dataService',
      // 第三方庫模塊
      jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會(huì)報(bào)錯(cuò)
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

上例是在alerter.js文件中引入jQuery第三方庫,main.js文件也要有相應(yīng)的路徑配置。

小結(jié):require()函數(shù)在加載依賴的函數(shù)的時(shí)候是異步加載的,這樣瀏覽器不會(huì)失去響應(yīng),它指定的回調(diào)函數(shù),只有依賴的模塊都加載成功后,才會(huì)運(yùn)行,解決了依賴性的問題。

3.3 CMD 規(guī)范

CMD 即Common Module Definition通用模塊定義,CMD是另一種js模塊化方案,它與AMD很類似,不同點(diǎn)在于:AMD 推崇依賴前置、提前執(zhí)行,CMD推崇依賴就近、延遲執(zhí)行。此規(guī)范其實(shí)是在sea.js推廣過程中產(chǎn)生的。

/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 等于在最前面聲明并初始化了要用到的所有模塊
    a.doSomething();
    if (false) {
        // 即便沒用到某個(gè)模塊 b,但 b 還是提前執(zhí)行了
        b.doSomething()
    } 
});

/** CMD寫法 **/
define(function(require, exports, module) {
    var a = require('./a'); //在需要時(shí)申明
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

(1)CMD規(guī)范基本語法

定義暴露模塊:

//定義沒有依賴的模塊
define(function(require, exports, module){
  exports.xxx = value
  module.exports = value
})
//定義有依賴的模塊
define(function(require, exports, module){
  //引入依賴模塊(同步)
  var module2 = require('./module2')
  //引入依賴模塊(異步)
    require.async('./module3', function (m3) {
    })
  //暴露模塊
  exports.xxx = value
})

引入使用模塊:

define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

(2)sea.js簡(jiǎn)單使用教程

①下載sea.js, 并引入

然后將sea.js導(dǎo)入項(xiàng)目: js/libs/sea.js

②創(chuàng)建項(xiàng)目結(jié)構(gòu)

|-js
  |-libs
    |-sea.js
  |-modules
    |-module1.js
    |-module2.js
    |-module3.js
    |-module4.js
    |-main.js
|-index.html

③定義sea.js的模塊代碼

// module1.js文件
define(function (require, exports, module) {
  //內(nèi)部變量數(shù)據(jù)
  var data = 'atguigu.com'
  //內(nèi)部函數(shù)
  function show() {
    console.log('module1 show() ' + data)
  }
  //向外暴露
  exports.show = show
})
// module2.js文件
define(function (require, exports, module) {
  module.exports = {
    msg: 'I Will Back'
  }
})
// module3.js文件
define(function(require, exports, module) {
  const API_KEY = 'abc123'
  exports.API_KEY = API_KEY
})
// module4.js文件
define(function (require, exports, module) {
  //引入依賴模塊(同步)
  var module2 = require('./module2')
  function show() {
    console.log('module4 show() ' + module2.msg)
  }
  exports.show = show
  //引入依賴模塊(異步)
  require.async('./module3', function (m3) {
    console.log('異步引入依賴模塊3  ' + m3.API_KEY)
  })
})
// main.js文件
define(function (require) {
  var m1 = require('./module1')
  var m4 = require('./module4')
  m1.show()
  m4.show()
})

④在index.html中引入

<script type="text/javascript" src="js/libs/sea.js"></script>
<script type="text/javascript">
  seajs.use('./js/modules/main')
</script>

最后得到結(jié)果如下:

module1 show() atguigu.com
module4 show() I Will Back
異步引入依賴模塊3 
abc123

Es6模塊化

在之前的javascript中是沒有模塊化概念的。如果要進(jìn)行模塊化操作,需要引入第三方的類庫。隨著技術(shù)的發(fā)展,前后端分離,前端的業(yè)務(wù)變的越來越復(fù)雜化。直至ES6帶來了模塊化,才讓javascript第一次支持了module。ES6的模塊化分為導(dǎo)出(export)與導(dǎo)入(import)兩個(gè)模塊。

export的用法

在ES6中每一個(gè)模塊即是一個(gè)文件,在文件中定義的變量,函數(shù),對(duì)象在外部是無法獲取的。如果你希望外部可以讀取模塊當(dāng)中的內(nèi)容,就必須使用export來對(duì)其進(jìn)行暴露(輸出)。先來看個(gè)例子,來對(duì)一個(gè)變量進(jìn)行模塊化。我們先來創(chuàng)建一個(gè)test.js文件,來對(duì)這一個(gè)變量進(jìn)行輸出:

export let myName="laowang";

然后可以創(chuàng)建一個(gè)index.js文件,以import的形式將這個(gè)變量進(jìn)行引入:

import {myName} from "./test.js";
console.log(myName);//laowang

如果要輸出多個(gè)變量可以將這些變量包裝成對(duì)象進(jìn)行模塊化輸出:

let myName="laowang";
let myAge=90;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"歲了"
}
export {
    myName,
    myAge,
    myfn
}
/******************************接收的代碼調(diào)整為**********************/
import {myfn,myAge,myName} from "./test.js";
console.log(myfn());//我是laowang!今年90歲了
console.log(myAge);//90
console.log(myName);//laowang

如果你不想暴露模塊當(dāng)中的變量名字,可以通過as來進(jìn)行操作:

let myName="laowang";
let myAge=90;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"歲了"
}
export {
    myName as name,
    myAge as age,
    myfn as fn
}
/******************************接收的代碼調(diào)整為**********************/
import {fn,age,name} from "./test.js";
console.log(fn());//我是laowang!今年90歲了
console.log(age);//90
console.log(name);//laowang
默認(rèn)導(dǎo)出(default export)

一個(gè)模塊只能有一個(gè)默認(rèn)導(dǎo)出,對(duì)于默認(rèn)導(dǎo)出,導(dǎo)入的名稱可以和導(dǎo)出的名稱不一致。

/******************************導(dǎo)出**********************/
export default function(){
    return "默認(rèn)導(dǎo)出一個(gè)方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意這里默認(rèn)導(dǎo)出不需要用{}。名稱隨意起
console.log(myFn());//默認(rèn)導(dǎo)出一個(gè)方法
export和default export的區(qū)別

1.exports default 后面跟一個(gè)具體的值, export 后面跟變量申明語句。

本質(zhì)上,export default value就是輸出一個(gè)叫做default的變量。default是被value賦值的,正是因?yàn)閑xport default命令其實(shí)只是輸出一個(gè)叫做default的變量,所以它后面不能跟變量聲明語句。require引入default的值,并為其起一個(gè)名字。接下來,我們可以實(shí)踐下。

首先看看 export 的執(zhí)行情況:

export let test1 = 'test1';
import {test1} from "./index";
console.log(test1);
輸出// test1 

接下來,我們來看看export default 的執(zhí)行情況:

export default let test1 = 'test1'
import test1 from "./index";
console.log(test1);
//  報(bào)錯(cuò)。
2.使用export, import 需要加大括號(hào)(* 除外), export default 則不需要

3.export default 在一個(gè)模塊里只能有一個(gè),但是export可以有多個(gè)

首先看看 export 的執(zhí)行情況:

let test1 = 'test1'
let test2 = 'test2'
export {
test1,
test2
}
import {test1, test2}  from "./index";
console.log(test1, test2);

上述代碼的執(zhí)行結(jié)果如下:

// test1 
// test2

接下來看看export default 的執(zhí)行情況

let test1 = 'test1'
let test2 = 'test2'
export default test1
export default test2

import test1, test2 from "./index";
console.log(test1, test2);

復(fù)制代碼上述代碼的執(zhí)行結(jié)果如下:

// 報(bào)錯(cuò)

4.通過export導(dǎo)出的屬性或者方法可以修改,通過export default 導(dǎo)出的基本類型不可修改

首先看看 export 的執(zhí)行情況:

let test1 = 'test1'
let test2 = {
    a: '1'
}
export  {
    test1,
    test2
}
test1 = 'test1 modify'
test2.a = '1 modify'

import {test1, test2} from "./index";
console.log(test1, test2);

上述代碼的執(zhí)行結(jié)果如下:

test1 modify {a: "1 modify"}

接下來看看export default 的執(zhí)行情況

let test1 = 'test1'
export default test1
test1 = 'test1 modify'
import test1  from "./index";
console.log(test1);

上述代碼的執(zhí)行結(jié)果如下:

// test1

上述代碼證明export導(dǎo)出的屬性或者方法可以修改,無論是基本類型,還是引用類型。

let test1 = {
    a: 'test1'
}
export default test1
test1.a = 'test1 modify'
import test1  from "./index";
console.log(test1);

上述代碼的執(zhí)行結(jié)果如下:

{a: "test1 modify"}
  • export default value ,相當(dāng)于default = value 。 import的時(shí)候可以將值賦值給任意一個(gè)變量,a/b/c等都行。所以當(dāng)value是基本數(shù)據(jù)類型的時(shí)候。value修改并不會(huì)引起require引入的值的修改。

  • 因?yàn)閑xport default也是一種export,所以all 實(shí)質(zhì)上是{a,default}

  • 因?yàn)閑xport導(dǎo)出的是一個(gè)變量。所以可以修改。

ES6 模塊與 CommonJS 模塊的差異

它們有兩個(gè)重大差異:

  • ① CommonJS 模塊輸出的是一個(gè)值的拷貝,ES6 模塊輸出的是值的引用。

  • ② CommonJS 模塊是運(yùn)行時(shí)加載,ES6 模塊是編譯時(shí)輸出接口。

第二個(gè)差異是因?yàn)?CommonJS 加載的是一個(gè)對(duì)象(即module.exports屬性),該對(duì)象只有在腳本運(yùn)行完才會(huì)生成。而 ES6 模塊不是對(duì)象,它的對(duì)外接口只是一種靜態(tài)定義,在代碼靜態(tài)解析階段就會(huì)生成

到此,前端模塊化講完。接下來講node模塊。


image.png

4. Node.js模塊分類

前文說, 在 Node.js 中, 每個(gè)文件就被視為一個(gè)模塊. 這個(gè)文件可能是 JavaScript 編寫的文件、JSON 或者用 C/C++ 編譯的二進(jìn)制文件.

模塊可以分成三類:

image.png
  • 核心模塊 』: Node.js 自帶的原生模塊. 比如, http, fs, url. 其中分為 C/C++ 編寫的和 JavaScript 編寫的兩部分. C/C++ 模塊存放在 Node.js 源代碼目錄的 src/ 目錄下. JavaScript 模塊存放在 lib/ 目錄下. 核心模塊在Node源碼編譯成可執(zhí)行文件時(shí)存為二進(jìn)制文件,直接加載在內(nèi)存中,所以不用文件定位和編譯執(zhí)行。
  • 文件模塊 』: 開發(fā)人員在本地寫的模塊. 加載時(shí)通過相對(duì)路徑, 絕對(duì)路徑來定位模塊所在位置.在運(yùn)行時(shí)動(dòng)態(tài)加載,包括了上述完整的路徑分析、文件定位、編譯執(zhí)行這些過程
  • 第三方模塊 』: 別人編寫的模塊, 通過包管理工具, 比如 npm, yarn, 可以將其從網(wǎng)絡(luò)上引入到本地項(xiàng)目, 供己使用.

5.nodejs模塊使用

在了解了什么是模塊之后, 讓我們來看看如何在 Node.js 中實(shí)際應(yīng)用模塊機(jī)制. 在使用上, 可以很簡(jiǎn)單的分為三個(gè)步驟: 創(chuàng)建, 導(dǎo)出, 引入.。先創(chuàng)建一個(gè)模塊, 然后導(dǎo)出功能或數(shù)據(jù), 模塊之間可以互相引入導(dǎo)出的內(nèi)容.

Node.js 提供了exportsrequire 兩個(gè)對(duì)象,其中exports用于導(dǎo)出模塊,require 用于從外部引入另一個(gè)模塊, 即獲取模塊的 exports 對(duì)象.

5.1創(chuàng)建 & 導(dǎo)出模塊

先讓我們來看看如何創(chuàng)建并把模塊的內(nèi)容導(dǎo)出. 在Node.js中, 一個(gè)文件就是一個(gè)模塊. 創(chuàng)建模塊的方法就是創(chuàng)建一個(gè)文件.

通過 exports對(duì)象來指定一個(gè)模塊的導(dǎo)出內(nèi)容.
示例:

// 文件名: nameModule.js
var name = 'Garrik';

exports.setName = function(newName) {
name = newName;
}

exports.getName = function() {
return name;
}

在以上示例中, nameModule.js 文件通過 exports 對(duì)象將 setName 和 getName 作為模塊的訪問接口. 其他的模塊可以引入導(dǎo)出的 exports 對(duì)象, 直接訪問 exports 對(duì)象的成員函數(shù).

5.2引入模塊

在 Node.js 中, 通過 require 函數(shù)來引入外界模塊導(dǎo)出的內(nèi)容. require 函數(shù)接受一個(gè)字符串作為路徑參數(shù), 函數(shù)根據(jù)這個(gè)字符串參數(shù)來進(jìn)行模塊查找. 找到后會(huì)返回目標(biāo)模塊導(dǎo)出的 exports 對(duì)象.

示例:
// 文件名: showNameModule.js
var nameModule = require('./nameModule.js');
console.log(nameModule.getName()); 
// 顯示: Garrik
nameModule.setName('Xiang');
console.log(nameModule.getName());
// 顯示: Xiang

上面示例中, 通過require引入了當(dāng)前目錄下nameModule.js導(dǎo)出的 exports對(duì)象, 并讓一個(gè)本地變量指向引入模塊的 exports 對(duì)象. 之后在 showNameModule.js 文件中就可以使用getNamesetName 這兩個(gè)方法了.

6. require的加載機(jī)制

image.png

上述模塊規(guī)范看起來十分簡(jiǎn)單,只有module、exportsrequire,但 Node 是如何實(shí)現(xiàn)的呢?

需要經(jīng)歷路徑分析(模塊的完整路徑)、文件定位(文件擴(kuò)展名或目錄)、編譯執(zhí)行三個(gè)步驟。

6.1 路徑分析

回顧require()接收 模塊標(biāo)識(shí) 作為參數(shù)來引入模塊,Node 就是基于這個(gè)標(biāo)識(shí)符進(jìn)行路徑分析。不同的標(biāo)識(shí)符采用的分析方式是不同的,主要分為以下幾類:

  • Node 提供的核心模塊,如 http、fs、path
    核心模塊在 Node 源碼編譯時(shí)存為二進(jìn)制執(zhí)行文件,在 Node 啟動(dòng)時(shí)直接加載到內(nèi)存中,因?yàn)椴恍枰窂椒治龊臀募ㄎ?,所以加載速度很快,而且也不用后續(xù)的文件定位和編譯執(zhí)行。

如果想加載與核心模塊同名的自定義模塊,如自定義 http 模塊,那必須選用不同標(biāo)志符或改用路徑方式。

  • .、..形式的文件模塊

.、..或/開始的標(biāo)識(shí)符都會(huì)當(dāng)成文件模塊處理,Node 會(huì)將require('./untils.js')中的路徑作為參數(shù)獲取模塊可能出現(xiàn)的位置,并以數(shù)組的形式返回文件所在的父級(jí),比如[E:/moudles]

  • 自定義文件模塊,即非路徑形式的文件模塊

自定義文件模塊是特殊的文件模塊,在路徑查找時(shí) Node 會(huì)從所在的父級(jí)開始逐級(jí)查找該模塊路徑中的node_modules路徑,直到根目錄,生成一個(gè)可能路徑的數(shù)組。并將這個(gè)數(shù)組返回。
模塊路徑查找策略示例如下:

// Module._resolveLookupPaths代碼相對(duì)復(fù)雜,這里簡(jiǎn)單起見只展示一些其執(zhí)行結(jié)果
tt.js文件目錄d/wedoctor
node tt.js
console.log(module.constructor._resolveLookupPaths('fs', module, true))
console.log(module.constructor._resolveLookupPaths('/hello', module, true))
console.log(module.constructor._resolveLookupPaths('../../hello', module, true))
console.log(module.constructor._resolveLookupPaths('hello', module, true))

// 1.加載核心模塊的時(shí)候,返回 null
// 2.加載絕對(duì)路徑的時(shí)候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韓\\.node_modules',
  'C:\\Users\\小韓\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]
// 由于是絕對(duì)路徑,所以在_findPath方法中會(huì)被清空
// 3.加載相對(duì)路徑的時(shí)候,返回
[ 'D:\\wedoctor' ]
// 4.加載自定義模塊的時(shí)候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韓\\.node_modules',
  'C:\\Users\\小韓\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]

//上面的數(shù)組,就是模塊所有可能的路徑?;旧鲜牵瑥漠?dāng)前路徑開始一級(jí)級(jí)向上尋找 node_modules 子目錄。

路徑分析只是獲取文件可能出現(xiàn)的位置,將可能出現(xiàn)位置組成的數(shù)組返回,其中文件模塊返回require引用文件的父級(jí)組成的數(shù)組:[modu.parent],第三方模塊返回沿當(dāng)前路徑向上逐級(jí)查找node_modules目錄直到根目錄組成的數(shù)組paths

6.2 文件定位

模塊路徑分析完成后緊接著的步驟是文件定位。文件定位分為以下幾個(gè)步驟:

  • 1.從 path數(shù)組中取出第一個(gè)目錄作為查找基準(zhǔn)。比如root/src 。Node 會(huì)將require()中的路徑./untils.js和當(dāng)前查找基準(zhǔn)合并。成為真實(shí)路徑root/src/untils.js。從目錄中查找該文件,如果存在,就結(jié)束查找。為索引,然后編譯執(zhí)行。如果不存在,就向上進(jìn)行下一條查找作。如果省略后綴名,將跳過第一步執(zhí)行第二步。

  • 2.通過添加.js .json .node后綴查找,如果存在文件就結(jié)束查找。如果不存在,則進(jìn)行下一條。

    1. 將require的參數(shù)作為一個(gè)包進(jìn)行查找,讀取目錄下的package.json文件,取得main(入口文件)指定的文件。如果沒有則進(jìn)行下一步
    1. 如果沒有pakage.json或者main屬性指定的文件名錯(cuò)誤,那 Node 會(huì)將 index 當(dāng)做默認(rèn)文件名,依次查找 index.js、index.json、index.node
    1. 如果仍沒找到,則取出module path數(shù)組中的下一個(gè)目錄作為基準(zhǔn)查找,循環(huán)1-4步驟,直到module path中的最后一個(gè)值。
    1. 如果找到,返回合并的絕對(duì)路徑。作為下一步要用的索引值。如果仍沒找到就會(huì)拋出異常。
  • 整個(gè)流程如下圖:

    image.png

整個(gè)查找過程類似原型鏈的查找和作用域的查找,但node對(duì)路徑查找實(shí)現(xiàn)了緩存機(jī)制,所以不會(huì)很耗性能。

6.3 編譯執(zhí)行

Node 中每個(gè)模塊都是一個(gè)對(duì)象,在具體定位到文件后,Node 會(huì)新建該模塊對(duì)象,然后根據(jù)路徑載入并編譯。不同的文件擴(kuò)展名載入方法為:

  • .js 文件: 通過 fs 模塊同步讀取后編譯執(zhí)行
  • .json 文件: 通過 fs 模塊同步讀取后,用JSON.parse()解析并返回結(jié)果
  • .node 文件: 這是用 C/C++ 寫的擴(kuò)展文件,通過process.dlopen()方法加載最后編譯生成的二進(jìn)制文件執(zhí)行即可。
  • 其他擴(kuò)展名: 都被當(dāng)做 js 文件載入

載入成功后 Node 會(huì)調(diào)用具體的編譯方式將文件執(zhí)行后返回給調(diào)用者。對(duì)于 .json 文件的編譯最簡(jiǎn)單,JSON.parse()解析得到對(duì)象后直接賦值給模塊對(duì)象的exports,而 .node 文件是C/C++編譯生成的,Node 直接調(diào)用process.dlopen()載入執(zhí)行就可以,下面重點(diǎn)介紹 .js 文件的編譯:

1. 包裝(Wrapping)

在 Node API 文檔中每個(gè)模塊有 module、exports 、 require __filename、__dirname這些變量,但是在模塊中或者全局作用域中沒有定義這些變量,那它們是怎么產(chǎn)生的呢?

事實(shí)上在編譯過程中,通過fs.readFileSync讀取js文件,把js內(nèi)容拼接到一個(gè)大大的閉包中。每個(gè)文件都是一個(gè)模塊,有自己的作用域。將exports,require,module,__dirname,__filename五大參數(shù)傳入。這樣模塊就可以使用它們了。例如一個(gè) JS 文件會(huì)被封裝成如下:

(function (exports, require, module, __filename, __dirname) {
    var math = require('math')
    export.add = function(){ //... }
})
  • 2.執(zhí)行(Evaluation):

傳入?yún)?shù),并執(zhí)行包裝得到的函數(shù)。

  • 3.緩存(Caching):

函數(shù)執(zhí)行完畢后,最后將運(yùn)行函數(shù)得到的結(jié)果放入module.exports并返回。編譯并執(zhí)行成功的模塊會(huì)將文件絕對(duì)路徑作為索引,將module的做為值組成一個(gè)對(duì)象緩存起來。例如下面代碼,會(huì)將sayHisayHaHa函數(shù)放入module.exports中并作為require()的返回值返回。最后緩存一個(gè)key,value的對(duì)象。

// untils.js
console.log('untils');
exports.sayHi = function() {
 console.log('Hi');
}
module.exports.sayHaHa = function() {
  console.log('haha');
}
// 緩存的值
{ 'E:\\node test\\index.js':
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: 'E:\\node test\\index.js',
     loaded: false,
     children: [ [Module] ],
     paths: [ 'E:\\node test\\node_modules', 'E:\\node_modules' ] },
  'E:\\node test\\extra\\src\\untils.js':
   Module {
     id: 'E:\\node test\\extra\\src\\untils.js',
     exports: { sayHi: [Function], sayHaHa: [Function] },
     parent:
      Module {
        id: '.',
        exports: {},
        parent: null,
        filename: 'E:\\node test\\index.js',
        loaded: false,
        children: [Array],
        paths: [Array] },
     filename: 'E:\\node test\\extra\\src\\untils.js',
     loaded: true,
     children: [],
     paths:
      [ 'E:\\node test\\extra\\src\\node_modules',
        'E:\\node test\\extra\\node_modules',
        'E:\\node test\\node_modules',
        'E:\\node_modules' ] } }

至此,module、exports 和 require的流程就介紹完了。


注意: 路徑分析時(shí)優(yōu)先查找緩存,提高二次引入的性能。所以在第二次使用使用該模塊時(shí)不會(huì)執(zhí)行模塊中的js。只會(huì)將函數(shù)運(yùn)行的結(jié)果引入。例如:

// untils.js
console.log('untils');
module.exports = function() {
  console.log('haha');
}
//index.js
const untils1 = require(./src/untils.js);
const untils2 = require(./src/untils.js);
untils1();
untils2();

// 打印結(jié)果
untils
haha
haha

7.node模塊間的循環(huán)引用

話不多少,直接上源碼吧:

modA.js:

module.exports.test = 'A';
const modB = require('./modB.js');
console.log( 'modA:', modB.test);
module.exports.test = 'AA';</pre>

modB.js:

module.exports.test = 'B';
const modA = require('./modA.js');
console.log( 'modB:', modA.test);
module.exports.test = 'BB';</pre>

main.js

const modA = require('./modA');

運(yùn)行結(jié)果如下:

image

剛開始學(xué)習(xí)和閱讀上述代碼,是有點(diǎn)覺得暈暈乎乎,如果A與B存在相互依賴、相互引用關(guān)系,不就形成了一個(gè)閉環(huán)或者說死循環(huán)?那程序怎么會(huì)繼續(xù)解析呢?很顯然,運(yùn)行結(jié)果告訴我們,nodejs引擎有自己的一套處理循環(huán)引用的機(jī)制。下面我們根據(jù)上述運(yùn)行結(jié)果,來推演了兩個(gè)module模塊的執(zhí)行順序,以了解nodejs打破閉環(huán)的機(jī)制。

image

過程分解:

①執(zhí)行modA第一行,輸出一個(gè)test接口

②執(zhí)行modA第二行,要引入modB此時(shí)斷點(diǎn)產(chǎn)生了,即開始執(zhí)行modB里的代碼, 程序開始走"breakpoint-out"路線

③執(zhí)行modB第一行

④執(zhí)行modB第二行,要因?yàn)閙odA,此步驟為打破閉環(huán)的關(guān)鍵,此時(shí)將A里斷點(diǎn)之前的執(zhí)行結(jié)果輸出給modB,如圖里的藍(lán)色虛線框標(biāo)識(shí)的部分,此時(shí)在modB中打印modA.test,打印'A'

⑤繼續(xù)執(zhí)行modB第三行

⑥繼續(xù)執(zhí)行modB第四行,對(duì)外輸出test接口('BB'),此后,modB執(zhí)行完畢,主程序返回至斷點(diǎn)處(modA中在②步驟產(chǎn)生的斷點(diǎn)),將modB的執(zhí)行結(jié)果保存在'modB' const變量中。

⑦執(zhí)行modA的第三行

⑧執(zhí)行modA的第四行,打印'modB'對(duì)象里的test接口,根據(jù)中指向結(jié)果可知,'modB'返回的test接口為'BB',因此,打印'BB',程序結(jié)束。

如果main.js調(diào)用的是'modB.js',分析過程完全一致,打印的結(jié)果將是'B, AA'。

根據(jù)上述分析可知,nodejs中的模塊互相引用形成的“閉環(huán)”其實(shí)是用“斷點(diǎn)”這一方式打開的,以斷點(diǎn)為出口去執(zhí)行其他模塊,也以斷點(diǎn)為入口進(jìn)行返回,之后繼續(xù)執(zhí)行斷點(diǎn)之后的代碼。

——學(xué)無止境,保持好奇。may stars guide your way.


8.站在巨人肩上

詳解Node模塊加載機(jī)制
nodejs模塊加載機(jī)制
模塊機(jī)制
node前后端模塊規(guī)范與模塊加載原理

最后編輯于
?著作權(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ù)。

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