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

node模塊加載機制.png

框架總覽

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

  • ?? 什么是模塊
  • ?? 模塊化的進化過程
  • ?? 模塊化的好處
  • ?? 引入多個<script>后出現出現問題

?? 模塊化規范

  • ?? CommonJS 模塊規范
  • ?? AMD 規范
  • ?? CMD 規范
  • ?? Es6模塊化
  • ?? 總結

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

  • ?? 創建 & 導出模塊
  • ?? 引入模塊

?? require的加載機制

  • ?? 路徑分析
  • ?? 文件定位
  • ?? 編譯執行

?? 模塊循環引用問題
?? 站在巨人肩上


1. 前言

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


1. 模塊化的理解

1.1.什么是模塊?

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

2.2.模塊化的進化過程

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

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

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

這樣的寫法會暴露所有模塊成員,內部狀態可以被外部改寫。

  • IIFE模式:匿名函數自調用(閉包)

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

最后得到的結果:

image.png
  • IIFE模式增強 : 引入依賴

這就是現代模塊實現的基石

// module.js文件
(function(window, $) {
  let data = 'www.baidu.com'
  //操作數據的函數
  function foo() {
    //用于暴露有函數
    console.log(`foo() ${data}`)
    $('body').css('background', 'red')
  }
  function bar() {
    //用于暴露有函數
    console.log(`bar() ${data}`)
    otherFun() //內部調用
  }
  function otherFun() {
    //內部私有的函數
    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庫,就把這個庫當作參數傳入。這樣做除了保證模塊的獨立性,還使得模塊之間的依賴關系變得明顯。可以參考公司的activity2018.js.

2.3 模塊化的好處

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

2.4 引入多個<script>后出現出現問題

  • 請求過多

首先我們要依賴多個模塊,那樣就會發送多個請求,導致請求過多

  • 依賴模糊

我們不知道他們的具體依賴關系是什么,也就是說很容易因為不了解他們之間的依賴關系導致加載先后順序出錯。

  • 難以維護

以上兩種原因就導致了很難維護,很可能出現牽一發而動全身的情況導致項目出現嚴重的問題。
模塊化固然有多個好處,然而一個頁面需要引入多個js文件,就會出現以上這些問題。而這些問題可以通過模塊化規范來解決,下面介紹開發中最流行的commonjs, AMD, ES6, CMD規范。


3. 模塊化規范

3.1 CommonJS規范

image.png
(1)概述

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

(2)特點

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

(3)基本語法

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

CommonJS規范規定,每個模塊內部,module變量代表當前模塊。這個變量是一個對象,它的exports屬性(即module.exports)是對外的接口。加載某個模塊,其實是加載該模塊的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和函數addX。

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

require命令用于加載模塊文件。require命令的基本功能是,讀入并執行一個JavaScript文件,然后返回該模塊的module.exports對象。如果沒有發現指定模塊,會報錯。

Node.js 借鑒了 CommonJS 規范的設計,特別是 CommonJS 的 Modules 規范,實現了一套模塊系統,同時 NPM 實現了 CommonJS 的 Packages 規范,模塊和包組成了 Node 應用開發的基礎。


3.2 AMD 規范

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

(1)AMD規范基本語法

定義暴露模塊:

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

引入使用模塊:

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

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

  • 多個js文件可能有依賴關系,被依賴的文件需要早于依賴它的文件加載到瀏覽器

  • js加載的時候瀏覽器會停止頁面渲染,加載文件越多,頁面失去響應時間越長

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

通過比較兩者的實現方法,來說明使用AMD規范的好處。

  • 未使用AMD規范
// 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>

最后得到如下結果:


image.png
  • 使用require.js

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

①下載require.js, 并引入

  • 官網: http://www.requirejs.cn/
  • github : https://github.com/requirejs/requirejs

然后將require.js導入項目: js/libs/require.js

②創建項目結構

|-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/', //基本路徑 出發點在根目錄下
    paths: {
      //映射: 模塊標識名: 路徑
      alerter: './modules/alerter', //此處不能寫成alerter.js,會報錯
      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>

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

// 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/', //基本路徑 出發點在根目錄下
    paths: {
      //自定義模塊
      alerter: './modules/alerter', //此處不能寫成alerter.js,會報錯
      dataService: './modules/dataService',
      // 第三方庫模塊
      jquery: './libs/jquery-1.10.1' //注意:寫成jQuery會報錯
    }
  })
  require(['alerter'], function(alerter) {
    alerter.showMsg()
  })
})()

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

小結:require()函數在加載依賴的函數的時候是異步加載的,這樣瀏覽器不會失去響應,它指定的回調函數,只有依賴的模塊都加載成功后,才會運行,解決了依賴性的問題。

3.3 CMD 規范

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

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

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

(1)CMD規范基本語法

定義暴露模塊:

//定義沒有依賴的模塊
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簡單使用教程

①下載sea.js, 并引入

然后將sea.js導入項目: js/libs/sea.js

②創建項目結構

|-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) {
  //內部變量數據
  var data = 'atguigu.com'
  //內部函數
  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>

最后得到結果如下:

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

Es6模塊化

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

export的用法

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

export let myName="laowang";

然后可以創建一個index.js文件,以import的形式將這個變量進行引入:

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

如果要輸出多個變量可以將這些變量包裝成對象進行模塊化輸出:

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

如果你不想暴露模塊當中的變量名字,可以通過as來進行操作:

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

一個模塊只能有一個默認導出,對于默認導出,導入的名稱可以和導出的名稱不一致。

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

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

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

首先看看 export 的執行情況:

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

接下來,我們來看看export default 的執行情況:

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

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

首先看看 export 的執行情況:

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

上述代碼的執行結果如下:

// test1 
// test2

接下來看看export default 的執行情況

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

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

復制代碼上述代碼的執行結果如下:

// 報錯

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

首先看看 export 的執行情況:

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);

上述代碼的執行結果如下:

test1 modify {a: "1 modify"}

接下來看看export default 的執行情況

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

上述代碼的執行結果如下:

// test1

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

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

上述代碼的執行結果如下:

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

  • 因為export default也是一種export,所以all 實質上是{a,default}

  • 因為export導出的是一個變量。所以可以修改。

ES6 模塊與 CommonJS 模塊的差異

它們有兩個重大差異:

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

  • ② CommonJS 模塊是運行時加載,ES6 模塊是編譯時輸出接口。

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

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


image.png

4. Node.js模塊分類

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

模塊可以分成三類:

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

5.nodejs模塊使用

在了解了什么是模塊之后, 讓我們來看看如何在 Node.js 中實際應用模塊機制. 在使用上, 可以很簡單的分為三個步驟: 創建, 導出, 引入.。先創建一個模塊, 然后導出功能或數據, 模塊之間可以互相引入導出的內容.

Node.js 提供了exportsrequire 兩個對象,其中exports用于導出模塊,require 用于從外部引入另一個模塊, 即獲取模塊的 exports 對象.

5.1創建 & 導出模塊

先讓我們來看看如何創建并把模塊的內容導出. 在Node.js中, 一個文件就是一個模塊. 創建模塊的方法就是創建一個文件.

通過 exports對象來指定一個模塊的導出內容.
示例:

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

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

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

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

5.2引入模塊

在 Node.js 中, 通過 require 函數來引入外界模塊導出的內容. require 函數接受一個字符串作為路徑參數, 函數根據這個字符串參數來進行模塊查找. 找到后會返回目標模塊導出的 exports 對象.

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

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

6. require的加載機制

image.png

上述模塊規范看起來十分簡單,只有module、exportsrequire,但 Node 是如何實現的呢?

需要經歷路徑分析(模塊的完整路徑)、文件定位(文件擴展名或目錄)、編譯執行三個步驟。

6.1 路徑分析

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

  • Node 提供的核心模塊,如 http、fs、path
    核心模塊在 Node 源碼編譯時存為二進制執行文件,在 Node 啟動時直接加載到內存中,因為不需要路徑分析和文件定位,所以加載速度很快,而且也不用后續的文件定位和編譯執行。

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

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

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

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

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

// Module._resolveLookupPaths代碼相對復雜,這里簡單起見只展示一些其執行結果
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.加載核心模塊的時候,返回 null
// 2.加載絕對路徑的時候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韓\\.node_modules',
  'C:\\Users\\小韓\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]
// 由于是絕對路徑,所以在_findPath方法中會被清空
// 3.加載相對路徑的時候,返回
[ 'D:\\wedoctor' ]
// 4.加載自定義模塊的時候,返回
[ 'D:\\wedoctor\\node_modules',
  'D:\\node_modules',
  'C:\\Users\\小韓\\.node_modules',
  'C:\\Users\\小韓\\.node_libraries',
  'D:\\tools\\nodejs\\lib\\node' ]

//上面的數組,就是模塊所有可能的路徑?;旧鲜牵瑥漠斍奥窂介_始一級級向上尋找 node_modules 子目錄。

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

6.2 文件定位

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

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

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

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

    image.png

整個查找過程類似原型鏈的查找和作用域的查找,但node對路徑查找實現了緩存機制,所以不會很耗性能。

6.3 編譯執行

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

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

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

1. 包裝(Wrapping)

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

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

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

傳入參數,并執行包裝得到的函數。

  • 3.緩存(Caching):

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

// 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的流程就介紹完了。


注意: 路徑分析時優先查找緩存,提高二次引入的性能。所以在第二次使用使用該模塊時不會執行模塊中的js。只會將函數運行的結果引入。例如:

// 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();

// 打印結果
untils
haha
haha

7.node模塊間的循環引用

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

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');

運行結果如下:

image

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

image

過程分解:

①執行modA第一行,輸出一個test接口

②執行modA第二行,要引入modB此時斷點產生了,即開始執行modB里的代碼, 程序開始走"breakpoint-out"路線

③執行modB第一行

④執行modB第二行,要因為modA,此步驟為打破閉環的關鍵,此時將A里斷點之前的執行結果輸出給modB,如圖里的藍色虛線框標識的部分,此時在modB中打印modA.test,打印'A'

⑤繼續執行modB第三行

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

⑦執行modA的第三行

⑧執行modA的第四行,打印'modB'對象里的test接口,根據中指向結果可知,'modB'返回的test接口為'BB',因此,打印'BB',程序結束。

如果main.js調用的是'modB.js',分析過程完全一致,打印的結果將是'B, AA'。

根據上述分析可知,nodejs中的模塊互相引用形成的“閉環”其實是用“斷點”這一方式打開的,以斷點為出口去執行其他模塊,也以斷點為入口進行返回,之后繼續執行斷點之后的代碼。

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


8.站在巨人肩上

詳解Node模塊加載機制
nodejs模塊加載機制
模塊機制
node前后端模塊規范與模塊加載原理

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,197評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,415評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,104評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,884評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,647評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,130評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,208評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,366評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,887評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,737評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,478評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,174評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,586評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,827評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,608評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,914評論 2 372