# 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 中,`` 组件的预取行为是在路由改变时发生的,而不一定需要页面视口中的链接可见。具体来说,预取会在以下情况下触发:

  1. 路由改变: 当用户点击 `` 组件导航到另一个页面时,路由会发生改变。
  2. 页面视口中的链接可见时: 在默认情况下,如果页面视口中的链接(即用户可见的链接)具有 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

...