浏览器操控历史记录真的好大一个坑啊,我的天,看上去好像99%的浏览器都对history兼容良好,实际上呢?啊,怎么某个操作突然就不好使了,为什么这里能跳到那里,为什么nextJS突然就报错了,是next的原因还是啥?
本文记录了开发一个“多步骤页面拆分成多个页面”的需求研发过程。
因为采用NEXT的SSR模式,每一次刷新页面都会发起一个新的http请求,导致之前的数据源清空,所以我们一开始采用pushState+replaceState的方式去做。其实所有的步骤还是放在同一页面,每一次的pushState让url变,同时更新步骤数据,让页面变化。
pushState:向历史记录中push一条新记录。
replaceState:更换当前的历史记录。
参数:pushState(stateObj,title,url)
- stateObj是每个历史记录中保存的state数据,可以通过history.state获取和修改。建议虽然不用,也可以push一个标识值在里面,而不是传null,因为查到资料说监听popState事件时可能会查询state值是否变动。
- title :一个暂时没有用,但是所有浏览器和文档都建议你保留的值,所以直接传
''
就好- url:主要使用,传入我们想要跳转的url。
window.addEventListener(‘popstate’, historyListener);
监听路由的前进后退触发事件,和pushState相配合,因为普通的页面跳转在很多浏览器内不会触发监听事件(尤其是移动端),只有pushState / replaceState之后的页面前进后退,才会触发这个监听事件。
它的作用在于:pushState变换url后,用户历史记录返回,如果没有监听到返回事件,页面就不会变动了。
pushState 和 replaceState是history中常用的两个API,都是用于url操控。15年出的标准,现在都2023年了基本都能支持上。
pushState的兼容性比replaceState更好,建议少用replaceState,因为移动端兼容性有坑。
平时最相信的就是google浏览器了,而且看兼容性也是支持的,但是奇了怪了就是不起作用,准确点,页面确实被替换了,但是url不会变动。而且这个bug在IOS safari / 安卓各种奇葩浏览器上都没有问题,当时心态就是一个啊???
查了好久资料,应该是google对历史记录有保护措施,对于这些修改历史记录的操作有限制造成的。行吧,毕竟IOS+google这个组合确实应该是隐私保密性最强的搭配了。听我说,IOS,谢谢你。
但是,于是,这个replace的方案就被毙了。
于是我们修改了一下需求计划,将所有的replaceState事件改回了pushState,要替换路由的地方通过路由去跳转。
先认识一下next的路由跳转吧。
import {router} from 'next/router'
常用API :push | replace | refresh | beforePopState | onlyAHashChange |changeState | scrollToHash 等等。
大部分顾名思义知道是做什么用的,但是next和其他框架不同的地方在于它的路由跳转是有CSR、SSR区别的。
根据NEXT的文档:https://nextjs.org/docs/app/api-reference/functions/use-router
- router.push(href: string): Perform a client-side navigation to the provided route. Adds a new entry into the browser’s history stack.
- router.replace(href: string): Perform a client-side navigation to the provided route without adding a new entry into the browser’s history stack.
- router.refresh(): Refresh the current route. Making a new request to the server, re-fetching data requests, and re-rendering Server Components. The client will merge the updated React Server Component payload without losing unaffected client-side React (e.g. useState) or browser state (e.g. scroll position).
push / replace是一个客户端使用的路由跳转方式,NEXT文档中介绍它们类似于window.history.pushState和replaceState,但是据我实测还是很大区别的。
看下方的原文中标粗的地方,其中说明了路由的跳转使用方式,一开始我没看懂,盲目使用,结果跳不过去,页面直接报错,因为我的项目是SSR项目,使用push / replace拉不到服务端请求数据。
经我测试,一般来说push的用法:router.push(href: string)
只能用于CSR,但是用router.push(urlObject:object)
可以做到SSR的路由跳转,就跟window.location.href差不多。
也就是说,如果我们想要实现/test/[test_query] ?test=111 这样的的动态路由使用push跳转,我们最好不要用string类型的url,而是用Urlobject中的query参数去跳转,这样可以达到pushState的效果。页面平滑转换。
动态路由的参数都是用query,比如我们要去/product/AAA?test=111,这样的路由,写法应该如下:router.push({ pathname: '/product/[ID]', query: { ID: 'AAA', test: 111} })
CSR使用 useRouter 获取动态路由参数,SSR可以在getInitialProps 通过 ctx.query 获取参数。
动态路由参考官方文档(英)
动态路由参考官方文档(中)
stackOverflow-How can I get (query string) parameters from the URL in Next.js?
asPath是next提出的一个“展示路由”,假如你的url是/AAA,但是你不想被其他人知道路由是AAA,可以使用asPath修饰成BBB,只需要传入第二个参数即可。
有以下4个参数 shallow、locale、scroll、unstable_skipClientCache
具体还是参考他们的官方文档使用,因为我用的是SSR,上面的参数很多用不到,scroll用的比较多,就是可以跳转自动上滑到顶部,unstable_skipClientCache可以跳转时强制刷新,shallow可以用作无刷新的浅路由。
由上所述,后续我们弃用了replaceState,全部用pushState来开发。却突然发现一个大问题:我们的历史记录后退后退,退回到第一个使用pushState 的页面时,页面突然直接崩溃了。
而且这个bug本地是好的,构建完才有问题!
当时我的心态也崩了,整啥啊这是。
一开始的时候,我自己以为这是本地运行时的报错“back to same url”
具体错误名我忘了,如果有遇到类似的报错提示的同学应该能理解到,就是next报错返回到了和之前相同的地址,导致报错,查了下是因为路由参数中的asPath相同,然后我试着用router.getRouterInfo()
打印了一下当前的路由aspath,发现路由停止在pushState的那个页面,一直没有变过,于是在跳转路由时,同步修改了router.state中的asPath参数。
但是等我构建完才发现问题没有丝毫解决,此时的我很淡定,以为是router路由跳转和window原生跳转的问题,于是花了一点时间将所有的跳转方法改为了router提供的路由pushState方法。使用push达不到效果,只有改用changeState('pushState')
才行。
然后信心满满地push上去之后,错误仍然存在!
啊?我心态又崩了,为啥呢,连原生的路由方法都解决不了这个问题,我还能咋办。
于是连夜上github上查isuee,关键词back button。一番搜索之后终于发现next的路由跳转在历史记录返回的时候就是这么坑。。。
issue链接:https://github.com/vercel/next.js/issues/33233
这个Fixed的解决方法中提出了可以ReplaceState替换当前路由来避免这个路由返回的问题。。
是的,然后我就在每个页面的一进来componentDidMount时先做了一个replaceState的操作,这样终于把这个问题给fix了。
就这样。俺的掉坑之旅完美解决。