又一個基于 Esbuild 的神器!esno

大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan02?參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》?包含20余篇源碼文章。歷史面試系列

esno我在我的很多源碼文章中提到過,但沒有寫文章,分享這篇好文章。


Node.js 并不支持直接執行 TS 文件,如果要執行 TS 文件的話,我們就可以借助 ts-node 這個庫。相信有些小伙伴在工作中也用過這個庫,關于 ts-node 這個庫的相關內容我就不展開介紹了,因為本文的主角是由 antfu 大佬開源的 esno 項目,接下來我將帶大家一起來揭開這個項目背后的秘密。

閱讀完本文后,你將了解 esno 項目是如何執行 TS 文件。此外,你還會了解如何劫持 Node.js 的 require 函數、如何為 ES Module 的 import 語句添加鉤子及如何自定義 https 加載器,以支持 import React from "https://esm.sh/react" 導入方式。

esno 是什么

esno 是基于 esbuild 的 TS/ESNext node 運行時。該庫會針對不同的模塊化標準,采用不同的方案:

  • esno - Node in CJS mode - by esbuild-register

  • esmo - Node in ESM mode - by esbuild-node-loader

使用 esno 的方式很簡單,你可以以全局或局部的方式來安裝它:

全局安裝

$?npm?i?-g?esno

在安裝成功后,你就可以通過以下方式來直接執行 TS 文件:

$?esno?index.ts
$?esmo?index.ts

局部安裝

$?npm?i?esno

而對于局部安裝的方式來說,一般情況下,我們會以 npm scripts 的方式來使用它:

{"scripts":?{"start":?"esno?index.ts"},"dependencies":?{"esno":?"0.14.0"}
}

esno 是如何工作的

在開始分析 esno 的工作原理之前,我們先來熟悉一下該項目:

├──?LICENSE
├──?README.md
├──?esmo.mjs
├──?esno.js
├──?package.json
├──?pnpm-lock.yaml
├──?publish.ts
└──?tsconfig.json

觀察以上的項目結構可知,該項目并不會復雜。在項目根目錄下的 package.json 文件中,我們看到了前面介紹的 esnoesmo 命令。

{"bin":?{"esno":?"esno.js","esmo":?"esmo.mjs"},
}

此外,在 package.jsonscripts 字段中,我們發現了 release 命令。顧名思義,該命令用來發布版本。

{"scripts":?{"release":?"npx?bumpp?--tag?--commit?--push?&&?node?esmo.mjs?publish.ts"},
}

需要注意的是,在 publish.ts 文件中,使用到了 2021 年度 Github 上最耀眼的項目 zx,利用該項目我們可以輕松地編寫命令行腳本。寫作本文時,它的 Star 數已經高達 27.5K,強烈推薦感興趣的小伙伴關注一下該項目。

簡單介紹了 esno 項目之后,接下來我們來分析 esno.js 文件:

#!/usr/bin/env?nodeconst?spawn?=?require('cross-spawn')
const?spawnSync?=?spawn.syncconst?register?=?require.resolve('esbuild-register')const?argv?=?process.argv.slice(2)process.exit(spawnSync('node',?['-r',?register,?...argv],?{?stdio:?'inherit'?}).status)

由以上代碼可知,當執行 esno index.ts 命令后,會通過 spawnSync 來啟動 Node.js 程序執行腳本。需要注意的是,在執行時使用了 -r 選項,該選項的作用是預加載模塊:

-r,?--require?=?...?module?to?preload?(option?can?be?repeated)

這里預加載的模塊是 esbuild-register,該模塊就是 esno 命令執行 TS 文件的幕后英雄。

esbuild-register 是什么

esbuild-register 是一個基于 esbuild 來轉換 JSX、TS 和 esnext 特性的工具。你可以通過以下多種方式來安裝它:

$?npm?i?esbuild?esbuild-register?-D
#?Or?Yarn
$?yarn?add?esbuild?esbuild-register?--dev
#?Or?pnpm
$?pnpm?add?esbuild?esbuild-register?-D

在成功安裝該模塊之后,就可以在命令行中,直接通過 node 應用程序來執行 ts 文件:

$?node?-r?esbuild-register?file.ts

?-r, --require ?= ?... module to preload (option can be repeated)

-r 用于指定預加載的文件,即在執行 file.ts 文件前,提前加載 esbuild-register 模塊

它將會使用 tsconfig.json 中的 jsxFactory, jsxFragmentFactorytarget 配置項來執行轉換操作。

esbuild-register 不僅可以在命令行中使用,而且還可以通過 API 的方式進行使用:

const?{?register?}?=?require('esbuild-register/dist/node')const?{?unregister?}?=?register({//?...options
})//?Unregister?the?require?hook?if?you?don't?need?it?anymore
unregister()

了解完 esbuild-register 的基本使用之后,接下來我們來分析它內部是如何工作的。

esbuild-register 是如何工作的

esbuild-register 內部利用了 pirates 這個庫來劫持 Node.js 的 require 函數,從而讓你可以在命令行中,直接執行 ts 文件。下面我們來看一下 esbuild-register 模塊中定義的 register 函數:

//?esbuild-register/src/node.ts
import?{?transformSync,?TransformOptions?}?from?'esbuild'
import?{?addHook?}?from?'pirates'export?function?register(esbuildOptions:?RegisterOptions?=?{})?{const?{extensions?=?DEFAULT_EXTENSIONS,hookIgnoreNodeModules?=?true,hookMatcher,...overrides}?=?esbuildOptions//?利用?transformSync?const?compile:?COMPILE?=?function?compile(code,?filename,?format)?{const?dir?=?dirname(filename)const?options?=?getOptions(dir)format?=?format????inferPackageFormat(dir,?filename)const?{code:?js,warnings,map:?jsSourceMap,}?=?transformSync(code,?{sourcefile:?filename,sourcemap:?'both',loader:?getLoader(filename),target:?options.target,jsxFactory:?options.jsxFactory,jsxFragment:?options.jsxFragment,format,...overrides,})//?省略部分代碼}const?revert?=?addHook(compile,?{exts:?extensions,ignoreNodeModules:?hookIgnoreNodeModules,matcher:?hookMatcher,})return?{unregister()?{revert()},}
}

觀察以上的代碼可知,在 register 函數內部是利用 esbuild 模塊提供的 transformSync API 來實現 ts -> js 代碼的轉換。其實最關鍵的環節,還是通過調用 pirates 這個庫提供的 addHook 函數來注冊編譯 ts 文件的鉤子。那么 addHook 函數內部到底做了哪些處理呢?下面我們來看一下它的實現:

//?pirates-4.0.5/src/index.js
export?function?addHook(hook,?opts?=?{})?{let?reverted?=?false;const?loaders?=?[];?//?存放新的loaderconst?oldLoaders?=?[];?//?存放舊的loaderlet?exts;const?originalJSLoader?=?Module._extensions['.js'];?//?原始的JS?Loader?//?省略部分代碼exts.forEach((ext)?=>?{//?獲取已注冊的loader,若未找到,則默認使用JS?Loaderconst?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)?{//?這里需要恢復成原來的_compile函數,否則會出現死循環mod._compile?=?compile;//?在編譯前先執行用戶自定義的hook函數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 函數的實現并不會復雜,該函數內部就是通過替換 mod._compile 方法來實現鉤子的功能。即在調用原始的 mod._compile 方法進行編譯前,會先調用 hook(code, filename) 函數來執行用戶自定義的 hook 函數,從而對代碼進行預處理。

而對于 esbuild-register 庫中的 register 函數來說,當 hook 函數執行時,就會調用該函數內部定義的 compile 函數來編譯 ts 代碼,然后再調用mod._compile 方法編譯生成的 js 代碼。

關于 esbuild-register 和 pirates 這兩個庫的內容就先介紹到這里,如果你想詳細了解 pirates 這個庫是如何工作的,可以閱讀 如何為 Node.js 的 require 函數添加鉤子? 這篇文章。

現在我們已經分析完 esno.js 文件,接下來我們來分析 esmo.mjs 文件。

esmo 是如何工作的

esmo 命令對應的是 esmo.mjs 文件:

#!/usr/bin/env?nodeimport?spawn?from?'cross-spawn'
import?{?resolve?}?from?'import-meta-resolve'
const?spawnSync?=?spawn.syncconst?argv?=?process.argv.slice(2)
resolve('esbuild-node-loader',?import.meta.url).then((path)?=>?{process.exit(spawnSync('node',?['--loader',?path,?...argv],?{?stdio:?'inherit'?}).status)
})

由以上代碼可知,當使用 node 應用程序執行 ES Module 文件時,會通過 --loader 選項來指定自定義的 ES Module 加載器。

--loader,?--experimental-loader?=?...?use?the?specified?module?as?a?custom?loader

需要注意的是,通過 --loader 選項指定的自定義加載器只適用于 ES Module 的 import 調用,并不適用于 CommonJS 的 require 調用。

那么自定義加載器有什么作用呢?在當前最新的 Node.js v17.4.0 版本中,還不支持以 https:// 開頭的說明符。我們可以在自定義加載器中,利用 Node.js 提供的鉤子機制,讓 Node.js 可以使用 import 導入以 https:// 協議開頭的 ES 模塊。

在分析如何自定義 https 資源加載器前,我們需要先介紹一下 import 說明符的概念。

import 說明符

import 語句的說明符是 from 關鍵字之后的字符串,例如 import { sep } from 'path' 中的 'path'。說明符也用于 export from 語句,并作為 import() 表達式的參數。

有三種類型的說明符:

  • 相對說明符,如 './startup.js''../config.mjs'。它們指的是相對于導入文件位置的路徑。對于這種類型,文件擴展名是必須的。

  • 裸說明符,如 'some-package''some-package/shuffle'。它們可以通過包名來引用包的主入口點。當包沒有 exports 字段的時候,才需要包含文件擴展名。

  • 絕對說明符,如 file:///opt/nodejs/config.js。它們直接且明確地引用完整路徑。

裸說明符解析由 Node.js 模塊解析算法處理,所有其他說明符解析始終僅使用標準的相對URL 解析語義進行解析。

和 CommonJS 一樣,包內的模塊文件可以通過在包名上添加路徑來訪問,除非包的 package.json 包含一個 "exports " 字段,在這種情況下,包中的文件只能通過 "exports " 中定義的路徑訪問。

介紹完 import 說明符之后,接下來我們來看一下如何自定義 https 加載器。

自定義 https 加載器

resolve 鉤子

resolve 鉤子用于根據模塊的說明符和 parentURL 生成導入目標的絕對路徑,調用該鉤子后會返回一個包含 format(可選) 和 url 屬性的對象。

//?https-loader.mjs
import?{?get?}?from?'https';export?function?resolve(specifier,?context,?defaultResolve)?{const?{?parentURL?=?null?}?=?context;if?(specifier.startsWith('https://'))?{return?{url:?specifier};}?else?if?(parentURL?&&?parentURL.startsWith('https://'))?{return?{url:?new?URL(specifier,?parentURL).href};}//?讓?Node.js?處理其它的說明符return?defaultResolve(specifier,?context,?defaultResolve);
}

在以上代碼中,會先判斷 specifier 字符串是否以 'https://' 開頭,如果條件滿足的話,該字符串的值直接作為 url 屬性的值,直接返回 { url: specifier } 對象。否則,會判斷 parentURL 是否以 'https://' 開頭,如果條件滿足的話,則會調用 URL 構造函數,創建 URL 對象。

parentURL 是從 context 對象上獲取的,那它什么時候會有值呢?假設在 ES 模塊 A 中,以相對路徑的形式導入 ES 模塊 B。在導入 ES 模塊 B 時,也會調用 resolve 鉤子,此時 context 對象上的 parentURL 就會有值。

load 鉤子

load 鉤子用于定義應該如何解釋、檢索和解析 URL 的方法,調用該方法后,會返回包含 formatsource 屬性的對象。其中 format 屬性值只能是 'builtin''commonjs''json''module''wasm' 中的一種。而 source 屬性值的類型可以為 stringArrayBufferTypedArray

import?{?get?}?from?'https';export?function?load(url,?context,?defaultLoad)?{if?(url.startsWith('https://'))?{return?new?Promise((resolve,?reject)?=>?{get(url,?(res)?=>?{let?data?=?'';res.on('data',?(chunk)?=>?data?+=?chunk);res.on('end',?()?=>?resolve({format:?'module',source:?data,}));}).on('error',?(err)?=>?reject(err));});}//?讓?Node.js?加載其它類型的文件return?defaultLoad(url,?context,?defaultLoad);
}

在以上代碼中,會通過 https 模塊中的 get 函數來加載 https:// 協議的 ES 模塊。如果不是以 'https://' 開頭,則會使用默認的加載器來加載其它類型的文件。

創建完 https-loader 之后,我們來測試一下該加載器。首先創建一個 main.mjs 文件并輸入以下內容:

//?main.mjs
import?React?from?"https://esm.sh/react@17.0.2"console.dir(React);

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

$?node?--experimental-loader?./https-loader.mjs?./main.mjs

當以上命令成功運行之后,控制臺會輸出以下內容:

{Fragment:?Symbol(react.fragment),StrictMode:?Symbol(react.strict_mode),Profiler:?Symbol(react.profiler),Suspense:?Symbol(react.suspense),...
}

了解完以上的內容后,我們回過頭來看一下 esmo.mjs 文件中所使用的 esbuild-node-loader 模塊。下面我們來簡單分析一下 load 鉤子:

//?loader.mjs(esbuild-node-loader?v0.6.4)
export?function?load(url,?context,?defaultLoad)?{if?(extensionsRegex.test(new?URL(url).pathname))?{const?{?format?}?=?context;let?filename?=?url;if?(!isWindows)?filename?=?fileURLToPath(url);const?rawSource?=?fs.readFileSync(new?URL(url),?{?encoding:?"utf8"?});const?{?js?}?=?esbuildTransformSync(rawSource,?filename,?url,?format);return?{format:?"module",source:?js,};}//?Let?Node.js?handle?all?other?format?/?sources.return?defaultLoad(url,?context,?defaultLoad);
}

通過觀察以上代碼,我們可知 load 鉤子的核心處理流程,可以分為兩個步驟:

  • 步驟一:使用 fs.readFileSync 方法讀取文件資源的內容;

  • 步驟二:使用 esbuildTransformSync 函數對源代碼進行轉換。

而在 esbuildTransformSync 函數中,使用了 esbuild 模塊提供的 transformSync 函數來實現代碼的轉換。該函數的相關代碼如下所示:

//?loader.mjs(esbuild-node-loader?v0.6.4)
function?esbuildTransformSync(rawSource,?filename,?url,?format)?{const?{code:?js,warnings,map:?jsSourceMap,}?=?transformSync(rawSource.toString(),?{sourcefile:?filename,sourcemap:?"both",loader:?new?URL(url).pathname.match(extensionsRegex)[1],target:?`node${process.versions.node}`,?format:?format?===?"module"???"esm"?:?"cjs",});//?省略部分代碼return?{?js,?jsSourceMap?};
}

關于 transformSync 函數的使用方式,我就不展開介紹了。感興趣的小伙伴可以自行閱讀一下 esbuild 官網上的相關文檔。

好的,esno 這個項目就介紹到這里。如果你對 Node.js 平臺下的 requireimport hook 機制感興趣的話,可以詳細閱讀一下 pirates、esbuild-register 和 esbuild-node-loader 這幾個項目的源碼。若有遇到問題的話,可以跟阿寶哥交流喲。

參考資源

  • esbuild 官網

  • Node.js 官網 - ESM

  • 如何為 Node.js 的 require 函數添加鉤子?


1e8ee662743715b980da5d02d4d5d2e4.gif

·················?若川簡介?·················

你好,我是若川,畢業于江西高校。現在是一名前端開發“工程師”。寫有《學習源碼整體架構系列》20余篇,在知乎、掘金收獲超百萬閱讀。
從2014年起,每年都會寫一篇年度總結,已經寫了7篇,點擊查看年度總結。
同時,最近組織了源碼共讀活動,幫助3000+前端人學會看源碼。公眾號愿景:幫助5年內前端人走向前列。

dc8ec507fa989a34e1c83274f4c2ec52.png

識別方二維碼加我微信、拉你進源碼共讀

今日話題

略。分享、收藏、點贊、在看我的文章就是對我最大的支持~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/274915.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/274915.shtml
英文地址,請注明出處:http://en.pswp.cn/news/274915.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

c# ui 滾動 分頁_UI備忘單:分頁,無限滾動和“加載更多”按鈕

c# ui 滾動 分頁重點 (Top highlight)When you have a lot of content, you have to rely on one of these three patterns to load it. So, which is best? What will your users like? What do most platforms use? These are the questions we will explore today.當內容…

1.20(設計模式)模板模式

模板模式,定義了一個模板,模板內容通過子類實現模板的抽象方法去添加。 就類似學校需要建一個新校區,新校區有多棟宿舍,找了多個施工方,每個施工方負責一棟宿舍樓。 各個施工方都有自己的想法,建造的宿舍樓…

少年,看你異于常人,有空花2小時來參加有3000人的源碼共讀嘛~

大家好,我是若川。按照從易到難的順序,前面幾期(比如:validate-npm-package-name、axios工具函數)很多都只需要花2-3小時就能看完,并寫好筆記。但收獲確實很大。開闊視野、查漏補缺、升職加薪。已經有400筆…

HDU 3488 KM

http://acm.hdu.edu.cn/showproblem.php?pid3488 依然KM, 可以最小費用流 與HDU1853 差不多,但是1853要判斷是否滿足回路的的條件,KM還不會判回路,所以做1853時學了最小費用流做的,說是學最小費用流 只是皮毛了。。…

Java 面向對象的程序設計(二)

編寫一個java程序,設計一個汽車類Vehicle,包含的屬性有車輪的個數wheels和車重weight。小汽車類Car是Vehicle的子類,包含的屬性有載人數loader。卡車類Truck是Car類的子類,其中包含的屬性有載重量payload。每個類都有構造方法和輸…

16位調色板和32位調色板_使調色板可訪問

16位調色板和32位調色板Accessibility has always been a tough sell. Admittedly, less so than in the ‘nineties, when no prospective client was interested. But even today — more enlightened times — the majority of companies I encounter still prefer to make …

從零開始發布自己的NPM包

大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan02 參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列在Ver…

flash不能訪問本地文件

flash出現"不能訪問本地資源";解決方案 linux下,如果沒有文件夾自行創建 在/home/{user}/.macromedia/Flash_Player/#Security/FlashPlayerTrust下面,隨便建個文本文件,比如1.txt 然后寫入路徑,最省事的辦法直接來個/ 兇…

Jest + React Testing Library 單測總結

大家好,我是若川。持續組織了6個月源碼共讀活動,感興趣的可以點此加我微信 ruochuan02 參與,每周大家一起學習200行左右的源碼,共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列1、背…

不怕神一樣的對手就怕豬一樣的隊友

“不怕神一樣的對手就怕豬一樣的隊友”這句話現在廣為流傳,實際上說的就是團隊重要性,一個好的團隊是可以克服很多你想象不大的困難, 做出你覺得不可能成績。 但是很多時候我們面臨的不是神一樣的對手,而是豬一樣的隊友&#xff0…

著迷英語900句_字體令人著迷

著迷英語900句I’m crazy about fonts. My favorite part of any text editing software is the drop down menu for picking fonts. When I look at any text, I try to identify the font. Roboto is my favorite font.我為字體瘋狂。 在任何文本編輯軟件中,我最喜…

hdu 2188悼念512汶川大地震遇難同胞——選拔志愿者(博弈)

簡單博弈就那樣&#xff0c;懂SG函數就成&#xff0c;最近做的博弈都千篇一律。。。 #include<cstdio> #include<cstring> #define N 11110 int sg[N],s[N],m,n; bool h[N]; void ssgg() {int i,j;sg[0]0;for(i1;i<N;i){ memset(h,0,sizeof(h));for(j1;j<n;j…

推薦一個大佬,文章適合偷偷讀!

大家好&#xff0c;我是若川。周末愉快。也許你看到這篇文章是周一的上午~我不得不推薦一位大佬給你&#xff01;這位大佬的文章很硬&#xff0c;卻一直在「抱怨沒有粉絲&#xff0c;沒人愿意分享」我去讀了讀&#xff0c;尼瑪這個「誰TM敢分享啊」&#xff0c;文章太「違規」了…

PERFORMANCE-MONITORING(轉)

Performance-Monitoring 是Intel提供的可以監測統計CPU內部所產生事件的一組方法。在Intel的手冊上介紹了兩類CPU事件監測方法&#xff1a;architectural performance monitoring 和 non-architectural performance monitoring。Architectural performance monitoring與平臺&am…

ux設計_為企業UX設計更好的數據表

ux設計重點 (Top highlight)If you have worked on enterprise products, you must have noticed the use of lots of data tables. Therefore, I am writing this article to collect the most common use cases and discuss how elegantly we can handle them.如果您使用過企…

hdu1728--------坑爹啊

尼瑪&#xff0c;就因為沒發現‘yes’寫成‘yrs’。整整讓哥找了一個小時的bug。有沒有..........此刻&#xff0c;內流滿面&#xff01; 分析&#xff1a; 開始以為是單純的BFS,結果WA無數次&#xff01;&#xff01; 后來分析后發現是要找到不超過轉向次數的轉向路徑, 最重要…

狼叔直播 Reaction《學習指北:Node.js 2022 全解析》

大家好&#xff0c;我是若川。持續組織了6個月源碼共讀活動&#xff0c;感興趣的可以點此加我微信 ruochuan02 參與&#xff0c;每周大家一起學習200行左右的源碼&#xff0c;共同進步。同時極力推薦訂閱我寫的《學習源碼整體架構系列》 包含20余篇源碼文章。歷史面試系列本文是…

figma下載_Figma中的高級圖像處理

figma下載Figma is not exactly suited for image manipulation, and that’s completely fine. While it does provide an ample amount of tools that let you apply some basic changes to your raster images, for anything more complex you need to look someplace else.…

ToString格式化

在很多對象顯示為字符串的時候都會使用到ToString中的格式化&#xff0c;由于以前沒怎么注意到這個問題&#xff0c;想總結一下各個基礎結構對象的格式化&#xff0c;以便后備之用&#xff01;&#xff01;&#xff01;Int.ToString(format): 格式字符串采用以下形式&#xff1…

xml學習4-dtd

1、DTD元素的定義 <?xml version"1.0" encoding"gb2312"?> <!--*表示0或者多個 表示至少要有一個 ?表示0個或者一個 內容模型 |表示只能包含分隔開中的一個 ,表示序列 下面是DTD元素的聲明 #PCDATA 表示字符數據 EMPTY表示 空元素…