Route Forge — 设计思路
作者原始思路记录,用于项目演进追溯与决策参考。
1. 缘起
多年 Laravel + Vue 项目实战中,发现一个反复出现的痛点:
- 后端有完整的命名路由体系(
route('user.show', $id)),但前端调用 API 时仍要手写 URL; - 一次性把全部路由导出到前端,对大型 SPA 来说首屏体积过大、敏感路由也容易泄露;
- 路由定义散落在前端代码里(
axios.post('/app/user/list')),URL 一旦变动要全局搜索替换。
在 Laravel 项目里,我用过一套自创的方案: 前端按“分级”懒加载后端路由定义。后端注入路由元信息(名称 + 路径 + 方法),前端按需拉取并缓存。效果非常好——调用方只需要知道层级名和路由名,无需关心 URL 和方法。
2. 关键洞察
2.1 命名路由的本质是"意图而非位置"
调用 API 应该表达"我要做什么",而不是"我要去哪里"。
// 意图(好) api('admin', 'users.show', { user: 1 })
// 位置(差) axios.post('/app/User/data-list', query)
2.2 分级懒加载是大型 SPA 的刚需
把所有路由一次性导出,对于有数百个接口的后台系统:
- 首屏要多加载 30-100KB 的路由表 JSON;
- 大多数路由用户在当前会话根本不会用到;
- 敏感接口信息(如管理端路由)不应对普通用户暴露。
按"级别"(level)分组、按需加载、按 level 隔离缓存,是更合理的方案。
2.3 方法应该和路由绑定
Laravel 路由定义里天然包含 HTTP 方法(GET/POST/PUT/DELETE),没理由让前端在调用时再指定一遍。
2.4 类型安全应贯穿全链路
路由名 → 路径参数 → 响应类型,整条链路都该有 TS 类型保护。任何一个环节 any 掉都会让系统变得脆弱。
3. 现有方案的不足
命名路由一次性导出方案
- ✅ 做得好的:命名路由导出、Vue 指令、类型生成;
- ❌ 缺的:
- 没有分级懒加载(全量导出);
- 没有并发控制(首屏并发请求可能雪崩);
- 没有请求封装(只生成 URL,还要手写 axios 调用);
- 没有登录态感知(无法决定何时加载)。
其他方案
- 自写 axios baseURL + 常量路由:每个项目重复造轮子,且无类型安全;
- OpenAPI/Swagger 生成:需要后端配合,对已有项目侵入性大。
4. Route Forge 的核心定位
"给 Laravel + 前端 SPA 项目提供一套完整、可分级、类型安全的命名路由解决方案。"
补齐现有命名路由方案缺失的能力:
| 维度 | Route Forge |
|---|---|
| 命名路由导出 | ✅ |
| 类型生成 | ✅ |
| 分级懒加载 | ✅ |
| 并发控制 | ✅ |
| 请求封装 | ✅ |
| 登录态感知 | ✅ |
| 缓存隔离 | ✅ |
| 请求流程优化(校验前置 + 错误恢复) | ✅ |
| 摘要端点(自动发现) | ✅ |
| middleware_match(DNF) | ✅ |
| 配置分级覆盖(后端权威) | ✅ |
| 自定义分类 | 完全可定制 |
5. 设计原则
- 约定优于配置:零配置也能跑,但所有约定都可通过配置覆盖。
- 默认宽松,按需严格:
strict_mode默认关闭,降低接入门槛;需要精确校验的项目显式开启,前后端strict_mode以后端为权威值。 - 类型安全全链路:路由名 → 参数 → 响应全程可推导。
- 零侵入:只通过 Laravel 的扩展点(macro、ServiceProvider)工作,不修改框架核心。
- 渐进式复杂度:简单项目用默认值;复杂项目才需要写配置。
- 可独立测试:每个组件都能脱离完整应用单独测试。
6. 关键技术决策
6.1 为什么不用 OpenAPI
- OpenAPI 需要后端主动写注解,对存量项目侵入大;
- Route Forge 直接从 Laravel 的
Route::getRoutes()读取,零注解; - 未来可作为可选能力(OpenAPI → Route Forge)桥接。
6.2 为什么 level 不固定
- 不同项目的权限结构差异极大(admin/user/tenant/v1/v2...);
- 固定三级会强迫用户适配包的思维;
- 应该让 level 体系完全可定制,从零配置到深度自定义都支持。
6.3 为什么取消 Blade 注入,统一走摘要端点
早期考虑过在 Blade 模板中通过 @forgeConfig 指令注入后端配置到 HTML 页面,前端初始化时直接读取。但发现:
- Vue 独立部署场景不支持:前后端分离项目中,HTML 不经过 Laravel 渲染,注入不存在;
- Vite dev 场景不支持:开发时前端走 Vite dev server,同样没有 Blade 渲染;
- 单一机制更简单:统一通过
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 命令,理由:
- 路由定义是后端数据:类型必须从唯一真相源(
Route::getRoutes())生成,手写或旁路生成都会与路由表脱节; - 离线可用:直接从内存路由注册表读取,不需要启动 HTTP 服务;
- CI 友好:PHP 构建阶段无需 Node 环境即可产出类型文件。
生成的类型采用二级映射结构(层级 → 路由名 → 类型约束),前端调用时必须同时传入层级名和路由名(
forge.api(level, name, params))。层级名是加载指定层级路由的前提——不传层级名,按层级懒加载就无从谈起。
6.5 端点中间件保护:按层级独立注册路由
元信息端点(/_forge/routes/{level})的中间件保护采用方案 B:按层级注册独立路由,每个层级直接挂载对应的 endpoint_middleware。
选择方案 B 而非动态中间件的原因:
- 实现直接:利用 Laravel 原生路由中间件机制,无额外调度层;
- 调试友好:
route:list输出中每个层级端点清晰可见其挂载的中间件; - 层级数量少:通常 3~5 个层级,路由数量不是问题;
- 性能无额外开销:不需要运行时解析中间件管道。
摘要端点(/_forge/routes)通过顶层 endpoint_middleware 配置独立保护,与层级端点中间件互不影响。
6.6 为什么别名放在元信息层而非前端 SDK 层
路由改名迭代时,需要让前端继续使用旧路由名。三种落点对比:
- 元信息层注入别名条目(最终选择):
->forgeAlias()宏 + configaliases双通道声明,RouteRepository扫描时把别名作为额外键注入目标路由所在层级的routes对象,元信息与目标路由纯复制一致。前端拿到的routes里新旧两个名字都在——对前端而言别名就是一个真实存在的路由名,校验与类型推断自然通过,前端三包(core/vue/react)零改动。别名只存在于元信息层,旧 URI 本身不可访问,不扩大安全面。 - 前端 SDK 解析别名映射:摘要端点下发映射、SDK 调用时解析。缺点:三个前端包都要改,且前端不升级 SDK 就完全无效——与"前端不改就能用"的目标矛盾。排除。
- 注册双路由(新旧 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. 长期愿景
- 成为 Laravel + SPA 项目的“命名路由事实标准”;
- 衍生产品:API 文档自动生成、权限粒度到 role 级;
- ✅ 可视化路由管理面板:已实现(v1.1,开发环境专属);
- 教育价值:通过这个项目传播“类型安全”、“关注点分离”、“约定优于配置”的工程理念。
9. 个人投入预期
- Phase 1(Laravel MVP:路由分级 + 端点 + Artisan 命令含类型生成):2-3 周 ✅ 已完成
- Phase 1.1(管理器页面:可视化路由管理面板):✅ 已完成
- Phase 2(文档 + 示例 + 发布):1 周
- 长期维护:每周约 4 小时