RushJS 自动化部署方案

在上一章节,我们搭建了基于Vue3的Monorepo项目,详情可点击 Vue3 + RushJS 前端Monorepo方案实践,这一篇文章将会逐步介绍自动化构建的方案。

1. 遇到的问题

  1. 自动化构建困难:像CI/CD,jenkins方案都难以满足多项目同时构建的需求
  2. 测试人员较为迷茫:该架构一旦实施,如果是由开发人员进行构建部署的话还好,毕竟我们知道哪些项目是需要进行构建的,然而当项目由测试人员进行主导测试的话,往往测试人员是需要合并他们想测试的分支进行测试的,现有的方案无非是缺少了可视化操作界面,以及无法达到测试的构建粒度。

2.方案思考

既然现有的方案已经无法满足现有的需求,那么就要重新写一套满足需求的自动化部署系统

  1. 选用一个前端框架作为展示,这里我选用的是 Arco Design Pro(Vue3版本) 进行开发,原因也很简单,因为上一篇是搭建Vue3的项目,所以对应的中台也要选择相应的技术栈来降低心智负担。
  2. 我们一般都会把代码部署到公司私有的gitlab仓库中,那么这个时候,我们要想方设法的拿到gitlab中所有的仓库信息,以及对应仓库下的分支信息。好在gitlab确实是给了我们相关的API进行调用,方便我们后续做拓展。( 什么?英文文档看不懂?没有关系,gitlab也有对应的中文文档站点供参考~)

3.方案实施 [前端部分]

  1. 请根据 Arco Design Pro(Vue3版本) 的快速入门文档,自行创建对应的项目,并成功运行。这里我创建的项目名为 monorepo-visual

  2. src -> router -> routes -> modules中新建 automatedBuild.ts文件用于路由生命,以及在 src -> views中新增automatedBuild文件夹用于存放代码,如图所示:
    image.png

  3. 接着我们在 gitlab官网中创建自己的账号,然后将之前所创建的 my-monorepo 项目推送到仓库中,注意咱们创建的仓库一定得是私有的,因为一般公司的源码都会私有化,如下图所示:
    image.png
    (注:如果你卡在了这一步,说明你的git知识还不过关,先去学习充分了再继续)

  4. 接着我们需要在用户个人中心里面申请access_token,因为我们的仓库是私有的,所以在访问gitlab API的时候,是需要带上access_token才能进行访问的,如图所示:
    image.png
    接着拷贝下access_token等待后续使用,如图所示:
    image.png

  5. 接着我们根据gitlab项目API仓库API分支API文档,在src -> api目录下,新建 automatedBuild.ts 接口文件,如下图所示:

    image.png
    注意getRepositoryTree接口所带的参数为path=apps,这是我们App所放置的目录,详见接口文档的参数解释。

  6. 在写对应接口的时候,我们发现少了接口的返回类型,但此时接口返回的参数数量实在是过多,这个时候我们可以借助vscode插件 JSON to TS,把返回的json数据转为TypeScript所对应的类型,如图所示:
    image.png
    然后我们把接口返回的JSON数据拷贝到 api -> automatedBuild.ts 文件中,按住 Shift + Ctrl + Alt + V 组合键,便能生成对应的interface类型了,然后我们把类型进行导出即可。

  7. 接着我们在 src -> views -> automatedBuild -> index.vue 文件中写入以下代码:

<template>
  <div class="container">
    <Breadcrumb :items="['自动化构建', '工作台']" />
    <a-table :columns="columns" :data="data.projectList">
      <template #appList="{ record }">
        <a-select
          v-model="record.currentSelectAppList"
          placeholder="请选择需要构建的项目"
          multiple
          :style="{ width: '320px' }"
        >
          <a-option
            v-for="item in record.appList"
            :key="item"
            :label="item"
          ></a-option>
        </a-select>
      </template>
      <template #branchList="{ record }">
        <a-select
          v-model="record.currentSelectbranch"
          placeholder="请选择需要构建的分支"
          :style="{ width: '320px' }"
          value-key="name"
        >
          <a-option
            v-for="item in record.branchList"
            :key="item.name"
            :label="item.name"
            :value="item"
          ></a-option>
        </a-select>
      </template>
      <template #opeartion="{ record }">
        <a-button type="primary" status="success" @click="openModal(record)"
          >开始构建</a-button
        >
      </template>
    </a-table>

    <a-modal
      v-model:visible="data.visible"
      @ok="startBuild"
      @cancel="
        data.visible = false;
        data.modalData!.currentSelectbranch = {};
      "
    >
      <template #title> 温馨提示 </template>
      <div>
        当前选择构建的项目为:{{
          data.modalData?.currentSelectAppList?.join(',')
        }}
      </div>
      <div>当前选择构建的分支信息如下:</div>
      <div v-if="data.modalData?.currentSelectbranch?.name">
        <a-collapse
          :default-active-key="[data.modalData?.currentSelectbranch.name]"
        >
          <a-collapse-item
            :key="data.modalData?.currentSelectbranch.name"
            :header="data.modalData?.currentSelectbranch.name"
          >
            <div
              >Hash值:{{ data.modalData?.currentSelectbranch.commit!.id }}</div
            >
            <div
              >提交人员:{{
                data.modalData?.currentSelectbranch.commit!.author_name
              }}</div
            >
            <div
              >提交时间:{{
                dayjs(
                  data.modalData?.currentSelectbranch.commit!.committed_date
                ).format('YYYY-MM-DD HH:mm:ss')
              }}</div
            >
            <div
              >提交内容:{{
                data.modalData?.currentSelectbranch.commit!.message
              }}</div
            >
          </a-collapse-item>
        </a-collapse>
      </div>
    </a-modal>
  </div>
</template>

<script lang="ts" setup>
  import {
    getAllProjects,
    getRepositoryTree,
    getRepositoryBranches,
    AllProjectItem,
    Commit,
  } from '@/api/automatedBuild';
  import { Message, TableColumnData } from '@arco-design/web-vue';
  import { onMounted, reactive } from 'vue';
  import dayjs from 'dayjs';

  interface ProjectItem {
    id: number;
    name: string;
    currentSelectAppList?: string[];
    appList?: string[];
    currentSelectbranch?: { name?: string; commit?: Commit };
    branchList?: { name: string; commit: Commit }[];
  }

  const columns: TableColumnData[] = [
    {
      title: '仓库ID',
      dataIndex: 'id',
      align: 'center',
    },
    {
      title: '仓库名称',
      dataIndex: 'name',
      align: 'center',
    },
    {
      title: '构建项目',
      dataIndex: 'appList',
      slotName: 'appList',
      align: 'center',
    },
    {
      title: '构建分支',
      dataIndex: 'branchList',
      slotName: 'branchList',
      align: 'center',
    },
    {
      title: '操作',
      dataIndex: 'opeartion',
      slotName: 'opeartion',
      align: 'center',
    },
  ];

  const data: {
    projectList: ProjectItem[];
    visible: boolean;
    modalData?: Required<
      Pick<ProjectItem, 'currentSelectAppList' | 'currentSelectbranch'>
    >;
  } = reactive({ projectList: [], visible: false });

  onMounted(async () => {
    try {
      const res = await getAllProjects();
      res.forEach(async (item) => {
        data.projectList.push({
          id: item.id,
          name: item.name,
          currentSelectAppList: [],
          appList: await handleRepositoryTree(item),
          branchList: await handleRepositoryBranches(item),
          currentSelectbranch: undefined,
        });
      });
    } catch (e) {
      Message.error(String(e));
    }
  });

  const handleRepositoryTree = async (item: AllProjectItem) => {
    try {
      const nameList: string[] = [];
      const res = await getRepositoryTree(item.id);
      res.forEach((item) => {
        nameList.push(item.name);
      });
      return nameList;
    } catch (e) {
      Message.error(String(e));
      return [];
    }
  };

  const handleRepositoryBranches = async (item: AllProjectItem) => {
    try {
      const branchList: { name: string; commit: Commit }[] = [];
      const res = await getRepositoryBranches(item.id);
      res.forEach((item) => {
        branchList.push({
          name: item.name,
          commit: item.commit,
        });
      });
      return branchList;
    } catch (e) {
      Message.error(String(e));
      return [];
    }
  };

  const openModal = (item: ProjectItem) => {
    if (!item.currentSelectAppList?.length || !item.currentSelectbranch?.name) {
      return Message.info('请先选择构建的项目/分支');
    }
    data.modalData = {
      currentSelectAppList: item.currentSelectAppList,
      currentSelectbranch: item.currentSelectbranch,
    };
    data.visible = true;
  };

  /** 请求后端接口 */
  const startBuild = async () => {
    try {
      if (!data.modalData?.currentSelectAppList.length) return;
      const params = {
        currentSelectAppList: data.modalData.currentSelectAppList,
        currentSelectbranch: data.modalData?.currentSelectbranch,
      };
      await startBuildAPI(params);
      Message.success('构建成功');
    } catch (e) {
      Message.error('构建失败');
    }
  };
</script>

<script lang="ts">
  export default {
    name: 'AutomatedBuild',
  };
</script>

<style scoped lang="less">
  .container {
    padding: 0 20px 20px;
  }
</style>

生成的UI界面如图所示:

image.png

image.png

4. 方案实施[后端部分]

前面我们主要实现了UI方面的功能,接着我们可以简单的实现一下后端部分。先在原有的my-monorepo项目中,新建server文件夹,然后进行初始化后。命令如下:

cd my-monorepo
mkdir server
cd server
npm init -y

接下来新增app.js文件,并且修改package.json文件,如下图所示:

image.png
然后我们在 rush.json -> projects 中,新增刚才的项目路径,如下图所示:

image.png

接着我们执行一次 rush update 命令,再执行 rush add -p express,将 express 框架安装到server中,然后在app.js文件中,写下以下代码:

const express = require('express')
const app = express()
const port = 3000

app.use(express.json())

app.post('/startBuild', (req, res) => {
  console.log(req.body)
  res.send({
    code:200,
    message:'成功'
  })
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

接着使用命令node app.js启动我们的express服务。

前端方面,三部曲:定义接口 -> 使用接口 -> 配置Proxy,如图所示:

image.png
通过以上的操作可以先测试下请求是否是正常的,接着我们继续修改app.js文件,如下所示:

const express = require('express')
const { execSync } = require("child_process");

const app = express()
const port = 3000

app.use(express.json())

app.post('/startBuild', (req, res) => {
  try{
    const body = req.body
    const currentSelectbranchName = body.currentSelectbranch.name
    execSync(`git checkout -B ${currentSelectbranchName}`)
    execSync(`git pull origin ${currentSelectbranchName}`)
    execSync('rush update')
    for(let appName of body.currentSelectAppList){
      console.log('appName: ', appName);
      execSync(`rush build --to ${appName}`)
    }
    res.send({
      code:200,
      message:'构建成功'
    })
  }catch(e){
    console.log('e: ', e);
    res.send({
      code:500,
      message:'构建失败'
    })
  }
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})

这样,只需要通知对应的运维,把打包后的 dist 路径告诉他即可进行部署了。

注意:如果你的后端程序[比如用Node写后端],是放在Monorepo仓库中的,并且你在编码的过程中,也依赖于其他的Monorepo项目。这个时候你就需要用 rush deploy 去进行部署了。

项目的所有代码,已经上传到gitlab中了,仓库为:my-monorepomonorepo-visual

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容