截至上节内容结束,「🎭头像定制」小程序简单版本的大体功能都已经全部完成,可以上线给朋友们使用啦。不过,在发布之前,我还想做一些小小的整理与优化,经过一番打磨之后它也许会焕然一新。待优化完成之后,将代码上传并提交审核,然后我们一起来回顾这段时间的开发经历,总结经验,反思不足,为下一阶段的开发做好准备。

代码整理与优化

第一个可以优化的点是操作引导,首页中,如果用户在没有完成信息访问授权的情况下通过另外两个选项进入到「确认图片」页面,然后又想「选择当前头像」而返回首页重新授权,此时,我们可以将首页的「选择当前头像」按钮设置为首要按钮样式( btn-primary)作为引导。其核心逻辑是「未授权访问个人信息 + 确认图片页面操作菜单点选取消 + 返回首页 → 为首页的选择当前头像按钮设置 btn-primary 样式类」,实现这个功能的同时,顺便也可以为大家演示 EventChannel 的用法 😉 相关代码以及完成之后的示意图如下:

  1. <!--miniprogram/pages/index/index.wxml-->
  2. <button open-type="getUserInfo" bindgetuserinfo="chooseCurrentAvatar" class="btn {{currentAvatarBtnPrimary ? 'btn-primary' : ''}}" style="width: 60%; margin: 2.5vh 5vw; padding: 0; font-size: inherit; font-weight: inherit; ">选择当前头像</button>
// miniprogram/pages/index/index.js
Page({
    data: {
      currentAvatarBtnPrimary: false
  },
  // 此处将跳转页面的逻辑单独提取出来,便于统一定义 eventChannel
   _navigateToAvatarConfirmPage() {
    wx.navigateTo({
      url: '../avatar-confirm/avatar-confirm',
      events: {
        currentAvatarBtnPrimary: () => {
          wx.getSetting({
            complete: (res) => {
              if (!res.authSetting['scope.userInfo']) {
                this.setData({
                  currentAvatarBtnPrimary: true
                })
              }
            },
          })
        }
      }
    })
  },
  chooseCurrentAvatar() {
    wx.getSetting({
      success: (res) => {
        if (res.authSetting['scope.userInfo']) {
          this.setData({
            currentAvatarBtnPrimary: false
          })
          wx.getUserInfo({
            success: (res) => {
              const avatarUrl = res.userInfo.avatarUrl;
              appInstance.globalData['avatar'] = avatarUrl;
              this._navigateToAvatarConfirmPage()
            }
          })
        } else {
          wx.showToast({
            title: '授权访问之后才可以获取当前头像',
            icon: 'none'
          })
        }
      }
    })
  }
  // ... 其它业务逻辑
})
// miniprogram/pages/avatar-confirm/avatar-confirm.js
Page({
    chooseAvatar() {
    wx.showActionSheet({
        itemList: this.data['_itemList'],
      success: (res) => {
          // 用户选择某一项的逻辑
      },
      fail: (res) => {
          this.getOpenerEventChannel().emit('currentAvatarBtnPrimary', {})
      }
    })
  },
  // ... 其它数据或逻辑
})

GIF 2020-4-24 18-49-31.gif

「确认图片」页面本身的存在意义就相当于是一个中转页面,我们可以直接将「选择贴图」按钮设置为主要按钮作为操作引导,修改的代码如下:

<!--miniprogram/pages/avatar-confirm/avatar-confirm.wxml-->
<view class="width-full flex-row-around-center">
    <view class="btn btn-default" bindtap="chooseAvatar">更换图片</view>
    <view class="btn btn-primary" bindtap="confirmAvatar">选择贴图</view>
</view>

第二点是用户头像的清晰度,与用户信息相关的 API 默认返回的结果中,用户头像图片并非最高清的一档,根据官方的说明,我们可以通过修改该地址最后的数字来获取最高清的头像图片,「首页」相关代码如下,「确认图片」页面同理:

// miniprogram/pages/index/index.js

function getHighQualityAvatar(url) {
  // https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTIWe0mLP2LPho9ZWhRoiamHAvufMic2WGsQCR4Ta7Ua3HapwyH4AaWqy56uTdVmLVrPD4A2uoL0XfpA/132
  // to
  // https://wx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTIWe0mLP2LPho9ZWhRoiamHAvufMic2WGsQCR4Ta7Ua3HapwyH4AaWqy56uTdVmLVrPD4A2uoL0XfpA/0
  return url.replace(/[0-9]+$/, '0')
}

Page({
    chooseCurrentAvatar() {
    wx.getSetting({
      success: (res) => {
        if (res.authSetting['scope.userInfo']) {
          this.setData({
            currentAvatarBtnPrimary: false
          })
          wx.getUserInfo({
            success: (res) => {
              const avatarUrl = res.userInfo.avatarUrl;
              appInstance.globalData['avatar'] = getHighQualityAvatar(avatarUrl);
              this._navigateToAvatarConfirmPage()
            }
          })
        } else {
          wx.showToast({
            title: '授权访问之后才可以获取当前头像',
            icon: 'none'
          })
        }
      }
    })
  },
  // ... 其它数据和逻辑
})

关于 UserInfo 的详细信息请查阅:API - 开放接口 - 用户信息 - UserInfo | 微信开放文档,涉及到正则表达式的知识请查阅:JavaScript - 正则表达式 | MDN Web Docs

第三点是贴图列表,贴图与贴图的间距现在感觉太紧凑了,最好留出一定的呼吸空间,修改前后对比如下:

image.pngimage.png

相关代码如下:

/* miniprogram/pages/sticker-select/sticker-select.wxss */
scroll-view {
  width: 90%;
  height: 15vh;
  margin: 2.5vh 5vw;
  white-space: nowrap;
  border: 2px solid #666;
}
scroll-view image {
  margin: 0.5vh;
  width: 14vh;
  height: 14vh;
}

第四点是合成贴图的过程,当素材尺寸过大或者用户设备性能不在状态的时候,新头像的绘制时间可能会稍微久一点,用户等待的时间会比较长,如果能够将绘制的过程在界面中体现出来就好啦,在之后的版本中,我们会单独做一个合成过程加载的页面,它会是一个放置流量主广告的好地方,但在当前版本中,我们简单添加一个 Loading 即可,用到 wx.showLoading 这个 API,调整之后代码如下:

// miniprogram/pages/sticker-select/sticker-select.js
Page({
    confirmSticker() {
    if (!this.data.stickerSelected) {
      wx.showToast({
        title: '🔊 请先选择贴图',
        icon: 'none'
      })
      return
    }

    wx.showLoading({
      title: '开始合成',
      mask: true,
      success: () => {
        this._composeSticker()
      }
    })

  },

  _showLoadingWithMask(title) {
    wx.showLoading({
      title,
      mask: true,
    })
  },

  _composeSticker() {
    const query = wx.createSelectorQuery()
    query.select('#avatarCanvas')
      .fields({ node: true, size: true })
      .exec((res) => {
        let { width, height, node: canvas } = res[0]
        const ctx = canvas.getContext('2d')

        this._showLoadingWithMask('调整画布')
        const dpr = wx.getSystemInfoSync().pixelRatio
        canvas.width = width * dpr
        canvas.height = height * dpr
        ctx.scale(dpr, dpr)

        const avatar = canvas.createImage()
        avatar.onload = () => {
          this._showLoadingWithMask('绘制原始头像')
          ctx.drawImage(avatar, 0, 0, width, height)
          const sticker = canvas.createImage()
          sticker.onload = () => {
            this._showLoadingWithMask('绘制贴图')
            ctx.drawImage(sticker, 0, 0, width, height)

            this._showLoadingWithMask('导出新头像')
            wx.canvasToTempFilePath({
              canvas: canvas,
              quality: 1,
              width: Math.round(width),
              height: Math.round(height),
              success(res) {
                wx.hideLoading({})
                appInstance.globalData.composedAvatarUrl = res.tempFilePath
                wx.navigateTo({
                  url: '../composition-complete/composition-complete',
                })
              }
            })
          }
          sticker.src = this.data.stickerSelected
        }
        avatar.src = this.data.avatarUrl
      })
  },
  // .. 其它数据或逻辑
})

第五点是「再做一个」的页面跳转逻辑,当前如果用户在合成完毕之后选择「再做一个」,我们会使用 navigateTo 帮他(她)导航至首页,这种情况下页面栈并不会重置,用户此时可以点击出现在「首页」左上角的按钮返回到结果页面,这样的操作不符合我们的设计预期,我们并不希望用户仍然可以回到上次合成的结果页面,可以换用 wx.navigateBack 来解决这个问题,示例代码如下:

// miniprogram/pages/composition-complete/composition-complete.js
Page({
    composeAgain() {
    if (!this.data.avatarSaved) {
      wx.showModal({
        title: '再做一个',
        content: '您还没有保存当前头像,确定重新合成吗?',
        success(res) {
          if (res.confirm) {
            wx.navigateBack({
              delta: 3
            })
          }
        }
      })
    } else {
      wx.navigateBack({
          delta: 3
      })
    }
  },
  // 其它数据或逻辑
})

关于「页面栈」的详细介绍,请查阅:指南 - 小程序框架 - 页面路由 | 微信开放文档

To be honest,以上五点完成之后我们的简单版本已经相当完善了,喜欢花心思的朋友可以换个更加细腻的 UI,比如换上由我开发的 Mobius UI 😬,它吸收了今年特别火的 Neumorphism 样式风格,有一种「来自未来」的观感,代码层面上使用了比较前沿的「功能类优先」的方案,使用起来也非常灵活,美中不足的地方是它还在预览阶段,你如果想尝试的话需要根据为数不多的文档自己摸索,或者直接咨询我相关的事宜,我很乐意提供帮助。

🎨 Mobius: GitHub RepositoryPreview Page,我个人微信是 kxycigaret,添加请备注「Coding the World 工作坊」。

小程序发布流程

发布小程序共分为「开发版本 → 审核版本 → 线上版本」三步。

首先我们在开发者工具的工具栏中找到「上传」按钮,按要求填写版本信息之后点击上传即可,示例如下:

image.png

我的项目有上传记录,这个界面可能跟初次上传的界面有略微的差异,大家不必困惑。

上传成功之后该版本即为「开发版本」,登录小程序网页后台,进入「版本管理」控制台,不出意外的话我们刚刚上传的版本会出现在页面最下方,点击最右侧的按钮,我们可以「删除版本」或者「设为体验版」,设为体验版之后该版本即可以由指定的体验成员试用。

image.png

体验成员的设置请点击左侧「成员管理」选项,关于成员权限的介绍,请查阅:小程序成员管理 | 腾讯客服

经过大家的反复体验和测试,版本确认无误的时候,就可以点击「提交审核」按钮将该版本小程序送审,提交审核的时候需要填写必要的辅助信息,如实填写即可,成功之后如下:

image.png

在审核完成之前,点击最右侧的按钮可以随时撤销该次审核,如果审核失败,官方会给出不通过的原因,需要根据原因对小程序进行完善和修改,重新上传并提交审核。

审核通过之后,后台显示如下:

image.png

点击「提交发布」,经管理员扫码验证之后即可将该版本发布上线,上线之后如果有 Bug 还可以版本回退或者暂停服务。为了避免线上 Bug 影响大范围用户导致用户流失,可以在发布时候选择「灰度发布」,边发布边收集反馈,如果有问题可以随时撤销发布,最大程度降低影响范围。当然,灰度期间如果确认了产品没有问题,可以提升至全量发布。成功上线之后显示如下:

image.png

大家现在可以在微信中「扫描以下二维码」或者搜索「头像中心」试用我发布的小程序啦~

扫码_搜索联合传播样式-标准色版.png

回顾及展望

浏览到这里意味着你已经顺利地完成了「从零到一上线一款小程序」的挑战,最起码我希望是如此的。

我们从满足用户「给头像添加特定贴图」这一需求出发,提取出了目标应用的功能点,使用文字尽可能全面地梳理了操作流程,然后借助 Xiaopiu 完成了简单原型的设计,将其作为开发阶段的实现目标和参考。

着手开发之前,我们以人类传播情景为引简单介绍了编程的基本思维方式和小程序开发的技术栈,在下载好开发者工具并完成项目初始化工作之后,还顺手做了「首页」的初步实现,看到自己亲手将圆形图片中的页面活生生地敲出来,那可能是你第一次尝到「开发」的乐趣。

乐趣建立在成就感的基础上,但随后的内容显然不是那么友好,我们开始正式接触标签、元素、组件、选择器、样式等概念和它们的具体语法规则,限于工作坊的主题我们没有花巨大的篇幅对基础性的知识进行介绍,于是那篇内容中开始涉及到大量开发文档作为「课外阅读」,在此基础之上我们最终完成了所有页面的开发工作。万事开头难,对于刚入门的朋友来说,能够静下心来按部就班阅读完所有资料并独立开发出所有页面是一件非常值得骄傲的事情。

页面开发完毕之后我们紧接着以良好复用性为原则对既有的代码结构进行了优化,主要涉及到公共样式信息的提取。同时我们也将原先白嫖的网络图片资源替换为本地资源,期间详细解释了路径和地址两种略有不同的资源定位方式。我们还介绍和使用了作为模板的 WXML,将页面中的部分静态内容调整为动态内容,为之后使用 JavaScript 为应用编写业务交互功能做好准备工作。末了,在了解了生命周期、事件、接口、回调、页面通信等概念之后,我们三下五除二做完了所有「简单版本」的功能,并在优化之后将开发完成的小程序发布上线。

我们的最终目标是要完成一个「符号墙」,可以以它为载体来记录过去的态度,目前为止我们已经初步完成了「为头像贴图表达态度」这样一个基础功能,有了态度之后还需要有记录,所以接下来我们会集中精力完成「记录头像」的功能。微信本身是具备历史头像功能的,但强度很弱,只支持切换回上一次使用的头像,而在我们的小程序中,用户可以像记日记一样将自己使用过的头像都收集起来,闲时回顾的时候,必然别有一番滋味。

在下一阶段的开发中,我们会使用小程序云开发来支持功能的实现,单就使用层面来说,它并不是特别复杂的概念,我们会留出一节的时间对云开发这种模式进行介绍,之后便进入到实战环节。实战中我们用得最多的还是 JavaScript,在此之前请大家务必熟悉并掌握 JavaScript 的基本语法,最简单的一条检验标准就是看自己能够独立完成「简单版本」的全部开发任务,如果感觉有点吃力,可能还需要稍微补补课,没有问题的话就完全可以进入下一阶段了。祝大家玩得开心 😋

by-nc-sa-4.0.png
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.