在一个正常的迭代周期内,一般会经历开发、测试、生产3个阶段,每个阶段都需要一个独立的环境,再加上某些特殊需求需要增多几个环境(如嵌入到其他应用下、迭代有新的阶段),如果没有一个好的多环境管理方案,那么就会出现代码强耦合、切环境操作复杂等问题
思路
如何实现一个优雅、易于管理的多环境配置方案呢?我们首先先根据3个环境写出一个最简单环境配置文件
// config/index.js
const Config={
dev:{
baseUrl:'https://www.dev.com'
},
test:{
baseUrl:'https://www.test.com'
}
prod:{
baseUrl:'https://www.prod.com'
}
}
业务代码需要获取环境参数时,我们希望这样去获取
import config from 'xx/config/index.js'
const baseUrl = config.baseUrl
所以,关键是如何确定我们此时处理哪个环境呢?我们需要一个代表环境名的全局变量,这个全局变量能够存在于项目的编译阶段和运行阶段,而已可以根据我们不同环境下不同的构建命令赋值相应的环境名
这就要用到node
的一个全局变量:process.env
process.env
我们先来看下process.env
是什么东西?官网是这么描述的
process.env
属性返回一个包含用户环境信息的对象。
简单来说,process.env
就是项目构建时执行的node
进程所在系统的环境变量(macOS、linux、windows)。
那么,如果在构建时设置一个临时的环境变量来代表当前的环境,如"ENV",那么就可以通过
process.env.ENV
在编译阶段获取当前的环境名,注意:这只是在编译阶段
那么如何设置临时的环境变量呢?不同的系统有不同的方式
// windows下
set NODE_ENV=production
// macOS&linux下
export NODE_ENV=production
我们需要根据不同系统使用不同的配置方式,幸运的是,已经有一个库帮我们做了这件事:cross-env
配合上面的环境配置文件,我们可以写出我们使用的process.env
方式的第一套方案
一、使用process.env
获取环境参数
1、基础版(编译阶段)
// package.json
{
"name": "xx",
"version": "1.0.0",
"scripts": {
"serve": "cross-env ENV=dev 项目serve命令", // serve命令代表本地构建起服务
"serve:test": "cross-env ENV=test 项目serve命令",
"serve:prod": "cross-env ENV=prod 项目serve命令",
"build": "cross-env ENV=dev 项目build命令", // build命令代表代码生产构建
"build:test": "cross-env ENV=test 项目build命令",
"build:prod": "cross-env ENV=prod 项目build命令",
},
...
}
// config.js
const env = process.env.ENV
const Config = {
dev:{
baseUrl:'https://www.dev.com'
},
test:{
baseUrl:'https://www.test.com'
}
prod:{
baseUrl:'https://www.prod.com'
}
}
const envConfig = Config[env] || 'dev'
module.exports = envConfig
值得注意的是,我们必须使用commonjs
的export
语法,不能使用es6
的import
语法(node
不支持)
2、 在运行阶段获取(DefinePlugin)
配置好上面的代码,使用webpack构建项目,在业务代码import这个config文件,我们可以发现打印出来的process.env
的值却是undefined,这是为什么呢?
原来,每个node
进程都会维护一个独立的process
对象,在进程开始生成,在进程结束时销毁,我们执行node
构建命令时,给它设置的临时的环境变量process.env
,只能在这个构建的进程里使用.
我们在业务代码使用的process.env
,是由浏览器进程去执行,属于一个新的运行时的进程里的对象,
那么,我们如何在运行阶段也能获取到process.env
的值呢
这就需要利用到webpack的一个plugin: DefinePlugin
DefinePlugin
是一个可以在 编译时 将你代码中的变量替换为字符串的webpack插件
利用这个特性,我们可以在编译阶段将项目代码里所有的process.env
替换为当前的环境对象,将process.env.xx
转化为环境对象.xx
接下来,还有一个问题,我们还需要告知 DefinePlugin
当前要替换哪个环境对象,我们可以利用我们上面设置的process.env
,因为webpack.config是在编译阶段调用,所以process.env
能获取到我们使用cross-env
设置的环境名;
获取到环境名之后,我们接下来需要根据这个环境名获取环境对象,这个环境对象里需要有ENV
属性供运行时使用,我们可以粗暴地直接在webpack
配置文件里写
const CurEnv = process.env.ENV
const env={
ENV:CurEnv
}
module.exports = {
plugins: [new webpack.DefinePlugin({ 'process.env': JSON.stringify(env) })]
}
但这种直接在webpack的config文件里写代码的方式不太优雅,也不利于扩展,我们可以将这个环境对象抽离出来
我们可以利用一个库dotenv
,它是用来加载并解析.env
文件
我们可以在项目根目录下按照新建多个环境文件,这些文件的文件名按照约定命名(如.env,.env.dev),
再通过dotenv
加载并解析当前环境对应的文件名,生成一个js对象,再利用DefinePlugin
在webpack
编译时将代码里所有的process.env
替换为这个js对象,那么我们使用process.env.xx
就能访问到环境文件下对应的属性的值
完整的方案如下:
// package.json
{
"name": "xx",
"version": "1.0.0",
"scripts": {
"serve": "cross-env ENV=dev 项目serve命令", // serve命令代表本地构建起服务
"serve:test": "cross-env ENV=test 项目serve命令",
"serve:prod": "cross-env ENV=prod 项目serve命令",
"build": "cross-env ENV=dev 项目build命令", // build命令代表代码生产构建
"build:test": "cross-env ENV=test 项目build命令",
"build:prod": "cross-env ENV=prod 项目build命令",
},
...
}
// webpack.config.js
const CurEnv = process.env.ENV
const env=require('.env.'+CurEnv)
module.exports = {
plugins: [new webpack.DefinePlugin({ 'process.env': JSON.stringify(env) })]
}
// env.dev
ENV='dev'
//.env.test
ENV='test'
//.env.prod
ENV='prod'
// config.js
const env = process.env.ENV
const Config = {
dev:{
baseUrl:'https://www.dev.com'
},
test:{
baseUrl:'https://www.test.com'
}
prod:{
baseUrl:'https://www.prod.com'
}
}
const envConfig = Config[env] || 'dev'
module.exports = envConfig
// 使用
import config from 'config/index.js'
const baseUrl = config.baseUrl
以上就是我们用cross-env
+DefinePlugin
+dotenv
来实现的多环境配置的第一套方案,使用过React或者Vue脚手架的同学可能会发现这套方案很熟悉,没错,create-react-app
和vue-ci
使用的.env
配置方式跟上述的方案在原理上是一致的,如果我们使用Vue
或React
的脚手架,我们不需要手动引入上述这3个库
以Vue
为例,我们可以这么写
// package.json
{
"name": "xx",
"version": "1.0.0",
"scripts": {
"serve": "vue-cli-service serve",
"build:test": "vue-cli-service build --mode test",
"build:prod": "vue-cli-service build --mode prod",
},
...
}
//.env.dev
VUE_ENV='dev' // vue要求写入process.env里的要加VUE_前缀
//.env.test
VUE_ENV='test'
//.env.prod
VUE_ENV='prod'
// config.js
const env = process.env.VUE_ENV
const Config = {
dev:{
baseUrl:'https://www.dev.com'
},
test:{
baseUrl:'https://www.test.com'
}
prod:{
baseUrl:'https://www.prod.com'
}
}
const envConfig = Config[env] || 'dev'
module.exports = envConfig
// 使用
import config from 'config/index.js'
const baseUrl = config.baseUrl
3、优化:分离环境文件
我们现在把所有的环境参数都写在一个文件里,项目初期这样写没什么问题,但是随着项目越来越大,环境越来越多,环境参数也越来越多,这个文件就会变得越来越臃肿,可读性、可维护性较低,所以我们需要把每一个环境的对象都抽离出来作为独立的环境文件,我们可以有两种形式来进行实现
(1)分离config文件,通过文件名检索
这种方式我们只改造config文件夹
环境抽离:
├── config
│ ├── dev.js
│ ├── test.js
│ ├── prod.js
│ └── index.js
各个环境文件:
// dev.js
const dev = {
baseUrl:'https://www.dev.com'
}
module.exports = dev
// test.js
const dev = {
baseUrl:'https://www.test.com'
}
module.exports = test
// prod.js
const dev = {
baseUrl:'https://www.prod.com'
}
module.exports = dev
获取环境文件:
// config/index.js
const env = process.env.ENV || 'dev'
const envConfig = require(`./${env}.js`) || require('./dev.js')
module.exports = {
baseUrl:envConfig.baseUrl
}
(2)使用.env文件来储存环境参数
上面的方案.env
系列文件只承担了提供环境名的作用,其他环境参数均由config
文件来提供,我们也可以完全将所有环境参数配置到.env
文件下,config
文件只承担一个媒介的作用
//.env.dev
VUE_ENV='dev'
VUE_BASE_URL='https://www.dev.com'
//.env.test
VUE_ENV='test'
VUE_BASE_URL='https://www.test.com'
//config.js
module.exports={
baseUrl:process.env.VUE_BASE_URL
}
以上,就是我们介绍的第一套多环境配置方案,虽然有多种变种,但大体思路是一致的,都是通过process.env
为业务代码提供当前环境的参数,这就要求process.env
能维持在编译和运行阶段。
我们可以看出,这套方案存在几个不好的缺点:
- 当环境数量变多时,根目录下.env系列文件会越来越多
- 业务代码使用环境参数时,开发人员点击跳转过去时不能直接看到具体值
- 开发人员无法直接获知当前环境的所有参数,需要先知道当前环境名,再从相应的环境文件查看
仔细想一下,上述方案不同环境下改变的是process.env
的值,在运行阶段config
文件依据这个值去获取环境文件或环境参数,那么,如果我们在编译阶段就把最终的环境文件生成出来,那么在运行阶段就不用去依赖process.env
,直接import这个环境文件,不也可以吗?由此就有了我们的第二套方案
二、使用动态生成的环境文件
我们首先写一个node脚本,这个脚本需要干3件事:
1、获取脚本命令定义的环境名和node环境名,通过这个环境名设置编译阶段的process.env
2、获取config/index.js
里的环境对象
3、将这个环境对象写入根目录下.env.json
文件
稍微会点nodejs的话很轻松的就能写出来
/// resetConfig.js
const Path = require('path')
const env = process.argv[3] // 规定业务环境环境在命令第4个参数
const nodeEnv = process.argv[2] // 规定node环境环境在命令第3个参数
process.env.ENV = env
process.env.NODE_ENV = nodeEnv
function changeJson(filePath, newData, callback) {
const fs = require('fs')
fs.readFile(filePath, 'utf-8', (err, file) => {
if (err) {
throw err
}
/** 转成json字符串并格式化 **/
const newFile = JSON.stringify(newData, null, 3)
fs.writeFile(filePath, newFile, 'utf-8', (error) => {
if (err) {
throw error
}
callback(!err)
})
})
}
const envInfo = require(`./index.js`)
changeJson(Path.resolve(__dirname, '../.env.json'), envInfo, function (success) {
console.log(require('../.env.json'))
})
至于config
文件的内容,跟我们第一套方案差不多,也是利用process.env
取获取当前环境名,不同的是,这套方案下config
文件只在编译阶段执行,我们可以用文件夹检索的方式来分离环境文件
// dev.js
const dev = {
baseUrl:'https://www.dev.com'
}
module.exports = dev
// test.js
const dev = {
baseUrl:'https://www.test.com'
}
module.exports = test
// prod.js
const dev = {
baseUrl:'https://www.prod.com'
}
module.exports = dev
// config/index.js
const env = process.env.ENV || 'dev'
const nodeEnv=process.env.NODE_ENV
const envConfig = require(`./${env}.js`) || require('./dev.js')
module.exports = {
baseUrl:envConfig.baseUrl
}
接下来,我们需要在package.json里编写node命令,注意,我们自定义的node脚本需要在项目构建命令前执行
// package.json
{
"name": "xx",
"version": "1.0.0",
"scripts": {
"config": "node ./config/resetConfig.js",
"serve": "yarn run config development dev && vue-cli-service serve",
"build:test": "yarn run config production test && vue-cli-service build",
"build:prod": "yarn run config production prod && vue-cli-service build",
},
...
}
接下来,执行yarn serve
,我们可以看到根目录下.env.json
文件已经引入了dev
环境的参数,
baseUrl:'https://www.dev.com'
使用时,只需要import这个json文件即可
import env from '.env.json'
const baseUrl = env.baseUrl
如果运行过程中,想切换成其他环境,可以直接用命令无缝切换
yarn run config development test