a href为什么不是我想要访问的那个URI?

1,329 阅读6分钟

a href

HTML <a> 元素(或称锚元素)可以通过它的 href 属性创建通向其他网页、文件、同一页面内的位置、电子邮件地址或任何其他 URL 的超链接。<a> 中的内容应该应该指明链接的意图。如果存在 href 属性,当 <a> 元素聚焦时按下回车键就会激活它。 - mozilla.org

当使用 a href 时,偶尔会遇到404,或者展示的页面不是我们想要它展示的那个, 这个问题是怎么一回事? 我们从简单的开始

href - 可真不简单

  1. a href 是绝对路径

如果你用 / 做开头, 它将以站点的URI路径作为前缀,而不是当前html的URI路径,例如域名是 localhost, 前缀则是 localhost:

<a href="/mywebsite/pictures/picture.png">

不论当前页面的URI是什么,它将指向:

"http://localhost/mywebsite/pictures/picture.png"

要访问一个资源,需要路径指明它在网站内的完整子目录

  1. a href 是相对路径

如果用一个文件{夹}的名称作开头, 将被加上你的html 文件的路径做前缀:

<a href="pictures/picture.png">

当前路径是 "http://localhost/index.html", 将访问

"http://localhost/pictures/picture.png"

当前路径是 "http://localhost/mywebsite/index.html", 将访问

"http://localhost/mywebsite/pictures/picture.png"

3. a href 是点-斜杠 (./)

点 (.) 是指当前目录,加上斜杠是去访问当前目录, 所以

"pictures/picture.png"

"./pictures/picture.png"

是相同含义

  1. a href 是点点-斜杠 (../)

点点是上级目录,而加上斜杠是去访问当前目录的上级, 这产生了一些细微的反应

"../picture.png"

当前路径是 "http://localhost/index.html", 将访问的路径在不同网页服务端会有所不同, 但为安全考虑,现在一般的服务端处理是去访问

"http://localhost/picture.png"

当前路径是 "http://localhost/mywebsite/index.html", 将访问

"http://localhost/picture.png"

(../) 可以多级使用,也可以和其他目录组合使用.

"../mywebsite/picture.png"

5. <base href="…">

base href 用于文档中 相对地址的基础 URL。允许绝对和相对URL。

base 给 href 带来剧烈的反应, 首先HTML规范定义了一个页面内出现多个 base时的情况

多个<base>元素:
如果指定了多个 <base>元素,只会使用第一个 href 和 target 值, 其余都会被忽略。

6. 其次HTML规范定义了a href是页内锚的情况 - <a href="#some-id">

当href 定义是指向文档中某个片段的链接,加上 <base> 后解析, 将触发前缀是 base 的 href 的 HTTP 请求。

例如:给定 `<base href="https://localhost">`

以及此链接 `<a href="#anchor">Anker</a>`

链接指向 https://localhost/#anchor

7. <a href="/mywebsite/pictures/picture.png"> - a href是绝对路径

不受base影响,仍将访问

"https://localhost/mywebsite/pictures/picture.png"

8. 相对路径的href受到的base影响, 分为4种情况

位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
* <base href="https://localhost">1. base 指示一个明确的带域名的URI

将访问

"https://localhost/pictures/picture.png"

非常好!建议这样使用base: 相对路径 a href 和 明确域名的base

注意,不带协议语句块的 (//) 也将被浏览器解析为一个域名, 例如

<base href="//localhost"> 

浏览器将自动测试端口去判断要使用的协议, https, http,等

位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
<base href="/">2.base是绝对路径

base是一个绝对路径, 结果开始令人意外, 所有的相对路径都变成了绝对路径,将访问

"https://localhost/pictures/picture.png"

<a href="picture.png"> 则访问

"https://localhost/picture.png"

9.

位置
<link rel="stylesheet" href="static/plugins/toastr/toastr.min.css">相对路径 href
http://localhost:8080/mywebsite/pictures当前访问的页面URI
* <base id="base" href="../">3. base也是相对路径

好消息是,结果很符合直觉,它们联合在一起,形成新的组合相对路径

"http://localhost:8080/mywebsite/static/plugins/toastr/toastr.min.css"

10.

位置
<link rel="stylesheet" href="static/plugins/toastr/toastr.min.css">相对路径
http://localhost:8080/mywebsite/pictures当前访问的页面URI
* <base id="base" href="/mywebsite">4. base是一个绝对路径,但不是带后缀(/)的完整目录

结果令人意外, 不是完整目录的 base href {tag}让所有的a href 变成了以域名为根目录的绝对路径, 最终将访问

http://localhost:8080/static/plugins/toastr/toastr.min.css

这样的base href 格式是错误的,不同浏览器间可能有不同的结果

  • 至于 <base id="base" href="/mywebsite/"> 的情况,和 8.绝对路径 相同

渐入佳境 - 乱成了一锅粥

Spring + JSTL + request.contextPath

纯粹的HTML要实现根据变量值动态展示数据十分繁琐,要按照HTML tag 解析和生成字符串. 所以前端开发语言和库基本都设置了HTML模板语法,用简单的模板语法生成复杂的HTML结果.

Spring 是最常用的 java 后端开发框架,也带有写前端界面的功能.

JSTL 是 Spring 用来写作 HTML 页面的模板语法.

mywarname.war - war 文件是spring 代码打包出的程序,它可以用tomcat网页服务端加载后展示出来.

${pageContext.request.contextPath} 是JSTL的一个语句,可以用来获得应用的被加载路径 - 一般是 "mywarname.war" 的 mywarname部分.

当把spring生成的WAR包放到 tomcat 网页服务器的webapp目录时, tomcat将拆开war, 把内里的页面布置到和 war 包的名称相同的路径. 而java程序常常需要相对 war包名称来定位路径,所以经常会用到 ${pageContext.request.contextPath}

${pageContext.request.contextPath} 有几种用法,常见的2种是

  • 设置 <base id="base" href="${pageContext.request.contextPath}">,修改页面的相对路径的基本前缀.
  • 设置一个spring变量 $(base), <#assign base=request.contextPath />,后面在需要的时候把${base}拼接到href的URI上

这两个主意都很机灵,正确的用法很多例子,可以搜索关键词查询,下面我们继续讨论 href 的复杂性

  1. JSTL 中base用绝对路径(/)带上${pageContext.request.contextPath}
<base id="base" href="/${pageContext.request.contextPath}/">

假设spring war 包名称是 mywarname.war, 则${pageContext.request.contextPath} 输出的是 /mywarname, 产生的HTML内将是

<base id="base" href="//mywarname/">

到这儿,事情开始异常的混乱起来,

位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
* <base id="base" href="//mywarname/">base是一个域名

将去访问捏造出的域名

//mywarname/pictures/picture.png

12. JSTL 中Base用相对路径(/)带上${pageContext.request.contextPath}

<base id="base" href="./${pageContext.request.contextPath}/">

产生的HTML内将是

<base id="base" href=".//mywarname/">

这样避免了产生新的域名,但 相对路径+相对路径 的结果,往往和设置这个base的初衷相反,

位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
* <base id="base" href=".//mywarname/">base是一个相对路径

将访问

http://localhost/mywebsite/mywarname/pictures/picture.png

你是要去哪里!

  1. 我们既不加绝对路径,也不用相对路径,
<base id="base" href="${pageContext.request.contextPath}">

产生的HTML内将是

<base id="base" href="/mywebsite">
位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
* <base id="base" href="/mywebsite">base是一个绝对路径,但不是带后缀(/)的完整目录

这产生了 10. 的情况,错误的导致所有 a href 变成了以域名为根目录的绝对路径

  1. tomcat 还支持把war部署到根目录 - 只要将 mywarname.war 命名为 ROOT.war

假设 mywarname.war 包含一个默认页面 index.html, 于是产生URL

http://localhost/index.html

同时当我们在用${pageContext.request.contextPath}设置 base href 时还指定了一个更深的一个子目录,例如

<base id="base" href="${pageContext.request.contextPath}/anotherSub/">

产生的HTML内将是

<base id="base" href="//anotherSub/">
位置
<a href="pictures/picture.png">相对路径
http://localhost/mywebsite/index.html当前路径
* <base id="base" href="//anotherSub/">base是一个域名

将访问捏造出的域名

//anotherSub/pictures/picture.png

这实在是乱透了.

  1. 直接在 a href 中使用${pageContext.request.contextPath}时,也会出现类似 11~15 的问题,不在赘述.

在a href 中直接用${pageContext.request.contextPath} 生成 href的好处是, 减少了 base tag 这一层对于URI的干扰, 让生成的URI更符合直觉了.

  1. 再看一种更复杂的情况, 当我们用nginx做了反向代理

    domain1.com/sub -> localhost/mysubsite

在访问时nginx上的网站, nginx 将认识 domain1 而不认识 localhost, 且nginx认识 sub目录而不认识 mysubsite 目录.

同时,若我们在href 中使用的路径是一个绝对路径,

<a href="/mysubsite/static/plugins/toastr/toastr.min.css">

nginx拉回去给浏览器显示的页面上, 上面的路径将生成

domain1.com/mysubsite/static/plugins/toastr/toastr.min.css

当客户端去访问nginx时, nginx并不认识domain1下的mysubsite目录,虽然可以通过 sub_filter 语句在反向代理拉回来的HTML代码中进行路径的替换. 但从此以后 nginx 的配置文件,就要跟着程序代码里写的URI变化而走了. 要在修改程序代码后,记得去同步修改nginx的配置, 这给自己造成了更多的麻烦.

  1. 所以我们就不要使用 ${pageContext.request.contextPath}-mywarname.war 名称 来访问资源了,写一个只使用 HTML语法设置 href的JSTL,这样解决了问题吗?

我们接下来要面对的情况已经简单多了.

要架设spring+tomcat的网站, 一般教程是让把WAR文件放到 tomcat 的 webapps 目录, 这是 tomcat默认配置开设的虚拟主机的代码目录.

可tomcat没有限制WAR文件必须放到根URI路径下才能访问, 要修改访问 war 包内部资源所使用的URI路径前缀:

  • 可以通过设置 server.xml , 在 <HOST> 块内使用<Context> 块指定 war包 所在的目录和它要占用的URI路径前缀, 例如 sub

  • 或者把 mywarname.war 命名成 sub#mywarname.war, 产生的URL将是

    http://localhost/sub/mywarname/index.html

若HTML中像 18. 一样使用了绝对路径的 href ,这个 href 将无法跟着 war 文件在tomcat设置中绷定的URI 前缀变化而变化.

于是为了适应tomcat设置的任意 war前缀路径,我们只能选择

  • 或使用${pageContext.request.contextPath} 获得移动后的路径,
  • 或使用纯HTML的相对路径指定 href

结论

到此可以做出总结,为避开href的各种坑, 理应:

  • 本{程序包}内的网页和资源,只用相对路径去访问.
  • 访问绝对路径时理应明确指定一个域名
  • 杜绝使用语言或模板库自带的URI定位器(如当前文件的路径)去设置 HTML 的 base tag. HTML 作为一种动态解析的标记语言,字符串可以任意组合,每引入一层字符串的变化都要增加数倍复杂度.