前端部署發展史


前端一說起刀耕火種,那肯定緊隨着前端工程化這一話題。隨着 react/vue/angulares6+webpackbabeltypescript 以及 node 的發展,前端已經在逐漸替代過去 scriptcdn 開發的方式了,掀起了工程化這一大浪潮。得益於工程化的發展與開源社區的良好生態,前端應用的可用性與效率得到了很大提高。

前端以前是刀耕火種,那前端應用部署在以前也是刀耕火種。那前端應用部署的發展得益於什么,隨前端工程化帶來的副產品?

這只是一部分,而更重要的原因是 devops 的崛起。

為了更清晰地理解前端部署的發展史,了解部署時運維和前端(或者更廣泛地說,業務開發人員)的職責划分,當每次前端部署發生改變時,可以思考兩個問題

  1. 緩存,前端應用中http 的 response header 由誰來配?得益於工程化發展,可以對打包后得到帶有 hash 值的文件可以做永久緩存
  2. 跨域,/api 的代理配置由誰來配?在開發環境前端可以開個小服務,啟用 webpack-dev-server 配置跨域,那生產環境呢

這兩個問題都是前端面試時的高頻問題,但話語權是否掌握在前端手里

時間來到 React 剛剛發展起來的這一年,這時已經使用 React 開發應用,使用 webpack 來打包。但是前端部署,仍是刀耕火種

刀耕火種

一台跳板機

一台生產環境服務器

一份部署腳本

前端調着他的 webpack,開心地給運維發了部署郵件並附了一份部署腳本,想着第一次不用套后端的模板,第一次前端可以獨立部署。想着自己基礎盤進一步擴大,前端不禁開心地笑了

運維照着着前端發過來的部署郵件,一遍又一遍地拉着代碼,改着配置,寫着 try_files, 配着 proxy_pass

這時候,前端靜態文件由 nginx 托管,nginx 配置文件大致長這個樣子

server {
  listen 80;
  server_name shanyue.tech;

  location / {
    # 避免非root路徑404
    try_files $uri $uri/ /index.html;
  }

  # 解決跨域
  location /api {
    proxy_pass http://api.shanyue.tech;
  }

  # 為帶 hash 值的文件配置永久緩存
  location ~* \.(?:css|js)$ {
      try_files $uri =404;
      expires 1y;
      add_header Cache-Control "public";
  }

  location ~ ^.+\..+$ {
      try_files $uri =404;
  }
}

不過...經常有時候跑不起來

運維抱怨着前端的部署腳本沒有標好 node 版本,前端嚷嚷着測試環境沒問題

這個時候運維需要費很多心力放在部署上,甚至測試環境的部署上,前端也要費很多心力放在運維如何部署上。這個時候由於怕影響線上環境,上線往往選擇在深夜,前端和運維身心俱疲

不過向來如此

魯迅說,向來如此,那便對么。

這個時候,無論跨域的配置還是緩存的配置,都是運維來管理,運維不懂前端。但配置方式卻是前端在提供,而前端並不熟悉 nginx

使用 docker 構建鏡像

docker 的引進,很大程度地解決了部署腳本跑不了這個大BUG。dockerfile 即部署腳本,部署腳本即 dockerfile。這也很大程度緩解了前端與運維的摩擦,畢竟前端越來越靠譜了,至少部署腳本沒有問題了 (笑

這時候,前端不再提供靜態資源,而是提供服務,一個 http 服務

前端寫的 dockerfile 大致長這個樣子

FROM node:alpine

# 代表生產環境
ENV PROJECT_ENV production
# 許多 package 會根據此環境變量,做出不同的行為
# 另外,在 webpack 中打包也會根據此環境變量做出優化,但是 create-react-app 在打包時會寫死該環境變量
ENV NODE_ENV production
WORKDIR /code
ADD . /code
RUN npm install && npm run build && npm install -g http-server
EXPOSE 80

CMD http-server ./public -p 80

單單有 dockerfile 也跑不起來,另外前端也開始維護一個 docker-compose.yaml,交給運維執行命令 docker-compose up -d 啟動前端應用。前端第一次寫 dockerfiledocker-compose.yaml,在部署流程中扮演的角色越來越重要。想着自己基礎盤進一步擴大,前端又不禁開心地笑了

version: "3"
services:
  shici:
    build: .
    expose:
      - 80

運維的 nginx 配置文件大致長這個樣子

server {
  listen 80;
  server_name shanyue.tech;

  location / {
    proxy_pass http://static.shanyue.tech;
  }

  location /api {
    proxy_pass http://api.shanyue.tech;
  }
}

運維除了配置 nginx 之外,還要執行一個命令: docker-compose up -d

這時候再思考文章最前面兩個問題

  1. 緩存,由於從靜態文件轉換為服務,緩存開始交由前端控制 (但是鏡像中的 http-server 不太適合做這件事情)
  2. 跨域,跨域仍由運維在 nginx 中配置

前端可以做他應該做的事情中的一部分了,這是一件令人開心的事情

當然,前端對於 dockerfile 的改進也是一個慢慢演進的過程,那這個時候鏡像有什么問題呢?

  1. 構建鏡像體積過大
  2. 構建鏡像時間過長

使用多階段構建優化鏡像

這中間其實經歷了不少坎坷,其中過程如何,詳見我的另一篇文章: 如何使用 docker 部署前端應用

其中主要的優化也是在上述所提到的兩個方面

  1. 構建鏡像體積由 1G+ 變為 10M+
  2. 構建鏡像時間由 5min+ 變為 1min (視項目復雜程度,大部分時間在構建時間與上傳靜態資源時間)
FROM node:alpine as builder

ENV PROJECT_ENV production
ENV NODE_ENV production

WORKDIR /code

ADD package.json /code
RUN npm install --production

ADD . /code

# npm run uploadCdn 是把靜態資源上傳至 oss 上的腳本文件,將來會使用 cdn 對 oss 加速
RUN npm run build && npm run uploadCdn

# 選擇更小體積的基礎鏡像
FROM nginx:alpine
COPY --from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/
COPY --from=builder code/public/static /usr/share/nginx/html/static

那它怎么做的

  1. ADD package.json /code, 再 npm install --production 之后 Add 所有文件。充分利用鏡像緩存,減少構建時間
  2. 多階段構建,大大減小鏡像體積

另外還可以有一些小優化,如

  • npm cache 的基礎鏡像或者 npm 私有倉庫,減少 npm install 時間,減小構建時間
  • npm install --production 只裝必要的包

前端看着自己優化的 dockerfile,想着前幾天還被運維吵,說什么磁盤一半的空間都被前端的鏡像給占了,想着自己節省了前端鏡像幾個數量級的體積,為公司好像省了不少服務器的開銷,想着自己的基礎盤進一步擴大,又不禁開心的笑了

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制,緩存在oss上設置,將會使用 cdn 對 oss 加速。此時緩存由前端寫腳本控制
  2. 跨域,跨域仍由運維在 nginx 中配置

CI/CD 與 gitlab

此時前端成就感爆棚,運維呢?運維還在一遍一遍地上線,重復着一遍又一遍的三個動作用來部署

  1. 拉代碼
  2. docker-compose up -d
  3. 重啟 nginx

運維覺得再也不能這么下去了,於是他引進了 CI: 與現有代碼倉庫 gitlab 配套的 gitlab ci

  • CIContinuous Integration,持續集成
  • CDContinuous Delivery,持續交付

重要的不是 CI/CD 是什么,重要的是現在運維不用跟着業務上線走了,不需要一直盯着前端部署了。這些都是 CI/CD 的事情了,它被用來做自動化部署。上述提到的三件事交給了 CI/CD

.gitlab-ci.ymlgitlab 的 CI 配置文件,它大概長這個樣子

deploy:
  stage: deploy
  only:
    - master
  script:
    - docker-compose up --build -d
  tags:
    - shell

CI/CD 不僅僅更解放了業務項目的部署,也在交付之前大大加強了業務代碼的質量,它可以用來 linttestpackage 安全檢查,甚至多特性多環境部署,我將會在我以后的文章寫這部分事情

我的一個服務器渲染項目 shfshanyue/shici 以前在我的服務器中就是以 docker/docker-compose/gitlab-ci 的方式部署,有興趣的可以看看它的配置文件

如果你有個人服務器的話,也建議你做一個自己感興趣的前端應用和配套的后端接口服務,並且配套 CI/CD 把它部署在自己的自己服務器上

而你如果希望結合 githubCI/CD,那可以試一試 github + github action

另外,也可以試試 drone.ci,如何部署可以參考我以前的文章: github 上持續集成方案 drone 的簡介及部署

使用 kubernetes 部署

隨着業務越來越大,鏡像越來越多,docker-compose 已經不太能應付,kubernetes 應時而出。這時服務器也從1台變成了多台,多台服務器就會有分布式問題

一門新技術的出現,在解決以前問題的同時也會引進復雜性。

k8s 部署的好處很明顯: 健康檢查,滾動升級,彈性擴容,快速回滾,資源限制,完善的監控等等

那現在遇到的新問題是什么?

構建鏡像的服務器,提供容器服務的服務器,做持續集成的服務器是一台!

需要一個私有的鏡像倉庫,這是運維的事情,harbor 很快就被運維搭建好了,但是對於前端部署來說,復雜性又提高了

先來看看以前的流程:

  1. 前端配置 dockerfiledocker-compose
  2. 生產環境服務器的 CI runner 拉代碼(可以看做以前的運維),docker-compose up -d 啟動服務。然后再重啟 nginx,做反向代理,對外提供服務

以前的流程有一個問題: 構建鏡像的服務器,提供容器服務的服務器,做持續集成的服務器是一台!,所以需要一個私有的鏡像倉庫,一個能夠訪問 k8s 集群的持續集成服務器

流程改進之后結合 k8s 的流程如下

  1. 前端配置 dockerfile,構建鏡像,推到鏡像倉庫
  2. 運維為前端應用配置 k8s 的資源配置文件,kubectl apply -f 時會重新拉取鏡像,部署資源

運維問前端,需不需要再擴大下你的基礎盤,寫一寫前端的 k8s 資源配置文件,並且列了幾篇文章

前端看了看后端十幾個 k8s 配置文件之后,搖搖頭說算了算了

這個時候,gitlab-ci.yaml 差不多長這個樣子,配置文件的權限由運維一人管理

deploy:
  stage: deploy
  only:
    - master
  script:
    - docker build -t harbor.shanyue.tech/fe/shanyue
    - docker push harbor.shanyue.tech/fe/shanyue
    - kubectl apply -f https://k8s-config.default.svc.cluster.local/shanyue.yaml
  tags:
    - shell

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制
  2. 跨域,跨域仍由運維控制,在后端 k8s 資源的配置文件中控制 Ingress

使用 helm 部署

這時前端與運維已不太往來,除了偶爾新起項目需要運維幫個忙以外

但好景不長,突然有一天,前端發現自己連個環境變量都沒法傳!於是經常找運維修改配置文件,運維也不勝其煩

於是有了 helm,如果用一句話解釋它,那它就是一個帶有模板功能的 k8s 資源配置文件。作為前端,你只需要填參數。更多詳細的內容可以參考我以前的文章 使用 helm 部署 k8s 資源

假如我們使用 bitnami/nginx 作為 helm chart,前端可能寫的配置文件長這個樣子

image:
  registry: harbor.shanyue.tech
  repository: fe/shanyue
  tag: 8a9ac0

ingress:
  enabled: true
  hosts:
  - name: shanyue.tech
    path: /

  tls:
  - hosts:
      - shanyue.tech
    secretName: shanyue-tls

    # livenessProbe:
    #   httpGet:
    #     path: /
    #     port: http
    #   initialDelaySeconds: 30
    #   timeoutSeconds: 5
    #   failureThreshold: 6
    #
    # readinessProbe:
    #   httpGet:
    #     path: /
    #     port: http
    #   initialDelaySeconds: 5
    #   timeoutSeconds: 3
    #   periodSeconds: 5

這時候再思考文章最前面兩個問題

  1. 緩存,緩存由前端控制
  2. 跨域,跨域由后端控制,配置在后端 Chart 的配置文件 values.yaml

到了這時前端和運維的職責所在呢?

前端需要做的事情有:

  1. 寫前端構建的 dockerfile,這只是一次性的工作,而且有了參考
  2. 使用 helm 部署時指定參數

那運維要做的事情呢

  1. 提供一個供所有前端項目使用的 helm chart,甚至不用提供,如果運維比較懶那就就使用 bitnami/nginx 吧。也是一次性工作
  2. 提供一個基於 helm 的工具,禁止業務過多的權限,甚至不用提供,如果運維比較懶那就直接使用 helm

這時前端可以關注於自己的業務,運維可以關注於自己的雲原生,職責划分從未這般清楚

統一前端部署平台

后來運維覺得前端應用的本質是一堆靜態文件,較為單一,容易統一化,來避免各個前端鏡像質量的參差不齊。於是運維准備了一個統一的 node 基礎鏡像,做了一個前端統一部署平台,而這個平台可以做什么呢

  1. CI/CD: 當你 push 代碼到倉庫的特定分支會自動部署
  2. http headers: 你可以定制資源的 http header,從而可以做緩存優化
  3. http redirect/rewrite: 如果一個 nginx,這樣可以配置 /api,解決跨域問題
  4. hostname: 你可以設置域名
  5. CDN: 把你的靜態資源推到 CDN
  6. https: 為你准備證書
  7. Prerender: 結合 SPA,做預渲染

前端再也不需要構建鏡像,上傳 CDN 了,他只需要寫一份配置文件就可以了,大致長這個樣子

build:
  command: npm run build
  dist: /dist

hosts:
- name: shanyue.tech
  path: /

headers:
- location: /*
  values:
  - cache-control: max-age=7200
- location: assets/*
  values:
  - cache-control: max-age=31536000

redirects:
- from : /api
  to: https://api.shanyue.tech
  status: 200

此時,前端只需要寫一份配置文件,就可以配置緩存,配置 proxy,做應該屬於前端做的一切,而運維也再也不需要操心前端部署的事情了

前端看着自己剛剛寫好的配置文件,悵然若失的樣子...

不過一般只有大廠會有這么完善的前端部署平台,如果你對它有興趣,你可以嘗試下 netlify,可以參考我的文章: 使用 netlify 部署你的前端應用

服務端渲染與后端部署

大部分前端應用本質上是靜態資源,剩下的少部分就是服務端渲染了,服務端渲染的本質上是一個后端服務,它的部署可以視為后端部署

后端部署的情況更為復雜,比如

  1. 配置服務,后端需要訪問敏感數據,但又不能把敏感數據放在代碼倉庫。你可以在 environment variablesconsul 或者 k8s configmap 中維護
  2. 上下鏈路服務,你需要依賴數據庫,上游服務
  3. 訪問控制,限制 IP,黑白名單
  4. RateLimit
  5. 等等

我將在以后的文章分享如何在 k8s 中部署一個后端

小結

隨着 devops 的發展,前端部署越來越簡單,可控性也越來越高,建議所有人都稍微學習一下 devops 的東西。

道阻且長,行則將至。

相關文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM