Web開發中不可避免的要使用到URL。用得最多的,就是生成一個指向應用中其他某個頁面的URL了。 開發者需要一個簡潔的、集中的、統一的方法來完成這一過程。
否則的話,在代碼中寫入大量的諸如?http://www.digpage.com/post/view/100?的代碼,一是過于冗長,二是易出錯且難排查, 三是日后修改起來容易有遺漏。因此,從開發角度來講,需要一種更簡潔、可以統一管理、 又能排查錯誤的解決方案。
同時,我們在?[_附錄2:Yii的安裝_](http://www.digpage.com/install.html#install)?部分講解了如何為Yii配置Web服務器,從中可以發現, 所有的用戶請求都是發送給入口腳本?index.php?來處理的。那么,Yii需要提供一種高效的分派 請求的方法,來判斷請求應當采用哪個 controller 哪個 action 進行處理。
結合以上2點需求,Yii為開發者提供了路由和URL管理組件。
所謂路由是指URL中用于標識用于處理用戶請求的module, controller, action的部分, 一般情況下由?r查詢參數來指定。 如?http://www.digpage.com/index.php?r=post/view&id=100?, 表示這個請求將由PostController 的 actionView來處理。
同時,Yii也提供了一種美化URL的功能,使得上面的URL可以用一個比較整潔、美觀的形式表現出來, 如?http://www.digpage.com/post/view/100?。 這個功能的實現是依賴于一個稱為 urlManager 的應用組件。
使用 urlManager 開發者可以解析用戶的請求,并指派相應的module, controller和action來進行處理, 還可以根據預義的路由規則,生成需要的URL返回給用戶使用。 簡而言之,urlManger具有解析請求以便確定指派誰來處理請求和根據路由規則生成URL 2個功能。
## 美化URL[](http://www.digpage.com/route.html#url "Permalink to this headline")
一般情況下,Yii應用生成和接受形如?http://www.digpage.com/index.php?r=post/view&id=100?的URL。這個URL分成幾個部分:
* 表示主機信息的?http://www.digapge.com
* 表示入口腳本的?index.php
* 表示路由的?r=post/view
* 表示普通查詢參數的?id=100
其中,主機信息部分從URL來講,一般是不能少的。當然內部鏈接可以使用相對路徑,這種情況下看似 可以省略,但是User Agent最終發出Request時,也是包含主機信息的。換句話說,Web Server接收并 轉交給Yii處理的URL,是完整的、帶有主機信息的URL。
而入口腳本?index.php?我們知道,Web Server會將所有的請求都是交由其進行處理。 也就是說,Web Server應當視所有的URL為請求?index.php?腳本。這在?:ref:install?部分我們 已經對Web Server進行過相應配置了。如Nginx:
~~~
location / {
try_files $uri $uri/ /index.php?$args;
}
~~~
即然這樣,URL中有沒有指定?index.php?已經不重要了,反正都是請求的它。 在URL里面假惺惺地留個?index.php?,實在是畫蛇添足。 因此,Yii允許我們不在URL中出現入口腳本?index.php?。
其次,路由信息對于Yii應用而言也必不可少,表明應當使用哪個controller和action來處理請求, 否則Yii只能使用默認的路由來處理請求。這個形式比較固定,采用的是一種類似路徑的形式, 一般為module/controller/action?之類的。
如果將URL省略掉入口腳本,并將路由信息轉換成路徑,上面的URL就會變成:http://www.digpage.com/post/view?id=100?, 是不是看起來舒服很多?
這樣的鏈接看起來簡潔美觀,對于用戶比較友好。同時,也比較適合搜索引擎的胃口, 據說是SEO的手段之一。
但到了這里還沒完,對于查詢參數?id=100?而言,這個URL請求的是編號為100的一個POST, 并執行view操作。那么我們可以再進一步改成?http://www.digpage.com/post/view/100?。 這樣是不是更爽?
有處女座的說了,這個編號100跟前面的字母們放一起顯得另類呀,要是都是字母的就更好了。 那我們假如所請求的編號100的文章,其標題為?Route?, 那么不妨使用用http://www.digpage.com/post/view/Route?來訪問。
這樣的話,干脆再加上?.html?好了。 變成?http://www.digpage.com/post/view/Route.html?, 這樣的URL對比原來,堪稱完美了吧?豈不是連處女座也滿意了?
我們把 URL?http://www.digpage.comindex.php?r=post/view&id=100?變成http://www.digpage.com/post/view/Route.html?的過程就稱為URL美化。
Yii有專門的?yii\web\UrlManager?來進行處理,其中:
* 隱藏入口腳本可以通過?yii\web\UrlManager::showScriptName?=?false?來實現
* 路由的路徑化可以通過?yii\web\UrlManager::enablePrettyUrl?=?true?來實現
* 參數的路徑化可以通過路由規則來實現
* 假后綴(fake suffix)?.html?可以通過?yii\web\UrlManager::suffix?=?'.html'?來實現
這里點一點,有個印象就可以下,在?[_Url管理_](http://www.digpage.com/urlmanager.html#urlmanager)?部分就會講到了。
## 路由規則[](http://www.digpage.com/route.html#id2 "Permalink to this headline")
所謂孤掌難鳴,urlManager要發揮功能靠單打獨斗是不行的,還要有另外一個的東東來配合。 這就是我們本篇要重點講的:路由規則。
路由規則是指 urlManager 用于解析請求或生成URL的規則。 一個路由規則必須實現yii\web\UrlRuleInterface?接口,這個接口定義了兩個方法:
* 用于解析請求的?yii\web\UrlRuleInterface::parseRequest()
* 用于生成URL的?yii\web\UrlRuleInterface::createUrl()
Yii中,使用?yii\web\UrlRule?來表示路由規則,一般這個類是足夠開發者使用的。 但是,如果開發者想自己實現解析請求或生成URL的邏輯,可以以這個類為基類進行派生, 并重載?parseRuquest()?和createUrl()?。
以下是配置文件中urlManager組件的路由規則配置部分,以幾個相對簡單、典型的路由規則的為例, 先有個感性認識:
~~~
'rules' => [
// 為路由指定了一個別名,以 post 的復數形式來表示 post/index 路由
'posts' => 'post/index',
// id 是命名參數,post/100 形式的URL,其實是 post/view&id=100
'post/<id:\d+>' => 'post/view',
// controller action 和 id 以命名參數形式出現
'<controller:(post|comment)>/<id:\d+>/<action:(create|update|delete)>'
=> '<controller>/<action>',
// 包含了 HTTP 方法限定,僅限于DELETE方法
'DELETE <controller:\w+>/<id:\d+>' => '<controller>/delete',
// 需要將 Web Server 配置成可以接收 *.digpage.com 域名的請求
'http://<user:\w+>.digpage.com/<lang:\w+>/profile' => 'user/profile',
]
~~~
上面的例子并沒有窮盡路由規則的例子,可以玩的花樣還有很多。至于這些例子所表達的規則, 讀者朋友們可以發揮想像去猜測,相信你們絕對可以猜個八九不離十。
目前不需要了解太多,只需大致了解上面這個數組用于為urlManager聲明路由規則。 數組的鍵相當于請求(需要解析的或將要生成的),而元素的值則對應的路由, 即?controller/action?。請求部分可稱為pattern,路由部分則可稱為route。 對于這2個部分的形式,大致上可以這么看:
* pattern 是從正則表達式變形而來。去除了兩端的?/?#?等分隔符。 特別注意別在pattern兩端畫蛇添足加上分隔符。
* pattern 中可以使用正則表達式的命名參數,以供route部分引用。這個命名參數也是變形了的。 對于原來?(?Ppattern)?的命名參數,要變形成??。
* pattern 中可以使用HTTP方法限定。
* route 不應再含有正則表達式,但是可以按??的形式引用命名參數。
也就是說,解析請求時,Yii從左往右使用這個數組;而生成URL時Yii從右往左使用這個數組。
至于具體實現過程,我們馬上就會講。
首先是?yii\web\UrlRule?的代碼,讓我們來大致看一看:
~~~
class UrlRule extends Object implements UrlRuleInterface
{
// 用于 $mode 表示路由規則的2種工作模式:僅用于解析請求和僅用于生成URL。
// 任意不為1或2的值均表示兩種模式同時適用,
// 一般未設定或為0時即表示兩種模式均適用。
const PARSING_ONLY = 1;
const CREATION_ONLY = 2;
// 路由規則名稱
public $name;
// 用于解析請求或生成URL的模式,通常是正則表達式
public $pattern;
// 用于解析或創建URL時,處理主機信息的部分,如 http://www.digpage.com
public $host;
// 指向controller 和 action 的路由
public $route;
// 以一組鍵值對數組指定若干GET參數,在當前規則用于解析請求時,
// 這些GET參數會被注入到 $_GET 中去
public $defaults = [];
// 指定URL的后綴,通常是諸如 ".html" 等,
// 使得一個URL看起來好像指向一個靜態頁面。
// 如果這個值未設定,使用 UrlManager::suffix 的值。
public $suffix;
// 指定當前規則適用的HTTP方法,如 GET, POST, DELETE 等。
// 可以使用數組表示同時適用于多個方法。
// 如果未設定,表明當前規則適用于所有方法。
// 當然,這個屬性僅在解析請求時有效,在生成URL時是無效的。
public $verb;
// 表明當前規則的工作模式,取值可以是 0, PARSING_ONLY, CREATION_ONLY。
// 未設定時等同于0。
public $mode;
// 表明URL中的參數是否需要進行url編碼,默認是進行。
public $encodeParams = true;
// 用于生成新URL的模板
private $_template;
// 一個用于匹配路由部分的正則表達式,用于生成URL
private $_routeRule;
// 用于保存一組匹配參數的正則表達式,用于生成URL
private $_paramRules = [];
// 保存一組路由中使用的參數
private $_routeParams = [];
// 初始化
public function init() {...}
// 用于解析請求,由UrlRequestInterface接口要求
public function parseRequest($manager, $request) {...}
// 用于生成URL,由UrlRequestInterface接口要求
public function createUrl($manager, $route, $params) {...}
}
~~~
從上面代碼看,?UrlRule?的屬性(可配置項)比較多。各屬性的意義在注釋中已經寫清楚了,這里就不再復述。 但是我們要著重分析一下初始化函數?yii\web\UrlRule::init()?,來加深對這些屬性的理解:
~~~
public function init()
{
// 一個路由規則必定要有 pattern ,否則是沒有意義的,
// 一個什么都沒規定的規定,要來何用?
if ($this->pattern === null) {
throw new InvalidConfigException('UrlRule::pattern must be set.');
}
// 不指定規則匹配后所要指派的路由,Yii怎么知道將請求交給誰來處理?
// 不指定路由,Yii怎么知道這個規則可以為誰創建URL?
if ($this->route === null) {
throw new InvalidConfigException('UrlRule::route must be set.');
}
// 如果定義了一個或多個verb,說明規則僅適用于特定的HTTP方法。
// 既然是HTTP方法,那就要全部大寫。
// verb的定義可以是字符串(單一的verb)或數組(單一或多個verb)。
if ($this->verb !== null) {
if (is_array($this->verb)) {
foreach ($this->verb as $i => $verb) {
$this->verb[$i] = strtoupper($verb);
}
} else {
$this->verb = [strtoupper($this->verb)];
}
}
// 若未指定規則的名稱,那么使用最能區別于其他規則的 $pattern
// 作為規則的名稱
if ($this->name === null) {
$this->name = $this->pattern;
}
// 刪除 pattern 兩端的 "/",特別是重復的 "/",
// 在寫 pattern 時,雖然有正則的成分,但不需要在兩端加上 "/",
// 更不能加上 "#" 等其他分隔符
$this->pattern = trim($this->pattern, '/');
// 如果定義了 host ,將 host 部分加在 pattern 前面,作為新的 pattern
if ($this->host !== null) {
// 寫入的host末尾如果已經包含有 "/" 則去掉,特別是重復的 "/"
$this->host = rtrim($this->host, '/');
$this->pattern = rtrim($this->host . '/' . $this->pattern, '/');
// 既未定義 host ,pattern 又是空的,那么 pattern 匹配任意字符串。
// 而基于這個pattern的,用于生成的URL的template就是空的,
// 意味著使用該規則生成所有URL都是空的。
// 后續也無需再作其他初始化工作了。
} elseif ($this->pattern === '') {
$this->_template = '';
$this->pattern = '#^$#u';
return;
// pattern 不是空串,且包含有 '://',以此認定該pattern包含主機信息
} elseif (($pos = strpos($this->pattern, '://')) !== false) {
// 除 '://' 外,第一個 '/' 之前的內容就是主機信息
if (($pos2 = strpos($this->pattern, '/', $pos + 3)) !== false) {
$this->host = substr($this->pattern, 0, $pos2);
// '://' 后再無其他 '/',那么整個 pattern 其實就是主機信息
} else {
$this->host = $this->pattern;
}
// pattern 不是空串,且不包含主機信息,兩端加上 '/' ,形成一個正則
} else {
$this->pattern = '/' . $this->pattern . '/';
}
// route 也要去掉兩頭的 '/'
$this->route = trim($this->route, '/');
// 從這里往下,請結合流程圖來看
// route 中含有 <參數> ,則將所有參數提取成 [參數 => <參數>]
// 存入 _routeParams[],
// 如 ['controller' => '<controller>', 'action' => '<action>'],
// 留意這里的短路判斷,先使用 strpos(),快速排除無需使用正則的情況
if (strpos($this->route, '<') !== false &&
preg_match_all('/<(\w+)>/', $this->route, $matches)) {
foreach ($matches[1] as $name) {
$this->_routeParams[$name] = "<$name>";
}
}
// 這個 $tr[] 和 $tr2[] 用于字符串的轉換
$tr = [
'.' => '\\.',
'*' => '\\*',
'$' => '\\$',
'[' => '\\[',
']' => '\\]',
'(' => '\\(',
')' => '\\)',
];
$tr2 = [];
// pattern 中含有 <參數名:參數pattern> ,
// 其中 ':參數pattern' 部分是可選的。
if (preg_match_all('/<(\w+):?([^>]+)?>/', $this->pattern, $matches,
PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
foreach ($matches as $match) {
// 獲取 “參數名”
$name = $match[1][0];
// 獲取 “參數pattern” ,如果未指定,使用 '[^\/]' ,
// 表示匹配除 '/' 外的所有字符
$pattern = isset($match[2][0]) ? $match[2][0] : '[^\/]+';
// 如果 defaults[] 中有同名參數,
if (array_key_exists($name, $this->defaults)) {
// $match[0][0] 是整個 <參數名:參數pattern> 串
$length = strlen($match[0][0]);
$offset = $match[0][1];
// pattern 中 <參數名:參數pattern> 兩頭都有 '/'
if ($offset > 1 && $this->pattern[$offset - 1] === '/'
&& $this->pattern[$offset + $length] === '/') {
// 留意這個 (?P<name>pattern) 正則,這是一個命名分組。
// 僅冠以一個命名供后續引用,使用上與直接的 (pattern) 沒有區別
// 見:http://php.net/manual/en/regexp.reference.subpatterns.php
$tr["/<$name>"] = "(/(?P<$name>$pattern))?";
} else {
$tr["<$name>"] = "(?P<$name>$pattern)?";
}
// defaults[]中沒有同名參數
} else {
$tr["<$name>"] = "(?P<$name>$pattern)";
}
// routeParams[]中有同名參數
if (isset($this->_routeParams[$name])) {
$tr2["<$name>"] = "(?P<$name>$pattern)";
// routeParams[]中沒有同名參數,則將 參數pattern 存入 _paramRules[] 中。
// 留意這里是怎么對 參數pattern 進行處理后再保存的。
} else {
$this->_paramRules[$name] = $pattern === '[^\/]+' ? '' :
"#^$pattern$#u";
}
}
}
// 將 pattern 中所有的 <參數名:參數pattern> 替換成 <參數名> 后作為 _template
$this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern);
// 將 _template 中的特殊字符及字符串使用 tr[] 進行轉換,并作為最終的pattern
$this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u';
// 如果指定了 routePrams 還要使用 tr2[] 對 route 進行轉換,
// 并作為最終的 _routeRule
if (!empty($this->_routeParams)) {
$this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u';
}
}
~~~
上面的代碼難點在于pattern等的轉換過程,有點翻來覆去,轉換過去、轉換回來的感覺,這里我們先放一放, 秋后再找他們來算帳,注意力先放在?init()?的前半部分,這些代碼提醒我們:
* 規則的?$pattern?和?$route?是必須配置的。
* 規則的名稱?$name?和主機信息?$host?在未配置的情況下,可以從?$pattern?來獲取。
* $pattern?雖然含有正則的成分,但不需要在兩端加入?/?,更不能使用?#?等其他分隔符。 Yii會自動為我們加上。
* 指定?$pattern?為空串,可以使該規則匹配任意的URL。此時基于該規則所生成的所有URL也都是空串。
* $pattern?中含有?:\\?時,Yii會認為其中包含了主機信息。此時就不應當再指定 host 。 否則,Yii會將 host 接在這個 pattern 前,作為新的pattern。這會造成該pattern 兩段?:\\?, 而這顯然不是我們要的。
接下來要啃稍硬點的骨頭了,就是?init()?的后半段, 我們以一個普通的['post//'?=>?'post/']?為例。 同時,我們假設這個路由規則默認有$defaults['id']?=?100?,表示在未指定 post 的 id 時, 使用100作為默認的id。那么這個UrlRule的初始過程如?[_UrlRule路由規則初始化過程示意圖_](http://www.digpage.com/route.html#img-urlrule-init)?所示。
[](http://www.digpage.com/_images/UrlRule_init.png)
UrlRule路由規則初始化過程示意圖
后續的初始化過程具體如下:
1. 從?['post//'?=>?'post/']?, 我們有$pattern?=?'/post///'?和?$route?=?'post/'?。
2. 首先從?$route?中提取出由??>?所包含的部分。這里可以得到?['action'?=>?'']?。 將其存入?$_routeParams[?]?中。
3. 再從?$pattern?中提取出由??>?所包含的部分,這里匹配2個部分,1個??和1個?。下面對這2個部分進行分別處理。
4. 對于??由于?$defaults[?]?中不存在下標為?action?的元素,于是向?$tr[?]?寫入(?P$pattern)?形式的元素,得到?$tr['']?=?'(?P\w+)'?。 而對于?,由于?$defaults['id']?=?100?,所以寫入?$tr[?]?的元素形式有所不同, 變成(/(?P$pattern))??。于是有?$tr['']?=?(/(?P\d+))??。
5. 由于在第1步只有?$_routeParams['action']?=?''?,而沒有下標為?id?的元素。 所以對于?,往?tr2[?]?中寫入?[''?=>?'(?P\w+)']?, 而對于??則往$_paramRules[?]?中寫入?['id'?=>?'#^\d+$#u']?。
6. 上面只是準備工作,接下來開始各種替換。首先將?$pattern?中所有??替換成?并作為?$_template?。因此,?$_template?=?'/post///'?。
7. 接下來用?$tr[?]?對?$_template?進行替換,并在兩端加上分隔符作為?$pattern?。 于是有$pattern?=?'#^post/(?P\w+)(/(?P\d+))?$#u'?。
8. 最后,由于第1步中?$_routeParams?不為空,所以需要使用?$tr2[?]?對?$route?進行替換, 并在兩端加上分隔符后,作為?$_routeRule?,于是有?$_routeRule?=?'#^post/(?P\w+)$#'?。
這些替換的意義在于方便開發者以簡潔的方式在配置文件中書寫路由規則,然后將這些簡潔的規則, 再替換成規范的正則表達式。讓我們來看看這個?init()?的成果吧。仍然以上面的['post//'?=>?'post/']?為例,經過?init()?處理后,我們最終得到了:
~~~
$urlRule->route = 'post/<action>';
$urlRule->pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u';
$urlRule->_template = '/post/<action>/<id>/';
$urlRule->_routeRule = '#^post/(?P<action>\w+)$#';
$urlRule->_routeParams = ['action' => '<action>'];
$urlRule->_paramRules = ['id' => '#^\d+$#u'];
// $tr 和 $tr2 作為局部變量已經完成歷史使命光榮退伍了
~~~
下面我們來講講 UrlRule 是如何創建和解析URL的。
## 創建URL[](http://www.digpage.com/route.html#id3 "Permalink to this headline")
URL的創建就 UrlRule 層面來講,是由?yii\web\UrlRule::createUrl()?負責的, 這個方法可以根據傳入的路由和參數創建一個相應的URL來。具體代碼如下:
~~~
public function createUrl($manager, $route, $params)
{
// 判斷規則是否僅限于解析請求,而不適用于創建URL
if ($this->mode === self::PARSING_ONLY) {
return false;
}
$tr = [];
// 如果傳入的路由與規則定義的路由不一致,
// 如 post/view 與 post/<action> 并不一致
if ($route !== $this->route) {
// 使用 $_routeRule 對 $route 作匹配測試
if ($this->_routeRule !== null && preg_match($this->_routeRule,
$route, $matches)) {
// 遍歷所有的 _routeParams
foreach ($this->_routeParams as $name => $token) {
// 如果該路由規則提供了默認的路由參數,
// 且該參數值與傳入的路由相同,則可以省略
if (isset($this->defaults[$name]) &&
strcmp($this->defaults[$name], $matches[$name]) === 0) {
$tr[$token] = '';
} else {
$tr[$token] = $matches[$name];
}
}
// 傳入的路由完全不能匹配該規則,返回
} else {
return false;
}
}
// 遍歷所有的默認參數
foreach ($this->defaults as $name => $value) {
// 如果默認參數是路由參數,如 <action>
if (isset($this->_routeParams[$name])) {
continue;
}
// 默認參數并非路由參數,那么看看傳入的 $params 里是否提供該參數的值。
// 如果未提供,說明這個規則不適用,直接返回。
if (!isset($params[$name])) {
return false;
// 如果 $params 提供了該參數,且參數值一致,則 $params 可省略該參數
} elseif (strcmp($params[$name], $value) === 0) {
unset($params[$name]);
// 且如果有該參數的轉換規則,也可置為空。等下一轉換就消除了。
if (isset($this->_paramRules[$name])) {
$tr["<$name>"] = '';
}
// 如果 $params 提供了該參數,但又與默認參數值不一致,
// 且規則也未定義該參數的正則,那么規則無法處理這個參數。
} elseif (!isset($this->_paramRules[$name])) {
return false;
}
}
// 遍歷所有的參數匹配規則
foreach ($this->_paramRules as $name => $rule) {
// 如果 $params 傳入了同名參數,且該參數不是數組,且該參數匹配規則,
// 則使用該參數匹配規則作為轉換規則,并從 $params 中去掉該參數
if (isset($params[$name]) && !is_array($params[$name])
&& ($rule === '' || preg_match($rule, $params[$name]))) {
$tr["<$name>"] = $this->encodeParams ?
urlencode($params[$name]) : $params[$name];
unset($params[$name]);
// 否則一旦沒有設置該參數的默認值或 $params 提供了該參數,
// 說明規則又不匹配了
} elseif (!isset($this->defaults[$name]) || isset($params[$name])) {
return false;
}
}
// 使用 $tr 對 $_template 時行轉換,并去除多余的 '/'
$url = trim(strtr($this->_template, $tr), '/');
// 將 $url 中的多個 '/' 變成一個
if ($this->host !== null) {
// 再短的 host 也不會短于 8
$pos = strpos($url, '/', 8);
if ($pos !== false) {
$url = substr($url, 0, $pos) . preg_replace('#/+#', '/',
substr($url, $pos));
}
} elseif (strpos($url, '//') !== false) {
$url = preg_replace('#/+#', '/', $url);
}
// 加上 .html 之類的假后綴
if ($url !== '') {
$url .= ($this->suffix === null ? $manager->suffix : $this->suffix);
}
// 加上查詢參數們
if (!empty($params) && ($query = http_build_query($params)) !== '') {
$url .= '?' . $query;
}
return $url;
}
~~~
我們以上面提到?['post//'?=>?'post/']?路由規則來創建一個URL, 就假設要創建路由為?post/view?,?id=100?的URL吧。具體的流程如?[_UrlRule創建URL的流程示意圖_](http://www.digpage.com/route.html#img-urlrule-create)?所示。
[](http://www.digpage.com/_images/UrlRule_create.png)
UrlRule創建URL的流程示意圖
結合代碼?[_UrlRule創建URL的流程示意圖_](http://www.digpage.com/route.html#img-urlrule-create)?,URL的創建過程大體上分4個階段:
第一階段
調用?createUrl(Yii::$app->urlManager,?'post/view',?['id'=>101])?。
傳入的路由為?post/view?與規則定義的路由?post/?不同。 但是,?post/view?可以匹配路由規則的?$_routeRule?=?'#^post/(?P\w+)$#'?。 所以,認為該規則是適用的,可以接著處理。而如果連正則也匹配不上,那就說明該規則不適用,返回?false?。
遍歷路由規則的所有?$_routeParams?,這個例子中,?$_routeParams['action'?=>?'']?。 這個我們稱為路由參數規則,即出現在路由部分的參數。
對于這個路由參數規則,我們并未為其設置默認值。但實際使用中,有的時候可能會提供默認的路由參數, 比如對于形如?post/index?之類的,我們經常想省略掉?index?,那么就可以為??提供一個默認值?index?。
對于有默認值的情況,我們的頭腦要清醒,目前是要用路由規則來創建URL, 規則所定義的默認值并非意味著可以處理不提供這個路由參數值的路由, 而是說在處理路由參數值與默認值相等的路由時,最終生成的URL中可以省略該默認值。
即默認是體現在最終的URL上,是體現在URL解析過程中的默認,而不是體現在創建URL的過程中。 也就是說,對于?post/index?類的路由,如果默認?index?,則生成的URL中可以不帶有?index?。
這里沒有默認值,相對簡單。由于?$_routeRule?正則中,使用了命名分組,即?(?P...)?。 所以,可以很方便地使用?$matches['action']?來捕獲?\w+?所匹配的部分,這里匹配的是?view?。 故寫入?$tr['']?=?view?。
第二階段
接下來遍歷所有的默認參數,當然不包含路由參數部分,因為這個在前面已經處理過了。這里只處理余下的參數。 注意這個默認值的含義,如同我們前面提到的,這里是創建時必須提供,而生成出來的URL可以省略的意思。 因此,對于?$params?中未提供相應參數的,或提供了參數值但與默認值不一致,且規則沒定義參數的正則的, 均說明規則不適用。 只有在?$params?提供相應參數,且參數值與默認值一致或匹配規則時方可進行后續處理。
這里我們有一個默認參數?['id'?=>?100]?。傳入的?$params['id']?=>?100?。兩者一致,規則適用。
于將該參數從?$params?中刪去。
接下來,看看是否為參數?id?定義了匹配規則。還真有?$_paramRules['id']?=>?'#^\d+$#u'?。 但這也用不上了,因為這是在創建URL,該參數與默認值一致,等下的?id?是要從URL中去除的。 因此,寫入?$tr['']?=?''?。
第三階段
再接下來,就是遍歷所有參數匹配規則?$_paramRules?了。對于這個例子, 只有$_paramRules?=?['id'?=>?'#^\d+$#u']?。
如果?$params?中并未定義該?id?參數,那么這一步什么也不用做,因為沒有東西要寫到URL中去。
而一旦定義了?id?,那么就需要看看當前路由規則是否適用了。 判斷的標準是所提供的參數不是數組,且匹配?$_paramRules?所定義的規則。 而如果?$parasm['id']?是數組,或不與規則匹配,或定義了?id?的參數規則卻沒有定義其默認值而?$params['id']?又未提供,則規則不適用。
這里,我們在是在前面處理默認參數時,已經將?id?從?$params?中刪去。 但判斷到規則為?id?定義了默認值的,所以認為規則仍然適用。只是,這里實際上不用做任何處理。 如果需要處理的情況,也是將該參數從?$params?中刪去,然后寫入?$tr['']?=?100?。
第四階段
上面一切準備就緒之后,就可以著手生成URL了。主要用?$tr?對路由規則的?$_template?進行轉換。 這里,?$_template?=?'/post///'?,因此,轉換后再去除多余的?/?就變成了$url?=?'post/view'?。其中?id=100?被省略掉了。
最后再分別接上?.html?的后綴和查詢參數,一個URL?post/view.html?就生成了。 其中,查詢參數串是?$params?中剩下的內容,使用PHP的?http_build_query(?)?生成的。
從創建URL的過程來看,重點是完成這么幾項工作:
* 看規則是否適用,主要標準是路由與規則定義的是否匹配,規則通過默認值或正則所定義的參數,是否都提供了。
* 看看當前要創建的URL是否與規則定義的默認的路由參數和查詢參數一致,對于一致的,可以省略。
* 看將這些與默認值一致的,規則已經定義了的參數從?$params?刪除,余下的,轉換成最終URL的查詢參數串。
## 解析URL[](http://www.digpage.com/route.html#parse-url "Permalink to this headline")
說完了路由規則生成URL的過程,再來看看其邏輯上的逆過程,即URL的解析。
先從路由規則?yii\web\UrlRule::parseRequest()?的代碼入手:
~~~
public function parseRequest($manager, $request)
{
// 當前路由規則僅限于創建URL,直接返回 false。
// 該方法返回false表示當前規則不適用于當前的URL。
if ($this->mode === self::CREATION_ONLY) {
return false;
}
// 如果規則定義了適用的HTTP方法,則要看當前請求采用的方法是否可以接受
if (!empty($this->verb) && !in_array($request->getMethod(),
$this->verb, true)) {
return false;
}
// 獲取URL中入口腳本之后、查詢參數 ? 號之前的全部內容,即為PATH_INFO
$pathInfo = $request->getPathInfo();
// 取得配置的 .html 等假后綴,留意 (string)null 轉成空串
$suffix = (string) ($this->suffix === null ? $manager->suffix :
$this->suffix);
// 有假后綴且有PATH_INFO
if ($suffix !== '' && $pathInfo !== '') {
$n = strlen($suffix);
// 當前請求的 PATH_INFO 以該假后綴結尾,留意 -$n 的用法
if (substr_compare($pathInfo, $suffix, -$n, $n) === 0) {
$pathInfo = substr($pathInfo, 0, -$n);
// 整個PATH_INFO 僅包含一個假后綴,這是無效的。
if ($pathInfo === '') {
return false;
}
// 應用配置了假后綴,但是當前URL卻不包含該后綴,返回false
} else {
return false;
}
}
// 規則定義了主機信息,即 http://www.digpage.com 之類,那要把主機信息接回去。
if ($this->host !== null) {
$pathInfo = strtolower($request->getHostInfo()) .
($pathInfo === '' ? '' : '/' . $pathInfo);
}
// 當前URL是否匹配規則,留意這個pattern是經過 init() 轉換的
if (!preg_match($this->pattern, $pathInfo, $matches)) {
return false;
}
// 遍歷規則定義的默認參數,如果當前URL中沒有,則加入到 $matches 中待統一處理,
// 默認值在這里發揮作用了,雖然沒有,但仍視為捕獲到了。
foreach ($this->defaults as $name => $value) {
if (!isset($matches[$name]) || $matches[$name] === '') {
$matches[$name] = $value;
}
}
$params = $this->defaults;
$tr = [];
// 遍歷所有匹配項,注意這個 $name 的由來是 (?P<name>...) 的功勞
foreach ($matches as $name => $value) {
// 如果是匹配一個路由參數
if (isset($this->_routeParams[$name])) {
$tr[$this->_routeParams[$name]] = $value;
unset($params[$name]);
// 如果是匹配一個查詢參數
} elseif (isset($this->_paramRules[$name])) {
// 這里可能會覆蓋掉 $defaults 定義的默認值
$params[$name] = $value;
}
}
// 使用 $tr 進行轉換
if ($this->_routeRule !== null) {
$route = strtr($this->route, $tr);
} else {
$route = $this->route;
}
Yii::trace("Request parsed with URL rule: {$this->name}", __METHOD__);
return [$route, $params];
}
~~~
我們以?http://www.digpage.com/post/view.html?為例,來看看上面的代碼是如何解析成路由['post/view',?['id'=>100]]?的。注意,這里我們仍然假設路由規則提供了?id=100?默認值。 而如果路由規則未提供該默認值,則請求形式要變成?http://www.digapge.com/post/view/100.html?。 同時,規則的?$pattern?也會不同:
~~~
// 未提供默認值,id 必須提供,否則匹配不上
$pattern = '#^post/(?P<action>\w+)/(?P<id>\d+)?$#u';
// 提供了默認值,id 可以不提供,照樣可以匹配上
$pattern = '#^post/(?P<action>\w+)(/(?P<id>\d+))?$#u';
~~~
這個不同的原因在于?UrlRule::init()?,讀者朋友們可以回頭看看。
在講URL的解析前,讓我們先從請求的第一個經手人Web Server說起,在?[_附錄2:Yii的安裝_](http://www.digpage.com/install.html#install)?中講到Web Server的配置時, 我們將所有未命中的請求轉交給入口腳本來處理:
~~~
location / {
try_files $uri $uri/ /index.php?$args;
}
# fastcgi.conf
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
~~~
以Nginx為例,?try_files?會依次嘗試處理:
1. /post/view.html?,這個如果真有一個也就罷了,但其實多半不存在。
2. /post/view.html/?,這個目錄一般也不存在。
3. /index.php?,這個正是入口腳本,可以處理。至此,Nginx與Yii順利交接。
由于請求最終交給入口腳本來處理,且我們隱藏了URL中入口腳本名,上述請求還原回來的話, 應該是?http://www.digapge.com/index.php/post/view.html?。 自然,這?post/view.html?就是PATH_INFO了。 有關PATH_INFO的更多知識,請看?[_Web應用Request_](http://www.digpage.com/web_request.html#web-request)?部分的內容。
好了,在Yii從Web Server取得控制權之后,就是我們大顯身手的時候了。在解析過程中,UrlRule主要做了這么幾件事:
* 通過 PATH_INFO 還原請求,如去除假后綴,開頭接上主機信息等。還原后的請求為?post/view。
* 看看當前請求是否匹配規則,這個匹配包含了主機、路由、參數等各方面的匹配。如不匹配,說明規則不適用, 返回?false?。在這個例子中,規則并未定義主機信息方面的規則, 規則中$pattern?=?'#^post/(?P\w+)(/(?P\d+))?$#u'?。這與還原后的請求完全匹配。 如果URL沒有使用默認值?id?=?100?,如?post/view/101.html?,也是同樣匹配的。
* 看看請求是否提供了規則已定義了默認值的所有參數,如果未提供,視為請求提供了這些參數,且他的值為默認值。 這里URL中并未提供?id?參數,所以,視為他提供了?id?=?100?的參數。簡單粗暴而有效。
* 使用規則定義的路由進行轉換,生成新的路由。 再把上一步及當前所有參數作為路由的參數,共同組裝成一個完整路由。
具體的轉換流程可以看看?[_UrlRule路由規則解析URL的過程示意圖_](http://www.digpage.com/route.html#img-urlrule-parse)?。

UrlRule路由規則解析URL的過程示意圖
如果覺得《深入理解Yii2.0》對您有所幫助,也請[幫助《深入理解Yii2.0》](http://www.digpage.com/donate.html#donate)。 謝謝!
- 更新記錄
- 導讀
- Yii是什么
- Yii2.0的亮點
- 背景知識
- 如何閱讀本書
- Yii基礎
- 屬性(Property)
- 事件(Event)
- 行為(Behavior)
- Yii約定
- Yii應用的目錄結構和入口腳本
- 別名(Alias)
- Yii的類自動加載機制
- 環境和配置文件
- 配置項(Configuration)
- Yii模式
- MVC
- 依賴注入和依賴注入容器
- 服務定位器(Service Locator)
- 請求與響應(TBD)
- 路由(Route)
- Url管理
- 請求(Reqeust)
- Web應用Request
- Yii與數據庫(TBD)
- 數據類型
- 事務(Transaction)
- AcitveReocrd事件和關聯操作
- 樂觀鎖與悲觀鎖
- 《深入理解Yii2.0》視頻教程
- 第一講:基礎配置
- 第二講:用戶登錄
- 第三講:文章及評論的模型
- 附錄
- 附錄1:Yii2.0 對比 Yii1.1 的重大改進
- 附錄2:Yii的安裝
- 熱心讀者