常見面試題 - URL 解析
本文源自之前面試的時候頻繁要求手寫 url parse ,故針對此種情況專門寫一文來簡述如何解析 URL ,如果您有更好的解析方法或題型變種歡迎討論
注意,本文僅討論開頭所列出的一種格式,尚未討論 URL 的更多格式,更多符合規範的格式(如使用相對路徑等的情況)詳見: ofollow,noindex">tools.ietf.org/html/rfc398…
URL 是啥樣的
首先讓我們看看一種完整的 URL 是長什麼樣的: <scheme>://<user>:<password>@<host>:<port>/<path>;<params>?<query>#<frag>
如果這樣太抽象了,那麼我們舉個例子具體化一下: https://juanni:[email protected]:8080/file;foo=1;bar=2?test=3&miao=4#test
元件 | 描述 | 預設值 | |
---|---|---|---|
scheme | 訪問伺服器獲取資源時使用的協議 | 無 | https |
user | 訪問資源時使用的使用者名稱 | 無(匿名) | juanni |
password | 使用者的密碼,和使用者名稱使用 : 分割 |
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 parameter
是 path
的一部分,因此我們將其歸為 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:[email protected]: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" 複製程式碼
這個正則確實有點難懂,不過相信有一些基礎的話加上下面兩張圖還是可以理解:


解析 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{}
等形式
再手擼一個(殘缺版)
要求:
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) 複製程式碼
當然,這裡還並不嚴謹,沒有考慮到如下問題
- 相同欄位如何處理
- 沒有替換
+
為 - 只有
key
- 只有
value
- 沒有解析相對路徑
- 更深入的解析
Object
最後,這裡推薦一個開源庫: url-parse ,對各種情況處理的比較好,同時這也意味著實現上略複雜,理解即可,面試中更需結合充分理解面試官要求進行解答與擴充套件