Remix 基于 React Router 构建,包含四个方面:
Remix 中的一切都从编译器开始:remix vite:build
。使用 Vite,它会创建以下几项:
build/server/index.js
(可配置),它包含所有路由和模块,以便能够在服务器上渲染并处理任何其他服务器端资源请求。build/client/*
。这包括按路由自动代码拆分、指纹资产导入(如 CSS 和图像)等。任何需要在浏览器中运行应用程序的内容。有了这些构建产物,应用程序就可以部署到任何运行 JavaScript 的托管服务。
虽然 Remix 在服务器上运行,但它实际上不是服务器。它只是一个提供给实际 JavaScript 服务器的处理程序。
它基于 Web Fetch API 而不是 Node.js 构建。这使得 Remix 可以在任何 Node.js 服务器(如 Vercel、Netlify、Architect 等)以及非 Node.js 环境(如 Cloudflare Workers 和 Deno 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 Response 适配到服务器的响应 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 就是视图(View)和控制器(Controller),但它将模型(Model)留给你自己处理。在 JavaScript 生态系统中,有很多很棒的数据库、ORM、邮件发送器等来填补这个空白。Remix 还为 cookie 和会话管理提供了围绕 Fetch API 的帮助程序。
Remix 路由模块承担了视图和控制器的双重责任,而不是将视图和控制器分开。
大多数服务器端框架都是“模型导向”的。一个控制器管理单个模型的多个 URL。
Remix 是UI 导向的。路由可以处理整个 URL 或仅 URL 的一部分。当路由仅映射到一部分时,嵌套的 URL 段将成为 UI 中的嵌套布局。这样,每个布局(视图)都可以是它自己的控制器,然后 Remix 将聚合数据和组件来构建完整的 UI。
通常情况下,一个 Remix 路由模块可以在同一个文件中包含 UI 以及与模型的交互,这带来了非常好的开发人员人体工程学和生产力。
路由模块有三个主要导出:loader
、action
和 default
(组件)。
// 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 路由视为已经拥有自己的 API 路由的 React 组件,并且已经知道如何在服务器上加载数据和向自身提交数据。
一旦 Remix 将文档提供给浏览器,它就会使用浏览器构建的 JavaScript 模块“水合”页面。这就是我们经常谈论 Remix“模拟浏览器”的地方。
当用户单击链接时,Remix 不会为整个文档和所有资源进行服务器往返,而是简单地获取下一页的数据并更新 UI。
此外,当用户提交 <Form>
来更新数据时,浏览器运行时将向服务器发出 fetch 请求,而不是执行正常的 HTML 文档请求,并自动重新验证页面上的所有数据并使用 React 更新它。
与进行完整文档请求相比,这具有许多性能优势
<a>
和 <form>
),因此你的应用程序即使在页面上加载 JavaScript 之前也能正常工作Remix 还具有一些内置的客户端导航优化。它知道两个 URL 之间哪些布局会持久存在,因此它只获取正在更改的布局的数据。完整的文档请求将需要服务器获取所有数据,浪费后端资源并减慢应用程序的速度。
这种方法还具有 UX 优势,例如不重置侧边栏导航的滚动位置,并允许你将焦点移动到比文档顶部更有意义的地方。
当用户即将单击链接时,Remix 还可以预取页面的所有资源。浏览器框架了解编译器的资源清单。它可以匹配链接的 URL,读取清单,然后预取下一页的所有数据、JavaScript 模块,甚至 CSS 资源。这就是 Remix 应用程序即使在网络缓慢的情况下也感觉快速的原因。
然后,Remix 提供客户端 API,因此你可以在不更改 HTML 和浏览器基本模型的情况下创建丰富的用户体验。
从之前的路由模块中,这里有一些小的但有用的 UX 改进,这些改进只能通过浏览器中的 JavaScript 来完成
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。