此文已由作者刘诗川授权网易云社区发布。
欢迎访问网易云社区,了解更多网易技术产品运营经验。
前段时间做了一个小程序的项目,出于一些原因,除了小程序的相关开发外,还使用到了小程序提供的webview组件,这里对其中小程序开发中遇到的一些问题和解决办法进行一些总结。
我的这部分小程序工作中经常要用到小程序的<web-view>
组件,具体原因后面会做介绍。
这些都是微信强制规定的,目的应该就是降低开发者使用webview的频率和场景,限制开发者在小程序框架内开发,一方面确实能提高小程序的体验,一方面也是为其他竞争平台制造技术壁垒。但是现在已经有很多技术方案(taro
、uni-app
、Megalo
等等)通过内部转换,基本实现了多平台上一致的开发体验,所以用什么技术已经不是主要问题了。这里也不详细讨论,本文主要介绍不得不在微信小程序上开发时的一些经验总结。
前两个特点受微信小程序的机制所限,基本是没有办法绕开的,只能按照微信的要求来,第三点,还是能通过一些办法来实现的。
微信小程序是提供了webview内网页向小程序发送消息的api的,但是这个能力受到了很大的限制,但是经过我的测试,确实也没有其他不借助第三方的方法直接在web里向小程序发送消息:
<!-- html -->
<script type="text/javascript" src="https://res.wx.qq.com/open/js/jweixin-1.3.2.js"></script>
<script>
wx.miniProgram.postMessage({ data: 'foo' })
wx.miniProgram.postMessage({ data: {foo: 'bar'} }) //建议采用这种格式,社区中有反馈上一种数据格式在有的安卓机上无法收到
</script>
还有一点需要注意的是:
网页向小程序 postMessage 时,会在特定时机(小程序后退、组件销毁、分享)触发并收到消息。e.detail = { data }
所以这个消息并不是实时的,小程序内部会把消息都压入消息栈,在触发时一起收到。
经过我的实践,如果需要保存一些重要数据,依赖postMessage
机制让小程序来兜底无法完全的到保证,测试阶段就发现有的安卓机即使按要求发送了数据依然在页面后退或者销毁时无法接收到,但是触发分享的时候基本还是能保证收到消息的。所以除了传递一些分享场景所需要的数据,其他重要数据还是建议直接通过Web发送到服务端来保存。
这里主要介绍一下如何通过小程序向Web发送消息,其原理利用就是<web-view>
组件的src
属性,动态改变页面的hash
,它不会导致页面刷新,还可以做到实时的消息传递。 比如我们产品中有一个场景,用户的小程序切换到后台或者用户的微信切换到后台时,停止消耗用户的阅读时长,就需要利用小程序的前后台的onShow
和onHide
事件,而Web这边的visibilitychange
事件的兼容性还没有那么乐观。
<web-view src="https://du.163.com#isShow={{isShow}}"></web-view>
Web这边要做的就是监听hashchange
事件再做处理就可以了
window.addEventListener("hashchange", () => {
const hashData = parseUrl(location.hash.slice(1));
if(hashData.isShow){
const isShow = hashData.isShow === 'true';
if(isShow){
//切到前台
this.sendReadLog(true); //上报阅读记录,并且开启定时上报
this.createWs(); //重新打开websocket,接收消息通知
}else{
//切到后台
this.sendReadProgress(true); //通过web上报一次阅读进度,防止阅读进度丢失
this.sendReadLog(false); //上报阅读记录,并且关闭定时上报
this.disconnectWs(); //关闭websocket,防止消息通知在后台展示
}
}
}, false);
小程序的web-view
组件其实与我们常见的客户端内的webview容器没有本质的区别,上面这种消息传递机制导致了一个问题:
webview容器就相当于一个浏览器,浏览器对于每个打开的标签页都会维护一个历史栈,小程序每次更改web-view
的src
属性向Web传递消息,都会增加这个历史栈的长度,而小程序的顶部导航栏是没有按钮可以直接关闭web-view
的,这就意味着传递了多少次消息,退出这个小程序页面的时候就需要多点击多少次返回按钮,这是相当影响用户体验的。
修改hash造成的多次返回问题:
想要解决这个问题,就需要借助onpopstate
事件,因为不管是修改web-view
的src
发送消息,还是点击返回按钮(包括右滑手势返回),都会触发浏览器历史栈的改变,不同的是:
所以,我们可以根据这个不同点来进行区分。一旦onpopstate
事件触发,但是历史栈长度又没有发生变化,此时可以判断用户是需要退出本页,直接调用小程序的api来直接退出整个页面,最终效果还是很理想的。
let oldHistoryLength = history.length;
window.onpopstate = () => {
//每次小程序切换前后台,都会修改history栈,触发popstate事件,并且增加history栈的长度,
//而点击返回按钮虽然会触发popstate事件,但是并不会修改堆栈长度,通过这个区别知道用户是想点击返回退出正文,调用小程序的返回接口
if(history.length > oldHistoryLength){
oldHistoryLength = history.length;
}else{
window.wx.miniProgram.navigateBack();
}
}
web-view
关联小程序与URS账号体系通常情况下,产品的小程序都只使用微信的账号体系就可以满足需求了,这样的话接入和使用也比较简单,但是我们所做的这个小程序需要使用原有账号体系下的一些数据和内容,所以不得不同时支持微信小程序账号体系与产品现有账号体系,并且将他们关联起来。 URS是公司的帐号体系负责部门,主要对外提供有关邮箱帐号、手机帐号、三方帐号的注册登录及其他相关密保服务。蜗牛读书这边使用的网易的URS账号体系,在客户端与Web端都是使用URS提供的登录SDK来实现账号的注册和登录功能。 由于URS这边并没有直接提供登录验证的接口,而是提供的一个Javascript SDK(其原理就是动态加载一个iframe
对输入进行验证后再写入cookie),所以如果要在微信账号体系之外使用其他登陆方式(邮箱、手机号等),就知道从web-view
上想办法了,以下是整个登录流程的示意图:
code
,再把这个code
发送到产品服务端,产品服务端再去向小程序服务器验证并获取用户的一些数据UnionID
,从而避免使用同一个微信产生2个不同的用户的情况dl.reg.163.com
和dl2.reg.163.com
加入小程序的业务域名,这里需要配合做一个验证,还好URS这边还是比较愿意配合的authToken
,再通过小程序的JS SDK
回传给小程序中转页authToken
后,将它储存下来,并跳转回之前的小程序页面JS SDK
,小程序中转页的作用是避免在每个需要登录的页面都得引入登录相关的逻辑代码,把这块的功能抽离出来,其他页面如果需要登录的话只需要跳转到小程序登陆页即可这个流程的核心就在于使用authToken
来作为小程序内部的登陆凭据,一旦在小程序内存储authToken
,之后所有的请求header
中都会附带这个信息,而这个authToken
一旦过期,后端的响应就会返回http status 401
的错误,遇到这个错误的时候就统一再跳转到登陆页要求用户重新登陆。
由于业务需要,我们的小程序需要实现类似webview原生的长按选中文字然后出现操作菜单的功能,类似下图:
这个就是我们很常见的,webview或者手机端浏览器的长按选中文字以及菜单的效果。 而我们业务需要的下面这个效果:
这个效果之前是在客户端内webview上实现了的,但那是因为客户端对自己使用的webview有足够的控制权,但是在小程序的webview里如果使用原生的选中机制就无法隐藏原生的选中菜单(出现2个菜单肯定是不好看的),所以这里的选中机制得自行实现了。
<span>
,然后再通过监听页面的手势事件touchstart
、touchmove
和touchend
来识别长按的事件的发生以及详细的信息;document.elementFromPoint(x, y)
,这个api的功能就是根据给定的相对于视口的坐标获取该位置的节点元素,就是因为这个api没能在小程序的体系中找到合适替代者,所以才选择了在小程序的web-view
组件中进行开发和实现;基本的实现方式就是上面所述,但是还需要解决几个性能问题:
throttle
方法来进行限流,设置其一定时间间隔内最多运行一次document.elementFromPoint(x, y)
,在每次滚动结束获取视口内的可见段落,只对这部分段落逐字渲染,不可见的段落还是整段渲染,这样就大大减少了dom元素的数量,但是又较好的保证的长按选中文字的体验dobounce
方法,限制滚动结束后一定时间内没有再次发生滚动时,才进行获取,防止用户的快速的多次滚动引起不必要的计算。我们在小程序中使用了WebSocket
来做一些实时消息的推送和展示,STOMP的具体内容本文不做详细展开,简单来说它就是一个WebSocket
通信中比较常用的消息协议,有了它我们可能更方便更专注的利用WebSocket
完成我们的一些业务需求。
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。
直接上代码
const Stomp = require('../../utils/stomp.js').Stomp;
createSocket: function () {
//新建一个简单的websocket对象
const ws = {
send: msg => {
wx.sendSocketMessage({
data: msg
})
},
close: () => {
console.log('stomp is close')
}
}
// stomp.setInterval是用来发心跳包的,而小程序没有window对象
Stomp.setInterval = function (interval, f) {
return setInterval(f, interval);
}
Stomp.clearInterval = function (id) {
return clearInterval(id);
}
//新建stomp客户端
const client = Stomp.over(ws);
client.debugger = console.log
//一些事件的处理
wx.onSocketError(function (err) {
console.log('socket err:', err)
})
wx.onSocketClose(() => {
client.disconnect()
})
//连接websocket
wx.connectSocket({
url: `wss://${config.host}/ws`,
header: {
'X-Auth-Token': wx.getStorageSync('authToken')
},
success: () => {
//连接stomp,connect方法有2个参数时第一个代表header
client.connect({}, () => {
console.log('stomp connect')
//订阅消息
client.subscribe(`/topic/shareRead.${this.shareReadId}`, this.handleMessage)
client.subscribe(`/user/topic/shareRead.${this.shareReadId}`, this.handleMessage)
}, (err) => {
console.log('err', err)
})
}
})
},
_handleMessage: function (message) {
console.log('websokcet get message !', message.headers.destination);
//发送消息回执
message.ack({
destination: message.headers.destination,
'message-id': message.headers['message-id']
})
},
以上就是我在开发小程序过程中的一些经验,有不当之处欢迎讨论指出,谢谢。
免费领取验证码、内容安全、短信发送、直播点播体验包及云服务器等套餐
更多网易技术、产品、运营经验分享请点击。