继续实现项目。
安装MySQL
数据库我们选择MySQL。官网:mysql.com,进入 downloads,下载并安装Community版。
MySQL官网提供了客户端工具 “MySQL Workbench”,可以下载使用。更好用的工具是DataGrip,可以免费使用30天。
官网:https://www.jetbrains.com/datagrip/
设置 Prisma
安装Prisma:npm i prisma
初始化:npx prisma init
在MySQL新建数据库issue-tracker。
修改prisma配置,使用我们创建的MySQL数据库。
提交Git, 命名:“Set up Prisma”.
创建问题模型
创建模型Issue,属性有id, title, description, status, createAt, updateAt。默认情况下 prisma的string 为 191个字符,我们可以设置成255个;status使用enum类型,mysql支持enum 类型,有些数据库不支持,如果使用其他数据库的话,需要查询Prisma文档。
数据库迁移:npx prisma migrate dev
用DataGrip查看数据库,发现Issue表已创建。
提交Git,命名:Create the issue model.
构建 API
按照惯例,API一般放在api路径下。app目录下新下api目录,再创建issues目录,新建routes.tsx。创建并导出POST函数。在POST函数处理客户端的POST方法时,首先验证数据是否合格,我们需要安装使用zod:
npm i zod@3.22.2
在Issue模型中,title和description需要验证,其他属性都有默认值。我们要求title的字符在1-255之间,descritpition字符要大于1。
Prisma创建数据库记录时,需要用到prisma的client。新建client.ts并在官网复制代码生成并导出client,确保client同时只有唯一实例。
下面是app/api/issues/route.tsx的实现代码。
用Postman测试没问题。提交Git,命名:Build an API for creating Issues.
设置 Radix UI
我们使用流行的组件库Radix UI来实现问题创建界面。
Radix-ui提供了两类UI:themes 、 primitives,我们使用themes。
根据文档说明,安装:
npm install @radix-ui/themes
再在Layout.tsx引入css:
import ‘@radix-ui/themes/styles.css’;
并用Theme将Layout.tsx的body内容包起来。
测试一下Radix-UI,我们在app/issues/page.tsx添加一个按钮<Button>。
查看效果,发现按钮已用Radix-UI样式了。
提交Git,命名:Set up Radix UI
构建新问题页面
在app/issues目录下新建new目录,再创建page.tsx。
该页面需要2个输入字段:title和description,以及一个按钮。查看Radix-ui官网查看用法,title我们选择TextField,description选择TextArea,并调整宽度(max-w-xl)和垂直间隔(space-y-3)。TextField、TextArea不能是服务器组件,需要改成客户端组件(“use client”;)。
app/issues/page.tsx的按钮也添加Link可以导航到 /issues/new.
提交Git,命名:Build the new issue page.
自定义 Radix UI 主题
Radix UI的主题是可以自定义的,我们在layout.tsx的main后加上ThemePanel组件。
查看网页就有一个Theme调整板,可以选择具体的颜色,比如我们选择voilet,radius选择Medium等查看效果,满意后点“Copy Theme”。
复制的Theme 代码可以加到 Theme的属性里。
仔细检查网页,发现使用Radix-UI,默认的字体改变了。如果想让它回归默认的inter字体,如何做?
查看网页并搜索“next”了解相关步骤。
https://www.radix-ui.com/themes/docs/theme/typography
修改layout.tsx里的inter字体集,加上variable属性,body的className使用该variable.
app目录下创建theme-config.css。
再在layout.tsx导入theme-config.css即可。
提交Git,命名:customize Radix UI theme
添加 Markdown 编辑器
搜索”react-simplemde-editor”, 找到网站。
https://www.npmjs.com/package/react-simplemde-editor
安装:npm install –save react-simplemde-editor easymde
将/issues/new的TextArea更换成SimpleMDE.
回浏览器查看编辑器效果。
提交Git, 命名:Add a markdown editor.
处理表单提交
处理表单提交,使用流行库React Hook Form。安装:
npm i react-hook-form@7.46.1
编辑/app/issues/new/page.tsx,导入useForm,定义IssueForm的结构:title 和 description。调用useForm,并解构register。
使用register绑定 TextField.Input的title, {…register(‘title’)};但不能绑到SimpleMDE,因为它不支持额外属性,需要借助useForm的Controller组件。
设置Controller组件,name为description;control使用useForm的control,render指向SimpleMDE, 并将field赋给SimpleMDE。这样就完成了绑定。
接着将title和description改成由form包裹,onSubmit调用handlesubmit,将表单的数据POST给 /api/issuses。POST我们使用axios,当然也可以用fetch。需要安装:
npm i axios@1.5.0
在浏览器界面,提交一条问题,查看数据库就能收到一条记录。
提交git,命名:handle form submission.
处理错误
应用总会有潜在的错误,我们不希望用户看到出错界面,可以使用try-catch将可能出错的代码包起来。
我们不输入title和description时会出错,错误信息打印出来有些混乱。这些错误信息是服务器端zod校验返回的。
我们可以将错误信息用format方法格式化。
错误提示信息也是可以自定义的,我们在调用规则时给出第2个参数即可。比如title为空时我们就提示“Title is required.”
目前得到错误信息是POST到服务器,由zod校验不通过给出的提示。按理,字段不符合要求的表单不应该提交到服务器,而是应该由客户端完成校验,但在某些情况下由服务端校验是有用的,比如注册用户时校验用户是否存在。
实际项目中自然不能用console.log,需要将相应信息展示在页面上。
先使用useState保存error及setError,然后使用Radix-UI的Callout组件来展示错误信息。Callout的使用说明可以查看文档。
https://www.radix-ui.com/themes/docs/components/callout
查看页面。
提交Git。命名:handle errors.
实现客户端验证
服务器已建了zod的验证规则,我们肯定不希望客户端重新写一遍,可以将原先的验证规则分离成一个单独的文件复用。利用vscode的重构功能很方便实现。
我们将重构产生的新文件移于app目录并改名 validationSchemas.ts,后续的验证规则到时也添加在这个文件里。
为使用客户端验证,安装下面的库,该库用于集成各类验证库。
npm i @hookform/resolvers@3.3.1
在issues/new/page.tsx里,导入 zodResolver,useForm函数添加参数resolver与Schema关联,并解构出formState的errors,然后在表单的每个输入框下面显示出错信息。
由于Interface IssueForm结构与schema一致,有点重复,改成从schema转变。
回到页面查看。
提交Git,命名:Implement client-side validation
提取 ErrorMessage 组件
前面的客户端验证有些小问题,为了使错误信息的样式一致,在每个错误信息加上了 color=’red’ as=’p’,很明显,这是重复的事情,我们需要将错误信息提取成组件。
在app目录下新建components目录,再新建ErrorMessage.tsx。
然后调用ErrorMessage组件并提供错误信息即可。
提交Git,命名:Extract the ErrorMessage component.
添加Spinner
当我们提交表单时,为提高用户体验,应该在按钮处显示Spinner表示正在提交。先查找Tailwind的Spinner样式,谷歌搜索“tailwind elements spinner”,查到文档说明。复制下基本的样式。
https://tw-elements.com/docs/standard/components/spinners/
app/components目录新建spinner.tsx,并使用刚复制的代码。根据实际需要的图标大小修改高宽厚等。
然后在 /app/issues/new/page.tsx中使用。
添加一个状态 isSubmitting, 根据这个状态来指示是否显示 Spinner,同时也在提交时禁用提交按钮避免二次提交。
页面查看。
提交Git,命名:Add a spinner.
讨论:代码组织
我们提交表单时执行handleSumbit是个内联函数,有一些逻辑在里面,有人说,这会污染组件的渲染,需要将这些逻辑移出来。
这就见仁见智了,没有对错。一般而言,内联逻辑不是什么邪恶的东西,如果不太长放着也挺好,免得来回跳转查看,但如果比较长,可能是应该移出来。我们就移出来。
可能有人又会说了,handleSubmit里的 axios 属于 http层次的事情,应该分离出来放在另一模块里实现,放在一起破坏了“关注点分离”原则。“关注点分离”是一个古老的原则,它是说一个模块只关注一事件,关注点分离得越好,可重用性就越好。
但就目前而言,看不出有分离的必要,软件工程没有放之四海皆准的原则,需要根据实际情况运用,这里我们就放在一起。当然当逻辑很复杂时,是需要分离的。
提交Git,命名:Move inline function.
小结
本文我们实现了新建问题的功能。
下一篇根据项目线路实现查看问题的功能。