Monorepo 与多环境发布
多包放在一个 Git 仓库里(monorepo),以及 dev / 测试 / 生产 的配置怎么进前端产物,是全栈最常踩的工程坑。工作区依赖 包管理;构建 Vite;密钥只能留服务端,见 Node 与浏览器运行时。容器侧发布见 Docker 与 灰度发布。
解决什么问题
| 痛点 | 做法 |
|---|---|
| 前端、组件库、Node 服务各一个仓,改接口要跨三个 PR | 工作区:一次改、一次 CI |
../../pkg 相对路径复制 | workspace: 协议引用内部包 |
| 生产打进了测试 API 地址 | 分环境 env;分清构建时注入 vs 运行时配置 |
| 密钥出现在 bundle | 前端只能拿公开配置;密钥走后端 |
一句话:内部包用工作区链接;前端环境变量默认在构建时写死,当配置而不是当密钥库。
Monorepo 先认现场
| 标志 | 含义 |
|---|---|
pnpm-workspace.yaml | pnpm 工作区 |
package.json 的 workspaces | npm / yarn 工作区 |
nx.json / turbo.json | 任务编排与缓存(Nx / Turborepo) |
多个 package.json 在 packages/、apps/ | 典型布局 |
# pnpm-workspace.yaml
packages:
- "apps/*"
- "packages/*"
{
"dependencies": {
"@acme/ui": "workspace:*"
}
}
pnpm install 把内部包链到一起,改 packages/ui 立刻被 apps/web 看到(还取决于构建:源码直接引用 vs 要先 build 内部包)。TS 要用 项目引用 或打包器 alias 指到源码,否则会引用过期的 dist/。
| 内部包形态 | 何时 |
|---|---|
| 纯 TS 源码,应用直接 alias | 中小仓,迭代快 |
| 先 build 再给应用 | 发布到 registry、或需单独产物 |
| 发 npm 的公开包 | 另有版本与 changelog,不只是 workspace |
不要把整仓打成一个巨大 vite 应用假装 monorepo——边界是 可独立发布或可独立测试的包。
Turborepo / Nx 解决的是「谁依赖谁、哪部分要重跑」的任务图,不是第二种包管理器。先有 workspace,再视 CI 时长引入。
环境变量:构建时 vs 运行时
浏览器里没有安全的 process.env 运行时读取密钥的方式。Vite 默认:
# .env.development / .env.production / .env.local(后者 gitignore)
VITE_API_BASE=https://api.example.com
const base = import.meta.env.VITE_API_BASE;
只有 VITE_ 前缀(可配 envPrefix)会进客户端。打进去的是 构建那一刻 的字符串。
| 模式 | 行为 | 适合 |
|---|---|---|
| 构建时注入 | vite build --mode staging 把 URL 写进 JS | 静态托管、每环境打一次包 |
| 运行时配置 | 容器里的 config.js 或接口下发,应用启动再读 | 同一镜像 多环境(K8s ConfigMap) |
| 后端 BFF | 浏览器只打相对路径 /api,网关分流 | 少暴露环境差异 |
同一 Docker 镜像要进测试和生产时,不要为每个环境重新 vite build 才改 API 域名——用运行时配置或反向代理同源。镜像构建见 Dockerfile 与镜像构建。
.env.local、密钥、.pem 不要进 Git。前端仓库里出现 SECRET、PRIVATE_KEY 就是事故。
发布清单(前端静态资源)
pnpm install --frozen-lockfile
pnpm lint && pnpm typecheck
pnpm build
把 dist/ 交给 Nginx / CDN / 对象存储
HTML 短缓存或不缓存;带 hash 的 assets 长缓存
多环境:
- 静态站:每环境一次 build(mode 不同)或运行时
window.__CONFIG__ - 容器:Nginx 镜像 + 启动脚本替换配置,或 Sidecar 下发
回滚 = 换回上一份 dist 或上一镜像 tag,所以产物和镜像都要 打 tag 留存。灰度见后端面试专题,前端配合的是:旧 HTML 不要长期指到已删的 hash 文件。
要点
- 一种包管理器 +
workspace:,内部包不要用复制粘贴 - 应用引用内部包时想清楚走源码还是走
dist VITE_*会进客户端,构建时就定死- 同一镜像多环境 → 运行时配置或反向代理,不要靠重新打包塞域名
- 密钥永不进前端环境文件
面试速记
- Monorepo 是什么? 一个仓库多个 package,用工作区链接内部依赖。
- 和多仓比的代价? CI 变复杂、权限边界糊;要用任务图做出增量构建才划算。
- Vite 环境变量何时写进代码? 构建时;不是 Node 服务器每次请求读。
- 为什么前端
.env装不了真正的密钥? 产物在用户磁盘上,任何人都能打开 JS。 - 静态资源和容器配置怎么配合? 要么每环境构建,要么构建一次、启动时注入公开配置。