用 Nuxt + Webhooks + Docker 擼一套自動化部署的 Vue SSR 項目

本文永久連接:github.com/HaoChuan942…html

前言

一個前端項目簡單的開發部署流程一般是這樣的:先本地開發,當完成某個功能後構建打包,而後將生成的靜態文件經過 ftp 或者其餘方式上傳到服務器中。並將代碼 pushGitHub 或者 碼雲 等遠端倉庫中進行託管(爲了突出本文的重點,暫不考慮測試的環節)。這種工做流難免有些勞神費力,並且天天頻繁的打包上傳也會佔用不少時間。前端

一種理想的方式是:你只須要在服務器上建立一個「腳本」,執行這個腳本,他就會自動從 git 服務器拉取你的項目代碼,並啓動你的項目,而當你每次向 git 服務器 push 代碼時,它又會自動拉取最新的代碼並從新編譯,更新服務。node

爲了實現上述的「理想方式」,本文將詳細介紹如何使用 Nuxt + Webhooks + Docker 來實現一個 Vue SSR 項目的自動化部署。但咱們首先須要解決這麼幾個問題:linux

  1. 若是在服務器上安全的拉取私有倉庫的代碼?
  2. 若是以生產環境(production)啓動你的項目?
  3. 若是「通知」服務器你的代碼已經更新了?
  4. 若是在不中止服務的前提下自動從新構建項目,自動更新?

要解決上面的問題,你須要瞭解如下基礎知識:webpack

  1. SSH Key
  2. 基本的 Nuxt + Docker 知識。
  3. 瞭解 Webhooks
  4. 基本的 Node + express 知識。

若是你對上述知識不是很瞭解或者不知道如何將他們結合在一塊兒來以達到所謂的「理想方式」,那麼接下的內容將從項目建立到實際部署,一步步的帶你完成這項工做。git

1、使用 create-nuxt-app 腳手架建立項目

建立時的各類選項以下圖所示,你能夠根據本身項目的實際狀況進行選擇,但 server framework 請選擇 express,本文也將以 express 做爲服務端框架展開介紹。github

2、修改 package.json 中的 npm scripts

Nuxt 腳手架生成的項目,默認在生產環境下須要先執行 npm run build 構建代碼,而後再執行 npm start 啓動服務,這略顯繁瑣,也不利於自動部署、從新構建等工做的展開,這裏將二者的功能合二爲一,執行 npm start,便可在編碼中使用構建並啓動服務。得益於 Nuxt 配置中的 dev 參數, 在不一樣的環境下(NODE_ENV),即便使用的都是 new Builder(nuxt).build() 來進行構建,但因爲 dev 參數的不一樣,Nuxt 的構建行爲也會相應的不一樣並進行鍼對性的優化。這裏生產環境(production)下啓動服務也再也不是經過 node 命令而是使用 nodemon,它用於監聽 server/index.js 文件的變化,在 server/index.js 更新時能夠自動重啓服務。調整先後的 npm scripts 以下:web

// 前
"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
  "build": "nuxt build",
  "start": "cross-env NODE_ENV=production node server/index.js"
}
複製代碼
// 後
"scripts": {
  "dev": "cross-env NODE_ENV=development nodemon server/index.js --watch server",
  "start": "cross-env NODE_ENV=production nodemon server/index.js --watch server"
}
複製代碼

同時,刪除 server/index.js 中本來的條件判斷:docker

//if (config.dev) {
  const builder = new Builder(nuxt);
  await builder.build();
//}
複製代碼

調整以後,執行 npm run dev,就會在 3000 端口啓動一個有代碼熱替換(HMR)等功能的一個開發(development)服務,而執行 npm start 就會構建出壓縮後的代碼,並啓動一個帶 gzip 壓縮等功能的生產(production)服務。express

3、添加 Webhooks 接口

Webhooks 是什麼?簡單來講:假如你向一個倉庫添加了 Webhook ,那麼當你 push 代碼時,git 服務器就會自動向你指定的地址,發送一個帶有更新信息(payload)的 post 請求。瞭解更多,請閱讀 GitHub 關於 Webhooks 的介紹文檔 或者 碼雲的文檔。因爲咱們使用了 express 來建立 http 服務,因此咱們能夠像這樣方便的添加一個接口,用於接收來自 git 服務器的 post 請求:

...
// 訂閱來自 git 服務器 的 Webhooks 請求(post 類型)
app.post('/webhooks', function(req, res) {
  // 使用 secret token 對該 API 的調用進行鑑權, 詳細文檔: https://developer.github.com/webhooks/securing/
  const SECRET_TOKEN = 'b65c19b95906e027c5d8';
  // 計算簽名
  const signature = `sha1=${crypto .createHmac('sha1', SECRET_TOKEN) .update(JSON.stringify(req.body)) .digest('hex')}`;
  // 驗證簽名和 Webhooks 請求中的簽名是否一致
  const isValid = signature === req.headers['x-hub-signature'];
  // 若是驗證經過,返回成功狀態並更新服務
  if (isValid) {
    res.status(200).end('Authorized');
    upgrade();
  } else {
    // 鑑權失敗,返回無權限提示
    res.status(403).send('Permission Denied');
  }
});
...
複製代碼

這裏的 app 是一個 express 應用,咱們經過了 Nodecrypto 模塊計算簽名並和 Webhooks 請求中的簽名比對來進行鑑權,以保證接口調用的安全性(這裏的可以獲取到 Webhooks 請求的請求體 —— req.body 是因爲使用了 body-parser 中間件)。若是鑑權經過則返回成功狀態,並執行 upgrade 函數來更新服務,若是鑑權失敗,則返回無權限提示。同時,你須要向倉庫添加 Webhook,以下圖:

4、如何無縫更新服務

若是你的項目已經在 http://www.example.com/ 下啓動成功,那麼當你每次向 GitHub 倉庫 push 代碼時,你的接口都會收到一個來自 GitHubpost 請求,並在鑑權經過後執行 upgrade 函數來更新服務。關於如何在服務器上啓動項目咱們按下不表,先介紹 upgrade 函數都作了什麼。

/** * 從 git 服務器拉取最新代碼,更新 npm 依賴,並從新構建項目 */
function upgrade() {
  execCommand('git pull -f && npm install', true);
}
複製代碼

execCommand 函數以下,這裏咱們使用了 Nodechild_process 模塊,用以建立子進程,來執行拉取代碼, 更新 npm 依賴等命令:

const { execSync } = require('child_process');
/** * 建立子進程,執行命令 * @param {String} command 須要執行的命令 * @param {Boolean} reBuild 是否從新構建應用 * @param {Function} callback 執行命令後的回調 */
function execCommand(command, reBuild, callback) {
  command && execSync(command, { stdio: [0, 1, 2] }, callback);
  // 根據配置文件,從新構建項目
  reBuild && build();
}
複製代碼

build 函數,會根據配置文件,從新構建項目,這裏的 upgrading 是一個標記應用是否正在升級的 flag

/** * 根據配置,構建項目 */
async function build() {
  if (upgrading) {
    return;
  }
  upgrading = true;
  // 導入 Nuxt.js 參數
  let config = require('../nuxt.config.js');
  // 根據環境變量 NODE_ENV,設置 config.dev 的值
  config.dev = !(process.env.NODE_ENV === 'production');
  // 初始化 Nuxt.js
  const nuxt = new Nuxt(config);
  // 構建應用,得益於環境變量 NODE_ENV,在開發環境和生產環境下這個構建的表現會不一樣
  const builder = new Builder(nuxt);
  // 等待構建
  await builder.build();
  // 構建完成後,更新 render 中間件
  render = nuxt.render;
  // 將 flag 置反
  upgrading = false;
  // 若是是初次構建,則建立 http server
  server || createServer();
}
複製代碼

createServer 函數以下,這裏有兩個全局變量,renderserver,其中 render 變量保存了最新構建後的 nuxt.render 中間件,而 server 變量是應用的 http server 實例。

/** * 建立應用的 http server */
function createServer() {
  // 向 express 應用添加 nuxt 中間件,從新構建以後,中間件會發生變化
  // 這種處理方式的好處就在於 express 使用的老是最新的 nuxt.render
  app.use(function() {
    render.apply(this, arguments);
  });
  // 啓動服務
  server = app.listen(port, function(error) {
    if (error) {
      return;
    }
    consola.ready({
      message: `Server listening on http://localhost:${port}`,
      badge: true
    });
  });
}
複製代碼

訪問這裏,查看完整的 server/index.js 文件。但這裏存在一個問題☝️,就是每次執行 build 函數,從新構建時,因爲 Nuxt 會刪除上一次構建生成的文件(清空.nuxt/dist/client.nuxt/dist/server 文件夾),而構建完成以後纔會生成新的文件,那麼若是用戶剛好在這個空檔期訪問網站怎麼辦?一種解決方案是干預 webpack 的這種行爲,不去清空這兩個文件夾,不過我目前沒有找到 Nuxt 中能夠修改這個配置的地方(歡迎評論),另外一種解決方案就是在項目從新構建的時候,給用戶返回一個友好的提示頁,告訴他系統正在升級中。這也是我設置 upgrading 變量來標記應用是否正在升級中的意義所在,下面這段代碼將展現,若是實現這種效果:

const express = require('express');
const app = express();
// 攔截因此 get 請求,若是系統正在升級中,則返回提示頁面
app.get('*', function(req, res, next) {
  if (upgrading) {
    res.sendFile('./upgrading.html', { root: __dirname });
  } else {
    next();
  }
});
複製代碼

要說明的一點是:app.get('*', ...) 必須寫在前面,你能夠在這裏Description 中找到解釋。如此一來,當用戶剛好在應用從新構建時訪問網站,就會出現一個友好的提示頁,而當構建完成後,用戶再次訪問網站,就是一個升級後的應用,整個過程,服務器始終是保持在線的狀態,http server 並無中止或者重啓。

至此,你已經能夠把項目代碼上傳到 GitHub 或者 碼雲了(不一樣的服務商對 Webhooks 的鑑權方式可能會有所不一樣,你須要參考他們的文檔對接口的鑑權方式進行一點調整)。

5、部署公鑰管理

爲私有項目添加部署公鑰,使得項目在服務器上或者在 Docker 中能夠安全的進行代碼克隆和後續的拉取更新,參考連接1參考連接2。這裏以 GitHub 爲例進行介紹:

  1. 生成一個 GitHub 用的 SSH key

    ssh-keygen -t rsa -C 'hc199421@gmail.com' -f ~/.ssh/github_id_rsa
    複製代碼

    通常狀況下,是不須要使用 -f ~/.ssh/github_id_rsa 來指定生成 SSH Key 的文件名的,默認生成的是 id_rsa。但考慮到一臺機器同時使用不一樣的 git 服務器的可能性,因此這裏對生成的 SSH key 名稱進行了自定義。這裏的郵箱是你的 git 服務器 (GitHub)登陸郵箱。

  2. ~/.ssh 目錄下新建一個 config 文件,添加以下內容,參考文檔

    # github
    Host github.com
    HostName github.com
    StrictHostKeyChecking no
    PreferredAuthentications publickey
    IdentityFile ~/.ssh/github_id_rsa
    複製代碼

    其中 HostHostName 填寫 git 服務器的域名,IdentityFile 指定私鑰的路徑,StrictHostKeyChecking 設置爲 no 能夠跳過下圖中 (yes/no) 的詢問,這一點對於 Docker 流暢的建立鏡像頗有必要(不然可能要寫 expect 腳本),固然你也能夠經過執行 ssh-keyscan github.com > ~/.ssh/known_hostshost keys 提早添加到 known_hosts 文件中。

  3. 在項目倉庫添加部署公鑰

  4. 測試公鑰是否可用

    ssh -T git@github.com
    複製代碼

    若是出現下圖所示內容則代表大功告成,能夠執行下一步了。👏👏👏🎉🎉🎉

至此,若是你不須要使用 Docker 部署,而是使用傳統的部署方式,那麼你只須要在服務器上安裝 Nodegit,並把倉庫代碼克隆到服務器上,而後執行 npm start 在 80 端口啓動服務就能夠了。你可使用 nohup 命令或者 forever 等使服務常駐後臺。

6、Docker 部署

1. 安裝 Docker CE (阿里雲 Ubuntu 18.04 已親試)

2. 建立 Dockerfile

# 添加 node 鏡像,:8 是指定 node 的版本,默認會拉取最新的
FROM node:8
# 定義 SSH 私鑰變量
ARG ssh_prv_key
# 定義 SSH 公鑰變量
ARG ssh_pub_key
# 在 /home 下建立名爲 webhooks-nuxt-demo 的文件夾
RUN mkdir -p /home/webhooks-nuxt-demo
# 爲 RUN, CMD 等命令指定工做區
WORKDIR /home/webhooks-nuxt-demo
# 建立 .ssh 目錄
RUN mkdir -p /root/.ssh
# 生成 github_id_rsa、github_id_rsa.pub 和 config 文件
RUN echo "$ssh_prv_key" > /root/.ssh/github_id_rsa && \
    echo "$ssh_pub_key" > /root/.ssh/github_id_rsa.pub && \
    echo "Host github.com\nHostName github.com\nStrictHostKeyChecking no\nPreferredAuthentications publickey\nIdentityFile /root/.ssh/github_id_rsa" > /root/.ssh/config
# 修改私鑰的用戶權限
RUN chmod 600 /root/.ssh/github_id_rsa
# 克隆遠端 git 倉庫代碼到工做區,注意最後的 . 不能省略
RUN git clone git@github.com:HaoChuan9421/webhooks-nuxt-demo.git .
# 安裝依賴
RUN npm install
# 對外暴露 3000 端口
EXPOSE 3000
# 啓動時的執行腳本
CMD npm start
複製代碼

3. 建立 Docker Image

經過 cat 命令讀取以前建立的 SSH 公鑰和私鑰的內容並做爲變量傳遞給 Docker。因爲 build 鏡像的過程須要執行 git clonenpm install,取決於機器性能和帶寬,可能須要花費必定的時間。一個正常的 build 過程以下圖:

docker build \
-t webhooks-nuxt-demo \
--build-arg ssh_prv_key="$(cat ~/.ssh/github_id_rsa)" \
--build-arg ssh_pub_key="$(cat ~/.ssh/github_id_rsa.pub)" \
.
複製代碼

4. 啓動容器

在後臺啓動容器,並把容器內的 3000 端口 發佈到主機的 80 端口。

sudo docker run -d -p 80:3000 webhooks-nuxt-demo
複製代碼

5. 進入執行中的容器

必要的時候能夠進入容器中執行一些操做:

# 列出全部容器
docker container ls -a
# 進入指定的容器中
docker exec -i -t 容器名稱或者容器ID bash
複製代碼

7、留個後門

有時候咱們可能須要執行一些命令,來對項目進行更佳靈活的操做,好比切換項目的分支、進行版本回滾等。但若是隻是爲了執行一行命令就須要鏈接服務器,再進入容器內,不免有些繁瑣,啓發於 Webhooks,咱們不妨留個後門👻:

// 預留一個接口,必要時能夠經過調取這個接口,來執行命令。
// 如:經過發起下面這個 AJAX 請求,來進行 npm 包的升級並從新構建項目。
// var xhr = new XMLHttpRequest();
// xhr.open('post', '/command');
// xhr.setRequestHeader('access_token', 'b65c19b95906e027c5d8');
// xhr.setRequestHeader('Content-Type', 'application/json; charset=UTF-8');
// xhr.send(
// JSON.stringify({
// command: 'npm update',
// reBuild: true
// })
// );
app.post('/command', function(req, res) {
  // 若是必要的話能夠進行更嚴格的鑑權,這裏只是一個示範
  if (req.headers['access_token'] === 'b65c19b95906e027c5d8') {
    // 執行命令,並返回命令的執行結果
    execCommand(req.body.command, req.body.reBuild, function( error, stdout, stderr ) {
      if (error) {
        res.status(500).send(error);
      } else {
        res.status(200).json({ stdout, stderr });
      }
    });
    // 若是是純粹的從新構建,沒有須要執行的命令,直接結束請求,不須要等待命令的執行結果
    if (!req.body.command && req.body.reBuild) {
      res.status(200).end('Authorized and rebuilding!');
    }
  } else {
    res.status(403).send('Permission Denied');
  }
});
複製代碼

8、總結

若是你按照上述步驟成功了部署了你的 Vue SSR 項目,那麼當你每次 push 代碼到 git 服務器,它都會自動拉取並更新。👏👏👏🎉🎉🎉

雖然我試圖全面詳細的介紹如何擼一套自動化部署的前端項目,但這對於一個真實的項目來講,可能遠遠不夠。

例如,對於測試而言,可能咱們須要建立兩個的 Docker 鏡像(或者使用兩臺服務器),一個啓動在 80 端口,一個啓動在 3000 端口,分別拉取 master 分支和 dev 分支的代碼,經過對 Webhookspayload 進行判斷,來決定此次的 push 行爲應該更新哪一個服務,一般咱們在 dev 上進行頻繁的提交,由測試人員測試經過以後,咱們將 dev 分支的代碼階段性地合併到 master 分支,來進行正式版的更新。

又好比日誌監控的完善等等,因此個人這篇博客權當拋磚迎玉,歡迎各位大佬指正不足之處,評論交流,或者給個人這個項目提交 PR,你們一塊兒來完善這個事情。

相關文章
相關標籤/搜索