跟着 Nextjs 13+ 的推出,官方推出了一种很新的路由形式,叫 App Router 的,今日来看看这玩意怎么运用吧。
他不同于之前的 pages 路由机制,而是以 app 文件夹为根目录,以 page.tsx(jsx) 为入口,以 layout.tsx (jsx) 为布局组件,参数获取不相同了,同时也引进了不少新的API。
P.S. 新东西层出不穷,真的学不动了啊…
Node >= v18.17
今后官方的更新方向全在 next 和 turbopack 了,所以传统的 create-react-app 和 react-router 逐渐被扔掉,假如将来 react-router 官方也不更新了,不知道 vite 将来要怎么支撑 react 的创立?
创立项目
履行指令:
npx create-next-app nextjs-app-router-test
最新的 CLI 会有很多提示选项,咱们是 App router 的讲解,其他无关的选项关掉即可:
✔ Would you like to use TypeScript? … [No] / Yes // 传统 js
✔ Would you like to use ESLint? … [No] / Yes
✔ Would you like to use Tailwind CSS? … [No] / Yes // 不运用 css 结构,就会生成 css module 文件,比方 page.module.css
✔ Would you like to use `src/` directory? … [No] / Yes // 扁平化办理
✔ Would you like to use App Router? (recommended) … No / [Yes] // 敞开 App Router
✔ Would you like to customize the default import alias (@/*)? … No / [Yes]
✔ What import alias would you like configured? … [@]/*
生成后,工程目录主要文件如下:
./app
├── favicon.ico
├── globals.css
├── layout.js
├── page.js
└── page.module.css
./public
├── next.svg
└── vercel.svg
next.config.js
package.json
咱们安装依赖后发动项目:
路由
路由结构
咱们修正一下 page.js:
import styles from './page.module.css'
export default function Home() {
return (
<main className={styles.main}>
Home
</main>
)
}
而 layout.js 组件:
export default function RootLayout({ children }) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
能够看到他是项目的主体结构,他传入的 children 是 page.js 中的内容。
以上就是 App Router 的主体结构了 (app 目录 + page.js + layout.js)。能够对比 pages 路由(pages 目录 + _app.js + _document.js).
此外还有一个 public 目录,它里面的文件是能够被运用 /
直接引证的。
app 文件夹中能够创立子文件夹用于差异子路由,在app顶层的文件就表明跟路由文件。每个项目文件夹单元能够包含的文件如下:
文件名 | 可选的后缀 | 功用 |
---|---|---|
layout |
.js .jsx .tsx
|
布局的顶层组件 |
page |
.js .jsx .tsx
|
页面主体 |
loading |
.js .jsx .tsx
|
页面加载时的 loading |
not-found |
.js .jsx .tsx
|
页面 404 时显现,仅在顶层收效 |
error |
.js .jsx .tsx
|
页面错误时显现,仅在顶层收效 |
global-error |
.js .jsx .tsx
|
大局错误时显现,仅在顶层收效 |
route |
.js .ts
|
API 恳求定制文件 |
template |
.js .jsx .tsx
|
重烘托时定制 layout |
default |
.js .jsx .tsx
|
平行路由回落页面(官方文档还在弥补中。。。) |
假如某一级目录下没有 layout 和 page,则不会被以为是一个正确的路由结构
从 app 文件夹开端往下,遵照目录路由结构:
比方我在 app 下新建 dashboard 目录,在下边写上自己的 layout.js 和 page.js 文件们就能够这样拜访:http://localhost:3000/dashboard
(<Link href="https://juejin.im/dashboard">Dashboard</Link>
)。此刻目录结构可能是这个样子的:
咱们也能够运用 template.js 来作为中间层,它介于 layout.js 和其孩子组件之间:
// dashboard/template.js
import React from 'react'
export default function template({ children }) {
return (
<div>
接纳来自 page.js 的数据:
{children}
</div>
)
}
然后页面显现:
template 可用于页面布局,拆分页面功用等。
路由组
有时候咱们想用一个大的文件夹办理分组文件,可是又不想 nextsjs 将其辨以为路由,那么能够将文件夹用小括号包裹:
此刻就能够这样拜访了:http://localhost:3000/about
。
需注意,有多个路由组时,内部的二级目录,比方 about 不能重名,不然会辨认错误。
当然了,各个路由组顶层也能够有自己的 layout.js ,或许各个二级路由(比方 about 文件夹)下别离放置自己的 layout.js 也能够。
动态路由
仅在服务端组件运用
能够将文件夹称号用中括号括起来表明动态路由:
./app/posts
└── [id]
└── page.js
其间 page.js:
export default function Post({ params }) {
return (
<div>Post: {params.id}</div>
)
}
此刻可经过路由 http://localhost:3000/posts/1
来拜访:
当然了,参数不是只能传递一级参数,咱们假如这样定义文件夹:[...ids]
:
./app/posts
└── [...ids]
└── page.js
此刻拜访:http://localhost:3000/posts/12/56
,此刻在服务端可打印拿到的参数:
{ params: { ids: [ '12', '56' ] }, searchParams: {} }
上面的写法还有一种变体:[[...ids]]
,运用双括号和单括号的功用是相同的,辨认的根本没有差别,仅有的不同是,运用双括号能够辨认不带参数的途径:http://localhost:3000/posts
,而单括号的装备就 404 了。
平行路由
平行路由的文件夹以 @
开头,其默许也不会生成在路由中,不能直接拜访。
比方咱们有两个文件夹:@dashboard
和 @login
,一个表明登录后的欢迎界面,一个表明没有权限时的登录页面:
咱们重写一下 app/layout.js
:
import { getUser } from '@/lib/auth'
export default function Layout({ dashboard, login }) {
const isLoggedIn = getUser()
return isLoggedIn ? dashboard : login
}
getUser 是获取当前用户的,这儿忽略其完成。这样,在根路由下,就能够经过条件语句动态挂载和删去路由组件,当然了,他们也能够同时被烘托在同一个页面上。
路由阻拦
路由阻拦一般针对弹出层,在 nextjs 中,弹出层 modal 也有自己的路由,在弹出层路由下原地改写页面,不该该再出现模态框,而是直接展现本来弹窗里面的概况,这样的一个url展现两种形状的路由,叫做阻拦路由。
路由阻拦,是在路由目录命名前面加上小括号表明:
-
(.)
匹配同一级其他段 -
(..)
匹配上一级的段 -
(..)(..)
匹配上面两级的段 -
(...)
匹配根app
目录中的段
咱们举例来说明。
上面的图片中,photo目录下放置的是路由 photo/id
要展现的图片概况内容,@modal
目录用于将 photo 目录阻拦并包裹在一个模态框中。这儿 (..)
会从 @modal
上一级目录导入同级目录中去找 photo
目录来匹配。
此刻,当经过 <Link key={id} href={/photos/${id}
}> 来拜访目录时,就会出现主页面上弹出的模态框。
在列表页点击 link 拜访:
直接经过 url 拜访:
demo 地址:路由阻拦
路由处理程序 (API路由)
每一套路由,包含根路由,都是能够配有一个 route.js
(ts) 文件用于自定义处理数据和路由回来内容的。咱们看一下如下的路由目录:
./posts
└── [...ids]
├── page.js
└── route.js
在 route.js 中咱们写入:
export async function GET(
request,
params
) {
const ids = params.params.ids;
console.log(ids)
return new Response('Hello, Next.js!', {
status: 200,
headers: { 'Set-Cookie': `token=${ids}` },
})
}
能够看到,他接受了路由参数 ids,并且依据自定义的逻辑回来了页面呼应。页面拜访 http://localhost:3000/posts/111/122
检查作用:
自定义的 cookie 设置进去了。
路由中间件
在项目目录中放置文件 middleware.ts
(或 .js)来定义中间件。放置目录与pages
或app
同级,或在内部src
顶层(假如有 src),nextjs 会主动辨认中间件文件,并在内容缓存和路由切换之前履行该文件。
举一个我在项目中装备 i18n 的例子:
// src/middleware.js
export function middleware(req) {
let lng
if (req.cookies.has(cookieName)) {
lng = acceptLanguage.get(req.cookies.get(cookieName).value)
console.log('Cookie language:', lng)
}
if (!lng) {
lng = acceptLanguage.get(req.headers.get('Accept-Language'))
console.log('Accept-Language:', lng)
}
if (!lng || !languages.includes(lng)) lng = fallbackLng
// Redirect if lng in path is not supported
if (
!languages.some(loc => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
!req.nextUrl.pathname.startsWith('/_next')
) {
return NextResponse.redirect(new URL(`/${lng}${req.nextUrl.pathname}`, req.url))
}
return NextResponse.next()
}
上面的代码,阻拦恳求 req,判别恳求的 cookie 里有没有言语的装备,假如没有就运用 Accept-Language 获取浏览器引荐的言语,假如还没有就运用回落兜底方案;最后运用 NextResponse.redirect
重定向到对应的言语途径。
数据获取方法
Next.js 也能够恳求第三方的服务来获取数据。
服务端
服务端组件中,官方引荐运用 fetch:
// page.js
async function getData() {
const res = await fetch('https://api.example.com/...')
// The return value is *not* serialized
// You can return Date, Map, Set, etc.
if (!res.ok) {
// This will activate the closest `error.js` Error Boundary
throw new Error('Failed to fetch data')
}
return res.json()
}
export default async function Page() {
const data = await getData()
console.log(data)
return <main></main>
}
此外,fetch 能够运用 { cache: 'force-cache' }
来缓存数据。
客户端
要声明一个组件是客户端组件,需求这样写:
'use client'
export default function ClientComponent({ updateItem }) {
return <form action={updateItem}>{/* ... */}</form>
}
客户端组件就跟平常的 react 开发相同,能够运用 React 的各种状况 Hook 和 第三方东西(比方 axios)来处理数据。
客户端咱们还能够恳求前面的 API 路由来获取数据,API 路由中能够是服务端调用第三方接口恳求的数据。
服务端烘托与客户端烘托
服务端战略
- 默许为静态烘托
路由和服务端数据在 build 时同步获取,成果会存在服务端,支撑缓存 CDN 中,此刻就是规范的后端直出的静态页面。
- 动态烘托
路由在每次拜访时动态获取页面数据,适用于一些需求展现实时数据的场景(比方 cookies、url params等)。
假如页面中有动态函数或许未被缓存的数据,则主动转换为动态页面烘托:
Dynamic Functions | Data | Route |
---|---|---|
No | Cached | Statically Rendered |
Yes | Cached | Dynamically Rendered |
No | Not Cached | Dynamically Rendered |
Yes | Not Cached | Dynamically Rendered |
关于动态函数,指的是只要在恳求后才知道回来成果的函数,动态函数有必要是内置的,因为 fetch 会被缓存:
import { cookies } from 'next/headers'
import { headers } from 'next/headers'
- 流式加载
渐进式的 UI 烘托战略,能够经过 loading.js 文件,或许运用 React.Suspense
来敞开。默许情况下,流式加载内置于 Next.js App Router 中,助于进步初始页面加载功能,比方加载哪些 依赖于较慢数据获取(会阻挠烘托整个路由)的 UI(产品页面上的谈论等):
// page.js
import { Suspense } from 'react'
import { PostFeed, Weather } from './Components'
export default function Posts() {
return (
<section>
<Suspense fallback={<p>Loading feed...</p>}>
<PostFeed />
</Suspense>
<Suspense fallback={<p>Loading weather...</p>}>
<Weather />
</Suspense>
</section>
)
}
客户端战略
客户端组件在 build 时不参加获取数据和预烘托,他在客户浏览器运行时才开端履行并烘托数据。客户端组件能够运用状况、作用和事情侦听器,往往用于交互相对杂乱的场景。
在需求客户端烘托的代码片段顶层写上:"use client"
便能够声明客户端组件:
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
)
}
Next.js 烘托的战略是:在用户客户端拉取服务端组件代码和客户端组件代码,先烘托出无交互的页面信息,完成初始页面加载,之后运用客户端水合技术,更新 DOM 树,植入交互信息和事情监听。
关于缓存
恳求缓存
为了不必每次都调用接口获取重复的数据,Next.js 扩展了 fetch API 去主动缓存恳求数据,缓存射中战略是:相同的 url、协议和恳求体
。
在需求数据的地方,能够放心的直接运用 fetch 即可,Next.js 会进行兜底的。缓存战略作业流程如下:
- 在一个 route 加载时,第一次恳求,不进行缓存
- 第二次恳求,假如满足咱们上面说的条件,则射中缓存,这次恳求回来数据就从缓存中取
- 一旦 route 重烘托了,缓存则会铲除,那么调用就会建议 API 恳求了。
假如不想缓存这个恳求,能够这样写:
const { signal } = new AbortController()
fetch(url, { signal })
数据的缓存
数据缓存和恳求回忆之间的差异
官方解释:
数据缓存在传入恳求和布置中是持久的,而恳求缓存的生命周期仅继续在恳求过程中。
经过恳求缓存,能够防止重复恳求缓存服务器、CDN等的数量。而经过数据缓存,咱们减少了对原始数据源宣布的恳求数量。
僵尸栽花,具体的差异我也理解不深,为啥要做两层~~
咱们看图:
恳求缓存是在数据缓存前边的,恳求缓存没有射中,才会去找数据缓存,还没有射中才会去恳求原始数据源。
咱们能够这样恳求:
fetch('https://...', { next: { revalidate: 3600 } })
表明 3600 秒之内,数据是缓存有用的。3600 秒之后会主动从头验证,若没有改变则仍是用缓存。
咱们还能够禁用缓存:
fetch(`https://...`, { cache: 'no-store' })
小结
咱们来看一下全路由缓存的流程图:
- 运行时,客户端的路由缓存,比方 Suspense.
- 编译时,路由现已确认,恳求的路由假如射中缓存,则直接回来。
- 编译时,API 恳求现已确认,恳求的 API 假如射中缓存,则直接回来成果,不必再次建议对数据缓存的恳求。
- 编译时,非动态 API 回来的成果现已确认,假如恳求相同的数据,且缓存不过期,则运用缓存成果,不向元数据恳求。
关于款式
能够运用 css 模块:
import styles from './styles.module.css'
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return <section className={styles.dashboard}>{children}</section>
}
也能够运用 CSS-in-JS,比方 emotion 等,当然一些预处理器,比方 sass、less 也能够运用,不过要在装备里添加装备项:
const path = require('path')
module.exports = {
sassOptions: {
includePaths: [path.join(__dirname, 'styles')],
},
}
当然,关于企业级大型应用,仍是引荐运用 Tailwindcss,下一期讲实战会讲解。
SEO 装备
meta
在各个 page.js 文件中,都能够装备当前页面对应的 meta 信息:
import { Metadata } from 'next'
// 运用 静态 meta
export const metadata: Metadata = {
title: '...',
}
// 运用 动态 meta
export async function generateMetadata({ params }) {
return {
title: '...',
}
}
export default function Page() { return '...'}
当然你也能够直接在 layout 中写入。
sitemap
引荐安装 next-sitemap
库,运用时只需求在 package.json 中写入指令即可:"postbuild": "next-sitemap"
,这样在打包后就会在 public 文件夹生成 robots.txt 和 sitemap.xml 文件。当然了,假如你想自定义装备,可在根目录创立一个装备文件 next-sitemap.config
:
/** @type {import('next-sitemap').IConfig} */
module.exports = {
siteUrl: process.env.SITE_URL || "https://xxx.com",
generateIndexSitemap: false,
generateRobotsTxt: true,
robotsTxtOptions: {
policies: [{ userAgent: '*', allow: '/' }],
},
};
附录:Next.js 13+ 新功用一览
React 服务器组件 (RSC)
RSCs 答应在客户端和服务器上采用更精细的烘托方法。React 答应开发者选择组件是在服务器上仍是在客户端烘托,而不是在用户恳求时被逼决定在客户端仍是服务器上烘托整个页面。这能够让你在搜索引擎成果页面中获得巨大优势。
Streaming UI
流式 UI(Streaming UI)代码块是一个新式的规划形式,称为岛屿架构(the island architecture)
,旨在初次加载时尽量向客户端发送最少的代码。
其完成目标是:向客户端发送一个无需 JavaScript 的彻底烘托的页面,然后再发送剩余的内容。
当 Next.js 在服务器端烘托页面时,一般会将页面的一切 JavaScript 捆绑并与之一同发送。而流式 UI 代码块的引进消除了这种需求,答应向客户端发送一个非常小的静态页面,明显改善了诸如初次内容呈现时间和整体页面速度等指标。
流式 UI 的步骤大致如下:
- 用户建议初始恳求
- 烘托并发送根本的 HTML 页面给客户端
- 服务器预备 JavaScript 捆绑文件
- 在客户端浏览器中显现需求 JavaScript 的页面部分
- 仅将该组件所需的 JavaScript 捆绑文件发送给客户端
App Router
app 目录下的一切东西都是预先装备好的,以答应 RSCs 和流式 UI 的出现。你只需求创立一个loading.js组件,它将彻底包住页面组件和 suspense 边界内的任何子节点。
升级的 Next Image 组件
运用了浏览器原生支撑的懒加载战略,添加了一些对 SEO 有很大协助的改善:
- 默许需求 alt 标签。
- 更好的验证,以确认涉及无效属性错误。
- 由于有了一个更像 HTML 的界面,更简单进行款式规划。
Font 组件
网页功能中,CLS 是一个重要的指标(CLS 是 Cumulative Layout Shift 的缩写,衡量在网页的整个生命周期内产生的一切意外布局偏移的得分总和。),依据你所运用的结构(比方 Gatsby),让字体有用地预加载可能是很扎手的。一段时间以来,向谷歌等字体库宣布外部恳求是一个防止不了的行为,在许多 SPA 应用程序中造成了一个难以办理的瓶颈(页面文字会闪烁一下)。
Next Font Component 旨在解决这个问题,它在构建时获取一切的外部字体,并从你自己的域中自我保管它们。字体也被主动优化,并且经过主动运用 CSSsize-adjust(巨细调整) 属性完成了零累积的布局搬运。
比方这样运用 google 字体:
import { Roboto } from 'next/font/google'
const roboto = Roboto({ weight: '400', subsets: ['latin'], display: 'swap',})
...
<html lang="en" className={roboto.className}> <body>{children}</body> </html>
关于新功用 App router 就讲这么多啦,欢迎大家一同讨论沟通,一同学习。
下一讲会讲一下实战技术,怎么运用 Next.js 14 + App router + Tailwindcss 从 0 开发一个官网!