在开发环境下使用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和代理,咱们就能够把请求导向本身须要的资源上去,从而知足开发环境的多种开发需求。

相关文章
相关标签/搜索