ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Redwood 教程:保存数据——从 Contact 模型到表单提交的完整数据写入实战

Redwood 教程:保存数据——从 Contact 模型到表单提交的完整数据写入实战 后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载本篇教程带你走完 Redwood 中保存数据的完整闭环从在schema.prisma中定义 Contact 数据模型、执行数据库迁移到通过 SDL 与 Service 暴露 GraphQL 的createContactmutation再到前端表单用useMutation提交数据最后用 loading 禁用、Toast 通知、Service Validations 与FormError打磨出健壮的用户体验。读完本文你将掌握 Redwood 全栈数据写入的标准姿势并理解requireAuth/skipAuth指令、secure by default 设计理念以及 Service 层业务校验的底层原理。本文基于仓库中的官方教程 saving-data.md 展开并结合源码实现进行纵深讲解。添加 Contact 模型打开api/db/schema.prisma在现有的Post模型之后添加一个Contact模型。这是 Redwood 应用数据层的起点——所有数据库表结构都由 Prisma Schema 定义datasource db { provider sqlite url env(DATABASE_URL) } generator client { provider prisma-client-js binaryTargets native } model Post { id Int id default(autoincrement()) title String body String createdAt DateTime default(now()) } // highlight-start model Contact { id Int id default(autoincrement()) name String email String message String createdAt DateTime default(now()) } // highlight-endContact模型包含四个字段自增主键id、联系人姓名name、邮箱email、留言内容message以及自动记录创建时间的createdAt。这些字段全部是必填的String类型。:::tip 可选字段的写法若要将某个字段标记为可选即允许NULL作为值可在数据类型后加问号例如name String?。这样name的值既可以是String也可以是NULL。:::创建并应用迁移模型定义好后运行 Prisma 迁移命令yarn rw prisma migrate dev按提示为这次迁移命名例如create contact。该命令会为 Contact 表生成迁移 SQL 并应用到开发数据库同时重新生成 Prisma Client让db.contact在 Service 中可用。创建 SDL 与 Service有了数据模型接下来创建 GraphQL 接口来访问这张表。scaffold命令其实一直在背后使用generate命令这里我们直接使用它yarn rw g sdl Contact与scaffold一样这条命令会在api目录下生成几个新文件api/src/graphql/contacts.sdl.{js,ts}以 GraphQL 的 Schema Definition LanguageSDL定义 GraphQL schemaapi/src/services/contacts/contacts.{js,ts}包含应用的业务逻辑同时生成关联的测试文件还记得 Side Quest: How Redwood Deals with Data 中的讨论吗SDL 文件中的查询queries和变更mutations会自动映射到 Service 中定义的解析器resolvers。因此生成 SDL 文件时一定会配套生成 Service 文件——两者相互依赖。打开api/src/graphql/contacts.sdl.{js,ts}你会看到与 Post scaffold 生成的完全一致的 Query 和 Mutation 类型结构Contact、CreateContactInput、UpdateContactInput三个类型包含contacts和contact的Query类型以及包含createContact、updateContact和deleteContact的Mutation类型。SDL 文件内容解析export const schema gql type Contact { id: Int! name: String! email: String! message: String! createdAt: DateTime! } type Query { contacts: [Contact!]! requireAuth contact(id: Int!): Contact requireAuth } input CreateContactInput { name: String! email: String! message: String! } input UpdateContactInput { name: String email: String message: String } type Mutation { createContact(input: CreateContactInput!): Contact! requireAuth updateContact(id: Int!, input: UpdateContactInput!): Contact! requireAuth deleteContact(id: Int!): Contact! requireAuth } TypeScript 版本的内容完全一致仅文件后缀不同contacts.sdl.ts。requireAuth 与 skipAuthsecure by defaultQuery和Mutation类型后面跟的requireAuth是一个 schema 指令schema directive意思是访问这个 GraphQL 查询前用户必须已通过认证。目前我们还没有添加认证所以这个指令暂时没有任何效果——任何人都能查询它无论是否登录。原因在于在搭建认证系统之前requireAuth背后的函数始终返回true。正如原文档所说为了不让新应用产生令人沮丧的错误requireAuth指令在设置认证系统之前只是简单地返回true设置之后才会用真实的逻辑判断用户是否登录并按预期行事。值得一提的是Redwood 在架构上对指令做了抽象源码 makeDirectives.ts 中提供了createValidatorDirective与createTransformerDirective两个工厂函数requireAuth/skipAuth就是基于它们构建的内置指令。而useRedwoodDirective插件的测试用例见 useRedwoodDirective.test.ts则详细验证了requireAuth会拒绝未授权执行、skipAuth会放行、带roles参数的requireAuth会校验角色等行为。CreateContactInput 与 UpdateContactInput 的设计CreateContactInput和UpdateContactInput是什么Redwood 遵循 GraphQL 官方建议在 mutation 中使用 Input Types 而非逐个列出所有可设置字段。schema.prisma中必填的字段在CreateContactInput中也必填没有它们就无法创建合法记录UpdateContactInput中没有任何字段是显式必填的因为你可能只想更新一个、两个或全部字段。另一种做法是为每种字段组合单独创建 Input 类型但 Redwood 认为只保留一个 update input 类型是对开发者体验更优的折中。:::info id 与 createdAt 不在 Input 类型中Redwood 假定你的代码不会试图给id或createdAt字段赋值所以把它们排除在 Input 类型之外。但如果你的数据库允许手动设置这两个字段可以自行修改CreateContactInput或UpdateContactInput把它们加进去。:::由于schema.prisma中的所有列都是必填的它们对应的 GraphQL 类型也通过数据类型后的!后缀标记为必填如name: String!。:::tip SDL 与 Prisma 的可选性语法区别GraphQL 的 SDL 语法要求字段必填时多加一个!而schema.prisma语法要求字段非必填时多加一个?。两者恰好相反注意区分。:::无显式 Resolver 的映射约定正如 Side Quest: How Redwood Deals with Data 所述SDL 文件中没有显式定义 resolver。Redwood 遵循一个简单的命名约定sdl文件api/src/graphql/contacts.sdl.{js,ts}的Query和Mutation类型中的每个字段都会映射到services文件api/src/services/contacts/contacts.{js,ts}中同名的一个函数。:::tip 只读 SDL 的快捷方式如果你只需要一个简单的只读 SDL可以跳过创建 create/update/delete mutation给 SDL 生成器传一个标志yarn rw g sdl Contact --no-crud这样你只会得到一个返回全部数据的contacts查询。:::精简 mutation只保留 createContact联系页面只需要createContact。它接受单个变量input一个符合CreateContactInput结构的对象即{ name, email, message }。这个 mutation 应该允许任何人访问访客要能匿名留言所以把requireAuth改成skipAuth。skipAuth表示不要求认证允许任何人匿名提交留言。注意每个Query和Mutation至少需要一个 schema 指令否则会报错。Redwood 拥抱 secure by default 的理念——即使你不做任何特殊防护也尽量保证应用安全在这种情况下抛出一个错误远比把用户数据意外暴露到互联网上安全得多。:::info 巧合且合适默认指令requireAuth恰好正是contacts查询返回所有留言所需要的——只有博客的主人应该有权读取全部留言。:::我们不打算让任何人更新或删除留言所以把这些字段整个移除。修改后的 SDL 文件如下JavaScript 与 TypeScript 版本一致export const schema gql type Contact { id: Int! name: String! email: String! message: String! createdAt: DateTime! } type Query { contacts: [Contact!]! requireAuth contact(id: Int!): Contact requireAuth } input CreateContactInput { name: String! email: String! message: String! } // highlight-start type Mutation { createContact(input: CreateContactInput!): Contact! skipAuth } // highlight-end SDL 文件到此为止接下来看 Service。Service业务逻辑的载体import { db } from src/lib/db export const contacts () { return db.contact.findMany() } export const contact ({ id }) { return db.contact.findUnique({ where: { id }, }) } export const createContact ({ input }) { return db.contact.create({ data: input, }) } export const updateContact ({ id, input }) { return db.contact.update({ data: input, where: { id }, }) } export const deleteContact ({ id }) { return db.contact.delete({ where: { id }, }) }TypeScript 版本会额外引入生成的类型将每个函数标注为对应 resolver 类型import type { QueryResolvers, MutationResolvers } from types/graphql import { db } from src/lib/db export const contacts: QueryResolvers[contacts] () { return db.contact.findMany() } export const contact: QueryResolvers[contact] ({ id }) { return db.contact.findUnique({ where: { id }, }) } export const createContact: MutationResolvers[createContact] ({ input }) { return db.contact.create({ data: input, }) } export const updateContact: MutationResolvers[updateContact] ({ id, input }) { return db.contact.update({ data: input, where: { id }, }) } export const deleteContact: MutationResolvers[deleteContact] ({ id }) { return db.contact.delete({ where: { id }, }) }非常直观createContact()接收input参数直接把它透传给 Prisma 的create()调用。updateContact和deleteContact如果不想保留可以删掉——反正 GraphQL 层已经没有对应的字段了客户端无法调用它们。GraphQL Playground在实现 UI 前调试 API在把功能接入 UI 之前Redwood 提供了一个非常好用的 GUI。运行yarn redwood dev或yarn rw dev时实际上同时启动了第二个进程。打开新浏览器标签页访问 http://localhost:8911/graphql这是 GraphQL Yoga 提供的 GraphiQL——一个基于 Web 的 GraphQL API 图形界面。左侧选择 Docs 标签页并点击query: Query可以看到由我们的 SDL 文件定义的完整 schemaPlayground 会解析这些定义并给出自动补全提示帮助我们从零构建查询。例如尝试获取数据库中所有帖子的 ID在左侧输入查询语句点击 Play 按钮执行。GraphQL Playground 非常适合在编码中途排查问题——无论是 API 层还是 Web 层的拼写错误都可以先在原始形态下实验和调用 API。前端创建 Contact定义 Mutation 并调用后端的 GraphQL mutation 已经就绪剩下的就是在前端调用它。与表单相关的所有内容都在ContactPage中所以 mutation 调用就放在这里。首先把 mutation 定义为一个常量可以定义在组件外部紧跟 import 语句之后import { MetaTags } from redwoodjs/web import { FieldError, Form, Label, TextField, TextAreaField, Submit, } from redwoodjs/forms // highlight-start const CREATE_CONTACT gql mutation CreateContactMutation($input: CreateContactInput!) { createContact(input: $input) { id } } // highlight-end const ContactPage () { const onSubmit (data) { console.log(data) } return ( MetaTags titleContact descriptionContact page / Form onSubmit{onSubmit} config{{ mode: onBlur }} Label namename errorClassNameerror Name /Label TextField namename validation{{ required: true }} errorClassNameerror / FieldError namename classNameerror / Label nameemail errorClassNameerror Email /Label TextField nameemail validation{{ required: true, pattern: { value: /^[^][^.]\..$/, message: Please enter a valid email address, }, }} errorClassNameerror / FieldError nameemail classNameerror / Label namemessage errorClassNameerror Message /Label TextAreaField namemessage validation{{ required: true }} errorClassNameerror / FieldError namemessage classNameerror / SubmitSave/Submit /Form / ) } export default ContactPage我们引用了 Contacts SDL 中定义的createContactmutation向它传入一个包含 name、email 和 message 实际值的input对象。TypeScript 版本在此基础上增加一个FormValues接口并为onSubmit标注SubmitHandlerFormValues类型。useMutation执行 mutation 的 Hook接下来调用 Redwood 提供的useMutationhook它允许我们在合适的时机执行 mutation别忘了 import 它import { MetaTags, useMutation } from redwoodjs/web // ...其余 import 不变 const ContactPage () { const [create] useMutation(CREATE_CONTACT) const onSubmit (data) { console.log(data) } // ...return 不变 }TypeScript 版本中useMutation会使用 Redwood 自动生成的类型import { CreateContactMutation, CreateContactMutationVariables, } from types/graphql // ... const ContactPage () { const [create] useMutation CreateContactMutation, CreateContactMutationVariables (CREATE_CONTACT) // ... }:::tip 关于自动生成的类型只要开发服务器在运行或手动运行yarn rw generate typesRedwood 就会为 GraphQL 查询和 mutation 自动生成类型。一旦你定义了CreateContactMutationGraphQL 那个Redwood 就会据此生成CreateContactMutation和CreateContactMutationVariables类型。更深的内容可参考 Generated Types。:::create是一个调用 mutation 的函数它接收一个带variables键的对象其中再包含一个input键。例如可以这样调用create({ variables: { input: { name: Rob, email: robredwoodjs.com, message: I love Redwood!, }, }, })如果你还记得Form会把所有字段打包成一个以字段名作键的对象就会发现onSubmit中接收到的data对象恰好已经是input需要的格式因此可以直接把onSubmit改成调用 mutationconst ContactPage () { const [create] useMutation(CREATE_CONTACT) const onSubmit (data) { create({ variables: { input: data } }) } // ...return 不变 }试着填写并提交表单——数据库里应该已经有新的 Contact 记录了可以通过 Prisma Studio 或上面提到的 GraphQL Playground 验证。从源码层面看Redwood 的useMutation实际来自 Apollo Client在 packages/web/src/apollo/index.tsx 中RedwoodApolloProvider通过GraphQLHooksProvider把 Apollo 的useQuery、useMutation、useSubscription等 hooks 注入应用上下文。同时它还通过 Apollo Link 链路withToken→authMiddleware→updateDataApolloLink→ 终止 link自动处理 token 注入、认证头、错误追踪与 HTTP 上传。改进联系表单表单能工作了但目前还有几个问题多次点击提交按钮会产生多次提交用户不知道提交是否成功如果服务器端出错我们无法通知用户逐一解决。提交时禁用 Save 按钮useMutationhook 除了返回调用函数外还返回更多内容。我们可以从返回的数组中解构第二个元素——其中我们关心的是loading和errorconst ContactPage () { const [create, { loading, error }] useMutation(CREATE_CONTACT) const onSubmit (data) { create({ variables: { input: data } }) } // ...return 不变 }现在通过loading可以知道数据库调用是否仍在进行。修复重复提交问题的简单做法当响应仍在进行时禁用提交按钮把 Save 按钮的disabled属性设为loadingreturn ( // ... Submit disabled{loading}Save/Submit // ... )开发环境下提交太快可能看不出区别但可以通过 Chrome Web Inspector 的 Network 面板启用网络限速network throttling模拟慢速连接——你会看到等待响应时 Save 按钮被禁用了一两秒。提交成功通知接下来显示一条通知让用户知道提交成功。Redwood 内置了 react-hot-toast可以快速在页面上显示弹出通知。useMutation接受一个选项对象作为第二个参数。其中一个选项是回调函数onCompletedmutation 成功完成后会被调用。我们用这个回调调用toast()函数把消息显示到Toaster组件中。给useMutation加上onCompleted回调并在return中、Form之前放入Toaster组件import { MetaTags, useMutation } from redwoodjs/web import { toast, Toaster } from redwoodjs/web/toast // ...其余 import 不变 const ContactPage () { const [create, { loading, error }] useMutation(CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) }, }) const onSubmit (data) { create({ variables: { input: data } }) } return ( MetaTags titleContact descriptionContact page / Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} {/* ...表单字段不变 */} Submit disabled{loading}Save/Submit /Form / ) }完整文档可参考 toast-notifications.md。显示服务器错误Service Validations 与 FormError接下来向用户展示服务器端错误。到目前为止我们只通知了客户端错误字段缺失或格式不正确。但如果存在服务器端约束Form无从知晓我们仍然需要让用户知道出了问题。前端有 email 校验但任何一个合格的开发者都知道永远不要信任客户端never trust the client。为了确保即使有人绕过前端校验也不会有坏数据进入数据库需要在 api 侧也加上 email 校验。:::info 为什么某些字段不需要服务器端必填校验为什么 name、email、message 的存在性不需要服务器端校验因为 GraphQL 已经替我们做了还记得 SDL 文件中Contact类型的String!声明吗它给字段加了一条约束这些字段一到 api 侧就不能是null。如果是nullGraphQL 会拒绝请求并抛错给客户端。但是如果你在一个 Service 内部调用另一个 Service就不会有任何校验GraphQL 只在外部发起请求时比如浏览器介入。如果你确实想确保字段存在或格式正确需要在 Service 内部添加校验。这样无论谁调用这个 Service 函数GraphQL 还是另一个 Service数据都保证被检查。我们还有一层免费的额外校验因为 name、email、message 在schema.prisma中被设为必填数据库本身会阻止任何null被写入。不过通常不建议只依赖数据库做输入校验数据格式应该是业务逻辑的关切而在 Redwood 应用中业务逻辑就住在 Service 里:::我们之前讨论过业务逻辑属于 Service 文件这正是完美例证。由于输入校验是常见需求Redwood 提供了 Service Validations。在contactsService 中调用新的validate函数确保email字段确实是邮箱格式// highlight-next-line import { validate } from redwoodjs/api // ... export const createContact ({ input }) { // highlight-next-line validate(input.email, email, { email: true }) return db.contact.create({ data: input }) }TypeScript 版本一致只是createContact标注了MutationResolvers[createContact]类型。这里有三个email引用逐一解释第一个参数是我们要检查的值。这里input包含全部联系数据其中email的值就是检查目标第二个参数是TextField的name属性这样 Redwood 就知道页面上哪个输入框有错误第三个参数是一个包含要调用的**校验指令validation directives**的对象。这里只有一个email: true表示使用内置的 email 校验器所以当createContact被调用时会先校验输入只有没有抛错才继续在数据库中创建记录。从源码看validate的实现位于 packages/api/src/validations/validations.ts。它内部维护一个VALIDATORS表内置absence、acceptance、email、exclusion、format、inclusion、length、numericality、presence、custom等校验器并根据校验类型抛出对应的XxxValidationError如EmailValidationError。其中email校验器使用的正则非常宽松/^[^\s][^.\s]\.[^\s]$/——官方邮箱正则约有 6300 个字符Redwood 认为这个简化版本已经足够如需更严格可以用format校验器自定义正则。暂时移除客户端校验以测试服务端校验目前我们甚至无法测试服务器端校验因为TextField的validation属性已经在客户端检查邮箱格式了。先暂时移除它让坏数据能送到服务器TextField nameemail validation{{ required: true, - pattern: { - value: /^[^][^.]\..$/, - message: Please enter a valid email address, - }, }} errorClassNameerror /还记得之前说Form还有一招吗来了添加一个FormError组件传入从useMutation拿到的error常量并加一点wrapperClassName样式别忘了import。同时把error也传给Form让它建立 contextimport { FieldError, Form, // highlight-next-line FormError, Label, TextField, TextAreaField, Submit, } from redwoodjs/forms // ... return ( MetaTags titleContact descriptionContact page / Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} FormError error{error} wrapperClassNameform-error / {/* ...其余字段不变 */} Submit disabled{loading}Save/Submit /Form / )现在用无效邮箱提交一条消息页面顶部会以通俗易懂的英文显示错误信息同时出错的字段本身也会被高亮和行内校验一样顶部信息框对短表单来说可能有点多余但对多屏长表单至关重要——用户能在同一处看到错误摘要不必在冗长表单里到处找红框。如果不需要顶部信息框直接移除FormError字段仍然会按预期高亮。从源码看Formpackages/forms/src/Form.tsx会读取error?.graphQLErrors?.[0]?.extensions?.properties?.messages存入ServerErrorsContext而FormErrorpackages/forms/src/FormError.tsx则负责渲染错误信息框二者共同构成了服务器错误 → 表单上下文 → 页面展示的完整链路。:::info FormError 的样式选项FormError提供了多个样式选项分别作用于消息的不同部分wrapperStyle/wrapperClassName整个消息的容器titleStyle/titleClassNameErrors prevented this form... 标题listStyle/listClassName包含错误列表的ullistItemStyle/listItemClassName包裹每条错误的li:::Service Validations 的更多玩法这只是 Service Validations 能力的冰山一角。你可以做更复杂的校验包括在单次调用中组合多个指令。假设有一个代表Car的模型用户可以在我们的高端汽车售卖网站上提交车辆。如何确保只收到精品车Service validations 允许我们仅用内置的validate()调用就精确限制可提交的值无需任何自定义检查export const createCar ({ input }) { validate(input.make, make, { inclusion: [Audi, BMW, Ferrari, Lexus, Tesla], }) validate(input.color, color, { exclusion: { in: [Beige, Mauve], message: No one wants that color } }) validate(input.hasDamage, hasDamage, { absence: true }) validate(input.vin, vin, { format: /[A-Z0-9]/, length: { equal: 17 } }) validate(input.odometer, odometer, { numericality: { positive: true, lessThanOrEqual: 10000 } }) return db.car.create({ data: input }) }对照源码 validations.ts各校验器的能力非常清晰inclusion值必须在给定列表内可用in指定列表caseSensitive控制大小写敏感默认敏感exclusion值不能等于列表中的任何一项与inclusion相反absence字段必须不存在即必须是null或undefinedallowEmptyString: true时空字符串也算不存在format值必须匹配给定正则可用{ pattern: /.../ }形式length支持min、max、equal、between如{ between: [2, 255] }numericality支持integer、lessThan、lessThanOrEqual、greaterThan、greaterThanOrEqual、equal、otherThan、even、odd、positive、negative你仍然可以嵌入自己的自定义校验逻辑并让错误以与内置校验相同的方式处理validateWith(() { const oneWeekAgo new Date() oneWeekAgo.setDate(oneWeekAgo.getDate() - 7) if (input.lastCarWashDate oneWeekAgo) { throw new Error(We dont accept dirty cars) } })这样就能保证你不会收到一辆破旧老爷车从源码看validateWith的默认导出是异步版本内部await func()另有一个同步版本validateWithSync。其设计动机在源码注释里写得很清楚为什么不直接写自己的函数因为 GraphQL 会吞掉它只给客户端返回 Something went wrong。而validateWith会捕获你抛出的任何自定义错误把它转换成ServiceValidationError从而让真实的错误信息能显示给客户端。此外validations.ts还导出了validateUniqueness它把校验包在数据库事务中可配合$self更新时排除自身与$scope在子集内校验唯一使用适合邮箱、用户名等唯一性约束场景。最后一步提交后清空表单由于提交后没有重定向至少应该清空表单字段。这需要用到 React Hook Form 提供的reset()函数但Form的基础用法就像我们目前的用法拿不到它。Redwood 包含一个useForm()hook来自 React Hook Form平时由Form在内部调用。要重置表单我们需要自己调用这个 hook。但useForm()提供的功能仍然需要被Form使用。做法如下。首先 importuseFormimport { FieldError, Form, FormError, Label, Submit, TextAreaField, TextField, // highlight-next-line useForm, } from redwoodjs/forms在组件内部调用它const ContactPage () { // highlight-next-line const formMethods useForm() //...最后让Form使用我们从useForm()拿到的formMethods而不是自己内部调用return ( Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} // highlight-next-line formMethods{formMethods} // ...现在可以在调用toast()之后对formMethods调用reset()const [create, { loading, error }] useMutation(CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) // highlight-next-line formMethods.reset() }, })从源码 Form.tsx 可以印证Form内部调用useForm(config)得到hookFormMethods然后const formMethods propFormMethods || hookFormMethods——即你传入的formMethods会优先于内部自动创建的那份。config只在useForm()由Form内部调用时才会被自动传入。:::caution 别忘了把客户端校验加回来现在可以把邮箱校验放回TextField了但建议保留服务器端校验以防万一。::::::info 为什么手动 useForm 后 onBlur 失效了你可能注意到一旦自己调用useForm()onBlur 表单配置就失效了。因为 Redwood 之前在后台调用useForm()并自动把Form上的config属性传进去。现在 Redwood 不再替你调用useForm()了如果需要传递某些选项就得手动来const ContactPage () { const formMethods useForm({ mode: onBlur }) //...:::完整页面代码把上述所有改进合并完整的联系页面如下JavaScript 版本TypeScript 版本在此基础上增加FormValues接口、SubmitHandler类型与生成的CreateContactMutation类型标注import { MetaTags, useMutation } from redwoodjs/web import { toast, Toaster } from redwoodjs/web/toast import { FieldError, Form, FormError, Label, Submit, TextAreaField, TextField, useForm, } from redwoodjs/forms const CREATE_CONTACT gql mutation CreateContactMutation($input: CreateContactInput!) { createContact(input: $input) { id } } const ContactPage () { const formMethods useForm() const [create, { loading, error }] useMutation(CREATE_CONTACT, { onCompleted: () { toast.success(Thank you for your submission!) formMethods.reset() }, }) const onSubmit (data) { create({ variables: { input: data } }) } return ( MetaTags titleContact descriptionContact page / Toaster / Form onSubmit{onSubmit} config{{ mode: onBlur }} error{error} formMethods{formMethods} FormError error{error} wrapperClassNameform-error / Label namename errorClassNameerror Name /Label TextField namename validation{{ required: true }} errorClassNameerror / FieldError namename classNameerror / Label nameemail errorClassNameerror Email /Label TextField nameemail validation{{ required: true, pattern: { value: /^[^][^.]\..$/, message: Please enter a valid email address, }, }} errorClassNameerror / FieldError nameemail classNameerror / Label namemessage errorClassNameerror Message /Label TextAreaField namemessage validation{{ required: true }} errorClassNameerror / FieldError namemessage classNameerror / Submit disabled{loading}Save/Submit /Form / ) } export default ContactPage这样就完成了React Hook Form 提供了大量Form未暴露的功能。当你想用到这些功能时可以自己调用useForm()但一定要把返回的对象我们命名为formMethods作为 prop 传给Form这样校验和其他功能才能继续正常工作。小结至此公共站点看起来相当不错了。回顾本篇的核心链路数据层在api/db/schema.prisma定义Contact模型用yarn rw prisma migrate dev生成迁移API 层yarn rw g sdl Contact生成 SDL 与 Service借助命名约定自动完成字段到 resolver 的映射用skipAuth开放匿名提交用requireAuth保护数据读取前端useMutation执行createContact配合loading防重复提交、toast做成功通知、validate()FormError做双层校验与错误展示、useForm()formMethods.reset()清空表单下一步自然是管理功能把创建和编辑帖子的功能移到某个管理后台区域并放在登录之后避免随便浏览 URL 的匿名用户创建垃圾内容——这正是 章节后续内容若有要解决的问题。本教程对应的完整源码位于 test-project 等测试项目中可对照研读。赞分享后端前端Web框架开发工具【免费下载链接】redwoodRedwoodGraphQL项目地址https://gitcode.com/gh_mirrors/re/redwood点击查看免费下载相关推荐RedwoodJS 教程从数据模型到表单提交完整实现联系表单的数据保存RedwoodJS 教程从数据模型到表单提交完整实现联系表单的数据保存 本文是 RedwoodJS 官方教程2.x 版本Saving Data 一章后端前端Web框架开发工具RedwoodJS 保存数据全攻略从 Prisma 数据模型到前端表单提交的完整实战RedwoodJS 保存数据全攻略从 Prisma 数据模型到前端表单提交的完整实战 本篇技术指南以 RedwoodJS 官方教程第三章 Saving Da后端前端Web框架开发工具Redwood 保存数据实战从 Prisma 模型到联系表单的全栈闭环Redwood 保存数据实战从 Prisma 模型到联系表单的全栈闭环 本指南是 Redwood 教程保存数据章节的完整实战讲解以博客的联系我们表单后端前端Web框架开发工具创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表