首页
/ React Router 架构决策解读:用 npm 管理 Deno 项目的 NPM 依赖

React Router 架构决策解读:用 npm 管理 Deno 项目的 NPM 依赖

2026-09-06 13:44:42作者:盛欣凯Ernestine

本篇解读 React Router 仓库中一份已接受(accepted)的架构决策记录:《Use npm to manage NPM dependencies for Deno projects》。文章围绕这份 决策文档 的完整脉络展开——Deno 的三种依赖管理方式、项目对依赖体系的三项硬性要求(Tree-shaking、运行时环境切换、peer dependencies)、各方案的缺陷分析,以及最终"以 npm + node_modules 管理 NPM 依赖、保留 URL 导入、放弃 import maps"的决策细节与配套编辑器配置,并结合当前仓库源码验证其决策逻辑在今日代码中的延续。读完本文,你能理解为什么"看起来更现代的 URL 导入"不适合承载 react 这类 peer 依赖,并掌握在 Deno 编辑器中为 npm 依赖恢复类型提示的完整配置方法。

决策记录(ADR)在 React Router 仓库中的位置

React Router 仓库在 decisions/ 目录下维护了一套架构决策记录(Architecture Decision Record),每份记录遵循 decisions/template.md 定义的统一结构:标题、日期、状态(proposed / rejected / accepted / deprecated / superseded),以及 Context(背景)、Decision(决策)、Consequences(后果)三节。本文解读的是其中 2022-05-10 记录、状态为 accepted 的 decisions/0001-use-npm-to-manage-npm-dependencies-for-deno-projects.md

从仓库的决策编年看,这份 ADR 成文于项目以 Remix 为主体的时期——后续的 decisions/0005-remixing-react-router.mddecisions/0007-remix-on-react-router-6-4-0.md 记录了 Remix 能力并入 React Router 的过程,而本文所讲的依赖策略正是那段时期的框架级工程决策。它的结论(NPM 依赖走 npm、Deno 原生模块走 URL 导入)为所有在 Deno 运行时使用 React 生态的部署形态划定了依赖边界。

背景:Deno 的三种依赖管理方式

决策文档的 Context 部分首先列出 Deno 管理依赖的三条既有路径:

  1. 内联 URL 导入:直接在源码中写 import {...} from "https://deno.land/x/blah",模块地址即 import 语句的一部分;
  2. deps.ts 模式:用一个 deps.ts 文件集中转述第三方模块再被业务代码引用(Deno 官方手册中推荐的手动依赖管理方式);
  3. Import maps:以 JSON 映射表把裸说明符(如 "react")重定向到 URL。

除此之外,NPM 包还可以通过 Deno-friendly CDN(如 esm.sh 这类按 ESM 编译 NPM 包的 CDN)以 Deno 模块的形式访问。

框架对依赖体系的三项硬性要求

文档指出,Remix(React Router 的前身)对依赖管理有三条硬性要求,这构成了评估上述各方案的判据:

  • Tree-shaking:框架会对无副作用的依赖做树摇,以优化包体积并区分浏览器端与服务端代码;
  • 运行时环境:框架在运行时通过 NODE_ENV 环境变量统一设置 dev/prod/test 环境,且该设置必须覆盖依赖代码;
  • Peer dependencies:框架依赖若干必须声明为 peer dependency 的 NPM 包,最典型的是 reactreact-dom

Tree-shaking:URL 导入缺少副作用标记机制

为优化 bundle 体积,框架会 tree-shake 用户代码及其依赖。从源码结构看,这一要求的实现基础就在仓库中:packages/react-router/package.json 明确声明了 "sideEffects": false

决策文档对此的技术解释是:底层编译器(当时为 esbuild,与其他打包器一样)依赖 package.json 中的 sideEffects 字段来判断"删除未使用 import 是否安全"。而 URL 导入没有标准机制来标记一个包是 side-effect free 的——没有 package.json,就没有可供打包器消费的 sideEffects 元数据。这就是 URL 导入方案在"Tree-shaking"这一要求上先天不合格的原因。

当前仓库中 Vite 插件在解析配置时同样以 production 作为默认 mode 与默认 NODE_ENV(参见 packages/react-router-dev/vite/plugin.tsvite.resolveConfig 的调用),延续了"构建/运行时环境统一显式可控"的工程取向,与决策文档中"环境必须可统一设置"的要求一脉相承。

环境切换:CDN 用查询参数而非环境变量

Deno-friendly CDN 通过 URL 查询参数(例如 ?dev)而非环境变量来区分 dev/prod/test 构建。这意味着切换环境就必须修改源码里的 import URL。文档分析了用多份 import map(dev.jsonprod.json 等)绕行的思路,但指出 import maps 还有两个结构性限制:

  • 管理 import maps 的标准化工具链不可用;
  • import maps 不可组合:任何自身使用 import map 的依赖都必须由用户手工逐一登记,无法自动传递。

Peer dependencies:CDN 的孤立编译使版本对齐成为噩梦

即便把 import maps 做到完美,CDN 也是逐个孤立地编译每个依赖。指定 peer dependency 因此变得繁琐且易错,用户必须:

  • 判断哪些依赖(哪怕只是间接地)依赖 react 之类的 peer dependency;
  • 人工推导出一个能在所有这些依赖间工作的 react 版本;
  • 每一个被识别依赖的 URL 中,把该版本以查询参数形式设置上去。

一旦依赖集合发生任何变化(新增、删除、版本升级),上述全部步骤必须重做。这正是 URL + CDN 方案在"Peer dependencies"要求上的致命伤。

决策内容:三条子决策

文档的 Decision 部分由三条明确的子决策组成:

1. 用 npm 管理 Deno 项目的 NPM 依赖

不要在 Deno 项目的 Remix/React Router 项目中使用 Deno-friendly CDN 来承载 NPM 依赖。即使在 Deno 环境下运行,react 这类 NPM 依赖也应像常规 NPM 项目一样,通过 npmnode_modules/ 来管理。

而 Deno 原生模块(例如来自 https://deno.land 的依赖)仍然可以继续使用 URL 导入管理。换言之,决策划出了一条清晰边界:NPM 生态走 node_modules/,Deno 生态走 URL。

这一边界在当前仓库中仍有对应痕迹:脚手架包 packages/create-react-router/index.ts 中的 validPackageManagers 列表包含 denodetectPackageManager 通过 npm_config_user_agent 环境变量识别运行 create-react-router 的包管理器——当用户以 Deno 安装 CLI 时会被识别为 deno 包管理器。这印证了"项目级工具链与依赖管理可以并存于不同机制"的设计前提。

2. 允许 URL 导入:打包器将其保留为外部依赖

框架在构建产物中原样保留任何 URL 导入,将其视为外部依赖,交给浏览器运行时或服务端运行时自行处理。由此得到的能力是:

  • 可以在浏览器端使用 URL 导入;
  • 在服务端使用 URL 导入——前提是服务端运行时支持它。

文档给出的例子是:Node 对 URL 导入会抛错,而 Deno 会像处理普通模块一样解析 URL 导入。这一"打包器不干预、运行时自负"的策略,把 Deno 原生模块的可用性完全交给运行时能力,避免了框架替用户做不可靠的 URL 解析假设。

3. 不支持 import maps

Remix/React Router 不会支持 import maps。这是基于上一节三个结构性缺陷(无标准工具链、不可组合、无法承载副作用与环境语义)作出的明确取舍。

决策后果(Consequences)

文档的 Consequences 部分列出了三条直接后果:

  • URL 导入不会被 tree-shake——因为如前所述,没有 sideEffects 元数据可供打包器使用;
  • 用户可以在运行时通过 NODE_ENV 环境变量指定环境,不再需要改 import URL;
  • 用户不再需要做易错的、手工的依赖版本协调,peer dependency 的版本解析回归 npm 的常规机制。

编辑器配套:用 VS Code 导入 map 恢复类型提示

由于运行时的 import map 被放弃,Deno 编辑器(VS Code 的 Deno 扩展)无法自动为 node_modules/ 中的 NPM 依赖提供类型提示。文档给出的解法是:仅针对 Deno 扩展配置一份 import map,仅用于编辑器的解析,不参与构建。

.vscode/resolve_npm_imports_in_deno.json

{
  "// This import map is used solely for the denoland.vscode-deno extension.": "",
  "// Remix does not support import maps.": "",
  "// Dependency management is done through `npm` and `node_modules/` instead.": "",
  "// Deno-only dependencies may be imported via URL imports (without using import maps).": "",

  "imports": {
    "react": "https://esm.sh/react@18.0.0",
    "react-dom": "https://esm.sh/react-dom@18.0.0",
    "react-dom/server": "https://esm.sh/react-dom@18.0.0/server"
  }
}

.vscode/settings.json

{
  "deno.enable": true,
  "deno.importMap": "./.vscode/resolve_npm_imports_in_deno.json"
}

配置文件里以 "// ..." 开头的键值对是 JSON 里模拟注释的惯用写法,其语义值得注意:这份 import map 仅供 denoland.vscode-deno 扩展使用;框架本身不支持 import maps;依赖管理走 npmnode_modules/;Deno 专属依赖仍可直接用 URL 导入(不经过 import map)。settings.json 中的 deno.importMap 指向该文件,从而让编辑器把 "react" 等裸说明符解析到 CDN 上的类型声明来源,恢复智能提示能力。

小结:这条决策边界为何仍然成立

把整份决策记录串起来看,它实际上回答了一个常见困惑——"Deno 都能直接 import URL 了,为什么还要 node_modules?"。答案浓缩为三点判据:

  1. 打包器语义:URL 导入没有 sideEffects 之类的标准元数据,tree-shaking 无从谈起(仓库自身以 packages/react-router/package.json"sideEffects": false 声明了对打包器元数据的依赖);
  2. 环境语义:CDN 以查询参数编码 dev/prod,破坏了"用 NODE_ENV 一处切换全局环境"的模型;
  3. 依赖图语义:CDN 孤立编译使 peer dependency 的版本对齐退化为手工劳动。

因此最终方案是一个务实的双轨制:NPM 生态(react 等)走 npm,Deno 生态走 URL 导入,import map 既不做运行时机制也不做构建机制,只在编辑器配置里作为类型提示的"影子"存在。这条 ADR 虽写于 Remix 时期,但其确立的依赖边界与 decisions/ 目录下其他决策一起,构成了当前 React Router 在 Deno 部署形态下的依赖管理基线,供使用 deno 作为包管理器(如 packages/create-react-router/index.ts 所识别的形态)的读者作为工程判断的参照。

登录后查看全文
热门项目推荐
相关项目推荐