new URL的一个小坑引发的许多坑

1,527 阅读5分钟

前言

只有遇到了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,整理完之后又顺手随便选了个数据自测,提交表单。

image.png

Oh!!!! 能怎么办呢?把console.log又加上了,本以为是给的数据没有端口号,还想着要怎么处理这种情况,没想到有

image.png

看一眼大概猜出来是默认端口号可能有什么问题。

3 论证一番

3.1 80端口

image.png

3.2 非80端口

image.png

遇事不要慌打开MDN先

image.png 解释的非常透彻,但是我很蛋疼了呀。

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];
};

测试一下:

image.png

初衷是想返回默认的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返回什么?

1729740562543.jpg

反正最终都是Object,但是拓展运算符...失效,

首先我们我翻的肯定是es6相关的文档 对象的扩展 - ECMAScript 6入门

image.png

image.png

那就来看一下URL对象的属性标志

不知道属性标志概念的可以看一下 属性标志和属性描述符

那就通过 Object.getOwnPropertyDescriptors()来看一下URL对象

image.png

image.png

居然是个空对象,和我们控制台所看到的有的差异

image.png

这不都是对象的属性吗????

我不死心还做过几个方法的尝试

image.png

虽然还没搞懂这是什么原因,但是根据打印结果enumerable是false,所以拓展运算符...才会失效。

难道就没有办法了吗?只能硬编码一个个写key:value?

虽然没几个key,但是我还是打开了URL对象的原型来查看

image.png

最终其实还是Object,而在原型的第二层上看到了和第一层级一样的,试一下反正也不吃亏

image.png

哦吼,这不就有操作空间了

image.png 其实更推荐使用Object.getPrototypeOf()

image.png 这不就可以修改了

const url = new URL(str, base);
const urlObj = {};
Object.keys(Object.getPrototypeOf(url)).forEach((key) => {
  urlObj[key] = url[key];
});

之后我就在研究像URL对象第一层这种让Object方法失效的对象是如何实现的,有了个意外收获

image.png

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的代码,一开始文章只是想记录怎么解决默认端口号没有的情况,慢慢的编写中发现了各种问题,就顺便记录一下过程。