ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

@remix-run/spa 深入解析:用 `render()` 中间件与 `run()` 运行时搭建客户端渲染的 Remix 应用

@remix-run/spa 深入解析:用 `render()` 中间件与 `run()` 运行时搭建客户端渲染的 Remix 应用 remix-run/spa 深入解析用render()中间件与run()运行时搭建客户端渲染的 Remix 应用【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remixremix-run/spa是 Remix 仓库中负责客户端渲染SPA路由的包它把标准的 fetch router 连接到浏览器的 UI 运行时让路由处理器直接返回 Remix 节点RemixNode由浏览器端完成渲染。阅读本文后你将掌握该包的render()渲染中间件与run(router, { fallback? })浏览器运行时的工作原理理解它如何保留 fetch router 的Request → Response契约并能在自己的客户端渲染应用中完整落地这套方案。一、SPA 包定位从 CHANGELOG 看它解决了什么问题根据 packages/spa/CHANGELOG.md该包在v0.1.0首次发布核心内容一句话可以概括新增初始的remix-run/spa包提供render()中间件与run(router, { fallback? })浏览器运行时用于客户端渲染的 Remix 应用。路由处理器使用context.render()包本身保留路由器的Request到Response契约并隐藏 SPA 响应载体response carrier。同时 v0.1.0 将依赖提升到了render-middleware0.2.0与ui0.8.0这意味着 SPA 包是建立在两套基础设施之上的remix-run/render-middleware提供请求作用域内的渲染器中间件renderWith让context.render()可以安装到任意 fetch router 的请求上下文上remix-run/ui提供 Remix 组件、frame帧与浏览器运行时run、spaResponse负责真正把路由节点画到页面上。从 packages/spa/package.json 可以看到包的元信息名称remix-run/spa、版本0.1.0、描述为 Client-rendered application routing for Remix运行时依赖正是上述两个包加上remix-run/fetch-router。二、快速上手三行核心代码跑起一个 SPA2.1 安装与导入包对外以remix/spa子路径导出。仓库的 packages/remix/src/spa.ts 是一个自动生成的转发文件内容为export * from remix-run/spa因此在应用里可以统一从remix导入npm i remiximport { render, run } from remix/spa2.2 最小可运行示例packages/spa/README.md 给出了完整的入门示例先安装render()中间件必须放在使用context.render()的中间件与路由处理器之前再把 router 交给run()import { createRouter } from remix/router import { get, route } from remix/routes import { render, run } from remix/spa const routes route({ home: get(/), about: get(/about), }) const router createRouter({ middleware: [render()], defaultHandler({ render }) { return render(h1Not Found/h1, { status: 404 }) }, }) router.map(routes, { actions: { home({ render }) { return render(h1Home/h1) }, about({ render }) { return render(h1About/h1) }, }, }) const app run(router, { fallback: pLoading…/p }) await app.ready()这段代码的要点在于router 仍然是一个普通的 fetch router它的 actions、controllers、middleware 与 context 类型体系全部照常生效只是路由处理器不再返回 HTML而是通过context.render()返回一个携带 Remix 节点的响应。2.3 用请求感知的 transform 包裹应用外壳当每个路由都希望共享一个应用外壳如导航栏 主内容区时可以给render()传一个请求感知的转换函数const router createRouter({ middleware: [ render((content, { url }) ( main>export function render(transform?: RenderTransform): RenderMiddleware { return renderWith( (context) function render(node: RemixNode, init?: ResponseInit): Response { return spaResponse.create(transform ? transform(node, context) : node, init) }, ) }3.1 底层机制renderWith与Renderer上下文键render()复用的是remix-run/render-middleware的renderWith见 packages/render-middleware/src/lib/render.ts。renderWith会为每个请求调用传入的工厂函数生成一个渲染器通过context.set(Renderer, renderer, { property: render })把渲染器挂到请求上下文上同时绑定为context.render属性因此任何中间件、路由处理器都能以context.render(node, init?)的形式调用它。Renderer是一个用createContextKey创建的上下文键SSR 场景下的render()render-ui.ts与 SPA 场景下的render()共用同一套上下文键机制只是生成响应的方式不同SSR 渲染为 HTML 流SPA 则生成spaResponse。3.2 响应载体Response CarrierspaResponseSPA 路由处理器返回的响应本身没有任何响应体真正的内容挂在 carrier 上。packages/ui/src/runtime/spa-response.ts 的实现显示它用一个WeakMapResponse, SPAResponseData把 Response 与{ node, redirectedTo? }关联起来create(node: RemixNode, init?: ResponseInit): Response { if (typeof document undefined) { throw new TypeError(spaResponse.create() can only be used in a browser) } let response new Response(null, init) let responses (spaResponses ?? new WeakMap()) responses.set(response, { node }) return response }两个值得注意的细节仅限浏览器spaResponse.create()在非浏览器环境没有document会直接抛TypeError这由 packages/spa/src/lib/spa-response.test.ts 的用例验证无响应体new Response(null, init)只携带状态码与头信息Remix 节点通过 WeakMap 关联这正是 README 所说的隐藏 SPA 响应载体——路由处理器只看到一个普通的Response。finalize(response, redirectedTo)则负责校验响应确实由spaResponse.create()创建否则抛Expected a Remix SPA response并记录重定向后的最终 URL见 packages/ui/src/runtime/spa-response.ts。3.3 关键类型Render与RenderTransformpackages/spa/src/lib/spa.ts 定义了配套类型interface Render { (node: RemixNode, init?: ResponseInit): Response } interface RenderTransform { (node: RemixNode, context: RequestContext): RemixNode }Render路由处理器里context.render的签名init可携带状态码与头信息例如{ status: 404 }RenderTransformrender(transform)的入参可以在节点渲染前基于当前请求上下文做包裹或替换。注意它拿到的是完整RequestContext因此可以读取context.url、context.request等任何中间件写入的上下文数据。四、run()浏览器运行时把 URL 变成页面4.1 最小路由器契约run()只要求传入一个极简契约packages/spa/src/lib/spa.tsinterface Router { fetch(input: string | URL | Request, init?: RequestInit): PromiseResponse }也就是说任何满足 fetch 签名的东西都能接入——真实的路由器、mock 对象甚至测试里的mock.fn。浏览器运行时通过resolveFrame把当前 URL 与后续的同源导航全部交给这个router.fetch分发。4.2 运行流程与fallbackrun()的实现packages/spa/src/lib/spa.ts本质是remix-run/ui的run()的 SPA 适配包装export function run(router: Router, options: RunOptions {}): Runtime { let app runRuntime({ loadModule() { throw new Error(SPA responses cannot hydrate client entries) }, async resolveFrame(src, options) { let url new URL(src, document.baseURI) let { response, redirectedTo } await followFrameRedirects(router, url, { method: options?.method, body: getRequestBody(options), signal: options?.signal, }) return spaResponse.finalize(response, redirectedTo) }, }) let readyPromise app.ready().then(async () { if (options.fallback ! undefined) { await app.frames.top.replace(options.fallback) } await app.frames.top.reload() }) return Object.assign(app, { ready: () readyPromise, }) }流程拆解resolveFrame每个 frame 导航先基于document.baseURI解析出绝对 URL再调用followFrameRedirects通过router.fetch取响应最后spaResponse.finalize校验并记录重定向目标fallback是可交互的 Remix 节点app.ready()解析后先用app.frames.top.replace(options.fallback)把占位 UI 放入顶层 frame再reload()触发首次路由加载。之所以不叫 loading spinner 而叫 fallback是因为它可以是带状态的组件——测试 packages/spa/src/lib/spa.test.browser.tsx 中有一个可点击计数的Fallback按钮在首次路由尚未完成时依然可以交互Loading: 0→ 点击 →Loading: 1loadModule直接抛错SPA 响应不经过客户端 entry 水合hydrate这与 SSR 场景形成鲜明对比返回的RuntimeOmitAppRuntime, ready并重写ready()保证客户端运行时启动且初始路由已渲染之后该 Promise 才 resolve。RunOptions目前只有fallback?: RemixNode一个选项packages/spa/src/lib/spa.ts。五、重定向语义同源跟随与 Fetch 方法规范followFrameRedirectspackages/spa/src/lib/spa.ts是 SPA 路由里最容易出问题、也最值得展开的部分const redirectStatuses new Set([301, 302, 303, 307, 308]) const maxRedirects 10跟随 5 种重定向状态码301/302/303/307/308其余状态直接返回最多 10 次跳转超过则抛TypeError(SPA route exceeded 10 redirects)防止死循环严格同源限制重定向目标nextUrl.origin必须等于初始 origin否则抛TypeError(SPA routes cannot redirect to another origin)遵循 Fetch 标准的方法改写303 See Other非GET/HEAD一律改写为GET并清空 body301/302POST改写为GET并清空 body其余情况保持原方法与 body。测试 packages/spa/src/lib/spa.test.browser.tsx 中验证了302 → 同源目标的场景两次请求方法均为GET最终渲染出Redirected页面。这套语义与 SSR 侧render-ui.ts的followFrameRedirects上限 20 次保持一致的思路只是 SPA 侧更简单——没有跨域 frame 的复杂头处理。六、表单提交请求体编码的三种形态getRequestBodypackages/spa/src/lib/spa.ts处理 frame 手动重载时携带的原始FormData目的是按表单声明的编码发送请求体而不是一律 multiparttext/plain拼成namevalue\r\n格式的 Blob且对 name/value 都做了换行符归一化\r\n | \r | \n→\r\n见normalizeLineBreaksapplication/x-www-form-urlencoded转为URLSearchParams文件类型字段取.name其他默认原样透传FormData浏览器会自动编码为 multipart。测试用例验证了text/plain场景表单encTypetext/plain提交后请求头的Content-Type为text/plain请求体为notefirst\r\nsecond\r\ncityParis\r\n——textarea 内部的换行被正确归一化。另外GET/HEAD方法不携带请求体直接return符合浏览器表单语义。七、仓库内的实战验证7.1 浏览器端单元测试packages/spa/src/lib/spa.test.browser.tsx 覆盖了核心行为测试用例验证点render给普通 router 上下文注入请求感知渲染器context.render(node, { status: 201 })后document.querySelector(h1)渲染出包含context.url.pathname的内容render对 defaultHandler 可用未匹配路由经defaultHandler返回404并渲染Not Foundready()前完成初始 URL 渲染初始 URL 携带的 query 参数出现在渲染结果中fetch只调用 1 次fallback 可交互首次路由挂起期间 fallback 按钮可点击、计数更新路由完成后被替换text/plain表单编码换行符归一化为\r\nContent-Type 正确同源重定向302 被跟随方法按 Fetch 语义保持GET7.2 端到端演示应用demos/spa 是一个完整的 Vite Remix 纯客户端演示应用覆盖了直接深链、客户端链接导航、取消被替代的慢路由、POST 表单数据、push/replace 历史行为等场景。运行方式pnpm -C demos/spa dev # 打开 http://localhost:44100 pnpm -C demos/spa test # 端到端测试默认对生产构建跑可改为 development 模式其 demos/spa/app/main.tsx 展示了比 README 更完整的工程形态组合中间件[wrapRender, logSpaRequests]其中logSpaRequests是一个普通 fetch-router 中间件演示了 SPA 场景下中间件体系完全复用异步路由home/about处理器里await sleep(700, request.signal)配合请求信号实现可取消的慢加载POST 表单submitGreet读取request.formData()后返回带状态的GreetingPage错误监听app.addEventListener(error, ...)捕获运行时错误。路由定义在 demos/spa/app/routes.tsget(/)、get(/about)、get(/greet)与post(/greet)。/greet同时挂 GET 与 POST正好对应端到端测试中提交新表单 push 历史记录、提交当前 URL replace 历史记录的行为断言见 demos/spa/app/app.test.e2e.ts。八、类型、导出与依赖全景packages/spa/src/index.ts 对外导出render, run, type Render, type RenderTransform, type Router, type RunOptions, type Runtime其中Router与Runtime是 SPA 特有的抽象前者把路由器压缩到单个fetch方法后者把 ui 运行时的ready语义改写为初始路由已渲染。依赖关系上remix-run/spa直接依赖fetch-router路由与上下文、render-middleware渲染中间件与ui帧与浏览器运行时三者缺一不可见 packages/spa/package.json。九、总结SPA 包的设计要点回到 CHANGELOG 的原始表述remix-run/spav0.1.0 的全部设计可以归结为三条原则契约保留router 始终是标准 fetch routerRequest → Response的调度、重定向、状态码、头信息、中间件与取消语义一脉相承README 的 Features 第一条即 Standard fetch routing载体隐藏context.render()对路由处理器暴露的是普通ResponseRemix 节点藏在spaResponse的 WeakMap carrier 中由run()的resolveFrame取出并渲染运行时接管run(router, { fallback })把当前 URL 与后续同源导航统一交给 router 分发ready()等待首次渲染完成fallback 则提供了可交互的首屏占位。如果你要继续深入推荐按依赖顺序阅读三个文件packages/render-middleware/src/lib/render.ts渲染中间件基础、packages/ui/src/runtime/spa-response.ts响应载体实现、packages/spa/src/lib/spa.tsSPA 包的render与run全部源码。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表