利用这个css属性,你也能轻松实现一个新手引导库

news2024/11/14 22:55:56

相信大家或多或少都在各种网站上使用过新手引导,当网站提供的功能有点复杂时,这是一个对新手非常友好的功能,可以跟随新手引导一步一步了解网站的各种功能,我们要做的只是点击下一步或者上一步,网站就能滚动到指定位置,然后高亮页面的一部分,并且配以一些图文介绍。

目前有很多帮你实现这种功能的开源库,当然,自己实现一个也不难,而且核心就是一个简单的css样式,不信你接着往下看。

基本思路

假设我们的新手引导库是一个类,名为NoviceGuide,我们可以这样使用它:

new NoviceGuide({
    steps: [
        {
            element: '',// 页面上的元素,可以是节点,也可以是节点的选择器
            text: '我是第一步',
            img: '我是第一步的图片'
        },
        {
            element: '',
            text: '我是第二步'
        }
    ]
}).start()

我们稍微思考一下就会发现,实现原理其实很简单,只要找到某一步指定节点的位置和宽高,然后将页面滚动到该节点的位置,最后高亮它,并且在旁边显示信息即可。

我们的类基本结构如下:

class NoviceGuide {
  constructor(options) {
    this.options = options
    // 步骤数据
    this.steps = []
    // 当前所在步骤
    this.currentStepIndex = -1
    // 处理步骤数据
    this.initSteps()
  }

  initSteps() {
    this.options.steps.forEach((step) => {
      this.steps.push({
        ...step,
        element:
          typeof step.element === "string"
            ? document.querySelector(step.element)
            : step.element,
      })
    })
  }

  start() {
    this.next()
  }

  next() {}
}

滚动到目标元素

获取到当前步骤的元素,然后再获取它的位置,最后再滚动页面,让目标元素居中即可。

class NoviceGuide {
    next() {
        // 已经是最后一步,那么结束引导
        if (this.currentStepIndex + 1 >= this.steps.length) {
          return this.done()
        }
        this.currentStepIndex++
        this.to()
    }
    
    to() {
        // 当前步骤
        const currentStep = this.steps[this.currentStepIndex]
        // 当前步骤元素的尺寸和位置信息
        const rect = currentStep.element.getBoundingClientRect()
        const windowHeight = window.innerHeight
        // 浏览器窗口滚动到元素所在位置
        window.scrollBy(0, rect.top - (windowHeight / 2 - rect.height / 2))
    }
    
    done() {}
}

使用window.scrollBy滚动相对距离,距离的计算可以参考下图:

不过如果元素已经在可视窗口内,其实不需要将它居中,否则如果多个步骤都在一个窗口内,那么切换步骤会频繁的滚动页面,体验反而不好,所以先判断一下元素是否在视口内:

class NoviceGuide {
    to() {
        const currentStep = this.steps[this.currentStepIndex]
        const rect = currentStep.element.getBoundingClientRect()
        const windowHeight = window.innerHeight
        if (!this.elementIsInView(currentStep.element)) {
            window.scrollBy(0, rect.top - (windowHeight - rect.height) / 2)
        }
    }

    elementIsInView(el) {
        const rect = el.getBoundingClientRect()
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= window.innerHeight &&
            rect.right <= window.innerWidth
        )
    }
}

高亮元素

目标元素可见了,接下来要做的是高亮它,具体的效果就是页面上只有目标元素是亮的,其他地方都是暗的,这个实现方式我考虑过使用svgcanvas等,比如canvas实现:

class NoviceGuide {
    to() {
        // ...
        this.highlightElement(currentStep.element)
    }
    
    highlightElement(el) {
        const rect = el.getBoundingClientRect();
        const canvas = document.createElement('canvas')
        document.body.appendChild(canvas)
        const ctx = canvas.getContext('2d')
        canvas.width = window.innerWidth
        canvas.height = window.innerHeight
        canvas.style.cssText = `
            position: fixed;
            left: 0;
            top: 0;
            z-index: 99999999;
            `
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'
        ctx.fillRect(0, 0, window.innerWidth, window.innerHeight)
        ctx.clearRect(rect.left, rect.top, rect.width, rect.height)
    }
}

原理很简单,创建一个和窗口一样大的canvas,然后全部填充成半透明,最后再清除掉目标元素所在位置的绘制,就达到了高亮的效果:

不过这种方式想要效果更好一点比较麻烦,后来在其他库中看到一个很简单的实现,使用一个box-shadow属性即可:

class NoviceGuide {
    highlightElement(el) {
        const rect = el.getBoundingClientRect()
        if (!this.highlightEl) {
            this.highlightEl = document.createElement("div")
            this.highlightEl.style.cssText = `
                position: absolute;
                box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.5);
                z-index: 99999999;
                border-radius: 5px;
                transition: all 0.3s ease-out;
                `
            document.body.appendChild(this.highlightEl)
        }
        this.highlightEl.style.left = rect.left + window.pageXOffset + "px"
        this.highlightEl.style.top = rect.top + window.pageYOffset + "px"
        this.highlightEl.style.width = rect.width + "px"
        this.highlightEl.style.height = rect.height + "px"
    }
}

核心就是box-shadow: 0 0 0 5000px rgba(0, 0, 0, 0.5);这一行样式,创建一个和目标元素一样大小的元素,然后盖在它上面,然后把这个元素的阴影大小设置成非常大,这样除了这个元素的内部,页面其他地方都是它的阴影,就达到了高亮的效果,果然是css学的好,每天下班早。

使用DOM简单很多,修改样式比较方便,另外只要设置transition,就能轻松实现切换步骤时高亮的过渡动画效果。

另外为什么这里没有使用固定定位,而是使用绝对定位,其实是因为如果使用固定定位,页面可以滚动,但是高亮框并不会滚动,那么就对不上了。

切换步骤

接下来,我们创建一个新元素用来存放信息和上一步下一步的按钮:

class NoviceGuide {
    constructor(options) {
        // ...
        this.infoEl = null
    }

    to() {
        // ...
        this.showStepInfo(currentStep)
    }

    showStepInfo(step) {
        if (!this.infoEl) {
            this.infoEl = document.createElement("div")
            this.infoEl.style.cssText = `
                position: absolute;
                z-index: 99999999;
				background-color: #fff; 
				border-radius: 5px;
                `
            document.body.appendChild(this.infoEl)
            // 绑定单击事件
            this.infoEl.addEventListener("click", (e) => {
                let type = e.target.getAttribute("data-type")
                if (type) {
                    if (type === "prev") {
                        this.prev()
                    }
                    if (type === "next") {
                        this.next()
                    }
                }
            })
        }
        this.infoEl.innerHTML = `
            <div>
                ${
                     step.img ? `<div>
                        <img src="${step.img}" style="width: 250px" />
                     </div>` : ''
                }
                <div>${step.text}</div>
            </div>
            <div>
                <button data-type="prev">上一步</button>
                <button data-type="next">下一步</button>
           	</div>
            `
        const rect = step.element.getBoundingClientRect()
        this.infoEl.style.left = rect.left + window.pageXOffset + "px"
        this.infoEl.style.top = rect.bottom + window.pageXOffset + "px"
    }
}

很简单,同样是创建一个绝对定位的元素,里面存放信息、图片、按钮,然后监听一下点击事件,判断点击的是上一步还是下一步,补充一下上一步和结束的逻辑:

class NoviceGuide {
    prev() {
        if (this.currentStepIndex - 1 < 0) {
            return
        }
        this.currentStepIndex--
        this.to()
    }

    done() {
        document.body.removeChild(this.highlightEl)
        document.body.removeChild(this.infoEl)
        this.currentStepIndex = -1
    }
}

结束的话直接删除创建的两个元素就可以了,看看目前的效果:

优化

加点内边距

目前视觉上不是很好看,高亮框和目标元素大小是完全一样的,高亮框和信息框完全挨着,信息框没有内边距,所以优化一下:

class NoviceGuide {
    constructor(options) {
        this.options = Object.assign(
            {
                padding: 10,
                margin: 10
            },
            options
        )
    }

    highlightElement(el) {
        // ...
        let { padding } = this.options
        this.highlightEl.style.left = rect.left + window.pageXOffset - padding + "px"
        this.highlightEl.style.top = rect.top + window.pageYOffset - padding + "px"
        this.highlightEl.style.width = rect.width + padding * 2 + "px"
        this.highlightEl.style.height = rect.height + padding * 2 + "px"
    }

    showStepInfo(step) {
        let { padding, margin } = this.options
         if (!this.infoEl) {
             this.infoEl.style.cssText = `
				padding: ${padding}px;
			`
         }
        // ...
        this.infoEl.style.left = rect.left + window.pageXOffset - padding + "px"
      	this.infoEl.style.top = rect.bottom + window.pageYOffset + padding + margin + "px"
    }
}

支持某个步骤没有元素

某些步骤可能是纯信息,不需要元素,这种情况直接显示在页面中间即可:

class NoviceGuide {
    to() {
        const currentStep = this.steps[this.currentStepIndex]
        if (!currentStep.element) {
            // 当前步骤没有元素
            this.highlightElement()
            this.showStepInfo(currentStep)
            return
        }
        // ...
    }

    highlightElement(el) {
        // ...
        if (el) {
            const rect = el.getBoundingClientRect()
            let { padding } = this.options
            // ...
            // 原有逻辑
        } else {
            // 当前步骤没有元素高亮元素的宽高设置成0,并且直接定位在窗口中间
            this.highlightEl.style.left = window.innerWidth / 2 + window.pageXOffset + "px"
            this.highlightEl.style.top = window.innerHeight / 2 + window.pageYOffset + "px"
            this.highlightEl.style.width = 0 + "px"
            this.highlightEl.style.height = 0 + "px"
        }
    }

    showStepInfo(step) {
        // ...
        if (step.element) {
            const rect = step.element.getBoundingClientRect()
            // ...
            // 原有逻辑
        } else {
            // 当前步骤没有元素,信息框定位在窗口中间
            const rect = this.infoEl.getBoundingClientRect()
            this.infoEl.style.left = (window.innerWidth - rect.width) / 2 + window.pageXOffset + "px"
            this.infoEl.style.top = (window.innerHeight - rect.height) / 2 + window.pageYOffset + "px"
        }
    }
}

当然,上述实现还是有点问题的,比如网速慢的时候,或者图片比较大时,图片还没加载出来,那么获取到的信息框的大小是不对的,导致定位会出现偏差,这个问题本文就不考虑了。

动态计算信息的位置

目前我们的信息框是默认显示在高亮元素下方的,这样显然是有问题的,比如高亮元素刚好在屏幕底部,或者信息框的高度很高,底部无法完全显示,这种情况,我们就需要改成动态计算的方式,具体来说就是依次判断信息框能否在高亮元素下方、上方、左方、右方四个方向显示,如果都不行的话,还要尝试调整页面滚动的位置使高亮框和信息框都能显示。

class NoviceGuide {
    showStepInfo(step) {
        // ...
        if (step.element) {
            this.computeInfoPosition(step)
        } else {
            // ...
        }
    }
}

计算的逻辑我们放到一个新函数里:

class NoviceGuide {
    computeInfoPosition(step) {
        const { padding, margin } = this.options
        const windowWidth = window.innerWidth
        const windowHeight = window.innerHeight
        const windowPageXOffset = window.pageXOffset
        const windowPageYOffset = window.pageYOffset
        const rect = step.element.getBoundingClientRect()
        const infoRect = this.infoEl.getBoundingClientRect()
        // ...
    }
}

获取和保存一些基本信息,继续:

class NoviceGuide {
    computeInfoPosition(step) {
        let left = 0
        let top = 0
        const adjustLeft = () => {
            // 优先和高亮框左对齐
            if (windowWidth - rect.left - padding >= infoRect.width) {
                return rect.left - padding + windowPageXOffset
            } else {
                // 否则水平居中显示
                return (windowWidth - infoRect.width) / 2 + windowPageXOffset
            }
        };
        if (
            rect.bottom + padding + margin + infoRect.height <= windowHeight && // 下方宽度可以容纳
            infoRect.width <= windowWidth // 信息框宽度比浏览器窗口小
        ) {
            // 可以在下方显示
            left = adjustLeft()
            top = rect.bottom + padding + margin + windowPageYOffset
        } else if (
            rect.top - padding - margin >= infoRect.height &&
            infoRect.width <= windowWidth
        ) {
            // 可以在上方显示
            left = adjustLeft()
            top = rect.top - padding - margin - infoRect.height + windowPageYOffset
        }
        // 省略后续两个判断
    }
}

判断高亮框的下方和上方的剩余空间能否容纳信息框,另外还要判断一下信息框的宽度是否比浏览器窗口小。

对于信息框的水平位置,我们优先让它和高亮框左对齐,如果空间不够,那么就让信息框在浏览器窗口水平居中。

对于左侧和右侧的判断也是类似的,完整代码可以去文末的仓库里查看。

当上下左右四个方向都无法满足条件时,我们还可以再检查一种情况,也就是高亮框和信息框的总高度是否比浏览器窗口高度小,是的话我们可以通过滚动页面位置来达到完整显示的目的:

class NoviceGuide {
    computeInfoPosition(step) {
        // ...
        else {
            // 否则检查高亮框高度+信息框高度是否小于窗口高度
            let totalHeightLessThenWindow =
                rect.height + padding * 2 + margin + infoRect.height <= windowHeight
            if (
                totalHeightLessThenWindow &&
                Math.max(rect.width + padding * 2, infoRect.width) <= windowWidth
            ) {
                // 上下排列可以放置
                // 滚动页面,居中显示两者整体
                let newTop =
                    (windowHeight -
                     (rect.height + padding * 2 + margin + infoRect.height)) /
                    2
                window.scrollBy(0, rect.top - newTop)
            } else {
                // 恕我无能为力
                // 回到默认位置
            }
            left = adjustLeft()
            top = rect.bottom + padding + margin + windowPageYOffset
        }
        this.infoEl.style.left = left + "px"
        this.infoEl.style.top = top + "px"
    }
}

如果总高度小于窗口高度,那么可以调整页面滚动位置,否则就不做任何处理,这两种情况对于信息框来说,都是显示在高亮框下方。

如果目标元素位于可滚动元素内

这个问题是什么意思呢,比如我们想高亮下图中红框内的元素:

它所在的可滚动父元素并不是document.body,事实上这个页面body元素压根无法滚动,宽高是和窗口宽高一致的,而我们的实现逻辑是通过滚动body来使元素可见的,那么我们就做不到让这个元素出现在视口。

解决这个问题可以这么考虑,我们先找到目标元素的最近的可滚动的祖先元素,如果元素不在该祖先元素的可视区域内,那么就滚动父元素让元素可见,当然这样还没完,因为该祖先元素也可能存在一个可滚动的祖先元素,它也不一定是在它的祖先元素内可见,所以还得判断和让它可见,很明显,这是一个向上递归的过程,一直检查到body元素为止。

先来写一个获取最近的可滚动祖先元素的方法:

class NoviceGuide {
    getScrollAncestor(el) {
        let style = window.getComputedStyle(el)
        const isAbsolute = style.position === 'absolute'
        const isFixed = style.position === 'fixed'
        const reg = /(auto|scroll)/
        // 如果元素是固定定位,那么可滚动祖先元素为body
        if (isFixed) return document.body
        let parent = el.parentElement
        while (parent) {
            style = window.getComputedStyle(parent)
            // 如果是绝对定位,那么可滚动的祖先元素必须是有定位的才行
            if (!(isAbsolute && style.position === 'static')) {
                // 如果某个祖先元素的overflow属性为auto或scroll则代表是可滚动的
                if (reg.test(style.overflow + style.overflowX + style.overflowY)) {
                    return parent
                }
            }
            parent = parent.parentElement
        }
        return document.body
    }
}

就是不断向上递归,接下来修改一下to方法,在获取目标元素尺寸位置信息之前先让它可见:

class NoviceGuide {
    to() {
        // ...
        this.scrollAncestorToElement(currentStep.element)
        const rect = currentStep.element.getBoundingClientRect()
        // ...
    }

    scrollAncestorToElement(element) {
        // 获取可滚动的祖先元素
        const parent = this.getScrollAncestor(element)
        if (parent === document.body) return
        // 祖先元素和目标元素的尺寸位置信息
        let parentRect = parent.getBoundingClientRect()
        let rect = element.getBoundingClientRect()
        // 滚动祖先元素,让目标元素可见
        parent.scrollTop = parent.scrollTop + rect.top - parentRect.top
        // 继续向上递归
        this.scrollAncestorToElement(parent)
    }
}

结尾

本文详细的介绍了如何实现一个新手引导的功能,可能还有没有考虑到的问题或者实现上的缺陷,欢迎留言指出。

完整代码:https://github.com/wanglin2/simple-novice-guide。

在线示例:https://wanglin2.github.io/simple-novice-guide/。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710393.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

被中文乱码折磨的我在此总结一下编码相关知识

本文大致介绍了三个问题&#xff1a; 常见的字符编码以及他们是如何编码从而被计算机识别的&#xff1f;为什么会有这些字符编码和他们被创建的背景和顺序&#xff1f;常见的乱码问题应该如何防止以及如何解决&#xff1f; 常见的字符编码 ASCII&#xff0c;GB2312&#xff…

(小程序)按钮切换对应展示区域

(小程序)按钮切换对应展示区域 需求&#xff1a;点击按钮切换表格和图表两种展示方式 html <u-button type"primary" size"mini" text"图表" v-if"form.curType table"click"showEcharts"></u-button> <u…

同一个分支maven构建出来的包不一样?

现象 最近发布spring boot项目时遇到了一个奇怪的问题&#xff0c;日志异常信息如下&#xff1a; Caused by: java.lang.IllegalArgumentException: LoggerFactory is not a Logback LoggerContext but Logback is on the classpath. Either remove Logback or the competing …

Image Watch 的使用

目录 1、下载地址 2、安装完成 3、调试 1、下载地址 Image Watch - Visual Studio Marketplace 2、安装完成 打开VS&#xff0c;在项目->其他窗口中有image watch选项 3、调试 一直放大图像可以查看详细的色彩值&#xff0c;通道数为3

各省绿色创新效率原始数据+测算(2020-2021)

采用了考虑非期望产出的超效率SBM模型来测量中国各省的绿色创新效率。该数据包括原始数据测算结果&#xff0c;包括人力资本、资本、能源投入&#xff0c;以及技术、经济、生态效益等多个维度的数据。可以帮助研究人员了解中国各省的绿色创新效率情况&#xff0c;揭示不同省份在…

搜索引擎概念解析

搜索引擎概念解析 什么是搜索引擎 MySQL搜索引擎举例 搜索引擎是一种用于在互联网上搜索并呈现相关信息的工具。它通过自动扫描和索引大量网页内容&#xff0c;并根据用户提供的关键词或查询条件&#xff0c;返回与之相关的网页链接和摘要。 当用户在搜索引擎中输入关键词或…

系列二、RocketMQ基本概念 系统架构

一、基本概念 1.1、消息&#xff08;Message&#xff09; 消息是指&#xff0c;消息系统所传输信息的物理载体&#xff0c;生产和消费数据的最小单位&#xff0c;每条消息必须输入一个主题。 1.2、主题&#xff08;Topic&#xff09; Topic表示一类消息的集合&#xff0c;每个…

筑牢三大新型能源基础设施,能源变革的分水岭和路线图

当下面临百年未有之大变局&#xff0c;创新科技密集发生&#xff0c;面对瞬息万变的世界&#xff0c;寻找到一条通往未来的确定性道路&#xff0c;绝对是行稳致远的前置条件。 “双碳战略”持续推进&#xff0c;距离2030“碳达峰”越来越近&#xff0c;能源产业变革的重要性和迫…

【SpringMVC 学习笔记】

SpringMVC 笔记记录 1. SpringMVC 简介2. 入门案例3. 基本配置3.1 xml形式配置3.2 注解形式配置 4. 请求4.1 请求参数4.1.1 普通类型传参4.1.2 实体类类型传参4.1.3 数组和集合类型传参 4.2 类型转换器4.3 请求映射 5. 响应 1. SpringMVC 简介 三层架构 2. 入门案例 3. 基本…

如何建立自己的知识体系?202209

知识太多了&#xff0c;无法全部快速吸收进大脑&#xff0c;需要通过特定的方法、技能&#xff0c;在面对大量知识的情况下&#xff0c;快速梳理&#xff0c;构建自己的知识体系。 学习的目标&#xff0c;不仅仅是记忆知识&#xff0c;而是搜索知识、并过滤、洞察、理解、使用…

Spring Boot 整合视图层技术 Thymeleaf

大家好&#xff01;我是今越。简单记录一下在 Spring Boot 框架中如何整合 Thymeleaf 及使用。 Thymeleaf 简介 Thymeleaf 是新一代 Java 模板引擎&#xff0c;它类似于 Velocity、FreeMarker 等传统 Java 模板引擎&#xff0c;但是与传统 Java 模板引擎不同的是&#xff0c;T…

爆肝整理,App测试小技巧,全覆盖功能到性能测试...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 app测试是使用手动…

ES查询[全网最全免费送付费内容]

ES查询 相关度搜索&#xff0c;需要计算评分 _score 相关度评分用于对搜索结果排序&#xff0c;评分越高则认为其结果和搜索的预期值相关度越高&#xff0c;即越符合搜索预期值。在7.x之前相关度评分默认使用TF/IDF算法计算而来&#xff0c;7.x之后默认为BM25。 源数据&…

Win 10 重装系统(PE方式)

前言&#xff1a; 最近这个笔记本&#xff08;ThinkPad E480&#xff0c;使用了四年左右&#xff09;用起来很卡&#xff0c;经常开机状态时&#xff0c;合上之后&#xff0c;再打开屏幕就卡死了&#xff0c;鼠标和键盘按了都没有反应&#xff0c;无奈之下只能强制按电源关机后…

SpringCloud——Nacos下载

文章目录 nacos简介nacos下载nacos的启动访问nacos nacos简介 Nacos&#xff08;全称为 “Naming and Configuration Service”&#xff09;是阿里巴巴开源的一个用于实现动态服务发现、服务配置和服务元数据管理的项目。它是一个分布式系统的服务基础设施&#xff0c;为云原生…

turtle画春联

import turtle #右边春联 turtle.penup() turtle.goto(100,150) turtle.pendown() turtle.color(red,red) turtle.begin_fill() turtle.forward(50) turtle.right(90) turtle.forward(400) turtle.right(90) turtle.forward(50) turtle.right(90) turtle.forward(400) turtle.e…

【大数据Hive】Hive 窗口函数使用详解

目录 一、前言 二、hive 窗口函数概述 2.1 聚合函数与窗口函数差别 2.1.1 创建一张表 2.1.2 加载数据到表中 2.1.3 sumgroup by普通常规聚合操作 2.1.4 sum窗口函数聚合操作 三、窗口函数 3.1 窗口函数语法 3.2 参数说明 3.2.1 Function(arg1,..., argn) 3.2.2 OV…

ElasticSearch 索引设计

ElasticSearch 索引设计 在MySQL中数据库设计非常重要&#xff0c;同样在ES中数据库设计也是非常重要的 概述 创建索引就像创建表结构一样&#xff0c;必须非常慎重的&#xff0c;索引如果创建不好后面会出现各种各样的问题 索引设计的重要性 索引创建后&#xff0c;索引的分片…

找工作第二弹——挑战CSS重难点一篇就够

目录 前言CSS知识点篇1. 选择器2. CSS三大特性三大特性计算权重 3. 显示模式显示模式转化 4. 解决高度塌陷问题5. 浮动浮动介绍为什么要清除浮动-解决高度塌陷问题清除浮动额外标签法&#xff08;W3C推荐做法&#xff09;给父亲添加overflow伪元素法双伪元素 6. BFC7. 定位子绝…

辉哥带你学hive第八讲

1.自定义函数 1.1 自定义函数类型 根据用户自定义函数类别分为以下三种&#xff1a; &#xff08;1&#xff09;UDF&#xff08;User-Defined-Function&#xff09; 一进一出。 &#xff08;2&#xff09;UDAF&#xff08;User-Defined Aggregation Function&#xff09; 用户…