微信小程序开发心得

达芬奇密码2018-08-09 11:22

微信小程序目前正处于一个高速发展的阶段,官方的开发文档有着很多不完善的地方。最近美学上线的微信小程序“美学福利社”就是在不断踩坑的过程中开发完成的。本篇文章总结了我在开发时遇到的一些问题和解决方案,供大家参考。

自定义组件

在接到需求的时候微信小程序还没有开放自定义组件的能力(现在已有官方实现),代码是以页面为基本单位来组织的。并且官方提供的一些组件可定制性不强,限制太多。虽然也有一些第三方的解决方案如Minwx-component等,但考虑到本身业务复杂度不高,需要定制的组件也都比较简单,引入这些解决方案带来的额外的开发成本比较高,于是选择了自己实现自定义组件。

一个自定义组件由js文件、wxml模板文件和wxss样式文件组成,这三个文件实际是互相没有关联的。使用组件的页面要在对应的文件中分别引入这三个文件,这样它们通过页面联系在一起,成为一个完整的组件。js文件定义了组件类,它负责管理组件的属性和方法,由于组件类没有办法和模板进行直接的数据绑定,所以UI的状态管理只能交给引用组件的页面来做。

以一个toast组件为例:

组件类

 export default class Toast {
   constructor(host, showTime = 1500) {
     this.host = host; // 引用组件的宿主对象
     this.showTime = showTime; // 默认展示时间1500ms
   }
   /**
    * 展示toast
    * msg: 要提示的信息
    */
   showToast({ msg, showTime}) {
     this.host.setData({
       msg: msg,
       toastVisible: true
     });
     // showTime后隐藏
     setTimeout(() => {
       this.host.setData({
         toastVisible: false
       })
     }, showTime || this.showTime);
   }
 }

组件的host属性保留了对实例化组件的页面的引用,便于通过宿主页面对组件的状态进行管理。这样的实现方法其实存在着组件和页面、以及多个组件间状态污染的问题,只能通过合理的命名来规避,管理起来比较困难。

模板

 <template name="toast">
   <view wx:if="{{ toastVisible }}" class="toast">
     <text class="toast__msg">{{ msg }}</text>
   </view>
 </template>

样式

.toast {
  position: fixed;
  z-index: 1000;
  display: inline-block;
  width: 438rpx;
  left: 50%;
  margin-left: -219rpx;
  padding: 13rpx 23rpx 25rpx 25rpx;
  top: 250rpx;
  box-sizing: border-box;
  background-color: rgba(0, 0, 0, .8);
  border-radius: 10rpx;
  text-align: center;
}
.toast__msg {
  font-size: 28rpx;
  line-height: 40rpx;
  color: #fff;
}

由于wxss不支持css嵌套,我们采用了BEM命名法来避免可能的类名污染。

使用组件时引入必要的文件,在页面中实例化对象

const toast = new Toast(host, showTime);

然后在需要的地方调用组件的方法即可。

这里的自定义组件实现方式是比较简陋的,不过对于简单的定制组件来说已经够用了。值得一提的是现在官方已经开放了自定义组件,最重要的是使用官方的自定义组件只需要在页面的json配置文件中进行引用声明就好了,对于一个懒癌来说不用手动引入诸多文件简直不要太幸福!虽然支持该特性的微信版本比较高,并且有着诸多的坑等着我们去踩,但也不妨一试。


获取用户信息方案

小程序中有一个绕不过去的场景就是获取用户信息,获取用户信息就要取得用户授权。而当用户拒绝授权时我们需要重新发起授权。那么怎么重新发起授权呢?

微信的授权方法是wx.authorize,有三个关键参数:

  • scope
  • success
  • fail

scope指定要获取授权的scope,success和fail分别是授权成功和失败的回调。坑爹的是这个方法在开发者工具2017.11.16更新之前的版本里,不管用户允许还是拒绝授权都会进入成功回,只能用真机调试。

言归正传,怎么重新发起授权?代码是这样的:

// 申请授权
wx.authorize({
    scope: 'scope.userInfo',
    success(res) {
      that.logIn();
    },
    fail(res) {
      // 未授权提示
      wx.showModal({
        title: '提示',
        content: '需要授权,才可以申请试用',
        showCancel: false
      })
    }
});

这是一个点击操作的响应函数中的代码片段。本以为重新发起授权是一个默认的行为,结果发现拒绝授权之后,除非用户本地的授权数据被清除,不然就无法重新发起授权,调用授权接口只会直接走拒绝授权逻辑。查阅文档后发现小程序提供了一个设置页面(wx.openSetting)可以让用户选择重新授权,但是操作步骤多,成本较高,并且会打断当前的交互流程。怎么办?继续看文档,终于发现了一个解决方案——— button组件的open-type属性

button组件设置open-type属性值为getUserInfo后,如果用户未授权则每次点击都会再次弹出授权弹窗提示用户授权。

值得注意的是,用户拒绝授权的情况要妥善处理。必须要授权才能进行的操作可以提示用户授权,不处理用户拒绝授权的情况可能会无法通过小程序审核。


异步流程处理

小程序的的接口大部分都提供了异步和同步方法。同步方法使用过多的话阻塞时间较长,所以平时开发异步方法用的比较多。而异步方法的问题就是方法间互相依赖时免不了陷入回调地狱。新版本的基础库中又移除了对promise的支持,所以我引入了第三方库es6-promise来处理异步流程。

将小程序api改造成promise,以获取用户信息为例:

function getEncryptedData(options) {
  return new Promise((resolve = Promise.resolve, reject = Promise.reject) => {
    wx.getUserInfo({
      ...options,
      withCredentials: true,// 这个参数表示需要获取敏感信息,需要在登录态下才能成功
      success(res) {
        resolve(res)
      },
      fail(errMsg) {
        reject();
        console.error(errMsg);
      }
    });
  });  
}

将小程序api的成功与失败回调通过resolve、reject处理,其他参数可在初始化promise时传入。

多个异步方法有数据依赖时有可能会出现这样的情况:

 // promiseFactory 为返回promise的函数
 promiseFactory1()
 .then(promiseFactory2)
 .then(promiseFactory3)
 ...
 .then(promiseFactoryN)

如果这个调用链长了的话未免看起来不太清爽。小程序不支持es7,没有办法用async/await来处理。于是我想到了co这个框架,co是一个基于es6 generator和yield特性实现的框架,可以用同步的语法来处理异步的流程,彻底摆脱回调的烦恼,并且它的新版本也支持promise了。于是我兴冲冲地引入了co,结果小程序不支持yield关键字,说好的支持es6呢!

好吧,接受这个残酷的事实,看来只能自己处理了。

/**
 * 顺序执行promise
 * promiseFactories 返回promise的函数或函数数组
 */            
function executeSequentially(promiseFactories) {
  var result = Promise.resolve();
  promiseFactories.forEach(function (promiseFactory) {
    if (promiseFactory instanceof Array) {
      result = result.then(...promiseFactory);
    } else {
      result = result.then(promiseFactory);
    }
  });
  return result;
}

具有依赖关系需要依次执行的promise可以交给executeSequentially处理,像这样:

 executeSequentially([promiseFactory1, promiseFactory2, ..., promiseFactoryN])

虽然和.then调用链的写法没有本质上的区别,不过能少写几行代码多少也算是个进步吧。


模板消息

模板消息是微信服务号向用户推送的一种服务消息,而发送模板消息则需要获取formId,一个formId对应一条模板消息。在小程序中有两个获取formId的渠道:

页面的 form 组件,属性report-submit为true时,可以声明为需发模板消息,此时点击按钮提交表单可以获取formId,用于发送模板消息。或者当用户完成支付行为,可以获取prepay_id用于发送模板消息。

我们的需求里,需要在用户操作后的一段时间,比如15天后向用户发送模板消息。而一个formId的有效期一般不超过7天,怎么才能解决这个问题呢?我们的解决方案是将所有存在点击操作的地方都包装成form表单,通过表单提交来处理点击操作,以此来尽可能地收集formId。只要用户在发送模板消息的前七天内进入过小程序,基本就能收集到所需的formId。

<form report-submit bindsubmit="sendFormId">
  <button class="share" open-type="share" form-type='submit'>
    <text>分享</text>
  </button>
</form>

要注意的是需要重置button的默认样式。


input组件层级问题

<view class="form-item">
  <label class="form-label">验证码</label>
  <input class="form-input input--captcha" type="number" placeholder='输入验证码' placeholder-class="input-placeholder" bindinput="handleCaptcha"></input>
  <button class="captcha" plain bindtap="getCaptcha" disabled="{{!mobile || sending}}">{{ sending ? countDownText : "发送验证码" }}</button>
</view>

登录流程中有一个发送验证码的操作,发送验证码的按钮是绝对定位在input上面的。 小程序的input组件未激活时可以通过在按钮设置z-index使按钮覆盖在input组件上,而input组件获得焦点激活时,它的层级是最高的,覆盖在按钮上,无法触发按钮的点击事件。 

解决方法是把input的长度缩短,这样即使激活也不会盖住按钮了。



微信小程序做为微信仍在快速迭代中的一个功能,还有许多不完善的地方,如果本文能让大家减少一些摸索的成本,那就再好不过了。

最后欢迎大家关注美学福利社(°:з」∠)

参考:


网易云新用户大礼包:https://www.163yun.com/gift

本文来自网易实践者社区,经作者丰凡程授权发布。