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

11,391 阅读26分钟

0 前言


最近的工作重心一直在小程序,也开发了几个小程序,对小程序开发的流程及相关技术相对比较熟悉,在开发过程中也总结了一些心得经验、了解一些小程序文档上没有的东西、踩了一些坑。所以想着写篇文章记录下来,并借此将小程序开发的相关知识进行梳理,方便以后参考,也作为自己工作的阶段性总结。同时也希望可以通过文章,结识更多朋友,多交流,互相学习,共同进步。另文章若有不对之处,还望指出与不吝赐教。

1 微信小程序基本知识与概念


微信小程序开发,入门算是非常简单,只要看官文文档即可小程序简易教程。如何申请小程序账号,如何开发自己第一个小程序,如何发布,这一系列hello world操作官方文档都有手把手教学。小程序开发的每个步骤,提供的能力文档里都有,个人觉得,做小程序开发,有事没事都看下文档,因为小程序更新比较快速,同时一些细小的能力我们可能会漏掉,所以多看文档。

1.1 简单说下目录结构和app.json


文件目录结构很灵活

先来看看小程序项目的文件目录结构

文件目录结构

除了app.json必须位于根目录下,其他文件随意,并且都可以删。并且页面文件可以放到如何位置,只要在app.json中的pages中配置了就可以。可以说是很灵活。你还可以多个页面放在同个文件夹下(我相信你不会这样做的,何必糟蹋自己呢)。

接下来简单介绍下各个文件:

全局配置文件app.json

对于一个小程序项目而言,最重要的文件是app.json,它也是开发工具识别一个文件夹是否为小程序项目的标识。当使用开发者工具创建一个项目是,如果选择的是空文件夹,它会创建一个新的项目。如果是一个有文件的文件夹,它会看该文件夹中是否有app.jon文件,如果有,则它会认为是一个小程序项目,则会打开该项目,如果文件夹中没有app.json文件,则提示无法创建项目。

app.json必须放置于项目的根目录下,它是小程序项目的全局配置文件。在小程序代码包准备完成进行启动后(下文会详细介绍小程序从用户点击打开小程序到小程序销毁的整个过程),会先读取app.json文件,进行小程序的初试化,比如初始化整个小程序外框样式,获取首页页面地址等。

其实小程序就是微信提供的一个容器,各个页面就在这个容器里加载运行销毁

下面介绍下小程序的全局配置选项:

注意:

  • 所有配置项key必须使用双引号括起来,value值为字符串类型的也必须使用双引号,不支持单引号
  • 因为小程序功能迭代非常迅速,基础库版本更新也很快,所以下面的介绍是截止目前的最新版本库2.4.0
  • pages

    "pages": [
        "pages/index/index",
        "pages/log/log"
    ]

在app.json中,pages选项是必须配置的。该配置项注册了小程序所有页面的地址,其中每一项都是页面的 路径+文件名 。配置的字符串其实就是每个页面wxml路径,去掉.wxml后缀。因为框架会自动去寻找路径下.json、.js、.wxml、.wxss四个文件进行整合。也就意味着.json、.js、.wxss这三个文件的文件名必须要和.wxml的一致,否则不生效。所以一个页面至少必须得有.wxml文件。

总结:

  • 页面的.json、.js、.wxss文件必须与.wxml文件同名,否则不生效
  • 每个页面都必须pages下注册,没有注册的页面,如果不访问,编译能通过,一旦试图访问该页面则会报错
  • 可以通过在pages下添加一个选项快速新建一个页面,开发工具会自动生成对应的文件
  • window

  "window":{
    "enablePullDownRefresh": ture,
    "navigationStyle": "custom"
  }

该配置项用于配置小程序的全局外观样式,具体请查阅文档。这里重点提一下两个比较实用的

//去掉默认的导航栏,轻松实现全面屏
"navigationStyle": "custom" , 
//开启自带的下拉刷新,减少自己写样式
"enablePullDownRefresh": ture, 
  • tabBar

该选项可以让我们轻松实现导航栏tab效果,不过有个不足就是跳转可操作性非常低。就是每个tab只能跳当前小程序页面,不同跳到其他小程序。如果需要跳到其他小程序,还需自己封装个组件。

  • networkTimeout

这是网络请求超时时间,可以设置不同类型请求的超时时间,比如wx.request、wx.uploadFile等。其实很多时候我们都会忽略这个选项,小程序默认是60s超时,但我们应该手动设置更低的值,因为我们的接口一般都会在10s内完成请求(如果超过10s,那你是时候优化了),所以如果网络或者服务器出问题了,那么会让用户等60s,最后还是失败,这对用户很不友好,还不如提前告诉用户,现在出问题了,请稍后再试。

前段时间由于公司服务器网关出现了点小问题,导致有些请求连接不上,出现大量连接超时。通过之前添加的错误信息收集插件(这个是性能优化,下文有讲到)看到了很多接口返回time-out 60s。让用户等了60s还是失败,这不友好。所以这个超时时间一般设置15s-30s比较好。

  • debug

是否开启debug功能,开启后查看更多的调试信息,方便定位问题,开发阶段可以考虑开启

  • functionalPages

这个是结合插件使用的,因为微信小程序插件有很大限制,插件里提供的api很有限,wx.login 和 wx.requestPayment 在插件中不能使用,如果需要获取用户信息和进行支付,就必须通过插件提供的功能也实现。当你的小程序下的插件启用了插件功能也时,必须设置该选项为true

小程序插件必须挂载在一个微信小程序中,一个小程序也只能开通一个插件。当你小程序开通的插件启用了插件功能也时,必须设置该选项为true

  • plugins

    "plugins": {
        "myPlugin": {
            "version": "1.0.0",
            "provider": "wxidxxxxxxxxxxxxxxxx"
        }
    }

当小程序使用了插件就必须在这里声明引入。小程序自身开通的小程序不能在本身应用

  • navigateToMiniProgramAppIdList

    "navigateToMiniProgramAppIdList": [
        "wxe5f52902cf4de896"
    ]

之前小程序之间只要是关联了通过公众号就可以相互跳转,如今微信做出了限制,要这个这里配置好需要跳转的小程序,上限为10个,还必须写死,不支持配置。所以当小程序有跳转到其他小程序,一定要配好这个,否则无法跳转。

  • usingComponents

  "usingComponents": {
    "hello-component": "plugin://myPlugin/hello-component"
  }

使用自定义组件或者插件提供的组件前,必须先在这里声明

1.2 小程序启动与生命周期


下面来说说小程序从用户点击打开到销毁的整个过程。用图说话更清晰,特地画了个流程图:

小程序启动会有两种情况,一种是「冷启动」,一种是「热启动」。 假如用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时无需重新启动,只需将后台态的小程序切换到前台,这个过程就是热启动;冷启动指的是用户首次打开或小程序被微信主动销毁后再次打开的情况,此时小程序需要重新加载启动。

上面的流程图包含了所有内容,但毕竟文字有限,接下来详细说下几个点。

  1. 小程序会先检测本地是否有代码包,然后先使用本地代码包进行小程序启动,再异步去检测远端版本。这就是小程序的离线能力,相对于H5,这是优点,能加快小程序启动速度。
  2. 当本地有小程序代码包时,会异步去请求远端是否有最新版本。有则下载到本地,但该次的启动还是会用之前的代码。所以当我们发布了最新的版本,需要用户两次冷启动,才能使用到最新版本。如果想要用户一次冷启动就可以使用到最新版本,可以使用小程序提供的版本更新API更新。代码如下,只要在app.js的onShow函数加上以下代码,每次小程序有更新,都会提示用户更新小程序。不过这个每次提示更新,一定程度上影响用户体验。如果结合后端配置,每次进来读取配置,就可以实现根据需要是否进行该版本的更新,比如一定需要用户更新才能使用的,那就使用强制更新。对于一些小版本,就不需要使用这个强制更新。
    if (wx.canIUse('getUpdateManager')) {
        //检测是否有版本更新
        var updateManager = wx.getUpdateManager()
        updateManager.onCheckForUpdate(function (res) {
            // 请求完新版本信息的回调,有更新
            if (res.hasUpdate) {
                wx.showLoading({
                    title: '检测到新版本',
                })
            }
        })
        updateManager.onUpdateReady(function () {
            wx.hideLoading();
            wx.showModal({
                title: '更新提示',
                content: '新版本已经准备好,是否重启应用?',
                success: function (res) {
                    if (res.confirm) {
                        //清楚本地缓存
                        try {
                            wx.clearStorageSync()
                        } catch (e) {
                            // Do something when catch error
                        }
                        // 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
                        updateManager.applyUpdate()
                    }
                }
            })
        })
        updateManager.onUpdateFailed(function () {
            // 新的版本下载失败
            console.log('新版本下载失败');
        })
    }

1.3 开发工具


对于小程序开发工具,还没有一款让开发者满意的工具,至少我不满意,哈哈哈!微信提供的微信开发者工具。除了编译器不行外,其他都还行。但由于开发工具、ios、android三个平台运行小程序的内核不同。所以有时会出现开发工具上没问题,真机有问题的情况,特别是样式,可以通过在开发工具中设置上传代码时样式自动补全来解决大多数问题。另外微信开发者工具提供了真机调试功能,该功能对真机调试非常方便

还有就是可以自定义编译条件

可以模拟任意场景值、设置页面参数、模拟更新等。基本满足了所有的调试。不过还有一些效果,开发工具和真机可能会不同,所以还是需要在真机上确认。

1.4 测试-审核-上线的那些事


服务器域名request合法域名每个月只能修改5次。所以不应该每次请求一个新域名就添加一次。在开发阶段,在微信开发者工具上勾上不校验合法域名,真机上需要开启调试模式,就可以先不配置合法域名的情况下请求任何域名甚至ip地址。待开发完成了,再一次性配置所有合法域名,在微信开发者工具上取消不校验合法域名,真机上关闭调试模式,然后开始测试。

使用体验版+线上环境的接口,这就是和线上环境一模一样的,所以在发布前,使用体验版+线上环境过一遍。如果没问题,发布以后也就没问题了。

小程序二维码只要发布了线上版本调用生成小程序二维码接口才能成功返回二维码。而且二维码识别是线上版本,所以还未发布的小程序是无法生成二维码的。

线上版本有个版本回退功能,这里有个坑,就是版本回退以后,退回的版本需要重新审核才能发布

还有设置体验版时可以设置指定路径和参数,这样很方便测试

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%,你会发现,你页面底部可能会缺失一段。上图:

因为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文件

    <!--toView的值动态变化,当toView为luckydraw时,会定位到id为luckydraw的view
    需要注意的是,这里需要设置高度为屏幕高度-->
    <scroll-view scroll-y scroll-into-view="{{toView}}" 
    scroll-with-animation = "true" style="height: 100%; white-space:nowrap">
        <view id="top"></view>
        <view id="luckydraw"></view>
        <view id="secskill"></view>
    <scroll-view>

2.3 canvas


画布标签,它是原生组件,所以它必须位于屏幕最上边,而且是不能隐藏的。所以如果想要使用canvas动态生成分享照片。那你要设置她的宽高和屏幕一样。要不导出为照片时就会失真。因为这个原因,所以生成分享照片还是有服务端实现吧,照片失真太严重了。

3 formid收集


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

    <form bindsubmit="formSubmit" report-submit='true'>
        <button  formType="submit">点击</button>
    </form>

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

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

wxml文件

    <!--在整个页面的最外层包裹form标签,这样就不同对每个button都包裹一个form标签,代码简洁-->
    <form bindsubmit="formSubmit" report-submit='true'>
        <view>页面内容</view>
        <view>页面内容</view>
        <button  formType="submit">点击</button>
        <view>页面内容</view>
        <view>
            <button  formType="submit">点击</button>
        </view>
    </form>

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 让小程序更快


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

前面状态小程序代码大小是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.<anonymous> (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.<anonymous> (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

4 总结


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