需求背景

项目是运营活动内嵌app的网页活动,采用的是MPA形式,快速迭代一期一期的;在布置方面,采用CDN缓存,每次发布的代码都会在服务器上进行增量存储;跟着项目的增加,每次全量构建所需花费的时刻越来越久有时乃至达到5分钟,对于要常常修正从头打包布置的运营活动,无疑是无法忍受的时刻成本;而且全量布置还会导致躲藏的危险,比如后面项目修正了大局公共函数或许组件,可能引发不兼容旧的已经上线的活动而开发测验并不知情的危险; 因而咱们设想,是否能够仅仅将新增的或许新修正的页面进行布置;或许更安全的,自己指定要打包哪个活动就只build该活动是不是更好呢!

思维导索

  1. 匹配文件途径来获取进口点(entry points)的信息【ps优化点: 获取文件创立修正时刻,依照最新的排在最前面】
  2. 用Node.js的inquirer模块创立交互式指令行界面,搜集用户要打包哪个进口活动;
    1. inquirer.prompt(questions)向用户提问并搜集用户输入
    2. inquirer.prompt(questions).then(answers => {})用于处理用户的输入
  3. 咱们的项目是布置在GitLab上,所以能够经过GitLab API开发文档手动调用获取或触发CI/CD流水线进行编译布置
  4. 编译时要将上述2中用户输入的用户信息存储起来,终究动态写入到vue.config.json中的多进口的装备中;也便是终究将pages下的key-value改成终究自己在第二步中搜集的
    学习按需构建
  5. 目前来思考一个问题,第一步和第二步都是在本地的node环境中,大局各处变量/函数均能够拿到;可是经过第三步CI/CD是在远端的gitlab容器中在跑多数是Unix体系,用户2中的输入如何存储,存储在哪,CI/CD履行时如何才能读取到呢???带着这个问题,咱们边开发边思考

一、 获取MPA形式下所有的活动页面目录

function getEnterPoints() {
  const moduleFilePathList = glob.sync('./src/activity/**/main.js')
  .map(filePath => {
    const name = filePath.match(/\/activity\/(.+)\/main.js/);
    return {
      outputName: snakeCase(name[1]), // 蛇形命名法(snake_case)作为输出名称
      filePath,
      retFilePath: filePath.replace('./src', '../src')
    }
  });
}
function getFsDirEditLastTime(filePath) {
    const file = filePath.replace("/main.js", "")
    return fs.statSync(file).mtime.getTime()
}
const entryPoints = getEnterPoints()
const entryPointFileNames = 
    entryPoints.sort((a,b) => getFsDirEditLastTime(b.filePath) - getFsDirEditLastTime(a.filePath))
    .map(item => item.outputName)

二、交互式界面搜集用户要打包的文件

const inquirer = require("inquirer");
const axios = require("axios");
// 交互式问题
const questions = [
    {
      type: "checkbox",
      name: "page",
      message: chalk.yellow("**请挑选需求发布构建的活动页面**"),
      choices,
    }
]
function inquirerPrompt(questions) {
    return inquirer.prompt(questions)
}

三、手动触发一个新的流水线

# Pipelines API文档地址 创立一个新的pipeline

学习按需构建

async function triggerPipeline() {
    const { page } = await inquirerPrompt(questions)
    axios.post(
        `https://gitlab.example.com/api/v4/projects/${yourProjectId}/pipeline`, 
        {
            ref: 'main', // 便于了解,暂时写死branch
            variables: [], // array {'key': 'TEST', 'value': 'test variable'}]
        }
    )
}

注:yourProjectId在你的项目中的Settings/General中能够看到Project ID

三(1)问题-为什么是Create a new pipeline而不是Trigger a pipeline with a token?

Trigger a pipeline with a token 这个功用允许经过运用特定的令牌(token)来触发流水线。每个 GitLab 项目都能够生成一个仅有的触发令牌,用户能够运用该令牌来触发流水线的履行。触发令牌一般用于与外部体系或服务集成,例如在继续集成/继续布置东西中装备触发器,或经过 API 调用来触发流水线。运用触发令牌触发的流水线能够履行与手动创立流水线相同的使命和操作;

Create a new pipeline 这个功用允许用户手动创立一个新的流水线。用户能够在 GitLab 界面上找到这个功用,并经过点击相应的按钮或链接来触发流水线的创立。创立新流水线时,GitLab 会依据预界说的流水线界说文件(一般是.gitlab-ci.yml文件)来履行一系列的使命。这些使命能够包含构建、测验、布置等操作,依据流水线界说文件中的装备进行主动化履行。

依据上述介绍,能够看出假如咱们需求在打包布置时做一些其他操作,比如测验啊,转换文件等其他操作,那么创立一个流水线,然后在.gitlab-ci.yml文件中做更多一系列的使命,显然这种办法的可扩展性更好;

三(2)问题-参数ref填写什么?

依照说明填写分支名或许tag; 咱们项目是依据分支来确定服务器的,所以会挑选branch;那么这个branch怎样来呢? 能够在【二、交互式界面搜集用户要打包的文件】中加一个question;

const questions = [
    {
      type: "list",
      name: "branch",
      message: chalk.yellow("**请挑选trigger的分支**"),
      choices: ["main", "release"],
      default: ["main"]
    },
    {
      type: "checkbox",
      name: "buildPage",
      message: chalk.yellow("**请挑选trigger的entry point**"),
      choices,
    },
]
这样ref中就能够填写经过拿到inquirerPrompt回来的branch了

三(3)问题:回到思维导索中的第5个问题-如何将我choose的page暂存起来能让CI/CD中运用??

"Create a new pipeline"中有关键字variables咱们还没运用,文档提供必有其用;查阅文档抑制,在里面是能够增加多个变量并为每个变量指定一个键值对key - value; 所以我能够增加一个名为PAGE_VARIABLE的变量,设置其值为上述挑选的page名;这儿为了可扩展性,存储成为一个json字符串会更友好

async function triggerPipeline() {
    const { page, branch } = await inquirerPrompt(questions)
    axios.post(
        `https://gitlab.example.com/api/v4/projects/${yourProjectId}/pipeline`, 
        {
            ref: branch,
            variables: [
                { key: 'PAGE_VARIABLE', value: JSON.stringify({ page, branch }) }
            ], // array {'key': 'TEST', 'value': 'test variable'}]
        }
    )
}

在CI/CD过程中,你能够经过$VARIABLE_NAME的办法来拜访这些变量。例如,在一个Job中,你能够运用以下办法输出这个变量的值:

job_name:
    script: 
        - echo $PAGE_VARIABLE

这样,当你创立一个新的Pipeline,并携带了变量PAGE_VARIABLE时,CI/CD过程中的Job就能够拜访并运用这个变量了。

请留意,经过”Create a new pipeline”页面携带的变量仅适用于该次Pipeline运转,不会影响项目中的大局变量或其他Pipeline运转。假如你需求在多个Pipeline运转之间共享变量,你能够考虑运用项目等级的环境变量或将变量界说在.gitlab-ci.yml文件中。

上述仅仅将PAGE_VARIABLE打印在终端了,假如咱们想将其存储,就能够用cho $PAGE_VARIABLE > ./build/catch.json掩盖式写入到catch.json文件中;假如想在文件末尾追加而不是掩盖就用echo $PAGE_VARIABLE >> ./build/catch.jso

所以接下来履行.gitlab-ci.yml文件时,咱们就要将上述的PAGE_VARIABLE写入到一个临时文件中

四、履行流水线界说文件.gitlab-ci.yml

stages:
  # 记录page
  - record_page
  # 打包构建
  - build
  # 翻开表明主动布置
  - deploy
record_page:
  stage: record
  # image: centos:7
  image: registry.example.com/library/centos:7
  cache:
    # 写入之后需求缓存,共享给其它stage
    untracked: false
    key: 
      files: 
        - package-lock.json
    paths:
      # 发布模块记录(构建发布用)
      - build/.catch.json
  script:
    - echo $PAGE_VARIABLE > build/.catch.json
  rules:
    # $CI_PIPELINE_SOURCE === "api" -> pipeline 是由 api 触发
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "api"
main_build:
  # image: node:16.15-bullseye-slim
  image: registry.example.com/library/node:16.15-bullseye-slim
  stage: build
  variables:
    BUILD_MODE: "test"
  script:
      - start_time=$(date +%s)
      - npm install --registry https://registry.example.com
      - end_time=$(date +%s)
      - echo "脚本履行时刻:$((end_time - start_time))秒"
      - npm run build:$BUILD_MODE
  cache:
    untracked: false
    key: 
      files: 
        - package-lock.json
    paths:
      - node_modules
  artifacts:
    paths:
      - dist
    exclude:
      - dist/**/*.map
  rules:
    - if: $CI_COMMIT_BRANCH == "main" && $CI_PIPELINE_SOURCE == "api"

经过record_page后,就会在远端生成一个./build/catch.json文件,并且其间内容为

{
    page: 'login',
    branch: 'main'
}

上述终究便是履行npm run build:test

在package.json中咱们装备build:test为履行build,并设置环境mode为test

  "scripts": {
    "build:test": "vue-cli-service build --mode test",
  },

履行build:test就会主动查找vue.config.js中的装备

functoiin getManualChooseActivityPage() {
   // 这儿咱们就能够拿到catch.json中写的文件的相关信息,将其装备到构建的多文件进口即可;在此不做缀叙
}
module.exports = {
   pages: getManualChooseActivityPage(),
   outputDir: `./dist/`,
   ……
}

知识点扩大

1. process学习

在 Node.js 中,process是一个大局目标,提供了与当时 Node.js 进程相关的信息和操控能力。它是一个EventEmitter的实例,能够用于处理进程的事情和信号。 process目标具有许多特点和办法,下面是其间一些常用的:

  1. process.argv:一个包含指令行参数的数组,类似于前面提到的process.argv。它能够拜访传递给 Node.js 脚本的指令行参数。
  2. process.env:一个包含当时进程环境变量的目标。能够经过该目标读取和修正环境变量的值。
  3. process.cwd():回来当时工作目录的途径。
  4. process.exit([code]):退出当时 Node.js 进程。可选的code参数指定退出码,默以为 0。
  5. process.on(event, listener):用于注册事情监听器。常见的事情包含'exit'(进程退出时触发)、'uncaughtException'(捕获未处理的异常)等。
  6. process.stdout:规范输出流。能够运用它来打印信息到操控台。
  7. process.stderr:规范过错流。用于输出过错信息到操控台。
  8. process.stdin:规范输入流。能够经过它读取用户的输入。

除了上述特点和办法,process还提供了其他一些功用,如内存运用情况的监控、事情循环的操控、信号处理等。经过process目标,咱们能够获取和操控当时 Node.js 进程的各种信息,以及与其交互。

需求留意的是,process是一个大局目标,因而无需运用require来引入它,能够直接在 Node.js 脚本中运用。

1.1 process.argv

process.argv是一个Node.js中的大局变量,它包含了当时正在履行的Node.js脚本的指令行参数。它是一个数组,其间的第一个元素是Node.js的可履行文件的途径,第二个元素是正在履行的脚本文件的途径,后续的元素是指令行参数 例如,假如你在指令行中运转以下指令:

node script.js arg1 arg2 arg3

那么process.argv的值将是一个包含以下元素的数组:

['node', 'script.js', 'arg1', 'arg2', 'arg3']

你能够经过索引拜访特定的指令行参数,例如process.argv[2]将回来'arg1'process.argv[3]将回来'arg2',以此类推。