【源码阅读】vercel/ms

237 阅读2分钟

前言

阅读源码的分支是 master

源码传送门:github.com/vercel/ms

ms 介绍

ms 是用于将各种时间格式转化为毫秒

ms('2 days')  // 172800000
ms('1d')      // 86400000
ms('10h')     // 36000000

功能

通过单元测试和 readme.md 可以大概了解这个包的功能 image.png

// src/index.test.ts
import ms from './index';

describe('ms(string)', () => {
  it('should not throw an error', () => {
    expect(() => {
      ms('1m');
    }).not.toThrowError();
  });
  
  // ...
});

// long strings
// numbers

可以得出 ms 方法以下信息

  • 接收 stringnumber 类型的输入
  • long 为默认值 false 的情况下输入带时间单位的字符串会转换为毫秒数值
  • long 为默认值 false 的情况下输入数字会转换为对应的带缩写单位的字符串
  • longtrue 的情况下输入数字会转换为对应的正常单位的字符串

源代码

package.json 中找到入口文件是 index.ts,在主入口文件中对主函数(msFn)进行调用,使用 auto-attach 进行调试。

// src/index.ts

function msFn(value: StringValue, options?: Options): number;
function msFn(value: number, options?: Options): string;
function msFn(value: StringValue | number, options?: Options): number | string {
  try {
    if (typeof value === 'string' && value.length > 0) {
      return parse(value);
    } else if (typeof value === 'number' && isFinite(value)) {
      return options?.long ? fmtLong(value) : fmtShort(value);
    }
    throw new Error('Value is not a string or number.');
  } catch (error) {
    const message = isError(error)
      ? `${error.message}. value=${JSON.stringify(value)}`
      : 'An unknown error has occurred.';
    throw new Error(message);
  }
}

export default msFn;

使用 typescript 的函数重载,第一个函数声明接受两个参数:必填参数 value 和可选参数 optionsvalue 的类型是 StringValue,是 ms 自定义的类型。

// ...
const y = d * 365.25;

type Unit =
  | 'Years'
  | 'Year'
  | 'Yrs'
  | 'Yr'
  | 'Y'
// ... 

type UnitAnyCase = Unit | Uppercase<Unit> | Lowercase<Unit>;

export type StringValue =
  | `${number}`
  | `${number}${UnitAnyCase}`
  | `${number} ${UnitAnyCase}`;
  • 从以上联合类型可看出 value 接收的值是 '1'\'1Years'\'1 Years' 等等
  • 此处使用 儒略历;平年365日,4年1闰,历年的平均长度为365.25日

string 类型参数

匹配第一个判断,进入到 parse 函数:

const match =
    /^(?<value>-?(?:\d+)?\.?\d+) *(?<type>milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(
    str,
);

const groups = match?.groups as { value: string; type?: string } | undefined;
if (!groups) {
    return NaN;
}
const n = parseFloat(groups.value);
const type = (groups.type || 'ms').toLowerCase() as Lowercase<Unit>;

switch (type) {
    case 'years':
    case 'year':
    case 'yrs':
    case 'yr':
    case 'y':
      return n * y;
    // ...
  • 使用正则两个 命名捕获组,分别是 (?<value>x)(?<type>x),用来匹配参数的 数值 和 单位
  • (?:\d+):是非捕获组,不能用来提取文本。正则引擎不需要记录捕获型括号匹配的内容,速度会更快,所用的内存也更少
  • match?.groups as { value: string; type?: string } | undefinedtypescript 中命名捕获组需要手动指定

number 类型参数

匹配第二个判断

typeof value === 'number' && isFinite(value) 实现了 Number.isFinitePolyfill。既避免单独使用全局的 isFinite 会强制将一个非数值的参数转换成数值,同时又增强了兼容性。

function plural(
  ms: number,
  msAbs: number,
  n: number,
  name: string,
): StringValue {
  const isPlural = msAbs >= n * 1.5;
  return `${Math.round(ms / n)} ${name}${isPlural ? 's' : ''}` as StringValue;
}

function fmtLong(ms: number): StringValue {
  const msAbs = Math.abs(ms);
  if (msAbs >= d) {
    return plural(ms, msAbs, d, 'day');
  }
  if (msAbs >= h) {
    return plural(ms, msAbs, h, 'hour');
  }
  if (msAbs >= m) {
    return plural(ms, msAbs, m, 'minute');
  }
  if (msAbs >= s) {
    return plural(ms, msAbs, s, 'second');
  }
  return `${ms} ms`;
}
  • 取参数的绝对值,若参数大于等于本身的 1.5 倍时,时间格式单位后缀加上 s

错误类型守卫

function isError(value: unknown): value is Error {
  return typeof value === 'object' && value !== null && 'message' in value;
}

官方文档

请各位不吝赐教(逃