怎么给新手解释 react 的 spa 应用锚点跳转的问题

3,758 阅读4分钟

更多内容在个人语雀:www.yuque.com/xiezhongfu/…

前言

我们先区分下“页面”这个词:

  • 单页面应用(spa)就只有一个页面
  • 文中所指的页面是从浏览器的 url 角度去理解的,只要 protocol + port + pathname 不同,浏览器就认为是新页面

问题是什么

<Route path="/foo" componet={Foo} />
<Route path="/bar" componet={Bar} />
// Foo 页面组件 和 Bar 页面组件都是静态化组件(这两个组件里面的数据都是不变的)

....

<Link to="/foo">foo</Link>
<Link to="/bar#test">bar</Link>

单页面应用,在同一个页面使用 hash 跳转到页面不同位置(锚点跳转)。此功能是不正常的。
还可以看看前人发的 issue 参见:github.com/ReactTraini…。针对这个问题也有相应解决方案:

顺便说一下,scrollIntoView 和 scrollTo 可以设置平滑滚动;在滚动区域 css 设置 scroll-behavior 也可以平滑滚动,粗暴一点我们给 * 设置 scroll-behavior 吧。

猜测为什么

让我们在看下这段代码

<Route path="/foo" componet={Foo} />
<Route path="/bar" componet={Bar} />
// Foo 页面组件 和 Bar 页面组件都是静态化组件(这两个组件里面的数据都是不变的)

....

<Link to="/foo">foo</Link>
<Link to="/bar#test">bar</Link>

在不同的页面跳转不同锚点,history 有更新,初始化渲染组件,功能正常。
在同一个页面跳转不同锚点,history 有更新,重新渲染原组件,功能不正常。
原生方式下,就算在同一个页面的不同锚点间跳转,功能正常。

场景 A
假设我们这样路由了 2 个组件,我们先点击了 foo,渲染了 Foo 组件。然后我们点击 bar ,pathname 从 /foo 路由到 /barBar 组件会初始化渲染,页面效果是直接定位到了 Bar 组件渲染的页面中的 test 锚点。
场景 B
假设在 Bar 页面内有一个自己页面内的锚点,我们点击它,虽然这个锚点就在同一个页面,锚点跳转失效。效果是 url 上看到锚点变了,但是没跳到对应的新锚点。
因为使用了 Link 组件,在 Bar 页面内点击了一个新的锚点,Link 组件内使用了 push 或者 replace 产生了新的 window.history 记录,这个新 window.history 的 pathname 和以前一样,只是有新的 hash。然后就去 Route 里去找匹配的组件,发现匹配到了 Bar 组件 。因为已经渲染过了,那就重新渲染吧。
因为 protocol,port,pathname 都没变,只是 hash 变了,浏览器认为这是老页面上的一个新锚点,那就在老页面上跳转吧。但是页面组件如果在重新渲染,也许在浏览器刚想要跳新锚点的时候重现渲染导致锚点没了,跳转失败。

我们总结下这个过程:
在 react 应用中的锚点跳转的实现方案是:Link 组件。跳转新页面锚点成功,但是跳转同页面锚点失败。
Link 组件默认行为包括调用 history 库的 push 或 replace,组件会初始化渲染或者重新渲染。渲染的过程是 js 对象转为 html 的过程。如果这个过程还没结束,html 还没及时出现,锚点功能就失效了。
。我们知道锚点能成功,一定是浏览器在跳转的时候有 html 节点上有对应的 id 或 name(比如:a 标签可以使用 name 用来标记别人可以跳到它这),如果没有那就跳转失败。

以上推理都是根据经验猜测,我们怀疑:
场景 A 在因为是新页面,在浏览器跳转锚点前 react 组件初始化渲染已经完成, html 已经有对应锚点了。
场景 B 由于是同一个页面,在浏览器跳转锚点前 react 组件重新渲染还没完成,跳转失败了。
那浏览器到底是怎么执行锚点跳转的呢?开始查材料......

这真的是原因么

我们先看标准文档是怎么解释锚点的:html.spec.whatwg.org/multipage/b…

在看 chrome 是怎么处理的:cs.chromium.org/chromium/sr… (这是巢鹏大佬的解答)

image.png

我们再来看回顾下遇到的问题:

  • Link 组件新跳转带锚点的页面,组件初始化渲染,锚点功能正常
  • Link 组件在同一个页面跳转新锚点,组件重新渲染,锚点功能异常
  • 如果是 a 标签不管是不是新页面,锚点功能都正常

从 chrome 的实现和我们遇到的问题中能体会出关键在于是不是新页面。整个过程说得比较绕,期望是表达清楚了。

最后

在这个过程中还遇到一些老知识,复习下吧: