Remix 3 如何用 context.render 与 Document 组件构建服务端渲染页面

Remix 3 如何用 context.render 与 Document 组件构建服务端渲染页面 Remix 3 如何用 context.render 与 Document 组件构建服务端渲染页面【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix在 Remix 3 应用中路由 action 返回的并不一定是手写 HTML 字符串标准的做法是把 Remix 组件树交给context.render(...)由 render 中间件在服务端把它渲染成 HTMLResponse同时用一个共享的Document组件输出完整的html文档。这篇文章基于 Remix 官方指南与项目模板走通这条路径安装渲染中间件、在 action 中调用context.render(...)、用Document组件构建 head 与 body并给出文档中提供的验证方式。适用环境Node 服务端模板要求node 24.3.0运行时依赖只有一个remix包版本^3.0.0。准备条件创建并启动一个 Remix 应用按照 Start Here 章节 的 Quickstart用 CLI 创建项目npx remixnext new my-remix-app cd my-remix-app npm i生成的package.json中唯一的运行时依赖就是remix{ name: my-remix-app, private: true, type: module, dependencies: { remix: ^3.0.0 } }启动开发服务器npm run dev服务器启动时会打印本地地址默认监听 http://localhost:44100。此时访问该地址就能看到模板自带的服务端渲染首页模板中的相关命令还有npm run start生产模式启动、npm run test、npm run typecheck见 template/README.md。第一步在 router 中安装 render 中间件context.render(...)不是路由自带的——它由remix/middleware/render提供。按 Request Handling 章节 的说明render()的作用是“安装标准的 Remix UIcontext.render(...)函数”并且它的类型会随中间件栈进入请求上下文。生成的 template/app/router.ts 就是完整接法import { createRouter, type MiddlewareContext } from remix/router import { render } from remix/middleware/render import { staticFiles } from remix/middleware/static import controller from ./actions/controller.tsx import { assets } from ./assets.ts import { routes } from ./routes.ts const renderMiddleware render({ assets }) type AppContext MiddlewareContext[typeof renderMiddleware] declare module remix/router { interface RouterTypes { context: AppContext } } export const router createRouterAppContext({ middleware: [staticFiles(./public, { index: false }), renderMiddleware], }) router.map(routes, controller)要点render({ assets })的assets参数是一个 asset server由app/assets.ts中的createAssetServer(...)创建。按 packages/render-middleware/README.md 的说明只有当组件使用基于源码的clientEntry(import.meta.url, Component)时才需要传它如果客户端入口已经用 public URL、或应用没有 client entry可以省略。模块扩展declare module remix/router让所有 controller 的 action 都默认拿到带context.render的上下文类型。多 router 的应用应改为显式传递上下文类型而不是设置全局默认。render()建议放在“可能提前响应、不需要渲染”的中间件如staticFiles之后模板里的顺序即为如此。render()还有一个onError选项用于处理服务端渲染错误不传时 UI 渲染器使用默认错误上报。第二步在 action 中调用 context.render渲染页面就是在匹配的 action 里把组件树传给context.render(...)。Rendering UI 章节 中的示例是一个专辑详情页import { createController } from remix/router; import { routes } from ../../routes.ts; import { getAlbum } from ./data.ts; import { AlbumPage } from ./show-page.tsx; export default createController(routes.albums, { actions: { async show(context) { let album await getAlbum(context.params.albumId); if (album undefined) { return new Response(Album not found, { status: 404 }); } return context.render(AlbumPage album{album} /); }, }, });生成的模板里更简单的版本是首页 action直接返回context.render(HomePage /)见 template/app/actions/controller.tsx。当渲染出的页面需要自定义状态码或响应头时context.render接受第二个参数ResponseInitreturn context.render(AlbumPage album{album} /, { status: 404, headers: { Cache-Control: no-store }, });context.render(node, init)返回一个 HTMLResponse并保留你传入的 status 与 headerspackages/render-middleware/README.md。组件本身的模型一个 Remix 组件是“setup 函数返回 render 函数”。setup 在该组件实例创建时运行一次render 在初始渲染和每次更新后执行组件通过Handle读取handle.propsimport type { Handle } from remix/ui; type AlbumHeadingProps { artist: string; title: string; }; export function AlbumHeading(handle: HandleAlbumHeadingProps) { // Setup: runs once for this component instance. return () { // Render: runs for the initial render and every update. return ( header p{handle.props.artist}/p h1{handle.props.title}/h1 /header ); }; }同一个组件既可以在服务端渲染成 HTML也可以挂载到 client-only 根节点或在服务端渲染的页面里 hydrate——后两种情况才会在浏览器中执行。第三步用 Document 组件输出完整文档指南要求“页面应该通过一个共享组件渲染完整文档”默认应用把这个组件放在app/actions/document.tsx。Rendering UI 章节给出的版本import type { Handle, RemixNode } from remix/ui; import { ImportMap } from remix/ui/server; import { scriptEntry } from ../assets.ts; export interface DocumentProps { children?: RemixNode; head?: RemixNode; title?: string; } export function Document(handle: HandleDocumentProps) { return () { let { children, head, title Albums } handle.props; let { href, importMap, preloads } scriptEntry; return ( html langen head meta charSetutf-8 / meta nameviewport contentwidthdevice-width, initial-scale1 / link relicon href/favicon.svg typeimage/svgxml / title{title}/title {head} ImportMap value{importMap} / {preloads.map((preloadHref) ( link key{preloadHref} relmodulepreload href{preloadHref} / ))} script typemodule src{href}/script /head body{children}/body /html ); }; }模板中的实际版本 template/app/actions/document.tsx 结构相同只是默认标题从 CLI 生成的应用名读取DEFAULT_TITLE并额外带了一个color-schememeta。各部分的职责均来自 Rendering UI 章节title、meta、link、style元素都放在文档显式的head里连同全局样式表、module preload、图标和浏览器入口脚本。入口 href 及其模块图在app/assets.ts中通过assets.getScriptEntry(entry)一次性解析不要在组件里现构造 asset URL。模板里这一步在 template/app/assets.tsexport const scriptEntry await assets.getScriptEntry(entry)。ImportMap会把脚本入口的 import map 与阻塞式 client entry 的映射合并使初始文档包含一份完整的 import map。渲染器会把这棵树交给createHtmlResponse()保留已有 doctype 或在前面补!DOCTYPE html除非 action 自己提供了Content-Type否则设置为Content-Type: text/html; charsetUTF-8最终返回一个普通的 WebResponse。样式css 静态规则与 style 动态值组件样式用css(...)声明静态规则它支持伪类、伪元素、后代与属性选择器以及普通 CSS 嵌套的媒体查询import { css } from remix/ui; import type { Handle } from remix/ui; const cardStyle css({ border: 1px solid #d6d6d6, borderRadius: 12px, padding: 1rem, :hover: { borderColor: #6d28d9, }, media (max-width: 40rem): { borderRadius: 0, }, }); export function AlbumCard(handle: Handle{ soldOut: boolean; title: string }) { return () ( article mix{cardStyle} style{{ opacity: handle.props.soldOut ? 0.55 : 1 }} h2 classtitle{handle.props.title}/h2 p{handle.props.soldOut ? Sold out : In stock}/p /article ); }文档给出的取舍css(...)生成一个 class 和静态规则适合不变的规则styleprop 更适合进度、坐标、透明度这类每次更新都可能变的值——把它们放进css(...)会为每个值多生成一条规则。服务端渲染时Remix 会收集生成的规则、去重并把style>layer base, rmx, app;rmx之前的 layer 提供可被组件样式覆盖的默认值rmx之后的 layer 可以有意识地覆盖组件样式未分层的作者 CSS 优先于普通分层 CSS要刻意使用。验证确认返回的是服务端渲染的 HTML两条文档给出的验证路径浏览器验证npm run dev启动后访问 http://localhost:44100默认端口服务器启动时打印本地 URL。首页是模板的服务端渲染页面上文的截图即该结果。修改app/actions/document.tsx里的title后查看页面源码应能看到新的title和Content-Type: text/html。代码级验证router 本身就是一个 fetch handler可以在测试中直接构造Request调用Request Handling 章节 的文档示例URL 为文档中的示例地址let request new Request(https://albums.example/albums/thriller, { method: GET, headers: { Accept: text/html }, }); let response await router.fetch(request); console.log(response.status); // 200 console.log(response.headers.get(Content-Type));按文档这个调用在 Node 服务器、Bun、Cloudflare Worker 或测试里是同一个契约。另外npm run typecheck可以确认context.render的类型经模块扩展后在整个 controller 中都可用。边界与可选分支没有匹配路由时router 返回404响应中间件或 action 抛错时router.fetch(...)会 rejectserver.ts是最后一个把错误转成500的地方模板的 server.ts 即按此模式包裹router.fetch。不需要组件树的页面feed、邮件、小片段可以不走组件运行时改用remix/html-template的html模板生成转义后的SafeHtml再交给createHtmlResponse(...)返回插值字符串会被转义html.raw只用于应用已经信任的标记不要传入用户输入。自定义渲染管线context.render的输入不是 Remix UI 节点时可用renderWith()安装 JSON、邮件等自定义 rendererpackages/render-middleware/README.md。浏览器端的事件处理与 hydration 是下一章的主题clientEntry(...)边界本文的页面在不处理事件、不使用浏览器 API 的部分不需要客户端 JavaScript。完整的章节原文见 Rendering UI 与 Request Handling模板文件见 template/app/router.ts 与 template/app/actions/document.tsx。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考