0%

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

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

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

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

文件目录结构很灵活

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

image

除了 app.json 必须位于根目录下,其他文件随意,并且都可以删。并且页面文件可以放到任何位置,只要在 app.json 中的 pages 中配置了就可以。可以说是很灵活。你还可以多个页面放在同个文件夹下。

image

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

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

image

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

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

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

注意:

  • 所有配置项 key 必须使用双引号括起来,value 值为字符串类型的也必须使用双引号,不支持单引号
  • 因为小程序功能迭代非常迅速,基础库版本更新也很快,所以下面的介绍是截止目前的最新版本库 2.4.0
  • pages
1
2
3
4
"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
1
2
3
4
"window":{
"enablePullDownRefresh": ture,
"navigationStyle": "custom"
}

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

1
2
3
4
//去掉默认的导航栏,轻松实现全面屏
"navigationStyle": "custom" ,
//开启自带的下拉刷新,减少自己写样式
"enablePullDownRefresh": true,
  • 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
1
2
3
4
5
6
"plugins": {
"myPlugin": {
"version": "1.0.0",
"provider": "wxidxxxxxxxxxxxxxxxx"
}
}

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

  • navigateToMiniProgramAppIdList
1
2
3
"navigateToMiniProgramAppIdList": [
"wxe5f52902cf4de896"
]

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

  • usingComponents
1
2
3
"usingComponents": {
"hello-component": "plugin://myPlugin/hello-component"
}

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

1.2 小程序启动与生命周期

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

image

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

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

  1. 小程序会先检测本地是否有代码包,然后先使用本地代码包进行小程序启动,再异步去检测远端版本。这就是小程序的离线能力,相对于 H5,这是优点,能加快小程序启动速度。
  2. 当本地有小程序代码包时,会异步去请求远端是否有最新版本。有则下载到本地,但该次的启动还是会用之前的代码。所以当我们发布了最新的版本,需要用户两次冷启动,才能使用到最新版本。如果想要用户一次冷启动就可以使用到最新版本,可以使用小程序提供的版本更新API 更新。代码如下,只要在 app.js 的 onShow 函数加上以下代码,每次小程序有更新,都会提示用户更新小程序。不过这个每次提示更新,一定程度上影响用户体验。如果结合后端配置,每次进来读取配置,就可以实现根据需要是否进行该版本的更新,比如一定需要用户更新才能使用的,那就使用强制更新。对于一些小版本,就不需要使用这个强制更新。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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 三个平台运行小程序的内核不同。所以有时会出现开发工具上没问题,真机有问题的情况,特别是样式,可以通过在开发工具中设置上传代码时样式自动补全来解决大多数问题。另外微信开发者工具提供了真机调试功能,该功能对真机调试非常方便

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

image

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

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

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

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

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

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

image

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

image

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

image

因为 web-view 是默认铺满全屏的,也就是 web-view 宽高和屏幕宽高一样。然后 H5 页面这是高度 100%,这是相对 web-view 的高度,也是屏幕高度。但是关键问题:web-view 里 H5 页面是从导航栏下开始渲染的。这就导致了 H5 页面溢出了屏幕,无法达到全屏效果。

解决方法

这个问题我在前段时间的实际项目碰到过,我们要做个 H5 游戏,要求是全屏,刚开始我也是设置高度 100%。后来发现底部一块不见了。我的解决方法比较粗暴,如果有更好的解决方法,欢迎评论交流。
我的解决方法是:通过拼接宽高参数在 H5 页面 url 上,这个宽高是在 web-view 外层计算好的。H5 页面直接读取 url 上的宽高,动态设置页面的宽高。页面高度的计算,根据上图,很显然就是屏幕高度减去导航栏高度。宽度都是一样的,直接是屏幕宽度。

但问题又来了,貌似没有途径获取导航栏高度。而且对于不同机型的手机,导航栏高度不同。经过了对多个机型导航栏跟屏幕高度的比较。发现了一个规律,导航栏高度与屏幕高度、屏幕宽高比有一定的关系。所以根据多个机型就计算出了这个比例。这解决了 95%以上手机的适配问题,只有少数机型适配不是很好。基本实现了全屏效果。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
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-view 属性,具体实现我们直接上代码

scroll-into-view | String | 值应为某子元素 id(id 不能以数字开头)。设置哪个方向可滚动,则在哪个方向滚动到该元素

wxml 文件

1
2
3
4
5
6
7
8
<!--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,在需要的时候给用户推送消息。我们可以给每个 button 都包上 form 标签,只要有用户点击行为都可以收集到 formid.

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

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

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

wxml 文件

1
2
3
4
5
6
7
8
9
10
<!--在整个页面的最外层包裹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 文件

1
2
3
4
5
6
7
8
9
//每次用户有点击,都将formid添加到全局数组中
formSubmit(e) {
//需要实时发送的,不添加
if(e.target.dataset.sendMsg){
formid = e.detail.formId;
return;
}
app.appData.formIdArr.push(e.detail.formId);
}

app.js

1
2
3
4
onHide: function () {
//小程序切到后台时上传formid
this.submitFormId();
},

4 性能优化相关

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

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

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

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

4.1 让小程序更快

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

image

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

  1. 因为我们上传代码到微信服务器时,它会将我们的代码进行压缩的,所以用户下载的代码包并不是我们开发时的那个大小。对此,开发时也没必要删空行、删注释这些。在开发工具项目详情中可以看到上次上传大小,这个大小就是用户最终使用的大小。如果觉得微信压缩还不够好,可以通过第三方工具对我们代码进行一次压缩再上传,然后对比效果,有没有更小。这个没有使用过。如果有什么好工具,欢迎推荐。

  2. 将静态资源文件上传到我们自己服务器或者 cdn 上。一个小程序,最耗空间的往往是图片文件。所以我们可以抽离出来,图片文件可以异步获取,在小程序启动以后再去获取。这样,代码包就会小很多。

  3. 使用分包加载。小程序提供了分包加载功能。如果你的小程序很庞大,可以考虑使用分包加载功能,先加载必要功能代码。这样就可以极大降低代码包大小
    接下来是首屏渲染,从上图的小程序生命周期可以看出,从加载首页代码到首页完成渲染,这段时间就是白屏时间,也就是首次渲染时间。而小程序在这段时间内,主要工作是:加载首页代码、创建 View 和 AppService 层、初始数据传输、页面渲染。在这四个步骤中,加载首页代码,前面已经说过;创建 View 和 AppService 层,是微信完成的,跟用户手机有关,这不是我们可控的。我们能做的就是减少初始数据传输时间和页面渲染时间。

  4. 我们知道 page.js 中的 data 对象在首次渲染时会通过数据管道传到视图层进行页面渲染。所以我们应该控制这个 data 对象的大小。对于与视图渲染无关的数据,不要放在 data 里面,可以设置个全局变量来保存。

1
2
3
4
5
6
7
8
9
10
Page({
//与页面渲染有关的数据放这里
data: {
goods_list:[]
},
//与页面渲染无关的数据放这里
_data: {
timer: null
}
})
  1. 页面渲染速度还跟 html 的 dom 结构有关。这一点的优化空间算是非常少了,就是写高质量 html 代码,减少 dom 嵌套,让页面渲染速度快一丢丢。

4.2 让小程序更强

接下来就是给小程序增加错误信息收集,包括 js 脚本错误信息收集和 http 请求错误信息收集。前段时间,在实际工作开发中,为了更好的复用和管理,我把这个错误信息收集功能做成了插件。然而做成插件并没有想象中的那么美好,下面再具体说。

脚本错误收集

对于脚本错误收集,这个相对比较简单,因为在 app.js 中提供了监听错误的 onError 函数

image

只不过错误信息是包括堆栈等比较详细的错误信息,然后当上传时我们并不需要这么多信息,第一浪费宽带,第二看着累又无用。我们需要的信息是:错误类型、错误信息描述、错误位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

这是错误信息字符串,接下来我们对它进行截取只需要拿我们想要的信息即可。我们发现这个字符串是有规则的。第一行是错误类型,第二行是错误详情和发生的位置,并且是”;”分号分开。所以我们还是很容易就可以拿到我们想要的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//格式化错误信息
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.defineProperty 对 wx 对象的 request 进行重新定义。具体实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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 了。具体实现如下:

1
2
3
4
5
function my_request(){
//只要执行一次拦截代码即可
!_isInit && rewriteRequest();
return wx.request(options)
}

接下来我们看下后台数据

image

image

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

4 总结

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