在開發環境下使用nginx重寫uri及代理功能

本文同步在我的博客shymean.com上,歡迎關注html

這篇文章整理了在前端開發中,在開發環境下使用nginx重寫uri及代理功能的方法。前端

參考node

location匹配

參考jquery

多個項目共用同一個域名時,每每須要根據url將請求轉發到不一樣的項目上,此時須要配置locationnginx

location [ = | ~ | ~* | ^~ ] uri { ... }
複製代碼

在location指令和uri請求中間能夠添加可選的修飾符,四種修飾符的含義分別以下git

  • = 表示精確匹配。只有請求的url路徑與後面的字符串徹底相等時,纔會命中。github

  • ~ 表示該規則是使用正則定義的,區分大小寫。web

  • ~* 表示該規則是使用正則定義的,不區分大小寫。ajax

  • ^~ 表示若是該符號後面的字符是最佳匹配,採用該規則,再也不進行後續的查找。canvas

當不添加任何修飾符時,則使用請求資源路徑與配置的uri進行前綴匹配:若是請求資源路徑以配置的uri開頭,則表示可命中該規則。

須要注意的是,不能同時存在相同的uri匹配規則,即

location /img/ {}
location ^~ /img/ {}
複製代碼

會提示錯誤

nginx: [emerg] duplicate location "/img/" in /usr/local/etc/nginx/servers/test.conf:61

注意uri末尾帶斜槓與不帶斜槓會被視做兩條匹配規則,他們的處理也是不同的,在下面的例子中也有提到(上面引用的文章裏面關於這點描述貌似錯了)。

location的具體的匹配過程爲

  • 首先先檢查無修飾符的規則,並進行前綴匹配,選擇最長匹配的項並記錄下來。
  • 而後檢查是否存在精確匹配的location(使用了=修飾符),若是存在,則結束查找,使用它的配置。
  • 而後查找是否存在最優匹配,若是存在,則選擇最優匹配結果最長的項,並使用它的配置
  • 而後按順序查找使用正則修飾符定義的location,若是匹配,則中止查找,使用它定義的配置。
  • 最後,若是沒有匹配的正則location,則使用前面記錄的最長匹配前綴字符location。

從上面的匹配過程能夠看出,匹配順序是:

精準匹配 > 最優匹配 > 按順序的正則匹配 > 最長的前綴匹配

接下來咱們將編寫一些測試來練習location,其大概形式以下

# uri表示location的須要匹配的規則
location uri {
  	# config表示某個config配置
    [ config ]
}
複製代碼

爲了驗證"存在多個location時,究竟是哪一個location匹配規則生效"的問題,咱們能夠將請求轉發到某個不存在的文件上,而後使用錯誤日誌查看某個請求對應的location是啥

如今,讓咱們開始動手測試吧

server {
    listen 80;
    server_name test.com;
    index index.html;
  
    error_log  /usr/local/etc/nginx/logs/error.log error;
  
    location / {
        root /Users/Txm/A/;
    }
    location /img {
        root /Users/Txm/B/;
    }
    location /img/ {
        root /Users/Txm/C/;
    }
}
複製代碼

接下來準備了一些請求連接,經過訪問並查看日誌,就能夠知道請求到底去了那個地方

請求url 匹配規則 備註
test.com/s/img/1.png A 只有/符合前綴匹配規則
test.com/img212/1.pn… B 只有/img符合前綴匹配,/img/不符合
test.com/img/1/1.png C /img/和/img末尾有無/是有區別的
test.com/img/1.png C /img/比/img的前綴匹配更長,更符合

接下來測試正則匹配,往上面的server模塊中添加以下location配置

location ~* /im {
  root /Users/Txm/D/;
}
location ~* \.png {
  root /Users/Txm/E/;
}
複製代碼

此時再訪問/img212/1.png/img/1/1.png/img/1.png這三個連接,都會命中規則D,如上面的匹配規則:

  • 正則匹配的優先級大於前綴匹配,所以不會匹配規則ABC
  • 正則匹配是按照定義的順序進行匹配的,若是命中,則中止查找,所以雖然規則E也符合規則,可是沒有被命中

咱們繼續來測試^~最優匹配

location ~ /bund {
  root /Users/Txm/F/;
}
location ~ /bundle/1 {
  root /Users/Txm/F1/;
}
location ^~ /bundl {
  root /Users/Txm/G/;
}
location ^~ /bundle/ {
  root /Users/Txm/G1/;
}
location ~ \.js$ {
  root /Users/Txm/H/;
}
複製代碼

咱們用http://test.com/bundle/1.js這個連接來進行測試,理論上來講這個連接符合上述全部規則,實際上該請求命中規則G1,個人理解是:

  • 最優匹配的優先級高於正則匹配
  • 存在多個最優匹配的規則時,命中匹配長度最長的規則

所以此處命中了G1,若是刪除G1,則根據優先級應該匹配G,繼續刪除G,此時狀態回到了上面的正則匹配,根據正則按順序匹配的規則,此時應該匹配F。

最後,讓咱們測試一下精準匹配,精準匹配表示請求的路徑與配置的uri要徹底一致才能夠

location = /img {
	root /Users/Txm/I/;
}
複製代碼
請求url 匹配規則 備註
test.com/img I 精準匹配優先級最高
test.com/img/ D 不知足精準匹配和最優匹配,在順序上知足正則匹配D

root 和alias

參考

上面整理了location的語法和匹配規則,可是location並不會改變請求的uri,實際上請求到的文件路徑是由其餘指令進行處理的。

root與alias均可以用來指定文件的路徑,他們的主要區別在於nginx如何解釋location後面的uri,這會使二者分別以不一樣的方式將請求映射到服務器文件上

  • root的處理方式:root路徑+location路徑
  • alias的處理方式:使用alias路徑替換location路徑

http://test.com/test/index.html請求爲例,

server_name test.com;

location /test/ {
  # 當配置爲root時,實際請求的Users/Txm/nginx_test/test/index.html
  # root能夠放在 http、server、location、if等多個配置段下面
  # root /Users/Txm/nginx_test/; 
  
  # 當配置爲alias時,實際請求的是/Users/Txm/nginx_test/index.html
  # alias只能放在location中
  # 注意alias末尾必須跟/
	alias /Users/Txm/nginx_test/; 
}
複製代碼

換句話說,alias是一個目錄別名的定義,root則是最上層目錄的定義。

結合location,使用root或alias就能夠把請求的url映射到磁盤上對應的真實目錄。可是在某些時候,僅僅有目錄卻沒有真實文件也是不夠的(最多見的場景大概是開發環境沒有生成環境文件名的緩存hash值和.min.等後綴),此時能夠經過rewrite重寫請求uri路徑。

rewrite

參考

rewrite模塊容許使用正則修改請求的URI,發起內部跳轉再匹配location,或者直接作30x重定向返回客戶端。

rewrite regex replacement [flag]
複製代碼

其中的regex是PCRE風格的正則,rewrite的運行規則以下

  • 若是regex匹配當前請求的uri,則replacement 會被看成是新的uri參與後續處理。
    • 若是在server級別設置該選項,那麼他們將在location以前生效。
    • 若是新URI字符中有關於協議的任何東西,好比http://或者https://等,則終止處理並直接響應302
  • 若是同一個上下文中(server、location、if)有多個可以匹配uri的rewrite正則,則會根據rewrite指令出現的前後順序連續進行重寫替換,並將替換後的replacement看成新的uri參與後續處理
  • 若是想要終止匹配,可使用第三個參數flag,其取值以下
    • last表示中止處理任何rewrite相關的指令,並用替換後的uri開始下一輪的location匹配
    • break表示中止處理任何rewrite相關的指令,且直接使用該uri來處理請求,再也不進行location匹配
    • redirect若是不包含協議,且是一個新的uri,則用新的uri匹配的location去處理請求,不會進行30x跳轉;可是他也能夠直接返回30x,讓瀏覽器本身進行重定向請求
    • permanentredirect相同,區別在於它是直接返回301永久重定向

須要注意的是lastbreak的區別,若是在location中使用location,則會再次以新的URI從新發起重定向,並再次進行location匹配,若是新的uri和舊的uri都再次匹配到一個相同的location,就會發生死循環,當這種循環超過10次時,nginx就會返回500。所以牢記:在server上下文中使用last,而在location上下文中使用break

接下來讓咱們經過一些例子來驗證rewrite的規則。

server {
    listen 80;
    server_name test2.com;
    index index.html;
    root  /Users/Txm/nginx_test/;

    access_log  /usr/local/etc/nginx/logs/test2.access.log;
    error_log  /usr/local/etc/nginx/logs/error2.log error;

	  rewrite ^/baidu http://www.baidu.com;
    rewrite ^/bai http://image.baidu.com;

    return 403;
}
複製代碼
請求url 最終rewrite的uri 備註
test2.com/bai image.baidu.com
test2.com/baidu www.baidu.com 匹配到baidu,則直接返回
而後增長location
location /bundle/ {
	rewrite ^/bundle/(.*?)$ /dist/$1 break;
}

location /dist/ {
	rewrite ^/dist/(.*?)$ /src/$1 break;
}
複製代碼
請求url 最終rewrite的uri 備註
test2.com/bundle/1.js /Users/Txm/nginx_test/dist/1.js break再也不進行location匹配
test2.com/dist/1.js /Users/Txm/nginx_test/src/1.js

而後將/bundle/裏面的標識符break修改成last

location /bundle/ {
  rewrite ^/bundle/(.*?)$ /dist/$1 break;
}
複製代碼

則能夠看見最終的uri跟/dist/同樣,重寫成了/Users/Txm/nginx_test/src/1.js,所以牢記在location中使用break的警告。

經過rewrite,咱們能夠重寫路徑,將一些本來不存在的文件修改成實際存在磁盤上的文件,下面是一個去掉.min後綴和-hash後綴的重寫規則,能夠將歷史項目中使用grunt打包的靜態資源映射到src對應源文件去

rewrite ^(.*?)((\.min)?\-.*?)(\..*?)$ $1$4 last;
複製代碼

nginx代理的一些用法

反向代理是爲服務端服務的,反向代理能夠幫助服務器接收來自客戶端的請求,幫助服務器作請求轉發,負載均衡等。

反向代理對服務端是透明的,對咱們是非透明的,即咱們並不知道本身訪問的是代理服務器,而服務器知道反向代理在爲他服務。

正向代理是爲咱們服務的,即爲客戶端服務的,客戶端能夠根據正向代理訪問到它自己沒法訪問到的服務器資源,一種應用場景是:假設公司的局域網不容許訪問外網,則局域網中的客戶端要訪問Internet,則須要經過代理服務器來訪問。

正向代理對咱們是透明的,對服務端是非透明的,即服務端並不知道本身收到的是來自代理的訪問仍是來自真實客戶端的訪問。

下面是在前端開發中,可使用nginx代理實現的一些功能場景

在本地開發環境模擬線上請求場景

代碼運行環境能夠分爲本地開發環境、測試環境和線上生產環境。以現有開發流程中某個web項目爲例:

  • 開發時是本地啓動的3015端口號
  • 測試時在提測平臺根據工單拉取相關服務,經過k8s部署在容器並運行服務,最終輸入一系列的host列表
  • 工單可上線時,經過發佈平臺合併代碼到develop及master,而後按命令步驟部署到生產服務器上

假設訪問服務的連接是http://m.xxx.com/h5/test爲了達到三個環境相同的訪問場景,通常來講須要作下面處理:

須要訪問生產環境時,直接在瀏覽器輸入當前連接便可,線上的域名通常會預先解析到對應的服務器上ip上,默認狀況下輸入域名訪問到的就是生產環境的服務。

能夠把測試環境理解成生產環境的鏡像,應用也是部署在一臺服務器上,須要訪問測試環境時,咱們就須要將域名指向測試服務器的ip地址

在本地開發時,若是須要經過域名訪問本地開發環境,則能夠經過修改host,而後將域名請求域名代理到本地node服務

127.0.0.1 xxx.com
複製代碼

而後修改nginx配置,經過nginx將xxx.com域名的請求代理到本地的服務端口號上面

server {
    listen 80;
    server_name m.xxx.com;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forward-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_pass http://127.0.0.1:3015;
    }
}
複製代碼

這裏推薦一個超級好用的host修改工具:SwitchHosts,能夠很方便地在本地、測試環境、生成環境進行域名切換。

將線上請求資源文件映射到本地

因爲生產環境的靜態資源如樣式表、JavaScript文件通常是通過壓縮和打包的,爲了緩存控制甚至爲文件名添加了hash後綴,若是在某些時候須要對線上代碼進行調試,通常由兩種方式

第一種方式是經過charles等抓包工具,將請求的文件經過Map Local的方式映射到本地磁盤上,此時請求資源實際返回的是本地文件,而後能夠經過修改本地文件來達到調試的目的。這種方式適合未通過代碼合併打包處理的文件,在維護一些使用requirejs、seajs等模塊管理工具加載文件的老項目比較有用。

第二種將靜態資源域名配置到本地,而後經過nginx的locationaliasrewrite實現靜態資源文件代理

server  {
    listen 80;
    server_name cnd.shymean.com;
    
    location /wargame/ {
        alias /Users/Txm/github/wargame/dist/;
    		# 移除請求如jquery.min-a2dfg.js連接上的hash值a2dfg
    		# http://cnd.shymean.com/wargame/jquery.min-a2dfg.js 實際返回文件爲 /Users/Txm/github/wargame/dist/jquery.min.js
        rewrite ^(.+)/(.+)[-.]\w+\.(\w+)$ $1/$2.$3 last;
    }
  
    location /blog/ {
    		# 直接映射到本地目錄
        alias /Users/Txm/blog/public/;
    }
}
複製代碼

nginx配置跨域

正向代理一個經典的場景是使用nginx繞開瀏覽器的跨域限制,在先後端分離的開發調試過程當中,本地起的前端功能多是localhost:port域名形式,通常狀況下會使用本地mock數據進行開發,若是須要直接訪問遠程接口進行聯調,則會碰見跨域問題。

這種場景主要是在開發時頁面域名(localhost)和接口域名(api.xxx.com)不一致致使的,經過nginx的location和proxy_pass,將指定請求代理到對應服務商,從瀏覽器的角度來看,請求的都是同一個域名,也就不存在跨域限制了

server {
    listen 80;
  	server_name shymean.com;
    location /api/ {
      	# 局域網中後臺開發的本地服務,用於聯調
        proxy_pass http://192.168.132.253:7654;
    }
}
複製代碼

另一種場景是:出於性能優化的目的,一些靜態資源等每每使用單獨的cdn域名,當業務須要使用跨域資源(如在canvas上繪製網絡圖片最後須要調用canvas.toDataURL),此時也會存在跨域限制,經過nginx的add_header功能能夠很是簡單地實現CORS。

# 靜態資源
map $http_origin $imgCorsHost {
    default 0;
    "~http://shymean.com" http://shymean.com;
    "~https://shymean.com" https://shymean.com;
}

server {
    listen 80;
    listen 443;
    server_name cdn.shymean.com;

    root /Users/Txm/Desktop/blog/static;
   
    add_header Access-Control-Allow-Origin $imgCorsHost;
    add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
    add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';

    if ($request_method = 'OPTIONS') {
        return 204;
    }
   
}
複製代碼

從上面的例子中能夠看到,若是須要指定Access-Control-Allow-Origin爲多個域名,可使用nginx的map結構。

經過nginx修改響應的內容

有時會有一些只在開發環境下生效的邏輯,如引入mock代碼、向移動端頁面增長eruda調試工具等。

在本地開發時,能夠經過運行時指定環境變量爲development來判斷是否爲生產環境,從而修改響應內容

<% if(!app.isProduction()) { %>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Mock.js/1.0.1-beta3/mock-min.js"></script>
    <%- IncludeAssets('start:statics/h5/act/js/_mock.js') %>
    <script>
        window.isMockReuquest = true
    </script>
<% } %>
複製代碼

上面這種方式,在代碼中增長額外的環境判斷代碼,而後注入mock代碼,經過nginx經過攔截並修改響應內容,能夠一樣實現這個功能。在最開始實現這個需求的時候,花了大量時間來查閱相關的實現方法,發現最簡單的方式應該是經過openresty來實現。參考:

location ~* 1.js$ {
	body_filter_by_lua_file /usr/local/etc/openresty/lua/hello.lua;
}
複製代碼

而後新增hello.lua腳本,編寫相關邏輯

-- body_filter_by_lua, body filter模塊,ngx.arg[1]表明輸入的chunk,ngx.arg[2]表明當前chunk是否爲last
local chunk, eof = ngx.arg[1], ngx.arg[2]
local buffered = ngx.ctx.buffered
if not buffered then
   buffered = {}  -- XXX we can use table.new here 
   ngx.ctx.buffered = buffered
end
if chunk ~= "" then
   buffered[#buffered + 1] = chunk
   ngx.arg[1] = nil
end
if eof then
   local whole = table.concat(buffered)
   ngx.ctx.buffered = nil
	 whole = string.gsub(whole, "console.log",  "console.warn")

   ngx.arg[1] = whole
end
複製代碼

須要注意的是,修改後的whole內容長度,不能超過以前本來的內容長度,不然後面的數據會被截取掉,估計跟content-length頭部有關。關於openrestylua,以前接觸的也不是不少,後面會繼續深刻學習一下。

小結

本文總結了幾個與nginx匹配uri相關的一些指令,包括

  • 使用location匹配uri
  • 使用rootalias指定請求資源目錄
  • 使用rewrite重寫uri及後續匹配規則

結合uri和代理,咱們就能夠把請求導向本身須要的資源上去,從而知足開發環境的多種開發需求。

相關文章
相關標籤/搜索