更多内容在个人语雀: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…。针对这个问题也有相应解决方案:
- 如果是 hash 路由,hash 特殊处理。我们使用的是 HTML5 history 的路由,这种情况不考虑
- scrollIntoView(developer.mozilla.org/zh-CN/docs/…)
- scrollTo(developer.mozilla.org/zh-CN/docs/…)
- 换用 a 标签,避免组件重新渲染,而是新页面渲染
顺便说一下,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
路由到 /bar
。Bar
组件会初始化渲染,页面效果是直接定位到了 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… (这是巢鹏大佬的解答)
我们再来看回顾下遇到的问题:
Link
组件新跳转带锚点的页面,组件初始化渲染,锚点功能正常Link
组件在同一个页面跳转新锚点,组件重新渲染,锚点功能异常- 如果是 a 标签不管是不是新页面,锚点功能都正常
从 chrome 的实现和我们遇到的问题中能体会出关键在于是不是新页面。整个过程说得比较绕,期望是表达清楚了。
最后
在这个过程中还遇到一些老知识,复习下吧:
- a 标签站外跳转+noopener 的性能优势,不阻塞主线程:jakearchibald.com/2016/perfor…。
- css-scroll-snap:developers.google.com/web/updates… 实现平滑定位滚动。同样的 scrollIntoView 和 scrollTo 也可以用来平滑滚动哦。
- sticky-change 事件:developers.google.com/web/updates… sticky 属性变化的时候触发。还有相关联的事件:
- intersectionobserver:developers.google.com/web/updates…
- Mutation Observers:developers.google.com/web/updates…