跟着 Nextjs 13+ 的推出,官方推出了一种很新的路由形式,叫 App Router 的,今日来看看这玩意怎么运用吧。

他不同于之前的 pages 路由机制,而是以 app 文件夹为根目录,以 page.tsx(jsx) 为入口,以 layout.tsx (jsx) 为布局组件,参数获取不相同了,同时也引进了不少新的API。

P.S. 新东西层出不穷,真的学不动了啊…

Nextjs第二弹: App Router 的官网解读

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

咱们安装依赖后发动项目:

Nextjs第二弹: App Router 的官网解读

路由

路由结构

咱们修正一下 page.js:

import styles from './page.module.css'
export default function Home() {
  return (
    <main className={styles.main}>
      Home
    </main>
  )
}

Nextjs第二弹: App Router 的官网解读

而 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 文件夹开端往下,遵照目录路由结构:

Nextjs第二弹: App Router 的官网解读

比方我在 app 下新建 dashboard 目录,在下边写上自己的 layout.js 和 page.js 文件们就能够这样拜访:http://localhost:3000/dashboard<Link href="https://juejin.im/dashboard">Dashboard</Link>)。此刻目录结构可能是这个样子的:

Nextjs第二弹: App Router 的官网解读

咱们也能够运用 template.js 来作为中间层,它介于 layout.js 和其孩子组件之间:

// dashboard/template.js
import React from 'react'
export default function template({ children }) {
  return (
    <div>
      接纳来自 page.js 的数据:
      {children}
    </div>
  )
}

然后页面显现:

Nextjs第二弹: App Router 的官网解读

template 可用于页面布局,拆分页面功用等。

路由组

有时候咱们想用一个大的文件夹办理分组文件,可是又不想 nextsjs 将其辨以为路由,那么能够将文件夹用小括号包裹:

Nextjs第二弹: App Router 的官网解读

此刻就能够这样拜访了: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 来拜访:

Nextjs第二弹: App Router 的官网解读

当然了,参数不是只能传递一级参数,咱们假如这样定义文件夹:[...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 ,一个表明登录后的欢迎界面,一个表明没有权限时的登录页面:

Nextjs第二弹: App Router 的官网解读

咱们重写一下 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目录中的段

咱们举例来说明。

Nextjs第二弹: App Router 的官网解读

上面的图片中,photo目录下放置的是路由 photo/id 要展现的图片概况内容,@modal 目录用于将 photo 目录阻拦并包裹在一个模态框中。这儿 (..)会从 @modal 上一级目录导入同级目录中去找 photo 目录来匹配。

此刻,当经过 <Link key={id} href={/photos/${id}}> 来拜访目录时,就会出现主页面上弹出的模态框。

在列表页点击 link 拜访:

Nextjs第二弹: App Router 的官网解读

直接经过 url 拜访:

Nextjs第二弹: App Router 的官网解读

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 检查作用:

Nextjs第二弹: App Router 的官网解读

自定义的 cookie 设置进去了。

路由中间件

在项目目录中放置文件 middleware.ts(或 .js)来定义中间件。放置目录与pagesapp同级,或在内部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、协议和恳求体

Nextjs第二弹: App Router 的官网解读

在需求数据的地方,能够放心的直接运用 fetch 即可,Next.js 会进行兜底的。缓存战略作业流程如下:

  1. 在一个 route 加载时,第一次恳求,不进行缓存
  2. 第二次恳求,假如满足咱们上面说的条件,则射中缓存,这次恳求回来数据就从缓存中取
  3. 一旦 route 重烘托了,缓存则会铲除,那么调用就会建议 API 恳求了。

假如不想缓存这个恳求,能够这样写:

const { signal } = new AbortController()
fetch(url, { signal })

数据的缓存

数据缓存和恳求回忆之间的差异

官方解释:

数据缓存在传入恳求和布置中是持久的,而恳求缓存的生命周期仅继续在恳求过程中。

经过恳求缓存,能够防止重复恳求缓存服务器、CDN等的数量。而经过数据缓存,咱们减少了对原始数据源宣布的恳求数量。

僵尸栽花,具体的差异我也理解不深,为啥要做两层~~

咱们看图:

Nextjs第二弹: App Router 的官网解读

恳求缓存是在数据缓存前边的,恳求缓存没有射中,才会去找数据缓存,还没有射中才会去恳求原始数据源。

咱们能够这样恳求:

fetch('https://...', { next: { revalidate: 3600 } })

表明 3600 秒之内,数据是缓存有用的。3600 秒之后会主动从头验证,若没有改变则仍是用缓存。

咱们还能够禁用缓存:

fetch(`https://...`, { cache: 'no-store' })

小结

咱们来看一下全路由缓存的流程图:

Nextjs第二弹: App Router 的官网解读

  1. 运行时,客户端的路由缓存,比方 Suspense.
  2. 编译时,路由现已确认,恳求的路由假如射中缓存,则直接回来。
  3. 编译时,API 恳求现已确认,恳求的 API 假如射中缓存,则直接回来成果,不必再次建议对数据缓存的恳求。
  4. 编译时,非动态 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 开发一个官网!