构建 Next.js platform
这一课你将能做到
- 写出完整的 platform job:
setup-node + npm ci + lint / test / build,工作目录落在 platform/
- 处理 Next 构建期的环境变量(如
NEXT_PUBLIC_API_BASE),避免「本机有 .env.local、CI 没有」导致的假红
- 说清 CI 验证的是「可生产构建」,与本地
next dev 开发流的区别与分工
为什么要学
本站的 platform 是 npm + Next.js。前端的失败形态和 Java 不同:lint 违规、类型错误、next build 编译期才暴露的问题(导入错路径、页面级类型不匹配、SSR 兼容问题),在 dev 热更新下常被忽略。
CI 里跑一遍 lint + test + build,等于强制「合并前必须能产出生产包」。对 monorepo 而言,它与 backend job 并行,组成完整门禁。
本课给出可粘贴的 job 骨架,并点出 env、lockfile、缓存三个最常见卡点。
先建立图像
job: platform(ubuntu-latest)
checkout
setup-node 20 + cache npm(lock: platform/package-lock.json)
working-directory: platform
npm ci ← 按锁文件干净安装
npm run lint
npm test
npm run build ← 生产构建,最接近「能不能上线」
人话:把 CI 当成一位从不打开 IDE 的前端同事——只认 package-lock.json 与 npm run 脚本。你本地靠 .env.local 的侥幸,他那里没有。
| 脚本 | 作用 | CI 是否建议 |
|---|
npm ci | 按锁文件干净安装 | 是(优于随手 npm i) |
lint | 风格与部分错误 | 是 |
test | 单元/组件测 | 是(有则跑) |
build | 生产编译 | 是 |
dev | 开发服务器 | 否(不适合当门禁) |
跟着做
步骤 1:本地先对齐脚本
cd platform
npm ci
npm run lint
npm test
npm run build
任一失败先本地修。同时确认 package.json 里的脚本名(lint/test/build)与即将写入的 YAML 一致——脚本名对不上是入门常见乌龙。
步骤 2:写入 platform job
jobs:
platform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: platform
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 安装 Node 20
uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
# monorepo:锁文件不在仓库根
cache-dependency-path: platform/package-lock.json
- name: 干净安装依赖
run: npm ci
- name: Lint
run: npm run lint
- name: 测试
run: npm test
- name: 生产构建
env:
# CI 无 .env.local 时给构建期占位,避免缺变量直接炸
NEXT_PUBLIC_API_BASE: http://localhost:8080
run: npm run build
与 backend 放同一 ci.yml、不写 needs,即可并行。
步骤 3:验证锁文件纪律
临时把 npm ci 改成 npm install 对比一次(看完改回):npm install 会容忍 lock 与 package.json 漂移,npm ci 则直接报错——这正是它的优点。笔记写:CI 用 npm ci 保卫锁文件,不被「随手 install」漂版本。
步骤 4:故意制造 lint/build 失败
引入一条 eslint 违规(如未使用变量)→ push → 看红在哪个 step;再弄坏一个页面导入,看红移到 build。修回后确认全绿。练习「点开失败 step 直接读日志」,而不是从头翻十分钟下载日志。
步骤 5:认识构建产物(为 Docker 课铺路)
若项目启用 Next 的 standalone 输出,构建后常见:
platform/.next/standalone ← 可独立运行的 server.js 及最小 node_modules
platform/.next/static ← 静态资源
本站打包脚本与 Dockerfile 会消费这些产物。CI 本课以「build 成功」为门禁;要不要把 .next 传成 artifact 看下游需求——若 Docker 构建在 CI 里重跑 next build(多阶段构建),则无需传递。
步骤 6:paths 过滤(与第 11 课呼应)
仅改 backend/** 时,可让 platform job 跳过(paths / paths-ignore 或 job 级条件表达式)。入门阶段先两边都跑保证简单;分钟数吃紧再优化。
讲清楚要点
- 因为生产环境跑的是 build 产物,所以 CI 必须跑
npm run build——大量错误只在 build 编译期出现,dev 永远发现不了。
- 因为依赖版本应可复现,所以提交
package-lock.json 并用 npm ci——与本地「能跑就行」的惯性对抗。
- 因为 Next 构建期读
NEXT_PUBLIC_*,所以 CI 要显式提供所需 env——不要假设存在 .env.local(它通常被 gitignore)。
- 因为 monorepo 锁文件在子目录,所以
cache-dependency-path 与 working-directory 都要指对——根目录 npm 会装错地方。
- 因为 lint/test/build 失败原因不同,所以拆成多个 step——一条
&& 糊到底,日志没法定位。
- 因为前端 CI 与后端 CI 目标对称,所以与 backend job 并列组成仓库门禁——一边绿不够。
你可能会懵的地方
-
「本机 build 过、CI 报缺少 env?」
本机有 .env*.local(不进 Git)。把构建真正需要的变量写进 workflow env,或在仓库维护 .env.example 文档化。注意:NEXT_PUBLIC_* 会被打进客户端产物,所以不要放敏感值。
-
「npm ci 报 lock 不同步?」
在 platform/ 重新生成并提交 lock(npm install 一次后提交变更),不要在 CI 里用 npm install 掩盖——那等于放弃版本锁定。
-
「只要 lint 绿是不是就够了?」
不够。类型错误、非法导入、服务端组件约束等都在 next build 才爆。门禁必须包含 build。
-
「Node 18 还是 20?」
与项目 engines 字段/文档对齐;主路径先钉一版(如 20),需要兼容验证时用 matrix node: [20, 22]。
-
「构建时内存不够(OOM)?」
Next 大项目偶见堆内存不足,可加 env: NODE_OPTIONS: --max-old-space-size=4096。托管 runner 7GB 内存,够绝大多数场景;仍 OOM 说明构建本身要瘦身(依赖、图片、未用代码)。
错法对照
| 错法 | 问题 | 正解 |
|---|
CI 用 npm install 且不提交 lock | 版本漂移,复现困难 | 提交 lock + npm ci |
| 只跑 lint 不 build | 假绿,上线才炸 | lint + test + build |
| 在仓库根执行 npm 脚本 | 找错 package.json | working-directory: platform |
依赖本机 .env.local | CI 缺变量假红 | workflow env 或 .env.example |
把敏感值放进 NEXT_PUBLIC_* | 打进客户端产物公开可见 | 敏感值走服务端 env / secrets |
卡点急救
npm ci 报 lock file ... does not satisfy:lock 与 package.json 不同步。本地在 platform/ 跑一次 npm install,提交更新的 package-lock.json;顺带检查是不是有人手改了 package.json 没装。
- build 报
NEXT_PUBLIC_XXX is not defined 或行为异常:列出项目构建期真正读取的公开变量,全部写进 build step 的 env;区分构建期(NEXT_PUBLIC_*)与运行期(服务端 process.env)变量,前者必须 build 时存在。
- 构建耗时从 2 分钟涨到 10 分钟:先看是否缓存 miss(lock 变更属正常);再看是否新增了巨型依赖或开启了未用的分析/追踪;必要时把 lint 与 build 拆成并行 job 缩短墙钟。
面试常问
-
npm ci 和 npm install 有什么区别?CI 里为什么用 ci?
npm install 按 package.json 解析并可能更新 lock;npm ci 严格按 lock 全新安装,lock 与 manifest 不一致时直接失败。CI 要的是可复现:同一 lock 永远装出同一依赖树,还能顺带把「lock 忘了提交」这种问题在 PR 阶段暴露。
-
Next.js 的 NEXT_PUBLIC_* 环境变量有什么特殊性?对 CI 有什么要求?
以 NEXT_PUBLIC_ 开头的变量在构建时被内联进客户端产物,所以必须在 next build 时可用且会随构建固化——运行时改环境变量不生效,敏感值绝不能放这里。因此 CI 的 build step 要显式提供这些变量(或约定默认值),并且构建用的值要与部署环境约定一致。
-
前端 CI 门禁应该包含哪些环节?为什么?
依赖安装(npm ci 保可复现)→ lint(秒级,便宜检查放前面)→ 测试(单元/组件)→ 生产构建(catch 类型与编译期错误)。顺序遵循「便宜的先跑」:lint 3 秒挂掉就不浪费后面几分钟的 build。如果产出镜像,build 可挪进 Docker 多阶段构建,CI 门禁至少保证能产出可运行包。
自测三问
- 为什么 CI 推荐
npm ci 而不是 npm install?
- platform job 建议的四个命令阶段是什么?顺序为什么这样排?
- monorepo 里
cache-dependency-path 应指向哪个文件?
小结一句话
npm ci + lint + test + build(含必要的构建期 env)是本站 Next.js 前端 CI 四件套。
延伸