ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # 基本路由 构建最基本的路由只需要一个 URI 与一个 `闭包`,这里提供了一个非常简单优雅的定义路由的方法: ~~~ Route::get('foo', function () { return 'Hello World'; }); ~~~ 我们以在安装配置文档中新建的 blog 应用为例,在 routes/web.php 中定义该路由: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052313460721.jpg) 在浏览器中通过 `http://blog.test/hello` (我使用 Valet 作为开发环境,故而对应域名是 blog.test,实际域名以自己配置的为准)即可访问我们刚刚定义的路由,页面输出内容如下: ~~~ Hello, welcome to LaravelAcademy.org ~~~ **默认路由文件** 所有的 Laravel 路由都在 `routes` 目录中的路由文件中定义,这些文件都由框架自动加载。`routes/web.php` 文件用于定义 web 界面的路由。这里面的路由都会被分配给 `web` 中间件组,它提供了会话状态和 CSRF 保护等功能。定义在 `routes/api.php` 中的路由都是无状态的,并且被分配了 `api` 中间件组。 大多数的应用构建,都是以在 `routes/web.php` 文件定义路由开始的。可以通过在浏览器中输入定义的路由 URL 来访问 `routes/web.php` 中定义的路由。例如,你可以在浏览器中输入 `http://your-app.dev/user` 来访问以下路由: ~~~ Route::get('/user', 'UserController@index'); ~~~ `routes/api.php` 文件中定义的路由通过 `RouteServiceProvider` 被嵌套到一个路由组里面。在这个路由组中,会自动添加 URL 前缀 `/api` 到此文件中的每个路由,这样你就无需再手动添加了。你可以在 `RouteServiceProvider` 类中修改此前缀以及其他路由组选项。 :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052333453991.jpg) 可用的路由方法 路由器允许你注册能响应任何 HTTP 请求的路由: ~~~ Route::get($uri, $callback); Route::post($uri, $callback); Route::put($uri, $callback); Route::patch($uri, $callback); Route::delete($uri, $callback); Route::options($uri, $callback); ~~~ 有的时候你可能需要注册一个可响应多个 HTTP 请求的路由,这时你可以使用 match 方法,也可以使用 any 方法注册一个实现响应所有 HTTP 请求的路由: ~~~ Route::match(['get', 'post'], '/', function () { // }); Route::any('foo', function () { // }); ~~~ 测试 GET 请求的时候直接在浏览器中输入请求地址即可,测试 POST 请求可以通过客户端工具,比如 Advanced REST Client,该工具可以在 Chrome 应用商店下载到,此外如果上面的路由是定义在 `routes/web.php` 的话,在测试 POST 请求之前,需要将对应路由取消 CSRF 保护检查,否则会返回 `419` 状态码导致无法请求成功,取消的方法是在 `app/Http/Middleware/VerifyCsrfToken` 中设置排除检查路由: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052348676428.jpg) 下面我们来测试下 POST 请求: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052346109636.jpg) 如果路由是定义在 `routes/api.php` 的话,则无需关注 CSRF 保护问题,比如我们在 `routes/api.php` 定义 `bar` 路由,并且在 `VerifyCsrfToken` 的 `$except` 属性数组中移除 `bar`,然后我们测试下对 `http://blog.test` 的 POST 请求: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2017/09/15052358360037.jpg) 正如我们所预测的,完全没有任何问题,背后的原因是因为 web 路由文件中定义的路由都位于 `web` 中间件群组,该群组默认启用 CSRF 保护检查,而 api 路由文件位于 api 路由群组,该群组下的路由主要用于 第三方 API 请求,没办法进行 CSRF 检查,所以不需要做任何处理。 **CSRF 保护** 指向 `web` 路由文件中定义的 `POST`、`PUT` 或 `DELETE` 路由的任何 HTML 表单都应该包含一个 CSRF 令牌字段,否则,这个请求将会被拒绝。可以在 `CSRF 文档` 中阅读有关 CSRF 更多的信息: ~~~ <form method="POST" action="/profile"> @csrf ... </form> ~~~ 还是以上面的 `foo` 路由为例,如果我们不在 `VerifyCsrfToken` 中间件中排除对它的检查(事实上,这样的操作也不安全),那么就需要在表单提交中带上 `csrf_token` 字段: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052354946691.jpg) 这样,当我们访问 `http://blog.test/form` 然后在页面点击提交按钮后,页面会跳转到 `http://blog.test/foo` 并显示如下内容: ~~~ This is a request from get or post ~~~ ## 重定向路由 如果要定义重定向到另一个 URI 的路由,可以使用 `Route::redirect`方法。这个方法可以快速的实现重定向,而不再需要去定义完整的路由或者控制器: ~~~ Route::redirect('/here', '/there', 301); ~~~ 其中 `here` 表示原路由,`there` 表示重定向之后的路由,`301` 是一个 HTTP 状态码,用于标识重定向。 路由视图 如果你的路由需要返回一个视图,可以使用 `Route::view` 方法,和 `redirect` 方法类似,这个方法也很方便,以至于你不需要在额外定义一个路由或控制器。`view` 方法接收一个 URI 作为第一个参数,以及一个视图名称作为第二个参数,此外,你还可以提供一个数组数据传递到该视图方法作为可选的第三个参数,该数组数据可用于视图中的数据渲染: ~~~ Route::view('/welcome', 'welcome'); Route::view('/welcome', 'welcome', ['name' => '学院君']); ~~~ 我们在 `routes/web.php` 定义一个路由视图如下: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052382479139.jpg) 为了保证可以共用 `welcome.blade.php` 这个视图文件,我们也对默认提供的 `/` 路由做了调整,接下来,我们需要修改 `resources/views/welcome.blade.php` 代码以支持 `website` 数据变量: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052384193901.jpg) 我们将原来写死的 `Laravel` 文本调整为支持变量传入的方式,这样,我们就可以在浏览器中通过 `http://blog.test/view` 访问路由视图了: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15052385161233.jpg) # 路由参数 ## 必填参数 有时我们需要在路由中获取 URI 请求参数。例如,如果要从 URL 中获取用户ID,需要通过如下方式定义路由参数: ~~~ Route::get('user/{id}', function ($id) { return 'User ' . $id; }); ~~~ 这样我们在浏览器中访问 `http://blog.test/user/1`,就会得到以下输出: ~~~ User 1 ~~~ 可以根据需要在路由中定义多个路由参数: ~~~ Route::get('posts/{post}/comments/{comment}', function ($postId, $commentId) { return $postId . '-' . $commentId; }); ~~~ 根据上面的示例,路由参数需要通过花括号 `{}` 进行包裹并且是拼音字母,这些参数在路由被执行时会被传递到路由的闭包。路由参数名称不能包含 `-` 字符,如果需要的话可以使用 `_` 替代,比如如果某个路由参数定义成 `{post-id}` 则访问路由会报错,应该修改成 `{post_id}` 才行。路由参数被注入到路由回调/控制器取决于它们的顺序,与回调/控制器名称无关。 ## 可选参数 有必选参数就有可选参数,这可以通过在参数名后加一个 `?` 标记来实现,这种情况下需要给相应的变量指定默认值,当对应的路由参数为空时,使用默认值: ~~~ Route::get('user/{name?}', function ($name = null) { return $name; }); Route::get('user/{name?}', function ($name = 'John') { return $name; }); ~~~ 这时如果定义的路由是下面这个的话,访问 `http://blog.test/user` 会返回 `John`。 ## 正则约束 可以通过路由实例上的 `where` 方法来约束路由参数的格式。`where` 方法接收参数名和一个正则表达式来定义该参数如何被约束: ~~~ Route::get('user/{name}', function ($name) { // $name 必须是字母且不能为空 })->where('name', '[A-Za-z]+'); Route::get('user/{id}', function ($id) { // $id 必须是数字 })->where('id', '[0-9]+'); Route::get('user/{id}/{name}', function ($id, $name) { // 同时指定 id 和 name 的数据格式 })->where(['id' => '[0-9]+', 'name' => '[a-z]+']); ~~~ 使用正则约束还有一个好处就是避免了 `user/{id}` 和 `user/{name}` 的混淆。 **全局约束** 如果想要路由参数在全局范围内被给定正则表达式约束,可以使用 `pattern` 方法。需要在 `RouteServiceProvider` 类的 `boot` 方法中定义这种约束模式: ~~~ /** * 定义路由模型绑定,模式过滤器等 * * @param \Illuminate\Routing\Router $router * @return void * @translator http://laravelacademy.org */ public function boot() { Route::pattern('id', '[0-9]+'); parent::boot(); } ~~~ 一旦模式被定义,将会自动应用到所有包含该参数名的路由中: ~~~ Route::get('user/{id}', function ($id) { // 只有当 {id} 是数字时才会被调用 }); ~~~ 除此之外,该模式还会被应用到诸如下面这些路由参数上: ~~~ Route::get('post/{id}', function ($id) { // 只有当 {id} 是数字时才会被调用 }); Route::get(`product/{id}', function ($id) { // 只有当 {id} 是数字时才会被调用 }); ~~~ 很显然这种方式让代码更简洁,也为我们实现同一参数统一约束带来了方便。 ## 命名路由 命名路由为生成 URL 或重定向提供了方便,实现起来也很简单,在路由定义之后使用 `name` 方法链的方式来定义该路由的名称: ~~~ Route::get('user/profile', function () { // 通过路由名称生成 URL return 'my url: ' . route('profile'); })->name('profile'); ~~~ 还可以为控制器动作指定路由名称: `Route::get('user/profile', 'UserController@showProfile')->name('profile');` 这样我们就可以通过以下方式定义重定向: ~~~ Route::get('redirect', function() { // 通过路由名称进行重定向 return redirect()->route('profile'); }); ~~~ **为命名路由生成 URL** 正如上面代码所展示的,为给定路由分配名称之后,就可以通过辅助函数 `route` 为该命名路由生成 URL 或者通过 `redirect` 函数进行重定向: ~~~ // 生成URL $url = route('profile'); // 生成重定向 return redirect()->route('profile'); ~~~ 如果命名路由定义了参数,可以将该参数作为第二个参数传递给 `route` 函数。给定的路由参数将会自动插入到 URL 中: ~~~ Route::get('user/{id}/profile', function ($id) { $url = route('profile', ['id' => 1]); return $url; })->name('profile'); ~~~ 这样,当我们访问 `http://blog.test/user/123/profile` 页面输出内容也是 `http://blog.test/user/123/profile`。 **检查当前路由** 如果你想要判断当前请求是否被路由到给定命名路由,可以使用 Route 实例上的 `named` 方法,例如,你可以从路由中间件中检查当前路由名称: ~~~ /** * 处理输入请求 * * @param \Illuminate\Http\Request $request * @param \Closure $next * @return mixed */ public function handle($request, Closure $next) { if ($request->route()->named('profile')) { // } return $next($request); } ~~~ ## 路由分组 路由分组的目的是让我们在多个路由中共享相同的路由属性,比如中间件和命名空间等,这样的话我们定义了大量的路由时就不必为每一个路由单独定义属性。共享属性以数组的形式作为第一个参数被传递给 `Route::group` 方法。 # 中间件 要给某个路由分组中定义的所有路由分配中间件,可以在定义分组之前使用 `middleware` 方法。中间件将会按照数组中定义的顺序依次执行: ~~~ Route::middleware(['first', 'second'])->group(function () { Route::get('/', function () { // Uses first & second Middleware }); Route::get('user/profile', function () { // Uses first & second Middleware }); }); ~~~ 关于中间件的使用我们在后面单独讲`中间件`时再进行示例演示,这里我们先了解这样使用就行。 ## 命名空间 路由分组另一个通用的例子是使用 `namespace` 方法分配同一个 PHP 命名空间给该分组下的多个控制器: ~~~ Route::namespace('Admin')->group(function () { // Controllers Within The "App\Http\Controllers\Admin" Namespace }); ~~~ 默认情况下,`RouteServiceProvider` 在一个命名空间分组下引入所有路由文件,并指定所有控制器类所在的默认命名空间是 `App\Http\Controllers`,因此,我们在定义控制器的时候只需要指定命名空间 `App\Http\Controllers` 之后的部分即可。 关于命名空间后面我们单独讲控制器的时候还会再详细演示,这里先了解用法即可。 ## 子域名路由 路由分组还可以被用于处理子域名路由,子域名可以像 URI 一样被分配给路由参数,从而允许捕获子域名的部分用于路由或者控制器,子域名可以在定义分组之前调用 `domain` 方法来指定: ~~~ Route::domain('{account}.blog.dev')->group(function () { Route::get('user/{id}', function ($account, $id) { return 'This is ' . $account . ' page of User ' . $id; }); }); ~~~ 比如我们设置会员子域名为 `account.blog.test`,那么就可以通过 `http://account.blog.test/user/1` 访问用户ID为 `1` 的会员信息了: ~~~ This is account page of User 1 ~~~ ## 路由前缀 `prefix` 方法可以用来为分组中每个路由添加一个给定 URI 前缀,例如,你可以为分组中所有路由 URI 添加 `admin` 前缀 : ~~~ Route::prefix('admin')->group(function () { Route::get('users', function () { // Matches The "/admin/users" URL }); }); ~~~ 这样我们就可以通过 `http://blog.test/admin/users` 访问路由了。 ## 路由名称前缀 `name` 方法可通过传入字符串为分组中的每个路由名称设置前缀,例如,你可能想要在所有分组路由的名称前添加 `admin` 前缀,由于给定字符串和指定路由名称前缀字符串完全一样,所以需要在前缀字符串末尾后加上 . 字符: ~~~ Route::name('admin.')->group(function () { Route::get('users', function () { // 新的路由名称为 "admin.users"... })->name('users'); }); ~~~ # 路由模型绑定 注入模型 ID 到路由或控制器动作时,通常需要查询数据库才能获取相应的模型数据。Laravel 路由模型绑定让注入模型实例到路由变得简单,例如,你可以将匹配给定 ID 的整个 `User` 类实例注入到路由中,而不只是注入用户 ID。 ## 隐式绑定 Laravel 会自动解析定义在路由或控制器动作(变量名匹配路由片段)中的 Eloquent 模型类型声明,例如(我们将这个路由定义在 `routes/api.php` 文件中): ~~~ Route::get('users/{user}', function (App\User $user) { return $user->email; }); ~~~ 在这个例子中,由于类型声明了 Eloquent 模型 `App\User`,对应的变量名 $`user` 会匹配路由片段中的 `{user}`,这样,Laravel 会自动注入与请求 URI 中传入的 ID 对应的用户模型实例。如果匹配模型实例在数据库中不存在,会自动生成 404 响应。 在演示本功能之前,我们需要先创建数据表,由于我是在 Valet 开发环境中开发,需要自己创建数据库,我们将数据库命名为 `valet`,本地的数据库用户名为 `root`,密码为空,对应地,修改 `.env` 文件配置如下: ~~~ DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=valet DB_USERNAME=root DB_PASSWORD= ~~~ 具体配置值以你自己的开发环境设置为准。我们将基于 Laravel 强大的数据库迁移功能创建 `users` 表,关于数据库迁移后面在数据库部分会详细讨论,这里我们通过以下命令来生成 `users` 表即可: ~~~ php artisan migrate ~~~ 进入数据库可以看到该表已经生成: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15056082684042.jpg) 这时,`users` 数据表还没有任何记录,如果数据库中找不到对应的模型实例,会自动生成 HTTP 404 响应,提示页面不存在,所以我们需要在这张表中插入一条记录,这里我们基于 Laravel 强大的数据库填充器来快速完成数据填充功能,首先通过如下命令生成 `users` 对应的数据表填充器: ~~~ php artisan make:seeder UsersTableSeeder ~~~ 该命令会在 `database/seeds` 目录下生成一个 `UsersTableSeeder` 文件,编辑该文件内容如下: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2017/09/15056088409282.jpg) 然后编辑同目录下的 DatabaseSeeder.php 文件如下(取消调用数据表填充器前的注释): :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15056088911380.jpg) 最后执行 `php artisan db:seed` 即可插入对应数据到 `users` 表了,这样我们在浏览器中再次访问 `http://blog.test/api/users/1` 的时候就会显示 `User` 模型数据了: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2018/03/15056090941038.jpg) 接下来,你就可以在应用代码中直接拿 `$user` 模型去做你想做的事情了,而不需要自己去数据库查询,从而提高了开发的效率。 **自定义键名** 如果你想要在隐式模型绑定中使用数据表的其它字段而不是 `id` 字段,可以重写 Eloquent 模型类的 `getRouteKeyName` 方法,以 `User` 模型为例,可以在该模型类中添加这个方法 : ~~~ /** * Get the route key for the model. * * @return string */ public function getRouteKeyName() { return 'name'; } ~~~ 这样我们就可以通过 `http://blog.test/api/users/jroJoGP71W` 访问同一个模型实例了。这里需要注意的点是如果该字段不是唯一键,则会返回结果集的第一条记录,对应的底层实现在这里: :-: ![](http://static.laravelacademy.org/wp-content/uploads/2017/09/15056099610954.jpg) ## 显式绑定 有隐式绑定,就有显式绑定。要注册显式绑定,可以使用路由器的 `model` 方法来为给定参数指定绑定类。你需要在 `RouteServiceProvider` 类的 `boot` 方法中定义显式模型绑定: ~~~ public function boot() { parent::boot(); Route::model('user_model', App\User::class); } ~~~ 接下来,在 `routes/api.php` 中定义一个包含 `{user}` 参数的路由: ~~~ $router->get('profile/{user_model}', function(App\User $user) { dd($user); }); ~~~ 由于我们已经绑定 `{user_model}` 参数到 `App\User` 模型,`User` 实例会被注入到该路由。因此,如果请求 URL 是 `http://blog.test/api/profile/1`,就会注入一个用户 ID 为 `1` 的 `User` 实例。 如果匹配的模型实例在数据库不存在,会自动生成并返回 HTTP 404 响应。 **自定义解析逻辑** 如果你想要使用自定义的解析逻辑,可以在 `RouteServiceProvider` 类的 `boot` 方法中使用 `Route::bind` 方法,传递到 `bind` 方法的闭包会获取到 URI 请求参数中的值,并且返回你想要在该路由中注入的类实例: ~~~ public function boot() { parent::boot(); Route::bind('user', function($value) { return App\User::where('name', $value)->first() ?? abort(404); }); } ~~~ 有了这些方法,基本上可以满足你对路由模型绑定的各种需求了。 # 频率限制 Laravel 自带了一个中间件用于限制对应用路由的访问频率。开始使用该功能之前,分配 `throttle` 中间件到某个路由或路由分组,`throttle` 中间件接收两个参数用于判断给定时间内(单位:分钟)的最大请求次数。例如,我们指定登录用户每分钟只能访问下面的分组路由 60 次: ~~~ Route::middleware('auth:api', 'throttle:60,1')->group(function () { Route::get('/user', function () { // }); }); ~~~ 超出访问次数后,会返回 `429` 状态码并提示”Too many requests”。 **动态频率限制** 此外,还可以基于 `User` 模型的属性来动态设置最大请求次数。例如,如果 `User` 模型包含 `rate_limit` 属性,就可以将其这个属性名传递到 `throttle` 中间件,这样就可以将属性值作为计算最大请求次数的数据来源: ~~~ Route::middleware('auth:api', 'throttle:rate_limit,1')->group(function () { Route::get('/user', function () { // }); }); ~~~ # 表单方法伪造 HTML 表单不支持 `PUT`、`PATCH` 或者 `DELETE` 请求方法,因此,在 HTML 表单中调用 `PUT`、`PATCH` 或 `DELETE` 路由时,需要添加一个隐藏的 _method 字段,其值被用作该表单的 HTTP 请求方法: ~~~ <form action="/foo/bar" method="POST"> <input type="hidden" name="_method" value="PUT"> <input type="hidden" name="_token" value="{{ csrf_token() }}"> </form> ~~~ 还可以直接使用 Blade 指令 `@method` 来生成 `_method` 字段: ~~~ <form action="/foo/bar" method="POST"> @method('PUT') @csrf </form> ~~~ # 访问当前路由 你可以使用 `Route` 门面上的 `current`、`currentRouteName` 和 `currentRouteAction` 方法来访问处理当前输入请求的路由信息: ~~~ // 获取当前路由实例 $route = Route::current(); // 获取当前路由名称 $name = Route::currentRouteName(); // 获取当前路由action属性 $action = Route::currentRouteAction(); ~~~ 参考API文档了解路由门面底层类以及Route实例的更多可用方法。