山东大学项目实训(二)—— 骨刻文项目web端性能优化与自适应调整

一、性能优化——点赞功能防抖函数实现

项目图片

在项目中,点击点赞按钮,需要向服务器连续发送两次请求:一次是请求点赞一个帖子,一次是获取所有帖子信息以更新页面;

再次点击点赞按钮,视为取消点赞,同样需要向服务器发送两次请求:一次是请求取消点赞一个帖子,一次是获取所有帖子信息以更新页面;

这时就会发现有个问题,假如用户对点赞按钮进行暴力点击(非常频繁地点击),那么浏览器就需要非常频繁地向服务器发送请求,这样非常的消耗资源,甚至会造成卡死!

作为一个严谨的web前端开发者,这个时候就需要想办法解决这个问题!(认真)

方案一:修改后端接口

这是我以前比较常采用的方法,就是把2次请求合并为1次请求

即前端发起点击点赞按钮请求时,后端如果更新成功,会返回一个更新后的列表,而不是需要前端再次发起请求

方案二:前端修改状态

前端状态改变后,可以不需要再向后端请求完整的更新后的列表,前端根据修改后的状态进行相应更新即可


不过,这两种方法依然会有对后端服务器进行的一次请求,如果用户暴力点击,频繁地发送请求还是比较难以避免,下面重点介绍第三种方法


方案三:使用防抖函数

这是这篇文章介绍的重点,我实现的是一个较为简单的防抖函数,大概只是把它的核心思想实现了,后续有时间可以再把这个函数封装出来,或者再完善一下

什么是防抖?为什么要使用防抖?

防抖函数就是为了防止用户暴力点击而产生对服务器过度频繁的访问,使用防抖函数可以保证用户如果进行多次重复操作,只对服务器发送少量请求,以提高性能和减少卡顿。

防抖函数的核心是设置一个计时器,如果在计时内该函数再次被触发,就销毁原来的计时器,重新计时;若没有再次触发,则当时间到达时,就会执行相应动作。

我这里实现的属于延迟防抖,其他还有前缘防抖、可以设置是否立即执行的防抖等等…防抖函数的实现也有很多方式,网上资料还是挺多的,我这里写的是比较符合我们项目需求的,还未进行比较好的封装的防抖函数

项目防抖函数实现

我的思路是,当用户点击按钮时,前端页面中的变量会随用户点击发生改变,但是对后端的请求只有过了一定时间才会触发

于是我先定义了2个函数:

frontClickLike:

// 该函数用于根据用户点击操作修改前端变量,即点赞按钮的样式和点赞数量
function frontClickLike() {
    
    
        let newAction, newLikes;
        if (action === 'like') {
    
    
            newAction = 'unlike';
            newLikes = likes - 1;
        } else {
    
    
            newAction = 'like';
            newLikes = likes + 1;
        }
        setAction(newAction)
        setLikes(newLikes)
        return newAction;
    }

clickLikeToBack:

// 该函数用于根据最终的点赞状态,向后端发起请求
function clickLikeToBack(newAction) {
    
    
        const url = (newAction === 'like' ? '/post/like' : '/post/unlike')
        console.log(url)
        // 请求服务器点赞或取消点赞
        axios({
    
    
            method: 'post',
            url,
            data: qs.stringify({
    
    
                postId: props.postId,
            }),
            headers: {
    
    
                token:sessionStorage.getItem('token')
            }
        })
            .then((res) => {
    
    
                console.log(res.data)
            })
            .catch((error) => console.log(error))
    }

最后是防抖函数

antiShakeClickLike:

    function antiShakeClickLike() {
    
    

        console.log("func called")
        
        // 打印一下当前计时器
        console.log(`timeout: ${
      
      timeout}`)

        // 调用frontClickLike,获得更新后的点赞状态
        // 为什么要用返回的newAction而不是直接用action呢?
        // 因为setAction是异步更新的,如果直接使用action,后面clickLikeToBack得到的是之前的action,而不是更新后的action
        let newAction = frontClickLike();

       // 如果timeout不为null,则说明之前已经有个计时器了,则清除计时器
        if (timeout) {
    
    
            console.log("timeout force cleared ")
            clearTimeout(timeout)
        }

        // 重新计时,1秒后将当前like发送给后端
        timeout = setTimeout(() => {
    
    
            clickLikeToBack(newAction);
            console.log(`将当前like发送给后端`)
        }, 1000)

    }

可能大家会有个问题,就是别人代码里的防抖函数都会返回一个函数,去调用那个函数实现防抖,我这里为什么直接写在函数体里面呢

我觉得,返回一个函数是因为他们使用一个全局的timeout变量去存储计时器,这个全局的timeout变量就放在一个大函数里,返回的函数就是一个闭包,记住了大函数里的timeout变量,并根据它进行是否执行相应操作的判断

我也可以像那样把timeout变量定义在antiShakeClickLike函数中,然后返回一个小函数。这是我原先的写法,代码如下:

    function antiShakeClickLike() {
    
    
        let timeout;
        return function () {
    
    
            if (!ifLogin) {
    
    
                Modal.info({
    
    
                    content: "请先登录!"
                });
                return;
            }

            console.log("func called")

            console.log(`timeout: ${
      
      timeout}`)


            let newAction = frontClickLike();

            if (timeout) {
    
    
                console.log("timeout force cleared ")
                clearTimeout(timeout)
            }

            timeout = setTimeout(() => {
    
    
                clickLikeToBack(newAction);
                console.log(`将当前like发送给后端`)
            }, 1000)
        }
    }

但是这样会有一个问题,就是我在函数体内有一个更新函数frontClickLike,这个函数会对组件的点赞状态、点赞数量状态进行更新(详见上述frontClickLike代码),每次更新操作,都会刷新页面,也就是重新渲染组件,这时,定义在onClick上的antiShakeClickLike函数也就会重新加载,也就是说,刷新后的antiShakeClickLike函数已经不是原来的antiShakeClickLike函数了,它里面的timeout变量会一直是undefined!达不到防抖的效果。

如图:
效果

解决这个问题有两种方法。
一是把当前组件变成类组件,然后将该防抖函数绑定在类上,这样就不会每次渲染都刷新,也就可以在该防抖函数里面定义timeout变量了。
二是像我代码写的这样,组件依然是函数组件,每次onClick都会刷新antiShakeClickLike函数,我的timeout变量定义在了组件之外,因为刷新的只是组件,这样就可以保证timeout变量的全局性。不过这种做法不是很好,破坏了封装性,后续维护可能不太方便。

最后的效果:
效果1
可以看到,多次点击时,它一直在进行计时器的清除操作,计时器也一直在更新。
效果2
当销毁完135号计时器后,没有再点击,这时就调用clickLikeToBack函数,将最新的点赞状态发送给服务器,完成!

不对,似乎还有点问题——

我代码这样写,可以保证它的延时调用,但是如果多次触发以后请求的点赞状态和原来一样,比如说,原来是已点赞的状态,我快速点击2n次,它最终还是一个已点赞的状态,按照我的代码,它还是会把一个点赞请求发送给服务器,这样岂不是多此一举?而且如果服务器代码没有对这种错误进行处理,还可能会造成想不到的bug!

于是,我又定义了一个全局变量lastAction,保留上一次向服务器发送/从服务器获得的点赞状态。然后每次向服务器发送请求更改状态,就更新该变量。如果是第一次调用防抖函数,那么lastAction就赋值为当前action。具体修改后的代码如下:

clickLikeToBack:

    function clickLikeToBack(newAction) {
    
    
    
        // add=============================
        if(lastAction===newAction){
    
    
             //和之前的状态一样,不发送
            console.log("点赞状态和之前一样,不发送给后端");
            return;
        }

        console.log(`将当前like发送给后端`)
        lastAction=newAction;
        //===================================

        const url = (newAction === 'like' ? '/post/like' : '/post/unlike')
        console.log(url)
        // 请求服务器点赞或取消点赞
        axios({
    
    
            method: 'post',
            url,
            data: qs.stringify({
    
    
                postId: props.postId,
            }),
            headers: {
    
    
                token:sessionStorage.getItem('token')
            }
        })
            .then((res) => {
    
    
                console.log(res.data)
                if (res.data.success === true) {
    
    
                    props.getList();
                }
            })
            .catch((error) => console.log(error))
    }

antiShakeClickLike:

    function antiShakeClickLike() {
    
    

        if (!ifLogin) {
    
    
            Modal.info({
    
    
                content: "请先登录!"
            });
            return;
        }

        console.log("func called")

        console.log(`timeout: ${
      
      timeout}`)
        
        // add:判断是否是第一次调用该函数============
        let firstCall=!timeout;
        if(firstCall) lastAction=action;
        //=========================================
        
        let newAction = frontClickLike();

        if (timeout) {
    
    
            console.log("timeout force cleared ")
            clearTimeout(timeout)
        }

        timeout = setTimeout(() => {
    
    
            clickLikeToBack(newAction);
        }, 1000)

    }

效果:
效果1
可以看到,双数次点击,则点赞状态和原来是一样的,就不会发送给后端
效果2
后面再进行单数次点击,发送功能正常!

到此应该算完成了~

二、自适应调整——浏览器窗口缩放时内容调整

主要关注一下wekitBox@media的使用

在主页中,有一个介绍栏

主页介绍栏
文字部分使用了wekitBox,控制行数为10行,溢出部分用省略号表示

.wekitBox{
    
    
    display: -webkit-box;
    -webkit-line-clamp: 10;
    text-overflow: ellipsis;
    -webkit-box-orient:vertical;
    overflow: hidden;
}

当浏览器窗口缩小时,会发现好像有点别扭
浏览器窗口变换后
可不可以对文字部分进行自适应调整呢?我使用了@media去做了一个微小调整
在css中添加样式:

@media screen and (max-width: 600px) {
    
    
    .wekitBox{
    
    
        display: -webkit-box;
        -webkit-line-clamp: 3;
        text-overflow: ellipsis;
        -webkit-box-orient:vertical;
        overflow: hidden;
    }
}

表示当窗口大小小于600px时,控制wekitBox显示的行数为3,效果如图:
调整后效果
这样就好一丢丢了,同理也可以对标题进行一下调整~

总结

本篇文章介绍了一下项目的性能优化与页面自适应调整,重点介绍了一下项目中防抖函数的实现。虽然这个只是一个小项目,但是严谨的编程对以后工作还是有很大帮助的,我也从中学到了很多新的知识,这篇文章涉及到了很多面试的考点,像防抖函数、@media等等。如有错误,欢迎交流指正~

猜你喜欢

转载自blog.csdn.net/weixin_45830447/article/details/124828841