
读者好,我是 Randy, 一个前端开发工程师,非常感谢你对这本书有兴趣。这是一本十分迷你的书,很快你就可以把它读完。
我在业余的时间做了很多全栈 SaaS 应用,这些应用都是使用 Next.js 来做的。Next.js 的好处是它让你不需要关注业务开发无关的东西(例如前端构建配置,前后端结合的胶水代码等等),你可以专心编写你的 React 页面,以及所需要的 HTTP API. 更神奇的是,前后端可以共用一份 TypeScript 类型声明,让你在写代码的时候更安全。
但事实上在头几次用 Next.js 开发应用的时候,它并没有我想像中那么好用。在像 Express/Koa/Nest/Hapi 这些框架,它们都自带了 Web Server 开发的一些 helper. 例如利用它们的中间件机制,可以实现面向切面编程(AOP),大量地降低编写重复代码的情况;它们都自带了统一的错误处理机制,你可以在程序的任何角落抛出一个 HTTP 异常。但在 Next.js 里,这些都是缺失的 —— 它没有中间件机制,也没有错误处理的机制(所有 throw error 在 Next.js 的 API route 都会向客户端响应一个无意义的 500 Internal Server Error)。甚至在 Next.js 里,也没有自带的向客户端写入 cookies 的 helper. 这使得我在实现用户登录的时候,要研究写入 cookies 的最佳实践。
全栈应用也少不了前端页面的开发。数据请求我应该直接用 axios, 还是使用像 react-query 和 swr 这样的数据请求封装?它们两个我应该用哪个?在复杂的项目里,我应该如何正确地使用它们?这些都是我在头几次开发时候不知道的。
还好,在做了几个项目之后,我大概总结了一些非常有用的「最佳实践」,把上面提到的很多在 Next.js 中缺失的东西,以及在前端开发中对数据请求的处理,都研究出了很好的解决方法。这让我在接下来开发新的应用的时候非常高效,而且写出来的 HTTP API 的健壮性可以和用传统 Web 框架(Express/Koa/Hapi)写出来的匹敌。当然我的这些最佳实践就是借鉴它们的。
本来在开发完 Cusdis 后我打算开始开发下一个 SaaS 应用,但仔细一想,不如先把我用 Next.js 开发全栈应用的这些宝贵经验总结起来,写成一本小书,分享给更多人。于是就有了你现在正在读的这一本小书。
这本书面向的读者,是那些想要或者已经正在使用 Next.js 开发一个全栈应用的开发者。如果你从来没有用 Next.js 完整开发过一个应用,你可以在本书看到一个大概的流程和实践。如果你已经有一定的使用经验,那么你可能仍然可以从这本书的例子中得到启发,让你可以把一些重复的代码变得通用。
这不是一本教你什么是 Next.js 的教程。我不会在这本书教你什么是 getServerSideProps, 什么是 res.status(), 更不会教你什么是箭头函数,什么是 async/await. 我假定读者已经知道这些基本的东西,我也不认为你应该把钱花在一本会变的书里学习这些经常会变的东西,你应该在它们的官方文档中学习。这就是为什么这本书这么便宜,因为在很多技术书里,读者会为很多在官方文档里已经有的东西买单。这本书只兜售在文档中没有的实践经验。
在头几个章节,我会大致介绍一些技术:
你会发现我并没有花很多笔墨去讲解这些库(再次重申,你应该在它们的官方文档去学习它们的 API, 而不是在一本书里)。我只会重点讲解它们是什么,它们解决了什么问题,它们如何与 Next.js 整合。
因为本书的精华部分在后面的两个「实例」,你可以从两个「实例」中学习到如何在 Next.js 中应用这些技术,从而使你的开发效率倍增。也就是说,头几个介绍这些库的章节,只是在为后面的两个「实例」作铺垫。我希望读者可以从这两个「实例」中提炼出其中那些不变的开发思路。
你可以免费阅读本书的 HTML 版本。也可以可以在 这里 购买这本书的 PDF 版本。 本书定价 12 元人民币,这是因为我人生中买的第一本编程书就是 12 块钱,仅以此对那本启蒙我的读物致敬。
如果你希望转载这本小书的某些章节,请联系我的邮箱,支付 30 元人民币作为转载费用。
如果读完本书后,你学到了一点什么,请不吝在 Twitter 上给我反馈,我的 Twitter ID 是 @randyloop. 又或者直接给我 ([email protected]) 写信。如果可以为此写一篇博客,那就更好了。
倘若你读完觉得这本书是垃圾,我只能说我为这本垃圾所花的时间和精力,应该也还是值 12 块钱。
无论如何,感谢你的阅读。
在这一章节,将介绍前端数据请求的方案。为什么前端数据请求需要专门讨论?甚至需要用一个专门的库?我们先来看一个最简单的,用 axios 发送请求的例子,我们通过请求 GET https://jsonplaceholder.typicode.com/posts 获取所有文章,通过 POST https://jsonplaceholder.typicode.com/posts 添加一篇文章:
xxxxxxxxxx481import axios from 'axios'2import React from 'react'34async function getPosts() {5 const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')6 return result.data7}89async function addPost(title, body) {10 await axios.post('https://jsonplaceholder.typicode.com/posts', {11 title, body12 })13}1415function IndexPage() {1617 const [posts, setPosts] = React.useState([])1819 async function initial() {20 const result = await getPosts()21 setPosts(result)22 }2324 React.useEffect(() => {25 initial()26 }, [])2728 async function onClickAddPost() {29 await addPost('foo', 'bar')30 }3132 return (33 <>34 <div>35 {posts.map(post => {36 return (37 <div key={post.id}>38 {post.title}39 </div>40 )41 })}42 </div>43 <button onClick={onClickAddPost}>add post</button>44 </>45 )46}4748export default IndexPage
在这个例子里,共有两个请求,一个是用于获取所有文章的 GET 请求,另一个是用于创建文章 的 POST 请求。
在真实的应用中,我们通常需要知道每个异步请求的状态,以便显示不同的 UI. 例如,在获取所有文章时,如果请求尚未结束,则在 UI 中显示 <div>Loading...</div>. 另外,一个异步请求还有成功和失败的状态,分别也有对应的 UI.
所以,每一个异步请求,实际上是一个独立的「状态机」,它们都在几个状态中流转:
isLoading: 请求是否在进行data: 如果请求成功,它的响应数据error: 如果请求失败,它的响应数据如果每一个异步请求都要我们都要为它们写维护这些状态的代码,那么代码量会非常多,而且都是基本相同的逻辑。
当然,我们可以把这些状态封装成一个可复用的 hooks. 但数据请求还面临另一个问题:当一个请求完成之后,它可能需要更新其它的请求数据。例如在上面的例子,当我们通过接口创建了一篇文章后,另一个获取文章的接口数据已经不是最新的了,如果我们想在 UI 尽快保持显示最新的数据,我们必须重新请求已经过期的那些数据对应的请求。然而异步请求散落在整个应用的不同地方,很难把它们都一起更新。
react-query 能很好地解决这些问题。
在 Next.js 中引入 react-query
xxxxxxxxxx11$ yarn add react-query在
pages/_app.tsx中, 在顶层组件套上QueryClientProvider:xxxxxxxxxx151// pages/_app.tsx23import React from 'react'4import { QueryClient, QueryClientProvider } from 'react-query'56export const queryClient = new QueryClient()78export default function MyApp({ Component, pageProps }) {910return (11<QueryClientProvider client={queryClient}>12<Component {pageProps} />13</QueryClientProvider>14)15}
在 react-query 中,有两个关键的概念 —— Query 和 Mutation.
任何获取数据的请求都是 Query, 下面是用 react-query 获取所有文章数据的例子:
xxxxxxxxxx331// pages/index.tsx23import axios from 'axios'4import React from 'react'5import { useQuery } from 'react-query'67async function getPosts() {8 const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')9 return result.data10}1112function IndexPage() {1314 const getPostsQuery = useQuery('getPosts', getPosts)1516 return (17 <>18 <div>19 {getPostsQuery.isLoading && <div>Loading...</div>}20 {getPostsquery.error && <div>Something error</div>}21 {getPostsQuery.data?.map(post => {22 return (23 <div key={post.id}>24 {post.title}25 </div>26 )27 })}28 </div>29 </>30 )31}3233export default IndexPage和之前的例子一样,在 getPosts() 中我们用 axios 发起了一个 GET 请求。不同的是,我们使用 useQuery() 把这个请求方法套了一层,并给了它一个 key: getPosts. 这个 key 的作用,我们之后会提到。
从上面的代码可以看到,用 useQuery() 这个 hooks 包装过的请求,他有 isLoading, error, data 这三个属性,我们不需要手动地管理这些状态,react-query 会处理好它们,我们只需要在 UI 上获取它们并按照我们的业务逻辑进行显示就可以了。
上面的例子中,
getPostsQuery.data?.map用了 optional chaining 的语法。因为在异步请求完成之前还没有响应的数据,它有可能是空的,这时如果用data.map, 会抛出异常。用 optional chaining 可以避免这种错误.
另外,react-query 会自动替你缓存 Query 的返回,当在另一个组件中,同样有 Query 使用了 getPosts 这个 key, react-query 会首先返回这个 key 对应的缓存数据,用户就可以直接看到数据,而非一个 loading 状态了。这就是 query key 的作用,他可以给请求一个标识,方便后续我们可以用这个标识对请求进行操作和优化。
对资源进行修改的请求(POST, PUT, DELETE 等)都是 Mutation. 下面是在我们上面的例子中,加入了创建文章这个请求,像 useQuery 一样,用 useMutation() 嵌套这个请求方法。
xxxxxxxxxx471// pages/index.tsx23import axios from 'axios'4import React from 'react'5import { useMutation, useQuery } from 'react-query'67async function getPosts() {8 const result = await axios.get('https://jsonplaceholder.typicode.com/posts?_limit=5')9 return result.data10}1112async function addPost({ title, body }) {13 await axios.post('https://jsonplaceholder.typicode.com/posts', {14 title, body15 })16}1718function IndexPage() {1920 const getPostsQuery = useQuery('getPosts', getPosts)2122 const addPostMutation = useMutation(addPost)2324 function onClickAddPost() {25 addPostMutation.mutate({ title: 'foo', body: 'bar' })26 }2728 return (29 <>30 <div>31 {getPostsQuery.isLoading && <div>Loading...</div>}32 {getPostsQuery.data?.map(post => {33 return (34 <div key={post.id}>35 {post.title}36 </div>37 )38 })}3940 {addPostMutation.isLoading && <div>Adding post...</div>}41 <button onClick={onClickAddPost}>Add post</button>42 </div>43 </>44 )45}4647export default IndexPage和 Query 相同,Mutation 同样有 isLoading, error, data 这些状态。在点击 button 后,我们用 .muate() 方法执行这个请求,并且传入了一些变量,这些变量会被传递到方法 addPost 中。
我们还可以在 .muate() 中传入 onSuccess 和 onError, 分别指定在请求成功和请求错误后执行的逻辑。比如弹一个 toast 之类的。
xxxxxxxxxx101function onClickAddPost() {2 addPostMutation.mutate({ title: 'foo', body: 'bar' }, {3 onSuccess(data) {4 // ...5 },6 onError(err) {7 // ...8 }9 })10}上面我们提到过,当用户创建了新文章后,理论上请求所有文章这个请求的数据就已经不是最新的了。这时如果我们想再次请求获取所有文章这个接口,可以在 onSuccess 中,手动地执行这个请求的 refetch 方法:
xxxxxxxxxx101function onClickAddPost() {2 addPostMutation.mutate({ title: 'foo', body: 'bar' }, {3 onSuccess(data) {4+ getPostsQuery.refetch()5 },6 onError(err) {7 // ...8 }9 })10}但这不是一个很好的实践,想像一下,这个获取文章的接口,有可能在这个应用中的多个组件用到,难道我们需要一个一个地执行 refetch 吗?当然不是。这时就要介绍 react-query 的一个重要功能 —— Query Invalidation.
Query Invalidation 的意思是「使请求失效」。我们可以用 queryClient 实例的 invalidateQueries() 方法,使某个 key 的请求失效。react-query 会自动重新请求失效的 Query.
xxxxxxxxxx121+ import { queryClient } from './_app'2function onClickAddPost() {3 addPostMutation.mutate({ title: 'foo', body: 'bar' }, {4 onSuccess(data) {5- getPostsQuery.refetch()6+ queryClient.invalidateQueries('getPosts')7 },8 onError(err) {9 // ...10 }11 })12}我们用 queryClient.invalidateQueries('getPosts') 使用 key 为 getPosts 的 Query 失效,那么所有用到 key 为 getPosts 的 Query 都会被重新请求,并更新最新的数据到 UI 上。
Next.js 没有中间件机制。首先让我简单解释一下什么是中间件,为什么我们需要中间件。
在 Express/Koa, 我们可以用中间件进入一个请求的生命周期,一个典型的中间件是一个函数,它接受 req, res 和 next 参数:
xxxxxxxxxx91function exampleMiddleware(req, res, next) {2 if (/** ...*/) {3 req.foo = 'bar'4 next()5 } else {6 res.statusCode = 4037 res.send('forbiddon')8 }9}这个中间件的模式起源于一个 Node.js Web 框架 connect, 早期的 Express 也基于 Connect 开发,于是很多框架也兼容了这种模式,所以这种中间件模式我们通常称为 connect 中间件。
在中间件里,我们可以:
req 对象注入一些属性,这些属性可以被下一个中间件或者 controller 获取到。next() 来中止请求,同时修改 res 的属性从而改变 response 的状态。这使得中间件可以很好地使代码在不同的路由之间重用。假设我们需要在一个路由跟据 cookies 获取用户信息,我们可以把这个获取用户信息的方法写成中间件,然后把用户信息注入到 req.user,这样所以使用了这个中间件的路由可以通过 req.user 取得用户信息。而且在中间件中,如果判断用户没有登录,可以中止这个请求,并返回 403.
下面是 Express 编写和使用中间件的例子:
xxxxxxxxxx231function authMiddleware(req, res, next) {2 // 假设 cookies 中用 `token` 保存用户信息3 if (req.cookies.token) {4 const user = getUserByToken(req.cookies.token)5 req.user = user6 next()7 } else {8 // cookies.token 不存在,中止请求并返回 4039 res.statusCode = 40310 res.send('please sign in first')11 }12}1314// 不使用这个中间件的路由15app.get('/', (req, res) => {16 res.send('hello world')17})1819// 使用这个中间件的路由20app.get('/profile', authMiddleware, (req, res) => {21 // 可以通过 `req.user` 取得用户信息22 res.send(`welcome! ${req.user.name}`)23})如果在 Next.js 要做同样的事,我们会这么做:
xxxxxxxxxx221// pages/api/example.ts23function auth(req, res) {4 if (req.cookies.token) {5 const user = getUserByToken(req.cookies.token)6 return user7 } else {8 // 用户未登录9 res.status(403)10 res.send('please sign in first')11 }12}1314export default (req, res) => {15 if (req.method === 'GET') {16 res.send('hello')17 } else if (req.method === 'POST') {18 const user = auth(req, res)19 console.log('do other things')20 res.send(`welcome! ${user.name}`)21 }22}但在 Next.js, 我们没有任何办法中止请求。理论上 console.log('do other things') 在用户未登录时不应该被执行。
next-connect要在 Next.js 中像 Express/Koa 这样使用 connect 中间件,我们可以使用 next-connect 这个库。
安装
next-connect:xxxxxxxxxx11$ yarn add next-connect
现在,让我们用 next-connect 重写上面的例子:
xxxxxxxxxx191// pages/api/example.ts23import nc from 'next-connect'45function authMiddleware(req, res, next) {6 res.status(403)7 res.send('please sign in first')8}910// 用 `nc()` 创建一个 api handler11const handler = nc()12 .get((req, res) => {13 res.send('hello')14 })15 .post(authMiddleware, (req,res) => {16 res.send('hello')17 })1819export default handler可以看到,现在我们在 Next.js 的API route 可以像在 Express 一样使用中间件。
在 authMiddleware中,我们返回了一个 403,并且没有执行 next(), 模拟了用户未登录的情况。由于 next() 没有执行,这个 POST 请求不会执行这个 POST handler 的代码。
用 next-connect 的另一个好处是,我们可以用.get(), .post(), put() 这样的 helper 来创建对应的 handler, 而不需要用 if (req.method === XXX) 这样的判断。让代码更好读。
因为 next-connect 兼容 connect 中间件,所以我们可以直接用社区上成熟的 connect 中间件,例如用于修改跨域设置的中间件 cors:
安装
cors:xxxxxxxxxx11$ yarn add cors
xxxxxxxxxx201// pages/api/example.ts23import nc from 'next-connect'4+ import * as cors from 'cors'56const corsOptions = {7 origin: 'http://example.com',8 optionsSuccessStatus: 2009}1011const handler = nc()12+ .use(cors(corsOptions))13 .get((req, res) => {14 res.send('hello')15 })16 .post(authMiddleware, (req,res) => {17 res.send('hello')18 })1920export default handler在实现 HTTP API 时,一个良好的实践应该在出现错误的时候响应错误对应的状态码。例如在资源不存在的时候返回 404,在没有权限时返回 403.
在一个复杂的 Web 应用,业务逻辑一般单独存在于 service (model) 层,例如一个根据 id 获取项目信息的 API:
xxxxxxxxxx101class ProjectService {2 getProjectById(id) {3 const project = db.project.find(id)4 if (!project) {5 // TODO: 响应 4046 } else {7 return project8 }9 }10}当 project 找不到时,我们应该返回一个 404 的响应。但在 service (model) 层,我们很难接触到 res 对象,所以很难在 service (model) 层修改响应。也无法在这个层面中止响应。
我们来看看在 Express 中怎么做。Express 有自带的 error handler:,当 catch 到任何地方的 error 后,响应的格式会根据 error 的对象自动改变。也就是说,我们可以通过 throw error 来改变响应的信息:
xxxxxxxxxx181class ProjectService {2 getProjectById(id) {3 const project = db.project.find(id)4 if (!project) {5 throw {6 status: 4047 }8 } else {9 return project10 }11 }12}1314app.get('/project/:id', (req, res) => {15 const projectService = new ProjectService()16 const project = service.getProjectById(req.params.id)17 res.json({ data: project })18})上面的代码中,当 project 无法找到时,会抛出一个 { status: 404 }, Express 在捕获后,会把 res.statusCode 设为 404。
另一个流行的 web 框架 hapi 还提供了 @hapi/boom 这个模块,提供了很多 helper 用于抛出 HTTP 异常。例如,Boom.forbidden('Please sign in first') 会抛出一个这样的对象:
xxxxxxxxxx51{2 "statusCode": 403,3 "error": "Forbidden",4 "message": "Please sign in first"5}用 Boom 来改写我们上面的例子,可以变成:
xxxxxxxxxx181+import Boom as from '@hapi/boom'23class ProjectService {4 getProjectById(id) {5 const project = db.project.find(id)6 if (!project) {7+ throw Boom.notFound('Project not found')8 } else {9 return project10 }11 }12}1314app.get('/project/:id', (req, res) => {15 const projectService = new ProjectService()16 const project = service.getProjectById(req.params.id)17 res.json({ data: project })18})然而,在 Next.js, 无论你在 API route 中 throw 一个怎样的 error, 它都会响应一个 500 Internal Server Error. 并且你根本没有方法统一处理这些 error.
幸运的是,我们在上一节介绍的 next-connect 提供了一个统一的 error 处理机制。使用 next-connect 的所有 api route 或中间件,如果抛出异常,都可以在 onError 中捕获到:
xxxxxxxxxx131import nc from 'next-connect'23const handler = () => nc({4 onError(err, req, res) {5 console.log(err.message) // => 'oops'6 res.send(err.message)7 }8})910export default handler()11 .get((req, res) => {12 throw new Error('oops')13 })
因此,我们甚至可以借助 @hapi/boom 的力量,在 Next.js 中得到很好的错误处理机制:我们可以在 api route 或者中间件中抛出 Boom 异常,然后在 onError 中根据 Boom 异常的信息改变 res:
安装
@hapi/boom:xxxxxxxxxx11$ yarn add @hapi/boom
xxxxxxxxxx381// pages/api/example.ts23import nc from "next-connect";4import * as Boom from '@hapi/boom'56const handler = () =>7 nc({8 onError(err, req, res) {9 // 如果是一个 Boom 异常,则根据 Boom 异常结构修改 `res`10 if (Boom.isBoom(err)) {11 res.status(err.output.payload.statusCode);12 res.json({13 error: err.output.payload.error,14 message: err.output.payload.message,15 });16 } else {17 res.status(500);18 res.json({19 message: "Unexpected error",20 });21 console.error(err);22 // unexcepted error23 }24 },25 });2627function getProjects() {28 throw Boom.forbidden('Please sign in first')29}3031export default handler()32 .get(async (req, res) => {33 const projects = getProjects()34 res.json({35 projects36 })37 });38这样,在任何地方抛出了 Boom 异常,都可以得到对应的响应,而不需要手动修改 res 对象:

在 Node.js 中访问数据库的方法有很多,优秀的 ORM 也有很多。但根据我自己的使用经验,在 Next.js 使用 Prisma 的体验还是最好的。
Prisma 是一个 TypeScript 友好的 Node.js ORM. 如果你没有听说过 Prisma, 在这里我可以用一句话介绍它:Prisma 让你用一个非常简洁易懂的 DSL 语法描述你的数据库结构和表之前的关系,然后自动生成 TypeScript 友好的请求 SDK.
在项目里接入 Prisma 很简单:
xxxxxxxxxx31$ yarn add prisma @prisma/client23$ yarn prisma init运行 yarn prisma init 后,会在项目里创建 prisma/schema.prisma 文件。Prisma 支持 MySQL, SQLite, PostgreSQL. 为了方便,我们在这里使用 SQLite. 只要把 schema.prisma 里的 provider 改成 sqlite即可:
xxxxxxxxxx1112datasource db {3- provider = "postgresql"4- url = env("DATABASE_URL")5+ provider = "sqlite"6+ url = "file:../db.sqlite"7}89generator client {10 provider = "prisma-client-js"11}url 指定了数据库链接串,用 SQLite 的时候,可以指定为文件的目录。
It means Prisma will use the SQLite database in ../db.sqlite.
想像我们正在开发一个博客系统,我们需要三张表:Post, Comment, Tag:
Post 会有多个 Comment, 但一个 Comment 只属于一个 Post, 它们是一对多的关系。在一对多的关系中,我们通常会设置一个外键 (foreign key), 指向这条记录所属于的父级 idPost 会有多个 Tag, 一个 Tag 也可以属于多个 Post, 它们是多对多的关系。在多对多的关系中,我们通常会单独建立一张关系表。
或许你的 SQL 能力不是特别强,写下这样的建表 SQL 可能比较吃力(即使你精通 SQL, 每次开发项目都要写一堆 SQL 也不是什么好事)。幸运的是,如果你使用 Prisma, 你不需要自己写这些 SQL, 只需要在 prisma/schema.prisma 文件中定义你的数据模型,以及它们之前的对应关系即可:
xxxxxxxxxx361datasource db {2 provider = "sqlite"3 url = "file:../db.sqlite"4}56generator client {7 provider = "prisma-client-js"8}910model Post {11 id String @id1213 content String1415 createdAt DateTime @default(now())1617 tags Tag[] @relation("post_tag")1819 comments Comment[] @relation("post_comment")20}2122model Comment {23 id String @id24 content String2526 postId String27 post Post @relation(name: "post_comment", references: [id], fields: [postId])2829 createdAt DateTime @default(now())30}3132model Tag {33 label String @id3435 posts Post[] @relation("post_tag")36}即使你从来没有学过 Prisma Schema 的语法,你还是可以从中大概看出数据模型,和他们之间的关系。
[Prisma Schema 语法文档
Schema 文件写好后,运行 yarn prisma db push. Prisma 会按照这个 Schema, 把数据模型应用到数据库中,它会处理好一对多,多对多的关系,帮助你生成外键、关系表等等。
数据库生成后,运行 yarn prisma generate, 这个命令会在 node_modules/@prisma/clinet 生成操作数据库的 SDK —— PrismaClient. 在代码中你可以直接引入使用:
xxxxxxxxxx101import { PrismaClient } from '@prisma/client'23const prisma = new PrismaClient()45export default (req, res) => {6 const posts = await prisma.post.findMany()7 res.json({8 posts9 })10}
yarn prisma db push 只能用于项目的原型阶段,因为一但数据库有改变,有可能导致数据丢失。正常的做法应该在每次修改完数据结构以后,生成一份迁移文件,在部署上线前执行这份迁移,安全地变更数据库结构。
Primsa 自带了生成 Migration 的功能:
xxxxxxxxxx11$ yarn prisma migrate dev
迁移文件会生成到 prisma/migrations 中,当你需要把他应用到生产数据库时,执行:
xxxxxxxxxx11$ yarn prisma migrate deploy
在本地开发 Next.js 时,每次代码更改会导致热加载,导致 PrismaClient 一直被重复地实例化,造成出现重复的数据库连接。
我们应该让 Prisma 保持是一个单例,把它缓存到 global 对象中:
xxxxxxxxxx41import { PrismaClient } from '@prisma/client'23export const prisma = global.