前言
思来想去很久,我都不知道该最先介绍哪一个组件才好?虽然我写的第一个组件是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✨
感谢看到最后💟💟💟