用状态机实现一个云构建平台

2,443 阅读5分钟

本文正在参加「金石计划 . 瓜分6万现金大奖」

最近在升级团队云构建的工具,由于构建任务的状态跳转很复杂,早期的IF-ELSE写法已经无法满足任务管理的扩展了,比如我想加一个代码规则的校验,那我就需要加上新的条件分支,各种IF-ELSE散落到各种异步逻辑中,这就给后续产品逻辑调整继续增加负担,因此决定用状态机来描述构建任务的流转,让逻辑变得可预测。

本文的重点是介绍用状态机描述一个云构建任务的流转,具体的构建方式可以根据自己团队的实际情况来设计

闲言少叙,开始

云构建是什么

顾名思义,就是在服务端进行代码的build。体现在前端就是通过在云端提供的node环境上,运行打包工具,将源代码构建成浏览器可执行的代码,并将打包物返回给调用方的方式 目前很多在用的travis、gitlab-ci都是一种云构建的工具,通过简单的配置的就可以进行自动化的构建,比如博主在构建github page时,在github上用到的travis的配置如下:

language: node_js
node_js: "10"
branches:
  only:
  - master
cache:
  apt: true
  yarn: true
  directories:
    - node_modules
before_install:
- git config --global user.name "xxx"
- git config --global user.email "xxx@xxx.com"
- curl -o- -L https://yarnpkg.com/install.sh | bash
- export PATH=$HOME/.yarn/bin:$PATH
- npm install -g hexo-cli
install:
- yarn
script:
- hexo clean
- hexo generate
after_success:
- cd ./public
- git init
- git add --all .
- git commit -m "Travis CI Auto Builder"
- git push --quiet --force https://$REPO_TOKEN@github.com/zhyjor/zhyjor.github.io
  master

gitlab ci上配置也差不多,这样云端的构建基本就完成了;自己的项目中完全可以在success后,将构建物推到cdn上,然后通过cms工具发布一下html即可,这样一个简单的云构建流程就起来了

为什么需要云构建呢

博主之前遇到多次由于打包环境不一致引发的线上问题,引起环境不一致的可能是node版本不一致引发的问题,也可能是构建时代码未进行pull造成功能缺失等等,总结下有一下几点

  • 提供纯净的无干扰的环境,避免本地环境不同(node、os等),引起构建物的差异
  • 提供可溯源的管控平台,可对项目发布进行管理
  • 避免代码未提交引起线上分支功能缺失问题
  • 记录构建日志、构建时长、分析依赖,未项目优化提供数据支持

状态机是什么

前端场景下说到状态机,就不可避免的谈到状态管理,使用vue全局状态管理一般是vuex,react一般是Redux。UI=Fx(Data),使用了这些框架后,前端写的大部分业务逻辑都是在管理状态,然后框架帮我吗映射为UI

随着业务的逐渐膨胀,业务逻辑层(数据层)逐渐变成了一团逐渐失控的代码,状态,由于本文重点不是讨论状态管理框架的,这里贴一张克军大佬的ppt,很明确的表现了复杂逻辑下,状态失控的问题

复杂的业务逻辑带来的是太多的逻辑分支,各种IF-ELSE分散在不同的组件内,造成代码难阅读,难扩展

使用状态的好处

有限状态机,是表示多个有限状态以及这些状态间的转移、动作的数学模型,通过构建这个数学模型,将业务逻辑描述出来;通俗来说,状态机的描述文件就是一个剧本,通过这个剧本,逻辑就变得可预测了

XState

我们选择了XState作为云构建的状态机框架,状态机的库也挺多的,XState、JavaScript-State-Machine、Robot等,XState的优势是文档更好,同时具有可视化工具,使用VSCode的扩展可以很方便的进行状态机设计

状态机设计

状态机设计这里社区没什么最优实践,也没什么明确的方法论,只能结合自己的业务逻辑给出一个适合自己的方案

场景分析

一个云构建任务分为如下的几个过程,其中check阶段作为并行任务实现

由上图可得出状态机的简单描述文件

import { createMachine, interpret, assign } from 'xstate';


function startAsyncTask() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(new Date().getTime());
    }, 1000);
  })
}

const buildMachine =
  /** @xstate-layout N4IgpgJg5mDOIC5QCMCuBLANhAdOimYAxAAoBKAoiQIKUD6FAcgGoDaADALqKgAOA9rHQAXdPwB2PEAA9EARgDMcnO1WqATAE45ADl3qdAdgA0IAJ6J1AFnU5DmvQFYdVw1Z3tNhnQF8fptCxcCH4AYwBrMAAnIhDxMDxxADd+SJxYYQBDKOEAYQks9Hiojm4kEAEhUQkpWQQrR00cBU12ADY5dS7FDp1TCwR1BQUcLzbHKwUrTq05Nr8AjGwcEIjoomio-iicXkxM4QAzbYBbdKyc-PFC4tKpSpExSXK6hqaW9pn1Ht1++SnRpogZoFIZQZour5-CBAstQpgJMQ4gkiik0lARLkEfE7uUHtVnqA6jpQYDvo42oZHHNXJo-oNHIYcFY2u45A57FMjAsYUtcLCILFEYk0QksrBwgAhPm4viCR41F7yORUnA6AzsHTOdiOdgqkzmSyM5msvQckHuQw8gW7VDITDoWAACyF8RFqQSvDtDudABV+LkIOJZRV5QTavJqUzXOMddYdBCFG16epjSy2eauVboTbDpksKgosRKL6yABNEP4p4RhAUxzNNwOdkJ9RtbT09WA4FtsZtdUKPzQ8T8CBwKQ2-CEe5h6tK+rqFMAzSmuaswxDBMDnN8lZhSJRadVWdEyzLnAGVqNORyaYKPqGwbs89N9dtJQKdTsUHWnfwxGHhVCRkRBKSsNVWjaL9WSsGxpkXEZl3cdpE3UNx2GzRYghwAUAPDOc5D1WxX0cD820MewrHg0ZTSmL9nEaVCfywr17UdJ1cOPYCEEUCYGysTRHG+aYezpB8hgQmirDorUIQw3ksNgVBQlCOB4DxGdFRPbj1yaIx+ME-jVAhFM01NLRtG8Dp1CY5Y8wLIsOM0rjQPA9ooJg2C5HpORBOZHR1RcRQtAmOYbIgRygLqABaZMHxilQ1EUTxIR1KxBx8IA */
  createMachine({
    context: {
      id: 42,
      repo_url: undefined,
      recordTime: {
        docker: { start: undefined, end: undefined },
        clone: { start: undefined, end: undefined },
        install: { start: undefined, end: undefined },
        build: { start: undefined, end: undefined },
      },
      imageUrl: "node:16",
    },
    id: "build",
    initial: "idle",
    states: {
      idle: {
        on: {
          PREPARE_ENV: {
            target: "docker",
          },
        },
      },
      docker: {
        invoke: {
          id: "startContainer",
          src: (context, event) => {
            // 更新一下一下时间
            const dockerTimeStart = new Date().getTime();
            const dockerTime = context.recordTime.docker;
            dockerTime.start = dockerTimeStart;
            context.recordTime.docker = dockerTime;
            assign({ recordTime: { ...context.recordTime } });
            // image start
            return startAsyncTask(context.imageUrl);
          },
          onDone: [
            {
              target: "clone",
              actions: assign({
                recordTime: (context, event) => {
                  const dockerTime = context.recordTime.docker;
                  dockerTime.end = event.data;
                  context.recordTime.docker = dockerTime;
                  return context.recordTime;
                }
              }),
            },
          ],
          onError: [
            {
              target: "failure",
              actions: assign({ error: (context, event) => event.data }),
            },
          ],
        },
      },
      clone: {
        invoke: {
          src: (context, event) => {
            return startAsyncTask(context.repo_url);
          },
          id: "gitClone",
          onDone: [
            {
              target: "build",
            },
          ],
        },
      },
      build: {
        invoke: {
          src: (context, event) => {
            return startAsyncTask();
          },
          id: "taskBuild",
          onDone: [
            {
              target: "publish",
            },
          ],
        },
      },
      publish: {
        invoke: {
          src: (context, event) => {
            return startAsyncTask();
          },
          id: "publishToCdn",
          onDone: [
            {
              target: "success",
            },
          ],
        },
      },
      success: {
        type: "final",
      },
      failure: {
        type: "final",
        on: {
          RETRY: {
            target: "idle",
          },
        },
      },
    },
  });

const buildService = interpret(buildMachine).onTransition((state) => {
  console.log(state.value);
  if (state.value === 'clone') {
    console.log(state.context.recordTime);
  }
});

buildService.start();
buildService.send('PREPARE_ENV');

// 输出
idle
docker
clone
{
  docker: { start: 1668248311128, end: 1668248312130 },
  clone: { start: undefined, end: undefined },
  install: { start: undefined, end: undefined },
  build: { start: undefined, end: undefined }
}
build
publish
success

总结

上述用状态机模拟了一个简单的构建过程,其实还有很多值得深究的地方,比如check阶段使用并行状态来进行多种检查等等,另外构建平台也有更多提高稳定性的逻辑,gitlab-ci构建方式也有docker in docker、bash等不同的方式,有兴趣的话可以后续再更新几篇关于云构建的文章

最后的最后,欢迎关注个人的github博客

本文正在参加「金石计划 . 瓜分6万现金大奖」