## **简介**
对任何一个 Web 应用框架而言,通过 HTTP 协议处理用户请求并返回响应都是核心必备功能,也就是说,对于我们学习和使用一个 Web 框架,第一件要做的事情就是定义应用路由(使其指向要执行的代码并处理各种路有需求),否则将无法与终端用户进行交互。
## **路由入门**
在Laravel应用中,定义路由有两个入口:1. `routes/web.php`,用于处理终端用户通过 Web 浏览器直接访问的请求;2. `routes/api.php`,用于处理其他接入方的 API 请求(curl? 通常是跨语言、跨应用的请求)。在本章中,我们将主要聚焦于`routes/web.php`,关于`routes/api.php`将会后续介绍。
定义路由最简单的方式是在`routes/web.php`中定义一个路径以及一个映射到该路径的闭包函数:
```
//routes/web.php
Route::get('/',function(){
return 'Hello, Wrold';
});
```
这样,当我们访问应用首页`http://blog.test`时,就可以看到页面显示`Hello, World!`这一行字符串。
> 注:这里需要注意的是,我们并没有通过`echo`或`print`显示输出内容,而是通过`return`将其返回,Laravel 会通过内置的响应栈和中间件对返回内容进行处理。
很多简单的静态 Web 站点通过这种最基本的路由定义就可以完成了,比如一些企事业单位宣传网站,只有一些静态页面,通过几个 GET 路由以及视图模板就可以搞定了:
```
// 首页
Route::get('/', function () {
return view('welcome');
});
// 关于我们
Route::get('about', function () {
return view('about');
});
// 产品页
Route::get('products', function () {
return view('products');
});
// 服务页
Route::get('services', function () {
return view('services');
});
```
### **路由动作**
我们在上面的路由定义中使用了`Route::get`,这种语法的含义是只匹配 GET 请求路由。自然而然,Laravel 框架也为我们提供了 POST、PUT、DELETE 请求相应的路由定义方法:
```
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/{id}/{slug}', function ($id, $slug) {
return $id. ':' . $slug;
})->where(['id' => '[0-9]+', 'slug'=> '[A-Za-z]+']);
```
如果传入的路由参数与指定正则不匹配,则会返回404页面:
![](https://img.kancloud.cn/17/a1/17a19cf514caab8b0a867e38fd34d5b3_2766x1230.jpg)
### **路由命名**
在应用其他地方引用路由的最简单的方式就是通过定义路由的第一个路径参数,你可以在视图中通过辅助函数`url()`来引用指定路由,该函数会为传入路径加上完整的域名前缀,比如,你可以在视图文件中这么使用:
```
<a href ="{{ url('/') }}" >
//对应输出是 http://blog.test
```
此外,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
```
此外,我们还可以简化对路由参数的传递,比如上例可以简化为:
```
<a href="{{ route('user.profile', [100]) }}{" >
```
这样调用的话,数组中的参数顺序必须与定义路由时的参数顺序保持一致,而使用关联数组的方式传递参数则没有这样的约束。
> 注:在实际开发过程中,推荐使用路由命名来引用路由。
## **路由分组**
在日常开发中,我们通常会将具有某些共同特征的路由进行分组,这些特征包括是否需要认证、是否具有共同的路由前缀或者子域名、以及是否具有相同的控制器命名空间等,显然,对路由按照共同特征进行分组后可以避免重复为某些路由定义相同的路由特征,让代码更加简洁,可读性和可维护性更好。
所谓路由分组,就是通过`Route::group`将几个路由聚合到一起,然后给它们应用对应的共享特征:
```
Route::group([], function() {
Route::get('hello', function () { return 'Hello'; });
Route::get('world', function () { return 'World'; });
});
```
由于没有应用任何共享特征(第一个参数是空数组),所以这样的路由分组其实并没有什么意义,下面我们来介绍一些常见的共享特征设置。
### **中间件**
我们使用路由分组最常见的场景恐怕就是为一组路由应用共同的中间件了,关于中间件可以参考[官方文档](https://laravelacademy.org/post/9539.html),后面也会有单独章节来讲解,使用中间件可以对 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']。 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');
});
```
### **子域名路由**
子域名路由和路由路径前缀一样,不过是通过子域名而非路径前缀对分组路由进行约束,子域名路由有两个使用场景:
1. 为应用子系统设置不同的子域名:
```
Route::domain('admin.blog.test')->group(function () {
Route::get('/', function () {// 处理 http://admin.blog.test 路由});});
});
```
2. 通过参数方式设置子域名,适用于网站拥有多租户的场景(比如天猫,顶级知名商家拥有自己独立的子域名,如`https://xiaomi.tmall.com`):
```
Route::domain('{account}.blog.test')->group(function () {
Route::get('/', function ($account) {// });
Route::get('user/{id}', function ($account, $id) {// });
});
});
```
这种情况下,`$account`永远是所有分组路由的第一个路由参数。
### **子命名空间**
以控制器方式定义路由的时候,当我们没有显式指定控制器的命名空间时,默认的命名空间是`App\\Http\\Controllers`(在`app/Providers/RouteServiceProvider.php`中设置),如果某些控制器位于这个命名空间下的子命名空间中,该如何设置分组规则呢?我们可以通过`Route::namespace`为同一子命名空间下的分组路由设置共同的子命名空间:
```
Route::get('/', 'Controller@index');
Route::namespace('Admin')->goup(function() {
// App\Http\Controllers\Admin\AdminController
Route::get('/admin', 'AdminController@index');
});
```
### **路由命名前缀**
除了通过上述共同特征对路由进行分组外,对于某一类资源路由,比如用户,往往拥有相同的路由命名前缀,如`user.`,我们还可以基于这一特征对路由进行分组,使用`Route::name`方法即可实现:
```
// 路由命名+路径前缀
Route::name('user.')->prefix('user')->group(function () {
Route::get('{id?}', function ($id = 1) {
// 处理 /user/{id} 路由,路由命名为 user.show
return route('user.show');
})->name('show');
Route::get('posts', function () {
// 处理 /user/posts 路由,路由命名为 user.posts
})->name('posts');
});
```
在这个示例中,我们通过链式调用的方式为该路由分组应用了路由命名前缀和路由路径前缀两个共享特征,我们还可以组合调用上述所有五个特征,调用方法参考上面这种链式调用,从而组合出更加复杂的分组规则。
## **兜底路由**
在 Laravel 5.6 中,引入了兜底路由功能。所谓兜底路由,就是当路由文件中定义的所有路由都无法匹配用户请求的 URL 时,用来处理用户请求的路由,在此之前,Laravel 都会通过异常处理器为这种请求返回 404 响应,使用兜底路由的好处是我们可以对这类请求进行统计并进行一些自定义的操作,比如重定向,或者一些友好的提示什么的,兜底路由可以通过`Route::fallback`来定义:
```
Route::fallback(function () { return '我是最后的屏障';})
```
## **频率限制**
在 Laravel 5.6 中,还引入了频率限制功能。所谓频率限制,指的是在指定时间单个用户对某个路由的访问次数限制,该功能有两个使用场景:1. 在某些需要验证/认证的页面限制用户失败尝试次数,提高系统安全性;2. 避免非正常用户(比如爬虫)对路由的过度频繁访问,从而提高系统的可用性。此外,在流量高峰期还可以借助此功能进行有效的限流。
在 Laravel 中该功能通过内置的`throttle`中间件来实现,该中间件接收两个参数,第一个是次数上限,第二个是指定时间段(单位:分钟):
```
Route::middleware('throttle:60,1')->group(function() {
Route::get('/users', 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`(运行此命令之前先要清理之前的缓存),即只在生产环境中使用路由缓存,本地开发环境路由经常变动,且没有性能方面的考虑,无需缓存。