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 的時候注入環境變量。那么編譯時的環境變量能不能解決問題呢?看情況了...可以做一個簡單的對比。
要知道我們通常要把什么樣子的環境變量注入到 spa 中。額,我這里的需求很有限,為了讓前后端一起運作,我所需要的環境變量就是后端 API 的入口。對于部署流程簡單到之后生產環境且生產環境固定(尤其是后端生產環境 IP、域名固定)的情況,直接在編譯時將后端的入口寫死注入就行了。但如果有多個環境(staging)的需求就不適用了,假如沒有運行時環境變量的支持為不同的環境提供不同的入口只能重新編譯應用并注入不同的變量。
有沒有需求在應用運行時修改我們的環境變量。很明顯運行時的環境變量支持通過重啟就能修改環境變量的功能,如果有這種靈活修改環境變量的情況,編譯時環境變量很明顯也不能滿足。
在編譯時對代碼選擇和裁剪。很明顯,這個是最應該使用編譯時環境變量的地方了。
說白了,其實不同時期的環境變量的作用是不一樣的。兩者不可能做到相互替代,在 [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'
}
});
- 直接在文件中引入
window.__env
全局變量 - 在需要的地方引用其中的變量即可
當然,這種依賴 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-spa
的 Dockerfile
即可。
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 文件。
這里 是一個樣例項目。
相關資料
更多內容請見 aisensiy.github.io