解决微信小程序wx.request请求会偶发重复调用的问题

2,183 阅读4分钟

我正在参与掘金创作者训练营第6期,  点击了解活动详情

正逢周六,泡一杯浓茶,绿色叶子慢慢从皱皱巴巴样子慢慢展开,随着热气浮沉,借着等接口的时间,聊一聊之前手底下小朋友做代码优化引起的一次客诉。

问题出现

一次常规代码上线后不到二小时,有部分客户反馈出现重复提交单据的情况,经实施与客户沟通反馈,客户未重复点击。

问题定位

首先第一时间,通过查看trace日志,发现两次提交的时间间隔在10毫秒内,初步判断可能是出现了重复提交的情况,然后进行代码审查,发现已经加上了节流函数,时间间隔为1000毫秒,理论上是不会重复触发的。

节流与防抖

节流与防抖的前提,都是 某个行为持续地触发,这里采用节流函数,是否会有问题?首先就需要知道节流与防抖函数的实现以及他们各自起到的效果

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时

这里我们采用的是节流函数,而节流又有时间戳和定时器两种实现

  1. 时间戳版,这里利用了闭包的原理,保持了oldtime这个变量,这里,规定延时内,函数不会重复进行调用
function throttled1(fn, delay = 500) {
    let oldtime = Date.now()
    return function (...args) {
        let newtime = Date.now()
        if (newtime - oldtime >= delay) {
            fn.apply(null, args)
            oldtime = Date.now()
        }
    }
}

2.定时器版,和时间戳的版的区别就是定时器不会立即执行,而是会等待延时结束再执行,延时结束后再点击,会继续延时后执行

function throttled2(fn, delay = 500) {
    let timer = null
    return function (...args) {
        if (!timer) {
            timer = setTimeout(() => {
                fn.apply(this, args)
                timer = null
            }, delay);
        }
    }
}

问题解析

既然节流函数没有问题,那么我们就需要看一下代码里对节流函数的使用是否有问题,节流函数的本质,是通过闭包的方式,返回了一个匿名函数,通过闭包特性在匿名函数内实现对我们真实函数的节流调用。

那么,问题就出现了,我们的节流函数是否生效了呢?

项目采用的是uni-app的框架,写法也和vue语法基本一致,那么在vue内,我们如何进行函数的节流?和正常我们想的不一样

<view @click="throttled"></view>
import throttle from 'xx'
methods: {
throttled () {
    throttle(this.click, 1000)
}
}

上面那样的写法能实现节流吗?答案是不能的,因为节流的本质,是 当我们事件触发的时候,实际上是在调用节流函数返回的匿名函数,这里,点击事件依然调用的是我们的普通函数而不是节流函数,导致的结果就是,我们的事件延迟触发了,但是,并没有节流的效果,所以这里需要进行改造

<view @click="click"></view>
import throttle from 'xx'
methods: {
click:throttle(function () {....some code}, 1000)
}

这样,我们才能起到节流的效果,这里注意传入的函数一定不要用箭头函数的写法,因为如果是箭头函数,会绑定执行期上下文,那么this指向的就不是你的vm实例了。

一波二折

移动端的click与tap事件是不同的,区别是tap会立即触发,而click则有300ms的延迟,这里会和事件穿透有关吗?于是我们又加上了防止事件穿透的相关代码

一波三折

虽然改动了节流函数,但是后续还是有客户反馈依然还是偶发重复单据的情况,这里我们不禁思考,会不会是因为微信服务器代理转发导致的呢?小程序发起请求是使用了wx.request方法,于是我在微信开放平台以wx.request()请求服务器重复提交数据 为关键字进行检索,发现了很多同类型的提问,所以,单纯在前端做节流可能已经不能完全解决这个问题了,所以,我们需要对请求做改造,针对同一个请求,让他携带一个trace标识,在request请求实例上维护一个全局的时间戳+业务类型的traceid,在请求拦截器的部分,判断当前请求的traceId是否等于全局的traceId,如果相等,则直接终止请求,同样的,后台也需要在redis和aop内针对我的traceId进行处理。

结语

知之愈明,则行之愈笃,行之愈笃,则知之益明

见本而知末,观指而睹归,执一而应万,握要而治详,谓之术