為 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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,969評論 19 139
  • 寫在開頭 先說說為什么要寫這篇文章, 最初的原因是組里的小朋友們看了webpack文檔后, 表情都是這樣的: (摘...
    Lefter閱讀 5,324評論 4 31
  • 無意中看到zhangwnag大佬分享的webpack教程感覺受益匪淺,特此分享以備自己日后查看,也希望更多的人看到...
    小小字符閱讀 8,237評論 7 35
  • 分享一本書,日本作家的《整理是一切的開始》,書的封面上最醒目的一句話是,省下更多時間,花在自己喜歡的人和事上!不懂...
    雪穎清葙閱讀 2,235評論 0 3
  • 敢跟我爭寵的女人都得死。 華妃娘娘的霸氣,即便《甄嬛傳》熱播過去很久,依舊能夠讓觀眾脊背發涼,尤其是那余光一撇的眼...
    媽小咪閱讀 671評論 1 2