Remix 开发小技巧(五)
文章目录
- 类型安全的 Fetcher 钩子
- 一切从资源路由开始
- RPC 只是使用内置的 URL 获取
- 使用 Zod 验证您的 RPC
- 下一步是自定义提取器钩子
- 黑暗模式主题切换
- “最佳用户体验”是什么意思?
- 第一个要求
- 第二个要求
- 第三个要求
- 第四个要求
类型安全的 Fetcher 钩子
RPC 是一种远程过程调用,这是一种奇特的说法,表示“在服务器上运行的函数”。
他们现在正在经历一个鼎盛时期,gRPC、tRPC 和 Next.js Server Actions 等工具越来越受欢迎,并重新激发了对该模式的兴趣。
但我不建议将它们与 Remix 一起使用。
Remix 的工作方式与典型的 Web 框架略有不同。它的设计重点是渐进式增强和利用浏览器的强大功能。
通过使用 RPC 库,您将远离这些好处。
例如,不能使用 tRPC 路由器生成与基本 HTML 表单兼容的 Endpoints。
在 Next.js 服务器操作宣布之前,Next 框架从未真正承认数据突变是一回事。由于没有内置的支持,tRPC 非常适合该利基市场,两者成为开发的绝佳组合。
通过在编写 Remix 应用程序的方式中采用一些新习惯,您可以在不牺牲 Remix 优势的情况下获得 RPC 的好处。
一切从资源路由开始
Remix 源于 React Router,路由是它所说的语言。Remix 应用是通过创建路由来获取数据、处理突变、提供文件、呈现页面等来构建的。
在单个文件中,任何页面都可以通过指定操作函数成为 POST 端点。
export async function action({ request }: ActionArgs) {const body = await request.formData()const title = body.get("title")if (!title) {throw new Response("Title is required", { status: 400 })}const description = body.get("description")const item = db.create({title: title.toString(),description: description?.toString(),})return json(item, { status: 201 })
}
或者,它可以通过指定加载程序函数成为 GET 终结点。
export async function loader({ params }: LoaderArgs) {const item = db.get(params.id)if (item) {return json(item, { status: 200 })}throw new Response("Not found", { status: 404 })
}
这些函数的终结点 URL 是根据文件路径自动生成的。要调用这些函数,任何组件都需要知道它要调用的资源路由的 URL,然后它可以向该 URL 发出请求。
下面是一些以编程方式调用上一个 POST 终结点的客户端代码。
const body = new FormData()
body.append("title", title)
body.append("description", description)
const response = await fetch("/items", {method: "POST",body,
})
由于几个原因,这并不完全理想
- URL 是硬编码的,因此如果 URL 发生更改,您必须在使用它的所有位置更新它
- 您无法知道端点需要哪些参数
- 你无法知道响应会是什么样子
这就是 RPC 模式的用武之地
RPC 只是使用内置的 URL 获取
Web 应用程序通过在客户端和服务器之间发送 HTTP 请求来工作。
大多数(如果不是全部)专用 RPC 库的运行方式相同。它们只是抽象出HTTP请求和响应的细节,并为您提供一个不错的API。
我们可以自己做!以前面的请求为例,并将其包装在一个函数中。
我们可以使用 Typescript 来定义一个 Item 类型,该类型与我们传入的参数以及我们期望的响应相匹配。
type Item = {id: stringtitle: stringdescription?: string
}
export async function createItem(item: Omit<Item, "id">,
): Item {const body = new FormData()body.append("title", item.title)body.append("description", item.description)const response = await fetch("/items", {method: "POST",body,})if (!response.ok) {throw new Error("Failed to create item")}const createdItem = await response.json()if (!createdItem.id || !createdItem.title) {throw new Error("Invalid response")}return createdItem
}
如果从资源路由导出该函数,则可以在应用中的任何位置使用它,并获得完整的端到端类型安全性和自动完成功能。
import { createItem } from "~/routes/items.server"
使用 Zod 验证您的 RPC
手动验证可能会很痛苦,尤其是当类型变得更加复杂时。幸运的是,有一个库!
您可以使用 Zod 和 zod-form-data 在 RPC 和操作函数中验证表单数据。
import { z } from "zod"
import { zfd } from "zod-form-data"
const itemSchema = zfd.formData({title: z.string().min(1),description: z.string().optional(),
})
export async function action({ request }: ActionArgs) {const body = itemSchema.parse(await request.formData())const item = db.create({title: body.title,description: body.description,})return json(item, { status: 201 })
}
export async function createItem(item: z.infer<itemSchema>,
) {const body = new FormData()body.append("title", item.title)body.append("description", item.description)const response = await fetch("/items", {method: "POST",body,})if (!response.ok) {throw new Error("Failed to create item")}const createdItem = await response.json()return itemSchema.parse(createdItem)
}
现在,您可以在客户端和服务器中使用相同的验证,并且可以确信要发送和接收的数据是有效的。
下一步是自定义提取器钩子
如果您尝试调用的终端节点影响加载程序使用的数据,您可能不希望只对其进行常规提取调用。
Remix 的 useFetcher 钩子有很多你想要利用的生活质量功能,例如
- 自动重新获取装载机
- 重复请求取消
- 避免具有多个请求的争用条件
- 如果服务器返回重定向响应,则重定向客户端
因此,为了在这里正确使用它,我们可以在模式中采用创建一个自定义的类型安全获取器钩子,我们可以在应用程序中的任何位置使用它。
export async function useSubmitItem() {const fetcher = useFetcher()const submit = useCallback((item: z.infer<itemSchema>) => {const body = new FormData()body.append("title", item.title)body.append("description", item.description)fetcher.submit(body, {method: "POST",action: "/items",})},[fetcher],)return submit
}
这是使我们与 tRPC 等解决方案具有平价功能缺失的部分。
它感觉不像一个 RPC,更像是一个自定义钩子,但用法是相同的:
- 每个资源路由导出客户端可以调用以与服务器交互的函数
- 客户端与服务器交互的主要方式是通过这些功能
- 当服务器上的类型更新时,客户端将收到类型错误,直到它更新其函数的使用
此外,您还可以获得 RPC 库无法提供的好处,例如
- 对本机表单和表单组件的开箱即用支持
- 服务器代码与客户端代码的共置,因此您不需要定义所有 RPC 函数的中央路由器文件
黑暗模式主题切换
今天,多亏了像Tailwind这样的工具,我们可以轻松地在我们的应用程序中实现暗模式。现在,通过此功能(暗模式)寻求最佳用户体验是另一回事。这就是 Remix 的亮点,让您完全控制从后端到前端的用户体验。
“最佳用户体验”是什么意思?
为了获得更好的暗模式体验,我认为(这是个人意见)的要求是:
- 用户首次访问页面时,服务器必须以深色或浅色模式发送页面,具体取决于用户当时的计算机设置。
否则,用户将在应用程序中遇到闪光,这是因为服务器最初发送具有一个主题的页面,但随后应用程序在用户的计算机上检测到不同的主题并进行切换。如下图所示:
- 如果用户未选择任何模式,则当用户更改其计算机的模式时,页面将切换到深色或浅色模式。
- 如果用户选择某种模式,页面将切换到该模式,但如果他们更改其计算机上的模式,则不会影响页面。
- 如果用户选择模式,则模式将更改为用户计算机上当前设置的 System 模式。如果用户更改其计算机上的模式,则会影响页面。
第一个要求
为了满足第一个要求,我们需要在从服务器提供页面之前以某种方式确定用户在其计算机上选择的模式。据我了解,这是无法实现的,因为服务器不知道用户在其计算机上的选择。
那么,我们如何解决这个问题呢?
我学到的解决此问题的技巧是在组件中呈现一个
function ThemeMonitor() {return (<script dangerouslySetInnerHTML={{ __html: `console.log('Theme script is running');const allCookies = (document.cookie || "").split(";");const themeCookie = allCookies.find((cookie) => cookie.trim().startsWith("theme="));if (!themeCookie && navigator.cookieEnabled) {const themeDetected = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";document.cookie = 'theme=' + JSON.stringify({ detected: themeDetected, selected: "" }) + ';path=/';window.location.reload();}`,}}/>
);
}
然后我们可以在我们的 root.tsx 中添加:
<html><head><ThemeMonitor /><!--more tags here...--></head><!--more tags here...--></html>
这个技巧使我们能够在用户看到呈现的页面😎之前检测用户的模式。
存储在 Cookie 中的值是一个对象,我将在后面进一步研究,但它具有以下结构:
const theme = {detected: 'dark', selected: ''}
第二个要求
发现如何满足这一要求是一个惊喜。说实话,我不知道当用户在计算机上切换模式时,可以在浏览器中检测到。
我利用首选配色方案来解决这个问题。这一创新功能允许网站无缝适应用户在其操作系统或浏览器上的首选颜色模式。通过检测用户是否选择了浅色或深色模式,网站可以相应地定制其视觉外观,从而提高可读性和整体浏览体验。例:
@media (prefers-color-scheme: dark) {/* Styles for Dark Mode */body {background-color: #1a1a1a;color: #ffffff;}
}
让我们深入研究一下我如何在 ThemeMonitor 组件中实现此功能。
function ThemeMonitor() {const { revalidate } = useRevalidator();useEffect(() => {const themeQuery = window.matchMedia("(prefers-color-scheme: dark)");function handleThemeChange() {const currentTheme = getTheme(document.cookie);document.cookie = commitTheme({...currentTheme,detected: themeQuery.matches ? "dark" : "light",});revalidate();}themeQuery.addEventListener("change", handleThemeChange);return () => {themeQuery.removeEventListener("change", handleThemeChange);};}, [revalidate]);return <script dangerouslySetInnerHTML={{ __html: `***previous code here***`}} />
}
这里要提到的一些相关要点是:
- 由于页面从服务器接收需要处理的主题,因此在使用此钩子重新验证页面时,我们有可用的更新数据:
export async function loader({ request }: LoaderArgs) {const theme = getTheme(request.headers.get("Cookie"));return json({ theme }); // {detected: 'dark', selected: 'light'}
}
- MatchMedia: matchMedia 是一个JavaScript API,通过允许您向浏览器查询特定CSS媒体查询的当前状态来实现响应式设计。它提供了一种以编程方式检测设备特征(如屏幕宽度、方向和配色方案)的方法。
通过为首选配色方案(“深色”)创建媒体查询,我使用更改事件监视对此首选项的更改。每当发生更改时,我都会更新 Cookie 中检测到的主题并触发重新验证。
- Cookie:当您使用以下格式将 Cookie 分配给文档时:
document.cookie = newCookie;
它不会删除现有的 Cookie。相反,它会设置或更新您分配的特定 Cookie。这并不直观,但这就是我们拥有😩的 API .
第三个要求
为了满足第三个要求,我采用了一种策略,该策略涉及结构化数据,使我能够根据需要确定检测到的主题和用户选择的主题:
const theme = {detected: 'dark', selected: 'ligth'}
这种方法使我们能够确定要在页面上应用的主题将是:
const data = useLoaderData<typeof loader>();
const theme = data.theme.selected || data.theme.detected;
如果用户选择了模式,则 data.theme.selected || data.theme.detected 评估结果将是所选主题。😎
第四个要求
如果用户选择了以下 System 选项:
该 selected 属性将保持为空。因此,将应用检测到的主题。
好吧,就是这样。😀
相关文章:

Remix 开发小技巧(五)
文章目录 类型安全的 Fetcher 钩子一切从资源路由开始RPC 只是使用内置的 URL 获取使用 Zod 验证您的 RPC下一步是自定义提取器钩子 黑暗模式主题切换“最佳用户体验”是什么意思?第一个要求第二个要求第三个要求第四个要求 类型安全的 Fetcher 钩子 RPC 是一种远程…...
hive抽取mysql里的表,如果mysql表没有时间字段如何做增量抽取数据
如果MySQL表中没有时间字段,你可以通过其他方式实现增量抽取数据,以下是一些常见的方式: 使用自增主键:如果MySQL表中有自增主键,你可以记录上一次抽取数据时最大的主键值(即上一次抽取数据的结束位置&…...

20和遍历以及迭代器有关的一些东西
知识点有点散,只能这样记录了 1、这边是和遍历有关的: class Person:def __init__(self):self.result 1def __getitem__(self, item):self.result 1if self.result > 6:raise StopIteration(停止遍历)return self.resultpassp Person() for i in…...

前端工程化(editorconfig+ESLint+Prettier+StyleLint+Husky、Commitlint)
前言 致谢:有来技术大大 通过学习有来技术大大的文章和结合自己的实践,写一篇笔记记录一下 所使用的工具: ide项目风格(editorconfig)代码检查(ESLint)代码风格(Prettier)样式风格(StyleLint)git提交规范(Husky、Commitlint) 一、ide项目…...

UI自动化测试:Selenium+PO模式+Pytest+Allure整合
本人目前工作中未涉及到WebUI自动化测试,但为了提升自己的技术,多学习一点还是没有坏处的,废话不多说了,目前主流的webUI测试框架应该还是selenium,考虑到可维护性、拓展性、复用性等,我们采用PO模式去写我…...

【排序算法】详解冒泡排序及其多种优化稳定性分析
文章目录 算法原理细节分析优化1优化2算法复杂度分析稳定性分析总结 算法原理 冒泡排序(Bubble Sort) 就是从序列中的第一个元素开始,依次对相邻的两个元素进行比较,如果前一个元素大于后一个元素则交换它们的位置。如果前一个元素小于或等于后一个元素…...

使用 Go 和 Wails 构建跨平台桌面应用程序
由于多种原因,Electron 曾经(并且仍然)大受欢迎。首先,其跨平台功能使开发人员能够从单个代码库支持 Linux、Windows 和 macOS。最重要的是,它对于熟悉 Javascript 的开发人员来说有一个精简的学习曲线。 尽管它有其缺…...

花2个月时间学习,面华为测开岗要30k,面试官竟说:你不是在搞笑。。。
背景介绍 计算机专业,代码能力一般,之前有过两段实习以及一个学校项目经历。第一份实习是大二暑期在深圳的一家互联网公司做前端开发,第二份实习由于大三暑假回国的时间比较短(小于两个月),于是找的实习是…...

【Python学习笔记】字符串
1. 字符串定义 可以用双引号 、 单三引号、双三引号,下面的定义都是正确的 "你好" 你好 """你好"""其中三引号可以 直接写内容有多行 的字符串。如下 letter 刘总:您好!您发的货我们已经收到&am…...

【AUTOSAR中断管理】TC3XX中断系统介绍
摘要 这段文本主要介绍了AURIX TC3XX的中断系统(Interrupt Router,简称IR)以及中断注册的过程以及举例说明中断机制。 AURIX TC3XX 中断系统(Interrupt Router)介绍 流程图描述中断路由器(IR)处理服务请求并与服务提供者交互。 中断系统的作用是将service request进行…...

Unity实现摄像机向屏幕中间发射射线射击物体
1.创建一个准星放在屏幕中间 外部找个PNG透明图,拖到Unity文件夹,右上角改成精灵sprite2d 2.添加到UI画布 3.写脚本 首先,我们需要引入一些 "工具",就像我们在玩游戏时要先下载游戏客户端一样。这里的 "工具&quo…...
测试时数据增广(TTA)与mmdetection3d中的实现
1. 测试时数据增广 测试时数据增广(TTA)在测试时使用数据增广技术获取同一数据的多个“变体”,使用同一网络在这些“变体”以及原始数据上进行推断,最后整合所有结果作为该原始数据最终的预测结果。 TTA类似于集成学习,…...

深入探索BP神经网络【简单原理、实际应用和Python示例】
人工神经网络(Artificial Neural Networks)是一种受到生物神经网络启发的机器学习模型,它的应用范围广泛,包括图像识别、语音识别、自然语言处理等领域。其中,BP神经网络(Backpropagation Neural Network&a…...

【LVGL】SquareLine Studio入门基础操作
1.SquareLine Studio基础 在这篇文章中将介绍SquareLine Studio的基础操作、解释如何加载一个项目、布局结构。 启动软件后,可以加载之前的项目、创建项目、加载一个示例。 这里以打开示例audio_mixer为例,可以双击该项目打开或者选中该项目点击右下角的【创建】按…...

【单片机】19-TFT彩屏
一、背景知识--显示器 1.什么是TFT (1)LCD显示器的构成:液晶面板驱动器【电压驱动】控制器【逻辑控制】 (2)液晶面板大致分为:TN,TFT,IPS等 (3)驱动器是跟随…...

高质量!推荐一些免费自学网站
大家好,我是 jonssonyan 说到自学网站,大家第一印象肯定是”菜鸟教程“、”w3school“、B 站大学。这些教程当然非常的好,而且适合入门学习,但是存在一些缺点,第一,知识点比较分散,没有一个整体…...

Linux之open/close/read/write/lseek记录
一、文件权限 这里不做过多描述,只是简单的记录,因为下面的命令会涉及到。linux下一切皆是文件包括文本、硬件设备、管道、数据库、socket等。通过ls -l 命令可以查看到以下信息 drwxrwxrwx 1 root root 0 Oct 10 17:06 open -rwxrwxrwx 1 root roo…...
3D调研-摄像头
参考资料: 来源1:https://leap2.ultraleap.com/leap-motion-controller-2 来源2: Gemini 2 _双目结构光相机_机器人感知-奥比中光官网 来源3: 国内外深度相机大盘点,仅用于学习科普!--机器视觉网 来源4&…...

光耦合器继电器与传统继电器:哪种最适合您的项目?
在电子和电气工程领域,继电器的选择可以显着影响项目的性能和安全性。两种常见类型的继电器是光耦合器继电器和传统机电继电器。每个都有其优点和缺点,因此选择过程对于项目的成功结果至关重要。 光耦合器继电器:基础知识 光耦合器继电器&…...
分享关于职场心态
1.解决问题而不是解释原因 2.秉承工匠思维而不是激情思维 什么是工匠思维? 工匠思维(The craftsman mindset)对待职业生涯的一种方式;是以产出为中心的职业观,关注自己给世界(工作)带来的价值…...

多云管理“拦路虎”:深入解析网络互联、身份同步与成本可视化的技术复杂度
一、引言:多云环境的技术复杂性本质 企业采用多云策略已从技术选型升维至生存刚需。当业务系统分散部署在多个云平台时,基础设施的技术债呈现指数级积累。网络连接、身份认证、成本管理这三大核心挑战相互嵌套:跨云网络构建数据…...
零门槛NAS搭建:WinNAS如何让普通电脑秒变私有云?
一、核心优势:专为Windows用户设计的极简NAS WinNAS由深圳耘想存储科技开发,是一款收费低廉但功能全面的Windows NAS工具,主打“无学习成本部署” 。与其他NAS软件相比,其优势在于: 无需硬件改造:将任意W…...
【杂谈】-递归进化:人工智能的自我改进与监管挑战
递归进化:人工智能的自我改进与监管挑战 文章目录 递归进化:人工智能的自我改进与监管挑战1、自我改进型人工智能的崛起2、人工智能如何挑战人类监管?3、确保人工智能受控的策略4、人类在人工智能发展中的角色5、平衡自主性与控制力6、总结与…...

PPT|230页| 制造集团企业供应链端到端的数字化解决方案:从需求到结算的全链路业务闭环构建
制造业采购供应链管理是企业运营的核心环节,供应链协同管理在供应链上下游企业之间建立紧密的合作关系,通过信息共享、资源整合、业务协同等方式,实现供应链的全面管理和优化,提高供应链的效率和透明度,降低供应链的成…...
JVM垃圾回收机制全解析
Java虚拟机(JVM)中的垃圾收集器(Garbage Collector,简称GC)是用于自动管理内存的机制。它负责识别和清除不再被程序使用的对象,从而释放内存空间,避免内存泄漏和内存溢出等问题。垃圾收集器在Ja…...

(二)原型模式
原型的功能是将一个已经存在的对象作为源目标,其余对象都是通过这个源目标创建。发挥复制的作用就是原型模式的核心思想。 一、源型模式的定义 原型模式是指第二次创建对象可以通过复制已经存在的原型对象来实现,忽略对象创建过程中的其它细节。 📌 核心特点: 避免重复初…...

视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
python如何将word的doc另存为docx
将 DOCX 文件另存为 DOCX 格式(Python 实现) 在 Python 中,你可以使用 python-docx 库来操作 Word 文档。不过需要注意的是,.doc 是旧的 Word 格式,而 .docx 是新的基于 XML 的格式。python-docx 只能处理 .docx 格式…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...