常見面試題 - URL 解析

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

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

URL 是啥樣的

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

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

組件 描述 默認值
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 的一部分,所以咱們將其歸爲 pathgithub

同時,若是要表示哪些部分是可選的,則能夠表示爲: [scheme:]//[user[:password]@]host[:port][/path][?query][#fragment]面試

如何獲取每一個組件

咱們先不考慮組件內部的數據,先獲取每一個組件json

讓瀏覽器幫咱們解析 - 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,對各類狀況處理的比較好,同時這也意味着實現上略複雜,理解便可,面試中更需結合充分理解面試官要求進行解答與擴展

參考

相關文章
相關標籤/搜索