解析URL的前端方案

写在前面

7月11日-8月27日,一个半月过去了,以周为单位的时间过得特别快

每天都忙,却记不清在忙些什么。期间

  • 没有再翻过新书(写博客时偶尔翻过Node和JS,因为内容有些淡忘了)

  • 看了1.5篇散文

  • Kindle充电2次,但从未使用过

  • 日语学习到第七課结束

  • 周一FEX周刊(有一次是周二发的),周五奇舞周刊(这周来自凹凸实验室的mock淘宝造物节全景很不错)

最近在看什么书,有推荐的吗?

日语书,没有大片时间看书,对日常工作熟练了就有时间了

真是这样吗?为什么没有大片时间?

一.问题简述

请用URL规范的办法取出host,然后判断host,不要按照单个字符判断

场景就是这样,当前页的query string携带了一个url参数,我们需要从中解析出hostname

location.hostname能够取出当前页的主机名,那对于任意一个URL串呢?有非正则方案吗?

之前笔者认为是没有的,记忆中JS API没有提供new URL()之类的,以为必须通过正则解析

出去搓的时候提出了这个问题,三位老司机瞬间给出了3种方案

二.解决方案

标准URL格式为:scheme://domain:port/path?query_string#fragment_id,简单的正则捕获分分钟解析好

也有一些奇怪的(精心构造的)URL:

http://www.example.com/public/page/2015/index.html?url=http://12.23.34.45/hack.html?http://www.example.com//check.htm

http://www.example.com/public/page/2015/index.html?url=http://www.example.com@12.23.34.45/hack.html

P.S.为了看清楚,url参数部分没有进行encode,这样的参数看着就不怀好意

甚至还有一些不合规范的URL,使用一般的正则很难应对,比如:

// 这个东西作何解?
http://www.example.com/what??key=val?&&#123http://@www.abc.com?query=2#45
// 那这个呢?
http://www.example.com:8899@www.abc.com/what??key=val?&&#123http://?query=2#45
// 这个?
http://www.example.com:$88;9,9@www.abc.com$/what??key=val?&&#123http://?query=2#45
//...

1.a标签自动解析URL

var a = document.createElement('a');
a.href = 'http://www.example.com/news.php?id=10#footer';

var div = document.createElement('div');
for (var key in a) {
    !(key in div) && console.log(`${key} = ${a[key]}`);
}

输出结果是这样:

// 说明在哪里显示指向的资源,当前窗体、新标签页等等
target = 
// 通知UA下载指向的资源
download = 
// 点击链接时异步POST指定地址,用于广告统计
ping = 
// 表明指向的资源与当前资源的关系,备胎、书签等等
rel = 
// 指向的资源的language
hreflang = 
// 指向的资源的MIME type
type = 
// 请求头referer字段策略,用于保护用户隐私
referrerpolicy = 
// 
text = 
// 已废弃。支持自定义形状,传入一系列坐标点
coords = 
// 已废弃。指向的资源的字符编码
charset = 
// 已废弃。跳转到指定name的标签
name = 
// 已废弃。反向关系,rel的反义词
rev = 
// 已废弃。用于指定自定义形状热区
shape = 
// 指向的资源的URL,或者URL的#fragment_id部分
href = http://www.example.com/news.php?id=10#footer
origin = http://www.example.com
protocol = http:
username = 
password = 
host = www.example.com
hostname = www.example.com
port = 
pathname = /news.php
search = ?id=10
hash = #footer

这些都是a标签特有的属性,里面就有我们想要的hostname,也就是说,a标签自动完成了URL解析,对于前端来说,这曾经是解析URL最廉价的方式:

var getHostname = function(url) {
    var a = document.createElement('a');
    a.href = url;
    return a.hostname;
};

100%可靠,再复杂的URL也骗不了浏览器

2.JS URL API

var url = new URL('http://www.example.com:$88;9,9@www.abc.com$/what??key=val?&&#123http://?query=2#45');
for (var key in url) {
    console.log(`${key} = ${url[key]}`);
}

Chrome下输出结果:

searchParams = %3Fkey=val%3F
href = http://www.example.com:$88%3B9,9@www.abc.com%24/what?%3Fkey=val%3F#123http://?query=2#45
origin = http://www.abc.com%24
protocol = http:
username = www.example.com
password = $88%3B9,9
host = www.abc.com%24
hostname = www.abc.com%24
port = 
pathname = /what
search = ?%3Fkey=val%3F
hash = #123http://?query=2#45

因为这个URL太不合标准,UA处理细节有差异,FF下的结果不同:

"href = http://www%2Eexample%2Ecom:$88%3B9,9@www.abc.com$/what??key=val?&&#123http://?query=2#45"
"origin = http://www.abc.com$"
"protocol = http:"
username = www%2Eexample%2Ecom
password = $88%3B9,9
host = www.abc.com$
hostname = www.abc.com$
port = 
pathname = /what
search = ??key=val?&&
searchParams = %3Fkey=val%3F
hash = #123http://?query=2#45

浏览器确实偷偷提供了URL类,不是ES5也不是ES6、7标准,目前只是实验性的特性,兼容性如下:

Android4.0  webkitURL
Android4.4  URL
Safari6.0   webkitURL
Chrome32    URL
FF19        URL
IE10        URL

移动端基本可以放心使用,更多兼容性信息请查看URL – Web APIs | MDN

var getHostname = function(url) {
    return new URL(url).hostname;
};

3.正则解析

var parseUrl = function(url) {
    var urlParseRE = /^\s*(((([^:\/#\?]+:)?(?:(\/\/)((?:(([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/;

    var matches = urlParseRE.exec(url || "") || [];

    return {
        href:         matches[0] || "",
        hrefNoHash:   matches[1] || "",
        hrefNoSearch: matches[2] || "",
        domain:       matches[3] || "",
        protocol:     matches[4] || "",
        doubleSlash:  matches[5] || "",
        authority:    matches[6] || "",
        username:     matches[8] || "",
        password:     matches[9] || "",
        host:         matches[10] || "",
        hostname:     matches[11] || "",
        port:         matches[12] || "",
        pathname:     matches[13] || "",
        directory:    matches[14] || "",
        filename:     matches[15] || "",
        search:       matches[16] || "",
        hash:         matches[17] || ""
    };
};

被吓哭了,试一试够不够健壮:

var url = parseUrl('http://www.example.com:$88;9,9@www.abc.com$/what??key=val?&&#123http://?query=2#45');
for (var key in url) {
    console.log(`${key} = ${url[key]}`);
}

输出结果:

href = http://www.example.com:$88;9,9@www.abc.com$/what??key=val?&&#123http://?query=2#45
hrefNoHash = http://www.example.com:$88;9,9@www.abc.com$/what??key=val?&&
hrefNoSearch = http://www.example.com:$88;9,9@www.abc.com$/what
domain = http://www.example.com:$88;9,9@www.abc.com$
protocol = http:
doubleSlash = //
authority = www.example.com:$88;9,9@www.abc.com$
username = www.example.com
password = $88;9,9
host = www.abc.com$
hostname = www.abc.com$
port = 
pathname = /what
directory = /
filename = what
search = ??key=val?&&
hash = #123http://?query=2#45

与FF完全一致,结果可信度很高。那么我们尝试解读一下这个无敌的正则:

/^                      #href
\s*
(                       #hrefNoHash
  (                     #hrefNoSearch
    (                   #domain
      ([^:\/#\?]+:)?    #protocol
      (?:
        (\/\/)          #doubleSlash
        (               #authority
          (?:
            (           #取结果时$7被跳过了,应该也用非捕获型括号(?:
              ([^:@\/#\?]+)     #username
              (?:
                \:
                ([^:@\/#\?]+)   #password
              )?
            )
            @
          )?
          (                     #host
            ([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])    #hostname
            (?:
              \:
              ([0-9]+)  #port
            )?
          )
        )?
      )?
    )?
    (                   #pathname
      (\/?(?:[^\/\?#]+\/+)*)    #directory
      ([^\?#]*)         #filename
    )
  )?
  (\?[^#]+)?            #search
)
(#.*)?                  #hash
/

根据上面的分析,第9个左括号应该用非捕获型括号(?:,取值的时就不用跳过$7了,如下:

var getHostname = function(url) {
    // 第9个小括号改掉了
    var urlParseRE = /^\s*(((([^:\/#\?]+:)?(?:(\/\/)((?:(?:([^:@\/#\?]+)(?:\:([^:@\/#\?]+))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((\/?(?:[^\/\?#]+\/+)*)([^\?#]*)))?(\?[^#]+)?)(#.*)?/;

    var matches = urlParseRE.exec(url || "") || [];

    return matches[10] || "";
};

看瞎了,正则再见。

三.方案分析

a标签

老司机1前端经验丰富,冷门技巧瞬间解决问题

兼容性没问题(很多年前的技巧了),纯前端方案,简单粗暴疗效确切,a标签竟然这么强大

更多冷门技巧请查看前端不为人知的一面–前端冷知识集锦,昨天新发现的另一位老前辈,追随之

URL类

老司机2眼界开阔,细节扎实

非标准的URL类都知道,我天天用console也没主意这个,不细心就会少经验,就像超级玛丽

无敌正则

老司机3解决问题经验丰富,资源积累很多

这正则,吓哭了,orz

四.总结

早出晚归,不涨经验,我在忙些什么?

时间碎片化,没有明确的当前时段task,一抬头,又半小时过去了,一转眼,又该开周会了……一个半月过去了,经验条一动不动

长此以往,泯然码农矣(3年工作经历,1年工作经验)

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*

code