0


nginx的location与proxy_pass指令超详细讲解及其有无斜杠( / )结尾的区别

本文所使用的环境信息如下:

  • windows11 (主机系统)
  • virtual-box-7.0环境下的ubuntu-18.04
  • nginx-1.22.1 (linux)

斜杠结尾之争

实践中,nginx里最常用的指令就是location和proxy_pass了。前者用于为不同请求uri指定不同nginx配置,后者用于匹配的location进行转发(通常是动态内容)。关于二者的配置,有一个老生常谈的话题,那便是:配置的值是否有斜杠结尾,对文件路径查找(或请求转发)行为有哪些影响?相关文章也非常多,且多数粗看一眼,照其行事,也能立即解决问题。鄙人私以为其中部分文章的说法是不严谨的,故特撰此文,以备己查。

结论

不再废话,直接上结论(如果对location和proxy_pass的功能和基本配置还不熟悉,建议先看后面的章节):

  • location 指令有斜杠 / 结尾的情况一般情况下,location 指令不会对是否有斜杠结尾这个场景做特殊处理,除非满足以下条件:- location指令配置为前缀匹配- 前缀的最后一个字符为斜杠 /- 指令内嵌入了其它代理类指令 这些代理类指令有:proxy_pass、fastcgi_pass、uwsgi_pass、scgi_pass、memcached_pass、grpc_pass ①满足以上条件后,也只会对一个特定的 uri 做特殊处理,这个 uri 除了没有尾部的斜杠外,正好与 location 定义的前缀一模一样。对这个特殊的uri的处理方式为:返回一个301重定向,重定向的地址为:原始请求 uri + /,也就是说,重定向地址与 location 的前缀内容完全相同 ②示例配置如下:location /films/nature/ { ``````proxy_pass http://film-server;``````}假定请求的 url 为http://localhost/films/nature,则 location 的处理方式为:返回一个301重定向,重定向的地址为http://localhost/films/naure/。与原始请求的唯一差别就是,新的 uri 地址比原来的 uri 地址尾部多了一个斜杠 / 😁是的,你没看错,对 location 的配置里,其最后一个字符为斜杠的特殊处理,就仅此而已了。用大白话讲就是:嘿,客户端伙计,你这家伙不讲武德呀,你请求的uri在我们(服务器端)看来就是一个目录呀,那你咋不在尾部加上目录符号(即 / )呢?念你初始犯,从轻发落,打回本次请求,将格式改正确后再重新发过来(即301重定向)。上面这个特殊操作,有很多限制条件,最终也只作用于一个特殊的 uri 。如果请求的 url 为http://localhost/films/nature/cheetah.mp4,它同样与上面的 loation 匹配,但不会发起301重定向。但如果我们就是要访问http://localhost/films/nature 这个资源,并要求服务器不要返回重定向,该如何处理呢?可以使用精确匹配来解决这个问题,就像下面这样配置:location /films/nature/ { ``````proxy_pass http://film-server;``````}``````location = /films/nature { # 通过精确匹配,可避免重定向``````proxy_pass http://film-server;``````}很显然,这个十分独特的场景和处理方式,我们基本上是无感知的,并且绝大多数情况下,业务也不需要对这个特殊的场景做特别的处理,因此,完全可以忽略这个特性的存在。> 🍁 特别说明> 通过实测发现,location指令还会对一类特别的uri做重定向处理(这个特性没有在官方文档上注明),这类uri的特点是:> > > - uri最后一个字符不是斜杠 /> - 整个uri正好指向location内root指令目录下的某个子目录> > 简而言之,就是一个uri在服务器端正好指向了一个目录,但这个uri却没有以目录符号/结尾。> 比如有下面这样一个配置(点击查看): > > 假定服务器上有 /var/www/book-store/books/ 和 /var/www/book-store/books/society/ 这两个目录,当访问http://localhost/bookshttp://localhost/books/society 时,都会返回301重定向,且重定向的地址为http://localhost/books/http://localhost/books/society/> > 这看上去很合理,但如果请求是通过代理类指令(参见①处)转发过来时,很可能引发域名解析问题,最终导致访问不了。因为重定向的host就是请求的host,这在直接访问时没有问题,但对于通过代理指令转发过来的请求,其host已不再是初始请求的host,通常都是内部的虚拟主机或主机群组(用于负载均衡)名称,这些名称外部是不可解析的,从而会在最初的请求客户端引发域名解析问题。下面举一个稍微复杂一点的例子来说明:> 示例配置如下(点击查看): > > 假定服务器上存在 /var/www/film-doc/film/ 和 /var/www/film-doc/film/war/ 两个目录,当访问http://localhost/film/war 时,会匹配到⑴处,然后会发起代理,地址为http://film-server/film/war,这个地址将通过 upstream 最终执行到⑵处。由于这个 uri 正好对应磁盘目录 /var/www/film-doc/film/war/,但却没有以目录符号/结尾,因此⑵处的这个server将返回一个301重定向响应。> > 对于⑵处的这个 server 而言,它收到的请求为http://film-server/film/war,其主机名 film-server 已经不是最初的 localhost 了,因此返回的重定向地址为http://film-server/film/war/。客户端再次发起http://film-server/film/war/时,会解析不了 film-server 这个主机名,因为它是一个 nginx 内部虚拟的主机群的名字,最终原始的http://localhost/film/war 请求会失败,浏览器上表现为接收不到任何响应,通过F12打开调试面板,可以看到,浏览器收到了一次重定向响应,并根据重定向地址再次发起了新的请求。> > 有趣的是,如果客户端发起http://localhost/film 这个请求会怎么样呢,会不会也失败呢?> > 答案是不会失败,这个请求的执行流程如下:> > > - 请求匹配到⑴处,尽管/film并不与/film/匹配(少了末尾的斜杠),但根据前面②处的规则,将返回301重定向,地址为http://localhost/film/> - 客户端收到重定向响应后,发起新的请求,地址为http://localhost/film/> - 请求与⑴处完全匹配,发起内部代理请求,地址为http://film-server/film/> - 代理请求与⑵处匹配,由于服务器上正好存在 /var/www/film-doc/film/ 这个目录,且请求的 uri 是以目录符号/结尾的,因此不会再返回301重定向响应了> - 接下来会检查 /var/www/film-doc/film/ 目录下是否存在index.html文件,如果有则返回,没有则返回403 Forbidden响应(返回403是默认行为,如果 location 内添加了autoindex on指令,即允许访问目录,则会返回一个目录列表页面)。
  • proxy_pass 指令有斜杠 / 结尾的情况proxy_pass 指令不会对其配置是否有斜杠结尾做任何特殊处理更严格而准确地说法是:proxy_pass 指令会针对其配置是否包含有 uri (这里的 uri 是指 url 中,端口与问号之间的部分)做特殊处理,具体如下 ③ :- 不带uri时(如http://localhost:8379) 新的地址构成为:proxy_pass的配置内容 + 原始请求uri中去除掉协议、主机和端口后的剩余内容- 配置了uri时(如http://localhost:8379**/** 或http://localhost:8379**/foo** ) 新的地址构成为:proxy_pass的配置内容 + 原始请求 uri 中去除掉协议、主机、端口和location配置内容后的剩余部分。上述两个 url 中,**/** 和 /foo 就是这里说的 uri从上面可以看出,proxy_pass在创建新的转发地址时,总是会剔除掉原始uri中的协议、主机、端口。核心差异在于是否要去除掉location指令的配置内容。如果proxy_pass配置带有uri就去除,反之则不去除。另外,像http://localhost:8379/这个地址很特别,因为去除掉协议、主机、端口后,就只剩下 / 了,这大概就是“以斜杠结尾的 proxy_pass 会去除掉 location 的配置内容”这个错误说法的源头了。事实上,像http://localhost:8379/foo这个地址,uri 为 /foo,它并没有以 / 结尾,但在生成新的转发 uri 时,同样会去除掉 location 的配置内容。

实例演示

上面只是对location和proxy_pass两个指令的斜杠结尾之争这个误会,从理论上做出了解释。但还不形象,不易理解,下面给出一个实例,以加深理解。

location中无proxy_pass的情况

示例配置如下(点击查看):

下面是几个不同的uri,所匹配的location,以及它们对应的资源文件在服务器磁盘上的路径位置。
请求匹配的location文件位置http://localhost/books/red-rock.html配置A**/var/www/mall**/books/red-rock.htmlhttp://localhost/shop/goods/keep-alive.html配置B**/var/www/mall/sell**/shop/goods/keep-alive.htmlhttp://localhost/store/yalu-river.html配置C**/var/www/mall/store**/store/yalu-river.htmlhttp://localhost/store-central.html配置C**/var/www/mall/store**/store-central.html
简而言之,这种情况下,资源文件在磁盘的路径为:location配置的根目录 + uri。可见,location配置结尾的 / 字符,并没有特别之处,就是一个普通的字符。

location中有proxy_pass的情况

示例配置如下(点击查看):

下面是一组WEB请求的转发情况
请求匹配的location生成的代理地址说明http://localhost/films/wandering-earth配置Ahttp://film-server/wandering-earth由于proxy_pass的配置指定了uri,新的代理地址将不包含location的配置(即/films/)http//localhost/comments/list配置Bhttp://comment-server**/comments/**list由于proxy_pass的配置不带uri,新的代理地址会保留location的配置(即/comments/)http://localhost/comments/top/asc100配置Chttp://comment-server/top/asc100新的代理地址将不包含location的配置, 其中的top来自proxy_pass的配置,而非locationhttp://localhost/comments/top/..//desc50配置Bhttp://comment-server/comments/desc50请求的uri经过规范化处理后变成了http://localhost/comments/desc50,然后才开始匹配,因此匹配的location为配置Bhttp://localhost/feedbacks/list配置Dhttp://comment-server/center**list**新的代理地址将不包含location的配置, 由于proxy_pass的配置末尾没有斜杠,所以直接拼接后,会得到centerlist这个串

location指令配置详解

location指令是ngx_http_core_module中的一个最常用的指令,它将不同的请求uri映射到不同的Web资源配置。

🍁 特别说明:location指令匹配的是请求uri,而不是整个请url,这里的uri是url中端口与问号(?)之间的部分。比如http://localhost:1983/books/list?author=jane这个URL,参与匹配的uri为/books/list

  • 指令格式 有两种语法格式,如下:- location [ = | ~ | * | ^ ] uri { ... } - location @name { ... } 
  • 适用范围 在server指令内,或location指令内(即location指令是可以自嵌套的)
  • 功能用途 根据匹配的uri查找服务器上的磁盘文件,再以合适的web形式返回,或代理(转发)到其它web服务(这需要代理类指令配合,如proxy_pass)

location指令在功能层面上的流程如下 ④:

location内是否有代理类指令
+--------------------------------+ 
WebRequest --> | Does contains proxy instruct ? |---- No ---> 返回服务器磁盘上的文件 ★
+--------------------------------+
|
Yes
|
+---------> 代理到其它WEB服务

本小节只介绍location内没有代理类指令的情况,即上面流程中标★处。有代理的情况,将在prxoy_pass指令中详细阐述。

模板location

模板型location,即 location [ = | ~ | * | ^ ] uri { ... } 这种语法,它共有四部分组成,分别是:

  • location关键字:这个没什么好说的,就是location指令的标识
  • uri匹配模式:也叫匹配修饰符,它表明以什么方式来匹配请求的uri,共5种,分别是:空、= 、~ 、* 、^
  • uri匹配样式:也叫uri模板,它是各个具体的匹配模式要使用的uri样式内容

location的匹配模式分为三类,分别是:前缀匹配精确匹配正则匹配,详细说明如下:

  • 无或空 即不指定任何匹配模式符号,此时,它代表前缀匹配。比如 location /books/ { ... },它可以匹配/books/index.html、/books/computer/GitDefinitiveGuide.pdf
  • = 符号 = 代表精确匹配,要求请求的uri与该符号后的uri样式完全一样,比如 location = /books { ... },它可以精确的匹配/books这样的uri,像/books/、/books/index.html、/books.doc、/booksmark.pdf这样的uri均无法匹配。精确匹配一旦成功,则整个匹配过程结束,不再继续尝试匹配其它的location
  • ~ 符号 ~ 代表正则表达式匹配,并且是区分大小的。比如 location ~ .(gif|jpg|PNG)$ { ... },它可以匹配/red-rock.jpg和/img/pigion.gif,但不能匹配/img/greatwall.png,因为这里的png是小写的,而 ~ 匹配的字符是大小写敏感的。
  • ~* 符号 ~* 代表正则表达式匹配,并且它不区分大小的。比如 location ~* .(gif|jpg|PNG)$ { ... },它可以匹配/red-rock.jpg和/img/pigion.gif,或可以/img/greatwall.png,尽管这里的png是小写的,但 ~* 匹配不区分大小写,依然可以匹配。
  • ^~ 符号 ^~ 代表的匹配规则很特别,它看上去像是一个正则匹配,实则不是,它依然代表的是前缀匹配,与默认的前缀匹配(即没有任何符号的那种)的区别是:假定一个server内配置了多个前缀型的location和多个正则location,如果这些前缀location中,最终匹配的location是一个 ^~ 的话,则不再尝试后续的正则匹配。简而言之,^就是一个禁止做正则匹配的前缀匹配,从它的功能定义上来看,这个^符号改成!~更形象些,毕竟感叹号!就代表否定的意思,而^本身就是一个正则表达式的特殊符号,很容易引起误会。

location指令的处理流程,总体上分类三个阶段,分别是:uri规范化处理、uri匹配、后置处理,详细说明如下:

  1. uri规范化处理 这一步是处理原始uri串中不规范的内容,将其规范花后,方便后续做匹配,这些处理包括:- 解码 &xx 这样的url编码字符- 解析 . 和 .. 到这些符号所引用的目录上,比如/comment/top/../top100,处理后会变成/comment/top100- 将多个连续的/压缩成一个,比如/films/science-fiction///wandering-earth,处理后会变成/films/science-fiction/wandering-earth
  2. uri匹配 uri匹配有前面提到的三种类型,即:精确匹配、前缀匹配和正则匹配。假定一个server配置中,有多个精确匹配的location、多个前缀匹配的location和多个正则匹配的location。则整个匹配流程是这样的:- 先做精确匹配,按照精确匹配location在配置文件中的出现顺序进行,一旦命中一个,则整个匹配过程结束。- 若精确匹配没有命中,则执行前缀匹配,按照配置文件中前缀location出现的顺序执行,如果命中多个,则只记录location配置内容最长的那个。 假定有两个前缀配置分别是:location /films/ { ... } 和 location /films/nature/ { ... }。请求地址/films/nature/aerial-view-of-china.mp4与这两个前缀locaton配置均能匹配,但最终将只保留第二个匹配,因为它的配置前缀(即/films/nature)最长。由此可以推断出,默认的配置(即 location / { ... } )一定是兜底的匹配,当所有其它类型的匹配均未命中时,它一定能命中。- 接着再按照配置文件中的顺序,执行正则匹配,与前缀匹配不同的是:一旦命中一个正则匹配,整个匹配就结束了,不再对后面的正则location进行匹配。并且整个匹配的结果就是这个正则location。如果没有一个正则命中,则整个匹配的结果就是上面前缀匹配中,记录的那个location内容最长的命中结果。 假定有两个正则配置分别是:location * .(jpg|gif|png)$ { ... } 和 location ~* .(png|jpeg|svg)$ { ... }。请求地址/img/logo.png将只会与第一个匹配,尽管它也满足第二个正则表达式,但由于第一个正则的位置在前,并且匹配成功,因此就不再进行后面的匹配了。> 🍁 例外情况:> 有一个特殊的前缀匹配,即:^,如果在前缀匹配结束后,命中的location是用 ^~ 修饰的,就不会进入正则匹配阶段了。
  3. 后置处理 在uri匹配结束后,便执行命中location指令中,花括号{}内的指令。主要有两类,要么是从root指令配置的目录下查找相应的文件,要么执行其它代理类(见第①处)指令,整个流程与④处描述一致。

下图展示了整个location指令的执行流程

下表对各个匹配修饰符进行了整理对比,其中的优先级数字1、2、3,是数字越小,优先级越高
修饰符优先级是否为正则区分大小写命中后继续匹配备注2❌✔️✔️第一列没有内容,因为没有任何字符,其含义就是前缀匹配=1❌✔️❌^2❌✔️✔️在整个前缀匹配结束后,如果最终结果是一个^修饰的location, 则不进行后续的正则匹配3✔️❌❌*3✔️✔️❌

一个小优化

通常情况下,server里都会有一个location / {...} 这样的配置,它可以匹配所有的请求。如果一个网站的首页访问最频繁,比如http://localhsot/,但该网站却配置了非常多的location,那么首页uri这个请求,需要在匹配完所有的location后,才能得出最终命中的location为 / 。在此期间,其它的那些匹配尝试明显是多余的。为此,可能通过为 / 提供一个精确匹配来提高性能,就像下面这样:

location = / {
...
}
location / {
...
}

另外,像location = / {...} 这种精确匹配,内部再嵌套location指令就没有意义了。

几个特例

前缀匹配是区分大小写的,比如 location /books/ {...} 这个指令,请求/boOKs/red-rock.html就不匹配。但在一些大小写不敏感的操作系统上(如MacOS和Cygwin),0.7.7版本以后的nginx,则是能够匹配的。如果你打算验证一下这个大小写敏感的问题,建议不要通过浏览器方式,因为浏览器会将uri地址做规范化处理。建议通过命令行工具来验证,比如linux下的curl和wget。

0.7.40版本以后的nginx,其正则匹配中的捕获组是可以在其后的其它指令中引用的。

0.7.1 ~ 0.8.41这个版本范围内的nginx,有点特殊,只要命中一个前缀匹配就立即结束,不再匹配后续的前缀location,也不匹配其它正则location。因此,如果你使用的是这个范围内的nginx,在配置location时,就得精心按排好各个location在配置文件中的顺序,避免一个相似的,内容较短的location配置出现在较长者的前面。

文件路径排除URI中的匹配项

有时候,我们希望资源文件的路径中,不要出现URI中已匹配的部分,比如下面这个场景:

location /book/ {
root /var/www/book;
}

请求地址http://localhost/book/ruby-on-rails-guide.pdf,对应到硬盘上的文件为/var/www/**book/book**/ruby-on-rails-guide.pdf。其中有两个连续的book目录,第一book来自root指令配置的根目录路径,第二个book来自请求uri。但我们希望这个路径上只有一个book目录,或者说文件路径中,不要包含uri中匹配的location部分,就像这样: /var/www/book/nginx-definitive-guide.pdf。用alias指令替换root可以达到这个目的,即像下面配置:

location /book/ {
alias /var/www/book/; # 将原来的root替换成alias
}

alias指令表明,在生成文件路径时,用alias指令后面的字符串,替换掉请求uri中与location匹配的部分,因此,该指令后面的内容,就可以看作是uri中匹配部分的别名。

如果location配置为正则匹配,根据alias的功能定义,它会替换掉整个请求的uri,也就是说,所有匹配此正则location的请求,都将返回相同的内容,即alias指令所配置的文件或目录。此时,

  • 如果alias后面的文本指向一个已存在的目录,则会引发重定向
  • 如果alias后面的文本是一个不存在的文件或目录,则会返回404

上述行为显然不是我们所期望的,它应该根据uri的不同,返回不同内容(web资源文件)。要达到此目的,需要在正则表达式中添加捕获组,并在alias指令中引用它们。示例如下:

location ~* ^/avatar/(.+\.(?:jpg|jpeg|gif|png))$ {
alias /var/www/user/avatar/$1;
}

当访问http://localhost/avatar/jane.png时,将返回/var/wwww/user/avatar/jane.png文件。

上面这个示例中,alias指令仅仅是简单的引用了变量$1,并将其追加在末尾,它完全可以用root /var/www/user指令替代。下面这个示例更符合正则location与alias的组合:

location ~ "^/novel/(\d{4})/(\d{2})/\d{2})/(.+)-(.+)$" {
alias /var/wwww/novel/$4/$1-$2-$3/$5.txt;
}

这是一个模拟小说文件的配置,$1、$2、$3、$4、$5分别代表年、月、日、作者、作品名,请求http://localhost/novel/2023/03/15/YuHua-KeepAlive,对应的响应文件路径为:/var/www/novel/YuHua/2023-03-15/KeepAlive.txt

示例二中的整个正则表达式写在了一对引号内,这是因为正则表达式中含有花括号 { 和 },它们都是nginx配置的关键字,需要做特殊处理。经验证,将正则表达式写在一对引号内可解决此问题,而采用对花括号字符使用反斜杠(即{ 和 })转义这种方式,是不行的。

命名location

除了模板location外,还支持一种叫做命名location的语法,即 location @name {...} 这种。顾名思义,命名location就是一个具有名称的location定义,这个location不匹配任何外部uri,它只能在nginx内部使用,通常与error_page、try_files等指令一起使用。

比如下面这个配置(点击查看):

根据上面☆处的配置,如果出现了404错误,会内部重定向到名称为fallback的location中。就本示例而言,它最终会代理到baby-home-server(宝贝回家)这个Web服务上,通常是返回一个亲子寻找页面。

proxy_pass指令配置详解

proxy_pass指令主要用在location指令内,将匹配的请求转发到代理的服务上,这对于API类服务非常适用,再配合upstream指令的话,还可实现负载均衡。

  • 指令语法:proxy_pass uri
  • 适用范围:location指令内、limit_expect指令内、location指令内的if语句块

指令中的uri,协议部分支持http和https。主机部分,支持ip地址、nginx虚拟主机组和unix socket。整个指令的执行分为两个阶段,如下:

在合成转发地址时,有时候无法确定请求uri中,哪些部分应该被替换掉。此时,要么转发的地址与指令不带uri的情况相同,要么通过变量来指定,下面罗列出3种最常见的情况。

  1. location 为一个正则匹配,或 proxy_pass 指令位于命名 location 内 这种情况下,proxy_pass的配置不允许指定 uri 部分。 比如下面这个配置 与不带 uri 的 proxy_pass 指令一样,上面的配置,最终得到的转发url就是 proxy_pass 配置内容 + 请求uri上述配置里⑴和⑵处的proxy_pass指令,不可以包含uri信息,否则将导致配置文件解析报错。假如⑴处的配置为proxy_pass http://novel-server/;proxy_pass http://novel-server/novels/;,启动nginx时,将得到类似下面的错误:nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block in /usr/local/nginx/conf/nginx.conf:89> 还可以通过> > nginx -t -c 配置文件路径> > 命令来验证配置文件是否正确,避免直接重启nginx或重新加载配置文件
  2. location 中有 rewirte 指令,并且请求 uri 匹配 rewrite 的正则表达式 这种情况下,原始请求的uri会被rewirte指令修改,修改后的uri,将直接传递给proxy_pass指令,而proxy_pass本身的uri(如果有的话)将被替换。示例如下:location /name/ {``````rewrite /name/([^/]+) /users?name=$1 break;``````proxy_passs http://user-center/main/basicinfo/;``````}假如请求url为 /name/jane-lotus, 该uri与rewrite的正则表达式是匹配的,因此,处理过程如下:- 原始url被rewrite指令修改为:/users?name=jane-lotus- 修改后的url传递给proxy_pass指令,新的转发地址为http://user-center/users?name=jane-lotus 可以看到,proxy_pass中的uri部分,即/main/basicinfo/,被替换成了/user?name=jane-lotus如果请求uri为 /name/regions/shuangliu,由于它不匹配 rewrite 指令的正则表达式,因此,proxy_pass 的行为处于正常状态(即与“附带有uri”时的行为一致),最后得到的转发地址为http://user-center/main/basicinfo/regions/shuangliu,不包含location中匹配的/name/
  3. proxy_pass 中使用了变量 在使用了变量的情况下,将变量内容替换为真实文本后即为新的转发地址,示例如下:location /novel/ {``````proxy_passs http://book-server/books$request_uri;``````}当请求uri为/novel/three-body.html?page=3时,最终的转发地址为http://book-server/books/novel/three-body.html?page=3 。是直接替换掉proxy_passs中的$request_uri变量得来的。

小结

通过 location 指令,可实现根据不同请求 uri 进行不同配置的效果,因此它也更像是 http 请求路由。可分为精确匹配、前缀匹配和正则匹配。

proxy_pass 用于将一个 location 请求代理到另一个或另一组Web服务,在生成新的转发地址时,会根据 proxy_pass 是否带有uri而有所不同。

总的说来,location与proxy_pass,对其配置末尾的字符是否为斜杠,没有做特殊处理。

标签: 前端

本文转载自: https://blog.csdn.net/2301_76725413/article/details/131629164
版权归原作者 2301_76725413 所有, 如有侵权,请联系我们删除。

“nginx的location与proxy_pass指令超详细讲解及其有无斜杠( / )结尾的区别”的评论:

还没有评论