微信小程序开发中的二三事之网易云信IMSDK DEMO

勿忘初心2018-12-11 15:25

本文由作者邹永胜授权网易云社区发布。


简介

为了更好的展示我们即时通讯SDK强悍的能力,网易云信IM SDK微信小程序DEMO的开发就提上了日程。用产品的话说就是:

  • 云信 IM 小程序 SDK 的能力演示

  • 提供开发者小程序开发参考

换句话说就是在微信里面通过我们云信的IM SDK再实现一个mini版微信。整个小程序主要功能点总的来说是:

  • 登录注册(为了实现不同端同一账号体系,所以没有采用微信授权登录)

  • 最近会话展示

  • 通讯录

  • 单聊对话

  • 用户名片

废话不多说直接上图:

一期已经上线,不足的地方,恳请斧正

本文从基础开始介绍在开发云信DEMO的过程中的一些难点、整体的结构设计、思考的一些解决方案以及踩过的一些坑,希望对大家有些帮助当然希望更多人接入网易云信SDK。

基础

小程序开发基本零门槛,难度基本与模板语言相当,如果你有使用MVVM框架开发前端的经验,基本花个半小时过一遍微信小程序官方文档,即可入门,具体开发细节可以边做边查本人就是这样的。。。。

首先需要明白小程序的运行环境,它运行在微信的上下文中,处于微信这个沙盒中,没有window对象,不能访问基于browser context下的DOM。在ios设备上是运行在JSCore(苹果开发),在android设备上则是在X5(腾讯基于webkit开发),在开发工具中运行在nwjs(同类型还有electron)

一个标准的小程序是由一个应用实例以及多个页面实例构成。仔细想来微信小程序不就是由多个相互关联的页面组成的嘛,在每个页面中,需要考虑与外部以及与其他页面进行交互。本着“3W+1H”原则,因此也就可以提炼出在开发整个IM DEMO过程中需要关注的点:

  • 如何定义页面、修改样式

  • 页面怎么进行屏幕适配

  • 多页面间怎么进行通信

  • 每个页面的生命周期过程

  • 如何定义组件、组件间如何通信

  • 局部与全局状态的通信

  • 交互事件的处理

  • 官方提供的一些组件以及能力

0x01 静态页面

在微信官网下载开发工具,然后新建一个小程序工程,会发现项目根目录下会有一个 app.json和project.config.json以及pages/logs 目录下的 logs.json,这里来阐述下它们的区别:

  • app.json 是对当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等,具体每一项代表什么可以查看

  • project.config.json是针对小程序开发工具的一个配置文件,记载了你针对开发工具配置进行的一些修改,例如界面颜色、编译配置等,详细配置可查看这里

  • page.json页面级的配置文件,可以单独定义该页面的一些属性,例如顶部颜色、是否可下拉、使用组件定义等,详细配置可查看这里

阐述完各种配置文件之后,可以开始进行页面的编程。传统的网页编程采用的三剑客 HTML+CSS+JS来实现,在微信小程序中同样有三剑客 WXML+WXSS+JS。

  • WXML与HTML十分类似,可以说就是带有模板语法,经过微信封装的自定义标签的集合。

    • 操碎了心的微信给我们封装了很多组件,例如view、button、text、map、video、audio等等,全部通过自定义标签的方式实现,部分组件渲染提升为原生组件,提高了整体效率(也带来了不少麻烦)。

    • 当然整个页面上还支持个人十分喜爱的一种模板语法-Mustache语法,与Vue类似,你可以在表达式中访问在data中已经定义好的数据,一旦数据发生变化,绑定的页面会自动刷新,实现渲染与逻辑分离。当然还需要条件、循环等控制能力,这些在整个模板中都有,更为详细的可以查看文档

  • WXSS说白了就是弱化版CSS,并在此基础上增加了尺寸单位rpx,以此为基础实现屏幕的适配(具体原理与rem方案适配屏幕类似);可以在页面wxss定义页面级样式,在app.wxss定义全局样式;仅仅支持部分css选择器(要特别注意)

  • JS和我们写网页的有些区别,所有的方法、属性均以Page实例中的对象属性的形式存在,我们可以在此声明微信提供的页面生命周期钩子、自定义方法以及页面数据。需要注意的是js中没有与DOM和BOM相关对象以及属性,也就是常见的window、document等是没有的。后面会阐述如果你想获取dom结构以及样式时的解决方案。

0x02页面间通信

整个小程序是由多个页面组成,有时候会遇到需要跨页面进行通信的场景,例如聊天跳转到聊天界面、删除、拉黑好友后通知外部进行好友列表的刷新等等。思考后有如下几种方式可供参考:

  • 页面跳转情况下可以通过querystring进行数据的传递。缺点是数据量受到querystring大小的限制而且仅仅局限于页面间跳转。

  • 全局状态下存储,每次变化后修改全局数据(localStorage或globalData),然后页面每次onShow时检查此全局数据,并做出相应的反应。缺点是显而易见的,业务复杂时,冗余代码十分多,且需要触发onShow方法,存在一定的局限性。

要想满足耦合性小、不局限于页面跳转通信、通信数据量不受限制这些需求,很明显发布/订阅模式(观察者模式)符合我们的要求。既能做到时间上解耦又能做到对象间解耦。iOS端的Notification Center以及android端的EventBus都是通过这一设计模式来处理跨页面间通信的的需求的。然后微信小程序内部并没有集成这一事件通知机制,因此需要手动去实现一个并将其与微信小程序的页面生命周期结合起来。

观察者模式是由调度中心、发布者、订阅者组成。订阅者会先在调度中心订阅某一特定事件并注册对应的回调函数,当发布者发布了该事件后,订阅中心就会取出订阅了该事件的所有订阅者注册的回调函数进行执行。

观察者模式不难实现,重点是如何在微信小程序中搭配其特有的生命周期来使用。本项目在用户登录以及注册成功时会初始化消息订阅中心,并全局(globalData)保存,使得订阅器一直驻存在内存中,调用时直接从globalData调用即可。当然这其中还存在一些小问题,在页面进行切换时需要注意订阅者、发布者之间的时序,比如订阅早于发布或者发布之后还未订阅的情况。后期会详细介绍该种模式的实现过程敬请期待。

0x03交互事件

传统的DOM事件传递类型有冒泡型与捕获型,微信小程序中自然也有。通常会使用bind、catch(冒泡)和capture-bind、capture-catch(捕获)前缀来装饰具体的交互事件,两者的区别如下:

  • bind绑定的事件不会阻止冒泡事件冒泡

  • catch绑定的事件阻止冒泡事件冒泡

而小程序支持的事件类型与传统的H5的差不多,新增了长按事件以及css动画相关触发(类似于Vue的js动画钩子)事件,具体为触摸事件touchstart、touchmove、touchcancel、touchend、tap;长按事件longpress、longtap;动画相关事件transitionend、animationstart、animationiteration、animationend;3Dtouch事件touchforcechange

整个事件命名还是较为清晰,基本做到了见名知意,详细可以查看文档

页面跳转时触发的钩子以及Page实例的生命周期,请自行查看官网,这里不再赘述,这部分内容同样重要。

说完了事件,肯定要说事件传参了,方法主要有两种:

  • 绑定到标签上,然后在event对象中获取(具体是target或currentTarget则视情况而定)

  • 使用页面状态数据或者全局数据

0x04 自定义组件

从小程序基础库版本 1.6.3 开始,它支持了组件化编程。组件类似于每一个页面,同样由四个文件构成json、wxml、wss、js,只在js中默认的一些钩子函数变化了、json中定义变化了,wxml和wss基本类似。多说一句,组件wxss中不应使用ID选择器、属性选择器和标签名选择器被你干了这么多,还剩啥。。。下面就来展开讲讲

对于json文件的话需要将 component 字段设为 true

对于wxml文件,它的写法与页面模板相同。组件模版与数据拼接后生成的节点树,将被插入到组件的引用位置上。但是组件模板多出一个功能就是:支持slot。用过vue的对它肯定十分熟悉,在制作容器组件(承载组件使用者提供的wxml结构)时用起来十分方便。同时它还支持多插槽(name区分),只需在js文件中声明下即可。

对于wxss文件,写法与css类似,只是有几点区别:作用域仅仅局限于组件内;只使用class选择器(其他选择器要么不支持,要么在特殊情况下会有问题);除了继承样式外,例如font、color等,全局样式(app.wxss)对自定义组件无效。至于外部引入样式则从 1.9.90 基础库才开始支持。。。

对于js文件则与前面的页面类似,整个js文件基本就是一个自定义组建的构造器,调用构造器可以指定组件的属性、数据、方法等。比较常用的有:

  • properties:Object Map

    • 外部传入组件的属性,用于模板渲染,可设置三个字段, type 表示属性类型、 value 表示属性初始值、 observer 表示属性值被更改时的响应函数(注意需要使用驼峰法写法,在wxml中则使用连字符写法)

  • data:Object

    • 组件内部数据,用于模板渲染

  • methods:Object

    • 组件的方法,包括事件响应函数和任意的自定义方法

  • 生命周期钩子

    • created: 组件实例进入页面节点树时执行,此时不能调用 setData

    • attached: 组件实例进入页面节点树时执行

    • ready: 组件布局完成后执行,此时可以使用 SelectorQuery获取节点信息

    • moved: 组件实例被移动到节点树的另一个位置时执行

    • detached:组件实例在页面节点树被移除时执行

  • behaviors:String Array

    • 组件间代码复用机制(类似于mixins)

组件实例this可以自组件方法、生命周期、属性observer中访问。通过组件实例可以获取许多有用的属性和方法,例如is(组件文件路径)、triggerEvent(触发事件,外部可监听)、setData(设置data并渲染视图)等

了解了组件的实现过程,接下来就是使用。用法很简单,只需在json文件中声明组件,然后在wxml中引入使用即可。

// index.json 引入组件,并定义引用名字{  "usingComponents": {    "input-modal": "/path/to/inputmodal"
  }
}// index.html 引入组件并传入属性以及监听事件<input-modal title="输入提醒"     catch:inputModalClick="tipClickHandler">
    <view>内部slot</view>
    </input-modal>// index.js 实现事件监听函数Page({
    tipClickHandler(e) {
        console.log('自定义组件事件');
    }
})

工程结构

整个微信小程序DEMO目录结构如下:

|- components 自定义组件目录
|- images 项目中使用的一些高频次图片
|- pages 主功能一级页面
    |- contact 通讯录页
    |- login 登录页
    |- recentchat 最近会话页
    |- register 注册页
    |- setting 设置页
|- partials 二级页面
    |- addfriend 添加好友页
    |- blacklist 黑名单页
    |- chatting 聊天页
    |- forwardcontact 转发消息通讯录页 
    |- historyfromcloud 云端历史记录页
    |- messagenotification 消息通知中心页
    |- modify 修改个人资料页
    |- personcard 非陌生人个人名片页
    |- strangercard 陌生人个人名片页
|- utils 存放一些工具类js
    |- config.js 存放项目的基本配置
    |- emojimap.js emoji文本与对应图片的映射关系,自定义emoji组件使用
    |- event.js 观察者模式具体实现
    |- imageBase64.js 存储一些小图标bese64编码
    |- imeventhandler.js 网易IM SDK初始化以及对应的回调函数注册,通过消息发布、订阅与外部通信
    |- pinyin.js 获取汉字的拼音
    |- util.js 一些工具方法的集合
|- vendors 引入外部的库,主要有网易云信 IM 的SDK以及md5加密
|- app.js 小程序根实例,存储了全局中的一些数据
|- app.json 注册页面以及定义页面一些基本样式
|- app.wxss 全局样式
|- project.config.json 设置整个小程序工程的一些属性,包括编译类型(截止2018年3月新增加了微信插件)、基础库版本等

技术栈的一些思考

这里探讨下目前(截止2018年3月)比较流行的三种开发微信小程序的方式:微信小程序原生、wepy、mpVue


微信小程序 wepy mpvue
开发规范 小程序开发规范 vue开发规范 vue开发规范
状态管理 vuex
组件化 比较原始 自定义组件规范 vue组件
多端复用 不可 可转化为H5 可转化为H5
构建方式 开发工具内置自动构建 框架内置 webpack
构建原理 开发工具自动构建 构建为dist后转化为小程序支持类型然后将开发工具指向dist目录,支持热更新 构建为dist后转化为小程序支持类型然后将开发工具指向dist目录,支持热更新

接着分析下云信IM DEMO的需求,首先受限于同一设备下一个用户的Storage的上限为10MB,所以这边不做聊天数据的持久化,所有的聊天数据、用户数据存储在内存中,在小程序被微信关闭(驻留后台过久)或者用户手动关闭(杀了微信进程)时所有数据会被重置;其次本期需求主要为p2p单聊,后期还会添加上群聊功能等功能,所以这边整体代码量需要控制,不能引入非必要框架;本期需求支持的消息类型有文本、emoji、地图、视频、语音、图片,部分组件可以借助微信提供的能力,加速渲染。。。

接下来大致评估下实现每个页面的技术点

一级页面:

  • 最近会话页

    • 滑动删除 - 自定义组件实现

    • 单条消息条目 - 全局拿到数据,然后进行清洗渲染

    • 消息通知 - 消息订阅器

  • 通讯录页

    • 昵称排序 - 汉字转拼音

    • 新增、拉黑、删除好友 - 消息订阅器

  • 设置页

    • 展示个人数据 - 数据清洗

    • 修改个人资料 - 调用照相、相册接口实现修改头像以及其他类型数据

  • 登录注册页

二级页面:

  • 聊天页

    • 聊天界面布局

    • emoji键盘 - 自定义emoji组件(图片资源存储在网易nos上)

    • 多种消息类型 - 支持语音、地理位置、文本、图像、视频、猜拳、emoji消息,本质就是实现了一个富文本渲染自定义组件,能够有效渲染不同的消息类型

    • 支持消息的多种手势操作,支持消息的撤消、删除、转发操作,单击不同类型消息实现语音、视频消息的播放

  • 个人资料

    • 分为两种,一种是陌生人个人资料、一种是好友个人资料,两种不同类型页面展示的页面组件是不一致的

    • 入口分为如下几种:单击通讯录条目进入好友信息列表;单击聊天记录头像进入好友列表;添加好友,结果不同则展示不同的类型用户资料

  • 修改个人资料页

    • 支持修改头像、昵称、性别、生日、手机、邮箱、签名,尽可能做到最大的复用

  • 黑名单列表

  • 消息通知界面

    • 自定义顶部tabbar组件

  • 聊天历史记录界面

初步结合框架特点以及几大开放方式特性,矛头重点集中在如何解决应用状态管理上面,经过评估后功能点较多,因此需要尽可能的减少引入外部框架,所以这边在微信小程序的基础上实现全局存储一个消息订阅器,然后在每个功能页面中订阅相应的事件,在相应的地方发布对应的事件。这样就解决了状态管理这个痛点。对于其他的一些区别个人觉得没有任何问题,对于一个有过现代前端开发经验,有使用过mvvm框架经验的开发者来说,入门小程序也就是几个小时的时间本人就是。既然花个几个小时能够入门小程序原生开发,为何还要去选那些坑较多,入门时间相同的框架呢。。。

因此制定了如下开发原则:尽量采用微信提供的原生组件,减少引入外部组件,手撸项目中所需的自定义组件,全局存储数据。页面间采用观察者模式进行通信

观察者模式

常规的观察者模式实现起来并不复杂,总结来说就是:订阅器中存储了所有订阅者注册的所有回调函数,当事件发生时,订阅器就会循环遍历所有的订阅者,并找出订阅该事件的订阅者所注册的回调函数并执行;取消订阅则是重订阅者数组中删除对应的回调函数。

结合在小程序中使用就是在一开始初始化登录组件时就初始化消息订阅器,并将其保存在全局数据(globalData)中,这样全局就驻留了该对象,在各个页面中就可以轻松调用订阅器中的订阅、发布方法来实现通信了。

// 订阅function _bind(eventName, callback, isOne, context) {  /* eslint valid-typeof: 0 */
  if (typeof eventName !== 'string' || typeof callback !== 'function') {    throw new Error('args: ' + stringStr + ', ' + functionStr + '')
  }  if (!hasOwnKey(__onfireEvents, eventName)) {
    __onfireEvents[eventName] = {}
  }
  __onfireEvents[eventName][++__cnt] = [callback, isOne, context]  return [eventName, __cnt]
}// 发布function fire(eventName) {  // fire events
  var args = slice(arguments, 1)
  setTimeout(function() {    if (hasOwnKey(__onfireEvents, eventName)) {
    _each(__onfireEvents[eventName], function(key, item) {
      item[0].apply(item[2], args) // do the function
      if (item[1]) delete __onfireEvents[eventName][key] // when is one, delete it after triggle
    })
  }
  })
}

上面是整个观察者模式的核心:订阅、发布,当然如果你还想继续完善,可以尝试增加命名空间来防止事件名冲突以及增加离线事件支持。

自定义组件

微信小程序自定义组件比较简单,详情可以查看。这里就以聊天界面中使用的自定义emoji组件举例,来阐述如何实现一个自定义组件。

组件的定义方式,以及对应的生命周期钩子这边就不再说明,请查阅上面文档。本组件借助了小程序提供的swiper组件(省的自己判断scroll的位置来切换页面),每个swiper-Item里面再通过模板循环出每张emoji图片,而每个emoji的key对应线上地址则由自己提前准备好的一个已经抽象好的js提供,每组swiper内含有的emoji数量则通过程序自动分割,并在最后添加删除按钮。

// 为每页分配对应的emojifunction splitAlbumKeys(arr, space, currentAlbum) {  const delta = space || 23
  let result = []  let factor = Math.ceil(arr.length / delta)  let begin = 0
  let end = 1
  if (factor == 1) {
    result = [[...arr]]
  } else {    for (let i = 1; i < factor; i++) {      let temp = []
      temp = [...arr.slice(begin, i * delta)]
      begin = i * delta
      result.push(temp)
    }
    result.push([...arr.slice(delta * (factor - 1), arr.length)])
  }  if (currentAlbum == 'emoji' || this.data.currentAlbum == 'emoji') { // 只有emoji才添加删除按钮
    result.map((cata, index) => {      if(index != (result.length-1)) {
        cata.push('[删除]')
      }
    })
  }  return result
}// 单击emoji,向外传递事件function emojiTap(e) {  let emoji = e.target.dataset.emoji  if (!emoji) return
  this.triggerEvent("EmojiClick", emoji)
}// 单击发送,向外传递事件function emojiSend () {    this.triggerEvent("EmojiSend")
}

在外部使用过程中,只需要监听对应的事件即可.

<component-emoji bind:EmojiClick="emojiCLick" bind:EmojiSend="emojiSend"></component-emoji>

遇到的一些坑

微信小程序开发可以说是在坑中前行,经常会遇到一些很奇怪的问题,在此记录在册,希望后来人可以跳过,增加开发效率

  • 小程序模板引擎的列表循环支持数组,不支持对象

  • text 组件实质是行内标签

  • background-image 只能用网络url或者base64

  • 注意事件对象中target 和 currentTarget的区别

  • URL 传参数时微信会自动拦截'=',导致后面页面onLoad中options参数容易解析出错

  • 二级页面无法再使用tabbar,必须自定义

  • 自定义组件中methods对象中定义的方法必须使用es5的函数定义

  • 注意多种小程序授权情况

    • 直接同意

    • 拒绝后,进引导,继续拒绝

    • 拒绝后,进引导,点击授权,进入授权设置页,再点授权

    • 拒绝后,进引导,点击授权,进入授权设置页,直接退出

  • input组件渲染级别提升为原生,某些布局下会出问题

  • 注意某些提升为原生组件(例如video、map等)导致的层级问题

  • 当然还有一些微信提供的createSelectorQuery的一些问题,只能等待大家去探索了

这边仅仅是抛出一些我开发中遇到的部分问题,在整个开发过程中踩过的坑远远不止这些,希望我们一起在坑中前行。。。

写在最后

初步统计本项目大概8500行代码(去除IM SDK以及MD5加密库),换句话说不到9000行代码,你就能在微信中实现一个mini 微信,这一切均借助于云信IM SDK强大的即时通讯能力。当然本期版本还存在很多不足的地方,希望在做第二期群聊功能的时候,能继续升级整体的组织架构。

网易云信IM免费试用请点击这里


更多网易技术、产品、运营经验分享请访问网易云社区

相关文章:
【推荐】 十年•杭研程序猿 | 反垃圾运营的匠心之路
【推荐】 微服务的接入层设计与动静资源隔离