1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【Nodejs】1129- 如何為 Node.js 的 require 函數(shù)添加鉤子?

        共 6923字,需瀏覽 14分鐘

         ·

        2022-02-13 01:30

        Node.js 是一個(gè)基于 Chrome V8 引擎的 JavaScript 運(yùn)行時(shí)環(huán)境。早期的 Node.js 采用的是 CommonJS 模塊規(guī)范,從 Node v13.2.0 版本開始正式支持 ES Modules 特性。直到 v15.3.0 版本 ES Modules 特性才穩(wěn)定下來并與 NPM 生態(tài)相兼容。

        (圖片來源:https://nodejs.org/api/esm.html)

        本文將介紹 Node.js 中 require 函數(shù)的工作流程、如何讓 Node.js 直接執(zhí)行 ts 文件及如何正確地劫持 Node.js 的 require?函數(shù),從而實(shí)現(xiàn)鉤子的功能。接下來,我們先來介紹 require 函數(shù)。

        require 函數(shù)

        Node.js 應(yīng)用由模塊組成,每個(gè)文件就是一個(gè)模塊。對(duì)于 CommonJS 模塊規(guī)范來說,我們通過 require 函數(shù)來導(dǎo)入模塊。那么當(dāng)我們使用 require 函數(shù)來導(dǎo)入模塊的時(shí)候,該函數(shù)內(nèi)部發(fā)生了什么?這里我們通過調(diào)用堆棧來了解一下 require 的過程:

        由上圖可知,在使用 require 導(dǎo)入模塊時(shí),會(huì)調(diào)用 Module 對(duì)象的 load 方法來加載模塊,該方法的實(shí)現(xiàn)如下所示:

        //?lib/internal/modules/cjs/loader.js
        Module.prototype.load?=?function(filename)?{
        ??this.filename?=?filename;
        ??this.paths?=?Module._nodeModulePaths(path.dirname(filename));

        ??const?extension?=?findLongestRegisteredExtension(filename);

        ??Module._extensions[extension](this,?filename);
        ??this.loaded?=?true;
        ??//?省略部分代碼
        };

        注意:本文所引用 Node.js 源碼所對(duì)應(yīng)的版本是 v16.13.1

        在以上代碼中,重要的兩個(gè)步驟是:

        • 步驟一:根據(jù)文件名找出擴(kuò)展名;
        • 步驟二:通過解析后的擴(kuò)展名,在 Module._extensions 對(duì)象中查找匹配的加載器。
        在 Node.js 中內(nèi)置了 3 種不同的加載器,用于加載?node、json?和?js?文件。
        node 文件加載器
        //?lib/internal/modules/cjs/loader.js
        Module._extensions['.node']?=?function(module,?filename)?{
        ??return?process.dlopen(module,?path.toNamespacedPath(filename));
        };

        json 文件加載器

        //?lib/internal/modules/cjs/loader.js
        Module._extensions['.json']?=?function(module,?filename)?{
        ?const?content?=?fs.readFileSync(filename,?'utf8');
        ?try?{
        ????module.exports?=?JSONParse(stripBOM(content));
        ?}?catch?(err)?{
        ???err.message?=?filename?+?':?'?+?err.message;
        ???throw?err;
        ?}
        };

        js 文件加載器

        //?lib/internal/modules/cjs/loader.js
        Module._extensions['.js']?=?function(module,?filename)?{
        ??//?If?already?analyzed?the?source,?then?it?will?be?cached.
        ??const?cached?=?cjsParseCache.get(module);
        ??let?content;
        ??if?(cached?.source)?{
        ????content?=?cached.source;
        ????cached.source?=?undefined;
        ??}?else?{
        ????content?=?fs.readFileSync(filename,?'utf8');
        ??}
        ??//?省略部分代碼
        ??module._compile(content,?filename);
        };

        下面我們來分析比較重要的 js 文件加載器。通過觀察以上代碼,我們可知 js 加載器的核心處理流程,也可以分為兩個(gè)步驟:

        • 步驟一:使用 fs.readFileSync 方法加載 js 文件的內(nèi)容;
        • 步驟二:使用 module._compile 方法編譯已加載的 js 代碼。
        那么了解以上的知識(shí)之后,對(duì)我們有什么用處呢?其實(shí)在了解?require?函數(shù)的工作流程之后,我們就可以擴(kuò)展 Node.js 的加載器。比如讓 Node.js 能夠運(yùn)行?ts?文件。
        //?register.js
        const?fs?=?require("fs");
        const?Module?=?require("module");
        const?{?transformSync?}?=?require("esbuild");

        Module._extensions[".ts"]?=?function?(module,?filename)?{
        ??const?content?=?fs.readFileSync(filename,?"utf8");
        ??const?{?code?}?=?transformSync(content,?{
        ????sourcefile:?filename,
        ????sourcemap:?"both",
        ????loader:?"ts",
        ????format:?"cjs",
        ??});
        ??module._compile(code,?filename);
        };

        在以上代碼中,我們引入了內(nèi)置的 module 模塊,然后利用該模塊的 _extensions 對(duì)象來注冊(cè)我們的自定義 ts 加載器。

        其實(shí),加載器的本質(zhì)就是一個(gè)函數(shù),在該函數(shù)內(nèi)部我們利用 esbuild 模塊提供的 transformSync API 來實(shí)現(xiàn) ts -> js 代碼的轉(zhuǎn)換。當(dāng)完成代碼轉(zhuǎn)換之后,會(huì)調(diào)用 module._compile 方法對(duì)代碼進(jìn)行編譯操作。

        看到這里相信有的小伙伴,也想到了 Webpack 中對(duì)應(yīng)的 loader,想深入學(xué)習(xí)的話,可以閱讀 多圖詳解,一次性搞懂Webpack Loader 這篇文章。

        篇幅有限,具體的編譯過程,我們就不展開介紹了。下面我們來看一下如何讓自定義的 ts 加載器生效。要讓 Node.js 能夠執(zhí)行 ts 代碼,我們就需要在執(zhí)行 ts 代碼前,先完成自定義 ts 加載器的注冊(cè)操作。慶幸的是,Node.js 為我們提供了模塊的預(yù)加載機(jī)制:

        ?$?node?--help?|?grep?preload
        ???-r,?--require=...?module?to?preload?(option?can?be?repeated)

        即利用 -r, --require 命令行配置項(xiàng),我們就可以預(yù)加載指定的模塊。了解完相關(guān)知識(shí)之后,我們來測(cè)試一下自定義 ts 加載器。

        首先創(chuàng)建一個(gè) index.ts 文件并輸入以下內(nèi)容:

        //?index.ts
        const?add?=?(a:?number,?b:?number)?=>?a?+?b;

        console.log("add(a,?b)?=?",?add(3,?5));

        然后在命令行輸入以下命令:

        $?node?-r?./register.js?index.ts

        當(dāng)以上命令成功運(yùn)行之后,控制臺(tái)會(huì)輸出以下內(nèi)容:

        add(a,?b)?=??8

        很明顯我們自定義的 ts 文件加載器生效了,這種擴(kuò)展機(jī)制還是值得我們學(xué)習(xí)的。另外,需要注意的是在 load 方法中,findLongestRegisteredExtension 函數(shù)會(huì)判斷文件的擴(kuò)展名是否已經(jīng)注冊(cè)在 Module._extensions 對(duì)象中,若未注冊(cè)的話,默認(rèn)會(huì)返回 .js 字符串。

        //?lib/internal/modules/cjs/loader.js
        Module.prototype.load?=?function(filename)?{
        ??this.filename?=?filename;
        ??this.paths?=?Module._nodeModulePaths(path.dirname(filename));

        ??const?extension?=?findLongestRegisteredExtension(filename);

        ??Module._extensions[extension](this,?filename);
        ??this.loaded?=?true;
        ??//?省略部分代碼
        };

        這就意味著只要文件中包含有效的 js 代碼,require 函數(shù)就能正常加載它。比如下面的 a.txt 文件:

        ??module.exports?=?"hello?world";

        看到這里相信你已經(jīng)了解 require 函數(shù)是如何加載模塊及如何自定義 Node.js 文件加載器。那么讓 Node.js 支持加載 ts、pngcss 等其它類型的文件,有更優(yōu)雅、更簡單的方案么?答案是有的,我們可以使用 pirates 這個(gè)第三方庫。

        pirates 是什么

        pirates 這個(gè)庫讓我們可以正確地劫持 Node.js 的 require?函數(shù)。利用這個(gè)庫,我們就可以很容易擴(kuò)展 Node.js 加載器的功能。

        pirates 的用法

        你可以使用 npm 來安裝 pirates:

        npm?install?--save?pirates

        在成功安裝 pirates 這個(gè)庫之后,就可以利用該模塊導(dǎo)出提供的 addHook 函數(shù)來添加鉤子:

        //?register.js
        const?addHook?=?require("pirates").addHook;

        const?revert?=?addHook(
        ??(code,?filename)?=>?code.replace("@@foo",?"console.log('foo');"),
        ??{?exts:?[".js"]?}
        );

        需要注意的是調(diào)用 addHook 之后會(huì)返回一個(gè) revert 函數(shù),用于取消對(duì) require 函數(shù)的劫持操作。下面我們來驗(yàn)證一下 pirates 這個(gè)庫是否能正常工作,首先新建一個(gè) index.js 文件并輸入以下內(nèi)容:

        //?index.js
        console.log("@@foo")

        然后在命令行輸入以下命令:

        $?node?-r?./register.js?index.js

        當(dāng)以上命令成功運(yùn)行之后,控制臺(tái)會(huì)輸出以下內(nèi)容:

        console.log('foo');

        觀察以上結(jié)果可知,我們通過 addHook 函數(shù)添加的鉤子生效了。是不是覺得挺神奇的,接下來我們來分析一下 pirates 的工作原理。

        pirates 是如何工作的

        pirates 底層是利用 Node.js 內(nèi)置 module 模塊提供的擴(kuò)展機(jī)制來實(shí)現(xiàn) Hook 功能。前面我們已經(jīng)介紹過了,當(dāng)使用 require 函數(shù)來加載模塊時(shí),Node.js 會(huì)根據(jù)文件的后綴名來匹配對(duì)應(yīng)的加載器。

        其實(shí) pirates 的源碼并不會(huì)復(fù)雜,我們來重點(diǎn)分析 addHook 函數(shù)的核心處理邏輯:

        //?src/index.js
        export?function?addHook(hook,?opts?=?{})?{
        ??let?reverted?=?false;
        ??const?loaders?=?[];?//?存放新的loader
        ??const?oldLoaders?=?[];?//?存放舊的loader
        ??let?exts;

        ??const?originalJSLoader?=?Module._extensions['.js'];?//?原始的JS?Loader?

        ??const?matcher?=?opts.matcher?||?null;
        ??const?ignoreNodeModules?=?opts.ignoreNodeModules?!==?false;
        ??exts?=?opts.extensions?||?opts.exts?||?opts.extension?||?opts.ext?
        ????||?['.js'];
        ??if?(!Array.isArray(exts))?{
        ????exts?=?[exts];
        ??}
        ??exts.forEach((ext)?{?
        ????//?...?
        ??}
        }

        為了提高執(zhí)行效率,addHook 函數(shù)提供了 matcherignoreNodeModules 配置項(xiàng)來實(shí)現(xiàn)文件過濾操作。在獲取到 exts 擴(kuò)展名列表之后,就會(huì)使用新的加載器來替換已有的加載器。

        exts.forEach((ext)?=>?{
        ????if?(typeof?ext?!==?'string')?{
        ??????throw?new?TypeError(`Invalid?Extension:?${ext}`);
        ????}
        ????//?獲取已注冊(cè)的loader,若未找到,則默認(rèn)使用JS?Loader
        ????const?oldLoader?=?Module._extensions[ext]?||?originalJSLoader;
        ????oldLoaders[ext]?=?Module._extensions[ext];

        ????loaders[ext]?=?Module._extensions[ext]?=?function?newLoader(
        ???mod,?filename
        )?
        {
        ??????let?compile;
        ??????if?(!reverted)?{
        ????????if?(shouldCompile(filename,?exts,?matcher,?ignoreNodeModules))?{
        ??????????compile?=?mod._compile;
        ??????????mod._compile?=?function?_compile(code)?{
        ??? //?這里需要恢復(fù)成原來的_compile函數(shù),否則會(huì)出現(xiàn)死循環(huán)
        ????????????mod._compile?=?compile;
        ??? //?在編譯前先執(zhí)行用戶自定義的hook函數(shù)
        ????????????const?newCode?=?hook(code,?filename);
        ????????????if?(typeof?newCode?!==?'string')?{
        ??????????????throw?new?Error(HOOK_RETURNED_NOTHING_ERROR_MESSAGE);
        ????????????}

        ????????????return?mod._compile(newCode,?filename);
        ??????????};
        ????????}
        ??????}

        ??????oldLoader(mod,?filename);
        ????};
        });

        觀察以上代碼可知,在 addHook 函數(shù)內(nèi)部是通過替換 mod._compile 方法來實(shí)現(xiàn)鉤子的功能。即在調(diào)用原始的 mod._compile 方法進(jìn)行編譯前,會(huì)先調(diào)用 hook(code, filename) 函數(shù)來執(zhí)行用戶自定義的 hook 函數(shù),從而對(duì)代碼進(jìn)行處理。

        好的,至此本文的主要內(nèi)容都介紹完了,在實(shí)際工作中,如果你想讓 Node.js 直接執(zhí)行 ts 文件,可以利用 ts-node 或 esbuild-register 這兩個(gè)庫。其中 esbuild-register 這個(gè)庫內(nèi)部就是使用了 pirates 提供的 Hook 機(jī)制來實(shí)現(xiàn)對(duì)應(yīng)的功能。

        1. JavaScript 重溫系列(22篇全)
        2. ECMAScript 重溫系列(10篇全)
        3. JavaScript設(shè)計(jì)模式 重溫系列(9篇全)
        4.?正則 / 框架 / 算法等 重溫系列(16篇全)
        5.?Webpack4 入門(上)||?Webpack4 入門(下)
        6.?MobX 入門(上)?||??MobX 入門(下)
        7. 120+篇原創(chuàng)系列匯總

        回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~


        瀏覽 89
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            天天日小穴天天插小穴天天干小穴 | 乱伦91| 男女肉粗暴 | 日本色网络 | 日逼免费视频 | 玉女名器爽到娇喘不停小说 | 色老头一区二区 | 清清草在线观看 | 日韩在线6| 91久国产|