Remix 开发小技巧

news2024/11/15 20:53:09

文章目录

  • 请求搜索参数重定向
  • 进行服务器端分页
    • 从 URL 读取查询参数
    • 不要覆盖其他查询参数
    • 使用提交按钮而不是链接
    • 向按钮添加标签
  • Loaders & Actions 中中止异步调用
  • 全局类型及类型安全

请求搜索参数重定向

假设您要确保始终设置特定的搜索参数。为此,您可以首先检查是否设置了搜索参数,如果没有,则重定向到设置了搜索参数的同一路由。

export async function loader({ request }: LoaderFunctionArgs) {
  let url = new URL(request.url);

  if (!url.searchParams.has("key")) {
    url.searchParams.set("key", "value");
    throw redirect(url.toString());
  }

  // the rest of your code
}

首先,我们创建一个 URL 实例,这使我们能够更轻松地使用 URL。

然后我们访问 URLSearchParams 实例 url.searchParams 并检查它是否没有我们的密钥。如果没有,我们将其设置为 url.searchParams.set(“key”, “value”) .

最后,我们使用新 URL 抛出重定向。这会将用户重定向到相同的路由,但设置了搜索参数。

如果请求 URL 已设置搜索参数,则加载程序将继续执行代码的其余部分。

进行服务器端分页

开发人员通常将客户端分页作为折衷措施实现,以避免服务器端分页的复杂性。

在服务器上处理分页时,需要将特定的页面和页面大小传达给服务器,并且需要返回项总数,以便客户端可以呈现分页控件。

随着页面的更改,您需要更新 URL 并创建新的历史记录条目,以便用户可以使用后退按钮转到上一页,并与其他用户共享特定页面。

然后是数据获取问题:每次页面更改时,您都需要从服务器获取新数据并将其呈现在页面上。

但Remix就是为这种事情而构建的。

为每个 URL 显示正确的数据是 Remix 的目的,因此使用 Remix 实现服务器端分页实际上比在客户端上实现分页更容易。

如果您将页码和大小存储在 URL 中,Remix 将获取新数据并在 URL 更改时立即显示在页面上,并且由于数据库通常有办法一次返回一页数据,因此您也可以免费获得。

从 URL 读取查询参数

OData 规范规定,应使用 和 查询参数分别指定页面大小 $top$skip 页码。

因此,如果要显示 10 个项目的第一页,请使用 ?$top=10 ,而要显示第二页,请使用 ?$top=10&$skip=10

从加载程序中的请求 URL 读取查询参数,并使用它们从数据库中获取正确的数据页。

根据您的数据库适配器,这部分看起来会有所不同,但通常有一种一流的方法可以做到这一点,例如 Prisma 的选项 { take, skip } 或 SQL 和 LIMIT OFFSET 子句。

export async function loader({ request }: LoaderArgs) {
  const url = new URL(request.url)
  const $top = Number(url.searchParams.get("$top")) || 10
  const $skip = Number(url.searchParams.get("$skip")) || 0
  // Slice the current page of issues from the database
  const issues = db.issues.slice($skip, $skip + $top)
  return json({
    total: db.issues.length,
    issues,
  })
}

让我们创建一个滚动分页组件,显示当前页面以及它之前和之后的几个页面,就像 Google 搜索一样。

在这里插入图片描述

关于此实现,有几点需要注意

  • 选择第一页时,上一页按钮被禁用,并在当前页面右侧显示 6 页
  • 选择最后一页时,下一页按钮将被禁用,并在当前页面左侧显示 6 页
  • 当当前页面位于中间时,它向左显示 3 页,向右显示 3 页

由于组件可以独立访问 URL 参数,因此我们不需要任何回调或状态管理即可完成此操作。每个按钮都只是一个表单中的提交按钮,用于将 $skip 参数设置为正确的值。

我们需要传递给组件的一个值是项目总数,因此它可以计算总页数。

export function PaginationBar({
  total,
}: {
  total: number
}) {
  const [searchParams] = useSearchParams()
  const $skip = Number(searchParams.get("$skip")) || 0
  const $top = Number(searchParams.get("$top")) || 10
  const totalPages = Math.ceil(total / $top)
  const currentPage = Math.floor($skip / $top) + 1
  const maxPages = 7
  const halfMaxPages = Math.floor(maxPages / 2)
  const canPageBackwards = $skip > 0
  const canPageForwards = $skip + $top < total
  const pageNumbers = [] as Array<number>
  if (totalPages <= maxPages) {
    for (let i = 1; i <= totalPages; i++) {
      pageNumbers.push(i)
    }
  } else {
    let startPage = currentPage - halfMaxPages
    let endPage = currentPage + halfMaxPages
    if (startPage < 1) {
      endPage += Math.abs(startPage) + 1
      startPage = 1
    }
    if (endPage > totalPages) {
      startPage -= endPage - totalPages
      endPage = totalPages
    }
    for (let i = startPage; i <= endPage; i++) {
      pageNumbers.push(i)
    }
  }
  const existingParams = Array.from(
    searchParams.entries(),
  ).filter(([key]) => {
    return key !== "$skip" && key !== "$top"
  })
  return (
    <Form
      method="GET"
      className="flex items-center gap-1"
      preventScrollReset
    >
      <>
        {[["$top", String($top)], ...existingParams].map(
          ([key, value]) => {
            return (
              <input
                key={key}
                type="hidden"
                name={key}
                value={value}
              />
            )
          },
        )}
      </>
      <button
        type="submit"
        name="$skip"
        className="text-neutral-600"
        value="0"
        disabled={!canPageBackwards}
        aria-label="First page"
      >
        <Icon name="double-arrow-left" />
      </button>
      <button
        variant="outline"
        size="xs"
        type="submit"
        name="$skip"
        className="text-neutral-600"
        value={Math.max($skip - $top, 0)}
        disabled={!canPageBackwards}
        aria-label="Previous page"
      >
        <Icon name="arrow-left" />
      </button>
      {pageNumbers.map((pageNumber) => {
        const pageSkip = (pageNumber - 1) * $top
        const isCurrentPage = pageNumber === currentPage
        const isValidPage =
          pageSkip >= 0 && pageSkip < total
        if (isCurrentPage) {
          return (
            <button
              type="submit"
              name="$skip"
              className="min-w-[2rem] bg-neutral-200 text-black"
              key={`${pageNumber}-active`}
              value={pageSkip}
              aria-label={`Page ${pageNumber}`}
              disabled={!isValidPage}
            >
              {pageNumber}
            </button>
          )
        } else {
          return (
            <button
              type="submit"
              className="min-w-[2rem] font-normal text-neutral-600"
              name="$skip"
              key={pageNumber}
              value={pageSkip}
              aria-label={`Page ${pageNumber}`}
              disabled={!isValidPage}
            >
              {pageNumber}
            </button>
          )
        }
      })}
      <button
        type="submit"
        name="$skip"
        className="text-neutral-600"
        value={Math.min($skip + $top, total - $top + 1)}
        disabled={!canPageForwards}
        aria-label="Next page"
      >
        <Icon name="arrow-right" />
      </button>
      <button
        type="submit"
        name="$skip"
        className="text-neutral-600"
        value={(totalPages - 1) * $top}
        disabled={!canPageForwards}
        aria-label="Last page"
      >
        <Icon name="double-arrow-right" />
      </button>
    </Form>
  )
}

不要覆盖其他查询参数

URL是一个非常方便的全局状态,但它带有与所有全局状态解决方案相同的警告:它们是全局的。

应用的其他部分可能依赖于自己的查询参数,当我们创建设置参数的分页组件时,我们不希望覆盖其他查询 $skip 参数。

为了解决这个问题,我们可以使用 useSearchParams 钩子来获取现有的查询参数,并为每个参数创建隐藏的输入。这样,当提交表单时,现有查询参数将包含在请求中。

如果没有 $top 参数,我也想将其添加到表单中,以便始终发送。只要加载器代码给出合理的默认值,从技术上讲,您就不需要这样做,但是您需要记住在两个地方更新默认页面大小。

const [searchParams] = useSearchParams()
const existingParams = Array.from(
  searchParams.entries(),
).filter(([key]) => {
  return key !== "$skip" && key !== "$top"
})
return (
  <Form method="GET" className="flex items-center gap-1">
    <>
      {[["$top", String($top)], ...existingParams].map(
        ([key, value]) => {
          return (
            <input
              key={key}
              type="hidden"
              name={key}
              value={value}
            />
          )
        },
      )}
    </>
    {/* ... */}
  </Form>
)

使用提交按钮而不是链接

链接在这里似乎很自然,使用链接不会错,但是我更喜欢为此使用表单有几个原因。

首先,没有办法在HTML中实际禁用链接。当用户位于分页的任一端时,我们希望禁用“上一个”和“下一个”按钮,因此你只能用非交互式元素替换它们以指示它们已禁用。

每个链接都需要一个构造的 URL,其中包含页面上的所有现有查询参数以及适当的 $skip 值。当屏幕阅读器用户访问页面时,这些链接中的每一个都会被列为导航点,这将非常嘈杂。

搜索爬虫也可能跟踪链接并索引它们,这可能会用一堆重复的页面或分裂的页面排名污染您的 SEO 个人资料。

使用表单意味着您可以为任何现有查询参数添加一次隐藏输入,无论单击哪个提交按钮,它都将包含在内。

向按钮添加标签

这里有一些带有箭头而不是文本的图标按钮,因此请确保为它们添加辅助标签,以便屏幕阅读器用户知道他们的操作。

简单的 aria-label 属性可以处理大多数情况,尽管一些专家建议改用视觉隐藏的文本,以便更好地跨设备兼容。

Loaders & Actions 中中止异步调用

假设您正在编写一个加载器,该加载器需要执行 fetch 调用来获取一些数据,仅举个例子就很简单:

import { json } from "@remix-run/node";

export async function loader() {
  let response = await fetch("https://jsonplaceholder.typicode.com/todos");
  return json(await response.json());
}

现在让我们假设用户点击 到这条路线,所以 Remix 在执行导航之前获取加载器数据。但是如果用户在我们的加载器发送响应之前点击 到另一个路由,它会发送一个信号来中止请求,这基本上会忽略响应。

但是,由于我们的加载器的工作方式,由于我们已经收到了请求,我们仍将完全执行加载器并生成响应。

相反,我们可以做得更好:我们可以知道浏览器何时中止请求并停止执行我们的请求;我们甚至可以中止自己的获取调用!

export async function loader({ request }: LoaderFunctionArgs) {
  let response = await fetch("https://jsonplaceholder.typicode.com/todos", {
    signal: request.signal
  });
  return json(await response.json());
}

request.signal 如果浏览器中止它,它将中止,因此我们可以将相同的信号传递给我们的获取调用以获得相同的结果。

如果我们执行多个 fetch 调用,我们也可以重用它,因此每个调用都将中止。

import {
  json,
  type LoaderFunctionArgs
} from "@remix-run/node";

export async function loader({ request }: LoaderFunctionArgs) {
  let [res1, res2] = await Promise.all([
    fetch(url1, { signal: request.signal }),
    fetch(url2, { signal: request.signal }),
  ]);
  // more code
}

要考虑的是 中止的获取会引发错误 AbortError: The operation was aborted. .

这意味着我们的代码在获取之后或之后 Promise.all 将不会运行,因为抛出此错误。但这也意味着,如果我们使用 try/catch 处理加载器中的错误,我们将需要考虑它。

export async function loader({ request }: LoaderFunctionArgs) {
  try {
    let response = await fetch("https://jsonplaceholder.typicode.com/todos", {
      signal: request.signal
    });
    return json(await response.json());
  } catch (error) {
    if (error instanceof Error && error.name === "AbortError") {
      // for aborted errors send a 204 No Content response
      return new Response(null, { status: 204 });
    }
    throw error;
  }
}

如果我们只做获取调用,这一切都很棒,但是如果我们做任何其他异步代码怎么办?

好吧,这在很大程度上取决于异步代码是否支持 AbortSignal,但如果不支持,我们始终可以手动检查中止状态。

if (request.signal.aborted) console.log("aborted!");

通过使用 request.signal.aborted ,我们将知道何时发生这种情况。因此,假设您正在从文件系统读取文件;根据内容,您读取第二个文件。

let pkg = await readFile(resolve("./package.json"), "utf-8");

if (request.signal.aborted) {
  let error = new Error("Aborted");
  // this is required to simulate an AbortError, but we can
  // also throw a normal Error or a custom Error subclass and
  // then handle it in our try/catch
  error.name = "AbortError";
  throw error;
}

let tsConfig = await readFile(resolve("./tsconfig.json"), "utf-8");

如果我们正在使用数据库 ORM,我们还可以使用数据库事务让我们在请求中止时中止对数据库所做的一项或多项更改。

export async function action({ request }: ActionFunctionArgs) {
  let result = await db.transaction(async trx => {
    // perform and await first query
    if (request.signal.aborted) throw new Error("Aborted");
    // perform and await second query
    if (request.signal.aborted) throw new Error("Aborted");
  });
  return json(result);
}

通过在数据库查询之间进行检查 request.signal.aborted ,我们可以随时停止,事务将确保我们不会进行半途而废的更改。

如果ORM支持AbortSignal,我们可能会简化它。

let result = await db.transaction(async trx => {
  // perform and await first query
  // perform and await second query
}, { signal: request.signal });

但是我现在不知道有任何支持AbortSignal的ORM。

需要考虑的事项:如果我们中止对另一个 API 的 POST 请求,并且该 API 没有中止数据库更改,我们可能仍然会遇到一半突变发生的问题,因为一半已经运行但另一半已中止。

如果我们不确定,最好不要将其用于操作,而将其限制为仅加载程序。这样,我们的加载器仍然可以更早地停止运行,并且我们的突变可以安全地执行。

全局类型及类型安全

TypeScript 有两种主要类型的文件。 .ts 文件是包含类型和可执行代码的实现文件。这些是生成 .js 输出的文件,是您通常编写代码的位置。

.d.ts 文件是仅包含类型信息的声明文件。这些文件不生成 .js输出;它们仅用于类型检查。

让我们利用这个事实来创建我们自己的声明文件,并使用它来键入我们的 Remix 应用程序。

首先,在 Remix 项目的根目录上创建一个 index.d.ts 文件。然后,您可以将以下代码粘贴到其中:

// As long as Remix is using React this part shouldn't change
import type { useActionData, useLoaderData, ShouldRevalidateFunction as RemixShouldRevalidateFunction } from "@remix-run/react";
// These imports will depend on the runtime you are using, if it's node you
// don't need to change anything, if it's vercel for example, change to
// from "@remix-run/vercel";
import type {
  V2_MetaFunction,
  LinksFunction as RemixLinksFunction,
  LoaderArgs as RemixLoaderArgs,
  ActionArgs as RemixActionArgs,
  DataFunctionArgs as RemixDataFunctionArgs,
  HeadersFunction as RemixHeadersFunction
// Change based on runtime
} from "@remix-run/node";

// Declares global types so we don't have to import them anywhere we just consume them
export declare global { 
  declare type MetaFunction = V2_MetaFunction;
  declare type LinksFunction = RemixLinksFunction;
  declare type LoaderArgs = RemixLoaderArgs;
  declare type ActionArgs = RemixActionArgs;
  declare type DataFunctionArgs = RemixDataFunctionArgs;
  declare type HeadersFunction = RemixHeadersFunction;
  declare type ShouldRevalidateFunction = RemixShouldRevalidateFunction;
  declare type LoaderData<Loader> = ReturnType<typeof useLoaderData<Loader>>;
  declare type ActionData<Action> = ReturnType<typeof useActionData<Action>>;
}

好吧,那么这可能想知道这样做有什么好处?好了,现在您可以在路由内执行以下操作:

export const loader = ({ request }: LoaderArgs) => {
  // fully typed request object
}

export const action = ({ request }: ActionArgs) => {
  // fully typed request object
}

// Fully typed
export const meta: V2_MetaFunction = () => {
  return [
    { title: "Very cool app | Remix" }, 
  ];
};
// you get the point, fully typed
export const shouldRevalidate: ShouldRevalidateFunction = ({ 
  currentParams,
  currentUrl,
  ...props
}) => {
  return true;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1083786.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MyBatis基础之结果集映射

基本概念 [!danger] 注意 配置结果集映射&#xff0c;只用看 SQL 执行结果&#xff0c;不看 SQL 语句&#xff01; 在前面的内容中&#xff0c;由于我们的 PO 类的属性名与数据库中表的列名是一致的&#xff0c;因此&#xff0c;在 Mapper.xml 配置文件中&#xff0c;Mybatis 省…

对paddleOCR中的字符识别模型转ONNX

对paddle OCR中的模型转换成ONNX。 转换代码&#xff1a; import os import sys import yaml import numpy as np import cv2 import argparse import paddle from paddle import nnfrom argparse import ArgumentParser, RawDescriptionHelpFormatter import paddle.distribu…

没有执行力,一切都是空谈!如何提高执行力

决定人生高度的并非空谈&#xff0c;而是实干&#xff0c;没有执行力一切都是零。 执行力对于达成目标至关重要。即使将目标细分拆解得再细致&#xff0c;若无法切实执行&#xff0c;一切仍然是徒劳。 一旦制定目标&#xff0c;必须进行层层细分的拆解&#xff0c;包括每日的…

信钰证券:汇金增持提振市场情绪 保险、银行等板块集体拉升

12日&#xff0c;两市股指盘中全线走高&#xff0c;沪指一度克复3100点&#xff0c;上证50指数涨超1%。 稳妥、银行、券商板块团体拉升&#xff0c;到发稿&#xff0c;银行板块方面&#xff0c;瑞丰银行涨约6%&#xff0c;盘中一度涨停&#xff1b;紫金银行、渝农银行、西安银…

Python获取本机IP地址的几种方式~转

Python获取本机IP地址的几种方式 目录 1、使用专用网址 2、使用自带socket库 3、使用第三方netifaces库 1、使用专用网站 获取的是公网IP。 网址&#xff1a; http://myip.ipip.net 代码&#xff1a; import requests res requests.get(https://myip.ipip.net, timeout5)…

【C++】C++11 —— 右值引用和移动语义

​ ​&#x1f4dd;个人主页&#xff1a;Sherry的成长之路 &#x1f3e0;学习社区&#xff1a;Sherry的成长之路&#xff08;个人社区&#xff09; &#x1f4d6;专栏链接&#xff1a;C学习 &#x1f3af;长路漫漫浩浩&#xff0c;万事皆有期待 上一篇博客&#xff1a;【C】C11…

Java网络编程1

Java网络编程1 网络相关概念 把java网络编程的基础知识学习完之后&#xff0c;我们才会更加了解那些高性能的网络框架像neety它为什么要这样设计&#xff1f;才能把知识掌握的更加清晰。 网络通信 1&#xff09;概念&#xff1a;两台设备之间&#xff0c;通过网络&#xff0c…

2023年中国车用磁传感器市场发展趋势分析:未来市场规模将保持较高速增长趋势[图]

磁传感器是把磁场、电流、应力应变、温度、光等外界因素引起敏感元件磁性能变化转换成电信号&#xff0c;以这种方式来检测相应物理量的器件。磁传感器广泛用于现代工业和电子产品中以感应磁场强度来测量电流、位置、方向等物理参数。在现有技术中&#xff0c;有许多不同类型的…

最新科技喜报!统一图像和文字生成的MiniGPT-5来了!

原创 | 文 BFT机器人 当前视觉和语言模型的应用非常广泛&#xff0c;包括多模态对话代理、先进的内容创作工具等。这些模型的多模态特征集成不仅是一种发展趋势&#xff0c;更是一项关键的进步&#xff0c;正在塑造着各种应用程序。 那如何在视觉和语言之间建立有效的联系&…

Matlab地理信息绘图—数据诊断

文章目录 数据诊断分析&#xff08;均值方差&#xff09;Matlab代码实现结果展示 数据诊断分析&#xff08;均值方差&#xff09; 均值方差检测是一种简单但有效的异常检测方法&#xff0c;主要基于样本的均值和方差的统计信息。该方法的核心思想是假设正常的样本点应该聚集在…

用Cmake快速生成vs工程

文章目录 1 安装cmake2 生成vs工程 1 安装cmake 官方网址&#xff1a; https://cmake.org/download/ 打开官网&#xff0c;根据自己需求下载所需文件。&#xff08;本人是安装在Windows10-x64平台上&#xff0c;所以下文步骤均基于此平台&#xff09; 下载好后&#xff0c;双…

大数据之Hudi数据湖_基本概念_时间轴_TimeLine---大数据之Hudi数据湖工作笔记0005

然后看一下hudi的,时间轴概念,很简单了,就是之前说的时间旅行,其实就是 比如在某个时间点,记录,这个时间点做了什么,就是这个意思 然后像回去看看的时候,可以找到这个时间点做了什么 一个时间点就是一个Instant (时刻 瞬间的意思) 可以看到时刻的解释 instant 时刻instant包…

【信创】 JED on 鲲鹏(ARM) 调优步骤与成果 | 京东云技术团队

项目背景 基于国家对信创项目的大力推进&#xff0c;为了自主可控的技术发展&#xff0c;基础组件将逐步由国产组件替代&#xff0c;因此从数据库入手&#xff0c;将弹性库JED部署在 国产华为鲲鹏机器上(基于ARM架构)进行调优&#xff0c;与Intel (X86)进行性能对比。 物理机…

基于全息感知的智慧高速IT设施监控运维方案

作为智能交通的重要细分领域&#xff0c;建设智慧高速是实施交通强国战略的重要基础。在信息化时代&#xff0c;交通行业已经依托信息化建设取得了显著的成果&#xff0c;其中以收费网络、办公网络、监控网络和通讯网络为基础的网络架构已经形成&#xff0c;并且正在逐步完善网…

Nginx proxy_set_header参数设置

一、不设置 proxy_set_header Host 不设置 proxy_set_header Host 时&#xff0c;浏览器直接访问 nginx&#xff0c;获取到的 Host 是 proxy_pass 后面的值&#xff0c;即 $proxy_host 的值&#xff0c;参考Module ngx_http_proxy_module 1 2 3 4 5 6 7 8 # cat ngx_header.c…

NIO基础-ByteBuffer,Channel

文章目录 1. 三大组件1.1 Channel1.2 Buffer1.2 Selector 2.ByteBuffer2.1 ByteBuffer 正确使用姿势2.2 ByteBuffer 结构2.3 ByteBuffer 常见方法分配空间向 buffer 写入数据从 buffer 读取数据mark 和 reset字符串与 ByteBuffer 互转分散度集中写byteBuffer黏包半包 3. 文件编…

简历石层大海,为何今年秋招那么难?技术面考官想听啥?

上个月发完关于《2023年的IC求职究竟有多难&#xff1f;》文章&#xff0c;后台就出现很多私信&#xff0c;大家都在频繁的问秋招的事情&#xff0c;今年的秋招提前批让很多人直接破防&#xff0c;感觉书读了那么久&#xff0c;学校也还不错&#xff0c;但是为什么企业招聘的简…

单车模型:横向动力学

文章目录 1 模型推导2 参考资料 较高车速下&#xff0c;不能再假设车轮朝向和车轮速度一致。因此运动学模型在这里的误差就会比较大&#xff0c;必须要考虑动力学模型。 现考虑2自由度单车模型&#xff0c;如下图所示。2自由度表示为&#xff1a; 车辆横线位置 y y y&#xff…

2023-2024-1 高级语言程序设计实验一: 选择结构

7-1 古时年龄称谓知多少&#xff1f; 输入一个人的年龄&#xff08;岁&#xff09;&#xff0c;判断出他属于哪个年龄段 &#xff1f; 0-9 &#xff1a;垂髫之年&#xff1b; 10-19&#xff1a; 志学之年&#xff1b; 20-29 &#xff1a;弱冠之年&#xff1b; 30-39 &#…

Docker开启远程访问+idea配置docker+dockerfile发布java项目

一、docker开启远程访问 1.编辑docker服务文件 vim /usr/lib/systemd/system/docker.servicedocker.service原文件如下&#xff1a; [Unit] DescriptionDocker Application Container Engine Documentationhttps://docs.docker.com Afternetwork-online.target docker.socke…