稀土掘金 稀土掘金

揭开前端项目里process.env的真面目

前言

我们都知道,在node里有一个process全局变量,其中的env属性可以让我们访问系统中的环境变量。如下图所示,其中只截取了部分变量名。

image.png

神奇的process.env

大家都知道,最终我们的项目是运行在浏览器的,截止到2022年3月,浏览器并没有支持访问系统环境变量。但是那么多前端项目里写的process.env究竟是什么呢?它们是同一个东西吗?例如像下面这样

image.png

vue-element-admin 中使用的环境变量

VUE_APP_BASE_API是在.env*文件中定义的,在项目启动时,vue-cli会将以VUE_APP开头的变量读取至环境变量,这是vue-cli强制要求的。“你想用我的工具,就得遵循我定的规则😈”

image.png

.env文件中的内容

其实这背后是一系列工具链(webpack + dotenv + webpack DefinePlugin)相互作用的成果,也是我第一次理解了前端工程化

读完文章,你将:

  • vue-cli加载.env文件有一个更好的认识
  • 初步理解前端工程化
  • 多人开发时,合理配置.env文件

先重点介绍一下dotenv

dotenv

dotenv 是一个零依赖模块,它将环境变量从 .env 文件加载到 process.envdotenv的默认策略是如果.evn文件中存在与系统中相同的环境变量, 那么将跳过该变量的加载, 记住这句话,后面要考哦😎

让我们来试一下,首先初始化项目

npm init -y

然后安装dotenv

npm i dotenv -D

在项目根目录(package.json所在的目录)创建.env文件,内容自定,只要格式满足key=value的形式即可

image.png

src/main.js文件内容,调用config()函数加载变量

const dotenv = require('dotenv')
dotenv.config({})
console.log(`name=> ${process.env.NAME}, male=> ${process.env.SEX}`)

结果如下

image.png 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上运行吗?跟运行在浏览器上的前端项目有什么联系吗”

别急,接下来我们请出老大哥——

Background.png

webpack 结合 dotenv使用

首先安装webpackwebpack-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上进行的,所以诞生了以下想法

  1. 使用dotenv加载.env中的环境变量
  2. 在打包阶段读取环境变量
  3. 以某种方式将环境变量合并到打包结果中

例如下面这样

// 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运行效果

image.png 虽然举得例子不太恰当,但是可以肯定的是,我们这种思路行得通。

DefinePlugin 闪亮登场

我们现在面临的一个问题就是,如何更加优雅的注入环境变量到打包结果中呢?webpack里有一个内置插件, DefinePlugin——可以很好的解决此类问题。

DefinePlugin 允许在 编译时 将你代码中的变量替换为其他值或表达式。简单来说,它的作用就是文本替换,将值或表达式硬编码到代码中。 比如说,我想定义一个全局变量,在任意其它文件中, 可以直接访问到 age 这个变量

// webpack.config.js
const age = 3;
new DefinePlugin({
  age
})

// anyOther.js
console.log(age) // 直接访问 age, 输出结果 3

这是因为在打包时,DefinePlugin进行了文本替换,将我们的 age替换成3 image.png

特别注意: 因为是直接进行的文本替换, 如果想替换字符串, 例如'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

image.png 看到这里,答案已经揭晓了。我们项目里写的process.envnode中的process.env根本不是一个东西,你甚至可以写成foo.envbar.envbaz.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文件中写点其他内容试试😏 image.png

构建后重新查看运行结果,现在暴露出来的只有以ROMAN_APP为前缀的环境变量。因为没有改变src/main.js中的代码, 所以process.env.NAMEundefined

image.png 至此,我们实现了 “按需加载” 环境变量,下面简单说一下多人协助开发下,怎么合理使用.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会是什么? image.png

🤔

🤨

😐

🙄

😲

你答对了吗? 至于为什么就留给朋友们自己去思考了(其实答案已在文中揭晓了) image.png

最后, 想要在提交代码时忽略本地.env文件,还要在.gitignore文件中添加.local

dist
node_modules
.local

总结

前端项目里的process.envnode里的process.env根本不是同一个东西,它只是DefinePlugin插件在webpack打包阶段做的一些hack手段,只不过为了语义化,我们都写成了process.env

demo地址

4617作文网小满节气出生起名周易免费取名称周公解梦梦见好多蛇是什么预兆姓纪起名字大全周公解梦胎梦大全周易取名子大全大泽山镇北随村算命给男宝宝起名字姓许清字辈起名计算机重命名快捷键中文名字起英文名字网站周易29卦周公解梦梦见被误会张起名字大全男孩免费起赵字的名字给合作社起个好名字房屋产权起名周易起名网真实可靠么给阮起名字女包包商标起名周易八卦算命入门起名茶叶店吗周易名字评分打分测试周公解梦梦见厂房倒塌周中解梦鼠年 孩子起名燕氏姓名学 起名周公解梦和尚诗经起公司名公司名称起名的四字词语淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男子给前妻转账 现任妻子起诉要回网友建议重庆地铁不准乘客携带菜筐月嫂回应掌掴婴儿是在赶虫子重庆警方辟谣“男子杀人焚尸”国产伟哥去年销售近13亿新的一天从800个哈欠开始男孩疑遭霸凌 家长讨说法被踢出群高中生被打伤下体休学 邯郸通报男子持台球杆殴打2名女店员被抓19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警两大学生合买彩票中奖一人不认账德国打算提及普京时仅用姓名山西省委原副书记商黎光被逮捕武汉大学樱花即将进入盛花期今日春分张家界的山上“长”满了韩国人?特朗普谈“凯特王妃P图照”王树国3次鞠躬告别西交大师生白宫:哈马斯三号人物被杀代拍被何赛飞拿着魔杖追着打315晚会后胖东来又人满为患了房客欠租失踪 房东直发愁倪萍分享减重40斤方法“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火手机成瘾是影响睡眠质量重要因素考生莫言也上北大硕士复试名单了妈妈回应孩子在校撞护栏坠楼网友洛杉矶偶遇贾玲呼北高速交通事故已致14人死亡西双版纳热带植物园回应蜉蝣大爆发男孩8年未见母亲被告知被遗忘张立群任西安交通大学校长恒大被罚41.75亿到底怎么缴沈阳一轿车冲入人行道致3死2伤奥运男篮美国塞尔维亚同组周杰伦一审败诉网易国标起草人:淀粉肠是低配版火腿肠外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万杨倩无缘巴黎奥运男子被猫抓伤后确诊“猫抓病”春分“立蛋”成功率更高?记者:伊万改变了国足氛围奥巴马现身唐宁街 黑色着装引猜测

4617作文网 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化