CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
路由系统:从 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:listActuator 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:路由分组 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 的目录。
延伸