RM新时代网站-首页

0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復
登錄后你可以
  • 下載海量資料
  • 學習在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Nodejs的CommonJS規(guī)范實現(xiàn)原理

OSC開源社區(qū) ? 來源:oschina ? 2023-11-25 10:21 ? 次閱讀

了解 Node.js

Node.js 是一個基于 ChromeV8 引擎的 JavaScript 運行環(huán)境,使用了一個事件驅(qū)動、非阻塞式 I/O 模型,讓 JavaScript 運行在服務(wù)端的開發(fā)平臺,它讓 JavaScript 成為與 PHP、Python、Perl、Ruby 等服務(wù)端語言平起平坐的腳本語言。Node 中增添了很多內(nèi)置的模塊,提供各種各樣的功能,同時也提供許多第三方模塊。

模塊的問題

為什么要有模塊

復雜的前端項目需要做分層處理,按照功能、業(yè)務(wù)、組件拆分成模塊, 模塊化的項目至少有以下優(yōu)點:

便于單元測試

便于同事間協(xié)作

抽離公共方法,開發(fā)快捷

按需加載,性能優(yōu)秀

高內(nèi)聚低耦合

防止變量沖突

方便代碼項目維護

幾種模塊化規(guī)范

CMD (SeaJS 實現(xiàn)了 CMD)

AMD (RequireJS 實現(xiàn)了 AMD)

UMD (同時支持 AMD 和 CMD)

IIFE (自執(zhí)行函數(shù))

CommonJS (Node 采用了 CommonJS)

ES Module 規(guī)范 (JS 官方的模塊化方案)

Node 中的模塊

Node 中采用了 CommonJS 規(guī)范

實現(xiàn)原理:

Node 中會讀取文件,拿到內(nèi)容實現(xiàn)模塊化, Require 方法 同步引用

tips:Node 中任何 js 文件都是一個模塊,每一個文件都是模塊

Node 中模塊類型

內(nèi)置模塊,屬于核心模塊,無需安裝,在項目中不需要相對路徑引用, Node 自身提供。

文件模塊,程序員自己書寫的 js 文件模塊。

第三方模塊, 需要安裝, 安裝之后不用加路徑。

Node 中內(nèi)置模塊

fs filesystem

操作文件都需要用到這個模塊

const path = require('path'); // 處理路徑
const fs = require('fs'); // file system
// // 同步讀取
let content = fs.readFileSync(path.resolve(__dirname, 'test.js'), 'utf8');
console.log(content);

let exists = fs.existsSync(path.resolve(__dirname, 'test1.js'));
console.log(exists);

path 路徑處理

const path = require('path'); // 處理路徑


// join / resolve 用的時候可以混用

console.log(path.join('a', 'b', 'c', '..', '/'))

// 根據(jù)已經(jīng)有的路徑來解析絕對路徑, 可以用他來解析配置文件
console.log(path.resolve('a', 'b', '/')); // resolve 不支持/ 會解析成根路徑

console.log(path.join(__dirname, 'a'))
console.log(path.extname('1.js'))
console.log(path.dirname(__dirname)); // 解析父目錄

vm 運行代碼

字符串如何能變成 JS 執(zhí)行呢? 1.eval eval 中的代碼執(zhí)行時的作用域為當前作用域。它可以訪問到函數(shù)中的局部變量。

let test = 'global scope'
global.test1 = '123'
function b(){
  test = 'fn scope'
  eval('console.log(test)'); //local scope
  new Function('console.log(test1)')() // 123
  new Function('console.log(test)')() //global scope
}
b()

2.new Function new Function () 創(chuàng)建函數(shù)時,不是引用當前的詞法環(huán)境,而是引用全局環(huán)境,F(xiàn)unction 中的表達式使用的變量要么是傳入的參數(shù)要么是全局的值 Function可以獲取全局變量,所以它還是可能會有變量污染的情況出現(xiàn)
function getFn() {
  let value = "test"
  let fn = new Function('console.log(value)')
  return fn
}

getFn()()

global.a = 100 // 掛在到全局對象global上
new Function("console.log(a)")() // 100

3.vm

前面兩種方式,我們一直強調(diào)一個概念,那就是變量的污染

VM 的特點就是不受環(huán)境的影響,也可以說他就是一個沙箱環(huán)境

在 Node 中全局變量是在多個模塊下共享的,所以盡量不要在 global 中定義屬性

所以,vm.runInThisContext可以訪問到global上的全局變量,但是訪問不到自定義的變量。而vm.runInNewContext訪問不到global,也訪問不到自定義變量,他存在于一個全新的執(zhí)行上下文

const vm = require('vm')
global.a = 1
// vm.runInThisContext("console.log(a)")
vm.runInThisContext("a = 100") // 沙箱,獨立的環(huán)境
console.log(a) // 1
vm.runInNewContext('console.log(a)')
console.log(a) // a is not defined

Node 模塊化的實現(xiàn) node 中是自帶模塊化機制的,每個文件就是一個單獨的模塊,并且它遵循的是 CommonJS 規(guī)范,也就是使用 require 的方式導入模塊,通過 module.export 的方式導出模塊。 node 模塊的運行機制也很簡單,其實就是在每一個模塊外層包裹了一層函數(shù),有了函數(shù)的包裹就可以實現(xiàn)代碼間的作用域隔離。 我們先在一個 js 文件中直接打印 arguments,得到的結(jié)果如下圖所示,我們先記住這些參數(shù)。
console.log(arguments) // exports, require, module, __filename, __dirname
2dbe1c6c-8ab6-11ee-939d-92fbcf53809c.png ??

Node 中通過 modules.export 導出,require 引入。其中 require 依賴 node 中的 fs 模塊來加載模塊文件,通過 fs.readFile 讀取到的是一個字符串。 在 javascrpt 中可以通過 eval 或者 new Function 的方式來將一個字符串轉(zhuǎn)換成 js 代碼來運行。但是前面提到過,他們都有一個致命的問題,就是變量的污染。

實現(xiàn) require 模塊加載器

首先導入依賴的模塊path,fs,vm, 并且創(chuàng)建一個Require函數(shù),這個函數(shù)接收一個modulePath參數(shù),表示要導入的文件路徑

const path = require('path');
const fs = require('fs');
const vm = require('vm');
// 定義導入類,參數(shù)為模塊路徑
function Require(modulePath) {
   ...
}

在 Require 中獲取到模塊的絕對路徑,使用 fs 加載模塊,這里讀取模塊內(nèi)容使用 new Module 來抽象,使用 tryModuleLoad 來加載模塊內(nèi)容,Module 和 tryModuleLoad 稍后實現(xiàn),Require 的返回值應該是模塊的內(nèi)容,也就是 module.exports。

// 定義導入類,參數(shù)為模塊路徑
function Require(modulePath) {
    // 獲取當前要加載的絕對路徑
    let absPathname = path.resolve(__dirname, modulePath);
    // 創(chuàng)建模塊,新建Module實例
    const module = new Module(absPathname);
    // 加載當前模塊
    tryModuleLoad(module);
    // 返回exports對象
    return module.exports;
}

Module 的實現(xiàn)就是給模塊創(chuàng)建一個 exports 對象,tryModuleLoad 執(zhí)行的時候?qū)?nèi)容加入到 exports 中,id 就是模塊的絕對路徑。

// 定義模塊, 添加文件id標識和exports屬性
function Module(id) {
    this.id = id;
    // 讀取到的文件內(nèi)容會放在exports中
    this.exports = {};
}

node 模塊是運行在一個函數(shù)中,這里給 Module 掛載靜態(tài)屬性 wrapper,里面定義一下這個函數(shù)的字符串,wrapper 是一個數(shù)組,數(shù)組的第一個元素就是函數(shù)的參數(shù)部分,其中有 exports,module,Require,__dirname,__filename, 都是模塊中常用的全局變量.

第二個參數(shù)就是函數(shù)的結(jié)束部分。兩部分都是字符串,使用的時候?qū)⑺麄儼谀K的字符串外部就可以了。

// 定義包裹模塊內(nèi)容的函數(shù)
Module.wrapper = [
    "(function(exports, module, Require, __dirname, __filename) {",
    "})"
]

_extensions 用于針對不同的模塊擴展名使用不同的加載方式,比如 JSON 和 javascript 加載方式肯定是不同的。JSON 使用 JSON.parse 來運行。

javascript 使用 vm.runInThisContext 來運行,可以看到 fs.readFileSync 傳入的是 module.id 也就是 Module 定義時候 id 存儲的是模塊的絕對路徑,讀取到的 content 是一個字符串,使用 Module.wrapper 來包裹一下就相當于在這個模塊外部又包裹了一個函數(shù),也就實現(xiàn)了私有作用域。

使用 call 來執(zhí)行 fn 函數(shù),第一個參數(shù)改變運行的 this 傳入 module.exports,后面的參數(shù)就是函數(shù)外面包裹參數(shù) exports, module, Require, __dirname, __filename。/

// 定義擴展名,不同的擴展名,加載方式不同,實現(xiàn)js和json
Module._extensions = {
    '.js'(module) {
        const content = fs.readFileSync(module.id, 'utf8');
        const fnStr = Module.wrapper[0] + content + Module.wrapper[1];
        const fn = vm.runInThisContext(fnStr);
        fn.call(module.exports, module.exports, module, Require,__filename,__dirname);
    },
    '.json'(module) {
        const json = fs.readFileSync(module.id, 'utf8');
        module.exports = JSON.parse(json); // 把文件的結(jié)果放在exports屬性上
    }
}

tryModuleLoad 函數(shù)接收的是模塊對象,通過 path.extname 來獲取模塊的后綴名,然后使用 Module._extensions 來加載模塊。

// 定義模塊加載方法
function tryModuleLoad(module) {
    // 獲取擴展名
    const extension = path.extname(module.id);
    // 通過后綴加載當前模塊
    Module._extensions[extension](module); // 策略模式???
}

到此 Require 加載機制基本就寫完了。Require 加載模塊的時候傳入模塊名稱,在 Require 方法中使用 path.resolve (__dirname, modulePath) 獲取到文件的絕對路徑。然后通過 new Module 實例化的方式創(chuàng)建 module 對象,將模塊的絕對路徑存儲在 module 的 id 屬性中,在 module 中創(chuàng)建 exports 屬性為一個 json 對象。

使用 tryModuleLoad 方法去加載模塊,tryModuleLoad 中使用 path.extname 獲取到文件的擴展名,然后根據(jù)擴展名來執(zhí)行對應的模塊加載機制。

最終將加載到的模塊掛載 module.exports 中。tryModuleLoad 執(zhí)行完畢之后 module.exports 已經(jīng)存在了,直接返回就可以了。

接下來,我們給模塊添加緩存。就是文件加載的時候?qū)⑽募湃刖彺嬷?,再去加載模塊時先看緩存中是否存在,如果存在直接使用,如果不存在再去重新加載,加載之后再放入緩存。

// 定義導入類,參數(shù)為模塊路徑
function Require(modulePath) {
  // 獲取當前要加載的絕對路徑
  let absPathname = path.resolve(__dirname, modulePath);
  // 從緩存中讀取,如果存在,直接返回結(jié)果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 創(chuàng)建模塊,新建Module實例
  const module = new Module(absPathname);
  // 添加緩存
  Module._cache[absPathname] = module;
  // 加載當前模塊
  tryModuleLoad(module);
  // 返回exports對象
  return module.exports;
}

增加功能:省略模塊后綴名。

自動給模塊添加后綴名,實現(xiàn)省略后綴名加載模塊,其實也就是如果文件沒有后綴名的時候遍歷一下所有的后綴名看一下文件是否存在。

// 定義導入類,參數(shù)為模塊路徑
function Require(modulePath) {
  // 獲取當前要加載的絕對路徑
  let absPathname = path.resolve(__dirname, modulePath);
  // 獲取所有后綴名
  const extNames = Object.keys(Module._extensions);
  let index = 0;

  // 存儲原始文件路徑
  const oldPath = absPathname;
  function findExt(absPathname) {
      if (index === extNames.length) {
         return throw new Error('文件不存在');
      }
      try {
          fs.accessSync(absPathname);
          return absPathname;
      } catch(e) {
          const ext = extNames[index++];
          findExt(oldPath + ext);
      }
  }
  
  // 遞歸追加后綴名,判斷文件是否存在
  absPathname = findExt(absPathname);
  // 從緩存中讀取,如果存在,直接返回結(jié)果
  if (Module._cache[absPathname]) {
      return Module._cache[absPathname].exports;
  }
  // 創(chuàng)建模塊,新建Module實例
  const module = new Module(absPathname);
  // 添加緩存
  Module._cache[absPathname] = module;
  // 加載當前模塊
  tryModuleLoad(module);
  // 返回exports對象
  return module.exports;
}

源代碼調(diào)試

我們可以通過 VSCode 調(diào)試 Node.js

步驟

創(chuàng)建文件 a.js

module.exports = 'abc'

1. 文件 test.js

let r = require('./a')

console.log(r)

1. 配置 debug,本質(zhì)是配置.vscode/launch.json 文件,而這個文件的本質(zhì)是能提供多個啟動命令入口選擇。

一些常見參數(shù)如下:

program 控制啟動文件的路徑(即入口文件)

name 下拉菜單中顯示的名稱(該命令對應的入口名稱)

request 分為 launch(啟動)和 attach(附加)(進程已經(jīng)啟動)

skipFiles 指定單步調(diào)試跳過的代碼

runtimeExecutable 設(shè)置運行時可執(zhí)行文件,默認是 node,可以設(shè)置成 nodemon,ts-node,npm 等?

修改launch.json,skipFiles 指定單步調(diào)試跳過的代碼

2de4c704-8ab6-11ee-939d-92fbcf53809c.png

??將 test.js 文件中的 require 方法所在行前面打斷點

執(zhí)行調(diào)試,進入源碼相關(guān)入口方法

梳理代碼步驟

1. 首先進入到進入到 require 方法:Module.prototype.require

2e0876b8-8ab6-11ee-939d-92fbcf53809c.png

2e22e4d0-8ab6-11ee-939d-92fbcf53809c.png

2. 調(diào)試到 Module._load 方法中,該方法返回 module.exports,Module._resolveFilename 方法返回處理之后的文件地址,將文件改為絕對地址,同時如果文件沒有后綴就加上文件后綴。

2e67039a-8ab6-11ee-939d-92fbcf53809c.png

??3. 這里定義了 Module 類。id 為文件名。此類中定義了 exports 屬性

2e992c62-8ab6-11ee-939d-92fbcf53809c.png ??

4. 接著調(diào)試到 module.load 方法,該方法中使用了策略模式,Module._extensions [extension](this, filename) 根據(jù)傳入的文件后綴名不同調(diào)用不同的方法

2ec0560c-8ab6-11ee-939d-92fbcf53809c.png

??5. 進入到該方法中,看到了核心代碼,讀取傳入的文件地址參數(shù),拿到該文件中的字符串內(nèi)容,執(zhí)行 module._compile

2eda5df4-8ab6-11ee-939d-92fbcf53809c.png ??

6. 此方法中執(zhí)行 wrapSafe 方法。將字符串前后添加函數(shù)前后綴,并用 Node 中的 vm 模塊中的 runInthisContext 方法執(zhí)行字符串,便直接執(zhí)行到了傳入文件中的 console.log 代碼行內(nèi)容。

2f04952e-8ab6-11ee-939d-92fbcf53809c.png

2f319cf4-8ab6-11ee-939d-92fbcf53809c.png ??

至此,整個 Node 中實現(xiàn) require 方法的整個流程代碼已經(jīng)調(diào)試完畢。







審核編輯:劉清

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學習之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • javascript
    +關(guān)注

    關(guān)注

    0

    文章

    516

    瀏覽量

    53850
  • python
    +關(guān)注

    關(guān)注

    56

    文章

    4792

    瀏覽量

    84627
  • JSON
    +關(guān)注

    關(guān)注

    0

    文章

    117

    瀏覽量

    6963

原文標題:前端技術(shù)探秘 - Nodejs的CommonJS規(guī)范實現(xiàn)原理

文章出處:【微信號:OSC開源社區(qū),微信公眾號:OSC開源社區(qū)】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。

收藏 人收藏

    評論

    相關(guān)推薦

    前端技術(shù)探秘-NodejsCommonJS規(guī)范實現(xiàn)原理

    了解Node.js Node.js是一個基于ChromeV8引擎的JavaScript運行環(huán)境,使用了一個事件驅(qū)動、非阻塞式I/O模型,讓JavaScript 運行在服務(wù)端的開發(fā)平臺,它讓JavaScript成為與PHP、Python、Perl、Ruby等服務(wù)端語言平起平坐的腳本語言。Node中增添了很多內(nèi)置的模塊,提供各種各樣的功能,同時也提供許多第三方模塊。 模塊的問題 為什么要有模塊 復雜的前端項目需要做分層處理,按照功能、業(yè)務(wù)、組件拆分成模塊, 模塊化的項目至少有以下優(yōu)點: 1.便于單元測試 2.便于同事間
    的頭像 發(fā)表于 11-05 11:56 ?760次閱讀
    前端技術(shù)探秘-<b class='flag-5'>Nodejs</b>的<b class='flag-5'>CommonJS</b><b class='flag-5'>規(guī)范</b><b class='flag-5'>實現(xiàn)</b>原理

    buildroot中添加nodejs后推薦加哪些包?

    現(xiàn)有一IoT中運行的nodejs 16項目,計劃用buildroot部署在設(shè)備上。我將開發(fā)板中的buildroot升級后,成功編譯運行了nodejs16本體。但在啟動項目、依賴等時遇到各種“依賴
    發(fā)表于 11-01 12:57

    02.002 NodeJS入門 為什么要學習NodeJS

    nodejs
    充八萬
    發(fā)布于 :2023年07月19日 14:27:44

    07.007 NodeJS入門 命令的結(jié)構(gòu)

    nodejs
    充八萬
    發(fā)布于 :2023年07月19日 14:30:13

    04.004 NodeJS入門 NodeJS的作用 #硬聲創(chuàng)作季

    nodejs
    充八萬
    發(fā)布于 :2023年07月19日 14:33:58

    05.005 NodeJS入門 NodeJS的安裝

    nodejs
    充八萬
    發(fā)布于 :2023年07月19日 14:35:12

    01.001 NodeJS視頻簡介

    nodejs
    充八萬
    發(fā)布于 :2023年07月19日 19:05:48

    【W(wǎng)RTnode2R試用體驗】nodejs

    看官網(wǎng)信息,WRTnode2R是支持nodejs的,但是我通過opkg命令無法下載nodejs。有誰有二進制安裝包的?官網(wǎng)鏈接地址:http://wrtnode.cc/html/hardware_2.html#wrtnode2r
    發(fā)表于 12-05 19:34

    通過Linux命令直接下載nodejs

    我這里通過Linux命令直接下載nodejs,因為直接通過wget命令下載的話需要知道nodejs的下載地址。
    發(fā)表于 07-05 07:29

    nodejs與java的互調(diào)用方法

    nodejs 與java的互調(diào)用方法很多,我們可選的是使用oracle 新的vm 引擎(graalvm很不錯) 還有就是基于browserify進行包裝,同時給java 提供一套require
    發(fā)表于 11-04 07:31

    nodejs 如何調(diào)用 CH375函數(shù)?

    我是小白一枚,最近在用nodejs做一個上位機軟件,通過CH375拿到設(shè)備發(fā)送過來的數(shù)據(jù)。廠家給了CH375DLL64.dll,由于nodejs調(diào)用dll文件比較麻煩,經(jīng)過在網(wǎng)上找資料
    發(fā)表于 11-03 13:58

    nodejs-樹莓派安裝文件

    一款開源的物聯(lián)網(wǎng)服務(wù)器平臺,利用nodejs寫成,此文件適用于樹莓派上安裝
    發(fā)表于 11-06 17:00 ?7次下載

    CommonJs,AMD,CMD區(qū)別

    CommonJs CommonJs是服務(wù)器端模塊的規(guī)范,Node.js采用了這個規(guī)范。根據(jù)CommonJS
    發(fā)表于 11-27 13:33 ?1136次閱讀

    nodejs 后端技術(shù)介紹

    筆者最開始學的后端技術(shù)是 python 的 Django 框架,由于很久沒有使用過 python 語法,便想著了解一些 nodejs 的后端技術(shù)。下面將最近的收獲總結(jié)一下。
    的頭像 發(fā)表于 05-05 16:41 ?1101次閱讀

    使用Homebridge和HAP NodeJS來模擬HomeKit API

    電子發(fā)燒友網(wǎng)站提供《使用Homebridge和HAP NodeJS來模擬HomeKit API.zip》資料免費下載
    發(fā)表于 07-10 10:42 ?0次下載
    使用Homebridge和HAP <b class='flag-5'>NodeJS</b>來模擬HomeKit API
    RM新时代网站-首页