Anchor锚点定位跳转,早期前端刀耕火种时代使用的是<a>标签的href属性做锚点跳转,固定模式下有点类似于固钉Affix,事实也确实如此。

API参数

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
export interface AnchorProps {
prefixCls?: string;
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
/** 距离窗口顶部达到指定偏移量后触发 */
offsetTop?: number;
/** 锚点区域边界 */
bounds?: number;
/** 固定模式 */
affix?: boolean;
/** affix={false} 时是否显示小圆点 */
showInkInFixed?: boolean;
/** 指定滚动的容器 */
getContainer?: () => AnchorContainer;
/** 自定义高亮锚点 */
getCurrentAnchor?: () => string;
onClick?: (
e: React.MouseEvent<HTMLElement>,
link: { title: React.ReactNode; href: string },
) => void;
/** 锚点滚动偏移量,默认与 offsetTop 相同 */
targetOffset?: number;
/** 监听锚点链接改变 */
onChange?: (currentActiveLink: string) => void;
}

render函数以及对应的Element

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
render = () => {
const { getPrefixCls, direction } = this.context;
const {
prefixCls: customizePrefixCls,
className = '',
style,
offsetTop,
affix,
showInkInFixed,
children,
} = this.props;
const { activeLink } = this.state;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
this.prefixCls = prefixCls;
const inkClass = classNames(`${prefixCls}-ink-ball`, {
visible: activeLink,
});
const wrapperClass = classNames(
`${prefixCls}-wrapper`,
{ [`${prefixCls}-rtl`]: direction === 'rtl' },
className,
);
const anchorClass = classNames(prefixCls, {
fixed: !affix && !showInkInFixed,
});
const wrapperStyle = {
maxHeight: offsetTop ? `calc(100vh - ${offsetTop}px)` : '100vh',
...style,
};

const anchorContent = (
<div ref={this.wrapperRef} className={wrapperClass} style={wrapperStyle}>
<div className={anchorClass}>
<div className={`${prefixCls}-ink`}>
<span className={inkClass} ref={this.saveInkNode} />
</div>
{children}
</div>
</div>
);

return (
<AnchorContext.Provider
value={{
registerLink: this.registerLink,
unregisterLink: this.unregisterLink,
activeLink: this.state.activeLink,
scrollTo: this.handleScrollTo,
onClick: this.props.onClick,
}}
>
{!affix ? (
anchorContent
) : (
<Affix offsetTop={offsetTop} target={this.getContainer}>
{anchorContent}
</Affix>
)}
</AnchorContext.Provider>
);
};

这里载入了一些antd专有的样式和用户自定义的样式,固定模式下引用了Affix组件包装,多组件之间使用Context进行通信,共享了一些状态和函数。

生命周期函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
componentDidMount() {
this.scrollContainer = this.getContainer();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}

componentDidUpdate() {
if (this.scrollEvent) {
const currentContainer = this.getContainer();
if (this.scrollContainer !== currentContainer) {
this.scrollContainer = currentContainer;
this.scrollEvent.remove();
this.scrollEvent = addEventListener(this.scrollContainer, 'scroll', this.handleScroll);
this.handleScroll();
}
}
this.updateInk();
}

componentWillUnmount() {
if (this.scrollEvent) {
this.scrollEvent.remove();
}
}

①组件初始化对指定容器元素(默认为window)添加滚动事件监听,并执行handleScroll(默认也会执行一次)。②当调用setState状态更新也就是当前链接activeLink发生变动触发componentDidUpdate函数,函数中判断滚动的容器是否发生变动,做相应的处理后调用handleScrollupdateInk函数。

Context中的函数

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
32
33
34
35
36
37
38
39
40
// Context
registerLink = (link: string) => {
if (!this.links.includes(link)) {
this.links.push(link);
}
};

unregisterLink = (link: string) => {
const index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
};

handleScrollTo = (link: string) => {
const { offsetTop, targetOffset } = this.props;
this.setCurrentActiveLink(link);
const container = this.getContainer();
const scrollTop = getScroll(container, true);
const sharpLinkMatch = sharpMatcherRegx.exec(link);
if (!sharpLinkMatch) {
return;
}
const targetElement = document.getElementById(sharpLinkMatch[1]);
if (!targetElement) {
return;
}

const eleOffsetTop = getOffsetTop(targetElement, container);
let y = scrollTop + eleOffsetTop;
y -= targetOffset !== undefined ? targetOffset : offsetTop || 0;
this.animating = true;

scrollTo(y, {
callback: () => {
this.animating = false;
},
getContainer: this.getContainer,
});
};

registerLinkunregisterLink函数主要作用是添加和移除锚点项队列links,handleScrollTo函数作用是对Link传过来的href属性找到需要滚动的元素,计算滚动的top值调用scrollTo滚动到对应的位置,这一小段应该配合子元素的解析食用效果更佳。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function getOffsetTop(element: HTMLElement, container: AnchorContainer): number {
if (!element.getClientRects().length) {
return 0;
}

const rect = element.getBoundingClientRect();

if (rect.width || rect.height) {
if (container === window) {
container = element.ownerDocument!.documentElement!;
return rect.top - container.clientTop;
}
return rect.top - (container as HTMLElement).getBoundingClientRect().top;
}

return rect.top;
}

这个函数用于获取点击锚点后关联元素距离容器元素的top值,getBoundingClientRect()方法返回元素的大小及其相对于视口的位置(bottom、height、left、right、top、width、x、y),所以rect.top - container.clientTop就是锚点关联元素相对于容器元素的top值,接下来看子组件AnchorLink。

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
componentDidMount() {
this.context.registerLink(this.props.href);
}

componentDidUpdate({ href: prevHref }: AnchorLinkProps) {
const { href } = this.props;
if (prevHref !== href) {
this.context.unregisterLink(prevHref);
this.context.registerLink(href);
}
}

componentWillUnmount() {
this.context.unregisterLink(this.props.href);
}

handleClick = (e: React.MouseEvent<HTMLElement>) => {
const { scrollTo, onClick } = this.context;
const { href, title } = this.props;
onClick?.(e, { title, href });
scrollTo(href);
};

renderAnchorLink = ({ getPrefixCls }: ConfigConsumerProps) => {
const { prefixCls: customizePrefixCls, href, title, children, className, target } = this.props;
const prefixCls = getPrefixCls('anchor', customizePrefixCls);
const active = this.context.activeLink === href;
const wrapperClassName = classNames(
`${prefixCls}-link`,
{
[`${prefixCls}-link-active`]: active,
},
className,
);
const titleClassName = classNames(`${prefixCls}-link-title`, {
[`${prefixCls}-link-title-active`]: active,
});
return (
<div className={wrapperClassName}>
<a
className={titleClassName}
href={href}
title={typeof title === 'string' ? title : ''}
target={target}
onClick={this.handleClick}
>
{title}
</a>
{children}
</div>
);
};

render() {
return <ConfigConsumer>{this.renderAnchorLink}</ConfigConsumer>;
}

子组件作为Context的消费者,初始化的时候调用this.context.registerLink把传入props的href属性添加到锚点队列中,组件状态更新对比前后两个href差异并做添加移除操作,点击锚点元素后出触发handleClick并在内调用onClick回调和父级的handleScrollTo函数,做对应的滚动处理。

总结:当共享的属性和方法较多的情况下,可使用Context进行父子组件间的通信;element.getBoundingClientRect()方法可以获取元素相对于视口的位置信息。