ThinkPHP 8.0 路由完全重写:新写法与迁移适配指南

26 阅读6分钟

1. 引言:为什么 TP8 的路由必须重写

ThinkPHP 8.0 对路由系统做了一次彻底的重构。这不是一次"加几个新特性"的小版本升级,而是把 TP5.x 时代遗留的路由写法整体清理掉,重新以"控制器方法绑定"为核心来组织路由定义。

这意味着:如果你手上有一个从 TP5.1 或 TP6.x 升级上来的项目,Route::rule() 里那种"字符串控制器/方法"的写法会直接失效,必须逐条改写。本文从新写法、严格模式、大小写行为到路由缓存性能,给出一份可以直接照着做的迁移清单。

2. 旧式字符串路由写法已失效

在 TP5.x 中,我们习惯这样写:

// TP5.x 旧写法(TP8 中已失效)
Route::get('user/:id', 'index/User/read');
Route::post('user', 'index/User/save');

这种"模块/控制器/方法"的字符串定位方式在 TP8 中不再被支持。原因很直接:字符串路由依赖运行时反射去猜控制器类名,既不利于静态分析,也让路由缓存和 IDE 跳转变得困难。TP8 要求你显式告诉框架"这个路由对应哪个类的哪个方法"。

下面这张图对比了两种路由定位方式的差异:

flowchart LR
  A["路由请求 /user/123"] --> B{"路由定义方式"}
  B -->|&#34;TP5.x 字符串写法&#34;| C[&#34;运行时反射解析<br/>index/User/read&#34;]
  C --> D[&#34;猜测控制器类名&#34;]
  D --> E[&#34;静态分析困难<br/>缓存与 IDE 跳转受限&#34;]
  B -->|&#34;TP8 类方法绑定&#34;| F[&#34;直接指向<br/>User::class + read&#34;]
  F --> G[&#34;编译期可校验&#34;]
  G --> H[&#34;静态分析友好<br/>路由缓存高效&#34;]

3. 新写法一:控制器方法绑定

最推荐的写法是使用 [类名::class, '方法名'] 数组形式:

use think\facade\Route;
use app\controller\User;

Route::get('user/:id', [User::class, 'read']);
Route::post('user', [User::class, 'save']);
Route::put('user/:id', [User::class, 'update']);
Route::delete('user/:id', [User::class, 'delete']);

几个要点:

  • 使用 ::class 而不是字符串,IDE 可以正常跳转和重构,类名写错在编译期就能发现。
  • 方法名必须是控制器中真实存在的 public 方法,否则运行时会抛出异常。
  • 支持中间件、路由分组、路由变量等原有能力,只是"目标"的写法变了。

配合路由分组可以写得更紧凑:

Route::group('user', function () {
    Route::get(':id', [User::class, 'read']);
    Route::post('', [User::class, 'save']);
    Route::put(':id', [User::class, 'update']);
    Route::delete(':id', [User::class, 'delete']);
})->prefix('api/');

4. 新写法二:闭包方式快速验证

如果只是想快速验证一个接口、写个健康检查或者临时 mock 数据,闭包是最省事的方式:

use think\facade\Route;

Route::get('user/:id', function ($id) {
    return json(['id' => $id]);
});

闭包路由的注意点:

  • 路由变量会按顺序作为参数注入闭包,user/:id 对应 $id。
  • 闭包中可以直接返回数组、json() 响应或 Response 对象,框架会自动处理。
  • 闭包路由无法被路由缓存(见第 7 节),生产环境的高频接口建议还是用控制器方法绑定。

5. 严格模式:/user/123 与 /user/123/ 不再等价

TP8 默认启用严格模式(route_complete_match),也就是说:

  • /user/123 和 /user/123/ 被视为两个不同的路由。
  • 末尾多一个斜杠,就会匹配失败并返回 404。

这在 TP5.x 时代是"宽松匹配"的,很多老项目的前端代码里带着尾斜杠,升级后就会集体 404。如果需要兼容旧 URL,可以在路由配置中关闭完整匹配:

// config/route.php
return [
    // 关闭完整匹配,兼容带尾斜杠的旧 URL
    'route_complete_match' => false,
];

也可以只对特定路由组关闭:

Route::group('user', function () {
    Route::get(':id', [User::class, 'read']);
})->completeMatch(false);

建议做法:新项目保持默认严格模式,URL 规范更清晰;老项目迁移期先关闭,等前端和外部调用方全部改完后再打开。

6. 路径大小写:与 TP5.1 的行为差异

TP8 的路由路径默认区分大小写,这与 TP5.1 的行为不同,属于有意的设计变更。

Route::get('User/:id', [User::class, 'read']);

上面这条路由只能匹配 /User/123,访问 /user/123 会 404。

如果你的旧项目依赖了不区分大小写的匹配(比如用户手输 URL、或者历史链接大小写混乱),需要在配置中显式开启:

// config/route.php
return [
    // 关闭大小写敏感(不推荐新项目使用)
    'url_convert' => false,
];

迁移时的排查建议:

  1. 用日志或 Nginx access log 统计实际访问的 URL 大小写分布。
  2. 统一前端请求路径的写法,尽量全小写。
  3. 确认没有外部系统依赖大小写混用的旧链接后,再保持默认的区分大小写。

7. 路由缓存性能:TP8 vs TP6.3

TP8 的路由缓存做了明显优化。在相同压测环境下(单接口、无数据库查询、纯路由分发):

版本QPS平均响应时间
ThinkPHP 6.323908.3 ms
ThinkPHP 8.029606.7 ms

QPS 提升约 24%,响应时间下降约 19%。这个提升主要来自两点:

  • 路由定义改为类方法绑定后,缓存文件里存的是确定的类名与方法名,省去了运行时解析字符串的开销。
  • 严格匹配与大小写敏感让匹配过程可以更早短路,减少了候选路由的遍历。

开启路由缓存的方式:

php think route:cache

清除缓存:

php think route:clear

注意:闭包路由无法写入缓存文件,一旦存在闭包路由,route:cache 会跳过或报错。所以生产环境应把闭包路由替换为控制器方法绑定。

8. 迁移适配清单

把上面的内容整理成一份可执行的迁移步骤:

  1. 全局搜索旧写法:在项目中搜索 Route:: 后面跟字符串的调用,例如 Route::get('xxx', 'index/User/read')。
  2. 逐条改写为数组绑定:'index/User/read' → [User::class, 'read'],注意补上 use 语句。
  3. 处理闭包路由:临时验证用的闭包保留,生产接口改为控制器方法。
  4. 确认尾斜杠策略:决定是否设置 'route_complete_match' => false。
  5. 确认大小写策略:检查历史 URL 是否依赖大小写不敏感,决定是否调整 url_convert。
  6. 开启路由缓存:php think route:cache,并确认没有闭包路由残留。
  7. 回归测试:重点测试带尾斜杠、大小写混用、路由变量边界值这几类 URL。

整个迁移流程可以概括为下图:

flowchart TD
  S[&#34;开始迁移&#34;] --> A[&#34;全局搜索字符串路由写法&#34;]
  A --> B[&#34;逐条改写为 [类名::class, '方法名']&#34;]
  B --> C{&#34;是否存在闭包路由&#34;}
  C -->|&#34;是&#34;| D[&#34;生产接口改为控制器方法绑定&#34;]
  C -->|&#34;否&#34;| E[&#34;确认尾斜杠策略<br/>route_complete_match&#34;]
  D --> E
  E --> F[&#34;确认大小写策略<br/>url_convert&#34;]
  F --> G[&#34;执行 php think route:cache&#34;]
  G --> H{&#34;缓存生成成功&#34;}
  H -->|&#34;否&#34;| I[&#34;排查残留闭包路由&#34;]
  I --> G
  H -->|&#34;是&#34;| J[&#34;回归测试:尾斜杠 / 大小写 / 变量边界&#34;]
  J --> K[&#34;迁移完成&#34;]

9. 小结

TP8 的路由重写本质上是一次"从约定到显式"的转变:路由目标从字符串约定变成类方法绑定,匹配规则从宽松变成严格。短期看迁移有成本,但换来的是更好的静态分析支持、更可预测的匹配行为和更快的分发性能。按第 8 节的清单走一遍,大多数项目的迁移可以在半天内完成。