小程序之机器人CI_CD集成
什么是 CI_CD 集成
自微信小程序诞生以来,上传体验版/生成开发版这个事就离不开微信开发者工具, 那 CI/CD 的集成,就是一个从构建到发布以及通知,实现自动化的工具。 本文主要用官方提供的 miniprogram-ci 依赖包和一些 node 的模块来实现。
为什么要做 CI_CD 集成
做为一个前端开发,在开发微信小程序的时候,我们应该都遇到过这样的场景: 我们双手正在在键盘上飞舞,像写诗一样认真的编写每一行代码,脑海里想着解耦合、设计模式、抽象和分层的时候。 突然之间,产品经理轻轻拍了拍了你肩膀, 假装满怀歉意的跟你说:“我需要一个最新测试环境的体验二维码” 于是你端起了印着“和气生财”的马克杯,喝了口水,开始了一波操作。
- git stash
- git checkout branch
- npm build env
- 打开开发者工具
- 点击“预览”,生成二维码
- 点击发布,生成体验版二维码
- 发送给对方
搞定后,你活动了一下有些许酸疼的手指,切回分支恢复进度,脑海里努力地回忆着刚刚得思路。 这时,测试工程师突然又找你,他也想要一个测试环境的二维码,这时候你又看了看一眼你的马克杯,黯然神伤…
怎么实现 CI_CD
前置准备
- 做好环境区分
- 配置小程序代码上传白名单
- 准备脚本文件 upload.js
- 钉钉群机器人 webhook 地址
1、添加环境变量(环境区分)
- 打包时引入
VUE_APP_ENV
关键字 - 通过关键字引用不同环境的 api 域名
2、配置白名单
小程序后台:开发管理-->开发设置-->小程序代码上传 第一步先生成密钥,下载密钥文件至本地。然后配置 IP 白名单。


3、准备脚本文件 upload.js
-
新建 upload.js
-
安装 miniprogram-ci ,开始编写脚本代码:
npm i miniprogram-ci -D
const ci = require('miniprogram-ci')
const path = require('path')
const project = new ci.Project({
appid: appid,
type: 'miniProgram',
projectPath: path.join(__dirname, './dist/build/mp-weixin'),
privateKeyPath: path.join(__dirname, './private.' + appid + '.key'),
ignores: ['node_modules/**/*', '.vscode', '.hbuilderx'],
})
/** 上传 */
async function upload({ version = '0.0.0', desc = 'test', robot = 1 }) {
await ci.upload({
project,
version,
desc,
setting: {},
robot,
onProgressUpdate: console.log,
})
}
upload({ version, desc, robot })
- 新建 project.config.json

- 优化代码
const project_config = require('./project.config.json');
const child_process = require('child_process');
const request = require('request');
const util = require('util');
const exec = util.promisify(child_process.exec);
function message(content) {
const data = {
msgtype: 'text',
text: { content },
};
request({
url: 'webhook地址',
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(data),
});
};
/** 上传 */
async function upload({
env = 'dev',
name = '<your default mp name>',
env_desc = '测试环境'
...
}) {
message(`${name}小程序-${env_desc},正在部署`);
await exec(`npm run build:mp-weixin-${env}`, { cwd: './' });
DO something...
};
upload(project_config);
- 安装inquirer:
npm i inquirer -D
并再次优化代码
const project_config = require('./project.config.json');
const inquirer = require('inquirer');
const fs = require('fs');
const example = {
hema_appid: '<your appid>',
tcHema_appid: '<your another appid>',
applets: ['<your mp name>', '<your another mp name>'],
choices: ['测试环境-dev', '联调环境-local', '修复环境-fix', '预发环境-pre', '生产环境-prod', '演示环境-rep'],
robot_1: 1, // 机器人1号
robot_2: 2, // 机器人2号
robot_3: 3, // 机器人3号
version: '1.0.0',
};
const config = {};
function fs_rewrite_config() {
fs.writeFileSync('./project.config.json', JSON.stringify(config), err => {
if (err) {
console.log('自动写入project.config.json文件失败,请手动填写,并检查错误!');
}
});
}
async function upload({...}) {
....
// 上传完成
fs_rewrite_config()
};
async function update_config(user_info) {
const env = user_info.env.split('-')[1];
const env_desc = user_info.env.split('-')[0];
const appid = user_info.name.split('-')[1];
const name = user_info.name.split('-')[0];
const config = {
name,
env,
env_desc,
version: user_info.version,
desc: user_info.desc || env_desc,
appid,
robot: env === 'prod' ? example.robot_3 : env === 'rep' ? example.robot_2 : example.robot_1,
};
return config;
};
async function inquirer(config) {
return inquirer.prompt([
{
type: 'list',
name: 'name',
message: `请选择部署的小程序:`,
default: 0,
choices: example.applets,
filter(value) {
if (value === 'tcHema') {
return `${value}-${example.tcHema_appid}`;
}
return `${value}-${example.hema_appid}`;
},
},
{
type: 'list',
name: 'env',
message: `请选择部署环境:`,
default: 0,
choices: example.choices,
},
{
type: 'input',
name: 'version',
message: `设置上传的版本号(当前版本号:${config.version}):`,
filter(opts) {
if (opts === '') {
return config.version;
}
return opts;
},
},
{
type: 'input',
name: 'desc',
message: `请写一个简单的介绍来描述这个版本改动过:`,
},
]);
};
async init() {
const result_data = await inquirer(project_config);
// 更新配置信息
config = await update_config(result_data);
// 打包上传
await upload();
};
init();

- 最后换成 es6 写法,整体如下:
const project_config = require('./project.config.json')
const child_process = require('child_process')
const ci = require('miniprogram-ci')
const inquirer = require('inquirer')
const request = require('request')
const util = require('util')
const path = require('path')
const fs = require('fs')
const example = {
hema_appid: '<your appid>',
tcHema_appid: '<your another appid>',
applets: ['<your mp name>', '<your another mp name>'],
choices: [
'测试环境-dev',
'联调环境-local',
'修复环境-fix',
'预发环境-pre',
'生产环境-prod',
'演示环境-rep',
],
robot_1: 1, // 机器人1号
robot_2: 2, // 机器人2号
robot_3: 3, // 机器人3号
version: '1.0.0',
}
class appletCI {
exec = util.promisify(child_process.exec)
config = {}
async init() {
const result_data = await this.inquirer(project_config)
// 更新配置信息
this.config = await this.update_config(result_data)
// 打包上传
await this.upload()
}
async upload() {
const {
name = '<your mp name>',
appid = example.hema_appid,
env = 'dev',
env_desc = '测试环境',
robot = example.robot_1,
desc = '测试环境',
version = example.version,
} = this.config || {}
console.log(`${env_desc}正在打包`)
this.message(`${name}小程序-${env_desc},正在部署`)
await this.exec(`npm run build:mp-weixin-${env}`, { cwd: './' })
console.log(`${env_desc}正在部署`)
const project = new ci.Project({
appid,
type: 'miniProgram',
projectPath: path.join(__dirname, './dist/build/mp-weixin'),
privateKeyPath: path.join(__dirname, './private.' + appid + '.key'),
ignores: ['node_modules/**/*', '.vscode', '.hbuilderx'],
})
await ci
.upload({
project,
robot,
desc,
version,
onProgressUpdate: console.log,
})
.then(() => {
this.message(`${name}小程序-${env_desc},部署完成`)
console.log('部署完成')
// 重写配置文件
this.fs_rewrite_config()
})
.catch((error) => {
if (error.errCode == -1) {
this.message(`${name}小程序-${env_desc},部署完成`)
console.log('部署完成')
// 重写配置文件
this.fs_rewrite_config()
return
}
this.message(`${name}小程序-${env_desc},部署失败,原因为:${error}`)
console.log('部署失败', error)
process.exit(-1)
})
}
async inquirer(config) {
return inquirer.prompt([
{
type: 'list',
name: 'name',
message: `请选择部署的小程序:`,
default: 0,
choices: example.applets,
filter(value) {
if (value === 'tcHema') {
return `${value}-${example.tcHema_appid}`
}
return `${value}-${example.hema_appid}`
},
},
{
type: 'list',
name: 'env',
message: `请选择部署环境:`,
default: 0,
choices: example.choices,
},
{
type: 'input',
name: 'version',
message: `设置上传的版本号(当前版本号:${config.version}):`,
filter(opts) {
if (opts === '') {
return config.version
}
return opts
},
},
{
type: 'input',
name: 'desc',
message: `请写一个简单的介绍来描述这个版本改动过:`,
},
])
}
async update_config(user_info) {
const env = user_info.env.split('-')[1]
const env_desc = user_info.env.split('-')[0]
const appid = user_info.name.split('-')[1]
const name = user_info.name.split('-')[0]
const config = {
name,
env,
env_desc,
version: user_info.version,
desc: user_info.desc || env_desc,
appid,
robot:
env === 'prod'
? example.robot_3
: env === 'rep'
? example.robot_2
: example.robot_1,
}
return config
}
message(content) {
const data = {
msgtype: 'text',
text: { content },
}
request({
url: '<your webhook url>',
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify(data),
})
}
fs_rewrite_config() {
fs.writeFileSync(
'./project.config.json',
JSON.stringify(this.config),
(err) => {
if (err) {
console.log(
'自动写入project.config.json文件失败,请手动填写,并检查错误!'
)
}
}
)
}
}
const CI = new appletCI()
CI.init()
4、配置 webhook 地址
