# Next.js
# 安装
node 18.17 以后
npm install next@latest react@latest react-dom@latest
npx create-next-app@latest
# 编写组件
export default ({children})=><button>{children}</button>
import Button from './components/button.js'
<button>这是一个按钮</button>
# 路由跳转
import Link form 'next/link'
import React from 'react'
import Link from 'next/link'
export default () => (
<>
<div>KaiA page</div>
<Link href="/">
<a>返回首页</a>
</Link>
</>
)
# 路由
# /app 应用式路由
目录 src/app/ =>
layout.js 共享UI
page.js 当前页面
loading 加载时UI
not-found 未找到page.js的UI
error 错误的UI
global-error 全局错误的UI
route 服务器端API端点
template 专门重新渲染布局UI
default 并行路由的后备UI ???
# 目录结构
<Layout>
<Template>
<ErrorBoundary fallback={<Error />}>
<Suspense fallback={<Loading />}>
<ErrorBoundary fallback={<Error />}>
<Page />
</ErrorBoundary>
</Suspense>
</ErrorBoundary>
</Template>
</Layout>
只要 目录下有 page.js 就能访问到
比如 /app/product/page.js => '':3000/product'
# 链接和导航
# Link组件
import Link from 'next/link'
<Link href="/info">go info</Link>
import Link form 'next/link'
export default function PostList({posts}){
return(
<div>
{posts.map((post)=>{
<div key={post.id}>
<Link href={`/blog/${post.slug}`} >
{post.title}
</Link>
<div/>
})
}
<div/>
)
}
# usePathname() 获取当前激活路由
'use client'
import { usePathname } from "next/navigation"
import Link from 'next/link'
export default function Nav({ children }) {
const pathname = usePathname()
const lightNavBar = {
color: 'blue',
padding: '8px'
}
const normalNavBar = {
padding: '8px'
}
return (
<div style={{ display: 'flex', justifyContent: 'space-around' }}>
<Link style={pathname === '/' ? lightNavBar : normalNavBar} href="/" >index</Link>
<Link style={pathname === '/info' ? lightNavBar : normalNavBar} href="/info" > info</Link >
</div >
)
}
# 滚动到id
<Link href={'/info#' + title.titleName} scroll={true} >{title.titleName}</Link>
// href # 后面添加id
// 通过scroll=false禁用
# useRouter()钩子
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
# redirect功能
import { redirect } from 'next/navigation'
redirect('/login')
# 使用本机历史API
# window.history.pushState
window.history.pushState(null, '', `?${params.toString()}`)
// 使用 pushState 方法更新浏览器历史记录,以反映新的查询参数。这样做可以在不刷新页面的情况下改变 URL,实现页面状态的更新。
# window.history.replaceState
window.history.replaceState(null, '', newPath)
使用 replaceState
// 方法更新浏览器历史记录,以反映新的页面路径。这样做可以在不刷新页面的情况下改变 URL,实现页面语言的切换。
# 路由和导航工作原理
路由和导航的工作原理 App Router 使用混合方法进行路由和导航。在服务器上,您的应用程序代码会自动按路由段进行代码分割。在客户端,Next.js预取并缓存路由段。这意味着,当用户导航到新路线时,浏览器不会重新加载页面,而只会重新渲染发生更改的路线段,从而改善导航体验和性能。 可不可以理解为 我从 info/works/apple 跳转到 info/works/banana 页面 ,如果 他们是 共同 的 info/works 组件 或者 布局 ,就只刷新 apple到 banana 而公用的部分不刷新 从而提升性能
ChatGPT
ChatGPT
没错,你的理解是正确的。当用户从一个页面跳转到另一个页面,如果这两个页面共享相同的组件或布局,Next.js 会自动处理页面之间的差异,只重新渲染发生更改的部分,而不是重新加载整个页面。这种行为称为客户端导航,它提高了导航体验并提升了性能,因为只有发生变化的部分才会重新渲染,而公用的部分则会被缓存,避免了不必要的重复渲染和加载。
# 1.代码分割
将应用程序代码分割成更小的包 从而减少了每个请求和传输数据量和执行时间,从而提高了性能
服务器组件 自动把代码根据根据路由分割 仅加载当前路由的代码
# 2.预取
预取只发生在生产环境 开发中不启用
在用户访问路由之前 后台预先加载路由
组件** 组件预取:** 当使用 `` 组件创建链接时,Next.js 会自动进行预取。这意味着当用户视口中可见该链接时,Next.js 会自动预取该链接指向的页面。预取发生在页面首次加载时或用户滚动到链接处时。默认情况下,只有在路由树中共享布局的页面才会被预取并缓存,缓存时间为 30 秒。这样做是为了降低获取整个动态路由页面的成本,同时提供即时加载状态,给用户更好的视觉反馈。
link 就是 我在当前 屏幕上 看到 比如 info 链接 , 这个info链接 显示在 屏幕上 ,就以及发生预取了?
ChatGPT
ChatGPT
是的,你理解得很对。在 Next.js 中,当用户在当前屏幕上看到一个由 `` 组件创建的链接时,该链接就会发生预取。具体来说,当该链接进入用户的视口(即用户能够看到链接所在的位置)时,Next.js 会自动预取该链接指向的页面内容。这样做的目的是为了在用户导航到该页面时能够提前加载页面内容,从而加快页面的加载速度,提高用户体验。
您
是不是可以理解为 游戏里的 预判
ChatGPT
ChatGPT
是的,可以类比为游戏中的预判。在游戏中,预判通常指的是玩家根据当前的游戏状态和对手的动作,提前预测下一步可能发生的情况,并做出相应的决策。类似地,在 Next.js 中,预取路由也是一种预判的行为,系统会根据用户当前的浏览状态(例如,当前屏幕上显示的内容)预先加载可能被访问的页面内容,以提高用户体验。这样做可以让用户在导航到新页面时,能够更快地加载页面内容,从而提高整体的响应速度和流畅度,就像游戏中的预判可以让玩家更快地做出反应一样。
import Link from 'next/link';
// 禁用预取行为
<Link href="/about" prefetch={false}>
<a>About</a>
</Link>
// 启用预取超出视口加载边界的整个页面数据
<Link href="/contact" prefetch={true}>
<a>Contact</a>
</Link>
在 Next.js 中,`` 组件的预取行为是在路由改变时发生的,而不一定需要页面视口中的链接可见。具体来说,预取会在以下情况下触发:
- 路由改变: 当用户点击 `` 组件导航到另一个页面时,路由会发生改变。
- 页面视口中的链接可见时: 在默认情况下,如果页面视口中的链接(即用户可见的链接)具有
prefetch
属性,并且链接指向的页面尚未被预取,则该链接的页面内容会在用户点击之后被预取。
这意味着,虽然页面视口中的链接可见时会发生预取,但实际上只要用户点击了 `` 组件,不管链接是否在页面视口中可见,都会触发预取行为。因此,即使链接不在页面视口中可见,只要用户点击了该链接,指向的页面内容就会被预取。
# 3.缓存
Router Cache 内存客户端缓存
用户在导航时,已经访问过的页面和 预取的页面 会存储进去
如果这个页面被加载过,存储在Cache里 ,再次进这个页面的时候 就可以直接从缓存里加载,省去了请求服务器的过程 ,提高了用户体验,页面加载更快
# 4.部分渲染
部分渲染是指在用户进行页面导航时,只重新渲染导航变化的部分,同时保留所有共享的部分。
提升导航效率和速度,节省服务器和客户端资源,提高用户体验
# 5.软导航
Next.js App Router 支持页面之间的“软导航”,确保仅重新渲染已更改的部分,使得客户端React状态能够在导航期间保留。
# /pages 页面路由
/pages 文件夹
例: /pages/info.js 直接访问/pages/info
//实践 同时 存在 /app 和 /pages 未成功打开/pages下文件
# 加载UI和流式传输
React Suspense 创建有意义的加载UI
loading.js
# 即时加载状态
export default function Loading(){
return <LoadingSkeleton />
}
// 在同一个文件夹中 loading.js 将嵌套在layout.js 它会自动将page.js文件和下面的所有子文件包裹在<Suspense>边界中
导航是即时的
导航是可中断的 更改路线不需要等待路线内容完全加载后再导航到另一条路线
加载新路线段时,共享布局保持交互状态。
# 充满悬念的直播
除了 之外loading.js
,您还可以为自己的 UI 组件手动创建 Suspense Boundaries。 App Router 支持使用Suspense进行流式传输 (opens new window)对于Node.js 和 Edge 运行时 (opens new window)。
# 什么是流媒体
使用SSR 在用户可以查看页面并与页面交互之前需要完成一系列步骤
1 在服务器上获取相应页面所有数据
2 在服务器上渲染该页面的HTML
3 加载HTML,CSS,JS代码到客户端
4 使用HTML,CSS代码显示非交互式界面
5 react 、js使得界面具有交互性
这些步骤是连续的和阻塞的,这意味着服务器只能在获取所有数据后才能呈现页面的 HTML。而且,在客户端,只有下载了页面中所有组件的代码后,React 才能对 UI 进行水合。
结合 React 和 Next.js 的 SSR 通过尽快向用户显示非交互式页面来帮助提高感知加载性能。
但是,它仍然可能很慢,因为在向用户显示页面之前需要完成服务器上的所有数据获取。
流式传输允许您将页面的 HTML 分解为更小的块,并逐步将这些块从服务器发送到客户端。
一个一个组件传输
A = =》data => html,css => js => 可交互
B = = 》data => html,css => js => 可交互
C = = 》data => html,css => js => 可交互
# SEO搜索引擎优化
- Next.js 将等待内部数据获取
generateMetadata
(opens new window)完成,然后再将 UI 流式传输到客户端。这保证了流式响应的第一部分包含``标签。 - 由于流媒体是服务器渲染的,因此不会影响 SEO
# 错误处理
error.js处理嵌套路由的错误
根布局的错误
app\global-error.js
如果 /info 下 没有 error.js文件 会去上一级找error.js,但是 会从整个layout都替换为error
# 嵌套路由
# not-found.js
import { notFound } from 'next/navigation';
export default function info() {
notFound()
}
// 层级
<Layout>
<Template>
<Error>
<Loading>
<Page />
</Loading>
</Error>
</Template>
</Layout>
# useRouter()
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
# NextResponse.redirect
// 身份验证未通过时 重定向到登录
export function middleware(request: NextRequest) {
const isAuthenticated = authenticate(request)
// If the user is authenticated, continue as normal
if (isAuthenticated) {
return NextResponse.next()
}
// Redirect to login page if not authenticated
return NextResponse.redirect(new URL('/login', request.url))
}
# 路线组(route)
/info 如果根目录有layout.js 则内部其他页面公用布局,如果不想要公用布局 ,则删除 根目录的layout.js,并在/info/apple每个里面建立layout.js
/info/apple
/info/banana
# (filename)
用括号(filename)的方式建立文件夹=》
app/(pages)/apple/page.js 访问==> ‘/apple’
# 项目组织
app/button/page.js
app/info/route.js
通过这两种文件名(page.js | route.js)可以访问到页面
其他都不行
app/shop/apple.js X
app/shop/index.js X
# 私人文件夹
文件夹名称前添加下划线:_foldername
# 路线组
(foldername)/info/page.js ==> ''/info'
# src目录
src/app/page.js ==> ‘/’
# 模块路径别名
./../../../components/button ==> @/components/button
# 动态路线
通过 文件夹名称 外添加中括号
[foldername] [id] [slug]
动态段作为prop传递params给layout,page,route,generateMetadata
//例如,博客可以包含以下路线,app/blog/[slug]/page.js其中[slug]是博客帖子的动态细分。
export default function Page({ params }: { params: { slug: string } }) {
return <div>My Post: {params.slug}</div>
}
# 生成静态参数
export async function generateStaticParams() {
const posts = await fetch('https://.../posts').then((res) => res.json())
return posts.map((post) => ({
slug: post.slug,
}))
}
# [...slug]添加省略号
app/shop/[...slug]/page.js
将匹配/shop/clothes
,但也匹配/shop/clothes/tops
, /shop/clothes/tops/t-shirts
,等等。
# [[...slug]] 省去第一个page.js
shop/[[..product]]/page.js 不需要 shop/page.js
shop/[..product]/page.js 需要 shop/page.js
# 并行路线
@folder
app/@head/page.js
app/@hand/page.js
app/layout.js
export default function lay({ children, hand, head }) {
return (
<>
{head}
{children}
{hand}
</>
)
}
@folder 不是路由段
# default.js
作为初始加载或整页重新加载期间 不匹配插槽的后备
# 拦截路由
当在一个页面 跳转到另外一个页面之前
图片列表 =》点击图片 拦截 实际不跳转,显示大图
但是正常访问图片url 正常显示
file/(.)photo 同级
file/(..)photo 上一级
# 路由处理程序
app/api/route.js => '/api'
export const dynamic = 'force-dynamic' // defaults to auto
export async function GET(request: Request) {}
export async function GET() {
const res = await fetch('https://data.mongodb-api.com/...', {
headers: {
'Content-Type': 'application/json',
'API-Key': process.env.DATA_API_KEY,
},
})
const data = await res.json()
return Response.json({ data })
}
具体来说,当使用 GET
方法处理路由请求时,如果没有显式地禁用缓存,那么在处理程序返回的响应被缓存后,对相同路由的后续请求会直接返回缓存的响应,而不会再次触发路由处理程序。这样可以提高响应速度和减轻服务器负载,因为不需要每次都重新执行路由处理程序。
但是,有时候你可能需要在特定情况下禁用缓存,例如当处理程序的响应需要动态生成或根据请求参数进行变化时。在这种情况下,你可以采取一些措施来选择退出缓存,例如使用其他 HTTP 方法、使用动态函数、或手动指定动态模式等。这样可以确保每次请求都会触发路由处理程序,从而获取最新的响应数据。
# 最佳实践
# css
# 使用import 会更快
因为import有浏览器缓存,而styled是react注入内联样式
√ import './index.css'
x import styled from '...'
# next15速成 (opens new window)
npx create-next-app
# react
# 服务器组件
仅在服务器上呈现,永远不会发送到客户端。
无法使用useState useEffect
# 客户端组件
next-15默认是 服务器组件
使用了useState 或者useEffect的组件会报错
"use Client" // 在文件顶部添加"use client" 转为客户端组件
尽可能多的使用服务端组件 需要交互情况才使用客户端组件
用服务端组件获取数据 访问后端资源 敏感信息保留在服务端
客户端进行交互和事件监听
# 路由
文件夹路由
文件命名 /[path]/page.ts
# 获取动态路由参数 /path/[id]
import React from 'react'
export default async function Product({ params }: { params: { id: string } }) {
const { id } = await params
return (
<div>product {id}</div>
)
}
# 逻辑分组/(auth)/login
/(auth)/login
/(auth)/register
直接访问 /login /register
# 布局layout
# Link组件
<Link href='/login'/>
# 接口route.ts
// src\app\users\route.ts
import { NextResponse } from "next/server";
export const users = [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
{ id: 3, name: "John Smith" },
{ id: 4, name: "Jane Smith" },
];
export async function GET() {
return NextResponse.json(users);
}
export async function POST(request: Request) {
const { name } = await request.json();
const newUser = { id: users.length + 1, name };
users.push(newUser);
// return NextResponse.json(newUser);
return new Response(JSON.stringify(newUser), {
status: 201,
headers: {
"Content-Type": "application/json",
},
});
}
接口params参数
// src\app\users\[id]\route.ts
import { NextResponse } from "next/server";
import { users } from "../route";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const { id } = await params;
const user = users.find((user) => user.id === parseInt(id));
return NextResponse.json(user);
}
# 数据获取
# client
// src\app\users\users-client\page.tsx
"use client"
import { useEffect, useState } from "react";
type User = {
id: number;
name: string;
username: string;
email: string;
phone: string;
}
export default function UsersClient() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState("");
useEffect(() => {
async function fetchUsers() {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) throw new Error("Failed to fetch users");
const data = await response.json();
setUsers(data);
} catch (error) {
setError('failed to fetch users');
if (error instanceof Error) {
setError(error.message);
}
} finally {
setIsLoading(false);
}
}
fetchUsers();
}, []);
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul className="space-y-4 p-4">
{users.map((user) => (
<li key={user.id} className="bg-gray-100 p-4 rounded-md">
<h2 className="text-lg font-bold">{user.name}</h2>
<p className="text-sm text-gray-500">{user.email}</p>
</li>
))}
</ul>
)
}
# server
// src\app\users\users-server\page.tsx
type User = {
id: number;
name: string;
username: string;
email: string;
phone: string;
}
export default async function UsersServer() {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
const users = await response.json();
return (
<ul className="space-y-4 p-4">
{users.map((user: User) => (
<li key={user.id} className="bg-gray-100 p-4 rounded-md">{user.name}</li>
))}
</ul>
)
}
# loading
// src\app\users\users-server\loading.tsx
export default function Loading() {
return <div>Loading users...</div>;
}
# error
// src\app\users\users-server\error.tsx
"use client"
import { useEffect } from "react";
export default function Error({ error }: { error: Error }) {
useEffect(() => {
console.error(error);
}, [error]);
return (
<div className="p-4 bg-red-100 text-red-800 rounded-md">
<h2 className="text-lg font-bold">Error fetching users</h2>
<p className="text-sm text-red-600">{error.message}</p>
</div>
)
}
使用这种方式 更低的延迟 更小的模块大小 改进的SEO 直接访问后端资源 保护敏感数据
应该仅在绝对必要时才选择客户端组件进行数据获取
因为当您需要实时更新或当数据依赖于无法预测的客户端交互时,服务器端会负责获取数据
# Server Actions
服务器操作是在服务器上执行异步函数,他们允许我们的组件定义和执行服务器逻辑
// src\app\mock-users\page.tsx
type MockUser = {
id: number;
name: string;
}
export default async function MockUsers() {
const response = await fetch("https://6757dd3cc0a427baf94e31d5.mockapi.io/users");
const users = await response.json();
return (
<ul className="space-y-4 p-4">
{users.map((user: MockUser) => (
<li key={user.id} className="bg-gray-100 p-4 rounded-md">{user.name}</li>
))}
</ul>
)
}
// src\app\mock-users\page.tsx
import { revalidatePath } from "next/cache";
type MockUser = {
id: number;
name: string;
}
export default async function MockUsers() {
const response = await fetch("https://6757dd3cc0a427baf94e31d5.mockapi.io/users");
const users = await response.json();
async function addUser(formData: FormData) {
"use server"
const name = formData.get("name");
const res = await fetch("https://6757dd3cc0a427baf94e31d5.mockapi.io/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
// Authorization: "Bearer 1234567890",
},
body: JSON.stringify({ name }),
});
const newUser = await res.json();
console.log(newUser);
revalidatePath("/mock-users");
}
return (
<div className="p-4">
<form className="mb-4" action={addUser}>
<input type="text" name="name" placeholder="Name" required className="border p-2 mr-2" />
<button type="submit">Add User</button>
</form>
<ul className="space-y-4 p-4 max-w-md flex flex-col flex-wrap gap-2 w-full">
{users.map((user: MockUser) => (
<li key={user.id} className="bg-gray-100 p-4 rounded-md">{user.name}</li>
))}
</ul>
</div>
)
}
# Auth
# clerk
...