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('entity.list', { entity: 'User' })
// 位置(差) 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 |
|---|---|
| 命名路由导出 | ✅ |
| 类型生成 | ✅ |
| 分级懒加载 | ✅ |
| 并发控制 | ✅ |
| 请求封装 | ✅ |
| 拦截器(登录态/Token 由用户控制) | ✅ |
| 缓存隔离 | ✅ |
| 请求流程优化(校验前置 + 错误恢复) | ✅ |
| 摘要端点(自动发现) | ✅ |
| 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 HTTP 客户端适配策略
早期方案考虑过用 ofetch 作为默认 HTTP 客户端(API 现代、类型好、体积小),但实际落地时发现三个问题:
- 存量项目已有 axios:强制引入第二个 HTTP 库会造成包体积浪费和拦截器配置分裂;
- 零依赖新项目:不想装任何 HTTP 库的轻量项目,
ofetch仍然是一个外部依赖; - 统一拦截器行为:不同 adapter 的拦截器语义不一致,用户切换 adapter 时行为可能变化。
最终方案:
- 默认
'auto':检测到宿主有 axios 就复用(继承其拦截器/默认配置),没有则降级到内置@route-forge/builtin-http(基于原生fetch,零外部依赖); - 内置实现刻意对齐 axios 拦截器 API 与执行顺序(请求 LIFO、响应 FIFO),确保两套 adapter 行为一致;
- 保留自定义 Fetcher 接口,极端场景可完全替换。
6.3 为什么拆成两个仓库
早期采用 monorepo(pnpm workspace + turborepo)统一管理 Laravel 包、TS core、Vue 插件三个包。随着项目演进,决定拆分为两个独立仓库:
- route-forge/route-forge(本仓库):npm 侧,包含
@route-forge/core与@route-forge/vue; - route-forge/route-forge-laravel:Composer
侧,包含
route-forge/laravel。
拆分理由:
- Packagist 原生流程:独立仓库根目录直接放
composer.json,tag 即版本,无需 stub 或前缀绕行; - 版本独立:PHP 与 npm 两侧可按各自节奏发版,互不干扰;
- CI 隔离:PHP 测试与 JS 构建彻底分离,workflow 逻辑更清晰;
- 契约清晰:两侧通过 HTTP manifest 契约交互,拆分后契约边界更显式。
拆分后的协作纪律:
- manifest 格式(字段、tier 语义、端点路径)是跨仓契约,变更时两边 PR 同步;
- 端点响应带
schemeVersion字段(拼写为 scheme 非 schema),便于前端做兼容判断; - 两个仓库的 README 互相链接,
.docs/目录保留在本仓库作为全链路设计文档。
6.4 为什么 level 不固定
- 不同项目的权限结构差异极大(admin/user/tenant/v1/v2...);
- 固定三级会强迫用户适配包的思维;
- 应该让 level 体系完全可定制,从零配置到深度自定义都支持。
6.5 Blade 注入的取舍:从「统一摘要端点」到「可选首页加速」
早期(见旧版本文档)曾考虑并否决了 Blade 注入:担心 ① Vue/SPA 独立部署 HTML 不经 Laravel、② Vite dev 无 Blade 渲染、③ 多一套机制增加条件判断。这三条至今成立——所以 Blade 注入不能成为唯一路径。
后来把它重新引入,但定位变了:不是"替换摘要端点",而是投递同一份 SummaryResponse 的可选加速通道。
- 唯一 producer 不变:内嵌的值直接来自后端
RouteRepository::getSummary()(@forgeSummary指令渲染),与摘要端点逐字段一致,杜绝契约漂移。 - 解决的痛点:省掉首屏那一次摘要 HTTP 往返,并让 auto-discovery 同步完成——
createRouteForge()返回后route()/ready()立即可用,消除首屏"路由未就绪"闪烁。 - 不影响其它场景:Laravel 未书写
@forgeSummary的页面(SPA 独立部署 / Vite dev)里没有该全局,前端自动回落网络摘要,行为与过去完全一致。 - 懒加载与受保护价值保留:只嵌摘要(层级概览),各层级路由明细仍按
levels[].route.uri走 HTTP 懒加载——受保护路由的明细不预置进公开 HTML,这正是当初否决"全量内嵌"的理由,方案 A 刻意回避。 - 消费契约:后端用
Object.defineProperty(window, '__ROUTE_FORGE__', { get, enumerable:false, configurable:true })注入一次性访问器,前端读一次即触发delete(运行时不在window残留);core 侧 module 级 memo 兜住同页多实例。诚实边界:一次性自删只缩小运行时window驻留面,数据仍随 HTML 源码可见,不是抗 XSS / 抗网络窃取的硬边界。
最终口径:摘要端点仍是配置权威与默认来源;Blade 内嵌是其上叠加的可选首页加速(级联:内嵌 > createRouteForge({summary}) > 网络),见 SPEC §3.1.6 / §3.1.8 / §4.1.1。
6.6 为什么类型由后端 Artisan 命令生成
早期考虑过由前端 CLI(请求摘要端点)生成 TS 类型,最终改为后端 route:forge:types Artisan 命令,理由:
- 路由定义是后端数据:类型必须从唯一真相源(
Route::getRoutes())生成,手写或旁路生成都会与路由表脱节; - 离线可用:直接从内存路由注册表读取,不需要启动 HTTP 服务;
- CI 友好:PHP 构建阶段无需 Node 环境即可产出类型文件。
7. 风险与权衡
| 风险 | 缓解策略 |
|---|---|
| Laravel 大版本升级导致 API 变动 | 抽象一层适配器;CI 跑 Laravel 9/10/11 三矩阵 |
| 开源项目维护疲劳 | 设定清晰的 Roadmap;拒绝超出定位的 feature request |
| TS 类型推导在某些边界情况下失败 | 提供 escape hatch(api<any>(...)),但不鼓励 |
fallback_level=null 暴露未标记路由 |
文档建议生产环境开启 strict_mode=true 或显式标记所有路由 |
levels 数组顺序影响多命中结果 |
文档明确"后定义覆盖前定义"语义;测试矩阵覆盖多命中场景 |
8. 长期愿景
- 成为 Laravel + SPA 项目的"命名路由事实标准";
- 衍生产品:可视化路由管理面板、API 文档自动生成、权限粒度到 role 级;
- 教育价值:通过这个项目传播"类型安全"、"关注点分离"、"约定优于配置"的工程理念。
9. 个人投入预期
- Phase 1(Laravel MVP:路由分级 + 端点 + Artisan 命令含类型生成):2-3 周
- Phase 2(前端包 + Vue 集成):2 周
- Phase 3(文档 + 示例 + 发布):1 周
- Phase 4(生态:React 版、Vite 插件):持续
- 长期维护:每周约 4 小时