简体中文 繁體中文 English 日本語 Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français

站内搜索

搜索

活动公告

11-02 12:46
10-23 09:32
通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31
10-23 09:28
通知:签到时间调整为每日4:00(东八区)
10-23 09:26

Next.js与Prisma强强联手打造类型安全的全栈开发新体验

3万

主题

349

科技点

3万

积分

大区版主

木柜子打湿

积分
31898

三倍冰淇淋无人之境【一阶】财Doro小樱(小丑装)立华奏以外的星空【二阶】⑨的冰沙

发表于 2025-9-10 14:00:00 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
1. 引言

在现代Web开发中,类型安全和开发效率是开发者们追求的重要目标。Next.js作为React的全栈框架,提供了服务端渲染、静态站点生成和API路由等功能,而Prisma作为下一代ORM(对象关系映射),提供了类型安全的数据库访问。当这两个强大的工具结合在一起时,它们为开发者提供了一个无缝的、类型安全的全栈开发体验。

2. Next.js与Prisma简介

2.1 Next.js概述

Next.js是一个基于React的轻量级全栈框架,它提供了以下核心功能:

• 服务端渲染(SSR)
• 静态站点生成(SSG)
• API路由(构建无服务器API)
• 自动代码分割和优化
• 内置CSS和Sass支持
• 文件系统路由

Next.js允许开发者在同一个项目中构建前端界面和后端API,使得全栈开发变得更加简单和高效。

2.2 Prisma概述

Prisma是一个现代的数据库工具包,它主要由三部分组成:

1. Prisma Client:自动生成的、类型安全的查询构建器
2. Prisma Migrate:声明式数据库模式迁移系统
3. Prisma Studio:用于查看和编辑数据库数据的GUI

Prisma支持多种数据库,包括PostgreSQL、MySQL、SQLite、SQL Server、MongoDB(预览)等,通过其类型安全的查询构建器,开发者可以在编译时捕获错误,而不是在运行时。

3. 为什么选择Next.js与Prisma组合

3.1 类型安全的全栈开发

Next.js与Prisma的结合提供了端到端的类型安全。从数据库模式到前端组件,整个数据流都是类型安全的,这意味着:

• 数据库模式变更会自动反映到类型定义中
• API请求和响应的类型可以被验证
• 前端组件接收到的props类型可以被验证

这种类型安全大大减少了运行时错误,提高了代码质量和开发效率。

3.2 开发效率提升

使用Next.js和Prisma的组合,开发者可以:

• 使用相同的语言(TypeScript)进行前端和后端开发
• 自动生成类型定义,减少手动类型编写的工作量
• 快速构建和迭代全栈应用

3.3 卓越的开发者体验

Prisma的自动完成功能和类型检查,以及Next.js的热重载和快速刷新功能,为开发者提供了卓越的开发体验。开发者可以更快地编写代码,更早地发现错误,并更轻松地进行重构。

4. 设置Next.js与Prisma项目

4.1 创建Next.js项目

首先,让我们创建一个新的Next.js项目:
  1. npx create-next-app@latest my-next-prisma-app --typescript
  2. cd my-next-prisma-app
复制代码

4.2 安装和配置Prisma

接下来,安装Prisma:
  1. npm install prisma @prisma/client
  2. npx prisma init
复制代码

这将创建一个prisma目录,其中包含一个schema.prisma文件。让我们配置这个文件以使用SQLite数据库(为了简单起见,你也可以选择PostgreSQL或MySQL):
  1. // prisma/schema.prisma
  2. generator client {
  3.   provider = "prisma-client-js"
  4. }
  5. datasource db {
  6.   provider = "sqlite"
  7.   url      = env("DATABASE_URL")
  8. }
  9. model User {
  10.   id        Int      @id @default(autoincrement())
  11.   email     String   @unique
  12.   name      String?
  13.   posts     Post[]
  14.   createdAt DateTime @default(now())
  15.   updatedAt DateTime @updatedAt
  16. }
  17. model Post {
  18.   id        Int      @id @default(autoincrement())
  19.   title     String
  20.   content   String?
  21.   published Boolean  @default(false)
  22.   author    User     @relation(fields: [authorId], references: [id])
  23.   authorId  Int
  24.   createdAt DateTime @default(now())
  25.   updatedAt DateTime @updatedAt
  26. }
复制代码

4.3 设置环境变量

在项目根目录创建一个.env文件,并添加数据库URL:
  1. # .env
  2. DATABASE_URL="file:./dev.db"
复制代码

4.4 运行迁移

现在,让我们运行迁移来创建数据库:
  1. npx prisma migrate dev --name init
复制代码

这将创建数据库并应用我们定义的模式。

4.5 生成Prisma客户端

每次修改schema后,都需要重新生成Prisma客户端:
  1. npx prisma generate
复制代码

5. 在Next.js中使用Prisma

5.1 创建Prisma客户端实例

为了在Next.js应用中使用Prisma,我们需要创建一个Prisma客户端实例。为了避免在开发模式下创建多个实例,我们可以使用单例模式:
  1. // lib/prisma.ts
  2. import { PrismaClient } from '@prisma/client'
  3. const prismaClientSingleton = () => {
  4.   return new PrismaClient()
  5. }
  6. declare global {
  7.   var prisma: undefined | ReturnType<typeof prismaClientSingleton>
  8. }
  9. const prisma = globalThis.prisma ?? prismaClientSingleton()
  10. export default prisma
  11. if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma
复制代码

5.2 在API路由中使用Prisma

Next.js的API路由允许我们创建后端端点。让我们创建一个获取所有用户的API:
  1. // pages/api/users.ts
  2. import type { NextApiRequest, NextApiResponse } from 'next'
  3. import prisma from '../../lib/prisma'
  4. export default async function handler(
  5.   req: NextApiRequest,
  6.   res: NextApiResponse
  7. ) {
  8.   if (req.method === 'GET') {
  9.     try {
  10.       const users = await prisma.user.findMany({
  11.         include: {
  12.           posts: true,
  13.         },
  14.       })
  15.       res.status(200).json(users)
  16.     } catch (error) {
  17.       res.status(500).json({ error: 'Error fetching users' })
  18.     }
  19.   } else {
  20.     res.setHeader('Allow', ['GET'])
  21.     res.status(405).end(`Method ${req.method} Not Allowed`)
  22.   }
  23. }
复制代码

5.3 在getServerSideProps中使用Prisma

Next.js的getServerSideProps函数允许我们在页面渲染前从服务器获取数据。让我们创建一个用户列表页面:
  1. // pages/users.tsx
  2. import { GetServerSideProps } from 'next'
  3. import prisma from '../lib/prisma'
  4. import { User } from '@prisma/client'
  5. interface UsersPageProps {
  6.   users: (User & {
  7.     posts: {
  8.       id: number;
  9.       title: string;
  10.       published: boolean;
  11.     }[];
  12.   })[];
  13. }
  14. export default function UsersPage({ users }: UsersPageProps) {
  15.   return (
  16.     <div>
  17.       <h1>Users</h1>
  18.       <ul>
  19.         {users.map((user) => (
  20.           <li key={user.id}>
  21.             <h2>{user.name || user.email}</h2>
  22.             <p>Email: {user.email}</p>
  23.             <h3>Posts:</h3>
  24.             <ul>
  25.               {user.posts.map((post) => (
  26.                 <li key={post.id}>
  27.                   {post.title} ({post.published ? 'Published' : 'Draft'})
  28.                 </li>
  29.               ))}
  30.             </ul>
  31.           </li>
  32.         ))}
  33.       </ul>
  34.     </div>
  35.   )
  36. }
  37. export const getServerSideProps: GetServerSideProps<UsersPageProps> = async () => {
  38.   const users = await prisma.user.findMany({
  39.     include: {
  40.       posts: {
  41.         select: {
  42.           id: true,
  43.           title: true,
  44.           published: true,
  45.         },
  46.       },
  47.     },
  48.   })
  49.   return {
  50.     props: {
  51.       users: JSON.parse(JSON.stringify(users)),
  52.     },
  53.   }
  54. }
复制代码

注意:我们使用JSON.parse(JSON.stringify(users))来序列化数据,因为Prisma返回的对象包含Date对象,这些对象不能直接通过props传递。

5.4 在React组件中使用SWR获取数据

对于客户端数据获取,我们可以使用SWR(Stale-While-Revalidate)库。首先安装SWR:
  1. npm install swr
复制代码

然后创建一个获取函数:
  1. // lib/fetcher.ts
  2. const fetcher = async (url: string) => {
  3.   const res = await fetch(url)
  4.   if (!res.ok) {
  5.     const error = new Error('An error occurred while fetching the data.')
  6.     error.message = await res.json()
  7.     throw error
  8.   }
  9.   return res.json()
  10. }
  11. export default fetcher
复制代码

现在,我们可以在组件中使用SWR:
  1. // pages/posts.tsx
  2. import useSWR from 'swr'
  3. import fetcher from '../lib/fetcher'
  4. interface Post {
  5.   id: number;
  6.   title: string;
  7.   content?: string;
  8.   published: boolean;
  9.   authorId: number;
  10. }
  11. export default function PostsPage() {
  12.   const { data: posts, error } = useSWR<Post[]>('/api/posts', fetcher)
  13.   if (error) return <div>Failed to load</div>
  14.   if (!posts) return <div>Loading...</div>
  15.   return (
  16.     <div>
  17.       <h1>Posts</h1>
  18.       <ul>
  19.         {posts.map((post) => (
  20.           <li key={post.id}>
  21.             <h2>{post.title}</h2>
  22.             <p>{post.content}</p>
  23.             <p>Status: {post.published ? 'Published' : 'Draft'}</p>
  24.           </li>
  25.         ))}
  26.       </ul>
  27.     </div>
  28.   )
  29. }
复制代码

6. 类型安全的实现方式

6.1 Prisma生成类型

Prisma的一个强大功能是它能够根据数据库模式自动生成TypeScript类型。当我们运行npx prisma generate时,Prisma会生成一个@prisma/client模块,其中包含所有模型和它们的类型。

例如,对于我们的User和Post模型,Prisma会生成如下类型:
  1. export interface User {
  2.   id: number
  3.   email: string
  4.   name?: string | null
  5.   createdAt: Date
  6.   updatedAt: Date
  7. }
  8. export interface Post {
  9.   id: number
  10.   title: string
  11.   content?: string | null
  12.   published: boolean
  13.   authorId: number
  14.   createdAt: Date
  15.   updatedAt: Date
  16. }
复制代码

6.2 在API路由中使用类型

在API路由中,我们可以使用这些类型来确保请求和响应的类型安全:
  1. // pages/api/posts.ts
  2. import type { NextApiRequest, NextApiResponse } from 'next'
  3. import prisma from '../../lib/prisma'
  4. import { Post } from '@prisma/client'
  5. type ResponseData = {
  6.   message?: string
  7.   post?: Post
  8.   posts?: Post[]
  9.   error?: string
  10. }
  11. export default async function handler(
  12.   req: NextApiRequest,
  13.   res: NextApiResponse<ResponseData>
  14. ) {
  15.   if (req.method === 'GET') {
  16.     try {
  17.       const posts = await prisma.post.findMany()
  18.       res.status(200).json({ posts })
  19.     } catch (error) {
  20.       res.status(500).json({ error: 'Error fetching posts' })
  21.     }
  22.   } else if (req.method === 'POST') {
  23.     try {
  24.       const { title, content, authorId } = req.body
  25.       if (!title || !authorId) {
  26.         return res.status(400).json({ error: 'Title and authorId are required' })
  27.       }
  28.       const post = await prisma.post.create({
  29.         data: {
  30.           title,
  31.           content,
  32.           authorId,
  33.         },
  34.       })
  35.       res.status(201).json({ post })
  36.     } catch (error) {
  37.       res.status(500).json({ error: 'Error creating post' })
  38.     }
  39.   } else {
  40.     res.setHeader('Allow', ['GET', 'POST'])
  41.     res.status(405).end(`Method ${req.method} Not Allowed`)
  42.   }
  43. }
复制代码

6.3 使用Zod进行输入验证

为了进一步增强类型安全,我们可以使用Zod库来验证输入数据。首先安装Zod:
  1. npm install zod
复制代码

然后创建验证模式:
  1. // lib/validations.ts
  2. import { z } from 'zod'
  3. export const createPostSchema = z.object({
  4.   title: z.string().min(1, 'Title is required').max(100),
  5.   content: z.string().optional(),
  6.   authorId: z.number().int().positive('Author ID must be a positive integer'),
  7. })
  8. export type CreatePostInput = z.infer<typeof createPostSchema>
复制代码

现在,我们可以在API路由中使用这些验证模式:
  1. // pages/api/posts.ts
  2. import type { NextApiRequest, NextApiResponse } from 'next'
  3. import prisma from '../../lib/prisma'
  4. import { Post } from '@prisma/client'
  5. import { createPostSchema } from '../../lib/validations'
  6. type ResponseData = {
  7.   message?: string
  8.   post?: Post
  9.   posts?: Post[]
  10.   error?: string
  11.   validationErrors?: Record<string, string[]>
  12. }
  13. export default async function handler(
  14.   req: NextApiRequest,
  15.   res: NextApiResponse<ResponseData>
  16. ) {
  17.   if (req.method === 'GET') {
  18.     try {
  19.       const posts = await prisma.post.findMany()
  20.       res.status(200).json({ posts })
  21.     } catch (error) {
  22.       res.status(500).json({ error: 'Error fetching posts' })
  23.     }
  24.   } else if (req.method === 'POST') {
  25.     try {
  26.       // 验证输入
  27.       const result = createPostSchema.safeParse(req.body)
  28.       
  29.       if (!result.success) {
  30.         const formattedErrors = result.error.format()
  31.         return res.status(400).json({
  32.           validationErrors: formattedErrors as Record<string, string[]>
  33.         })
  34.       }
  35.       const { title, content, authorId } = result.data
  36.       const post = await prisma.post.create({
  37.         data: {
  38.           title,
  39.           content,
  40.           authorId,
  41.         },
  42.       })
  43.       res.status(201).json({ post })
  44.     } catch (error) {
  45.       res.status(500).json({ error: 'Error creating post' })
  46.     }
  47.   } else {
  48.     res.setHeader('Allow', ['GET', 'POST'])
  49.     res.status(405).end(`Method ${req.method} Not Allowed`)
  50.   }
  51. }
复制代码

7. 实际应用示例

让我们创建一个完整的博客应用,展示Next.js和Prisma的强大功能。

7.1 创建博客文章列表页面
  1. // pages/index.tsx
  2. import { GetServerSideProps } from 'next'
  3. import prisma from '../lib/prisma'
  4. import Link from 'next/link'
  5. interface Post {
  6.   id: number
  7.   title: string
  8.   content?: string
  9.   published: boolean
  10.   author: {
  11.     name?: string
  12.     email: string
  13.   }
  14.   createdAt: Date
  15. }
  16. interface HomePageProps {
  17.   posts: Post[]
  18. }
  19. export default function HomePage({ posts }: HomePageProps) {
  20.   return (
  21.     <div className="container mx-auto px-4">
  22.       <h1 className="text-3xl font-bold my-6">Latest Blog Posts</h1>
  23.       
  24.       {posts.length === 0 ? (
  25.         <p>No posts yet. Be the first to write!</p>
  26.       ) : (
  27.         <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
  28.           {posts.map((post) => (
  29.             <div key={post.id} className="bg-white rounded-lg shadow-md p-6">
  30.               <h2 className="text-xl font-semibold mb-2">
  31.                 <Link href={`/posts/${post.id}`} className="hover:text-blue-600">
  32.                   {post.title}
  33.                 </Link>
  34.               </h2>
  35.               <p className="text-gray-600 mb-4">
  36.                 {post.content ? post.content.substring(0, 100) + '...' : 'No content'}
  37.               </p>
  38.               <div className="flex justify-between items-center">
  39.                 <span className="text-sm text-gray-500">
  40.                   By {post.author.name || post.author.email}
  41.                 </span>
  42.                 <span className="text-sm text-gray-500">
  43.                   {new Date(post.createdAt).toLocaleDateString()}
  44.                 </span>
  45.               </div>
  46.             </div>
  47.           ))}
  48.         </div>
  49.       )}
  50.       
  51.       <div className="mt-8">
  52.         <Link href="/posts/new" className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700">
  53.           Create New Post
  54.         </Link>
  55.       </div>
  56.     </div>
  57.   )
  58. }
  59. export const getServerSideProps: GetServerSideProps<HomePageProps> = async () => {
  60.   const posts = await prisma.post.findMany({
  61.     where: { published: true },
  62.     orderBy: { createdAt: 'desc' },
  63.     include: {
  64.       author: {
  65.         select: {
  66.           name: true,
  67.           email: true,
  68.         },
  69.       },
  70.     },
  71.   })
  72.   return {
  73.     props: {
  74.       posts: JSON.parse(JSON.stringify(posts)),
  75.     },
  76.   }
  77. }
复制代码

7.2 创建博客文章详情页面
  1. // pages/posts/[id].tsx
  2. import { GetServerSideProps } from 'next'
  3. import prisma from '../../lib/prisma'
  4. import Link from 'next/link'
  5. interface Post {
  6.   id: number
  7.   title: string
  8.   content?: string
  9.   published: boolean
  10.   author: {
  11.     name?: string
  12.     email: string
  13.   }
  14.   createdAt: Date
  15.   updatedAt: Date
  16. }
  17. interface PostPageProps {
  18.   post: Post
  19. }
  20. export default function PostPage({ post }: PostPageProps) {
  21.   return (
  22.     <div className="container mx-auto px-4">
  23.       <div className="mb-6">
  24.         <Link href="/" className="text-blue-600 hover:underline">
  25.           &larr; Back to Home
  26.         </Link>
  27.       </div>
  28.       
  29.       <article className="bg-white rounded-lg shadow-md p-6">
  30.         <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
  31.         
  32.         <div className="flex justify-between items-center mb-6 text-gray-600">
  33.           <span>
  34.             By {post.author.name || post.author.email}
  35.           </span>
  36.           <span>
  37.             {new Date(post.createdAt).toLocaleDateString()}
  38.           </span>
  39.         </div>
  40.         
  41.         <div className="prose max-w-none">
  42.           {post.content ? (
  43.             <p className="whitespace-pre-line">{post.content}</p>
  44.           ) : (
  45.             <p className="text-gray-500">No content available.</p>
  46.           )}
  47.         </div>
  48.         
  49.         <div className="mt-8 pt-6 border-t border-gray-200 text-sm text-gray-500">
  50.           <p>
  51.             Created: {new Date(post.createdAt).toLocaleString()}
  52.           </p>
  53.           {post.updatedAt.getTime() !== post.createdAt.getTime() && (
  54.             <p>
  55.               Updated: {new Date(post.updatedAt).toLocaleString()}
  56.             </p>
  57.           )}
  58.         </div>
  59.       </article>
  60.       
  61.       <div className="mt-6">
  62.         <Link href={`/posts/${post.id}/edit`} className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 mr-2">
  63.           Edit Post
  64.         </Link>
  65.       </div>
  66.     </div>
  67.   )
  68. }
  69. export const getServerSideProps: GetServerSideProps<PostPageProps, { id: string }> = async (context) => {
  70.   const { id } = context.params!
  71.   const post = await prisma.post.findUnique({
  72.     where: { id: parseInt(id) },
  73.     include: {
  74.       author: {
  75.         select: {
  76.           name: true,
  77.           email: true,
  78.         },
  79.       },
  80.     },
  81.   })
  82.   if (!post) {
  83.     return {
  84.       notFound: true,
  85.     }
  86.   }
  87.   return {
  88.     props: {
  89.       post: JSON.parse(JSON.stringify(post)),
  90.     },
  91.   }
  92. }
复制代码

7.3 创建博客文章编辑页面
  1. // pages/posts/[id]/edit.tsx
  2. import { GetServerSideProps } from 'next'
  3. import { useState } from 'react'
  4. import { useRouter } from 'next/router'
  5. import prisma from '../../../lib/prisma'
  6. import { Post } from '@prisma/client'
  7. interface EditPostPageProps {
  8.   post: Post
  9. }
  10. export default function EditPostPage({ post }: EditPostPageProps) {
  11.   const router = useRouter()
  12.   const [title, setTitle] = useState(post.title)
  13.   const [content, setContent] = useState(post.content || '')
  14.   const [published, setPublished] = useState(post.published)
  15.   const [isSaving, setIsSaving] = useState(false)
  16.   const [error, setError] = useState('')
  17.   const handleSubmit = async (e: React.FormEvent) => {
  18.     e.preventDefault()
  19.     setIsSaving(true)
  20.     setError('')
  21.     try {
  22.       const response = await fetch(`/api/posts/${post.id}`, {
  23.         method: 'PUT',
  24.         headers: {
  25.           'Content-Type': 'application/json',
  26.         },
  27.         body: JSON.stringify({
  28.           title,
  29.           content,
  30.           published,
  31.         }),
  32.       })
  33.       if (!response.ok) {
  34.         throw new Error('Failed to update post')
  35.       }
  36.       await router.push(`/posts/${post.id}`)
  37.     } catch (err) {
  38.       setError('An error occurred while updating the post. Please try again.')
  39.       console.error(err)
  40.     } finally {
  41.       setIsSaving(false)
  42.     }
  43.   }
  44.   return (
  45.     <div className="container mx-auto px-4">
  46.       <div className="mb-6">
  47.         <Link href={`/posts/${post.id}`} className="text-blue-600 hover:underline">
  48.           &larr; Back to Post
  49.         </Link>
  50.       </div>
  51.       
  52.       <h1 className="text-3xl font-bold mb-6">Edit Post</h1>
  53.       
  54.       {error && (
  55.         <div className="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
  56.           {error}
  57.         </div>
  58.       )}
  59.       
  60.       <form onSubmit={handleSubmit} className="bg-white rounded-lg shadow-md p-6">
  61.         <div className="mb-4">
  62.           <label htmlFor="title" className="block text-gray-700 font-bold mb-2">
  63.             Title
  64.           </label>
  65.           <input
  66.             type="text"
  67.             id="title"
  68.             value={title}
  69.             onChange={(e) => setTitle(e.target.value)}
  70.             className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
  71.             required
  72.           />
  73.         </div>
  74.         
  75.         <div className="mb-4">
  76.           <label htmlFor="content" className="block text-gray-700 font-bold mb-2">
  77.             Content
  78.           </label>
  79.           <textarea
  80.             id="content"
  81.             value={content}
  82.             onChange={(e) => setContent(e.target.value)}
  83.             rows={10}
  84.             className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
  85.           />
  86.         </div>
  87.         
  88.         <div className="mb-6">
  89.           <label className="flex items-center">
  90.             <input
  91.               type="checkbox"
  92.               checked={published}
  93.               onChange={(e) => setPublished(e.target.checked)}
  94.               className="form-checkbox h-5 w-5 text-blue-600"
  95.             />
  96.             <span className="ml-2 text-gray-700">Published</span>
  97.           </label>
  98.         </div>
  99.         
  100.         <div className="flex items-center justify-between">
  101.           <button
  102.             type="submit"
  103.             disabled={isSaving}
  104.             className={`bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline ${
  105.               isSaving ? 'opacity-50 cursor-not-allowed' : ''
  106.             }`}
  107.           >
  108.             {isSaving ? 'Saving...' : 'Save Changes'}
  109.           </button>
  110.          
  111.           <button
  112.             type="button"
  113.             onClick={() => router.push(`/posts/${post.id}`)}
  114.             className="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
  115.           >
  116.             Cancel
  117.           </button>
  118.         </div>
  119.       </form>
  120.     </div>
  121.   )
  122. }
  123. export const getServerSideProps: GetServerSideProps<EditPostPageProps, { id: string }> = async (context) => {
  124.   const { id } = context.params!
  125.   const post = await prisma.post.findUnique({
  126.     where: { id: parseInt(id) },
  127.   })
  128.   if (!post) {
  129.     return {
  130.       notFound: true,
  131.     }
  132.   }
  133.   return {
  134.     props: {
  135.       post: JSON.parse(JSON.stringify(post)),
  136.     },
  137.   }
  138. }
复制代码

7.4 创建API路由处理博客文章的CRUD操作
  1. // pages/api/posts/[id].ts
  2. import type { NextApiRequest, NextApiResponse } from 'next'
  3. import prisma from '../../../../lib/prisma'
  4. import { Post } from '@prisma/client'
  5. import { updatePostSchema } from '../../../../lib/validations'
  6. type ResponseData = {
  7.   message?: string
  8.   post?: Post
  9.   error?: string
  10.   validationErrors?: Record<string, string[]>
  11. }
  12. export default async function handler(
  13.   req: NextApiRequest,
  14.   res: NextApiResponse<ResponseData>
  15. ) {
  16.   const { id } = req.query
  17.   if (req.method === 'GET') {
  18.     try {
  19.       const post = await prisma.post.findUnique({
  20.         where: { id: Number(id) },
  21.         include: {
  22.           author: {
  23.             select: {
  24.               id: true,
  25.               name: true,
  26.               email: true,
  27.             },
  28.           },
  29.         },
  30.       })
  31.       if (!post) {
  32.         return res.status(404).json({ error: 'Post not found' })
  33.       }
  34.       res.status(200).json({ post })
  35.     } catch (error) {
  36.       res.status(500).json({ error: 'Error fetching post' })
  37.     }
  38.   } else if (req.method === 'PUT') {
  39.     try {
  40.       // 验证输入
  41.       const result = updatePostSchema.safeParse(req.body)
  42.       
  43.       if (!result.success) {
  44.         const formattedErrors = result.error.format()
  45.         return res.status(400).json({
  46.           validationErrors: formattedErrors as Record<string, string[]>
  47.         })
  48.       }
  49.       const { title, content, published } = result.data
  50.       const post = await prisma.post.update({
  51.         where: { id: Number(id) },
  52.         data: {
  53.           title,
  54.           content,
  55.           published,
  56.         },
  57.       })
  58.       res.status(200).json({ post })
  59.     } catch (error) {
  60.       res.status(500).json({ error: 'Error updating post' })
  61.     }
  62.   } else if (req.method === 'DELETE') {
  63.     try {
  64.       await prisma.post.delete({
  65.         where: { id: Number(id) },
  66.       })
  67.       res.status(204).end()
  68.     } catch (error) {
  69.       res.status(500).json({ error: 'Error deleting post' })
  70.     }
  71.   } else {
  72.     res.setHeader('Allow', ['GET', 'PUT', 'DELETE'])
  73.     res.status(405).end(`Method ${req.method} Not Allowed`)
  74.   }
  75. }
复制代码

8. 最佳实践和性能优化

8.1 数据库查询优化

使用Prisma时,有一些优化数据库查询的最佳实践:

1. 选择性加载字段:只查询你需要的字段,而不是加载整个对象。
  1. // 不推荐
  2. const users = await prisma.user.findMany()
  3. // 推荐
  4. const users = await prisma.user.findMany({
  5.   select: {
  6.     id: true,
  7.     name: true,
  8.     email: true,
  9.   },
  10. })
复制代码

1. 使用分页:对于可能返回大量结果的查询,使用分页。
  1. const page = 1
  2. const pageSize = 10
  3. const posts = await prisma.post.findMany({
  4.   skip: (page - 1) * pageSize,
  5.   take: pageSize,
  6.   orderBy: { createdAt: 'desc' },
  7. })
复制代码

1. 批量操作:使用批量操作减少数据库往返。
  1. // 不推荐
  2. for (const post of posts) {
  3.   await prisma.post.update({
  4.     where: { id: post.id },
  5.     data: { published: true },
  6.   })
  7. }
  8. // 推荐
  9. await prisma.post.updateMany({
  10.   where: { id: { in: posts.map(p => p.id) } },
  11.   data: { published: true },
  12. })
复制代码

8.2 缓存策略

在Next.js中实现缓存可以提高性能:

1. 使用SWR进行客户端缓存:
  1. import useSWR from 'swr'
  2. import fetcher from '../lib/fetcher'
  3. function Posts() {
  4.   const { data: posts, error } = useSWR('/api/posts', fetcher, {
  5.     refreshInterval: 3000, // 每3秒刷新一次
  6.     revalidateOnFocus: true, // 窗口聚焦时重新验证
  7.   })
  8.   if (error) return <div>Failed to load</div>
  9.   if (!posts) return <div>Loading...</div>
  10.   return (
  11.     <ul>
  12.       {posts.map((post) => (
  13.         <li key={post.id}>{post.title}</li>
  14.       ))}
  15.     </ul>
  16.   )
  17. }
复制代码

1. 使用getStaticProps进行静态生成:
  1. export async function getStaticProps() {
  2.   const posts = await prisma.post.findMany({
  3.     where: { published: true },
  4.     orderBy: { createdAt: 'desc' },
  5.   })
  6.   return {
  7.     props: {
  8.       posts: JSON.parse(JSON.stringify(posts)),
  9.     },
  10.     revalidate: 60, // 每60秒重新生成页面
  11.   }
  12. }
复制代码

8.3 Prisma中间件

Prisma中间件允许你在查询执行前后运行自定义逻辑,这对于日志记录、性能监控等非常有用:
  1. // prisma/middleware.ts
  2. import { Prisma } from '@prisma/client'
  3. export const prismaMiddleware: Prisma.Middleware = async (params, next) => {
  4.   const start = Date.now()
  5.   
  6.   const result = await next(params)
  7.   
  8.   const end = Date.now()
  9.   const duration = end - start
  10.   
  11.   console.log(`Query ${params.model}.${params.action} took ${duration}ms`)
  12.   
  13.   return result
  14. }
复制代码

然后在Prisma客户端实例上应用中间件:
  1. // lib/prisma.ts
  2. import { PrismaClient } from '@prisma/client'
  3. import { prismaMiddleware } from '../prisma/middleware'
  4. const prismaClientSingleton = () => {
  5.   const client = new PrismaClient()
  6.   client.$use(prismaMiddleware)
  7.   return client
  8. }
  9. // ... 其余代码不变
复制代码

9. 常见问题和解决方案

9.1 解决序列化问题

Prisma返回的对象包含Date对象,这些对象不能直接通过props传递。以下是几种解决方案:

1. 使用JSON.parse(JSON.stringify()):
  1. export const getServerSideProps: GetServerSideProps = async () => {
  2.   const posts = await prisma.post.findMany()
  3.   
  4.   return {
  5.     props: {
  6.       posts: JSON.parse(JSON.stringify(posts)),
  7.     },
  8.   }
  9. }
复制代码

1. 创建自定义序列化函数:
  1. // lib/serialize.ts
  2. export function serialize<T>(obj: T): T {
  3.   return JSON.parse(JSON.stringify(obj))
  4. }
复制代码

然后在代码中使用:
  1. import { serialize } from '../lib/serialize'
  2. export const getServerSideProps: GetServerSideProps = async () => {
  3.   const posts = await prisma.post.findMany()
  4.   
  5.   return {
  6.     props: {
  7.       posts: serialize(posts),
  8.     },
  9.   }
  10. }
复制代码

1. 使用superjson:
  1. npm install superjson
复制代码

然后创建一个自定义的appWithTranslation:
  1. // lib/withSuperjson.ts
  2. import superjson from 'superjson'
  3. import { AppProps } from 'next/app'
  4. export function withSuperjson({ Component, pageProps }: AppProps) {
  5.   return <Component {...pageProps} />
  6. }
  7. export function serializeData(data: any) {
  8.   return superjson.serialize(data).json
  9. }
  10. export function deserializeData(data: any) {
  11.   return superjson.deserialize(data)
  12. }
复制代码

9.2 环境变量管理

在Next.js项目中管理环境变量时,需要注意以下几点:

1. 创建.env.local文件用于本地开发:
  1. # .env.local
  2. DATABASE_URL="file:./dev.db"
复制代码

1. 在生产环境中设置环境变量:

在部署平台(如Vercel、Netlify等)上设置环境变量,不要将.env文件提交到版本控制系统。

1. 在Next.js中访问环境变量:
  1. // 在服务器端访问
  2. console.log(process.env.DATABASE_URL)
  3. // 在客户端访问(需要以NEXT_PUBLIC_为前缀)
  4. console.log(process.env.NEXT_PUBLIC_API_URL)
复制代码

9.3 数据库连接管理

在开发模式下,Next.js的热重载可能会导致创建多个数据库连接。以下是解决方案:

1. 使用单例模式(如前面所示):
  1. // lib/prisma.ts
  2. import { PrismaClient } from '@prisma/client'
  3. const prismaClientSingleton = () => {
  4.   return new PrismaClient()
  5. }
  6. declare global {
  7.   var prisma: undefined | ReturnType<typeof prismaClientSingleton>
  8. }
  9. const prisma = globalThis.prisma ?? prismaClientSingleton()
  10. export default prisma
  11. if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma
复制代码

1. 在Vercel上使用Serverless函数:

在Vercel上,每个请求都会创建一个新的函数实例,因此不需要担心连接池问题。但是,你应该确保在函数结束时关闭Prisma连接:
  1. // pages/api/users.ts
  2. import type { NextApiRequest, NextApiResponse } from 'next'
  3. import prisma from '../../lib/prisma'
  4. export default async function handler(
  5.   req: NextApiRequest,
  6.   res: NextApiResponse
  7. ) {
  8.   try {
  9.     const users = await prisma.user.findMany()
  10.     res.status(200).json(users)
  11.   } catch (error) {
  12.     res.status(500).json({ error: 'Error fetching users' })
  13.   } finally {
  14.     await prisma.$disconnect()
  15.   }
  16. }
复制代码

10. 结论

Next.js与Prisma的结合为现代Web应用开发提供了强大的工具组合。通过类型安全的数据库访问、自动生成的类型定义和灵活的渲染策略,开发者可以构建高效、可维护的全栈应用。

在本文中,我们详细介绍了如何设置Next.js与Prisma项目,如何实现类型安全,以及如何构建一个完整的博客应用。我们还探讨了最佳实践、性能优化和常见问题的解决方案。

通过采用Next.js和Prisma,开发者可以享受以下优势:

• 端到端的类型安全
• 高效的开发体验
• 灵活的数据获取策略
• 强大的数据库查询能力
• 优化的性能

随着这两个工具的不断发展,我们可以期待更多的功能和改进,使全栈开发变得更加简单和高效。

希望本文能够帮助你开始使用Next.js和Prisma构建类型安全的全栈应用。Happy coding!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

频道订阅

频道订阅

加入社群

加入社群

联系我们|TG频道|RSS

Powered by Pixtech

© 2025 Pixtech Team.