常見面試題 - URL 解析

眷你發表於2018-10-30

本文源自之前面試的時候頻繁要求手寫 url parse ,故針對此種情況專門寫一文來簡述如何解析 URL ,如果您有更好的解析方法或題型變種歡迎討論

注意,本文僅討論開頭所列出的一種格式,尚未討論 URL 的更多格式,更多符合規範的格式(如使用相對路徑等的情況)詳見:tools.ietf.org/html/rfc398…

URL 是啥樣的

首先讓我們看看一種完整的 URL 是長什麼樣的: <scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>

如果這樣太抽象了,那麼我們舉個例子具體化一下: https://juanni:miao@www.foo.com:8080/file;foo=1;bar=2?test=3&miao=4#test

元件 描述 預設值
scheme 訪問伺服器獲取資源時使用的協議 https
user 訪問資源時使用的使用者名稱 無(匿名) juanni
password 使用者的密碼,和使用者名稱使用:分割 E-mail miao
host 資源伺服器主機名或IP地址 www.foo.com
port 資源伺服器監聽的埠,不同的scheme有不同的預設埠(HTTP使用80作為預設埠) 和scheme有關 8080
path 伺服器上的資源路徑。路徑與伺服器和scheme有關 預設值 /file
params 在某些scheme下指定輸入引數,是鍵值對。可以有多個,使用;分割,單個內的多個值使用, 分割 預設值 foo=1;bar=2
query 該元件沒有通用的格式,HTTP中打多使用&來分隔多個query。使用?分隔query和其他部分 test=3&miao=4
frag/fragment 一小片或一部分資源名稱。引用物件時,不會將fragment傳送給伺服器,客戶端內部使用。通過#分隔fragment和其餘部分 test

由於 path parameterpath 的一部分,因此我們將其歸為 path

同時,如果要表示哪些部分是可選的,則可以表示為: [scheme:]//[user[:password]@]host[:port][/path][?query][#fragment]

如何獲取每個元件

我們先不考慮元件內部的資料,先獲取每個元件

讓瀏覽器幫我們解析 - URLUtils

先介紹一個偷懶的方式: URLUtils ,可以通過該介面獲取 href 、 hostname 、 port 等屬性。

在瀏覽器環境中,我們的 a 標籤,也就是 HTMLAnchorElement 實現了 URLUtils 中定義的屬性,那麼就可以用如下程式碼獲得每個元件了

/**
 * @param  {string} url
 * 利用 URLUtils 簡單解析 URL
 * @returns {protocol, username, password, hostname, port, pathname, search, hash}
 */
function URLParser(url) {
    const a = document.createElement('a');
    a.href = url;
    return {
        protocol: a.protocol,
        username: a.username,
        password: a.password,
        hostname: a.hostname, // host 可能包括 port, hostname 不包括
        port: a.port,
        pathname: a.pathname,
        search: a.search,
        hash: a.hash,
    }
}
複製程式碼

缺點:

  • 依賴瀏覽器宿主環境介面

使用 URL 物件

上面使用 a 標籤的方法在 Node 環境中就失效了,但是我們還有其他方法可以讓底層 API 幫我們解析 —— URL

/**
 * @param  {string} url
 * 利用 URLUtils 簡單解析 URL
 * @returns {protocol, username, password, hostname, port, pathname, search, hash}
 */
function URLParser(url) {
    const urlObj = new URL(url);
    return {
        protocol: urlObj.protocol,
        username: urlObj.username,
        password: urlObj.password,
        hostname: urlObj.hostname,
        port: urlObj.port,
        pathname: urlObj.pathname,
        search: urlObj.search,
        hash: urlObj.hash,
    }
}
複製程式碼

老老實實手擼一個

那要是面試官要老老實實的手擼,那也只能對著擼了:

function parseUrl(url) {
    var pattern = RegExp("^(?:([^/?#]+))?//(?:([^:]*)(?::?(.*))@)?(?:([^/?#:]*):?([0-9]+)?)?([^?#]*)(\\?(?:[^#]*))?(#(?:.*))?");
    var matches =  url.match(pattern) || [];
    return {
        protocol: matches[1],
        username: matches[2],
        password: matches[3],
        hostname: matches[4],
        port:     matches[5],
        pathname: matches[6],
        search:   matches[7],
        hash:     matches[8]
    };
}
parseUrl("https://juanni:miao@www.foo.com:8080/file;foo=1;bar=2?test=3&miao=4#test")
// hash: "#test"
// hostname: "www.foo.com"
// password: "miao"
// pathname: "/file;foo=1;bar=2"
// port: "8080"
// protocol: "https:"
// search: "?test=3&miao=4"
// username: "juanni"
複製程式碼

這個正則確實有點難懂,不過相信有一些基礎的話加上下面兩張圖還是可以理解:

image.png | left | 747x138

image.png | left | 747x70

解析 search(query) 部分

偷懶使用 URLSearchParams

/**
 * @param  {string} search 類似於 location.search
 * @returns {object}
 */
function getUrlQueyr(search) {
    const searchObj = {};
    for (let [key, value] of new URLSearchParams(search)) {
        searchObj[key] = value;
    }
    return searchObj;
}
複製程式碼

優點:

  • 不需要手動使用 decodeURIComponent
  • 會幫著把 query 上的 + 自動轉換為空格(單獨使用 decodeURIComponent 做不到這點)(至於什麼情況把 空格 轉換為 + ,什麼情況把空格轉換為 %20,可以參考這裡等
  • 不支援如 array[] / obj{} 等形式

再手擼一個(殘缺版)

要求:

  • 對於非法字元不予解析
  • 對於形如 list[] 的解析成陣列
  • 對於形如 obj{} 的解析為物件(暫時只需要用 JSON.parse 進行解析)
/**
 * @param  {string} query 形如 location.search
 * @returns {object}
 */
function parseQueryString(query) {
    if (!query) {
        return {};
    }
    query = query.replace(/^\?/, '');
    const queryArr = query.split('&');
    const result = {};
    queryArr.forEach(query => {
        let [key, value] = query.split('=');
        try {
            value = decodeURIComponent(value || '').replace(/\+/g, ' ');
            key = decodeURIComponent(key || '').replace(/\+/g, ' ');
        } catch (e) {
            // 非法
            console.log(e);
            return;
        }
        const type = getQuertType(key);
        switch(type) {
            case 'ARRAY':
                key = key.replace(/\[\]$/, '')
                if (!result[key]) {
                    result[key] = [value];
                } else {
                    result[key].push(value);
                }
                break;
            case 'JSON': 
                key = key.replace(/\{\}$/, '')
                value = JSON.parse(value);
                result.json = value;
                break;
            default:
                result[key] = value;
        }
        
    });
    return result;
    function getQuertType (key) {
        if (key.endsWith('[]')) return 'ARRAY';
        if (key.endsWith('{}')) return 'JSON';
        return 'DEFAULT';
    }
}

const testUrl = 
'?name=coder&age=20&callback=https%3A%2F%2Fmiaolegemi.com%3Fname%3Dtest&list[]=a&list[]=b&json{}=%7B%22str%22%3A%22abc%22,%22num%22%3A123%7D&illegal=C%9E5%H__a100373__b4'
parseQueryString(testUrl)
複製程式碼

當然,這裡還並不嚴謹,沒有考慮到如下問題

  1. 相同欄位如何處理
  2. 沒有替換 +
  3. 只有 key
  4. 只有 value
  5. 沒有解析相對路徑
  6. 更深入的解析 Object

最後,這裡推薦一個開源庫:url-parse,對各種情況處理的比較好,同時這也意味著實現上略複雜,理解即可,面試中更需結合充分理解面試官要求進行解答與擴充套件

參考

相關文章