经过一段时间的微信小程序开发,总结了一些代码片段,主要是以下几个方面:

  • 小程序(授权、网络、录音、图像)
  • mpvue(分包、全局变量、svg组件、组件class绑定)

授权逻辑

  1. 初次请求 -> 请求用户授权 -> 同意授权(-> 不同意授权 -> 结束) -> 使用对应功能
  2. 二次请求 -> 跳转小程序设置页面modal -> 设置页面 -> 开启scope -> 使用对应功能
const checkPermission = scope =>
  new Promise((resolve, reject) => {
    wx.getSetting({
      success: res => {
        // 是否存在认证配置
        let hasAuthorized = res.authSetting.hasOwnProperty(scope)
        if (hasAuthorized) {
          // 已授权
          if (res.authSetting[scope]) {
            resolve('已授权')
            return
          }
          // 未授权,提示进入小程序设置页面,wx限制:需要主动点击才能执行openSetting(),因此使用modal
          wx.showModal({
            title: '没有权限',
            content: '体验该功能需要您授权功能权限,现在前往设置开启',
            success: res => {
              if (res.confirm) {
                reject('设置页面')
                wx.openSetting()
              } else if (res.cancel) {
                reject('不进入设置')
              }
            }
          })
        }
      },
      fail: err => { reject(err.errMsg) }
    })
  })

网络

微信小程序不同环境下网络请求的不同之处:

  • 预览及体验模式下会校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书,需要在后台设置允许的request,upload等域名
  • 真机调试与测试环境模式下,可以在详情里勾选不校验选项跳过校验

网络请求与拦截器

可以使用fly.js作为小程序的网络请求库,在使用拦截器等功能时也较为方便。

小程序中一个特殊的地方是: content-typemultipart/formdata类型的POST请求不能通过自定义请求的方式发出,需要使用小程序的wx.uploadFile方法,可以如下简单封装下:

const formDataRequest = (url, filePath, params = {}) =>
  new Promise((resolve, reject) => {
    let token = wx.getStorageSync("token")
    wx.uploadFile({
      url,
      filePath,
      name: "file",
      header: { token },
      formData: params,
      success: async res => {
        // 一些对响应数据的处理...
        resolve(res.data)
      },
      fail: err => {
        reject(err)
      }
    });
  });

判断是否在线

使用getNetworkType方法即可

export const isOnline = () =>
  new Promise((resolve, reject) => {
    wx.getNetworkType({
      success(res) {
        const networkType = res.networkType
        resolve(networkType !== 'none')
      },
      failed(res) {
        reject(res)
      }
    })
  })

录音处理

主要是录音时的API检测、状态控制与事件监听器的处理。

// 1. 检测录音管理器是否可用
if (wx.getRecorderManager) {
  this.recorder = wx.getRecorderManager()
  this.addRecorderListener()
} else {
  wx.showModal({
    title: '提示',
    showCancel: false,
    content:
      '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
  })
}
// 2. 录音前检测scope.record授权情况
async startRecordHandle() {
  if (!this.recorder) return
  try { await this.checkPermission('scope.record') }  
  catch (err) { return }
  this.recorder.start(this.audioOption)
},
// 3. 添加事件监听器
addRecorderListener() {
  if (!this.recorder) return
  this.recorder.onStart(() => {
    ...
    this.recording = true
  })
  this.recorder.onStop(path => {
    ...
    this.recording = false
    this.audioPath = path.tempFilePath
  })
}

若需实现长按录音的场景,可以结合lonepress事件与setTimeout来实现。

<template>
  <g-button type="primary"
    ...
    @long-press="longPressHandle"
  />
</template>
<script>
export default {
  methods: {
    longPressHandle() {
      // longpress事件会在350ms后出发
      this.canRecordStart = true
    },
    touchStartHandle() {
      this.canRecordStart = true
      let delay = 400 // 设置400ms延迟
      setTimeout(() => {
        if (this.canRecordStart) {
          this.startRecordHandle()
        }
      }, delay)
    },
    touchEndHandle() {
      if (!this.canRecordStart) return
      this.canRecordStart = false
      this.stopRecordHandle()
    },
  }
}
</script>

图像处理

获取图片信息

wx.getImageInfo

不管是CDN的图片还是本地选择的图片都需要先使用getImageInfo获取图片的基本信息

getImageInfo(img) {
  return new Promise((resolve, reject) => {
    wx.getImageInfo({
      src: img,
      success: res => { resolve(res) },
      fail: () => { reject('获取图片信息失败') }
    })
  })
}

选择图片

wx.chooseImage

让用户选择本地相册中或拍摄的图片,以选择单张图片为例:

const MB = 1024 * 1024
chooseSingleImage() {
  return new Promise((resolve, reject) => {
    wx.chooseImage({
      count: 1, // 默认9,为1获取单张图片
      sizeType: ['original', 'compressed'], // 指定是原图还是压缩图,默认二者都有
      sourceType: ['album', 'camera'], // 指定来源是相册还是相机,默认二者都有
      success: res => {
        let file = res.tempFiles[0]
        // 可以对所选图片尺寸或其他属性做一些限制
        // let { size } = file
        // if (size > 20 * MB) { reject('图片大小应小于20MB') }
        resolve(file)
      },
      fail: () => { reject('图片选取失败') }
    })
  })
}

读取图片

wx.getFileSystemManager()

使用小程序的FS相关API读取文件内容

readFileInBase64(filePath) {
  return new Promise((resolve, reject) => {
    if (wx.getFileSystemManager) {
      // 以base64编码读取图片
      wx.getFileSystemManager().readFile({
        filePath: filePath,
        encoding: 'base64',
        success: res => { resolve(res) },
        file: () => { reject('读取文件失败') }
      })
    } else {
      // 兼容处理,若不支持则提示更新
      wx.showModal({
        title: '提示',
        showCancel: false,
        content:
          '当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'
      })
    }
  })
}

Canvas绘制图像

小程序中使用CanvasContext API与H5的形式基本相同。需要注意的是,在小程序中绘制canvas时尺寸的单位是px,而不是响应式的rpx

需要注意的是从基础库1.9.90开始CanvasContext的API变化了很多,在使用时需要注意兼容性,比如下面两个函数:

export const drawPoint = (ctx, x, y) => {
  let pointColor = '#2ba5ff'
  ctx.beginPath()
  ctx.arc(x, y, 1, 0, Math.PI * 2, true)
  ctx.closePath()
  // 兼容画布填充色方法
  if (ctx.fillStyle) {
    // 1.9.90+
    ctx.fillStyle = pointColor
  } else {
    ctx.setFillStyle(pointColor)
  }
  ctx.fill()
}

export const drawRect = (ctx, x, y, width, height) => {
  let marginColor = '#ff0000'
  // 兼容笔触色彩方法
  if (ctx.strokeStyle) {
    // 1.9.90+
    ctx.strokeStyle = marginColor
  } else {
    ctx.setStrokeStyle(marginColor)
  }
  ctx.lineWidth = 1
  ctx.strokeRect(x, y, width, height)
}

mpvue

分包及分包预加载

mpvue-loader: ^1.1.2

直接在app.json中配置subPackages即可:

{
  ...
  "subPackages": [
    {
      "root": "pages/module-bob/",
      "pages": ["subpage-a/main", "subpage-b/main", "subpage-c/main"]
    },
    {
      "root": "pages/module-alice/",
      "pages": ["subpage-d/main", "subpage-e/main", "subpage-f/main"]
    }
  ],
  "preloadRule": {
    "pages/index/main": {
      "network": "wifi",
      "packages": ["pages/module-bob"]
    }
  }
  ...
}

其中preloadRule为预加载配置,上面的设置意为进入index页面时当为wifi网络时预加载module-bob子包。

使用globalData全局变量

在小程序中将自带的globalData挂载到vue的原型方法上。

在src中的main.js最后添加如下代码:

import Vue from 'vue'
import App from './App'
...
const app = new Vue(App)
app.$mount()
Vue.prototype.$globalData = getApp().globalData // 添加该行

然后就可以在其他页面使用该命令操作全局变量了

// page A
this.$globalData.userInfo = {name: 'yrq110'}
// page B
console.log(this.$globalData.userInfo)
// page C
this.$globalData.userInfo.name: 'yrq110'

注意,在子页面中使用globalData时,将变量赋值的操作放在data中是无效的,如下:

export default {
  data() {
    // 无效
    // isIPX: this.$globalData.isIPX
  },
  computed: {
    isIPX() {
      // 有效
      return this.$globalData.isIPX
    }
  },
}

SVG图标组件的默认尺寸与预设尺寸

在图标组件中加载svg时使用父标签上的尺寸作为默认尺寸,并在传入特定props参数时使用预设尺寸。

业务中碰到了这个问题,使用如下的方法进行了解决:在image组件的load事件处理器中将加载的原始尺寸绑定到style上。

实现了:

  1. 默认使用svg标签自带尺寸
  2. 当传入size属性则使用预设尺寸
<template>
  <image
    ...
    @load="loadHandle"
    :style="{ width: !size ? iconWidth + 'rpx' : '100%', height: !size ? iconHeight + 'rpx' : '100%'}"
    ...
  />
</template>

<script>
export default {
  ...
  data() {
    return {
      iconWidth: 0,
      iconHeight: 0,
      loaded: false // 是否加载完毕
    }
  },
  props: {
    ...
    size: String
  },
  computed: {
    ...
    getSizeClass() {
      let { size } = this
      return size || ''
    },
    setSizeStyle() {
      if (!this.loaded || this.size) return {}
      return {
        width: this.iconWidth + 'rpx',
        height: this.iconHeight + 'rpx'
      }
    }
  },
  methods: {
    loadHandle(e) {
      this.loaded = true
      // 使用加载后的默认尺寸
      const { detail } = e.mp
      this.iconWidth = detail.width * 2
      this.iconHeight = detail.height * 2
    }
  }
  ...
}
</script>

解决无法在组件上绑定class的trick

将keyword作为prop属性传入组件并通过Computed属性绑定到class上,这样在外部引用时就可以根据keyword设置自定义的样式了。

组件中的关键代码如下:

<template>
  <div :class="customClass">
    <slot></slot>
  </div>
</template>

<script>
export default {
  ...
  props: {
    type: String,
    ...
  },
  computed: {
    customClass() {
      let type = this.type || ''
      return type
    }
  }
  ...
}
</script>

在外部引用时就可以使用自定义class来在外部使用样式了:

<template>
  ...
  <g-button type="custom-button"></g-button>
  ...
</template>
...
<style lang="scss">
...
.custom-button {
  .text {
    margin-left: 10px;
  }
...
}
</style>