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)对待职业生涯的一种方式;是以产出为中心的职业观,关注自己给世界(工作)带来的价值…...
可靠性+灵活性:电力载波技术在楼宇自控中的核心价值
可靠性灵活性:电力载波技术在楼宇自控中的核心价值 在智能楼宇的自动化控制中,电力载波技术(PLC)凭借其独特的优势,正成为构建高效、稳定、灵活系统的核心解决方案。它利用现有电力线路传输数据,无需额外布…...
Keil 中设置 STM32 Flash 和 RAM 地址详解
文章目录 Keil 中设置 STM32 Flash 和 RAM 地址详解一、Flash 和 RAM 配置界面(Target 选项卡)1. IROM1(用于配置 Flash)2. IRAM1(用于配置 RAM)二、链接器设置界面(Linker 选项卡)1. 勾选“Use Memory Layout from Target Dialog”2. 查看链接器参数(如果没有勾选上面…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
AI编程--插件对比分析:CodeRider、GitHub Copilot及其他
AI编程插件对比分析:CodeRider、GitHub Copilot及其他 随着人工智能技术的快速发展,AI编程插件已成为提升开发者生产力的重要工具。CodeRider和GitHub Copilot作为市场上的领先者,分别以其独特的特性和生态系统吸引了大量开发者。本文将从功…...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
3403. 从盒子中找出字典序最大的字符串 I
3403. 从盒子中找出字典序最大的字符串 I 题目链接:3403. 从盒子中找出字典序最大的字符串 I 代码如下: class Solution { public:string answerString(string word, int numFriends) {if (numFriends 1) {return word;}string res;for (int i 0;i &…...
Docker 本地安装 mysql 数据库
Docker: Accelerated Container Application Development 下载对应操作系统版本的 docker ;并安装。 基础操作不再赘述。 打开 macOS 终端,开始 docker 安装mysql之旅 第一步 docker search mysql 》〉docker search mysql NAME DE…...
站群服务器的应用场景都有哪些?
站群服务器主要是为了多个网站的托管和管理所设计的,可以通过集中管理和高效资源的分配,来支持多个独立的网站同时运行,让每一个网站都可以分配到独立的IP地址,避免出现IP关联的风险,用户还可以通过控制面板进行管理功…...
掌握 HTTP 请求:理解 cURL GET 语法
cURL 是一个强大的命令行工具,用于发送 HTTP 请求和与 Web 服务器交互。在 Web 开发和测试中,cURL 经常用于发送 GET 请求来获取服务器资源。本文将详细介绍 cURL GET 请求的语法和使用方法。 一、cURL 基本概念 cURL 是 "Client URL" 的缩写…...
MySQL 主从同步异常处理
阅读原文:https://www.xiaozaoshu.top/articles/mysql-m-s-update-pk MySQL 做双主,遇到的这个错误: Could not execute Update_rows event on table ... Error_code: 1032是 MySQL 主从复制时的经典错误之一,通常表示ÿ…...
