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 基于文件的路由约定、自动代码拆分、路由模块预取、头部标签管理等优点。

这为 React Router 的用户解锁了一条全新的迁移路径,让他们可以在不必切换到服务器渲染架构的情况下迁移到 Remix——这对很多人来说甚至不是一个选项。对于将来希望在其 Remix 应用程序中引入服务器的任何人来说,迁移路径现在也更加直接。

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

Basename 支持

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

随着迁移到 Vite,缺少 basename 支持变得更加明显,因为 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 提供了 Node 代理到本地 workerd 绑定。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 新的(仍在实验中的)Runtime 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 预处理器 等,则此方法不适用于 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 现在具有一个默认值为 "build" 的单个顶级 buildDirectory 选项,从而生成 build/clientbuild/server 目录。

有趣的是,尽管我们最初只是为了修复一个 bug 而实现这个改动,但实际上我们更喜欢这种结构。而且根据我们收到的反馈,我们的早期采用者也这么认为!

不仅仅是一个 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:devremix vite:build 命令。在 Remix v3 中,这些命令将成为默认的 devbuild 命令。

因此,虽然我们不再“只是一个 Vite 插件”,但说我们仍然主要只是一个 Vite 插件是合理的 🙂

下一步

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

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

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

如果您在此过程中有任何反馈意见,请与我们联系。我们很乐意收到您的来信!

感谢

感谢 Remix 社区的所有早期采用者,感谢你们提供反馈、提出问题和提交拉取请求。没有你们,我们不可能走到今天。

我们还要特别感谢外部贡献者 Hiroshi Ogawa,他向 Remix Vite 🔥 提交了惊人的 25 个拉取请求

一如既往,感谢 Vite 团队为我们提供了如此出色的工具,让我们可以在其基础上进行构建。我们很期待看到我们一起能将其带向何方。

💿⚡️🚀


获取最新的 Remix 新闻

第一时间了解 Remix 的新功能、社区活动和教程。