简介,技术说明
本页内容

简介,技术说明

Remix 建立在 React Router 之上,包含四个方面

  1. 一个编译器
  2. 一个服务器端 HTTP 处理程序
  3. 一个服务器框架
  4. 一个浏览器框架

编译器

Remix 中的一切都始于编译器:remix vite:build。使用 Vite,它会创建以下内容

  1. 一个服务器 HTTP 处理程序,通常位于 build/server/index.js 中(可配置),其中包含所有路由和模块,以便能够在服务器上渲染并处理任何其他服务器端资源请求。
  2. 一个浏览器构建,通常位于 build/client/* 中。这包括按路由自动代码分割、带指纹的资产导入(如 CSS 和图像)等。浏览器中运行应用程序所需的一切。
  3. 一个资产清单。客户端和服务器都使用此清单来了解整个依赖关系图。这对于在初始服务器渲染中预加载资源以及为客户端过渡预取资源很有用。这就是 Remix 如何能够消除当今 Web 应用程序中常见的渲染+获取级联。

有了这些构建工件,应用程序就可以部署到任何运行 JavaScript 的托管服务。

HTTP 处理程序和适配器

虽然 Remix 在服务器上运行,但它本身并不是一个服务器。它只是一个传递给实际 JavaScript 服务器的处理程序。

它建立在 Web Fetch API 而不是 Node.js 之上。这使得 Remix 可以在任何 Node.js 服务器(如 VercelNetlifyArchitect 等)以及非 Node.js 环境(如 Cloudflare WorkersDeno Deploy)中运行。

以下是在 express 应用程序中运行时的 Remix 示例

const remix = require("@remix-run/express");
const express = require("express");

const app = express();

app.all(
  "*",
  remix.createRequestHandler({
    build: require("./build/server"),
  })
);

Express(或 Node.js)是实际的服务器,Remix 只是该服务器上的一个处理程序。"@remix-run/express" 包被称为适配器。Remix 处理程序与服务器无关。适配器通过在传入时将服务器的请求/响应 API 转换为 Fetch API,然后将来自 Remix 的 Fetch 响应适配到服务器的响应 API,使它们能够在特定服务器上工作。以下是适配器执行操作的伪代码

export function createRequestHandler({ build }) {
  // creates a Fetch API request handler from the server build
  const handleRequest = createRemixRequestHandler(build);

  // returns an express.js specific handler for the express server
  return async (req, res) => {
    // adapts the express.req to a Fetch API request
    const request = createRemixRequest(req);

    // calls the app handler and receives a Fetch API response
    const response = await handleRequest(request);

    // adapts the Fetch API response to the express.res
    sendRemixResponse(res, response);
  };
}

实际的适配器执行的操作比这多一点,但这就是要点。这不仅使您能够将 Remix 部署到任何地方,而且还允许您在现有的 JavaScript 服务器中逐步采用它,因为您可以在 Remix 之外拥有服务器继续处理的路由,然后再到达 Remix。

此外,如果 Remix 还没有针对您的服务器提供适配器,您可以查看其中一个适配器的源代码并构建自己的适配器。

服务器框架

如果您熟悉 Rails 和 Laravel 等服务器端 MVC Web 框架,Remix 就是视图和控制器,但它将模型留给您。JavaScript 生态系统中有很多优秀的数据库、ORM、邮件发送器等来填补这个空白。Remix 还提供了一些围绕 Fetch API 的帮助程序,用于 Cookie 和会话管理。

Remix 路由模块承担了视图和控制器的双重职责,而不是将它们分开。

大多数服务器端框架都是“模型为中心”的。一个控制器管理单个模型的多个 URL

Remix 是以 UI 为中心的。路由可以处理整个 URL 或 URL 的一部分。当路由仅映射到一部分时,嵌套的 URL 部分会成为 UI 中的嵌套布局。这样,每个布局(视图)都可以成为它自己的控制器,然后 Remix 将聚合数据和组件以构建完整的 UI。

通常情况下,Remix 路由模块可以在同一个文件中包含 UI 和与模型的交互,这带来了非常好的开发人员人体工程学和生产力。

路由模块有三个主要导出:loaderactiondefault(组件)。

// Loaders only run on the server and provide data
// to your component on GET requests
export async function loader() {
  return json(await db.projects.findAll());
}

// The default export is the component that will be
// rendered when a route matches the URL. This runs
// both on the server and the client
export default function Projects() {
  const projects = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();

  return (
    <div>
      {projects.map((project) => (
        <Link key={project.slug} to={project.slug}>
          {project.title}
        </Link>
      ))}

      <Form method="post">
        <input name="title" />
        <button type="submit">Create New Project</button>
      </Form>
      {actionData?.errors ? (
        <ErrorMessages errors={actionData.errors} />
      ) : null}

      {/* outlets render the nested child routes
          that match the URL deeper than this route,
          allowing each layout to co-locate the UI and
          controller code in the same file */}
      <Outlet />
    </div>
  );
}

// Actions only run on the server and handle POST
// PUT, PATCH, and DELETE. They can also provide data
// to the component
export async function action({
  request,
}: ActionFunctionArgs) {
  const form = await request.formData();
  const errors = validate(form);
  if (errors) {
    return json({ errors });
  }
  await createProject({ title: form.get("title") });
  return json({ ok: true });
}

实际上,您也可以将 Remix 仅用作服务器端框架,而无需使用任何浏览器 JavaScript。用于数据加载的路由约定(使用 loader)、用于突变的约定(使用 action 和 HTML 表单)以及在 URL 上呈现的组件,可以提供许多 Web 项目的核心功能集。

这样,Remix 就可以缩减规模。并非应用程序中的每个页面都需要在浏览器中包含大量 JavaScript,也不是每个用户交互都需要浏览器默认行为之外的额外功能。在 Remix 中,您可以先以简单的方式构建它,然后在不更改基本模型的情况下进行扩展。此外,大多数应用程序在浏览器加载 JavaScript 之前就开始工作,这使得 Remix 应用程序在设计上能够抵御网络状况不佳的影响。

如果您不熟悉传统的后端 Web 框架,您可以将 Remix 路由视为 React 组件,它们已经是自己的 API 路由,并且已经知道如何在服务器上加载和提交数据给自己。

浏览器框架

一旦 Remix 将文档提供给浏览器,它就会使用浏览器构建的 JavaScript 模块“水化”页面。这就是我们经常谈论 Remix“模拟浏览器”的地方。

当用户点击链接时,Remix 只需获取下一页的数据并更新 UI,而不是往返服务器获取整个文档和所有资产。

此外,当用户提交 <Form> 以更新数据时,浏览器运行时将改为向服务器发出获取请求,而不是执行正常的 HTML 文档请求,并自动重新验证页面上的所有数据并使用 React 更新数据。

与发出完整文档请求相比,这具有许多性能优势

  1. 不需要重新下载(或从缓存中提取)资产
  2. 浏览器不需要再次解析资产
  3. 获取的数据比整个文档小得多(有时数量级上都不同)
  4. 因为 Remix 增强了 HTML API(<a><form>),所以您的应用程序即使在页面上加载 JavaScript 之前也往往可以工作

Remix 还内置了一些针对客户端导航的优化。它知道哪些布局会在两个 URL 之间保持不变,因此它只获取正在更改的布局的数据。完整的文档请求需要在服务器端获取所有数据,这会浪费后端资源并减慢应用程序速度。

这种方法也有一些 UX 优势,例如不会重置侧边栏导航的滚动位置,并允许您将焦点移动到比文档顶部更有意义的位置。

Remix 还可以预取用户即将点击链接的页面的所有资源。浏览器框架了解编译器的资产清单。它可以匹配链接的 URL,读取清单,然后预取下一页的所有数据、JavaScript 模块,甚至 CSS 资源。这就是为什么 Remix 应用程序即使在网络速度慢的情况下也能快速运行的原因。

然后,Remix 提供客户端 API,因此您可以在不更改 HTML 和浏览器的基本模型的情况下创建丰富的用户体验。

以我们之前使用的路由模块为例,以下是一些对表单进行的小而有用的 UX 改进,这些改进只能在浏览器中使用 JavaScript 完成。

  1. 提交表单时禁用按钮
  2. 服务器端表单验证失败时将焦点设置到输入框
  3. 为错误消息添加动画
export default function Projects() {
  const projects = useLoaderData<typeof loader>();
  const actionData = useActionData<typeof action>();
  const { state } = useNavigation();
  const busy = state === "submitting";
  const inputRef = React.useRef();

  React.useEffect(() => {
    if (actionData.errors) {
      inputRef.current.focus();
    }
  }, [actionData]);

  return (
    <div>
      {projects.map((project) => (
        <Link key={project.slug} to={project.slug}>
          {project.title}
        </Link>
      ))}

      <Form method="post">
        <input ref={inputRef} name="title" />
        <button type="submit" disabled={busy}>
          {busy ? "Creating..." : "Create New Project"}
        </button>
      </Form>

      {actionData?.errors ? (
        <FadeIn>
          <ErrorMessages errors={actionData.errors} />
        </FadeIn>
      ) : null}

      <Outlet />
    </div>
  );
}

这个代码示例最有趣的地方在于它是**纯增量的**。整个交互在本质上仍然是同一件事,甚至在 JavaScript 加载之前也能在基本级别上工作,唯一的区别是用户反馈将由浏览器(旋转的 favicon 等)而不是应用程序(useNavigation().state)提供。

因为 Remix 可以访问后端的控制器级别,所以它可以无缝地做到这一点。

虽然它不像 Rails 和 Laravel 这样的服务器端框架那样深入到堆栈底部,但它确实深入到堆栈顶部进入浏览器,使后端到前端的过渡变得无缝。

例如,在后端为主的 Web 框架中构建一个简单的 HTML 表单和服务器端处理程序与在 Remix 中一样容易。但是,一旦您希望将体验扩展到包含动画验证消息、焦点管理和正在进行的 UI,则需要对代码进行根本性的更改。通常,人们会构建一个 API 路由,然后引入一些客户端 JavaScript 来连接这两者。使用 Remix,您只需在现有的“服务器端视图”周围添加一些代码,而无需更改其工作原理。浏览器运行时接管服务器通信,以提供超出默认浏览器行为的增强用户体验。

我们在 Remix 中借用了旧术语并将其称为渐进增强。从一个简单的 HTML 表单开始(Remix 可以缩减规模),然后在您有时间和愿望时扩展 UI。

文档和示例许可证 MIT