前言
只有遇到了bug,才能学到东西,只有记录下来,才不会忘记。
1 前因
在一个风和日丽的上午,从第三方接口放回的数据是一个标准的地址如: http://22.22.22.22:2
可是我的表单页面分别是需要,请求协议、主机名、端口号。 我第一反应是写正则表达式把不要的字符串replace
短暂的犹豫后,写正则我从来都是百度,这好累啊,维护也累,万一给我的变成https呢?
思虑一番想到了new URL这个方法。
const addressUrl = new URL('http://22.22.22.22:2')
console.log(addressUrl.hash) //获取#号后面的数据
console.log(addressUrl.host) //获取主机+端口号 22.22.22.22:2
console.log(addressUrl.hostName) //获取主机(域名) 22.22.22.22
console.log(addressUrl.port) //获取端口号 2
完美!!!!
2 自测
一开始选的是自己造的数据,完美,表单渲染正常,提交数据ok。
git commit:神创造了你
午饭午休过后,是一个炎热的酷暑,无聊的我在整理ts报错,删除调试的console.log,整理完之后又顺手随便选了个数据自测,提交表单。
Oh!!!! 能怎么办呢?把console.log又加上了,本以为是给的数据没有端口号,还想着要怎么处理这种情况,没想到有
看一眼大概猜出来是默认端口号可能有什么问题。
3 论证一番
3.1 80端口
3.2 非80端口
遇事不要慌打开MDN先
解释的非常透彻,但是我很蛋疼了呀。
4 加强一下
没有什么是一个中间件解决不了的,如果有那就再套一个
分析一下思路:
- 1:mdn没有找到支持哪些协议,那就搞知道的呗!http:80 https:443 ftp:21
- 2:目前想要解决的是prot问题
- 3:扩展性要留一点,万一增加协议之类的
export const getUrlAttr = (
url: URL,
attr: keyof URL,
option: {
isReturnDefaultPort?: boolean;
defaultPortExp?: Record<string, number>;
} = {},
) => {
// 定义一些默认端口
let defaultPort = {
'https:': '443',
'http:': '80',
'ws:': '80',
'wss:': '443',
'ftp:': '21',
};
// 设置初始值
const { isReturnDefaultPort = false, defaultPortExp } = option;
if (defaultPortExp) {
defaultPort = { ...defaultPort, ...defaultPortExp };
}
const portRelateAttrs = {
href: () => {
return url.origin + ':' + defaultPort[url.protocol as keyof typeof defaultPort] + '/';
},
origin: () => {
return url.origin + ':' + defaultPort[url.protocol as keyof typeof defaultPort];
},
port: () => {
return defaultPort?.[url?.protocol as keyof typeof defaultPort] ?? '';
},
};
// 如果
if (isReturnDefaultPort) {
if (Object.keys(portRelateAttrs).includes(attr)) {
// @ts-ignore
return portRelateAttrs[attr]();
}
} else {
if (attr === 'port') return portRelateAttrs.port();
}
return url[attr];
};
测试一下:
初衷是想返回默认的port,但是为了和port关联的如href和origin到时候也要有port的话还要再写麻烦,直接一步到位。留了一个拓展口出来补一些默认的端口号参数。
5 链式调用觉得更酷一点
那就弄一个全要port的吧
export const strengthenURL = (
str: string,
base?: string | URL,
option: { defaultPortExp?: Record<string, string> } = {},
) => {
// 定义一些默认端口
let defaultPort = {
'https:': '443',
'http:': '80',
'ws:': '80',
'wss:': '443',
'ftp:': '21',
};
// 设置初始值
const { defaultPortExp } = option;
if (defaultPortExp) {
defaultPort = { ...defaultPort, ...defaultPortExp };
}
const url = new URL(str, base);
return {
...url,
port: defaultPort[url?.protocol as keyof typeof defaultPort] ?? '',
href: url.origin + ':' + defaultPort[url.protocol as keyof typeof defaultPort] + '/',
origin: url.origin + ':' + defaultPort[url.protocol as keyof typeof defaultPort],
};
};
6 bug解决
经过疯狂的测试发现了以下几种情况
- getUrlAttr没有对protocol进行判空处理,如果是用都要defaultPort的话,就会拼接原本的端口+我们的默认端口
- strengthenUrl的...url没有拓展出数据
6.1
第一个bug很好解决我们价格判断就好
const portRelateAttrs = {
href: () => {
return `${url.origin}${
url.port !== '' ? '' : `:${defaultPort[url.protocol as keyof typeof defaultPort]}`
}/`;
},
origin: () => {
return `${url.origin}${
url.port !== '' ? '' : `:${defaultPort[url.protocol as keyof typeof defaultPort]}`
}`;
},
port: () => {
return url.port !== ''
? url.port
: defaultPort?.[url?.protocol as keyof typeof defaultPort] ?? '';
},
};
6.2
第二个问题就很有意思了 翻了老半天文档,敲了许多测试代码
6.2.1 new URL返回什么?
反正最终都是Object,但是拓展运算符...失效,
首先我们我翻的肯定是es6相关的文档 对象的扩展 - ECMAScript 6入门
那就来看一下URL对象的属性标志
不知道属性标志概念的可以看一下 属性标志和属性描述符
那就通过 Object.getOwnPropertyDescriptors()来看一下URL对象
居然是个空对象,和我们控制台所看到的有的差异
这不都是对象的属性吗????
我不死心还做过几个方法的尝试
虽然还没搞懂这是什么原因,但是根据打印结果enumerable是false,所以拓展运算符...才会失效。
难道就没有办法了吗?只能硬编码一个个写key:value?
虽然没几个key,但是我还是打开了URL对象的原型来查看
最终其实还是Object,而在原型的第二层上看到了和第一层级一样的,试一下反正也不吃亏
哦吼,这不就有操作空间了
其实更推荐使用Object.getPrototypeOf()
这不就可以修改了
const url = new URL(str, base);
const urlObj = {};
Object.keys(Object.getPrototypeOf(url)).forEach((key) => {
urlObj[key] = url[key];
});
之后我就在研究像URL对象第一层这种让Object方法失效的对象是如何实现的,有了个意外收获
for...in 循环 一个被eslint经常禁用的方法 ESLint: The body of a for-in should be wrapped in an if statement to filter unwanted properties from the prototype.(guard-for-in)
他是可以遍历到继承的,那最终的代码就改成了这样
export const strengthenURL = (
str: string,
base?: string | URL,
option: { defaultPortExp?: Record<string, string> } = {},
) => {
// 定义一些默认端口
let defaultPort = {
'https:': '443',
'http:': '80',
'ws:': '80',
'wss:': '443',
'ftp:': '21',
};
// 设置初始值
const { defaultPortExp } = option;
if (defaultPortExp) {
defaultPort = { ...defaultPort, ...defaultPortExp };
}
const url = new URL(str, base);
const urlObj = {};
for (const key in url) {
urlObj[key] = url[key];
}
return {
...urlObj,
port: url.port !== '' ? url.port : defaultPort[url?.protocol as keyof typeof defaultPort] ?? '',
href: `${url.origin}${
url.port !== '' ? '' : `:${defaultPort[url.protocol as keyof typeof defaultPort]}`
}/`,
origin: `${url.origin}${
url.port !== '' ? '' : `:${defaultPort[url.protocol as keyof typeof defaultPort]}`
}`,
};
};
7 总结
- URL对象不能直接拓展运算符遍历
- new URL的地址如果是默认端口不会port为空字符串
- 方法其实还有优化的空间,如return的判断其实如果!==空可以返回自身,不用拼接,还有对传入str的判断做一下try...catch之类的
- 可惜的是没搞懂URL对象是怎么弄出来的,我们自己能够通过代码创建出来不,等搞懂了再写一下记录下,有知道的大佬也可以直接告诉我,像这种内置对象可能不是javascript层面的也是可能的,等有时间再研究研究
文章整体其实比较杂,涉及了挺多点,整体是边查边写,所以前面一开始会有bug的代码,一开始文章只是想记录怎么解决默认端口号没有的情况,慢慢的编写中发现了各种问题,就顺便记录一下过程。