2019年5月工作总结

4月末开发了一款提问小程序,因为第一次使用wepy,所以遇到很多的坑,同时开发小程序时有些功能第一次使用,记录一下

微信小程序API填坑

上传图片(七牛云上传)

微信上传图片到七牛云多张需要循环上传
七牛云上传前需要获取到七牛云token,获取到token之后保存到全局变量中。

上传图片时有延时,注意设置截取图片数组,以及每次打开上传图片时可同时上传图片的张数需要根据已上传的图片张数动态改变

小程序界面实现(使用wepy框架)

<template>
  <view class="wrapper">
    <view wx:if="{{imageList.length > 0}}" class="uploader-files">
      <repeat for="{{imageList}}" key="index" index="index" item="image">
        <view class="uploader-image">
          <image class="image-remove" @tap="removeImage" data-src="{{image}}" src="../assets/icons/image@remove.png"></image>
          <image mode="aspectFill" src="{{image}}" data-src="{{image}}" @tap="previewImage"></image>
        </view>
      </repeat>
    </view>
    <view wx:if="{{imageList.length < 3}}" class="uploader-input" @tap="chooseImage">
      <image class="icon-add" src="../assets/icons/image@add.png"></image>
    </view>
  </view>
</template>

上传图片逻辑部分

import wepy from 'wepy'
import request from '@/utils/request'
import tip from '@/utils/tip'

export default class ImageSelector extends wepy.component {
  props = {
    // 父子双向动态传值
    imageList: {
      type: Array,
      default: [],
      twoWay: true
    }
  };

  methods = {
    chooseImage() {
      let that = this
      wepy.chooseImage({
        count: 3 - that.imageList.length,
        sizeType: 'compressed'
      }).then(async (res) => {
        const tempFilePaths = res.tempFilePaths
        let list = []
        // 组件中获取app.wpy的全局数据
        const qiniuToken = wepy.$instance.globalData.qiniuToken
        // 上传多张图片时循环上传到七牛云上
        for (let i = 0; i < tempFilePaths.length; i++) {
          let data = await request.uploadImg(tempFilePaths[i], qiniuToken)
          list.push(data.imageURL)
        }
        const imageList = that.imageList.concat(list)
        // 注意:上传多张时会出现延时,设置可传3张图片易多传然后显示超过3张图片,因此使用截取图片数组的前3张
        that.imageList = imageList.slice(0, 3)
        that.$apply()
      }).catch((res) => {
        if (res.errMsg === 'chooseImage:fail:system permission denied') {
          tip.toastIcon('请打开微信调用摄像头的权限', 'none')
        }
      })
    },
    // 预览图片
    previewImage(e) {
      const current = e.target.dataset.src
      wepy.previewImage({
        current,
        urls: this.imageList
      })
    },
    // 删除图片
    removeImage(e) {
      const src = e.target.dataset.src
      this.imageList.splice(this.imageList.indexOf(src), 1)
    }
  };
}
页面滚动监听

实现页面滚动监听有两个方法,onPageScroll()<scroll-view>中的bindscroll事件。

比较:使用onPageScroll()方法的灵敏度没有bindscroll事件的灵敏度高,onPageScroll()只有scrollTop一个参数,bindscroll事件有多个参数。

注意:页面滚动监听有一定的时延,不是每滑动1px就会响应一次滚动事件,如果滑动较快则是十几甚至是几十px响应一次滚动事件,滚动到特定位置也有一段时间去反应

使用<scroll-view>则不支持页面回弹,即下拉刷新事件,如果要实现相同的下拉刷新动效,需要自定义。还有bindscrolltoupperbindscrolltolower事件等。

支付

第一次写支付有很多不懂的
大致流程是:向后台提交表单数据-》成功后调用后台支付接口-》将后台返回的支付数据加密后,调用微信支付API-》支付成功/失败(有些人说要点击支付完成界面的返回按钮才算支付成功,并不是这样,支付成功后退出支付成功界面才会执行接下来的程序)

文档链接:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_7&index=5

// util.js signPay支付签名
export function signPay (data) {
  let {
    appid,
    nonce_str,
    prepay_id,
    timeStamp
  } = data;
  let result = `appId=${appid}&nonceStr=${nonce_str}&package=prepay_id=${prepay_id}&signType=MD5&timeStamp=${timeStamp}&key=JbXAzDijmbdSUf87l3gz6fpf5NQQVgck`
  // 需要导入md5.js
  return md5.hex_md5(result);
}
// 获取当前时间戳
let timeStamp = new Date().getTime().toString()
if (payRes.errCode === errCode.SUCCESS.errCode) {
    wepy.requestPayment({
        timeStamp,
        nonceStr: payRes.payInfo.nonce_str,
        package: `prepay_id=${payRes.payInfo.prepay_id}`,
        signType: 'MD5',
        paySign: signPay({
            ...payRes.payInfo,
            timeStamp
        })
    }).then(() => {
        // 防止支付太快后台来不及更新订单状态
        return new Promise(resolve => {
            setTimeout(() => {
            resolve()
            }, 1000)
        })
    }).then((res) => {
        return getQuestionDetail(data.questionId)
    }).then(res => {
        if (res.errCode === errCode.SUCCESS.errCode) {
            this.navigateTo(data.questionId)
        } else {
            wepy.hideLoading()
            this.isClick = false
            this.$apply()
            tip.alert('支付失败')
        }
    }).catch(error => { // 支付失败
        wepy.hideLoading()
        this.isClick = false
        this.$apply()
        tip.alert('支付失败')
    })
封装request和promise使用
const baseUrl = ''

/**
 * 请求
 * @param params {header, token, data}
 * @param url
 * @param method
 * @returns {Promise<*>}
 */
const wxRequest = async (params = {}, url, method = 'GET') => {
  let token = wepy.getStorageSync('token') || params.token || ''
  if (token) {
    params.header = params.header ? params.header : {
      'Authorization': `WenToken ${token}`
    }
  }
  try {
    let res = await wepy.request({
      url: baseUrl + url,
      method: method,
      data: params.data || {},
      header: Object.assign({
        'Content-Type': 'application/json',
        'Accept': 'application/json'
      }, params.header || {})
    })
    const code = res.statusCode.toString()
    if (code.startsWith('2')) {
      // console.log('err_code', err_code)
      return res.data
    } else if (res.statusCode === 401) {
      console.log('requset 401')
      wepy.setStorageSync('token', '')
      await getLogin()
    }
  } catch (e) {
    console.log('requset err', e)
    tip.error('请检查网络或重试')
  }
}

// 上传图片
const uploadImg = (imageURL, uptokenURL) => {
  return new Promise((resolve, reject) => {
    qiniuyun.upload(imageURL, (res) => {
      resolve(res)
    }, (error) => {
      reject(error)
    }, {
      region: 'ECN',
      domain: 'https://xxxxx',
      uptoken: uptokenURL
    })
  })
}

module.exports = {
  wxRequest,
  uploadImg
}
版本更新

在小程序初始化时进行版本更新

onLaunch(res) {
    // 提示版本更新
    if (wx.canIUse('getUpdateManager')) {
        const updateManager = wx.getUpdateManager()
        // 检测更新
        updateManager.onCheckForUpdate((res) => {
            if (res.hasUpdate) {
                // 监听小程序有版本更新事件
                updateManager.onUpdateReady((res) => {
                    wx.showModal({
                    title: '更新提示',
                    content: '新版本已经准备好,是否重启应用?',
                    success: function (res) {
                        if (res.confirm) {
                            // 强制重启小程序更新
                            updateManager.applyUpdate()
                        }
                        }
                    })
                })
                // 更新失败
                updateManager.onUpdateFailed(()  => {
                    wx.showModal({
                    title: '已经有新版本了哟~',
                    content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~'
                    })
                })
            }
        })
    }
}
进入场景

小程序有不同进入的方式,此次开发的小程序进入方式主要有三种:点击服务通知进入小程序,点击群分享进入小程序,识别小程序码进入小程序。

群分享获取群数据

转发页面需要设置当前页面的转发按钮withShareTickettrueonShareAppMessage()转发卡片会携带shareTicket参数

// page.js
onLoad(option) {
    // 设置分享按钮
    wepy.showShareMenu({
    withShareTicket: true
    })
}

// 监听用户点击页面内转发按钮(<button> 组件 open-type="share")或右上角菜单“转发”按钮的行为,并自定义转发内容。
onShareAppMessage () {
    return {
    title: detail.SHARE_TITLE,
    path: `/pages/detail?id=${this.question.questionId}&shareId=${this.userInfo.userId}`,
    imageUrl: detail.SHARE_IMG
    }
}

打开群内卡片获取加密信息,在App.onLaunchApp.onShow获取到一个shareTicket。通过调用wx.getShareInfo接口传入此 shareTicket可以获取到转发信息。

App.onLaunch监听小程序初始化,如果小程序在后台运行再次打开群内的卡片时无法监听,所以推荐在App.onShow中获取卡片中的数据。

// app.js 获取到卡片的加密数据
globalData = {
    shareTicket: null,
    scene: ''
}
onShow(ops) {
    // 判断是否是群点击进入
    this.globalData.scene = ops.scene
    if (ops.scene === 1044 && ops.shareTicket !== undefined) {
    this.globalData.shareTicket = ops.shareTicket
    } else {
    // 初始化shareTicket
    this.globalData.shareTicket = ''
    }
}

// page.js 卡片打开跳转的页面
onLoad() { // 处理加密数据
    if (this.$parent.globalData.scene === 1044 && this.$parent.globalData.shareTicket !== null && this.$parent.globalData.shareTicket !== '') {
        const shareTicket = this.$parent.globalData.shareTicket
        this.shareTicket = shareTicket
        this.$parent.globalData.shareTicket && this.getIvData(Number.parseInt(option.id), option.shareId ? Number.parseInt(option.shareId) : 0)
    }
}

// 获取群加密信息
getIvData (questionId, shareId) {
    wepy.checkSession().then(() => {
        // 获取转发信息
        return wepy.getShareInfo({
            shareTicket: this.$parent.globalData.shareTicket
        }).then((res) => {
            // 提交群信息
            return submitGroups({
            data: {
                shareId: shareId,
                questionId: questionId,
                iv: res.iv,
                encryptedData: res.encryptedData
            }
            })
        }).catch((res) => {
            log('err', res)
        })
    })
}

bug记录:出现过没有分享到群里的卡片却将卡片信息添加到群内了,bug分析是在app.js中没有初始化shareTicket数据

获取群信息注意开放数据校验与解密
服务端获取开放数据,其中签名校验以及数据加解密涉及用户的会话密钥session_key。所以前端需要进行wx.login()登录流程获取到回话密钥session_key
需要注意的是密钥session_key的有效性,会遇到因session_key不正确而校验签名失败或解密失败的bug。

  1. wx.login调用时,用户的session_key可能会被更新而致使旧session_key失效(刷新机制存在最短周期,如果同一个用户短时间内多次调用wx.login,并非每次调用都导致session_key刷新)。开发者应该在明确需要重新登录时才调用wx.login,及时通过 auth.code2Session接口更新服务器存储的session_key
  2. 微信不会把session_key的有效期告知开发者。我们会根据用户使用小程序的行为对session_key进行续期。用户越频繁使用小程序,session_key有效期越长。
  3. 开发者在session_key失效时,可以通过重新执行登录流程获取有效的session_key。使用接口wx.checkSession可以校验session_key是否有效,从而避免小程序反复执行登录流程。
  4. 当开发者在实现自定义登录态时,可以考虑以session_key有效期作为自身登录态有效期,也可以实现自定义的时效性策略。

所以前端需要在调用wx.login时检测session_key的有效性,无效需要重新调用wx.login,避免重复调用wx.login

formId 服务通知

服务通知是需要收集用户操作的formId,只有当用户点击button时会产生formId,在模拟器中无法产生有效的formId

submit事件可以获取到formId

<form report-submit="true" @submit="submit">
<button
    class="item-btn answer-btn {{btnDisable ? '' : 'isActive'}}"
    form-type="submit"
    @tap="submitForm"
    disabled="{{isClick}}"
>
    按钮
</button>
</form>
输入校验
金额

通过动态设置输入框maxlength来控制小数点位数。

amountInput(e) {
    let money = e.detail.value
    if (e.detail.value < question.AMOUNT_MIN) {
        this.showError(question.AMOUNT_ERROR)
    } else if (e.detail.value > question.AMOUNT_MAX) {
        this.showError(question.AMOUNT_EXCEED)
    }
    if (/^(\d?)+(\.\d{0,2})?$/.test(money)) { // 正则验证,提现金额小数点后不能大于两位数字
        this.amountMaxLength = 6
        if (/^.$/.test(money)) {
            this.redAmount = money.replace('.', '0.')
            this.btnActive()
            return money.replace('.', '0.')
        }
    } else {
        this.amountMaxLength = money.length - 1
        if (!/^[0-9]\d*\.\d*$/g.test(money)) {
            this.redAmount = ''
            this.btnActive()
            return ''
        }
    }
    this.redAmount = money
    this.btnActive()
},
amountBlur(e) {
    this.amountMaxLength = 6
    this.redAmount = e.detail.value
}
空白字符串
textInput(e) {
    this.text = e.detail.value.replace(/(^\s*)/g, '')
}
获取相册授权

微信小程序授权api调用时分三种情况:

  • 如果用户未接受或拒绝过此权限,会弹窗询问用户,用户点击同意后方可调用接口;
  • 如果用户已授权,可以直接调用接口;
  • 如果用户已拒绝授权,则不会出现弹窗,而是直接进入接口 fail 回调。请开发者兼容用户拒绝授权的场景。

用户一旦用户明确同意或拒绝过授权,其授权关系会记录在后台,直到用户主动删除小程序。

注意: 获取相册或者地理位置等权限关系到用户是否授权给微信app

弹窗弹起情况分析:

如获取用户信息授权,如果用户拒绝后,第二次点击还会进行弹窗(这和使用<button>open-type属性相关)。

不需要用户进行触发进行授权的情况就需要判断用户是否授权过,若从未进行授权可以进行弹窗,若已拒绝授权则需要用户打开小程序授权设置进行手动开启。

需要先获取用户授权设置(wx.getSetting),如果没有就向用户发起授权请求(wx.authorize),如果请求失败可以调用(wx.openSetting)引导用户开启授权,或者让用户手动打开 [右上角]-》[关于]-》[右上角]-》[设置] 进行小程序的授权状态设置。

getSetInfo (filePath) {
    wepy.getSetting().then(res => {
        if (!res.authSetting['scope.writePhotosAlbum']) {
            wepy.authorize({
                scope: 'scope.writePhotosAlbum'
            }).then(res => {
                log(res)
                this.saveImage(filePath)
            }).catch(err => {
                log(err)
                tip.alert('请打开相册授权')
            })
        } else {
            this.saveImage(filePath)
        }
    })
}

saveImage (filePath) {
    wepy.saveImageToPhotosAlbum({
        filePath
    }).then(res => {
        log(res)
        this.createImage = false
        this.$apply()
        tip.success('保存成功', 1000)
    }).catch(err => {
        log(err)
        tip.alert('保存到相册失败')
    })
}
canvas画图初体验

根据后台提供的数据画一个图片用于朋友圈分享。

canvas中用到的网络图需要下载下来(wx.downloadFile)或者获取图片信息(wx.getImageInfo),本地的图就不用下载。

当时遇到一个问题就是获取当前用户头像信息时速度非常的慢,最后的解决方案是后端下载后传给前端链接不用微信端的链接。

export default class RedCreate extends wepy.page {
    config = {
        navigationBarTitleText: '生成答题红包'
    }

    mixins = [SubmitMixin]

    customData = {
        avatarUrl: null,
        codeUrl: null,
        pixelRatio: 2
    }

    data = {
        userInfo: {},
        red: {},
        coverSelected: 0,
        coverImages: ['../assets/images/cover1.png', '../assets/images/cover2.png', '../assets/images/cover3.png'],
        question: {},
        createImage: false,
        canvasWidth: 375,
        canvasHeight: 500,
        changeNum: 0,
        isClickCreate: false,
        isDraw: false
    }

    async onLoad(opt) {
        this.red = red
        this.userInfo = wepy.getStorageSync('userInfo')
        wepy.showShareMenu({
            withShareTicket: true
        })
        this.getDetail(opt.id) // 这里三个函数调用相互没有影响所以可以不使用await
        this.getSystemInfo()
        this.downImages()
    }

    onShareAppMessage () {
        return {
            title: this.red.TITLE,
            path: `/pages/detail?id=${this.question.questionId}&shareId=${this.userInfo.userId}`,
            imageUrl: this.coverImages[this.coverSelected]
        }
    }

    methods = {
        handleChange () {
            this.changeNum = this.changeNum + 1
            this.coverSelected = this.coverSelected === 2 ? 0 : this.coverSelected + 1
        },
        handleShare () {
            wepy.showLoading({
                title: '生成图片中'
            })
            this.isClickCreate = true
            if (this.isDraw) {
                wepy.hideLoading()
                this.createImage = true
            }
        },
        handleSave() {
            this.saveCanvas()
        }
    }

    saveCanvas () { // canvas转图片
        wepy.canvasToTempFilePath({
            x: 0,
            y: 0,
            width: this.canvasWidth,
            height: this.canvasHeight,
            destWidth: this.canvasWidth * this.customData.pixelRatio,
            destHeight: this.canvasHeight * this.customData.pixelRatio,
            canvasId: 'poster',
            fileType: 'jpg',
            quality: 1
        }).then(res => {
            this.getSetInfo(res.tempFilePath)
        }).catch(() => {
            tip.alert('保存到相册失败')
        })
    }

    async getDetail(id) {
        const data = await getQuestionDetail(Number.parseInt(id))
        this.question = data
        this.$apply()
    }

    downImages () {
        const that = this
        const userInfo = this.userInfo
        wepy.downloadFile({
            url: userInfo.avatarUrl
        }).then(res => {
            that.customData.avatarUrl = res.tempFilePath
            this.$apply()
            return wepy.downloadFile({
                url: that.question.codeUrl
            })
        }).then(res => {
            that.customData.codeUrl = res.tempFilePath
            this.$apply()
            that.createPoster()
        }).catch(error => {
            log(error)
        })
    }

    createPoster () {
        const that = this
        const width = this.canvasWidth ? Number.parseInt(this.canvasWidth) : 375
        const height = this.canvasHeight ? Number.parseInt(this.canvasHeight) : 578
        const center = Number.parseInt(this.canvasWidth) / 2
        let top = 24 / 578 * height // 每个元素的顶端距离,相对位置
        const question = this.question
        const userInfo = wepy.getStorageSync('userInfo')

        const context = wx.createCanvasContext('poster')
        const bg = '../assets/images/share.png'
        const avatar = this.customData.avatarUrl
        context.drawImage(bg, 0, 0, width, height) // 背景图
        // 头像
        context.save()
        context.beginPath()
        context.arc(center, top + 30 / 578 * height, 30 / 578 * height, 0, 2 * Math.PI)
        context.closePath()
        context.clip()
        context.drawImage(avatar, center - 30 / 578 * height, top, 60 / 578 * height, 60 / 578 * height)
        context.restore()

        const fontColor = '#EFB77D'
        // nickName
        top = top + 80 / 578 * height
        const nickName = userInfo.nickName
        context.setFontSize(15 / 578 * height)
        context.setFillStyle(fontColor)
        context.setTextAlign('center')
        context.fillText(nickName, center, top)
        // 发了一个红包给你
        top = top + 26 / 578 * height
        const sendRed = '发了一个红包给你'
        context.setFontSize(18 / 578 * height)
        context.setFillStyle(fontColor)
        context.setTextAlign('center')
        context.fillText(sendRed, center, top)
        // 问题详情 最多24个字
        top = top + 56 / 578 * height
        let content = question.content
        if (content.length > 24) {
            content = content.slice(0, 23) + '...'
        }
        // 第一行
        context.setFontSize(26 / 578 * height)
        context.setFillStyle(fontColor)
        context.setTextAlign('center')
        context.fillText(content.slice(0, 12), center, top)
        // 第二行
        top = top + 34
        if (content.length > 12) {
            context.setFontSize(26 / 578 * height)
            context.setFillStyle(fontColor)
            context.setTextAlign('center')
            context.fillText(content.slice(12), center, top)
        }
        // 小程序二维码
        top = top + 140 / 578 * height
        const codeUrl = this.customData.codeUrl
        context.save()
        context.beginPath()
        context.arc(center, top, 70 / 578 * height, 0, 2 * Math.PI)
        context.closePath()
        context.setFillStyle('#ffffff')
        context.fill()
        context.clip()
        context.drawImage(codeUrl, center - 60 / 578 * height, top - 60 / 578 * height, 120 / 578 * height, 120 / 578 * height)
        context.restore()
        // 长按识别二维码,答问题,抢红包
        top = top + 100 / 578 * height
        const tip = '长按识别二维码,答问题,抢红包'
        context.setFontSize(13 / 578 * height)
        context.setFillStyle('rgba(255,255,255,.7)')
        context.setTextAlign('center')
        context.fillText(tip, center, top)
        context.draw(true, setTimeout(() => { // 这里的canvas显示是弹窗的方式,所以如果用户在当前页面多次点击生成海报,不要重复绘制,动态控制显隐
            that.isDraw = true
            if (that.isClickCreate) {
                log('生成海报')
                wepy.hideLoading()
                that.createImage = true
            }
            that.$apply()
        }, 200))
    }

    getSystemInfo () { // 获取用户手机信息,通过手机屏幕信息来动态计算海报中的元素
        wepy.getSystemInfo().then(res => {
            log('getSystemInfo', res)
            this.customData.pixelRatio = res.pixelRatio
            this.canvasWidth = res.windowWidth
            this.canvasHeight = res.windowHeight
            this.$apply()
        }).catch(err => {
            log(err)
        })
    }

    getSetInfo (filePath) { // 获取用户设置信息
        wepy.getSetting().then(res => {
            if (!res.authSetting['scope.writePhotosAlbum']) {
                wepy.authorize({
                    scope: 'scope.writePhotosAlbum'
                }).then(res => {
                    log(res)
                    this.saveImage(filePath)
                }).catch(err => {
                    log(err)
                    tip.alert('请打开相册授权')
                })
            } else {
                this.saveImage(filePath)
            }
        })
    }

    saveImage (filePath) { // 保存图片
        wepy.saveImageToPhotosAlbum({
            filePath
        }).then(res => {
            log(res)
            this.createImage = false
            this.$apply()
            tip.success('保存成功', 1000)
        }).catch(err => {
            log(err)
            tip.alert('保存到相册失败')
        })
    }
}

wepy填坑(wepy 1.x版本)

组件和js中使用app.wpy中的属性

page页面中可以直接使用this.$parent.globalData获取到app中的globalData数据。

在组件和js中需要使用wepy.$instance.globalData获取到app中的globalData数据。

组件使用

组件最好只嵌套一层,数据传递也是只有一层有效

组件中的onLoad事件只会触发一次。

父子组件的数据传递

props静态传值,静态传值为父组件向子组件传递常量数据,因此只能传递String字符串类型。

动态传值是指父组件向子组件传递动态数据内容,父子组件数据完全独立互不干扰。但可以通过使用.sync修饰符来达到父组件数据绑定至子组件的效果,也可以通过设置子组件propstwoWay: true来达到子组件数据绑定至父组件的效果。

双向绑定:如果既使用.sync修饰符,同时子组件props中添加的twoWay: true时,就可以实现数据的双向绑定了。

twoWaytrue时,表示子组件向父组件单向动态传值,而twoWayfalse(默认值,可不写)时,则表示子组件不向父组件传值。这是与Vue不一致的地方,而这里之所以仍然使用twoWay,只是为了尽可能保持与Vue在标识符命名上的一致性。

注意:父子组件间的双向绑定只支持一层数据,即支持value,不支持obj.value

事件分发

$invoke是一个页面或组件对另一个组件中的方法的直接调用,通过传入组件路径找到相应的组件,然后再调用其方法。

this.$invoke('ComA', 'someMethod', 'someArgs'); // 调用组件ComA中的方法
// ComG是ComA的兄弟组件的子组件,即叔侄关系。
this.$invoke('./../ComB/ComG', 'someMethod', 'someArgs'); // 组件ComA中调用组件ComG的某个方法

$broadcast事件是由父组件发起,所有子组件都会收到此广播事件,除非事件被手动取消

// 父组件向下分发initData事件
navigateTo (id) {
    wepy.navigateTo({
        url: `./redCreate?id=${id}`
    }).then(() => {
        wepy.hideLoading()
        this.$broadcast('initData')
        this.isClick = false
        this.$apply()
    })
}

// 子组件接收initData事件
events = {
    'initData': ($event) => {
        this.initData()
    }
}

$emit事件发起组件的所有祖先组件会依次接收到$emit事件

// 子组件向上分发submit事件
that.$emit('submit', form, event)

// 父组件监听到submit事件
events = {
    'submit': async (form, event, $event) => {
    	this.otherFunction()
    }
}
promise使用

wepy提供了微信小程序APIpromise写法,同时支持await/async

使用前需要在app.wpy中配置

constructor () { // 构造函数
    super()
    this.use('requestfix') // 解决了同时发起多个request时候的异常修复
    this.use('promisify') // 开启promise await async等功能的必需代码
}
mixins

混合可以将组件之间的可复用部分抽离,从而在组件中使用混合时,可以将混合的数据,事件以及方法注入到组件之中。

默认式混合

对于组件data数据,components组件,events事件以及其它自定义方法采用默认式混合,即如果组件未声明该数据,组件,事件,自定义方法等,那么将混合对象中的选项将注入组件之中。对于组件已声明的选项将不受影响。

兼容式混合

对于组件methods响应事件,以及小程序页面事件将采用兼容式混合,即先响应组件本身响应事件,然后再响应混合对象中响应事件。注意,这里事件的执行顺序跟Vue中相反,Vue中是先执行mixin中的函数, 再执行组件本身的函数。

methods响应事件和events自定义事件不同,methods响应点击等事件,此类事件定义在methods对象中。

开发逻辑

登录和认证
业务逻辑分析

这个是我认为很难的一个地方,感觉有很复杂的逻辑,但是我可能想的有点复杂了。首先要理清楚进入场景和进入方式,然后理清楚次序,最好画个流程图或者写下来。

首先有三个页面需要登录认证,其中问题页面为小程序起始页,但是在小程序初始化时进行登录获取用户信息需要一定的时间,页面加载时不一定已完成获取用户信息,因此需要用户手动登录。

其中只有“我的”页面需要在页面载入时获取用户信息判断是否授权,根据判断显示不同的界面元素。

“问题”和“回答”页面中提交按钮需要根据用户信息是否授权动态显示。

因为业务逻辑中的授权有点特殊,这里特殊说明,新用户第一次授权时需要传递用户信息中的加密信息给后台,然后后台记录用户加密信息后修改用户授权状态,也就是说用户是否授权是按照后台的数据判断,如果后台没有记录用户授权需要用户进行重新授权或获取用户信息去更新后台用户授权状态。

具体实现逻辑

微信小程序获取用户信息授权需要使用按钮点击事件进行触发,需要使用<button open-type="getUserInfo" bindgetuserinfo="getUserInfo"/>触发用户授权,事件getuserinfo是异步回调,在弹窗消失后进行触发,通过返回的参数中的detail.userInfo是否存在来判断用户是否同意授权或者拒绝授权。

点击获取用户授权按钮显示弹窗情况:用户未授权情况下会弹出,即使用户上一次拒绝授权也能够弹出,和其他权限授权有差别。

用户授权更新后台数据后进行下一步操作。

当回调中含有用户信息就表明用户同意授权。

<!-- html部分 -->
<button
    wx:if="{{!isLogin}}"
    class="item-btn question-btn {{btnDisable ? '' : 'isActive'}}"
    form-type="submit"
    lang="zh_CN"
    open-type="getUserInfo"
    @getuserinfo="gotUserInfo"
    disabled="{{isClick}}"
>
    {{questionPage.btn}}
</button>
// 逻辑部分
gotUserInfo(e) {
    if (e.detail.userInfo) { // 授权成功
        this.isClick = true
        this.getSettingInfo(e)
    }
    this.$apply()
},

async getAuth (userId, detail) { // 授权 按钮点击进行授权 更新登录状态
    await auth({
        data: {
            encryptedData: detail.encryptedData,
            iv: detail.iv
        }
    }, userId).then(res => {
        console.log('getAuth !isLogin userInfo', res)
        if (res.errCode === errCode.SUCCESS.errCode && res.isAuthorized === 1) { // 返回成功并认证
            console.log('getAuth !isLogin userInfo', res.isAuthorized)
            wepy.setStorageSync('isLogin', true)
            wepy.setStorageSync('userInfo', res)
        }
    }).catch(err => {
        log('getAuth', err)
    })
}

async getSettingInfo (event, callback) {
    // 是否显示弹框
    if (event.detail.userInfo) { // 进行了弹框授权
        let isLogin = wepy.getStorageSync('isLogin') // 是否已授权登录
        let userInfo = wepy.getStorageSync('userInfo')
        if (isLogin) {
            callback
        } else {
            if (userInfo) {
                if (isLogin) {
                    callback
                } else { // 未授权
                    userInfo = await wepy.getStorageSync('userInfo')
                    await this.getAuth(userInfo.userId, event.detail)
                    isLogin = wepy.getStorageSync('isLogin')
                    if (isLogin) {
                        callback && callback
                    }
                }
            } else {
                await getLogin() // 登录后重新获取用户数据
                isLogin = wepy.getStorageSync('isLogin')
                if (isLogin) {
                    callback
                } else { // 未授权
                    userInfo = wepy.getStorageSync('userInfo')
                    await this.getAuth(userInfo.userId, event.detail)
                    isLogin = wepy.getStorageSync('isLogin')
                    if (isLogin) {
                        callback && callback
                    }
                }
            }
        }
    }
}
callback回调函数

因为有三处用户授权操作,所以想提出来使用callback的方式进行,因为当时第一次使用所以没有理解透彻如何正确使用callback。

难点在callback传参,分两种传参:

  1. callback携带参数
function A(a,callback){ 
    var b=callback; 
    console.log('A', a+b); 
} 
function B(c){ 
	return (-c); 
} 
A(4, B(3));

callback先定义了参数,父函数传入的callback就不是函数了,此时在父函数中不能使用callback进行传参,如果使用callback(5)会报callback is not a function的错误。

  1. 父函数向callback传参
function A(a,callback){ 
    var b=callback(5); 
    console.log('A', a+b); 
} 
function B(c){ 
	return (-c); 
} 
A(4, B);

父函数传入callback传递的是整个函数,所以可以在父函数中给callback传参。

注意,可以在回调函数中使用当前对象中的方法和属性,不过要注意作用域。