静态导入正在削弱 JavaScript 的同构性

发布: (2026年2月25日 GMT+8 12:51)
5 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的正文内容,我将按照要求保留原始格式、代码块和链接,仅翻译文本部分。

TL;DR

  • 静态导入在模块加载时绑定依赖。
  • 早期绑定会编码平台假设。
  • 声明式依赖将这些决定移至组合根。
  • 这不是新的模块系统;它是标准依赖注入在模块层面的应用。

JavaScript 在浏览器和服务器上都能原生运行,使得真正的同构成为可能。然而,现代 JavaScript 架构常常与之背道而驰。

import fs from "node:fs";

这行代码直接在模块中嵌入了仅 Node 可用的功能。浏览器默认无法满足 "node:fs",因此该模块不再是同构的。

问题不在于 fs;问题在于 早期绑定。静态导入在模块求值期间解析依赖,在代码运行之前就固定了依赖图。如果某个依赖是平台特定的,那么该模块也会变成平台特定的。

明确依赖关系

// user-service.mjs
export const __deps__ = {
  fs: "node:fs",
  logger: "./logger.mjs",
};

export default function makeUserService({ fs, logger }) {
  return {
    readUserJson(path) {
      const raw = fs.readFileSync(path, "utf8");
      logger.log(`Read ${raw.length} bytes`);
      return JSON.parse(raw);
    },
  };
}

该模块不直接导入任何内容。它声明了一个依赖契约,并从外部接收具体实现。这是在模块层面上使用的依赖注入;组合根(composition root)决定传入什么实现。

手动组合根

Node

// node-entry.mjs
import fs from "node:fs";
import logger from "./logger.mjs";
import makeUserService from "./user-service.mjs";

const service = makeUserService({ fs, logger });

浏览器

// browser-entry.mjs
import fsAdapter from "./browser-fs-adapter.mjs";
import logger from "./logger.mjs";
import makeUserService from "./user-service.mjs";

const service = makeUserService({
  fs: fsAdapter,
  logger,
});

模块本身没有改变;只有组合根发生变化。平台决策保持在系统的边缘,并且由于依赖被显式注入,测试可以直接传入假实现,而不需要对模块导入进行模拟。

自动化组合

因为合约通过 __deps__ 暴露,组合根可以变为数据驱动:

// link.mjs
export async function link(entrySpecifier, overrides = {}) {
  const mod = await import(entrySpecifier);
  const depsSpec = mod.__deps__ ?? {};
  const deps = {};

  for (const [name, specifier] of Object.entries(depsSpec)) {
    const finalSpecifier = overrides[specifier] ?? specifier;
    const imported = await import(finalSpecifier);
    deps[name] = imported.default ?? imported;
  }

  return mod.default(deps);
}

Node

const service = await link("./user-service.mjs");

Browser

const service = await link("./user-service.mjs", {
  "node:fs": "./browser-fs-adapter.mjs",
});

绑定变为显式的程序逻辑,而不是加载器的副作用。

这与 import maps 和 exports 的区别

机制它控制的内容
Import maps加载时的标识符解析(主机层面)。
package.json exports每个环境的入口点(包层面)。
Bundlers构建时的依赖图优化。
Composition root + DI模块在运行时接收的具体能力(应用层面)。
  • Import maps 的答案是:这个模块在哪里?
  • Composition root 的答案是:这个模块获得了什么能力?

不同层次,关注点不同。

权衡

  • 静态可分析性和 tree‑shaking 精度降低。
  • TypeScript 集成变得更手动。
  • 对于小型或纯单运行时的应用来说是多余的。
  • 引入了架构纪律(组合根)。

这是一种工具,而非默认选项。

何时使用它

使用场景:

  • 您需要真正的跨运行时模块(Node、浏览器、Edge)。
  • 您希望将环境决策集中管理。
  • 可测试性很重要且不想进行大量模拟。
  • 您倾向于明确的能力边界。

不适用场景:

  • 您的应用只针对单一运行时。
  • 构建时优化和树摇(tree‑shaking)是主要关注点。
  • 简单性胜过架构灵活性。

静态导入并没有错——它们高效且符合惯例。但它们在早期绑定,隐含平台假设。如果我们在意保持 JavaScript 的同构性,就应当有意识地决定绑定发生的时机。

0 浏览
Back to Blog

相关文章

阅读更多 »

三层响应式电子商务页眉

封面图片(Triple-Tier Responsive E-commerce Header) https://media2.dev.to/dynamic/image/width=1000,height=420,fit=cover,gravity=auto,format=auto/https%3A%2...

Algorhymer的故事:寻找Nessie

又一本带原声带的 Computer Science flipbook!上次我们做了一个带 cold opening 的 time traveling Action Movie https://dev.to/algorhymer/tales-of-the-al