☰
Nuxt 4环境变量与runtimeConfig配置实战指南
2026/10/7 5:06:46 网站建设 项目流程

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_BASE=https://staging.example.com,同时系统环境变量里也有 NUXT_PUBLIC_API_BASE,最终生效的是系统环境变量。

这个链路背后的设计逻辑是:代码仓库里应该只有默认值,本地差异写进 .env 且不进仓库,真正的环境差异由部署平台注入系统环境变量。这样换环境不用改代码、不用改文件,只要调整部署平台的配置就行。

环境变量是如何映射到 runtimeConfig 字段的呢?规则很简单:变量名以 NUXT_ 开头,后面接 runtimeConfig 的字段路径,路径层级用 _ 分隔,全部大写。比如 runtimeConfig.public.apiBase 对应 NUXT_PUBLIC_API_BASE,runtimeConfig.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(),发现整个对象里私有字段的值确实是 undefined,public 字段正常。查了代码,确认组件没有写在服务端专属目录。最后怀疑是否字段名拼错了,对比 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_BASE=https://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.ts:

declare 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/schema',Nuxt 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_BASE,Nuxt 会用 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 的变量管理理顺,少走我之前走过的弯路。

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

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

立即咨询