Nuxt 4环境变量与runtimeConfig配置实战指南
1. Nuxt 4 目录结构变化为什么最先崩的是环境变量我是在把项目从 Nuxt 3 升级到 Nuxt 4 的时候第一次认认真真把环境变量和 runtimeConfig 的机制翻了个底朝天。之前 Nuxt 3 项目里那套“随便在 .env 里写个变量代码里 process.env 直接用”的做法到了 Nuxt 4 里开始花式出问题有的变量在服务端好好的客户端一访问就是 undefined有的值改了十几遍重启 dev server 才生效最诡异的是明明 production 构建成功了线上却用着本地调试的 API 地址。很多人把这些问题归结为“Nuxt 4 的锅”实际上是因为 Nuxt 4 换了一套目录结构和构建模型旧习惯还没跟上。这篇文章我打算从 Nuxt 4 的实际行为出发把环境变量和 runtimeConfig 怎么配合、怎么配置、怎么排错讲透。内容会涉及目录结构、构建时与运行时的区别、配置映射规则、类型安全、多环境部署方案适合正在用 Nuxt 4 写项目、或者准备从 Nuxt 3 升级的开发者。1.1 app/ 目录与 server/ 目录带来的读取路径问题Nuxt 4 最直观的变化是目录重构应用代码从根目录移到了 app/ 子目录服务端代码放在 server/。components/、composables/、pages/ 这些文件夹全部挪进了 app/ 下面nuxt.config.ts 还是在项目根目录。这个改动对环境变量最直接的影响是很多人开始纠结 .env 到底放根目录还是放 app/ 目录。如果你把 .env 放进了 app/Nuxt 4 默认是不认的因为它的 env 加载逻辑仍然以项目的 cwd当前工作目录为基准。项目根目录那个 .env 才会被自动加载。我见过一个同事的惨痛经历他为了让“应用目录更清爽”把 .env 挪到了 app/.env结果服务端 API 密钥全部失效本地一直用的 fallback 空值接口疯狂 401。排查半天才发现是文件位置不对。除了 .env 的位置public/ 目录也变了。Nuxt 4 中静态资源默认放在 app/public/对应的 public runtimeConfig 路径和资源访问路径也都在 app/ 下。这个看似跟环境变量无关但如果你在配置里写了相对路径、又依赖旧的根目录结构构建出来的客户端资源地址就会错位现象就是“线上静态资源 404、环境变量好像也没生效”。1.2 构建模型变了客户端 bundle 里到底固化了什么Nuxt 3 到 Nuxt 4 的构建模型也有调整。服务端渲染和客户端打包的边界更清晰了服务端用 Nitro 作为独立运行时客户端则是标准的 Vite 构建产物。这意味着什么意味着服务端有自己的运行时环境可以在进程启动时读取真实的系统环境变量而客户端代码一旦被打包浏览器里根本没有“环境变量”这个概念所有配置必须在构建时被编译进 bundle。所以你在客户端组件里直接写 process.env.API_BASE_URL大多数情况下是会失效的。Vite 在构建时也许会把部分 process.env 替换成字符串但替换规则跟 Nuxt 的 runtimeConfig 完全不是一回事更别提浏览器运行阶段根本没法动态感知环境变量。正确的姿势只有一个通过 runtimeConfig 定义配置项服务端用 useRuntimeConfig() 读取客户端只读取 public 部分的配置。这个过程自动完成了“构建时固化”和“运行时读取”的分工不需要你手动处理 process.env。2. runtimeConfig 的工作机制客户端与服务端的配置边界2.1 公开配置与私有配置的分水岭runtimeConfig 里最重要的概念就是 public。看下面这个典型配置// nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { apiSecret: , // 私有配置只在服务端可读 dbPassword: , public: { apiBase: https://api.example.com, // 公开配置客户端可读 siteName: My App } } })apiSecret 和 dbPassword 是私有配置只存在于服务端 Nitro 运行时。public 对象下的 apiBase 和 siteName 会被序列化随后注入到客户端 bundle 里。记住一条铁律凡是会进客户端 bundle 的配置都不能放密钥、Token、数据库账号这类敏感信息。很多人觉得“public 字段名的意思是我主动暴露那我不写在 public 里不就安全了”这句话对一半。只要你的代码在客户端组件里调用了服务端私有的 runtimeConfig 字段Nuxt 不会直接报错而是返回 undefined从现象上“保护”了值。但如果你不小心把密钥写在 public 里或者把本应私有的值通过注入的方式塞给了客户端那就是纯纯的泄露事故了。我常用的一个判断方法问自己“这个配置如果被所有用户看见会有问题吗”公众配置包括 API 基础地址、网站名、CDN 域名、埋点开关私有配置包括数据库连接串、第三方 API 密钥、管理员Token。把这两类分开写是 runtimeConfig 正确使用的第一课。2.2 从 nuxt.config.ts、.env 到最终运行值的映射链路runtimeConfig 的值有多个来源它们的覆盖顺序非常关键。实际运行时Nuxt 会按下面的优先级合并配置优先级来源说明低nuxt.config.ts 中的 runtimeConfig 默认值兜底配置代码里写死的初始值中.env 文件中的 NUXT_ 变量本地开发或构建时从文件读取高系统环境变量process.env部署平台或 shell 中注入的真实环境变量也就是说如果 nuxt.config.ts 里写了 apiBase: https://dev.example.com.env 里写了 NUXT_PUBLIC_API_BASEhttps://staging.example.com同时系统环境变量里也有 NUXT_PUBLIC_API_BASE最终生效的是系统环境变量。这个链路背后的设计逻辑是代码仓库里应该只有默认值本地差异写进 .env 且不进仓库真正的环境差异由部署平台注入系统环境变量。这样换环境不用改代码、不用改文件只要调整部署平台的配置就行。环境变量是如何映射到 runtimeConfig 字段的呢规则很简单变量名以 NUXT_ 开头后面接 runtimeConfig 的字段路径路径层级用 _ 分隔全部大写。比如 runtimeConfig.public.apiBase 对应 NUXT_PUBLIC_API_BASEruntimeConfig.apiSecret 对应 NUXT_API_SECRET。Nitro 层面的 runtimeConfig 也支持自定义映射如果你有一些非 NUXT_ 前缀的外部环境变量可以通过 nitro.runtimeConfig 或一些额外的配置来做映射。但说实话常规 Nuxt 4 项目用 NUXT_ 前缀就够了除非是要对接第三方的固定环境变量。2.3 一个容易忽视的事实public 配置是构建时写死的这一点我必须重点强调因为它解释了非常多“线上改了没生效”的疑难杂症。public 下的配置在构建时会被编译进客户端 bundle。也就是说你执行 nuxi build 的那一刻apiBase 的值就被固化成字符串写死在 JS 文件里了。之后你在服务器上修改系统环境变量客户端 bundle 不会自动变必须重新构建、重新部署前端产物。服务端的私有配置不一样。Nitro 进程启动时读取环境变量运行时再通过 useRuntimeConfig() 获取。修改环境变量后只要重启服务端进程或触发 Nitro 的 reload新值就能生效不需要重新 build。我遇到过一个典型案例测试环境部署脚本里改了 NUXT_PUBLIC_API_BASE然后只重启了 Node 服务没重新构建。结果打开页面一看浏览器发出的请求还是旧的 API 地址。后来团队成员在构建日志里发现 public 配置已经固化才明白“重启进程不等于重新构建”。这个问题的本质是客户端代码没有运行时环境它面对的是浏览器而不是操作系统进程。所有需要客户端感知的配置必须在“构建时”完成注入所有只需要服务端感知的配置才可以在“运行时”灵活读取。理解了这一层你对 runtimeConfig 的设计意图就掌握了一半。3. 环境变量命名、类型转换与读取时机细节里的魔鬼3.1 NUXT_ 前缀映射规则与大小写陷阱前面说了映射规则NUXT_ 路径 _ 分隔 大写。但真正实操的时候有几个细节要留意。首先是大小写。NUXT_PUBLIC_API_BASE 能映射到 runtimeConfig.public.apiBase中间路径的 public 也必须小写。有人手一抖写成了 NUXT_PUBLIC_ApiBase结果怎么都不生效。环境变量名本身通常是大小写敏感的NUXT_PUBLIC_APIBASE 和 NUXT_PUBLIC_API_BASE 是两个完全不同的变量。其次是连字符问题。runtimeConfig 的字段名如果用了驼峰对应环境变量里用 _ 分隔即可。但如果字段名里本身有下划线或者数字映射起来容易混。比如 runtimeConfig.public.api_base对应环境变量 NUXT_PUBLIC_API_BASE而字段 api_base 里的下划线正好和路径分隔符重合解析时 Nuxt 是按点路径展开的所以 NUXT_PUBLIC_API_BASE 实际映射的是 public.apiBase 还是 public.api_base取决于你在 nuxt.config.ts 里的字段命名。最好的习惯是runtimeConfig 字段统一用驼峰命名法环境变量统一用大写下划线。这样可以最大化减少歧义。3.2 布尔值、数字到底会不会被转换另一个高频问题环境变量全是字符串配置里需要布尔值和数字怎么办Nuxt 对 runtimeConfig 的处理有一点“智能转换”。在服务端读取时字符串 true 会被转成布尔值 true字符串 123 会被转成数字 123。这个转换并不是万能的它主要应用在 NUXT_ 环境变量与 runtimeConfig 合并的过程中。但对象和数组不会自动解析。比如你在 .env 里写NUXT_PUBLIC_ALLOWED_ORIGINS[https://a.com,https://b.com]最终读到的很可能是一整个字符串 [https://a.com,https://b.com] 或者带引号的字符串而不是真正的数组。因为环境变量的本质就是文本Nuxt 不会自作主张去 JSON.parse。碰上这种情况我的建议是拆细粒度字段或者用公共配置项配合 JSON 序列化。比如在 nuxt.config.ts 里定义 allowedOrigins 为字符串客户端读取后用 JSON.parse 处理。不过要注意 parse 失败时的兜底逻辑。还有一个常见问题空字符串和 undefined 的区别。如果你在 nuxt.config.ts 里写了 apiSecret: 那么这个字段是存在的只是值为空。假如某个第三方 SDK 要求环境变量必须存在你在服务端读取时要做非空判断而不是直接传空字符串给 SDK。3.3 useRuntimeConfig 该在哪儿调用不该在哪儿调用useRuntimeConfig() 是访问 runtimeConfig 的主要入口但它对调用位置有隐式要求。在 Nuxt 组件、composable、插件、中间件里调用没问题在普通的 .ts 工具模块顶层调用或者在模块加载阶段调用是拿不到正确值的。因为 useRuntimeConfig 依赖 Nuxt 的应用上下文Nuxt context。一个工具模块如果被服务端和客户端同时引用又不在组件渲染链路里你就不能确定它当前的上下文是什么。我自己更倾向的做法是工具函数不直接调用 useRuntimeConfig而是把需要的配置作为参数传进来。比如写一个请求封装函数// utils/request.ts export function createApiClient(baseURL: string) { return $fetch.create({ baseURL }) }在组件或 composable 里通过 useRuntimeConfig() 拿到 apiBase 后再传给 createApiClient。这样代码的可测试性也更好不用跑在 Nuxt 环境里就能单测。在 server/ 目录的 Nitro 代码里也有一个全局的 useRuntimeConfig用法类似但它读取的是真正的服务端运行时配置。注意 server/api 下也可以直接用不要再用 process.env 去读因为有些环境变量在 Nitro 打包后已经无法直接访问了。4. 我在环境变量上踩过的四个坑以及完整的排查链这一章我把自己在 Nuxt 4 项目里真实踩过的坑整理出来每个都附上排查思路避免你在同样的问题上浪费一晚上。4.1 坑一客户端组件里出现 undefined 的私有配置现象页面渲染出来某个配置项在客户端显示为 undefined服务端日志里却是正常值。排查过程我先在组件里打印 useRuntimeConfig()发现整个对象里私有字段的值确实是 undefinedpublic 字段正常。查了代码确认组件没有写在服务端专属目录。最后怀疑是否字段名拼错了对比 nuxt.config.ts 后才发现原来是客户端组件代码里试图读取 apiSecret。这是 runtimeConfig 的预期行为不是 bug。客户端拿不到私有配置设计如此。正确做法是检查这个配置是否真的需要进客户端如果需要就放到 public 下如果不需要就明确它只能服务端读取不要在客户端代码里引用。4.2 坑二改了 .env 但值没变是缓存还是没重启现象本地开发时修改了 .env 里的 NUXT_PUBLIC_SITE_NAME刷新浏览器页面网站名还是旧的。排查过程一开始以为是 Nuxt 的缓存问题按网上教程删了 .nuxt 目录重新 dev依然没变。后来才发现dev server 进程一直没停.env 的改动没有被 Nuxt 监听到需要手动重启 nuxi dev。Nuxt 3 以后的版本对 .env 的修改不是每次都自动 apply。最稳妥的做法是改了 .env 就重启 dev server。这个问题很多人踩我后来习惯在修改 .env 后顺手重启不再纠结有没有热更新。如果是构建后的环境变量没生效回到前面说的构建时固化问题public 配置改了必须重新构建服务端私有配置改了需要重启 Nitro 进程。4.3 坑三对象和数组在 .env 里存不出来现象在 .env 里写了 NUXT_PUBLIC_NAV_LINKS[{text:Home,path:/}]读取后拿到的是一整个字符串JSON.parse 还报错。排查过程先确认 .env 的引号和转义没问题又试了不同的括号风格最后阅读 Nuxt 文档才意识到环境变量映射本身不负责解析复杂结构。runtimeConfig 只能做简单类型转换复杂 JSON 需要自己解析。我最终放弃在 .env 里放 JSON改为在 nuxt.config.ts 里用函数生成默认值再通过简单的环境变量开关或按环境动态计算。比如runtimeConfig: { public: { navLinks: process.env.NUXT_PUBLIC_NAV_LINKS ? JSON.parse(process.env.NUXT_PUBLIC_NAV_LINKS) : [{ text: Home, path: / }] } }这种方法至少保证构建时能正确解析但注意 JSON.parse 如果失败整个 Nuxt 构建会异常所以需要 try/catch。4.4 坑四配置覆盖顺序搞反生产环境用了开发值现象同事在 .env 里写了 NUXT_PUBLIC_API_BASEhttps://dev-api.example.com然后这个 .env 被提交进了仓库因为配置分散管理生产构建时系统环境变量里也有同名变量但最终线上跑的是开发地址。排查过程当时以为是环境变量优先级不对还翻了好一会儿源码。后来发现生产部署脚本会把整个项目目录包括 .env复制到服务器而 .env 中变量的优先级高于 nuxt.config.ts 默认值所以生产构建读取到了仓库里的开发值。这里的问题不是 Nuxt 的覆盖顺序而是不该把 .env 提交到仓库。.env 应该加入 .gitignore仓库里只保留 .env.example 模板。本地差异文件不共享生产环境用部署平台注入的系统环境变量覆盖。这套流程理顺之后再也没有发生过环境串值的问题。5. 让 runtimeConfig 有类型TypeScript 增强与编辑器提示5.1 声明模块增强的两种写法runtimeConfig 的键值最初在 nuxt.config.ts 里只是普通对象你在 useRuntimeConfig() 时拿到的类型是宽泛的写起来没有提示拼错字段名也不报错。借助模块增强可以让 Nuxt 知道你的 runtimeConfig 具体有哪些字段。在 Nuxt 4 中可以创建一个 types/runtime-config.d.tsdeclare module nuxt/schema { interface RuntimeConfig { apiSecret: string dbPassword: string public: { apiBase: string siteName: string } } } export {}之后在代码中const config useRuntimeConfig() console.log(config.apiSecret) // 有类型提示 console.log(config.public.apiBase) // 有类型提示 config.notExist // 会报 TS 错误需要注意的是模块路径。Nuxt 3 时代习惯写 nuxt/schemaNuxt 4 里推荐使用 nuxt/schema。如果你的 IDE 里没识别确认一下项目安装的 Nuxt 版本对应的声明入口。还有一种写法是直接增强 NuxtConfig 类型用于 nuxt.config.ts 内部的类型校验declare module nuxt/schema { interface NuxtConfig { runtimeConfig?: { apiSecret?: string public?: { apiBase?: string } } } }我个人的经验是RuntimeConfig 增强已经覆盖了大部分场景NuxtConfig 增强在你封装模块、动态生成 config 时更有用。5.2 类型安全带来的实际收益有了类型之后收益是立竿见影的。最大的好处是“重构时能发现问题”。比如某个字段从 apiBase 改名为 apiEndpoint直接全项目搜索引用点TS 编译器会列出所有编译报错而不是等运行到某个页面才发现 undefined。另一个收益是安全意识的强化。当你把 RuntimeConfig 里的字段定义清楚了public 和私有字段一目了然。在客户端代码里不小心引用了私有字段即使类型检查没拦住review 代码的人也能通过类型声明很快看出问题。对于团队项目我强烈建议把 runtime-config.d.ts 作为项目初始化的一部分提交到仓库。新成员接手时看一眼这个文件就知道项目里有哪些配置、哪些能进客户端、哪些是敏感的。6. 多环境工程化方案本地、测试、预发布、生产的配置组织6.1 基于 .env 文件的按环境拆分本地开发最简单的方案就是根目录一个 .env写本地专用配置。但项目一旦有测试环境、预发布环境、生产环境单文件就不够用了。Nuxt 支持按环境自动加载 .env.[NODE_ENV] 之类的文件但实际用起来有些细节要小心。我更推荐的组合方式是.env本地开发默认配置不提交仓库.env.example所有字段的示例模板提交仓库真正的环境差异交给部署平台注入系统环境变量部署平台比如 Docker、GitHub Actions、Vercel、自有 CI/CD上直接配置 NUXT_PUBLIC_API_BASE、NUXT_API_SECRET 等变量。这样代码仓库里没有任何真实密钥环境配置的变更也有平台审计记录。6.2 部署平台注入环境变量的注意事项在部署平台注入环境变量时有一个问题经常被忽略构建阶段和运行阶段的环境变量是分开的。很多平台构建和运行是两个阶段public 配置在构建阶段就需要被读取并固化所以构建阶段的系统环境变量也必须配好。如果构建阶段没有配置 NUXT_PUBLIC_API_BASENuxt 会用 nuxt.config.ts 里的默认值完成构建线上就出现了“默认 API 地址”。这时候你只在运行阶段改环境变量是无济于事的必须保证构建阶段和运行阶段同时具备正确的 public 配置。私有配置则主要影响运行阶段。构建时可以不用填真实值运行阶段由 Nitro 进程读取系统环境变量。你可以让构建阶段的 NUXT_API_SECRET 为空占位等容器启动时再注入。6.3 最终推荐的项目配置布局我目前维护的 Nuxt 4 项目配置部分长这样project/ ├── .env # 本地开发gitignore ├── .env.example # 环境变量模板提交仓库 ├── nuxt.config.ts # runtimeConfig 默认值 ├── types/ │ └── runtime-config.d.ts # RuntimeConfig 类型增强 ├── app/ # 应用代码 ├── server/ # 服务端 API 与业务逻辑nuxt.config.ts 里只保留最保守的默认值比如 apiSecret 为空字符串、apiBase 为 localhost。本地开发通过 .env 覆盖测试和环境部署通过平台注入覆盖。这套布局我在多个项目里验证下来很稳定核心思路就一句话默认值写代码差异值走环境。不要让环境配置散落在代码的不同位置集中管理类型明确构建和运行阶段分开对待。最后再分享一点个人体会环境变量这些年被很多人当成“小事”直到线上出事故才意识到它的分量。runtimeConfig 设计得好项目换环境时只需要改部署平台配置设计得随意switch 环境比切换数据库还痛苦。我个人在踩完这些坑之后给自己定了一个规矩任何新接手的 Nuxt 项目第一件事不是看页面结构而是先找 nuxt.config.ts 里的 runtimeConfig 和 .env.example把配置边界理清楚。配置清晰了后面的开发效率高很多。希望这篇文章也能帮你把 Nuxt 4 的变量管理理顺少走我之前走过的弯路。