配置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=http://choerodon.com.cn
AUTH_URL=https://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)。
如上所述,構建時配置將滿足大多數場景,既可以自定義增加變量,用自定義的值作爲最終指,也可以用環境變量覆蓋。
關於Choerodon豬齒魚
Choerodon豬齒魚開源多雲技術平臺,是基於開源技術Kubernetes,Istio,knative,Gitlab,Spring Cloud來實現本地和雲端環境的集成,實現企業多雲/混合雲應用環境的一致性。平臺通過提供精益敏捷、持續交付、容器環境、微服務、DevOps等能力來幫助組織團隊來完成軟件的生命週期管理,從而更快、更頻繁地交付更穩定的軟件。
大家也可以通過以下社區途徑瞭解豬齒魚的最新動態、產品特性,以及參與社區貢獻: