前提是 HBuilderX 项目可以正常打包,且配置好了 Android Studio 项目了
在玩 uniapp 并且打包 Android app 的时候,每次都要:
- 先更改 HBuilderX
manifest.json里的版本号 - 生成
wgt和apk资源 - 复制生成好的
apk资源文件夹到 Android Studio 里 - 更改 Android Studio 的版本号
- 运行
build,构建出apk
繁琐,并且我经常忘记更改版本号😭,能不能需要点击一下,就能全部搞定?答案肯定是可以的。
能对文件系统做操作的,我第一个想到了 node,于是根据上面的步骤,一一对应的让 node 来帮我们做好,我们只需要执行:node index.js 就行。
前提所需的配置文件内容
module.exports = {
"title": "示例",
"debug": true, // 是否开启调试模式,开启后控制台会打印日志
"appid": "__UNI__xxxxx", // manifest.json 里的 appid
"version": { // 每次打包的版本,在这更改后,会同步到 manifest.json 和 Android Studio 里,就不需要再去更改其他地方的版本号了
"code": 1,
"name": "1.0"
},
"hbuilder": { // hbuilder 软件的安装路径
"cli": "S:\HBuilderX"
},
"as": { // 用来打包 apk 的 Android Studio 工程路径
"project": "S:\uniapp\demo"
}
}
// 构建成功后,会收到 apk 和 wgt 路径,可以将这俩玩意在这上传服务器覆盖
module.exports.success = function (apk, wgt) {
console.log('打包成功:', { apk, wgt })
}
第一步(先更改 HBuilderX manifest.json 里的版本号)
updateFileContent(path.resolve(baseDir, 'manifest.json'), [
{
value: /"versionName" : "(\d|.)+"/,
content: `"versionName" : "${env.version.name}"`
},
{
value: /"versionCode" : "\d+"/,
content: `"versionCode" : "${env.version.code}"`
}
])
baseDir = process.env.INIT_CWD; 是拿到运行 node 的命令行所在路径,这个通常就是项目所在的位置
第二步(生成 wgt 和 apk 资源)
const project = baseDir.split(sep).slice(-1)[0]
const startTime = Date.now()
await Promise.all([
// apk -> unpackage/resources/__UNI__90BD841/www
runNodeCommand(
'cli publish --platform APP --type appResource --project ' + project,
{ cwd: env.hbuilder.cli },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}),
// wgt -> unpackage\release\xxx.wgt, xxx 是项目名称
runNodeCommand(
`cli publish --platform APP --type wgt --project ${project} --name ${project}.wgt`,
{ cwd: env.hbuilder.cli },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
})
])
// 生成打包资源少于 8s 都认为失败
if ((Date.now() - startTime) < 8000) {
return log('构建打包资源失败,请检查 HBuilderX:\n1.是否启动\n2.能否正常打包', 'red')
}
第三步(复制生成好的 apk 资源文件夹到 Android Studio 里)
const asAppRes = path.resolve(env.as.project, 'app/src/main/assets/apps')
fs.rmdirSync(asAppRes, { recursive: true })
fs.mkdirSync(asAppRes)
log(`正在压缩 ${path.resolve(baseDir, 'unpackage/resources/' + env.appid)}...`)
const zipRes = await zip(path.resolve(baseDir, 'unpackage/resources/' + env.appid))
log(`压缩成功 ${zipRes}`, 'green')
const toAS = path.resolve(asAppRes, env.appid + '.zip')
log(`正在从 ${zipRes} 移动到 ${toAS}...`)
await mv(zipRes, toAS)
log('移动成功!', 'green')
fs.unlinkSync(zipRes)
unzip(toAS)
fs.unlinkSync(toAS)
第四步(更改 Android Studio 的版本号)
跟更改 manifest.json 的方式一样:
updateFileContent(path.resolve(env.as.project, 'app/build.gradle'), [
{
value: /versionCode \d+/,
content: `versionCode ${env.version.code}`
},
{
value: /versionName "(\d|.)+"/,
content: `versionName "${env.version.name}"`
}
])
第五步(运行 build,构建出 apk)
// 清除缓存,并构建 apk
await runNodeCommand(
'gradlew clean',
{ cwd: env.as.project },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}
)
await runNodeCommand(
'gradlew assembleRelease',
{ cwd: env.as.project },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}
)
到这一步就基本完成了。
完整源码
/**
* 自动部署
* https://www.modb.pro/db/41498
* https://blog.csdn.net/youcijibi/article/details/103674402
* @author zeng/704729872@qq.com
* @date 2022/3/28 8:39
*/
const fs = require('fs');
const path = require('path');
const childProcess = require('child_process');
let { log } = require("./utils");
const sep = path.sep;
const zlib = require("zlib");
const baseDir = process.env.INIT_CWD;
const AdmZip = require("adm-zip");
(async () => {
try {
// 配置文件放在项目根目录,并且取名 deploy.config.js
const env = require(path.resolve(baseDir, './deploy.config.js'))
console.log(`
/\--\ /\--\ /\--\ /\--\
\:\ \ /::\ \ /::| | /::\ \
\:\ \ /:/\:\ \ /:|:| | /:/\:\ \
\:\ \ /::\~\:\ \ /:/|:| |__ /:/ \:\ \
_______\:\__\ /:/\:\ \:\__\ /:/ |:| /\__\ /:/__/_\:\__\
\::::::::/__/ \:\~\:\ \/__/ \/__|:|/:/ / \:\ /\ \/__/
\:\~~\~~ \:\ \:\__\ |:/:/ / \:\ \:\__\
\:\ \ \:\ \/__/ |::/ / \:\/:/ /
\:\__\ \:\__\ /:/ / \::/ /
\/__/ \/__/ \/__/ \/__/
`)
!env.debug && (log = () => {})
// 修改 HBuilder 版本
updateFileContent(path.resolve(baseDir, 'manifest.json'), [
{
value: /"versionName" : "(\d|.)+"/,
content: `"versionName" : "${env.version.name}"`
},
{
value: /"versionCode" : "\d+"/,
content: `"versionCode" : "${env.version.code}"`
}
])
// 生成打包资源
const project = baseDir.split(sep).slice(-1)[0]
const startTime = Date.now()
await Promise.all([
// apk -> unpackage/resources/__UNI__90BD841/www
runNodeCommand(
'cli publish --platform APP --type appResource --project ' + project,
{ cwd: env.hbuilder.cli },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}),
// wgt -> unpackage\release\xxx.wgt, xxx 是项目名称
runNodeCommand(
`cli publish --platform APP --type wgt --project ${project} --name ${project}.wgt`,
{ cwd: env.hbuilder.cli },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
})
])
// 生成打包资源少于 10s 都认为失败
if ((Date.now() - startTime) < 10000) {
return log('构建打包资源失败,请检查 HBuilderX:\n1.是否启动\n2.能否正常打包', 'red')
}
// 将生成的 apk 资源移动到 as 工程里
const asAppRes = path.resolve(env.as.project, 'app/src/main/assets/apps')
fs.rmdirSync(asAppRes, { recursive: true })
fs.mkdirSync(asAppRes)
log(`正在压缩 ${path.resolve(baseDir, 'unpackage/resources/' + env.appid)}...`)
const zipRes = await zip(path.resolve(baseDir, 'unpackage/resources/' + env.appid))
log(`压缩成功 ${zipRes}`, 'green')
const toAS = path.resolve(asAppRes, env.appid + '.zip')
log(`正在从 ${zipRes} 移动到 ${toAS}...`)
await mv(zipRes, toAS)
log('移动成功!', 'green')
fs.unlinkSync(zipRes)
unzip(toAS)
fs.unlinkSync(toAS)
// 修改 as 的版本
updateFileContent(path.resolve(env.as.project, 'app/build.gradle'), [
{
value: /versionCode \d+/,
content: `versionCode ${env.version.code}`
},
{
value: /versionName "(\d|.)+"/,
content: `versionName "${env.version.name}"`
}
])
// 清除缓存,并构建 apk
await runNodeCommand(
'gradlew clean',
{ cwd: env.as.project },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}
)
await runNodeCommand(
'gradlew assembleRelease',
{ cwd: env.as.project },
{
stdout: data => log(data.replace('\n', '')),
stderr: data => log(data.replace('\n', ''), 'yellow')
}
)
// 打开 wgt 和 apk 的地址
if (env.open) {
childProcess.exec(`start "" "${path.resolve(baseDir, 'unpackage/release')}"`, {})
childProcess.exec(`start "" "${path.resolve(env.as.project, 'app/build/outputs/apk/release')}"`, {})
}
typeof env.success === 'function' && env.success(
path.resolve(env.as.project, 'app/build/outputs/apk/release/app-release.apk'),
path.resolve(baseDir, 'unpackage/release/' + project + '.wgt')
)
log(`\n\x1B[1m${env.title}\x1B[22m 打包顺利,祝帅气的你没有 BUG!`, 'green')
} catch (e) {
log('执行远程端命令失败:' + (e.message || e), 'red')
}
})()
function mv (form, to) {
return new Promise((resolve, reject) => {
const is = fs.createReadStream(form);
const os = fs.createWriteStream(to);
is.pipe(os);
is.on('end',resolve);
is.on('error', reject)
})
}
function updateFileContent (file, regs) {
let fileContent = fs.readFileSync(file, { encoding: 'utf-8' })
regs.forEach(reg => fileContent = fileContent.replace(reg.value, reg.content))
fs.writeFileSync(file, fileContent, { encoding: 'utf-8' })
}
/**
* 运行 node 命令
* @param {string} command 需要执行的命令
*
* @param {Object} opt 命令环境的一些配置,只列出可能常用的
* @param {string | URL} [opt.cwd] 子进程的当前工作目录
*
* @param {Object} [listener] 事件监听器
* @param {(data: string | any)} [listener.stdout] 命令行 普通 字符串的输出事件
* @param {(data: string | any)} [listener.stderr] 命令行 警告/错误 字符串的输出事件
// * @param {(code?: string) => {}} [listener.close] 命令行退出事件,接受一个 code 参数
*/
function runNodeCommand (command, opt = {}, listener = {}) {
return new Promise(resolve => {
const exec = childProcess.exec(command, opt)
listener.stdout && exec.stdout.on('data', listener.stdout)
listener.stderr && exec.stderr.on('data', listener.stderr)
exec.on('close', resolve)
})
}
/**
* 压缩本地文件
* @return {Promise<unknown>}
*/
function zip (path) {
const admzip = new AdmZip();
const paths = path.split(sep)
const fileName = paths.pop() + '.zip'
const directory = paths.join(sep)
admzip.addLocalFolder(directory)
const zipPath = directory + sep + fileName
return admzip.writeZipPromise(zipPath).then(() => zipPath);
}
// 解压
function unzip (file) {
const zip = new AdmZip(file)
zip.extractAllTo(path.resolve(file.split('.')[0], '../'), true)
}
尾
如果本文对你有帮助,那就赞善资助我吧👻