前端多环境配置实践篇

2,558 阅读5分钟

前言

接上篇的原理介绍文章,笔者最近终于在项目中又一次落地实践了一下,所以才有了这篇实践篇。

虽然说之前也有其中一种方式的实践,但是由于时间的问题(懒),一再搁置,直到最近把另外一种方式也尝试了,觉得是时候沉淀一下了。

下面就来介绍一下实现多环境部署的两种方式吧!

持续交付 - 容器化

在没有容器化之前一般都是,upload 文件到服务器的指定位置 或 使用 git 仓库的能力不断的更新文件,虽说可行,但不是很优雅。

在容器化后,一般前端持续交付都会使用 Docker 镜像,构建完成之后,把 dist 目录放到 nginx 镜像中,启动的时候挂载一下静态资源就完成了交付工作。

容器启动之间可以由我们自定义一些脚本或者启动命令,此时就大大的增加了可操作性。

无论是 shell 脚本或者是 js 脚本都能够用来改变流程解决问题。

实践

首先我们对于多环境的需求的前提大家可能都不一样,下面都是笔者在实际工作中遇到需要多环境的问题

  1. SaaS 类需要将产品私有化部署给不同的客户(配置的域名可能不一样),此时 publich_path 等就需要不一样了
  2. git 流程下,希望使用 release 分支构建出产物后,测试完成后,用来发布 preview 或者 master 保持环境统一

运行时构建

第一种方式,曾经在 ssr 镜像中尝试过,因为本来就需要把 node_modules 里面的依赖一并打包到镜像中上传的,所以使用这种方式并不会增加多小改造成本。

但是随之而来也会有启动时间的问题:构建需要时间,如果太久的话,可能会影响到项目的快速回滚。

流程为大致为: yarn install -> docker build image -> upload -> docker run image -> build && yarn start

在 run 之前我们可以进行变量注入,可以通过 docker 的参数注入到环境变量中,也可以通过 dotenv 复制 .env 文件到 docker 中,这样使用 config 的时候也能读取到变量。

占位符替换

理论知识

第二种方式其实在理论篇里面也说的很明白了

其本质上是利用打包时,先填入一个占位符,例如 _PLACEHOLDER__加在对应变量 key 前面,然后在部署前替换字符串为实际使用的变量达到多环境变量的效果


这里会依赖一个 shell 脚本 作为替换的工具,可以使用 js 重写一个作为替换

步骤是在打包时读取一份独特的 .env 把所有的希望动态的环境变量都加上前缀 _PLACEHOLDER__

.env.placeholder 中,此时应该会长这样子,然后在构建时选择使用这份作为打包变量即可

API_URL=_PLACEHOLDER__API_URL
LOGIN_URL=_PLACEHOLDER__LOGIN_URL
CAPTCHA_URL=_PLACEHOLDER__CAPTCHA_URL

然后打包的时候还是照常打包,但是可以独立出来一个专门的构建命令,例如 "build:image": "cross-env ENV=docker yarn build"

注意我们这里写入的时候对 env 变量注入了一个字符串 docker 来标记打包的环境。

拿到这个标记,我们就可以在构建的时候用来加载不同的 env 文件

const dotenv = require('dotenv')

const env = process.env.ENV || 'development'

if (env !== 'docker') {
	dotenv.config()
} else {
 dotenv.config({
   path: './.env.placeholder'
 })
}

那么在构建之后,我们的代码里使用到这些变量的地方,就会先用这些占位符做替代了。

在启动镜像之前获取真实变量,还原回去即可。

正常使用

正常使用下,上面的 replace.sh 需要接受变量,使用方式在启动的脚本后面加入对应变量的健值对即可

例如 sh ./replace.sh key1=value1 key2=value2

demo 在这里

在构建之后把 replace.sh copy 到 build 目录,一起打包到镜像中,然后启动的时候,以 replace.sh 先执行即可

yarn deploy:build # 构建

yarn deploy:start # 执行替换, 

cd build && npx live-server --port=1111

完成后就会出现以后界面,中间圈起来的字符就是可以通过穿参数进行替换的变量

deploy:start 中所执行的是下面的代码 REACT_APP_CUSTOM_KEY 后面的值可以自定义成其他的值

cd build && sh ./replace.sh REACT_APP_CUSTOM_KEY=cjfff

配合 .env 使用

有时候我们期望直接复制一下 .env 文件,读取里面的变量,不想一个个复制粘贴列在上面进行替换,此时配合 .env 读取会有奇效

先放一下 demo

跟上面的启动步骤一样,先执行 build, 再执行 start

其中不一样的是里面的内容

build 的时候会多复制 .env 文件,start.js 文件 build 目录

"deploy:build": "yarn build && cp ./replace.sh ./build && cp ./start.js ./build && cp ./.placeholder.env ./build/.env",

start 启动的时候,会使用 dotenv 先读取 .env 变量,传递进去 ./replace.sh 启动

cd build && node start.js

start.js

const config = require('dotenv').config()
const { exec } = require('child_process')

const object = config.parsed ?? {}

const valueStr = Object.entries(object).reduce(((arr, [k, v]) => {
    return arr.concat([k, v].join('='))
}, [])).join(' ')

exec(`./replace.sh ${valueStr}`

.env 不一定需要在 build 的时候就确认,这里为了方便演示而已,到时候可以是在启动的时候再确认这个文件,先初始化,再进行 start 替换即可

未解决问题

但这也会有问题,对于 sourceMap 来说,如果我们要替换的变量很长,就可能会导致我们的错误上报的代码行数从而查看源代码不一致的问题。


全局变量

方案

其实一般情况下,使用占位符就可以解决问题,但是有时候我们希望使用 sentry 上报错误等,需要在不修改原文件的情况下完成多环境发布的需求

这时候可以使用全局变量的方式,类似于,在全局创建一个 window.config = {appId: 'cjfff'}

在使用到变量的地方,使用 window.config.xxx 的全局变量进行替换本来的 process.env.xxx 即可

方案定下来后,我们还是要解决一下生产开发环境统一的问题

由于笔者的 demo 使用的 create-react-app 下文中,会简称为 cra

遇到这种需要定制化的时候,就直接 run eject 配置了

首先我们要改造的是 html 模板, html-webpack-plugin 本身就支持 ejs 语法,所以我们利用 ejs 语法,直接把 env 对象通过对象的形式透传进去生成对象即可

开发环境

先在模板的 head 上方加入一个 script标签,为什么加在 head 里?因为需要在所有的 js bundle 之前加载我们的全局变量

<!DOCTYPE html>
<html lang="en">
  <head>
    <script>
      window.CONFIG = <%= JSON.stringify(htmlWebpackPlugin.options.__variables__) %>
    </script>
  </head>

然后在 eject 出来的配置中加入 __variables__的传入

找到我们 html-webpack-plugin 的配置

这样当然也可以直接用 react 读取出来的 env 对象,但是它本身有个判断,比如为 REACT_APP_前缀的变量才会被加载

所以如果不想所有的变量都加前缀,就可以跟我一样直接使用 dotenv读取变量

.env 文件中加入变量

现在 app.js中我们加入读取变量的代码

现在启动一下项目查看一下状态

此时开发环境的配置就完成了

生产环境

上面是开发环境的配置,接下来是生产环境的配置,也就是如何做到多环境更换变量的关键

首先我们构建之后,index.html 的变量已经是固定的了,我们拿着构建好的 build目录,此时就要想别的办法去进行变量的替换了

这里我想到了最原始的字符串替换

    <!-- config start -->
    <script>
      window.CONFIG = <%= JSON.stringify(htmlWebpackPlugin.options.__variables__) %>
    </script>
    <!-- config end -->

通过在最外面加上特定的标记符,让我们启动的时候,对这个标记符进行简单的字符串替换即可

这里有一点要注意,我们的标记符是个注释,默认 html-webpack-plugin 会把所有的注释清除掉,需要配置成

minify: {
    removeComments: false,
}

然后我们看一眼打包出来的效果

然后使用写好的 js 脚本进行替换即可

先在 package.json加入我们的自定义 script

  "scripts": {
    "deploy:build": "yarn build && cp ./start.js ./build && cp ./.env ./build",
    "deploy:start": "cd build && node start.js"
  }

start.js

const dotenv = require('dotenv')
const path = require('path')
const fs = require('fs')

const envName = getEnv()

// 载入 process.env
const envConfig = dotenv.config()

// 读取变量
const config = envConfig.parsed || {}

console.log('==== current config ====')
console.log(config)

const filePaths = getHtmlPaths()

writeHtml(filePaths)

console.log(`replace ${envName} successful!!`)

function writeHtml(filePaths) {
  filePaths.forEach((path) => {
    const html = fs.readFileSync(path, 'utf-8')
    fs.writeFileSync(path, replaceHtml(html, config), 'utf-8')
  })
}

function replaceHtml(html, config) {
  const configPlaceholder = /<!-- config start -->[\s\S]*?<!-- config end -->/

  const renderConfig = (config) => `
<script>
  window.CONFIG = ${JSON.stringify(config)}
</script>
    `

  // config
  html = html.replace(configPlaceholder, renderConfig(config))

  return html
}

function getHtmlPaths() {
  const ROOT_DIR = path.join(__dirname, './')

  const HTML_PATH = path.join(ROOT_DIR, './index.html')

  return [HTML_PATH]
}

function getEnv() {
  return `.env`
}

start.js 的作用是读取 env 然后生成字符串重写进去 index.htmlenv 的步骤这里为了演示是在 build 的时候 copy 进去build目录的


启动的时候先执行 yarn deploy:start 即可,实际读取变量需要动态的时候,可以通过接口,或者不同文件来源来进行变量的读取,这里是为了方便演示

执行完毕之后进入此命令查看效果 npx live-server --port=1111

看到画面中的变量已经写进去了,到这里全局变量方案已经大功告成了

总结

在本文中,我尝试了三种可以让前端进行多环境部署的方案,其中有运行时构建,占位符替换,全局变量三种不同的方式,对于读者来说,可以结合实际分析哪个更适用自己的项目来结合整改使用。

如果你有其他更好的实现方式,也可以留言交流

Demo 地址

  1. 占位符替换
  2. 占位符替换配合 env
  3. 全局变量