Choerodon前端環境變量方案

配置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環境時,只有環境變量才起效(本地設置的值是無效的)。

缺點

通過上一章節和示意圖的分析,可以發現如下缺點:

  1. 增加環境變量是很複雜的:當增加一個環境變量,要修改至少三處地方(enterpoint.sh, contants.js, updateWebpackConfig.js)。如果使用@choerodon/boot的其他項目要加入一個環境變量(這個變量可能只有該項目使用),即使boot(啓動器項目,腳手架)沒有做任何修改,也必須增加了變量發佈一個新版本。

  2. 而且從上文可以看出,界定哪些變量是config.js中配置,哪些是環境變量注入是很不明確的(或者說是隨@choerodon/boot開發者確定的)

  3. 無法明確知道變量是否還在使用。

  4. 部署生產環境時,有些變量是必須有環境變量的(一般的邏輯是環境變量覆蓋用戶變量再覆蓋默認值)。

新方案

還是使用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腳本進行合併。

使用

  1. 將@choerodon/boot版本進行升級

  2. 在項目根目錄下(和package.json同級)創建.env文件(如果沒有自定義變量可以不創建)

  3. 以鍵=值的形式寫入變量,如

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等能力來幫助組織團隊來完成軟件的生命週期管理,從而更快、更頻繁地交付更穩定的軟件。

大家也可以通過以下社區途徑瞭解豬齒魚的最新動態、產品特性,以及參與社區貢獻:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章