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)中运行。
以下是在 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 和与模型的交互,这带来了非常好的开发人员人体工程学和生产力。
路由模块有三个主要导出: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 路由视为 React 组件,它们已经是自己的 API 路由,并且已经知道如何在服务器上加载和提交数据给自己。
一旦 Remix 将文档提供给浏览器,它就会使用浏览器构建的 JavaScript 模块“水化”页面。这就是我们经常谈论 Remix“模拟浏览器”的地方。
当用户点击链接时,Remix 只需获取下一页的数据并更新 UI,而不是往返服务器获取整个文档和所有资产。
此外,当用户提交 <Form>
以更新数据时,浏览器运行时将改为向服务器发出获取请求,而不是执行正常的 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。