Route Forge — 设计思路

作者原始思路记录,用于项目演进追溯与决策参考。

1. 缘起

多年 Laravel + Vue 项目实战中,发现一个反复出现的痛点:

在 Laravel 项目里,我用过一套自创的方案: 前端按“分级”懒加载后端路由定义。后端注入路由元信息(名称 + 路径 + 方法),前端按需拉取并缓存。效果非常好——调用方只需要知道层级名和路由名,无需关心 URL 和方法。

2. 关键洞察

2.1 命名路由的本质是"意图而非位置"

调用 API 应该表达"我要做什么",而不是"我要去哪里"。

// 意图(好) api('admin', 'users.show', { user: 1 })
// 位置(差) axios.post('/app/User/data-list', query)

2.2 分级懒加载是大型 SPA 的刚需

把所有路由一次性导出,对于有数百个接口的后台系统:

按"级别"(level)分组、按需加载、按 level 隔离缓存,是更合理的方案。

2.3 方法应该和路由绑定

Laravel 路由定义里天然包含 HTTP 方法(GET/POST/PUT/DELETE),没理由让前端在调用时再指定一遍。

2.4 类型安全应贯穿全链路

路由名 → 路径参数 → 响应类型,整条链路都该有 TS 类型保护。任何一个环节 any 掉都会让系统变得脆弱。

3. 现有方案的不足

命名路由一次性导出方案

其他方案

4. Route Forge 的核心定位

"给 Laravel + 前端 SPA 项目提供一套完整、可分级、类型安全的命名路由解决方案。"

补齐现有命名路由方案缺失的能力:

维度 Route Forge
命名路由导出
类型生成
分级懒加载
并发控制
请求封装
登录态感知
缓存隔离
请求流程优化(校验前置 + 错误恢复)
摘要端点(自动发现)
middleware_match(DNF)
配置分级覆盖(后端权威)
自定义分类 完全可定制

5. 设计原则

  1. 约定优于配置:零配置也能跑,但所有约定都可通过配置覆盖。
  2. 默认宽松,按需严格strict_mode 默认关闭,降低接入门槛;需要精确校验的项目显式开启,前后端 strict_mode 以后端为权威值。
  3. 类型安全全链路:路由名 → 参数 → 响应全程可推导。
  4. 零侵入:只通过 Laravel 的扩展点(macro、ServiceProvider)工作,不修改框架核心。
  5. 渐进式复杂度:简单项目用默认值;复杂项目才需要写配置。
  6. 可独立测试:每个组件都能脱离完整应用单独测试。

6. 关键技术决策

6.1 为什么不用 OpenAPI

6.2 为什么 level 不固定

6.3 为什么取消 Blade 注入,统一走摘要端点

早期考虑过在 Blade 模板中通过 @forgeConfig 指令注入后端配置到 HTML 页面,前端初始化时直接读取。但发现:

  1. Vue 独立部署场景不支持:前后端分离项目中,HTML 不经过 Laravel 渲染,注入不存在;
  2. Vite dev 场景不支持:开发时前端走 Vite dev server,同样没有 Blade 渲染;
  3. 单一机制更简单:统一通过 GET /_forge/routes 摘要端点获取配置,两种部署场景行为一致,无需条件判断。

最终决策:取消 Blade 注入,所有后端配置下发均通过摘要端点,前端在 createRouteForge() 初始化时按需请求。

后续演进(@forgeSummary 可选首页内嵌):上述"取消"针对的是把 Blade 注入当作唯一配置下发通道——那会让 SPA 独立部署与 Vite dev 两类无 Blade 渲染的场景失效。如今重新引入的是一个叠加在摘要端点之上的可选加速:仅对「由 Laravel 服务端渲染 HTML、JS 只在浏览器执行」的首页,用 @forgeSummary 把摘要内嵌进 <head>,让前端跳过首屏的一次摘要 HTTP 往返。摘要端点与层级端点原样保留、仍是默认与回落路径——未书写指令的页面(含 SPA / Vite dev)行为完全不变,不注入、走网络摘要。层级路由明细始终按 level 走 HTTP 懒加载,绝不内嵌(受保护路由不得预置进公开 HTML)。契约与实现见 SPEC §3.1.8。

6.4 为什么类型由后端 Artisan 命令生成

早期考虑过由前端 CLI(请求摘要端点)生成 TS 类型,最终改为后端 route:forge:types Artisan 命令,理由:

  1. 路由定义是后端数据:类型必须从唯一真相源(Route::getRoutes())生成,手写或旁路生成都会与路由表脱节;
  2. 离线可用:直接从内存路由注册表读取,不需要启动 HTTP 服务;
  3. CI 友好:PHP 构建阶段无需 Node 环境即可产出类型文件。

生成的类型采用二级映射结构(层级 → 路由名 → 类型约束),前端调用时必须同时传入层级名和路由名( forge.api(level, name, params))。层级名是加载指定层级路由的前提——不传层级名,按层级懒加载就无从谈起。

6.5 端点中间件保护:按层级独立注册路由

元信息端点(/_forge/routes/{level})的中间件保护采用方案 B:按层级注册独立路由,每个层级直接挂载对应的 endpoint_middleware

选择方案 B 而非动态中间件的原因:

  1. 实现直接:利用 Laravel 原生路由中间件机制,无额外调度层;
  2. 调试友好route:list 输出中每个层级端点清晰可见其挂载的中间件;
  3. 层级数量少:通常 3~5 个层级,路由数量不是问题;
  4. 性能无额外开销:不需要运行时解析中间件管道。

摘要端点(/_forge/routes)通过顶层 endpoint_middleware 配置独立保护,与层级端点中间件互不影响。

6.6 为什么别名放在元信息层而非前端 SDK 层

路由改名迭代时,需要让前端继续使用旧路由名。三种落点对比:

  1. 元信息层注入别名条目(最终选择)->forgeAlias() 宏 + config aliases 双通道声明,RouteRepository 扫描时把别名作为额外键注入目标路由所在层级的 routes 对象,元信息与目标路由纯复制一致。前端拿到的 routes 里新旧两个名字都在——对前端而言别名就是一个真实存在的路由名,校验与类型推断自然通过,前端三包(core/vue/react)零改动。别名只存在于元信息层,旧 URI 本身不可访问,不扩大安全面。
  2. 前端 SDK 解析别名映射:摘要端点下发映射、SDK 调用时解析。缺点:三个前端包都要改,且前端不升级 SDK 就完全无效——与"前端不改就能用"的目标矛盾。排除。
  3. 注册双路由(新旧 URI 同时可访问):改变运行时行为(旧地址仍可打),安全面扩大且语义混乱。排除。

关键洞察:"散落声明难维护"是工具问题而非声明位置问题。因此双通道并存——宏 ->forgeAlias() 就近声明(改名的上下文贴切、生命周期贴着路由),config aliases 集中声明(批量迁移与长期稳定对外名);优先级沿用 §6.5/tier 的"显式 > 配置"哲学。可审计性由既有工具链承接:route:forge:list--aliases 过滤 + warnings)与管理器页面别名标注,让所有生效别名在运行时集中可见,清理时 --aliases + grep 即可。

失败语义沿用包的 fail-fast 传统:悬空别名(目标路由不存在)抛 AliasTargetException(RF_BE_008)——路由删除后忘记同步别名映射应立即暴露,而不是让前端拿着永远 404 的"可用路由名"。撞车(别名与真实路由名重名)则真实路由优先并警告,因为真实路由是更强势的事实。别名既可作改名迁移的过渡(用完清理),也可作易变路由的长期稳定对外名(永不过期,改名只更新映射目标),schemeVersion 不递增(routes 多出条目对既有前端无破坏,向后兼容增量)。

7. 风险与权衡

风险 缓解策略
Laravel 大版本升级导致 API 变动 覆盖 Router/Registrar 扩展点做适配;CI 跑 PHP 8.2–8.5 × Laravel 11/12/13 矩阵
开源项目维护疲劳 设定清晰的 Roadmap;拒绝超出定位的 feature request
TS 类型推导在某些边界情况下失败 提供 escape hatch(api<any>(...)),但不鼓励
unassigned 兜底暴露未标记路由 文档建议生产环境开启 strict_mode=true 或显式标记所有路由
levels 数组顺序影响多命中结果 文档明确"后定义覆盖前定义"语义;测试矩阵覆盖多命中场景

设计变更记录:早期版本曾提供 fallback_level 配置(未命中路由兜底到指定层级), 因与 unassigned 特殊层级语义重复且增加心智负担,已移除。未命中层级路由统一归入 unassigned,经 GET /{endpoint_prefix}/unassigned 获取。

8. 长期愿景

9. 个人投入预期