likes
comments
collection
share

Affix图钉组件源码解析(vue3.0+ts)

作者站长头像
站长
· 阅读数 42

Affix 图钉📌

使用图钉,可以将内容固定在屏幕上,并且不随页面的滚动而滚动。常用于侧边菜单等

例子🌰

接下来我们先看一个例子再开始我们的源码之旅,下图展示了 Affix 组件的基本功能

Affix图钉组件源码解析(vue3.0+ts)

以上是最基本的功能接下来让我们一起去实现这个功能吧👀

基础用法

创建最基本的显示

<template>
    <div>
        <!-- slot -->
        <div>
            <slot></slot>
        </div>
    </div>
</template>

<script lang='ts'>
import { defineComponent } from 'vue';

const prefixCls = 'ivue-affix';

export default defineComponent({
    name: prefixCls,
});
</script>

外部调用

<template>
    <ivue-affix>
        <div class="demo-affix">固定在最顶部</div>
    </ivue-affix>
</template>

<script>
export default {};
</script>

<style lang="scss" scope>
.demo-affix {
    width: 100px;
    height: 30px;
    text-align: center;
    line-height: 30px;
    font-size: 12px;
    border-radius: 5px;
    background: rgba(0, 153, 229, 0.7);
    color: #fff;
}
</style>

预览

下图为以上代码之后的显示

Affix图钉组件源码解析(vue3.0+ts)

接下来我们去实现基本的固定功能

固定功能

  • onMounted 时执行一次 handleScroll 方法设置固定位置
  • window 上添加 scroll 事件
  • window 上添加 resize 事件
  • 使用 @vueuse/core 插件的 useEventListener 方法添加事件
  • 实现固定到顶部和固定到底部
<script lang='ts'>
import { defineComponent, onMounted } from 'vue';
import { useEventListener } from '@vueuse/core';

const prefixCls = 'ivue-affix';

export default defineComponent({
    name: prefixCls,
    setup() {

        // methods
        
        // 监听滚动
        const handleScroll = () => {

        };

        // mounted
        onMounted(() => {
            // 初始化
            handleScroll();

            // 监听滚动和缩放事件
            useEventListener(window, 'resize', handleScroll);
            useEventListener(window, 'scroll', handleScroll);
        });
    },
});
</script>

滚动条到达顶部的距离

首先需要获取滚动条到达顶部的距离

// 获取滚动数值
const getScroll = (target: Window, top: boolean) => {
    const prop = top ? 'pageYOffset' : 'pageXOffset';
    const method = top ? 'scrollTop' : 'scrollLeft';

    // 在 window 中查找参数 pageYOffset || pageXOffset
    let ret = target[prop];

    // 如果 没有 pageYOffset || pageXOffset
    if (typeof ret !== 'number') {
        // 返回 scrollTop || scrollLeft
        ret = window.document.documentElement[method];
    }

    return ret;
};

// 监听滚动
const handleScroll = () => {
    // 获取垂直滚动高度
    const scrollTop = getScroll(window, true);
};

获取当前组件在视图上的位置

获取当前组件在视图上的位置用于判断组件是否到达顶部或者底部

<template>
    <div ref="wrapper">
        <!-- slot -->
        <div>
            <slot></slot>
        </div>
    </div>
</template>

<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue';
import { useEventListener } from '@vueuse/core';

const prefixCls = 'ivue-affix';

export default defineComponent({
    name: prefixCls,
    setup() {
        // ref = wrapper
        const wrapper = ref<HTMLDivElement>();

        // methods

        ........

        // 获取元素坐标
        const getOffset = (element: HTMLDivElement) => {
            // 方法返回元素的大小及其相对于视口的位置
            const rect = element.getBoundingClientRect();

            // 滚动条的垂直位置
            const scrollTop = getScroll(window, true);
            // 滚动条的水平位置
            const scrollLeft = getScroll(window, false);

            const docEl = window.document.body;
            // 视口高度
            const clientTop = docEl.clientTop || 0;
            //视口宽度
            const clientLeft = docEl.clientLeft || 0;

            return {
                // 元素上边到视窗上边的距离 + 滚动条的垂直位置 - 顶部边框的宽度(顶部边框的宽度)
                top: rect.top + scrollTop - clientTop,
                // 元素左边到视窗左边的距离 + 滚动条的水平位置 - 元素的左边框的宽度(不包括左外边距和左内边距)
                left: rect.left + scrollLeft - clientLeft,
            };
        };

        // 监听滚动
        const handleScroll = () => {
            ........

            // 获取元素坐标
            const elOffset = getOffset(wrapper.value);
        };

        ........

        return {
            // ref
            wrapper,
        };
    },
});
</script>

获取window高度和插槽元素的高度

获取这两个高度用于判断组件是否到达顶部和底部

........ 
// ref = content
const content = ref<HTMLDivElement>();
........ 
        
const handleScroll = () => { 
    ........ 
    
    // window高度
    const windowHeight = window.innerHeight;
    // 元素高度
    const elHeight = content.value.offsetHeight;
};

........ 

return {
    // ref
    ........,
    content,
};

固定到顶部

判断是否固定到顶部

添加 props 用于自定义达到指定偏移量后触发

props: {
    /**
     * 距离窗口顶部达到指定偏移量后触发
     *
     * @type {Number}
     */
    offsetTop: {
        type: Number,
        default: 0,
    },
    /**
     * 距离窗口底部达到指定偏移量后触发
     *
     * @type {Number}
     */
    offsetBottom: {
        type: Number,
    },
},

添加 reactive 用于保存滚动状态

TS类型判断详见此处

// data
const data = reactive<Data>({
    // 组件状态是否开启固定
    affix: false,
    // styles
    styles: {},
});

添加 initData 方法用于初始化滚动状态

// 初始化数据
const initData = () => {
    data.affix = false;
    data.styles = {};
};

添加计算属性 offsetType 判断当前组件是固定到顶部还是底部

// computed

// 滚动状态值
const offsetType = computed(() => {
    let type = 'top';

    // 是否有底部偏移量
    if (props.offsetBottom >= 0) {
        type = 'bottom';
    }

    return type;
});

添加计算属性 classes 判断是否可以添加固定 class 样式

样式详见此处

// computed

// 是否添加class设置 fixed
const classes = computed(() => {
    return [
        {
            [`${prefixCls}`]: data.affix,
        },
    ];
});

handleScroll 中添加固定到顶部的判断

// 监听滚动
const handleScroll = () => {
    ........
    
    // 固定在头部 Top
    if (
        // 元素的顶部 减去 需要到达指定位置的数值 < window 的滚动高度 向上滚动
        elOffset.top - props.offsetTop < scrollTop &&
        // 固定头部
        offsetType.value === 'top' &&
        // 没有开启固定状态
        !data.affix
    ) {
        // 开启固定状态
        data.affix = true;

        // 固定状态样式
        data.styles = {
            top: `${props.offsetTop}px`,
            left: `${elOffset.left}px`,
            width: `${wrapper.value.offsetWidth}px`,
        };
    }
    // 头部取消固定顶部
    else if (
        // 没有达到指定位置的数值
        elOffset.top - props.offsetTop > scrollTop &&
        // 固定头部
        offsetType.value == 'top' &&
        // 开启固定状态
        data.affix
    ) {
        // 初始化数据
        initData();
    }
    
    ........
};

预览

完成以上步骤后的效果图

Affix图钉组件源码解析(vue3.0+ts)

固定到底部

接下来继续实现判断是否固定到底部

// 监听滚动
const handleScroll = () => {
    ........
   
    // 固定在底部 Bottom
    if (
        // 元素的顶部 + 距离窗口底部达到指定偏移量后触发 + 元素高度 > 获取垂直滚动高度 + window高度
        elOffset.top + props.offsetBottom + elHeight >
            scrollTop + windowHeight &&
        // 固定底部
        offsetType.value == 'bottom' &&
        // 没有开启固定状态
        !data.affix
    ) {
        // 开启固定状态
        data.affix = true;
        // 固定状态样式
        data.styles = {
            bottom: `${props.offsetBottom}px`,
            left: `${elOffset.left}px`,
            width: `${wrapper.value.offsetWidth}px`,
        };
    }
    // 头部取消固定底部
    else if (
        // 元素的顶部 + 距离窗口底部达到指定偏移量后触发 + 元素高度 < 获取垂直滚动高度 + window高度
        elOffset.top + props.offsetBottom + elHeight <
            scrollTop + windowHeight &&
        // 固定底部
        offsetType.value == 'bottom' &&
        // 开启固定状态
        data.affix
    ) {
        // 初始化数据
        initData();
    }
};

另外需要显式设置 offset-bottom 才可开启固定到底部

<template>
    <div class="affix">
        <div class="padding"></div>
        <ivue-affix :offset-bottom="20">
            <div class="demo-affix">固定在最底部</div>
        </ivue-affix>
    </div>
</template>

<script>
export default {
    methods: {},
};
</script>

<style lang="scss" scope>
.affix {
    width: 100px;
    height: 3000px;
}

.demo-affix {
    width: 100px;
    height: 30px;
    text-align: center;
    line-height: 30px;
    font-size: 12px;
    border-radius: 5px;
    background: rgba(0, 153, 229, 0.7);
    color: #fff;
}

.padding {
    padding-top: 200vh;
}
</style>

预览

完成以上步骤后的效果图

Affix图钉组件源码解析(vue3.0+ts)

优化固定状态

接下来我们还需要优化固定后,组件没固定前在 dom 中所站的位置

我们先看没有优化之前的在 dom 中的位置

Affix图钉组件源码解析(vue3.0+ts)

添加占位元素

为组件添加占位元素节点,添加占位元素显示判断和样式设置

<template>
    <div ref="wrapper">
        <!-- slot -->
        ......
        <!-- 占位元素 -->
        <div v-show="data.slot" :style="data.slotStyle"></div>
    </div>
</template>

<script lang='ts'>
......

export default defineComponent({
    ......
    setup(props: Props) {
        ......
        
        // data
        const data = reactive<Data>({
            ......
            // slot 是否开启
            slot: false,
            // slotStyle
            slotStyle: {},
        });

        ......

        // methods

        // 初始化数据
        const initData = () => {
            ......
            
            data.slot = false;
            data.slotStyle = {};
        };

        // 监听滚动
        const handleScroll = () => {
            ......

            // 固定在头部 Top
            if (
                ......
            ) {
                ......
                
                // 占位元素样式
                data.slotStyle = {
                    width: `${content.value.clientWidth}px`,
                    height: `${content.value.clientHeight}px`,
                };

                // 显示占位元素
                data.slot = true;
            }
            // 头部取消固定顶部
            else if (
                ......
            ) {
                ......
            }

            // 固定在底部 Bottom
            if (
               ......
            ) {
                ......
                
                // 占位元素样式
                data.slotStyle = {
                    width: `${content.value.clientWidth}px`,
                    height: `${content.value.clientHeight}px`,
                };
                
                // 显示占位元素
                data.slot = true;
            }
            // 头部取消固定底部
            else if (
                ......
            ) {
                ......
            }
        };

        ......
    },
});
</script>

预览

下图为添加占位元素判断后显示的效果

Affix图钉组件源码解析(vue3.0+ts)

完善更多功能

在固定状态发生改变时触发事件

handleScroll 监听滚动方法中添加 emit 触发在固定状态发生改变时触发事件

setup(props: Props, { emit }) {
    ......

    // 监听滚动
    const handleScroll = () => {
        ......
        // 固定在头部 Top
        if (
           ......
        ) {
            ......
            // 在固定状态发生改变时触发
            emit('on-change', true);
        }
        // 头部取消固定顶部
        else if (
           ......
        ) {
            ......
            // 在固定状态发生改变时触发
            emit('on-change', false);
        }

        // 固定在底部 Bottom
        if (
           ......
        ) {
            ......
            // 在固定状态发生改变时触发
            emit('on-change', true);
        }
        // 头部取消固定底部
        else if (
           ......
        ) {
            ......
            // 在固定状态发生改变时触发
            emit('on-change', false);
        }
    };

   ......
},

预览

Affix图钉组件源码解析(vue3.0+ts)

结语

以上就是该组件的基本实现了,更多功能请参见源码

Github: 源码地址

官网: Affix官网地址

转载自:https://juejin.cn/post/7184685266961956925
评论
请登录