博客 / 詳情

返回

前端自動化部署

前端自動化部署

本文參考自: 作者:yeyan1996 鏈接:https://juejin.cn/post/684516... 來源:稀土掘金 著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

docker簡介

開發5分鐘,打包半小時, 早已是前端的痛點, 更着, 開發者自身環境的差異會導致最終的產物也有不同

docker 可以靈活的創建/銷燬/管理多個“服務器”,這些“服務器”被稱為 容器 (container)

在容器中你可以做任何服務器可以做的事,例如在有 node 環境的容器中運行 npm run build 打包項目,在有 nginx 環境的容器中部署項目,在有 mysql 環境的容器中做數據存儲等等

一旦服務器安裝了 docker ,就可以自由創建任意多的容器,上圖中 docker 的 logo 形象的展示了它們之間的關係,🐳就是 docker,上面的一個個集裝箱就是容器

本機安裝docker

官方下載地址: Get Started with Docker | Docker

下載時最好順手也註冊一個dockerHub

我的電腦是windows, 安裝完成後點擊docker圖標啓動docker, 在終端輸入docker, 看到如下輸出則代表docker正常運行

image-20220127112024967

  • 補充:

    在此你打開docker, 可能會看到docker是紅色的, 此時你可以在終端進入docker目錄, 運行DockerCli.exe -SwitchDaemon

    1. cd "C:\Program Files\Docker\Docker"
    2. DockerCli.exe -SwitchDaemon

      // 執行失敗時執行

      ./DockerCli.exe -SwitchDaemon

    如果執行上述步驟後, docker仍然是紅色, 而且無法執行docker其他命令(出現error), 例如

    image-20220127114210359

    那就只能依照軟件提示安裝Linux內核

    image-20220127114252177

    點擊進入網址後依照操作進行下載安裝 https://aka.ms/wsl2kernel

    1. 下載最新的適用於x64計算機的更新包

      如果不確定自己計算機的類型, 打開PowerShell輸入systeminfo | find "系統類型", 如果你看到如下輸出,則代表計算機是x64類型的:

      image-20220127114551128

      下載安裝完成後在PowerShell運行如下命令, 將WSL2設為默認版本

      wsl --set-default-version 2

      接着再次執行:

      cd "C:\Program Files\Docker\Docker"

      DockerCli.exe -SwitchDaemon

      如果還是出現錯誤, 那就重啓電腦再試一次吧(我有一次就是這樣, 重啓之後執行上述命令就好了)

docker概念:

  • 鏡像(image)
  • 容器(container)
  • 倉庫(repositiory)

容器可以類比一個服務器, 鏡像則是創建容器的模板, 一個docker鏡像可以創建多個容器

有兩種獲取鏡像的方式:

  • Dockerfile文件創建
  • 使用dockerHub或其他倉庫上現有的鏡像

創建docker鏡像

首先創建一個hello-docker目錄, 在目錄中創建index.htmlDockerfile文件

<!--index.html-->
<h1>Hello docker</h1>
# Dockerfile
FROM nginx
COPY index.html /usr/share/nginx/html/index.html
EXPOSE 80
  • Dockerfile內容解析:

    • FROM nginx:基於官方 nginx 鏡像
    • COPY index.html /usr/share/nginx/html/index.html:將當前目錄下 index.html 替換容器中 /usr/share/nginx/html/index.html 文件, /usr/share/nginx/html 是容器中 nginx 默認存放網頁文件的目錄,訪問容器 80 端口會展示該目錄下 index.html 文件
    • EXPOSE 80:容器對外暴露 80 端口,主要起聲明作用,真實端口映射還需要在創建容器時定義
  • 目前的文件結構:

    hello-docker
      |____index.html
      |____Dockerfile

在當前目錄(項目目錄)運行以下命令創建doker鏡像

docker build . -t test-image:latest
  • 命令解析:

    • build:創建 docker 鏡像
    • .:使用當前目錄下的 dockerfile 文件
    • -t:使用 tag 標記版本
    • test-image:latest:創建名為 test-image 的鏡像,並標記為 latest(最新)版本

創建完成後, 可以通過docker images命令查看所有鏡像

image-20220127122327921

創建docker容器

鏡像成功創建後, 運行以下命令可以創建一個docker容器

docker run -d -p 8081:80 --name test-container test-image:latest
  • 命令解析:

    • run:創建並運行 docker 容器
    • -d: 後台運行容器
    • 8081:80:將當前服務器的 8081 端口(冒號前的 8081),映射到容器的 80 端口(冒號後的 80)
    • --name:給容器命名,便於之後定位容器
    • test-image:latest:基於 test-image 最新版本的鏡像創建容器

通過docker ps -a命令查看所有容器

image-20220127123344952

現在在本地瀏覽器輸入: localhost:8081即可訪問服務內容(即項目中的index.html)

image-20220127123445069

  • docker其他命令

    • 停止、啓動、殺死、重啓一個容器

      docker stop Name或者ID
      docker start Name或者ID
      docker kill Name或者ID
      docker restart name或者ID
    • 刪除所有停止的容器

      docker container prune
    • 查看所有 name 以 docker 開頭的 docker 容器,並只輸出容器名

      docker ps -a -f "name=^docker" --format="{{.Names}}"
    • 停止 name 為 docker-container 的容器

      docker stop docker-container
    • 刪除 name 為 docker-container 的容器(停止狀態的容器才能被刪除)

      docker rm docker-container

dockerHub

dockerhub是存儲鏡像的倉庫, 開發者可以將 Dockerfile 生成的鏡像上傳到 dockerhub 來存儲自定義鏡像,也可以直接使用官方提供的鏡像

  • 使用官方鏡像啓動一個容器

    docker pull nginx
    docker run -d -p 81:80 --name nginx-container nginx

    第一步拉取了官方的 nginx 鏡像

    第二步用基於官方 nginx 鏡像創建名為 nginx-container 的容器

    這裏使用 81 端口映射到容器的 80 端口,訪問 localhost:81 可以看到 nginx 啓動頁面

image-20220127124048928

docker的好處

docker將環境統一起來, 保證生成環境和開發環境項目均能正常運行

開發者將開發環境用docker鏡像上傳到docker倉庫, 在生成環境拉取並運行相同環境即可保持環境一直

  • 提交名為docker-test-image的鏡像, 鏡像名前加上dockerhub賬號作為前綴

    docker push wzc520pyfm/docker-test-image:latest
  • 服務器拉取賬號wzc520pyfm下的docker-test-image鏡像

    docker pull wzc520pyfm/docker-test-image:latest

docker也有版本控制, 在創建鏡像時可以使用tag標記版本, 如果某個版本的環境有問題, 可以快速回滾到之前的版本

docker將項目構建需要的環境放在容器中, 與服務器隔離

容器創建和銷燬都十分高效

高效的前端自動化部署

沒有自動化的部署, 我們需要 npm run build生成構建產物(dist), 將dist文件上傳到服務器, 同時還需要將代碼提交到倉庫(團隊合作總要提交的吧).

實現自動化部署後, 我們要做的僅僅是git push提交代碼到倉庫, 其餘均由腳本自動執行.

  • 腳本需要做的事:

    • 自動更新鏡像
    • 鏡像中自動運行npm run build生成構建產物
    • 自動創建容器

登錄Linux雲服務器

參考各大雲服務器廠商官方文檔, 附上騰訊雲CentOS登錄指南文檔: 輕量應用服務器 使用遠程登錄軟件登錄 Linux 實例 - 操作指南 - 文檔中心 - 騰訊雲 (tencent.com)

我的是騰訊雲CentOS 7.6 64位的操作系統, 學生購買雲服務器有優惠, 應該是99/年

Linux服務器安裝必要的系統工具

安裝必要工具

sudo yum install -y yum-utils

添加軟件軟件源, 使用阿里雲鏡像

sudo yum-config-manager --add-repo http://mirrors.aliyun.com/doc...

Linux安裝docker

安裝docker

sudo yum install docker-ce docker-ce-cli containerd.io

開啓docker服務

sudo systemctl start docker

運行hello-world項目

sudo docker run hello-world

如果能夠看到輸出Hello from Docker!, 證明Docker安裝成功

img

Linux安裝git

用於從代碼倉庫拉取代碼

yum install git

Linux安裝nvm

前端自動化部署, 那當然處理邏輯是用js來寫, node可以讓js在服務端運行

nvm: 管理node版本

官方地址: nvm-sh/nvm:節點版本管理器 - 符合 POSIX 標準的 bash 腳本,用於管理多個主動節點.js版本 (github.com)

首先運行安裝命令:

curl -o- https://raw.githubusercontent... | bash

安裝時輸出示例如下:

[root@VM-12-9-centos /]# curl -o- https://raw.githubusercontent... | bash
% Total % Received % Xferd Average Speed Time Time Time Current

                          Dload  Upload   Total   Spent    Left  Speed

100 13226 100 13226 0 0 7281 0 0:00:01 0:00:01 --:--:-- 7283
=> Downloading nvm from git to '/root/.nvm'
=> Cloning into '/root/.nvm'...
remote: Enumerating objects: 278, done.
remote: Counting objects: 100% (278/278), done.
remote: Compressing objects: 100% (245/245), done.
remote: Total 278 (delta 31), reused 101 (delta 20), pack-reused 0
Receiving objects: 100% (278/278), 142.25 KiB | 54.00 KiB/s, done.
Resolving deltas: 100% (31/31), done.
=> Compressing and cleaning up git repository

=> nvm source string already in /root/.bashrc
=> Appending bash_completion source string to /root/.bashrc
=> Close and reopen your terminal to start using nvm or run the following to use it now:

export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion

程序自動地嘗試將環境變量添加到正確的位置, 在此之後, 我們需要手動運行使能命令,讓環境變量生效

source ~/.bashrc

驗證是否安裝成功

command -v nvm

正常輸出示例:

[root@VM-12-9-centos /]# command -v nvm
nvm

Linux安裝node

下載、編譯、安裝最新版本的node: (本次操作執行此命令)

nvm install node # "node" is an alias for the latest version

如果需要安裝特定版本的node, 請運行:

nvm install 14.7.0 # or 16.3.0, 12.22.1, etc

安裝的第一個版本將成為默認版本。新 shell 將從節點的默認版本(例如) 開始。

其他命令:

  • 切換node版本

    nvm use 14.17.3
  • 列出已安裝的node版本

    nvm ls
  • 卸載指定版本的node

    nvm uninstall 14.17.3

Linux安裝pm2

pm2可以讓我們的js腳本在服務器後台運行

npm i pm2 -g

創建前端項目

本地創建一個簡單的前端基礎項目

vue create docker-test

創建完成後將demo上傳到github (建議創建public共有倉庫, 這樣可以免去鑑權直接clone, 如果創建了private私有倉庫, 在運行時需要輸入密碼, 代碼運行時當然不希望這樣, 解決辦法是使用token, 參見: (37條消息) 【突發】解決remote: Support for password authentication was removed on August 13, 2021. Please use a perso_日積月累,天道酬勤-CSDN博客) , 接下來配置webhook

webhook

github倉庫有一個hook(鈎子), 它會在當前倉庫觸發某些事件時, 發送一個post形式的http請求

當倉庫有提交代碼時,通過將 webhook 請求地址指向雲服務器 IP 地址,雲服務器就能知道項目有更新,之後運行相關代碼實現自動化部署

  • 配置webhook

    image-20220127090737223

    image-20220127091223460

  • 測試是否配置成功

    1. 本地修改代碼並提交到倉庫
    2. image-20220127091655811

​ 參數主要涉及當前倉庫和本地提交的信息,這裏我們只用 repository.name 獲取更新的倉庫名即可

請求如何處理?

當我們的服務器收到項目更新後發送的post請求後, 需要創建/更新鏡像來實現自動化部署

創建Dockerfile

在本地項目裏新建一個Dockerfile文件, 用於之後創建鏡像

# dockerfile
# build stage
FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# production stage
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
  • 配置解析

    • FROM registry.cn-hangzhou.aliyuncs.com/dyjutil/node:v14.8.0 as build-stage:採用阿里版本境像的階段命名為 build-stage
    • WORKDIR /app:將工作區設為 /app,和其他系統文件隔離
    • COPY package*.json ./:拷貝 package.json/package-lock.json 到容器的 /app 目錄
    • RUN npm install:運行 npm install 在容器中安裝依賴
    • COPY . .:拷貝其他文件到容器 /app 目錄,分兩次拷貝是因為保持 node_modules 一致
    • RUN npm run build:運行 npm run build 在容器中構建

    這裏用到了 docker 一個技巧:多階段構建

    將構建分為兩個階段,第一階段基於 node 鏡像,第二階段基於 nginx 鏡像

    • FROM nginx:lts-alpine as production-stage:基於 nginx stable-alpine 版本鏡像,並將有 nginx 環境的階段命名為 production-stage
    • COPY --from=build-stage /app/dist /usr/share/nginx/html:通過 --form 參數可以引用 build-stage 階段生成的產物,將其複製到 /usr/share/nginx/html
    • EXPOSE 80:容器對外暴露 80 端口
    • CMD ["nginx", "-g", "daemon off;"]:容器創建時運行 nginx -g daemon off 命令,一旦 CMD 對應的命令結束,容器就會被銷燬,所以通過 daemon off 讓 nginx 一直在前台運行

通過scp命令, 將Dockerfile文件複製到雲服務器上

scp ./Dockerfile root@121.5.110.8:/root

image-20220127131456618

創建.dockerignore

.dockerignore 可以在創建鏡像複製文件時忽略複製某些文件

在本地項目裏新建.dockerignore

# .dockerignore
node_modules

接着將.dockerignore文件也複製到雲服務器上

scp ./.dockerignore root@121.5.110.8:/root

image-20220127132047601

創建http服務器並編寫自動部署腳本

使用node來開啓簡單的http服務器處理webhook發送的post請求,腳本需要包含創建http服務器、拉取倉庫代碼、創建鏡像和容器。

在本地項目新建index.js

const http = require("http");
const { execSync } = require("child_process");
const path = require("path");
const fs = require("fs");

// 遞歸刪除目錄
function deleteFolderRecursive(path) {
  if (fs.existsSync(path)) {
    fs.readdirSync(path).forEach(function (file) {
      const curPath = path + "/" + file;
      if (fs.statSync(curPath).isDirectory()) {
        // recurse
        deleteFolderRecursive(curPath);
      } else {
        // delete file
        fs.unlinkSync(curPath);
      }
    });
    fs.rmdirSync(path);
  }
}

const resolvePost = (req) =>
  new Promise((resolve) => {
    let chunk = "";
    req.on("data", (data) => {
      chunk += data;
    });
    req.on("end", () => {
      resolve(JSON.parse(chunk));
    });
  });

http
  .createServer(async (req, res) => {
    console.log("receive request");
    console.log(req.url);
    if (req.method === "POST" && req.url === "/") {
      const data = await resolvePost(req);
      const projectDir = path.resolve(`./${data.repository.name}`);
      deleteFolderRecursive(projectDir);

      // 拉取倉庫最新代碼  data.repository.name 即 webhook 中記錄倉庫名的屬性
      execSync(
        `git clone https://github.com/wzc520pyfm/${data.repository.name}.git ${projectDir}`,
        {
          stdio: "inherit",
        }
      );
      // 複製 Dockerfile 到項目目錄
      fs.copyFileSync(
        path.resolve(`./Dockerfile`),
        path.resolve(projectDir, "./Dockerfile")
      );

      // 複製 .dockerignore 到項目目錄
      fs.copyFileSync(
        path.resolve(__dirname, `./.dockerignore`),
        path.resolve(projectDir, "./.dockerignore")
      );

      // 創建 docker 鏡像
      execSync(`docker build . -t ${data.repository.name}-image:latest `, {
        stdio: "inherit",
        cwd: projectDir,
      });

      // 銷燬 docker 容器
      execSync(
        `docker ps -a -f "name=^${data.repository.name}-container" --format="{{.Names}}" | xargs -r docker stop | xargs -r docker rm`,
        {
          stdio: "inherit",
        }
      );

      // 創建 docker 容器  -- 這裏使用了服務器的8888端口
      execSync(
        `docker run -d -p 8888:80 --name ${data.repository.name}-container  ${data.repository.name}-image:latest`,
        {
          stdio: "inherit",
        }
      );

      console.log("deploy success");
    }
    res.end("ok");
  })
  .listen(3000, () => {
    console.log("server is ready");
  });

在銷燬 docker 容器部分用到了 linux 的管道運算符和 xargs 命令,過濾出以 docker-test 開頭容器(用 docker-test 倉庫的代碼製作的鏡像創建的容器),停止,刪除並重新創建它們

最後, 通過scp將index上傳到雲服務器上

scp ./index.js root@121.5.110.8:/root

image-20220127154500978

現在, 我們的項目結構為:

image-20220127204833035

雲服務器上使用pm2運行index.js

pm2 start index.js

image-20220127160209979

  • pm2命令

    • pm2 list 查看運行的pm2服務
    • pm2 logs 查看pm2日誌
    • pm2 flush 清除pm2日誌
    • pm2 start index.js 運行index.js文件
    • pm2 stop id 停止指定id的pm2服務

小插曲

因為我們需要通過服務器的8888端口訪問部署的項目, 倉庫需要通過服務器3000端口通知服務器代碼更新, 所以服務器需要放通8888和3000端口, 下面介紹騰訊雲服務器放通端口的步驟:

  1. image-20220127204350525
  2. image-20220127204509146

接着就可以訪問 http://服務器ip:8888 看到頁面, 如果無響應, 嘗試向倉庫push一次代碼, 查看pm2日誌是否成功拉取代碼並更新鏡像, 如果出現網絡問題無法拉取代碼, 最好是改用gitee倉庫, gitee的webhook配置訪問與github一致.

image-20220127202846153

image-20220127202906803

接下來對項目中的App.vue稍作更改,並提交倉庫, 測試自動化部署

image-20220127203640027

重新打開 http://服務器ip:8888

image-20220127203724224

可以看到內容已經更新

示例代碼

wzc520pyfm/docker-test - 碼雲 - 開源中國 (gitee.com)

關注 Dockerfile ,.dockerignore, index.js 文件

距離真實環境仍有一定差距

上述 demo 只創建了單個 docker 容器,當項目更新時,由於容器需要經過銷燬和創建的過程,會存在一段時間頁面無法訪問情況

而實際投入生產時一般會創建多個容器,並逐步更新每個容器,配合負載均衡將用户的請求映射到不同端口的容器上,確保線上的服務不會因為容器的更新而宕機

image-20200701210630305

另外基於 github 平台也有非常成熟的 CI/CD 工具,例如

  • travis-ci
  • circleci

通過 yml 配置文件,簡化上文中註冊 webhook 和編寫更新容器的 index.js 腳本的步驟

# .travis.yml
language: node_js
node_js:
  - 8
branchs:
  only:
    - master
cache:
  directories:
    - node_modules
install:
  - yarn install
scripts:
  - yarn test
  - yarn build

另外隨着環境的增多,容器也會逐漸增加,docker 也推出了更好管理多個容器的方式 docker-compose

img

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.