本文永久連接:github.com/HaoChuan942…html
一個前端項目簡單的開發部署流程一般是這樣的:先本地開發,當完成某個功能後構建打包,而後將生成的靜態文件經過 ftp
或者其餘方式上傳到服務器中。並將代碼 push
到 GitHub
或者 碼雲 等遠端倉庫中進行託管(爲了突出本文的重點,暫不考慮測試的環節)。這種工做流難免有些勞神費力,並且天天頻繁的打包上傳也會佔用不少時間。前端
一種理想的方式是:你只須要在服務器上建立一個「腳本」,執行這個腳本,他就會自動從 git
服務器拉取你的項目代碼,並啓動你的項目,而當你每次向 git
服務器 push
代碼時,它又會自動拉取最新的代碼並從新編譯,更新服務。node
爲了實現上述的「理想方式」,本文將詳細介紹如何使用 Nuxt
+ Webhooks
+ Docker
來實現一個 Vue SSR
項目的自動化部署。但咱們首先須要解決這麼幾個問題:linux
production
)啓動你的項目?要解決上面的問題,你須要瞭解如下基礎知識:webpack
SSH Key
。Nuxt
+ Docker
知識。Webhooks
。Node
+ express
知識。若是你對上述知識不是很瞭解或者不知道如何將他們結合在一塊兒來以達到所謂的「理想方式」,那麼接下的內容將從項目建立到實際部署,一步步的帶你完成這項工做。git
建立時的各類選項以下圖所示,你能夠根據本身項目的實際狀況進行選擇,但 server framework
請選擇 express
,本文也將以 express
做爲服務端框架展開介紹。github
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
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
應用,咱們經過了 Node
的 crypto
模塊計算簽名並和 Webhooks
請求中的簽名比對來進行鑑權,以保證接口調用的安全性(這裏的可以獲取到 Webhooks
請求的請求體 —— req.body
是因爲使用了 body-parser 中間件)。若是鑑權經過則返回成功狀態,並執行 upgrade
函數來更新服務,若是鑑權失敗,則返回無權限提示。同時,你須要向倉庫添加 Webhook
,以下圖:
若是你的項目已經在 http://www.example.com/
下啓動成功,那麼當你每次向 GitHub
倉庫 push
代碼時,你的接口都會收到一個來自 GitHub
的 post
請求,並在鑑權經過後執行 upgrade
函數來更新服務。關於如何在服務器上啓動項目咱們按下不表,先介紹 upgrade
函數都作了什麼。
/** * 從 git 服務器拉取最新代碼,更新 npm 依賴,並從新構建項目 */
function upgrade() {
execCommand('git pull -f && npm install', true);
}
複製代碼
execCommand
函數以下,這裏咱們使用了 Node
的 child_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
函數以下,這裏有兩個全局變量,render
和 server
,其中 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
的鑑權方式可能會有所不一樣,你須要參考他們的文檔對接口的鑑權方式進行一點調整)。
爲私有項目添加部署公鑰,使得項目在服務器上或者在 Docker
中能夠安全的進行代碼克隆和後續的拉取更新,參考連接1、參考連接2。這裏以 GitHub
爲例進行介紹:
生成一個 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
)登陸郵箱。
在 ~/.ssh
目錄下新建一個 config 文件,添加以下內容,參考文檔。
# github
Host github.com
HostName github.com
StrictHostKeyChecking no
PreferredAuthentications publickey
IdentityFile ~/.ssh/github_id_rsa
複製代碼
其中 Host
和 HostName
填寫 git
服務器的域名,IdentityFile
指定私鑰的路徑,StrictHostKeyChecking
設置爲 no
能夠跳過下圖中 (yes/no)
的詢問,這一點對於 Docker
流暢的建立鏡像頗有必要(不然可能要寫 expect
腳本),固然你也能夠經過執行 ssh-keyscan github.com > ~/.ssh/known_hosts
將 host keys
提早添加到 known_hosts
文件中。
在項目倉庫添加部署公鑰
測試公鑰是否可用
ssh -T git@github.com
複製代碼
若是出現下圖所示內容則代表大功告成,能夠執行下一步了。👏👏👏🎉🎉🎉
至此,若是你不須要使用 Docker
部署,而是使用傳統的部署方式,那麼你只須要在服務器上安裝 Node
和 git
,並把倉庫代碼克隆到服務器上,而後執行 npm start
在 80 端口啓動服務就能夠了。你可使用 nohup
命令或者 forever 等使服務常駐後臺。
# 添加 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
複製代碼
經過 cat
命令讀取以前建立的 SSH
公鑰和私鑰的內容並做爲變量傳遞給 Docker
。因爲 build
鏡像的過程須要執行 git clone
和 npm 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)" \
.
複製代碼
在後臺啓動容器,並把容器內的 3000 端口 發佈到主機的 80 端口。
sudo docker run -d -p 80:3000 webhooks-nuxt-demo
複製代碼
必要的時候能夠進入容器中執行一些操做:
# 列出全部容器
docker container ls -a
# 進入指定的容器中
docker exec -i -t 容器名稱或者容器ID bash
複製代碼
有時候咱們可能須要執行一些命令,來對項目進行更佳靈活的操做,好比切換項目的分支、進行版本回滾等。但若是隻是爲了執行一行命令就須要鏈接服務器,再進入容器內,不免有些繁瑣,啓發於 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');
}
});
複製代碼
若是你按照上述步驟成功了部署了你的 Vue SSR
項目,那麼當你每次 push
代碼到 git
服務器,它都會自動拉取並更新。👏👏👏🎉🎉🎉
雖然我試圖全面詳細的介紹如何擼一套自動化部署的前端項目,但這對於一個真實的項目來講,可能遠遠不夠。
例如,對於測試而言,可能咱們須要建立兩個的 Docker
鏡像(或者使用兩臺服務器),一個啓動在 80 端口,一個啓動在 3000 端口,分別拉取 master
分支和 dev
分支的代碼,經過對 Webhooks
的 payload
進行判斷,來決定此次的 push
行爲應該更新哪一個服務,一般咱們在 dev
上進行頻繁的提交,由測試人員測試經過以後,咱們將 dev
分支的代碼階段性地合併到 master
分支,來進行正式版的更新。
又好比日誌監控的完善等等,因此個人這篇博客權當拋磚迎玉,歡迎各位大佬指正不足之處,評論交流,或者給個人這個項目提交 PR
,你們一塊兒來完善這個事情。