对任何一个 Web 应用框架而言,通过 HTTP 协议处理用户请求并返回响应都是核心必备功能,也就是说,对于我们学习和使用一个 Web 框架,第一件要做的事情就是定义应用路由,否则,将无法与终端用户进行交互。

路由入门

在 Laravel 应用中,定义路由有两个入口,一个是 routes/web.php,用于处理终端用户通过 Web 浏览器直接访问的请求,另一个是 routes/api.php,用于处理其他接入方的 API 请求(通常是跨语言、跨应用的请求)。在本章中,我们将主要聚焦于 routes/web.php,关于 routes/api.php 将会在后面编写 API 章节中重点介绍。

路由动作

Route::get('/', function () {}); 
Route::post('/', function () {}); 
Route::put('/', function () {});
Route::delete('/', function () {});

此外,还可以通过 Route::any 定义一个可以捕获任何请求方式的路由:

Route::any('/', function () {});

从安全角度说,并不推荐上述这种路由定义方式,但是兼顾到便利性,我们可以通过 Route::match 指定请求方式白名单数组,比如下面这个路由可以匹配 GET 或 POST 请求:

Route::match(['get', 'post'], '/', function () {});

对于稍微复杂一些的业务逻辑,我们可以将其拆分到控制器方法中实现,然后在定义路由的时候使用控制器+方法名来取代闭包函数:

Route::get('/', 'WelcomeController@index');

这段代码的含义是将针对 / 路由的 GET 请求传递给 App\Http\Controllers\WelcomeController 控制器的 index 方法进行处理。

路由参数

如果你定义的路由需要传递参数,只需要在路由路径中进行标识并将其传递到闭包函数即可:

Route::get('user/{id}', function ($id) {
    return "用户ID: " . $id;
});

这样,当你访问 http://blog.test/user/1000 的时候,就可以在浏览器看到 用户ID: 1000 字符串。

此外,你还可以定义可选的路由参数,只需要在参数后面加个 ? 标识符即可,同时你还可以为可选参数指定默认值:

Route::get('user/{id?}', function ($id = 1) {
    return "用户ID: " . $id;
});

这样,如果不传递任何参数访问 http://blog.test/user,则会使用默认值 1 作为用户 ID。

更高级的,你还可以为路由参数指定正则匹配规则:

Route::get('page/{id}', function ($id) {
    return '页面ID: ' . $id;
})->where('id', '[0-9]+');

Route::get('page/{name}', function ($name) {
    return '页面名称: ' . $name;
})->where('name', '[A-Za-z]+');

Route::get('page/{id}/{slug}', function ($id, $slug) {
    return $id . ':' . $slug;
})->where(['id' => '[0-9]+', 'slug' => '[A-Za-z]+']);

如果传入的路由参数与指定正则不匹配,则会返回 404 页面

路由命名

在应用其他地方引用路由的最简单的方式就是通过定义路由的第一个路径参数,你可以在视图中通过辅助函数 url() 来引用指定路由,该函数会为传入路径加上完整的域名前缀,所以 url('/') 对应的输出是 http://blog.test。你可以在视图文件中这么使用:

<a href="{{ url('/') }}">

此外,Laravel 还允许你为每个路由命名,这样一来,不必显式引用路径 URL 就可以对路由进行引用,这样做的好处是你可以为一些复杂的路由路径定义一个简单的路由名称从而简化对路由的引用,另一个更大的好处是即使你调整了路由路径(在复杂应用中可能很常见),只要路由名称不变,那么就无需修改前端视图代码,提高了系统的可维护性。

路由命名很简单,只需在原来路由定义的基础上以方法链的形式新增一个 name 方法调用即可:

Route::get('user/{id?}', function ($id = 1) {
    return "用户ID: " . $id;
})->name('user.profile');

前端视图模板中可以通过辅助函数 route 并传入路由名称(如果有路由参数,则以数组方式作为第二个参数传入)来引用该路由:

<a href="{{ route('user.profile', ['id' => 100]) }}">
// 输出:http://blog.test/user/100

如果没有路由参数,通过 route('user.profile') 引用即可。此外,我们还可以简化对路由参数的传递,比如上例可以简化为:

<a href="{{ route('user.profile', [100]) }}">

这样调用的话,数组中的参数顺序必须与定义路由时的参数顺序保持一致,而使用关联数组的方式传递参数则没有这样的约束。

注:在实际开发过程中,推荐使用路由命名来引用路由。

路由分组

在日常开发中,我们通常会将具有某些共同特征的路由进行分组,这些特征包括是否需要认证、是否具有共同的路由前缀或者子域名、以及是否具有相同的控制器命名空间等,显然,对路由按照共同特征进行分组后可以避免重复为某些路由定义相同的路由特征,让代码更加简洁,可读性和可维护性更好。

路由分组,其实就是通过 Route::group 将几个路由聚合到一起,然后给它们应用对应的共享特征:

Route::group([], function () { 
    Route::get('hello', function () { return 'Hello'; }); 
    Route::get('world', function () { return 'World'; }); 
});

由于没有应用任何共享特征(第一个参数是空数组),所以这样的路由分组其实并没有什么意义

中间件

我们使用路由分组最常见的场景恐怕就是为一组路由应用共同的中间件了,关于中间件可以参考官方文档,后面也会有单独章节来讲解,使用中间件可以对 HTTP 请求进行过滤或重定向,比如以认证中间件(别名auth)为例,如果用户已经认证可以进行后续处理,否则将会把用户重定向到登录页面。

下面我们就来创建一个包含 dashboard 和 account 的路由分组,这两个路由都需要认证,所以我们可以通过 Route::middleware 为其设置共同的中间件 auth 并以此对其进行分组:

Route::middleware('auth')->group(function () {
    Route::get('dashboard', function () {
        return view('dashboard');
    });
    Route::get('account', function () {
        return view('account');
    });
});

如果是多个中间件,可以通过数组方式传递参数,比如 ['auth', 'another'],以上是 Laravel 5.5+ 提供的新语法,在此之前的版本,需要这么调用:

Route::group(['middleware' => 'auth'], function () { 
    Route::get('dashboard', function () { 
        return view('dashboard'); 
    }); 
    Route::get('account', function () { 
        return view('account'); 
    }); 
});

当然,链式调用只是语法糖,底层最终还是下面 Route::group 这种定义实现的,感兴趣的同学可以去看下源码是如何实现的: vendor/laravel/framework/src/Illuminate/Routing/RouteRegistrar.php,下面路径前缀、子域名和命名空间的链式调用原理也是一样,以后我们都用链式调用来定义,因为这样代码可读性更好。

路由路径前缀

如果某些路由拥有共同的路径前缀,例如,所有 API 路由都以 /api 前缀开头,我们可以使用 Route::prefix 为这个分组路由指定路径前缀并对其进行分组:

Route::prefix('api')->group(function () {
    Route::get('/', function () {
        // 处理 /api 路由
    })->name('api.index');
    Route::get('users', function () {
        // 处理 /api/users 路由
    })->name('api.users');
});

路由模型绑定

我们在使用路由的时候一个很常见的使用场景就是根据资源 ID 查询资源信息:

Route::get('task/{id}', function ($id) {
    $task = \App\Models\Task::findOrFail($id);
});

Laravel 提供了一个「路由模型绑定」功能来简化上述代码编写,通过路由模型绑定,我们只需要定义一个特殊约定的参数名(比如 {task})来告知路由解析器需要从 Eloquent 记录中根据给定的资源 ID 去查询模型实例,并将查询结果作为参数传入而不是资源 ID。

有两种方式来实现路由模型绑定:隐式绑定和显式绑定。

隐式绑定

使用路由模型绑定最简单的方式就是将路由参数命名为可以唯一标识对应资源模型的字符串(比如 $task 而非 $id),然后在闭包函数或控制器方法中对该参数进行类型提示,此处参数名需要和路由中的参数名保持一致:

Route::get('task/{task}', function (\App\Models\Task $task) {
    dd($task); // 打印 $task 明细
});

这样就避免了我们传入 $id 后再进行查询,而是把这种模板式代码交由 Laravel 框架底层去实现。

由于路由参数({task})和方法参数($task)一样,并且我们约定了 $task 类型为 \App\Models\Task,Laravel 就会判定这是一个路由模型绑定,每次访问这个路由时,应用会将传入参数值赋值给 {task},然后默认以参数值作为资源 ID 在底层通过 Eloquent 查询获取对应模型实例,并将结果传递到闭包函数或控制器方法中。

显式绑定

显式绑定需要手动配置路由模型绑定,通常需要在 App\Providers\RouteServiceProvider 的 boot() 方法中新增如下这段配置代码:

public function boot()
{
    // 显式路由模型绑定
    Route::model('task_model', Task::class);

    parent::boot();
}

编写完这段代码后,以后每次访问包含 {task_model} 参数的路由时,路由解析器都会从请求 URL 中解析出模型 ID ,然后从对应模型类 Task 中获取相应的模型实例并传递给闭包函数或控制器方法:

Route::get('task/model/{task_model}', function (\App\Models\Task $task) {
    dd($task);
});

注:如果路由模型绑定对应匹配记录不存在,将自动返回 404 响应。

由于在正式开发中,出于性能的考虑通常会对模型数据进行缓存,此外在很多情况下,需要关联查询才能得到我们需要的结果,所以并不建议过多使用这种路由模型绑定。

兜底路由

在 Laravel 5.6 中,引入了兜底路由功能。所谓兜底路由,就是当路由文件中定义的所有路由都无法匹配用户请求的 URL 时,用来处理用户请求的路由,在此之前,Laravel 都会通过异常处理器为这种请求返回 404 响应,使用兜底路由的好处是我们可以对这类请求进行统计并进行一些自定义的操作,比如重定向,或者一些友好的提示什么的,兜底路由可以通过 Route::fallback 来定义:

Route::fallback(function () {
    return '我是最后的屏障';
});

这样,当我们访问一些不存在的路由,比如 http://blog.test/test/111,就会执行兜底路由中的处理逻辑,而不是返回 404 响应了。

频率限制

在 Laravel 5.6 中,还引入了频率限制功能。所谓频率限制,指的是在指定时间单个用户对某个路由的访问次数限制,该功能有两个使用场景,一个是在某些需要验证/认证的页面限制用户失败尝试次数,提高系统的安全性,另一个是避免非正常用户(比如爬虫)对路由的过度频繁访问,从而提高系统的可用性,此外,在流量高峰期还可以借助此功能进行有效的限流。

在 Laravel 中该功能通过内置的 throttle 中间件来实现,该中间件接收两个参数,第一个是次数上限,第二个是指定时间段(单位:分钟):

Route::middleware('throttle:60,1')->group(function () {
    Route::get('/user', function () {
        //
    });
});

以上路由的含义是一分钟能只能访问路由分组的内路由(如 /user)60 次,超过此限制会返回 429 状态码并提示请求过于频繁。

如果你觉得这种静态设置频率的方式不够灵活,还可以通过模型属性来动态设置频率,例如,我们可以为上述通过 throttle 中间件进行分组的路由涉及到的模型类定义一个 rate_limit 属性,然后这样来动态定义这个路由:

Route::middleware('throttle:rate_limit,1')->group(function () {
    Route::get('/user', function () {
        // 在 User 模型中设置自定义的 rate_limit 属性值
    });
    Route::get('/post', function () {
        // 在 Post 模型中设置自定义的 rate_limit 属性值
    });
});

这样,我们就可以通过为不同的模型类设置不同的 rate_limit 属性值来达到动态设置频率限制的效果了。

路由缓存

使用路由缓存之前,需要知晓路由缓存只能用于控制器路由,不能用于闭包路由,如果路由定义中包含闭包路由将无法进行路由缓存,只有将所有路由定义转化为控制器路由或资源路由后才能执行

路由缓存命令:

php artisan route:cache

如果想要删除路由缓存,可以运行:

php artisan route:clear

路由缓存对系统性能的提升应该是微乎其微的,但如果你很在意那几毫秒,则可以考虑,但是需要付出的代价是不能使用任何闭包路由,此外,由于使用路由缓存需要在每次变动路由后重新生成缓存,所以建议在应用部署脚本中执行 php artisan route:cache(运行此命令之前先要清理之前的缓存),即只在生产环境中使用路由缓存,本地开发环境路由经常变动,且没有性能方面的考虑,无需缓存。