<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Vite on 飞天造物手记</title><link>https://roper.cool/tags/vite/</link><description>Recent content in Vite on 飞天造物手记</description><generator>Hugo -- gohugo.io</generator><language>zh-cn</language><lastBuildDate>Tue, 19 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://roper.cool/tags/vite/index.xml" rel="self" type="application/rss+xml"/><item><title>在 Monorepo 里统一 TypeScript 路径别名：一次 Vite、TS Server 与测试工具的对齐记录</title><link>https://roper.cool/posts/vite-alias-and-monorepo/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://roper.cool/posts/vite-alias-and-monorepo/</guid><description>&lt;p&gt;最近整理一个前端 Monorepo 时，我又踩到了那个很典型的问题：编辑器里 &lt;code&gt;@shared/utils&lt;/code&gt; 能正常跳转，类型提示也没问题，但一到构建阶段，Vite 就报模块找不到。更糟糕的是，测试环境和本地开发环境表现还不一致。&lt;/p&gt;
&lt;p&gt;这种问题的根源通常不是某一个工具配置错了，而是“我们以为只有一个别名系统，实际上每个工具都在各自解析路径”。&lt;/p&gt;
&lt;h2 id="1-typescript-的-paths-只解决类型系统不自动影响运行时"&gt;&lt;a href="#1-typescript-%e7%9a%84-paths-%e5%8f%aa%e8%a7%a3%e5%86%b3%e7%b1%bb%e5%9e%8b%e7%b3%bb%e7%bb%9f%e4%b8%8d%e8%87%aa%e5%8a%a8%e5%bd%b1%e5%93%8d%e8%bf%90%e8%a1%8c%e6%97%b6" class="header-anchor"&gt;&lt;/a&gt;1. TypeScript 的 paths 只解决类型系统，不自动影响运行时
&lt;/h2&gt;&lt;p&gt;这是最容易被忽略的点。&lt;code&gt;tsconfig.json&lt;/code&gt; 里的 &lt;code&gt;compilerOptions.paths&lt;/code&gt; 本质上是告诉 TypeScript：&lt;/p&gt;
&lt;p&gt;“当你做类型检查和模块解析推断时，可以把这个别名映射到这些目录。”&lt;/p&gt;
&lt;p&gt;它并不会自动修改 Vite、Node.js、Vitest、Jest、ESLint 或打包产物里的真实导入路径。&lt;/p&gt;
&lt;p&gt;所以当我看到编辑器一切正常时，我一度误以为别名已经配置完成。实际上只是 TS Server 帮我“看懂了”，真正运行代码的工具链并没有同步知道这层映射。&lt;/p&gt;
&lt;h2 id="2-monorepo-场景更容易失配"&gt;&lt;a href="#2-monorepo-%e5%9c%ba%e6%99%af%e6%9b%b4%e5%ae%b9%e6%98%93%e5%a4%b1%e9%85%8d" class="header-anchor"&gt;&lt;/a&gt;2. Monorepo 场景更容易失配
&lt;/h2&gt;&lt;p&gt;单仓库多包结构下，问题会进一步放大，因为常见目录形态是这样的：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;apps/web&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apps/admin&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/shared&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;packages/ui&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;如果根目录有一份 &lt;code&gt;tsconfig.base.json&lt;/code&gt;，子应用又各自 &lt;code&gt;extends&lt;/code&gt; 它，那么路径别名经常在“类型层面共享成功”，但具体打包器是在每个应用目录启动的。此时工具解析根相对路径、工作目录、软链接包、源码入口都可能产生分歧。&lt;/p&gt;
&lt;p&gt;我后来统一的原则是：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根层定义类型语义。&lt;/li&gt;
&lt;li&gt;每个运行时工具都显式继承或读取同一份映射。&lt;/li&gt;
&lt;li&gt;优先让包通过标准导出工作，路径别名只作为开发体验增强，而不是跨包依赖的唯一方式。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="3-先问自己你是在做包依赖还是源码别名"&gt;&lt;a href="#3-%e5%85%88%e9%97%ae%e8%87%aa%e5%b7%b1%e4%bd%a0%e6%98%af%e5%9c%a8%e5%81%9a%e5%8c%85%e4%be%9d%e8%b5%96%e8%bf%98%e6%98%af%e6%ba%90%e7%a0%81%e5%88%ab%e5%90%8d" class="header-anchor"&gt;&lt;/a&gt;3. 先问自己：你是在做“包依赖”还是“源码别名”？
&lt;/h2&gt;&lt;p&gt;这一步非常关键。我把依赖关系分成两类：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;业务应用依赖公共包，例如 &lt;code&gt;@roper/shared&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;应用内部为了减少相对路径层级，用 &lt;code&gt;@/components&lt;/code&gt; 指向本地源码目录。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这两类需求不要混在一起。&lt;/p&gt;
&lt;p&gt;如果是第一类，最好走标准 package 导出，也就是在 &lt;code&gt;packages/shared/package.json&lt;/code&gt; 中声明 &lt;code&gt;name&lt;/code&gt;、&lt;code&gt;exports&lt;/code&gt;、&lt;code&gt;types&lt;/code&gt; 等，让应用像依赖正常 npm 包一样依赖它。这样打包器、测试器、IDE 对它的认知会更一致。&lt;/p&gt;
&lt;p&gt;如果是第二类，本地源码别名才适合用 &lt;code&gt;paths&lt;/code&gt; 加 &lt;code&gt;resolve.alias&lt;/code&gt;。这种别名的边界应该尽量局限在单个应用内部，避免跨应用、跨包互相引用源码目录。&lt;/p&gt;
&lt;h2 id="4-vite-与-vitest-需要明确共享解析配置"&gt;&lt;a href="#4-vite-%e4%b8%8e-vitest-%e9%9c%80%e8%a6%81%e6%98%8e%e7%a1%ae%e5%85%b1%e4%ba%ab%e8%a7%a3%e6%9e%90%e9%85%8d%e7%bd%ae" class="header-anchor"&gt;&lt;/a&gt;4. Vite 与 Vitest 需要明确共享解析配置
&lt;/h2&gt;&lt;p&gt;我最初只在 &lt;code&gt;vite.config.ts&lt;/code&gt; 里写了 &lt;code&gt;resolve.alias&lt;/code&gt;，结果 &lt;code&gt;vitest&lt;/code&gt; 虽然能复用一部分配置，但在某些拆分配置场景下仍然没有完整继承，最后表现成“页面能跑，测试却找不到模块”。&lt;/p&gt;
&lt;p&gt;后来我做了两件事：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;把路径映射抽到一个共享函数里。&lt;/li&gt;
&lt;li&gt;明确让 Vite 与 Vitest 都读取同一份解析规则。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这样做的价值不只是少写几行配置，而是降低“有的人改了构建配置，却忘了同步测试配置”的概率。路径别名这种基础设施类配置，越分散越容易慢慢漂移。&lt;/p&gt;
&lt;h2 id="5-node-运行脚本时要额外留心"&gt;&lt;a href="#5-node-%e8%bf%90%e8%a1%8c%e8%84%9a%e6%9c%ac%e6%97%b6%e8%a6%81%e9%a2%9d%e5%a4%96%e7%95%99%e5%bf%83" class="header-anchor"&gt;&lt;/a&gt;5. Node 运行脚本时要额外留心
&lt;/h2&gt;&lt;p&gt;很多团队会在仓库里保留一些直接用 Node 执行的脚本，比如代码生成、数据迁移、构建辅助命令。此时如果脚本源码里也用了 TypeScript 别名，问题会再次出现，因为裸 Node 并不认识 &lt;code&gt;tsconfig.paths&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;这时候通常有三种选择：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;构建后再执行，让打包器或转译器处理掉路径。&lt;/li&gt;
&lt;li&gt;使用支持别名解析的运行器。&lt;/li&gt;
&lt;li&gt;干脆让这类脚本只使用相对路径或 package 名称，避免再引一层复杂度。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;我最后倾向第三种。工程中有些地方值得抽象，有些地方最好老老实实朴素一点，维护成本反而更低。&lt;/p&gt;
&lt;h2 id="6-这次整理后我保留的实践"&gt;&lt;a href="#6-%e8%bf%99%e6%ac%a1%e6%95%b4%e7%90%86%e5%90%8e%e6%88%91%e4%bf%9d%e7%95%99%e7%9a%84%e5%ae%9e%e8%b7%b5" class="header-anchor"&gt;&lt;/a&gt;6. 这次整理后我保留的实践
&lt;/h2&gt;&lt;p&gt;最后落下来的做法如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根 &lt;code&gt;tsconfig.base.json&lt;/code&gt; 只放通用编译选项与跨项目共享的基础别名。&lt;/li&gt;
&lt;li&gt;应用内别名统一用 &lt;code&gt;@/*&lt;/code&gt; 指向自身 &lt;code&gt;src/*&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;跨包引用优先使用 package name 与 exports，不直接别名到另一个包的 &lt;code&gt;src&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;Vite、Vitest、ESLint 解析器尽量复用同一份路径信息。&lt;/li&gt;
&lt;li&gt;能不用别名的简单脚本就不用别名。&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;整理完之后，最明显的收益其实不是“错误没了”，而是新同事进入项目时更容易理解：什么场景该用包导出，什么场景只是为了避免 &lt;code&gt;../../../&lt;/code&gt;。一套约定如果能被快速解释清楚，往往才是真正稳定的约定。&lt;/p&gt;</description></item></channel></rss>