☰
Next.js环境变量完全指南:NEXT_PUBLIC_前缀与前后端部署边界
2026/10/1 12:34:42 网站建设 项目流程

说到环境变量,很多人第一反应可能是配置 JDK 或 Python 的 PATH,也可能是 Linux 服务器上那串 export 命令。不同生态做法五花八门,核心诉求却完全一致:把跟环境有关的配置从代码里拆出来,让同一个项目在不同环境里使用不同的值。Next.js 的环境变量机制就是围绕这个目标设计的,但它比普通后端项目多了一层麻烦——代码同时跑在服务端和浏览器两端,环境变量的可见范围必须分清楚。这篇内容我会完整拆解 Next.js 环境变量的加载规则、NEXT_PUBLIC_ 前缀的底层逻辑、服务端与客户端的安全边界,以及我在实际项目里排查过的典型问题。无论你是刚接触 Next.js 的新手,还是已经写了几个项目但被 .env 文件搞晕过的人,都能从中找到可以直接落地的方案。

1. 环境变量在 Next.js 里的角色与加载顺序

1.1 Next.js 如何读取环境变量

很多从其他框架转过来的同学,第一件事就是去装dotenv,然后在入口文件里手动require('dotenv').config()。在 Next.js 里这件事完全不需要做,框架内置了环境变量加载能力。当你执行next dev、next build或next start时,Next.js 会自动读取项目根目录下的.env系列文件,并把里面的键值对注入到process.env中。注意,这里说的是项目根目录,不是src目录,也不是app目录。文件放错位置,框架根本找不到。

这个内置加载机制的底层是@next/env这个包,它本质上封装了 dotenv 的逻辑,并加入了 Next.js 自己的文件优先级和环境模式判断。所以你不必手动引入 dotenv,也不需要自己实现按环境加载文件的逻辑。我见过有人在next.config.js里自己写了一段读取.env文件的代码,结果反而跟框架内置逻辑冲突,导致某些变量时有时无。建议新项目一律使用框架机制,少写自定义逻辑。

1.2 .env 系列文件的分工与优先级

Next.js 支持以下环境变量文件:.env、.env.local、.env.development、.env.production、.env.test。它们的含义和加载优先级可以从下表看出来。

文件名加载条件建议是否提交到仓库
.env所有环境默认加载可以提交,放公共且非敏感的配置
.env.local本地环境覆盖,优先级较高不要提交,默认被 gitignore
.env.development仅当 NODE_ENV 为 development 时加载可以提交,团队共享开发配置
.env.production仅当 NODE_ENV 为 production 时加载可以提交,团队共享生产配置
.env.test仅当 NODE_ENV 为 test 时加载可以提交,保证测试环境一致

这里有一个容易记混的细节:优先级从高到低大致是“真实环境变量 →.env.local→.env.development或.env.production→.env”。也就是说,如果同一个变量在多个文件里出现,取高优文件的值。这种设计的意图很明确:.env提供公共默认值,环境文件提供按环境覆盖,.env.local又允许每个开发者在本地做个性化覆盖,而不影响团队其他人。

特别注意:.env.local在测试环境下不会被加载。这是 Next.js 为了方便自动化测试而定的规矩,因为测试需要可复现、可预测的环境,不能因为某台机器上残留一个.env.local导致测试结果漂移。如果你在跑测试时发现某些变量加载不到,先确认一下是不是把配置写进了.env.local。

1.3 环境变量值的书写格式与常见反模式

.env 文件的格式大部分人已经见过,但一些隐藏的小坑还是值得专门说。基本格式就是KEY=VALUE,每行一个变量,#开头表示注释。value 里面如果包含空格、#、=这类特殊字符,建议用双引号把整个值包起来。比如密码是abc#123,如果你直接写PASSWORD=abc#123,dotenv 解析时会把#123当作注释的一部分,最终拿到的 value 只是abc。这种情况在数据库连接串里尤其常见,连接串往往包含#或@等字符。

另一个常见反模式是在.env文件里写export DATABASE_URL=xxx。dotenv 本身支持不带 export 的写法,但如果你从别的环境复制命令到文件里,多写export在某些解析器下会直接报错。此外,等号两边不要加空格,KEY = VALUE这种写法容易被解析成奇怪的键值,排查起来非常难受。最后再提醒一点:NODE_ENV不能通过在.env里设置来改变,它是 Next.js 内部控制的,由运行命令决定。你在.env里写NODE_ENV=production不会生效,别在这上面浪费时间。

2. 服务端与客户端的边界:NEXT_PUBLIC_ 前缀

2.1 为什么会有 NEXT_PUBLIC_ 前缀

这是 Next.js 环境变量体系里最核心、也最容易踩坑的设计。普通后端项目只有一个运行环境,所有环境变量默认都在服务端,浏览器端根本接触不到。但 Next.js 是前后端一体的框架,同一个代码仓库里既有服务端逻辑,也有发到浏览器执行的客户端代码。问题来了:浏览器端的代码里如果写了process.env.XXX,这个变量到哪去取?

答案就是NEXT_PUBLIC_前缀。带这个前缀的变量,Next.js 在构建阶段会把代码里出现的process.env.NEXT_PUBLIC_XXX直接替换成实际值,然后打包进浏览器产物。也就是说,它本质上是一种“编译期替换”,而不是运行时读取。不带前缀的变量,默认只在服务端可用,浏览器端拿到的永远是undefined。如果你之前写过客户端组件去读process.env.DATABASE_URL然后得到 undefined,那就是撞上了这个边界。

2.2 构建时内联的工作原理

理解 NEXT_PUBLIC_ 的底层原理,很多疑惑会迎刃而解。它靠的是 webpack 的 DefinePlugin 思想,在打包阶段把变量引用替换成字符串字面量。举个例子,你在代码里写:

const apiBase = process.env.NEXT_PUBLIC_API_BASE_URL;

构建时如果.env.production里定义了这个变量,打包工具会把上面这行代码直接替换成:

const apiBase = "https://api.example.com";

整个过程发生在next build阶段,跟运行时无关。这也是为什么你修改了带上 NEXT_PUBLIC_ 前缀的变量之后,只重启next start没用,必须重新执行next build。因为浏览器收到的已经是替换后的静态代码,不是现场去读环境变量。这个道理用一句话概括:NEXT_PUBLIC_ 变量是写给构建时的,不是写给运行时的。

2.3 哪些变量可以放 NEXT_PUBLIC_

既然前缀变量会被打进前端包,那就意味着任何人通过浏览器开发者工具都能看到它的值。所以这里有一条非常明确的安全红线:不要把密钥、密码、Token 等敏感信息放进 NEXT_PUBLIC_ 变量。很多安全事故就是这样发生的,某个人误把NEXT_PUBLIC_SUPABASE_SERVICE_KEY或NEXT_PUBLIC_STRIPE_SECRET写进环境变量,密钥直接被曝光。

适合使用 NEXT_PUBLIC_ 前缀的典型场景有:前端需要访问的 API 基础地址、站点对外域名、第三方 SDK 的公开 Key(比如地图服务的 public key)、灰度开关标志位。这些信息就算公开也不会造成直接损失。凡是需要保密的数据,一律只放在服务端变量里,由服务端读取后按需使用。

2.4 在不同运行环境中的使用位置

服务端组件、路由处理程序和 Server Actions 属于 Node.js 服务端运行环境,可以直接读取不带前缀的环境变量。因为在服务端代码里,process.env就是真实的进程环境,密钥、数据库连接串都在这里读取。这一点完全符合直觉。

客户端组件属于浏览器运行环境,只能读取 NEXT_PUBLIC_ 前缀变量。如果你在一个'use client'文件里直接读不带前缀的变量,拿到的一定是 undefined。这不是配置错了,而是框架刻意做的隔离保护。中间件(middleware.ts)运行在 Edge Runtime,情况比较特殊,它介于服务端和构建产物之间。自托管部署时,middleware 会被打包成边缘函数,构建时读取的变量才能可靠地出现在里面,运行时才注入的宿主机环境变量不一定能访问到。所以我在实际项目中的习惯是:middleware 里只读取 NEXT_PUBLIC_ 变量或确定构建时就存在的配置,不依赖运行时动态注入的敏感项。

2.5 安全地把服务端数据传递给客户端

你可能会遇到这样的需求:页面要展示一些来自数据库或第三方服务的配置信息,这些信息需要用密钥去服务端获取,但展示时又必须在浏览器端渲染。正确的做法是通过服务端组件读取环境变量和处理数据,然后把处理后的结果通过 props 传给客户端组件,而不是把原始密钥通过 props 传下去。

// app/dashboard/page.tsx import { fetchStats } from 'lib/stats'; export default async function DashboardPage() { // 密钥只在这里读 const stats = await fetchStats(); return <StatsChart data={stats} />; }
'use client'; export function StatsChart({ data }: { data: Record<string, number> }) { return <div>{JSON.stringify(data)}</div>; }

这种方式既保证了密钥不会进前端包,又能在浏览器端展示服务端数据。如果你想彻底防止客户端代码误引入服务端模块,可以在lib/stats.ts顶部加上import 'server-only',这样一旦某个客户端组件试图引入这个模块,构建会直接报错。这个技巧我几乎在每个项目里都会用,能提前暴露很多因文件引用混乱导致的安全问题。

3. 实操:配置一个带环境变量的 Next.js 项目

3.1 创建项目与文件结构

先创建一个全新的 Next.js 项目,这里我直接使用当前稳定的 App Router 结构。

npx create-next-app@latest env-demo

项目创建好之后,在根目录创建三个文件:.env.example、.env.local、.env.production。.env.example的作用是给团队一个配置清单,里面不填真实值,只写变量名和注释,提交到仓库里,方便新成员快速了解需要配置哪些项。.env.local放你自己的本地配置,默认被 gitignore,不进仓库。.env.production放生产环境的公共配置,如果变量本身不敏感,可以提交给团队共用。

# .env.example SITE_NAME="Env Demo" DATABASE_URL="postgresql://user:pass@localhost:5432/demo" NEXT_PUBLIC_API_BASE_URL="https://api.example.com" NEXT_PUBLIC_SHOW_BANNER="true"

在.env.local里复制一份并填入本机真实值,比如把 DATABASE_URL 指向本地数据库,NEXT_PUBLIC_API_BASE_URL 指向你本地运行的接口服务。这样本地开发和线上生产走的是完全不同的配置路径,但代码完全一致。

3.2 在服务端组件与客户端组件中使用

接下来在页面里分别演示服务端和客户端读取环境变量的方式。服务端组件可以直接读取任何变量,因为它运行在 Node.js 进程里。

// app/page.tsx export default async function Home() { const databaseConfigured = Boolean(process.env.DATABASE_URL); return ( <main> <p>数据库连接:{databaseConfigured ? '已配置' : '未配置'}</p> <p>站点名称:{process.env.SITE_NAME ?? '未配置'}</p> </main> ); }

客户端组件就不要轻易读取非前缀变量了,这里用一个功能开关演示 NEXT_PUBLIC_ 的典型用法。假设.env.local里设置了NEXT_PUBLIC_SHOW_BANNER="true",那么只有值为字符串"true"时页面才展示公告条。

'use client'; export default function Banner() { const showBanner = process.env.NEXT_PUBLIC_SHOW_BANNER === 'true'; if (!showBanner) return null; return <div className="banner">系统公告:当前为测试环境</div>; }

注意这里的代码不依赖任何运行时接口,构建时就已经决定了 showBanner 的值。如果你想精细控制每个变量在哪个端可用,最好在代码里留下注释,说明该变量的用途和公开性,防止后来的人误改。

3.3 TypeScript 类型增强与运行时校验

默认情况下,process.env.XXX的类型是string | undefined,用起来没问题,但团队协作时很容易因为变量名拼错而踩坑。我习惯在项目里加一个类型声明文件,让编辑器自动补全环境变量名。

// src/types/env.d.ts declare namespace NodeJS { interface ProcessEnv { DATABASE_URL: string; NEXT_PUBLIC_API_BASE_URL: string; NEXT_PUBLIC_SHOW_BANNER?: string; SITE_NAME?: string; } }

这样写之后,在服务端代码里访问process.env.DATABASE_URL就有类型提示了。还有一个更严格的做法是在启动阶段用 zod 对环境变量做运行时校验,只要缺失或格式不对就直接抛出异常,让问题在启动瞬间暴露,而不是等到某个功能真正用到变量时才报错。这个做法适合团队规模较大、环境变量数量较多的项目。

import { z } from 'zod'; const envSchema = z.object({ DATABASE_URL: z.string().min(1), NEXT_PUBLIC_API_BASE_URL: z.string().url(), }); export const env = envSchema.parse(process.env);

这里的取舍很简单:类型声明只解决编码体验,zod 校验解决运行时可靠性。我个人的推荐是至少加上类型声明,如果你的项目涉及支付、权限等关键链路,再上 zod 校验。

3.4 用调试接口快速确认变量是否加载

排查环境变量问题时,与其反复猜测,不如写一个临时接口把加载结果一五一十地暴露出来。下面这个 API 路由会返回几个关键变量的加载状态,注意千万不要把密钥值直接返回给调用方,只返回是否存在。

// app/api/env/route.ts import { NextResponse } from 'next/server'; export async function GET() { return NextResponse.json({ nodeEnv: process.env.NODE_ENV, hasDatabaseUrl: Boolean(process.env.DATABASE_URL), apiBase: process.env.NEXT_PUBLIC_API_BASE_URL ?? null, showBanner: process.env.NEXT_PUBLIC_SHOW_BANNER ?? null, }); }

浏览器或终端里访问http://localhost:3000/api/env,就能看到当前进程实际加载了哪些变量。运行接口的是服务端进程,所以这里能读到 DATABASE_URL 这样的敏感变量是否存在,但不会暴露具体内容。如果你确认.env.local里明明写了某个变量,接口却返回 false,那就可以从文件位置、拼写、加载顺序这几个方向去查。这个接口在生产环境记得删掉或加上鉴权,避免信息泄露。

4. 运行时环境变量:Docker、SSR 与边缘运行时

4.1 构建时替换与运行时读取的本质区别

很多人会混淆“构建时”和“运行时”这两个时间点。Next.js 服务端代码里的process.env在两种情况下表现不一样。动态渲染的服务端组件、API 路由和 Route Handler 在运行时执行,访问的是宿主机进程里真实的环境变量,所以只要你重启服务并注入新值,就能动态生效,不需要重新构建前端。但是带有 NEXT_PUBLIC_ 前缀的变量在构建时已经被替换成字符串了,运行时再改宿主机环境变量,对已经构建好的产物没有影响。

静态生成(SSG)页面还要额外注意:如果页面在构建时预渲染,即使是非前缀变量,也会被内联到生成的 HTML 里。此时修改运行时的环境变量并不会更新这些静态页面,你必须重新触发构建或改为动态渲染才能拿到新值。这个细节在自托管部署中很常见,一不小心就会造成线上页面配置过期。

4.2 Docker 部署时的环境变量注入

Docker 部署是自托管最常见的场景。很多人在容器里遇到的现象是:明明docker run -e DATABASE_URL=xxx传了变量,服务端能读到,但浏览器端拿不到。原因仍然是 NEXT_PUBLIC_ 前缀变量在构建时就已经固化。如果你在构建镜像时没有传入 NEXT_PUBLIC_ 变量,构建产物里的对应值就是 undefined,运行时docker run -e NEXT_PUBLIC_XXX=yyy也不会补进去。

正确做法是把next build和next start分开考虑。构建镜像时把你希望出现在浏览器端的公开变量作为构建参数传入,比如 Dockerfile 里的 ARG 和 ENV;运行时通过docker run -e传入的则是服务端需要动态读取的敏感配置。

docker run -d \ -e DATABASE_URL="postgresql://user:pass@host:5432/db" \ -e JWT_SECRET="your-secret" \ -p 3000:3000 my-next-app

上面的命令适合服务端敏感变量。如果你还想在容器启动时动态改变前端展示的某些公开配置,可以考虑采用 4.3 里的服务端传参方式,用接口或服务端组件把配置下发到前端,而不是强行依赖 NEXT_PUBLIC_ 变量。

4.3 通过服务端向客户端传递动态配置

如果业务上确实需要前端展示一些运行时才能确定的配置,比如 A/B 实验的开关、营销活动展示位,最稳妥的方案是把服务端读取到的环境变量通过 props 传递到客户端组件。

// app/promotion/page.tsx export default async function PromotionPage() { const bannerText = process.env.PROMOTION_BANNER_TEXT ?? 'default'; return <PromotionBanner text={bannerText} />; }

这样做的优势很明显:你不需要在构建时决定这些配置,只要修改宿主机环境变量并滚动重启服务,新配置就能通过服务端组件渲染到页面上。它虽然多了一点代码,但比维护一套复杂的运行时配置框架简单得多。我的建议是:所有需要频繁变化的非敏感配置,都走这条路径;NEXT_PUBLIC_ 只用来承载真正稳定且公开的基础配置。

4.4 边缘运行时的环境变量限制

Next.js 的 middleware 运行在 Edge Runtime,它的打包和执行模型更接近浏览器而不是 Node.js。在这个环境里,环境变量的来源主要是构建时能拿到的值以及平台在边缘节点注入的值。自托管部署时,如果你依赖一个只有在next start启动后才 export 的变量,middleware 很可能读不到。

我踩过的一个真实坑是:把权限校验逻辑放在 middleware 里,需要读取一个只能从部署平台动态获取的 Token,结果生产环境里 middleware 一直拿不到值,导致所有请求都被拦截。后来把 Token 读取挪到了 API 路由里处理,middleware 只做轻量级判断,问题就解决了。在边缘运行时里处理环境变量,原则是能少读就少读,不要塞入过多运行时依赖。如果你确实需要复杂决策,优先放在 Node.js 服务端,那里环境变量行为最容易预测。

5. 常见问题与排查技巧实录

5.1 修改 .env 后变量不生效

这个问题我在社区里看到过无数次,原因通常有三个。第一,进程缓存问题,修改环境变量后没有重启next dev或next start,进程内存里还是旧值。第二,修改的是 NEXT_PUBLIC_ 变量但只重启服务没有重新构建,浏览器拿到的是旧构建产物。第三,改错了文件,开发环境要看.env.development,生产环境要看.env.production,结果你把值写进了.env.local,而目标环境根本不读这个文件。

如果你已经排除了这三类原因,可以尝试清掉构建缓存再启动。Next.js 的缓存目录是.next,删除后重新构建通常能解决很多莫名其妙的变量不生效问题。

rm -rf .next npm run dev

5.2 process.env.XXX 为 undefined

遇到 undefined,先别急着怀疑框架出 bug。按顺序检查变量名拼写,环境变量区分大小写,DATABASE_URL和database_url是两个完全不同的变量。再检查文件位置,环境变量文件必须在项目根目录,否则不会被加载。接着检查访问位置,如果是在客户端组件里访问非前缀变量,那么永远都是 undefined,这是设计使然。

最后检查加载优先级,是否被另一个文件里的同名空值覆盖了。我曾经遇到过.env.local里写了API_KEY=空值,导致.env里的真实配置被覆盖成 undefined 的情况。用 3.4 里的调试接口可以快速验证。

5.3 前端产物中出现敏感信息

当你在浏览器开发者工具或 GitHub 仓库里看到不该出现的密钥,基本可以断定是下面三种原因之一。变量名错误地加了 NEXT_PUBLIC_ 前缀;服务端把原始密钥通过 props 传给了客户端组件;代码里直接硬编码了密钥字符串。前两种属于使用不当,第三种属于程序员懒省事。

验证方法很简单,构建完成后搜索一下静态产物:

npm run build grep -R "SUPABASE_SERVICE_KEY" .next/static || echo "静态产物未发现敏感变量"

如果搜到了,就需要立刻修正环境变量名和引用方式,然后重新构建并部署。养成在发布前跑一遍搜索的习惯,能帮你挡住大多数误泄事件。

5.4 特殊字符与解析问题

.env 文件里最常见的解析事故来自#和空格。数据库连接串里经常出现#作为密码或参数的一部分,如果不用引号包起来,解析会被截断。正确的写法如下:

DATABASE_URL="postgresql://user:pass#word@localhost:5432/db"

另一种情况是环境变量值需要以空格结尾,或者值本身就是空字符串。空字符串KEY=会被解析成空值,而不是 undefined,这能用来有意覆盖默认配置。还有人在 Windows 上编辑 .env 文件导致 BOM 头混入键名,也会让变量无法匹配,建议统一使用 UTF-8 无 BOM 编码。

5.5 next.config.js 里读取环境变量

next.config.js 默认运行在 Node.js 环境下,而且 Next.js 在加载这个文件之前会先加载 .env 系列文件,所以在里面直接读取process.env是可行的。很多团队会在 next.config.js 里根据环境变量做构建配置,比如按环境切换图片域名白名单。

const nextConfig = { images: { domains: [process.env.NEXT_PUBLIC_IMAGE_DOMAIN || 'localhost'], }, }; export default nextConfig;

这里有一个历史遗留的坑:next.config.js 里的env字段可以把变量暴露给浏览器端,但它本身也是一种构建时替换,而且默认暴露范围是整个客户端 bundle,行为跟 NEXT_PUBLIC_ 几乎一样但不够显式。新项目建议不要再用这个字段,统一用 NEXT_PUBLIC_ 前缀暴露公开变量,代码可读性会好很多。

5.6 跨生态排查思路的通用性

聊了这么多 Next.js 环境变量的问题,其实它的排查思路跟其他生态是相通的。搜索热词里那些关于 JDK、Python、conda、Maven 环境变量配置失败的求助,核心原因翻来覆去无非就是三类:配置文件没生效、作用域不对、拼写或路径错误。Next.js 只不过把“作用域”从操作系统进程粒度细化到了服务端与浏览器端粒度。你如果已经养成了“先确认加载源、再确认作用范围、最后验证运行环境”的排查习惯,无论切到哪个技术栈都不会慌。

写在最后的一点经验

最后分享一个我自己的习惯,算是这些年踩坑换来的教训。我手里每个 Next.js 项目都会维护一份严格的.env.example,每个变量都写明用途、是否公开、对应运行环境;.env.local永远不进仓库,部署平台的环境变量面板是敏感配置的唯一下发渠道;任何需要给客户端使用的变量必须显式带 NEXT_PUBLIC_ 前缀,并在代码注释里标注这是公开值;每次发版前用 grep 检查静态产物里有没有出现预期外的敏感关键词。这套流程看起来繁琐,但确实帮我挡掉了不少线上事故。环境变量从来不是高深的技术,真正的杀伤力都藏在“我以为它不会出现在那里”的疏忽里,养成随手确认变量的习惯,比背十篇教程都管用。

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

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

立即咨询