最近攻克了一個以前部署 single-page-app 的一個痛點:支持在運行時環境變量。這裏講述一下問題以及目前的解決方案。javascript
目前個人絕大部分的項目都是一個先後端分離的方式開發的。其中前端基本都是用 create-react-app
建立出來的標準的 react 的 spa 應用。這種 spa 在部署是將全部的 js 和 css 打包成一個或多個文件而後用 serve
或者其餘相似的 http server 以靜態文件的形式對外提供服務,可是這種前端靜態文件話的應用沒有 nodejs 的支持,沒辦法使用 process.env
這樣的運行時注入環境變量的功能。css
目前 create-react-app
提供了一個編譯運行時環境變量的方案,由於在 build
的時候是有 nodejs
支持的,經過 REACT_APP_API_URL=http://xxx.com yarn run build
的方式在編譯 spa 的時候注入環境變量。那麼編譯時的環境變量能不能解決問題呢?看狀況了...能夠作一個簡單的對比。html
要知道咱們一般要把什麼樣子的環境變量注入到 spa 中。額,我這裏的需求頗有限,爲了讓先後端一塊兒運做,我所須要的環境變量就是後端 API 的入口。對於部署流程簡單到以後生產環境且生產環境固定(尤爲是後端生產環境 IP、域名固定)的狀況,直接在編譯時將後端的入口寫死注入就好了。但若是有多個環境(staging)的需求就不適用了,假如沒有運行時環境變量的支持爲不一樣的環境提供不一樣的入口只能從新編譯應用並注入不一樣的變量。前端
有沒有需求在應用運行時修改咱們的環境變量。很明顯運行時的環境變量支持經過重啓就能修改環境變量的功能,若是有這種靈活修改環境變量的狀況,編譯時環境變量很明顯也不能知足。java
在編譯時對代碼選擇和裁剪。很明顯,這個是最應該使用編譯時環境變量的地方了。node
說白了,其實不一樣時期的環境變量的做用是不同的。二者不可能作到相互替代,在 [1]
[2]
兩個場景都是使用運行時環境變量比較舒服的地方,採用編譯時的環境變量實在是不太方便。下面就介紹一下目前讓 spa 應用支持運行時環境變量的方法,這裏仍是以 create-react-app
的模板爲示例。react
前端沒有 process.env
這樣的東西,咱們只能用 javascript 的全局變量模擬。在將這個打包好的 spa 運行起來的時候,咱們須要利用 shell 腳本生成這個 config.js 文件,讓它把必要的環境變量翻譯成全局變量。而後讓默認的入口 html 文件引入這個全局變量文件。ios
首先,咱們須要一段 shell 腳本,把環境變量翻譯成 config.js
文件:nginx
#!/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 "$@"
若是咱們提供這樣的環境變量git
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