使用带有 react-router 的锚点

Posted

技术标签:

【中文标题】使用带有 react-router 的锚点【英文标题】:Use anchors with react-router 【发布时间】:2017-03-09 21:18:10 【问题描述】:

如何使用 react-router,并让链接导航到特定页面上的特定位置? (例如/home-page#section-three

详情

我在我的 React 应用程序中使用 react-router

我有一个站点范围的导航栏,需要链接到页面的特定部分,例如 /home-page#section-three

因此,即使您说/blog,单击此链接仍会加载主页,第三部分会滚动到视图中。这正是标准<a href="/home-page#section-three> 的工作方式。

注意:react-router 的创建者并没有给出明确的答案。他们说它正在进行中,同时使用其他人的答案。我会尽我最大的努力保持这个问题的最新进展和可能的解决方案,直到出现一个占主导地位的问题。

研究


How to use normal anchor links with react-router

这个问题来自 2015 年(所以 10 年前的反应时间)。最受欢迎的答案是使用HistoryLocation 而不是HashLocation。基本上,这意味着将位置存储在窗口历史记录中,而不是存储在哈希片段中。

坏消息是……即使使用 HistoryLocation(大多数教程和文档在 2016 年都这么说),锚标签仍然不起作用。


https://github.com/ReactTraining/react-router/issues/394

关于如何在 react-router 中使用锚链接的 ReactTraining 线程。这不是确定的答案。请小心,因为大多数建议的答案都已过时(例如,在 <Link> 中使用“hash”道具)


【问题讨论】:

【参考方案1】:

React Router Hash Link 为我工作,易于安装和实施:

$ npm install --save react-router-hash-link

在您的 component.js 中将其作为链接导入:

import  HashLink as Link  from 'react-router-hash-link';

不要使用锚<a>,而是使用<Link>

<Link to="home-page#section-three">Section three</Link>

注意:我用@987654322@代替@987654323@

【讨论】:

这是我的首选解决方案 对我来说很好。网址看起来像 localhost:8080/#!/#services 它也有使用 typescript 的类型:npm install @types/react-router-hash-link 此解决方案无法以编程方式推送历史记录。 这终于对我有用了。它不起作用,因为我将 ID 插入到组件中,而不是 DOM 元素中。【参考方案2】:

这是我找到的一种解决方案(2016 年 10 月)。它是跨浏览器兼容的(在 Internet Explorer、Firefox、Chrome、移动 Safari 和Safari 中测试)。

您可以为路由器提供onUpdate 属性。每当路线更新时,都会调用此方法。此方案使用 onUpdate 属性检查是否存在与哈希匹配的 DOM 元素,然后在路由转换完成后滚动到该元素。

您必须使用 browserHistory 而不是 hashHistory。

答案是 Hash links #394 中的“Rafrax”。

将此代码添加到您定义&lt;Router&gt;的位置:

import React from 'react';
import  render  from 'react-dom';
import  Router, Route, browserHistory  from 'react-router';

const routes = (
  // your routes
);

function hashLinkScroll() 
  const  hash  = window.location;
  if (hash !== '') 
    // Push onto callback queue so it runs after the DOM is updated,
    // this is required when navigating from a different page so that
    // the element is rendered on the page before trying to getElementById.
    setTimeout(() => 
      const id = hash.replace('#', '');
      const element = document.getElementById(id);
      if (element) element.scrollIntoView();
    , 0);
  


render(
  <Router
    history=browserHistory
    routes=routes
    onUpdate=hashLinkScroll
  />,
  document.getElementById('root')
)

如果您感到懒惰并且不想复制该代码,则可以使用仅为您定义该功能的 Anchorate。 https://github.com/adjohnson916/anchorate

【讨论】:

只想提一下,这个解决方案将不再适用,因为react-router 的第 4 版,onUpdate 方法已被删除。 刚刚发布了react-router V4的解决方案,见下文。【参考方案3】:

此解决方案适用于 react-router v5

import React,  useEffect  from 'react'
import  Route, Switch, useLocation  from 'react-router-dom'

export default function App() 
  const  pathname, hash, key  = useLocation();

  useEffect(() => 
    // if not a hash link, scroll to top
    if (hash === '') 
      window.scrollTo(0, 0);
    
    // else scroll to id
    else 
      setTimeout(() => 
        const id = hash.replace('#', '');
        const element = document.getElementById(id);
        if (element) 
          element.scrollIntoView();
        
      , 0);
    
  , [pathname, hash, key]); // do this on route change

  return (
      <Switch>
        <Route exact path="/" component=Home />
        .
        .
      </Switch>
  )

在组件中

<Link to="/#home"> Home </Link>

【讨论】:

这很好用。我希望这个解决方案更加突出! tnx @JimmyTheCode 不错的答案。 react-router-hash-link 对我来说效果不佳。我进行了编辑以改进答案:(1)hash 缺少作为useEffect 的依赖项(2)如果我们依赖location.key,我们可以保证它仍然会滚动到&lt;Link /&gt; 上的目标点击。用例:假设用户点击&lt;Link /&gt;然后滚动离开,如果我们不依赖key,再次点击同样的&lt;Link /&gt;不会有任何效果。 啊,0ms 的超时时间对于本地路由更改效果很好,但是从另一个页面来看,它没有足够的时间来渲染目标元素。 我可以确认这适用于react-router v6。 react-router-hash-link 对我不起作用。【参考方案4】:

这是一个简单的解决方案,不需要任何订阅或第三方软件包。它应该适用于react-router@3 及以上和react-router-dom

工作示例:https://fglet.codesandbox.io/

来源(不幸的是,它目前在编辑器中不起作用):


#ScrollHandler 钩子示例

import  useEffect  from "react";
import PropTypes from "prop-types";
import  withRouter  from "react-router-dom";

const ScrollHandler = ( location, children ) => 
  useEffect(
    () => 
      const element = document.getElementById(location.hash.replace("#", ""));

      setTimeout(() => 
        window.scrollTo(
          behavior: element ? "smooth" : "auto",
          top: element ? element.offsetTop : 0
        );
      , 100);
    , [location]);
  );

  return children;
;

ScrollHandler.propTypes = 
  children: PropTypes.node.isRequired,
  location: PropTypes.shape(
    hash: PropTypes.string,
  ).isRequired
;

export default withRouter(ScrollHandler);

#ScrollHandler 类示例

import  PureComponent  from "react";
import PropTypes from "prop-types";
import  withRouter  from "react-router-dom";

class ScrollHandler extends PureComponent 
  componentDidMount = () => this.handleScroll();

  componentDidUpdate = prevProps => 
    const  location:  pathname, hash   = this.props;
    if (
      pathname !== prevProps.location.pathname ||
      hash !== prevProps.location.hash
    ) 
      this.handleScroll();
    
  ;

  handleScroll = () => 
    const  location:  hash   = this.props;
    const element = document.getElementById(hash.replace("#", ""));

    setTimeout(() => 
      window.scrollTo(
        behavior: element ? "smooth" : "auto",
        top: element ? element.offsetTop : 0
      );
    , 100);
  ;

  render = () => this.props.children;
;

ScrollHandler.propTypes = 
  children: PropTypes.node.isRequired,
  location: PropTypes.shape(
    hash: PropTypes.string,
    pathname: PropTypes.string,
  )
;

export default withRouter(ScrollHandler);

【讨论】:

甜蜜,谢谢。为什么是 element.offsetTop 而不是 window.scrollY + element.getBoundingClientRect().top ?后者使其独立于最近的亲属。 在这个简单的例子中,计算element.offsetTop 将得到与window.scrollY + element.getBoundingClientRect().top 相同的结果。但是,如果您将元素嵌套在 table 中,那么是的,您需要使用后者而不是前者。例如嵌套table:jsfiddle.net/pLuvbyx5,未嵌套元素:jsfiddle.net/8bwj6yz3 有什么办法可以避免 setTimeOut 吗?我们可以在不使用 setTimeOut 的情况下实现相同的功能吗? ***.com/questions/64224547/… 不幸的是,没有。某些浏览器(如 Safari)不会立即更新滚动位置。 @MattCarlotta 假设我的页面渲染时间超过 100 毫秒,在这种情况下它可以工作吗?如果是的话,请告诉我们一些关于它的信息。你能解决这个***.com/questions/64224547/…【参考方案5】:

只是避免使用 react-router 进行本地滚动:

document.getElementById('myElementSomewhere').scrollIntoView() 

【讨论】:

理想情况下,本地滚动会通过路由器,因为这样您就可以从外部链接到文档的特定部分,但是这个答案仍然非常感谢,因为它告诉了我需要在我的this.props.history.listen 回调。 在我的情况下,我只是想通过模仿与 href 为 #myElementId 的链接相同的方式向下滚动到一个 div ......这确实是最好和简单的答案,谢谢!【参考方案6】:

Don P's answer 的问题是,如果该部分依赖于某些异步操作,则有时仍会呈现或加载具有 id 的元素。以下函数将尝试通过 id 查找元素并导航到该元素并每 100 毫秒重试一次,直到达到最多 50 次重试:

scrollToLocation = () => 
  const  hash  = window.location;
  if (hash !== '') 
    let retries = 0;
    const id = hash.replace('#', '');
    const scroll = () => 
      retries += 0;
      if (retries > 50) return;
      const element = document.getElementById(id);
      if (element) 
        setTimeout(() => element.scrollIntoView(), 0);
       else 
        setTimeout(scroll, 100);
      
    ;
    scroll();
  

【讨论】:

上限为 5 秒。如果页面在 5 秒后加载,它会起作用吗? 在这个问题的所有 SO 解决方案中,这必须是迄今为止最简单的。与&lt;Link to= pathname: "/", hash: "elementIDtoScrollTo"&gt; 一起使用 使用后最好使用if (hash.length &gt; 0)而不是if (hash == "")【参考方案7】:

我将 Don P's solution(见上文)改编为 react-router 4(2019 年 1 月),因为 &lt;Router&gt; 上不再有 onUpdate 属性。

import React from 'react';
import * as ReactDOM from 'react-dom';
import  Router, Route  from 'react-router';
import  createBrowserHistory  from 'history';

const browserHistory = createBrowserHistory();

browserHistory.listen(location => 
    const  hash  = location;
    if (hash !== '') 
        // Push onto callback queue so it runs after the DOM is updated,
        // this is required when navigating from a different page so that
        // the element is rendered on the page before trying to getElementById.
        setTimeout(
            () => 
                const id = hash.replace('#', '');
                const element = document.getElementById(id);
                if (element) 
                    element.scrollIntoView();
                
            ,
            0
        );
    
);

ReactDOM.render(
  <Router history=browserHistory>
      // insert your routes here...
  />,
  document.getElementById('root')
)

【讨论】:

这仍然是最新的吗?我不存在 history 属性。【参考方案8】:
<Link to='/homepage#faq-1'>Question 1</Link>
useEffect(() => 
    const hash = props.history.location.hash
    if (hash && document.getElementById(hash.substr(1))) 
        // Check if there is a hash and if an element with that id exists
        document.getElementById(hash.substr(1)).scrollIntoView(behavior: "smooth")
    
, [props.history.location.hash]) // Fires when component mounts and every time hash changes

【讨论】:

【参考方案9】:

对于简单的页面内导航,您可以添加类似这样的内容,尽管它不处理初始化页面 -

// handle back/fwd buttons
function hashHandler() 
  const id = window.location.hash.slice(1) // remove leading '#'
  const el = document.getElementById(id)
  if (el) 
    el.scrollIntoView()
  

window.addEventListener('hashchange', hashHandler, false)

【讨论】:

当我在 API 调用后调用它以获取页面内容时,该代码实际上对我在 React 应用程序中进行初始页面加载有效。我喜欢它的简单性,并且相同的页面链接已经对我有用。【参考方案10】:

另一种选择:react-scrollchor https://www.npmjs.com/package/react-scrollchor

react-scrollchor:一个 React 组件,用于滚动到具有流畅动画的 #hash 链接。 Scrollchor 是 Scroll 和 Anchor 的混合体

注意:它不使用 react-router

【讨论】:

以上是关于使用带有 react-router 的锚点的主要内容,如果未能解决你的问题,请参考以下文章

使用带反应路由器的锚点

带有 id 链接的锚点的技术术语是啥?

react-router解决锚点跳转问题

通过单击带有 jQ​​uery 的锚点来平滑滚动元素

AR应用中的锚点检测问题

使用 ARKit 显示 DAE 文件并跟踪场景中的锚点