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; showInkInFixed?: boolean; getContainer?: () => AnchorContainer; getCurrentAnchor?: () => string; onClick?: ( e: React.MouseEvent<HTMLElement>, link: { title: React.ReactNode; href: string }, ) => void; 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
函数,函数中判断滚动的容器是否发生变动,做相应的处理后调用handleScroll
和updateInk
函数。
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
| 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, }); };
|
registerLink
和unregisterLink
函数主要作用是添加和移除锚点项队列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。
子组件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()
方法可以获取元素相对于视口的位置信息。