背景:
多人开发时,通过微信小程序工具上传代码体验版都会不同,需要不断频繁切换体验码/仅限某个人仅限代码上传。
解决方案:
通过微信小程序 miniprogram-ci 脚本 + git 操作进行代码上传
官方文档地址:概述 | 微信开放文档
步骤:
1.密钥及 IP 白名单配置
使用 miniprogram-ci 前应访问"微信公众平台-开发-开发设置"后下载代码上传密钥,并配置 IP 白名单 开发者可选择打开 IP 白名单,打开后只有白名单中的 IP 才能调用相关接口。我们建议所有开发者默认开启这个选项,降低风险 代码上传密钥拥有预览、上传代码的权限,密钥不会明文存储在微信公众平台上,一旦遗失必须重置,请开发者妥善保管
!注意:上传密钥需要保存到指定文件,防止丢失
2.安装miniprogram-ci
vsvscode打开小程序项目,在终端安装ci:
npm install miniprogram-ci
3.创建文件夹及相关文件
安装完 CI 插件后会自动创建 package.json 文件,需要在同级目录创建一个deploy文件夹(命名自定义),并创建 index.js 文件,把第一步下载的上传密钥放在 weapp.key 文件中
上传密钥
上传思路:
上传时怎么获取当前体验版版本号+版本备注信息?
普通的CI上传已不能满足多人多分支的需求,可结合git+本地文件版本号获取线上体验版信息
代码实现:
在 package.json 文件中定义相关项目版本信息,定义 wxconfig 对象,本地读取版本号 + 备注,在上传时进行更改并上传(并同步到远程git)
注:需要额外安装 prompts node交互插件(原生nodejs终端交互已不能满足需求,后面会解释)
安装 prompts 插件 , 官方文档
npm install --save prompts
注:在 package.json 文件中的 miniprogram-ci 跟 prompts 需要声明在 devDependencies 对象内,而非默认的 dependencies 。
index.js 文件(内容不做解释,部分复杂的已做注释)
/**
* 修改版本号并自动上传脚本思路
* 1.获取传进来的参数 √
* 2.根据参数进行逻辑处理 √
* 3.获取package.json中的version参数 √
* 4.修改version的值写入package.json文件 √
* 5.git提交package.json文件 √
* 6.微信自动上传脚本上传到体验版 √
*/
const prompts = require('prompts')
const fs = require('fs').promises // 使用 fs.promises API 来获取异步方法
// 异步子进程
const exec = require('child_process').exec
const ci = require('miniprogram-ci')
const path = require('path')
// 引入package文件内容
const packageJSON = require('../package.json')
// prompts取消时 ctrl + c 不然主动退出会打印错误
const onCancel = (prompt) => {
process.exit()
}
// exec异步封装成异步函数
function execAsync(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(error)
} else {
resolve(stdout.trim())
}
})
})
}
// 设置ANSI转义码来添加颜色 colorCode: 91 红色 92 绿色
const colorizeText = (text, colorCode) => {
return `\x1b[${colorCode}m${text}\x1b[0m`
}
let curBranch = ''
// 获取当前本地分支名字
const getCurrentBranch = async () => {
try {
curBranch = await execAsync('git rev-parse --abbrev-ref HEAD')
// if (curBranch !== 'dev') {
// console.error(colorizeText(`请切换到 dev 分支进行上传操作!`, 91))
// process.exit(1)
// }
} catch (error) {
console.error(colorizeText(`获取分支名字失败!${error}`, 91))
process.exit(1)
}
}
// 填写版本号
const askVersion = async function () {
console.log(
`${colorizeText('*', 32)} 当前线上版本号:${colorizeText(
packageJSON.wxconfig.version,
36
)}:\n `
)
try {
// 这里注意不能提前在函数外部定义,
// initial 不会取到变量,只会取初始值
let versionQuestions = [
{
type: 'select',
name: 'version',
message: '请选择当前要修改版本为?',
hint: '使用 ⇧ / ⇩ 进行选择,回车键确认',
choices: [
{
title: '不做修改',
description: '版本号不变',
value: 'old',
},
{
title: '修订补丁',
description: 'X.Y.Z -> Z+1',
value: 'z',
},
{
title: '特性更新',
description: 'X.Y.Z -> Y+1',
value: 'y',
},
{
title: '版本升级',
description: 'X.Y.Z -> X+1',
value: 'x',
},
{
title: '自行填写',
value: 'new',
},
],
initial: 0,
},
// 选择自行填写
{
type: (prev) => (prev === 'new' ? 'text' : null),
name: 'version',
message: '请输入当前版本',
initial: packageJSON.wxconfig.version,
validate: (value) => {
const pass = value.trim().match(/^[\dA-Za-z.]+$/)
if (pass) {
return true
} else {
return `版本号仅限字母、数字和小数点!`
}
},
},
]
const res = await prompts(versionQuestions, { onCancel })
let newVersion = ''
let parts = packageJSON.wxconfig.version.split('.')
// 判断是否符合版本更新特性
const updateArr = ['x', 'y', 'z']
if (updateArr.includes(res.version)) {
switch (res.version) {
case 'z':
parts[2] = parseInt(parts[2]) + 1
newVersion = parts.join('.')
break
case 'y':
parts[1] = parseInt(parts[1]) + 1
newVersion = parts.join('.')
break
case 'x':
parts[0] = parseInt(parts[0]) + 1
newVersion = parts.join('.')
break
}
} else if (res.version === 'old') {
// 不做修改
newVersion = packageJSON.wxconfig.version
} else {
newVersion = res.version
}
return newVersion
} catch (e) {
process.exit(1)
}
}
// 填写描述
const askDesc = async function () {
let descQuestions = [
{
type: 'text',
name: 'desc',
message: '请输入项目备注:(tab键回显)',
initial: packageJSON.wxconfig.desc,
},
]
const res = await prompts(descQuestions, { onCancel })
return res.desc
}
//
const askQuestion = async function () {
try {
const version = await askVersion()
const desc = await askDesc()
// 修改package对应的内容
packageJSON.wxconfig.version = version
packageJSON.wxconfig.desc = desc
// 修改更新时间 防止默认回车 git提交时会识别为未更改内容
packageJSON.wxconfig.updateTime = Date.now()
console.log('\n修改后版本号信息:', {
version: version,
desc: desc,
})
} catch (error) {
console.log(colorizeText(error, 91))
process.exit(1)
}
}
// 写入package.json
const writeFile = async () => {
try {
// JSON.stringify 方法将 JavaScript 对象转换为 JSON 字符串,第二个参数 null 表示没有替换函数,第三个参数 2 表示缩进空格数为 2,用于格式化输出的 JSON 字符串。
// 异步写回到文件中
await fs.writeFile('package.json', JSON.stringify(packageJSON, null, 2))
console.log(colorizeText('\n修改package.json文件成功!\n', 92))
} catch (err) {
console.error(colorizeText(`fs.writeFile err:${err}`, 91))
process.exit(1)
}
}
// 提交代码到当前git分支
const gitSave = async () => {
try {
await execAsync(`git add .`)
await execAsync(
`git commit -m '修改version为:${packageJSON.wxconfig.version};desc:${packageJSON.wxconfig.desc}'`
)
// 前面限制了dev分支,这时候就无需一定要推送到dev
await execAsync(`git push`)
console.log(colorizeText(`\n成功推送到${curBranch}分支!\n`, 92))
} catch (error) {
console.error(colorizeText(`推送git失败!${error.message}`, 91))
process.exit(1)
}
}
// 机器人自动化上传
const autoUpload = async () => {
const project = new ci.Project({
appid: '填写你的appid',
type: 'miniProgram',
// projectPath:项目的路径,即 project.config.json 所在的目录
projectPath: path.join(__dirname, '../'),
// privateKeyPath:私钥的路径
privateKeyPath: path.join(__dirname, './weapp.key'),
// 指定需要排除的规则
ignores: ['node_modules/**/*', 'package-lock.json'],
})
console.log(colorizeText(`\n正在上传...\n`, 92))
const uploadResult = await ci.upload({
project,
version: packageJSON.wxconfig.version,
desc: packageJSON.wxconfig.desc,
setting: {
es6: true,
es7: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
minify: true,
},
onProgressUpdate: console.log,
})
console.log(uploadResult)
console.log(colorizeText('上传完成!', 92)) // 亮绿色代码是 '92'
console.log(colorizeText('版本号:' + packageJSON.wxconfig.version, 92))
console.log(colorizeText('描述:' + packageJSON.wxconfig.desc, 92))
}
;(async () => {
// 获取当前本地分支名字
await getCurrentBranch()
// 交互修改
await askQuestion()
// 异步写入package.json文件
await writeFile()
// // 保存并提交git
await gitSave()
// // 机器人自动化上传
await autoUpload()
})()
在 package.json 文件配置npm命令
即在终端运行 npm run deploy 就会执行deploy文件下的index.js文件命令
npm run deploy
即以下部分按顺序执行
(async () => {
// 获取当前本地分支名字
await getCurrentBranch()
// 交互修改
await askQuestion()
// 异步写入package.json文件
await writeFile()
// // 保存并提交git
await gitSave()
// // 机器人自动化上传
await autoUpload()
})()
问题2:
当前脚本只能做体验版上传,因为体验版本号更新过快,需要根据线上发布版的版本号去做上传。
解决方案:利用 git tag 唯一性模拟发布版本的版本信息
思路跟 index.js 差不多一致
新建 tag.js 文件
/**
* 修改版本号并自动上传脚本思路(发布版)
* 1.获取当前分支
* 2.获取远程tag版本(小程序线上版本)
* 3.获取tag版本 + 附注信息,进行版本提示
* 4.选择/输入版本号
* 5.修改version的值写入package.json文件
* 6.写入附注tag,并提交到远程
* 7.删除时间最久的一条tag,防止tag分支过多(可选)
* 8.微信自动上传脚本上传到体验版(注意机器人号) √
*/
const prompts = require('prompts')
const fs = require('fs').promises // 使用 fs.promises API 来获取异步方法
// 异步子进程
const exec = require('child_process').exec
const ci = require('miniprogram-ci')
const path = require('path')
// 引入package文件内容
const packageJSON = require('../package.json')
// prompts取消时 ctrl + c 不然主动退出会打印错误
const onCancel = (prompt) => {
process.exit()
}
// exec异步封装成异步函数
function execAsync(command) {
return new Promise((resolve, reject) => {
exec(command, (error, stdout, stderr) => {
if (error) {
reject(error)
} else {
resolve(stdout.trim())
}
})
})
}
// 设置ANSI转义码来添加颜色 colorCode: 91 红色 92 绿色
const colorizeText = (text, colorCode) => {
return `\x1b[${colorCode}m${text}\x1b[0m`
}
let curBranch = ''
// 最新tag名称
let latestTagName = ''
// 最新tag附注
let latestTagDesc = ''
// 获取当前本地分支名字
const getCurrentBranch = async () => {
try {
curBranch = await execAsync('git rev-parse --abbrev-ref HEAD')
console.log(colorizeText(`!请注意当前git分支: ${curBranch}`, 91))
// if (curBranch !== 'dev') {
// console.error(colorizeText(`请切换到 dev 分支进行上传操作!`, 91))
// process.exit(1)
// }
} catch (error) {
console.error(colorizeText(`获取分支名字失败!${error}`, 91))
process.exit(1)
}
}
// 获取最新tag名称
const getTagName = async () => {
try {
/*
--sort=-creatordate: 这个选项指定了按照标签的创建日期进行排序。-creatordate 表示按照创建日期的逆序(从最新到最旧)排序。
|: 这是管道符号,用于将一个命令的输出传递给另一个命令作为输入。
head -1: 这个命令用于从输入中获取第一行。head 命令默认会显示输入的前 10 行,但是通过 -1 选项,它只会显示第一行。
*/
latestTagName = await execAsync('git tag --sort=-creatordate | head -1')
} catch (error) {
console.error(colorizeText(`获取最新tag名称失败!${error}`, 91))
process.exit(1)
}
}
// 获取最新tag附注
const getTagDesc = async () => {
try {
/*
git tag -n --format='%(contents)': 这个命令用于列出所有标签的附注消息
并且使用 --format='%(contents)' 选项指定只显示标签的附注消息而不包含标签名。
*/
latestTagDesc = await execAsync(
`git tag ${latestTagName} -n --format="%(contents)" | head -1`
)
} catch (error) {
console.error(colorizeText(`获取最新tag附注失败!${error}`, 91))
process.exit(1)
}
}
// 获取tag信息
const getTagInfo = async () => {
try {
// git fetch --tags 从远程仓库中获取所有的标签,并将它们拉取到你的本地仓库。
await execAsync('git fetch --tags')
// 获取最新tag名称
await getTagName()
await getTagDesc()
} catch (error) {
console.error(colorizeText(`获取tag信息失败!${error}`, 91))
process.exit(1)
}
}
// 填写版本号
const askVersion = async function () {
console.log(
`${colorizeText('*', 32)} 当前正式环境线上版本号: ${colorizeText(
latestTagName,
36
)}:\n `
)
try {
// 这里注意不能提前在函数外部定义,
// initial: latestTagName不会取到变量,只会取初始值
let versionQuestions = [
{
type: 'select',
name: 'version',
message: '请选择当前要修改版本为?',
hint: '使用 ⇧ / ⇩ 进行选择,回车键确认',
choices: [
{
title: '不做修改',
description: '版本号不变',
value: 'old',
},
{
title: '修订补丁',
description: 'X.Y.Z -> Z+1',
value: 'z',
},
{
title: '特性更新',
description: 'X.Y.Z -> Y+1',
value: 'y',
},
{
title: '版本升级',
description: 'X.Y.Z -> X+1',
value: 'x',
},
{
title: '自行填写',
value: 'new',
},
],
initial: 0,
},
// 如果是自行填写
{
type: (prev) => (prev === 'new' ? 'text' : null),
name: 'version',
message: '请输入当前版本',
initial: latestTagName,
validate: (value) => {
const pass = value.trim().match(/^[\dA-Za-z.]+$/)
if (pass) {
return true
} else {
return `版本号仅限字母、数字和小数点!`
}
},
},
]
const res = await prompts(versionQuestions, { onCancel })
let newVersion = ''
let parts = latestTagName.split('.')
// 判断是否符合版本更新特性
const updateArr = ['x', 'y', 'z']
if (updateArr.includes(res.version)) {
switch (res.version) {
case 'z':
parts[2] = parseInt(parts[2]) + 1
newVersion = parts.join('.')
break
case 'y':
parts[1] = parseInt(parts[1]) + 1
newVersion = parts.join('.')
break
case 'x':
parts[0] = parseInt(parts[0]) + 1
newVersion = parts.join('.')
break
}
} else if (res.version === 'old') {
// 不做修改
newVersion = latestTagName
} else {
newVersion = res.version
}
return newVersion
} catch (e) {
process.exit(1)
}
}
// 填写描述
const askDesc = async function () {
let descQuestions = [
{
type: 'text',
name: 'desc',
message: '请输入项目备注:(tab键回显)',
initial: latestTagDesc,
},
]
const res = await prompts(descQuestions, { onCancel })
return res.desc
}
//
const askQuestion = async function () {
try {
const version = await askVersion()
const desc = await askDesc()
// 修改package对应的内容 !(本地备份)
packageJSON.wxconfig.tagName = version
packageJSON.wxconfig.tagDesc = desc
// 修改更新时间 防止默认回车 git提交时会识别为未更改内容
packageJSON.wxconfig.updateTime = Date.now()
console.log('\n修改后版本号信息:', {
version: version,
desc: desc,
})
} catch (error) {
console.log(colorizeText(error, 91))
process.exit(1)
}
}
// 写入package.json
const writeFile = async () => {
try {
// JSON.stringify 方法将 JavaScript 对象转换为 JSON 字符串,第二个参数 null 表示没有替换函数,第三个参数 2 表示缩进空格数为 2,用于格式化输出的 JSON 字符串。
// 异步写回到文件中
await fs.writeFile('package.json', JSON.stringify(packageJSON, null, 2))
console.log(colorizeText('\n修改package.json文件成功!\n', 92))
} catch (err) {
console.error(colorizeText(`fs.writeFile err:${err}`, 91))
process.exit(1)
}
}
// 提交代码到当前git分支
const gitSave = async () => {
try {
await execAsync(`git add .`)
await execAsync(
`git commit -m '正式环境修改version为:${packageJSON.wxconfig.tagName};desc:${packageJSON.wxconfig.tagDesc}'`
)
// 前面限制了dev分支,这时候就无需一定要推送到dev
await execAsync(`git push`)
console.log(colorizeText(`\n成功推送到 ${curBranch} 分支!\n`, 92))
} catch (error) {
console.error(colorizeText(`推送git失败!${error.message}`, 91))
process.exit(1)
}
}
// 进行打tag
const gitTag = async () => {
try {
// 本地进行打一个 附注tag ,注意,需要强制更新tag,防止本地已有相同tag会报错
await execAsync(
`git tag -a ${packageJSON.wxconfig.tagName} -m "${packageJSON.wxconfig.tagDesc}" --force`
)
// 并上传到远程仓库
await execAsync(`git push origin ${packageJSON.wxconfig.tagName} --force`)
console.log(
colorizeText(
`\n成功推送 ${packageJSON.wxconfig.tagName} tag标签到远程\n`,
92
)
)
} catch (error) {
console.error(colorizeText(`git tag失败!${error.message}`, 91))
process.exit(1)
}
}
//删除指定tag
const deleteTag = async () => {
try {
// 删除时间最久的一条tag,防止tag分支过多(可选)
// 根据时间正序获取最久远的一条tag名称
let lastTagName = await execAsync(`git tag --sort=creatordate | head -1`)
// 删除本地一条tag
await execAsync(`git tag -d ${lastTagName}`)
// 删除远程一条tag
await execAsync(`git push origin --delete ${lastTagName}`)
console.log(colorizeText(`\n删除 ${lastTagName} tag成功\n`, 92))
} catch (error) {
console.error(colorizeText(`删除tag失败!${error.message}`, 91))
process.exit(1)
}
}
// 机器人自动化上传
const autoUpload = async () => {
const project = new ci.Project({
appid: '填写你的appid',
type: 'miniProgram',
// projectPath:项目的路径,即 project.config.json 所在的目录
projectPath: path.join(__dirname, '../'),
// privateKeyPath:私钥的路径
privateKeyPath: path.join(__dirname, './weapp.key'),
// 指定需要排除的规则
ignores: ['node_modules/**/*', 'package-lock.json'],
})
console.log(colorizeText(`\n正在上传...\n`, 92))
const uploadResult = await ci.upload({
project,
// 自定义版本号
version: packageJSON.wxconfig.tagName,
desc: packageJSON.wxconfig.tagDesc,
setting: {
es6: true,
es7: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
minify: true,
// autoPrefixWXSS 对应小程序开发者工具的 "样式自动补全"
// autoPrefixWXSS: true,
},
onProgressUpdate: console.log,
// 指定使用哪一个 ci 机器人,可选值:1 ~ 30
robot: 8,
// 指定本地编译过程中开启的线程数
// threads: 10,
})
console.log(uploadResult)
console.log(colorizeText('tag版本上传完成!', 92)) // 亮绿色代码是 '92'
console.log(colorizeText('版本号:' + packageJSON.wxconfig.tagName, 92))
console.log(colorizeText('描述:' + packageJSON.wxconfig.tagDesc, 92))
}
;(async () => {
// 获取当前本地分支名字
await getCurrentBranch()
// 获取最新tag信息
await getTagInfo()
// 交互修改
await askQuestion()
// 异步写入package.json文件
await writeFile()
// 保存并提交git
await gitSave()
// git tag操作
await gitTag()
// 删除指定tag,可选
// await deleteTag()
// 机器人自动化上传
await autoUpload()
})()
在 package.json 文件配置npm命令,并运行 npm run tag 即可上传符合发布版的版本,再到小程序管理进行发布升级