关于前端处理url你想知道的都在这儿,js手写实现常见url处理的总结🔥查询字符串的解析

2,956 阅读14分钟

这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战

JavaScript中的URL对象以及如何解析URL

URL —— Uniform Resource Locator,统一资源定位符,或统一资源定位器,是对网络资源的引用,通过它可以定位一个唯一的网络资源或位置,比如网页、图像或文件等。同时 URL 中还包含获取资源的机制(也就是协议),比如 http、ftp、mailto 等。对于 JavaScript 来说,通常处理的都是http或https协议的url。

对于 url 的处理,浏览器原生提供了 URL 对象,通过 URL() 构造函数,可以构造、解析和编码URL,获取各个组成部分。此外,还可以借助正则、字符串处理,实现自己的解析方式!

URL结构

一个完整的url结构,可以分为:protocol://username:password@hostname:port/pathname?search#hash

如下图所示,hostname:port 组成 hostpathname?search 组成 pathsearch 部分通常被称为查询字符串(Query String),页面中的锚点(#)叫做哈希。

URL() 构造函数

创建 URL()

通过 window.URL 就可以在浏览器中获取到 URL() 构造函数。如下,是构造函数的说明

const url = new URL(relativeOrAbsolute [, absoluteBase]); // 或 new URL([URL Instance]);

relativeOrAbsolute 既可以是绝对路径,也可以是相对路径。如果第一个参数是相对路径,则第二个参数 absoluteBase 必传,它表示第一个参数相对应的绝对路径基础。

传递一个绝对路径的url参数。

const url = new URL('http://www.example.com/index.html');
url.href;   // => 'http://www.example.com/index.html'

传递相对路径和绝对路径的参数。

const url1=new URL('path/index.html','http://www.example.com');
url1.href;  // => 'http://www.example.com/path/index.html'

URL的参数还可以是另一个URL实例。

const url2=new URL(url);
url2.href   // => 'http://www.example.com/index.html'

URL() 实例属性

URL() 实例可以获取到url的各个部分,包含的属性与 Location 对象的属性基本一致。

  • URL.href:返回整个 URL
  • URL.protocol:返回协议,以冒号:结尾
  • URL.hostname:返回域名
  • URL.host:返回域名与端口,包含:号,默认的80和443端口会省略
  • URL.port:返回端口
  • URL.origin:返回协议、域名和端口
  • URL.pathname:返回路径,以斜杠/开头
  • URL.search:返回查询字符串,以问号?开头。不包含 #hash 哈希部分。
  • URL.searchParams:返回一个URLSearchParams实例,该属性是Location对象没有的。并且 .searchParams 会包含 #hash 进去。
  • URL.hash:返回片段识别符,以井号#开头
  • URL.password:返回域名前面的密码
  • URL.username:返回域名前面的用户名

下面是 URL() 实例的接口列表:

interface URL {
  href:     USVString;
  protocol: USVString;
  username: USVString;
  password: USVString;
  host:     USVString;
  hostname: USVString;
  port:     USVString;
  pathname: USVString;
  search:   USVString;
  hash:     USVString;

  readonly origin: USVString;
  readonly searchParams: URLSearchParams;

  toJSON(): USVString;
}

USVString 参数会在JavaScript中映射成字符串。

对于属性的修改会立即生效。

const url = new URL('http://www.example.com/index.html');
url.pathname='/a/b/c';
url.href;   // => 'http://www.example.com/a/b/c'
url.hash='key';
url.href;   // => 'http://www.example.com/a/b/c#key'

在 URL() 实例中,只有 origin 和 searchParams 属性是只读的。 其他属性都是可写的,会修改原来的URL。

静态方法

URL.createObjectURL()

URL.createObjectURL()方法用来为 上传/下载的文件、流媒体文件 生成一个 URL 字符串。这个字符串代表了File对象或Blob对象的 URL。

也就是,可以为一个生成URL,便于在页面显示和操作。

// HTML 代码如下
// <div id="display"/>
// <input
//   type="file"
//   id="fileElem"
//   multiple
//   accept="image/*"
//   onchange="handleFiles(this.files)"
//  >
let div = document.getElementById('display');

function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    let img = document.createElement('img');
    img.src = window.URL.createObjectURL(files[i]);
    div.appendChild(img);
  }
}

URL.createObjectURL()方法用来为上传的文件生成一个 URL 字符串,作为<img>元素 src 的图片来源。

生成的 URL 类似这样的格式:blob:http://localhost/c745ef73-ece9-46da-8f66-ebes574789b1

每次使用 URL.createObjectURL() 方法,都会在内存里面生成一个 URL 实例。如果不再需要该 URL 字符串,为了节省内存,需要使用 URL.revokeObjectURL() 方法释放这个实例。

URL.revokeObjectURL()

URL.revokeObjectURL() 方法用来释放 URL.createObjectURL() 生成的 URL 实例。它的参数就是 URL.createObjectURL() 方法返回的 URL 字符串。

为上面的示例加上 URL.revokeObjectURL()

let div = document.getElementById('display');

function handleFiles(files) {
  for (let i = 0; i < files.length; i++) {
    let img = document.createElement('img');
    img.src = window.URL.createObjectURL(files[i]);
    div.appendChild(img);
    img.onload = function() {
      window.URL.revokeObjectURL(this.src);
    }
  }
}

图片加载成功后,为本地文件生成的 URL 字符串就不需要了。可以在 img.onload 回调函数里释放该实例。

查询字符串(Query String)

url.search

url.search 可以获取到 URL 当中 ? 后面的 query 字符串。

const url=new URL('http://example.com/path/index?message=hello&who=world');
url.search;     // => '?message=hello&who=world'

查询字符串不存在,url.search 返回空字符串 ''

url.searchParams 属性和 URLSearchParams 对象

url.searchParams 属性返回的是一个 URLSearchParams 对象,它也是浏览器的原生对象。用来构造、解析和处理 URL 的查询字符串。可以方便的获取具体的参数,相对于 url.search 来说,有着更高的实用性和应用场景。

URLSearchParams 对象

URLSearchParams 也是一个构造函数,参数可以是查询字符串(开头的?可以省略),也可以是数组和对象。

// 方法一:传入字符串
let params = new URLSearchParams('?message=hello&who=world');
// 或者当前url的查询字符串
let params = new URLSearchParams(document.location.search);

// 方法二:传入数组
let params = new URLSearchParams([['message', 'hello'], ['who', 'world']]);

// 方法三:传入对象
let params = new URLSearchParams({'message' : 'hello' , 'who' : 'world'});

URLSearchParams会自动对查询字符串编码。

let params1 = new URLSearchParams([['message', '你好'], ['who', 'world']]);
params1.toString();     // => 'message=%E4%BD%A0%E5%A5%BD&who=world'

URLSearchParams 获取元素

  • get方法,获取指定查询字符串参数的值

URLSearchParams 可以与 URL() 接口结合使用,如下,获取当前url中查询字符串的 who 参数值“

let url = new URL(window.location);
let foo = url.searchParams.get('who') || 'somedefault';

get() 参数为查询字符串的键。返回的是字符串,如果需要,要进行类型转换处理;键名不存在是,返回值为 null

多个同名键,将只返回最前的键值:

let params5 = new URLSearchParams('?foo=3&foo=2&foo=1');
params5.get('foo');     // '3'

getAll() 返回指定键的所有键值组成的数组。

params5.getAll('foo');      // => ['3', '2', '1']

URLSearchParams 实例作为网络请求的表单数据

向服务器发送表单数据时,可以直接使用 URLSearchParams 实例:

const params = new URLSearchParams({foo: 1, bar: 2});
fetch('https://example.com/api', {
  method: 'POST',
  body: params
}).then(...)

fetch 命令向服务器发送命令时,可以直接使用 URLSearchParams 实例。

使用 for...of 遍历 URLSearchParams

URLSearchParams 实例有遍历器接口,可以使用 for...of 遍历。

let params1 = new URLSearchParams([['message', '你好'], ['who', 'world']]);
for(let p of params1) { 
    console.log(p[0],':',p[1]) 
}
// message : 你好
// who : world

URLSearchParams.toString() 获取查询字符串

const url=new URL('http://example.com/path/index?message=hello&who=world')

url.searchParams.toString();        // => 'message=hello&who=world'
url.search                          // => '?message=hello&who=world'

URLSearchParams 对象在使用时,如果需要字符串,会自动调用 toString() 方法。

let params = new URLSearchParams({version: 2.0});
window.location.href = location.pathname + '?' + params;

URLSearchParams.append(key,value) 追加查询参数

参数为 键名 和 键值。并且 append() 方法不会识别键名是否存在,有同名键也会追加。

let params = new URLSearchParams({'foo': 1 , 'bar': 2});
params.append('baz', 3);
params.toString() // "foo=1&bar=2&baz=3"

URLSearchParams.set(key,value) 设置或追加查询参数

URLSearchParams.set(key,value) 指定的键名参数,会修改对应的值;不存在则会追加键名和值。

let params = new URLSearchParams('?foo=1');
params.set('foo', 2);
params.toString() // "foo=2"
params.set('bar', 3);
params.toString() // "foo=2&bar=3"

如果存在多个同名键,则都会被移除,只剩下设置的最新的键值。

URLSearchParams.delete(key) 删除指定的查询参数

URLSearchParams.has(key) 判断查询字符串是否包含指定的键,返回bool值

URLSearchParams.sort() 对查询字符串里的键进行排序

按照 Unicode 码点从小到大排列。无返回值。

let params = new URLSearchParams('c=4&a=2&b=3&a=1');
params.sort();
params.toString() // "a=2&a=1&b=3&c=4"

两个同名的键a保持原始的顺序。

URLSearchParams.keys(),URLSearchParams.values(),URLSearchParams.entries()返回遍历器对象

这三个方法都可以用于 for...of 循环。keys方法返回键名的遍历器,values方法返回键值的遍历器,entries返回键值对的遍历器。

let params = new URLSearchParams('a=1&b=2');

for(let p of params.keys()) {
  console.log(p);
}
// a
// b

for(let p of params.values()) {
  console.log(p);
}
// 1
// 2

for(let p of params.entries()) {  // 等同于 for (let p of params)
  console.log(p);
}
// ["a", "1"]
// ["b", "2"]

URL() 构造函数检测url是否正确

URL() 构造函数调用时,如果参数不是合法的url,将会抛出 TypeError 错误!

如下,使用非法的 http ://www.example.com ,构造初始化URL时会报错:

try {
  const url = new URL('http ://www.example.com');
} catch (error) {
  error; // => TypeError, "Failed to construct URL: Invalid URL"
}

解析查询字符串的实现

特殊的查询字符串(如a==b&b=&c=0&d=kp=13a&=m&e=

在总结解析查询字符串的实现之前,让我们先看一下,对于特殊的Query String,浏览器原生提供的方法,是怎么处理的,这个主要是为后面的正则的写法提供依据。

比如,一个这种类型的查询字符串 ?a==b&b=&c=0&d=kp=13a&=m&e=

let a=new URLSearchParams('a==b&b=&c=0&d=kp=13a&=m&e=');

for (let p of a){
    console.log(p[0],':',p[1]);
}
// 输出的结果:
// a : =b
// b : 
// c : 0
// d : kp=13a
//  : m
// e : 

可以看到,原生方法,是完全按照 & 拆分,并以 第一个 = 作为 key/value 的分割的。

再比如,帶hash的特殊查询字符串 ?a==b&b=&c=0&d=kp=13a&=m&e=#hash

let a=new URLSearchParams('a==b&b=&c=0&d=kp=13a&=m&e=#hash');

for (let p of a){
    console.log(p[0],':',p[1]);
}
// 输出的结果:
// a : =b
// b : 
// c : 0
// d : kp=13a
//  : m
// e : #hash

注意最后的hash,也是作为查询字符串的参数值来获取的。如果想要严格来实现,可以自己再额外判断并去除hash,也可以修改正则去除【参见最后一部分】。

for...of 循环实现

通过构造传入的url或查询字符串,生成或获取 URLSearchParams 对象,然后,使用 for...of 循环即可获取到 key/value。当然,也可以使用 forfor in 循环。

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;
    try {
        let searchParams={};
        if(url.startsWith('http')){
            searchParams=new URL(url).searchParams;
        }
        else{
            searchParams=new URLSearchParams(url);
        }
        for(let p of searchParams){
            obj[p[0]]=p[1];
        }
        return obj;
    } catch (error) {
        return obj;//防止是非法的url
    }
}

测试: 通过如下代码,查看解析后的结果。

let queryStr1="a==b&b=&c=0&d=kp=13a&=m&e=";
let queryStr2="?a==b&b=&c=0&d=kp=13a&=m&e=";
let queryStr3="?a==b&b=&c=0&d=kp=13a&=m&e=#hash";
let url="http://www.example.com?a==b&b=&c=0&d=kp=13a&=m&e=";


console.log(parseQueryStr(queryStr1));
console.log(parseQueryStr(queryStr2));
console.log(parseQueryStr(queryStr3));
console.log(parseQueryStr(url));

4个输出的结果如下:

{
    "": "m"
    a: "=b"
    b: ""
    c: "0"
    d: "kp=13a"
    e: ""
}

{
    "": "m"
    a: "=b"
    b: ""
    c: "0"
    d: "kp=13a"
    e: ""
}

{
    "": "m"
    a: "=b"
    b: ""
    c: "0"
    d: "kp=13a"
    e: "#hash"
}

{
    "": "m"
    a: "=b"
    b: ""
    c: "0"
    d: "kp=13a"
    e: ""
}

还可以考虑,获取查询字符串中某个键的值的实现。

const getQueryStr=function(url,key){
    if(!url){
        url=window.location.href
    }
    let value='';
    if(typeof(url)!=='string') return value;
    try {
        let searchParams={};
        if(url.startsWith('http')){
            searchParams=new URL(url).searchParams;
        }
        else{
            searchParams=new URLSearchParams(url);
        }
        return searchParams.get(key);
    } catch (error) {
        return value;//防止是非法的url.或key为空
    }
}

测试查询的结果:

console.log(getQueryStr(queryStr1,'a'));    // => =b
console.log(getQueryStr(queryStr2,'d'));    // => kp=13a
console.log(getQueryStr(queryStr3,'c'));    // => 0
console.log(getQueryStr(queryStr3,'e'));    // => #hash
console.log(getQueryStr(url,'e'));          // => 

正则表达式实现

如果不考虑查询字符串中key为空的情况,则查找 Query string 中键值对的正则表达式为:/([^?=&]+)=([^&]*)/g

而,如果考虑查询字符串中key为空的情况,则对应的正则为:/([^?=&]*)=([^&]*)/g

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;
    const kvMatches = url.matchAll(/([^?=&]*)=([^&]*)/g);
    for (const match of kvMatches){
        obj[match[1]] = match[2];
    }
    return obj;
}

当然,还可以借助 regexp.exec(str) 正则循环执行,获取结果。

此处,关于获取 key=value 对,还可以借助 replace 方法曲线实现。比如,下面的代码

let obj={};
if(typeof(url)!=='string') return obj;
url.replace(/([^?=&]*)=([^&]*)/g, (m, $1, $2) => obj[$1] = $2);
return obj;

replace 中的第二个函数参数。还可以为如下方式:

url.replace(reg, (...arg) => {
        obj[arg[1]] = arg[2]
    })

甚至使用 不推荐的 arguments

url.replace(reg, function() {
        obj[arguments[1]] = arguments[2]
    })

获取查询字符串中某个键的值的实现,可以判断key匹配:

const getQueryStr=function(url,key){
    if(!url){
        url=window.location.href
    }
    let value='';
    if(typeof(url)!=='string') return value;
    const kvMatches = url.matchAll(/([^?=&]*)=([^&]*)/g);
    for (const match of kvMatches){
        if(match[1] === key){
            return match[2];
        }
    }
    return value;
}

也可以将 key,添加进正则中判断:

const getQueryStr = function (url, key) {
    if (!url) {
        url = window.location.href
    }
    if (typeof (url) !== 'string') return '';
    let reg = new RegExp('[?&]' + key + '=([^&]*)', 'i');
    let match = url.match(reg);
    if(match){
     return match[1];
    }
    else{
        match=url.match(new RegExp('^' + key + '=([^&]*)', 'i'))
    }
    return match ? match[1] : '';
}

测试: 执行上面的相同的测试部分代码,查看解析和查询的测试结果如下:

split分割和循环实现

使用 split 分割查询字符串中的 &,然后,再获取第一个 = 对应的前后字符内容(此处使用 = 将不严谨,在特殊查询字符串中产生错乱!)

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;
    if (url.indexOf('?') >= 0) url=url.substring(url.indexOf('?')+1);
    let arr=url.split('&');
    for(let i=0;i<arr.length;i++){
        if(arr[i].indexOf('=') >= 0){
            obj[arr[i].substring(0,arr[i].indexOf('='))]=arr[i].substring(arr[i].indexOf('=')+1);
        }
        else{
            obj[arr[i]]='';
        }
    }
    return obj;
}

String.prototype.substr() 不推荐使用,将来可能会被移除掉。所以,

在js中,通常推荐使用 substring(),即 String.prototype.substring(startIndex,endIndex);或者,推荐 slice()String.prototype.slice(startIndex,endIndex),它和 substring 的用法完全一样。并且都不改变原始字符串。

获取查询字符串中某个键的值的实现,可以通过判断key匹配:

const getQueryStr=function(url,key){
    if(!url){
        url=window.location.href
    }
    let value='';
    if(typeof(url)!=='string') return value;
    if (url.indexOf('?') >= 0) url=url.substring(url.indexOf('?')+1);
    let arr=url.split('&');
    for(let i=0;i<arr.length;i++){
        if(arr[i].indexOf('=') >= 0){
            if(arr[i].substring(0,arr[i].indexOf('='))===key){
                return arr[i].substring(arr[i].indexOf('=')+1);
            }
        }
        else{
            if(arr[i]===key){
                return value;
            }
        }
    }
    return value;
}

测试: 执行上面的相同的测试部分代码,查看解析和查询的测试结果如下:

for...of、正则表达式、split分割和循环 各自实现不包含 #hash 的修正

上面的示例,都是对照 URL.searchParams 的实现,但是,有一个确定,就是,解析的结果中,包含 #hash 锚点哈希部分。

因此下面分别对这三种方法的实现,进行一些修改,去除 #hash 部分。

for...of

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;    
    try {
        if(url.startsWith('http')){
            url=new URL(url).search;
        }
        else if (url.indexOf('#')>0){
            url=url.substring(0,url.indexOf('#'));
        }
        let searchParams=new URLSearchParams(url);
     
        for(let p of searchParams){
            obj[p[0]]=p[1];
        }
        return obj;
    } catch (error) {
        return obj;//防止是非法的url
    }
}

const getQueryStr=function(url,key){
    if(!url){
        url=window.location.href
    }
    let value='';
    if(typeof(url)!=='string') return value;
    try {
        if(url.startsWith('http')){
            url=new URL(url).search;
        }
        else if (url.indexOf('#')>0){
            url=url.substring(0,url.indexOf('#'));
        }
        let searchParams=new URLSearchParams(url);
        return searchParams.get(key);
    } catch (error) {
        return value;//防止是非法的url.或key为空
    }
}

正则表达式

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;
    const kvMatches = url.matchAll(/([^?=&]*)=([^&#]*)/g);
    for (const match of kvMatches){
        obj[match[1]] = match[2];
    }
    return obj;
}

const getQueryStr = function (url, key) {
    if (!url) {
        url = window.location.href
    }
    if (typeof (url) !== 'string') return '';
    let reg = new RegExp('[?&]' + key + '=([^&#]*)', 'i');
    let match = url.match(reg);
    if(match){
     return match[1];
    }
    else{
        match=url.match(new RegExp('^' + key + '=([^&#]*)', 'i'))
    }
    return match ? match[1] : '';
}

split分割和循环

/**
 * @description: 解析url中的查询字符串
 * @param {url} 传入的url字符串,可以是"?"开头(如window.location.search)或不带"?"的查询字符串部分,也可以是一个完整的url。如果传入的是空值或不传入参数,将会解析当前页面的url
 * @return {*} 解析后的key:value格式对象
 */
const parseQueryStr=function(url){
    if(!url){
        url=window.location.href
    }
    let obj={};
    if(typeof(url)!=='string') return obj;
    if (url.indexOf('#') > 0) url=url.substring(0,url.indexOf('#'));
    if (url.indexOf('?') >= 0) url=url.substring(url.indexOf('?')+1);
    let arr=url.split('&');
    for(let i=0;i<arr.length;i++){
        if(arr[i].indexOf('=') >= 0){
            obj[arr[i].substring(0,arr[i].indexOf('='))]=arr[i].substring(arr[i].indexOf('=')+1);
        }
        else{
            obj[arr[i]]='';
        }
    }
    return obj;
}

const getQueryStr=function(url,key){
    if(!url){
        url=window.location.href
    }
    let value='';
    if(typeof(url)!=='string') return value;
    if (url.indexOf('#') > 0) url=url.substring(0,url.indexOf('#'));
    if (url.indexOf('?') >= 0) url=url.substring(url.indexOf('?')+1);
    let arr=url.split('&');
    for(let i=0;i<arr.length;i++){
        if(arr[i].indexOf('=') >= 0){
            if(arr[i].substring(0,arr[i].indexOf('='))===key){
                return arr[i].substring(arr[i].indexOf('=')+1);
            }
        }
        else{
            if(arr[i]===key){
                return value;
            }
        }
    }
    return value;
}

修正后,测试的结果如下,已经没有和 hash 内容。更多示例可自行测试。

附:encodeURIComponent() 考虑中文等Unicode编码的查询字符串参数

考虑中文等Unicode编码的查询字符串参数的解析,可以使用 encodeURIComponent() 解码,以获取正确的参数和值。

参考