需求
需求不复杂,如上图所示:
- 导航栏,里面有若干个链接按钮
- 点击切换的时候,会有一个高亮标记随着选中的按钮移动
- 按钮尺寸不确定,高亮标记的尺寸随着选中的按钮变化
选择技术方案
我的第一想法是 CSS 纯原生,这样使用起来会非常简单。但是考察之后,发现不太可行,难点在于:
- 高亮标记如果跟着按钮走,无法处理按钮间切换
- 如果放在父容器里,可以在按钮间切换,但是尺寸没法跟着按钮走
所以短暂思考之后,我只能退而求其次,选择和框架结合的开发方式:高亮标记使用绝对定位来控制位置;当用户切换导航(点击按钮)的时候,使用 JS 获取目标按钮的位置和尺寸,并且赋给高亮标记;使用 CSS 变量控制动画,以提升代码的复用性,确保 CSS 规则不要太死硬。
这个过程并不复杂,我们可以利用事件冒泡机制,获取被点击的按钮,然后利用 getBoundingClientRect()
计算坐标。
CSS
CSS 部分比较简单,我们用一个伪元素作为高亮标记,然后配合 CSS 变量移动它即可。唯一需要注意的是,伪元素要跟按钮重合,所以必须用到 z-index
,按钮也必须手动设置成 position: relative
。
(WP 代码高亮插件挂了,大家将就看……)
.slide-navigator {
--highlight-x: 0; /* 标记的 x 坐标 */
--highlight-width: 0; /* 标记的宽度 */
position: relative;
&::before {
content: '';
position: absolute;
z-index: 0;
bottom: 0;
left: var(--highlight-x);
width: var(--highlight-width);
background: #ffd850;
transition: all 0.2s;
}
> * {
position relative;
}
}
这里暂时没考虑响应式,如果要切换横纵,可以增加一个 Y 坐标。如果需要定制样式,可以将高亮标记的样式抽出来单独处理。
React JSX 组件
作为组件,导航里面的按钮要跟着业务逻辑走,最好在业务组件里生成,然后通过 props.children
传进来。点击事件可以直接交由父容器,也就是本组件侦听,然后利用 event.target
来判断选中的是哪个按钮。
这里还有一个抉择,即怎么确定高亮按钮是哪个。我考虑了一下,觉得将这个逻辑一并放在业务组件里比较好,然后通过 props.currentIndex
传进来使用。这样能确保本组件的复用性。坏处就是每个用到这个组件的地方都要写一个函数来判断,略嫌麻烦。
最终组件代码如下:
(WP 代码高亮插件挂了,大家将就看……)
import {
CSSProperties,
FC,
MouseEventHandler,
useEffect,
useRef,
useState
} from 'react';
interface SlideHighlightProps {
children: React.ReactNode;
className: string;
currentIndex: number;
}
type SlideNavigatorHighlight = CSSProperties & {
'--highlight-x'?: string;
'--highlight-width'?: string;
};
const SlideHighlight: FC<SlideHighlightProps> = function (props) {
const { className, children, currentIndex } = props;
const root = useRef<HTMLDivElement>(null);
const [navStyle, setNavStyle] = useState<SlideNavigatorHighlight>();
/* 处理按钮点击事件,找到被点击的按钮,并将其位置和宽度赋给高亮标记 */
const onClick: MouseEventHandler<HTMLDivElement> = (event) => {
if (!root.current) return;
const target = Array.from(root.current.children).find((v) =>
v.contains(event.target as Node)
) as HTMLElement;
const { left } = root.current.getBoundingClientRect();
const { left: l, width } = target.getBoundingClientRect();
setNavStyle({
'--highlight-x': `${l - left}px`,
'--highlight-width': `${width}px`
});
};
/* 组件初始化时,或者导航切换时,改变高亮标记的位置 */
useEffect(() => {
if (!root.current) return;
/* 如果导航不应该高亮,就把标记隐藏起来 */
if (currentIndex === -1) {
setNavStyle({
'--highlight-width': `0px`
});
return;
}
const { left } = root.current.getBoundingClientRect();
const target = root.current.children[currentIndex] as HTMLElement;
const { left: l, width } = target.getBoundingClientRect();
setNavStyle({
'--highlight-x': `${l - left}px`,
'--highlight-width': `${width}px`
});
}, [currentIndex]);
return (
<div ref={root} className={'slide-navigator ' + className} style={navStyle} onClick={onClick}>
{children}
</div>
);
};
export default SlideHighlight;
总结
我的 React 经验不太多,不考虑项目结构倒也问题不大,但是抽象组件的经验比较少,这次也是在 ChatGPT 帮助下才写好,所以赶紧趁热发一篇笔记。
如果你对 React 经验比较丰富,发现我上面的代码有问题,欢迎指出。如果你对 React 开发有问题,对组件开发有疑问,也欢迎留言讨论。
欢迎吐槽,共同进步