这是我参与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 组成 host,pathname?search 组成 path,search 部分通常被称为查询字符串(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。当然,也可以使用 for、for 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() 解码,以获取正确的参数和值。