为 Single Page App 提供运行时环境变量

SPA 没有运行时环境变量的痛点

目前我的绝大部分的项目都是一个前后端分离的方式开发的。其中前端基本都是用 create-react-app 创建出来的标准的 react 的 spa 应用。这种 spa 在部署是将所有的 js 和 css 打包成一个或多个文件然后用 serve 或者其他类似的 http server 以静态文件的形式对外提供服务,但是这种前端静态文件话的应用没有 nodejs 的支持,没办法使用 process.env 这样的运行时注入环境变量的功能。

目前 create-react-app 提供了一个编译运行时环境变量的方案,因为在 build 的时候是有 nodejs 支持的,通过 REACT_APP_API_URL=http://xxx.com yarn run build 的方式在编译 spa 的时候注入环境变量。那么编译时的环境变量能不能解决问题呢?看情况了...可以做一个简单的对比。

  1. 要知道我们通常要把什么样子的环境变量注入到 spa 中。额,我这里的需求很有限,为了让前后端一起运作,我所需要的环境变量就是后端 API 的入口。对于部署流程简单到之后生产环境且生产环境固定(尤其是后端生产环境 IP、域名固定)的情况,直接在编译时将后端的入口写死注入就行了。但如果有多个环境(staging)的需求就不适用了,假如没有运行时环境变量的支持为不同的环境提供不同的入口只能重新编译应用并注入不同的变量。

  2. 有没有需求在应用运行时修改我们的环境变量。很明显运行时的环境变量支持通过重启就能修改环境变量的功能,如果有这种灵活修改环境变量的情况,编译时环境变量很明显也不能满足。

  3. 在编译时对代码选择和裁剪。很明显,这个是最应该使用编译时环境变量的地方了。

说白了,其实不同时期的环境变量的作用是不一样的。两者不可能做到相互替代,在 [1] [2] 两个场景都是使用运行时环境变量比较舒服的地方,采用编译时的环境变量实在是不太方便。下面就介绍一下目前让 spa 应用支持运行时环境变量的方法,这里还是以 create-react-app 的模板为示例。

全局配置 + Docker 化部署

前端没有 process.env 这样的东西,我们只能用 javascript 的全局变量模拟。在将这个打包好的 spa 运行起来的时候,我们需要利用 shell 脚本生成这个 config.js 文件,让它把必要的环境变量翻译成全局变量。然后让默认的入口 html 文件引入这个全局变量文件。

首先,我们需要一段 shell 脚本,把环境变量翻译成 config.js 文件:

#!/bin/bash

if [[ $CONFIG_VARS ]]; then

  SPLIT=$(echo $CONFIG_VARS | tr "," "\n")
  ARGS=
  for VAR in ${SPLIT}; do
      ARGS="${ARGS} -v ${VAR} "
  done

  JSON=`json_env --json $ARGS`

  echo " ==> Writing ${CONFIG_FILE_PATH}/config.js with ${JSON}"

  echo "window.__env = ${JSON}" > ${CONFIG_FILE_PATH}/config.js
fi

exec "$@"

如果我们提供这样的环境变量

export REACT_APP_API_PREFIX=http://petstore-backend.example.com
export CONFIG_VARS=REACT_APP_API_PREFIX

那么所生成的 config.js 文件是这个样子的:

window.__env = {
  'REACT_APP_API_PREFIX': 'http://petstore-backend.example.com'
}

然后,我们需要在 原来的 index.html 模板文件中引入这个我们生成的 config.js 文件:

<!doctype html>
<html lang="en">
  <head>
  ...
  </head>
  <body>
    <noscript>
      You need to enable JavaScript to run this app.
    </noscript>
    <div id="root"></div>
    <script type="text/javascript" src="config.js"></script>
  </body>
</html>

这样,我们就拥有了一个 window.__env 的全局对象,它包含了所有的运行时环境变量。我们可以以如下的方式使用它:

axios.defaults.adapter = httpAdapter;

let baseUrl;
let env = window.__env || {}; // 1

if (process.env.NODE_ENV === 'test') {
  baseUrl = 'http://example.com';
} else if (process.env.NODE_ENV === 'development') {
  baseUrl = env.REACT_APP_API_PREFIX || 'http://localhost:8080'; // 2
} else {
  baseUrl = env.REACT_APP_API_PREFIX;
}

const fetcher = axios.create({
  baseURL: baseUrl,
  headers: {
    'Content-Type': 'application/json'
  }
});
  1. 直接在文件中引入 window.__env 全局变量
  2. 在需要的地方引用其中的变量即可

当然,这种依赖 shell 生成 config.js 的方案只有我们将 spa 打包好的之后才会使用,为了更好的使用这个 shell 我们可以采用 docker 化的方式把其启动流程以 entrypoint 的方式固化在应用的启动流程中。SocialEngine/docker-nginx-spa 就实现了这个方案,是一个很好的用 base image。如果我们需要创建一个支持运行时环境变量的 create-react-app spa 的时候,首先按照上面的步骤修改 public/index.html 并且用 window.__env 作为环境变量使用。然后提供一个继承自 SocialEngine/docker-nginx-spaDockerfile 即可。

FROM socialengine/nginx-spa

COPY build/ /app

其中 build/create-react-app 编译生成静态文件的默认目录。然后打包运行这个应用的方式如下:

$ yarn run build
$ docker build -t spa-app .
$ docker run -e CONFIG_VARS=REACT_APP_API_PREFIX -e REACT_APP_API_PREFIX=http://petstore-backend.example.com -p 3000:80 spa-app

当然,我们本地开发环境不用这么麻烦。只需要在 public/ 目录下自己创建一个 config.js 然后把开发需要的环境变量塞进去就可以了。在 docker 化后,entrypoint 触发的命令会自动覆盖这个 config.js 文件。

这里 是一个样例项目。

相关资料

  1. create-react-app
  2. compile-time-vs-runtime
  3. serve
  4. SocialEngine/docker-nginx-spa

更多内容请见 aisensiy.github.io

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 224,242评论 6 522
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 95,953评论 3 402
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 171,299评论 0 366
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 60,709评论 1 300
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 69,723评论 6 399
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 53,236评论 1 314
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 41,629评论 3 428
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 40,594评论 0 279
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,135评论 1 324
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,156评论 3 345
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,285评论 1 354
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,914评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 42,600评论 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,073评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,203评论 1 275
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 49,798评论 3 381
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 46,339评论 2 365

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,735评论 18 139
  • 写在开头 先说说为什么要写这篇文章, 最初的原因是组里的小朋友们看了webpack文档后, 表情都是这样的: (摘...
    Lefter阅读 5,300评论 4 31
  • 无意中看到zhangwnag大佬分享的webpack教程感觉受益匪浅,特此分享以备自己日后查看,也希望更多的人看到...
    小小字符阅读 8,185评论 7 35
  • 分享一本书,日本作家的《整理是一切的开始》,书的封面上最醒目的一句话是,省下更多时间,花在自己喜欢的人和事上!不懂...
    雪颖清葙阅读 2,208评论 0 3
  • 敢跟我争宠的女人都得死。 华妃娘娘的霸气,即便《甄嬛传》热播过去很久,依旧能够让观众脊背发凉,尤其是那余光一撇的眼...
    妈小咪阅读 652评论 1 2