样式穿透和实现固钉效果
最近在写代码时遇到一个问题,就是我想要实现一个固钉效果,让某个元素在超过屏幕时会悬浮在屏幕上的某个位置。这就类似与flex布局。通常情况下许多的ui组件库都有提供affix
组件来实现固钉效果,就比如element plus给我们提供的el-affix
组件就有这样的效果,但是在某些情况之下el-affix
就会失效。
在我们通常的vue项目中为了防止我的些的css样式冲突,我们都会在style上加上scoped
<style lang="scss" scoped>
<style>
关于scoped
就像这样,这时可能就会出现el-affix
组件失效,其原因就是因为我加上了scoped
这个属性,因为vu是单页面应用,所以我们写的不同页面最后都会被打包成一个..html
文件,所以你在不同页面定义的css样式最后都会被引入到这个html文件中,这就导致如果你在不同的文件中定义的相同的文件名称,就很有可能导致样式错乱。所以就有了scoped
,他的原理就是通过给样式加上识别标签来进行分类,不同的页面识别标签也就不同,
如图div后面的一大串就是vue为我们加上的识别标签。
同样在引入的css文件中vue也会给我们加上识别标签,这样就不会出现样式名冲突的问题,但是我们有时在使用第三方样式库(比如element,ant等)时就会出现无法修改样式的问题,这是因为vue没有将识别标签加到组件上
vue只会在你的最外层上加上data-v-xxxxxx
,但是在css文件中vue会帮你吧data-v-xxxxxx
拼接在样式的最后一段。所以就导致了你都样式永远无法命中我们想要的元素,除非我们把scoped
属性去掉,但是这样就可能倒是我们上面说的问题,所以我们不推荐。
样式穿透
这时我们就引入了另一个概念,就是样式穿透。样式穿透的使用方式很多,有>>>
,::v-deep
,/deep/
等。在我们使用了样式穿透后vue就会把唯一的识别标志移到我们写::v-deep
之后
.home_main {
max-width: 1100px;
margin: 48px auto 28px;
display: flex;
.page{
&:deep(button){
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
}
&:deep(.el-pager){
.number{
border-radius: 4px;
background-color: #fff;
box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
}
.more{
border-radius: 4px;
box-shadow: 0 2px 1px -2px rgb(0 0 0 / 20%), 0 2px 2px 0 rgb(0 0 0 / 14%), 0 1px 5px 0 rgb(0 0 0 / 12%);
background-color: #fff;
}
.active{
box-shadow: 0 2px 4px -1px rgb(0 0 0 / 20%), 0 4px 5px 0 rgb(0 0 0 / 14%), 0 1px 10px 0 rgb(0 0 0 / 12%);
}
}
}
再看没有加样式穿透之前的样式
随意通过样式穿透我们就可以实现scoped所带来的副作用,顺便一提>>>
的兼容性不是很高,所以不是很推荐,最好写后面两种。(因为最近在写vue3的项目,在vue3中提示使用&:deep(.name)
来实现样式穿透,目前我也不知道他是vue3的特定写法还是,新的写法,就和大家分享一下。还有一个我觉得说样式穿透讲的挺好的视频https://www.douyin.com/video/6987146287137180966 如果还是有不懂什么是样式穿透以及原理的,推荐可以看看)
.el-card {
&:deep(.el-card__body) {
padding: 0;
display: flex;
height: 250px;
align-items: center;
}
}
说到这里大部分的样式问题都可以解决,但是当我去尝试el-effix
时结果发现还是不行,弄得我快疯了,最后去看了el-affix
的源码才知道,elememt plus 是通过判断el-affix指定的容器,当超过屏幕时给内层div添加样式,让el-affix变成悬浮的状态,在没超过屏幕的状态下移除div的悬浮样式
样式穿透可以解决当容器超出屏幕悬浮的问题,但是当回到原始位置时,就无法消除内层div的el-affix--fixed
样式,导致出现一直悬浮的状态,可能是我技术还不够吧,如果有解决的朋友可以分享下。
我只好另找出路,既然element plus的组件用不了,那就自己写一个(其实是网上cv的),由于我的项目的vue3+ts的,但是网上大多都是vue2和js的所以就自己改了那么一点点,本人小白,写的不好的地方还请轻点喷,不多说,上代码
<template>
<div class="affix-placeholder" :style="data.wrapStyle">
<div class="affix-div" :class="{'affix': data.affixed}" :style="data.styles">
<slot />
</div>
</div>
</template>
<script lang="ts">
import {
computed,
defineComponent,
getCurrentInstance,
onMounted,
reactive,
onUnmounted } from 'vue';
import { Styles, affixProps } from './affixProps';
export default defineComponent({
name: 'Affix',
props: affixProps,
setup(props: any) {
const data = reactive({
affixed: false,
styles: {} as Styles,
affixedClientHeight: 0,
wrapStyle: {},
});
const instance = getCurrentInstance();
const getScroll = function(w: Window, top?: boolean){
let ret = w[`page${(top ? 'Y' : 'X')}Offset`];
const method = `scroll${top ? 'Top' : 'Left'}`;
if (typeof ret !== 'number') {
const d = w.document;
// ie6,7,8 standard mode
ret = d.documentElement[method];
if (typeof ret !== 'number') {
// quirks mode
ret = d.body[method];
}
}
return ret;
};
const getOffset = function (element: Element) {
const rect = element.getBoundingClientRect();
const body = document.body;
const clientTop = element.clientTop || body.clientTop || 0;
const clientLeft = element.clientLeft || body.clientLeft || 0;
const scrollTop = getScroll(window, true);
const scrollLeft = getScroll(window);
return {
top: rect.bottom + scrollTop - clientTop - data.affixedClientHeight,
left: rect.left + scrollLeft - clientLeft
};
};
const offsets = computed(()=>{
if (props.boundary) {
return 0;
}
return props.offset;
});
const handleScroll = () => {
const scrollTop = getScroll(window, true) + offsets.value; // handle setting offset
const elementOffset = getOffset(instance?.proxy?.$el);
if (!data.affixed && scrollTop > elementOffset.top) {
data.affixed = true;
data.styles = {
top: `${offsets.value}px`,
left: `${elementOffset.left}px`,
width: `${instance?.proxy?.$el.offsetWidth}px`
};
props.onAffix(data.affixed);
}
// if setting boundary
if (props.boundary && scrollTop > elementOffset.top) {
const el = document.getElementById(props.boundary);
if (el) {
const boundaryOffset = getOffset(el);
if ((scrollTop + offsets.value) > boundaryOffset.top) {
const top = scrollTop - boundaryOffset.top;
data.styles.top = `-${top}px`;
}
}
}
if (data.affixed && scrollTop < elementOffset.top) {
data.affixed = false;
data.styles = {};
props.onAffix(data.affixed);
}
if (data.affixed && props.boundary) {
const el = document.getElementById(props.boundary.slice(1));
if (el) {
const boundaryOffset = getOffset(el);
if ((scrollTop + offsets.value) <= boundaryOffset.top) {
data.styles.top = 0;
}
}
}
};
onMounted(()=> {
data.affixedClientHeight = instance?.proxy?.$el.children[0].clientHeight;
data.wrapStyle = { height: `${data.affixedClientHeight}px` };
window.addEventListener('scroll', handleScroll);
window.addEventListener('resize', handleScroll);
});
onUnmounted(()=>{
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleScroll);
});
return {
data,
};
}
});
</script>
<style lang="sass">
.affix
position: fixed
</style>
import { PropType } from 'vue';
import { FunctionType } from '@/constant/Type';
export interface Styles{
top?: string | number,
left?: string | number,
width?: string | number,
}
export const affixProps = {
offset: {
type: Number as PropType<number>,
default: 0,
},
onAffix: {
type: Function as PropType<FunctionType>,
default() {}
},
boundary: {
type: String as PropType<string>,
default: 0,
}
};
export interface FunctionType {
(): void;
}
这样一个affix组件就写完了,但是经过我的一轮测试,发现还是不行,最后发现是因为自定义的affix组件是在初始化时获取父级组件或绑定的父级组件的高度,但是我的父级组件的高度是动态的,通过后端获取数据后再通过v-for渲染,所以初始高度很小,在高度改变后的affix内的父级高度却没有发生变化。
sticky粘性布局
在经过了又一番的查找资料后,我又看到了一个解决方案,就是posistion:sticky
在css的posistion中我们常用到的属性有relative
,absolute
,sticky就在粘性布局我们可以通过设置
position: -webkit-sticky;
position: sticky;
这个属性来实现类似固钉的效果,在官方文档中的说明是sticky 英文字面意思是粘,粘贴,所以可以把它称之为粘性定位。
position: sticky; 基于用户的滚动位置来定位。
粘性定位的元素是依赖于用户的滚动,在 position:relative 与 position:fixed 定位之间切换。
它的行为就像 position:relative; 而当页面滚动超出目标区域时,它的表现就像 position:fixed;,它会固定在目标位置。
元素定位表现为在跨越特定阈值前为相对定位,之后为固定定位。
这个特定阈值指的是 top, right, bottom 或 left 之一,换言之,指定 top, right, bottom 或 left 四个阈值其中之一,才可使粘性定位生效。否则其行为与相对定位相同。
反正看这么多也不是很懂先cv再说,不出意外的话就要出意外了,果然不行,没办法只能再去看文档了,最后才知道posistion:stucky
要生效也需要有条件,总结来说
- 须指定 top, right, bottom 或 left 四个阈值其中之一,就是需要设置这几个元素任意一个值,并且 top 和 bottom 同时设置时,top 生效的优先级高,left 和 right 同时设置时,left 的优先级高。
- 最重要的一点,在设置了
posistion:stucky
后该元素的所有父级元素都不能设置overflow:hidden
不然也不会生效,具体可以查看官方文档。
最后终于可以了,就这一个问题困了我三天,本以为事情就这样结束了,结果过几天天我在加其他的功能时突然发现,又失灵了。。。。。
最后排查了好久才发现原来是原先在设置el-scrollbar
的overflow
时没有设置成功,再加一个
overflow: visible !important;
终于所有问题都解决了。