ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Remix 中间件编排与服务器搭建:从请求生命周期到开发期 HMR

Remix 中间件编排与服务器搭建:从请求生命周期到开发期 HMR Remix 中间件编排与服务器搭建从请求生命周期到开发期 HMR【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix导读本文围绕 Remix 框架的核心服务器侧编排能力展开如何用一条有序的中间件链组装请求生命周期压缩、静态文件、表单解析、会话、认证、渲染如何编写携带类型化上下文的自定义中间件如何用remix/node-fetch-server把路由桥接到标准 Node HTTP 服务器以及如何在保留默认开发/生产启动路径的前提下为server.ts叠加一套开发期 HMR 热更新方案。读完本文你将掌握一套可直接复制到仓库中template/、demos/bookstore等应用下的中间件组合规范与服务器适配实践并理解其底层实现packages/fetch-router/src/lib/middleware.ts、packages/fetch-router/src/lib/router.ts的工作方式。中间件堆栈为每个请求建立有序生命周期在 Remix 中中间件按注册顺序对每一个请求依次执行。核心原则是快退型fast-exit中间件放前面请求增强型request-enriching中间件放后面。静态文件、CORS 预检这类「要么立刻响应、要么尽快放行」的中间件应该尽早会话、认证、数据加载这类需要先为下游准备上下文的中间件应该靠后。推荐的中等复杂度应用编排如下对应仓库中template/app/router.ts与各 demo 的写法import { createRouter } from remix/router import { compression } from remix/middleware/compression import { formData } from remix/middleware/form-data import { logger } from remix/middleware/logger import { methodOverride } from remix/middleware/method-override import { render } from remix/middleware/render import { session } from remix/middleware/session import { staticFiles } from remix/middleware/static import { asyncContext } from remix/middleware/async-context let middleware [] if (process.env.NODE_ENV development) { middleware.push(logger()) } middleware.push(compression()) middleware.push(staticFiles(./public)) middleware.push(formData()) middleware.push(methodOverride()) middleware.push(session(cookie, storage)) middleware.push(asyncContext()) middleware.push(loadDatabase()) middleware.push(loadAuth()) middleware.push(render({ assets })) let router createRouter({ middleware })底层如何运行createRouter的完整实现位于 packages/fetch-router/src/lib/router.ts。router.fetch()创建RequestContext后进入dispatchRouter若配置了路由级中间件则调用runMiddleware(routerMiddleware, context, dispatch)逐层执行最后才进入dispatchMatches用多匹配器createMultiMatcher按 URL 匹配路由。runMiddleware的实现位于 packages/fetch-router/src/lib/middleware.ts它维护一个index指针中间件返回Response时短路整个链中间件调用并返回next()时采用下游后续中间件/最终 handler的响应中间件既没返回响应也没调用next()会抛出Middleware must return a Response or call next()同一中间件重复调用next()会抛出next() called multiple times。同时每一环都会用raceRequestAbort将 handler 的执行与request.signal的 abort 竞速请求被中断时抛出request.signal.reason。内置中间件目录中间件导入路径适用场景说明staticFiles(dir, opts?)remix/middleware/static从public/或其他目录按磁盘原样提供文件快退型通常放在最靠前位置compression()remix/middleware/compression压缩文本类响应通常全局启用logger()remix/middleware/logger记录请求与响应通常仅开发环境colors可强制开/关颜色输出cors(opts?)remix/middleware/cors端点需服务跨域浏览器或响应预检OPTIONS请求通常放早期让预检尽早短路cop(opts?)remix/middleware/cop无同步令牌时拒绝不安全的跨域浏览器请求使用时放在session()或 CSRF 之前formData(opts?)remix/middleware/form-data解析FormData请求体尤其是表单与上传_csrf表单字段提取依赖它methodOverride()remix/middleware/method-overrideHTML 表单需要PUT/PATCH/DELETE语义必须在表单解析之后运行session(cookie, storage)remix/middleware/sessionCookie 支撑的会话必须在会话支撑的 auth/CSRF 之前运行csrf(opts?)remix/middleware/csrf会话支撑的表单流程需要同步令牌 CSRF 防护前置条件session()已注册asyncContext()remix/middleware/async-contexthandler 之外的工具函数需要通过getContext()获取请求上下文在依赖它的助手之前添加auth({ schemes })remix/middleware/auth把认证状态解析进context.get(Auth)会话支撑的 auth 需在session()之后requireAuth()remix/middleware/authcontroller 或 action 必须拒绝匿名访问通常作为 controller/action 中间件render({ assets? })remix/middleware/renderaction 通过context.render(node, init)渲染 Remix UI传入 asset server 以支持基于源码的客户端入口这些包的导出均可在 packages/remix/src 下找到对应入口例如static-middleware.ts、compression-middleware.ts、session-middleware.ts、render-middleware.ts等均为指向各自独立包的再导出。静态文件与浏览器模块的分工需要按磁盘原样直出的文件图片、字体、已构建产物放在根public/目录用staticFiles()提供需要从源码编译、带导入重写import rewriting、preload 或指纹 URL 的浏览器模块交给remix/assets资产服务器app/下的public/目录存放可供资产服务器直接访问的浏览器源码。排序注意事项快退型靠前staticFiles()、cors()预检处理、以及使用时的cop()先解析请求体再运行依赖它的中间件如methodOverride()与csrf()的表单字段令牌提取session()必须先于csrf()和会话支撑的auth()asyncContext()必须在任何调用getContext()的助手或共享代码之前路由保护如requireAuth()保持为 controller 或 action 中间件除非整个应用都是私有的。常见组合栈会话支撑的 HTML 应用→compression()、staticFiles()、可选cop()、formData()、methodOverride()、session()、可选csrf()、asyncContext()、auth({ schemes })、render({ assets })跨域 API→compression()、cors()、可选asyncContext()、可选auth({ schemes })上传流程→compression()、staticFiles()、formData({ uploadHandler })之后按需追加会话、认证与数据加载中间件可选开发 HMR→ 保留server.ts作为子应用服务器为remix/node-hmr增加hmr.ts并通过createHmrReadyFetch()代理公共请求带选项的中间件// 静态文件带缓存头 staticFiles(./public, { cacheControl: no-store, must-revalidate, etag: false, lastModified: false, }) // 表单数据带上传处理器 import type { FileUpload } from remix/form-data-parser import { createFsFileStorage } from remix/file-storage/fs let fileStorage createFsFileStorage(./tmp/uploads) formData({ uploadHandler(fileUpload: FileUpload) { return fileStorage.set(fileUpload.name, fileUpload) }, })uploadHandler抛出或被 reject 的错误会直接向上传播当领域相关的上传错误需要变成用户可见的Response时应在路由边界捕获它们。staticFiles 选项在源码中的展开staticFiles的实现位于 packages/static-middleware/src/lib/static.ts。除上述缓存选项外它还支持filter(path?)按相对路径决定哪些文件允许服务返回false时直接放行到下一个中间件acceptRanges: boolean | (file) boolean是否支持 HTTP Range 请求返回206 Partial Content。默认仅对不可压缩 MIME 类型启用——因为Accept-Ranges: bytes出现时压缩中间件不会压缩该响应两者互斥index: boolean | string[]请求指向目录时尝试的索引文件默认[index.html, index.htm]false关闭listFiles: boolean是否生成目录文件列表 HTML 页与index同时设置时index优先。实现上中间件只对GET/HEAD生效用 URL pathname 去根目录解析真实路径文件不存在或出错时一律next()放行。编写自定义中间件中间件是一个接收(context, next)的函数返回Response→ 短路链路调用并返回next()→ 透传下游响应什么都不返回 → 仅设置上下文路由器自动继续。设置类型化上下文值用context.set(key, value)添加类型化值下游用context.get(key)读取import type { Middleware } from remix/router import { databaseContext } from ~/middleware/database.ts export function loadDatabase(): Middleware { return async (context, next) { context.set(databaseContext, db) return next() } }RequestContext的运行时实现见 packages/fetch-router/src/lib/request-context.ts它内部维护一个Mapobject, unknowncontext.set(key, value, options?)还支持property选项把值安装为上下文的直接属性如文档示例中的context.auth.identity且会校验属性名不能与既有上下文冲突。createContextKey创建的键可携带defaultValuecontext.get()在键未设置但存在默认值时返回默认值。守卫路由import { Auth } from remix/middleware/auth export function requireAdmin(): Middleware { return (context, next) { let auth context.get(Auth) if (auth.identity?.role ! admin) { return new Response(Forbidden, { status: 403 }) } return next() } }为助手提供异步上下文asyncContext()把请求上下文存入AsyncLocalStorage实现见 packages/async-context-middleware/src/lib/async-context.ts即storage.run(context, next)使深层工具函数无需逐层透传 context 即可取到当前请求上下文。把getContext()封装进应用自己的助手// app/utils/context.ts import { getContext } from remix/middleware/async-context import { Auth } from remix/middleware/auth import { databaseContext } from ~/middleware/database.ts import { Session } from remix/session export function getCurrentDb() { return getContext().get(databaseContext) } export function getCurrentSession() { return getContext().get(Session) } export function getCurrentUser() { let auth getContext().get(Auth) if (!auth.ok) { throw new Error(Expected an authenticated user. Run requireAuth() before this code.) } return auth.identity } export function getCurrentUserSafely() { let auth getContext().get(Auth) return auth.ok ? auth.identity : null }注意在asyncContext()未注册时调用getContext()会抛出No request context found. Make sure the asyncContext middleware is installed.这就是「必须在助手使用之前注册asyncContext()」的原因。中间件的三种类型中间件共有三种 API 所属形式1. 路由中间件router middleware——每个请求都会执行let router createRouter({ middleware: [logger(), session(cookie, storage)] })2. 控制器中间件controller middleware——只作用于某个控制器内的直接 actionexport default createController(routes.account, { middleware: [requireAuth()], actions: { ... }, })控制器中间件不会流入其他控制器需要保护的每个控制器都要各自添加。源码层面createRouter的mapController会通过mergeMiddleware(controllerMiddleware, action.middleware)把控制器级与 action 级中间件合并后再注册packages/fetch-router/src/lib/router.ts且要求控制器actions的键与路由映射严格一一对应缺失会抛Missing action/Unknown action类型错误。3. action 中间件action middleware——只作用于单个 actionrouter.get(routes.account.index, { middleware: [requireAuth()], handler(context) { return render(AccountPage identity{context.auth.identity} /) }, })类型层面的建议middleware选项优先使用内联数组inline arrays用RouterContexttypeof router从使用内联中间件的路由器推导应用上下文只有当中间件链存入变量、需要保留其精确元组类型时才使用createMiddleware()例如在没有路由器值的情况下推导MiddlewareContexttypeof rootMiddleware、导出可复用链、或从工厂函数返回一条链。createMiddleware与MiddlewareContext的类型定义见 packages/fetch-router/src/lib/middleware.ts。Node 服务器搭建新应用自带一个用remix/node-fetch-server适配应用路由器的server.ts。除非任务明确需要改变运行时行为如 host/protocol 处理、TLS、HTTP/2、WebSockets、部署生命周期或仅测试用服务器否则保留该生成服务器。remix/node-fetch-server的createRequestListener(handler, options?)把 fetch 风格的 handler 转成标准 Nodehttp.RequestListener实现见 packages/node-fetch-server/src/lib/request-listener.ts它把 Node 的req/res构造成标准Request处理响应关闭观察、请求创建错误、abort 错误与错误响应兜底。当你想直接持有标准 Nodehttp、https或http2服务器时就用它。仓库demos/bookstore/server.ts给出了完整范例创建http.createServer(createRequestListener(...))在listen回调里按REMIX_NODE_HMR环境变量决定是否上报就绪并注册SIGINT/SIGTERM优雅关闭。开发期 HMR把 HMR 当作可选的快速 UI 编辑模式并让它远离正常应用代码真实应用服务器放在server.tsdev脚本直接运行它通常借助 Node 的 watch 模式只有当项目确实受益于 HMR 时才额外增加一个仅开发使用的hmr.ts脚本。// hmr.ts import * as http from node:http import { createFetchProxy } from remix/fetch-proxy import { createHmrReadyFetch, run } from remix/node-hmr import { createRequestListener } from remix/node-fetch-server const hmrProxyPort 44100 const hmrEventPort 44101 const appPort 44102 const hmrRunner run(./server.ts, { env: { ...process.env, PORT: String(appPort), HMR_PROXY_PORT: String(hmrProxyPort), }, nodeArgs: [--import, remix/node-tsx, --import, remix/ui-hmr/node], browserHmrChannel: { port: hmrEventPort }, }) let proxyFetch createFetchProxy(http://127.0.0.1:${appPort}, { xForwardedHeaders: true, }) let server http.createServer(createRequestListener(createHmrReadyFetch(hmrRunner, proxyFetch))) server.listen(hmrProxyPort, 127.0.0.1)要点保持browserHmrChannel.port稳定这样即使开发服务器被手动重启浏览器 HMR 客户端也能重连到同一个事件通道当浏览器请求可能发生在子服务器重启期间使用稳定的公共代理。createHmrReadyFetch()会等待当前活跃的子代就绪后再转发请求并在请求过程中子代切换时重试安全的不可用响应实现见 packages/node-hmr/src/index.tsshouldRetrySafeUnavailableRequest只在新 generation 出现时重试避免无意义重放run(./server.ts, options)启动一个受监督的子进程并 watch 其加载的模块图被接受的模块变更原地应用未接受的变更则重启子进程packages/node-hmr/src/index.ts。在子进程server.ts中监听成功后上报就绪server.listen(port, () { if (process.env.REMIX_NODE_HMR) { import(remix/node-hmr/runtime).then((nodeHmr) nodeHmr.emitServerReady()) } })规则用process.env.REMIX_NODE_HMR守卫remix/node-hmr/runtime的导入只有当服务端渲染的 Remix UI 组件模块需要热更新时才使用--import remix/ui-hmr/node保持默认的开发/生产启动路径独立于hmr.ts在SIGINT和SIGTERM关闭流程中同时关闭公共服务器与hmrRunner。上述模式在仓库demos/bookstore含hmr.ts与server.ts中有完整可运行的对应实现可作为 HMR 落地的参考范本。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进