Remix Vite is Now Stable
2024 年 2 月 20 日

Remix Vite 现已稳定

Mark Dalgleish
工作人员开发人员
Pedro Cattori
工作人员开发人员

今天,我们很高兴地宣布,Remix v2.7.0 中对 Vite 的支持现已稳定!在 Remix Vite 的初始不稳定版本发布 之后,在过去几个月里,我们一直在努力完善和扩展它,并得到了所有早期采用者和社区贡献者的帮助。

以下是我们的工作

让我们分解自初始版本发布以来的最重大变化。

SPA 模式

我们做出的最重大改变非常重要,我们将在以后的文章中保留讨论它对 React 生态系统的影响。

简而言之,Remix 现在支持构建纯静态站点,这些站点在生产中不需要 JavaScript 服务器,同时保留 Remix 基于文件的路由约定、自动代码拆分、路由模块预取、head 标签管理等优点。

这为 React Router 消费者提供了一条全新的迁移路径,可以迁移到 Remix,而无需切换到服务器渲染的架构 - 对于许多人来说,这甚至不是一个选择。对于任何希望在将来为他们的 Remix 应用程序引入服务器的人来说,迁移路径现在变得更加简单。

有关更多信息,请查看 SPA 模式文档

基本名称支持

React Router 支持为您的应用程序设置基本名称,允许您将整个应用程序嵌套在子路径中 - 但此功能在 Remix 中显着缺失。虽然可以通过手动添加路由和链接前缀来解决此问题,但显然不如设置单个配置值方便。

随着迁移到 Vite,由于 Vite 公开它自己的“base”选项,基本名称支持的缺乏变得更加明显。许多消费者错误地认为这将与 Remix 一起使用,但此选项实际上与 Remix 的“publicPath”选项 相同。

为了避免这种混淆,不再存在 publicPath 选项(您应该改用 Vite 的 base 选项),Remix Vite 插件现在有一个全新的 basename 选项。

因此,将您的 Remix 应用程序嵌套在站点的子路径中从未如此简单,无需触碰您的应用程序代码。

import { vitePlugin as remix } from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  base: "/my-app/public/",
  plugins: [
    remix({
      basename: "/my-app",
    }),
  ],
});

Cloudflare Pages 支持

在 Remix Vite 的初始不稳定版本发布时,Cloudflare Pages 支持还没有完全准备好。Cloudflare 的 workerd 运行时与 Vite 的 Node 环境完全独立,因此我们需要找出桥接此差距的最佳方法。

随着 Remix Vite 变得稳定,我们现在提供了一个内置的 Vite 插件,用于在本地开发期间将 Cloudflare 的工具与 Remix 集成。

为了在 Vite 中模拟 Cloudflare 环境,Wrangler 提供 到本地 workerd 绑定的 Node 代理。Remix 的 cloudflareDevProxyVitePlugin 为您设置了这些代理

import {
  vitePlugin as remix,
  cloudflareDevProxyVitePlugin as remixCloudflareDevProxy,
} from "@remix-run/dev";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [remixCloudflareDevProxy(), remix()],
});

然后,代理在您的 loaderaction 函数中的 context.cloudflare 中可用

export const loader = ({ context }: LoaderFunctionArgs) => {
  const { env, cf, ctx } = context.cloudflare;
  // ... more loader code here...
};

我们仍在积极与 Cloudflare 团队合作,以确保 Remix 用户获得最佳体验。将来,集成可能会通过利用 Vite 的新的(仍在试验阶段)运行时 API 变得更加无缝,敬请关注进一步的更新。

有关此功能的更多信息,请查看 Remix Vite + Cloudflare 文档

服务器捆绑包

对于那些在 Vercel 上运行 Remix 的人,您可能已经注意到 Vercel 允许您将服务器构建拆分为多个捆绑包,其中不同的路由针对 无服务器边缘函数

您可能没有意识到的是,此功能实际上是通过 Vercel 在其 Remix 构建器 中使用的 Remix 的分叉 实现的。

随着迁移到 Vite,我们希望确保不再需要另一个构建系统的分叉,因此我们一直在与 Vercel 团队合作,将此功能带到 Remix Vite。现在任何人 - 不仅仅是 Vercel 用户 - 都可以根据自己的喜好将服务器构建拆分为多个捆绑包。

非常感谢 Vercel,尤其是 Nathan Rajlich,感谢他们在这项工作中提供的帮助。有关此功能的更多信息,请查看 服务器捆绑包文档

预设

在调查 Vercel 对 Remix Vite 的支持时,很明显我们需要一种方法来让其他工具和托管提供商自定义 Vite 插件的行为,而无需访问内部或运行自己的分叉。为了支持这一点,我们引入了“预设”的概念。

预设只能做两件事

  • 代表您配置 Remix Vite 插件。
  • 验证已解析的配置。

预设旨在发布到 npm 并用于您的 Vite 配置中。

Vercel 预设即将推出,我们很高兴看到社区想出其他预设 - 尤其是在预设可以访问所有 Remix Vite 插件选项并且因此不严格限于托管提供商支持的情况下。

有关此功能的更多信息,包括有关如何创建自己的预设的指导,请查看 预设文档

更好的服务器和客户端分离

Remix 允许您使用 .server.ts 扩展名命名文件,以确保它们永远不会意外地出现在客户端。但是,事实证明,我们以前的实现与 Vite 的 ESM 模型不兼容,因此我们不得不重新审视我们的方法。

相反,如果我们每次在客户端代码路径中导入 .server.ts 文件时都将其设为编译时错误呢?

我们以前的方法会导致运行时错误,这些错误很容易在生产中漏掉。在构建过程中引发这些错误可以防止它们影响实际用户,同时为开发人员提供更快、更全面的反馈。我们很快意识到这好得多

作为奖励,由于我们已经在该领域工作,因此我们决定添加对 .server目录的支持,而不仅仅是文件,从而可以轻松地将项目的整个部分标记为仅服务器。

如果您想深入了解此更改背后的基本原理,请查看我们关于 在 Vite 中拆分客户端和服务器代码的决策文档

vite-env-only

为了提高速度,Vite 惰性地独立编译每个文件。开箱即用,Vite 假设客户端代码引用的任何文件都是完全客户端安全的。

Remix 自动处理从路由文件中删除 loaderactionheaders 导出,确保它们始终对浏览器安全。但是非 Remix 导出呢?我们如何知道要从浏览器构建中删除哪些内容 - 不仅要从路由中删除,还要从项目中的任何模块中删除?

例如,如果您想编写类似以下内容呢?

import { db } from "~/.server/db";

// This export is server-only ❌
export const getPosts = async () => db.posts.findMany();

// This export is client-safe ✅
export const PostPreview = ({ title, description }) => (
  <article>
    <h2>{title}</h2>
    <p>{description}</p>
  </article>
);

在此文件当前状态下,由于在客户端使用了 .server 模块,因此 Remix 会引发编译时错误。这是件好事!您绝对不希望将仅服务器代码泄露到客户端。您可以通过将仅服务器代码拆分为单独的文件来解决此问题,但是如果您不想重新构建代码,最好不必重新构建代码 - 特别是如果您正在迁移现有项目!

这个问题不是 Remix 特有的。它实际上影响了任何全栈 Vite 项目,因此我们编写了一个名为 vite-env-only 的独立 Vite 插件来解决它。此插件允许您将单个表达式标记为仅服务器或仅客户端。

例如,使用 serverOnly$ 宏时

import { serverOnly$ } from "vite-env-only";

import { db } from "~/.server/db";

export const getPosts = serverOnly$(async () => db.posts.findMany());

export const PostPreview = ({ title, description }) => (
  <article>
    <h2>{title}</h2>
    <p>{description}</p>
  </article>
);

在客户端上,这将变为

export const getPosts = undefined;

export const PostPreview = ({ title, description }) => (
  <article>
    <h2>{title}</h2>
    <p>{description}</p>
  </article>
);

值得重申的是,这是一个单独的 Vite 插件,而不是 Remix 的功能。 您可以选择使用 vite-env-only、将仅服务器代码拆分为单独的文件,甚至引入您自己的 Vite 插件。

有关更多信息,请查看我们关于 拆分客户端和服务器代码的文档

.css?url 导入

从一开始,Remix 就提供了一种 管理 CSS 导入的替代模型。导入 CSS 文件时,其 URL 作为字符串提供,用于在 link 标签中渲染

import type { LinksFunction } from "@remix-run/node"; // or cloudflare/deno

import styles from "~/styles/dashboard.css";

export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];

虽然 Vite 很早就支持 将静态资产作为 URL 导入,但如果 CSS 文件需要任何处理,例如 PostCSS(包括 Tailwind)、CSS 模块CSS 预处理器 等,则这将不起作用。

随着最近发布的 Vite v5.1.0,现在可以通过 .css?url 导入语法实现完整的 CSS 支持

import styles from "~/styles/dashboard.css?url";

更简洁的构建输出

旧的 Remix 编译器将客户端和服务器构建到可以独立配置的单独目录中。默认情况下,输出目录是客户端资产的 public/build 和服务器的 build。事实证明,这种结构与 Vite 的公共目录 冲突。

由于 Vite 将文件从 public 复制到客户端构建目录中,而 Remix 的客户端构建目录嵌套在公共目录中,因此一些用户发现他们的公共目录被递归地复制到自身中 🫠

为了解决这个问题,我们不得不重新排列构建输出。Remix Vite 现在有一个单一的顶级 buildDirectory 选项,默认值为 "build",生成 build/clientbuild/server 目录。

有趣的是,即使我们只实施了此更改来修复错误,但我们实际上更喜欢这种结构。根据我们收到的反馈,我们的早期采用者也是如此!

不仅仅是一个 Vite 插件

我们最早的采用者直接运行 Vite CLI——在本地开发中使用 `vite dev`,以及在生产环境中使用 `vite build && vite build --ssr` 进行构建。由于缺乏围绕 Vite 的自定义包装器,我们最初的不稳定版本发布后提到,Remix 现在“只是一个 Vite 插件”。

然而,随着服务器捆绑包的引入,我们无法继续使用这种方法。当使用 `serverBundles` 选项时,现在将存在数量动态变化的服务器构建。我们之前假设我们可以为 Vite 的 `ssr` 构建定义多个输入和输出,但事实并非如此,因此 Remix 需要一种方法来协调整个构建过程。Vite 插件现在还提供了一个新的 `buildEnd` 钩子,这样您就可以在 Remix 构建完成后运行您自己的自定义逻辑。

我们通过最大限度地将 Vite 插件中的代码量(我们很高兴我们做到了!)尽可能地保留了我们旧的架构,并在 Remix CLI 中添加了 `remix vite:dev` 和 `remix vite:build` 命令。在 Remix v3 中,这些命令将成为默认的 `dev` 和 `build` 命令。

因此,虽然我们不再是“仅仅是一个 Vite 插件”,但可以公平地说,我们仍然主要只是一个 Vite 插件🙂

下一步

现在 Remix Vite 已经稳定了,您将开始看到我们的文档和模板默认迁移到 Vite。

就像我们最初的不稳定版本发布一样,我们为那些希望将现有 Remix 项目迁移到 Vite 的用户提供了一个迁移指南

请放心,旧的 Remix 编译器将在 Remix v2 中继续工作。但是,从现在开始,所有需要编译器集成的新功能和改进都将仅针对 Vite。将来,Vite 将是构建 Remix 应用的唯一官方方式,因此我们鼓励您尽快开始迁移。

如果您在迁移过程中有任何反馈,请与我们联系。我们很乐意听到您的声音!

感谢

感谢 Remix 社区中所有早期采用者提供反馈,提出问题并提交拉取请求。没有您,我们无法走到今天。

我们还要特别感谢Hiroshi Ogawa,他是一位外部贡献者,在 Remix Vite 中完成了令人惊叹的25 个拉取请求🔥

一如既往,感谢 Vite 团队为我们提供如此出色的工具,我们可以在其基础上进行构建。我们很高兴看到我们可以一起将它带到哪里。

💿⚡️🚀


获取有关最新 Remix 新闻的更新

成为第一个了解 Remix 新功能、社区活动和教程的人。