跳到主要内容

3 篇博文 含有标签「前端工程化」

查看所有标签

Vite 环境变量读不到?变量必须加 VITE_ 前缀才暴露给前端

· 阅读需 5 分钟

在 Vite 项目的 .env 里写好了 API_URL=http://localhost:3005,前端代码里 import.meta.env.API_URL 打印出来却是 undefined,接口请求全部落到错误地址。

在为客户构建 AI Agent SaaS 平台时遇到此问题,记录根因与解法。

TL;DR​

Vite 只把 VITE_ 前缀的环境变量暴露给客户端代码,这是防止服务端密钥被打进浏览器 bundle 的安全设计。

# ❌ 不会暴露给前端
API_URL=http://localhost:3005

# ✅ 暴露给前端
VITE_API_URL=http://localhost:3005

代码里对应改为 import.meta.env.VITE_API_URL。TypeScript 项目再加一步 env.d.ts 类型声明,补全智能提示。

问题现象​

两个典型表现:

表现一:变量是 undefined

// .env: API_URL=http://localhost:3005
console.log(import.meta.env.API_URL) // undefined
console.log(import.meta.env) // 里面有 BASE_URL、MODE、PROD...,就是没有 API_URL

.env 文件确实被加载了(MODE、PROD 这些内置变量都在),唯独自己写的变量不见——说明不是加载失败,是暴露规则把它拦下了。

表现二:加了前缀但拼错访问名

// .env: VITE_API_URL=...
const url = import.meta.env.VITE_APIURL // undefined —— 大小写必须完全一致

服务端 Node.js 项目里「环境变量读出来是 undefined」另有常见根因(dotenv 加载顺序),前端和后端这两类 undefined 根因不同,别混着排查。

根因:Vite 的暴露规则是一道安全边界​

Vite 的设计:.env 里可能同时存在两类值——前端要用的公开配置(接口地址)和绝不能进浏览器的密钥(数据库连接串、第三方 API key)。如果全部暴露,一次疏忽就把密钥打进了公开的静态产物。

所以 Vite 定了硬规则:只有 VITE_ 前缀的变量会出现在 import.meta.env 里,其余变量只在 vite.config.ts(Node 侧)通过 loadEnv 可见。

暴露的实现方式也值得知道:构建时静态替换。Vite 打包时直接把 import.meta.env.VITE_API_URL 替换成字符串字面量,运行时根本没有「读环境变量」这个动作。两个推论:

  1. 值会明文出现在产物 JS 里——VITE_ 变量天然是公开的
  2. 运行时改环境(改容器 env、改系统变量)不影响已构建的产物——换环境必须重新构建,或把配置改成运行时注入(如 window.__CONFIG__)

解法:加前缀 + 类型声明​

第一步:.env 里的变量加 VITE_ 前缀。 命名保持语义,前缀只是暴露标记:

# .env
VITE_API_URL=http://localhost:3005

第二步:代码统一走 import.meta.env.VITE_XXX。 建议收敛到一个配置模块,别在组件里散着读:

// src/config.ts
export const API_URL = import.meta.env.VITE_API_URL

第三步(TypeScript 项目):src/env.d.ts 补类型声明,拿到完整智能提示:

/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_URL: string
}

interface ImportMeta {
readonly env: ImportMetaEnv
}

开发、生产两套地址用模式文件分开,Vite 按场景自动选:

.env.development    # dev server 用
.env.production # npm run build 用
.env # 两者都加载,放公共配置

改完任何 .env 文件必须重启 dev server——环境变量只在启动时加载一次,热更新不覆盖它们,这是「明明改了却没生效」的第二大来源。

注意事项

VITE_ 变量等于公开信息:值会被明文内联进客户端 bundle,任何人可见。接口地址、功能开关可以放;数据库连接串、第三方密钥绝不放,密钥只走服务端环境变量。前端需要受保护的配置时,由后端接口下发,而不是塞进 .env。

常见问题​

Vite 环境变量配置在哪个文件里?​

项目根目录的 .env 系列:.env 两种场景都加载,.env.development 只在 dev server 生效,.env.production 只在构建时生效。改完任何 .env 文件都必须重启 dev server——Vite 只在启动时加载一次环境变量,热更新不覆盖它们。

为什么 VITE_ 前缀的变量会泄露?不能放密钥吗?​

不能放密钥。VITE_ 变量在构建时被静态内联进客户端 bundle,任何访问页面的人打开源码就能看到明文。前缀机制的设计目的就是把「允许公开的配置」(接口地址、功能开关)和「必须留在服务端的密钥」分开,密钥走后端环境变量。

import.meta.env 和 process.env 在 Vite 里有什么区别?​

浏览器端代码只能用 import.meta.env——Vite 会把 VITE_ 前缀变量静态替换进去;process.env 是 Node.js 的 API,只在 Vite 配置文件(vite.config.ts)和 SSR 场景可用,前端代码里写 process.env.XXX 打包后就是 undefined。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询

Docusaurus scripts 添加 inline 脚本构建失败?只支持 src 不接受 content

· 阅读需 5 分钟

在 docusaurus.config.ts 的 scripts 数组里用 { content: '...' } 注入 inline 脚本(例如百度统计的 IIFE),执行 npm run build 直接报错。

在开发 CCLEE Docusaurus Theme 时遇到此问题——基于 Docusaurus 3.x 的高级文档主题,紫色主题 + 深色模式 + Tailwind 排版增强,开箱即用的生产级文档站点模板。

TL;DR​

Docusaurus 的 scripts 配置只接受 src,不支持 inline content。把 inline 脚本挪到 static/js/ 下,用 { src: '/js/xxx.js', async: true } 引用即可;若脚本加载外部域名,还要同步更新 CSP。

问题现象​

按百度统计官方代码,本能地想直接塞进 scripts:

// docusaurus.config.ts
const config: Config = {
scripts: [
{
content: `var _hmt=_hmt||[];(function(){var hm=document.createElement("script");hm.src="https://hm.baidu.com/hm.js?XXXX";var s=document.getElementsByTagName("script")[0];s.parentNode.insertBefore(hm,s);})();`,
},
],
};

构建立刻失败:

[ERROR] Error: "scripts[1]" is invalid.
A script must be a plain string (the src), or an object with at least a "src" property.
at validateConfig (.../configValidation.js:397:15)

根因​

Docusaurus 的 scripts 配置在构建期被 validateScripts 逐条校验,每条只允许两种形态:

  1. 纯字符串:直接当作 src 处理
  2. 对象:必须包含 src 属性,可选 async、defer、data-* 等

设计上 scripts 只生成形如 <script src="..." /> 的标签,没有为 inline 脚本保留 content / innerHTML 字段。所以无论 inline 内容多短,校验都会在 src 缺失时直接抛错,本地构建、Vercel 远端构建同样失败。

想注入带构建期变量的 inline 脚本,应改用顶层的 headTags 配置(tagName: 'script' + innerHTML),而不是 scripts。

解决方案​

1. 把 inline 脚本放进 static/js/​

// static/js/baidu-tongji.js
var _hmt = _hmt || [];
(function () {
var hm = document.createElement('script');
hm.src = 'https://hm.baidu.com/hm.js?XXXX';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(hm, s);
})();

static/ 目录下的文件会被原样拷贝到站点根目录,最终 URL 即 /js/baidu-tongji.js。

2. 在 scripts 用 src 引用​

// docusaurus.config.ts
scripts: [
// 已有的 Umami(参见 /blog/docusaurus-umami-analytics)
{
src: 'https://tj.ccleeai.com/script.js',
async: true,
'data-website-id': 'xxxx',
},
// 百度统计:走静态文件
{
src: '/js/baidu-tongji.js',
async: true,
},
],

3. 同步更新 CSP​

如果站点启用了 Content-Security-Policy(推荐做法,参见我们之前总结的 Umami 集成与 CSP 配置),新加的外部域名必须放行,否则脚本会被浏览器拦截:

themeConfig: {
metadata: [
{
'http-equiv': 'Content-Security-Policy',
// script-src 加 https://hm.baidu.com
// connect-src、img-src 同步放行(hm.js 会发图片像素和 fetch 上报)
content: "default-src 'self'; " +
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://tj.ccleeai.com https://hm.baidu.com; " +
"connect-src 'self' https://tj.ccleeai.com https://hm.baidu.com; " +
"img-src 'self' data: https://hm.baidu.com; " +
"style-src 'self' 'unsafe-inline'; " +
"object-src 'none'; base-uri 'self'",
},
],
},

百度统计的 IIFE 内部用 document.createElement('script') 动态注入 <script src="hm.baidu.com/...">,所以光放 'unsafe-inline' 不够,必须把 hm.baidu.com 加进 script-src。

注意事项

  • scripts 数组里的纯字符串和 src 是等价的:'https://x/a.js' 与 { src: 'https://x/a.js' } 等效
  • 路径以 / 开头时是相对站点根目录(static/ 拷贝产物),不是文件系统根
  • 若脚本依赖运行期变量、必须 inline,请用顶层 headTags 而非 scripts;headTags 支持 innerHTML
  • 多套统计(Umami + 百度 + GA)可以共存,但每个外部域名都要单独加进 CSP,否则该域名上报被静默拦截

常见问题​

如何在 Docusaurus 配置自定义 scripts?​

在 docusaurus.config.ts 顶层的 scripts 数组中添加条目,每条要么是字符串(视作 src),要么是带 src 属性的对象。inline 内容不能用 content 字段——把脚本放进 static/js/ 目录后用 src: '/js/xxx.js' 引用即可。如果还需要 async、defer 或自定义 data-* 属性,把它们和 src 放在同一个对象里。

Docusaurus scripts 为什么不支持 inline content?​

Docusaurus 的 scripts 配置在构建期生成的是 <script src="..."> 标签,本身没有为 inline 脚本设计字段。校验器(configValidation.ts 中的 validateScripts)逐条检查,只要对象缺少 src 就抛出 A script must be a plain string, or an object with at least a "src" property,无论你的 content 写得多完整。这是刻意的 API 边界,把 inline 注入的能力交给了 headTags。

Docusaurus 如何注入 inline JavaScript(如百度统计)?​

最稳的方式是「静态文件 + 内部动态注入」:把官方那段 IIFE 脚本完整保存到 static/js/baidu-tongji.js,scripts 用 { src: '/js/baidu-tongji.js', async: true } 引用;脚本内部再 document.createElement('script') 加载 hm.baidu.com/hm.js。最后别忘了在 CSP 的 script-src、connect-src、img-src 都加上 https://hm.baidu.com,否则上报会被浏览器拦截。

CCLEE

独立开发者,24年电商行业实战经验,专注将AI能力落地于真实商业场景。

合作咨询