20公里的徒步-真难
群里的伙伴发起了一场天目山20公里徒步的活动,想着14公里都轻松拿捏了,思考了30秒后,就借着春风带着老婆孩子就出发了。一开始溪流清澈见底,小桥流水没有人家;青山郁郁葱葱,枯藤老树没有乌鸦,微风习习,鸟语花香,好不惬意。大有我看青山多妩媚,料青山见我应如是的舒坦,但是,但是没多一会儿画风突变了,爬过一座山还有数不尽的山,关键还一山更比一山高。响午温度升高,加之水资源极度匮乏(因为没有人家,划重点: 徒步一定要多带水),小宝先哭为敬了。好在睡意过去后,又坚强的跑了起来,一直用他的格言激励自己:放弃很多简单,坚持很难,我要坚持。大宝一直在在前面跟着大队伍,想想也是克服了极大的困难,他一直在山顶殷切的期盼着我们,当我们出现视野里时,又高兴的喊着爸爸,妈妈,小宝加油。或许大宝还是蛮优秀的,只是有时对大宝可能过于严厉了些。最后,大家都是笑着走过了最难的路。
走到16公里的地方,天已经黑了,已经到大路了,是不是20公里也不重要了,大家开心的找了家饭店,酣畅淋漓的吃喝了一顿,途中的跌倒与艰难全都成了豪爽的谈资,第二天大家都还可以自豪的说全身酸痛不已。
流量来了-心动了
领略了天目山的秀丽风景,回归正题。书接上文,之前捣鼓了一个小程序,没有想到日活居然过1000了,日新增200+,活跃用户次日留存超40%...
看着这些数据,陷入了沉思,思绪竟然来到了明朝末年(估计最近读《明朝那些事儿》魔怔了吧),农民起义纷争的年代,自己化身高迎祥、李自成、张献忠,手握数万雄兵,但不知所措...思绪一阵乱飞后得到这样一个结论:一个小程序1000,100个小程序就是10万(10万日活广告费真是不得了)- 构建小程序矩阵,构建100个程序,小程序就是雄兵,去攻城略地。
这事儿只有开头简单
有了目标,一口气又注册了5个小程序,备案,各种配置,上传,提交审核,发布...一套动作下来,虽是幸苦,总算是5个小程序都上架了,但是心中总有点不得劲儿的感觉,又说不出是哪里出了问题。还没等回过劲儿,发现程序有bug, 又吭哧吭哧一个个修改,上传,提交审核,发布...这会儿明白问题在哪里了:机械重复。光明白还没用,因为又有bug了,又是全套流程要做完。更多严重的问题是:这个过程又中注册了5个新小程序...应了那句老话:万事开头难,开头后更难。看着10个小程序要机械的重复发布,我没有崩溃,也没有去重复了,去捣鼓自动化了,解放双手才是正确的路。虽然只是解决了代码上传的问题,已是一个巨大的进步。
提前在 key目录下添加小程序代码上传密钥文件格式 private.wx0d8d56e152eb16xx.key
const
const fileExists = require('file-exists');
const del = require('del');
const child_process = require('child_process');
const ci = require('miniprogram-ci');
const gulp = require('gulp');
const less = require('gulp-less');
const uglify = require('gulp-uglify');
const cleanCSS = require('gulp-clean-css');
const rename = require('gulp-rename');
const gulpif = require('gulp-if');
const replace = require('gulp-replace');
const alias = require('gulp-path-alias');
const autoprefixer = require('gulp-autoprefixer');
const pkg = require('./package.json');
let projectConfig = require('./project.config.json');
const buildPath = path.join(__dirname, 'dist/');
const argv = require('minimist')(process.argv.slice(1));
const appId = argv["appId"];
if (appId){
console.log('set appId = ',appId);
projectConfig['appid'] = appId;
}
const env = process.env.NODE_ENV
console.log("evn=", env)
const isPro = env === 'production';
console.log("isPro=", isPro)
const branchName = child_process.execSync('git symbolic-ref --short HEAD', {
encoding: 'utf8',
});
const paths = {
styles: {
src: ['src/**/*.less'],
dest: buildPath,
},
images: {
src: 'src/images/**/*.{png,jpg,jpeg,svg,gif}',
dest: buildPath,
},
scripts: {
src: 'src/**/*.js',
dest: buildPath,
},
copy: {
src: [
'src/**',
'!src/**/*.less',
'!src/**/*.js',
'package.json',
],
dest: buildPath,
},
};
// 删除构建
function clean() {
return del([buildPath]);
}
function log() {
const data = Array.prototype.slice.call(arguments);
console.log(data);
}
// 任务处理函数
function styles() {
return gulp
.src(paths.styles.src, { base: 'src' })
.pipe(
alias({
paths: {
'@': path.resolve(__dirname, './src/'),
},
})
)
.pipe(less())
.pipe(autoprefixer())
.pipe(gulpif(isPro, cleanCSS()))
.pipe(rename((path) => (path.extname = '.wxss')))
.pipe(gulp.dest(paths.styles.dest));
}
function scripts() {
return (
gulp
.src(paths.scripts.src, { base: 'src' })
.pipe(
alias({
paths: {
'@': path.resolve(__dirname, './src/'), // src 目录
},
})
)
// .pipe(babel({ presets: ['@babel/env'], 'plugins': [] }))
.pipe(replace('%ENV%', process.env.NODE_ENV)) // 环境变量静态替换
.pipe(replace('%VERSION%', pkg.version))
.pipe(gulpif(isPro, uglify()))
.pipe(gulp.dest(paths.scripts.dest))
);
}
// 不需要处理的文件直接复制过去
function copy() {
return gulp
.src(paths.copy.src)
.pipe(gulp.dest(paths.copy.dest));
}
function watchFiles() {
const w1 = gulp.watch(paths.styles.src, styles).on('unlink', function (file) {
log(file + ' is deleted');
const filePath = file.replace(/src\\/, 'dist\\');
del([filePath]);
});
const w2 = gulp
.watch(paths.scripts.src, scripts)
.on('unlink', function (file) {
log(file + ' is deleted');
const filePath = file.replace(/src\\/, 'dist\\');
del([filePath]);
});
const w3 = gulp.watch(paths.copy.src, copy).on('unlink', function (file) {
log(file + ' is deleted');
const filePath = file.replace(/src\\/, 'dist\\');
del([filePath]);
});
return Promise.all([w1, w2, w3]);
}
/**
* 小程序ci相关函数
*/
let project = {};
const keyFile = fileExists.sync(`./key/private.${appId}.key`);
if (keyFile) {
project = new ci.Project({
appid: appId,
type: 'miniProgram',
projectPath: './dist',
privateKeyPath: `./key/private.${appId}.key`,
});
}
async function npmBuild() {
await ci.packNpmManually({
packageJsonPath: './package.json',
miniprogramNpmDistDir: './src/',
});
}
const envLabels = {
'production': '正式环境',
'development': '测试环境',
'pre': '预发环境',
};
// 机器人代号,有效范围[1-30]
const robotMap = {
'development': 1,
'production': 2,
'pre': 3,
}
async function mpUpload() {
log('mpUpload appid',appId);
if (!appId) {
console.log('\x1b[35m%s\x1b[0m', `
════════════════════════════════════════════════════════════════════════
⚡【${envLabels[env]}】小程序打包失败,请先执行 export APPID=你的appid 命令,设置appid
════════════════════════════════════════════════════════════════════════
`);
return false;
}
projectConfig['appid'] = appId;
log('projectConfig appid',projectConfig.appid);
const uploadResult = await ci.upload({
project,
version: pkg.version,
desc: `【${envLabels[env]}】${pkg.description}`,
setting: {
es7: true,
es6: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
minify: true,
autoPrefixWXSS: true,
},
robot: robotMap[env],
onProgressUpdate: console.log,
});
console.log('[uploadResult:]', uploadResult);
console.log('\x1b[35m%s\x1b[0m', `
════════════════════════════════════════════════════════════════════════
🚀【${envLabels[env]}】小程序打包已完成,可以去发布了https://mp.weixin.qq.com/
════════════════════════════════════════════════════════════════════════
`);
}
async function preview() {
const previewResult = await ci.preview({
project,
desc: `【${envLabels[env]}】${pkg.description}`, // 此备注将显示在"小程序助手"开发版列表中
qrcodeFormat: 'image',
qrcodeOutputDest: './preview.jpg',
setting: {
es7: true,
es6: true,
minifyJS: true,
minifyWXML: true,
minifyWXSS: true,
minify: true,
autoPrefixWXSS: true,
},
robot: robotMap[env],
onProgressUpdate: console.log,
// pagePath: 'pages/index/index', // 预览页面
// searchQuery: 'a=1&b=2', // 预览参数 [注意!]这里的`&`字符在命令行中应写成转义字符`\&`
});
console.log('[previewResult:]', previewResult);
console.log('\x1b[35m%s\x1b[0m', `
════════════════════════════════════════════════════════════════════════
🚀【${envLabels[env] || '测试环境'}】小程序预览已完成,可以去小程序助手中查看了
════════════════════════════════════════════════════════════════════════
`);
}
exports.watch = watchFiles;
exports.preview = preview;
// ci 自动构建npm
exports.npm = npmBuild;
exports.upload = mpUpload;
exports.default = gulp.series(styles, scripts, copy, watchFiles);
exports.build = gulp.series(clean, styles, scripts, copy);
再写个python 处理批量的问题
import subprocess
# 指定的目录
directory = '/Users/jijunjian/wealth'
commandList = [
'npm run deploy:pro -- --appId=wx7f4984150494f817',
'npm run deploy:pro -- --appId=wx1e8e9dc2e337b821',
'npm run deploy:pro -- --appId=wx7493b6cfe63e360e',
'npm run deploy:pro -- --appId=wx7c7c8a0e9e242133',
'npm run deploy:pro -- --appId=wxfc6898107cb428a7',
'npm run deploy:pro -- --appId=wx4fc01c82126749bc',
'npm run deploy:pro -- --appId=wx842b1d5e54ddff47'
]
index = 0;
# 要执行的shell命令
for i in commandList:
# 使用subprocess.run来执行命令,cwd参数指定工作目录
result = subprocess.run(i, cwd=directory, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# 打印命令的输出和错误信息
print(result.stdout) # 命令的输出
print(result.stderr) # 命令的错误信息
index += 1
print("一共:%s" % index)
虽然没有完全解决重复的问题,10万日活还在远远的招手,小程序还得继续注册,但这事儿还在心里萌芽着。
日活上来了,重复还在继续
为了解决若干小程序界面一样的问题,可能会被下线,又风风火火的做了多个模板。维护变得愈发难了,难是发一次版本都会成为一次浩大的工程。如果更新20个程序,必须要登陆20次mp后台,选账号都会成为一个难点,见图可知。
苦不堪言时,终于想起了之前参加微信生态线下交流时,一个同学提到的服务商模式,之前觉得接入成本也挺高,就放下了,现在已是非常时期,抽出一个周末开始了摸索。
拨云见日,终觅良方
注册开放平台,创建第三方平台应用,绑定小程序,上传草稿箱,设置普通模板,提交审核,上线... 2天时间终于摸索得7788了。几乎所有操作都可以通过接口完成,比如设置域名,设置隐私,提交审核,甚至上线...有了接口就可以配置自动化了,直接使用apifox的编排能力,60分钟搞定配置。这一套下来,直接节省了90%的工作。
管理100小程序真不难了
有了上面的一套配置,配合模板库,对应不同的版本。发布变得非常轻松,根本不用登陆MP后台,轻松管理100个小程序,甚至可以说多多益善。这个过程大概经历了一个月,回头来看,也许正是困难让我们更强大。恰巧最近在读老舍先生的《骆驼祥子》,连在地府都可以当个好鬼儿的祥子,却没能够从苦难中强大起来,着实可惜了。最后来一张效果图,接口自动化提交审核,发布;统一的平台管理所有的小程序,管理100个小程序就是这么简单。有兴趣的朋友可以体验下
二维码 : 官方不让放二维码,只能放一个链接了。