豬齒魚前端環境變數方案
配置React應用程式的方法有很多,本文中將向大家展示Choerodon平台前端的新環境變數方案,該方案可以實現在運行時配置,所以不需要針對每個環境都進行構建。
需求
希望能夠將React應用程式使用Docker運行,只構建一次,能夠在任何地方運行,並且希望在運行時提供重新配置容器的時機,允許在docker-compose文件內進行變數配置。
例如:
version: "3.2"
services:
my-react-app:
image: my-react-app
ports:
- "3000:80"
environment:
- "API_URL=production.example.com"
*註: 在開發中,有兩種不同的環境變數。一是在編譯時已確定的,通過類似config.js的配置文件進行配置;而另一種是在部署時(運行時)才確定的,比較常見的是根據環境進行區分的一些變數,比如API請求地址前綴,根據部署的環境不同而不同。當前端鏡像生成後,需要通過外部去注入這個變數。
原來的方案
原來的方案將兩種變數混合在一起,導致很難區分到底哪些是可以通過環境變數注入修改的。
程式碼如下:
// updateWebpackConfig.js
const { apimGateway } = choerodonConfig;
...
if (mode === 'start') {
defaultEnterPoints = {
APIM_GATEWAY: apimGateway,
};
} else if (mode === 'build') {
if (isChoerodon) {
defaultEnterPoints = getEnterPointsConfig();
}
}
const mergedEnterPoints = {
NODE_ENV: env,
...defaultEnterPoints,
...enterPoints(mode, env),
};
const defines = Object.keys(mergedEnterPoints).reduce((obj, key) => {
obj[`process.env.${key}`] = JSON.stringify(process.env[key] || mergedEnterPoints[key]);
return obj;
}, {});
customizedWebpackConfig.plugins.push(
new webpack.DefinePlugin(defines),
...
// getEnterPointsConfig.js
const enterPoints = {
APIM_GATEWAY: 'localhost:apimgateway',
};
export default function getEnterPointsConfig() {
return enterPoints;
}
具體步驟為:
當為本地啟動(start,development)時:
- 先通過config.js中獲取到變數值
- 構造defaultEnterPoints對象,把變數放入
- 然後通過DefinePlugin插件把這個對象的鍵作為變數注入
為了便於管理,在@choerodon/boot/lib/containers/common/constants中有如下程式碼:
export const TYPE = `${process.env.TYPE}`;
export const RESOURCES_LEVEL = `${process.env.RESOURCES_LEVEL || ''}`;
export const APIM_GATEWAY = `${process.env.APIM_GATEWAY}`;
export const UI_CONFIGURE = `${process.env.UI_CONFIGURE}`;
export const EMAIL_BLOCK_LIST = `${process.env.EMAIL_BLOCK_LIST}`;
在程式碼中就可以使用了。
當為生產環境(build, product)時:
- 直接使用默認的環境變數及其佔位值(這些值是後來替換環境變數的依據)
- 通過structure/enterpoint.sh去執行全局替換
#!/bin/bash
set -e
find /usr/share/nginx/html -name '*.js' | xargs sed -i "s localhost:8080 $PRO_API_HOST g"
find /usr/share/nginx/html -name '*.html' | xargs sed -i "s localhost:titlename $PRO_TITLE_NAME g"
exec "$@"
使用腳本去進行全局搜索,然後進行字元替換。
可見,當為product環境時,只有環境變數才起效(本地設置的值是無效的)。
缺點
通過上一章節和示意圖的分析,可以發現如下缺點:
-
增加環境變數是很複雜的:當增加一個環境變數,要修改至少三處地方(enterpoint.sh, contants.js, updateWebpackConfig.js)。如果使用@choerodon/boot的其他項目要加入一個環境變數(這個變數可能只有該項目使用),即使boot(啟動器項目,腳手架)沒有做任何修改,也必須增加了變數發布一個新版本。
-
而且從上文可以看出,界定哪些變數是config.js中配置,哪些是環境變數注入是很不明確的(或者說是隨@choerodon/boot開發者確定的)
-
無法明確知道變數是否還在使用。
-
部署生產環境時,有些變數是必須有環境變數的(一般的邏輯是環境變數覆蓋用戶變數再覆蓋默認值)。
新方案
還是使用shell腳本的方式,但這次直接生成js文件,通過window.env = {}來注入一個全局的變數,使用時只要通過window._env_yourVarName來獲取即可。
具體分為如下幾個文件:
- .default.env: 該文件一般是一些初始環境變數的默認值,目前包括一些原有的環境變數,達到平滑升級的效果。
- .env: 用戶使用的環境變數文件,用戶可以在該文件中以鍵=值的形式聲明環境變數
- \env-config.js: 通過運行shell腳本後生成的最終環境變數對象,結構如下:
- env.sh: sh腳本,進行用戶環境變數和注入環境變數的合併
#!/bin/bash
mode=$1
# Recreate config file
rm -rf ./env-config.js
touch ./env-config.js
# Add assignment
echo "window._env_ = {" >> ./env-config.js
# Read each line in .env file
# Each line represents key=value pairs
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
# Read value of current variable if exists as Environment variable
value=$(printf '%s\n' "${!varname}")
# Otherwise use value from .env file
[[ -z $value ]] && value=${varvalue}
# Append configuration property to JS file
echo " $varname: \"$value\"," >> ./env-config.js
done < .env
while read -r line || [[ -n "$line" ]];
do
# Split env variables by character `=`
if printf '%s\n' "$line" | grep -q -e '='; then
varname=$(printf '%s\n' "$line" | sed -e 's/=.*//')
varvalue=$(printf '%s\n' "$line" | sed -e 's/^[^=]*=//')
fi
# Read value of current variable if exists as Environment variable
value=$(printf '%s\n' "${!varname}")
# Otherwise use value from .env file
[[ -z $value ]] && value=${varvalue}
# Append configuration property to JS file
echo " $varname: \"$value\"," >> ./env-config.js
done < .default.env
echo "}" >> ./env-config.js
echo "// ${mode}" >> ./env-config.js
步驟解析:
- 創建env-config.js文件
- 寫入第一行window.env={
- 遍歷.default.env,如果存在環境變數,則寫入鍵:環境變數值,不存在則寫入鍵:值
- 遍歷.env,處理邏輯同上(這裡使用了js對象重複聲明一個值,後面的會覆蓋前面的特性)
- 在文件最後寫上}表示結束
優化
由於考慮到window平台的開發者,在node內部調用shell腳本可能不會運行,所以用node模擬了一套上述方案,在本地開發和打包時,使用node進行環境變數的合併,當使用環境變數注入時,調用shell腳本進行合併。
使用
-
將@choerodon/boot版本進行升級
-
在項目根目錄下(和package.json同級)創建.env文件(如果沒有自定義變數可以不創建)
-
以鍵=值的形式寫入變數,如
SERVER=//choerodon.com.cn AUTH_URL=//api.choerodon.com.cn/oauth/login
需要解決的問題
開發環境時,通過webpack-dev-server生成的html文件在記憶體中,那env-config.js寫到哪?
通過配置contentBase來載入
// start.js
const serverOptions = {
quiet: true,
hot: true,
...devServerConfig,
// contentBase: path.join(process.cwd(), output),
contentBase: [path.join(__dirname, '../../'), ...],
historyApiFallback: true,
host: 'localhost',
};
WebpackDevServer.addDevServerEntrypoints(webpackConfig, serverOptions);
shell的當前目錄相對於命令運行時的目錄而不是文件目錄
spawn.sync(shellPath, ['development'], { cwd: path.join(__dirname, '../../../'), stdio: 'inherit' });
通過指明cwd命令來改變當前文件路徑。
具體過程
當為本地啟動(start,development)時:
- 先在用戶根目錄下進行查找是否有.env文件
- 如果有.env文件,複製到@choerodon/boot根目錄下,與env.sh同級
- 根據shell中的邏輯,合併.default.env和.env的環境變數
- 生成env-config.js到同級目錄下,由於該目錄被設置為contentBase,所以啟動的程式碼中能夠載入到該目錄
當為生產環境打包(build,product)時:
- 先在用戶根目錄下進行查找是否有.env文件
- 如果有.env文件,複製到@choerodon/boot根目錄下,與env.sh同級
- 根據shell中的邏輯,合併.default.env和.env的環境變數
- 生成env-config.js到同級目錄下
- 複製.env,.default.env,env.shell,env-config.js到dist目錄下,與index.html同級
(全部複製是為了應對環境變數注入,也要考慮不注入環境變數的情況,此時env-config.js就是最終的變數)
環境變數替換時:
- 通過docker運行shell腳本
- 根據.default.env和.env的鍵去生成window._env_對象,此時有環境變數則替換為環境變數,無環境變數則使用原來值,生成的env-config.js在同級目錄下
Q&A
▍哪些變數適合放在這
當採用新的模式後,所有的決定權都在於開發人員(需要慎重),開發人員可以自己聲明一個變數,然後在程式碼中使用,這時當部署生產環境時,可以在.env中聲明一個值,然後通過環境變數去覆蓋,也可以只是聲明這個值(類似於原來的config.js中配置)。
但是總的來說,建議仔細考慮哪些變數是應該作為環境變數注入的。
有兩種比較方便的判斷方式:
- 當一個前端鏡像部署到不同環境時,該變數值是否應該改變,如果是,可能應該作為一個環境變數
- 變數是運用在程式碼打包時的,那麼該變數可能是個非環境變數
▍加入了環境變數後不起效
加入了環境變數後,可以在node_modules/@choerodon/boot/env-config.js中查看,自己的環境變數到底有沒有被注入,如果被別的庫覆蓋,可以考慮起個獨特的名字或者和他人進行商議(後期會考慮當環境變數重複時,進行警告等檢測)。
當部署後,可以通過瀏覽器直接打開env-config.js文件來查看變數的情況。
▍原來的環境變數方案會被剔除嗎
暫時不會剔除原先的環境變數方案,即還是可以通過costants.js中獲取部分環境變數值,但是不排除在今後剔除這種模式。
▍當環境變數不是字元類型時怎麼處理
一般環境變數都是以字元形式注入的,非字元形式可以通過config.js進行處理(如鉤子函數等),或者通過序列化來進行處理(JSON.stringfiy)。
如上所述,構建時配置將滿足大多數場景,既可以自定義增加變數,用自定義的值作為最終指,也可以用環境變數覆蓋。
本文由豬齒魚技術團隊原創,轉載請註明出處