Next.js v14 如何为多个根布局自定义不同的 404 页面?竟然还有些麻烦!

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

前言

在 Next.js 中,路由组这个功能非常常用,就比如我们经常会看到这样的目录结构:

javascript 复制代码
app                      
├─ (admin)               
│  ├─ admin              
│  │  └─ page.js         
│  └─ layout.js              
└─ (mobile)                 
   ├─ layout.js             
   └─ page.js                

这里我们根据站点对路由进行了分组,(mobile) 负责面向用户的手机端页面,(admin) 负责后台管理系统。两个站点的样式和功能完全不同,所以我们使用了两个不同的根布局(layout.js)。

此时问题来了,手机端页面和后台管理系统的 404 页面也完全不同,如何自定义不同的 404 页面呢?

不要觉得这是一个简单的问题,大家所知道的 not-found.js 也不能直接解决这个问题,直接写还会报错,所以才会单独写成一篇文章,而且其中还有很多值得探讨的问题。使用 Next.js 的同学,快收藏点赞这篇文章,以防日后遇到吧!

1. 使用路由组定义多个根布局

先让我们写个 Demo,在实战中体会这个问题。

使用 Next.js 官方脚手架创建项目:

bash 复制代码
npx create-next-app@latest

运行效果如下:

为了样式美观,我们会用到 Tailwind CSS,所以注意勾选 Tailwind CSS,其他随意。

进入项目目录,开启本地模式,检查项目是否能够启动成功:

bash 复制代码
npm i && npm run dev

涉及的目录如下:

javascript 复制代码
app               
├─ (admin)        
│  ├─ admin       
│  │  └─ page.js  
│  └─ layout.js   
├─ (mobile)       
│  ├─ layout.js   
│  └─ page.js     
├─ favicon.ico    
└─ globals.css    

删除掉原本的 app/layout.jsapp/page.js。其中 app/(mobile)/layout.js代码如下:

javascript 复制代码
import "../globals.css";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head></head>
      <body>
      <div className="bg-indigo-600">
            <div className="max-w-screen-xl mx-auto px-4 py-3 text-white sm:text-center md:px-8">
                <p className="font-medium">
                    We just launched a new version of our library! <a href="#" className="font-semibold underline duration-150 hover:text-indigo-100 inline-flex items-center gap-x-1">
                        Learn more
                    </a>
                </p>
            </div>
        </div>
        {children}</body>
    </html>
  );
}

app/(mobile)/layout.js代码如下:

javascript 复制代码
export default async function Page() {
  return (
    <div className="h-60 flex-1 rounded-xl bg-teal-400 text-white flex items-center justify-center m-6 bg-indigo-400">Hello Mobile!</div>
  )
}

app/(admin)/layout.js代码如下:

javascript 复制代码
import "../globals.css";

const navigation = [{ title: "Features", path: "#" }, { title: "Integrations", path: "#" }, { title: "Customers", path: "#" }, { title: "Pricing", path: "#" }];

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head></head>
      <body>
        <nav className="bg-white pb-5 text-sm shadow-lg rounded-xl border mx-2 mt-2 shadow-none border-none mx-2 my-2">
          <div className="gap-x-14 items-center max-w-screen-xl mx-auto px-4 flex px-8">
            <div className="flex-1 items-center mt-8 mt-0 flex block">
              <ul className="justify-center items-center space-y-6 flex space-x-6 space-y-0">
                {navigation.map((item, idx) => {
                  return (
                    <li key={idx} className="text-gray-700 hover:text-gray-900"><a href={item.path} className="block">{item.title}</a></li>
                  );
                })}
              </ul>
              <div className="flex-1 gap-x-6 items-center justify-end mt-6 space-y-6 flex space-y-0 mt-0">
                <a href="#" className="block text-gray-700 hover:text-gray-900"> Log in </a>
                <a href="#" className="flex items-center justify-center gap-x-1 py-2 px-4 text-white font-medium bg-gray-800 hover:bg-gray-700 active:bg-gray-900 rounded-full inline-flex"
                > Sign in </a>
              </div>
            </div>
          </div>
        </nav>
        {children}
      </body>
    </html>
  );
}

app/(admin)/admin/page.js,代码如下:

javascript 复制代码
export default async function Page() {
  return (
    <div className="h-60 flex-1 rounded-xl bg-teal-400 text-white flex items-center justify-center m-6">Hello Admin!</div>
  )
}

简单的来说,我们声明了两个根布局,然后添加了不同的样式效果。此时交互效果如下:

localhost:3000localhost:3000/admin分别使用了不同的布局和页面。

注:此时完整的代码地址为:github.com/mqyqingfeng...

2. 自定义 404 页面

自定义 404 页面,你可能会想到使用 not-found.js 这个约定文件。让我们在 app目录下直接添加一个 not-found.jsapp/not-found.js代码如下:

javascript 复制代码
export default function NotFound() {
  return (
    <main>
      <div className="mx-auto px-4 flex items-center justify-start px-8 mt-6">
        <div className="max-w-lg mx-auto text-center">
          <h3 className="text-gray-800 text-4xl font-semibold sm:text-5xl">
            Page not found
          </h3>
          <p className="text-gray-600 mt-3">
            Sorry, the page you are looking for could not be found or has been
            removed.
          </p>
        </div>
      </div>
    </main>
  );
}

随意访问一个未定义的地址,比如 http://localhost:3000/123,你可能以为会正常运行,显示自定义的页面,但其实会编译错误:

报错信息提示 not-found.js并没有一个根布局。想想也是,我们将根布局声明在了不同的路由组中,app下并没有直接的 layout.js

我们很自然的想到解决方法,那就将 not-found.js 放到其中一个路由组中,或者复制 2 份放到不同的路由组中,比如改为这种目录结构:

javascript 复制代码
app                 
├─ (admin)          
│  ├─ admin         
│  │  └─ page.js    
│  ├─ layout.js     
│  └─ not-found.js  
├─ (mobile)         
│  ├─ layout.js     
│  ├─ not-found.js  
│  └─ page.js       
├─ favicon.ico      
└─ globals.css      

这样是不是就可以了呢?此时确实不会报错了,但却走到了默认的 404 样式上:

这是为什么呢?

这就要说到 app/not-found.js 这个约定文件的特性了。它由两种情况触发:

  1. 当组件抛出了 notFound 函数的时候
  2. 当路由地址不匹配的时候

所以 app/not-found.js 可以修改默认 404 页面的样式。但是如果 not-found.js放到了任何子文件夹下的时候,它只能由 `notFound`函数手动触发。

但因为我们使用了路由组,根布局放到了不同的路由组文件夹下,没有 app/layout.js这个文件。修改默认的 404 页面效果需要 app/not-found.jsapp/not-found.js又需要 app/layout.js这个文件,没有 layout.js,编译就会报错......

那么该怎么办呢?

委曲求全

一种委曲求全的方法那就是干脆不使用多个根布局了,比如改成这种目录结构:

javascript 复制代码
app               
├─ (admin)        
│  ├─ admin       
│  │  └─ page.js  
│  └─ layout.js   
├─ (mobile)       
│  ├─ layout.js   
│  └─ page.js     
├─ favicon.ico    
├─ globals.css    
├─ layout.js      
└─ not-found.js   

app/layout.js中只包含最基础的代码:

javascript 复制代码
import "../globals.css";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head></head>
      <body>{children}</body>
    </html>
  );
}

具体导航栏和 Banner 的样式放到 app/(admin)/layout.jsapp/(mobile)/layout.js中。

这样做当然是可以的。不过因为不能再定义根布局,就不能方便的为各个根布局声明特殊的元标签或者引入文件,就像这样:

javascript 复制代码
import "./globals.css";

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <head>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
      </head>
      <body>{ children }</body>
    </html>
  );
}

比如移动端和后台管理系统需要引入不同的 cdn 样式或脚本文件,以及声明一些特殊的 Next.js 不支持的元数据,使用根布局是最方便的方式。如果不再使用根布局,这些问题处理起来又会麻烦一些。

曲线救国

难道就没有其他的方法了吗?

一种更常用的方式是使用 [.... folderName] 这种动态路由方式,涉及的文件和目录结构如下:

javascript 复制代码
app                   
├─ (admin)            
│  ├─ [...not-found]  
│  │  └─ page.js      
│  ├─ admin           
│  │  └─ page.js      
│  ├─ layout.js       
│  └─ not-found.js    
├─ (mobile)           
│  ├─ layout.js       
│  └─ page.js                 
└─ globals.css

新建 app/(admin)/[...not-found]/page.js,代码如下:

javascript 复制代码
import { notFound } from 'next/navigation';

export default function NotFoundDummy() {
  notFound();
}

app/(admin)/not-found.js代码跟之前一样:

javascript 复制代码
export default function NotFound() {
  return (
    <main>
      <div className="mx-auto px-4 flex items-center justify-start px-8 mt-6">
        <div className="max-w-lg mx-auto text-center">
          <h3 className="text-gray-800 text-4xl font-semibold sm:text-5xl">
            Page not found
          </h3>
          <p className="text-gray-600 mt-3">
            Sorry, the page you are looking for could not be found or has been
            removed.
          </p>
        </div>
      </div>
    </main>
  );
}

此时访问 http://localhost:3000/123,效果如下:

终于看到了我们自定义的 404 页面!

原理是 [...not-found] 会捕获所有未定义的路由,然后抛出 notFound 函数,由同级的 not-found.js 来处理,于是展现了自定义的 404 页面。

但问题也随之而来,因为我们的 not-found.js 自定义在了 (admin)路由组下,所以渲染 404 页面的时候,它会被包裹在 app/(admin)/layout.js这个布局下。对于 (admin) 下的路由自然没有问题,但是对于 (mobile) 下的路由则有些奇怪了......

如果我们把 not-found.js 放到 app/(mobile) 路由组下,则还是相同的问题,(mobile)下没有问题,(admin) 就有些奇怪了......

如果要解决这个问题,我们可以再建立一个路由组,专门用来处理错误,涉及的目录结构如下:

javascript 复制代码
app                   
├─ (404)              
│  ├─ [...not-found]  
│  │  └─ page.js      
│  ├─ layout.js       
│  └─ not-found.js    
├─ (admin)            
│  ├─ admin           
│  │  └─ page.js      
│  └─ layout.js       
├─ (mobile)           
│  ├─ layout.js       
│  └─ page.js         
└─ globals.css        

也就是说,我们将 404 的处理逻辑相关的文件都放到了 (404)路由组下,其中 (404)/layout.js代码如下:

javascript 复制代码
import "../globals.css";

export default function RootLayout({ children }) {
 return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

另外两个文件代码跟之前相同。此时完整交互效果如下:

从上图可以看出,当访问未定义的路由 http://localhost:3000/123 时,自定义 404 页面并没有被包裹在布局样式中。

注:此时完整的代码地址为:github.com/mqyqingfeng...

3. 自定义不同的 404 页面

回到最一开始的问题,如果我要为不同的路由组自定义不同的 404 页面呢?就比如后台管理系统和 C 端页面的 404 样式应该是不同的。

同理我们也可以使用 [.... folderName] 这种动态路由方式,涉及的文件和目录结构如下:

javascript 复制代码
app                      
├─ (admin)               
│  ├─ admin              
│  │  ├─ [...not-found]  
│  │  │  └─ page.js      
│  │  ├─ not-found.js    
│  │  └─ page.js         
│  └─ layout.js          
├─ (mobile)              
│  ├─ [...not-found]     
│  │  └─ page.js         
│  ├─ layout.js          
│  ├─ not-found.js       
│  └─ page.js            
└─ globals.css           

我们分别在 app/(mobile)app/(admin)/admin下定义了 [...not-found]动态路由,使用这种方式的前提是我们约定当用户访问 /xxxx的时候,走到 (mobile) 路由组下,当访问 /admin/xxx的时候,走到 (admin) 路由组下,我们使用 [...not-found] 分别进行捕获,然后由各自的 not-found.js 来处理。

交互效果如下:

注:此时完整的代码地址为:github.com/mqyqingfeng...

同样的问题现在又来了,两个自定义的 404 页面被包裹在各自的布局中,大部分时候这并不是什么问题,甚至本来就是要这样做。但万一中的万一,产品经理就是要你不包裹在布局中,单独展示处理啊,你该怎么办呢?

你可以结合上个处理问题的方式,在每个路由组中再添加一个路由组,涉及的文件和目录结构如下:

javascript 复制代码
app                         
├─ (admin)                  
│  ├─ (404)                 
│  │  ├─ admin              
│  │  │  └─ [...not-found]  
│  │  │     └─ page.js      
│  │  ├─ layout.js          
│  │  └─ not-found.js       
│  └─ (admin)               
│     ├─ admin              
│     │  └─ page.js         
│     └─ layout.js          
├─ (mobile)                 
│  ├─ (404)                 
│  │  ├─ [...not-found]     
│  │  │  └─ page.js         
│  │  ├─ layout.js          
│  │  └─ not-found.js       
│  └─ (mobile)              
│     ├─ layout.js          
│     └─ page.js                       
└─ globals.css              

这个文件结构可谓是非常复杂了,我们在 (mobile) 下又添加了 (404) 和 (mobile) 路由组,由 (404) 处理自定义 404 页面,由 (mobile)/(mobile) 处理原本的路由逻辑。(admin)同理。此时交互效果如下:

这样我们就成功的排除了布局,但文件结构同时也变得复杂了,如果有更好的方法,欢迎留言讨论,最好附实现 Demo。

注:此时完整的代码地址为:github.com/mqyqingfeng...

总结

其实定义不同的 404 页面原本应该是一件非常容易的事情,但在 Next.js 下却变得有些复杂了,希望未来 Next.js 会优化这部分的实现吧。

之所以会有些复杂,是因为 app/not-found.js 既可以被 notFound 函数触发,又可以在路由不匹配时触发,但 not-found.js 放在子文件的时候,只能由 notFound 函数触发。而 app/not-found.js 需要 app/layout.js,这就导致使用路由组定义多个根布局的时候会出现问题。

目前的解决方案是使用 [...folderName] 动态路由方式捕获不匹配的路由,手动调用 notFound 函数,再由自定义的 not-found.js 来实现样式效果。不算是一个优雅的方案,但能解决问题。希望大家不要遇到这种问题吧。

相关推荐
FØund404几秒前
antd form.setFieldsValue问题总结
前端·react.js·typescript·html
Backstroke fish几秒前
Token刷新机制
前端·javascript·vue.js·typescript·vue
zwjapple几秒前
typescript里面正则的使用
开发语言·javascript·正则表达式
小五Five2 分钟前
TypeScript项目中Axios的封装
开发语言·前端·javascript
小曲程序2 分钟前
vue3 封装request请求
java·前端·typescript·vue
临枫5412 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
酷酷的威朗普3 分钟前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
前端每日三省4 分钟前
面试题-TS(八):什么是装饰器(decorators)?如何在 TypeScript 中使用它们?
开发语言·前端·javascript
小刺猬_9854 分钟前
(超详细)数组方法 ——— splice( )
前端·javascript·typescript
契机再现5 分钟前
babel与AST
javascript·webpack·typescript