Skip to content

Latest commit

 

History

History
182 lines (127 loc) · 7.96 KB

navigation.md

File metadata and controls

182 lines (127 loc) · 7.96 KB

NextJS路由导航总结

导航和刷新视图缓存

问题:server components下3个模式:(SSRSSGISR)的缓存和重新验证,在官方文档所有说明中,只针对新开、刷新当前路由,而不包括路由导航之间的跳转。这就意味着,所有非单一用户产生的状态,需要在路由跳转后实时返回状态信息的页面,不能及时同步状态。

示例: /rendering/src/app/link (查看)

解决办法:

  • 服务端渲染,用原生<a>标签代替<Link>组件,缺点:浏览器会有明显的刷新感,路由中layout页会被刷新
  • 客户端渲染,异步给链接添加hash值,缺点:导航链接会多出一串随机的hash值,路由中layout页不会被刷新
  • server action,缺点:只接受post请求,路由中layout页不会被刷新
  • 本地异步获取信息,缺点:不是服务端渲染,服务端不会输出任何静态资源,浏览器必须允许JS执行下才能运行
  • 继续往下看高级用法

相关链接: https://segmentfault.com/q/1010000044106831/a-1020000044112750

附加情况:页面小部件

  • 例如:个人中心出票状况、最新订单发货进度等等,这种可能在个人中心某一处位置的小部件,它不是整个页面主导部分,但又需要实时同步状态;
  • 那么建议通过本地异步请求状态,而不要使用服务端加载(这就意味着在并行路由中,获取状态需要在本地进行异步fetch请求)
  • 或者继续往下看高级用法

当然,如果存在非单一用户产生的状态(出票状况,快递进度、订单状态等),而又不需要实时同步信息的页面。无需考虑以上情况。 为何强调非单一用户,因为单一用户产生的状态可以通过server action提交信息的同时无感刷新路由视图

源码解析:

趴了源码看到这段,会阻止所有Link标签的点击事件:

function linkClicked(e, router, href, as, replace, shallow, scroll, locale, isAppRouter, prefetchEnabled) {
    const { nodeName  } = e.currentTarget;
    // anchors inside an svg have a lowercase nodeName
    const isAnchorNodeName = nodeName.toUpperCase() === "A";
    if (isAnchorNodeName && (isModifiedEvent(e) || // app-router supports external urls out of the box so it shouldn't short-circuit here as support for e.g. `replace` is added in the app-router.
    !isAppRouter && !(0, _islocalurl.isLocalURL)(href))) {
        // ignore click for browser’s default behavior
        return;
    }
    e.preventDefault();
    const navigate = ()=>{
        // If the router is an NextRouter instance it will have `beforePopState`
        const routerScroll = scroll != null ? scroll : true;
        if ("beforePopState" in router) {
            router[replace ? "replace" : "push"](href, as, {
                shallow,
                locale,
                scroll: routerScroll
            });
        } else {
            router[replace ? "replace" : "push"](as || href, {
                forceOptimisticNavigation: !prefetchEnabled,
                scroll: routerScroll
            });
        }
    };
    if (isAppRouter) {
        _react.default.startTransition(navigate);
    } else {
        navigate();
    }
}

高级用法:

  • 目录:/rendering/src/app/link/server-action (查看)
  • 原理:服务端通过server action刷新,客户端通过异步发起请求,并通过React Cachezustand这类状态机记录请求步骤

解决的问题:

  • 能够每次导航后更新当前数据和page视图,不刷新整个layout,能够做到无感更新数据;
  • 不需要通过给url添加随机hash后缀,也不用手动刷新页面;

阻塞导航

来自Semi群友的一个场景,当在表单提交或内容发布页时,需要监听用户离开,并阻塞其行为展开提示信息,待用户决定去留。这项功能在App router模式以前,可以监听router.event来实现,而在App router之后这个事件取消了。于是我通过react context的方式实现用户监听操作

cf6e2e9c42a4f29b1dacadffb58c9a1f_723815601830_v_1702122801840414.mp4

我设想了两个方法:

方法1:监听router变化阻塞用户

  • 目录:/routing-file/src/app/leaving/form (查看)

实现原理:

  • 监听URL的变化
  • 一旦发生改变发起确认框,点击“取消”之后立即返回前一页

缺点:存在一个闪动的过程,于是我设想了第二个方法

方法2:代理router对象和Link组件(推荐)

  • 目录:/routing-file/src/app/leaving/proxy (查看)

实现原理:

  • router对象,通过ES6proxy代理转发调用
  • Link组件,通过包装一层forware来实现onClick转发

以上两种方法共同用到的技术:

  • 通过React context上下文的方式,在子组件中通知什么情况开始阻塞用户,示例采用表单内容发生变化时
  • 通过useEffect + beforeunload,对浏览器默认行为进行阻塞

chrome及相关内核中,要阻塞浏览器默认行为的前提是打开页面后,至少在页面发生或事件才能生效

这样就监听并阻止了:

  • 点击Link组件发生的导航
  • 通过Router触发的导航事件
  • 浏览器默认行为:关闭、刷新、前进、后退、更改URL

缺点:本来以为这样就结束了,结果并没有,由于NextJS也是SPA,所以在以缓存路由的情况下,前进后退是不走beforeunload的,而是走popstate,这样就有可能无法拦截

  • 于是我前置一个popstate监听对象,通过event.stopImmediatePropagation这个API拦截事件不再交给NextJS后续处理,这样就完成了拦截

做到这一步基本就完成了,但是浏览器默认的行为还是不对的,毕竟用户已经点了前进和后退,这个堆栈的行为已经发生了,如果放着不管后续导航是会存在问题的

  • 于是我代理了history这个对象的pushStatereplaceState,增加判断用户是前进还是后退
  • 当拦截事件发起时根据用户的行为去修正history

详细见:/routing-file/src/components/proxyProvider/index.tsx [查看]

这样就监听并阻止了:

  • SPA应用中,用户点击前进和后退,在popstate下实现拦截

写在最后:

在解决这个需求的时候,我发现了NProgress.js,可实现导航切换时顶部加载动画

'use client'

import Link from 'next/link'
import { PropsWithChildren, useEffect } from 'react'
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'

export const CustomLink: React.FC<PropsWithChildren<{ href: string }>> = ({
    href,
    children
}) => {
    useEffect(() => {
        return () => {
            NProgress.done()
        }
    }, [])

    return (
        <>
            <Link href={href} onClick={() => NProgress.start()}>
                {children}
            </Link>
        </>
    )
}

相关链接:

一个坑点

App Dir模式下不支持waitUntil

当时给出了3个方案:

  1. server action非表单默认提交(无效),从上面例子中证实,server action并不根据<Link>组件跳转而执行;
  2. Api Route异步fetch,有效但设置很繁琐;
  3. middleware发起异步fetch(推荐),因为一个页面内容可以no data,但是绝对不会没有header