nextjs@15
# install
npx create-next-app@latestpages 架构
// https://nextjs.org/docs/app/api-reference/file-conventions/route
import { cookies } from 'next/headers'
export async function GET(request: Request, { params }: { params: Promise<{ team: string }> }) {
const { team } = await params
const data = {
hello: "world",
};
const res = Response.json(data);
res.headers.set('x-token', 'abc')
return res
}
export async function POST(params:type) {
const cookieStore = await cookies()
const a = cookieStore.get('a')
const b = cookieStore.set('b', '1')
const c = cookieStore.delete('c')
// ...
}
Example URL params
app/dashboard/[team]/route.js /dashboard/1 Promise<{ team: '1' }>
app/shop/[tag]/[item]/route.js /shop/1/2 Promise<{ tag: '1', item: '2' }>
app/blog/[...slug]/route.js /blog/1/2 Promise<{ slug: ['1', '2'] }>// 可以在 dashboard 前面再加个 (admin) 目录,此时访问 url 不变,但是从文件夹结构上会更好区分路由模块
import { Metadata } from 'next/types';
export default function page(params:type) {
return (
<div>my page</div>
)
}
export const metadata: Metadata = {
title: "首页",
description: "描述信息",
}// 就相当于 route-view
import "./globals.css";
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<h1 className="text-indigo-200 text-center py-3 px-2 leading-8 bg-indigo-700 font-bold">这是 app 的layout</h1>
{children}
</body>
</html>
);
}'use client'
export default function MyError() {
return <div>My Error Page</div>
}页面缓存
export default async function Page() {
const data = await fetch('https://...', { next: { revalidate: 3600 } })
}import { unstable_cache } from 'next/cache'
import { getUserById } from '@/app/lib/data'
export default async function Page({ params }: { params: Promise<{ userId: string }> }) {
const { userId } = await params
const getCachedUser = unstable_cache(
async () => {
return getUserById(userId)
},
[userId] // add the user ID to the cache key
)
}export async function getUserById(id: string) {
const data = await fetch(`https://...`, {
next: {
tags: ['user'],
},
})
}
// app/lib/actions.ts 调用 updateUser,将刷新所有标记为 'user' 的 tag 请求
import { revalidateTag } from 'next/cache'
export async function updateUser(id: string) {
// Mutate data
revalidateTag('user')
}import { revalidatePath } from 'next/cache'
export async function updateUser(id: string) {
// Mutate data
revalidatePath('/profile')linking-and-navigating
nextjs 中,默认是 React Server Components,在后续渲染中,包括有两种渲染方式:静态渲染(结果已经被缓存或者 revalidation 结果一致)、动态渲染(客户端请求)
预取
page 中使用到了
Link组件(a 标签不行),那么会自动的在后台加载对应的资源,当用户点击的时候能更快的拿到响应结果如果是静态路由,那么就是完整预取;动态路由则取决于有没有 loading.js(存在则部份预取)
Streaming
允许先返回部份已经准备好的动态路由,而不是一直等待完整的页面;开启该功能应该创建
loading.tsx文件优点:减少可视等待时间;页面其余部份是可操作的;提升页面性能指标:TTFB、FCP、TTI.
Client-side transitions
传统页面通过 a 链接跳转会触发整个页面的重新加载,nextjs 使用
Link组件避免这种情况优点:客户端过渡使得服务端渲染带来类似客户端(SPA)渲染的效果 其他:
<Link prefetch={false} href="/blog">blog</Link>禁用 prefetchLink 可以监听 hover,动态修改 prefetch 属性,来达到预加载
Image
- 默认懒加载(可视区)
- 防止偏移(设置宽高(在线 url),本地图片(import jpg from './xx.jpg')无需设置,会自动读取并设置)
loading.js
如果 server component 引入 client component(使用到了某些 client 才允许的 api),且没有将该组件置于 <Suspense> 中,此时 server component 没有同级的 loading.js,那么就会构建错误,如果有,就会先显示 loading.js,然后再客户端再执行完整的渲染
Fonts
构建之后,fonts 将作为静态资源存在,不会再请求 google
import { Geist } from 'next/font/google'
const geist = Geist({
subsets: ['latin'],
})
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
)
}import localFont from 'next/font/local'
const myFont = localFont({
src: './my-font.woff2',
})
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en" className={myFont.className}>
<body>{children}</body>
</html>
)
}css
locallycss 文件以 module.css 结尾; 如 button.module.cssglobalapp/global.css
When to use Server and Client Components?
client components
- 需要状态维护(
useState)、事件处理(onClick,onChange) - 生命周期(
useEffect) - 浏览器专属 api(
localStorage,window,Navigator.geolocation) - 自定义 hook
- 需要状态维护(
Server Components
- 从数据库或者 api 获取数据
- tokens 等不想暴露在客户端的数据
- 减少发送到浏览器的 JavaScript 数量
- 提升客户端体验、浏览器页面评分
渲染流程
- 服务端
Server Components 在服务器被渲染成特殊的数据结构:React Server Component Payload (RSC Payload),包含渲染结果、渲染位置、props
客户端
- 快速渲染 无交互 HTML
- 使用 RSC Payload 调和 components tree
- 水合(激活交互事件)
"use client"
服务组件跟客户端组件的边界标识,如果一个文件使用了该标识,那么它本身以及它 import 的全部组件都会被视为 client component,子组件也不再需要都加上该标识
TIP
需要交互的就标识为 client component,其余使用 server component
数据传递
import LikeButton from '@/app/ui/like-button'
import { getPost } from '@/lib/data'
export default async function Page({ params }: { params: { id: string } }) {
const post = await getPost(params.id)
return <LikeButton likes={post.likes} />
}'use client'
export default function LikeButton({ likes }: { likes: number }) {
// ...
}使用 use, 服务端通过 props 传递 promise,交由客户端获取
import Posts from '@/app/ui/posts
import { Suspense } from 'react'
export default function Page() {
// Don't await the data fetching function
const posts = getPosts()
return (
<Suspense fallback={<div>Loading...</div>}>
<Posts posts={posts} />
</Suspense>
)
}'use client'
import { use } from 'react'
export default function Posts({
posts,
}: {
posts: Promise<{ id: string; title: string }[]>
}) {
const allPosts = use(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}react context
server component 不支持直接使用 react context,需要按下面的方式进行使用
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({
children,
}: {
children: React.ReactNode
}) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}如何使用第三方组件
如果第三方组件中使用 client-only 功能,将不能直接在 server component import 使用
- 在一个 'use client' 的组件中导入使用(其子组件被默认为都是 client)
- 文件中转,在 'use client' 文件下中 import 再 export 下
隐私变量
process.env.xxx, 其中 xxx 必须以 NEXT_PUBLIC_ 开头才能够保留,否则在客户端中会被替换成空字符串,也不会正常运行
fetching Data
export default async function Page() {
const data = await fetch('https://api.vercel.app/blog')
const posts = await data.json()
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}import { db, posts } from '@/lib/db'
export default async function Page() {
const allPosts = await db.select().from(posts)
return (
<ul>
{allPosts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}Client Components 获取数据方式
use(上面已经有示例 -> 300 行)A community library like SWR or React Query
// swr 示例
'use client'
import useSWR from 'swr'
const fetcher = (url) => fetch(url).then((r) => r.json())
export default function BlogPage() {
const { data, error, isLoading } = useSWR(
'https://api.vercel.app/blog',
fetcher
)
if (isLoading) return <div>Loading...</div>
if (error) return <div>Error: {error.message}</div>
return (
<ul>
{data.map((post: { id: string; title: string }) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}Streaming
Server Components 使用 async/await 的时候,会触发动态渲染,即等待请求结束结果才返回,耗时较长;
如何开启 Streaming
在需要开启的 page.tsx 同级目录下增加 loading.tsx file (一旦 page 渲染完成,新的内容会立即替换 loading )
使用
Suspense(Any content wrapped in a<Suspense>boundary will be streamed)
渲染顺序
按序:使用
await阻塞了接下来的渲染并发:promise.all (请求多个无依赖关系接口的场景)
preload:可以在 await api() 前面运行 preloadApi(),该函数执行的 fetch 会被缓存,所以等实际组件发起 fetch 的时候能更快的拿到接口返回
缓存跟重校验数据
开发阶段不会启用,生产环境生效
命中缓存的接口将不会再次发起网络请求,revalidation 能够允许更新缓存而不用重新构建
// fetch 默认不会缓存,可以修改 cache 参数(但在 nextjs 中会预先渲染并缓存 html,可以使用 connection api 使得每次渲染都是动态的)
export default async function Page() {
const data = await fetch('https://...', { cache: 'force-cache',next: { revalidate: 3600 } })
}// unstable_cache 允许你缓存 promise 函数结果
import { unstable_cache } from 'next/cache'
import { getUserById } from '@/app/lib/data'
export default async function Page({ params }: { params: Promise<{ userId: string }> }) {
const { userId } = await params
const getCachedUser = unstable_cache(
async () => {
return getUserById(userId)
},
[userId], // 根据不同的 userId 来判断是否读取缓存
{
tags: ['user'],
revalidate: 3600,
}
)
}// 给缓存添加 tag 标识,比如说登录用户切换了,可以一键刷新全部依赖此 tag 的缓存
export async function getUserById(id: string) {
const data = await fetch(`https://...`, {
next: {
tags: ['user'],
},
})
}
// 刷新
import { revalidateTag } from 'next/cache'
export async function updateUser(id: string) {
// Mutate data
revalidateTag('user')
}import { revalidatePath } from 'next/cache'
export async function updateUser(id: string) {
// Mutate data
revalidatePath('/profile')Server Functions
通过在异步函数上或者文件(该文件所以的导出函数都被认为是)顶部标记 use server,声明这是服务器函数
client component 文件内不能使用 use server,但是可以引入并使用 Server Function
export async function createPost(formData: FormData) {
'use server'
const title = formData.get('title')
const content = formData.get('content')
// Update data
// Revalidate cache
}
export async function deletePost(formData: FormData) {
'use server'
const id = formData.get('id')
// Update data
// Revalidate cache
}如何使用
<form action={createPost}>- Event Handlers:
onClick
搭配
// loading 效果
'use client'
import { useActionState, startTransition } from 'react'
import { createPost } from '@/app/actions'
import { LoadingSpinner } from '@/app/ui/loading-spinner'
export function Button() {
const [state, action, pending] = useActionState(createPost, false)
return (
<button onClick={() => startTransition(action)}>
{pending ? <LoadingSpinner /> : 'Create Post'}
</button>
)
}// 更新缓存
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
'use server'
// Update data
// ...
revalidatePath('/posts')
}// 数据更新后,跳转到新的页面
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
// Update data
// ...
redirect('/posts')
}provider
共享全局数据
'use client'
import { createContext } from 'react'
export const ThemeContext = createContext({})
export default function ThemeProvider({ children }: { children: React.ReactNode }) {
return <ThemeContext.Provider value="dark">{children}</ThemeContext.Provider>
}import ThemeProvider from './theme-provider'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html>
<body>
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
)
}服务端 api(Route Handlers)
创建 route.ts,位置: src/api/{server}/route.ts
跟 server action 比较,前者只适用于简单的表单交互:数据提交、获取,无法修改响应头等完整的 http 配置
import { NextResponse } from 'next/server'
export async function GET() {
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const messages = ['Hello', 'World', 'Streaming', 'Example']
for (const message of messages) {
controller.enqueue(encoder.encode(`${message}\n`))
await new Promise((resolve) => setTimeout(resolve, 1000)) // 模拟延迟
}
controller.close()
},
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Transfer-Encoding': 'chunked',
},
})
}
export async function POST() {
return NextResponse.json({ name: 'rick' })
}