搭建一个前端云构建服务

654 阅读4分钟

0. 引言

传统前端发布会将项目本地构建后通过工具或webhook部署到CDN,但当项目多人本地开发和发布,不同的环境可能会导致大家最后构建发布的内容不一致,引起线上问题后不便排查。因此,目前大部分公司都使用了云构建,在统一的容器中构建,保证打包环境和内容的一致性。

本着造轮子的想法,想造一个简单的docker云构建功能,目标是提供一个服务,能够接收gitlab URL后进行拉取,然后docker内部 打包/构建,并提供日志同步功能和zip包下载。最终产出一个可复用的docker镜像。

效果如下:

上图是将最终镜像(wolfulfl/web-build:1.0.4)拉到本地(docker pull wolfulfl/web-build:1.0.4),起的一个docker容器,里面有构建服务和界面,填写需要构建的仓库,可开始构建并获取构建产物。

接下来是实践记录, 源码地址

1. 环境准备

2. 具体实现

主体是构建node服务,并将该服务构建在一个docker镜像中,本地和其他机器上都可以拉取后运行。其中node服务功能为接收一个参数为gitlab url的请求后,进行代码拉取和构建,同时记录构建日志并输出,之后将打包完的文件部署到静态服务器上提供下载。

为了测试,前端用一个页面支持输入框输入github url实现websocket请求服务,并将日志打印在页面上。

按照上述思路,选websocket来实现实时日志输出,接下来是搭建websocket服务、docker构建以及相关的shell操作。

2.1 添加websocket和静态文件服务

新建node空项目(koa),添加websocket服务接收前端的URL,并不断返回构建的日志。
为了让打包后的文件可以下载,加上功能比较简单,这里就直接将静态文件服务也放在同一个脚本中。

const fs = require('fs');
const Koa = require('koa');
const websockify = rquire('koa-websocket');
const path = require('path');
const enforceHttps = require('koa-sslify').default;
const static = require('koa-static');
const spawn = require('child_process').spawn;

const staticPath = './';
const app = websockify(new Koa());
app.use(static(path.join(__dirname, staticPath)));

app.use(async(ctx) => {
  ctx.body = 'This resource not found';
});

// 由wss这个后缀承接请求,其他当作静态文件请求
// 接受gitlab 地址,生成hash,解析仓库名
app.ws.use(router.all('/wss', (ctx, next) => {
  const ws = ctx.websocket;
  ws.on('message', function incoming(message) {
    const repoName = getRepoName(message);
    const hash = stringHash(`${message}-${Date.now().toString()}`);
    // 运行脚本
    const mainSh = spawn("sh", ["main.sh", hash, message, repoName]);
    ws.send(`server receive url: ${message} and hash = ${hash}, repoName = ${repoName}, start to building...`);
  });
}));

app.listen(8090);

2.2 编写shell脚本,实现日志记录

先编写主要脚本入口main.sh,把前端参数透传下去,执行上述脚本,并创建日志文件把操作日志都记录下来。

#!/bin/bash
prefix="log/"
endfix="log"

logname=$prefix$1.$endfix;
# $1是hash名,$2是github地址,$3是仓库名
sh build.sh $1 $2 $3>>$logname

下面来编写构建操作的shell脚本build.sh。除了传入参数,主要做几件事,首先是clone仓库,再是构建,最后拷贝出项目构建的产物,放到当前文件夹中(有静态文件服务)。

#!/bin/bash\
echo repoHash $1\
echo repoURL $2\
echo repoName $3\
\
mdkdir $1\
cd $1\
\
git clone $2\
cd $3\
\
npm install\
npm run build\
\
zip -r ../../code/$1.zip ./dist/\
echo Builded, you can download it at <http://location.host/cloud-build/code/$1.zip>\

code/是存储构建内容的目录,log/是日志目录

以上步骤即可将项目打包,并且把zip包把放到静态目录下提供下载。

2.3 实现websocket同步输出日志

在接收请求处理中使用tail监听构建日志的log(也可以使用其他方式),并不断读出新增的内容,利用websocket同步。\

// 监听构建日志的log
const tail = spawn("tail", ["-f", `log/${hash}.log`, "-n", 30]);
tail.stdout.on('data', (data) => {
  ws.send(`${data.toString('utf-8')}`);
});

2.4 前端输入页面

前端实现一个能输入框输入,并且把接收到的websocket日志打印出来的页面:

const ip = `ws://${location.host}/wss`;
class WS {
  constructor(opt) {
    this.ws = new WebSocket(ip);
    this.ws.onmessage = this.onmessage;
    this.listeners = [];
  }
  push = (callback) => {
    this.listeners.push(callback);
  }
  send = (message) => {
    this.ws.send(message);
  }
  onmessage = (message) => {
    this.listeners.forEach(listener => {
      listener(message.data);
    });
  }
}
const ws = new WS();
export default class extends React.Component {
  componentDidMount() {
    setTimeout(() => {
      ws.push(this.onReceiveWsMessage);
    }, 2000);
  }
  onInput = (e) => {
    this.inputValue = e.target.value;
  }
  onSubmit = (e) => {
    ws.send(this.inputValue);
  }
  onReceiveWsMessage = (message) => {
    document.getElementById('code').textContent += message + '\n';
  }
  render() {
    return (
      <div>
        <input onInput={this.onInput} className="input" />
        <button onClick={this.onSubmit} className="submit-btn">确定</button>
        <pre id="code"></pre>
      </div>
    );
  }

通过以上步骤可实现输入一个github地址,对仓库进行构建,提供构建产物zip包下载。

2.5 编写dockerfile完成镜像构建

上面步骤完成后,基本上本地就可以运行了(需要提前安装zip/git),为了方便部署,以及保持开篇提到的预发/线上或者多机器上构建环境是一致的,下面将上述服务构建成docker镜像,pull后运行docker即可启动上述服务,且任意机器构建结果一致。

# 基础镜像选alpine比较轻量
FROM alpine:3.7
FROM node:9.2.1-alpine

RUN apk update \
  && apk add git --no-cache \
  && apk add zip --no-cache \
  && mkdir -p /home/Service

WORKDIR /home/Service
COPY . /home/Service

RUN npm config set registry https://registry.npm.taobao.org \
  && npm install \
  && npm run build

EXPOSE 8099

CMD ["npm", "start"]

dockerfile完成后,进行构建并push到docker hub(相当于代码的Github,其他人可拉取),需提前在docker hub上注册账号。

// 构建镜像
docker build -t yourName/yourRepoName:yourVersion .
// 发布镜像
docker push yourName/yourRepoName:yourVersion

// 镜像拉取
// 镜像已发布至docker hub,可以通过`docker pull wolfulfl/web-build:1.0.4`拉取
docker pull yourName/yourRepoName:yourversion

// 运行容器
// pull或build后,docker images 可查看imageId,前面的8090为宿主机(本地机器)开放的端口,可设置
docker run -d -p 8090:8090 imagesId

// docker ps,查看容器是否正常启动
// 打开浏览器访问 127.0.0.1:8090

3. 可能遇到的问题

  • 通过Nginx代理端口,可能会遇到WebSocket handshake的错误,主要原因是ws是基于http协议的,初次http请求与ws服务端会协商升级协议。协议升级完成后,后续的数据交换则遵照ws的协议,所以代理的http请求中需要添加以下字段标示协议升级,也就是告诉服务端这是websocket协议:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
  • wss 配置Nginx后,由于Nginx proxy_read_timeout的超时影响,即超过60s没有响应会关闭连接,尤其是在npm install的时候,因此最好设置多些超时的时长,以及用淘宝的npm镜像提高速度。

4. 不足和扩展

上述是一个简单的demo实现,距离一个真正的云构建系统还存在很多不足和可以改进扩展的地方:

  • 构建和部署模式优化

    • 通过配置脚本拉取自定义的构建配置,避免上述固定npm run build命令和dist/文件夹这类操作
    • 通过将请求内置到命令行中,以及关联webhooks,避免手动到网页上输入
    • 使用DB记录历史版本号和内容,支持回滚等操作
    • 支持将文件部署到指定CDN上去
    • 或直接结合jenkinsTravis ci来做持续集成,避免自己造轮子
  • 构建速度优化

    • docker构建速度优化
    • 生产环境,任务多时多机器和调度

5. 地址