news 2026/10/8 16:07:04

Vite + pnpm Monorepo 部署到 Vercel 的踩坑指南与配置解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vite + pnpm Monorepo 部署到 Vercel 的踩坑指南与配置解析

踩了两天坑,终于把一个用 Vite 构建的 pnpm Monorepo 项目部署到 Vercel。起初我以为只需要在仪表盘里把构建命令改成那个子应用的命令,结果发现事情远没有这么简单。Root Directory、输出目录、环境变量、共享包变更、路由 rewrite、构建缓存,一层一层挖下去,才真正搞懂 Vercel 和 Monorepo 之间是怎么配合的。这篇文章我按自己的排坑顺序来写,先讲整体设计思路,再给配置,最后把每个坑的原理和解决方式拆开说。无论你是刚把项目改成 monorepo,还是已经部署但时不时冒出诡异问题,这篇应该能帮你省下不少时间。

1. 整体设计与思路拆解

1.1 为什么是 Vite + Monorepo

很多团队一开始不是刻意选择 Monorepo,而是项目拆包拆着拆着就自然长成了这种结构。我这边的情况是,一个主应用和一个后台应用,它们共用 UI 组件、请求封装、通用类型和工具函数。早期把这些公共代码直接复制到各自仓库里,每次改个组件要改两遍,偶尔还会改漏。后来决定抽成独立的包,用 workspace 把它们组织在一个仓库内。选 Vite 而不是 Webpack,是因为开发阶段的热更新体验差距太大了。几个包加在一起,Vite 冷启动基本在 1-2 秒,esbuild 预处理依赖之后,改动代码的反馈几乎是实时的。这对每天高频改组件的场景是刚需。

但 Monorepo + Vite 的爽点主要集中在开发体验,到了生产构建和部署环节,就轮到“架构债”还利息了。Vite 默认针对单包应用,构建产物的输出路径、依赖解析、环境变量作用域,全都是围绕“当前项目目录”设计的。当这个“当前目录”变成仓库根目录时,很多默认行为都会变。所以我们部署的第一性原理就是:明确定义 Vercel 的“项目根”和 Vite 的“应用根”,并让两者之间的所有路径显式化,不要靠隐式推断。

1.2 Vercel 在 Monorepo 部署中的定位

Vercel 本质是一个“把 Git 仓库拉下来,跑 install + build,然后把产物托管到全球 CDN”的托管平台。它对单包项目的零配置体验做得非常好,检测到 Vite 会自动安装依赖、跑 build、输出 dist。但在 Monorepo 里,它无法自动判断该构建哪个子应用,因为一个仓库可能有多个可部署的前端应用。为此 Vercel 引入了 Project 和 Root Directory 的概念。Project 是一个虚拟的部署单元,Root Directory 是项目在仓库中的根目录。每个 Project 只能配置一个 Root Directory。这样你可以为仓库中的apps/web、apps/admin分别创建两个 Project,它们共享同一个 Git 仓库,但各自维护自己的构建配置、环境变量和部署域名。

这种模型的优点是灵活,缺点是隐藏了很多 Monorepo 的联动逻辑。Vercel 的自动检测只能解决“目录内”的事情,无法解决“跨目录依赖”的事情。比如apps/web依赖packages/ui,当packages/ui的代码变化时,Vercel 并不知道这两个目录之间存在关系。这就是很多 Monorepo 部署诡异问题的根源:平台在目录维度上的理解,和你项目在依赖维度上的理解,并不一致。看清楚这一点,后面遇到的坑基本都是可以预判的。

1.3 部署架构与目录规划

我最终的仓库布局如下:

monorepo-root/ ├─ pnpm-workspace.yaml ├─ pnpm-lock.yaml ├─ package.json ├─ vercel.json ├─ apps/ │ ├─ web/ │ │ ├─ src/ │ │ ├─ vite.config.ts │ │ ├─ package.json │ │ └─ tsconfig.json │ └─ admin/ │ ├─ src/ │ ├─ vite.config.ts │ └─ package.json └─ packages/ ├─ ui/ │ ├─ src/ │ └─ package.json └─ shared/ ├─ src/ └─ package.json

注意vercel.json放在了仓库根目录,这让它成为所有 Project 的默认配置。我曾在apps/web下也放了一个vercel.json,后来发现如果 Root Directory 不是apps/web,那个文件根本不会被读取。这是一个重要经验:Vercel 只读取“Project 的 Root Directory”下的vercel.json。

在部署时,我最初想当然把 Root Directory 设为apps/web,因为 Vercel 的文档说“如果是 monorepo,请手动指定 root directory”。结果构建时 pnpm 报错说找不到 workspace 协议workspace:*对应的包。原因很直白:当 Root Directory 被认定为项目根目录后,Vercel 会在该目录下执行安装命令和构建命令,它根本不知道上层还有个pnpm-workspace.yaml。所以设计部署架构时,一定要想清楚“部署单元”和“仓库根”的关系。我最终采用的方式一眼看上去不太常规,却非常稳定:把 Root Directory 设为仓库根目录.,安装命令用pnpm install,构建命令用pnpm --filter web build,输出目录写成apps/web/dist。这样 Vercel 视角里,整个仓库就是“一个项目”,但它打包的其实是 web 子应用。这种做法的缺点是所有文件变动都会触发这个项目部署,后面我会用忽略命令做一层兜底。

2. 环境准备与工程配置

2.1 包管理器选择:pnpm workspace 的优势

包管理器推荐使用 pnpm。pnpm 的依赖结构是“全局存储 + 硬链接 + 符号链接”,每个包都只会真实安装一次,然后通过 node_modules 的软链来引用。这比 npm 的平铺结构节省空间得多,更关键的是它有效避免了幽灵依赖。举个例子,packages/ui依赖了lodash,但在它自己的 package.json 里没有声明,只是某个间接依赖把 lodash hoist 到了根 node_modules。在 npm 下,packages/ui可以直接 importlodash,因为 node 解析路径时会向上找到根目录的 node_modules。虽然能用,但这个依赖是“隐形”的,哪天根目录的 node_modules 布局变了就崩。pnpm 下就不能这么玩,它保证了每个包只能访问自己显式声明的依赖,这对长期维护非常友好。

同时,Vercel 原生的 pnpm 支持做得不错。你只要在根 package.json 里声明"packageManager": "pnpm@8.15.9",Vercel 会自动处理 pnpm 版本。但如果手动指定 install 命令,一定要带上--frozen-lockfile,避免因为 lockfile 不是最新导致安装出意外。另外,pnpm 7+ 会默认阻止依赖安装阶段执行构建脚本(postinstall),比如 esbuild、sharp 这类依赖需要额外批准。Vercel 平台默认会做一步pnpm approve-builds之类的处理,但如果你自定义安装命令时忘了这一步,日志里会看到类似Ignored build scripts: esbuild的提示。这时候需要在pnpm-workspace.yaml里配置onlyBuiltDependencies或执行pnpm approve-builds。

2.2 工程结构初始化与 workspace 协议

初始化步骤很简单,在仓库根目录创建pnpm-workspace.yaml:

packages: - "apps/*" - "packages/*"

根目录package.json设为私有,防止仓库根被意外发布到 npm:

{ "name": "monorepo-root", "private": true, "packageManager": "pnpm@8.15.9", "engines": { "node": ">=18" }, "scripts": { "dev:web": "pnpm --filter web dev", "build:web": "pnpm --filter web build", "build:packages": "pnpm -r --filter './packages/**' build" } }

在apps/web/package.json中,通过workspace:*引用共享包:

{ "name": "web", "dependencies": { "@monorepo/ui": "workspace:*", "@monorepo/shared": "workspace:*" } }

workspace:*让 pnpm 在本地安装时自动链接到packages/ui和packages/shared的实际源码目录。这样做的好处是,开发时改动共享包代码,上层应用可以立即热更新,不需要每次手动重新发布。但代价也在这里:如果部署平台没有正确识别 workspace 协议,它会在安装依赖时尝试从 npm registry 下载@monorepo/ui,结果必然 404。所以 Vercel 的 install 命令必须使用 pnpm,而且 Root Directory 必须在包含pnpm-workspace.yaml的目录下。

2.3 Vite 构建配置要点

Vite 的构建配置中,核心是root基准和envDir。默认情况下,Vite 会把包含配置文件的目录当作 root。当我们在根目录执行pnpm --filter web build时,实际进入apps/web后执行vite build,所以 root 是apps/web,输出目录相对这个 root 计算。vite.config.ts可以参考这样:

import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' export default defineConfig({ plugins: [react()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), '@monorepo/ui': fileURLToPath(new URL('../../packages/ui/src', import.meta.url)) }, dedupe: ['react', 'react-dom'] }, build: { outDir: 'dist', emptyOutDir: true, sourcemap: true }, envDir: '../../' })

这里envDir: '../../'是指从apps/web向上两级到仓库根目录,让 Vite 也读取根目录下的.env文件。不过这样做有个副作用:它会把仓库根目录的所有.env*都拉进来,需要你自己看清楚哪些变量是给 web 这个应用用的。更稳妥的做法是仍然在apps/web内管理.env,Vercel 的注入变量属于进程级环境变量,不需要envDir也能读到,所以envDir这行可以按需保留。

关于 alias 指向共享包源码的路径,这取决于共享包的发布形态。如果共享包是用 TypeScript 源码直接对外开发,那么 alias 指向源码目录没问题;如果共享包是通过tsup编译后的 dist,就应该指向packages/ui/dist。指向源码的好处是开发时不需要额外构建共享包,且 Tree Shaking 更彻底;坏处是 Vite 在打包时会把共享包的源码和 web 的源码一起编译,如果两个包用的 tsconfig 不一致,可能报错。我这里的做法是:开发期 alias 指向源码,生产构建时先执行pnpm --filter ui build,再让 alias 指向 dist。这样兼顾体验和稳定。

3. Vercel 部署配置与关键参数

3.1 Root Directory 的两种姿势与坑

Root Directory 有两种选择,各有坑。

第一种,Root Directory 设为apps/web。这种情况下,Vercel 认为项目的构建上下文是apps/web,install、build 命令都会在apps/web下执行。如果你的仓库根没有 pnpm-workspace.yaml(或者不打算用 workspace 协议),这样是最直观的。但一旦共享包存在,就必须在 install 命令里手动cd ../..,构建命令里用pnpm --filter web build,输出目录写dist。看上去简单,实际上每次部署都要担心脚本里的相对路径对不对,而且 Vercel 对根目录之外的变更不会触发该 Project。一旦有人改了packages/ui,这个 Project 不会重新构建,这就形成了一个大坑。

第二种,Root Directory 设为.。整仓库作为一个项目,install 和 build 在根目录执行,输出目录写apps/web/dist。优点是所有命令都可以在根目录语境下正常工作,特别是 pnpm workspace 的--filter能按包名精确选择。缺点也很明显:任何子目录的文件变更都会触发这个 Project 的部署,导致不必要的构建。如果你在同一个仓库还有apps/admin,你需要为它单独再建 Project,同样把 Root Directory 设为.,但buildCommand改为pnpm --filter admin build。这样两个 Project 都会因为任意文件变更而重新构建,会有资源浪费。

我在实践中最终选择了第二种,然后在 Ignored Build Step 中加了文件变更过滤,把“任意文件变更都触发”带来的副作用降到了最低。这种方式在多个应用并存时依然可控,因为你只是在每个 Project 的过滤脚本里列出自己关心的路径。

3.2 构建命令、安装命令和输出目录组合

把配置写到vercel.json的好处是版本可控、改动可 review。我的最终配置如下:

{ "rootDirectory": ".", "outputDirectory": "apps/web/dist", "framework": "vite", "buildCommand": "pnpm --filter web build", "installCommand": "pnpm install --frozen-lockfile", "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ] }

这里有个关键:outputDirectory和 Vite 的outDir不是一回事。outputDirectory是告诉 Vercel,最终托管目录从哪里获取。如果你把outputDirectory写成dist,而实际产物在apps/web/dist,Vercel 会报Output directory not found。反过来,如果你用 Root Directory 为apps/web的方案,那么outputDirectory应该写dist。记不住的话,就记一个原则:所有路径都是从rootDirectory出发。

buildCommand里可以用pnpm --filter web exec vite build,也可以直接用pnpm --filter web build,后者会读取apps/web/package.json里的 build 脚本。如果 build 脚本里已经包含vite build --mode production,就不要重复指定 mode,除非你想覆盖某个场景。framework: "vite"并不是必须的,但写上可以省去 Vercel 自动检测的步骤。如果遇到平台误判为 Next.js 或其他框架,显式声明能避免不少麻烦。

3.3 环境变量与 .env 加载顺序

Monorepo 环境变量的问题可以分成两层:一是 Vercel 面板上的环境变量是否注入到构建进程,二是 Vite 如何把这些变量应用到import.meta.env。

Vercel 的环境变量是在构建开始时以 shell 环境变量的形式注入的,所以只要在构建命令里能通过process.env读取,Vite 也能通过loadEnv读取。但 Vite 默认会根据 mode 加载项目 root 下的.env文件:.env、.env.production等。如果你的仓库根目录也有一份.env,而 Vite 的 root 是apps/web,那么根目录的.env是不会被加载的,除非你设置了envDir指向那层目录。这正是很多人在 Monorepo 中配置环境变量后不生效的第一个原因。

第二个原因是缓存。Vercel 的构建缓存包含 node_modules、build 缓存等,但环境变量本身属于构建上下文,不会缓存到产物里。真正的问题往往是你修改了 Vercel 面板环境变量后,旧版本里可能缓存了上一次的import.meta.env值,由于 Vite 预构建依赖缓存没有失效,导致构建时引用了旧值。解决办法和构建缓存一样:在构建命令前做一次干净的清理,或者强制 Vite 重新执行依赖预构建。

第三个原因是变量命名。只有以VITE_前缀开头的变量才会被 Vite 暴露到客户端。如果你在 Vercel 面板里设置了一个叫API_BASE的变量,前端代码是拿不到的,只能在服务端构建流程中使用。想在前端用,必须命名为VITE_API_BASE。这是一个很低级但很常见的坑。

4. 踩坑实录与底层原理

4.1 依赖符号链接引起的幽灵依赖问题

pnpm 的 node_modules 结构不是平铺文件,而是一堆符号链接。在 Vercel 的 Linux 构建环境中,pnpm 的符号链接通常能正常存在,但在最终上传产物时,Vercel 只会上传outputDirectory里的静态文件,node_modules 压根不在发布范围。所以符号链接想象中的“部署后找不到路径”不会发生。真正的问题是构建阶段。

Vite 在解析依赖时,默认会扫描 node_modules 中的符号链接。当应用依赖@monorepo/ui,而 ui 内部又引用了 react,Vite 会尝试分析依赖图。如果没有resolve.dedupe,Vite 可能把 react 解析成packages/ui/node_modules/react,而 web 自身引用的 react 在apps/web/node_modules/react。两个不同路径的 react 会被视为两个模块,导致 React 内部状态错乱。典型症状是:页面能渲染,但切换路由后状态不共享,或者使用 context 时出现异常。排查方法是使用vite build --debug看看模块图中 react 的路径。解决方式是resolve.dedupe: ['react', 'react-dom'],告诉 Vite 强制将这些模块解析到单一位置。

另一个符号链接相关的问题是 Node 的模块解析。有些构建工具会启用resolve.preserveSymlinks,这会改变 node 的默认解析行为,让它不再顺着 symlink 去解析真实路径,从而绕开一些重复安装问题。但 Vite 默认关闭这个选项,保持 Node 的默认行为。所以如果你在 Vercel 上同时使用其他框架,而构建链中涉及符号链接,务必要弄明白各个工具对 symlink 的处理策略,否则会出现本地和线上行为不一致。

4.2 构建缓存引发的更新不生效

Vercel 的构建缓存机制很激进,它是为了加快迭代速度。每个部署任务,如果检测到 lockfile 没有变化、安装步骤可以复用镜像,就会直接跳过 install 阶段,并复用上一份 node_modules。同时,它也会缓存一些框架的构建产物,比如.next/cache、node_modules/.vite等。这原本是好事,但对 Monorepo 却是陷阱。

我一共遇到过两种缓存坑。第一种是依赖预构建缓存:改了共享包的依赖关系,或者共享包里新增了导出,但 Vite 的预构建缓存没有失效,导致打包时仍然用了旧的依赖图。表现是:构建日志显示成功,但新页面里找不到新增的导出。解决方法是给 Vite 构建命令加--force,或者清理node_modules/.vite目录。第二种是产物缓存:Vercel 在检测到输出目录下文件未变化时,可能复用上一次的产物,但这种情况比较少见,更多是依赖安装问题。

更麻烦的是,缓存 key 对 Vercel 来说是仓库整体快照,但在 Monorepo 中,各应用共享同一个快照。如果你有两个应用在一个仓库里,A 应用改了代码,触发了 A 的构建,B 应用没触发,但 B 的下一次构建时,Vercel 的缓存系统可能把 A 产生的中间产物也缓存进去,导致 B 的构建环境混入了 A 的残留。这种问题很难复现,我建议在 Monorepo 场景下,构建命令尽量保持“短小、确定”,不依赖跨包的中间产物,需要共享的东西显式构建并放在明确的目录。

4.3 SPA 路由刷新与 rewrite 原理

SPA 路由的 404 问题不是 Vercel 独有,任何静态托管都会遇到。原理是:服务器收到/about请求后,按文件系统找about.html,发现不存在就返回 404。而 SPA 的约定是,所有深层路由都应由index.html接管,由前端路由器决定渲染什么页面。所以需要在托管层做一次“所有路径重写到 /index.html”。

vercel.json的rewrites数组里,source支持路径匹配,destination是最终响应文件。写成/(.*)表示匹配所有路径,/index.html是目标。注意rewrites不会改变 URL,浏览器地址栏依然是/about,返回内容却是index.html。这是实现 SPA 刷新不 404 的标准方法。我见过有人用redirects写得不对,造成循环重定向,原因是把重定向目标写成了网站根路径而不是文件路径。记住:rewrites优先于redirects,且 source 是相对站点根路径的匹配,不是文件系统路径。

还有一个容易忽视的细节:如果 Vite 配置了base: '/admin/',那么应用部署在/admin子路径下,rewrite 的 source 要相应改成"/admin/(.*)",destination"/admin/index.html"。否则直接访问子路径下的深层路由也会 404。

4.4 共享包改动如何触发重新部署

这是 Monorepo 部署中最疼的一个点。Vercel 的触发机制是根据 Project 的设置决定哪些 Git 文件变化会触发构建。默认情况下,如果 Project 的 Root Directory 设置为apps/web,那么packages/ui下的变化不会触发这个 Project 的构建。从触发角度,Root Directory 为.的方案最直接,任何文件变化都会触发构建,但会造成多个 Project 一起构建。

所以我在 Ignored Build Step 中设置了一个脚本,比如:

if git diff --name-only HEAD~1 HEAD | grep -qE '^(apps/web|packages/ui)/'; then # 有相关变更,返回非零让 Vercel 继续构建 exit 1 else # 没有相关变更,跳过构建 exit 0 fi

这个脚本的逻辑是:拿出最近一次提交涉及的文件列表,筛选出apps/web或packages/ui开头的路径。如果命中,则继续构建;否则跳过。需要特别提醒:不同时期的 Vercel 对构建跳过退出码的定义不完全一样,网上很多教程互有出入,最靠谱的做法是在自己的账号里实测一次,跑一个无关文件的提交,看看到底是跳过还是继续。把脚本的语义确认清楚后,这个方案就能稳定工作了。

4.5 Vercel Labs scriptc 与缓存破局

最近社区里聊 Vercel Labs 的 scriptc 比较多。我理解 scriptc 是 Vercel 对构建脚本执行结果做缓存的一种优化,目的是在相同配置、相同源文件的情况下,跳过部分可重复执行的脚本,缩短构建时间。它的原理和 Vercel 构建缓存类似,都基于内容 hash 和快照比较。但在 Monorepo 环境下,scriptc 很可能把“安装依赖”和“构建共享包”这两个环节过度缓存:你的packages/ui代码变了,但共享包在 package.json 中的版本号没变,脚本执行结果被判定为“无变化”,于是继续使用旧的产物。

对付这种问题,最简单的破局手段是给构建命令注入一个随提交变化的变量,比如:

FORCE_REBUILD=$(git rev-parse --short HEAD) pnpm --filter web build

这样每次提交的 hash 不同,环境变量不同,脚本缓存无法命中,就会强制重建。代价是每次都全量执行,牺牲了构建速度。如果你信任缓存但想让它失效得更合理,更好的做法是给共享包设置独立的版本号,并在 web 的 package.json 里用明确的版本依赖,而不是workspace:*。当然,这会失去开发时自动化连接,需要配合 release 流程。两种方式各有取舍,小项目不需要折腾,大项目再考虑。

5. 常见问题排查与性能优化

5.1 部署失败日志速查表

为了方便排查,整理一个速查表,覆盖最常遇到的几种错误表现。

症状可能原因重点检查
安装阶段报ERR_PNPM_WORKSPACE_PACKAGEpnpm 未在包含 workspace 的根目录执行Root Directory 是否为.,install 命令是否显式使用 pnpm
构建时提示Could not resolve '@monorepo/ui'alias 配置错误,或 workspace 包未链接检查 pnpm-workspace.yaml、web package.json 的 workspace 协议、vite alias 路径
构建后找不到输出目录outputDirectory与实际产物路径不一致构建日志最后几行,看 Vite 输出的路径
页面能打开但刷新 404没有配置 SPA rewrite确认 vercel.json 位置和 rewrites 规则
环境变量值为 undefined变量名缺少VITE_前缀,或 envDir 未配置设置 VITE_ 前缀,确认 envDir 指向 root
代码改了但线上没变化构建缓存 / 依赖预构建缓存清理.vite或使用--force重新构建

每一类问题我都在前面详细讲过,这里主要是用来快速定位。拿到日志后,先看是哪个阶段失败,install 阶段失败先看包管理器,build 阶段失败先看输出目录,upload 阶段失败先看文件大小和路径。

5.2 本地正常线上失败的原因

“本地跑得好好的,一上 Vercel 就挂”是最高频的求助。除了环境变量和依赖管理差异,还有几个 Vite + Monorepo 特有的原因。

第一个是 Vite 的root推断差异。本地你在apps/web目录直接vite build,root 是apps/web;在 Vercel 上如果你用根目录执行pnpm --filter web build,实际上 Vite 依然会把apps/web作为 root,因为pnpm --filter web build会进入包目录再执行 npm script。如果构建命令写成了pnpm --filter web exec vite build,而当前工作目录是根目录,那么 Vite 会认为 root 是根目录,此时outDir、envDir全部乱套。所以最好统一用包的 script,避免用exec。

第二个是 Node 版本。Vercel 默认 Node 版本可能和你本地不一致,而 Vite 5+ 要求 Node 18+。如果你的package.json没有engines,也没在 Vercel 面板指定 Node 版本,旧版本可能直接语法报错。建议在根package.json中加上"engines": { "node": ">=18.0.0" },并在 Vercel 的 Settings -> Node.js Version 里选择对应的 20.x。

第三个是 TS 和构建工具的依赖关系。Monorepo 里如果共享包是 TypeScript 写的,而共享包没有构建产物,web 构建时需要让 Vite 能正确处理 TS 文件。直接 alias 到源码时,Vite 会使用 esbuild 转译 TS,但如果你共享包的 tsconfig 里指定了declaration或路径别名,可能会引入解析问题。最稳妥的方式是共享包用tsup或 Vite 的 library mode 提前构建,产出 JS 和 d.ts,再让 web 引用产物。这会多一步构建,但省去了很多奇奇怪怪的 TS 解析问题。

5.3 构建提速与缓存策略

Monorepo 的构建提速,核心是减少“重复构建”和“重复安装”。Vercel 已经帮我们把 install 缓存做得不错,只要 lockfile 不变,install 基本秒过。我们自己能优化的点是共享包的预构建和 web 的按需构建。

我采取的策略是:共享包用tsup进行独立构建,把它编译成 ESM + CJS 双格式产物,然后 web 的 alias 指向 dist。这样 Vercel 构建 web 时,Vite 不需要去编译 TypeScript 源码,也不需要把共享包源码纳入 watch 范围,构建速度会明显加快。构建命令可以写成一条链:

pnpm --filter ui build && pnpm --filter shared build && pnpm --filter web build

前面的产物作为后面的依赖。为了让缓存更精确,如果共享包源码没变,pnpm --filter ui build会很快,web 构建也能复用 Vite 的缓存。为了保险,还是建议把.vite缓存单独处理,或者由 Vercel 缓存node_modules/.vite。

另一个提速点是 Vite 的build.target。如果目标浏览器较新,可以把build.target: 'es2020',这能让 Vite 做更少的转译,减少代码体积和构建时间。还有sourcemap,生产环境如果不需要,可以关掉,构建和上传都会快很多。当然排查问题时就另当别论。

5.4 实战建议与最终配置样例

最后,给出一套我实测稳定的配置,可以直接参考。仓库根vercel.json:

{ "rootDirectory": ".", "outputDirectory": "apps/web/dist", "buildCommand": "pnpm --filter web build", "installCommand": "pnpm install --frozen-lockfile", "framework": "vite", "rewrites": [ { "source": "/(.*)", "destination": "/index.html" } ] }

根package.json重点字段:

{ "name": "monorepo-root", "private": true, "packageManager": "pnpm@8.15.9", "engines": { "node": ">=18" }, "scripts": { "build:packages": "pnpm --filter ui build && pnpm --filter shared build", "build:web": "pnpm --filter web build" } }

apps/web/package.json里的 build 脚本:

{ "scripts": { "build": "vite build --mode production" } }

如果你的 web 依赖共享包的 dist 产物,那么构建命令改为pnpm --filter ui build && pnpm --filter web build。如果共享包代码不经常变,且 Vercel 缓存没问题,可以只构建 web。环境变量确认前缀是VITE_,然后不需要额外配置 envDir,因为 Vercel 注入的是进程环境变量。Ignored Build Step 脚本也可以按需加上。不过我在实际项目里发现,如果 Root Directory 是.,共享包改动会触发构建,这其实也符合预期,只是多构建几个无关应用。如果你的项目数量多,再考虑用脚本过滤。

最后再分享一个我自己的习惯:每做一个新的 Monorepo 工程,我都会在首次部署时用一个空 commit 触发部署,观察完整日志,确认 install、build 和 upload 三条链路都正常后,再开始写业务代码。因为部署环境的怪问题往往藏在最开始时,越早暴露越好排查。遇到莫名其妙的线上问题,先别急着改代码,打开 Vercel 的构建日志往前翻十行,看有没有cache skipped或者scriptc的关键提示,这能省很多时间。Monorepo 的部署本质就是和“路径”以及“缓存”两个东西较劲,把这两个理清楚了,剩下的都是重复劳动。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/8 16:05:43

Java Web停车场系统:可部署、可答辩的完整实战项目

简介:本资源是一套面向Java初学者与课程设计学生的Web停车场管理系统完整开发实践包,聚焦B/S架构下的企业级应用开发全流程。资源涵盖系统源码、数据库脚本、毕业论文文档、部署与功能模块教学视频及多张界面截图,帮助学习者掌握Servlet/JSP或…

作者头像 李华
网站建设 2026/10/8 16:04:59

AI Native流式输出实战:SSE协议与AG-UI渲染深度解析

1. 这不是“加个loading动画”那么简单:AI Native流式输出到底在解决什么问题你有没有遇到过这样的场景:用户在对话界面输入一个问题,页面卡住3秒,然后“唰”一下整段回答全弹出来?或者更糟——等了10秒,只…

作者头像 李华
网站建设 2026/10/8 16:04:04

yt9215 车规交换芯片 Linux 驱动开发实战:DSA/switchdev 选型与调试

简介:本资源为基于Linux DSA框架的YT9215 switch驱动源码包,面向嵌入式网络开发与交换机调试人员。驱动加载后会生成lan 网卡用于获取各网口状态,实际数据通信则依赖eth 网卡,并可通过VLAN划分实现每个接口独立管理,…

作者头像 李华
网站建设 2026/10/8 16:00:52

从一堆检验文献到一篇综述:临床检验诊断学同学的 AI 搭子怎么选?

先把场景说具体:如果你是临床检验诊断学专业学生,正在做毕业论文里的文献综述,题目类似——“外周血 ctDNA 甲基化检测在结直肠癌早期筛查与辅助诊断中的价值”——那你大概率会经历这样一段过程: 要在 PubMed、Web of Science、…

作者头像 李华
网站建设 2026/10/8 16:00:50

OPD缩放定律:训练前预测知识蒸馏效果

1. 这不是玄学,是可计算的蒸馏效率预判——OPD scaling law到底在解决什么问题“OPD的scaling law: 训练前预测蒸馏效果”这个标题乍看像论文摘要,但对真正做过模型压缩、知识蒸馏、边缘部署的工程师来说,它直击一个持续数年的痛点&#xff1…

作者头像 李华
网站建设 2026/10/8 16:00:49

Jeepay开源聚合支付系统实战:Java工程师部署与二次开发指南

简介:一套基于Java的开源聚合支付系统Jeepay,面向需要整合微信、支付宝、云闪付等多渠道支付能力的开发者与企业。系统内置支付网关自动路由,已对接微信服务商及普通商户(V2/V3)、支付宝服务商及普通商户(R…

作者头像 李华