J'Blog

使用 nextJS 开发本站记录

使用 nextJS 开发本站记录

nextJS特性

fullstack framework(全栈框架)

相较于React,nextJS更全面,React只是个UI框架,开发项目还需要引入其他服务;使用nextJS可以在一个项目里干所有的事情,包括使用nodejs访问数据库...

Server-side Rendering(服务端渲染)

相较于React,使用服务端渲染页面,页面预渲染加快页面的渲染速度,便于SEO;使用场景:注重页面渲染速度以及SEO的项目可以使用nextJS

file-system based router(基于文件系统的路由)

相较于React,不用额外配置路由 路由包含App RouterPages Router两个方式

项目初始化命令

npx create-next-app@latest

脚手架初始化会提示是否要以App Router作为路由方式,默认是App Router,

页面跳转方式

  1. <Link>组件
// Navigate to /about
<Link href='/about'></Link>
// Navigate to /about?name=test
<Link href={{
    pathname: '/about',
    query: { name: 'test' },
}}
  1. 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>
  )
}
  1. 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')
  }
 
  // ...
}
  1. 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)

  1. 连接本地数据
  2. 创建数据表模型
  3. 通过模型操作数据库

10. jwt 身份验证和授权

安装 jsonwebtoken 安装包,整个身份验证和授权过程如下:

  1. 通过账号密码注册用户,密码加密存取,查询数据库中是否存在当前用户,jwt签名获取token,发送用户邮箱进行确认
  2. 用户点击邮箱地址进行身份验证,根据拿到的token使用jwt验证格式是否正确,如正确将用户账号密码存入数据库的user表中,并将用户信息存入header且自动登录
  3. 用户退出登录则清除header中的用户信息
  4. 如果用户再次登录,用注册的账号密码进行登录,验证账号是否存在于数据表中,如果没有就是不存在当前账户,如果存在下一步
  5. 将数据库中查询到的账户信息的密码与前端输入的密码(加密后)进行比对,如果比对失败就是密码错误,比对成功下一步
  6. 使用jwt根据用户的账户信息进行签名获取token,可以对token设置有效期,签名过程中需要设置只有你自己才能知道的密钥,后续在请求的过程中就会使用token,只有设置的密钥才能解出用户信息,从而进行身份验证
  7. 将获取到的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)
  1. 后续获取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" });
   }