本文永久连接: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
,你们一块儿来完善这个事情。