React Router v7 已發布。 查看文件
介紹、技術說明
本頁內容

簡介,技術說明

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 如何消除現今網路應用程式中常見的渲染 + 抓取瀑布式流程。

有了這些建置產物,應用程式就可以部署到任何執行 JavaScript 的託管服務。

HTTP 處理器和轉接器

雖然 Remix 在伺服器上執行,但它實際上並不是伺服器。 它只是一個被提供給實際 JavaScript 伺服器的處理器。

它是基於 Web Fetch API 而非 Node.js 建構的。 這使 Remix 能夠在任何 Node.js 伺服器(如 VercelNetlifyArchitect 等)以及非 Node.js 環境(如 Cloudflare WorkersDeno Deploy)中執行。

這是 Remix 在 express 應用程式中執行的樣子

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 沒有適用於您伺服器的轉接器,您可以查看其中一個轉接器的原始碼並建立自己的轉接器。

伺服器框架

如果您熟悉伺服器端 MVC 網頁框架(如 Rails 和 Laravel),Remix 是 View 和 Controller,但它會將 Model 交給您處理。 JavaScript 生態系統中有許多很棒的資料庫、ORM、郵件程式等等,可以填補這個空間。 Remix 在 Fetch API 周圍也有用於 Cookie 和 Session 管理的協助程式。

Remix 路由模組承擔 View 和 Controller 兩種責任,而不是將它們分開。

大多數伺服器端框架都是「以模型為中心」。一個控制器會為單一模型管理多個 URL

Remix 則是以 UI 為中心。路由可以處理整個 URL,或者只是 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 上渲染元件的路由慣例,可以提供許多網頁專案的核心功能集。

透過這種方式,Remix 可以向下擴展。您的應用程式中不是每個頁面都需要大量的瀏覽器 JavaScript,也不是每次使用者互動都需要超出瀏覽器預設行為的額外功能。在 Remix 中,您可以先以簡單的方式建構,然後在不改變基本模型的情況下向上擴展。此外,大部分應用程式在瀏覽器載入 JavaScript 之前就可以運作,這使得 Remix 應用程式在設計上能夠適應不穩定的網路狀況。

如果您不熟悉傳統的後端網頁框架,您可以將 Remix 路由視為 React 元件,它們已經是自己的 API 路由,並且已經知道如何在伺服器上載入資料並將資料提交給自己。

瀏覽器框架

一旦 Remix 將文件傳送到瀏覽器,它就會使用瀏覽器建置的 JavaScript 模組來「水合」頁面。這就是我們經常談論 Remix「模擬瀏覽器」的地方。

當使用者點擊連結時,Remix 並不會為了取得整個文件和所有資源而往返伺服器,而是簡單地提取下一個頁面的資料並更新 UI。

此外,當使用者提交 <Form> 來更新資料時,瀏覽器執行階段會改為向伺服器發出 fetch 請求,而不是執行正常的 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 等伺服器端框架那樣深入堆疊,但它的確更進一步深入到瀏覽器中,以實現從後端到前端的無縫轉換。

例如,在後端繁重的網頁框架中,建構純 HTML 表單和伺服器端處理程式與在 Remix 中一樣容易。但是,一旦您想要跨入具有動畫驗證訊息、焦點管理和待處理 UI 的體驗時,就需要對程式碼進行根本性的變更。通常,人們會建構一個 API 路由,然後引入一些客戶端 JavaScript 來連接兩者。使用 Remix,您只需在現有的「伺服器端視圖」周圍新增一些程式碼,而無需從根本上變更其運作方式。瀏覽器執行階段會接管伺服器通訊,以提供超出預設瀏覽器行為的增強使用者體驗。

我們借用了一個舊術語,並在 Remix 中稱其為漸進增強。從簡單的 HTML 表單開始(Remix 可以向下擴展),然後在您有時間和雄心時向上擴展 UI。

文件和範例依 MIT