又一个基于 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.mzph.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.当内容…

少年,看你异于常人,有空花2小时来参加有3000人的源码共读嘛~

大家好,我是若川。按照从易到难的顺序,前面几期(比如:validate-npm-package-name、axios工具函数)很多都只需要花2-3小时就能看完,并写好笔记。但收获确实很大。开阔视野、查漏补缺、升职加薪。已经有400笔…

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…

Jest + React Testing Library 单测总结

大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列1、背…

着迷英语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.我为字体疯狂。 在任何文本编辑软件中,我最喜…

推荐一个大佬,文章适合偷偷读!

大家好,我是若川。周末愉快。也许你看到这篇文章是周一的上午~我不得不推荐一位大佬给你!这位大佬的文章很硬,却一直在「抱怨没有粉丝,没人愿意分享」我去读了读,尼玛这个「谁TM敢分享啊」,文章太「违规」了…

PERFORMANCE-MONITORING(转)

Performance-Monitoring 是Intel提供的可以监测统计CPU内部所产生事件的一组方法。在Intel的手册上介绍了两类CPU事件监测方法: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.如果您使用过企…

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

大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan02 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含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.…

指针和指针的指针_网络上的iPad指针

指针和指针的指针a week ago I saw a new IPad Pointer presentation and was very excited about what they did. It was very interesting to see how they design different pointer modes and attention to details. Here is the presentation:一周前,我看到了一…

Vue 是如何用 Rollup 打包的?

大家好,我是若川。持续组织了6个月源码共读活动,感兴趣的可以点此加我微信 ruochuan12 参与,每周大家一起学习200行左右的源码,共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列Rollu…

leetcode 207课程表

class Solution { public:bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {//验证是否为DAG&#xff0c;每次验证指向的是否已经存在于当前图中//建图vector<int> indegree(numCourses,0);//入度vector<vector<int>> …

sketch怎么传到ps_2020年从Sketch移植到Figma的详细指南

sketch怎么传到psAs we’re locked up in our homes due to COVID-19 pandemic, many of us are working remotely and Figma is a go-to tool for designers for the same.由于COVID-19流行病使我们被关在家里&#xff0c;我们中的许多人都在远程工作&#xff0c;而Figma是设计…

还没搭建过Vue3.x项目?几行代码搞定~

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列相信现…

一步步创建 边栏 Gadget(二)

相信使用上篇中创建的边栏Gadget之后&#xff0c;大家会很郁闷。难道视频窗口就那么小吗&#xff1f;看起来真费劲。我能通过该Gadget看着一部电视剧。而不能够定制自己需要的或者想要看的电视剧。 在上一篇一步步创建 边栏 Gadget&#xff08;一&#xff09;中&#xff0c;我们…

tableau 自定义图表_一种新的十六进制美国地图布局的案例-Tableau中的自定义图表

tableau 自定义图表For whatever reason, 无论出于什么原因 maps are cool. Even though the earth has mostly been the same since those 地图很酷 。 即使自Pangaea days, we humans make and remake maps constantly. It might be that old maps remind us of how things …

2022,前端工具链十年盘点

大家好&#xff0c;我是若川。持续组织了6个月源码共读活动&#xff0c;感兴趣的可以点此加我微信 ruochuan12 参与&#xff0c;每周大家一起学习200行左右的源码&#xff0c;共同进步。同时极力推荐订阅我写的《学习源码整体架构系列》 包含20余篇源码文章。历史面试系列2021 …

书籍排版学习心得_为什么排版是您可以学习的最佳技能

书籍排版学习心得重点 (Top highlight)I was introduced to design in a serpentine fashion. I don’t have any formal training. Instead, I’ve learned everything through the Web, books, and by interacting with designers daily.我被介绍为蛇形设计。 我没有任何正规…