uniapp 项目自动构建 wgt 和 apk

2,940 阅读3分钟

前提是 HBuilderX 项目可以正常打包,且配置好了 Android Studio 项目了

在玩 uniapp 并且打包 Android app 的时候,每次都要:

  1. 先更改 HBuilderX manifest.json 里的版本号
  2. 生成 wgtapk 资源
  3. 复制生成好的 apk 资源文件夹到 Android Studio 里
  4. 更改 Android Studio 的版本号
  5. 运行 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 的命令行所在路径,这个通常就是项目所在的位置

第二步(生成 wgtapk 资源)

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)
}

如果本文对你有帮助,那就赞善资助我吧👻

7a3384924fdf45938efa23dc0594a56.jpg