30分钟教你搞定前端(React、Vue)多环境配置

在一个正常的迭代周期内,一般会经历开发、测试、生产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  

值得注意的是,我们必须使用commonjsexport语法,不能使用es6import语法(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对象,再利用DefinePluginwebpack编译时将代码里所有的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-appvue-ci使用的.env配置方式跟上述的方案在原理上是一致的,如果我们使用VueReact的脚手架,我们不需要手动引入上述这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

猜你喜欢

转载自juejin.im/post/7123157744894345230