微信小程序开发总结与心得(二)

Fundebug专注于JavaScript、微信小程序、微信小游戏,Node.js和Java实时BUG监控。真的是一个很好用的bug监控费服务,众多大佬公司都在使用。

2 重点介绍几个组件


接下来说说使用频率比较多,功能强大,但又有比较多坑的几个组件

2.1 web-view


web-view的出现,让小程序和H5网页之前的跳转成为了可能。通过把H5页面放置到web-view中,可以让H5页面在小程序内运行。同时在H5页面中也可以跳转回小程序页面。可以说是带来了很大的便利,但同时由于web-view的诸多限制,用起来也不是很舒服。

  1. 需要打开的H5页面必须在后台业务页面中配置,这其中还有个服务校验。另外H5页面必须是https协议,否则无法打开
  2. web-view中无法在页面中调起分享,如果需要分享,比如跳回小程序原生页面
  3. 小程序与web-view里H5通信问题。小程序向web-view传递,不敏感信息可以通过页面url传递。如果是敏感信息比如用户token等,可以让服务端重定向,比如请求服务端一个地址,让他把敏感信息写在cookie中,再重定向到我们的H5页面。之后H5页面就可以通过在cookie中拿这些敏感数据了,或者http-only,发送请求时直接带上。
  4. 每次web-view中src值有变化就会重新加载一次页面。所以个src拼接参数时,需要先赋值给个变量拼接好再一次性setData给web-view的src,防止页面重复刷新
  5. 从微信客户端6.7.2版本开始,navigationStyle: custom对组件无效。也就意味着使用web-view时,自带的导航栏无法去掉。
  6. 因为导航栏无法去掉,这里就出现了一个巨大的坑。实现全屏效果问题。如果想要实现H5页面全屏,就是不滑动,全屏显示完所有内容。这时如果你使用width:100%;height:100%,你会发现,你页面底部可能会缺失一段。上图:

 

微信小程序开发总结与心得(二)_第1张图片

 

 

因为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动态生成分享照片。那你要设置她的宽高和屏幕一样。要不导出为照片时就会失真。因为这个原因,所以生成分享照片还是有服务端实现吧,照片失真太严重了。

3 formid收集


给用户发送消息对一个小程序是非常重要的,它可以召唤回用户,导量效果非常明显。我们可以通过模板消息想小程序用户发送消息,但前提是我们得获取到openid和formid。用户登录我们即可即可获取到用户openid。而只要用户有点击行为,我们即可获取到formid获取formid。所以说formid是很重要的。我们可以提前收集好formid,在需要的时候给用户推送消息。我们可以个每个button都包上form标签,只要有用户点击行为都可以收集到formid.

    
复制代码

我们实现一个formid收集系统,为了尽量减少冗余代码和减少对业务的影响,我们的设计是这样的

  1. 在整个页面的最外层包裹form标签,不是每个button都包裹一个,这样只要是页面中formTpye=submit的button有点击都能获取到formid。
  2. formid保存在全局变量数组中,当小程序切换到后台是一次性发送。
  3. 对于需要实时发送消息的,不添加值全局数组中,直接保存在页面变量中。

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();
    },
复制代码

4 性能优化相关


从用户打开小程序到小程序销毁,我们可以想想有哪些地方是可以优化的。首先是打开速度。小程序打开速度直接影响了用户留存。在小程序后台,运维中心-监控告警下有个加载性能监控数据,我们可以看到小程序启动总耗时、下载耗时、首次渲染耗等加载相关的数据。而这里的打开速度其实就是小程序的启动总耗时。它包括了代码包下载、首次渲染,微信内环境初始化等步凑。在这一步,我们能做的就是如何加快代码包下载速度和减少首次渲染时间

在小程序呈现给用户之后,接下来如何提高用户体验,增强小程序健壮性的问题了。每个程序都有bug。只是我们没发现而已,尽管在测试阶段,我们进行了详尽的测试。但是在实际生产环境,不同的用户环境,不同的操作路径,随时会触发一些隐藏的bug。这时如果用户没有向我们报告,我们是无法获知的。所以有必要给我们的小程序增加错误信息收集,js脚本错误,意味着整个程序挂掉了,无法响应用户操作。所以对于运行时的脚本错误,我们应该上报。对出现的bug及时修复,增强程序健壮性,提供用户体验。

每个程序都有大量的前后端数据交互,这是通过http请求进行的。因此,还有一个错误信息收集就是接口错误信息收集。对那些请求状态码非2XX、3XX的,或者请求接口成功了,但是数据不是我们预期的,都可以进行信息采集。

通过对小程序运行时脚本和http请求进行监控,我们就可以实时了解我们线上小程序的运行状况,有什么问题可以及时发现,及时修复,极高地提高了用户体验性。

4.1 让小程序更快


让小程序快,主要因素有两个,代码包下载和首屏渲染。 我们来看一个数据:

 

微信小程序开发总结与心得(二)_第2张图片

 

 

前面状态小程序代码大小是650Kb左右,这是下载耗时(虽然跟用户网络有关,但这个是全部用户平均时间)是1.3s左右。但是经过优化,将代码包降低至200kb左右时。下载耗时只有0.6s左右。所以说,代码包减少500kb,下载耗时能减少0.5s。这个数据还是非常明显和。所以说,在不影响业务逻辑的情况下,我们小程序代码包应该尽可能地小。那么如何降低代码包大小呢?以下有几点可以参考

  1. 因为我们上传代码到微信服务器时,它会将我们的代码进行压缩的,所以用户下载的代码包并不是我们开发时的那个大小。对此,开发时也没必要删空行、删注释这些。在开发工具项目详情中可以看到上次上传大小,这个大小就是用户最终使用的大小。如果觉得微信压缩还不够好,可以通过第三方工具对我们代码进行一次压缩再上传,然后对比效果,有没有更小。这个没有使用过。如果有什么好工具,欢迎推荐。
  2. 将静态资源文件防止到我们自己服务器或者cdn上。一个小程序,最耗空间的往往是图片文件。所以我们可以抽离出来,图片文件可以异步获取,在小程序启动以后再去获取。这样,代码包就会小很多。
  3. 使用分包加载。小程序提供了分包加载功能。如果你的小程序很庞大,可以考虑使用分包加载功能,先加载必要功能代码。这样就是可以极大降低代码包大小

接下来是首屏渲染,从上图的小程序生命周期可以看出,从加载首页代码带首页完成渲染,这段时间就是白屏时间,也就是首次渲染时间。而小程序在这段时间内,主要工作是:加载首页代码、创建View和AppService层、初试数据传输、页面渲染。在这四个步骤中,加载首页代码,前面已经说过;创建View和AppService层,是微信完成的,跟用户手机有关,这不是我们可控的。我们能做的就是减少初试数据传输时间和页面渲染时间。

  1. 我们知道page.js中的data对象在首次渲染时会通过数据管道传个视图层进行页面渲染。所以我们应该控制这个data对象的大小。对于与视图渲染无关的数据,不要放在data里面,可以设置个全局变量来保存。
    Page({
        //与页面渲染有关的数据放这里
        data: {
            goods_list:[]
        },
        //与页面渲染无关的数据放这里
        _data: {
            timer: null
        }
    })
复制代码
  1. 页面渲染速度还跟html的dom结构有关。这一点的优化空间算是非常少了,就是写高质量html代码,减少dom嵌套,让页面渲染速度快一丢丢。

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)
    }
复制代码

接下来我们看下后台数据

 

微信小程序开发总结与心得(二)_第3张图片

 

 

 

微信小程序开发总结与心得(二)_第4张图片

 

 

持续监控,会帮我们找出很多隐藏的bug

4 总结


洋洋洒洒写了这么多,或许有些地方说的不太清楚,慢慢锻炼吧。然后后面几点只是挑了重要的讲,我相信有过小程序开发经验的朋友应该没问题。然后有时间再补充和优化了。先到此,有缘看到的朋友,欢迎留言交流。


 

你可能感兴趣的:(微信小程序)