目录
- 《构建小程序 - 插件、目录、开发者工具、配置》
- 《构建小程序 - 异常、通讯、技巧》
- 《构建小程序 - 框架、Gulpjs、Task》
- 《构建小程序 - Generator》
- 《构建小程序 - CI》
开发辅助
为了让开发者能够通过 Node
来控制小程序的上传、预览等功能,微信官方提供了两种开发辅助:开发者工具(命令行/HTTP)
和 miniprogram-ci
模块 | 命令行 | HTTP | miniprogram-ci |
---|---|---|---|
登录工具 | 支持 | 支持 | 不支持 |
是否登录工具 | 支持 | 支持 | 不支持 |
预览 | 支持 | 支持 | 支持 |
上传代码 | 支持 | 支持 | 支持 |
自动预览 | 支持 | 支持 | 不支持 |
构建 npm | 支持 | 支持 | 支持 |
清除缓存 | 支持 | 支持 | 不支持 |
启动工具 | 支持 | 支持 | 不支持 |
打开其他项目 | 支持 | 支持 | 不支持 |
关闭项目窗口 | 支持 | 支持 | 不支持 |
关闭工具 | 支持 | 支持 | 不支持 |
开发者工具(命令行/HTTP)
功能丰富,支持面广,什么都好,就是一点都不好用!
- 需要先开启
HTTP
服务端口; - 关闭项目、打开其他项目等功能,在
Windows
MacOS
MacOS ARM64
表现不一致; - 通过脚本改写
project.config.json
属性后,需要清除缓存,再调用预览等功能; Windows
下拼接cli
端口号
不同客户端可能安装路径不统一;
如果你想封装一个 package
提供给他人使用,不通平台的兼容性、安装路径等的初始化就能直接劝退大部分使用者,尤其是略懂前端的测试同事、完全不懂开发的产品和运营同事,会额外增加学习成本。
构建 NPM
小程序引入
npm packages
时,千万小心你的主包大小!
- 构建 npm 后的 package 都会被计算在主包内
- 构建 npm 时只会构建
dependencies
中的package
- 使用
npm i dayjs --save --only=production
来减少体积
{
"dependencies": {
"dayjs": "^1.9.7"
}
}
结合 packageJsonPath
miniprogramNpmDistDir
来自定义 miniprogram_npm
输出目录
{
"setting": {
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./src/"
}
]
}
}
node_modules
+ src/miniprogram_npm
目录结构示例如下:
src
┣ home
┣ miniprogram_npm
┃ ┗ dayjs
┣ other subPackage
┣ subPackage1
┣ _shared
┣ app.js
┣ app.json
┗ app.scss
MINIPROGRAM-CI
前置条件
- 下载上传秘钥
- 添加 (公网)IP 白名单
构建思路
- 通过脚本参数获取
env
; - 下载代码仓库至本地的
workspace
; - 切换至
env-branch
; - 安装依赖;
- 构建项目;
- 调用
miniprogram-ci preview
或miniprogram-ci upload
;
ci
┣ keys
┃ ┣ private.${APPID_DEV}.key
┃ ┣ private.${APPID_PROD}.key
┃ ┣ private.${APPID_SIT}.key
┃ ┗ private.${APPID_UAT}.key
┣ build.js
┣ constants.js
┣ context.js
┣ miniprogramCI.js
┣ preview.js
┣ upload.js
┗ utils.js
注入 env
通过不同的 command
注入四套环境的 env
{
"scripts": {
"start:dev" : "gulp --env=dev",
"start:sit" : "gulp --env=sit",
"start:uat" : "gulp --env=uat",
"start:prod": "gulp --env=prod",
- "build:dev" : "gulp build --env=dev --output=./build --ignoreLocal",
+ "build:dev": "node ./scripts/ci/build --env=dev",
- "build:sit" : "gulp build --env=sit --output=./build --ignoreLocal",
+ "build:sit": "node ./scripts/ci/build --env=sit",
- "build:uat" : "gulp build --env=uat --output=./build --ignoreLocal",
+ "build:uat": "node ./scripts/ci/build --env=uat",
- "build:prod": "gulp build --env=prod --output=./build --ignoreLocal"
+ "build:prod": "node ./scripts/ci/build --env=prod",
+ "preview:dev": "node ./scripts/ci/preview --env=dev",
+ "preview:sit": "node ./scripts/ci/preview --env=sit",
+ "preview:uat": "node ./scripts/ci/preview --env=uat",
+ "preview:prod": "node ./scripts/ci/preview --env=prod",
+ "upload:dev": "node ./scripts/ci/upload --env=dev",
+ "upload:sit": "node ./scripts/ci/upload --env=sit",
+ "upload:uat": "node ./scripts/ci/upload --env=uat",
+ "upload:prod": "node ./scripts/ci/upload --env=prod"
}
}
初始化 workspace
在脚本中,设计了三种 scripts
,它们的前置条件,都需要将 workspace
准备好:
下载仓库 => 切换 env-branch => 安装依赖 => 构建项目
build:env
=>build.js
=> 迁移zip
构建产物preview:env
=>preview.js
=> 调用miniprogramCI.preview
upload:env
=>upload.js
=> 调用miniprogramCI.upload
将 前置条件 其提取,单独封装成 context.js
模块,共享给三种脚本:
// 克隆 git 仓库至本地时,指定 REPO_DOWNLOAD_FOLDER 作为存储目录
const REPO_DOWNLOAD_FOLDER = ".mp-repos";
const ENV_BRANCH_MAPS = {
// 环境名 : git branch-name
dev: "dev",
sit: "test",
uat: "pre-release",
prod: "master",
};
module.exports = {
REPO_DOWNLOAD_FOLDER,
ENV_BRANCH_MAPS,
};
// 根据仓库地址解析仓库名称
// http://gitlab.example.com/mp-example-repo.git
// =>
// mp-example-repo
function getRepoName(repo) {
const arr = repo.split("/");
return arr[arr.length - 1].replace(".git", "");
}
# 可以通过各种方式获取到仓库地址
# 1. 直接在 process.cwd() 路径下调用 git remote -v
# 2. 读取 package.json 中的 repository.url
# 3. 或者像我这样,在 .npmrc 里面硬编码
repo=http://gitlab.example.com/mp-example-repo.git
const path = require("path");
const { homedir } = require("os");
const yargs = require("yargs/yargs");
const args = yargs(yargs.hideBin(process.argv)).argv;
const { REPO_DOWNLOAD_FOLDER, ENV_BRANCH_MAPS } = require("./constants");
const { getRepoName } = require("./utils");
// 解构出脚本接收到的环境参数 --env=sit => sit
const { env } = args;
// 根据映射关系,获取 sit 对应的分支 test
const branch = ENV_BRANCH_MAPS[env];
// 读取 .npmrc 中的 repo 值,仓库地址 => http://gitlab.example.com/mp-example-repo.git
const repo = process.env.npm_config_repo;
// 获取到仓库名称 http://gitlab.example.com/mp-example-repo.git => mp-example-repo
// mp-example-repo 是拼接最终 workspace 目录中的一环
const repoName = getRepoName(repo);
// 拼接目录结构 => ~/.mp-repos/mp-example-repo/test/
// dev => ~/.mp-repos/mp-example-repo/dev/
// sit => ~/.mp-repos/mp-example-repo/test/
// uat => ~/.mp-repos/mp-example-repo/pre-release/
// prod => ~/.mp-repos/mp-example-repo/master/
const repoPath = path.join(homedir(), REPO_DOWNLOAD_FOLDER, repoName, branch);
// 约定输出目录 ./build,它将作为 miniprogram-ci 所读取的工作目录
// ~/.mp-repos/mp-example-repo/test/build
const workspace = path.join(repoPath, `./build`);
// 克隆代码并切换环境分支
const cloneCommand = `git clone ${repo} ${repoPath} && cd ${repoPath} && git checkout ${branch}`;
// 安装依赖,为了让安装更快,使用 npm install --only=production
const installCommand = `cd ${repoPath} && npm install --only=production`;
// 构建命令,--output 与 workspace 相互对应
// 当然,这里也可以通过额外的 args 参数注入变成动态的
const buildCommand = `cd ${repoPath} && gulp build --env=${env} --output=./build --ignoreLocal`;
module.exports = {
env,
branch,
repo,
repoName,
repoPath,
workspace,
cloneCommand,
installCommand,
buildCommand,
};
构建步骤
新增一个 runExec()
用于异步的执行 command
并在终端输出日志
const runExec = (command, options) =>
new Promise((resolve, reject) => {
try {
const result = execSync(command, {
stdio: "inherit",
cwd: process.cwd(),
...options,
});
resolve(result);
} catch (error) {
reject(error);
}
});
规划出每次脚本运行所执行的步骤
- 清空
workspace
确保目录干净、代码依赖都是最新的; - 执行
cloneCommand
; - 执行
installCommand
; - 执行
buildCommand
; - 执行
zip
preview
upload
;
const clc = require("cli-color");
const fse = require("fs-extra");
const path = require("path");
const context = require("./context");
const { runExec, getBuildName } = require("./utils");
const { env, repoName, repoPath, cloneCommand, installCommand, buildCommand } =
context;
run();
async function run() {
try {
// 1. 清空 `workspace`
console.log(
`[${clc.green("→")}] [${clc.blue(
clc.bold(repoName)
)}] 清理工作空间: ${repoPath}`
);
await fse.emptyDir(repoPath);
// 2. 执行 `cloneCommand`
console.log(
`[${clc.green("→")}] [${clc.blue(clc.bold(repoName))}] 开始克隆仓库...`
);
console.log();
await runExec(cloneCommand);
// 3. 执行 `installCommand`
console.log(
`[${clc.green("→")}] [${clc.blue(clc.bold(repoName))}] 开始安装依赖...`
);
console.log();
await runExec(installCommand);
// 4. 执行 `buildCommand`
console.log(
`[${clc.green("→")}] [${clc.blue(
clc.bold(repoName)
)}] 开始构建应用: ${env}`
);
console.log();
await runExec(buildCommand);
console.log(
`[${clc.green("✓")}] [${clc.blue(clc.bold(repoName))}] 构建成功`
);
// 5. 执行 `zip` `preview` `upload`;
} catch (error) {
console.log(
`[${clc.red("✗")}] [${clc.blue(clc.bold(repoName))}] ${clc.red(
clc.bold("请检查网络设置或者应用配置")
)}`
);
console.log(error.toString().trim().split(/\r?\n/));
}
}
run() {
// 5. 执行 `zip`;
+ // 拷贝输出文件至 process.cwd()
+ const fileName = getBuildName(repoPath)
+ console.log(
+ `[${clc.green('→')}] [${clc.blue(
+ clc.bold(repoName)
+ )}] 开始拷贝文件: ${fileName}`
+ )
+ await fse.copy(
+ path.join(repoPath, fileName),
+ path.join(process.cwd(), fileName)
+ )
+ // 移除工作目录
+ await fse.emptyDir(repoPath)
+ console.log(
+ `[${clc.green('✓')}] [${clc.blue(clc.bold(repoName))}] 拷贝完成`
+ )
}
+ const CI = require('./miniprogramCI')
run() {
// 5. 执行 `preview`;
+ new CI(workspace).preview({ qrcodeFormat: 'terminal' })
}
+ const CI = require('./miniprogramCI')
run() {
// 5. 执行 `preview`;
+ new CI(workspace).upload()
}
miniprogramCI
在 cloneCommand
installCommand
buildCommand
结束后,便可得到 workspace
# 开发环境
~/.mp-repos/mp-example-repo/dev/build/**/*
# 测试环境
~/.mp-repos/mp-example-repo/test/build/**/*
# 预发布环境
~/.mp-repos/mp-example-repo/pre-release/build/**/*
# 生产环境
~/.mp-repos/mp-example-repo/master/build/**/*
~
┗ .mp-repos
┃ ┗ .mp-example-repo
┃ ┃ ┣ dev
┃ ┃ ┣ master
┃ ┃ ┣ pre-release
┃ ┃ ┗ test
const fse = require('fs-extra')
const path = require('path')
const clc = require('cli-color')
const { preview, Project, upload } = require('miniprogram-ci')
const Table = require('cli-table3')
const { getPackageName, getFormatFileSize } = require('./utils')
const logger = require('../lib/logger')
class MiniProgramCI {
constructor(workspace) {
this.workspace = workspace
// 加载配置而后初始化项目对象
// https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html#项目对象
if (this.loadProjectConfig(workspace)) {
this.project = new Project({
appid: this.appid,
type: this.compileType,
projectPath: this.workspace,
privateKeyPath: this.privateKeyPath
})
}
}
// 1. 加载 project.config.json 并初始化 appid compileType
// 2. 加载 package.json 并初始化 version
// 3. 获取 上传秘钥 路径
loadProjectConfig(workspace) {
const projectConfigPath = path.join(workspace, 'project.config.json')
const packagePath = path.join(workspace, '../package.json')
if (
fse.pathExistsSync(projectConfigPath) &&
fse.pathExistsSync(packagePath)
) {
try {
const {
setting,
appid,
compileType,
projectname
} = fse.readJSONSync(projectConfigPath)
const { version } = fse.readJSONSync(packagePath)
this.appid = appid
this.setting = setting
this.compileType = compileType
this.desc = decodeURIComponent(projectname)
this.version = version
const privateKeyPath = path.join(
workspace,
`../scripts/ci/keys/private.${appid}.key`
)
if (fse.pathExistsSync(privateKeyPath)) {
this.privateKeyPath = privateKeyPath
return true
} else {
console.log(
`[${clc.red('✗')}] ${clc.red(clc.bold('上传秘钥不存在'))}`
)
return false
}
} catch (error) {
console.log(`[${clc.red('✗')}] ${clc.red(clc.bold('读取文件失败'))}`)
return false
}
} else {
console.log(`[${clc.red('✗')}] ${clc.red(clc.bold('工程文件不存在'))}`)
return false
}
}
// 优化打印输出
// https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html#返回
printResult(result) {
const { subPackageInfo = [], pluginInfo = [], devPluginId = '无' } = result
const table = new Table({
head: ['时间', '版本号', '项目备注']
})
table.push([new Date().toLocaleString(), this.version, this.desc])
console.log(table.toString())
console.log('包信息')
const packageTable = new Table({
head: ['类型', '大小']
})
subPackageInfo.forEach(packageInfo => {
const formatSize = getFormatFileSize(packageInfo.size)
packageTable.push([
getPackageName(packageInfo.name),
formatSize.size + formatSize.measure
])
})
console.log(packageTable.toString())
if (pluginInfo && pluginInfo.length) {
console.log('插件信息')
const pluginTable = new Table({
head: ['appid', '版本', '大小', 'devPluginId']
})
pluginInfo.forEach(pluginInfo => {
const formatSize = getFormatFileSize(pluginInfo.size)
pluginTable.push([
pluginInfo.pluginProviderAppid,
pluginInfo.version,
formatSize.size + formatSize.measure,
devPluginId
])
})
console.log(pluginTable.toString())
}
}
relsoveQrPath(qrcodeFormat, qrcodeOutputDest) {
if (qrcodeFormat === 'base64' || qrcodeFormat === 'image') {
return path.join(this.workspace, qrcodeOutputDest || 'preview.png')
}
return ''
}
// ci 机器人编号
get robot() {
return Math.floor(Math.random() * 31)
}
// miniprogram-ci upload
// https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html#上传
async upload() {
if (this.project) {
try {
console.log(`[${clc.green('→')}] 开始上传...`)
const uploadResult = await upload({
project: this.project,
version: this.version,
desc: this.desc,
setting: this.setting,
onProgressUpdate() {},
robot: this.robot
})
console.log(`[${clc.green('✓')}] 上传成功`)
this.printResult(uploadResult)
} catch (error) {
logger.fatal(error)
}
}
}
// miniprogram-ci preview
// https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html#预览
async preview(opts = {}) {
const { qrcodeFormat = 'image', qrcodeDest } = opts
if (this.project) {
try {
console.log(`[${clc.green('→')}] 开始预览...`)
const previewResult = await preview({
project: this.project,
version: this.version,
desc: this.desc,
setting: this.setting,
qrcodeFormat,
qrcodeOutputDest: this.relsoveQrPath(qrcodeFormat, qrcodeDest),
onProgressUpdate() {},
robot: this.robot
})
console.log(`[${clc.green('✓')}] 预览成功`)
this.printResult(previewResult)
} catch (error) {
logger.fatal(error)
}
}
}
}
module.exports = MiniProgramCI