Next
项目初始化
npx create-next-app@latest路由
layout && template
- layout(布局) 布局是多个页面共享UI,例如导航栏、侧边栏、底部等。
- template(模板) 基本功能跟布局一样,只是不会保存状态
- layout --> template --> page
loading 加载
- 触发异步会自动跳转到loading组件 异步结束正常返回页面
error 错误
not-found(404)
Next默认会生成一个404页面,但我们可能自定义404页面
Link 导航
- 直接跳转
- 通过query传参
- 预获取页面,生产环境下,访问当前页面会自动预获取页面
/about/me - 保持滚动位置
- 替换当前页面
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是临时重定向
'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] (可选)
'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
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与动态路由匹配规则一致
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}`});
}Cookie
安装组件库shadcn/ui
npx shadcn@latest init
npx shadcn@latest add button
npx shadcn@latest add inputimport {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});
}
}'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接口提供者
npm i ai @ai-sdk/deepseek @ai-sdk/react从deepseek上获取到单独的token,编写API接口
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,可以拦截所有请求
import {NextRequest, NextResponse} from "next/server";
export async function proxy(request: NextRequest) {
console.log(request.url, 'url');
}config
// 配置匹配路径
export const config = {
matcher: '/api/:path*',
// matcher: ['/api/:path*','/api/user/:path*'], 支持单个以及多个路径匹配
// matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], 同样支持正则表达式匹配
}复杂匹配
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 Rendering | Server-Side Rendering | Static Site Generation |
| 渲染位置 | 浏览器(JS 执行后渲染) | 服务端(返回完整 HTML) | 构建时(提前生成 HTML) |
| 首屏速度 | 慢(需加载 JS、请求接口) | 快(直接返回渲染好的页面) | 极快(静态文件直接返回) |
| SEO 表现 | 差(爬虫默认不执行 JS) | 好(返回完整可爬取内容) | 极好(纯静态 HTML) |
| 服务器压力 | 低 | 高(每次访问都要渲染) | 极低(仅静态资源托管) |
| 数据实时性 | 实时 | 实时 | 构建时固定,需重新部署更新 |
| 适用场景 | 后台管理、SPA、交互重应用 | 电商、资讯、需要 SEO 的动态页面 | 博客、文档、官网、内容不常更新站点 |
| 代表框架 | Vue/React 原生 SPA | Nuxt、Next、Vue SSR | Nuxt/Next SSG、VitePress、Hexo |
| 页面更新 | 前端路由切换,无刷新 | 页面重新请求 | 静态文件,更新需重新构建 |
| 构建部署 | 打包后 dist 直接部署 | 需要 Node 服务运行 | 纯静态文件,可放 CDN / 对象存储 |
RSC(React Server Components)
- 在
Next.js当中所有的组件默认都是服务器组件 - 服务器组件: 上面都是静态内容,例如正文,标题,图片等,这类组件之所以适合在服务端执行,核心原因在于服务端渲染HTML+CSS的速度更快,生成的内容对搜索引擎完全可见,且无需客户端额外处理交互逻辑,完美匹配静态内容的需求
- 客户端组件: 下面留言框是需要交互的,例如交互功能,如点赞按钮、计数器、表单等。这类组件需要依赖浏览器DOM事件、状态管理(useState)、副作用(useEffect)等客户端能力,必须在客户端完成渲染和水合(即添加事件处理程序的过程)才能实现交互效果
'use client'通过这个可以声明为客户端组件
npx fix-react2hell-nextServer Components
服务端组件可以访问node.js API,包括处理数据库db
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'声明客户端组件,客户端组件会在服务端进行一次预渲染,所以访问document、window 等API需要在useEffect 中访问
服务端组件可以嵌套客户端组件,客户端只能嵌套不能嵌套服务端组件
server-only
随着Nodejs的发展,很多API已经可以跟浏览器共用了例如fetch,webSocket,但有时候我们只想让他在服务端使用
npm install server-only安装完成这个包之后,只需要在文件的顶部编写 import 'server-only' 声明即可
Cache Components
在next.config.ts当中进行配置启用
import type {NextConfig} from "next";
const nextConfig: NextConfig = {
cacheComponents: true, // 启用缓存组件
};
export default nextConfig;静态内容
仅依赖同步 I/O(如 fs.readFileSync)、模块导入、纯计算的组件,console输出会标识为prerender
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请求、cookies、headers等动态数据。必须配合Suspense使用
- 非确定操作:随机数、时间戳等非确定操作,每次请求都可能生成不同结果
- 使用Suspense包裹,然后使用connection表示不要预渲染这部分
- 缓存组件,可以使用
use cache声明这是一个缓存组件,然后使用cacheLife声明缓存时间- cacheLife参数:
- stale:客户端在此时间内直接使用缓存,不向服务器发请求(单位:秒)
- revalidate:超过此时间后,服务器收到请求时会在后台重新生成内容(单位:秒)
- expire:超过此时间无访问,缓存完全失效,下次请求需要等待重新计算(单位:秒)
- cacheLife参数:
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
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属性,可以设置缓存时间,单位为秒
export const revalidate = 5方案二:使用dynamic属性
表示将禁用缓存,每次请求都会重新获取数据
// 动态更新 缓存组件不需要使用这个 默认都是动态内容
export const dynamic = 'force-dynamic'方案三:禁用缓存
使用cache属性,并且设置为no-store
const randomImage = await fetch('https://www.loliapi.com/acg/pc?type=json', {cache: 'no-store'})方案四:任意动态内容API
当你使用以下任意API时,该路由会被视为动态内容,不会被缓存。
cookiesheadersconnectionsearchParamsfetch和{ cache: ‘no-store’ }
启用缓存组件
启用缓存组件之后,所有组件默认为动态内容,因此
export const dynamic = 'force-dynamic'不需要配置。
内置组件
Image
src直接使用
注意点:
Next.js建议我们把图片放在根目录下的public文件夹中,然后使用/开头访问loading="eager"表示提前加载(默认是懒加载的,即lazy)(LCP警告)width和height必须配置,并且和原图的比值一致- image支持的属性
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/,
{
"paths": {
"@/*": [
"./src/*"
],
"@/public/*": [
"./public/*"
]
}
}直接使用即可,因为动态import导入即ImageByPng对象里面可以获取到宽高,所以不需要配置宽高
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>
)
}远程图片
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文件中进行配置
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 配置文件当中进行修改
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支持的属性
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>标签
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代码即可
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属性设置脚本内容
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
MDX
即md+react混合写法,先安装依赖
npm install @next/mdx @mdx-js/loader @mdx-js/react @types/mdx在next.config.ts当中配置mdx功能
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文件,在这个文件当中可以进行全局样式的设置
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
先安装依赖
npm install next-mdx-remote-client可以使用http-server启动一个服务器进行模拟请求远程文档
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属性绑定函数即可
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方法绑定参数
const userFunction = handleLogin.bind(null, 1)参数校验
安装zod库
npm i zod使用,通过safeParse方法进行参数校验,
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.json、zh.json、ja.json、ko.json四种语言文件 - 通过暴露一个函数
getDictionary,返回一个Promise对象,这个对象会返回一个json文件 - 通过
select选择语言,切换语言,这里是动态路由/[lang]/home,改了语言直接塞到路由里面 - 最后通过
getDictionary函数获取语言文件,并返回给app组件 proxy代理统一拦截,获取请求头的语言信息,这时访问/home即可,在proxy当中自动重定向到,例如:/zh/home
npm i negotiator # 用于解析Accept-Language
npm i @formatjs/intl-localematcher # 用于匹配语言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)
}'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>
}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>
}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配置
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
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的话可以用数组形式
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
<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
TDK是 Title、Description、Keywords 的缩写
title是页面标题,通常会出现在浏览器标签页和搜索引擎结果页(SERP)上,对点击率影响最大。建议简洁、准确,并体现当前页与站点/栏目的关系description是页面摘要,常被用作 SERP 中的描述文案(搜索引擎也可能根据内容自行改写)。应用一两句话概括页面价值,避免堆砌关键词keywords用于概括页面主题。主流搜索引擎对 的排序权重已很低,但规范填写仍有利于内部归类、CMS 或后续扩展;不要为 SEO 而重复、堆砌无意义词组
title:不同页面应有区分度;全站共用的后缀可通过根布局的 title.template 统一拼接description:长度适中即可(常见建议约 150 字以内作参考),重点写清「这一页解决什么问题」keywords:用数组表达多个词即可,与页面内容一致即可
Next.js中实现TDK
- 通过变量指定类型:
Metadata,在全局layout.tsx上定义几表示为全局,在子路由下定义表示为子路由 - 右键-检查源代码哭找到刚才定义的TDK
- 值传递,父级中通过对象的形式+
%s可以定义一个插槽,子级中定义的title,会自动填充进来 generateMetadata动态配置
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
keywords: ['modify', 'next'],
};<title>Create Next App</title>
<meta name="description" content="Generated by create next app"/>
<meta name="keywords" content="modify,next"/>export const metadata: Metadata = {
title: {
default: 'modify',
template: '%s | next',
},
description: "Generated by create next app",
keywords: ['modify', 'next'],
}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:*"> 描述标题、描述、封面图、类型等。当链接被分享到微信、Slack、Discord、LinkedIn等平台时,抓取方会读取这些标签来生成卡片预览
看下Apple官网,查看网页源代码去搜og
<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即可
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',
},
],
},
};Open Graph- 协议首页:https://ogp.me
- type 说明与扩展类型入口:https://ogp.me/#types
- 已定义的对象类型列表:https://ogp.me/#structured
ORM框架(Prisma+postgreSql)
通过docker安装postgreSQL,之后启动镜像,需要设置POSTGRES_PASSWORD也就是数据库密码
vscode:安装Database Client插件webstrom:Database Tool社区版是用不了的
在现有的nextjs项目当中使用
# 安装`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文件(数据库链接配置)
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文件,创建表结构
npx prisma migrate dev --name init接着执行生成客户端代码命令,生成路径是 schema.prisma 文件中client output的目录
npx prisma generate完成之后,定义一个src/lib/prisma.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插件
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)
}### 创建用户
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"
}