Remix 路由与控制器设计:从 route map、路由助手到 createController 的类型安全请求处理

发布时间:2026/9/10 5:51:32

Remix 路由与控制器设计:从 route map、路由助手到 createController 的类型安全请求处理 Remix 路由与控制器设计从 route map、路由助手到 createController 的类型安全请求处理【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix本篇指南围绕 Remix 中路由与控制器这一核心边界展开app/routes.ts里声明的 route map 约定应用接受哪些 URL 和 HTTP 方法app/actions/下的控制器实现这些约定每个 action 返回的 WebResponse最终成为浏览器看到的页面、重定向、JSON 载荷或错误。读完后你将掌握 Remix 的route(...)/get(...)/form(...)/resources(...)等路由助手的完整用法以及createController(...)router.map(...)的组合方式与注册期校验机制并了解context.params、context.set/get等请求上下文的类型来源。1. route map 作为 URL 契约在 Remix 中路由route与处理请求的 action 是分离定义的。这种分离让服务端代码和浏览器模块可以共享类型化的 URL 助手而不必把控制器拖进浏览器。你在app/routes.ts中声明应用接受哪些 URL 和哪些方法。一个唱片店的小 route map 长这样// app/routes.ts import { route } from remix/routes; export const routes route({ home: /, albums: { show: { method: GET, pattern: /albums/:albumId }, edit: { index: { method: GET, pattern: /albums/:albumId/edit }, action: { method: POST, pattern: /albums/:albumId/edit }, }, }, });route(...)返回一个与参数同构的类型化 route map叶子如routes.albums.show是一条带有 method、pattern 和href(...)助手的路由分支如routes.albums、routes.albums.edit则是另一个 route map。路由器用这些叶子去匹配请求应用的其他部分则用它来构造类型化 URLroutes.albums.show.href({ albumId: thriller }); // /albums/thrillershow路由的href(...)助手要求传albumId属性因为:albumId是路径变量。路径变量在单个分段内匹配并在控制器的context.params上成为同名的类型化属性。如果把 pattern 改成/albums/:id那些仍在传{ albumId: ... }的旧调用会在 TypeScript 层面直接报错。同一个routes对象可以被控制器和浏览器模块共同导入用于链接、表单、重定向和测试——URL 变更不会被藏在散落各处的字符串字面量里。从源码看route(...)就是createRoutes(...)的别名导出自 packages/fetch-router/src/routes.ts实现在 packages/fetch-router/src/lib/route-map.ts它遍历定义对象把字符串、Route实例、RoutePatternAST 或{ method, pattern }对象统一归一为Route类实例method 解析后的 pattern AST分支递归构造为嵌套 map叶子上的href(...)则委托给route-pattern包的createHref(...)完成 URL 构造。这意味着 pattern 在构建期就被解析成 AST运行时匹配不再重复解析源字符串。路径变量只是 Remix 路由 pattern 语法的一部分。pattern 还支持通配符、可选分组、search 约束、转义字面量、hostname 变量、完整 origin 和具体度specificity规则并且可以组合在一条 pattern 里/docs(/v:version)/:category/*slug.:format?preview; // matches /docs/v2/guides/routing/route-maps.html?preview1无需背诵这套 pattern 语法也能继续往下读完整文法和更低层的remix/route-pattern/href、remix/route-pattern/matchAPI 由独立包 packages/route-pattern 提供仓库中 路由模式与 URL 模式的设计决策 和 基于 Trie 的匹配方案 记录了底层选择。上面edit分支展示了另一种常见形态同一 URL 上放两个叶子index处理GET、action处理POSTroutes.albums.edit.index.href({ albumId: thriller }); // GET /albums/thriller/edit routes.albums.edit.action.href({ albumId: thriller }); // POST /albums/thriller/edit叶子与嵌套 map的区分在下一章映射控制器时会再次变得重要。2. 路由助手route、get、post、put、del、form、resources路由助手把{ method, pattern }对象换成用调用命名 HTTP 方法或约定形态的函数它们生成的是与上节完全相同的叶子和 route map。// app/routes.ts import { del, get, post, route } from remix/routes; export const routes route({ home: get(/), albums: { show: get(/albums/:albumId), create: post(/albums), destroy: del(/albums/:albumId), }, });各助手的职责如下助手用途get、post、put、patch、del、head、options收窄到单个 HTTP 方法的一条路由叶子form(pattern)同一 URL 上的GET页面 POSTactionroute(prefix, defs)子 pattern 相对共享前缀的嵌套 route mapresources(pattern)约定式集合collection路由resource(pattern)约定式单例singleton路由路由也可以只用一个字符串定义例如webhook: /webhooks/github会匹配任意请求方法因此它的 action 在方法有意义时必须自行检查context.method。这一点在源码中可以得到印证route-map.ts 的buildRouteMap对字符串定义统一构造为new Route(ANY, ...)即方法标记为ANY。用 route(prefix, defs) 组合独立路由区当一块路由区域需要独立归属时route(prefix, routes)把它的 route map 组合到共享前缀之下该区域拥有自己的名称和相对 pattern应用决定它挂在哪里。// app/actions/albums/routes.ts import { get, route } from remix/routes; export const albumRoutes route({ index: get(/), show: get(/:albumId), });// app/routes.ts import { route } from remix/routes; import { albumRoutes } from ./actions/albums/routes.ts; export const routes route({ albums: route(/albums, albumRoutes), }); // routes.albums.index - GET /albums // routes.albums.show - GET /albums/:albumIdform(pattern)页面与提交同 URLform(...)生成最常见的页面 提交形态页面和它的 action 位于同一 URL。// app/routes.ts import { form, route } from remix/routes; export const routes route({ albums: { edit: form(/albums/:albumId/edit), }, }); // routes.albums.edit.index - GET /albums/:albumId/edit // routes.albums.edit.action - POST /albums/:albumId/edit从实现看packages/fetch-router/src/lib/route-helpers/form.tsform(...)本质上是在给定 pattern 下生成一个包含indexGET /和action默认POST /两个叶子的 route map并支持formMethod默认POST与names自定义两个叶子名称两个选项。后续的表单与变更Forms and Mutations主题会基于这一形态展开校验、重定向与渐进增强。resources(pattern) 与 resource(pattern)约定式 CRUD 路由resources(...)为一个集合生成七条约定式路由index、new、show、create、edit、update、destroyresource(...)为单例生成路由因为没有集合页所以省略index。only可以只保留约定的部分param可以把路径变量名从默认的id改掉// app/routes.ts import { resources, route } from remix/routes; export const routes route({ albums: resources(/albums, { only: [index, show, create], param: albumId, // default is id }), }); // routes.albums.index - GET /albums // routes.albums.show - GET /albums/:albumId // routes.albums.create - POST /albums源码中packages/fetch-router/src/lib/route-helpers/resources.ts这七个方法名由常量ResourcesMethods [index, new, show, create, edit, update, destroy]定义only与exclude互斥同时传两者会在运行期抛出Cannot specify both only and exclude options错误。所有这些助手生成的都是普通的 route map 和叶子因此可以和手写定义任意嵌套把生成的 map 传给createController(...)再像手写 map 一样用router.map(...)注册即可。remix/router的完整路由助手与路由 API 由 packages/fetch-router 包实现各助手的测试位于该包src/lib/route-helpers/与src/lib/route-map.test.ts中可用于核对行为细节。3. 控制器与 action一个控制器controller拥有某一个 route map 中所有直接叶子的请求处理逻辑。它可以运行这些 action 共享的中间件actions对象为每个叶子提供一个处理器。createController(...)利用 route map 为每个 action 名和它的params提供类型同时把请求处理行为留在 route map 之外。对于routes.albums控制器拥有show叶子// app/actions/albums/controller.tsx import { createController } from remix/router; import { routes } from ../../routes.ts; export default createController(routes.albums, { actions: { show(context) { return new Response(Album: ${context.params.albumId}); }, }, });showaction 接收请求上下文并返回 WebResponse。因为该路由定义为get(/albums/:albumId)匹配成功后context.params.albumId就是string。Remix 在 action 运行之前完成 pattern 匹配所以 action 不需要解析 URL也不需要防御albumId缺失。从实现看createController定义在 packages/fetch-router/src/lib/controller.ts它本身不改变传入的控制器对象void routes; return controller而是通过泛型Controllerroutes, context, middleware让 TypeScript 依据 route map 的类型推导每个 action 的context.params。类型侧还做了两件事ControllerActions只允许 key 落在该 map 的真实叶子上嵌套 map 的 key 被映射为never即不允许出现在 actions 中Action类型同时接受纯函数和带middlewarehandler的对象两种形态isAction/isActionObject在运行期做同样的区分。每个 action 都能拿到这些内置请求上下文属性和方法属性或方法提供什么context.request原始的 WebRequestcontext.url解析后的URL对象context.method请求方法context.params从匹配到的路由 pattern 解析出的类型化值context.headers请求头的可变更Headers副本context.router正在处理该请求的路由器context.set(key, value)在共享上下文上存储请求级值context.get(key)读取某个上下文键存储的请求级值context.has(key)某个上下文键是否已存储值这些属性与方法的运行时载体是 packages/fetch-router/src/lib/request-context.ts 中的RequestContext类url在构造时由new URL(request.url)生成headers是首次访问时才从request.headers惰性拷贝的可变副本set/get/has基于内部Mapobject, unknown键必须是对象通常用createContextKey(...)创建的类型安全键set还支持{ property }选项通过Object.defineProperty把值直接安装为context上的只读属性——这正是中间件能扩展出context.formData、context.render这类类型化属性的机制来源。get(...)在值不存在时会回退到上下文键上的defaultValue如果定义了的话。这些属性依赖应用的中间件栈因此不构成一个固定清单。内置的methodOverride()中间件packages/method-override-middleware有意修改一个基础值它在路由匹配之前更新context.method让带_method字段的POST表单能够命中PUT、PATCH或DELETE路由context.request.method仍保留原始方法RequestContext.method的文档注释也明确说明了这一点。控制器还可以拥有自己的中间件作用于它拥有的每个 action。当只有某一条路由需要中间件时把该 action 定义为带middleware和handler的对象。下面只有POSTaction 会解析FormDataindex页面跳过这部分工作// app/actions/albums/edit/controller.tsx import { formData } from remix/middleware/form-data; import { createController } from remix/router; import { routes } from ../../../routes.ts; export default createController(routes.albums.edit, { actions: { index() { return new Response(Edit album); }, action: { middleware: [formData()], handler(context) { let title String(context.formData.get(title) ?? ); return new Response(Updated ${title}, { status: 200 }); }, }, }, });执行顺序是路由器中间件最先运行然后控制器中间件再然后 action 中间件最后是 action handler。这一顺序在源码中得到印证router.ts的mapController在注册每条路由时执行mergeMiddleware(controllerMiddleware, action.middleware)形成每路由的中间件链请求分发时再依次穿过路由器层、该路由层的中间件与 handler。中间件的完整作用域与类型化上下文在后续请求处理Request Handling主题中展开。4. 响应、重定向、响应头与错误Action 返回 WebResponse对象。要渲染页面把remix/middleware/render实现于 packages/render-middleware加入路由器它提供context.render(...)把一个 Remix 组件树转换为 HTML 响应。组件树与渲染器Streaming UI with Frames属于后续主题安装中间件后在 action 中这样使用// app/actions/albums/controller.tsx // inside the show action: return context.render(AlbumPage album{album} /);结果仍然是一个普通的 WebResponse。一个 action 可以渲染页面、返回文本或 JSON、重定向浏览器、发送文件或返回错误响应。预期内的结果——非法输入、冲突、记录不存在——应当返回带相应状态码的Response抛出的错误throw只留给意外故障。当 action 或中间件抛出异常时router.fetch(...)会 reject让服务边界server boundary记录日志并返回500响应该路径的完整机制属于错误与错误边界Errors and Error Boundaries主题。最简单的文本响应return new Response(Album not found, { status: 404 });redirect(...)创建重定向响应实现与测试见 packages/response。编辑 action 在 POST-redirect-GET 流程中常用303 See Other// app/actions/albums/edit/controller.tsx import { redirect } from remix/response/redirect; import { routes } from ../../../routes.ts; // inside an action: return redirect(routes.albums.show.href({ albumId: context.params.albumId }), 303);对于 Remix UI 渲染管线之外的 HTMLhtml模板标签会转义内插值createHtmlResponse(...)负责设置 HTML 内容类型并加上 doctype二者分别实现于 packages/html-template 与 packages/responseimport { html } from remix/html-template; import { createHtmlResponse } from remix/response/html; // inside an action: return createHtmlResponse(htmlp${album.title}/p);Remix 的redirect(...)和createHtmlResponse(...)与new Response(...)、Response.json(...)一样接受标准的ResponseInit以便设置状态和响应头上面的数字303是redirect(...)支持的简写形式。// inside an action: return Response.json(album, { headers: { Cache-Control: no-store, }, });一个常见误区需要澄清context.headers代表的是请求头不是响应头。控制器没有单独的响应头 API因为它的 action 返回的就是标准 Web 响应——直接操作 WebHeaders即可当需要Cache-Control、Set-Cookie这类值的类型化访问器时可以改用 packages/headers 包。5. 映射控制器router.map(...) 与注册期校验createController(...)定义控制器处理什么router.map(...)负责注册它。由于控制器只拥有其 route map 的直接叶子嵌套 map 若拥有自己的 action必须单独注册// app/router.ts import { createRouter } from remix/router; import rootController from ./actions/controller.tsx; import albumsController from ./actions/albums/controller.tsx; import albumsEditController from ./actions/albums/edit/controller.tsx; import { routes } from ./routes.ts; export const router createRouter(); router.map(routes, rootController); router.map(routes.albums, albumsController); router.map(routes.albums.edit, albumsEditController);在上述映射下rootController处理根级叶子如homealbumsController处理routes.albums.showalbumsEditController处理routes.albums.edit.index和routes.albums.edit.action。router.map(...)是应用层处理 route map 与控制器的约定式入口。路由器还提供了不经过控制器、直接注册单个 handler 的方法// app/router.ts // after creating the router: router.get(/health, () new Response(OK));router.get(...)、router.post(...)等动词方法在源码中与map(...)同属一个 route builderpackages/fetch-router/src/lib/router.ts 中的createRouteBuilder底层最终都汇入registerRoute写入共享的 pattern 匹配器。控制器中间件遵循同样的归属规则挂在albumsController上的中间件只对routes.albums.show生效不会对routes.albums.edit.index或routes.albums.edit.action生效——因为后两者由单独router.map注册的控制器拥有。注册期校验保证了这套约定不会悄悄出错。router.ts的mapController在设置阶段就抛出错误应用尚未开始服务请求控制器缺少它拥有的某个叶子的 actionMissing action ${key} in controller控制器中出现该 map 不认识的 actionUnknown action ${key} in controller控制器 actions 里出现了嵌套 route map 的键Cannot map nested route map key ${key} in controller actions; call router.map() for that route map separately——这正是上文嵌套 map 必须单独 map的强制点。6. 组织路由所属的代码文件树通常与 route map 保持同构app/actions/ ├── controller.tsx # routes └── albums/ ├── controller.tsx # routes.albums ├── routes.ts # albumRoutes └── edit/ ├── controller.tsx # routes.albums.edit └── page.tsx # route-local UI当某个路由分支拥有自己的嵌套路由时配一个同名目录和控制器既能保持父控制器精简又能从文件路径直接看出每个路由的归属者。组织原则可以归纳为两条路由局部 UI 放在渲染它的控制器旁边——albums.edit的页面与表单应位于actions/albums/edit/被多个路由区域共享的组件放在ui/下组件模型在渲染 UIRendering UI主题中展开。仓库中的示例应用都遵循了这一布局可直接参考 模板应用的 route 定义 以及demos/bookstore/app/actions/、demos/social-auth/app/actions/下的完整控制器目录结构。route map 与控制器接好线之后后续请求处理Request Handling主题将从服务器入口回看哪些中间件在这些 action 之前和之后运行以及请求上下文如何在整条链路中传递。本文对应的原始文档位于 docs/guides/app/actions/docs/chapters/02-routing-and-controllers.md。【免费下载链接】remixThe fully-stacked web framework项目地址: https://gitcode.com/GitHub_Trending/re/remix创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/10 5:51:32

堆垛机变频器双闭环控制技术:从选型到调试的实战指南

只要你碰过自动化立体仓库,就不可能绕开堆垛机。巷道里那台十几米高、跑起来像小火车一样的大家伙,每一次水平行走、垂直升降、货叉伸缩,背后都是变频器在推着电机干活。很多人觉得堆垛机变频器无非就是个调速器,电机转快转慢而已…

2026/9/10 6:41:38

OpenClaw+IoT:打造真正懂你的全屋智能实战

做全屋智能这行时间长了就会发现,客户对“智能”的期待,和设备厂商对“智能”的定义,完全是两回事。大部分厂商交付的是“能控制的灯”,而客户真正想要的是一个“不用自己动手的家”。这两年我陆续落地了12个全屋智能项目&#xf…

2026/9/10 6:41:38

AI招聘的演进:从单点提效到业务重构的关键路径

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/10 6:41:37

MySQL库操作指南:建库、字符集、权限与备份全解析

1. 库操作到底在操作什么刚接触MySQL的时候,很多人第一件事就是装环境、配变量、打开命令行敲几句SQL。折腾完mysql -uroot -p能进去了,接下来自然就会问一句:然后呢?然后就是建库、建表、塞数据。这里面的“建库”就是我今天想聊…

2026/9/10 6:41:37

SSM电商实战:从分层架构到订单事务与库存扣减

简介:SSM项目鲜花销售管理系统.zip 是一套基于 SpringSpringMVCMyBatis 框架的完整 Java Web 实战项目,适合正在学习 SSM 整合、MySQL 数据库及前端 LayUI 的中高级开发者。资源共收录 637 个文件,压缩后约 22.68MB,包含 105 个 J…

2026/9/10 6:36:37

本地大模型推理CLI工具真相:llama.cpp、Ollama与LMDEPLOY实操指南

1. “magnitude”不是命令行工具,而是被误传的模型推理服务代号最近在多个技术社区和开发者群聊里,频繁看到有人搜索“magnitude CLI”“magnitude inference server”“magnitude local models”,甚至把“magnitude”和“codex cli”“claud…

2026/9/9 13:11:35

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/8 7:15:15

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/7 16:23:03

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/7 22:46:00

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/9 10:21:54

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码