项目:react-travel | 版本:v1.1 | 生成日期:2026-10-06 | 依据:源码分析
本篇是配置的唯一权威清单:8 个环境变量逐一标注源码读取位置(而非仅凭 .env.example),外加配置文件走读(next.config.js/prisma.config.ts/tsconfig.json)、package.json 全部 scripts 说明、scripts/ 目录两个脚本、以及 standalone 双形态产物分析。《06-部署运行》已给出部署视角的精简表,本篇补充逐变量的实现细节与两类生命周期(构建期内联 vs 运行时读取)的完整推演。
先给变量总表(读取位置均为精确到函数/行的首次消费点):
| # | 变量 | 源码读取位置 | 生命周期 | 必填性 | 默认值 |
|---|---|---|---|---|---|
| 1 | DATABASE_URL | lib/prisma.ts(运行时连接串)、prisma.config.ts(CLI)、prisma/seed.ts 第 15 行、scripts/migrate-mysql-to-pg.ts | 运行时 | 必填(无它进程可启动但任何查询失败) | .env.example 给出示例连接串 |
| 2 | JWT_SECRET | lib/auth.ts 第 5 行(模块加载时读入常量) | 运行时 | 生产必填(守卫强校验) | 开发回退 "xiaoye" |
| 3 | SALT | lib/utils.ts 第 12 行(模块加载时读入) | 运行时 | 生产必填(守卫强校验) | 开发回退 "xiaoye" |
| 4 | JWT_EXPIRES_IN | lib/auth.ts 第 24 行 | 运行时 | 可选 | "7d" |
| 5 | REDIS_URL | lib/auth.ts#getRedis(第 75-79 行,惰性建连) | 运行时 | 可选 | 未配置 = 跳过存在性校验 |
| 6 | CITYS_API | app/api/commons/citys/route.ts 第 5 行(模块常量) | 运行时 | 可选 | 内置慕课接口(含硬编码 icode) |
| 7 | NEXT_PUBLIC_SITE_URL | lib/seo.ts 第 7-9 行 | 构建期内联 | 可选 | https://travel.zhangzhengyang.com |
| 8 | MYSQL_URL | scripts/migrate-mysql-to-pg.ts(仅脚本读取) | 脚本运行时 | 仅迁移脚本需要 | 无 |
.env.example 的收录口径:1-6 号有注释条目(4/6 注释掉的可选项);7 号 NEXT_PUBLIC_SITE_URL 与 8 号 MYSQL_URL 未列入——前者是文档化遗漏(lib/seo.ts 第 3 行注释有说明"可用 NEXT_PUBLIC_SITE_URL 覆盖"),后者是一次性脚本专用(按需临时设置),两者都是"读代码才知道"的隐藏配置,这正是本篇存在的理由。
两类生命周期的推演(改配置前必懂):运行时变量(1-6、8 号)由 Next 服务进程在启动时经 dotenv 加载(Next 内建 .env 支持,优先级 .env.production.local > .env.local > .env.production > .env,CLI 场景另由 prisma.config.ts 的 import "dotenv/config" 显式加载),改动只需重启进程。构建期内联变量(7 号)以 NEXT_PUBLIC_ 前缀标识,Next 在 next build 时把引用点替换为字面量——lib/seo.ts 的 SITE_URL 被 metadataBase(app/layout.tsx 第 11 行)、sitemap/robots 绝对链接、JSON-LD、OG 图地址全链消费,改域名必须重新 build,只改运行环境变量不会有任何效果。这是排障高频误区:站点分享图/sitemap 域名"配了不生效",先查是否重新构建。
把上图画成流程后还有三个值得点破的细节。第一,构建期与运行期的 .env 是两次独立的读取:构建机上的 .env 决定内联值,运行机上的 .env 决定运行时值——两台机器配置不一致时,产物里"冻结"的站点域名与运行时读到的数据库可以是两个世界的值,这既是灵活性也是坑。第二,密钥守卫的执行时机在"模块加载"而非"首个请求":lib/auth.ts 与 lib/utils.ts 是被几乎所有 Route Handler 与 RSC 页面引用的模块,Next 启动加载首个触达它们的代码时守卫即执行,因此密钥错误的表现是进程秒退而非慢速失败——这排障时反而省事(《12-FAQ》入口 A)。第三,NEXT_PHASE !== "phase-production-build" 这个条件是 Next 官方的构建阶段标记,它让"构建机不配密钥也能出包"成为可能;如果你自研 CI 脚本里用 next build 之外的途径触发预渲染(如直接 import lib 模块做测试),要注意该标记只在 next build 进程内存在,脱离它跑生产代码会误触守卫。
1. DATABASE_URL。PostgreSQL 连接串,示例 postgresql://postgres:postgres@localhost:5432/travel?schema=public。四个消费点行为不同:lib/prisma.ts 把它交给 PrismaPg({ connectionString }) 驱动适配器(! 非空断言意味着缺失时运行期 undefined 传入、首个查询报连接错误——它没有启动守卫,是唯一"无守卫的必填项",排障见《12-FAQ》入口 A);prisma.config.ts 经 env("DATABASE_URL") 供 Prisma CLI(push/generate/studio)使用;seed 与迁移脚本自建 PrismaClient 时直读。指向的库必须预先存在(db push 建表不建库)。2. JWT_SECRET:HS256 签名密钥,lib/auth.ts 的守卫逻辑——生产运行时(NODE_ENV=production && NEXT_PHASE !== "phase-production-build")缺失或等于 "xiaoye" 直接 throw;开发回退 + console.warn。更换密钥的连带效应:所有已签发 token 立即失效(全员重登)、Redis 缓存值与新 token 的比对不受影响(缓存按新登录覆盖)。3. SALT:仅参与存量 md5(password + SALT) 校验(md5Password),新 bcrypt 账户不依赖它——但守卫仍然强校验,理由写在注释里:"配置错误意味着所有老账户都无法登录,宁可启动失败及早暴露"。更换 SALT = 存量 md5 账户全员不可登录,这是比换 JWT_SECRET 后果更具体的操作,动前必须确认存量账户已迁移 bcrypt(《13-技术债务与演进建议》§2/D5)。4. JWT_EXPIRES_IN:parseExpiresToSeconds 支持 (\d+)(s|m|h|d|w|y) 格式(如 "12h"、"30d"),解析失败回落 7×86400 秒;该值同时决定 JWT 的 expiresIn 与 Redis token 缓存的 TTL(两者对齐的设计动机见 lib/auth.ts 第 85 行注释)。5. REDIS_URL:如 redis://localhost:6379;ioredis 以 maxRetriesPerRequest: 1, connectTimeout: 5000 创建并缓存在模块级单例(getRedis),宕机 fail-open。未配置时登录注销退化为纯客户端行为(旧 token 有效期内仍可用,语义见《08-安全与权限设计》§7.5)。6. CITYS_API:覆盖 app/api/commons/citys/route.ts 的内置常量(https://apis.imooc.com/?icode=B2060B086C0D78F9,注释说明这是"外部接口凭证失效时的应急切换开关");fetch 带 next: { revalidate: 86400 } 缓存 1 天。7. NEXT_PUBLIC_SITE_URL:见 §1 生命周期推演;尾部斜杠会被 .replace(/\/+$/, "") 归一(lib/seo.ts 第 9 行),staging 域名联调时设置它可让 sitemap/OG 全链指向 staging。8. MYSQL_URL:旧 MySQL egg_house 库连接串,仅 scripts/migrate-mysql-to-pg.ts 读取;脚本依赖 mysql2(不在 dependencies,需临时 pnpm add -D mysql2)。
next.config.js 共六块配置,每块都有注释记载动机:① reactStrictMode: true——开发期 effect 双跑,项目以三处 ref 守卫(recommendedFetched/reportedView/installed)配合,等于免费的 effect 幂等演练;② output: "standalone"——产出含 server.js 与裁剪版 node_modules 的独立部署产物,注释"便于 NAS 容器直接运行"(形态细节见 §6);③ images.disableStaticImages: true——对齐 CRA 的 import img 返回 URL 字符串语义(兼容层支柱之二,《01-系统架构》§2.3);④ images.remotePatterns——放行全部 http/https 主机(当前无 next/image 消费方,收紧建议见《13》D10);⑤ rewrites()——单条 /auth/:path* → /api/auth/:path*,服务旧前端 slice 的登录/注册路径;⑥ webpack 追加 asset/resource 规则(static/media/[name].[hash:8][ext] 文件名带 hash 支撑长缓存)。文件底部还有一条重要的"负配置"注释:不要用 redirects 做 /signIn → /signin——Next 的 redirects 匹配大小写不敏感会命中 /signin 自身造成无限重定向,归一化在 lib/router.tsx#normalizePath 内完成。该文件是纯 JS(module.exports),无实验性 flag,Next 15.1 下语义稳定。
配置评审视角再补三句判断。其一,这六块配置里五块是为兼容层服务或由迁移决策派生(②③⑤⑥直接服务旧代码零改动,①是迁移期防御),真正"新架构原生"的只有 remotePatterns——判断哪些配置可随兼容层退役一起清退(《13-技术债务与演进建议》§3 第四轮)是维护效率的关键线索。其二,rewrites 与 redirects/headers 一样属于请求管道层,在 middleware 之前执行,所以 /auth/login 先被改写成 /api/auth/login、再进入 middleware 的 matcher 判定——理解这个顺序才能解释为什么 middleware 的 PROTECTED_API 列表里写的是 /api/auth 不在列表(auth 前缀天然公开)而看不到 /auth。其三,webpack 数组追加写法(config.module.rules.push)依赖 Next 内部 webpack 配置形状,Next 大版本升级(如迁移 Turbopack 默认化)时这是第一个需要复查的位置——把它的存在记入升级检查清单,可以避免一次"升级后图片 import 全部变成对象"的夜间事故。
prisma.config.ts(Prisma 7 新配置入口)只有三件事:import "dotenv/config" 显式加载 .env(保证 CLI 子进程拿到 DATABASE_URL——这就是"db push/seed 必须在项目根目录执行"的原因);defineConfig 声明 schema: "prisma/schema.prisma";datasource.url: env("DATABASE_URL")。注意 schema.prisma 本身的 datasource db 块只写 provider = "postgresql" 不写 url(Prisma 7 允许 url 外置到 config),generator client 用新版 prisma-client 生成器输出到 ../generated/prisma(《04-数据模型》§1)。tsconfig.json 的关键项:strict: true(质量门禁一的基础);paths: { "@/*": ["./*"] } 别名;include 覆盖 next-env/types/app/lib/prisma/client/scripts 与 .next/types;exclude: ["node_modules", "generated"] 隔离生成物;moduleResolution: "bundler" 与 jsx: "preserve" 为 Next 标配。pnpm-workspace.yaml 不是多包工作区,只用了 allowBuilds 白名单放行四个包的构建脚本(@prisma/engines、prisma、sharp、@parcel/watcher)——pnpm 10+ 默认阻止依赖的 postinstall,此文件是 prisma generate 能在 install 后自动执行的先决条件,删掉它 install 后就会找不到 generated/prisma(《12-FAQ》坑 2 的另一种触发方式)。.gitignore 覆盖 .env 与 .env*.local、.next/、/generated、tsconfig.tsbuildinfo——生成物与密钥均不入库。
.env 文件的组织建议(源码之外的运维实务,供落地参考):本地开发只需 .env 一个文件,内容从 .env.example 拷贝后改 DATABASE_URL 即可跑通(密钥留空走 xiaoye 回退 + 控制台警告);生产环境建议遵循"运行目录 .env 存非敏感项 + 进程环境变量注入敏感项"的分层——DATABASE_URL/REDIS_URL/JWT_EXPIRES_IN/CITYS_API 放文件便于容器编排统一管理,JWT_SECRET/SALT 走编排系统的 secret 注入(docker secret、systemd EnvironmentFile 或 NAS 的环境变量面板),避免强随机密钥以明文躺在磁盘。两个容易忽视的卫生习惯:其一,.env.example 是仓库内唯一应提交的环境文件,修改它(如补充 NEXT_PUBLIC_SITE_URL 文档化条目,见《13-技术债务与演进建议》D11 同类整理)时保持"等号右侧全部为占位值",防止真实凭据经示例文件泄漏;其二,变量名拼写错误不会报任何错——JWT_SECREAT=xxx 这类手误的表现是开发环境静默回退 xiaoye(看得到警告)或生产启动即崩(守卫兜住),唯独"配了但没生效又不报错"的中间态极少出现,这正是守卫设计的价值。
| 命令 | 实际执行 | 用途与要点 |
|---|---|---|
pnpm dev | next dev | 开发服务器 :3000;Prisma query 日志全开;StrictMode 生效 |
pnpm build | next build | 生产构建(内含类型检查 + ISR 页预渲染 + sitemap 生成) |
pnpm start | next start | 标准生产运行(读取完整 .next) |
pnpm postinstall | prisma generate | 装依赖自动生成客户端到 generated/prisma |
pnpm prisma:generate | prisma generate | 手动再生成(切分支/改 schema 后) |
pnpm prisma:push | prisma db push | schema 同步库结构(无迁移历史,操作纪律见《06》§5.5) |
pnpm prisma:studio | prisma studio | 数据可视化 :5555 |
pnpm db:seed | npx -y tsx prisma/seed.ts | 高危:先清空 11 张表再灌种子(8 用户密码 123456 + 32 线路) |
scripts/ 目录两个独立脚本(均无 npm script 封装,需 npx tsx/pnpm exec tsx 手动执行):scripts/migrate-mysql-to-pg.ts(B6 存量迁移)——头注释给出完整用法:pnpm add -D mysql2 → 设 MYSQL_URL(旧库)与 DATABASE_URL(目标)→ pnpm exec tsx scripts/migrate-mysql-to-pg.ts;按 User→House→Imgs→Comment→Orders 依赖序迁移、保留原主键、幂等(目标表非空默认拒绝,--force 清空重灌)、末尾 setval 重置 PG 自增序列;项目上线后已完成使命,不要在生产重跑(《06》§5.3)。scripts/download-images.mts(图片资源重建)——从 prisma/seed-data/*.ts 正则提取 Unsplash photo id(photo-<id> 与 routes.ts 的 U("<id>") 两种写法),统一 w=800&q=80&fm=webp 下载到 public/travel-img/<id>.webp,已存在跳过(断点续跑)、120ms/张限速防风控;仅种子图片引用变更或缺失时需要。执行两个脚本的共同前提:在项目根目录(dotenv 相对路径)+ Node ≥ 20。
生产运行有两种形态,选择依据与各自的坑如下。形态 A:pnpm start(标准模式)——直接读取完整 .next 目录,需要源码目录中保留全部构建产物与 node_modules(运行时仍会加载部分依赖),适合常规服务器上"仓库即部署目录"的简单运维;无额外拷贝步骤,天然带 public/ 与 .next/static。形态 B:standalone(node .next/standalone/server.js)——next build 时把服务端运行所需的最小 node_modules 与 server.js 一起产出到 .next/standalone/,产物可整体拷贝到任意机器/NAS 容器独立运行,进程只需 Node ≥ 20。三个必须记住的差异:其一,静态资源不随产物——public/ 与 .next/static 必须手工拷入(cp -r public .next/standalone/public && cp -r .next/static .next/standalone/.next/static),遗漏症状是图片/JS/CSS 全 404(《06》§4.2);其二,环境加载以运行目录为准——server.js 加载其所在目录的 .env 与进程环境变量,密钥守卫在启动瞬间执行,缺失即退(《08》§6);其三,PORT/HOSTNAME 环境变量可覆盖默认监听(容器场景标准做法)。两形态共享的行为:ISR 缓存重启清空(发布后 curl 预热)、next start 与 server.js 的路由/中间件语义一致。镜像化(Dockerfile)时的推荐组合是形态 B + 多阶段构建——最终镜像只含 standalone 产物,体积与攻击面最小,落地方案见《13-技术债务与演进建议》§2/D1。运维速记:形态 A 的部署单元是"代码目录",形态 B 的部署单元是"产物目录"——混淆两者(在 standalone 目录里改源码、或在代码目录里找 server.js)是新手常见的十分钟迷路。