最新Next 14快速上手基础部分
最新Next 14快速上手基础部分
最新的NEXT快速上手文档,2023.10.27 英文官网同步,版本Next14.0.0
本项目案例:GitHub地址,可以根据git回滚代码到对应知识,若有错误,欢迎指正!
一、介绍
1.什么是Next.js
Next.js是一个用于构建全栈Web应用程序的React框架。你可以使用React组件来构建用户界面,使用Next.js来实现额外的功能和优化。
在引擎盖下,Next.js还抽象并自动配置React所需的工具,如捆绑,编译等。这使您可以专注于构建应用程序,而不是花费时间进行配置。
无论您是个人开发人员还是大型团队的一员,Next.js都可以帮助您构建交互式,动态和快速的React应用程序。
2. 主要特点
Next.js的特点主要包括:
| 特点 | 描述 |
|---|---|
| 路由 | 基于文件系统的路由器,构建在服务器组件之上,支持布局、嵌套路由、加载状态、错误处理等。 |
| 渲染 | 使用客户端和服务器组件进行客户端和服务器端渲染。通过Next.js在服务器上使用静态和动态渲染进一步优化。Edge和Node.js运行时上的流媒体。 |
| 数据获取 | 简化了服务器组件中的数据获取,并扩展了fetch API,用于请求存储,数据缓存和重新验证。 |
| 样式 | 支持您首选的样式化方法,包括CSS Modules、Tailwind CSS和CSS-in-JS |
| 优化项 | 图像、字体和脚本优化,以改善应用程序的核心Web关键点和用户体验。 |
| TypeScript支持 | 改进了对TypeScript的支持,具有更好的类型检查和更有效的编译,以及自定义TypeScript插件和类型检查器。 |
二、安装Next
Node版本要求在18.17以上,建议使用nvm切换
1. 安装步骤
-
打开终端(官网建议使用
create-next-app创建Next应用)npx create-next-app@latest -
接下来将看到如下提示:根据自己的习惯进行选择,这里我全选Yes,最后回车
What is your project named? my-next-app
Would you like to use TypeScript? No / Yes
Would you like to use ESLint? No / Yes
Would you like to use Tailwind CSS? No / Yes
Would you like to usesrc/directory? No / Yes
Would you like to use App Router? (recommended) No / Yes
Would you like to customize the default import alias (@/)? No / Yes
What import alias would you like configured? @/注意:选择使用项目根目录中的
src目录将应用程序代码与配置文件分开。这和我选择的方式是一致的
2. 项目结构
下面将介绍我们主要关注的几个目录
-
顶级目录文件夹
public服务的静态资产 src应用程序源文件夹,在这个文件夹下编写应用代码 -
src文件夹- 在
src文件夹中的app目录就是我们选择的App Router,在app文件夹创建文件夹及相关文件将对应相应的路由,后面将详细说明 - 在
src下,按照习惯,- 创建
components文件夹,用于放置自定义的组件 - 创建
styles文件夹,用于放置样式文件,当前使用的是CSS in JS方式 - 创建
lib文件夹,用于放置自定义的方法工具等
······
- 创建
- 在
三、构建应用程序
推荐使用路由器==(App Router )方式==
Next.js使用基于文件系统的路由器。App Router概览
路由目录对应的文件规则:
文件名(后缀.js .jsx .tsx) 描述 layout路由及其子路由的共享UI page路由的唯一UI并使路由可公开访问 loading路由加载及其子路由加载的UI not-found找不到路由及其子路由的UI errorError UI for a segment and its children段及其子段的错误UI global-error全局错误UI,在app(根)目录下 route服务器端API端点 template专门的重新渲染布局UI default并行路由的回退UI
I. 定义路由
Next.js使用基于文件系统的路由器,其中文件夹用于定义路由。
每个文件夹代表一个映射到URL段的路由段。要创建嵌套路由,可以将文件夹嵌套在彼此内部。

特殊的page.js文件用于使路由可公开访问。(主要后缀js本文用tsx)
例如,要创建第一个页面,在src/app目录下添加page.tsx文件,并导出React组件:
export default function Page() {return <h1>Hello, Next.js!</h1>
}
执行命令npm run dev,访问:http://localhost:3000/,页面如下:

II. 页面和布局
Next.js 13中的App Router引入了新的文件约定,可以轻松创建页面、共享布局和模板。本篇将指导您如何在Next.js应用程序中使用这些特殊文件。
-
页面
页面是路由所特有的UI。可以通过从
page.tsx文件导出组件来定义页面。使用嵌套文件夹定义路由和page.js文件以使路由可公开访问上一节,我们已经在
src/app下添加了page.tsx文件作为首页,我们更新这个文件:// `app/page.tsx` is the UI for the `/` URL export default function Page() {return <h1>Hello, Home page!</h1> }接下来我们将在
src/app下添加dashboard目录,并且在这个新增目录下添加page.tsx// `app/dashboard/page.tsx` is the UI for the `/dashboard` URL export default function Page() {return <h1>Hello, Dashboard Page!</h1> }当我们访问对应路由
/或/dashboard的时候,就会分别展示对应的page/tsx中的UI,对应目录和路由如下:
总结:要使路由可公开访问,需要使用
page.js文件。 -
布局
布局是在多个页面之间共享的UI。在导航时,布局将保留状态,保持交互性,并且不会重新呈现。布局也可以==嵌套==。
我们可以通过从
layout.js文件默认(default)导出React组件来定义布局。该组件应该接受一个childrenprop,该prop将在呈现过程中填充子布局(如果存在)或子页面。
最顶层的布局称为根布局,即app目录下的layout.tsx。该文件是必须存在的,且在应用程序中的所有页面之间共享。根布局必须包含html和body标签。
app/layout.tsx根布局如下(也可以自定义):export default function RootLayout({children, }: {children: React.ReactNode }) {return (<html lang="en"><body>{children}</body></html>) }为了演示这个效果,我们单独封装了个简单的共享组件,当然后面也会详细说明在Next中的路由跳转。
首先,在
src/components下新建文件夹links并在目录下创建文件index.tsx:'use client'import { usePathname } from 'next/navigation' import Link from 'next/link' type Props = {linkList: string[] } export function Links({ linkList }: Props) {const pathname = usePathname()return (<nav><ul style={{ display: 'flex', listStyle: 'none' }}>{linkList.map((link:string) => {return (<li key={link} style={{ margin: '0 20px' }}><Link className={`${pathname === link ? 'active' : ''}`} href={link === 'home' ? '/' : '/' + link}>{link?.toUpperCase()}</Link></li>)})}</ul></nav>) }接下来,我们将按如下目录创建文件:

在
src/app/dashboard下创建layout.tsx文件:import { Links } from '../../components/links'export default function DashboardLayout({ children }: { children: React.ReactNode }) {return (<section>{/* Include shared UI here e.g. a header or sidebar */}<Links linkList={['dashboard', 'dashboard/settings']} />{children}</section>) }在
src/app/dashboard/settings下创建page.tsx文件:外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传export default function Page() {return <h1>settings</h1> }效果如下:

嵌套布局,在文件夹(例如
app/dashboard/layout.js)中定义的布局适用于特定的路由(例如acme.com/dashboard),并在这些路由处于活动状态时进行渲染。默认情况下,文件层次结构中的布局是嵌套的,这意味着它们通过其children属性包装子布局。

-
模板Templates(目前先简单了解)
模板类似于布局,因为它们包装每个子布局或页面。与跨路径持久化并保持状态的布局不同,模板在导航上为其每个子项创建一个新实例。这意味着,当用户在共享模板的路由之间导航时,将挂载组件的新实例,重新创建DOM元素,不保留状态,并重新同步效果。
在某些情况下,您可能需要这些特定的行为,而模板将是比布局更合适的选择。例如:
- 依赖于
useEffect(例如记录页面浏览量)和useState(例如每页反馈表单)的功能。 - 更改默认框架行为。例如,布局内的Suspense Bouncement仅在首次加载布局时显示回退,而在切换页面时不显示。对于模板,回退显示在每个导航中。
模板可以通过从
template.js文件导出默认的React组件来定义。该组件应接受childrenprop。如src/app/template.tsx:export default function Template({ children }: { children: React.ReactNode }) {return <div>{children}</div> } - 依赖于
-
修改
<head>在
app目录中,您可以使用内置的SEO支持修改<head>HTML元素,例如title和meta。元数据即
html文件中head标签下的内容,可以在layout.tsx或page.tsx中导出metadata对象或generateMetadata函数来定义,如src/app/page.tsx:import { Metadata } from 'next'export const metadata: Metadata = {title: 'Next.js', }export default function Page() {return '...' }然后访问路由
/时,标签页的名就会变为Next.js
III. 链接和导航
在Next.js中有两种方法可以在路由之间导航:
- 使用
Link组件- 使用
useRouterHook
-
<Link>组件<Link>是一个内置组件,用于扩展 HTML<a>标记以提供路由之间的预取和客户端导航。这是在 Next.js 中的路由之间导航的主要方式。可以通过从
next/link导入<Link>并将href传递给组件来使用它,使用例子,如前面添加的
src/components/index.tsx,href属性传入跳转的对应路由,此外还可以以对象方式传入,Link组件具体使用'use client'import { usePathname } from 'next/navigation' import Link from 'next/link' type Props = {linkList: string[] } export function Links({ linkList }: Props) {const pathname = usePathname()return (<nav><ul style={{ display: 'flex', listStyle: 'none' }}>{linkList.map((link:string) => {return (<li key={link} style={{ margin: '0 20px' }}><Link className={`${pathname === link ? 'active' : ''}`} href={link === 'home' ? '/' : '/' + link}>{link?.toUpperCase()}</Link></li>)})}</ul></nav>) } -
useRouter()勾子此钩子只能在客户端组件中使用,并从
next/navigation导入。'use client'import { useRouter } from 'next/navigation'export default function Page() {const router = useRouter()return (<button type="button" onClick={() => router.push('/dashboard')}>Dashboard</button>) }有关
useRouter方法的完整列表,请参阅API参考。
IV. 路由分组
在
app目录中,嵌套文件夹通常映射到 URL 路径。但是,您可以将文件夹标记为路由组,以防止该文件夹包含在路由的 URL 路径中。这允许您将路由段和项目文件组织到逻辑组中,而不会影响 URL 路径结构。
-
路由分组的作用
-
将路线组织成组,例如按站点部分、意图或团队。
-
在同一路线段级别启用嵌套布局
- 在同一区段中创建多个嵌套布局,包括多个根布局
- 将布局添加到公共段中的路由子集
-
-
路由分组的使用:
可以通过将文件夹名称括在括号中来创建路由组:
(folderName) -
在不影响 URL 路径的情况下组织路由
要在不影响 URL 的情况下组织路由,请创建一个组以将相关路由保持在一起。括号中的文件夹将从 URL 中省略(例如或(marketing)(shop))。
同时,即使路由内部
(marketing)和(shop)共享相同的 URL 层次结构,您也可以通过在文件夹内添加layout.js文件来为每个组创建不同的布局。
-
创建多个根布局
要创建多个根布局,请移除顶级文件,然后在每个路由组内添加一个
layout.jslayout.js文件。这对于将应用程序划分为具有完全不同的 UI 或体验的部分非常有用。<html>需要将 和<body>标记添加到每个根布局中。
V. 动态路由
如果您提前不知道确切的路由名称,并且想要根据动态数据创建路由,则可以使用在请求时填充或在构建时预呈现的动态路由。
可以通过将文件夹的名称括在方括号中来创建动态路由: [folderName] 。例如, [id] 或 [slug] 。
动态路由作为 params prop传递给 、 layout route 、 page 和 generateMetadata 函数。
例如src/app/blog/[id]/page.tsx:
export default function Page({ params }: { params: { id: string } }) {return <div>My Post: {params.id}</div>
}
| 路由 | 示例网址 | params |
|---|---|---|
app/blog/[id]/page.js | /blog/a | { id: 'a' } |
app/blog/[id]/page.js | /blog/b | { id: 'b' } |
app/blog/[id]/page.js | /blog/c | { id: 'c' } |
generateStaticParams 函数可与动态路由段结合使用,以在构建时静态生成路由,而不是在请求时按需生成路由。
例如src/app/blog/[id]/page.tsx:
export async function generateStaticParams() {const posts = await fetch('https://.../posts').then((res) => res.json())return posts.map((post) => ({id: post.id,}))
}
最简单的动态路由案例(博客)实现,步骤:
-
首先引入我们需要使用的样式文件,在
src/styles/utils.module.css中写入代码:.heading2Xl {font-size: 2.5rem;line-height: 1.2;font-weight: 800;letter-spacing: -0.05rem;margin: 1rem 0; }.headingXl {font-size: 2rem;line-height: 1.3;font-weight: 800;letter-spacing: -0.05rem;margin: 1rem 0; }.headingLg {font-size: 1.5rem;line-height: 1.4;margin: 1rem 0; }.headingMd {font-size: 1.2rem;line-height: 1.5; }.borderCircle {border-radius: 9999px; }.colorInherit {color: inherit; }.padding1px {padding-top: 1px; }.list {list-style: none;padding: 0;margin: 0; }.listItem {margin: 0 0 1.25rem; }.lightText {color: #999; } -
在
src/posts文件夹下,准备两个Markdown文件-
pre-rendering.md--- title: 'Two Forms of Pre-rendering' date: '2020-01-01' ---Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request. - **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others. -
ssg-ssr.md--- title: 'When to Use Static Generation v.s. Server-side Rendering' date: '2020-01-02' ---We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.You can use Static Generation for many types of pages, including:- Marketing pages - Blog posts - E-commerce product listings - Help and documentationYou should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
-
-
安装三个包
npm i gray-matter remark remark-html -
在
src/lib/posts.ts中,编写要用到的代码import fs from 'fs' import path from 'path' import matter from 'gray-matter' import { remark } from 'remark' import html from 'remark-html'const postsDirectory = path.join(process.cwd(), 'src/posts')// 获取排序后的blog列表 export function getSortedPostsData() {// Get file names under /postsconst fileNames = fs.readdirSync(postsDirectory)const allPostsData = fileNames.map(fileName => {// Remove ".md" from file name to get idconst id = fileName.replace(/\.md$/, '')// Read markdown file as stringconst fullPath = path.join(postsDirectory, fileName)const fileContents = fs.readFileSync(fullPath, 'utf8')// Use gray-matter to parse the post metadata sectionconst matterResult = matter(fileContents)// Combine the data with the idreturn {id,...matterResult.data}})return new Promise(function (resolve, reject) {//做一些异步操作setTimeout(function () {resolve(// Sort posts by dateallPostsData.sort((a: any, b: any) => {if (a.date < b.date) {return 1} else {return -1}}))}, 1000)}) }// 获取所有动态路由 export function getAllPostIds() {const fileNames = fs.readdirSync(postsDirectory)// Returns an array that looks like this:// [// {// params: {// id: 'ssg-ssr'// }// },// {// params: {// id: 'pre-rendering'// }// }// ]return new Promise(function (resolve, reject) {//做一些异步操作setTimeout(function () {resolve(fileNames.map(fileName => {return {params: {id: fileName.replace(/\.md$/, '')}}}))}, 1000)}) }// 根据blog的ID获取博客内容 export async function getPostData(id: string) {const fullPath = path.join(postsDirectory, `${id}.md`)const fileContents = fs.readFileSync(fullPath, 'utf8')// Use gray-matter to parse the post metadata sectionconst matterResult = matter(fileContents)console.log('matterResult', matterResult)// Use remark to convert markdown into HTML stringconst processedContent = await remark().use(html).process(matterResult.content)const contentHtml = processedContent.toString()// Combine the data with the id and contentHtmlreturn {id,contentHtml,...matterResult.data} } -
在
src/app/blogs目录下新建page.tsx文件import { getSortedPostsData } from '@/lib/posts' import utilStyles from '../../styles/utils.module.css' import Link from 'next/link' export default async function Page() {// 获取按日期排序好的博客大纲const allPostsData: any = await getSortedPostsData()// console.log('allPostsData', allPostsData)return (<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}><h2 className={utilStyles.headingLg}>Blogs</h2><ul className={utilStyles.list}>{/* 渲染博客列表 */}{allPostsData.map(({ id, date, title }: { id: string; date: string; title: string }) => (<li className={utilStyles.listItem} key={id}><Link href={`/blogs/${id}`}>{title}</Link><br /><small className={utilStyles.lightText}>{date}</small></li>))}</ul></section>) }接下来,访问:http://localhost:3000/blogs
你将看到如下页面

-
在
src/app/blogs/[id]目录下新建page.tsx文件import Head from 'next/head' import { getAllPostIds, getPostData } from '../../../lib/posts' import utilStyles from '../../../styles/utils.module.css' import Link from 'next/link'// params中的属性对应文件夹[id] type pathProps = [{ params: { id: string } }] // generateStaticParams函数可以与动态路由段结合使用,以便在构建时静态生成路由,而不是在请求时按需生成路由。 // 若是无generateStaticParams函数不影响动态路由使用 // 静态生成的params参数数组,用于构建动态路由 export async function generateStaticParams() {const paths = (await getAllPostIds()) as pathPropsconsole.log('paths', paths)return paths }type pageParams = {params: {// 此处id对应动态路由文件夹 [id], 若是[slug]文件夹应该是 slug:stringid: string}// 此处的searchParams对应浏览器的query参数,即?username=xzq&age=18这种searchParams: {} } // 页面(默认导出),根据对应的动态路由渲染页面 export default async function Page({ params }: pageParams) {const postData: any = await getPostData(params.id)return (<><Head><title>{postData.id}</title></Head><article><h1 className={utilStyles.headingXl}>{postData?.id}</h1><div className={utilStyles.lightText}>{postData?.date}</div><div dangerouslySetInnerHTML={{ __html: postData?.contentHtml }} /></article><Link style={{ position: 'absolute', marginTop: 100 }} href={`/blogs`}>back blogs</Link></>) }接下来,访问:http://localhost:3000/blogs/ssg-ssr
你就看到如下页面:

-
最终案例效果如下:

此外,还有两种动态路由,详情见Next官网
VI. 加载UI
特殊文件
loading.js可帮助您使用 React Suspense 创建有意义的加载 UI。使用此约定,您可以在加载路由段的内容时显示来自服务器的即时加载状态。渲染完成后,新内容将自动交换。
-
立即加载状态
即时加载状态是导航时立即显示的回调 UI。您可以预渲染加载指示器,例如骨架和微调器,或者未来屏幕的一小部分但有意义的部分,例如封面照片、标题等。这有助于用户了解应用正在响应,并提供更好的用户体验。
通过在文件夹中添加
loading.js文件来创建加载状态。
在
src/app/dashboard下新建文件loading.tsxexport default function Loading() {// You can add any UI inside Loading, including a Skeleton.return <>加载中...</> }在同一个文件夹中,
loading.js将嵌套在layout.js.它会自动将page.js文件和下面的任何子项包装在<Suspense>边界中。
-
流式处理相关,见Next官网
VII. 错误处理
普通错误
文件 error.js 约定允许您正常处理嵌套路由中的意外运行时错误。
- 自动将路由段及其嵌套子级包装在 React 错误边界中。
- 使用文件系统层次结构创建针对特定段定制的错误 UI,以调整粒度。
- 将错误隔离到受影响的段,同时保持应用程序的其余部分正常运行。
- 添加功能以尝试从错误中恢复,而无需重新加载整个页面。
通过在路由段中添加 error.js 文件并导出 React 组件来创建错误 UI:

在src/app/dashboard下新建文件error.tsx
'use client' // Error components must be Client Componentsimport { useEffect } from 'react'export default function Error({error,reset,
}: {error: Error & { digest?: string }reset: () => void
}) {useEffect(() => {// Log the error to an error reporting serviceconsole.error(error)}, [error])return (<div><h2>Something went wrong!</h2><buttononClick={// Attempt to recover by trying to re-render the segment() => reset()}>Try again</button></div>)
}
注意:错误处理组件必须是一个客户端组件
error.tsx的工作原理

error.js自动创建一个 React 错误边界,用于包装嵌套的子段或page.js组件。- 从
error.js文件导出的 React 组件用作回退组件。 - 如果在错误边界内引发错误,则会包含该错误,并呈现回退组件。
- 当回退错误组件处于活动状态时,错误边界上方的布局将保持其状态并保持交互性,并且错误组件可以显示从错误中恢复的功能。
处理嵌套路由错误
通过特殊文件创建的 React 组件呈现在特定的嵌套层次结构中。
例如,具有两个包含 layout.js 和 error.js 文件的段的嵌套路由在以下简化的组件层次结构中呈现:

嵌套组件层次结构对嵌套路由中的 error.js 文件行为有影响:
- 错误冒泡到最近的父错误边界。这意味着
error.js文件将处理其所有嵌套子段的错误。通过将文件放置在error.js路由的嵌套文件夹中的不同级别,可以实现或多或少的粒度错误 UI。 - 错误处理
error.js不会处理同一段中layout.js组件中引发的错误,因为错误边界error.js嵌套在该布局layout.js的中。
处理布局中的错误
error.js 边界不会捕获抛出 layout.js 的错误或 template.js 同一段的组件。这种有意的层次结构使在发生错误时在同级路由(如导航)之间共享的重要 UI 可见且正常运行。
要处理特定布局或模板中的错误,请将 error.js 文件放在布局父段中。
处理根布局中的错误
根错误边界 app/error.js 不会捕获根布局app/layout.js 或模板 app/template.js 组件中引发的错误。
要处理根布局或模板中的错误,请使用命名的 error.js 变体:global-error.js 。
与根错误边界error.js不同, global-error.js 错误边界包装整个应用程序,其回退组件在活动时替换根 error.js 布局。因此,重要的是要注意必须 global-error.js 定义自己的 <html> 和 <body> 标签。
global-error.js 是最精细的错误 UI,可被视为整个应用程序的“全部捕获”错误处理。它不太可能经常触发,因为根组件通常不太动态,其他 error.js 边界将捕获大多数错误。
即使定义了 , global-error.js 仍建议定义一个根,其回退组件将在根 error.js 布局中呈现,其中包括全局共享的 UI 和品牌。
src/app/global-error.tsx如下
'use client'export default function GlobalError({error,reset,
}: {error: Error & { digest?: string }reset: () => void
}) {return (<html><body><h2>Something went wrong!</h2><button onClick={() => reset()}>Try again</button></body></html>)
}
处理服务器错误
如果在服务器组件中抛出错误,Next.js 会将一个 Error 对象(在生产中去除敏感错误信息)转发到最近的 error.js 文件作为 error prop。
保护敏感错误信息,在生产过程中,转发到客户端的 Error 对象仅包含泛型 message 和 digest 属性。这是一种安全预防措施,可避免将错误中包含的潜在敏感详细信息泄露给客户端。
该属性包含有关错误的通用消息,该 message digest 属性包含自动生成的错误哈希,可用于匹配服务器端日志中的相应错误。
在开发过程中,转发到客户端 Error 的对象将被序列化,并包含原始错误的 , message 以便于调试。
VIII. 并行路由
并行路由允许您在同一布局中同时或有条件地呈现一个或多个页面。对于应用的高度动态部分(例如社交网站上的仪表板和源),并行路由可用于实现复杂的路由模式。
例如,您可以同时呈现团队和分析页面。

并行路由允许您为每个路由定义独立的错误和加载状态,因为它们正在独立流式传输。

并行路由还允许您根据特定条件(如身份验证状态)有条件地呈现槽。这将在同一 URL 上启用完全分离的代码。

并行路由使用规则
并行路由是使用命名槽创建的。插槽是按照 @folder 约定定义的,并作为props传递到同一级别的布局。
槽不是路由段,不会影响 URL 结构。可在路由
/members访问文件路径/@team/members。
例如,以下文件结构定义了两个显式插槽: @analytics 和 @team 。

上面的文件夹结构意味着 app/layout.js 组件中现在接受 @analytics 和 @team 插槽 props,并且可以将它们与 children 并行渲染:
src/app/layout.tsx
export default function RootLayout({children,team,analytics
}: {children: React.ReactNodeteam: React.ReactNodeanalytics: React.ReactNode
}) {return (<html lang="en"><body><>{children}{team}{analytics}</></body></html>)
}
提示: children 道具是一个隐式插槽,不需要映射到文件夹。这意味着 app/page.js 等效于 app/@children/page.js 。
在src/app/@team/page.tsx中
export default function Page() {return <h1>Parallel Route Team </h1>
}
src/app/@analytics/page.tsx类似
最终我们访问:http://localhost:3000/

未被匹配的路由
==默认情况下,==插槽内渲染的内容将与当前 URL 匹配。
对于不匹配的插槽,Next.js 呈现的内容因路由技术和文件夹结构而异。
default.tsx
您可以定义一个文件default.tsx ,以便在 Next.js 无法根据当前 URL 恢复槽的活动状态时渲染。
- 在导航时,Next.js 将呈现槽以前处于活动状态的状态,即使它与当前 URL 不匹配。
- 重新加载时,Next.js 将首先尝试渲染不匹配的插槽的
default.tsx的文件。如果default.tsx不存在,则呈现 404。
用于不匹配路由的 404 有助于确保您不会意外渲染不应并行渲染的路由。
useSelectedLayoutSegment(s)
useSelectedLayoutSegment和 useSelectedLayoutSegments 接受 parallelRoutesKey,这允许您读取该插槽内的活动路由段,不包括并行路由内部的路由。
为了更好的演示,并行路由的以上情况,我们实现了一个案例来更好的解释,尤其是在未被匹配路由的情况下,以及重新加载时**default.tsx**的效果。(注意本操作,基于前面的项目)
-
首先新建两个并行路由目录
src/app/@team和src/app/@analytics在
@team目录下-
新建
page.tsxexport default function Page() {return <h1>Parallel Route Team </h1> } -
新建
default.jsexport default function Page() {return (<h1>Parallel Route Team <span style={{ color: 'red' }}>Default</span></h1>) } -
新建目录
settings,在这个目录下新建page.tsxexport default function Page() {return <h1>Parallel Route Team Settings </h1> }
在
@analytics目录下-
新建
page.tsxexport default function Page() {return <h1>Parallel Route Analytics </h1> } -
新建
default.jsexport default function Page() {return (<h1>Parallel Route Analytics <span style={{ color: 'yellow' }}>Default</span></h1>) }
-
-
然后更新
src/app/page.tsx和src/app/layout.tsx-
src/app/page.tsximport { Links } from '@/components/links' import { Metadata } from 'next'export const metadata: Metadata = {title: 'Next.js' }// `app/page.tsx` is the UI for the `/` URL export default function Page() {return (<><Links linkList={['dashboard', 'settings']} /></>) } -
src/app/layout.tsx'use client' import Link from 'next/link' import { useSelectedLayoutSegment, useSelectedLayoutSegments } from 'next/navigation'export default function RootLayout({children,team,analytics }: {children: React.ReactNodeteam: React.ReactNodeanalytics: React.ReactNode }) {const allSegments = useSelectedLayoutSegments()console.log('allSegments', allSegments)return (<html lang="en"><body><>{children}{team}{analytics}<Link style={{ position: 'absolute', marginTop: 100 }} href={`/`}>back index</Link></></body></html>) }
-
-
新建
src/app/default.tsxexport default function Page() {return (<h1>App <span style={{ color: 'blue' }}>Default</span>{' '}</h1>) }
完成好的目录结构如下

启动项目访问:http://localhost:3000/,注意观察路由、重新加载页面已经控制台信息,结合上面提到的情况
最终效果如下

可以看到,从首页导航进入/dashboard,页面渲染还是原来的并行路由,接着重新加载(刷新)页面,两个并行路由,分别渲染了对应的default.tsx;当从首页导航进入/settings时,其实这里访问的是/@team/settings的文件,页面渲染还是原来的并行路由,接着重新加载(刷新)页面,重新加载页面,因为当前路由是在/settings所以第一个并行路由渲染的是src/app/@team/settings/page.tsx,第二个并行路由则渲染自己的default.tsx,此外原来路由对应渲染的children,路由发生变化时,重新加载,也会使用自身的default.tsx
注意:前面说到的404页面,在当前项目下,当你将
/@team/default.tsx删除后,进入/dashboard,刷新页面,因为此时有一个并行路由找不到对应的default.tsx,所以会渲染404页面,这个可以选择去尝试一下
另有关登录Modal模态框及条件路由的相关使用见Next官网
IX. 拦截路由
拦截路由允许您从当前布局中应用程序的另一部分加载路由。当您希望在用户不切换到其他上下文的情况下显示路由的内容时,此路由范式非常有用。
例如,单击源中的照片时,可以以Modal模态框显示照片,覆盖源。在这种情况下,Next.js 会截获 /photo/123 路由,屏蔽 URL,并将其覆盖 /feed 在 上。

但是,当通过单击可共享的 URL 或刷新页面导航到照片时,应呈现整个照片页面而不是模式。不应发生路由拦截。

拦截路由的定义方式
拦截路由可以使用指定规则定义,该规则类似于相对路径 (..) 约定 ../ ,但适用于路由。
可以使用:
(.)匹配同一级别的路由段(..)匹配上一级的路由段(..)(..)匹配上两级的路由(...)匹配根app目录中的路由段
接下来,我们实现一个Modal框的小案例(建议将前面的记录用commit提交,后续方便回滚查看),我们将用到动态路由、并行路由和拦截路由的相关知识
首先,添加src/lib/photos.ts,代码如下
import { StaticImageData } from 'next/image'
import imgTemp from '@/assets/images/opengraph-image.jpg'
export type Photo = {id: stringname: stringhref: stringusername: stringimageSrc: StaticImageData
}const photos: Photo[] = [{id: '1',name: 'Kevin Canlas',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp,username: '@kvncnls'},{id: '2',name: 'Pedro Duarte',username: '@peduarte',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '3',name: 'Ahmad Awais',username: '@MrAhmadAwais',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '4',name: 'Leandro Soengas',username: '@lsoengas',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '5',name: 'Samina',username: '@saminacodes',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '6',name: 'lafond.eth',username: '@laf0nd',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '7',name: '山岸和利💛',username: '@ykzts',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '8',name: 'Altngelo',username: '@AfterDarkAngelo',href: 'https://wallhaven.cc/w/gp1j9l',imageSrc: imgTemp},{id: '9',name: 'Matias Baldanza',href: 'https://twitter.com/matiasbaldanza/status/1404834163203715073',username: '@matiasbaldanza',imageSrc: imgTemp}
]export default photos
新建src/assets/images文件夹,并放入一张自己喜欢的图片,我们这里命名为opengraph-image.jpg,此外我们将src/styles移动到src/assets中完善项目文件结构,其中的globals.css如下
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body {padding: 0;margin: 0;font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell, Fira Sans, Droid Sans,Helvetica Neue, sans-serif;line-height: 1.6;font-size: 18px;
}* {box-sizing: border-box;
}a {color: #0070f3;text-decoration: none;
}a:hover {text-decoration: underline;
}img {max-width: 100%;display: block;
}
新增两个组件在src/components
-
modal/Modal.tsx'use client' import { useCallback, useRef, useEffect, MouseEventHandler } from 'react' import { useRouter } from 'next/navigation'export default function Modal({ children }: { children: React.ReactNode }) {const overlay = useRef(null)const wrapper = useRef(null)const router = useRouter()const onDismiss = useCallback(() => {router.back()}, [router])const onClick: MouseEventHandler = useCallback(e => {if (e.target === overlay.current || e.target === wrapper.current) {if (onDismiss) onDismiss()}},[onDismiss, overlay, wrapper])const onKeyDown = useCallback((e: KeyboardEvent) => {if (e.key === 'Escape') onDismiss()},[onDismiss])useEffect(() => {document.addEventListener('keydown', onKeyDown)return () => document.removeEventListener('keydown', onKeyDown)}, [onKeyDown])return (<div ref={overlay} className="fixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60" onClick={onClick}><divref={wrapper}className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full sm:w-10/12 md:w-8/12 lg:w-1/3 p-6">{children}</div></div>) } -
frame/Frame.tsximport Image from 'next/image' import { Photo } from '../../lib/photos'export default function Frame({ photo }: { photo: Photo }) {return (<><Imagealt=""src={photo.imageSrc}height={600}width={600}className="w-full object-cover aspect-square col-span-2"/><div className="bg-white p-4 px-6"><h3>{photo.name}</h3><p>Taken by {photo.username}</p></div></>) }
更新src/app/page.tsx
import Link from 'next/link'
import swagPhotos from '../lib/photos'
import Image from 'next/image'export default function Home() {const photos = swagPhotosreturn (<main className="container mx-auto"><h1 className="text-center text-4xl font-bold m-10">Parallel routing and route interception achieve Modal</h1><div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-3 auto-rows-max gap-6 m-10">{photos.map(({ id, imageSrc }: { id: string; imageSrc: any }) => (<Link key={id} href={`/photos/${id}`}><Image alt="" src={imageSrc} height={500} width={500} className="w-full object-cover aspect-square" /></Link>))}</div></main>)
}
更新src/app/layout.tsx
'use client'
import '@/assets/styles/globals.css'
import Link from 'next/link'import { useSelectedLayoutSegment, useSelectedLayoutSegments } from 'next/navigation'export default function RootLayout({children,team,analytics,modal
}: {children: React.ReactNodeteam: React.ReactNodeanalytics: React.ReactNodemodal: React.ReactNode
}) {const allSegments = useSelectedLayoutSegments()console.log('allSegments', allSegments)return (<html lang="en"><body><>{children}{modal}<Link style={{ position: 'absolute', marginTop: 100 }} href={`/`}>back index</Link></></body></html>)
}
更新src/app/default.tsx
// app default
export default function Page() {return null
}
新建文件夹src/app/@modal
-
该文件夹下新建
default.tsx,和app/default.tsx返回null防止在并行路由刷新页面404的情况 -
该文件夹下新建
(.)photos/[id]/page.tsximport Frame from '../../../../components/frame/Frame' import Modal from '../../../../components/modal/Modal' import swagPhotos, { Photo } from '../../../../lib/photos'export default function PhotoModal({ params: { id: photoId } }: { params: { id: string } }) {const photos = swagPhotosconst photo: Photo = photos.find(p => p.id === photoId)!return (<Modal><Frame photo={photo} /></Modal>) }
新建src/app/photos/[id]/page.tsx,该动态路由的作用是当对应的路由被拦截后,刷新页面展示到这个路由页面
import Frame from '../../../components/frame/Frame'
import swagPhotos, { Photo } from '../../../lib/photos'export default function PhotoPage({ params: { id } }: { params: { id: string } }) {const photo: Photo = swagPhotos.find(p => p.id === id)!return (<div className="container mx-auto my-10"><div className="w-1/2 mx-auto border border-gray-700"><Frame photo={photo} /></div></div>)
}
新建src/app/photos/default.tsx,和前面的一样返回null就行
最终的效果

相关文章:
最新Next 14快速上手基础部分
最新Next 14快速上手基础部分 最新的NEXT快速上手文档,2023.10.27 英文官网同步,版本Next14.0.0 本项目案例:GitHub地址,可以根据git回滚代码到对应知识,若有错误,欢迎指正! 一、介绍 1.什么是…...
【uniapp/uview】Collapse 折叠面板更改右侧小箭头图标
最终效果是这样的: 官方没有给出相关配置项,后来发现小箭头不是 uview 的图标,而是 unicode 编码,具体代码: // 箭头图标 ::v-deep .uicon-arrow-down[data-v-6e20bb40]:before {content: \1f783; }附一个查询其他 u…...
企业如何落地搭建商业智能BI系统
随着新一代信息化、数字化技术的应用,引发了新一轮的科技革命,现代化社会和数字化的联系越来越紧密,数据也变成继土地、劳动力、资本、技术之后的第五大生产要素,这一切都表明世界已经找准未来方向,前沿科技也与落地并…...
RedisTemplate连接密码设置教程
最近在一个项目中使用Redis保存Token时,出现连接Redis报错的情况 org.springframework.data.redis.RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException: Unable to connect to localhos…...
基于SSM的二手车交易网站的设计与实现
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:Vue 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目:是 目录…...
温故知新:探究Android UI 绘制刷新流程
一、说明: 基于之前的了解知道ui的绘制最终会走到Android的ViewRootImpl中scheduleTraversals进行发送接收vsync信号绘制,在ViewRootImpl中还会进行主线程检测,也就是我们所谓子线程更新ui会抛出异常。 像我们常用的刷新ui,inval…...
设计模式-命令模式(Command)
设计模式-命令模式(Command) 一、命令模式概述1.1 什么是命令模式1.2 简单实现命令模式1.3 使用命令模式的注意事项 二、命令模式的用途三、命令模式实现方式3.1 使用匿名内部类实现命令模式3.2 使用Lambda表达式实现命令模式3.3 使用Java内置的函数式接…...
linux批量解压zip
方法一 1,创建unzip.sh #!/bin/bashwhile read line do unzip $linedone < filelist.txt #!/bin/bashwhile read line dounzip "$line" >& log & done < filelist.txt3. 在终端中执行以下命令 $ chmod x unzip.sh $ ./unzip.sh 这…...
HBase导出建表语句
HBase导出建表语句 HBase是一个面向大数据的分布式列存数据库,它以Hadoop作为底层存储和计算平台。在HBase中,数据以表的形式存储,每个表由行和列组成。本文将介绍如何使用HBase导出建表语句,并提供相应的代码示例。 HBase建表语…...
Linux环境配置(云服务器)
目录 1.第一步:购买云服务器 2.第二步:下载Xshell 7 3.第三步:打开Xshell,登录云服务器 4.第四步:更加便捷的云服务器登录方式 1.第一步:购买云服务器 (推荐:阿里云、华为云、腾…...
【性能测试】Linux下Docker安装与docker-compose管理容器(超细整理)
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 1、Linux下Docker…...
陪玩2.0升级版源码/价值18500元的最新商业版游戏陪玩语音聊天系统源码
陪玩2.0升级版源码,价值18500元的最新商业版游戏陪玩语音聊天系统源码。 修复部分逻辑以及bug 修复bug:店员拒单后,退款会退到店员账号里而不是用户账户里。 修复bug:客户在盲盒下单后,马上取消了订单,但…...
读程序员的制胜技笔记08_死磕优化(上)
1. 过早的优化是万恶之源 1.1. 著名的计算机科学家高德纳(Donald Knuth)的一句名言 1.2. 原话是:“对于约97%的微小优化点,我们应该忽略它们:过早的优化是万恶之源。而对于剩下的关键的3%,我们则不能放弃优化的机会。” 2. 过早…...
【gltf-pipeline】安装gltf-pipeline 进行文件格式转换
问题 想使用gltf-pipeline进行gltf和glb格式转换。简单记录一下安装过程。 解决 1、安装Node.js Node.js下载路径:https://nodejs.org/en 建议默认设置安装。 添加系统环境变量: 测试安装是否成功: 在cmd.exe中运行: no…...
Android OpenGL ES踩坑记录
因为项目中的一个自定义绘图控件性能不行,改用OpenGL实现,也是第一次使用OpenGL,由于只是绘制2D图形,参考官方以及网上的教程,实现起来还是比较顺畅的,开发时只用了两个手机测试,运行良好&#…...
Vue3 项目完整配置
目录 一、配置简述二、创建项目1、使用包管理工具 pnpm2、新增目录 三、配置 ESLint1、添加代码2、修改 VSCode 配置 四、husky 工具配置五、暂存区 eslint 校验六、axios 配置1、安装创建2、测试 七、导入 Element Plus八、Pinia 持久化实现九、其他导入 .scss 文件需要安装 s…...
二十三种设计模式全面解析-从线程安全到创新应用:探索享元模式的进阶之路
在软件开发领域,线程安全和设计模式都是我们经常遇到的话题。线程安全保证了多线程环境下的数据一致性和可靠性,而设计模式则提供了一套经验丰富的解决方案。在前文中,我们已经了解了线程安全的处理和享元模式的基本概念。但是,如…...
Qt之qobject_cast使用
描述 qobject_cast是Qt中的一个转换函数,主要用于在QObject子类之间进行转换,实现父类指针向子类指针的转换。其语法为: qobject_cast<T>(object);其中,T表示目标类型,object表示要转换的QObject对象指针。 q…...
如何实现云端开发能力快速提升?【DevRun】云上开发创新实践带你实现
随着企业数字化的转型趋势,软件成为数字化转型的关键驱动力,在云计算越来越普及且作用愈发重要的今天,现代应用正以难以想象的速度在增长,同时对软件开发工具提出了新的要求。 华为云CodeArts作为一站式云上开发创新工具…...
猫头虎博主第7期赠书活动:《计算机考研精炼1000题》
🌷🍁 博主猫头虎 带您 Go to New World.✨🍁 🦄 博客首页——猫头虎的博客🎐 🐳《面试题大全专栏》 文章图文并茂🦕生动形象🦖简单易学!欢迎大家来踩踩~🌺 &a…...
synchronized 学习
学习源: https://www.bilibili.com/video/BV1aJ411V763?spm_id_from333.788.videopod.episodes&vd_source32e1c41a9370911ab06d12fbc36c4ebc 1.应用场景 不超卖,也要考虑性能问题(场景) 2.常见面试问题: sync出…...
利用ngx_stream_return_module构建简易 TCP/UDP 响应网关
一、模块概述 ngx_stream_return_module 提供了一个极简的指令: return <value>;在收到客户端连接后,立即将 <value> 写回并关闭连接。<value> 支持内嵌文本和内置变量(如 $time_iso8601、$remote_addr 等)&a…...
【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
如何在看板中体现优先级变化
在看板中有效体现优先级变化的关键措施包括:采用颜色或标签标识优先级、设置任务排序规则、使用独立的优先级列或泳道、结合自动化规则同步优先级变化、建立定期的优先级审查流程。其中,设置任务排序规则尤其重要,因为它让看板视觉上直观地体…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...
蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练
前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1):从基础到实战的深度解析-CSDN博客,但实际面试中,企业更关注候选人对复杂场景的应对能力(如多设备并发扫描、低功耗与高发现率的平衡)和前沿技术的…...
Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
