前言
我们都知道,在node
里有一个process
全局变量,其中的env
属性可以让我们访问系统中的环境变量。如下图所示,其中只截取了部分变量名。
神奇的process.env
大家都知道,最终我们的项目是运行在浏览器的,截止到2022年3月
,浏览器并没有支持访问系统环境变量。但是那么多前端项目里写的process.env
究竟是什么呢?它们是同一个东西吗?例如像下面这样
VUE_APP_BASE_API
是在.env*
文件中定义的,在项目启动时,vue-cli
会将以VUE_APP
开头的变量读取至环境变量,这是vue-cli
强制要求的。“你想用我的工具,就得遵循我定的规则😈”
.env文件中的内容
其实这背后是一系列工具链(webpack
+ dotenv
+ webpack DefinePlugin
)相互作用的成果,也是我第一次理解了前端工程化。
读完文章,你将:
- 对
vue-cli
加载.env
文件有一个更好的认识 - 初步理解前端工程化
- 多人开发时,合理配置
.env
文件
先重点介绍一下dotenv
dotenv
dotenv
是一个零依赖模块,它将环境变量从 .env
文件加载到 process.env
。dotenv
的默认策略是如果.evn文件中存在与系统中相同的环境变量, 那么将跳过该变量的加载, 记住这句话,后面要考哦😎
让我们来试一下,首先初始化项目
npm init -y
然后安装dotenv
npm i dotenv -D
在项目根目录(package.json所在的目录)创建.env
文件,内容自定,只要格式满足key=value
的形式即可
src/main.js
文件内容,调用config()函数加载变量
const dotenv = require('dotenv')
dotenv.config({})
console.log(`name=> ${process.env.NAME}, male=> ${process.env.SEX}`)
结果如下
dotenv.config()
默认加载当前路径下的.env
文件, 可以传入一个对象参数,接口如下
interface DotenvConfigOptions {
path?: string;
encoding?: string;
debug?: boolean;
/**
* 是否使用 .env 文件中的值覆盖系统中已存在的环境变量
* 默认值为 false
*/
override?: boolean;
}
config()可以被多次调用,也就是说可以同时加载多个文件。
例如:
// 三个文件中的定义的变量都会被加载进来
config({path: 'path1'})
config({path: 'path2'})
config({path: 'path3'})
看到这有的同学可能会说了:“这不还是在node
上运行吗?跟运行在浏览器上的前端项目有什么联系吗”
别急,接下来我们请出老大哥——
webpack 结合 dotenv使用
首先安装webpack
、webpack-cli
。 为了方便管理html
文件,再安装一个html-webpack-plugin
npm i webpack webpack-cli html-webpack-plugin -D
简单的配置了一下webpack
const {resolve} = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
mode: 'development',
entry: './src/main.js',
output: {
filename: '[name].js',
path: resolve(__dirname, 'dist'),
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
title: '自定义加载env文件'
})
]
}
修改main.js
文件内容
- const dotenv = require('dotenv')
- dotenv.config({
- })
- console.log(`name=> ${process.env.NAME}, male=> ${process.env.SEX}`)
+ const p = document.createElement('p')
+ p.textContent = `您现在位于 development 环境中, 欢迎您: 开发者roman`
+ p.style.cssText = `
+ text-align: center;
+ color: teal;
+ font-size: 2em;`
+ document.body.appendChild(p)
虽然我们目前是硬编码,但是后面通过读取环境变量就能实现
development环境和production环境显示不同内容。dotenv
运行在node
环境中,但是我们的代码跑在浏览器啊,怎样才能将它们联系起来呢?
突然想到webpack
打包阶段是在node
上进行的,所以诞生了以下想法
- 使用
dotenv
加载.env
中的环境变量 - 在打包阶段读取环境变量
- 以某种方式将环境变量合并到打包结果中
例如下面这样
// webpack.config.js
+ const dotenv = require('dotenv')
+ dotenv.config()
+ const stringifiedEnv = JSON.stringify(process.env)
module.exports = {
+ mode: process.env.NODE_ENV || 'development',
// =====================省略========================
plugins: [
new HtmlWebpackPlugin({
- title: '自定义加载env文件'
+ title: stringifiedEnv
})
]
}
// main.js
const p = document.createElement('p')
+ const process = {}
+ process.env = JSON.parse(document.title)
+ document.title = '自定义加载env文件'
- p.textContent = '您现在位于 development 环境中, 欢迎您: 开发者roman'
+ p.textContent = `您现在位于 ${process.env.NODE_ENV} 环境中, 欢迎您: ${process.env.NAME}`
// =================================省略=================================
打开dist/index.html
运行效果
虽然举得例子不太恰当,但是可以肯定的是,我们这种思路行得通。
DefinePlugin 闪亮登场
我们现在面临的一个问题就是,如何更加优雅的注入环境变量到打包结果中呢?webpack
里有一个内置插件, DefinePlugin
——可以很好的解决此类问题。
DefinePlugin
允许在 编译时 将你代码中的变量替换为其他值或表达式。简单来说,它的作用就是文本替换,将值或表达式硬编码到代码中。 比如说,我想定义一个全局变量,在任意其它文件中, 可以直接访问到 age 这个变量
// webpack.config.js
const age = 3;
new DefinePlugin({
age
})
// anyOther.js
console.log(age) // 直接访问 age, 输出结果 3
这是因为在打包时,DefinePlugin
进行了文本替换,将我们的 age
替换成3
特别注意: 因为是直接进行的文本替换, 如果想替换字符串, 例如'roman', 必须要写成 "'roman'"
继续修改webpack.config.js
,将我们的环境变量内联至代码中
// ======================省略========================
+ const {DefinePlugin} = require('webpack')
// ======================省略========================
module.exports = {
// ======================省略========================
plugins: [
new HtmlWebpackPlugin({
- title: env
+ title: '自定义加载env文件'
}),
+ new DefinePlugin({
+ 'process.env' = stringifiedEnv
+ })
]
}
因为现在可以直接访问process.env
, 所以删掉src/main.js
中多余的代码
- const process = {}
- process.env = JSON.parse(document.title)
// ======================省略========================
+ console.log(process.env)
虽然运行结果还是一样,但是我们现在已经能够成功访问到process.env
。
看到这里,答案已经揭晓了。我们项目里写的
process.env
和node
中的process.env
根本不是一个东西,你甚至可以写成foo.env
、bar.env
、baz.env
。
如果你的项目是通过
vite
创建的,vite
中使用了import.meta.env
这个变量暴露环境变量。
但是,这么多用不到的环境变量看起来略显冗余, 接下来我们尝试只暴露出部分环境变量。
定制自己的env读取规则
因为系统中环境变量太多了,并且某些环境变量还涉及到隐私的问题。所以现在需要实现,只暴露出以ROMAN_APP
前缀的环境变量。为了将配置与代码隔离,在项目根目录创建env.js
文件
const dotenv = require("dotenv");
const ROMAN_APP = /^ROMAN_APP/i;
dotenv.config();
const raw = Object.keys(process.env)
// 遍历只符合正则表达式的环境变量
.filter((key) => ROMAN_APP.test(key))
.reduce(
(prev, key) => {
prev[key] = process.env[key];
return prev;
},
{
// 一般都有个NODE_ENV环境变量
NODE_ENV: process.env.NODE_ENV || "development",
}
);
const stringifiedEnv = JSON.stringify(raw)
module.exports = {
raw,
stringifiedEnv
}
env.js
内容很简单,使用正则表达式匹配以ROMAN_APP
为前缀的环境变量, 最后在webpack.config.js
中导入stringifiedEnv
就行了
现在往.env
文件中写点其他内容试试😏
构建后重新查看运行结果,现在暴露出来的只有以ROMAN_APP
为前缀的环境变量。因为没有改变src/main.js
中的代码, 所以process.env.NAME
为undefined
至此,我们实现了 “按需加载” 环境变量,下面简单说一下多人协助开发下,怎么合理使用
.env
文件
多人开发模式下的.env
使用
就拿axios来说, 一般会通过读取环境变量创建一个实例
const request = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
// 一些其他配置。。。
})
显然,这个VUE_APP_BASE_API
是在.env
文件中配置的,多人开发下,每个前端对接的是不同后端,这个VUE_APP_BASE_API
肯定就不一样, 难道要让前端在对接口的时候改一下,提交代码的时候又改一下?多人模式下这么改来改去肯定会出错。
其实我们可以保持项目原.env
文件不动,再写一份.env
文件,此时就可以任意修改.env
文件的内容,之后提交代码时只提交原来的那份.env
文件。
于是我们有了以下 公约
.env.[mode].local # 只在指定的模式中被载入,优先于.env.[mode], 但会被 git 忽略
.env.[mode] # 只在指定的模式中被载入
.env.local # 在所有的环境中被载入,优先于.env, 但会被 git 忽略
.env # 在所有的环境中被载入
其中mode
对应的是 production/development
下面我们完善一下env.js
的功能
const dotenv = require("dotenv");
+ const { resolve } = require("path");
+ const {existsSync} = require('fs')
const ROMAN_APP = /^ROMAN_APP/i;
+ const mode = process.env.NODE_ENV || 'development';
+ const envPath = resolve(__dirname, '.env')
+ const pathList = [
+ `${envPath}.${mode}.local`,
+ `${envPath}.${mode}`,
+ `${envPath}.local`,
+ `${env}`
+ ]
+ pathList.forEach(path => {
+ if (existsSync(path)) {
+ dotenv.config({path})
+ }
+ })
const raw = Object.keys(process.env)
// 遍历只符合正则表达式的环境变量
.filter((key) => ROMAN_APP.test(key))
.reduce(
(prev, key) => {
prev[key] = process.env[key];
return prev;
},
{
// 一般都有个NODE_ENV环境变量
- NODE_ENV: process.env.NODE_ENV || 'development'
+ NODE_ENV: mode
}
);
const stringifiedEnv = JSON.stringify(raw);
module.exports = {
raw,
stringifiedEnv,
}
将.env
文件复制一份,重命名为.env.loacl
, 大家猜猜最后NODE_ENV
会是什么?
🤔
🤨
😐
🙄
😲
你答对了吗? 至于为什么就留给朋友们自己去思考了(其实答案已在文中揭晓了)
最后, 想要在提交代码时忽略本地.env
文件,还要在.gitignore
文件中添加.local
dist
node_modules
.local
总结
前端项目里的process.env
和node
里的process.env
根本不是同一个东西,它只是DefinePlugin
插件在webpack
打包阶段做的一些hack
手段,只不过为了语义化,我们都写成了process.env
。