今天,我们很高兴地宣布,Remix v2.7.0 中对 Vite 的支持现已稳定!在 Remix Vite 的初始不稳定版本发布 之后,在过去几个月里,我们一直在努力完善和扩展它,并得到了所有早期采用者和社区贡献者的帮助。
以下是我们的工作
让我们分解自初始版本发布以来的最重大变化。
我们做出的最重大改变非常重要,我们将在以后的文章中保留讨论它对 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",
}),
],
});
在 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()],
});
然后,代理在您的 loader
或 action
函数中的 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 插件的行为,而无需访问内部或运行自己的分叉。为了支持这一点,我们引入了“预设”的概念。
预设只能做两件事
预设旨在发布到 npm 并用于您的 Vite 配置中。
Vercel 预设即将推出,我们很高兴看到社区想出其他预设 - 尤其是在预设可以访问所有 Remix Vite 插件选项并且因此不严格限于托管提供商支持的情况下。
有关此功能的更多信息,包括有关如何创建自己的预设的指导,请查看 预设文档。
Remix 允许您使用 .server.ts
扩展名命名文件,以确保它们永远不会意外地出现在客户端。但是,事实证明,我们以前的实现与 Vite 的 ESM 模型不兼容,因此我们不得不重新审视我们的方法。
相反,如果我们每次在客户端代码路径中导入 .server.ts
文件时都将其设为编译时错误呢?
我们以前的方法会导致运行时错误,这些错误很容易在生产中漏掉。在构建过程中引发这些错误可以防止它们影响实际用户,同时为开发人员提供更快、更全面的反馈。我们很快意识到这好得多。
作为奖励,由于我们已经在该领域工作,因此我们决定添加对 .server
目录的支持,而不仅仅是文件,从而可以轻松地将项目的整个部分标记为仅服务器。
如果您想深入了解此更改背后的基本原理,请查看我们关于 在 Vite 中拆分客户端和服务器代码的决策文档。
为了提高速度,Vite 惰性地独立编译每个文件。开箱即用,Vite 假设客户端代码引用的任何文件都是完全客户端安全的。
Remix 自动处理从路由文件中删除 loader
、action
和 headers
导出,确保它们始终对浏览器安全。但是非 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/client
和 build/server
目录。
有趣的是,即使我们只实施了此更改来修复错误,但我们实际上更喜欢这种结构。根据我们收到的反馈,我们的早期采用者也是如此!
我们最早的采用者直接运行 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 团队为我们提供如此出色的工具,我们可以在其基础上进行构建。我们很高兴看到我们可以一起将它带到哪里。
💿⚡️🚀