封面

image-20210614183732554

前言

读者好,我是 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 添加一篇文章:

image-20210612132751487

在这个例子里,共有两个请求,一个是用于获取所有文章的 GET 请求,另一个是用于创建文章 的 POST 请求。

在真实的应用中,我们通常需要知道每个异步请求的状态,以便显示不同的 UI. 例如,在获取所有文章时,如果请求尚未结束,则在 UI 中显示 <div>Loading...</div>. 另外,一个异步请求还有成功和失败的状态,分别也有对应的 UI.

所以,每一个异步请求,实际上是一个独立的「状态机」,它们都在几个状态中流转:

如果每一个异步请求都要我们都要为它们写维护这些状态的代码,那么代码量会非常多,而且都是基本相同的逻辑。

当然,我们可以把这些状态封装成一个可复用的 hooks. 但数据请求还面临另一个问题:当一个请求完成之后,它可能需要更新其它的请求数据。例如在上面的例子,当我们通过接口创建了一篇文章后,另一个获取文章的接口数据已经不是最新的了,如果我们想在 UI 尽快保持显示最新的数据,我们必须重新请求已经过期的那些数据对应的请求。然而异步请求散落在整个应用的不同地方,很难把它们都一起更新。

react-query 能很好地解决这些问题。

在 Next.js 中引入 react-query

在 pages/_app.tsx 中, 在顶层组件套上 QueryClientProvider:

在 react-query 中,有两个关键的概念 —— Query 和 Mutation.

Query

任何获取数据的请求都是 Query, 下面是用 react-query 获取所有文章数据的例子:

和之前的例子一样,在 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 的作用,他可以给请求一个标识,方便后续我们可以用这个标识对请求进行操作和优化。

Mutation

对资源进行修改的请求(POST, PUT, DELETE 等)都是 Mutation. 下面是在我们上面的例子中,加入了创建文章这个请求,像 useQuery 一样,用 useMutation() 嵌套这个请求方法。

和 Query 相同,Mutation 同样有 isLoading, error, data 这些状态。在点击 button 后,我们用 .muate() 方法执行这个请求,并且传入了一些变量,这些变量会被传递到方法 addPost 中。

我们还可以在 .muate() 中传入 onSuccess 和 onError, 分别指定在请求成功和请求错误后执行的逻辑。比如弹一个 toast 之类的。

上面我们提到过,当用户创建了新文章后,理论上请求所有文章这个请求的数据就已经不是最新的了。这时如果我们想再次请求获取所有文章这个接口,可以在 onSuccess 中,手动地执行这个请求的 refetch 方法:

但这不是一个很好的实践,想像一下,这个获取文章的接口,有可能在这个应用中的多个组件用到,难道我们需要一个一个地执行 refetch 吗?当然不是。这时就要介绍 react-query 的一个重要功能 —— Query Invalidation.

Query Invalidation

Query Invalidation 的意思是「使请求失效」。我们可以用 queryClient 实例的 invalidateQueries() 方法,使某个 key 的请求失效。react-query 会自动重新请求失效的 Query.

我们用 queryClient.invalidateQueries('getPosts') 使用 key 为 getPosts 的 Query 失效,那么所有用到 key 为 getPosts 的 Query 都会被重新请求,并更新最新的数据到 UI 上。

Next.js 中缺失的技术

在 API route 中使用中间件

Next.js 没有中间件机制。首先让我简单解释一下什么是中间件,为什么我们需要中间件。

在 Express/Koa, 我们可以用中间件进入一个请求的生命周期,一个典型的中间件是一个函数,它接受 req, res 和 next 参数:

这个中间件的模式起源于一个 Node.js Web 框架 connect, 早期的 Express 也基于 Connect 开发,于是很多框架也兼容了这种模式,所以这种中间件模式我们通常称为 connect 中间件。

在中间件里,我们可以:

这使得中间件可以很好地使代码在不同的路由之间重用。假设我们需要在一个路由跟据 cookies 获取用户信息,我们可以把这个获取用户信息的方法写成中间件,然后把用户信息注入到 req.user,这样所以使用了这个中间件的路由可以通过 req.user 取得用户信息。而且在中间件中,如果判断用户没有登录,可以中止这个请求,并返回 403.

下面是 Express 编写和使用中间件的例子:

如果在 Next.js 要做同样的事,我们会这么做:

但在 Next.js, 我们没有任何办法中止请求。理论上 console.log('do other things') 在用户未登录时不应该被执行。

使用 next-connect

要在 Next.js 中像 Express/Koa 这样使用 connect 中间件,我们可以使用 next-connect 这个库。

安装 next-connect:

 

现在,让我们用 next-connect 重写上面的例子:

可以看到,现在我们在 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:

API route 错误统一处理

在实现 HTTP API 时,一个良好的实践应该在出现错误的时候响应错误对应的状态码。例如在资源不存在的时候返回 404,在没有权限时返回 403.

在一个复杂的 Web 应用,业务逻辑一般单独存在于 service (model) 层,例如一个根据 id 获取项目信息的 API:

当 project 找不到时,我们应该返回一个 404 的响应。但在 service (model) 层,我们很难接触到 res 对象,所以很难在 service (model) 层修改响应。也无法在这个层面中止响应。

我们来看看在 Express 中怎么做。Express 有自带的 error handler:,当 catch 到任何地方的 error 后,响应的格式会根据 error 的对象自动改变。也就是说,我们可以通过 throw error 来改变响应的信息:

上面的代码中,当 project 无法找到时,会抛出一个 { status: 404 }, Express 在捕获后,会把 res.statusCode 设为 404。

另一个流行的 web 框架 hapi 还提供了 @hapi/boom 这个模块,提供了很多 helper 用于抛出 HTTP 异常。例如,Boom.forbidden('Please sign in first') 会抛出一个这样的对象:

用 Boom 来改写我们上面的例子,可以变成:

然而,在 Next.js, 无论你在 API route 中 throw 一个怎样的 error, 它都会响应一个 500 Internal Server Error. 并且你根本没有方法统一处理这些 error.

幸运的是,我们在上一节介绍的 next-connect 提供了一个统一的 error 处理机制。使用 next-connect 的所有 api route 或中间件,如果抛出异常,都可以在 onError 中捕获到:

 

image-20210612044906647

因此,我们甚至可以借助 @hapi/boom 的力量,在 Next.js 中得到很好的错误处理机制:我们可以在 api route 或者中间件中抛出 Boom 异常,然后在 onError 中根据 Boom 异常的信息改变 res:

安装 @hapi/boom:

 

这样,在任何地方抛出了 Boom 异常,都可以得到对应的响应,而不需要手动修改 res 对象:

image-20210612045920643

访问数据库

在 Node.js 中访问数据库的方法有很多,优秀的 ORM 也有很多。但根据我自己的使用经验,在 Next.js 使用 Prisma 的体验还是最好的。

Prisma 是一个 TypeScript 友好的 Node.js ORM. 如果你没有听说过 Prisma, 在这里我可以用一句话介绍它:Prisma 让你用一个非常简洁易懂的 DSL 语法描述你的数据库结构和表之前的关系,然后自动生成 TypeScript 友好的请求 SDK.

在项目里接入 Prisma 很简单:

运行 yarn prisma init 后,会在项目里创建 prisma/schema.prisma 文件。Prisma 支持 MySQL, SQLite, PostgreSQL. 为了方便,我们在这里使用 SQLite. 只要把 schema.prisma 里的 provider 改成 sqlite即可:

url 指定了数据库链接串,用 SQLite 的时候,可以指定为文件的目录。

It means Prisma will use the SQLite database in ../db.sqlite.

定义数据模型

想像我们正在开发一个博客系统,我们需要三张表:Post, Comment, Tag:

image-20210612155159566

 

 

或许你的 SQL 能力不是特别强,写下这样的建表 SQL 可能比较吃力(即使你精通 SQL, 每次开发项目都要写一堆 SQL 也不是什么好事)。幸运的是,如果你使用 Prisma, 你不需要自己写这些 SQL, 只需要在 prisma/schema.prisma 文件中定义你的数据模型,以及它们之前的对应关系即可:

即使你从来没有学过 Prisma Schema 的语法,你还是可以从中大概看出数据模型,和他们之间的关系。

[Prisma Schema 语法文档

Schema 文件写好后,运行 yarn prisma db push. Prisma 会按照这个 Schema, 把数据模型应用到数据库中,它会处理好一对多,多对多的关系,帮助你生成外键、关系表等等。

数据库生成后,运行 yarn prisma generate, 这个命令会在 node_modules/@prisma/clinet 生成操作数据库的 SDK —— PrismaClient. 在代码中你可以直接引入使用:

image-20210612162004027

数据迁移(Migration)

yarn prisma db push 只能用于项目的原型阶段,因为一但数据库有改变,有可能导致数据丢失。正常的做法应该在每次修改完数据结构以后,生成一份迁移文件,在部署上线前执行这份迁移,安全地变更数据库结构。

Primsa 自带了生成 Migration 的功能:

迁移文件会生成到 prisma/migrations 中,当你需要把他应用到生产数据库时,执行:

Prisma Migrate 文档

在 Next.js 中使用 Prisma

在本地开发 Next.js 时,每次代码更改会导致热加载,导致 PrismaClient 一直被重复地实例化,造成出现重复的数据库连接。

我们应该让 Prisma 保持是一个单例,把它缓存到 global 对象中:

之后在应用所有的地方只通过这个单例引入 prisma.

应用实例

前面几个简短的章节已经大概介绍完了一些我们会用到的技术和方法论,接下来在这一个章节,我们就开始利用这些技术,开发一些真实的应用了。

以下实例可以在 https://github.com/djyde/fullstack-nextjs-in-action-example 获取源码。

这个仓库的 master 分支是一个把以上技术都整合到一起的基础 Next.js 项目。你可以选择以 master 分支为基础开始一步一步地跟着本书开始。

你也可以选择只用来读代码。在实例教程中,每一个小节我都会在标题下方留下对应的 commit 的 hash, 你可以使用 git checkout xxx 直接跳到该小节的代码中进行阅读。

请注意,我们将会提供两个实例,这两个实例并非各自独立,实例 2 是基于实例 1 开发的,所以请勿直接跳过实例 1.

实例 1: 用户系统

第一个实例是实现一个用户系统。用户可以在页面中创建账号,登录和登出。本实例主要内容:

在这个实例中,我们需要实现的是:

登录页面

c1695d97d65b0

首页,创建一个文件 pages/login.tsx:

这是给用户登录和创建账号的页面 UI http://localhost:3000/login:

image-20210612165344992

创建账号

f3d9abb6cef

首先我们需要在数据库中创建一个存放用户的 User 表。编辑 prisma/schema.prisma,添加一个 User model:

然后运行 yarn prisma db push,把模型应用到数据库中。

推荐使用 DB Browser for SQLite 查看数据库

image-20210612170215874

这时,就可以创建一个 API 用于创建账号 pages/api/singup.ts:

apiHandler 是对 next-connect 的封装,详细可读源码

在这个 API 中,我们在请求体中取 username 和 password 这两个字段,然后使用 prisma.user.create() 在 User 表中创建一条记录。

我们使用 bcrypt 加密用户的密码。请记住任何时候都不要明文保存用户的密码。

「创建账号」的表单

Now, add a create account mutation in the create accout form:

createAccount() 是请求的方法,我们用 useMutation 把它封装成了一个 Mutation (createAccountMutation).

当用户点击 button 时,就通过 createAccountMutation.mutate() 执行这个 Mutation.

现在试一试创建一个账号:

image-20210612172449557

image-20210612172923744

在数据库中看到这个创建的用户。

登录 API

保存用户的登录状态主流的实现方式有两种,一种是 Session, 一种是 JWT. 两种方法各有区别,不在本书讨论的范围内。这里为了方便,我们使用 JWT 来实现。

验证用户名密码

5d6970536afb

无论是用 JWT 还是 Session, 在登录的接口中,都需要实现验证用户名和密码的方法。首先创建登录 API 的接口 pages/api/login.tsx

我们实现了一个 validate 方法,在数据库中寻找对应用户名的记录,然后匹配密码是否正确。如果正确,返回用户对象,相反,抛出一个 unauthorized 的 401 异常。

回顾:

在「API route 错误统一处理」一节中我们介绍了抛出和统一处理 Boom 异常的方法

现在我们为这个登录接口在页面创建一个 Mutation:

在上面的代码中,我们还可以通过 loginMutation.error.response.data.message 获取请求出现错误时返回的消息体。我们现在用一个错误的密码进行登录:

image-20210612205419926

生成 JWT

ef9529bf6db5

我们继续完成这个登录 API, 在验证完用户名和密码后,我们就要把用户信息加密到 JWT 中。我们使用 jsonwebtoken 来生成 JWT:

我们通过 jwt.sign() 生成一个 JWT. 把 { username: user.name } 放到这个 JWT 中,并且设置了 JWT 过期的时间是 3 天。

生成 JWT 需要一个只有服务器端知道的私钥,为了安全起见,我们不能把私钥写在代码里,而是放在环境变量中。

在项目中创建一个 .env 文件,然后把私钥设置到 JWT_SECRET 变量中,Next.js 会在启动时读取 `.env

客户端在哪里存放这个 JWT? 比较好的一种做法是保存在 httpOnly 的 cookies 中。然而 Next.js 没有自带一个比较方便的设置 Cookie 的方法,所以我们使用 cookie 这个库:

Install cookie:

我们把 JWT 保存在了名为 token 的 cookies 中。

现在我们尝试用正确的用户名和密码登录,可以看到登录成功后,cookies 被正确保存了。在这之后,所有请求都会带上这个 cookie.

image-20210612222759745

用户信息页面

21e4111ffbfb

现在,我们创建一个 /user 页面用来展示用户信息。这个页面只能被已登录的用户访问,如果未登录,就跳转到 /login.

如何在访问时判断是否已登录?我们可以在 getServerSideProps(ctx) 中从 ctx.req.cookies 中取得登录成功时设置好的 token cookies, 并验证 JWT 是否合法。

首先在 utils.server.ts 实现一个从 req 取 JWT 且验证 JWT 的方法:

然后创建 /user 页面 /pages/user.tsx:

在 getServerSideProps 中调用 getUserFromReq, 如果返回是一个 user 对象,则表明用户已登录,我们可以把用户信息通过 props 传到页面。

相反,如果返回 null, 代表用户没有登录。我们可以在这里返回一个 redirect 对象(形如 { destination: string, permanent: boolean }), 用户就会被跳转到指定的路由。这是 Next.js 其中一个特性。

image-20210612230734626

思考题

你学会了如何在页面访问时判断用户是否登录且控制是否跳转到其它路由。现在如果我们想要实现当已登录的用户在访问 /login 时把他跳转到 /user 路由,你会怎么做?

退出登录

6d0483a7a53

实现退出登录,我们只需要创建一个路由 /api/logout 把 cookies 设置为一个不合法的值,然后跳转到其它路由即可:

然后在页面添加一个退出登录的链接:

实例 2: 简易 HackerNews

来到第二个实例,我们基于实例 1 实现一个最简单的 HackerNews: 用户可以在上面提交链接和标题,也可以编辑和删除他们提交链接。

本实例主要内容:

「提交链接」的表单

f84d8006e31

在实现表单之前,我们先在网站的首页显示一个导航栏,如果用户已经登录,则显示他的用户名,以及一个退出登录的链接。如果用户未登录,就显示一个登录入口:

和实例 1 一样,我们在 getServerSideProps 中用 getUserFromReq 获取用户信息,然后通过 props 传递到页面。页面根据判断 props.user 是否为 null 进行不同的展示。

image-20210613005834626

现在我们编写用于提交链接的表单,这个表单只在用户已登录时可见:

image-20210613010425571

 

数据模型

51282d8145347

用户可以提交链接,所以我们需要一个 Link 表,每一个 Link 都有标题 title 和链接 url. 每个 Link 都是被某个用户提交的,所以我们还需要用一个外键 creatorName 标识 Link 是被哪个用户提交的。

image-20210613140842307

把数据模型写成 Prisma Schema (prisma/schema.prisma):

然后运行 yarn prisma db push 把数据模型应用到数据库:

image-20210613142046820

「提交链接」API

35b7bc1407

现在我们创建一个 API /api/link 用来给用户提交链接,创建 pages/api/link:

/api/link 接受一个 POST 请求,然后用 prisma.link.create 把链接存进数据库。

但代码其实还没完成,因为我们需要在 API 里知道请求这个接口的登录用户是谁。

我们可以写一个中间件专门用于取用户信息,然后把用户信息放在 req.user 中,我们把这个中间件写在 utils.server.ts:

这个中间件不止把用户信息放在 req.user, 当一个未登录的用户请求了进入了这个中间件时,中间件会抛出一个 403 响应,从而阻止未登录用户。

把这个 authMiddleware() 用在路由里:

现在,试试未登录用户请求这个 API:

image-20210613144330519

可以看到响应了 403.

在页面中获取/提交链接

1406f062d3f

现在,让我们在页面中获取所有提交的链接并展示出来。首先我们要创建一个 GET /api/link 来获取所有链接:

用 findMany() 查询所有记录,然后根据 createdAt 排序。

在页面中,创建一个 Query 来请求这个接口,并渲染到页面中:

然后编写提交链接的 Mutation:

当 submitLinkMutation 请求成功后,可以在日志中看到 submitted.

image-20210613153718953

当用户提交新的链接后,他并不能马上看到新的链接,因为这需要重新请求获取链接的接口。这里我们就可以用到 react-query 的 Query Invalidation 功能。回看一下我们在实现获取链接的 Query 的时候,给了这个 Query 一个 fetchAllLinks 的 key:

因为,我们可以在用户提交新的链接成功后,调用 queryClient.invalidateQueries('fetchAllLinks'), 让 react-query 知道 fetchAlllinks 这个 Query 的数据已经过时,需要重新请求。

现在,当用户提交链接后,react-query 就会重新请求获取链接的 Query, 然后展示最新的数据:

image-20210613154735706

 

编辑链接

607310047a29cc0f

用户可以编辑和删除他们提交的链接。现在创建一个 PUT /api/link/:linkId 的 API 在编辑链接的时候请求。在 Next.js 中,我们可以通过创建 /pages/api/link/[linkId]/index.ts 这样的文件来创建这样的动态路由。然后通过 req.query.linkId 来获取路由上面的参数:

image-20210613161249526

我们用 prisma.link.update 来更新一条 Link 的记录:

「编辑链接」的业务逻辑这样其实已经算是完成,但是,这个 API 不应该让未登录的用户,或不是这条链接的提交者请求。我们可以直接使用上一节封装的 authMiddleware, 把非登录用户排除在外。然后,再通过判断 Link 的 creatorName 是否和 req.user.name 相同即可:

在这一步,我们就见识到了中间件对于代码复用的好处。我们甚至可以再进一步,封装一个专门用于判断请求者是否是链接作者的中间件 linkCreatorGuard:

它接受一个函数,用于在 req 对象中取出链接的 id, 然后返回中间件。

现在我们就把他用在 PUT 的 api 接口:

我们给 linkCreatorGuard 传的第一个参数就是从 req.query 中取出 linkId 的方法。

接口准备就绪,现在就在页面实现编辑链接的表单和 Mutation:

和提交链接一样,修改链接这个 Mutation 成功以后,调用 queryClient.invalidateQueries('fetchAllLinks') 让链接列表更新。

让这个编辑链接的表单显示在每条链接的下方:

现在页面就变成了这个模样:

image-20210613170807605

我创建了两个用户,分别提交了一些链接。现在我们试试看编辑非自己提交的链接会怎么样:

image-20210613171024645

非常棒,这说明 linkCreatorGuard 是有效的。现在编辑自己提交的链接,成功后可以立即看到更新后的数据:

image-20210613171429560

删除链接

f64677053742f

删除链接和修改链接大致相同,但路由是 DELETE /api/link/:linkId. 我们用 prisma.link.delete 来删除一条数据库记录。

同样,只有自己可以删除自己的链接。然而因为我们在实现编辑链接的时候封装了 linkCreatorGuard, 所以在删除链接的 API 只需要实现删除链接的业务逻辑,而不需要再实现判断是否是提交者的逻辑了,这大大减少了工作量:

API 完成后,在前端页面增加删除链接的 Mutation:

删除一条不是自己提交的链接:

image-20210613172856789

删除自己提交的链接,链接会马上从列表中消失。

image-20210613172931985