## **簡介**
對任何一個 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頁面:

### **路由命名**
在應用其他地方引用路由的最簡單的方式就是通過定義路由的第一個路徑參數,你可以在視圖中通過輔助函數`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`(運行此命令之前先要清理之前的緩存),即只在生產環境中使用路由緩存,本地開發環境路由經常變動,且沒有性能方面的考慮,無需緩存。