大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
路由系统:从 URL 到控制器
这一课你将能做到
- 写出带参数、约束、命名的路由,并能用
route()反向生成 URL - 用路由分组统一配置前缀、中间件、子域名和控制器命名空间
- 使用路由模型绑定直接拿到模型实例,并用
route:list排查路由问题
为什么要学
路由是 URL 与代码之间的契约。Spring Boot 里你用 @GetMapping("/orders/{id}") 把 URL 绑到方法上,Laravel 的路由文件承担完全相同的职责,但表达能力更集中:参数约束、默认值、命名、分组、模型绑定、限流、限流退避都可以在一行链式调用里声明。
真实后端项目里路由还承载两个工程问题:接口版本化(/api/v1/...)和权限分层(游客、登录用户、管理员)。掌握分组和中间件组合后,这两件事都能用统一结构表达,而不用在每个控制器方法里重复写判断。
先建立图像
请求 GET /api/v1/orders/42
│
▼
routes/api.php
│
Route::prefix('v1')->middleware('auth:sanctum')->group(...)
│
Route::get('/orders/{order}', ...)
│ │
│ └─ 路由模型绑定:{order} → Order 模型(id=42)
▼
[OrderController::class, 'show']
与 Spring Boot 对照:
| Laravel | Spring Boot |
|---|---|
Route::get('/orders/{order}', ...) | @GetMapping("/orders/{order}") |
{order} 参数 + whereNumber() | @PathVariable Long order + 正则/校验 |
->name('orders.show') | 没有内建等价物(手写路径常量) |
Route::middleware(['auth']) | @PreAuthorize 或过滤器 |
| 路由模型绑定 | 需在方法内 orderRepository.findById() |
php artisan route:list | Actuator mappings(需额外开启) |
跟着做
步骤 1:基础动词路由
在 routes/api.php 中写一组资源路由:
<?php
use App\Http\Controllers\Api\OrderController;
use Illuminate\Support\Facades\Route;
Route::get('/ping', fn () => response()->json(['pong' => true]));
// 单个动词
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/orders', [OrderController::class, 'store']);
Route::get('/orders/{order}', [OrderController::class, 'show']);
Route::match(['put', 'patch'], '/orders/{order}', [OrderController::class, 'update']);
Route::delete('/orders/{order}', [OrderController::class, 'destroy']);
// 等价的资源路由一行声明(7 个 RESTful 动作)
Route::apiResource('orders', OrderController::class);
apiResource 与 resource 的区别:前者不注册 create / edit(返回 HTML 表单页)这两个动作,纯 API 项目用前者。它对应 Spring 社区常见的 REST 控制器约定。
步骤 2:参数、约束与默认值
// 必填参数 + 仅允许数字
Route::get('/users/{user}/orders/{order}', function (int $user, int $order) {
return response()->json(['user_id' => $user, 'order_id' => $order]);
})->whereNumber(['user', 'order']);
// 可选参数 + 默认值
Route::get('/reports/{type?}', function (string $type = 'daily') {
return response()->json(['type' => $type]);
});
// 正则约束:slug 只允许小写字母、数字、短横线
Route::get('/posts/{slug}', function (string $slug) {
return response()->json(['slug' => $slug]);
})->where('slug', '[a-z0-9\-]+');
// 全局约束:在 AppServiceProvider::boot() 中定义一次
// Route::pattern('id', '[0-9]+');
步骤 3:命名路由与反向生成 URL
Route::get('/orders/{order}/items', [OrderController::class, 'items'])
->name('orders.items');
// 在代码中生成 URL(不硬编码路径,重构路径时只改路由文件)
$url = route('orders.items', ['order' => 42, 'page' => 2]);
// http://127.0.0.1:8000/api/orders/42/items?page=2
// 控制器重定向到命名路由
return redirect()->route('orders.items', ['order' => 42]);
命名路由是 Laravel 比 Spring 更省事的一点:模板里的链接、重定向、校验失败回跳都用名字,路径变更时全局自动生效。
步骤 4:路由分组
真实项目的 API 通常长这样:
Route::prefix('v1')
->middleware('auth:sanctum')
->group(function () {
// 最终路径:/api/v1/orders
Route::get('/orders', [OrderController::class, 'index']);
// 仅管理员可访问:中间件叠加
Route::middleware('role:admin')->group(function () {
Route::post('/products', [\App\Http\Controllers\Api\ProductController::class, 'store']);
});
// 卖家中心:再加一层前缀
Route::prefix('seller')->group(function () {
Route::get('/statistics', [\App\Http\Controllers\Api\SellerStatController::class, 'show']);
// → /api/v1/seller/statistics
});
});
如果同一个控制器有多个方法,可以用 controller() 省略数组:
use App\Http\Controllers\Api\ProductController;
Route::controller(ProductController::class)->group(function () {
Route::get('/products', 'index');
Route::post('/products', 'store');
Route::get('/products/{product}', 'show');
});
步骤 5:路由模型绑定
创建模型和迁移(下一课详细讲 Eloquent,这里先跑通绑定):
php artisan make:model Order -m
控制器方法直接声明类型,框架自动按主键查库,查不到返回 404:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\Order;
class OrderController extends Controller
{
public function show(Order $order)
{
return response()->json($order);
}
}
默认按 id 绑定。想按订单号绑定,在模型里改写 getRouteKeyName():
// app/Models/Order.php
public function getRouteKeyName(): string
{
return 'order_no'; // /orders/{order_no}
}
更精细的「软删除数据也允许访问」或「限定当前用户的订单」可以用作用域绑定:
Route::get('/orders/{order}', function (\App\Models\Order $order) {
abort_unless($order->user_id === auth()->id(), 403);
return response()->json($order);
})->scopeBindings();
步骤 6:查看与排查
# 列出所有路由:方法、URI、名称、中间件、动作
php artisan route:list
# 只看 orders 相关
php artisan route:list --path=orders
# 按名称过滤
php artisan route:list --name=orders
# 生产环境缓存路由(闭包路由无法缓存,控制器路由可以)
php artisan route:cache
php artisan route:clear
讲清楚要点
- 路由定义顺序影响匹配:
/orders/report必须写在/orders/{order}之前,否则report会被当成{order}参数(当参数没有数字约束时)。这是初学者最常踩的坑。 - GET 与 HEAD 自动配对:注册
get路由时框架会同时响应 HEAD(只返回响应头)。POST 路由配合表单使用时必须通过 CSRF 中间件。 - 分组属性会合并而非覆盖:多层
prefix拼接、多层middleware叠加,命名可用->name('admin.')给整组加前缀。 - 限流直接挂路由:
Route::get(...) ->middleware('throttle:60,1')表示每分钟 60 次,底层通常用 Redis;对应 Spring Cloud Gateway 的 RequestRateLimiter。 - 回退路由:
Route::fallback(...)处理没有任何匹配的请求,可自定义 404 JSON。 - 表单不能发 PUT/DELETE:HTML 表单只支持 GET/POST,Blade 中用
@method('PUT')生成_method隐藏域,框架据此改写动词。AJAX 直接发 PUT/DELETE 则无需此处理。
你可能会懵的地方
-
路由写在
web.php还是api.php? 返回页面、需要登录态(Cookie + Session + CSRF)的写 web;给 App、小程序、第三方服务的 JSON 接口写 api,它默认带/api前缀且无状态。 -
route:cache报错说无法序列化闭包怎么办? 路由缓存只支持控制器类路由。把闭包路由改成指向控制器方法,再执行缓存。新项目从一开始就坚持「路由只指向控制器」可以避免这个问题。 -
模型绑定返回 404 而数据明明存在? 检查路由参数名与变量名是否一致(
{order}对应Order $order);若改了getRouteKeyName(),确认访问 URL 用的是新字段;再检查模型是否被全局作用域(如租户隔离)过滤。 -
怎么给 API 加版本号最合理? 最常见是 URI 版本
/api/v1/...+ 分组;也有用 Header(Accept: application/vnd.shop.v1+json)的方案,但对团队和网关更复杂。没有银弹,选定后全公司统一。 -
throttle中间件的计数存在哪? 默认走应用的缓存驱动,生产应配 Redis,否则多台机器各自计数、限流失效。Spring 网关同理,限流状态必须集中存储。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
/orders/report 写在 /orders/{order} 后面 | 动态路由抢先匹配 | 静态路径在前,动态参数在后 |
| 在路由闭包里写 200 行业务逻辑 | 无法缓存路由、无法测试 | 指向控制器方法 |
控制器方法里手动 Order::findOrFail($id) | 重复模型绑定做的事 | 直接类型声明 Order $order |
| 所有路由平铺、不做分组 | 前缀和中间件重复 | prefix + middleware group |
| 前端拼字符串生成接口 URL | 路径改动全端崩 | 后端用命名路由,前端集中管理 endpoint 常量 |
给公开接口也挂 auth:sanctum | 匿名用户 401 | 按资源公开性分组挂中间件 |
卡点急救
- 访问接口 404:先用
php artisan route:list --path=xxx确认路由真的注册了;api 路由别忘了访问时带/api前缀;再检查路由定义顺序与参数约束。 - POST 请求 419(CSRF):web 路由的表单缺少
@csrf;若是前后端分离的 API 接口,应写到api.php(该文件不启用 CSRF 校验),不要硬塞在 web 里。 - 改了路由文件没反应:跑过
route:cache就执行php artisan route:clear;OPcache 极端情况下可重启 PHP-FPM / Herd 服务确认。
面试常问
-
路由模型绑定是怎么实现的?查不到数据时发生什么? 路由器在分发前通过容器解析参数:发现类型是 Eloquent 模型时,取路由中同名参数值,隐式调用
whereKey(...)->firstOrFail()(或按getRouteKeyName指定字段),找不到抛ModelNotFoundException,异常处理器渲染为 404。还支持自定义「显式绑定」在Route::bind()中接管解析逻辑。 -
路由中间件、控制器中间件、全局中间件的区别? 全局中间件对每个请求执行(如维护模式、Cookie 加密);路由/分组中间件只作用于匹配的路由(如认证、限流);控制器中间件在构造函数中声明,可限定到某些方法(
$this->middleware('auth')->only('store'))。 -
怎么保证团队的 API 风格统一? 统一用
apiResource表达 RESTful 动作;统一版本前缀分组;统一响应结构(建议在中间件或基类控制器中包装);统一限流与认证中间件;再用route:list做代码评审检查项。
自测三问
- 不看资料,用
apiResource一行注册订单的 5 个 API 动作,并说出它们各自的 HTTP 动词和路径。 - 为什么
/users/{user}/orders这种路由要加参数约束?不加会怎样? - 要上线一个新版本接口且老版本保留 6 个月,路由文件你会怎么组织?
小结一句话
路由把 URL、参数、中间件和控制器用链式结构粘合成一份可反向生成、可缓存、可总览的契约表,写好路由等于写好了后端 API 的目录。
延伸
- Laravel 11 路由文档
- 路由模型绑定
- 限流
- Spring RequestMapping 对照学习
- 下一课:控制器、中间件与请求响应——进入路由的落点,学习 Request、Response、依赖注入与资源响应。