78-useFormStatus vs useActionState

image-20250915131359103

image-20250915131507160

nextjs对哪些情况下使用pending(from useFormStatus)或者isPending(from useActionState)没有详细说明,这个交给用户来决定了。

如果有的地方使用到了useActionState,那么就使用里面的isPending就行了。

79-Update Server Action

这节课我们学习怎么更新数据。

创建products-db/[id]/page.tsx,将products-db-create/page.tsx的代码粘贴过来。

1、获取该product的原始表单数据。

这里的报错其实可以使用useEffect来解决,client components不能使用async...await,但是里面的函数是可以使用async...await的。

但是报错,原因是client component里面不能使用async...await:

image-20250915133106564

怎么办呢?将表单操作单独做成一个client component。将app/products-db/[id]/page.tsx改成server component,这样就可以在里面使用async...await了。将form逻辑抽离出去之后的页面如下:

抽离的逻辑写到app/products-db/[id]/edit-product-form.tsx中去,同时在actions/products.ts里面新增一个编辑的action函数。这里面临一个问题,我该怎么把id传递给这个编辑函数呢?因为编辑函数是作为useActionState的参数传进去的,没有直接使用,怎么传呢?

可以使用bind绑定参数,创建一个新函数,指定editProduct函数第一个参数的值。注意:input和textarea里面使用了defaultValue来回显。

那么定义editProduct函数的时候,就要添加一个参数:

为了更好的显示效果,在products-db/page.tsx里面,每一个product的title上面包裹Link标签,可以跳转到编辑页面。

image-20250915141001907

查看效果:

这里说一下bind的用法,重点看预置参数这个案例:

JavaScript 中 Function.prototype.bind() 方法详解

Function.prototype.bind() 是 JavaScript 中一个内置的方法,用于函数对象。它允许你创建一个新的函数,该函数在调用时会将 this 关键字绑定到指定的对象,并可选地预置一些参数。这是一个非常有用的方法,尤其在处理回调函数、事件处理或需要控制上下文的场景中。

下面我将从多个方面详细讲解这个方法,包括语法、参数、返回值、工作原理、使用场景、示例、与其他方法的区别、浏览器兼容性以及常见陷阱。讲解基于官方文档和实际应用经验。

  1. 语法
  1. 参数
  1. 返回值返回一个新的函数(bound function),这个新函数:
  1. 工作原理
  1. 关键使用场景
  1. 示例简单示例:绑定 this

高级示例:预置参数(部分应用)

在事件处理中的示例

与 Next.js/React 中的使用(参考之前对话)如在 Server Actions 中绑定额外参数:

  1. 与 call() 和 apply() 的区别
  1. 浏览器兼容性和 Polyfill
  1. 常见陷阱和异常

如果你在特定上下文中(如 Next.js 的 Server Actions)遇到问题,或者需要更多代码示例,请提供更多细节,我可以进一步扩展!

80-Delete Server Action

先为products-db/page.tsx里面每一个product添加delete button,button的type设置为submit。

actions/products.ts里面,定义remove action函数。

此时我们遇到一个问题,如果直接把removeProduct函数绑定到button的onClick事件上,那么这个组件要变为client component,但是我们想让它保持为server component,因为在里面可以少定义很多变量,并且获取数据更加方便快速。

怎么做呢?在button外面包裹一层form,将removeProduct函数绑定到form的action上面去就行了。

image-20250915143612262

运行试一下:

可以看到,删除成功了,但是页面没有重新加载。那么可以使用next/cache提供的revalidatePath方法,这个方法可以重新加载页面。https://nextjs.org/docs/app/api-reference/functions/revalidatePath

image-20250915144120219

 

可以看到,删除成功之后,页面数据重新加载了。

这里其实还可以增加一个按钮的Loading,直接使用定义的Submit组件就行了。

81-Optimistic Updates

当我们点击删除的时候,等待了很久才有返回,因为我们在prisma数据操作里面加了一些延时,延时是客观存在的,那么有没有办法在延时存在的情况下,也能优化一下呢?

optimistic 乐观

image-20250915145242728

需要实现的效果就是,点击了删除按钮之后,从列表里面立即删除这一项。使用到了react提供的useOptimistic

image-20250915150605923

这里需要注意的是,useOptimistic返回的两个值,一个值就是乐观状态,它等于updateFn的返回值,这个就是我们需要的值。第二个值就是触发乐观更新的函数,也就是哪个操作你想返回乐观状态,就绑定到哪个操作上面去。

上面的解释确实很抽象,下面以例子说明:

1、首先我们遇到的问题就是,删除操作之后,app/products-db/page.tsx里面的products列表的更新不及时,那么就将products列表的值改为乐观状态值。

image-20250915152236174

2、需要使用addOptimistic函数,来触发乐观更新。这个函数接收一个optimisticValue参数,这个参数在这里就是productId。那么需要在删除操作的时候,也触发这个函数。

此时定义一个新函数,在里面触发addOptimistic函数,同时执行删除操作。这个函数就绑定到button上面去。

image-20250915152528830

但此时报错,因为server components里面不能使用react hooks,所以需要将useOptimistic相关的代码,抽离到一个client component里面去,之后引入这个client component就行了。

查看效果:

可以看到,删除一项之后,页面立即更新了,但是看右下角的数据请求,过了3s之后才真正返回结果。

82-Form Component

image-20250915153924397

以案例说明,在/主页添加一个搜索框,在搜索框里面输入关键字,点击按钮之后会导航到products-db页面,显示关键字相关的products。

先想一想如果不使用nextjs提供的Form组件,我可以怎么做。

我会创建一个Search组件,里面使用普通的form标签,编写一个action函数,使用"use server";,那么action函数里面就可以使用formData.get("query")的方法获取到input里面的内容。然后使用

这个方法来跳转并传参。之后在products-db/page.tsx里面,先获取参数,然后请求数据即可。

我想把app/products-db/page.tsx里面添加一个loading.tsx,并将获取products的接口添加查询参数。

image-20250915162006450

在prisma-db.ts里面,为查询函数添加查询参数:

image-20250915162042375

 

我先使用普通的form来做的看一下:

app/page.tsx里面使用这个组件,看一下效果:

效果还不错啊,loading也正常显示了,数据也正常返回了。

那nextjs的Form组件,有哪些好处呢?https://nextjs.org/docs/app/api-reference/components/form

image-20250915162636152

image-20250915163221255

可以看到,action属性可以接收string或者function,当action接收string类型时,会像native HTML form标签那样使用GET方法,form data被作为searchParams编译到url中,当form提交的时候,会跳转到具体的url。

 

Form的好处就是:可以提前获取loading UI,点击submit button可以实现navigation跳转,可以减少一些模板代码。

那么我就使用Form来是实现一下:

最大的好处就是可以不写action函数。

 

下面是nextjs里面的Form组件在这个案例中的作用。

image-20250915155153183

image-20250915155212505

image-20250915155234509

image-20250915155248639

到这里就已经学完了data mutations和data fetching的全部内容了,下面总结一下我们学了哪些内容:

image-20250915155531753

83-Authentication

image-20250915164922503

image-20250915165006894

虽然说可以自己定义authentication的方案,但是nextjs里面的权限非常复杂,nextjs都建议使用第三方库。https://nextjs.org/docs/app/guides/authentication

image-20250915165233057

老师推荐使用Clerk这个库,https://clerk.com/docs/quickstarts/nextjs

新建项目npx create-next-app authentication-demo

84-Clerk Setup

先创建一个clerk的账户,我使用anderson tom的账号登录。然后会进入一个创建应用的界面,输入应用名称和支持登录的账号类型。选择create application

image-20250915170013142

会进入用户引导页面,前3步严格按照这些引导操作。

image-20250915170225208

第4步,只需要将ClerkProvider引入到app/layout.tsx里面并使用即可。引导文档里面的内容其实包括了后面的学习内容,后面我们会逐步来学习。

85-Sign in and Sign out

clerk提供了pre-built components that handle all the authentication flows for us.

这节课我们来学习clerk提供的sign in和sign out功能,首先创建一个/src/components/navigation.tsx组件,这个组件就是网页的nav栏,直接复制老师的代码。引入到app/layout.tsx里面,放在{children}上面。

image-20250916111720462

运行看一下,可以看到导航栏的效果已经出来了:

image-20250916111838031

下面为nav添加sign in按钮,使用clerk提供的按钮,采用modal模式,这表示点击sign in按钮会打开一个弹窗而不是跳转到一个页面。

看一下效果,点击sign in按钮,会显示弹窗,由于我们没有账号,可以点击sign up注册账号。注册成功之后会关闭弹窗并返回原页面,但是clerk返回的信息我们还没有获取到,这些内容后面会讲到。

下面引入sign out按钮,点击这个按钮,clerk会自动处理所有事情:clear the session, remove tokens and update the UI if needed.

但是此时这个功能不完整,下面的课程会逐步完善。

image-20250916113044174

 

这节课只是导入了clerk提供的登录、登出按钮,体验一下操作而已,完整的流程在接下来会逐步完善。

86-Profile Settings

为了注销,clerk提供了UserButton组件,这个功能更加强大。

1、profile modal

这个是显示一个profile的弹窗。

注释掉SignOutButton组件,使用UserButton组件:

image-20250916114630671

可以看到,登录之后,nav栏的右上角出现了一个用户图标,点击图标可以管理账户信息,还可以登出账号。

2、profile page

这个是显示profile的单独页面。我们还是可以使用clerk的组件,不过使用方法大不相同。

image-20250916115256307

这里用到的知识,我学到这里已经忘记了,我翻了一下前面的学习内容,发现在lesson-9里面的optional catch all segments里面讲到了。/user-profile/[[...user-profile]]/page.tsx,这个文件,会匹配任何url为/user-profile开头的地址,显示的都是这个文件。事实上[[...user-profile]]里面的这个变量名,可以自定义,原来学习的时候就是[[...slug]],这个不影响什么。

创建一个app/user-profile/[[...user-profile]]/page.tsx,里面使用clerk提供的UserProfile组件。

image-20250916120847121

在nav里面使用Link链接跳转到这个页面,然后注销UserButton,还是使用SignOutButton。

查看效果,可以看到点击Profile链接之后,会跳转到/user-profile,不要把这个页面看成弹窗了,这个页面就是这么设计的。点击sign out按钮之后,会退出登录,并自动重定向到首页。

87-Conditional UI Rendering

这节课我们学习怎么做条件渲染。

上面实现的内容,在没有sign in的时候,profile和sign out其实应该隐藏。在sign in之后,sign in按钮应该隐藏。下面使用clerk提供的SignedInSignedOut组件来解决问题。

用法非常简单,使用SignedOut包裹SignInButton,表示在没有登录的时候,显示登录按钮,依此类推。

这里有一个报错,是关于middleware.ts文件的,我找了很久都找不到原因,最后发现是midddleware写错了,多一个d。文件名对了之后就不报错了。我找了一下文档,发现这里还可以指定哪些路由需要受保护,参考:https://clerk.com/docs/references/nextjs/custom-sign-in-or-up-page

可以看到,按照预期效果实现了。

88-Protecting Routes

这一节课学习这么保护页面,在用户没有登录的情况下,不能被访问。

现在访问/user-profile是这个效果:

image-20250916125258993

参考:https://clerk.com/docs/references/nextjs/custom-sign-in-or-up-page

设置protect routes有什么好处呢?

protect method will redirect the user to the sign-in page automatically if they are not signed in.

设置了protect routes之后,如果用户访问这个地址,clerk会帮忙自动跳转到登录页面。如果登录了之后,就会跳转到用户之前访问的地址。

在中间件文件中设置:

看一下效果,可以看到访问/user-profile,在没有登录的情况下,跳转到登录页面,等到登录之后,会回到之前访问的/user-profile,功能还是蛮强大的:

但是项目中的大多数页面都需要受到保护,所以最好还是定义一个isPublicRoutes,取反来判断。

 

还可以自定义保护的逻辑,比如说记录哪个ip地址的用户一直在尝试登录等等。

89-Read Session and User Data

这节课主要学习怎么获取user data,session data,在server components和client components里面使用的方法不同。

1、在server components里面获取user data,session data

创建app/dashboard/page.tsx

看一下输出内容:

image-20250916134556448

image-20250916134616870

在server components和route handlers中,我们通常会使用auth提供的userId,并用它来查询数据库。

2、在client components里面获取user data,session data

创建src/components/counter.tsx。这个组件要放到app/page.tsx里面去,也就是首页展示。这里会做一个功能,如果用户没有登录,这个counter组件就不显示;登录了才显示。

可以看到,没有登录的时候或者页面没有加载完成的时候,Counter组件没有显示。

这个“没有显示”很巧妙啊,也是react里面的方法,要记住。

90-Role Based Access Control

这节课学习怎么根据登录用户的角色来显示不同的页面,这个如果要我来做,我该怎么做?

在vue中的权限大部分都是后端控制的,菜单的权限是根据用户的角色来配置的,不同的角色配置不同的菜单。

不同的角色是单独的CURD页面,里面可以配置菜单、服务商角色等等。

前端做什么呢?1、前端会直接显示后端返回的菜单,没有做权限校验,返回什么显示什么。2、后端返回一些权限控制的字段,然后vue里面编写一些指令,比如说v-auth,放到一些标签上,指令的作用就是判断后端返回的字段里面有没有对应的权限字段,如果有就显示标签,否则不显示。

 

其实clerk里面也异曲同工,不过整个过程将前后端的配置都加了进来,刚开始我还是有点接受不了的,内容确实有点多,如果要我来写一整个项目,那会更加复杂。先从简单的开始,官方文档:https://clerk.com/docs/references/nextjs/basic-rbac,老师实际上是按照这个文档来讲解的。

image-20250916135935239

 

image-20250916140023571

 

1、Configure the session token

1、clerk 提供user metadata的方法

clerk提供user metadata的方法,https://clerk.com/docs/users/metadata,可以让我们来存储一个用户的信息,这里我们选择存储publicMetadata。

这个publicMetadata存储在了用户的信息当中,我们可以通过middleware.ts里面的clerkMiddleware的auth取出来。或者通过import { auth } from '@clerk/nextjs/server'的auth取出来。

这个变量虽然叫publicMetadata,但是实际返回的值还是metadata,那么里面定义了role的值,取出来就行了。

image-20250916163652693

2、在中间件中或者页面中获取role的值,做逻辑校验

使用auth来获取metadata。

3、刚开始其实是哪个用户都没有设置role的,那么role在哪里设置呢?

可以在clerk项目工作台里面设置,注意项目要准确,我就是因为搞错了项目,结果看到User里面没有数据,搞了几个小时。

image-20250916191248221

image-20250916191318370

在里面的Metadata的Public里面,添加:

 

其实掌握了上面三步之后,主要的流程就清楚了。

 

在clerk的项目控制台里面,选择configure页面,选择Sessions

image-20250916140214455

编辑自定义session token的内容,并保存,如下:

image-20250916140447006

2、Create a global typescript definition

这一步是定义roles有哪几种类型,采用枚举方法。同时定义一个CustomJwtSessionClaims的类型,里面存放的是metadata。至于为什么要这么定义,首先metadata是从auth的sessionClaims获取的,所以如果要定义类型的话,就要定义在这里面;其次,使用declare global,将这个interface定义成全局类型,这样使用的时候就不需要导入了。

参考:https://clerk.com/docs/guides/custom-types

3、添加app/admin/page.tsx,中间件里面为这个路径添加校验

image-20250916194148061

看一下效果,可以看到访问/admin的时候,就自动跳转到首页了。原因是此时所有的用户,role都没有设置:

4、修改用户的role

新增的用户的role默认都是空字符串,所以要手动进行修改。除非是新增的时候为它们添加默认值,这里先不考虑。

方法一:

在clerk项目控制台里面修改,上面已经说了。先看一下效果:

image-20250916194546324

可以看到,定义role为admin之后,可以进入admin页面了。

方法二:

在clerk里面修改肯定是不行的,最佳实践就是编写自己的权限CRUD页面,这里就在admin页面编写。页面好做,但是有几个难点:

1、要从clerk获取所有用户的数据。

2、要使用clerk的方法,来操作clerk里面的数据。

上面两个难点,都可以使用clerkClient来操作。clerkClient可以操作clerk的backend api,这些backend api有哪些呢?参考https://clerk.com/docs/reference/backend-api

image-20250916201135679

先定义app/admin/actions.ts

可以看到,修改之后数据解释刷新了。

 

下面就是我们达到的authentication 的目标:

image-20250916142049142

91-Customizing Clerk Components

1、添加注册入口

使用clerk的SignUpButton来添加注册入口。

image-20250916151541305

可以看到,signin和signup切换的非常流畅,估计它们触发的是同一个弹窗组件。只不过默认展示的内容不同。

2、自定义登录和注册按钮

我们使用的是clerk提供的登录、注册按钮组件,这里的样式能不能改一下呢?

可以,直接像传递children那样,把自定义的button传递给clerk的组件即可。

image-20250916152206697

可以看到样式修改了。

3、自定义登录、注册页面

有时候,我们不想使用弹窗来登录、注册,想跳转到单独的页面去,怎么做呢?

简单的做法就是将SignInButtonSignUpButton里面的mode属性去掉,这样就会跳转到单独的页面了。

image-20250916152559405

看一下效果:

但这样做有问题,域名是clerk的域名,我们想使用自己的域名和地址,该怎么做呢?

可以创建自己的登录、注册页面,使用clerk提供的SignUp和SignIn组件。

1、就像之前的/user-profile页面那样,创建app/sign-in/[[...sign-in]]/page.tsxapp/sign-up/[[...sign-up]]/page.tsx。在里面分别使用clerk的SignUp和SignIn组件。

这里注意要在middleware.ts的isPublicRoutes里面,加上这两个路径。因为这两个路径很明显不需要登录,就可以查看。

2、需要在.env.local文件里面添加变量,指定nextjs使用了clerk之后的登录、注册页面的路径。

需要注意的是,我们去掉了SignInButtonSignUpButton的mode属性,也没有指定点击了这两个组件里面的button的事件,说明clerk采用配置的方式来指定跳转的路径。

image-20250916153529112

重新启动之后查看效果:

clerk的重定向非常智能,当用户访问某个受保护页面时,clerk会跳转到登录界面,等到登录完成之后,会自动重定向到原来的页面,不需要程序员额外操作。

 

这种默认重定向的行为很好,但还是可以定义备用的重定向地址,防止原地址的参数丢失之后不知道该怎么办。

通过环境变量来定义它,在.env.local里面定义:

image-20250916160015335

 

还可以定义强制的重定向地址,无论用户原来的地址是什么,都强制跳转到定义的地址:

image-20250916160239323

92-Deploying Next.js Apps to Vercel

1、确保项目已经上传到github

2、注册vercel账号,我使用anderson tom的账号登录。选择关联到github:

image-20250916143137592

3、在控制台中选择新建项目,选择github里面的仓库。

image-20250916143443818

4、配置项目信息

image-20250916144655258

部署成功:

image-20250916145812269

可以通过https://shangguigu-learn.vercel.app/来访问项目了:

后面可以使用域名来替换掉这个域名。