1. 引言:为什么 TP8 的路由必须重写
ThinkPHP 8.0 对路由系统做了一次彻底的重构。这不是一次"加几个新特性"的小版本升级,而是把 TP5.x 时代遗留的路由写法整体清理掉,重新以"控制器方法绑定"为核心来组织路由定义。
这意味着:如果你手上有一个从 TP5.1 或 TP6.x 升级上来的项目,Route::rule() 里那种"字符串控制器/方法"的写法会直接失效,必须逐条改写。本文从新写法、严格模式、大小写行为到路由缓存性能,给出一份可以直接照着做的迁移清单。
2. 旧式字符串路由写法已失效
在 TP5.x 中,我们习惯这样写:
php
// TP5.x 旧写法(TP8 中已失效)
Route::get('user/:id', 'index/User/read');
Route::post('user', 'index/User/save');
这种"模块/控制器/方法"的字符串定位方式在 TP8 中不再被支持。原因很直接:字符串路由依赖运行时反射去猜控制器类名,既不利于静态分析,也让路由缓存和 IDE 跳转变得困难。TP8 要求你显式告诉框架"这个路由对应哪个类的哪个方法"。
下面这张图对比了两种路由定位方式的差异:
3. 新写法一:控制器方法绑定
最推荐的写法是使用 [类名::class, '方法名'] 数组形式:
php
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 方法,否则运行时会抛出异常。
- 支持中间件、路由分组、路由变量等原有能力,只是"目标"的写法变了。
配合路由分组可以写得更紧凑:
php
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 数据,闭包是最省事的方式:
php
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,可以在路由配置中关闭完整匹配:
php
// config/route.php
return [
// 关闭完整匹配,兼容带尾斜杠的旧 URL
'route_complete_match' => false,
];
也可以只对特定路由组关闭:
php
Route::group('user', function () {
Route::get(':id', [User::class, 'read']);
})->completeMatch(false);
建议做法:新项目保持默认严格模式,URL 规范更清晰;老项目迁移期先关闭,等前端和外部调用方全部改完后再打开。
6. 路径大小写:与 TP5.1 的行为差异
TP8 的路由路径默认区分大小写,这与 TP5.1 的行为不同,属于有意的设计变更。
php
Route::get('User/:id', [User::class, 'read']);
上面这条路由只能匹配 /User/123,访问 /user/123 会 404。
如果你的旧项目依赖了不区分大小写的匹配(比如用户手输 URL、或者历史链接大小写混乱),需要在配置中显式开启:
php
// config/route.php
return [
// 关闭大小写敏感(不推荐新项目使用)
'url_convert' => false,
];
迁移时的排查建议:
- 用日志或 Nginx access log 统计实际访问的 URL 大小写分布。
- 统一前端请求路径的写法,尽量全小写。
- 确认没有外部系统依赖大小写混用的旧链接后,再保持默认的区分大小写。
7. 路由缓存性能:TP8 vs TP6.3
TP8 的路由缓存做了明显优化。在相同压测环境下(单接口、无数据库查询、纯路由分发):
| 版本 | QPS | 平均响应时间 |
|---|---|---|
| ThinkPHP 6.3 | 2390 | 8.3 ms |
| ThinkPHP 8.0 | 2960 | 6.7 ms |
QPS 提升约 24%,响应时间下降约 19%。这个提升主要来自两点:
- 路由定义改为类方法绑定后,缓存文件里存的是确定的类名与方法名,省去了运行时解析字符串的开销。
- 严格匹配与大小写敏感让匹配过程可以更早短路,减少了候选路由的遍历。
开启路由缓存的方式:
bash
php think route:cache
清除缓存:
bash
php think route:clear
注意:闭包路由无法写入缓存文件,一旦存在闭包路由,route:cache 会跳过或报错。所以生产环境应把闭包路由替换为控制器方法绑定。
8. 迁移适配清单
把上面的内容整理成一份可执行的迁移步骤:
- 全局搜索旧写法 :在项目中搜索
Route::后面跟字符串的调用,例如Route::get('xxx', 'index/User/read')。 - 逐条改写为数组绑定 :
'index/User/read'→[User::class, 'read'],注意补上use语句。 - 处理闭包路由:临时验证用的闭包保留,生产接口改为控制器方法。
- 确认尾斜杠策略 :决定是否设置
'route_complete_match' => false。 - 确认大小写策略 :检查历史 URL 是否依赖大小写不敏感,决定是否调整
url_convert。 - 开启路由缓存 :
php think route:cache,并确认没有闭包路由残留。 - 回归测试:重点测试带尾斜杠、大小写混用、路由变量边界值这几类 URL。
整个迁移流程可以概括为下图:
9. 小结
TP8 的路由重写本质上是一次"从约定到显式"的转变:路由目标从字符串约定变成类方法绑定,匹配规则从宽松变成严格。短期看迁移有成本,但换来的是更好的静态分析支持、更可预测的匹配行为和更快的分发性能。按第 8 节的清单走一遍,大多数项目的迁移可以在半天内完成。