Fundebug专注于JavaScript、微信小程序、微信小游戏,Node.js和Java实时BUG监控。真的是一个很好用的bug监控费服务,众多大佬公司都在使用。
接下来说说使用频率比较多,功能强大,但又有比较多坑的几个组件
2.1 web-view
web-view的出现,让小程序和H5网页之前的跳转成为了可能。通过把H5页面放置到web-view中,可以让H5页面在小程序内运行。同时在H5页面中也可以跳转回小程序页面。可以说是带来了很大的便利,但同时由于web-view的诸多限制,用起来也不是很舒服。
- 需要打开的H5页面必须在后台业务页面中配置,这其中还有个服务校验。另外H5页面必须是https协议,否则无法打开
- web-view中无法在页面中调起分享,如果需要分享,比如跳回小程序原生页面
- 小程序与web-view里H5通信问题。小程序向web-view传递,不敏感信息可以通过页面url传递。如果是敏感信息比如用户token等,可以让服务端重定向,比如请求服务端一个地址,让他把敏感信息写在cookie中,再重定向到我们的H5页面。之后H5页面就可以通过在cookie中拿这些敏感数据了,或者http-only,发送请求时直接带上。
- 每次web-view中src值有变化就会重新加载一次页面。所以个src拼接参数时,需要先赋值给个变量拼接好再一次性setData给web-view的src,防止页面重复刷新
- 从微信客户端6.7.2版本开始,
navigationStyle: custom
对组件无效。也就意味着使用web-view时,自带的导航栏无法去掉。- 因为导航栏无法去掉,这里就出现了一个巨大的坑。实现全屏效果问题。如果想要实现H5页面全屏,就是不滑动,全屏显示完所有内容。这时如果你使用
width:100%;height:100%
,你会发现,你页面底部可能会缺失一段。上图:
因为web-view是默认铺满全屏的,也就是web-view宽高和屏幕宽高一样。然后H5页面这是高度100%,这是相对web-view的高度,也是屏幕高度。但是关键问题:web-view里H5页面是从导航栏下开始渲染的。这就导致了H5页面溢出了屏幕,无法达到全屏效果。
解决方法
这个问题我在前段时间的实际项目碰到过,我们要做个H5游戏,要求是全屏,刚开始我也是设置高度100%。后来发现底部一块不见了。我的解决方法比较粗暴,如果有更好的解决方法,欢迎评论交流。 我的解决方法是:通过拼接宽高参数在H5页面url上,这个宽高是在web-view外层计算好的。H5页面直接读取url上的宽高,动态设置页面的宽高。页面高度的计算,根据上图,很显然就是屏幕高度减去导航栏高度。宽度都是一样的,直接是屏幕宽度。
但问题又来了,貌似没有途径获取导航栏高度。而且对于不同机型的手机,导航栏高度不同。经过了对多个机型导航栏跟屏幕高度的比较。发现了一个规律,导航栏高度与屏幕高度、屏幕宽高比有一定的关系。所以根据多个机型就计算出了这个比例。这解决了95%以上手机的适配问题,只有少数机型适配不是很好。到基本实现了全屏效果。具体代码如下:
onLoad (options) {
//同步获取屏幕信息,现在用到的是屏幕宽高
var res = wx.getSystemInfoSync();
if (res) {
var widHeight = res.screenHeight;
//对于大多数手机,屏幕高度/屏幕宽度 = 1.78。此时导航栏占屏幕高度比为0.875
var raito = 0.875;
if (res.screenHeight / res.screenWidth > 1.95) {
//对于全屏手机,这个占比会更高些
raito = 0.885;
} else if (res.screenHeight / res.screenWidth > 1.885) {
raito = 0.88;
}
//做兼容处理,只有微信版本库高于6.7.2,有导航栏才去兼容,否则可以直接使用高度100%。res.statusBarHeight是手机顶部状态栏高度
//如果微信版本号大于6.7.2,有导航栏
if (util.compareVersion(res.version, "6.7.2") > 0) {
widHeight = Math.round(widHeight * raito) + (res.statusBarHeight || 0);
}
this.setDate({
//将H5页面宽高拼接在url上,赋值给web-view的src即可加载出H5页面
webview_src: util.joinParams(h5_src, {
"height": widHeight,
"width": res.screenWidth
})
})
}
}
复制代码
2.2 scroll-view
当我们要实现一个区域内滑动效果时,在H5页面中我们设置overflow-y: scroll
即可。但在小程序中,没有该属性。需要用到scroll-view标签。具体操作实现我们可以查看文件scroll-view。
锚点定位在前端开发中会经常用到,在H5页面中,我们会在url后面加上#来实现锚点定位效果。但是在小程序中这样是不起作用的,因为小程序内渲染页面的容易不是一个浏览器,无法实时监听Hash值得变化。但是使用scroll-view,我们可以实现锚点点位效果。主要是使用scroll-into-vie属性具体实现我们直接上代码
scroll-into-view | String | 值应为某子元素id(id不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素
wxml文件
复制代码
2.3 canvas
画布标签,它是原生组件,所以它必须位于屏幕最上边,而且是不能隐藏的。所以如果想要使用canvas动态生成分享照片。那你要设置她的宽高和屏幕一样。要不导出为照片时就会失真。因为这个原因,所以生成分享照片还是有服务端实现吧,照片失真太严重了。
给用户发送消息对一个小程序是非常重要的,它可以召唤回用户,导量效果非常明显。我们可以通过模板消息想小程序用户发送消息,但前提是我们得获取到openid和formid。用户登录我们即可即可获取到用户openid。而只要用户有点击行为,我们即可获取到formid获取formid。所以说formid是很重要的。我们可以提前收集好formid,在需要的时候给用户推送消息。我们可以个每个button都包上form标签,只要有用户点击行为都可以收集到formid.
复制代码
我们实现一个formid收集系统,为了尽量减少冗余代码和减少对业务的影响,我们的设计是这样的
formTpye=submit
的button有点击都能获取到formid。wxml文件
复制代码
page.js文件
//每次用户有点击,都将formid添加到全局数组中
formSubmit(e) {
//需要实时发送的,不添加
if(e.target.dataset.sendMsg){
formid = e.detail.formId;
return;
}
app.appData.formIdArr.push(e.detail.formId);
}
复制代码
app.js
onHide: function () {
//小程序切到后台时上传formid
this.submitFormId();
},
复制代码
从用户打开小程序到小程序销毁,我们可以想想有哪些地方是可以优化的。首先是打开速度。小程序打开速度直接影响了用户留存。在小程序后台,运维中心-监控告警下有个加载性能监控数据,我们可以看到小程序启动总耗时、下载耗时、首次渲染耗等加载相关的数据。而这里的打开速度其实就是小程序的启动总耗时。它包括了代码包下载、首次渲染,微信内环境初始化等步凑。在这一步,我们能做的就是如何加快代码包下载速度和减少首次渲染时间
在小程序呈现给用户之后,接下来如何提高用户体验,增强小程序健壮性的问题了。每个程序都有bug。只是我们没发现而已,尽管在测试阶段,我们进行了详尽的测试。但是在实际生产环境,不同的用户环境,不同的操作路径,随时会触发一些隐藏的bug。这时如果用户没有向我们报告,我们是无法获知的。所以有必要给我们的小程序增加错误信息收集,js脚本错误,意味着整个程序挂掉了,无法响应用户操作。所以对于运行时的脚本错误,我们应该上报。对出现的bug及时修复,增强程序健壮性,提供用户体验。
每个程序都有大量的前后端数据交互,这是通过http请求进行的。因此,还有一个错误信息收集就是接口错误信息收集。对那些请求状态码非2XX、3XX的,或者请求接口成功了,但是数据不是我们预期的,都可以进行信息采集。
通过对小程序运行时脚本和http请求进行监控,我们就可以实时了解我们线上小程序的运行状况,有什么问题可以及时发现,及时修复,极高地提高了用户体验性。
4.1 让小程序更快
让小程序快,主要因素有两个,代码包下载和首屏渲染。 我们来看一个数据:
前面状态小程序代码大小是650Kb左右,这是下载耗时(虽然跟用户网络有关,但这个是全部用户平均时间)是1.3s左右。但是经过优化,将代码包降低至200kb左右时。下载耗时只有0.6s左右。所以说,代码包减少500kb,下载耗时能减少0.5s。这个数据还是非常明显和。所以说,在不影响业务逻辑的情况下,我们小程序代码包应该尽可能地小。那么如何降低代码包大小呢?以下有几点可以参考
接下来是首屏渲染,从上图的小程序生命周期可以看出,从加载首页代码带首页完成渲染,这段时间就是白屏时间,也就是首次渲染时间。而小程序在这段时间内,主要工作是:加载首页代码、创建View和AppService层、初试数据传输、页面渲染。在这四个步骤中,加载首页代码,前面已经说过;创建View和AppService层,是微信完成的,跟用户手机有关,这不是我们可控的。我们能做的就是减少初试数据传输时间和页面渲染时间。
Page({
//与页面渲染有关的数据放这里
data: {
goods_list:[]
},
//与页面渲染无关的数据放这里
_data: {
timer: null
}
})
复制代码
4.2 让小程序更强
接下来就是给小程序增加错误信息收集,包括js脚本错误信息收集和http请求错误信息收集。前段时间,在时间工作开发中,为了更好的复用和管理,我把这个错误信息收集功能做成了插件。然而做成插件并没有想象中的那么美好,下面再具说。
脚本错误收集
对于脚本错误收集,这个相对比较简单,因为在app.js中提供了监听错误的onError函数
只不过错误信息是包括堆栈等比较详细的错误信息,然后当上传时我们并不需要这么信息,第一浪费宽带,第二看着累又无用。我们需要的信息是:错误类型、错误信息描述、错误位置。
thirdScriptError
aa is not defined;at pages/index/index page test function
ReferenceError: aa is not defined
at e.test (http://127.0.0.1:62641/appservice/pages/index/index.js:17:3)
at e. (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:31500)
at e.a (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:26386)
at J (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:20800)
at Function. (http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:22389)
at http://127.0.0.1:62641/appservice/__dev__/WAService.js:16:27889
at http://127.0.0.1:62641/appservice/__dev__/WAService.js:6:16777
at e.(anonymous function) (http://127.0.0.1:62641/appservice/__dev__/WAService.js:4:3403)
at e (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20291)
at r.registerCallback.t (http://127.0.0.1:62641/appservice/appservice?t=1543326089806:1080:20476)
复制代码
这是错误信息字符串,接下来我们对它进行截取只需要拿我们想要的信息即可。我们发现这个字符串是有规则的。第一行是错误类型,第二行是错误详情和发生的位置,并且是";"分好分开。所以我们还是很容易就可以拿到我们想要的信息。
//格式化错误信息
function formateErroMsg(errorMsg){
//包一层try catch 不要让信息收集影响了业务
try{
var detailMsg = '';
var detailPosition= '';
var arr = errorMsg.split('\n')
if (arr.length > 1) {
//错误详情和错误位置在第二行并用分好隔开
var detailArr = arr[1].split(';')
detailMsg = detailArr.length > 0 ? detailArr[0] : '';
if (detailArr.length > 1) {
detailArr.shift()
detailPosition = detailArr.join(';')
}
}
var obj = {
//错误类型就是第一行
error_type: arr.length > 0 ? arr[0] : '',
error_msg: detailMsg,
error_position: detailPosition
};
return obj
}catch(e){}
}
复制代码
获取到我们想要的信息,就可以发送到我们服务后台,进行数据整理和显示,这个需要服务端配合,就不深入讲了,我们拿到了数据,其他都不是事。
http请求错误信息收集 对于http请求错误信息收集方式,我们尽量不要暴力埋点,每个请求发送前发送后加上我们的埋点。这样工作量太大,也不易维护。因此,我们可以从底层出发,拦截wx.request请求。使用Object.definePropert对wx对象的request进行重新定义。具体实现如下
function rewriteRequest(){
try {
const originRequest = wx.request;
Object.defineProperty(wx, 'request', {
configurable:true,
enumerable: true,
writable: true,
value: function(){
let options = arguments[0] || {};
//对于发送错误信息的接口不收集,防止死循环
var regexp = new RegExp("https://xxxx/error","g");
if (regexp.test(options.url)) {
//这里要执行原来的方法
return originRequest.call(this, options)
}
//这里拦截请求成功或失败接口,拿到请求后的数据
["success", "fail"].forEach((methodName) => {
let defineMethod = options[methodName];
options[methodName] = function(){
try{ //在重新定义函数中执行原先的函数,不影响正常逻辑
defineMethod && defineMethod.apply(this, arguments);
//开始信息收集
let statusCode, result, msg;
//请求失败
if (methodName == 'fail') {
statusCode = 0;
result = 'fail';
msg = ( arguments[0] && arguments[0].errMsg ) || ""
}
//请求成功,
//收集规则为:
// 1、 statusCode非2xx,3xx
// 2、 statusCode是2xx,3xx,但接口返回result不为ok
if (methodName == 'success') {
let data = arguments[0] || {};
statusCode = data.statusCode || "";
if (data.statusCode && Number(data.statusCode) >= 200 && Number(data.statusCode) < 400 ) {
let resData = data.data ? (typeof data.data == 'object' ? data.data : JSON.parse(data.data)) : {};
//请求成功,不收集
if (resData.result == 'ok') {
return;
}
result = resData.result || "";
msg = resData.msg || "";
}else{
result = "";
msg = data.data || "";
}
}
//过滤掉header中的敏感信息
if (options.header) {
options.header.userid && (delete options.header.userid)
}
//过滤掉data中的敏感信息
if (options.data) {
options.data.userid && (delete options.data.userid)
}
var collectInfo = {
"url": options.url || '', //请求地址
"method": options.method || "GET", //请求方法
"request_header": JSON.stringify(options.header || {}), //请求头部信息
"request_data": JSON.stringify(options.data || {}), //请求参数
"resp_code": statusCode + '', //请求状态码
"resp_result": result, //请求返回结果
"resp_msg": msg, //请求返回描述信息
}
//提交参数与上一次不同,或者参数相同,隔了1s
if (JSON.stringify(collectInfo) != lastParams.paramStr || (new Date().getTime() - lastParams.timestamp > 1000)) {
//上传错误信息
Post.post_error(_miniapp, 'http', collectInfo)
lastParams.paramStr = JSON.stringify(collectInfo);
lastParams.timestamp = new Date().getTime()
}
}catch(e){
//console.log(e);
}
};
})
return originRequest.call(this, options)
}
})
} catch (e) {
// Do something when catch error
}
}
复制代码
在不使用插件的小程序中,我们可以在使用wx.request方法执行上面的代码,对wx.request进行拦截,然后其他无需加任何代码就可以收集http请求了。 上面说了,当我们封装成到插件时,这个就不管用了,因为当使用插件时,小程序不允许我们修改全局变量。所以执行上面代码时会报错。这时,我们退而求其次,只能是在插件中自己封装个方法,这个方法其实就是wx.request发送请求,但是在插件中我们就有可以拦截wx.request了。具体实现如下:
function my_request(){
//只要执行一次拦截代码即可
!_isInit && rewriteRequest();
return wx.request(options)
}
复制代码
接下来我们看下后台数据
持续监控,会帮我们找出很多隐藏的bug
洋洋洒洒写了这么多,或许有些地方说的不太清楚,慢慢锻炼吧。然后后面几点只是挑了重要的讲,我相信有过小程序开发经验的朋友应该没问题。然后有时间再补充和优化了。先到此,有缘看到的朋友,欢迎留言交流。