React Router 架构决策解读:用 npm 管理 Deno 项目的 NPM 依赖
本篇解读 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.md 与 decisions/0007-remix-on-react-router-6-4-0.md 记录了 Remix 能力并入 React Router 的过程,而本文所讲的依赖策略正是那段时期的框架级工程决策。它的结论(NPM 依赖走 npm、Deno 原生模块走 URL 导入)为所有在 Deno 运行时使用 React 生态的部署形态划定了依赖边界。
背景:Deno 的三种依赖管理方式
决策文档的 Context 部分首先列出 Deno 管理依赖的三条既有路径:
- 内联 URL 导入:直接在源码中写
import {...} from "https://deno.land/x/blah",模块地址即 import 语句的一部分; - deps.ts 模式:用一个
deps.ts文件集中转述第三方模块再被业务代码引用(Deno 官方手册中推荐的手动依赖管理方式); - 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 包,最典型的是
react与react-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.ts 中 vite.resolveConfig 的调用),延续了"构建/运行时环境统一显式可控"的工程取向,与决策文档中"环境必须可统一设置"的要求一脉相承。
环境切换:CDN 用查询参数而非环境变量
Deno-friendly CDN 通过 URL 查询参数(例如 ?dev)而非环境变量来区分 dev/prod/test 构建。这意味着切换环境就必须修改源码里的 import URL。文档分析了用多份 import map(dev.json、prod.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 项目一样,通过 npm 和 node_modules/ 来管理。
而 Deno 原生模块(例如来自 https://deno.land 的依赖)仍然可以继续使用 URL 导入管理。换言之,决策划出了一条清晰边界:NPM 生态走 node_modules/,Deno 生态走 URL。
这一边界在当前仓库中仍有对应痕迹:脚手架包 packages/create-react-router/index.ts 中的 validPackageManagers 列表包含 deno,detectPackageManager 通过 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;依赖管理走 npm 与 node_modules/;Deno 专属依赖仍可直接用 URL 导入(不经过 import map)。settings.json 中的 deno.importMap 指向该文件,从而让编辑器把 "react" 等裸说明符解析到 CDN 上的类型声明来源,恢复智能提示能力。
小结:这条决策边界为何仍然成立
把整份决策记录串起来看,它实际上回答了一个常见困惑——"Deno 都能直接 import URL 了,为什么还要 node_modules?"。答案浓缩为三点判据:
- 打包器语义:URL 导入没有
sideEffects之类的标准元数据,tree-shaking 无从谈起(仓库自身以 packages/react-router/package.json 的"sideEffects": false声明了对打包器元数据的依赖); - 环境语义:CDN 以查询参数编码 dev/prod,破坏了"用
NODE_ENV一处切换全局环境"的模型; - 依赖图语义:CDN 孤立编译使 peer dependency 的版本对齐退化为手工劳动。
因此最终方案是一个务实的双轨制:NPM 生态(react 等)走 npm,Deno 生态走 URL 导入,import map 既不做运行时机制也不做构建机制,只在编辑器配置里作为类型提示的"影子"存在。这条 ADR 虽写于 Remix 时期,但其确立的依赖边界与 decisions/ 目录下其他决策一起,构成了当前 React Router 在 Deno 部署形态下的依赖管理基线,供使用 deno 作为包管理器(如 packages/create-react-router/index.ts 所识别的形态)的读者作为工程判断的参照。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00