Nginx 配置指令的執行順序(十一)
緊跟在 post-access
階段之后的是 try-files
階段。這個階段專門用于實現標準配置指令 try_files 的功能,并不支持 Nginx 模塊注冊處理程序。由于 try_files 指令在許多 FastCGI 應用的配置中都有用到,所以我們不妨在這里簡單介紹一下。
try_files 指令接受兩個以上任意數量的參數,每個參數都指定了一個 URI. 這里假設配置了 N
個參數,則 Nginx 會在 try-files
階段,依次把前 N-1
個參數映射為文件系統上的對象(文件或者目錄),然后檢查這些對象是否存在。一旦 Nginx 發現某個文件系統對象存在,就會在 try-files
階段把當前請求的 URI 改寫為該對象所對應的參數 URI(但不會包含末尾的斜杠字符,也不會發生 “內部跳轉”)。如果前 N-1
個參數所對應的文件系統對象都不存在,try-files
階段就會立即發起“內部跳轉”到最后一個參數(即第 N
個參數)所指定的 URI.
前面在 (六) 和 (七) 中已經看到靜態資源服務模塊會把當前請求的 URI 映射到文件系統,通過 root配置指令所指定的“文檔根目錄”進行映射。例如,當“文檔根目錄”是 /var/www/
的時候,請求 URI/foo/bar
會被映射為文件 /var/www/foo/bar
,而請求 URI /foo/baz/
則會被映射為目錄 /var/www/foo/baz/
. 注意這里是如何通過 URI 末尾的斜杠字符是否存在來區分“目錄”和“文件”的。我們正在討論的 try_files配置指令使用同樣的規則來完成其各個參數 URI 到文件系統對象的映射。
不妨來看下面這個例子:
root /var/www/;
location /test {
try_files /foo /bar/ /baz;
echo "uri: $uri";
}
location /foo {
echo foo;
}
location /bar/ {
echo bar;
}
location /baz {
echo baz;
}
這里通過 root 指令把“文檔根目錄”配置為 /var/www/
,如果你系統中的 /var/www/
路徑下存放有重要數據,則可以把它替換為其他任意路徑,但此路徑對運行 Nginx worker 進程的系統帳號至少有可讀權限。我們在location /test
中使用了 try_files 配置指令,并提供了三個參數,/foo
、/bar/
和 /baz
. 根據前面對try_files 指令的介紹,我們可以知道,它會在 try-files
階段依次檢查前兩個參數 /foo
和 /bar/
所對應的文件系統對象是否存在。
不妨先來做一組實驗。假設現在 /var/www/
路徑下是空的,則第一個參數 /foo
映射成的文件/var/www/foo
是不存在的;同樣,對于第二個參數 /bar/
所映射成的目錄 /var/www/bar/
也是不存在的。于是此時 Nginx 會在 try-files
階段發起到最后一個參數所指定的 URI(即 /baz
)的“內部跳轉”。實際的請求結果證實了這一點:
$ curl localhost:8080/test
baz
顯然,該請求最終和 location /baz
綁定在一起,執行了輸出 baz
字符串的工作。上例中定義的 location /foo
和 location /bar/
完全不會參與這里的運行過程,因為對于 try_files 的前 N-1
個參數,Nginx 只會檢查文件系統,而不會去執行 URI 與 location
之間的匹配。
對于上面這個請求,Nginx 會產生類似下面這樣的“調試日志”:
$ grep trying logs/error.log
[debug] 3869#0: *1 trying to use file: "/foo" "/var/www/foo"
[debug] 3869#0: *1 trying to use dir: "/bar" "/var/www/bar"
[debug] 3869#0: *1 trying to use file: "/baz" "/var/www/baz"
通過這些信息可以清楚地看到 try-files
階段發生的事情:Nginx 依次檢查了文件 /var/www/foo
和目錄/var/www/bar
,末了又處理了最后一個參數 /baz
. 這里最后一條“調試信息”容易產生誤解,會讓人誤以為 Nginx 也把最后一個參數 /baz
給映射成了文件系統對象進行檢查,事實并非如此。當 try_files 指令處理到它的最后一個參數時,總是直接執行“內部跳轉”,而不論其對應的文件系統對象是否存在。
接下來再做一組實驗:在 /var/www/
下創建一個名為 foo
的文件,其內容為 hello world
(注意你需要有/var/www/
目錄下的寫權限):
$ echo 'hello world' > /var/www/foo
然后再請求 /test
接口:
$ curl localhost:8080/test
uri: /foo
這里發生了什么?我們來看,try_files 指令的第一個參數 /foo
可以映射為文件 /var/www/foo
,而 Nginx 在try-files
階段發現此文件確實存在,于是立即把當前請求的 URI 改寫為這個參數的值,即 /foo
,并且不再繼續檢查后面的參數,而直接運行后面的請求處理階段。
上面這個請求在 try-files
階段所產生的“調試日志”如下:
$ grep trying logs/error.log
[debug] 4132#0: *1 trying to use file: "/foo" "/var/www/foo"
顯然,在 try-files
階段,Nginx 確實只檢查和處理了 /foo
這一個參數,而后面的參數都被“短路”掉了。
類似地,假設我們刪除剛才創建的 /var/www/foo
文件,而在 /var/www/
下創建一個名為 bar
的子目錄:
$ mkdir /var/www/bar
則請求 /test
的結果也是類似的:
$ curl localhost:8080/test
uri: /bar
在這種情況下,Nginx 在 try-files
階段發現第一個參數 /foo
對應的文件不存在,就會轉向檢查第二個參數對應的文件系統對象(在這里便是目錄 /var/www/bar/
)。由于此目錄存在,Nginx 就會把當前請求的 URI 改寫為第二個參數的值,即 /bar
(注意,原始參數值是 /bar/
,但 try_files 會自動去除末尾的斜杠字符)。
這一組實驗所產生的“調試日志”如下:
$ grep trying logs/error.log
[debug] 4223#0: *1 trying to use file: "/foo" "/var/www/foo"
[debug] 4223#0: *1 trying to use dir: "/bar" "/var/www/bar"
我們看到,try_files 指令在這里只檢查和處理了它的前兩個參數。
通過前面這幾組實驗不難看到,try_files 指令本質上只是有條件地改寫當前請求的 URI,而這里說的“條件”其實就是文件系統上的對象是否存在。當“條件”都不滿足時,它就會無條件地發起一個指定的“內部跳轉”。當然,除了無條件地發起“內部跳轉”之外,try_files 指令還支持直接返回指定狀態碼的 HTTP 錯誤頁,例如:
try_files /foo /bar/ =404;
這行配置是說,當 /foo
和 /bar/
參數所對應的文件系統對象都不存在時,就直接返回 404 Not Found
錯誤頁。注意這里它是如何使用等號字符前綴來標識 HTTP 狀態碼的。