1. 路由基础:从零开始配置你的第一个路由
如果你刚开始接触ThinkPHP6,可能会觉得路由这个概念有点抽象。别担心,我刚开始用的时候也迷糊过。简单来说,路由就像是你们小区的快递驿站。用户(访客)在浏览器地址栏输入一个网址(比如 yourdomain.com/user/profile),这个请求并不会直接送到你写的某个PHP文件手里。它得先去“驿站”——也就是路由系统——那里报个到。路由系统一看这个地址是 /user/profile,查一下自己手上的“派送清单”(路由规则),哦,原来这个请求应该交给 User 控制器的 profile 方法来处理。然后它就把这个请求,连带可能附带的“包裹”(URL参数),一起转交给对应的控制器方法。这个过程,就是路由的核心工作:将特定的URL请求映射到指定的应用程序逻辑上。
为什么需要路由?直接访问 index.php?m=user&a=profile 不行吗?当然可以,但那已经是十年前的玩法了。现代框架使用路由,首先是为了让URL看起来更简洁、更友好,对搜索引擎也更有利,也就是常说的 SEO友好。其次,它能提供更好的安全性,隐藏了真实的脚本路径和参数名。最重要的是,它给了你极大的灵活性,可以随心所欲地设计你的网站URL结构,而无需改动实际的代码文件位置。
在ThinkPHP6里,路由的配置文件主要放在项目的 route 目录下。默认情况下,系统会加载 route/app.php 这个文件作为全局路由的起点。你打开这个文件,可能会发现里面空空如也,或者只有几行注释。没错,这就是你的画布,所有精彩的路由规则都将从这里开始绘制。要定义一条路由,你首先得引入路由门面类,这是ThinkPHP框架的常见做法:
use think\facade\Route;
引入之后,你就可以使用 Route 类的各种静态方法来定义规则了。最基础的定义方法就是 Route::rule(),它接受三个参数:路由表达式(用户访问的URL模式)、路由地址(这个请求该交给谁处理)、请求方式(GET、POST等)。比如,你想让用户访问 /hello 时,显示一个欢迎页面,而这个页面由 Index 控制器的 hello 方法生成,你可以这样写:
Route::rule('hello', 'index/hello', 'GET');
不过,在实际开发中,我们更常用的是那些语义更清晰的快捷方法,它们直接对应了HTTP的请求方法。这会让你的代码意图一目了然。
1.1 五种核心请求方式路由
ThinkPHP6为最常见的几种HTTP请求方式提供了直接的快捷方法,这几乎覆盖了90%的日常场景。我强烈建议你从一开始就养成使用这些快捷方法的习惯,而不是只用万能的 rule。
GET路由:这是最常用的一种,用于获取和展示数据。当用户在浏览器地址栏输入网址、点击链接,或者页面加载资源(如图片、CSS)时,通常都是GET请求。定义起来非常简单:
Route::get('article/:id', 'Article/read');
这条规则的意思是:当一个GET请求访问像 /article/5 或 /article/123 这样的地址时,调用 Article 控制器的 read 方法,并把URL中的 5 或 123 作为参数 $id 传给它。控制器里你就能用 $id 来查询对应的文章了。
POST路由:当用户提交表单、上传文件时,用的就是POST请求。它通常用于创建新的数据。
Route::post('article', 'Article/save');
用户向 /article 地址提交一个POST请求(比如表单提交),数据就会被 Article 控制器的 save 方法接收并处理,通常是将新文章存入数据库。
PUT 和 DELETE 路由:这两个在纯粹的Web表单中不常见,但却是构建 RESTful API 的基石。PUT用于更新整个资源,DELETE用于删除资源。虽然浏览器表单原生不支持,但你可以通过JavaScript(如Ajax)或者API测试工具(如Postman)来发送这类请求。
Route::put('article/:id', 'Article/update'); // 更新ID为:id的文章
Route::delete('article/:id', 'Article/delete'); // 删除ID为:id的文章
ANY路由:这是一个“通吃”型路由。无论用户用GET、POST、PUT还是其他任何HTTP方法访问这个地址,都会被这条规则匹配。听起来很方便对吧?但我要给你提个醒:谨慎使用。在大多数需要明确区分操作类型的场景(尤其是API设计)下,使用ANY路由会破坏接口的清晰性和安全性。它更适合一些简单的、不区分方法的展示页面。定义方式如下:
Route::any('about', 'Page/about'); // 无论怎么访问 /about,都显示关于页面
把这五种路由方式用熟了,你就能处理绝大多数常规的页面和表单交互了。它们就像是你工具箱里的五把标准螺丝刀,各有各的用途。接下来,我们要让路由变得更“聪明”,能处理动态变化的部分,这就需要用到路由参数。
1.2 路由参数:让URL“活”起来
静态路由(比如 /about)很有用,但动态路由才是Web应用的灵魂。想想看,知乎上每一篇回答、商品详情页、用户个人主页,它们的URL结构都很相似,只是中间的数字或用户名不同。在ThinkPHP6里,我们通过定义路由参数来实现这个功能。
在路由表达式中,使用冒号 : 加参数名来定义一个动态片段。最常见的例子就是文章详情页:
Route::get('blog/:id', 'Blog/read');
这里的 :id 就是一个路由参数。当用户访问 /blog/42 时,框架会自动将 42 捕获,并作为名为 $id 的参数传递给你的 Blog 控制器的 read 方法。你在控制器里直接接收它就行:
// 在 app/controller/Blog.php 的 read 方法中
public function read($id)
{
$article = ArticleModel::find($id);
return view('read', ['article' => $article]);
}
参数不限于一个,你可以定义多个,形成类似目录的结构,这对于按日期归档的文章列表非常有用:
Route::get('archive/:year/:month/:day', 'Archive/list');
访问 /archive/2023/10/01,你的 list 方法就会依次收到 $year=2023, $month=10, $day=01 这三个参数。
但是,这里有个坑我踩过:如果用户乱输入怎么办?比如他把 :id 位置输入成了 abc,而你的数据库ID是数字,这就会导致查询错误。所以,我们需要给参数加上“紧箍咒”——参数约束。
ThinkPHP6允许你使用正则表达式来约束参数的格式。比如,强制 :id 必须为纯数字:
Route::get('blog/:id', 'Blog/read')->pattern(['id' => '\d+']);
\d+ 是一个正则表达式,表示“一个或多个数字”。现在,如果用户访问 /blog/abc,他将看到一个404页面(或者你定义的路由未匹配错误),而不是一个程序报错页面。这既提升了用户体验,也增强了程序健壮性。你还可以定义更复杂的规则,比如用户名只允许字母数字组合:['username' => '[a-zA-Z0-9]+']。
除了用 ->pattern() 方法,还有一种更简洁的“内联”写法,直接在路由表达式中定义正则:
Route::get('blog/:id(\d+)', 'Blog/read');
这种写法把正则 (\d+) 直接放在了参数 :id 后面,效果和上面用 pattern 方法是一样的。我个人更喜欢这种内联方式,因为它把规则和参数绑定得更紧密,一目了然。掌握了参数和约束,你的路由就已经能应对很多复杂场景了。但当一个项目有几十上百条路由时,你会发现它们开始变得杂乱无章,管理起来很头疼。这时候,就需要请出路由的“收纳整理大师”——路由分组。
2. 路由进阶:分组、中间件与资源路由
当你的应用逐渐庞大,路由文件里塞满了各种规则,从用户管理到商品订单,全都混在一起,找起来就像在杂乱的抽屉里翻一枚特定的回形针。这时候,路由分组 功能就是你的救星。它允许你将具有某些共同特征的路由(比如相同的前缀、相同的处理逻辑)组织在一起,让代码结构瞬间变得清晰。
2.1 路由分组:化繁为简的整理术
路由分组最基本的用法,是为一组路由添加一个统一的前缀。想象一下后台管理系统,所有管理功能的URL通常都以 /admin 开头。如果没有分组,你需要为每一个后台路由手动加上 admin/ 前缀:
Route::get('admin/user', 'admin.User/index');
Route::get('admin/user/add', 'admin.User/add');
Route::post('admin/article', 'admin.Article/save');
// ... 还有很多
这不仅写起来麻烦,如果哪天你想把后台路径从 admin 改成 manage,那得一个一个去改,极易出错。用分组就优雅多了:
Route::group('admin', function () {
Route::get('user', 'admin.User/index');
Route::get('user/add', 'admin.User/add');
Route::post('article', 'admin.Article/save');
});
在这个 admin 分组下的所有路由,它们的实际访问路径都会自动带上 /admin 前缀。所以 user 对应的完整路径就是 /admin/user。你只需要在分组闭包内关注具体的功能路径就行了,前缀的修改只需改动分组名那一处。
分组的功能远不止加前缀。它更强大的地方在于可以统一设置组内路由的公共属性。比如,你的整个后台系统都需要验证用户是否登录,都需要记录操作日志,并且只允许通过HTTPS访问。如果给每条路由单独添加这些设置,会是灾难性的重复劳动。分组可以一次性搞定:
Route::group('admin', function () {
// 组内的所有路由
Route::get('dashboard', 'admin.Index/dashboard');
Route::get('user/list', 'admin.User/list');
})->middleware([\app\middleware\Auth::class, \app\middleware\AdminLog::class])->https(true);
通过链式调用 ->middleware() 和 ->https(),我们为整个 admin 分组下的所有路由统一添加了“身份认证”、“日志记录”两个中间件,并强制要求HTTPS连接。这种声明式的配置,让安全策略和横切关注点的管理变得极其方便。
分组还支持嵌套,这为构建复杂的多模块应用提供了可能。比如,一个电商后台,下面有商品、订单、会员等多个一级模块,每个模块下又有自己的子功能:
Route::group('admin', function () {
// 商品模块分组
Route::group('goods', function () {
Route::get('list', 'admin.goods.Index/list');
Route::get('category', 'admin.goods.Category/index');
});
// 订单模块分组
Route::group('order', function () {
Route::get('list', 'admin.order.Index/list');
Route::get('detail/:id', 'admin.order.Index/detail');
});
})->middleware(\app\middleware\Auth::class);
这样,商品列表的完整路径是 /admin/goods/list,订单详情的路径是 /admin/order/detail/5。代码结构层次分明,和你的业务模块完全对应,维护起来非常直观。说到了中间件,这绝对是ThinkPHP6路由系统里一个熠熠生辉的宝石,它把很多繁琐的通用逻辑从控制器里解放了出来。
2.2 路由中间件:在请求到达前的“安检员”
中间件是什么?你可以把它想象成一道道的安检门。一个HTTP请求在到达最终的目的地(你的控制器方法)之前,需要穿过这些门。每一道门(中间件)都可以检查这个请求、对它做一些处理,或者干脆拒绝它通过。
为什么需要中间件?假设你的应用有十个需要登录后才能访问的页面。按照传统的写法,你需要在十个控制器方法里,都写上相同的登录状态检查代码。这违反了DRY(Don‘t Repeat Yourself)原则。中间件就是解决这个问题的完美方案:你把检查登录的逻辑写成一个独立的中间件,然后把它“附加”到需要登录的路由上。这样,检查逻辑只有一份,却可以在无数个地方复用。
在ThinkPHP6中创建一个中间件非常简单。使用命令行工具生成一个:
php think make:middleware Auth
这会在 app/middleware 目录下生成一个 Auth.php 文件。它的核心是一个 handle 方法:
<?php
namespace app\middleware;
class Auth
{
public function handle($request, \Closure $next)
{
// 在这里进行权限检查
if (!session('user_id')) {
// 如果未登录,重定向到登录页
return redirect('/login');
}
// 如果检查通过,将请求传递给下一个中间件或控制器
return $next($request);
}
}
这个 handle 方法就是“安检门”的核心。$request 是当前的请求对象,$next 是一个闭包,代表“下一个要执行的中间件或最终控制器”。你的逻辑写在调用 $next($request) 之前或之后,分别对应“前置中间件”和“后置中间件”。
- 前置中间件:在
$next($request)之前执行。通常用于请求验证、权限检查、数据过滤。比如上面的登录检查,如果不通过,直接return redirect(...)就中断了流程,请求根本到不了控制器。 - 后置中间件:在
$next($request)之后执行。通常用于对响应进行统一处理,比如添加HTTP头、压缩输出内容、记录响应日志等。
把中间件应用到路由上有几种方式。最直接的就是在定义路由时链式调用:
Route::get('user/profile', 'User/profile')->middleware(\app\middleware\Auth::class);
更高效的是在路由分组上应用,就像上一节展示的那样,可以一次性保护整个功能区域。你还可以在全局路由配置文件 config/route.php 中定义 middleware 数组,这里面的中间件会对所有注册的路由生效,适合做最基础的全局处理,比如强制HTTPS、跨域请求处理等。
中间件让我们的控制器可以更专注于核心的业务逻辑,而把那些通用的、辅助性的代码剥离出去,这是框架设计上“关注点分离”思想的完美体现。聊完了分组和中间件,我们再来看看ThinkPHP6为一种非常流行的API设计风格提供的“开箱即用”支持——资源路由。
2.3 资源路由:快速构建RESTful API的利器
如果你要开发一套提供给手机App或者前端Vue/React项目使用的数据接口,RESTful 风格几乎是现在的标准选择。它的核心思想是:用URL来表示资源(名词),用HTTP方法来表示对资源的操作(动词)。比如:
GET /articles:获取文章列表POST /articles:创建一篇新文章GET /articles/1:获取ID为1的文章详情PUT /articles/1:更新ID为1的文章DELETE /articles/1:删除ID为1的文章
按照我们之前学的方法,你需要为这五个操作定义五条独立的路由。ThinkPHP6的 资源路由 功能,可以让你用一行代码,自动生成这全套符合RESTful规范的路由。这简直是生产力的大解放!
使用 Route::resource() 方法:
Route::resource('articles', 'Article');
就这么一行!框架会自动为你映射出下表所列的所有路由:
| HTTP方法 | 路由地址 | 对应控制器方法 | 操作说明 |
|---|---|---|---|
| GET | articles | index | 显示文章列表 |
| GET | articles/create | create | 显示创建文章的表单(Web用) |
| POST | articles | save | 保存新创建的文章 |
| GET | articles/:id | read | 显示单篇文章详情 |
| GET | articles/:id/edit | edit | 显示编辑文章的表单(Web用) |
| PUT/PATCH | articles/:id | update | 更新指定文章 |
| DELETE | articles/:id | delete | 删除指定文章 |
你会发现,它自动生成了7个标准操作。在纯API场景下,create 和 edit 这两个用于展示表单的方法可能用不到。你可以通过 only 或 except 方法来过滤:
// 只生成 index, save, read, update, delete 这5个API常用路由
Route::resource('articles', 'Article')->only(['index', 'save', 'read', 'update', 'delete']);
// 或者,排除掉 create 和 edit
Route::resource('articles', 'Article')->except(['create', 'edit']);
资源路由还支持嵌套,用于表达资源之间的从属关系。比如“评论”属于某篇“文章”:
Route::resource('articles.comments', 'Comment');
这会生成像 /articles/5/comments(获取文章5的所有评论)、/articles/5/comments/3(获取文章5下的评论3)这样的路由。控制器的 Comment 类方法里,你可以通过参数获取到 article_id。
我实测下来,在开发标准的数据增删改查接口时,资源路由能节省大量重复的、机械式的编码工作,并且强制你遵循统一的规范,让团队协作和前后端对接都更顺畅。当然,它并不是万能的,对于特别定制化的非CRUD接口,还是需要单独定义路由。掌握了分组、中间件和资源路由,你的路由配置水平已经超越了大多数初学者。但ThinkPHP6的路由系统还有一些“高级玩法”,能让你的应用更加灵活和强大。
3. 路由高级应用与实战技巧
当你对基础路由和分组应用自如之后,可能会遇到一些更特殊的需求。比如,如何优雅地处理用户输错的地址?如何在代码中方便地生成URL,避免硬编码?如何让路由支持不同的URL后缀?这些就是高级路由技巧要解决的问题。
3.1 路由的“安全网”:兜底路由与MISS路由
无论你的路由定义得多么详尽,总会有用户或爬虫尝试访问一些不存在的地址。默认情况下,ThinkPHP6会抛出一个 RouteNotFoundException。对于用户来说,一个专业的404错误页面比一堆技术报错信息要友好得多。这时候就需要 MISS路由。
MISS路由,顾名思义,就是当所有定义的路由规则都无法匹配当前请求时,最后会“落入”的这个兜底路由。它的定义非常简单:
Route::miss('public/Error/miss');
这里 public/Error/miss 是一个标准的“控制器/方法”地址。我通常会在 Public 控制器(或 Error 控制器)里创建一个 miss 方法,在这个方法里渲染一个自定义的404页面,记录一下访问日志,然后返回一个404状态码。
// 在 app/controller/Public.php 中
public function miss()
{
// 记录一下这个不存在的访问路径,有助于发现爬虫或用户错误
trace('访问了不存在的路径:' . request()->url(), 'warning');
// 返回404状态码和友好的错误页面
return view('public/404')->code(404);
}
除了全局的MISS路由,你还可以在路由分组内部定义MISS路由,这为不同模块提供不同的兜底处理提供了可能。比如,后台管理系统的未知地址,你可能想重定向到后台首页,而前台的未知地址则显示创意404页面。
Route::group('admin', function () {
Route::get('dashboard', 'admin.Index/index');
// ... 其他后台路由
Route::miss('admin.Index/index'); // 后台未知地址跳回首页
});
Route::miss('public/Error/miss'); // 全局兜底,显示404页面
这个特性非常实用,它能确保你的应用在任何情况下都能给出可控的响应,提升了整体的健壮性和用户体验。另一个能极大提升开发效率的特性是路由的命名与反向解析。
3.2 路由命名与反向解析:告别硬编码的URL
在视图文件或者控制器里,我们经常需要生成链接。最原始的做法是直接拼接字符串:
<a href="/article/detail/<?php echo $id; ?>">查看详情</a>
或者:
$url = '/admin/user/edit/' . $userId;
这种做法被称为“硬编码”。它的坏处非常明显:一旦你的路由规则发生变化(比如把 /article/detail/:id 改成了 /post/:id),你就必须在整个项目里搜索并修改所有生成这个URL的地方,工作量巨大且极易遗漏。
ThinkPHP6的路由命名和反向解析功能就是为了解决这个问题。你可以在定义路由时,给它起一个唯一的名字(标识):
Route::get('article/:id', 'Article/read')->name('article_read');
现在,无论这个路由的实际路径将来怎么变,它的名字 article_read 是不变的。在需要生成这个URL的地方,你不再关心它的具体路径,而是通过名字和参数来生成:
// 使用助手函数 url()
$url = url('article_read', ['id' => 42]);
// 生成:/article/42
// 在模板中使用
<a href="{:url('article_read', ['id' => $article.id])}">阅读全文</a>
如果某天你觉得路径太长,把它改成 post/:id,你只需要修改路由定义那一行代码:
Route::get('post/:id', 'Article/read')->name('article_read');
所有通过 url('article_read', ...) 生成的地方,都会自动变成 /post/42。这简直是维护大型项目的福音!我强烈建议你为每一条重要的、可能在多处引用的路由都起一个有意义的名字。命名路由在分组中同样有效,并且名字会自动带上分组前缀作为命名空间隔离,避免冲突。
3.3 灵活的路由地址与闭包支持
到目前为止,我们路由的终点都是“控制器/方法”。但ThinkPHP6的路由地址要灵活得多。它支持直接路由到一个类的静态方法或实例方法:
// 路由到某个服务类的静态方法
Route::get('clear_cache', '\app\service\CacheService::clearAll');
// 路由到某个类的实例方法(框架会自动实例化)
Route::get('generate_report', '\app\service\ReportGenerator@generate');
这在一些简单的、不需要完整控制器逻辑的场景下非常方便,比如执行一个清理任务、生成一个一次性报告等。
更有趣的是路由闭包。你可以直接把处理逻辑写在一个匿名函数里,完全跳过控制器:
Route::get('hello/:name', function ($name) {
return 'Hello, ' . $name . '! This is from a closure route.';
});
闭包路由非常适合处理一些极其简单的逻辑,比如一个健康检查接口、一个返回固定JSON的简单API端点。它写起来快,部署也无需创建额外的控制器文件。但要注意,闭包内无法直接使用依赖注入容器中的某些高级功能,逻辑复杂了还是建议用控制器。
此外,ThinkPHP6还支持路由重定向,这常用于网站改版后的旧链接跳转:
// 将旧的 /old-page 永久重定向 (301) 到新的 /new-page
Route::redirect('old-page', '/new-page', 301);
// 临时重定向 (302) 到外部网站
Route::get('go/to/baidu', 'https://www.baidu.com');
这些灵活的路由地址选项,让你可以根据实际情况选择最合适、最简洁的实现方式,而不是被“必须用控制器”的思维所束缚。
3.4 实战案例:构建一个带权限管理的后台路由系统
让我们把这些高级技巧串起来,设计一个实战场景:为一个内容管理系统(CMS)构建后台路由。
需求:
- 所有后台路由以
/admin开头。 - 需要登录才能访问(Auth中间件)。
- 记录所有后台操作日志(AdminLog中间件)。
- 文章(Article)和分类(Category)模块使用标准的RESTful资源路由。
- 有一个特殊的“仪表盘”页面和“个人资料”页面。
- 后台的未知地址跳转到仪表盘。
实现:
首先,创建两个中间件:Auth(检查session)和 AdminLog(记录操作到数据库)。然后,在 route/app.php 或一个独立的后台路由文件中进行配置:
use think\facade\Route;
// 后台路由分组
Route::group('admin', function () {
// 仪表盘和个人资料(非资源路由)
Route::get('dashboard', 'admin.Index/dashboard')->name('admin.dashboard');
Route::get('profile', 'admin.Index/profile')->name('admin.profile');
Route::post('profile/update', 'admin.Index/updateProfile')->name('admin.profile.update');
// 文章资源路由(排除create和edit表单页)
Route::resource('article', 'admin.Article')->except(['create', 'edit'])->name('admin.article');
// 分类资源路由
Route::resource('category', 'admin.Category')->except(['create', 'edit'])->name('admin.category');
// 后台内部的兜底路由:未知地址跳转到仪表盘
Route::miss('admin.Index/dashboard');
})->middleware([\app\middleware\Auth::class, \app\middleware\AdminLog::class]);
在这个配置里,我们综合运用了:
- 分组:统一前缀和中间件。
- 命名路由:为每个路由起了名字(如
admin.dashboard),方便在视图和代码中生成URL。 - 资源路由:快速生成文章和分类的CRUD接口。
- 中间件:统一处理认证和日志。
- 分组内MISS路由:处理后台内部的错误访问。
这样配置出来的路由系统,结构清晰、功能完整、易于维护,并且具备了良好的安全性和可扩展性。当你的项目需要增加新的后台模块时,只需要在分组内仿照格式添加即可,所有公共策略都会自动生效。
路由的学问还有很多,比如域名绑定、路由缓存(大幅提升性能)、路由绑定模型(自动注入Eloquent模型实例)等。但掌握了从基础配置到分组、中间件、资源路由以及这些高级技巧,你已经能够驾驭ThinkPHP6路由系统的绝大部分功能,足以应对从个人博客到企业级应用的开发需求。剩下的,就是在实际项目中不断实践和体会,你会发现它比你想象的还要强大和贴心。
2524



被折叠的 条评论
为什么被折叠?



