前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >听说你还在用开发者工具手动上传小程序,快来试试 miniprogram-ci 提效摸鱼

听说你还在用开发者工具手动上传小程序,快来试试 miniprogram-ci 提效摸鱼

作者头像
若川
发布2022-06-03 11:05:36
1.9K0
发布2022-06-03 11:05:36
举报
文章被收录于专栏:若川视野若川视野

本文是 @NewName 小伙伴参加源码共读活动第30期(@tarojs/plugin-mini-ci 小程序上传代码 ci)的投稿。

原文链接:https://juejin.cn/post/7089819849257385997

此人非同寻常,我发布了多少期源码共读、他就基本写了多少期文章。

Part1学习准备工作

阅读相关学习资料:

代码语言:javascript
复制
微信小程序CI :https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html
taro CI: https://link.juejin.cn/?target=https%3A%2F%2Ftaro-docs.jd.com%2Ftaro%2Fdocs%2Fnext%2Fplugin-mini-ci%2F
coding自动构建微信小程序:https://help.coding.net/docs/ci/practice/quick/wechat-mini-program.html
小打卡小程序自动化构建:https://www.yuque.com/jinxuanzheng/gvhmm5/uy4qu9#mmmx7

clone源码:源码路径如下:

代码语言:javascript
复制
https://github.com/NervJS/taro/tree/next/packages/taro-plugin-mini-ci

我是把taro整个clone下来的。

Part2源码学习

1小程序CI的整体流程

首先看index.js:

代码语言:javascript
复制
module.exports = require('./dist/index.js').default
module.exports.default = module.exports

含义是引入dist文件夹下的index.js文件, 但是刚克隆下来的代码中并没有dist文件夹。很容易想到dist文件夹是打包后产生的,所以执行命令:

代码语言:javascript
复制
npm i
npm run build

注意是在taro/packages/taro-plugin-mini-ci目录下执行 install和build命令:

build之后可以看到有了dist文件夹:

对应目录下也生成了index.js文件,生成的js文件和原来的ts文件也没差太多,再加上最近再自学ts,就看index.ts吧(代码有删减):

代码语言:javascript
复制
import { IPluginContext } from '@tarojs/service'
import * as minimist from 'minimist'
import { CIOptions } from './BaseCi'
import WeappCI from './WeappCI'
import TTCI from './TTCI'
import AlipayCI from './AlipayCI'
import SwanCI from './SwanCI'

export { CIOptions } from './BaseCi'
export default (ctx: IPluginContext, pluginOpts: CIOptions) => {
  const onBuildDone = ctx.onBuildComplete || ctx.onBuildFinish

  ctx.addPluginOptsSchema((joi) => {
    return joi
      .object()
      .keys({
        /** 微信小程序上传配置 */
        weapp: joi.object({
          appid: joi.string().required(),
          projectPath: joi.string(),
          privateKeyPath: joi.string().required(),
          type: joi.string().valid('miniProgram', 'miniProgramPlugin', 'miniGame', 'miniGamePlugin'),
          ignores: joi.array().items(joi.string().required())
        }),
        /** 字节跳动小程序上传配置 */
        /** 阿里小程序上传配置 */
        /** 百度小程序上传配置 */
        swan: joi.object({
          token: joi.string().required(),
          minSwanVersion: joi.string()
        }),
        version: joi.string(),
        desc: joi.string()
      })
      .required()
  })

  onBuildDone(async () => {
    const args = minimist(process.argv.slice(2), {
      boolean: ['open', 'upload', 'preview']
    })
    const { printLog, processTypeEnum } = ctx.helper
    const platform = ctx.runOpts.options.platform
    let ci
    switch (platform) {
      case 'weapp':
        ci = new WeappCI(ctx, pluginOpts)
        break
      case 'tt':
        ci = new TTCI(ctx, pluginOpts)
        break
      case 'alipay':
      case 'iot':
        ci = new AlipayCI(ctx, pluginOpts)
        break
      case 'swan':
        ci = new SwanCI(ctx, pluginOpts)
        break
      default:
        break
    }
    if (!ci) {
      printLog(processTypeEnum.WARNING, `"@tarojs/plugin-mini-ci" 插件暂时不支持 "${platform}" 平台`)
      return
    }
    switch (true) {
      case args.open:
        ci.open()
        break
      case args.upload:
        ci.upload()
        break
      case args.preview:
        ci.preview()
        break
      default:
        break
    }
  })
}

代码的整体流程比较简单,判断平台,创建CI实例, 执行对应的CI。

可以在启动Node.js 程序时直接指定命令行参数,例如:

代码语言:javascript
复制
node index.js --beep=boop -t -z 12 -n5 foo bar

Node.js 程序启动后可以直接从process.argv中读取到参数列表:

代码语言:javascript
复制
console.log(process.argv);
// ['/bin/node', '/tmp/index.js', '--beep=boop', '-t', '-z', '12', '-n5', 'foo', 'bar']

从上述代码中可以看到,process.argv 变量是一个数组,数组前两项分别是 node 程序位置和js脚本位置,数组中随后的元素都是我们启动Node.js后的参数,这些参数以空格分隔成数组。而minimist 是一个专门用于处理Node.js启动参数的库,可以将 process.argv 中的参数列表转换成更加易于使用的格式:

代码语言:javascript
复制
const argv = require('minimist')(process.argv.slice(2));
console.dir(argv);
// { _: [ 'foo', 'bar' ], beep: 'boop', t: true, z: 12, n: 5 }

具体使用可以参考https://www.npmjs.com/package/minimist, 使用的时候接收参数和配置对象。

2CI抽象类:BaseCI

packages/taro-plugin-mini-ci/src/BaseCi.ts(代码有删减):

代码语言:javascript
复制
import { IPluginContext } from '@tarojs/service'
import * as path from 'path'

export type ProjectType = 'miniProgram' | 'miniGame' | 'miniProgramPlugin' | 'miniGamePlugin';

/** 微信小程序配置 */

/** 头条小程序配置 */

/** 支付宝系列小程序配置 */

/** 百度小程序配置 */

export interface CIOptions {
  /** 发布版本号,默认取 package.json 文件的 taroConfig.version 字段 */
  version: string;
  /** 版本发布描述, 默认取 package.json 文件的 taroConfig.desc 字段 */
  desc: string;
  /** 微信小程序CI配置, 官方文档地址:https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html */
  weapp?: WeappConfig;
  /** 头条小程序配置, 官方文档地址:https://microapp.bytedance.com/docs/zh-CN/mini-app/develop/developer-instrument/development-assistance/ide-order-instrument */
  tt?: TTConfig;
  /** 支付宝系列小程序配置,官方文档地址:https://opendocs.alipay.com/mini/miniu/api */
  alipay?: AlipayConfig;
  /** 百度小程序配置, 官方文档地址:https://smartprogram.baidu.com/docs/develop/devtools/commandtool/ */
  swan?: SwanConfig;
}

export default abstract class BaseCI {
  /** taro 插件上下文 */
  protected ctx: IPluginContext

  /** 传入的插件选项 */
  protected pluginOpts: CIOptions

  /** 当前要发布的版本号 */
  protected version: string

  /** 当前发布内容的描述 */
  protected desc: string

  constructor (ctx: IPluginContext, pluginOpts: CIOptions) {
    this.ctx = ctx
    this.pluginOpts = pluginOpts

    const { appPath } = ctx.paths
    const { fs } = ctx.helper
    const packageInfo = JSON.parse(
      fs.readFileSync(path.join(appPath, 'package.json'), {
        encoding: 'utf8'
      })
    )
    this.version = pluginOpts.version || packageInfo.taroConfig?.version || '1.0.0'
    this.desc = pluginOpts.desc || packageInfo.taroConfig?.desc || `CI构建自动构建于${new Date().toLocaleTimeString()}`

    this._init()
  }

  /** 初始化函数,会被构造函数调用 */
  protected abstract _init():void;

  /** 打开小程序项目 */
  abstract open();

  /** 上传小程序 */
  abstract upload();

  /** 预览小程序 */
  abstract preview();
}

在抽象类中定义了一些属性是protected的,意味着可以在本类以及子类中访问;在constructor中对属性进行了初始化,并调用初始化函数。然后是定义了一些CI操作的抽象方法。

3CI子类:AlipayCI

packages/taro-plugin-mini-ci/src/AlipayCI.ts

代码语言:javascript
复制
/* eslint-disable no-console */
import * as miniu from 'miniu'
import * as path from 'path'
import BaseCI from './BaseCi'
import generateQrCode from './QRCode'

/** 文档地址:https://opendocs.alipay.com/mini/miniu/api */
export default class AlipayCI extends BaseCI {
  protected _init (): void {
    if (this.pluginOpts.alipay == null) {
      throw new Error('请为"@tarojs/plugin-mini-ci"插件配置 "alipay" 选项')
    }
    const { appPath } = this.ctx.paths
    const { fs } = this.ctx.helper
    const { toolId, privateKeyPath: _privateKeyPath, proxy } = this.pluginOpts.alipay
    const privateKeyPath = path.isAbsolute(_privateKeyPath) ? _privateKeyPath : path.join(appPath, _privateKeyPath)
    if (!fs.pathExistsSync(privateKeyPath)) {
      throw new Error(`"alipay.privateKeyPath"选项配置的路径不存在,本次上传终止:${privateKeyPath}`)
    }

    miniu.setConfig({
      toolId,
      privateKey: fs.readFileSync(privateKeyPath, 'utf-8'),
      proxy
    })
  }

  open () {
    const { printLog, processTypeEnum } = this.ctx.helper
    printLog(processTypeEnum.WARNING, '阿里小程序不支持 "--open" 参数打开开发者工具')
  }

  async upload () {
    const { chalk, printLog, processTypeEnum } = this.ctx.helper
    const clientType = this.pluginOpts.alipay!.clientType || 'alipay'
    printLog(processTypeEnum.START, '上传代码到阿里小程序后台', clientType)
    // 上传结果CI库本身有提示,故此不做异常处理
    // TODO 阿里的CI库上传时不能设置“禁止压缩”,所以上传时被CI二次压缩代码,可能会造成报错,这块暂时无法处理; SDK上传不支持设置描述信息
    const result = await miniu.miniUpload({
      project: this.ctx.paths.outputPath,
      appId: this.pluginOpts.alipay!.appId,
      packageVersion: this.version,
      clientType,
      experience: true,
      onProgressUpdate (info) {
        const { status, data } = info
        console.log(status, data)
      }
    })
    if (result.packages) {
      const allPackageInfo = result.packages.find(pkg => pkg.type === 'FULL')
      const mainPackageInfo = result.packages.find((item) => item.type === 'MAIN')
      const extInfo = `本次上传${allPackageInfo!.size} ${mainPackageInfo ? ',其中主包' + mainPackageInfo.size : ''}`
      console.log(chalk.green(`上传成功 ${new Date().toLocaleString()} ${extInfo}`))
    }
  }

  async preview () {
    const previewResult = await miniu.miniPreview({
      project: this.ctx.paths.outputPath,
      appId: this.pluginOpts.alipay!.appId,
      clientType: this.pluginOpts.alipay!.clientType || 'alipay',
      qrcodeFormat: 'base64'
    })
    console.log('预览二维码地址:', previewResult.packageQrcode)
    generateQrCode(previewResult.packageQrcode!)
  }
}

支付宝小程序子类的_init()方法主要做参数的验证和设置;open,upload,preview实现了抽象类定义的方法,分别用于打开开发者工具,上传代码,预览二维码。核心功能的实现依赖于miniu。可以查看相应的资料。

这篇文章介绍了使用MiniU完成CI/CD:https://forum.alipay.com/mini-app/post/35101018。生成二维码调用了generateQrCode方法:

代码语言:javascript
复制
 /**
 * 生产二维码输出到控制台
 * @param url 链接地址
 */
export default function generateQrCode (url: string) {
  require('qrcode-terminal').generate(url, { small: true })
}

generateQrCode实际上是通过三方包qrcode-terminal来实现的。

4CI子类:SwanCI

在SwanCI类中open方法和preview方法的实现与AlipayCI一样,upload实现有所不同:

代码语言:javascript
复制
async upload () {
    const { outputPath } = this.ctx.paths
    const { chalk, printLog, processTypeEnum } = this.ctx.helper
    printLog(processTypeEnum.START, '上传体验版代码到百度后台')
    printLog(processTypeEnum.REMIND, `本次上传版本号为:"${this.version}",上传描述为:“${this.desc}”`)
    shell.exec(`${this.swanBin} upload --project-path ${outputPath} --token ${this.pluginOpts.swan!.token} --release-version ${this.version} --min-swan-version ${this.pluginOpts.swan!.minSwanVersion || '3.350.6'} --desc ${this.desc} --json`, (_code, _stdout, stderr) => {
      if (!stderr) {
        // stdout = JSON.parse(stdout)
        console.log(chalk.green(`上传成功 ${new Date().toLocaleString()}`))
      }
    })
  }

上传的时候执行shell脚本,通过shelljs来实现的 。

5CI子类:WeappCI

WeappCI主要是使用了miniprogram-ci ,具体看一下open, upload, preview方法:open方法(代码有删减):

代码语言:javascript
复制
import * as cp from 'child_process'

async open () {
    const { fs, printLog, processTypeEnum, getUserHomeDir } = this.ctx.helper
    const { appPath } = this.ctx.paths
    // 检查安装路径是否存在
    /** 命令行工具所在路径 */
    // 检查是否开启了命令行
    cp.exec(`${cliPath} open --project ${appPath}`, (err) => {
      if (err) {
        printLog(processTypeEnum.ERROR, err.message)
      }
    })
  }

open方法用于打开开发者工具,通过node.js child_process的exec执行命令。upload方法:

代码语言:javascript
复制
import * as ci from 'miniprogram-ci'

async upload () {
    const { chalk, printLog, processTypeEnum } = this.ctx.helper
    try {
      printLog(processTypeEnum.START, '上传体验版代码到微信后台')
      printLog(processTypeEnum.REMIND, `本次上传版本号为:"${this.version}",上传描述为:“${this.desc}”`)
      const uploadResult = await ci.upload({
        project: this.instance,
        version: this.version,
        desc: this.desc,
        onProgressUpdate: undefined
      })

      if (uploadResult.subPackageInfo) {
        const allPackageInfo = uploadResult.subPackageInfo.find((item) => item.name === '__FULL__')
        const mainPackageInfo = uploadResult.subPackageInfo.find((item) => item.name === '__APP__')
        const extInfo = `本次上传${allPackageInfo!.size / 1024}kb ${mainPackageInfo ? ',其中主包' + mainPackageInfo.size + 'kb' : ''}`
        console.log(chalk.green(`上传成功 ${new Date().toLocaleString()} ${extInfo}`))
      }
    } catch (error) {
      console.log(chalk.red(`上传失败 ${new Date().toLocaleString()} \n${error.message}`))
    }
  }

上传代码的方法使用miniprogram-ci的upload方法,得到结果信息后根据分包信息来提示整体包大小和主包大小。preview方法(代码有删减):

代码语言:javascript
复制
async preview () {
  const { chalk, printLog, processTypeEnum } = this.ctx.helper
  try {
    printLog(processTypeEnum.START, '上传开发版代码到微信后台并预览')
    const uploadResult = await ci.preview({
      project: this.instance,
      version: this.version,
      desc: this.desc,
      onProgressUpdate: undefined
    })

    if (uploadResult.subPackageInfo) {
      const allPackageInfo = uploadResult.subPackageInfo.find((item) => item.name === '__FULL__')
      const mainPackageInfo = uploadResult.subPackageInfo.find((item) => item.name === '__APP__')
      const extInfo = `本次上传${allPackageInfo!.size / 1024}kb ${mainPackageInfo ? ',其中主包' + mainPackageInfo.size + 'kb' : ''}`
      console.log(chalk.green(`上传成功 ${new Date().toLocaleString()} ${extInfo}`))
    }
  } catch (error) {
    console.log(chalk.red(`上传失败 ${new Date().toLocaleString()} \n${error.message}`))
  }
}

preview方法使用的是miniprogram-ci的preview方法, 得到结果信息后根据分包信息来提示整体包大小和主包大小。

6CI子类:TTCI

TTCI使用tt-ide-cli来完成预览和上传,使用child_process的exec来完成打开开发者工具的功能。open(代码有删除):

代码语言:javascript
复制
open () {
    if (fs.existsSync(projectPath)) {
      console.log(chalk.green(`open projectPath: ${projectPath}`))
      const openPath = `${openCmd}?path=${projectPath}`
      cp.exec(openPath, (error) => {
        if (!error) {
          console.log('打开IDE成功', openPath)
        } else {
          console.log(chalk.red('打开IDE失败', error))
        }
      })
    }
  }

这里open方法也是通过node.js child_process的exec执行命令。upload(代码有删除):

代码语言:javascript
复制
import * as tt from 'tt-ide-cli'
async upload () {
    try {
      await tt.upload({
        entry: outputPath,
        version: this.version,
        changeLog: this.desc
      })
    } catch (error) {
    }
  }

上传代码使用tt-ide-cli的upload方法。preview(代码有删除):

代码语言:javascript
复制
import * as tt from 'tt-ide-cli'
  async preview () {
    try {
      await tt.preview({
        entry: outputPath,
        force: true,
        small: true
      })
    } catch (error) {
      console.log(chalk.red(`上传失败 ${new Date().toLocaleString()} \n${error.message}`))
    }
  }

生成预览二维码使用了tt-ide-cli的upload方法。

Part3总结

1.taro小程序ci的核心代码逻辑是:判断平台,创建CI实例, 执行对应的CI。2.不同平台对应不同的CI类,但都继承了基础的CI抽象类,实现了抽象类声明的open,upload和preview方法。3.实现具体的open,upload和preview方法时根据对应小程序是否提供了命令行工具,有用到miniu,tt-ide-cli,miniprogram-ci,还有的使用shelljs,qrcode-terminal,以及child_process来执行命令。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 若川视野 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Part1学习准备工作
  • Part2源码学习
    • 1小程序CI的整体流程
      • 2CI抽象类:BaseCI
        • 3CI子类:AlipayCI
          • 4CI子类:SwanCI
            • 5CI子类:WeappCI
              • 6CI子类:TTCI
              • Part3总结
              相关产品与服务
              云开发 CloudBase
              云开发(Tencent CloudBase,TCB)是腾讯云提供的云原生一体化开发环境和工具平台,为200万+企业和开发者提供高可用、自动弹性扩缩的后端云服务,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档