Skip to content

Next

Next.js官方文档

小满zs的Next.js教程文档

Next.js Project By GitHub

项目初始化

bash
npx create-next-app@latest

路由

layout && template

  • layout(布局) 布局是多个页面共享UI,例如导航栏、侧边栏、底部等。
  • template(模板) 基本功能跟布局一样,只是不会保存状态
  • layout --> template --> page

loading 加载

  • 触发异步会自动跳转到loading组件 异步结束正常返回页面

error 错误

not-found(404)

Next默认会生成一个404页面,但我们可能自定义404页面

  • 直接跳转
  • 通过query传参
  • 预获取页面,生产环境下,访问当前页面会自动预获取页面/about/me
  • 保持滚动位置
  • 替换当前页面
tsx
import Link from 'next/link';

export default async function Home() {
    return <div className="border border-black rounded-lg p-4">
        <div className="flex flex-col">
            <Link href="/about/me">跳转到me</Link>
            <Link href={{pathname: '/about/me', query: {name: '张三'}}}>跳转并传参</Link>
            <Link href="/about/me" prefetch={true}>预获取page页面</Link>
            <Link href="/about" scroll={true}>保持滚动位置</Link>
            <Link href="/about" replace={true}>替换当前页面</Link>
        </div>
    </div>;
}

useRouter Hook

redirect、permanentRedirect 重定向

permanentRedirect是永久重定向,而redirect是临时重定向

tsx
'use client';
import {redirect} from 'next/navigation';

export default function Me() {
    return <div>
        <button onClick={() => redirect('/login')}>重定向</button>
    </div>;
}

动态路由

匹配规则

  • [id].tsx 匹配 /about/1
  • [...id].tsx 匹配 /about/1/2/3 (多层级)
  • [[id]].tsx 匹配 /about/[1] (可选)
tsx
'use client';
import {useParams} from 'next/navigation';

export default function DynamicRoute() {
    const {id} = useParams();
    return <div>动态路由获取 {id}</div>;
}

路由处理API

定义GET和POST请求处理逻辑,地址为src/app/api/user/route.ts

ts
import {NextRequest, NextResponse} from 'next/server';

export async function GET(request: NextRequest) {
    const query = request.nextUrl.searchParams;
    const id = query.get('id');
    return NextResponse.json({message: `Get request successful! id = ${id}`});
}

export async function POST(request: NextRequest) {
    const body = await request.json();
    // const body = await request.formData();
    // const body = await request.text();
    // const body = await request.blob();
    // const body = await request.arrayBuffer();
    return NextResponse.json({message: `Post request successful! id = ${body}`});
}

其中如果是动态路由传参,则地址为src/app/api/[id]/route.ts动态路由匹配规则一致

ts
import {NextRequest, NextResponse} from 'next/server';

export async function GET(request: NextRequest, {params}: { params: Promise<{ id: string }> }) {
    const {id} = await params;
    return NextResponse.json({message: `动态路由API Get request successful! id = ${id}`});
}

安装组件库shadcn/ui

bash
npx shadcn@latest init 
npx shadcn@latest add button
npx shadcn@latest add input
ts
import {cookies} from "next/headers"; //引入cookies
import {NextRequest, NextResponse} from "next/server"; //引入NextRequest, NextResponse
//模拟登录成功后设置cookie
export async function POST(request: NextRequest) {
    const body = await request.json();
    if (body.username === 'admin' && body.password === '123456') {
        const cookieStore = await cookies(); //获取cookie
        cookieStore.set('token', '123456', {
            httpOnly: true, //只允许在服务器端访问
            maxAge: 60 * 60 * 24 * 30, //30天
        });
        return NextResponse.json({code: 1}, {status: 200});
    } else {
        return NextResponse.json({code: 0}, {status: 401});
    }
}

//检查登录状态
export async function GET(request: NextRequest) {
    const cookieStore = await cookies();
    const token = cookieStore.get('token');
    if (token && token.value === '123456') {
        return NextResponse.json({code: 1}, {status: 200});
    } else {
        return NextResponse.json({code: 0}, {status: 401});
    }
}
tsx
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { useRouter } from 'next/navigation';

export default  function HomePage() {
    const router = useRouter();
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');
    const handleLogin = () => {
        fetch('/api/login', {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({ username, password }),
        }).then(res => {
            return res.json();
        }).then(data => {
            if(data.code === 1){
                router.push('/about');
            }
        });
    }
    return (
        <div className='mt-10 flex flex-col items-center justify-center gap-4'>
            <Input value={username} onChange={(e) => setUsername(e.target.value)} className='w-[250px]' placeholder="请输入用户名" />
            <Input value={password} onChange={(e) => setPassword(e.target.value)} className='w-[250px]' placeholder="请输入密码" />
            <Button onClick={handleLogin}>登录</Button>
        </div>
    )
}

Css方案

TailwindCss

CSS Modules

Next.js内置Sass

全局Css

Style

Css in Js

AI集成

使用deepseek作为AI接口提供者

bash
npm i ai @ai-sdk/deepseek @ai-sdk/react

从deepseek上获取到单独的token,编写API接口

ts
import {NextRequest} from "next/server";
import {streamText, convertToModelMessages} from 'ai'
import {createDeepSeek} from "@ai-sdk/deepseek";
import {DEEPSEEK_API_KEY} from "./key";

const deepSeek = createDeepSeek({
    apiKey: DEEPSEEK_API_KEY, //设置API密钥
});

export async function POST(req: NextRequest) {
    const {messages} = await req.json(); //获取请求体
    // 这里为什么接受messages 因为我们使用前端的useChat 他会自动注入这个参数,所有可以直接读取
    const result = streamText({
        model: deepSeek('deepseek-chat'), //使用deepseek-chat模型
        messages: await convertToModelMessages(messages), //转换为模型消息
        //前端传过来的额messages不符合sdk格式所以需要convertToModelMessages转换一下
        system: '你是一个高级程序员,请根据用户的问题给出回答', //系统提示词
    });

    return result.toUIMessageStreamResponse() //返回流式响应
}

Proxy 代理

应用场景:

  • 处理跨域请求
  • 接口转发例如/api/user -> (可能是其他服务器java/go/python等) -> /api/user
  • 限流例如配合第三方服务做限流
  • 鉴权/判断是否登录

基本使用

在src下定义一个文件proxy.ts,可以拦截所有请求

ts
import {NextRequest, NextResponse} from "next/server";

export async function proxy(request: NextRequest) {
    console.log(request.url, 'url');
}

config

ts
// 配置匹配路径
export const config = {
    matcher: '/api/:path*',
    // matcher: ['/api/:path*','/api/user/:path*'], 支持单个以及多个路径匹配
    // matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], 同样支持正则表达式匹配
}

复杂匹配

ts
export const config: ProxyConfig = {
    matcher: [
        {
            source: '/home/:path*',
            // 表示匹配路径中必须(包含)Authorization头和userId查询参数
            has: [
                {type: 'header', key: 'Authorization', value: 'Bearer 123456'},
                {type: 'query', key: 'userId', value: '123'}
            ],
            // 表示匹配路径中(必须不包含)cookie和userId查询参数
            missing: [
                {type: 'cookie', key: 'token', value: '123456'},
                {type: 'query', key: 'userId', value: '456'},
            ]
        },
    ]
}

渲染方式

CSR、SSR、SSG

对比项CSR(客户端渲染)SSR(服务端渲染)SSG(静态站点生成)
全称Client-Side RenderingServer-Side RenderingStatic Site Generation
渲染位置浏览器(JS 执行后渲染)服务端(返回完整 HTML)构建时(提前生成 HTML)
首屏速度慢(需加载 JS、请求接口)快(直接返回渲染好的页面)极快(静态文件直接返回)
SEO 表现差(爬虫默认不执行 JS)好(返回完整可爬取内容)极好(纯静态 HTML)
服务器压力高(每次访问都要渲染)极低(仅静态资源托管)
数据实时性实时实时构建时固定,需重新部署更新
适用场景后台管理、SPA、交互重应用电商、资讯、需要 SEO 的动态页面博客、文档、官网、内容不常更新站点
代表框架Vue/React 原生 SPANuxt、Next、Vue SSRNuxt/Next SSG、VitePress、Hexo
页面更新前端路由切换,无刷新页面重新请求静态文件,更新需重新构建
构建部署打包后 dist 直接部署需要 Node 服务运行纯静态文件,可放 CDN / 对象存储

RSC(React Server Components)

  • Next.js当中所有的组件默认都是服务器组件
  • 服务器组件: 上面都是静态内容,例如正文,标题,图片等,这类组件之所以适合在服务端执行,核心原因在于服务端渲染HTML+CSS的速度更快,生成的内容对搜索引擎完全可见,且无需客户端额外处理交互逻辑,完美匹配静态内容的需求
  • 客户端组件: 下面留言框是需要交互的,例如交互功能,如点赞按钮、计数器、表单等。这类组件需要依赖浏览器DOM事件、状态管理(useState)、副作用(useEffect)等客户端能力,必须在客户端完成渲染和水合(即添加事件处理程序的过程)才能实现交互效果
  • 'use client'通过这个可以声明为客户端组件
bash
npx fix-react2hell-next

Server Components

服务端组件可以访问node.js API,包括处理数据库db

tsx
import fs from 'fs'
import path from 'path'

export default async function ServerPage() {
    const filePath = path.join(process.cwd(), 'README.md')
    const content = await fs.promises.readFile(filePath, 'utf-8')

    return (
        <div className="p-8">
            <h1 className="text-2xl font-bold mb-4">文件内容</h1>
            <pre className="bg-gray-100 p-4 rounded whitespace-pre-wrap">
                    {content}
                </pre>
        </div>
    )
}

Client Components

通过'use client'声明客户端组件,客户端组件会在服务端进行一次预渲染,所以访问documentwindow 等API需要在useEffect 中访问

服务端组件可以嵌套客户端组件,客户端只能嵌套不能嵌套服务端组件

server-only

随着Nodejs的发展,很多API已经可以跟浏览器共用了例如fetchwebSocket,但有时候我们只想让他在服务端使用

bash
npm install server-only

安装完成这个包之后,只需要在文件的顶部编写 import 'server-only' 声明即可

Cache Components

next.config.ts当中进行配置启用

ts
import type {NextConfig} from "next";

const nextConfig: NextConfig = {
    cacheComponents: true, // 启用缓存组件
};

export default nextConfig;

静态内容

仅依赖同步 I/O(如 fs.readFileSync)、模块导入、纯计算的组件,console输出会标识为prerender

tsx
import fs from 'fs'

export default async function StaticCachePage() {
    const data = fs.readFileSync('./src/app/cache/static/data.json', 'utf-8')
    console.log(data)
    const impData = await import('./data.json')
    console.log(impData)
    return (
        <div className="p-8">
            <h1 className="text-2xl font-bold mb-4">文件内容</h1>
            <pre className="bg-gray-100 p-4 rounded whitespace-pre-wrap">
                    {data}
                </pre>
        </div>
    )
}

动态内容

适用场景:fetch请求、cookiesheaders等动态数据。必须配合Suspense使用

  • 非确定操作:随机数、时间戳等非确定操作,每次请求都可能生成不同结果
    • 使用Suspense包裹,然后使用connection表示不要预渲染这部分
  • 缓存组件,可以使用use cache声明这是一个缓存组件,然后使用cacheLife声明缓存时间
    • cacheLife参数:
      • stale:客户端在此时间内直接使用缓存,不向服务器发请求(单位:秒)
      • revalidate:超过此时间后,服务器收到请求时会在后台重新生成内容(单位:秒)
      • expire:超过此时间无访问,缓存完全失效,下次请求需要等待重新计算(单位:秒)
tsx
import {Suspense} from "react"
import {cookies} from "next/headers"
import {connection} from "next/server";
import {cacheLife} from "next/cache";

const DynamicContent = async () => {
    const data = await fetch('https://www.mocklib.com/mock/random/name') //随机生成一个名称
    const json = await data.json()
    console.log(json)
    const cookieStore = await cookies() //获取cookie
    console.log(cookieStore)

    await connection() //使用connection表示不要预渲染这部分
    const random = Math.random()
    const now = Date.now()
    console.log(random, now)
    return (
        <div>
            <h2>动态内容</h2>
            <main>
                <ul>
                    <li>名称:{json.name}</li>
                    <li>随机数:{random}</li>
                    <li>时间戳:{now}</li>
                </ul>
            </main>
        </div>
    )
}

const CacheLifeComponent = async () => {
    // 使用预设参数
    'use cache'
    cacheLife("hours")
    const data = await fetch('https://www.mocklib.com/mock/random/name')
    const json = await data.json()
    return (
        <div>
            <h2>缓存内容</h2>
            <main>
                <ul>
                    <li>名称:{json.name}</li>
                </ul>
            </main>
        </div>
    )
}

export default async function DynamicCachePage() {

    return (
        <div>
            <h1>Home</h1>
            <Suspense fallback={<div>动态内容Loading...</div>}>
                <DynamicContent/>
                <CacheLifeComponent/>
            </Suspense>
        </div>
    )
}

缓存(缓存策略)

未启用缓存组件

next.config.ts文件当中设置cacheComponents: false

tsx
export default async function NoCachePage() {
    const randomImage = await fetch('https://www.loliapi.com/acg/pc?type=json') //这个接口随机返回一个二刺猿图片
    const data = await randomImage.json()
    console.log(data)
    return (
        <div>
            <h1>Home</h1>
            <img width={500} height={500} src={data.url} alt="random image"/>
        </div>
    )
}

在开发环境下是正常的,每次刷新图片都会改变,在打包后即(生产环境下)会发现图片不会变化

原因是:Next.js会尽可能多的进行缓存,以提高性能降低成本,这意味着路由会被静态渲染,以及数据请求也会被缓存,除非禁用缓存。

方案一:重新验证

使用revalidate属性,可以设置缓存时间,单位为秒

ts
export const revalidate = 5

方案二:使用dynamic属性

表示将禁用缓存,每次请求都会重新获取数据

ts
// 动态更新 缓存组件不需要使用这个 默认都是动态内容
export const dynamic = 'force-dynamic'

方案三:禁用缓存

使用cache属性,并且设置为no-store

ts
const randomImage = await fetch('https://www.loliapi.com/acg/pc?type=json', {cache: 'no-store'})

方案四:任意动态内容API

当你使用以下任意API时,该路由会被视为动态内容,不会被缓存。

  • cookies
  • headers
  • connection
  • searchParams
  • fetch{ cache: ‘no-store’ }

启用缓存组件

启用缓存组件之后,所有组件默认为动态内容,因此export const dynamic = 'force-dynamic'不需要配置。

内置组件

Image

src直接使用

注意点:

  • Next.js建议我们把图片放在根目录下的public文件夹中,然后使用/开头访问
  • loading="eager"表示提前加载(默认是懒加载的,即lazy)(LCP警告)
  • widthheight必须配置,并且和原图的比值一致
  • image支持的属性
tsx
import Image from "next/image"

export default function ImagePage() {
    return (
        <div>
            <h1>Home</h1>
            <Image
                src="/1.png"
                loading="eager"
                width={100}
                height={100}
                alt="1"
            />
        </div>
    )
}

import 静态引入

tsconfig.json配置文件当中进行设置别名@/public/

json
{
  "paths": {
    "@/*": [
      "./src/*"
    ],
    "@/public/*": [
      "./public/*"
    ]
  }
}

直接使用即可,因为动态import导入即ImageByPng对象里面可以获取到宽高,所以不需要配置宽高

tsx
import Image from "next/image"
import ImageByPng from '@/public/1.png'

export default function ImagePage() {
    return (
        <div>
            <h1>import动态引入</h1>
            <Image
                src={ImageByPng}
                alt="1"
            />
        </div>
    )
}

远程图片

tsx
export function RemoteImage() {
    const len = 20;
    return (
        <div>
            <h1>Home</h1>
            {Array.from({length: len}).map((_, index) => (
                <Image
                    key={index}
                    src={`https://eo-img.521799.xyz/i/pc/img${index + 1}.webp`}
                    alt="1"
                    width={192}
                    height={108}
                />
            ))}
        </div>
    )
}

直接使用远程图片会报错,因为Next.js默认只允许加载本地图片,在next.config.ts文件中进行配置

ts
import type {NextConfig} from "next";

const nextConfig: NextConfig = {
    images: {
        remotePatterns: [
            {
                protocol: 'https', // 协议
                hostname: 'eo-img.521799.xyz', // 主机名
                pathname: '/i/pc/**', // 路径
                port: '', // 端口
            },
        ],
    }
};

export default nextConfig;

格式转换

Next.js 会通过请求Accept头自动检测浏览器支持的图像格式,以确定最佳输出格式,默认为webp,可以在next.config.ts 配置文件当中进行修改

ts
const nextConfig: NextConfig = {
    images: {
        formats: ['image/avif', 'image/webp'], //默认是 ['image/webp']
        deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], // 设备尺寸
        imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], // 图片尺寸
    }
};

font组件

  • google字体,从next/font/google当中进行导入。如何选择字体?
  • 本地字体,使用localFont里面加上src属性即可。字体下载 web site
  • 上面两种字体初始化后会返回一个对象,对象里面有className属性,使用这个属性即可完成字体设置
  • font支持的属性
tsx
import {Playwrite_NO_Guides, ZCOOL_KuaiLe} from 'next/font/google' //引入字体库
import localFont from 'next/font/local'

const playwriteNOGuides = Playwrite_NO_Guides({
    weight: '400',
    display: 'swap',
})
const zcoolKuaiLe = ZCOOL_KuaiLe({
    weight: '400',
    display: 'swap',
})

const zydtFont = localFont({
    src: '../../font/zydtFont.ttf',
    display: 'swap',
})

export default function fontPage() {
    return <div>
        <div className={playwriteNOGuides.className}>Who first beheld the moon by the river,
            And when did the river moon first shine upon men. Font Google
        </div>
        <div className={zcoolKuaiLe.className}>江畔何人初见月,江月何年初照人。</div>
        <div className={zydtFont.className}>江畔何人初见月,江月何年初照人。</div>
    </div>
}

Script

Next.js允许我们使用Script组件去加载js脚本(外部/本地脚本),并且他还对Script组件进行优化。

Script组件需要给其添加唯一的id

直接引入

底层原理会把这个Script组件转换成<script>标签,然后插入到<head>标签

tsx
import Script from 'next/script'

export function LocalIntroduction() {
    return (
        <div>
            <Script src="https://unpkg.com/vue@3/dist/vue.global.js"/>
        </div>
    )
}

全局引入

即在app/layout.tsx中引入,他会自动在所有页面中引入,并且只会加载一次,然后纳入缓存

加载策略 strategy

  • beforeInteractive: 在代码和页面之前加载会阻塞页面渲染
  • afterInteractive(默认值): 在页面渲染到客户端之后加载
  • lazyOnload: 在浏览器空闲时稍后加载脚本
  • worker(实验性特性): 暂时不建议使

内联脚本 一

直接在Script组件中添加js代码即可

tsx
export function InLine() {
    return <div>
        <Script id="VGUBHJMK6"
                strategy="afterInteractive">
            {
                `
            const {createApp} = Vue
            createApp({
              template: '<h1>{{ message }}</h1>',
              setup() {
                return {
                  message: 'Next.js + Vue.js + InLine1'
                }
              }
            }).mount('#app1')
          `
            }
        </Script>
    </div>
}

内联脚本 二

使用dangerouslySetInnerHTML属性设置脚本内容

tsx
export function InLine2() {
    return <div>
        <div id="app2"></div>
        <Script id="GSJI76HHj" dangerouslySetInnerHTML={{
            __html: `
(function() {
        const {createApp} = Vue
        createApp({
            template: '<h1>{{ message }}</h1>',
            setup() {
            return {
                message: 'Next.js + Vue.js + InLine2'
            }
            }
        }).mount('#app2')
    })()
    `
        }} strategy="afterInteractive">
        </Script>
    </div>
}

事件监听(生命周期)

  • onload: 脚本加载完成时触发
  • onReady: 脚本加载完成后,且组件每次挂载的时候都会触发
  • onError: 脚本加载失败时触发

Script组件只有在导入客户端的时候才会生效,所以需要使用use client声明这是一个客户端组件。

静态导出SSG

静态导出SSG

MDX

md+react混合写法,先安装依赖

bash
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx

next.config.ts当中配置mdx功能

ts
import type {NextConfig} from "next";
import createMDX from '@next/mdx'

const withMDX = createMDX({
    // 默认只支持mdx文件,扩展让其支持md
    extension: /\.(md|mdx)$/
});
const nextConfig: NextConfig = {
    pageExtensions: ['js', 'jsx', 'md', 'mdx', 'ts', 'tsx'],
};

export default withMDX(nextConfig);

在根目录下新建mdx-components.tsx文件,在这个文件当中可以进行全局样式的设置

tsx
import type {MDXComponents} from 'mdx/types'

const components: MDXComponents = {
    h1: ({children}) => <h1 className='text-2xl font-bold'>{children}</h1>, //# 代表h1 你可以自定义修改样式
    li: ({children}) => <li className='list-disc list-inside'>{children}</li>, //- 代表li 你可以自定义修改样式
}

export function useMDXComponents(): MDXComponents {
    return components
}

远程加载MD/MDX

先安装依赖

bash
npm install next-mdx-remote-client

可以使用http-server启动一个服务器进行模拟请求远程文档

tsx
import {MDXRemote} from "next-mdx-remote-client/rsc";

async function MdxContent() {
    const res = await fetch('http://localhost:4000/http.md')
    const source = await res.text()

    return <MDXRemote source={source}/>
}

export default MdxContent;

Server Actions 服务器函数

核心原理

是因为React扩展了原生HTML form表单,允许通过action属性直接绑定server action函数,当表单提交后,函数会自动接受原生的 FormData数据。这不是又回到了前后端不分离的开发模式下(jsp + servlet)了,hahahahaha

直接使用,form表单的action属性绑定函数即可

tsx
import {Button} from "@/components/ui/button"

export default function Login() {
    async function handleLogin(formData: FormData) {
        'use server'
        const username = formData.get('username') //接受单个参数
        const password = formData.get('password') //接受单个数据
        const form = Object.fromEntries(formData) //接受所有数据 {username: '张三', password: '123456'}
        console.log(username, password, form)
    }

    return (
        <div>
            <h1>登录页面</h1>
            <div className="flex flex-col gap-2 mt-30">
                <form action={handleLogin} className="flex flex-col gap-2">
                    <input className="border border-gray-300 rounded-md p-2" type="text" name="username"
                           placeholder="用户名"/>
                    <input className="border border-gray-300 rounded-md p-2" type="password" name="password"
                           placeholder="密码"/>
                    <Button type="submit">登录</Button>
                </form>
            </div>
        </div>
    )
}

如果想要传一些自定义的数值,把action指向这个userFunction函数即可,再通过函数的bind方法绑定参数

ts
const userFunction = handleLogin.bind(null, 1)

参数校验

安装zod

bash
npm i zod

使用,通过safeParse方法进行参数校验,

ts
import {z} from "zod"

const loginSchema = z.object({
    username: z.string().min(6, '用户名不能少于6位'), //zod基本用法表示这是一个字符串,并且不能少于6位
    password: z.string().min(6, '密码不能少于6位') //zod基本用法表示这是一个字符串,并且不能少于6位
})

const result = loginSchema.safeParse(Object.fromEntries(formData)) //调用zod的safeParse方法进行校验

if (!result.success) {
    const errorMessage = z.treeifyError(result.error).properties; //调用zod的treeifyError方法将错误信息转换为对象
    let str = ''
    Object.entries(errorMessage!).forEach(([_key, value]) => {
        value.errors.forEach((error: any) => {
            str += error + '\n' //将错误信息拼接成字符串
        })
    })
    console.log('error----', str)
}

国际化 i18n

实现原理

Next.js建议我们使用http报文头来判断用户使用的语言Accept-Language,例如Accept-Language:zh-CN,zh;q=0.9,en;q=0.8 表示用户使用中文(中国),如果用户没有设置,则使用默认语言。

实现I18n

  • dictionaries文件夹下定义好:en.jsonzh.jsonja.jsonko.json四种语言文件
  • 通过暴露一个函数getDictionary,返回一个Promise对象,这个对象会返回一个json文件
  • 通过select选择语言,切换语言,这里是动态路由/[lang]/home,改了语言直接塞到路由里面
  • 最后通过getDictionary函数获取语言文件,并返回给app组件
  • proxy代理统一拦截,获取请求头的语言信息,这时访问/home即可,在proxy当中自动重定向到,例如:/zh/home
bash
npm i negotiator # 用于解析Accept-Language
npm i @formatjs/intl-localematcher # 用于匹配语言
ts
export type Dictionary = {
    title: string
    description: string
    keywords: string
}
// 支持的语言
export const locales = ['en', 'zh', 'ja', 'ko']
export const defaultLocale = 'zh'

// 去根据语言得到对应的json文件
export function getDictionary(locale: string): Promise<Dictionary> {
    return import(`./${locale}.json`).then(module => module.default)
}
tsx
'use client'
import {locales} from '@dict/index'
import {usePathname, useRouter} from 'next/navigation'

export default function SwitchI18n({lang}: { lang: string }) {
    const pathname = usePathname() // 获取当前路径
    const router = useRouter() // 获取路由实例
    const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
        const newLang = e.target.value // 获取新语言
        const newPath = pathname.replace(`/${lang}`, `/${newLang}`) // 替换语言
        router.replace(newPath) // 跳转新路径
    }
    return <div>
        <select value={lang} onChange={handleChange}>
            {locales.map(locale => <option key={locale} value={locale}>{locale}</option>)}
        </select>
    </div>
}
tsx
import {getDictionary} from '@dict/index'
import SwitchComponent from "./swtich";
import {Suspense} from "react";

export default async function Home({params}: { params: Promise<{ lang: string }> }) {
    //获取语言
    const {lang} = await params
    //获取字典 lang = zh/en/ja/ko等
    const dictionary = await getDictionary(lang)
    //返回页面
    return <div>
        <Suspense fallback={<div>Loading...</div>}>
            <SwitchComponent lang={lang}/>
            <h1>{dictionary.title}</h1>
            <p>{dictionary.description}</p>
            <p>{dictionary.keywords}</p>
        </Suspense>
    </div>
}
ts
import { NextRequest, NextResponse } from 'next/server'
import Negotiator from 'negotiator'
import { match } from '@formatjs/intl-localematcher'
import { locales, defaultLocale } from '@dict/index' // 导入项目支持的语言和默认语言
export default function proxy(req: NextRequest, res: NextResponse) {
    // 如果请求路径为根路径,则直接返回 首页不做任何处理
    if(req.nextUrl.pathname === '/') {
        return NextResponse.next()
    }
    //如果路径已经包含所支持的语言,则直接返回 例如 /zh/about /zs/home 等
    if(locales.some(locale => req.nextUrl.pathname.startsWith(`/${locale}`))){
        return NextResponse.next()
    }
    // 获取请求头
    const headers = {
        'accept-language': req.headers.get('accept-language') || ''
    }
    // 解析请求头
    const negotiator = new Negotiator({ headers })
    // 获取语言
    const language = negotiator.languages()
    //['zh-CN', 'zh', 'en-US', 'en', 'ja']  // 按优先级从高到低排序
    const lang = match(language, locales, defaultLocale)
    //language-浏览器支持的语言 locales-项目支持的语言 defaultLocale-项目默认语言
    // 匹配语言例如 zh-CN 则 lang 返回 zh en-US 则 lang 返回 en 如果没有匹配到则返回默认语言defaultLocale
    const pathname = req.nextUrl.pathname
    // 拼接语言
    req.nextUrl.pathname = `/${lang}${pathname}`
    // 重定向 例如用户访问的是/home 我们则读取语言后重定向到/zh/home 给它增加语言前缀
    return NextResponse.redirect(req.nextUrl)
}
export const config = {
    matcher:[
        '/((?!api|_next/static|_next/image|favicon.ico).*)', //跳过内部匹配路径
    ]
}

next.config.js配置

next.config.js配置-官方文档

SEO(Search Engine Optimization) 搜索引擎优化

黑帽SEO & 白帽SEO

对比维度白帽 SEO黑帽 SEO
核心定义遵循搜索引擎官方规则、算法规范,
采用正规合规手段优化网站,长期稳定提升排名
无视搜索引擎规则,利用漏洞、违规作弊手段,
短期快速刷排名、刷流量
优化手段1. 合理优化 TDK(标题 / 描述 / 关键词)
2. 优化网站目录结构、内链布局
3. 配置 robots.txt、sitemap.xml
4. 完善 JSON-LD 结构化数据、OG 标签
5. 优化 Web Vitals 核心性能指标
6. 原创优质内容、合规外链、用户体验优化
1. 关键词堆砌、隐藏文字 / 隐藏链接
2. 垃圾外链、购买外链、群发外链
3. 桥页、镜像网站、内容抄袭采集
4. 流量劫持、跳转欺诈、恶意刷点击
5. 伪原创拼接、批量低质内容
见效周期见效慢,属于长期积累,1~3 个月逐步提升见效极快,短时间快速飙升排名
稳定性排名稳定,抗算法更新,长期可持续运营极不稳定,算法更新极易断崖式下跌
风险程度无处罚风险,安全合规风险极高,易被降权、屏蔽、K 站、剔除索引
适用场景企业官网、品牌站点、长期运营网站、正规电商 / 自媒体灰产、短期流量收割、套利站点、一次性项目
核心本质以用户体验为核心,内容价值优先以投机作弊为核心,只针对搜索引擎爬虫

Google搜索引擎

原理

Google 派蜘蛛全网爬页面 → 把页面内容解析建库(倒排索引)→ 用户搜索时快速匹配并按 200 + 因子打分排序 → 毫秒级返回结果

核心流程(4大阶段):抓取(发现页面)→ 索引(存储解析)→ 排序(打分匹配)→ 呈现(返回结果),全程毫秒级完成

阶段核心执行者/动作关键要点
抓取(Crawl)Googlebot(谷歌蜘蛛)1. 从已知URL出发,顺链接发现新页面;
2. 受robots.txt、抓取预算、Sitemap控制;
3. 网站结构混乱易导致抓取不全。
索引(Index)内容解析+倒排索引构建1. 解析标题、正文、图片alt、结构化数据等;
2. 倒排索引实现“词→页面”毫秒匹配;
3. 低质、作弊内容不被索引。
排序(Rank)多算法+200+排序因子1. 核心算法:PageRank(外链权威度)、RankBrain(AI理解意图)、BERT(上下文解析);
2. 关键因子:内容相关性、用户体验、权威度等;3. 黑帽作弊会被惩罚。
呈现(Serve)语义解析+结果匹配1. 解析用户查询意图,匹配索引库内容;
2. 排序后返回自然结果+广告,毫秒级响应。
  • 排名的考量(相关性-内容与搜搜意图的匹配)(权威性-域名权重,外链质量)(用户体验-加载速度SEO友好)
  • 收录,在被抓如到索引之后,通常是2-3周才会被收录,排名需要一段时间的积累权重,一般是2-3个月
  • 结果,搜索的结果会全方面考量,用户的语言,设备,历史记录,SEO优化的是整体,而不是固定某个位置

robots.txt

robots.txt是搜索引擎爬虫访问网站时遵循的规则,它告诉搜索引擎哪些页面可以抓取,哪些页面不能抓取。一般是存放在网站根目录下。

指令参数作用含义使用说明
User-agent指定搜索引擎爬虫名称限定规则对哪一个蜘蛛生效;* 代表所有爬虫
Disallow禁止抓取 指定目录 / 文件填写禁止访问的 URL 路径;留空 Disallow: 代表允许全部抓取
Allow允许抓取 指定目录优先级高于 Disallow;用于全局禁止时,单独放行某些页面
Sitemap提交网站地图地址告诉搜索引擎本站 XML 站点地图链接,辅助收录
Crawl-delay设置爬虫抓取延迟限制爬虫访问间隔,降低服务器压力(Google 已弱化支持)

案例:https://www.bilibili.com/robots.txt

txt
User-agent: *
Disallow: /medialist/detail/
Disallow: /index.html

User-agent: Yisouspider
Allow: /

User-agent: Applebot
Allow: /

User-agent: bingbot
Allow: /

User-agent: Sogou inst spider
Allow: /

User-agent: Sogou web spider
Allow: /

User-agent: 360Spider
Allow: /

User-agent: Googlebot
Allow: /

User-agent: Baiduspider
Allow: /

User-agent: Bytespider
Allow: /

User-agent: PetalBot
Allow: /

User-agent: facebookexternalhit
Allow: /tbhx/hero

User-agent: Facebot
Allow: /tbhx/hero

User-agent: Twitterbot
Allow: /tbhx/hero

User-agent: *
Disallow: /

Next.js中实现robots.txt

使用到的是AppRouter,所以直接在app目录下创建一个robots.[ts | js] 文件即可。访问:http://localhost:3000/robots.txt

这里面单个rules的话可以直接用对象的形式(去掉数组),同理如果是多个sitemap的话可以用数组形式

ts
import type {MetadataRoute} from "next";

export default function robots(): MetadataRoute.Robots {
    return {
        rules: [
            {
                userAgent: 'Googlebot', // 搜索引擎爬虫的名称
                allow: '/', // 允许访问的页面
                disallow: '/api/', // 不允许访问的页面
                crawlDelay: 10, // 访问间隔时间(Google机器人不支持该参数,其他部分爬虫机器人支持该参数)
            }
        ],
        sitemap: 'xxxx'
    }
}

sitemap.xml

网站站点地图,是给搜索引擎爬虫看的 XML 文件,记录网站所有页面链接

核心作用

  • 主动递链:主动告诉搜索引擎全站 URL,不用只靠爬虫爬取发现
  • 加速收录:提升页面抓取、索引速度,尤其新站、新页面
  • 收录死角:收录内链少、深层页面、孤立页面
  • 传递信息:附带页面更新时间、更新频率、页面优先级
  • 优化抓取:降低爬虫抓取压力,提升爬取效率,适配 Google 等搜索引擎
  • 故障补救:网站结构复杂、JS 渲染、动态页面,弥补爬虫抓取短板

案例

https://www.bilibili.com/sitemap.xml

xml

<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
    <style>[class*="fullscreen"] #bilibili-helper-host,[class*="webscreen-fix"] #bilibili-helper-host {z-index:
        9!important;}
    </style>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/v.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/bangumi/static.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/bangumi/video.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/ranking.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/online.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/newlist.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/baidu_schema.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
    <sitemap>
        <loc>https://www.bilibili.com/sitemap/read/detail.xml</loc>
        <lastmod>2026-04-29T02:43:51.384Z</lastmod>
    </sitemap>
</sitemapindex>

标签参数说明:

标签 / 参数名称核心作用
<urlset>根节点声明站点地图协议、命名空间,文件最外层容器
<url>单条链接容器包裹单个页面的所有信息,一条页面对应一个<url>
<loc>页面地址必填,填写页面完整绝对 URL,搜索引擎精准收录
<lastmod>最后修改时间告知爬虫页面最新更新时间,优先抓取更新内容
<changefreq>更新频率提示页面更新周期:always/ hourly/ daily/ weekly/ monthly/ yearly/ never
<priority>页面权重取值 0.0~1.0,告知站内页面重要性,越高权重越高

Next.js中实现sitemap.xml

和实现robots.txt一样,创建一个sitemap.[ts | js] 文件,访问:http://localhost:3000/sitemap.xml

TDK + meta

TDKTitleDescriptionKeywords 的缩写

  • title 是页面标题,通常会出现在浏览器标签页和搜索引擎结果页(SERP)上,对点击率影响最大。建议简洁、准确,并体现当前页与站点/栏目的关系
  • description 是页面摘要,常被用作 SERP 中的描述文案(搜索引擎也可能根据内容自行改写)。应用一两句话概括页面价值,避免堆砌关键词
  • keywords 用于概括页面主题。主流搜索引擎对 的排序权重已很低,但规范填写仍有利于内部归类、CMS 或后续扩展;不要为 SEO 而重复、堆砌无意义词组

  • title:不同页面应有区分度;全站共用的后缀可通过根布局的 title.template 统一拼接
  • description:长度适中即可(常见建议约 150 字以内作参考),重点写清「这一页解决什么问题」
  • keywords:用数组表达多个词即可,与页面内容一致即可

Next.js中实现TDK

  • 通过变量指定类型:Metadata,在全局layout.tsx上定义几表示为全局,在子路由下定义表示为子路由
  • 右键-检查源代码哭找到刚才定义的TDK
  • 值传递,父级中通过对象的形式+%s可以定义一个插槽,子级中定义的title,会自动填充进来
  • generateMetadata动态配置
ts
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
  keywords: ['modify', 'next'],
};
html
<title>Create Next App</title>
<meta name="description" content="Generated by create next app"/>
<meta name="keywords" content="modify,next"/>
ts
export const metadata: Metadata = {
  title: {
    default: 'modify',
    template: '%s | next',
  },
  description: "Generated by create next app",
  keywords: ['modify', 'next'],
}
tsx
import type {Metadata, ResolvingMetadata} from 'next';

type Props = {
    params: Promise<{ id: string }>;
};

export async function generateMetadata(
    {params}: Props,
    parent: ResolvingMetadata
): Promise<Metadata> {
    const {id} = await params;
    const resolvedParent = await parent;

    const res = await fetch(
        `https://jsonplaceholder.typicode.com/posts/${id}`,
        {next: {revalidate: 3600}}
    );
    if (!res.ok) {
        return {title: '文章未找到'};
    }
    const data = await res.json();

    return {
        title: `${data.title} | ${resolvedParent.title?.absolute ?? '文章'}`,
        description: data.body.slice(0, 150),
        keywords: [data.title],
    };
}

export default async function IndexPage({params}: Props) {
    const {id} = await params;
    return <div>文章 id:{id}</div>;
}

JSON-LD

可以看ld+json

Open Graph(OG)

Facebook(现 Meta)提出的一套页面元数据协议,通过<meta property="og:*"> 描述标题、描述、封面图、类型等。当链接被分享到微信、SlackDiscordLinkedIn等平台时,抓取方会读取这些标签来生成卡片预览

看下Apple官网,查看网页源代码去搜og

html

<meta property="og:image" content="https://www.apple.com/ac/structured-data/images/open_graph_logo.png?202604211141"/>
<meta property="og:title" content="Apple"/>
<meta property="og:description"
      content="Discover the innovative world of Apple and shop everything iPhone, iPad, Apple Watch, Mac, and Apple TV, plus explore accessories, entertainment, and expert device support."/>
<meta property="og:url" content="https://www.apple.com/"/>
<meta property="og:locale" content="en_US"/>
<meta property="og:site_name" content="Apple"/>
<meta property="og:type" content="website"/>

使用:在metadata下面加一个openGraph即可

ts
export const metadata: Metadata = {
    title: "Create Next App",
    description: "Generated by create next app",
    keywords: ['modify', 'next'],
    openGraph: {
        title: 'Modify',
        description: '江畔何人初见月,江月何年初照人',
        url: 'https://www.apple.com/',
        siteName: 'Modify',
        locale: 'zh_CN',
        type: 'website',
        images: [
            {
                url: 'https://nextjs.org/_next/static/media/logo-github-light.0j2vz9_zw2uex.svg?dpl=dpl_C86UhvbeqbmPDQA9iqAkS3nCz5Da',
            },
        ],
    },
};

ORM框架(Prisma+postgreSql)

通过docker安装postgreSQL,之后启动镜像,需要设置POSTGRES_PASSWORD也就是数据库密码

  • vscode:安装Database Client插件
  • webstromDatabase Tool社区版是用不了的

在现有的nextjs项目当中使用

bash
# 安装`Prisma`和`Prisma`客户端
npm i prisma -D
npm install @prisma/client @prisma/adapter-pg pg dotenv

# 初始化Prisma,初始化完成之后他会自动生成prisma文件夹,并且生成schema.prisma文件,以及创建一个env文件和prisma.config.ts文件
npx prisma init

修改prisma/schema.prisma文件,往里面加上表的定义,并且修改env文件(数据库链接配置)

prisma
generator client {
  provider = "prisma-client" // 使用什么客户端
  output   = "../src/generated/prisma" // 生成客户端代码的目录
}

datasource db {
  provider = "postgresql" // 连接什么数据库
}

model User {
  id        String   @id @default(cuid()) // 主键
  name      String   // 用户名
  email     String   @unique // 邮箱
  password  String   // 密码
  createdAt DateTime @default(now()) // 创建时间
  updatedAt DateTime @updatedAt // 更新时间
  posts     Post[]   // 关联文章
}

model Post {
  id        String   @id @default(cuid()) // 主键
  title     String   // 标题
  content   String   // 内容
  createdAt DateTime @default(now()) // 创建时间
  updatedAt DateTime @updatedAt // 更新时间
  authorId  String   // 作者ID
  author    User     @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade) // 一对多关联
}

执行数据库迁移命令,执行完成之后他会在prisma/migrations文件夹中生成一个migration文件,并且生成一个sql文件,然后自动执行 sql文件,创建表结构

bash
npx prisma migrate dev --name init

接着执行生成客户端代码命令,生成路径是 schema.prisma 文件中client output的目录

bash
npx prisma generate

完成之后,定义一个src/lib/prisma.ts文件,引入生成的客户端代码,并创建一个适配器,并创建一个客户端,并导出客户端

ts
import {PrismaClient} from '@/generated/prisma/client' // 引入生成客户端代码
import {PrismaPg} from '@prisma/adapter-pg' // 引入适配器
const pool = new PrismaPg({connectionString: process.env.DATABASE_URL}) // 创建连接池
const prisma = new PrismaClient({adapter: pool}) // 创建客户端
export default prisma // 导出客户端

最后就是编写Nextjs的接口与测试,webstrom下的.http文件可以直接发送请求测试,vscode下的.http文件需要安装 REST Client插件

ts
import prisma from "@/lib/prisma"; //@lib是我在tsconfig.json中配置的别名,表示src目录下的lib文件夹
import {NextRequest, NextResponse} from "next/server"; //引入NextRequest, NextResponse

// 查询所有用户
export async function GET(request: NextRequest) {
    const users = await prisma.user.findMany()
    return NextResponse.json(users) //返回用户列表
}

// 创建用户
export async function POST(request: NextRequest) {
    const {name, email, password} = await request.json() //获取请求体
    const user = await prisma.user.create({
        data: {name, email, password}
    })
    return NextResponse.json(user)
}


// 更新用户
export async function PATCH(request: NextRequest) {
    const {id, name, email, password} = await request.json() //获取请求体
    const user = await prisma.user.update({
        where: {id},
        data: {name, email, password}
    })
    return NextResponse.json(user)
}

// 删除用户
export async function DELETE(request: NextRequest) {
    const {id} = await request.json()
    const user = await prisma.user.delete({
        where: {id} //删除用户
    })
    return NextResponse.json(user)
}
http
### 创建用户
POST http://localhost:3000/api/db
Content-Type: application/json

{
  "name": "test",
  "email": "1234@qq.com",
  "password": "123456"
}

### 查询所有用户
GET http://localhost:3000/api/db


### 更新用户
PATCH http://localhost:3000/api/db
Content-Type: application/json

{
  "id": "cmkyoxflr00004ck82ywc6joi",
  "name": "modify",
  "email": "modify@qq.com",
  "password": "dasdasda"
}

### 删除用户
DELETE http://localhost:3000/api/db
Content-Type: application/json

{
  "id": "cmkyoxflr00004ck82ywc6joi"
}

By Modify.

选择字体进行切换