跳到主要內容
黯羽輕揚每天積累一點點

解析 URL 的前端方案

免費2016-08-27#JS#利用a标签解析URL#正则解析URL#javascript URL class#如何解析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 年工作經驗)

參考資料

評論

暫無評論,快來發表你的看法吧

提交評論