render-middleware:为 Remix 路由注入请求级渲染器的中间件包解析
【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
@remix-run/render-middleware是 Remix 全栈框架中负责"请求级响应渲染"的中间件包:它通过renderWith()提供自定义渲染器的底层入口,并在 v0.2.0 引入render()约定式 Remix UI 渲染器,让开发者用一行中间件把 UI 节点流式渲染为带正确类型、状态码与响应头的 HTMLResponse。读完本文,你将掌握context.render(node, init)的类型化调用方式、嵌套与定向 Frame 的内部解析机制、客户端入口资产解析规则,以及如何用renderWith()构建 JSON、邮件等自定义响应管线。
一、包定位:从 CHANGELOG 看能力边界
render-middleware的 CHANGELOG 记录了两个关键里程碑:v0.1.0 的初始发布与 v0.2.0 的核心能力升级,其余版本均为对@remix-run/*依赖的常规升级。其能力范围概括为三件事:
- Remix UI 渲染:将节点流式转换为 HTML 响应(
render()); - 框架托管的 Frame:通过当前路由解析嵌套与定向
<Frame>请求; - 自定义渲染器:用
renderWith()安装 JSON、邮件等任意响应管线。
从 package.json 可以看到它的依赖关系:@remix-run/assets(源资产编译与浏览器模块 URL)、@remix-run/fetch-router(请求路由与类型化上下文)、@remix-run/response(Web Response 工具)、@remix-run/ui(UI 组件、Frame 与服务端渲染)。这正对应 CHANGELOG 中反复出现的依赖升级记录:assets@0.6.0、ui@0.8.0、fetch-router@0.19.0 → 0.21.0。
包的公开 API 全部集中在 src/index.ts,只有四个导出,边界非常收敛:
export { Renderer, renderWith } from './lib/render.ts' export type { AnyRenderer } from './lib/render.ts' export { render } from './lib/render-ui.ts' export type { RenderFunction, RenderOptions } from './lib/render-ui.ts'二、v0.1.0 基础:Renderer 上下文键与 renderWith()
CHANGELOG 明确记录了 v0.1.0 的初始能力:提供Renderer上下文键、Renderer类型,以及用于"为 fetch-router 请求上下文添加请求级渲染器"的renderWith()中间件。渲染器同时以context.render和context.get(Renderer)两种方式可用。
2.1 Renderer 类型:任何输入形状的渲染函数
lib/render.ts 中Renderer是一个泛型函数类型:接收任意input与应用数据,返回Response或Promise<Response>:
export interface Renderer<input = unknown, responseInit = ResponseInit> { (input: input, init?: responseInit): Response | Promise<Response> } export type AnyRenderer = Renderer<never, never>AnyRenderer是"任何输入与选项形状"的渲染器别名,用于context.get(Renderer)读取时的兜底类型。
2.2 renderWith():工厂函数,每请求执行一次
renderWith(createRenderer)接收一个工厂函数(context) => renderer,该工厂每个请求只运行一次,因此可以读取当前请求上下文(URL、请求头等)来构造渲染器。其实现只有寥寥数行:
export function renderWith<const renderer extends AnyRenderer>( createRenderer: RendererFactory<renderer>, ): Middleware<{ key: typeof Renderer; value: renderer; property: 'render' }> { return (context, next) => { context.set(Renderer, createRenderer(context), { property: 'render' }) return next() } }context.set(Renderer, renderer, { property: 'render' })这一调用同时完成两件事:把渲染器写入Renderer上下文键(可用context.get(Renderer)读取),并挂载为上下文的render直接属性。property: 'render'使 TypeScript 能推导出context.render的精确签名。
2.3 实战:用 renderWith() 构建 JSON 渲染器
CHANGELOG 描述的"自定义渲染器"场景在 README 中有完整示例——当输入不是 Remix UI 节点,或应用完全自持响应管线时使用renderWith():
import { renderWith } from 'remix/middleware/render' import { createRouter } from 'remix/router' let json = renderWith( () => function render(data: unknown, init?: ResponseInit) { return Response.json(data, init) }, ) let router = createRouter({ middleware: [json] }) router.get('/api/status', (context) => context.render({ ok: true }))类型化是该机制的核心卖点。render.test.ts 用IsEqual类型断言验证:context.render的输入类型与工厂返回的渲染器签名严格一致,传入错误类型(如对{ ok: boolean }渲染器传'no')会触发编译错误。测试同时验证了:
- 渲染器可读取请求上下文(工厂内使用
context.url.pathname拼装响应体); - 自定义
init选项类型被完整保留(如ResponseInit & { pretty?: boolean }); - 中间件栈推导出的
MiddlewareContext能自动获得类型化的context.render。
三、v0.2.0 核心:render() 约定式 Remix UI 渲染器
CHANGELOG v0.2.0 是本包最重要的一次变更:新增render({ assets?, onError? }),即约定式的请求级 Remix UI 渲染器。它通过context.render(node, init)返回类型化 HTML 响应;通过当前路由以"安全请求头 + 取消传播"解析嵌套与定向 Frame;保留 Frame 错误响应体;并可选通过资产服务器解析基于源码的客户端入口。
3.1 安装与最小使用
安装仅需一个命令(npm i remix,monorepo 内对应@remix-run/render-middleware),随后把render()挂进路由中间件栈:
import { createRouter } from 'remix/router' import { render } from 'remix/middleware/render' import { staticFiles } from 'remix/middleware/static' let router = createRouter({ middleware: [staticFiles('./public'), render()], }) router.get('/', (context) => context.render( <html> <body> <h1>Dashboard</h1> </body> </html>, ), )3.2 context.render(node, init):类型化 HTML 响应
context.render(node, init)返回一个 HTMLResponse,并保留传入的init中的状态码与响应头:
router.get('/missing', (context) => context.render(<h1>Not found</h1>, { status: 404, headers: { 'Cache-Control': 'no-store' }, }), )从 lib/render-ui.ts 的实现看,render()本质上是renderWith的封装:工厂闭包捕获context.request与topFrameSrc,返回的render函数调用renderToStream(node, {...})把 Remix UI 节点渲染为流,再经createHtmlResponse(stream, init)(来自@remix-run/response/html)包装为 HTML 响应。测试验证了返回响应携带text/html; charset=UTF-8、自定义头与状态码,且文档以<!DOCTYPE html>开头。
3.3 RenderOptions 两个选项
| 选项 | 类型 | 作用 |
|---|---|---|
assets | Pick<AssetServer, 'getHref' \| 'getPreloads'> | 将基于源码的客户端入口 ID 解析为浏览器模块 URL 与预加载 URL。当客户端入口已使用公开 URL、或应用没有客户端入口时可省略 |
onError | (error: unknown) => void | 服务端渲染错误回调。省略时使用 UI 渲染器默认的错误上报 |
关于onError有个值得一提的实现细节:当请求头X-Remix-Frame为'true'(即本请求本身就是内部 Frame 请求)时,onError会被替换为空函数——这样内层 Frame 的错误只在外层文档渲染时上报一次,避免重复报错,测试reports nested frame render errors once正是验证这一行为。
3.4 资产服务器:客户端入口解析
当组件使用基于源码的客户端入口(如clientEntry(import.meta.url, Component))时,需要把资产服务器传给render({ assets }):
import { createAssetServer } from 'remix/assets' import { render } from 'remix/middleware/render' import { staticFiles } from 'remix/middleware/static' import { createRouter } from 'remix/router' import { Frame } from 'remix/ui' let assets = createAssetServer({ basePath: '/assets', rootDir: process.cwd(), allowFiles: ['app/routes.ts', 'app/**/public/**'], allowPackages: ['remix'], denyFiles: ['app/**/*.test.*'], }) let router = createRouter({ middleware: [staticFiles('./public'), render({ assets })], }) router.get( '/assets/*path', async ({ request }) => (await assets.fetch(request)) ?? new Response('Not Found', { status: 404 }), )底层解析逻辑在resolveClientEntry(lib/render-ui.ts):
- entryId 格式:
sourceId#exportName,#后为显式导出名;未指定时回退到组件函数的name; file:前缀的源码入口:必须配置assets,否则抛出clientEntry() cannot use a file: source entry ID without an asset server错误;解析时并行调用assets.getHref(sourceId)与assets.getPreloads(sourceId)得到模块 URL 与预加载列表;- 公开 URL 入口(如
/public/widget.js#PublicEntry或 CDN 地址):直接作为href使用,无需资产服务器; - 缺失导出名:错误信息会根据是否配置资产服务器给出不同示例(
import.meta.url + "#ExportName"或"/js/module.js#ExportName"),且错误文本不会泄露file:源码路径(测试doesNotMatch断言了这一隐私保护)。
render-ui.test.ts 的resolves source client entries through the asset server用例验证了:file:入口被解析为/assets/counter-123.js,预加载输出为<link contenteditable="false">【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考