本文概要:
当我们遇到要解析处理 url 时,务必使用各种语言里处理 url 的标准库,例如
golang
中的url.Parse
,javascript
中的new URL
一、认识 URL
URL(Uniform Resource Locator)是统一资源定位符的缩写,用于标识和定位互联网上的资源。它是一个字符串,用于指定网络上某个资源的地址。
下图给出了一个 URL 关键部分的示例
- url规范中并不是仅仅包含上面一些部分,比如http协议的
Basic
认证,除了在header
部分Authorization
传递外,也可以直接在 url 中传递用户名密码https://username:password@www.domain.com
,当然除了极特殊场景外强烈不推荐这么干 - 除了
https/http
协议外,其实还有很多其他协议,比如ftp
、smtp
等都是标准的 URL Protocal,所以不要将 URL 局限在 http 协议上,甚至目前移动端的很多页面跟原生应用交互,都是通过自定义一些协议类型来实现的,比如android
的deeplink
,可以自定一个协议,比如myapp://path/to/resource
,然后触发唤起 app - 对于锚点(Anchaor/Fragment)这个是仅仅对浏览器有效,对于后端服务器或者搜索引擎实际上是读不到该参数的,即使锚点值不同,也会被认为是相同的 URL,但是 Query 不一样,服务器能读取 Query 参数,搜索引擎也会认为不同的 Query 为不同的页面
二、URL 是如何工作的
当我们在浏览器,敲入任意一个 URL 后,URL 是如何定位到具体资源的呢?这过程中,每一个环节又是如何被攻城狮们利用起来干涉用户最终访问到的资源呢?
当然上图中的一些系统架构是穿插在多个阶段的,并不是严格的在某个阶段完成
- 负载均衡:负载均衡本身可以在很多个阶段去处理,dns作为距离用户最近的服务,处理效率自然是最高的,当然控制起来可能也是最不精细的,dns本身会被各级缓存;
- CDN:cdn通常需要将域名托管到对应服务商,实际上即相当于将 dns 解析后的 ip 指向了 cdn 厂家,cdn 厂家再通过部署大量的 cdn 边缘节点,按照一定规则就近返回相关资源给用户,进而加速对用户的响应速度;
- 代理服务器:各个公司会建立一个统一的服务器(网关服务),统一来接受用户请求,并根据 Host 中的域名再将请求转发给下游服务,网关可以统一来做鉴权,日志,限流,熔断等通用服务能力控制;
- 服务器缓存&客户端缓存:现代服务中实际上有大量的缓存在应用,无论是客户端还是服务端缓存都会大大提高用户的体验,当然也会带来数据非及时更新的问题,所以 http 协议头中存在
Cache-Control
字段,业务研发可以根据自己的场景选择是否缓存以及缓存时间,目前像 css/js/image 这种资源一般都会缓存很久,甚至是永久缓存,所以想要更新这些资源的办法一般是在 html 中修改引用资源的地址,比如增加v=20230812
这样的参数,或者主动刷新 cdn - 服务端渲染&浏览器渲染:
- 服务端渲染:一种是传统的基于 php 模版,jsp,或者go template 等服务端直接输出 html 的渲染, 另外一种就是 react/vue 提供的服务端渲染,前后代码复用,通过一定的技术架构,在服务端运行渲染引擎输出 html(可以是即时的也可以是预先渲染好存成静态 html 文件)
- 浏览器渲染:目前前端最热的技术,数据驱动页面内容, 服务器返回的 html 中并无内容,通过接口返回数据后,渲染引擎根据数据在浏览器中渲染出 html
对于服务端渲染来说,不同的 url 会让服务器直接定位到不同的资源或者处理逻辑,对于客户端来说,可能不同的 url 背后是同样的 html/js/css 资源,客户端拿到这些资源后,再根据 url 的不同,路由到不同的处理逻辑中请求不同的接口获取数据,最终呈现不一样的页面
三、解析处理 URL 的最佳实践
1、面对 URL 常见的需求
- 客户端需要拦截特定的 URL,进行特定操作,比如拦截 /phone 路径,并跳转到特定原生页面
- URL 中附加一个参数,比如 from=bd
- 检查 URL,并保证其一定是 https
- 检查 URL,比保证 URL 都是以 / 结尾
- ...
总之日常中对 URL 的处理需求还是很多的,只要是对 URL 进行修改或者判断,实际上都在这个范畴中
2、如何正确的解析 URL:使用 URL 标准库
URL 是有专门规范的,并且是互联网非常基础的设施,所以各个语言都会有针对 URL 处理的标准库,所以处理 URL 最正确的方式就是各种语言的标准库,切记不要把 URL 当做普通字符串来处理,很有可能某个细节没注意到位就会导致线上事故。
示例:
// golang
package main
import (
"fmt"
"net/url"
)
func main() {
u, _ := url.Parse("https://www.domain.com:443/what-is-url/?from=bd#title")
fmt.Println("Scheme:", u.Scheme)
fmt.Println("Hostname:", u.Hostname())
fmt.Println("Post:", u.Port())
fmt.Println("Path:", u.Path)
fmt.Println("Query:", u.Query())
fmt.Println("Fragment:", u.Fragment)
}
// Output
Scheme: https
Hostname: www.domain.com
Post: 443
Path: /what-is-url/
Query: map[from:[bd]]
Fragment: title
// javascript
u = new URL("https://www.domain.com:443/what-is-url/?from=bd#title")
console.log(u)
// Output,js 下默认默认端口解析不出来,如果是非默认端口则会有值
{
hash: "#title"
host: "www.domain.com"
hostname: "www.domain.com"
href: "https://www.domain.com/what-is-url/?from=bd#title"
origin: "https://www.domain.com"
password: ""
pathname: "/what-is-url/"
port: ""
protocol: "https:"
search: "?from=bd"
searchParams: URLSearchParams {size: 1}
username: ""
}
java 中也有 java.net.URL
库,php 有 parse_url
函数
3、解析中常遇到的坑
- url 的解析通常并非严格模式,比如说
/what-is-url/
这个也会被解析,相应的 host,scheme 等内容都会被解析为空,所以在验证 url 是否完整时,务必判断 hostname 是否存在 www.domain.com/what-is-url
这种人看上去看似正确的 url,在标准库解析后反而结果会出异常,比如这个 path 会被解析为www.domain.com/what-is-url/
,并不符合预期,所以这种需要添加 http 协议头后再进行解析- path 部分解析处理是一定要注意开头结尾的
/
,最好是 trim("/") 后,然后根据需要判断 url 内容,不要自己按照 string 方式处理,查找 ? 后截取字符串这种处理方式 - 空字符串解析时仍然不会报错,所以处理时最好先判空
- 在 Query 中添加参数,不要自己解析,URL 处理库中都有解析修改 Query 参数的函数
- 对于 URL 来说,结尾加不加
/
,实际上对于前端SPA来说是两个路由(后端其实也是,只不过后端接口通常默认配置都会忽略结尾的/),所以前端要兼容/
这个路由
4、URL 推荐规范
- 使用 https
免费的
Let's Encrypt
与TrustAsia
,或者使用Caddy
http 服务器,可以自动注册 https 证书,增加自动 http 向 https 跳转基础配置
- 统一 url path 部分结尾,可以选择以
/
结尾,这个配置应该在 http 服务器层做,作为规范配置
切记在处理 url 结尾添加 / 时要注意,如果 url path 部分是有扩展名的,比如
.html
,.txt
等,不要处理;另外对于前端 SPA 页面来说,加不加/
是两个路由,需要配置支持
- 尽可能的使用 301 重定向
搜索引擎在区分 301 和 302 时有略微差异,对于 301 重定向,相关老页面的权重会被带到新页面