Remix 开发小技巧(三)

2023-10-14 11:52
文章标签 技巧 开发 remix

本文主要是介绍Remix 开发小技巧(三),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 自动保存表单输入
    • useSubmit 钩子在导航时被取消,但 useFetcher 没有
    • 使用提取程序设置表单
    • 添加已退回的自动保存
  • 使用 Conform、Zod 进行表单验证
  • 使用 Zod 类型安全的环境变量
    • 为您的环境变量创建 Zod 数据结构
    • 修复进程.env以使用这些类型
    • 如果缺少任何环境变量,则崩溃

自动保存表单输入

过去,用户在使用Adobe Photoshop或Microsoft Word等应用程序时经常明确按下保存按钮是很常见的。

如果他们忘记保存,或者程序崩溃,或者他们覆盖了保存文件,他们将失去数小时或更长时间的工作。

但随着世界转向基于云的应用程序,自动保存成为常态,用户开始期望如果他们进行更改,它会被记住。

useSubmit 钩子在导航时被取消,但 useFetcher 没有

以编程方式提交表单的自然选择是 useSubmit 挂钩,但它不是自动保存表单的好选择。

Remix 使用全局导航状态,因此,如果您单击指向一个页面的链接,然后在加载之前单击指向其他页面的链接,则对第一页的请求将被取消。

不幸的是, useSubmit 也使用相同的导航状态。如果您用于 useSubmit 提交表单,然后在表单完成之前导航到其他页面,则表单提交将被取消。

这对于您明确提交的表单可能有意义,但对于预期会自动保存的输入,您不希望仅因为用户单击链接太快而保存失败。

每个 useFetcher 钩子都有自己的状态,因此您可以使用它来发出请求,如果用户导航离开,该请求不会被取消。

使用提取程序设置表单

不要使用常规的 Remix 组件,而是使用提取程序返回的组件 fetcher.Form 。

import { useFetcher } from "@remix-run/react"
import { conform, useForm } from "@conform-to/react"
export default function Example() {const fetcher = useFetcher()const [form, fields] = useForm<{email: stringname: string}>()return (<fetcher.Form method="POST" {...form.props}><input {...conform.input(fields.email)} /><input {...conform.input(fields.name)} /><button type="submit">{fetcher.state === "submitting"? "Saving…": "Save"}</button></fetcher.Form>)
}

添加已退回的自动保存

我们将使用增强的 useDebounceFetcher 钩子,它将自动对单个输入的提交进行去抖动。

如果用户快速键入并无缝地按 Tab 键转到每个下一个输入,在填写大表单时从不暂停,则很多时间可能会因未保存而工作而过去。为了避免这种情况,我喜欢确保每个输入都是单独自动保存的。

  • 如果他们在键入时暂停片刻,输入将被保存。
  • 如果他们按 Tab 键转到下一个输入,则上一个输入将立即保存,而无需等待去抖动延迟通过。

多亏了useDebounceFetcher钩子,我们只需要几行代码就可以做到这一点。

创建一个 emailFetcher,并在电子邮件输入的 onChange 和 onBlur 处理程序中调用它。

import { useFetcher } from "@remix-run/react"
import { conform, useForm } from "@conform-to/react"
import { useDebounceFetcher } from "./use-debounce-fetcher"
export default function Example() {const fetcher = useFetcher()const [form, fields] = useForm<{email: stringname: string}>()const emailFetcher = useDebounceFetcher()return (<fetcher.Form method="POST" {...form.props}><input{...conform.input(fields.email)}onChange={(event) => {emailFetcher.debounceSubmit(event.currentTarget.form,{replace: true,debounceTimeout: 500,},)}}onBlur={(event) => {emailFetcher.debounceSubmit(event.currentTarget.form,{replace: true,},)}}/><input {...conform.input(fields.name)} /><button type="submit">{fetcher.state === "submitting"? "Saving…": "Save"}</button></fetcher.Form>)
}

您还需要对名称输入执行相同的操作,但现在是考虑如何抽象并避免重复的好时机。

  • prop 级抽象 – 创建一个接受获取器并返回对象的 { onChange(), onBlur() } 函数可以很好地工作。这将类似于函数 conform.input 。不过,您仍然需要为每个输入创建一个钩子。
  • 组件级抽象 – 创建一个输入组件,然后在组件中创建提取器和事件处理程序。

我喜欢组件级抽象,因为它将所有内容都保存在一个地方,它还提供了一个添加其他功能或放置Tailwind CSS的地方。

function CustomInput(props: React.InputHTMLAttributes<HTMLInputElement>,
) {const fetcher = useDebounceFetcher()return (<inputclassName="block w-96 rounded border border-gray-300 px-4 py-3 focus:ring-1 focus:ring-indigo-600"{...props}onChange={(event) => {fetcher.debounceSubmit(event.currentTarget.form, {replace: true,debounceTimeout: 500,})// optional: call the onChange prop if it existsprops.onChange?.(event)}}onBlur={(event) => {fetcher.debounceSubmit(event.currentTarget.form, {replace: true,})props.onBlur?.(event)}}/>)
}

现在,您可以使用去抖动输入组件代替常规输入,它将自动保存更改和模糊。

return (<fetcher.Form method="POST" {...form.props}><DebouncedInput {...conform.input(fields.email)} /><DebouncedInput {...conform.input(fields.name)} /></fetcher.Form>
)

使用 Conform、Zod 进行表单验证

Conform 是一个表单验证库,可帮助您使用服务器端验证和客户端错误处理构建表单。它与Remix和Zod配合得很好。

使用 Conform,您可以使用 Zod 表示您的表单架构。

import { z } from "zod"
const schema = z.object({title: z.string(),description: z.string().optional(),status: z.enum(["todo", "doing", "done"]),
})

在您的表单组件中,使用 Conform 的 useForm 钩子来获取使表单工作所需的道具。

  • 该方法 onValidate 是我们使用它使用 zod 模式的地方。
  • lastSubmission 道具从动作中获取响应,以便它可以为我们处理错误。
import { conform, useForm } from "@conform-to/react"
import {getFieldsetConstraint,parse,
} from "@conform-to/zod"
export default function Example() {const actionData = useActionData<typeof action>()const [form, fields] = useForm({id: "example",onValidate({ formData }) {return parse(formData, { schema })},lastSubmission: actionData?.submission,shouldRevalidate: "onBlur",})return ()
}

接下来,我们将使用该 fields 对象来获取表单中每个字段所需的道具。

  • 传递给 form.props 组件。
  • 每个输入都会获得一个自动生成的 HTML ID。传递 fields.title.id 到 htmlFor 道具上的
  • 传递到 conform.input(fields.title)

每个输入在 中 fields.title.errors 都有自己的错误列表。

export default function Example() {const [form, fields] = useForm({})return (<Form method="POST" {...form.props}><div><label htmlFor={fields.title.id}>Title</label><input {...conform.input(fields.title)} /></div><div><label htmlFor={fields.description.id}>Description</label><input {...conform.input(fields.description)} />{fields.description.errors ? (<div role="alert">{fields.description.errors[0]}</div>) : null}</div><div><label htmlFor={fields.status.id}>Status</label><select {...conform.select(fields.status)}><option value="todo">Todo</option><option value="doing">Doing</option><option value="done">Done</option></select></div><button type="submit"> Submit </button></Form>)
}

最后一步是创建一个将处理表单提交的操作。

  • 将 中的 @conform-to/zod parse 方法与架构一起使用
  • 失败的提交将具有空 value 属性,因此您可以使用该属性来处理错误。在响应中返回提交对象,以便表单可以显示错误。
  • 如果提交有效,则可以使用该 value 属性获取输入数据库所需的数据。
import { parse } from "@conform-to/zod"
export async function action({ request }: ActionArgs) {const formData = await request.formData()const submission = await parse(formData, { schema })if (!submission.value) {return json({ status: "error", submission },{ status: 400 },)}const { title, description, status } = submission.valueawait db.todos.create({title,description,status,})return redirect("/todos")
}

使用 Zod 类型安全的环境变量

环境变量是在运行时配置应用程序的一种方法。你可以告诉应用从环境中读取值,而不是将值硬编码到代码中。

这对于 API 密钥、数据库凭据和其他不希望存储在代码库中的敏感信息非常有用。

由于这些变量是在运行时提供的,而不是在构建时提供的,因此我们无法静态地保证将设置某些变量或它们将具有正确的类型。

这意味着,如果您尝试在 IDE 中访问 process.env ,它不会为您自动完成变量,并且您不会进行任何类型检查。当您尝试访问变量时,您需要检查以确保在使用之前已定义它。

创建名为 env.server.ts 的文件,我们将使用它来:

  • 如果缺少任何必需的环境变量,则在启动时使应用程序崩溃
  • 并为环境变量添加类型定义,以便我们可以在 IDE 中获得自动完成和类型检查

为您的环境变量创建 Zod 数据结构

为您的环境变量定义 Zod 架构。最简单的选择是将它们全部设置为 ,但如果需要 z.string() ,您可以通过格式检查和其他验证来获得更高级的服务。

import { z } from "zod"
const zodEnv = z.object({// DatabaseDATABASE_URL: z.string(),// CloudflareCLOUDFLARE_IMAGES_ACCOUNT_ID: z.string(),CLOUDFLARE_IMAGES_API_TOKEN: z.string(),// SentrySENTRY_DSN: z.string(),SENTRY_RELEASE: z.string().optional(),
})

修复进程.env以使用这些类型

默认情况下, process.env 只是一个具有未知值的普通对象。由于我们强制要求环境变量与我们的 Zod 模式匹配,因此我们可以告诉 TypeScript 将其视为 process.env 具有这些类型。

Zod的 TypeOf 实用程序会将我们的Zod模式转换为我们需要的Typescript类型。

import { TypeOf } from "zod"
declare global {namespace NodeJS {interface ProcessEnv extends TypeOf<typeof zodEnv> {}}
}

如果缺少任何环境变量,则崩溃

应用运行时无法修改环境变量。它们在启动时严格设置,因此如果不存在任何设置,我们将希望立即关闭应用程序。

try {zodEnv.parse(process.env)
} catch (err) {if (err instanceof z.ZodError) {const { fieldErrors } = err.flatten()const errorMessage = Object.entries(fieldErrors).map(([field, errors]) =>errors ? `${field}: ${errors.join(", ")}` : field,).join("\n  ")throw new Error(`Missing environment variables:\n  ${errorMessage}`,)process.exit(1)}
}

在应用程序中尽早导入此模块。

此代码在任何函数或类之外定义,因此它将在导入时立即运行。

如果你正在做一个Node/Express应用程序,最好的位置是在 server.ts 文件中,或者与Remix App Server一起。 entry.server.ts 中加入:

import "~/env.server"

完整代码:

// app/env.server.ts
import { z, TypeOf } from "zod"
const zodEnv = z.object({// DatabaseDATABASE_URL: z.string(),// CloudflareCLOUDFLARE_IMAGES_ACCOUNT_ID: z.string(),CLOUDFLARE_IMAGES_API_TOKEN: z.string(),// SentrySENTRY_DSN: z.string(),SENTRY_RELEASE: z.string().optional(),
})
declare global {namespace NodeJS {interface ProcessEnv extends TypeOf<typeof zodEnv> {}}
}
try {zodEnv.parse(process.env)
} catch (err) {if (err instanceof z.ZodError) {const { fieldErrors } = err.flatten()const errorMessage = Object.entries(fieldErrors).map(([field, errors]) =>errors ? `${field}: ${errors.join(", ")}` : field,).join("\n  ")throw new Error(`Missing environment variables:\n  ${errorMessage}`,)process.exit(1)}
}

这篇关于Remix 开发小技巧(三)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/210326

相关文章

基于Python开发电脑定时关机工具

《基于Python开发电脑定时关机工具》这篇文章主要为大家详细介绍了如何基于Python开发一个电脑定时关机工具,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 简介2. 运行效果3. 相关源码1. 简介这个程序就像一个“忠实的管家”,帮你按时关掉电脑,而且全程不需要你多做

怎么关闭Ubuntu无人值守升级? Ubuntu禁止自动更新的技巧

《怎么关闭Ubuntu无人值守升级?Ubuntu禁止自动更新的技巧》UbuntuLinux系统禁止自动更新的时候,提示“无人值守升级在关机期间,请不要关闭计算机进程”,该怎么解决这个问题?详细请看... 本教程教你如何处理无人值守的升级,即 Ubuntu linux 的自动系统更新。来源:https://

Java中的Opencv简介与开发环境部署方法

《Java中的Opencv简介与开发环境部署方法》OpenCV是一个开源的计算机视觉和图像处理库,提供了丰富的图像处理算法和工具,它支持多种图像处理和计算机视觉算法,可以用于物体识别与跟踪、图像分割与... 目录1.Opencv简介Opencv的应用2.Java使用OpenCV进行图像操作opencv安装j

将Python应用部署到生产环境的小技巧分享

《将Python应用部署到生产环境的小技巧分享》文章主要讲述了在将Python应用程序部署到生产环境之前,需要进行的准备工作和最佳实践,包括心态调整、代码审查、测试覆盖率提升、配置文件优化、日志记录完... 目录部署前夜:从开发到生产的心理准备与检查清单环境搭建:打造稳固的应用运行平台自动化流水线:让部署像

基于Qt开发一个简单的OFD阅读器

《基于Qt开发一个简单的OFD阅读器》这篇文章主要为大家详细介绍了如何使用Qt框架开发一个功能强大且性能优异的OFD阅读器,文中的示例代码讲解详细,有需要的小伙伴可以参考一下... 目录摘要引言一、OFD文件格式解析二、文档结构解析三、页面渲染四、用户交互五、性能优化六、示例代码七、未来发展方向八、结论摘要

Java 枚举的常用技巧汇总

《Java枚举的常用技巧汇总》在Java中,枚举类型是一种特殊的数据类型,允许定义一组固定的常量,默认情况下,toString方法返回枚举常量的名称,本文提供了一个完整的代码示例,展示了如何在Jav... 目录一、枚举的基本概念1. 什么是枚举?2. 基本枚举示例3. 枚举的优势二、枚举的高级用法1. 枚举

不删数据还能合并磁盘? 让电脑C盘D盘合并并保留数据的技巧

《不删数据还能合并磁盘?让电脑C盘D盘合并并保留数据的技巧》在Windows操作系统中,合并C盘和D盘是一个相对复杂的任务,尤其是当你不希望删除其中的数据时,幸运的是,有几种方法可以实现这一目标且在... 在电脑生产时,制造商常为C盘分配较小的磁盘空间,以确保软件在运行过程中不会出现磁盘空间不足的问题。但在

在 VSCode 中配置 C++ 开发环境的详细教程

《在VSCode中配置C++开发环境的详细教程》本文详细介绍了如何在VisualStudioCode(VSCode)中配置C++开发环境,包括安装必要的工具、配置编译器、设置调试环境等步骤,通... 目录如何在 VSCode 中配置 C++ 开发环境:详细教程1. 什么是 VSCode?2. 安装 VSCo

Python中列表的高级索引技巧分享

《Python中列表的高级索引技巧分享》列表是Python中最常用的数据结构之一,它允许你存储多个元素,并且可以通过索引来访问这些元素,本文将带你深入了解Python列表的高级索引技巧,希望对... 目录1.基本索引2.切片3.负数索引切片4.步长5.多维列表6.列表解析7.切片赋值8.删除元素9.反转列表

C#图表开发之Chart详解

《C#图表开发之Chart详解》C#中的Chart控件用于开发图表功能,具有Series和ChartArea两个重要属性,Series属性是SeriesCollection类型,包含多个Series对... 目录OverviChina编程ewSeries类总结OverviewC#中,开发图表功能的控件是Char