自定义 ESM 加载器:谁、什么、何时、何地、为什么、如何
大多数人可能不会编写自己的自定义 ESM 加载器,但使用它们可以大大简化您的工作流程。
自定义加载器是一种强大的应用程序控制机制,它提供了对模块加载的全面控制——无论是数据、文件还是其他什么。本文列举了一些实际用例。最终用户可能会通过包来使用这些加载器,但了解这些内容仍然很有用,而且进行一次简单的小规模加载非常容易,只需很少的努力就能省去很多麻烦(我见过/写过的大多数加载器大约只有 20 行代码,甚至更少)。
对于黄金时段的使用,多个 loader 会以“链接”的方式协同工作;它的工作方式类似于 Promise 链(因为它字面意思就是 Promise 链)。loader 是通过命令行以相反的顺序添加的,遵循其前身的模式--require
:
$> node --loader third.mjs --loader second.mjs --loader first.mjs app.mjs
node
内部处理这些加载器,然后开始加载应用程序(app.mjs
)。在加载应用程序时,node
会调用加载器:first.mjs
,然后second.mjs
,然后third.mjs
。这些加载器可以完全改变该过程中的所有内容,从重定向到完全不同的文件(即使在网络上的不同设备上),或者悄悄地提供修改过的或完全不同的文件内容。
在一个虚构的例子中:
$> node --loader redirect.mjs app.mjs
// redirect.mjs
export function resolve(specifier, context, nextResolve) {
let redirect = 'app.prod.mjs';
switch(process.env.NODE_ENV) {
case 'development':
redirect = 'app.dev.mjs';
break;
case 'test':
redirect = 'app.test.mjs';
break;
}
return nextResolve(redirect);
}
这将导致根据环境node
动态加载app.dev.mjs
、app.test.mjs
或(而不是)。app.prod.mjs
app.mjs
但是,以下内容提供了更强大和实用的用例:
$> node \
--loader typescript-loader \
--loader css-loader \
--loader network-loader \
app.tsx
// app.tsx
import ReactDOM from 'react-dom/client';
import {
BrowserRouter,
useRoutes,
} from 'react-router-dom';
import AppHeader from './AppHeader.tsx';
import AppFooter from './AppFooter.tsx';
import routes from 'https://example.com/routes.json' assert { type: 'json' };
import './global.css' assert { type: 'css' };
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<AppHeader />
<main>{useRoutes(routes)}</main>
<AppFooter />
</BrowserRouter>
);
以上列出了不少需要解决的问题。在 loader 出现之前,人们可能会使用基于 Node.js 的 Webpack。但现在,人们可以node
直接利用它实时处理所有这些问题。
TypeScript
首先是app.tsx
TypeScript 文件:node
无法理解 TypeScript。TypeScript 带来了许多挑战,第一个是最简单也是最常见的:转换为 JavaScript。第二个是一个令人讨厌的问题:TypeScript 要求导入说明符“说谎”,指向不存在的文件。node
当然无法加载不存在的文件,所以你需要知道node
如何检测“说谎”并找到真相。
您有以下几种选择:
- 别撒谎。使用
.ts
etc 扩展,并在你编写的加载器中使用类似esbuild的工具,或者使用像ts-node/esm这样的现成加载器来转译输出。除了正确性之外,这种方法还能显著提高性能。这是 Node.js 推荐的方法。
注意:类型检查期间tsc
很快就会支持文件扩展名: TypeScript#37582,所以希望您能够鱼与熊掌兼得。.ts
- 使用错误的文件扩展名并猜测(这将导致性能下降并可能出现错误)。
由于 TypeScript 中的设计决策,遗憾的是这两个选项都存在缺点。
如果您想编写自己的 TypeScript 加载器,Node.js Loaders 团队已经提供了一个简单的示例:nodejs/loaders-test/typescript-loader。ts-node/esm
可能更适合您。
CSS
node
它也不支持 CSS,所以需要一个加载器(css-loader
见上文)来将其解析成类似 JSON 的结构。我最常在运行测试时使用这种方法,因为测试中样式本身通常并不重要(只需要 CSS 类名)。因此,我使用的加载器只是将类名暴露为简单的匹配键值对。我发现,只要 UI 不实际绘制,这种方法就足够了:
.Container {
border: 1px solid black;
}
.SomeInnerPiece {
background-color: blue;
}
import styles from './MyComponent.module.css' assert { type: 'css' };
// { Container: 'Container', SomeInnerPiece: 'SomeInnerPiece' }
const MyComponent () => (<div className={styles.Container} />);
css-loader
这里有一个快速而简单的示例: JakobJingleheimer/demo-css-loader。
类似 Jest 的快照或类似的使用类名的方法运行良好,并且能够反映真实的输出。如果您在 JavaScript 中操作样式,则需要一个更健壮的解决方案(这仍然非常可行);然而,这可能不是最佳选择。根据您正在执行的操作,CSS 变量可能更好(并且完全不涉及操作样式)。
远程数据(文件)
node
目前尚不完全支持通过网络加载模块(目前有实验性的支持,但有意限制)。可以使用加载器(network-loader
见上文)来实现。Node.js 加载器团队已经整理了一个基本示例:nodejs/loaders-test/https-loader。
现在大家一起
如果您有一个“一次性”任务需要完成,例如编译应用程序以运行测试,那么您所需要的就是:
$> NODE_ENV=test \
NODE_OPTIONS='--loader typescript-loader --loader css-loader --loader network-loader' \
mocha \
--extension '.spec.js' \
'./src'
截至本周, Orbiit.ai团队已将此技术应用于其开发流程,测试运行速度提升近 800%。他们的新设置尚未完善,无法分享前后对比指标和一些精美的截图,但一旦完成,我会立即更新本文。
// package.json
{
"scripts": {
"test": "concurrently --kill-others-on-fail npm:test:*",
"test:types": "tsc --noEmit",
"test:unit": "NODE_ENV=test NODE_OPTIONS='…' mocha --extension '…' './src'",
"test:…": "…"
}
}
您可以在这里的开源项目中看到类似的工作示例:JakobJingleheimer/react-form5。
对于需要长期使用的东西(例如用于本地开发的开发服务器),类似esbuild
s 的东西serve
可能更适合。如果你热衷于使用自定义加载器,你还需要以下几个部分:
- 一个简单的 http 服务器(JavaScript 模块需要它)在请求的模块上使用动态导入。
- 一个缓存破坏自定义加载器(用于源代码更改时),例如quibble (在这里发布了一篇关于它的解释性文章)。
总而言之,自定义加载器非常实用。不妨在今天的 Node.js v18.6.0 版本中试用一下!
文章来源:https://dev.to/jakobjingleheimer/custom-esm-loaders-who-what-when-where-why-how-4i1o