實戰 web 應用 Docker 鏡像解耦交付

把大象放進冰箱須要幾步?把一個 web 應用塞進集裝箱呢?html

隨着幾回瀏覽器大戰的硝煙散盡和 Flash 的背影遠去,當下的 web 應用開發通過十餘年的發展,在工程化、測試、持續集成等方面都已經匯入了軟件開發的快車道。前端

然而雖然新概念、新特性層出不窮,細分領域越發專業化,但其究極奧義始終未變 -- 無論你怎麼折騰,生成出來的交付物還是 HTML/CSS/JS 老三樣等靜態資源,加上若干動態請求 的形式。從直接把文件拖放到 FTP 軟件中手動上傳的刀耕火種時代,到現在 Docker 鏡像成爲一種常見的部署格式 ,研發團隊和運維團隊的交互也在發生變化。vue

本文將在我的經驗的基礎上,嘗試以一個前端項目爲案例,淺談其面向部署時的一些固有問題,以及與 Docker 相關的部分實踐。node

擁抱 Docker 時的麻煩

在此以前,要部署一個前端項目,運維人員須要作什麼呢?react

  • 安裝完整的 node 環境並保持其更新
  • 閱讀前端項目中 README 中的相關說明並更改相關文件中的設置項
  • 用 npm 安裝一些全局依賴項
  • 保證 npm run build 流程的正確運行
  • 和前端開發同事協做解決因爲打包機器不一樣可能帶來的問題

等搞定這麼一全套的「份外」工做,才能獲得打包後的目標文件並開始部署;這不可是多麼痛的一種領悟,也是工做流層面一系列莫大的耦合。nginx

"All problems in computer science can be solved by another level of indirection." -- David John Wheelergit

面對代碼封裝中出現的耦合類問題,即使不瞭解 SOLID 原則、DRY 原則等等,以上這句 「啥都不叫事,抽象就對了!」 也算得上應該謹記的萬金油了,是解耦的根本所在。github

Docker 鏡像就做爲這樣一種優良的抽象層,爲研發團隊和運維團隊更好地解耦提供了可能。web

然而在實際開發和部署中,囿於舊有經驗和認知水平,可能會存在一些新問題:ajax

利用不一樣的環境變量分別編譯

嚴格來講這不算遇到 Docker 後纔有的問題,能夠說絕大部分前端項目一直都是默認這麼作的。

根據 BUILD_ENV 環境變量,分別對開發、測試、預發、生產環境等區分編譯不一樣的 API 的訪問前綴 -- 好比對 GET /api/shops 數據接口的訪問地址被分別編譯成 http://test.com/api/shopshttps://api.stage.com/shops 等,雖然在傳統的物理主機/虛擬主機工做流中這是無可指摘的標準作法;但在 Docker 語境中,這會致使分屢次生成幾個不一樣的鏡像,從理論上難以保證「所測試的就是所部署的」這一理念。

此外,沒法控制團隊中的開發人員會利用這一特性添加什麼其它的變量,甚至由於線上 bug 在本地難以重現而加以濫用做出特殊處理的也並不鮮見,這些都會對項目部署形成未知的干擾。

因此對於環境變量,或許咱們應該稍稍反思並保證最小化使用,從而探索更適於 Docker 的新經驗。

在鏡像外獨立構建等

不管對於分發仍是部署,鏡像越小越好,這是面對 Docker 時的一條廣泛共識。對於構建過程當中常見的優化方式有:

  • 選用 alpine 版本的基礎鏡像
  • && 操做符來實現鏈式的 RUN 等指令以減小分層
  • 在容器中使用 nginx 而非 node 來伺服靜態文件(服務器軟件自己至少能減小 70M+)

另外,編譯過程當中的依賴文件 也是沒有必要包含在最終鏡像中的,通常的處理如:

  • 在 Dockerfile 中編譯而後用指令語句刪除一些文件
  • 分爲可複用的依賴鏡像和最終打包鏡像
  • 利用 Docker 的多階段構建,在一個 Dockerfile 中解決問題;後面會有介紹

比較糟糕的一種作法多是,每次讓運維人員利用相似 npm run build && docker build ... 的命令,在服務器上構建項目再打包到 Docker 鏡像中。這樣作既增長了運維團隊的負擔,使其和傳統模式同樣深陷在環境依賴和繁複流程中;又沒法保證其手動調整項目配置項等代碼後總體的正確性;且 npm 打包環境異於開發者,有較高的不肯定性。

構建參數

--build-arg 自己是個很方便的屬性,能在 docker build 時傳入必要的參數。但和項目中的環境變量相似,若是應用不當也會形成不一樣環境下鏡像不一致的問題。所以交由運維人員或者自動化執行的 docker build 命令最好沒有構建參數。

SASS 依賴

不一樣於其它依賴項,npm 安裝 node-sass 包時,會從 github.com 上下載 .node 文件等。因爲網絡環境的問題,這個下載時間一般會很長,甚至致使超時失敗。這每每成爲了運維人員一個意料以外的痛點。

通常的解決辦法是在 Dockerfile 中用 ENV 指令指定淘寶源:

ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/
複製代碼

而有些項目的構建環境更加極端,出於安全等考慮沒法訪問外網,其它依賴從公司內部的私有 npm 源上獲取。這時針對 node-sass 問題,處理起來就要更特殊一些:

  • 訪問 github.com/sass/node-s… .node 文件
  • npm i node-sass --sass_binary_path=<下載的.node文件> 語句整合進 Dockerfile

讓鏡像更易於交付

彙總以前分析的種種細節,來相對完整地看看如何配置鏡像:

Dockerfile 多階段構建

Docker 多階段構建 是 17.05 版本開始後纔有的一個特性。多階段構建容許咱們將多個 FROM 語句放在同一個 Dockerfile 中。

每條 FROM 指令均可以使用各自不一樣的基礎鏡像。每一個 FROM 語句也都標記了 Docker 構建過程當中一個新階段的開始。咱們能夠拷貝一個階段的產出物到另外一個階段,也能夠拋棄不須要的部分。

這是個很是有用的特性,能避免最終鏡像中存在編譯過程當中的依賴文件,也就是鏡像會變得更小了 。

# stage 0
FROM node:10-alpine as build-stage
WORKDIR /app
COPY package.json ./
ENV SASS_BINARY_SITE https://npm.taobao.org/mirrors/node-sass/
RUN npm install --registry=https://registry.npm.taobao.org
COPY . .
RUN npm run build-prod --silent 

# stage 1 (nginx)
FROM nginx:1.17-alpine
COPY config/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 8081
CMD ["nginx", "-g", "daemon off;"]
複製代碼

注意咱們經過 –from= 引用了 構建階段 stage 0,並從構建階段的工做目錄拷貝了項目代碼。

用數據卷覆蓋鏡像內配置

既然說了 npm 項目構建階段用環境變量寫入 API 請求地址等行爲破壞 Docker 鏡像的一致性,那到底如何請求到正確的端點呢?總要有個相似變量的東西傳進去呀 ?!

但因爲一來瀏覽器中沒法用 process 感知環境,二來 Nginx 又不似 Node.js 應用同樣能夠直接傳入參數;咱們只好稍費周章,想辦法 寫入一些 Nginx 能夠伺服的文件做爲變量來源

採用的技術正是 Docker 中的數據卷(volume),也就是在 docker run 時加載指定的目錄或文件,用以在容器內建立或覆蓋某些路徑。單以寫入 API 請求地址的需求爲例,具體作法以下:

  • 在服務器上建立一個 endpoint.json 文件,內容爲:
{
	"ENDPOINT": "http://api.app.com:5678"
}
複製代碼
  • 在 ``docker run時加入參數-v`:
docker run -p 48081:8081 -v <JSON文件絕對地址>:/usr/share/nginx/html/endpoint.json:ro -d <鏡像名>
複製代碼

這樣就在容器中的項目根目錄下楔入了一個咱們能夠隨意配置的文件。

項目局部的異步改造

配置文件很輕鬆的就解決了,那麼有了 endpoint.json 配置文件,如何在 runtime 將其應用於每一次異步請求呢?思路彷佛也頗爲簡單:

  1. 項目啓動時先異步讀取配置文件中的 ENDPOINT 屬性
  2. 將讀取到的屬性放入項目中 fetch/ajax 框架的構造函數中,完成統一注入

注:某些構建糟糕的項目可能要多費些事了,須要將本來分散寫在各處的請求前綴收斂爲由統一的 fetch/ajax 框架處理

但或許麻煩就來自於異步請求這裏 -- 因爲一些狀態管理工具的 store 裏也存在異步請求,甚至 router 等處也會引用到 store,就頗有可能形成 其異步調用早於 fetch/ajax 框架的構造函數 執行,,從而形成一些請求的失敗;咱們要作的就是對這些部分改成延遲加載。

以 vue 項目爲例,在 main.js 中:

  • 刪除原有的 import 語句:
// import router from './router';
// import store from './store';
複製代碼
  • 改成延遲加載:
const init = async () => {
  const store = await import('./store');
  const router = await import('./router');
  return new Vue({
    i18n,
    router: router.default,
    store: store.default,
    render: h => h(App),
  }).$mount('#app');
};
複製代碼
  • 保證順序的初始化:
fetch('/endpoint.json').then(res => res.json()).then(cfg => {
  window.API_ENDPOINT = cfg.ENDPOINT;
  init();
});
複製代碼

以及,在 fetch 框架中的引用:

const FetchWrapper = function(option) {
  const r = new QuickFetch(mergeWith({
    endpoint: window.API_ENDPOINT,
    baseURL: '/api'
  }, option));
  
  ...
}
複製代碼

總結

面向以 Docker 鏡像爲交付物的前端開發,代碼層面所需的調整其實不是不少,主要是觀念上是否勇於從傳統溫馨的工做模式稍微跳脫出來。

另外在團隊中多換位思考,讓開發鏈條中處於下游的運維小夥伴更樂於對接你的工做,共同提高開發部署效率和質量,也是很重要的。

參考資料



--End--

查看更多前端好文
請搜索 fewelife 關注公衆號

轉載請註明出處

相關文章
相關標籤/搜索