☰
Vite Monorepo 项目部署到 Vercel 的构建避坑实践指南
2026/10/8 4:08:30 网站建设 项目流程

看着本地跑得好好的 Vite 项目,推到 Git 仓库后却眼睁睁看着 Vercel 构建失败,这种经历我相信每个用过 Vite + Monorepo 的人都懂。更糟心的是,明明昨天还能构建,今天因为 Node 版本、依赖缓存或者路径引用的某个小改动,又莫名其妙挂了。

我过去小半年把好几个 Vite 项目从单仓库拆到 Monorepo,再统一部署到 Vercel,前前后后踩了不下二十个坑。这篇文章不打算写成文档翻译,只记录那些我在实践中反复踩、翻源码、看 issue 才搞清楚的东西,以及背后的原理。如果你正准备把 Vite Monorepo 项目搬到 Vercel,或者已经在搬的路上但被构建日志折磨得头疼,这篇文章应该能帮你省下不少时间。

1. Vercel 的构建运行环境到底是怎么运作的

开始讲具体配置之前,先得把 Vercel 的构建机制说明白。很多人拿着 Vercel 当"云端电脑"用,其实从工程角度讲,它更像一个高度定制的 CI/CD 流水线。理解它的行为模式,排错才能有的放矢。

1.1 构建沙箱与路径规则

Vercel 的每一次部署都会启动一个全新的干净环境,然后执行一系列预定义 Hook。大致流程是这样的:拉取代码、安装依赖、执行构建命令、收集产物、部署到边缘网络。

这里有几个关键规则,几乎决定了 Monorepo 能不能在 Vercel 上顺利跑起来:

根目录(Root Directory)规则:Vercel 默认认为你的仓库根目录就是项目根目录。对于 Monorepo,情况完全不同——你的 Vite 应用可能放在packages/web里,构建命令工作目录应该在这里,而不是仓库根目录。没有指定 Root Directory,Vercel 会尝试在仓库根目录找package.json,要是没找到或者找错了,构建会直接失败。

我在很多项目里看到过这样的配置失误:明明 app 在apps/admin-web下,Vercel 设置里却把 Root Directory 留空了,结果构建时报"Could not find package.json",一脸懵。

注意:Root Directory 一旦设定,package.json、pnpm-workspace.yaml、turbo.json这些文件的相对路径解析都会基于这个目录重新定位,这一点要格外留意。

Node 版本差异性:Vercel 默认使用的 Node 版本可能会滞后于你本地版本。Vite 6 之后对 Node 版本要求明显提高(需要 18+ 或 20+),如果你的本地环境是 Node 20 但 Vercel 默认环境是 16,构建时可能直接报语法错误或者原生模块加载失败。设置里务必将 Node 版本显式指到项目实际使用的版本。

1.2 构建系统默认查找逻辑

Vercel 会自动探测项目类型。它会读取根目录的package.json,然后决定使用什么构建系统。对于纯 Vite 项目,它通常能自动识别出vite build命令。但问题来了——Monorepo 里这个"自动识别"经常给你错误答案。

比如,你用 pnpm workspace 管理多个包,Vercel 可能会把根目录当成一个应用去构建,然后跟着buildscript 的设置去执行。根目录没有 build script 时,它会尝试默认的vite build,结果是传统vite命令找不到,报 "vite: not found"。原因很简单——Vitest、Vite 这些依赖都装在工作区的子包里,根目录的 node_modules 里没有。

解决方式是显式告诉 Vercel:Root Directory 填apps/web,Build Command 填pnpm build,Install Command 填pnpm install。少一个,它就可能猜错。

2. Monorepo 部署前的基础设施准备

没有好的地基,部署再多都是徒劳。Monorepo 能顺利上 Vercel,前提是本地工程化足够规范。这一节我讲讲我在拆包、组织依赖、脚本编排时的核心做法。

2.1 包管理器与工作区:选型直接影响部署体验

先说结论:我在 Vite + Monorepo 组合里,强烈推荐pnpm。npm workspaces 虽然也能用,但在 Vercel 这种每次全新安装依赖的环境里,pnpm 的硬链接机制和严格依赖隔离会让安装更快、构建更可复现。

pnpm-workspace.yaml是 pnpm 工作区的声明文件,示例:

packages: - 'apps/*' - 'packages/*'

这里面有个细节:如果 workspace 里既包含前端应用又包含共享库,packages/*这个 glob 可能会把一些不应该被 workspace 管理的目录也包进来。比如你有个packages/scripts目录,里面装的是一些 Node 脚本,它不该被 app 直接引用。写了 glob 之后,pnpm 会默认把目录下所有包都纳入 workspace,容易在pnpm install时触发软链接解析问题。

更严格一点,我会显式排除集成测试目录、文档站点这类不应该进入生产构建依赖链的包:

packages: - 'apps/*' - 'packages/*' - '!packages/e2e-tests' - '!docs/**'

这种排除规则在部署时有一个很实际的好处:pnpm install时不会为无关包生成锁文件变更、不会多安装一堆不需要的依赖,构建缓存命中率也能提高。

2.2 package.json 里的 scripts 编排

Monorepo 项目里的package.json不只是给自己看的,Vercel 的构建命令会直接读取它。所以脚本设计必须兼顾本地开发、CI 和 Vercel 三套环境的语义。

根目录package.json的 scripts 我是这样设计的:

{ "scripts": { "build:web": "pnpm --filter web build", "build:admin": "pnpm --filter admin build", "build": "pnpm -r build", "typecheck": "pnpm -r typecheck" } }

在 Vercel 上给每个应用分别建 Project,各自的 Root Directory 指向apps/web、apps/admin,Build Command 统一设为pnpm build。应用级别的package.json里的 build 脚本就是:

{ "scripts": { "build": "vite build" } }

这样做的好处是:Vercel 感知到的每个项目的命令都是简单的pnpm build,实际执行什么完全由子包内部决定,升级 Vite、换构建插件都不用在 Vercel 配置面板里来回改。

2.3 Node 与包管理器版本锁定

除了.nvmrc(这个 Vercel 会自动读取),更重要是让 Vercel 使用正确的 pnpm 版本。pnpm 9 之后对 lockfile 版本管理更严格,偶尔会遇到这种情况:本地是pnpm@9.1.0生成 lockfile,Vercel 默认用的 pnpm 是 8.x,构建时直接报 "lockfileVersion mismatch"。

我在 Vercel 项目设置里用package.json的packageManager字段锁定版本:

{ "packageManager": "pnpm@9.1.0" }

Vercel 拿到这个字段后,会自动安装对应版本的 pnpm,不会出现本地和云端版本不一致的问题。这算是我踩完lockfileVersion坑之后总结出的最省心做法。

3. Vite 构建配置在 Monorepo 中容易踩的暗坑

Vite 本身非常适合 Monorepo,它的构建是面向浏览器 ES Module 的,天然支持跨包引用。但正因为 Vite 做了pre-bundling和按需加载,Monorepo 场景下反而出现了一些单仓库不会遇到的陷阱。

3.1 依赖预构建(optimizeDeps)与 workspace 包的纠葛

Vite 在开发模式启动时,会先用 esbuild 把依赖预构建成 optimized deps,然后缓存在node_modules/.vite/deps里。这个机制在 Monorepo 里有个经典问题:如果你直接在工作区里引用另一个包,并且这个包package.json里main或exports指向的是 TS 源文件而不是构建产物,Vite 会把它当作源码去解析,但是默认不会对它进行预构建。

常见的报错是:

[vite] optimized dependencies changed. reloading

或者说某个 workspace 包里的依赖无法被正确解析。解决方式通常是在vite.config.ts里添加optimizeDeps.include:

// apps/web/vite.config.ts import { defineConfig } from 'vite' export default defineConfig({ optimizeDeps: { include: ['@shared/ui', '@shared/utils'] } })

这个做法的原理是告诉 Vite:"这些 workspace 包里的模块,你要提前为我处理掉。"不加的话,Vite 在首次访问页面时才会发现新依赖,然后触发一整页 reload,开发体验糟糕到怀疑人生。

但在部署构建(vite build)场景下,optimizeDeps.include不是构建错误的主要来源,构建时 Vite 默认不做预构建,用的是 Rollup 直接打包。真实的大坑是另外一个:构建时 workspace 包的导出格式不兼容。

3.2 workspace 包的导出格式与构建输出互不匹配

我们有一个共享包@company/ui,它在package.json里定义成这样的 exports:

{ "exports": { ".": { "import": "./src/index.ts", "types": "./src/index.ts" } } }

本地开发一切正常,因为 Vite Dev Server 会直接把 TS 源码转给浏览器执行。打包构建时问题就来了——Vite 在处理import { Button } from '@company/ui'时,会去读取@company/ui的 package.json 的 exports,发现指向的是.ts文件,Rollup 确实能解析 TS,但有时会出现 ESM/CJS 格式判断的混乱。

我印象最深的报错是:

Rollup failed to resolve import "@company/ui/Button" from "src/App.tsx". This is most likely not possible in a monorepo setup.

排查半天,问题出在共享包里Button的导出路径:Button不是通过入口文件的 re-export 导出的,而是直接exports['./Button']指到子路径文件。Rollup 在 resolve 时,对外部包的子路径解析遵循exports字段的精确匹配,定义不全就解析不到。

规范做法是共享包的package.json里这样写:

{ "name": "@company/ui", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" }, "./Button": { "types": "./dist/Button.d.ts", "import": "./dist/Button.js" } } }

也就是 Monorepo 里的共享库,只要被 Vite 用于生产构建,最好以构建后的产物为出口,而不是直接把 TS 源文件暴露出去。虽然 pnpm workspace 开发时直接引用源码很爽,但部署环境的解析逻辑和本地 Dev Server完全不同,越早统一产物出口,部署越省心。

3.3 环境变量与import.meta.env在 Monorepo 中的传递

Vite 的构建过程会读取.env文件,默认读取的是项目根目录(也就是vite运行的当前目录)。Monorepo 里 Vercel 的 Root Directory 设成apps/web之后,Vite 就会从apps/web下读取.env、.env.production。

如果你的环境变量放在仓库根目录,Vercel 上构建时会发现import.meta.env.VITE_API_BASE是undefined。

这里要明确一个点:Vercel 的 Environment Variables(Project Settings > Environment Variables)和本地.env是两套东西。Vercel 注入的环境变量会存在进程环境里,Vite 默认不会把任意进程环境变量挂到import.meta.env上,只有以VITE_前缀开头的变量才会被 Vite 暴露给客户端代码。

所以正确做法是:

  1. 所有需要暴露给客户端的变量,一律命名为VITE_XXX
  2. 在 Vercel Project Settings 里配置这些VITE_XXX的值为生产环境对应的值
  3. 本地开发的默认值放.env.development

我遇到过同事把API_BASE_URL配在 Vercel 上,前端代码里用import.meta.env.API_BASE_URL去拿,结果生产环境永远为空,build 又不会报错——这类问题排查真挺费神的。

4. Vercel 上 Monorepo 的依赖安装与缓存机制

每次部署都全量安装依赖是 Vercel 最容易被忽视的成本点。构建慢不说,依赖安装阶段出问题还特别隐蔽。

4.1 pnpm 的 store 机制与 Vercel 缓存

Vercel 构建时会对依赖缓存做处理,通常是根据 lockfile 的内容判断是否需要重新安装。对于 pnpm 项目,Vercel 也会复用 cache 目录。

但 pnpm 的安装有一个特点:它会先在全局 store 里硬链接文件,再链接到项目node_modules。Vercel 的缓存机制对 pnpm store 目录的保留策略有时候不太理想,结果是重装依赖只快了一点点,不太能达到"秒级恢复"的效果。

如果构建时间太长,解决思路是开启 Vercel 的Build Caching,并且确保构建命令不输出随机内容(有些工具会在构建产物里内嵌时间戳,导致缓存每晚都失效)。

另外,不要在构建命令里加--frozen-lockfile之外的额外安装参数时,去强行改 lockfile。有一次我在 Vercel 上配 Install Command 为pnpm install --no-frozen-lockfile,想着放宽依赖版本范围来解决问题,结果是构建环境生成了新的 lockfile,和仓库里的不一致,导致后续所有部署都和本地依赖树对不上。为了一时省事,换来的是长时间的不稳定,挺不值的。Vercel 环境里应该用pnpm install --frozen-lockfile,保证严格按 lockfile 安装。

注意:Install Command 一旦自定义,Vercel 自动的依赖安装逻辑会被完全替换。很多"缓存不生效"的问题,其实是自定义 Install Command 把 Vercel 内置的缓存逻辑给顶掉了。

4.2 多 Project 共用一套 Monorepo 时注意构建隔离

很多 Monorepo 会长出多个可部署应用:管理后台、官网、用户端。在 Vercel 上逐个创建 Project,每个的 Root Directory 指到不同子目录。

这里有一个容易犯的错:多个应用之间如果通过 workspace 共享代码,它们的node_modules里有大量相同依赖。Vercel 的每个 Project 是独立缓存,意味着同一套 pnpm store 会被复制多份。虽然不是致命的,但每个 Project 首次构建都要重新解析所有依赖,部署多个 Project 的总时间线性叠加,这也算 Monorepo 上 Vercel 的一个隐性成本。

相对有效的缓解方式:

  • 让公共依赖尽量集中在少数几个 workspace 包中,避免每个 app 各自引一堆重的依赖
  • 在共享包内部做 Tree Shaking 友好的导出设计,减少 app 构建时需要打包的模块量

4.3 使用output: 'server'时要格外小心的 SSR 场景

如果你的 Vite 应用不是纯静态 SPA,而是 SSR(比如用 Vite + React Router 的 SSR 模式),Vercel 的部署方式会大不一样。纯静态站可以用默认的 output 模式,但 SSR 应用需要适配 Vercel 的 Function 机制。

我试过的做法是通过vite-plugin-ssr或vite-plugin-react-router这类插件生成dist/server入口,然后 Vercel 会把它包装成 Serverless Function。

这里有个印象深刻的坑:vite build的 SSR 构建产物是 CommonJS 格式时,Vercel 的 Serverless 环境跑起来没问题,但有时构建过程中 Rollup 会警告:

Module level directives cause errors when bundled. "use client" is not supported in CommonJS output.

这个警告一般不会导致构建失败,但如果你用了某个包同时发布module和main两种格式,SSR 构建时可能选错格式,最终在 Vercel Function 运行时报require is not defined之类的诡异错误。

经验判断:纯前端团队如果没有迫切的 SEO 需求,Monorepo 里尽可能先把应用做成静态站(output: 'static'),部署链路最简单,Vercel 的全球边缘网络也能发挥最大优势。SSR 带来的复杂度,是需要在部署层面额外买单的。

5. 手工部署与 Git 集成:用脚本补足 Vercel 配置的灵活性

Vercel 的 Web 控制台很好用,但 Monorepo 里常常需要自动化。Vercel CLI 在这时候就派上大用场了,特别是配合 CI 流程。

5.1 Vercel CLI 部署的多目录支持

假设你要在本地手动部署apps/web,命令很简单:

vercel deploy apps/web --prod

它的原理是把apps/web目录作为项目根目录上传,但这里有个坑:Vercel CLI 上传的是单目录内容,它不会自动带上仓库根目录的pnpm-workspace.yaml。

也就是说,如果你的apps/web依赖了 workspace 里的@shared/ui,但apps/web目录下没有pnpm-workspace.yaml(它通常在根目录),那么vercel deploy apps/web会创建一个只有apps/web内容的部署包,pnpm install根本找不到 workspace 上下文,构建就会失败。

解决方式是用--cwd参数或者直接在仓库根目录执行:

cd /path/to/monorepo vercel deploy --cwd apps/web --prod

但实测下来,CLI 对 Monorepo 的支持比较有限,术语上叫法也不统一。如果构建配置复杂,我建议直接走 Git 集成(push 触发部署),而不是用 CLI 处理本地 Monorepo。

5.2 Git 集成时的分支与忽略规则

Vercel 的 Git 集成默认对 main 分支自动部署生产环境,对其他分支创建 Preview Deployment。Monorepo 场景下,一个仓库里有多个应用,这样就会有个问题:你只是在packages/shared里改了一行代码,Vercel 也会触发所有 Project 的构建。浪费构建时长不说,改动如果无意中影响了多个 app,部署面也被不必要地放大了。

Vercel 针对这个场景提供Ignored Build Step配置。我踩过几次坑后,给每个 Project 都配了脚本,只有相关目录有变更时才真正构建。

比如在apps/web的 Project Settings 里,设置 Ignored Build Step 为:

git diff --quiet HEAD^ HEAD -- apps/web packages/shared pnpm-lock.yaml

一行命令,当apps/web、packages/shared或 lockfile 没变化时,Vercel 会跳过这次构建,直接复用上一次的部署。

这个技巧对 Monorepo 特别实用,铺开之后你会发现 Preview 部署的数量锐减,构建列表面貌清爽不少。

5.3 从构建日志中定位 Monorepo 特有报错

最后分享一个排查技巧:如果 Vercel 构建日志里报了 "Failed to resolve entry for package",赶紧去查package.json的exports和main字段。这类报错在 Monorepo 里八成是 workspace 包的产物路径配置和 Vite 的解析规则对不上。

如果报的是 "out of memory" 或者 "JavaScript heap out of memory",先看构建命令里有没有显式设置 NODE_OPTIONS:

NODE_OPTIONS="--max-old-space-size=4096" vite build

Monorepo 打包多个应用依赖时,Rollup 的 AST 和模块图会占不少内存。Vercel 默认的内存限额是 4GB 左右,大型项目很容易顶满。我遇到过某项目本地 8GB 内存构建正常,到了 Vercel 就 OOM,加上NODE_OPTIONS限制之后反而更稳定。所以有时候限制内存反而比无限制超卖更可控,有点反直觉。

6. 项目设置面板里的关键决策点

Vercel 控制台的项目设置页面,每个选项看起来都简单,组合起来却是天壤之别。我梳理一下 Vite Monorepo 部署时最关键的几个设置项。

6.1 Root Directory 的取舍

这一节再强调一次,因为太重要了。配 Root Directory 时,Vercel 会重新计算你的 Project 的工作目录,这个决定了:

  • package.json的读取位置
  • lockfile 的查找位置
  • .env的加载目录
  • 构建日志中相对路径的起始点

对于apps/web项目,Root Directory 就应该填apps/web。同时注意,Vercel 的 Framework Preset 应该选 Vite,Build Command 保持pnpm build时会自动拼接为cd apps/web && pnpm build,无需自己拼。

6.2 Build Command 的干净化

Build Command 不是越复杂越好。我见过有人把构建命令写成长长的 Bash 拼接,动不动就cd ../.. && pnpm install && pnpm --filter web build,这种写法反而容易出问题。

一个合格的 Build Command 应该是「简短、明确、不跨目录」:

pnpm build

其他的前置操作放在应用的package.json的prebuild脚本里:

{ "scripts": { "prebuild": "pnpm --filter @shared/ui build", "build": "vite build" } }

这样每当执行pnpm build时,pnpm 会先执行 prebuild,把依赖的共享包构建好,再开始打包应用。这个语义在本地、CI、Vercel 是完全一致的,不会因为是 Vercel 环境才生效。这是我认为 Monorepo 部署时性价比最高的收敛点。

6.3 Vercel Labs 新特性的留意价值

Vercel 团队一直在迭代部署基础设施,特别是 Monorepo 场景下的缓存与依赖分析。我在浏览 Vercel Labs 相关功能时,看到他们最近比较关注Scripts API和缓存细粒度控制方向的东西。

具体来说,Scripts 可以将部分构建行为脚本化,让部署前的检查、产物后处理更规范地纳入版本管理,而不是散落在 Vercel 面板的多个设置项里。这对 Monorepo 的意义在于:不同 app 之间可以复用同一套部署检查逻辑,分支保护、产物校验、缓存策略都能用代码表达。

这不是广告,我的真实看法是:项目早期的部署配置完全可以留在面板里,但一旦 app 数量接近 3 个,把配置往代码仓库转移是迟早的事。等你发现要在三个 Project 面板里手动同步同一份配置时,自然就懂了。

7. 完整实操:一个双应用 Monorepo 从零部署到 Vercel

前面讲了这么多原理和规则,这里放一个可以直接照着做的完整例子。我以一个包含web和admin两个 Vite 应用的 Monorepo 为例,说明整个流程。

7.1 仓库结构初始化

假设仓库结构是这样的:

my-monorepo/ ├── apps/ │ ├── admin/ │ │ ├── package.json │ │ └── vite.config.ts │ └── web/ │ ├── package.json │ └── vite.config.ts ├── packages/ │ └── ui/ │ ├── package.json │ └── src/ ├── package.json ├── pnpm-workspace.yaml └── turbo.json

根目录的package.json:

{ "name": "my-monorepo", "private": true, "scripts": { "dev": "turbo run dev", "build": "turbo run build" }, "devDependencies": { "turbo": "^2.0.0" }, "packageManager": "pnpm@9.1.0" }

packages/ui/package.json的关键配置(产物导出规范):

{ "name": "@my/ui", "version": "1.0.0", "main": "./dist/index.js", "module": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }, "scripts": { "build": "vite build --config ../../vite.lib.config.ts" } }

注意这里共享库也走 Vite 构建,配置放在仓库根目录的vite.lib.config.ts,设置build.lib和build.rollupOptions.output生成 ESM 产物。这种模式下,vite build的入口就是packages/ui/src/index.ts,输出到同目录下dist。

7.2 Vercel 上分别为两个应用建项目

在 Vercel 控制台创建 Project,导入同一个 Git 仓库,然后注意以下设置:

  • Project Name:my-monorepo-web
  • Root Directory:apps/web
  • Framework Preset: Vite
  • Build Command:pnpm build
  • Output Directory:dist(Vite 默认输出到dist,Vercel 会自动识别,但确认一下没坏处)
  • Install Command:pnpm install --frozen-lockfile

apps/web/package.json的关键脚本:

{ "scripts": { "prebuild": "pnpm --filter @my/ui build", "build": "vite build" } }

admin应用如法炮制,Root Directory 填apps/admin即可。

7.3 关键构建链路验证

部署时,Vercel 实际执行的逻辑是:

  1. 进入apps/web目录
  2. 读取这里的package.json,发现packageManager: pnpm@9.1.0,自动安装对应 pnpm
  3. 执行pnpm install --frozen-lockfile,通过 workspace 上下文安装根目录和子包的全部依赖
  4. 执行pnpm build,先触发 prebuild,构建@my/ui
  5. 再执行vite build,Vite 在打包时通过@my/ui的exports字段找到dist/index.js,完成打包
  6. 产物输出到dist,Vercel 收集并发布

这套链路的关键地方是第 3 步:因为apps/web目录下有packageManager字段,但同时需要 workspace 上下文,所以pnpm 必须在仓库根目录或能向上找到 workspace 的地方执行。Vercel 对 root directory 的支持方式确保了pnpm install能发现位于仓库根目录的pnpm-workspace.yaml。

实测中如果你的 root directory 是apps/web,而node_modules在根目录,那么pnpm install可能不识别。这个情况下,可以把 install command 改成:

cd ../../ && pnpm install --frozen-lockfile

但这样就违背了我前面说的"保持命令简单"原则。更优雅的解法是把pnpm install默认就放在根目录执行,Vercel 的 workspace 感知机制通常能处理好。如果确实不行,我推荐用如下 Install Command:

node -e "const { execSync } = require('child_process'); execSync('pnpm install --frozen-lockfile', { cwd: '../../', stdio: 'inherit' })"

这一串只是在 Vercel 的局限下绕了个弯,不推荐作为首选,但是应急时能救命。

7.4 部署完成后的验证清单

部署成功后,建议开一个浏览器无痕窗口,逐项确认:

  • 页面是否 200
  • 静态资源是否从/assets/xxx.js正常加载
  • 控制台有没有Failed to fetch dynamically imported module
  • 路由刷新(比如直接访问/about)不 404

最后一条很关键。Vite 的 SPA 默认使用 History 路由,Vercel 需要把/about这类路径重写到index.html。Vercel 对 Vite 项目会自动处理 rewrites,但如果你在某些 devDependency 或配置里动了它,刷新 404 就会突然出现。可以在项目根目录放一个vercel.json:

{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }

不过如果 Root Directory 是apps/web,vercel.json应该放在apps/web下,不然不生效。

8. 实战中遇到的高频报错与解决办法

这里整理的报错,是我在 Vite + Monorepo + Vercel 组合下真实遇到过的,按出现频率排序,每个都附了定位思路。

8.1ERR_PNPM_LOCKFILE_MISMATCH或 lockfile 版本错误

现象:构建日志提示当前 pnpm 版本无法解析 lockfileVersion,或者lockfile needs to be updated。

原因:本地的 pnpm 版本和 Vercel 的 pnpm 版本不一致,生成的 lockfile 格式存在差异。

对策:在根目录package.json里写死packageManager字段,例如"packageManager": "pnpm@9.1.0",同时把pnpm-lock.yaml提交到 Git。下次 Vercel 构建时,它会自动使用指定版本 pnpm,lockfile 就不会再闹脾气。

8.2vite: not found

现象:构建报sh: vite: command not found或类似 "vite" 无法识别。

原因:在 Monorepo 里,执行构建命令的工作目录不是 Vite 应用所在的目录,或者安装依赖时没有安装应用层级的依赖。

对策:先确认 Root Directory 是否指向应用目录;再检查 install 是否真的执行成功;然后确认应用本身的package.json的 devDependencies 里有vite(不要把 vite 放在根目录 devDependencies 里就万事大吉,pnpm --filter执行时依赖解析还是要看应用自身的依赖声明)。

8.3Module not found: Error: Can't resolve '@shared/ui'

现象:构建时 Vite 解析不到 workspace 包。

原因:workspace 包的exports字段没有给出合理的产物路径,或者该共享包没有在之前构建好。

对策:按 3.2 节的内容检查@shared/ui的package.json的exports;确保 build 命令里通过 prebuild 或 turbo 先构建共享包。

8.4Deployment failed: no output directory named 'dist' detected

现象:构建成功了,但 Vercel 没找到可以发布的产物目录。

原因:Vite 输出目录被改过(比如改成了outDir: 'build'),或者 Output Directory 和实际产物位置不一致。

对策:确认 Vite 配置里的build.outDir是什么,然后同步修改 Vercel 的 Output Directory 设置。如果不想每次改配置,建议 Vite 输出统一固定为dist。

8.5JavaScript heap out of memory

现象:构建中断,日志末尾有 heap OOM 信息。

原因:Monorepo 多包同时被 Vite 打包,或者某个共享包被重复打包成多份,导致 Rollup 内存暴涨。

对策:Vercel 环境变量NODE_OPTIONS设为--max-old-space-size=4096,或者在 build 命令前显式设置。同时检查是否引入了体积异常大的依赖(比如完整的lodash而不是lodash-es),用rollup-plugin-visualizer分析产物构成,砍掉不必要的依赖。

9. 我最后想分享的几条心得

回到开头那句话——本地跑得好好的项目部署到 Vercel 挂了,基本上都是环境假设不一致造成的。本地的 node_modules 里有半年前装的依赖缓存,Vercel 是全新的;本地用了 Node 20,Vercel 默认 Node 18;本地直接pnpm --filter web build,Vercel 可能用了另外的脚本。理解这一点,排错就成功了一半。

我在实际项目里形成的两个硬性习惯:

第一,所有部署相关命令(安装依赖、构建、类型检查)都放在package.json里,并且保证它们在本地可复现。也就是说,任何时候手动跑命令都要能获得和 Vercel 一样的结果,这样在本地就能提前发现部署问题。

第二,每次调整部署配置,一定要保留一次完整成功的构建日志作为基线。之后任何改动如果导致失败,先和基线对比差异,不要急着改配置。很多 Vercel 的面板配置项是组合生效的,改动一个可能影响其他行为,有基线比对会安全很多。

Vite 的构建哲学和其他打包器有很大不同。它是一个极简入口加超强插件的组合,开发者的自由度极大,但这也就意味着构建链路受工程结构影响远大于传统 Webpack 模板。Monorepo 解决的是代码组织问题,Vercel 解决的是部署环境问题,两者碰在一起,中间的所有缝隙都需要你理解原理后主动去填。这些缝隙填好了,整个部署链路其实可以很流畅——一次配置,全仓库多个应用并行发版,体验确实很香。

希望这篇记录能帮你在部署路上少走些弯路。如果你也踩过什么特别刁钻的坑,欢迎在评论区分享,大家互相补全一套 Monorepo 部署避坑手册,比各自在日志里挣扎要有用得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询