基于 luckymiaow/uni-builder 的“傻瓜式”极速打包脚本
在上一篇分享中,我们解决了 uni-app 本地生成 APK 的基础问题,但实际落地中发现:HBuilderX 手动操作(打开工程、导出资源、发布打包等)流程割裂,不仅容易出错,还会导致团队协作时操作标准不一致。
为此,我们基于 luckymiaow/uni-builder 镜像做了进一步优化:将复杂的安卓构建环境封装到 Docker 容器中,再通过 Node 脚本串联“自动生成打包资源 + 容器化构建”全流程。只需提前配置环境变量并登录 HBuilderX,一行命令即可完成出包,同时通过本机缓存挂载大幅提升构建速度。
核心优势:一句话完成全流程打包
- 执行方式:项目根目录运行
npm run gen:app(或直接执行node builder/docker-build.js) - 脚本入口:package.json 中配置的 gen:app 脚本
- 自动化流程:脚本自动完成「生成打包资源 → 复制到 Android 工程 → 触发 Docker 构建」全链路
- 速度优化:挂载本机
.gradle和android-sdk缓存到容器,规避重复下载依赖,显著提升构建效率(具体实现见脚本中 Docker 命令拼装逻辑) - 前置条件:配置好 env 环境变量,HBuilderX 已登录并绑定对应 uni-app 账号
- 无手动操作:无需打开 HBuilderX、无需手动导出资源,脚本内置 help 检查 → open → publish → 可选 quit 全自动化逻辑
前置准备
- 环境变量配置:在
env/.env或env/.env.local中配置以下变量:- VITE_UNI_APPID:uni-app 应用ID
- VITE_UNI_PROJECT:项目名称(对应项目目录名称,并且要在hb中打开过)
- VITE_UNI_CLI:HBuilderX 的 cli.exe 所在路径
- HBuilderX 配置:提前登录并绑定目标 uni-app 账号,确保账号有权限发布对应应用
- 缓存挂载(推荐):将本机
.gradle和android-sdk目录挂载到容器,首次构建后后续速度提升 80%+
常见问题与排查方案
| 问题现象 | 排查方向 |
|---|---|
| 环境变量未配置 | 检查 env 目录下是否存在 .env/.env.local,确认三项核心变量均已配置;脚本会在缺失时明确退出并提示 |
| publish 操作失败 | 确认 HBuilderX 已登录,且登录账号绑定了对应 uni-app 应用ID的开发者权限 |
| help 无响应/ open 重试失败 | 检查 VITE_UNI_CLI 路径是否正确,需指向包含 cli.exe 的 HBuilderX 安装目录 |
| 发布前报错退出 | 确认项目根目录存在 src 目录,源码结构符合 uni-app 规范 |
| Docker 报错/权限问题 | 1. 确认 Docker Desktop 已运行;2. 确认 luckymiaow/uni-builder 镜像可正常拉取;3. Windows 环境下路径与盘符需用双引号包裹;4. 优先挂载缓存目录提升速度 |
完整脚本(docker-build.js)
#!/usr/bin/env node
/* eslint-disable @typescript-eslint/no-var-requires */
const os = require('node:os')
const { execSync } = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')
const dotenv = require('dotenv')
// ---------- 打包耗时统计 ----------
const __startTime = Date.now()
console.log('⏱️ 打包流程开始...')
console.log('⚠️ 确保Hbuilder登录账号已绑定对应的uni-app账号,否则可能打包失败')
// ---------- 读取命令行参数 ----------
let projectPath = process.argv[2]
const buildType = process.argv[3] || 'appResource' // appResource 生成APK资源 | wgt 生成wgt包
// 封装读取 env/.env(.local) 环境变量的函数
function getEnvVars() {
const envDir = path.resolve(process.cwd(), 'env')
const envLocalPath = path.join(envDir, '.env.local')
const envPath = path.join(envDir, '.env')
let envVars = {}
let loaded = false
// 优先读取 .env,再读取 .env.local(local 覆盖默认配置)
if (fs.existsSync(envPath)) {
const result = dotenv.config({ path: envPath })
if (result.error) {
console.error('❌ 解析 .env 文件失败:', result.error)
process.exit(1)
}
envVars = { ...result.parsed }
loaded = true
}
if (fs.existsSync(envLocalPath)) {
const resultLocal = dotenv.config({ path: envLocalPath })
if (resultLocal.error) {
console.error('❌ 解析 .env.local 文件失败:', resultLocal.error)
process.exit(1)
}
envVars = { ...envVars, ...resultLocal.parsed }
loaded = true
}
if (!loaded) {
console.error('❌ 未找到 .env 或 .env.local 文件,请确保项目根目录有 env/.env 或 env/.env.local')
process.exit(1)
}
return envVars
}
// ---------- 自动补全项目路径(未传参时从环境变量读取) ----------
const envVars = getEnvVars()
if (!projectPath) {
console.log('⚠️ 未传入路径参数,尝试从 env 配置读取 VITE_UNI_APPID 自动生成路径...')
const appId = envVars.VITE_UNI_APPID
if (!appId) {
console.error('❌ 未在 .env/.env.local 中配置 VITE_UNI_APPID')
process.exit(1)
}
console.log(`✅ 读取到 VITE_UNI_APPID: ${appId}`)
// 根据打包类型设置默认输出路径
projectPath = buildType === 'wgt'
? `./unpackage/release`
: `./unpackage/resources/${appId}`
}
// ---------- 自动生成打包资源(配置了 CLI 路径时) ----------
const uniCliPath = envVars.VITE_UNI_CLI
if (!uniCliPath) {
console.warn('⚠️ 未配置 VITE_UNI_CLI(HBuilderX CLI 路径),请确保已手动生成打包资源')
} else {
console.log(`✅ 读取到 VITE_UNI_CLI: ${uniCliPath},开始自动生成打包资源`)
let needQuit = false
try {
// 校验源码目录
const srcDir = path.resolve(process.cwd(), 'src')
if (!fs.existsSync(srcDir)) throw new Error('src 目录不存在,无法生成打包资源')
// 拼接 CLI 可执行文件路径
const cliBin = `${uniCliPath}/cli.exe`
// 1. 检查 CLI 可用性
let helpOutput = ''
try {
helpOutput = execSync(`"${cliBin}" help`, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
})
} catch (e) {
helpOutput = ''
}
// 2. CLI 无响应时尝试启动 HBuilderX
if (!helpOutput?.trim()) {
console.log('ℹ️ HBuilderX CLI 无响应,尝试自动启动 HBuilderX...')
const { spawnSync } = require('node:child_process')
const openResult = spawnSync(`"${cliBin}"`, ['open'], { stdio: 'inherit', shell: true })
if (openResult.error || openResult.status !== 0) throw new Error('CLI open 命令执行失败')
// 重试检查 CLI 可用性(最多5次,间隔1秒)
let openOk = false
for (let i = 0; i < 5; i++) {
try {
helpOutput = execSync(`"${cliBin}" help`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] })
if (helpOutput?.trim()) {
openOk = true
break
}
} catch (e) { /* 忽略重试错误 */ }
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 1000)
}
if (!openOk) throw new Error('启动 HBuilderX 后 CLI 仍无响应')
needQuit = true // 标记后续需要退出 HBuilderX
}
// 3. 校验项目名称配置
const pkgName = envVars.VITE_UNI_PROJECT
if (!pkgName) {
console.error('❌ 未配置 VITE_UNI_PROJECT(项目名称)')
process.exit(1)
}
// 4. 执行打包命令
const cliCmd = `"${cliBin}" publish --platform APP --type ${buildType} --project ${pkgName}`
console.log(`🚦 执行打包命令: cd ${srcDir} && ${cliCmd}`)
execSync(`cd /d "${srcDir}" && ${cliCmd}`, { stdio: 'inherit' })
console.log(`✅ 打包资源生成完成 (类型: ${buildType})`)
// 5. 自动退出 HBuilderX(如果是脚本启动的)
if (needQuit) {
console.log('🛑 打包完成,尝试关闭 HBuilderX...')
try {
execSync(`"${cliBin}" app quit`, { stdio: 'inherit' })
} catch (e) {
console.warn('⚠️ 自动关闭 HBuilderX 失败,可手动关闭')
}
}
} catch (e) {
console.error('❌ 生成打包资源失败:', e.message)
process.exit(1)
}
}
// ---------- 路径规范化与目录创建 ----------
projectPath = path.resolve(process.cwd(), projectPath)
console.log(`📁 目标项目路径: ${projectPath}`)
if (!fs.existsSync(projectPath)) {
console.log(`📁 路径不存在,自动创建: ${projectPath}`)
fs.mkdirSync(projectPath, { recursive: true })
}
// ---------- 分类型处理打包流程 ----------
if (buildType === 'wgt') {
console.log('✅ wgt 包生成完成')
console.log(`📦 输出路径: ${projectPath}`)
} else {
// 复制资源到 Android 工程目录
const targetDir = path.resolve('./builder/override/simpleDemo/src/main/assets/apps', path.basename(projectPath))
// 清理旧资源
if (fs.existsSync(targetDir)) {
console.log(`🗑️ 清理旧资源目录: ${targetDir}`)
fs.rmSync(targetDir, { recursive: true, force: true })
}
// 复制新资源
console.log(`📦 复制打包资源: ${projectPath} → ${targetDir}`)
fs.cpSync(projectPath, targetDir, { recursive: true })
// 执行 Docker 构建
const absPath = path.resolve(process.cwd(), 'builder')
const gradleCache = path.join(os.homedir(), '.gradle')
const sdkCache = path.join(os.homedir(), 'android-sdk')
const dockerCmd = `docker run --rm -v "${gradleCache}:/root/.gradle" -v "${sdkCache}:/opt/sdk-cache" -v "${absPath}:/workspace" luckymiaow/uni-builder:latest`
console.log(`🚀 执行 Docker 构建命令:\n${dockerCmd}`)
execSync(dockerCmd, { stdio: 'inherit' })
}
// ---------- 输出打包耗时 ----------
const __endTime = Date.now()
const __cost = (__endTime - __startTime) / 1000
console.log(`⏱️ 打包流程全部完成,总耗时: ${__cost.toFixed(2)} 秒`)