实现一个vue3组件库 - scrollbar滚动条

前言

思来想去很久,我都不知道该最先介绍哪一个组件才好?虽然我写的第一个组件是button按钮, 但是也是因为简单所以第一个写,逻辑代码不是很多,样式倒是一大堆...感觉不适合用作开篇介绍,最后选择了scrollbar滚动条组件。

本组件将会涉及vueuse的一些hook函数 和 一些不是很难的计算

组件最终的呈现请移步: Scrollbar 滚动条 | SSS UI Plus

code

组件目录结构

由于是开篇组件,所以将会再次介绍组件的目录结构,在这之后将不会有此导航。若是不感兴趣可以跳到下一个同级导航

对于完整项目结构,请移步: 实现一个vue3组件库-项目初始化

packages

index.ts用于导出所有的组件

arduino 复制代码
```ts
import SScrollbar from "./SScrollbar";
//import 其余组件

export {
    SScrollbar,
    //....

}
```

其中,每一个组件结构都是一个src文件夹和一个index.ts组成,index.ts用于插入一个注册函数并导出此组件
php 复制代码
```ts
// SScrollbar->index.ts
import Scrollbar from "./src/scrollbar.vue";
import {App} from "vue";

Scrollbar.install = function (Vue:App) {
    Vue.component('SScrollbar',Scrollbar);
}

export default Scrollbar;
```

installer.ts

此文件用于注册所有的组件

ts 复制代码
import {App} from "vue";
import * as comps from "./packages";

const installer = function (Vue:App) {
    for (let key in comps){
        Vue.component(key, comps[key]);
    }
}

export default installer

/index.ts

此文件用于导出组件库

ts 复制代码
/*css引入 特别注意全局样式最先引入*/
import "./src/styles/animate.css"
import "./src/styles/variable.less"
import "./src/styles/global.less"
import "./src/styles/icons/iconfont.css"


import installer from "./installer";
export * from "./packages"
export * from "./packages/SMessage"



export default installer

scrollbar的html结构

简化结构

xml 复制代码
<div>  //最外层容器
    <div><slot></slot></div> 需要添加滚动条的元素
    
    <div></div> 垂直滚动条
    <div></div> 水平滚动条
    
</div>

实际结构

sss-ui-plus/packages/SScrollbar/src/scrollbar.vue

scrollbar 样式文件

sss-ui-plus/packages/SScrollbar/src/scrollbar.less

scrollbar 逻辑

数据约定:

  • wrap 整个滑动区域
  • view 视口,也就是你看到的元素
  • bar 滚动条的轨道
  • thumb 滚动条的滑块

对了....代码中的wrap我全部写成warp了,全部改起来很麻烦,请允许这个错误😭

计算滑块大小(核心函数)

滚动条实际和缩略图很像,我们将整个滑动区域映射为滚动条的轨道,将视口映射为滚动条的滑块。 因此我们可以得到:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> v i e w 高度 / w r a p 高度 = t h u m b 高度 / b a r 高度 \ view高度 / wrap高度 = thumb高度 / bar高度 </math> view高度/wrap高度=thumb高度/bar高度

当然,对于水平滚动条的宽度也是相同的计算方式

最后我们可以写出一个函数,专门用于计算滚动条滑块的大小:

ts 复制代码
const computedThumbSize = () => {
    const warpEl = unrefElement(warp);
    const barYEl = unrefElement(barY);
    const barXEl = unrefElement(barX);
    const {scrollHeight:warpHeight, scrollWidth:warpWidth,offsetHeight: viewHeight, offsetWidth:viewWidth} = warpEl!;

    const barHeight = barYEl!.offsetHeight;
    const barWidth = barXEl!.offsetWidth;

    const thumbHeight = viewHeight * barHeight / warpHeight;
    const thumbWidth = viewWidth * barWidth / warpWidth;

    thumbYStyle.value.height = `${thumbHeight}px`;
    thumbXStyle.value.width = `${thumbWidth}px`;

}

计算滑块偏移量(核心函数)

滑块thumb的偏移量完全受控于视口view的偏移量,在之后我们拖拽滑块时,实际上修改的也是视口的偏移量

同样的逻辑,对于偏移量也是一个映射关系:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> v i e w 偏移量 / w a r p 高度 = t h u m b 偏移量 / b a r 高度 \ view偏移量 / warp高度 = thumb偏移量 / bar高度 </math> view偏移量/warp高度=thumb偏移量/bar高度

ini 复制代码
const computedThumbPos = () => {
    const warpEl = unrefElement(warp);
    const barYEl = unrefElement(barY);
    const barXEl = unrefElement(barX);

    const {scrollHeight:warpHeight, scrollWidth:warpWidth,scrollTop: viewOffsetY, scrollLeft:viewOffsetX} = warpEl!;

    const barHeight = barYEl!.offsetHeight;
    const barWidth = barXEl!.offsetWidth;


    const thumbOffsetY = viewOffsetY * barHeight / warpHeight;
    const thumbOffsetX = viewOffsetX * barWidth / warpWidth;


    //滑块偏移量受控于视口偏移量
    thumbYStyle.value.top = `${thumbOffsetY}px`;
    thumbXStyle.value.left = `${thumbOffsetX}px`;


}

为warp添加滚动事件

只需要一句话,滑块就可以滚动了✨

ts 复制代码
useEventListener(warp, 'scroll', () => {
    computedThumbPos();
})

注意这里使用了vueuse的useEventListener

为滑块添加"拖拽"事件

严格来讲,并不是拖拽事件,而是由mousedown mousemove mouseup结合成的事件

首先我们有几个变量需要介绍:

ini 复制代码
// 记录偏移量 也就是记录滑块被拖拽的距离
const offset = {
    x: 0,
    y: 0
};
// 记录点击的坐标 也就是在滑块被点击时的位置
const down = {
    x: 0,
    y: 0
};
// 记录移动距离 在滑块移动时,鼠标的位置
const move = {
    x: 0,
    y: 0
};
// 记录原本位置 也就是视口原本的偏移量
const origin = {
    x: 0,
    y: 0
}

let flag:'thumbX' | 'thumbY'; 标记点击的是垂直滑块还是水平滑块

在滑块被点击时,需要记录点击的位置,和视口原本的偏移量,也就是记录down origin 同时启用mousemove事件。

需要注意的是,鼠标移动事件要添加到body上面,因为鼠标可以移动出整个视口

ini 复制代码
useEventListener(thumbY, "mousedown", (evt: MouseEvent) => {
    down.y = evt.clientY;    //记录点击位置
    origin.y = unrefElement(warp)!.scrollTop;    //记录原本的偏移量
    flag = 'thumbY';    //标记点击的是垂直滑块
    active.value = true;  //控制样式的,与逻辑无关

    //为body添加mousemove事件
    unrefElement(document.body)!.addEventListener('mousemove', handleMove);  
})

useEventListener(thumbX, "mousedown", (evt: MouseEvent) => {
    down.x = evt.clientX;
    origin.x = unrefElement(warp)!.scrollLeft;
    flag = 'thumbX';
    active.value = true;


    unrefElement(document.body)!.addEventListener('mousemove', handleMove);
})

相反的,在mouseup时,需要移除这个mousemove事件

javascript 复制代码
useEventListener(document.body, 'mouseup', () => {
    active.value = false;

    unrefElement(document.body)!.removeEventListener('mousemove', handleMove);

})

最后是如何处理鼠标移动,其实很简单,也是一开始的那一套映射关系
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> v i e w 偏移量 / w a r p 高度 ( 宽度 ) = t h u m b 偏移量 / b a r 高度 ( 宽度 ) \ view偏移量 / warp高度(宽度) = thumb偏移量 / bar高度(宽度) </math> view偏移量/warp高度(宽度)=thumb偏移量/bar高度(宽度)

此时thumb偏移量就是下面的offset变量了,而要计算的结果也变成了view偏移量

ini 复制代码
const handleMove = (evt: MouseEvent) => {
    move.x = evt.clientX;  //这里获取的是鼠标移动时的位置
    move.y = evt.clientY;

    offset.x = move.x - down.x;  //计算偏移量
    offset.y = move.y - down.y;


    const warpEl = unrefElement(warp);
    const barYEl = unrefElement(barY);
    const barXEl = unrefElement(barX);
    const warpHeight = warpEl!.scrollHeight;
    const warpWidth = warpEl!.scrollWidth;
    const barHeight = barYEl!.offsetHeight;
    const barWidth = barXEl!.offsetWidth;


    //最后根据点击的是哪一个滑块而设置视口的偏移量就行
    if (flag === 'thumbY') {
       unrefElement(warp)!.scrollTop = warpHeight * offset.y / barHeight + origin.y;
    }
    else if (flag === 'thumbX') {
       unrefElement(warp)!.scrollLeft = warpWidth * offset.x / barWidth + origin.x;
    }
}

还记得滑块的偏移量受控于视口偏移量么?当我们手动设置了视口的偏移量(scrollTop,scrollLeft)之后,会自动触发视口的滚动事件,进而触发 computedThumbPos()函数

为视口添加"resize"事件

实际上,只有浏览器视口才有resize事件,因此你直接为某个元素设置resieze事件是没用的,我们可以通过observer监听元素的大小进而实现这个事件。幸运的是,vueuse为我们提供了useElementSize

ts 复制代码
if (!props.noResize) {
    // 监听元素大小变化
    useResizeObserver(warp,() => {
       computedThumbPos();
       computedThumbSize();
    })
}

监听warp的子元素变化

但warp内部元素发生变化时,可能需要重新计算滑块的大小和位置,vueuse提供了useMutationObserver可以很方便的实现这个功能!

javascript 复制代码
useMutationObserver(warp, () => {
    computedThumbPos();
    computedThumbSize();
}, {
    attributes:true,   //是否观察节点属性变化
    childList:true,   //是否观察子节点变化
    subtree:true,  //子节点是否继承这个观察器
})

完整逻辑

sss-ui-plus/packages/SScrollbar/src/scrollbar.vue

写在最后

这个组件也许有很多不完善的地方,欢迎指出!

这个项目的地址是:lastertd/sss-ui-plus: 适用于vue3的组件库 (github.com)在这里求一个star✨

感谢看到最后💟💟💟

相关推荐
getaxiosluo41 分钟前
vue3使用element-plus,树组件el-tree增加引导线
前端·javascript·vue.js·elementui·css3·element-plus
大山同学1 小时前
最新开源DCL-SLAM:一种用于机器人群体的分布式协作激光雷达 SLAM 框架
人工智能·分布式·机器人·开源·slam·感知定位
再不会python就不礼貌了2 小时前
Ollama 0.4 发布!支持 Llama 3.2 Vision,实现多模态 RAG
人工智能·学习·机器学习·ai·开源·产品经理·llama
拼图2092 小时前
Vue.js开发基础——数据绑定/响应式数据绑定
前端·javascript·vue.js
刘志辉2 小时前
vue反向代理配置及宝塔配置
前端·javascript·vue.js
老胡说前端3 小时前
vue3 elemnetPlus table 数据id 相同的合并单元格
javascript·vue.js·elementui
废柴小z3 小时前
从零创建vue+elementui+sass+three.js项目
javascript·vue.js·elementui
周三有雨3 小时前
vue3 + vite 实现版本更新检查(检测到版本更新时提醒用户刷新页面)
前端·vue.js·typescript
何作欢4 小时前
day04 vue学习
javascript·vue.js·学习
程序媛小果5 小时前
基于java+SpringBoot+Vue的微服务在线教育系统设计与实现
java·vue.js·spring boot