Tiana随笔

Vite 环境变量和构建配置入门

· Vue

标签:#前端 #Vue

Vite 环境变量和构建配置入门

本地构建时 BASE_URL 写的是线上域名,CSS 里硬编码打进 bundle,后台接口地址全错,排查花了俩小时。这篇记 Vite 的环境变量命名规矩、按环境分文件、自定义 mode,以及一次把真密钥写进 env 文件的翻车。

第一次部署踩到的环境变量错

我这站后端 BASE_URL 写成 https://www.tiana.com.cn,本地 dev 时也是这个值。结果本地跑 npm run build 的时候,CSS 里硬编码把这个域名打进 bundle,nav 跳转看着正常,但后台接口地址错了。

排查路径现在说起来三句话就完事,当时花了我俩小时。原因是项目里没有区分环境,所有 BASE 全部走「相对路径」拼「后端 IP 端口」写死,本地开发走 8123 端口,后端 dev 还启在 8000,前端直接写 http://127.0.0.1:8000/api。

我说这些不是为了抒怀,而是环境变量本来就是用来解决这种鸡毛蒜皮的错的。但仍然有一堆人栽在这一步。

Vite 的环境变量命名规矩

Vite 跟传统 webpack 的 process.env 不一样,它用 import.meta.env。只有 VITE_ 开头的变量会被打包进客户端代码。

# .env.development
VITE_API_BASE_URL=http://127.0.0.1:8000
VITE_SITE_NAME=本地博客

# .env.production
VITE_API_BASE_URL=https://www.tiana.com.cn
VITE_SITE_NAME=Tiana 随笔

代码里这样读:

const baseURL = import.meta.env.VITE_API_BASE_URL
const siteName = import.meta.env.VITE_SITE_NAME

拼接到 fetch 上:

async function listPosts(page = 1) {
  const r = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/blog/posts/list`, {
    method: 'POST',
    credentials: 'include',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ page, page_size: 20 }),
  })
  return (await r.json()).data
}

只读 VITE_ 前缀这点是安全边界,不是我给你友情提示。SPA 所有代码最终都是公开的,没设前缀的变量连 bundle 都不会进,假设你在 .env.production 里写了 DATABASE_PASSWORD=xxx,构建出来文件里也没有。捡了芝麻丢了西瓜的事 Vite 已经帮你做了。

一个 .env 文件盯一类环境

我倾向于每个环境一个文件:

.env                  # 所有环境共享,最低优先级
.env.development      # npm run dev 默认加载
.env.production       # npm run build 默认加载
.env.staging          # 自定义环境,--mode staging 时加载

加载优先级是 mode > .env.<mode> > .env。相同 key 后加载的覆盖前加载的。

自定义 mode 给 SEO 渲染用

我这站首页要服务爬虫,我额外加了一个 mode:

# .env.crawler
VITE_RENDER_MODE=ssr-fake
vite build --mode crawler

这样打出来的 bundle 跟生产几乎一致,只是 embedding 了一段标记让后端知道这份产物是爬虫版。比起维护两套几乎一样的代码,mode 切换更省心。

一个真实翻车:环境变量里藏了真密钥

我刚开始做后台对接时,图省事把管理员 token 放在了 .env.production。结果构建出来之后,所有人打开浏览器的 devtools,都能 grep 到 VITE_ADMIN_TOKEN。

原因很简单:所谓「前端环境变量」其实是构建时写死在 bundle 里的常量,不是运行时由服务器发的。前端能拿到的值就是公开的。我把能进 bundle 的东西分了三类:

  • 安全边界明确的:公开配置。API_BASE_URL、SITE_NAME、CDN 域名、是否启用某种 feature
  • 半公开:构建时敏感配置。比如第三方分析 ID、错误监控的 dsn。理论上公开但有滥用的可能
  • 严格敏感:前端永远不能有。数据库密码、签名密钥、admin token、OAuth client secret。第三类必须留在后端

第一类的 subset 全部走 VITE_ 前缀。后两类的 subset 你不要打给前端,哪怕你用了某个不那么暴露的命名。如果非要给前端管理 token,正确做法是后端代前端鉴权,前端发送不带 token 的 cookie。

顺手的另一件事:__VITE_IS_MODERN__

Vite 在打包时识别旧浏览器的能力让我栽过一次。我用了某个新语法,但版本没设对,老用户拿到的 bundle 里语法没转译就崩了。

import { isModern } from './utils'

这里 isModern 源码是 const isModern = true,Vite 把它替换成字面量。问题是我以为打包时所有用户都拿同一份,结果老浏览器拿到的是替换后的常量,没办法分版本。

解决办法是用 Vite 自带的 build.target:

// vite.config.js
export default defineConfig({
  build: {
    target: 'es2020',
  },
})

如果非要兼容更老的浏览器,分两份 build:一份 modern(es2020+),一份 legacy(es5)。Vite 默认就支持,分文件输出就行。维护成本是个 trade-off:你的用户统计里 50% 是 IE11 吗?不是的话只发 modern 版本。

最后一个习惯:env.example

新同事入职都得 clone 项目才能跑起来。我留了一份 .env.example 在仓库里,里面是允许 commit 的所有 key(值留空)。这样新同事 clone 之后 cp .env.example .env.local,再填真值就能跑。

我本地开发值永远写在 .env.local 而不是 .env.development,因为 .env.local 默认被 git ignore,每个人填自己的值,不会互相污染。.env.development 写团队共同的部分,比如 API 默认走 8000 端口。

现在我对环境变量的态度

它就是项目的「配置文件」。能帮构建时区分环境、能帮团队共享 key、能让新人快速上手。但永远别把任何重要密钥写进会在浏览器里跑出来的代码,这事想清楚了就不再犯。

相关文章