需求背景
项目是运营活动内嵌app的网页活动,采用的是MPA模式,快速迭代一期一期的;在部署方面,采用CDN缓存,每次发布的代码都会在服务器上进行增量存储;随着项目的增加,每次全量构建所需花费的时间越来越久有时甚至达到5分钟,对于要经常修改重新打包部署的运营活动,无疑是无法忍受的时间成本;而且全量部署还会导致隐藏的风险,比方后面项目修改了全局公共函数或者组件,可能引发不兼容旧的已经上线的活动而开发测试并不知情的风险; 因此我们设想,是否可以只是将新增的或者新修改的页面进行部署;或者更安全的,自己指定要打包哪个活动就只build该活动是不是更好呢!
思维导索
- 匹配文件路径来获取入口点(entry points)的信息【ps优化点: 获取文件创建修改时间,按照最新的排在最前面】
- 用Node.js的inquirer模块创建交互式命令行界面,收集用户要打包哪个入口活动;
- inquirer.prompt(questions)向用户提问并收集用户输入
- inquirer.prompt(questions).then(answers => {})用于处理用户的输入
- 我们的项目是部署在GitLab上,所以可以通过GitLab API开发文档手动调用获取或触发CI/CD流水线进行编译部署
- 编译时要将上述2中用户输入的用户信息存储起来,最终动态写入到vue.config.json中的多入口的配置中;也就是最终将pages下的key-value改成最终自己在第二步中收集的
- 目前来思考🤔一个问题,第一步和第二步都是在本地的node环境中,全局各处变量/函数均可以拿到;但是经过第三步CI/CD是在远端的gitlab容器中在跑多数是Unix系统,用户2中的输入如何存储,存储在哪,CI/CD执行时如何才能读取到呢???带着这个问题,我们边开发边思考
一、 获取MPA模式下所有的活动页面目录
js
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)
二、交互式界面收集用户要打包的文件
js
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
js
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;
js
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字符串会更友好
js
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中,你可以使用以下方式输出这个变量的值:
js
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
js
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.npmmirror.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文件,并且其中内容为
json
{
page: 'login',
branch: 'main'
}
上述最终就是执行npm run build:test
在package.json中我们配置build:test
为执行build,并设置环境mode为test
json
"scripts": {
"build:test": "vue-cli-service build --mode test",
},
执行build:test
就会自动查找vue.config.js中的配置
js
functoiin getManualChooseActivityPage() {
// 这里我们就可以拿到catch.json中写的文件的相关信息,将其配置到构建的多文件入口即可;在此不做缀叙
}
module.exports = {
pages: getManualChooseActivityPage(),
outputDir: `./dist/`,
......
}
知识点扩充
1. process学习
在 Node.js 中,process
是一个全局对象,提供了与当前 Node.js 进程相关的信息和控制能力。它是一个 EventEmitter
的实例,可以用于处理进程的事件和信号。 process
对象具有许多属性和方法,下面是其中一些常用的:
process.argv
:一个包含命令行参数的数组,类似于前面提到的process.argv
。它可以访问传递给 Node.js 脚本的命令行参数。process.env
:一个包含当前进程环境变量的对象。可以通过该对象读取和修改环境变量的值。process.cwd()
:返回当前工作目录的路径。process.exit([code])
:退出当前 Node.js 进程。可选的code
参数指定退出码,默认为 0。process.on(event, listener)
:用于注册事件监听器。常见的事件包括'exit'
(进程退出时触发)、'uncaughtException'
(捕获未处理的异常)等。process.stdout
:标准输出流。可以使用它来打印信息到控制台。process.stderr
:标准错误流。用于输出错误信息到控制台。process.stdin
:标准输入流。可以通过它读取用户的输入。
除了上述属性和方法,process
还提供了其他一些功能,如内存使用情况的监控、事件循环的控制、信号处理等。通过 process
对象,我们可以获取和控制当前 Node.js 进程的各种信息,以及与其交互。
需要注意的是,process
是一个全局对象,因此无需使用 require
来引入它,可以直接在 Node.js 脚本中使用。
1.1 process.argv
process.argv
是一个Node.js中的全局变量,它包含了当前正在执行的Node.js脚本的命令行参数。它是一个数组,其中的第一个元素是Node.js的可执行文件的路径,第二个元素是正在执行的脚本文件的路径,后续的元素是命令行参数 例如,如果你在命令行中运行以下命令:
js
node script.js arg1 arg2 arg3
那么 process.argv
的值将是一个包含以下元素的数组:
js
['node', 'script.js', 'arg1', 'arg2', 'arg3']
你可以通过索引访问特定的命令行参数,例如 process.argv[2]
将返回 'arg1'
,process.argv[3]
将返回 'arg2'
,以此类推。