前言
前端在之前并没有工程化的概念,甚至开发环境、测试环境、生产环境全靠大家手动配置。
有了nodejs之后,环境变量 (environment variables)这个概念,便慢慢进入了前端的视野,方便了前端各种环境自动化配置及本地环境的运行。现如今 webpack 、 rollup、vite 等打包工具大行其道,我们不得不将它重视起来。在现代前端开发的整个链路中, 环境变量起到一个项目的配置枢纽作用,也是前端提效的重要一环。今天,我们就一步一步剖析环境变量在前端的使用场景及环境变量是如何在前端环境中发挥作用的。
1、cross-env 配置环境变量,
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"
大家经常会看到这种代码,其中cross-env NODE_ENV=production,指定的环境变量key为NODE_ENV,值为production,这样调用之后呢,我们可以通过process.env.NODE_ENV 获取对应的环境变量。
process 是node 的一个进程环境,其中通过process.env可以获取进程环境中的环境变量,方便node在调用时处理各种环境信息。
这种是最基本的环境变量配置方式,NODE_ENV基本上常用的只有两种,development和production,用来做开发和生产的区分。
2、先看一个pageage.json中script的部分。
"scripts": {"dev": "vite","build": "tsc && vite build","preview": "vite preview"},
我们在执行npm run dev的时NODE_ENV 会自动变成develop,为什么可以自动设置呢?只有了解了事情的本质我们才可以更放心大胆的使用各种命令。要想知道为什么会这么执行,我们首先要知道npm run dev 执行的vite方法干了什么,然后我们一步步的来扒源码。先找到vite这个文件夹。
首先我们要知道,当执行npm run 时会自动找到当前目录下的scripts对象,比如我们npm run dev,就会找到package.json下scripts 下的dev 这个指令。我相信可能有些同学会试过,直接在当前目录下执行vite,然后得到了一个:
或者大家会有这个疑问,为什么npm run dev 执行 script 下的dev触发vite可以执行,而我们直接执行不可以呢?这是因为在界面中执行vite是必须要全局安装的才可以的。而不全局安装的要用的话,使用scripts执行,而scripts会默认查找当前node_modules下对应的文件夹,比如vite。找到对应的vite文件夹之后会查找对应的package.json下的bin对应的执行文件,像vite如下:
然后承接上面的疑问,为什么会有默认的development 这个环境变量,在vite中的源代码如下:
他会有个默认值,如果不设置的话,就默认是development。
vite build的时候会默认传入production,所以就变成了production
所以在vite中,vite 和 vite build 这两个指令执行的时候,环境变量NODE_ENV的值默认为development和production 。
3、介绍了环境变量及vite中默认环境变量的配置,下面我们用vite打包工具来实际落地使用,在实际项目开发中,我们会区分多个环境,就我们自己的项目来说,会有开发、测试、灰度、生产。其中开发环境又分为开发1,开发2,开发3,开发4,测试分为测试1等。
3.1 常见基础项目最省事的做法
import { defineConfig } from "vite";import react from "@vitejs/plugin-react";import * as path from "path";const target = "https://dev1.advai.cn”;export default defineConfig(({ command, mode }) => {return {plugins: [react()],server: {host: "0.0.0.0",proxy: {"/api/dashboard": {target,changeOrigin: true,secure: false,xfwd: false,},},},base: mode === "production" ? "./" : "",resolve: {alias: {"@": path.resolve(__dirname, "./src"),},},};});
这种方案好处是一看就懂,坏处就是你改了地址,代码会被提上去,把别人的本地地址也会覆盖掉。
我们本地要区分多环境时,这种方案就不行,所以,各种构建工具都提供了环境变量的配置及使用,我们这里着重介绍vite的使用,及vite为什么可以这么用,知己知彼,才能用得更好,玩得更溜。
再来看我们之前的那个script案例,
"scripts": {"dev": "vite","build": "tsc && vite build","preview": "vite preview"},
执行vite的时候,会自动种下一个环境变量,叫NODE_ENV来区分我们当前的环境是什么。
那么,其实我们本地请求后端的各种环境也可以通过环境变量来设置。我们可以通过在根目录添加.env.development,.env.development.local两个文件。
其中.env.development 可以包含各个环境的所有配置项,更像是一个清单,大概内容如下:
# dev1# VITE_TARGET=https://dev1.advai.cn# dev2#VITE_TARGET=https://dev2.advai.cn# prodVITE_TARGET=https://prod.advai.cn
.env.development.local文件中是要用到的具体的后端环境,例如:
# dev1VITE_TARGET=https://dev1.advai.cn
这样做有个好处,正常的话,.env.development.local 这个文件是git ignore 要忽略的,这样代码提上去之后会有一个默认启用的VITE_TARGET。
配置完了,环境变量我们要怎么使用呢?
export default defineConfig(({ command, mode }) => {const env = loadEnv(mode, path.resolve(process.cwd()));console.log('env', env);});
这时我们就能获取到自己的环境变量。
完整示例:
import path from 'path';import fs from 'fs';import process from 'process';import { defineConfig, loadEnv } from 'vite';import legacy from '@vitejs/plugin-legacy';import react from '@vitejs/plugin-react';// https://vitejs.dev/config/export default defineConfig(({ command, mode }) => {const env = loadEnv(mode, path.resolve(process.cwd()));console.log('env', env);return {plugins: [react(),],server: {proxy: {'/api/service': {target: env.VITE_TARGET,changeOrigin: true,secure: false,},},host: true,},};});
通过使用env.VITE_TARGET我们就可以动态的获取到本地环境变量中的后端地址了。
其中有的小伙伴可能觉得VITE_ 有点碍事,我们直接写TARGET 不好吗?那我们来试一下,我们将env.development.local改为:
# dev1
TARGET=https://dev1.advai.cn
然后重新执行npm run dev,会发现获取到了一个空对象:
为什么呢?要了解这个问题,首先要知道咱们调用的loadEnv这个方法是干啥用的,官网上也有介绍:
loadEnv接收三个参数,第一个是.env后面的名字,第二个是绝对路径,第三个参数是你环境变量名的前缀,在vite中默认是VITE_,为什么不传VITE_的环境变量就不显示了呢?源码在这里:
vite/env.ts at 134ce6817984bad0f5fb043481502531fee9b1db · vitejs/vite · GitHub ,我将其中几个重要的地方拿出来:
(1)、需要抓取的文件名称
const envFiles = [/** default file */ `.env`,/** local file */ `.env.local`,/** mode file */ `.env.${mode}`,/** mode local file */ `.env.${mode}.local`,]
这里定义vite会抓取的文件名称,并且顺序就是优先级,也就是`.env.${mode}.local` > `.env.${mode}` >`.env.local` > `.env` 。这里的环境变量的优先级的根据文件的优先级加载,当有重名的环境变量时,会按文件的优先级来覆盖环境变量的值。
(2)判断包含prefixes的key
for (const key in process.env) {if (prefixes.some((prefix) => key.startsWith(prefix)) &&env[key] === undefined) {env[key] = process.env[key];}}
这段代码写的就是刚才为什么获取不到不带VITE_环境变量的原因,因为这个会判断key是否包含你写的prefixes,然后加载到env这个对象中。如果你没传prefixes 的话,那么默认就是VITE_。
(3)读取路径内容,并返回给process.env
for (const file of envFiles) {const path = lookupFile(envDir, [file], { pathOnly: true, rootDir: envDir });if (path) {const parsed = dotenv.parse(fs__default.readFileSync(path), {debug: ((_a = process.env.DEBUG) === null || _a === void 0 ? void 0 : _a.includes('vite:dotenv')) || undefined});// let environment variables use each othermain({parsed,// prevent process.env mutationignoreProcessEnv: true});// only keys that start with prefix are exposed to clientfor (const [key, value] of Object.entries(parsed)) {if (prefixes.some((prefix) => key.startsWith(prefix)) &&env[key] === undefined) {env[key] = value;}else if (key === 'NODE_ENV' &&process.env.VITE_USER_NODE_ENV === undefined) {// NODE_ENV override in .env fileprocess.env.VITE_USER_NODE_ENV = value;}}}}
遍历envFiles 文件列表,判断如果有对应路径的话,读取路径内容,并取出prefix 开头的环境变量,并返回给process.env。
还有的朋友可能要问,我为什么非要在根目录下创建两个文件?这样我看起来乱得很,我想创建一个文件夹来存放那些env环境变量。其实vite也提供了加载指定目录env文件的配置项,这是官网的介绍
最后我们的vite配置文件改成了:
export default defineConfig(({ command, mode }) => {const env = loadEnv(mode, path.resolve(process.cwd()));return {envDir: './env',plugins: [react(),],server: {proxy: {'/api/iam-service': {target: env.VITE_TARGET,changeOrigin: true,secure: false,},'/accounts/': {target: env.VITE_TARGET,changeOrigin: true,secure: false,},},host: true,},}});
环境变量的默认目录由当前路径改为当前路径下的env文件夹:
.env.development的内容:
# dev1# VITE_TARGET=https://dev1.advai.cn# dev2#VITE_TARGET=https://dev2.advai.cn# prodVITE_TARGET=https://prod.advai.cn
.env.development.local的内容:
# dev1VITE_TARGET=https://dev1.advai.cn
.git ignore别忘记修改,这样我们才可以让我们本地的环境变量只作用在本地,
node_modules
.DS_Store
dist
dist-ssr
*.local
node_modules/*
.vscode/