从零搭建Vue3.0组件库之无限滚动组件

一.无限滚动组件注册方式

import { App } from 'vue'
import InfiniteScroll from './src/index'
(InfiniteScroll as any).install = (app: App): void => {
  app.directive('InfiniteScroll', InfiniteScroll)
}
export default InfiniteScroll
1
2
3
4
5
6

二.滚动指令实现

1.指令定义

import { ComponentPublicInstance, ObjectDirective } from "vue";
type InfiniteScrollCallback = () => void;
type InfiniteScroll = HTMLElement & {
    'infinite': {
        container: HTMLElement       // 滚动容器
        delay: number                // 延迟时间
        cb: InfiniteScrollCallback   // 触发的滚动方法
        onScroll: () => void         // 滚动时触发的回调
        observer?: MutationObserver, // 用于监控高度不够时增加数据
        instance:ComponentPublicInstance // 绑定到哪个组件实例
    }
}
const InfiniteScroll: ObjectDirective<InfiniteScroll, InfiniteScrollCallback> = {
    mounted(el,bindings){
        const { value: cb, instance } = bindings;
        await nextTick(); // 保证父元素加载完成
    },
    unmounted(el){}
}
export default InfiniteScroll;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

2.获取指令参数

let { delay, immediate } = getScrollOptions(el, instance);
1

获取无限滚动信息

const attributes = {
    delay: { // 节流延迟
        type: Number,
        default: 200
    },
    distance: { // 触底距离
        type: Number,
        default: 0
    },
    disabled: { // 是否禁用
        type: Boolean,
        default: false
    },
    immediate: { // 立即撑满内容
        type: Boolean,
        default: true
    }
}
type Attrs = typeof attributes;
type ScrollOtions = { [K in keyof Attrs]: Attrs[K]['default'] }
const getScrollOptions = (el:HTMLElement, instance:ComponentPublicInstance):ScrollOtions => {
    return Object.entries(attributes).reduce((memo, [name, option]) => {
        const { type, default: defaultValue } = option;
        const attrVal = el.getAttribute(`infinite-scroll-${name}`);
        let value = instance[attrVal] ?? attrVal ?? defaultValue;
        value = value == 'false' ? false : value;
        value = type(value);
        memo[name] = value;
        return memo;
    }, {} as ScrollOtions)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

3.获取滚动容器

const container = getScrollContainer(el);
1

获取overflow 具备 scroll | auto的属性

const isScroll = (el: HTMLElement): RegExpMatchArray => {
    let scrollY = getComputedStyle(el, '');
    let overflow: string = scrollY['overflow-y'] || '';
    return overflow.match(/(scroll|auto)/);
}
const getScrollContainer = (el: HTMLElement) => {
    let parent = el;
    while (parent) {
        if (parent == document.documentElement) {
            return document.documentElement
        }
        if (isScroll(parent)) {
            return parent;
        }
        parent = parent.parentNode as HTMLElement;
    }
    return parent
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

4.定义scroll事件

const handleScroll = (el: InfiniteScroll, cb: InfiniteScrollCallback) => {
	// 稍后实现滚动逻辑
}
const onScroll = throttle(() => handleScroll(el, cb), delay);
1
2
3
4

5.自动填充容器

if(immediate){
    const observer = new MutationObserver(throttle(() => checkFull(el, cb), 100))
    observer.observe(el, { childList: true, subtree: true });
    el.infinite.observer = observer
    checkFull(el, cb);
}
1
2
3
4
5
6
const checkFull = (el: InfiniteScroll, cb: InfiniteScrollCallback) => {
    const { container ,instance} = el.infinite;
    const { disabled } = getScrollOptions(el,instance);
    if (disabled) return;
    if (container.scrollHeight <= container.clientHeight) {
        cb();
    } else {
        let ob = el.infinite.observer;
        ob && ob.disconnect();
        delete el.infinite.observer
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

6.滚动检测

container.addEventListener('scroll', onScroll)
1
const handleScroll = (el: InfiniteScroll, cb: InfiniteScrollCallback) => {
    const {
        container,
        observer,
        instance
    } = el.infinite
    const { disabled, distance } = getScrollOptions(el, instance);
    const { clientHeight, scrollHeight, scrollTop } = container
    let shouldTrigger = false;

    if (observer || disabled) return;
    if (container == el) {
        shouldTrigger = scrollHeight - (clientHeight + scrollTop) <= distance
    } else {
        const { clientTop, scrollHeight: height } = el;
        // 卷去的高度 + 可视区域    自己距离父亲的高度 + 自己的所有高度 - 距离
        // 有可能定位  父距离顶部距离 - 自己离顶部的距离 
        shouldTrigger = scrollTop + clientHeight >= 0 + clientTop + height - distance
    }
    if (shouldTrigger) {
        cb();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

7.指令卸载

const { container, onScroll } = el.infinite;
container.removeEventListener('scroll', onScroll);
1
2

移除监听事件