vue.js实现带表情评论功能前后端实现 (滚动加载效果)

学习链接

vue.js实现带表情评论功能前后端实现(仿B站评论)
实现在vue项目中通过滚动条来滑动加载数据
IntersectionObserver与无限滚动加载

效果图

每次加载2条数据

思路

要实现滚动加载,就是当滚动条滚动到底部的时候,再去请求后端评论数据,然后把数据添加到响应式数据数组中,然后由vue更新dom。
因此,我们需要能有方式能够监测的到滚动条是否滚动到了底部,一般有两种方式:

scrollTop + clientHeight == scrollHeight

  • 可以通过 网页被卷去的头部高度 scrollTop 加上 浏览器客户端的高度 clientHeight 和 整个网页的高度 scrollHeight 做比较, 如果scrollTop + clientHeight == scrollHeight, 那么就可以说明滚动条已经到底了。

  • 为了避免误差, 可能还需要给scrollHeight预留一个比较小的范围。
    比如 scrollHeight - (scrollTop + clientHeight) <= 10, 这也就是说网页高度在仅剩10像素还在屏幕下面时的条件。

    • 然后监听window的scroll事件即可

IntersectionObserver

  • 也可以通过IntersectionObserver这个浏览器提供的api, 在评论的下面,添加一个显示正在加载的div,当这个div出现在屏幕中时,此时需要请求后台数据,
  • 数据请求回来后,更新dom,新添加的dom会把这个显示正在加载的div挤到最下面,当用户把新添加的内容浏览完,又去滚动,又看到这个显示正在加载的div,然后又去请求数据,直到把数据请求完

问题

  1. 这个IntersectionObserver浏览器的api还不太熟悉,如果加载两条之后,那个div如果还在页面,没有被挤到屏幕外面,还会回调观察函数吗?

    • 为了测试这个问题,我尝试把loading-area放到comment-wrapper的第一个元素的位置,这样它一开始就是出现的,然后一直都在这个位置,然后发现它很快速的连着发请求,直到把所有分页数据都请求完了。
    • 咦,我是不是忽略了一个东西了,我好像在这个正在加载中的div刚进入的比例达到0.5时,触发的请求分页数据,在发起请求前,我把isLoading置为了true,它如果为true的话,这个正在加载中的div的display就会为none,也就相当于它不被看见,然后,请求完了之后,我又把它置为了false,它又能被看见了,这就意味着,这个过程它从没被看见到看见又是一个变化,那么有可能这个变化也能被浏览器的IntersectionObserver这个api所监测到,所以很快速的引发的一连串的请求。好像误打误撞的无意中解决了这个问题,因为,对于这个问题,我觉得如果div还在页面的话,那就不算它又进入了,也就不会触发观察函数。就好像给了一个元素动画,但是这个元素是display:none,但是当这个元素不是display:none时,这个元素会立即有个动画。
    • 经过又一波测试,发现就是第二点说的那样,当元素在视口范围内,对display:none进行切换时,它也会触发观察函数
    • mdn中介绍:在创建IntersectionObserver时,可以指定第二个参数,第二个参数是一个配置对象,其中有一个threshold属性,默认值为0,意思是只要超过1个像素出现在root元素(默认是视口)中,那就执行观察函数,并且如果是超过了,那么isIntersecting就是true,那又分成2种情况,一种是从在视口外进入视口,刚好元素有一个像素出现在视口,此时,观察函数回调,并且isIntersecting为true(因为超过了0),此时,这个元素又向反方向离开视口,又会触发观察函数回调,此时isIntersecting为false(因为没超过0)。如果设置为0.2,当元素刚好超过20%的像素时,isIntersecting为true,此时,当元素反方向离开视口,此时,观察函数又会回调,isIntersecting为false。所以这个isIntersecting就是看有没有超过指定的阈值,超过了就是true,没超过就是false
  2. 当前滚动在第二页时,我发表了一个一级评论,

代码

Comment.vue

<style lang="scss">
/* 封面图下移效果 */
@keyframes slidedown {
    0% {
        opacity: 0.3;
        transform: translateY(-60px);
    }

    100% {
        opacity: 1;
        transform: translateY(0px);
    }
}


.slidedown {
    animation: slidedown 1s;
}

/* 内容上移效果 */
@keyframes slideup {
    0% {
        opacity: 0.3;
        transform: translateY(60px);
    }

    100% {
        opacity: 1;
        transform: translateY(0px);
    }
}

.slideup {
    animation: slideup 1s;
}

.banner {
    height: 400px;
    background-image: url(@/assets/bg5.jpg);
    background-size: cover;
    background-position: center;
    position: relative;
    color: #eee;

    .banner-content {
        position: absolute;
        bottom: 25%;
        width: 100%;
        text-align: center;
        text-shadow: 0.05rem 0.05rem 0.1rem rgb(0 0 0 / 30%);

        height: 108px;
        font-size: 30px;
        letter-spacing: 0.3em;

    }
}

textarea {
    outline: none;
    border: none;
    background: #f1f2f3;
    resize: none;
    border-radius: 8px;
    padding: 10px 10px;
    font-size: 16px;
    color: #333333;
}

.height80 {
    height: 80px !important;
}

.comment-wrapper {
    // border: 1px solid red;
    max-width: 1000px;
    margin: 40px auto;
    background: #fff;
    padding: 40px 30px;
    border-radius: 10px;

    color: #90949e;

    .comment-header {
        font-size: 20px;
        font-weight: bold;
        color: #333333;
        padding: 0 20px;
        margin-bottom: 20px;
        display: flex;
        align-items: center;

        i {
            color: #90949e;
            margin-right: 5px;
            font-size: 20px;
        }
    }

    .loading-area {
        .loading-effect {
            height: 50px;
            // border: 1px solid red;
            text-align: center;

            &>div {
                width: 100%;
                height: 100%;
                display: flex;
                align-items: center;
                justify-content: center;
            }

            .loading-animation {
                position: relative;
            }
        }

        .bottom-line {
            height: 40px;
            text-align: center;
            position: relative;
            display: flex;
            align-items: center;
            justify-content: center;
            // border: 1px solid red;

            span {
                padding: 0 12px;
                background-color: #fff;
                z-index: 1;
            }

            &::before {
                content: '';
                position: absolute;
                width: 100%;
                border-bottom: 1px dashed #ccc;
                top: 20px;
                left: 0;
            }
        }
    }



}
style>

<template>
    <div>
        <navbar />
        <div class="banner slidedown">
            <div style="position: absolute;top: 0;left: 0;width: 100%;height: 100%;backdrop-filter: blur(5px);">div>
            <div class="banner-content">
                <div>
                    评论
                div>
            div>
        div>
        <div class="comment-wrapper  shadow slideup">
            <div class="comment-header">
                <i class="iconfont icon-pinglun1">i>
                评论
                <el-button @click="switchUser(1)">用户id1-zzhua195el-button>
                <el-button @click="switchUser(2)">用户id2-lsel-button>
                <el-button @click="switchUser(3)">用户id3-zjel-button>
            div>

            
            <emoji-text @comment="comment" :emojiSize="20">emoji-text>

            
            
            
            <Reply ref="commentReplyRef" @closeOtherCommentBoxExcept="closeOtherCommentBoxExcept" :index="idx"
                v-for="(reply, idx) in replyList" :key="idx" :reply="reply" />

            <div class="loading-area">

                <div class="loading-effect" v-show="hasMore">
                    <div class="loading-text" v-show="!isLoading" ref="loadingTextRef">
                        正在加载中{{ 'isLoading=' + isLoading }} - {{ 'hasMore=' + hasMore }}
                    div>
                    <div class="loading-animation" v-show="isLoading">
                        <Loading/>
                    div>
                div>

                <div class="bottom-line" v-show="!hasMore">
                    <span>我也是有底线的span>
                div>

            div>
        div>



    div>
template>

<script>
import Talk from '@/components/Talk/Talk'
import Navbar from './Navbar.vue';
import EmojiText from '@/components/EmojiText/EmojiText'
import Reply from '@/components/Reply/Reply'
import Loading from '@/components/Loading/Loading'

import { getCommentListByPage, addComment } from '@/api/commentApi';

export default {
    name: 'Comment',
    data() {
        return {
            replyList: [],
            pageNum: 0, /* 分页参数, 第几页, 默认第0页 */
            pageSize: 2,/* 分页参数, 页大小 */
            hasMore: true, /* 是否还有数据可供加载, 默认有数据 */
            isLoading: false, /* 是否加载中 */

        }
    },
    mounted() {
        /* 加载评论数据 */
        /* 首先尝试加载第一页数据 */
        // getCommentListByPage({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
        //     this.replyList = res.list || []
        //     this.$nextTick(() => {
        //         if (res.totalCount > res.pageNum * res.pageSize) {
        //             this.hasMore = true
        //             let observer = new IntersectionObserver(entries => {
        //                 for (let entry of entries) {
        //                     console.log(entry);
        //                     if (entry.isIntersecting) { // 当正在加载的文字出现的时候, 开始发起请求, 加载数据
        //                         console.log(this, 'this');
        //                         this.pageNum++ // 分页参数 +1
        //                         this.loadCommentListByPage(observer)
        //                     }
        //                 }
        //             }, { threshold: 0.5 })
        //             observer.observe(this.$refs['loadingTextRef'])
        //         } else {
        //             this.hasMore = false
        //         }
        //         /* // 自动滚动到最下面(方便调试使用的代码)
        //         let scrollTop = document.documentElement.scrollHeight - document.documentElement.clientHeight
        //         window.scroll({
        //             top: scrollTop, // top: 表示移动到距离顶部的位置大小
        //             behavior: 'smooth'
        //         }) */
        //     })
        // })

        /* 优化: 上面是还没滚动到评论下面,就去加载; 改成等到了看评论的时候,再去加载。 */
        let observer = new IntersectionObserver(entries => {
            for (let entry of entries) {
                console.log(entry);
                if (entry.isIntersecting) { // 当正在加载的文字出现的时候, 才开始发起请求, 加载数据
                    console.log(this, 'this');
                    this.pageNum++ // 分页参数 +1
                    this.loadCommentListByPage(observer)
                }
            }
        }, { threshold: 0.5 })
        observer.observe(this.$refs['loadingTextRef'])
    },
    methods: {
        loadCommentListByPage(observer) {
            this.isLoading = true // 显示加载动画
            getCommentListByPage({ pageNum: this.pageNum, pageSize: this.pageSize }).then(res => {
                this.isLoading = false // 关闭加载动画
                this.replyList.splice(this.replyList.length, 0, ...res.list) // 将数据添加到最后面, 由根据修改后的数据(响应式数据), 更新dom
                this.$nextTick(() => {
                    if (res.totalCount > res.pageNum * res.pageSize) { // 证明还有数据, 还可以继续加载
                        this.hasMore = true
                    } else {
                        this.hasMore = false // 当前页已经是最后一页了, 后面没有更多数据了
                    }
                })
            })
        },
        /* 添加评论 */
        comment(content) {
            addComment({
                userId: localStorage.getItem("userId"),
                commentContent: content,
            }).then(res => {
                this.replyList.splice(0, 0, res)
                this.$toast('success', '评论成功')
            })
        },
        /* 模拟不同用户 */
        switchUser(userId) {
            localStorage.setItem("userId", userId)
            this.$toast('success', `切换userId ${userId} 成功`)
        },
        /* 关闭其它一级评论的评论框 */
        closeOtherCommentBoxExcept(index) {
            /* 根据索引, 关闭其它的输入框, 除了指定的输入框外 */
            this.$refs['commentReplyRef'].forEach((commentReplyRef, idx) => {
                if (index != idx) {
                    commentReplyRef.hideCommentBox()
                }
            })
        }
    },
    watch: {

    },
    components: {
        Talk,
        Navbar,
        EmojiText,
        Reply,
        Loading
    }
}
script>

Loading.vue

<template>
    <div class="loader">div>
template>

<script>

export default {
    name: 'Loading',
    components: {
    }
}
script>

<style lang="scss">
	$colors:
	  hsla(337, 84, 48, 0.75)
	  hsla(160, 50, 48, 0.75)
	  hsla(190, 61, 65, 0.75)
	  hsla( 41, 82, 52, 0.75);
	$size: 2.5em;
	$thickness: 0.5em;
	
	// Calculated variables.
	$lat: ($size - $thickness) / 2;
	$offset: $lat - $thickness;
	
	.loader {
	  position: relative;
	  width: $size;
	  height: $size;
	  transform: rotate(165deg);
	  
	  &:before,
	  &:after {
	    content: '';
	    position: absolute;
	    top: 50%;
	    left: 50%;
	    display: block;
	    width: $thickness;
	    height: $thickness;
	    border-radius: $thickness / 2;
	    transform: translate(-50%, -50%);
	  }
	  
	  &:before {
	    animation: before 2s infinite;
	  }
	  
	  &:after {
	    animation: after 2s infinite;
	  }
	}
	
	@keyframes before {
	  0% {
	    width: $thickness;
	    box-shadow:
	      $lat (-$offset) nth($colors, 1),
	      (-$lat) $offset nth($colors, 3);
	  }
	  35% {
	    width: $size;
	    box-shadow:
	      0 (-$offset) nth($colors, 1),
	      0   $offset  nth($colors, 3);
	  }
	  70% {
	    width: $thickness;
	    box-shadow:
	      (-$lat) (-$offset) nth($colors, 1),
	      $lat $offset nth($colors, 3);
	  }
	  100% {
	    box-shadow:
	      $lat (-$offset) nth($colors, 1),
	      (-$lat) $offset nth($colors, 3);
	  }
	}
	
	@keyframes after {
	  0% {
	    height: $thickness;
	    box-shadow:
	      $offset $lat nth($colors, 2),
	      (-$offset) (-$lat) nth($colors, 4);
	  }
	  35% {
	    height: $size;
	    box-shadow:
	        $offset  0 nth($colors, 2),
	      (-$offset) 0 nth($colors, 4);
	  }
	  70% {
	    height: $thickness;
	    box-shadow:
	      $offset (-$lat) nth($colors, 2),
	      (-$offset) $lat nth($colors, 4);
	  }
	  100% {
	    box-shadow:
	      $offset $lat nth($colors, 2),
	      (-$offset) (-$lat) nth($colors, 4);
	  }
	}
	
	
	
	/**
	 * Attempt to center the whole thing!
	 */
	
	html,
	body {
	  height: 100%;
	}
	
	.loader {
	  position: absolute;
	  top: calc(50% - #{$size / 2});
	  left: calc(50% - #{$size / 2});
	}
style>

你可能感兴趣的:(前端学习,前端)