
Remix 3 服务端页面如何加交互clientEntry 与 run 启动浏览器运行时【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remixRemix 3 的服务端页面默认输出的是完整 HTML链接和表单在没有任何浏览器 JavaScript 的情况下也能工作。当某个局部 UI计数器、搜索框、带 pending 状态的提交按钮需要在浏览器里更新时Remix 的做法是两半用clientEntry(...)标记需要水合hydration的最小组件再在文档的浏览器入口模块里调用run()启动浏览器运行时。本文基于本仓库中的 Interactivity 指南05-interactivity.md、Hydration 文档和模板应用文件走一遍这条从服务端页面到浏览器交互的完整路径并给出如何确认水合确实发生。当前仓库中remix包的版本为3.0.0-rc.2。准备条件一个带浏览器入口的 Remix 应用Remix 应用模板已经把需要的结构放好了见 template/README.mdapp/actions/public/存放浏览器运行时入口和可交互组件app/router.ts把路由接到 handler并安装标准 Remix UI 渲染器交互组件本身位于app/actions/public/下的模块中服务端渲染时照常输出 HTML。启动方式是模板 README 给出的命令npm i npm run dev模板的 server.ts 默认监听http://localhost:44100可用PORT环境变量覆盖启动后控制台会打印Server listening on http://localhost:44100。文档对入口的要求只有一句话文档 shell 加载app/actions/public/entry.ts该模块调用run()一次。head里通过script typemodule src/assets/entry.js /之类的标签引入入口即可Getting Started 中的服务端示例展示了这种写法。用 clientEntry 标记需要交互的组件clientEntry(...)标记需要在浏览器运行的最小组件。第一个参数是客户端用来加载该组件的模块 URL 和导出名格式为moduleUrl#ExportName省略导出名时以函数名作为回退Hydration。下面是一个完整的计数器组件出自 Hydration 文档import { clientEntry, on, type Handle } from remix/ui export let Counter clientEntry( /assets/counter.js#Counter, function Counter(handle: Handle{ initialCount?: number; label: string }) { let count handle.props.initialCount ?? 0 return () ( div span {handle.props.label}: {count} /span button mix{[ on(click, () { count handle.update() }), ]} /button /div ) }, )Interactivity 指南中的专辑编辑表单用的是同一种写法只是第一个参数写作import.meta.url交互则是拦截表单submit事件、显示 pending 状态后跟随 action 的重定向。两种方式都是文档给出的真实用法差别在于模块 URL 的写法。写clientEntry组件时要遵守两条边界只标记最小边界。静态的父级和兄弟组件不需要水合数据库、已认证用户、请求上下文留在服务端只把组件需要的初始数据传进来。props 必须可序列化。它们从服务端序列化到浏览器支持的类型是字符串、数字、布尔、null、undefined、普通对象和数组、JSX 元素。不能传函数、类实例、带自定义原型的数据库记录等不透明运行时值两份文档的表述一致。在服务端clientEntry组件和普通风组件一样渲染服务端会把它们的输出包进注释标记并把 props 序列化进script typeapplication/json标签客户端靠这两样东西知道要水合什么、用什么数据Hydration 文档。用 run 启动浏览器运行时模板中的入口模块 template/app/actions/public/entry.ts 就是run()的完整实例import { detectMultipleImportMapSupport, importModule, preloadShim, } from remix/multiple-import-maps-polyfill import { run } from remix/ui const app run({ async loadModule(moduleUrl, exportName) { let mod await importModule(moduleUrl) let Component mod[exportName] if (typeof Component ! function) { throw new Error(Unknown component: ${moduleUrl}#${exportName}) } return Component }, async processClientEntryPreloads(preloads) { if (await detectMultipleImportMapSupport()) return preloads preloadShim(preloads) return [] }, }) if (import.meta.hot) { import.meta.hot.on(server:update, async () { try { await app.ready() await app.frames.top.reload() } catch (error) { console.error(Error reloading top frame on server update, error) } }) }各部分的职责Hydration 文档对run选项的说明loadModule(moduleUrl, exportName)必选对页面中发现的每个 client entry 被调用返回组件函数通常用动态import()实现。模板版本额外校验了返回值是函数否则抛Unknown component: ${moduleUrl}#${exportName}。processClientEntryPreloads(preloads)可选处理 frame 响应中发现的模块预加载返回应保留为原生link relmodulepreload的 URL。模板里用remix/multiple-import-maps-polyfill支持多 import map 的浏览器走原生 import不支持的浏览器在后续 frame 响应引入新映射时使用 polyfill。import.meta.hot块只在启用 HMR 时生效服务端更新后等待app.ready()再重载顶层 frame。不需要 HMR 时这部分可省略最小入口就是 Hydration 文档给出的run({ loadModule })await app.ready()。run()返回的app运行时有三个生命周期方法方法作用app.ready()在初始 client entries 全部水合完成后 resolveapp.flush()同步应用待处理的组件更新主要用于需要立即观察 DOM 的测试和集成app.dispose()移除运行时挂载的组件和监听器两点行为需要明确两份文档一致run()会同时创建app.frames.top并开始监听 Navigation API 事件因此即使页面只用了clientEntry()而没有渲染显式Frame符合条件的同源链接和表单也会走 soft navigationRemix 通过 frame resolver 抓取 HTML 并更新现有文档而不是加载新文档。run()只负责水合服务端渲染的页面不是第二个应用路由浏览器请求仍然打到拥有对应服务端行为的 route action。运行并确认交互生效在应用根目录执行npm i、npm run dev按控制台提示打开http://localhost:44100。打开页面源码确认两个服务端留下的水合痕迹Hydration 文档描述的机制client entry 组件的输出被包在!-- rmx:h:id --/!-- /rmx:h --注释标记中props 和模块元数据收集在script typeapplication/json idrmx-data标签里。点击计数器按钮数字应本地递增——客户端水合后匹配的元素被就地接管不匹配的部分会被修补。页面行为判断标准页面在模块加载完成前就已完整渲染没有空白闪烁只有被标记的组件携带 JavaScript未标记内容保持静态。如果想捕获组件内部错误app本身是EventTarget按 Hydration 文档的写法挂一个监听app.addEventListener(error, (event) { console.error(Component error:, event.error) })水合完成时机可以用await app.ready()判断它 resolve 时所有初始 client entries 已完成水合。可选分支没有服务端 HTML 可水合时用 createRoot如果某个区域根本没有服务端渲染的组件例如挂到另一个应用创建的容器里的命令式小组件用createRoot(container)而不是clientEntryInteractivity 指南import { createRoot } from remix/ui import type { Handle } from remix/ui function SupportWidget(_handle: Handle) { return () a href/supportContact support/a } let container document.getElementById(support-widget) if (container) { let root createRoot(container) root.render(SupportWidget /) window.addEventListener(pagehide, () root.dispose(), { once: true }) }root.render(node)挂载组件树root.flush()同步应用待处理工作root.dispose()移除树并执行清理。指南同时给出提醒正常页面 UI 应当从服务端开始并用clientEntry(...)否则用户要等 JavaScript 才能看到服务端本可以渲染的内容。限制与边界传给clientEntry的 props 会被序列化不要传类实例——如果多个组件共用同一个应用模型如 Interactivity 指南中的CartModel在最近的共享 client 边界创建它并通过组件 context 提供而不是作为 props 传递。位于data-rmx-preserve-dom内的 client entry 在初次启动时会水合但后续 frame 重载不会通过该保留宿主修补新的服务端渲染子节点或 propsHydration 文档。事件处理中只在增强路径会替浏览器完成工作时才调用preventDefault()仅添加 pending 状态时应让普通表单提交继续。handle.update()在 setup 期间调用会警告并跳过额外渲染在渲染期间调用会抛错需要提交后的 DOM 工作时用handle.queueTask()。服务端路由与 action 的写法表单的action、method指向 route action不在本文展开可继续看 05-interactivity.md 后续的表单增强部分以及 06-streaming-ui-with-frames.md 中用Frame独立重载路由区域的进阶场景。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考