Skip to content

Latest commit

 

History

History

前端构建发布

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

前端构建发布

试图讲清楚这几件事情:

[1. 构建过程做了什么?](#一、构建过程做了什么?)
[2. 构建平台做了什么?](#二、构建平台做了什么?)
[3. 发布平台做了什么?](#三、发布平台做了什么?)

除了从大体上了解整个有赞构建发布的流程和设计架构,我们还可以了解一下穿插在其中的奇思妙想以及一些技巧。

一、构建过程做了什么?

这个构建过程做了我们从本地打包构建入手,我们先分析本地打包构建的过程,才能进一步理解构建平台的实质工作内容。

beauty-web项目为例,我们构建的命令是make release,不同于我以前熟知一些项目的构建命令入口是npm srcipts。原因受限于构建平台的规范制约,我们大部分有赞的前端项目的打包命令入口都被Makefile命令把控。make 命令执行时,需要一个 Makefile 文件,以告诉 make 命令需要怎么样的去编译和链接程序。

两者所行使的任务职责没有太大区别,本质都是调用Shell去运行脚本命令,只是运行环境环境存在一些不同,npm srcipts依赖node环境和npm,是npm内置的功能,而Makefile只需要依赖基本的Linux环境。

我猜测之所以采用Makefile来执行编译构建过程的规范,除了计算机构建历史几十年的证明可靠之外,更多的体现在makefile文件更清晰,定义变量等功能比npm scripts更大,简单来说,Makefile更适用于支撑大型项目,有足够的兜底。

好了,现在我们来看一下Makefile文件,从最后一行的release看起:

release: dist_clean bower yarn_install fabu_branch build_server build_client cdn_assets fabu_push

这是Makefile的一个多任务目标的写法,他们之间是循环依赖关系,前者执行完成再执行下一个任务,如果我们认为任务之间没有依赖关系,可以同时进行我们可以在调用时带上参数-j,如:

make -j release

但是很显然,我们这个release目标执行的任务是同步执行,所以我们make release这样调用。

dist_clean:清除缓存目录 => 
bower yarn_install:安装依赖 =>
fabu_branch:git不忽略"dist_client|dist_server”目录,在当前分支新建拉出一个带时间戳的分支 =>
build_server:babel编译server文件到dist_server =>
build_client:webpack打包前端js =>
cdn_assets:使用superman cdn 上传静态资源到cdn =>
fabu_push:在带时间戳的分支下,添加一个名为beauty-web-fabu的远程仓库,提交刚才打包的内容,push到fabu的远程仓库的不带时间戳分支的源分支,push完成后切回源分支,删除带时间戳的分支,remove掉fabu的远程仓库地址。

二、构建平台做了什么?

架构/Users/zhouzhen/web/youzan_build/worker(打包系统worker/server/babel.app.js

- client(打包系统前后端 build.qima-inc.com)
- config(打包系统项目公共配置@youzan/builder-config)
- master(调度中心 build-master.qima-inc.com)
- worker(打包系统worker)

我们从使用流程讲起,然后

  1. 点击添加任务按钮,开始一个构建任务,需要提供应用名,分支名,构建命令,其他补充(部署预发 & 更新Composer)。
  • 应用名从@youzan/builder-config拉取枚举常量,可供选择。
  • 分支名只有检测功能,原理是从服务器调用gitlab判断是否有该分支的API。(为什么不提供列表选择功能,因为gitlab提供的分支API最多100条每页,需要多次调用进行包装,略显麻烦)
  • 构建命令也是从@youzan/builder-config拉取对应项目的commands数组供选择。
  • 其他补充(部署预发 & 更新Composer)会在开始任务消息带上参数doDeploy=1
  1. 点击开始任务,socket连接的服务端接受到startTask的消息,内部POST调用了自身http://build.qima-inc.com/api/task这个接口,返回的成功或者失败会通过socket传递回前端。

  2. api/task路由下的async函数逻辑:调用了@youzan/builder-db-serviceTaskService.createTask方法,往mysql数据库插入了一条数据,id是11位整形自增长数字。 然后拿着返回的result的id再去POST调用http://build-master.qima-inc.com/task/${result.id}接口,build-master.qima-inc.com项目就是master项目。

  3. 跑到master查看逻辑,更是一层套一层。。,我们找到核心的async函数TaskController.startTask,根据传入的任务id从刚才存入的mysql数据库,把这条数据重新查出来。拿到我们任务的详情,此处再进行一些基本的校验。然后又调用了@youzan/builder-db-serviceSubTaskService.createSubTask方法,存入进入状态的这条数据。

  4. 然后,寻找有没有可用的机器,我们调用了heartBeatWorker.workerList获取到了真正的执行构建任务的机器,通过currentStatus==='0'筛选可用机器,判断可用机器为0则返回没有空闲的worker

  5. 如果有可用机器,调用heartBeatWorker.requestStart发布开始构建请求。我们连续用到了heartBeatWorker的方法,它是我们打包机器的维护者。接着调用了被选中的构建机器的/api/worker/${id},这下我们得进入worker项目一探究竟。

  6. handleWorker的业务逻辑是这样的:首先根据buildWorker.worker是否存在判断当前buildWoker是否在运行,然后一些系列健壮性判断,如:拿到id到DB查看subTask任务和项目在Config是否存在,塞入该项目Config的GitLab id

  7. 接着执行buildWorker.startstart的逻辑是这样的,通过child_process.fork执行一个子进程,运行woker.js

this.worker = await child_process.fork(__dirname + '/worker_dist.js').on('exit', async () => {
    // 失败终止
    this.worker = null;
    await shell.kill();
});
// 向子进程发送`start`消息
await this.worker.send({ type: 'start', data });
  1. 绕了这么一圈,我们终于来到了构建机器执行构建的执行命令。
// 
const { id, tid, project, branch, command } = msg.data;
const projectConfig = Config.projects[project];

// 调用gitlab的branch API
const checkResult = await shell.checkBranch(tid, branch);

// sh git_pull.sh ../../../codes/${project}  ${branch}
// |
// cd ../../../codes/${project}     // changing working directory to $1
// unset GIT_DIR                    // unsetting git dir
// git add . && git reset --hard HEAD   // reseting code
// git checkout master                  // cleaning 除了master以外的branches 
// git branch | grep -v "master" | xargs git branch -D
// (if master) git fetch origin master && git merge --ff origin/master
// git fetch origin ${branch}       // fetching branch
// git checkout ${branch}           // checkout branch
// (if hotfix or featurek start)  git fetch origin master   git merge --ff origin/master git pull origin ${branch}
const updateResult = await shell.updateCode(project, branch);   // 如果之前没有这个项目则clone一份

// PATH=$PATH:/opt/gcc/bin CAS_USER=${user} sh ./make.sh ../../../codes/${project} ${command || 'release'}
// |
// cd ../../../codes/${project}
// PATH=$PATH:/opt/gcc/bin make -f $MAKEFILE ${command}
const makeResult = await shell.make(project, command, user);

// sh git_push.sh ../../../codes/${project} ${branch} ${buildProject || project} ${user}
// |
// cd ../../../codes/${project} ${branch}
// unset GIT_DIR
// git add . && git commit -m "$3 built by $4 on machine $HOSTNAME" -n
// git pull origin ${branch}
// git push -u origin ${branch} --no-verify
const pushResult = await shell.pushCode(project, branch, project, user);

process.send({ type: 'over' });