多元网站建设,重庆做网站怎么做呀,深圳东门希尔顿欢朋酒店,网站建设税金会计分录最新Next 14快速上手基础部分 最新的NEXT快速上手文档#xff0c;2023.10.27 英文官网同步#xff0c;版本Next14.0.0 本项目案例#xff1a;GitHub地址#xff0c;可以根据git回滚代码到对应知识#xff0c;若有错误#xff0c;欢迎指正#xff01; 一、介绍
1.什么是…最新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-applatest接下来将看到如下提示根据自己的习惯进行选择这里我全选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 use src/ 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路由及其子路由的共享UIpage路由的唯一UI并使路由可公开访问loading路由加载及其子路由加载的UInot-found找不到路由及其子路由的UIerrorError UI for a segment and its children段及其子段的错误UIglobal-error全局错误UI在app(根)目录下route服务器端API端点template专门的重新渲染布局UIdefault并行路由的回退UI I. 定义路由
Next.js使用基于文件系统的路由器其中文件夹用于定义路由。
每个文件夹代表一个映射到URL段的路由段。要创建嵌套路由可以将文件夹嵌套在彼此内部。 特殊的page.js文件用于使路由可公开访问。(主要后缀js本文用tsx)
例如要创建第一个页面在src/app目录下添加page.tsx文件并导出React组件
export default function Page() {return h1Hello, 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 h1Hello, Home page!/h1
}接下来我们将在src/app下添加dashboard目录并且在这个新增目录下添加page.tsx // app/dashboard/page.tsx is the UI for the /dashboard URL
export default function Page() {return h1Hello, Dashboard Page!/h1
}当我们访问对应路由/或/dashboard的时候就会分别展示对应的page/tsx中的UI对应目录和路由如下 总结要使路由可公开访问需要使用page.js文件。 布局 布局是在多个页面之间共享的UI。在导航时布局将保留状态保持交互性并且不会重新呈现。布局也可以嵌套。 我们可以通过从layout.js文件默认(default)导出React组件来定义布局。该组件应该接受一个children prop该prop将在呈现过程中填充子布局如果存在或子页面。 最顶层的布局称为根布局即app目录下的layout.tsx。该文件是必须存在的且在应用程序中的所有页面之间共享。根布局必须包含html和body标签。 app/layout.tsx根布局如下(也可以自定义) export default function RootLayout({children,
}: {children: React.ReactNode
}) {return (html langenbody{children}/body/html)
}为了演示这个效果我们单独封装了个简单的共享组件当然后面也会详细说明在Next中的路由跳转。 首先在src/components下新建文件夹links并在目录下创建文件index.tsx use clientimport { usePathname } from next/navigation
import Link from next/link
type Props {linkList: string[]
}
export function Links({ linkList }: Props) {const pathname usePathname()return (navul 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/linksexport 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 h1settings/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 nextexport 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.tsxhref属性传入跳转的对应路由此外还可以以对象方式传入Link组件具体使用 use clientimport { usePathname } from next/navigation
import Link from next/link
type Props {linkList: string[]
}
export function Links({ linkList }: Props) {const pathname usePathname()return (navul 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 clientimport { useRouter } from next/navigationexport default function Page() {const router useRouter()return (button typebutton 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.js layout.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 divMy Post: {params.id}/div
}路由示例网址paramsapp/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 users 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 users 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-htmlconst 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/h2ul 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}/Linkbr /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参数即?usernamexzqage18这种searchParams: {}
}
// 页面(默认导出)根据对应的动态路由渲染页面
export default async function Page({ params }: pageParams) {const postData: any await getPostData(params.id)return (Headtitle{postData.id}/title/Headarticleh1 className{utilStyles.headingXl}{postData?.id}/h1div className{utilStyles.lightText}{postData?.date}/divdiv dangerouslySetInnerHTML{{ __html: postData?.contentHtml }} //articleLink 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.tsx export 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 reactexport default function Error({error,reset,
}: {error: Error { digest?: string }reset: () void
}) {useEffect(() {// Log the error to an error reporting serviceconsole.error(error)}, [error])return (divh2Something went wrong!/h2buttononClick{// 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 clientexport default function GlobalError({error,reset,
}: {error: Error { digest?: string }reset: () void
}) {return (htmlbodyh2Something went wrong!/h2button 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 langenbody{children}{team}{analytics}//body/html)
}提示 children 道具是一个隐式插槽不需要映射到文件夹。这意味着 app/page.js 等效于 app/children/page.js 。
在src/app/team/page.tsx中
export default function Page() {return h1Parallel 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.tsx export default function Page() {return h1Parallel Route Team /h1
}新建default.js export default function Page() {return (h1Parallel Route Team span style{{ color: red }}Default/span/h1)
}新建目录settings在这个目录下新建page.tsx export default function Page() {return h1Parallel Route Team Settings /h1
}在analytics目录下 新建page.tsx export default function Page() {return h1Parallel Route Analytics /h1
}新建default.js export default function Page() {return (h1Parallel Route Analytics span style{{ color: yellow }}Default/span/h1)
}然后更新src/app/page.tsx和src/app/layout.tsx src/app/page.tsx import { Links } from /components/links
import { Metadata } from nextexport 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/navigationexport default function RootLayout({children,team,analytics
}: {children: React.ReactNodeteam: React.ReactNodeanalytics: React.ReactNode
}) {const allSegments useSelectedLayoutSegments()console.log(allSegments, allSegments)return (html langenbody{children}{team}{analytics}Link style{{ position: absolute, marginTop: 100 }} href{/}back index/Link//body/html)
}新建src/app/default.tsx export default function Page() {return (h1App 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/navigationexport 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} classNamefixed z-10 left-0 right-0 top-0 bottom-0 mx-auto bg-black/60 onClick{onClick}divref{wrapper}classNameabsolute 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.tsx import Image from next/image
import { Photo } from ../../lib/photosexport default function Frame({ photo }: { photo: Photo }) {return (Imagealtsrc{photo.imageSrc}height{600}width{600}classNamew-full object-cover aspect-square col-span-2/div classNamebg-white p-4 px-6h3{photo.name}/h3pTaken by {photo.username}/p/div/)
}更新src/app/page.tsx
import Link from next/link
import swagPhotos from ../lib/photos
import Image from next/imageexport default function Home() {const photos swagPhotosreturn (main classNamecontainer mx-autoh1 classNametext-center text-4xl font-bold m-10Parallel routing and route interception achieve Modal/h1div classNamegrid 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} classNamew-full object-cover aspect-square //Link))}/div/main)
}
更新src/app/layout.tsx
use client
import /assets/styles/globals.css
import Link from next/linkimport { useSelectedLayoutSegment, useSelectedLayoutSegments } from next/navigationexport 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 langenbody{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.tsx import Frame from ../../../../components/frame/Frame
import Modal from ../../../../components/modal/Modal
import swagPhotos, { Photo } from ../../../../lib/photosexport default function PhotoModal({ params: { id: photoId } }: { params: { id: string } }) {const photos swagPhotosconst photo: Photo photos.find(p p.id photoId)!return (ModalFrame photo{photo} //Modal)
}新建src/app/photos/[id]/page.tsx该动态路由的作用是当对应的路由被拦截后刷新页面展示到这个路由页面
import Frame from ../../../components/frame/Frame
import swagPhotos, { Photo } from ../../../lib/photosexport default function PhotoPage({ params: { id } }: { params: { id: string } }) {const photo: Photo swagPhotos.find(p p.id id)!return (div classNamecontainer mx-auto my-10div classNamew-1/2 mx-auto border border-gray-700Frame photo{photo} //div/div)
}新建src/app/photos/default.tsx和前面的一样返回null就行
最终的效果