使用 nextJS 开发本站记录

nextJS特性
fullstack framework(全栈框架)
相较于React,nextJS更全面,React只是个UI框架,开发项目还需要引入其他服务;使用nextJS可以在一个项目里干所有的事情,包括使用nodejs访问数据库...
Server-side Rendering(服务端渲染)
相较于React,使用服务端渲染页面,页面预渲染加快页面的渲染速度,便于SEO;使用场景:注重页面渲染速度以及SEO的项目可以使用nextJS
file-system based router(基于文件系统的路由)
相较于React,不用额外配置路由 路由包含App Router和Pages Router两个方式
项目初始化命令
npx create-next-app@latest
脚手架初始化会提示是否要以App Router作为路由方式,默认是App Router,
页面跳转方式
- <Link>组件
// Navigate to /about
<Link href='/about'></Link>
// Navigate to /about?name=test
<Link href={{
pathname: '/about',
query: { name: 'test' },
}}
- useRouter(Client Components)
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button type="button" onClick={() => router.push('/dashboard')}>
Dashboard
</button>
)
}
- redirect(Server Components)
import { redirect } from 'next/navigation'
async function fetchTeam(id) {
const res = await fetch('https://...')
if (!res.ok) return undefined
return res.json()
}
export default async function Profile({ params }) {
const team = await fetchTeam(params.id)
if (!team) {
redirect('/login')
}
// ...
}
- history API window.history.pushState window.history.replaceState
数据获取
React 扩展了 fetch 以提供自动请求重复数据删除,多次调用同一个请求,请求只发送一次;服务端/客户端数据缓存;并行/顺序执行等功能。
对于是使用服务端获取数据以及预渲染还是选择客户端获取数据,个人认为如果考虑到首屏加载速度以及seo的页面或数据可以使用服务端,对于复杂交互的ui可以使用客户端渲染,就目前的React项目代码该怎么写就怎么写。
服务端预渲染获取数据
数据获取建立在原生fetch API之上
let data = await fetch('https://api.vercel.app/blog' })
客户端数据获取
在React生命周期钩子函数中获取数据
一些组件/API
<Image>
使用webp等现代图像格式,自动为每台设备提供正确尺寸的图像 当图片进入视口时加载图片
import Image from 'next/image'
metadata
定义页面元数据信息,便于SEO
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '...',
description: '...',
}
export default function Page() {}
本站开发记录
涉及到的知识点
1. 页面路由
首先通过官方文档介绍创建nextjs项目:npx create-next-app@latest
创建过程会发现,nextjs默认使用TypeScript、Tailwind CSS、App Router路由方式;根据自身需求去选择用TypeScript还是JavaScript开发;Tailwind CSS 通过给元素设置具体的类名可以简化我们的css代码;
重点讲下App Router路由方式: 根据nextjs官网描述,nextjs支持 App Router 和 Pages Router 两种路由方式,默认情况下是 App Router,因为 App Router 可以使用最新的React特性,比如 Server Components 和 Streaming,就是 nextjs 可以将一个页面分成静态渲染部分和动态渲染部分,免于等待所有组件全加载完才渲染,造成页面 blocking。
两种路由方式下的页面路由也有差异,App Router 模式下,任意目录下可访问页面,必须固定定义为 page.tsx;Pages Router 模式下所有pages目录下的文件都被定义为路由。
2. api路由
根据上面提到的 App Router 与 Pages Router 的区别,两种路由方式下的api路由也有差异,App Router 模式下,api目录要放置在app目录下,而且必须命名 route.ts;而 Pages Router 模式下,api目录下的文件都被定义为api路由,具体api定义方式如下:
// App Router
export async function GET(request: NextRequest) {
// 获取数据逻辑
const data = await ...
return NextResponse.json({ data });
}
export async function POST(request: NextRequest) {
const body = await request.json();
// 插入数据逻辑
return NextResponse.json({ msg: 'Created success' });
}
export async function DELETE(request: NextRequest) {
const id = request.nextUrl.searchParams.get('id')!;
if (!id) {
return NextResponse.json({ msg: 'Delete Failed' });
}
// 删除数据逻辑
return NextResponse.json({ msg: 'Deleted Success ' });
}
export async function PUT(request: NextRequest) {
const id = request.nextUrl.searchParams.get('id');
if (!id) {
return NextResponse.json({ msg: 'Update Failed' });
}
// 更新数据逻辑
return NextResponse.json({ msg: 'Updated Success' });
}
// Pages Router
function handler(req, res) {
switch (req.method) {
case 'GET':
// ...
case 'POST':
// ...
case 'DELETE':
// ...
case 'PUT':
// ...
default:
// ....
}
}
3. layout
设置全局公共布局、公共样式或metadata等 App Router 模式下可以设置全局或单个页面下的layout.ts、template.ts文件,针对整个项目或单个页面下的布局文件。Pages Router 模式下可以设置_app.ts文件、—_document.ts文件,分别是设置布局以及设置html属性等。
4. 图片优化
请看上面说明
5. 导航
请看上面说明
6. SSG 与 SSR
SSG: 静态渲染,在服务端构建部署时,数据重新生效,产生的静态页面内容都是不变的,可以设置revalidate,没多少秒后刷新内容也会改变,访问速度快、利于SEO,针对不会变化、多页面使用的数据;SSR:动态渲染,在服务端接收到每个用户请求时,重新请求数据,重新渲染页面,针对显示实时的数据。
7. Client Component 与 Server Component
NextJs 默认将所有组件都视为服务端组件
Client Component: 客户端组件,组件顶部有 'use client'标识,,用户可以在浏览器进行页面交互,比如使用 useState、useEffect等,它既在服务器又在浏览器运行,先服务器,然后浏览器 Server Component: 服务端组件,完全在服务端渲染,因此无法进行用户交互 可以在不同的位置分别使用'use client'、'use server',标识某个部分使用client component,某个部分使用server component,从而加快页面的访问速度。
8. fetch data
请看上面说明
9. Database(mongodb)
- 连接本地数据
- 创建数据表模型
- 通过模型操作数据库
10. jwt 身份验证和授权
安装 jsonwebtoken 安装包,整个身份验证和授权过程如下:
- 通过账号密码注册用户,密码加密存取,查询数据库中是否存在当前用户,jwt签名获取token,发送用户邮箱进行确认
- 用户点击邮箱地址进行身份验证,根据拿到的token使用jwt验证格式是否正确,如正确将用户账号密码存入数据库的user表中,并将用户信息存入header且自动登录
- 用户退出登录则清除header中的用户信息
- 如果用户再次登录,用注册的账号密码进行登录,验证账号是否存在于数据表中,如果没有就是不存在当前账户,如果存在下一步
- 将数据库中查询到的账户信息的密码与前端输入的密码(加密后)进行比对,如果比对失败就是密码错误,比对成功下一步
- 使用jwt根据用户的账户信息进行签名获取token,可以对token设置有效期,签名过程中需要设置只有你自己才能知道的密钥,后续在请求的过程中就会使用token,只有设置的密钥才能解出用户信息,从而进行身份验证
- 将获取到的token存放在header中
const cookie = serialize('userInfo', JSON.stringify({token, role: user.role, userId: user._id }), {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // One week
path: '/',
})
res.setHeader('Set-Cookie', cookie)
- 后续获取token可以通过req.cookies获取
11. middleware 路由守卫
上面通过jwt获取token进行身份验证和授权,可以通过设置middleware.js文件,对部分页面或部分接口进行身份验证,从而限制部分功能或页面只有登录后的用户才能使用。
12. AI 接入, SSE即时数据更新技术
除了大家熟知的chatgpt以外,国内也有很多AI服务商,不需要外网就可以进行访问,比如阿里云的千问产品,接入方式很简单,可以参考官方文档,选择适合自己的产品,都是有赠送的token的,随便试着玩一玩~
接入方式有一次性返回结果和流式返回结果,参考chatgpt流式传输,这边使用到的server-side-event(SSE)技术。
SSE 是一种基于HTTP的服务器推送技术,允许服务器实时地向客户端推送数据。
SSE是一种服务器端到客户端的单向流式消息推送协议,它通过HTTP连接发送事件流。与传统的轮询或长轮询技术相比,SSE具有更低的延迟、更高的效率和更低的资源消耗。
实时通信技术也有我们熟知的websocket技术,websockert是双向通信,既可以从客户端推送消息到服务端,也可以从服务端推送消息到客户端,这边只需要服务端进行推送,所以SSE技术就足够了。
const completionStream = await openai.chat.completions.create({
model: "模型名称",
messages: messages,
stream: true,
max_tokens: 4096
});
const responseStream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
for await (const part of completionStream) {
const text = part.choices[0]?.delta.content ?? ''
// todo add event
const chunk = encoder.encode(`data:${text}`)
controller.enqueue(chunk)
}
controller.close()
},
})
return new Response(responseStream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
'Connection': 'keep-alive',
'Cache-Control': 'no-cache, no-transform',
},
})
// 前端获取数据
const reader = data.getReader();
const decoder = new TextDecoder();
let done = false;
while (!done) {
const { value, done: doneReading } = await reader.read();
done = doneReading;
const chunkValue = decoder.decode(value)?.replace(/data:/g, '')?.replace(/^\n{2}/, '')?.replace(/\n{2}$/, '');
// 更新值,将返回的chunkValue拼接在一起
dispatch({ type: "updatePromptAnswer", payload: chunkValue });
}
if (done) {
// 获取数据结束,可以进行存库等操作
dispatch({ type: "done" });
}