自定义 ESM 加载器:谁、什么、何时、何地、为什么、如何

2025-06-07

自定义 ESM 加载器:谁、什么、何时、何地、为什么、如何

大多数人可能不会编写自己的自定义 ESM 加载器,但使用它们可以大大简化您的工作流程。

自定义加载器是一种强大的应用程序控制机制,它提供了对模块加载的全面控制——无论是数据、文件还是其他什么。本文列举了一些实际用例。最终用户可能会通过包来使用这些加载器,但了解这些内容仍然很有用,而且进行一次简单的小规模加载非常容易,只需很少的努力就能省去很多麻烦(我见过/写过的大多数加载器大约只有 20 行代码,甚至更少)。

对于黄金时段的使用,多个 loader 会以“链接”的方式协同工作;它的工作方式类似于 Promise 链(因为它字面意思就是 Promise 链)。loader 是通过命令行以相反的顺序添加的,遵循其前身的模式--require

$> node --loader third.mjs --loader second.mjs --loader first.mjs app.mjs
Enter fullscreen mode Exit fullscreen mode

node内部处理这些加载器,然后开始加载应用程序(app.mjs)。在加载应用程序时,node会调用加载器:first.mjs,然后second.mjs,然后third.mjs。这些加载器可以完全改变该过程中的所有内容,从重定向到完全不同的文件(即使在网络上的不同设备上),或者悄悄地提供修改过的或完全不同的文件内容。

在一个虚构的例子中:

$> node --loader redirect.mjs app.mjs
Enter fullscreen mode Exit fullscreen mode
// 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);
}
Enter fullscreen mode Exit fullscreen mode

这将导致根据环境node动态加载app.dev.mjsapp.test.mjs或(而不是)。app.prod.mjsapp.mjs

但是,以下内容提供了更强大和实用的用例:

$> node \
   --loader typescript-loader \
   --loader css-loader \
   --loader network-loader \
   app.tsx
Enter fullscreen mode Exit fullscreen mode
// 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>
);
Enter fullscreen mode Exit fullscreen mode

以上列出了不少需要解决的问题。在 loader 出现之前,人们可能会使用基于 Node.js 的 Webpack。但现在,人们可以node直接利用它实时处理所有这些问题。

TypeScript

首先是app.tsxTypeScript 文件:node无法理解 TypeScript。TypeScript 带来了许多挑战,第一个是最简单也是最常见的:转换为 JavaScript。第二个是一个令人讨厌的问题:TypeScript 要求导入说明符“说谎”,指向不存在的文件。node当然无法加载不存在的文件,所以你需要知道node如何检测“说谎”并找到真相。

您有以下几种选择:

  • 别撒谎。使用.tsetc 扩展,并在你编写的加载器中使用类似esbuild的工具,或者使用像ts-node/esm这样的现成加载器来转译输出。除了正确性之外,这种方法还能显著提高性能。这是 Node.js 推荐的方法。

注意:类型检查期间tsc很快就会支持文件扩展名: TypeScript#37582,所以希望您能够鱼与熊掌兼得。.ts

  • 使用错误的文件扩展名并猜测(这将导致性能下降并可能出现错误)。

由于 TypeScript 中的设计决策,遗憾的是这两个选项都存在缺点。

如果您想编写自己的 TypeScript 加载器,Node.js Loaders 团队已经提供了一个简单的示例:nodejs/loaders-test/typescript-loaderts-node/esm可能更适合您。

CSS

node它也不支持 CSS,所以需要一个加载器(css-loader见上文)来将其解析成类似 JSON 的结构。我最常在运行测试时使用这种方法,因为测试中样式本身通常并不重要(只需要 CSS 类名)。因此,我使用的加载器只是将类名暴露为简单的匹配键值对。我发现,只要 UI 不实际绘制,这种方法就足够了:

.Container {
  border: 1px solid black;
}

.SomeInnerPiece {
  background-color: blue;
}
Enter fullscreen mode Exit fullscreen mode
import styles from './MyComponent.module.css' assert { type: 'css' };
// { Container: 'Container', SomeInnerPiece: 'SomeInnerPiece' }

const MyComponent () => (<div className={styles.Container} />);
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

截至本周, 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:…": "…"
  }
}
Enter fullscreen mode Exit fullscreen mode

您可以在这里的开源项目中看到类似的工作示例:JakobJingleheimer/react-form5

对于需要长期使用的东西(例如用于本地开发的开发服务器),类似esbuilds 的东西serve可能更适合。如果你热衷于使用自定义加载器,你还需要以下几个部分:

  • 一个简单的 http 服务器(JavaScript 模块需要它)在请求的模块上使用动态导入。
  • 一个缓存破坏自定义加载器(用于源代码更改时),例如quibble (在这里发布了一篇关于它的解释性文章)。

总而言之,自定义加载器非常实用。不妨在今天的 Node.js v18.6.0 版本中试用一下!

文章来源:https://dev.to/jakobjingleheimer/custom-esm-loaders-who-what-when-where-why-how-4i1o
PREV
use create-react-app to develop Chrome extensions Step 1: ⚛ create a react app Step 2: Modify public/manifest.json Step 3: 🏗 Structure Step 4: 🎩 The magic Step 5: 🎉 Run & enjoy Source code Final thoughts
NEXT
2024 年搭建网站的理想技术栈是什么?👨‍💻