Skip to content

mrdulin/dm-utils

Repository files navigation

@d-matrix/utils

NPM Downloads npm bundle size

A dozen of utils for Front-End Development

API

clipboard

  • writeImage(element: HTMLImageElement | null | string): Promise<void>

复制图片到剪贴板

  • writeText(text: string): Promise<void>

复制文本到剪切板

react

  • render<P>(element: ReactElement<P>): Promise<string>

渲染React组件,返回HTML字符串。

  • cleanup(): void

清理函数,需要在调用render()函数后调用。

  • useDisableContextMenu(target: ContextMenuTarget = defaultContextMenuTarget): void

target函数返回的元素上禁用右键菜单,默认的target() => document

例1:在idtest的元素上禁用右键菜单

import { react } from '@d-matrix/utils';

const TestComp = () => {
  react.useDisableContextMenu(() => document.getElementById('test'));

  return (
    <div>
      <div id='test'>此元素的右键菜单被禁用</div>
    </div>
  )
}

例2:在document上禁用右键菜单

const TestComp = () => {
  react.useDisableContextMenu();

  return <div>内容</div>
}
  • useStateCallback<T>(initialState: T): [T, (state: T, cb?: (state: T) => void) => void]

返回值setState()函数类似类组件中的setState(updater[, callback]),可以在callback中获取更新后的state

  • useIsMounted(): () => boolean

获取当前组件是否已挂载的 Hook

const Test = () => {
  const isMounted = useIsMounted();

  useEffect(() => {
      if (isMounted()) {
       console.log('component mounted')
      }
  }, [isMounted]);

  return null
};
  • useCopyToClipboard(props?: UseCopyToClipboardProps)

复制文本到剪切板, 用法见测试

  • EnhancedComponent.prototype.setStateAsync(state)

setState()方法的同步版本

import { react } from '@d-matrix/utils';

 class TestComponent extends EnhancedComponent<unknown, { pageIndex: number }> {
  state = {
    pageIndex: 1,
  };

  async onClick() {
    await this.setStateAsync({ pageIndex: 2 });
    console.log(this.state.pageIndex); // 2
  }

  render() {
    return (
      <button data-cy="test-button" onClick={() => this.onClick()}>
        click
      </button>
    );
  }
}
  • useDeepCompareRef(deps: DependencyList): React.MutableRefObject<number>

深比较deps。返回refref.current是一个自增数字,每次deps变化,ref.current1。用法见测试

  • InferRef<T>

推导子组件的ref类型,适用于组件没有导出其ref类型的场景, 更多用法见测试

interface ChildRefProps {
  prop1: () => void;
  prop2: () => void;
}

interface ChildProps {
  otherProp: string;
}

const Child = React.forwardRef<ChildRefProps, ChildProps>((props, ref) => {
  React.useImperativeHandle(
    ref,
    () => ({
      prop1() {},
      prop2() {},
    }),
    [],
  );

  return null;
});

type InferredChildRef = InferRef<typeof Child>;  // 等价于ChildRefProps

const Parent = () => {
  const childRef = React.useRef<InferredChildRef>(null);

  return <Child ref={childRef} otherProp="a" />;
};
  • useForwardRef = <T>(ref: ForwardedRef<T>, initialValue: any = null): React.MutableRefObject<T>

解决使用React.forwardRef后,在调用ref.current.someMethod()时, 出现Property 'current' does not exist on type '(instance: HTMLInputElement | null) => void' TS类型错误,具体问题见这里

const Input = React.forwardRef<HTMLInputElement, React.ComponentPropsWithRef<'input'>>((props, ref) => {
  const forwardRef = useForwardRef<HTMLInputElement>(ref);
  useEffect(() => {
    forwardRef.current.focus();
  });
  return <input type="text" ref={forwardRef} value={props.value} />;
});
  • useMediaQuery(query, options?): boolean

使用Match Media API 检测当前document是否匹配media query

import { useMediaQuery } from '@d-matrix/utils/react'

export default function Component() {
  const matches = useMediaQuery('(min-width: 768px)')

  return (
    <div>
      {`The view port is ${matches ? 'at least' : 'less than'} 768 pixels wide`}
    </div>
  )
}

dom

  • scrollToTop(element: Element | null | undefined): void

元素滚动条滚动到顶部,对老旧浏览器做了兼容,见浏览器兼容性

  • strip(html: string): string

从字符串中去除 HTML 标签并返回纯文本内容。

import { dom } from '@d-matrix/utils';

dom.strip('测试<em>高亮</em>测试'); // '测试高亮测试'

date

  • rangeOfYears(start: number, end: number = new Date().getFullYear()): number[]

创建startend之间的年份数组。

  • getYears()
export interface YearOption {
  label: string;
  value: number;
}

export enum YearOptionKind {
  Numbers,
  Objects,
}

export type GetYearsOptions = {
  // 开始年份
  startYear?: number;
  // 最近几年
  recentYears?: number;
  // 截止年份
  endYear?: number;
  // 后缀,默认为'年'
  suffix?: string;
};

export function getYears(options: GetYearsOptions & { type: YearOptionKind.Numbers }): number[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind.Objects }): YearOption[];
export function getYears(options: GetYearsOptions & { type: YearOptionKind }): number[] | YearOption[]

获取n年,typeYearOptionKind.Numbers,返回[2023, 2022, 2021]数字数组;typeYearOptionKind.Objects,返回如下的对象数组

[
  { value: 2023, label: '2023年' },
  { value: 2022, label: '2022年' },
  { value: 2021, label: '2021年' },
]

更多用法,见测试用例

  • dayOfWeek(num: number, lang: keyof typeof i18n = 'zh'): string

返回星期几, lang仅支持zhen, num必须为正整数,否则报错

dayOfWeek(0) // "日"

types

  • WithOptional<T, K extends keyof T>
type A = { a: number; b: number; c: number; };
type T0 = WithOptional<A, 'b' | 'c'>;  // { a: number; b?: number; c?: number }
  • FunctionPropertyNames<T>

获取对象中的方法名称,返回union type

class A {
  add() {}
  minus() {}
  div() {}
  public result: number = 0;
}
type T0 = FunctionPropertyNames<A>; // 'add' | 'minus' | 'div'

const t1 = {
  add() {},
  minus() {},
  div() {},
  result: 0,
};
type T1 =  FunctionPropertyNames<typeof t1>; // 'add' | 'minus' | 'div'
  • NonFunctionPropertyNames<T>

获取对象中非函数属性名称,返回union type

class A {
  add() {}
  minus() {}
  div() {}
  public result: number = 0;
}
type T0 = FunctionPropertyNames<A>; // 'result'

const t1 = {
  add() {},
  minus() {},
  div() {},
  result: 0,
};
type T1 =  FunctionPropertyNames<typeof t1>; // 'result'
  • ValueOf<T>

获取对象中key的值,返回由这些值组成的union type

const map = {
  0: '0m',
  1: '1m',
  2: '2m',
  3: '3m',
  4: '4m',
  5: '5m',
  6: '6m',
} as const;

type T0 = ValueOf<typeof map>; // '0m' | '1m' | '2m' | '3m' | '4m' | '5m' | '6m'

algorithm

  • moveMulti<T extends unknown>(arr: T[], indexes: number[], start: number): T[]

移动多个元素到数组中指定的位置,用法,见测试用例

file

  • toImage(file: BlobPart | FileURL, options?: BlobPropertyBag): Promise<HTMLImageElement>

转换BlobPart或者文件地址为图片对象

  • validateImageSize(file: BlobPart | FileURL, limitSize: { width: number; height: number }, options?: BlobPropertyBag): Promise<ImageSizeValidationResult>

返回值:

interface ImageSizeValidationResult {
  isOk: boolean;
  width: number;
  height: number;
}

图片宽,高校验

  • isImageExists(src: string, img: HTMLImageElement = new Image()): Promise<boolean>

检测图片地址是否可用

import { file } from '@d-matrix/utils';

const url = 'https://picsum.photos/200/300';
const res = await file.isImageExists(url);

传入HTML中已经存在的img元素

import { file } from '@d-matrix/utils';

const $img = document.getElementById('img');
const res = await file.isImageExists(url, $img);
  • getFilenameFromContentDispositionHeader(header: { ['content-disposition']: string }): string

Content-Disposition response header中获取filename

import { file } from '@d-matrix/utils';

const header = {
  'content-disposition': 'attachment;filename=%E5%A4%A7%E8%A1%8C%E6%8C%87%E5%AF%BC2024-06-27-2024-06-28.xlsx'
};
const filename = file.getFilenameFromContentDispositionHeader(header);
// '大行指导2024-06-27-2024-06-28.xlsx'
  • download(source: string | Blob, fileName = '', target?: HyperLinkTarget): void

文件下载,source是文件地址或blob对象。

type HyperLinkTarget = "_self" | "_blank" | "_parent" | "_top"
  • downloadFileByIframe(source: string): boolean

通过创建iframe进行文件下载

support

  • isBrowserEnv(): boolean

是否是浏览器环境

  • isWebSocket(): boolean

是否支持WebSocket

  • isSharedWorker(): boolean

是否支持SharedWorker

timer

  • sleep(ms?: number): Promise<unknown>

使用setTimeoutPromise实现,暂停执行ms毫秒

await sleep(3000); // 暂停3秒
console.log('continue'); // 继续执行

operator

  • trueTypeOf = (obj: unknown): string

检查数据类型

trueTypeOf([]); // array
trueTypeOf({}); // object
trueTypeOf(''); // string
trueTypeOf(new Date()); // date
trueTypeOf(1); // number
trueTypeOf(function () {}); // function
trueTypeOf(/test/i); // regexp
trueTypeOf(true); // boolean
trueTypeOf(null); // null
trueTypeOf(undefined); // undefined

decimal

  • format(value: number | string | undefined | null, options?: FormatOptions): string

格式化数字,默认保留3位小数,可添加前缀,后缀,默认值为'--',用法见测试

type FormatOptions = {
  decimalPlaces?: number | false;
  suffix?: string;
  prefix?: string;
  defaultValue?: string;
  operation?: {
    operator: 'add' | 'sub' | 'mul' | 'div' | 'toDecimalPlaces';
    value: number;
  }[];
};

object

  • removeZeroValueKeys = <T extends Record<string, any>>(obj: T, zeroValues = ZeroValues): T

移除零值的键, 默认的零值是:undefinednull, '', NaN, [], {}

removeZeroValueKeys({ a: '', b: 'abc', c: undefined, d: null, e: NaN, f: -1, g: [], h: {} })
// { b: 'abc', f: -1 }

array

  • moveImmutable<T>(array: T[], fromIndex: number, toIndex: number): T[]
import { array } from '@d-matrix/utils';

const input = ['a', 'b', 'c'];

const array1 = array.moveImmutable(input, 1, 2);
console.log(array1);
//=> ['a', 'c', 'b']

const array2 = array.moveImmutable(input, -1, 0);
console.log(array2);
//=> ['c', 'a', 'b']

const array3 = array.moveImmutable(input, -2, -3);
console.log(array3);
//=> ['b', 'a', 'c']
  • moveMutable<T>(array: T[], fromIndex: number, toIndex: number): void

  • moveToStart<T>(array: T[], predicate: (item: T) => boolean): T[]

移动元素到数组首位,不会修改原数组

import { array } from '@d-matrix/utils';

const list = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }];
const newList = array.moveToStart(list, (item) => item.id === 4);

// [{ id: 4 }, { id: 1 }, { id: 2 }, { id: 3 }, { id: 5 }]

echarts

  • mergeOption(defaults: EChartsOption, overrides: EChartsOption, option?: deepmerge.Options): EChartsOption

deep merge Echarts配置,用法见测试用例

  • fill<T extends Record<string, any>, XAxisField extends keyof T, YAxisField extends keyof T>(dataSource: T[], xAxisField:XAxisField, yAxisField: YAxisField): T[]

场景:后端接口返回某几个时间点的数据,需求是在接口数据的基础上每隔5分钟补一个点,以达到图中的效果: 折线图

填充的点的Y轴值为前一个点的值, 时间示例: [9:23, 9:27] => [9:23, 9:25, 9:27, 9:30],更多,见测试用例

  • calcYAxisRange<T extends Record<string, any>, Key extends keyof T>(data: T[], key: Key, decimalPlaces = 2, splitNumber = 5): { max:number; min:number }

计算echarts YAxis的max和min属性,以达到根据实际数据动态调整,使折线图的波动明显。且第一个点始终在Y轴中间位置,效果图

测试

运行全部组件测试

npm run cy:run -- --component

运行单个组件测试

npm run cy:run -- --component --spec tests/date.cy.ts

运行E2E测试

src通过tsc build到public/dist目录

npm run build:public

启动一个Web服务器来访问public/index.html文件,dist目录的脚本可以通过<script type="module"/>引入

npm run serve

最后启动cypress GUI客户端,选择E2E测试

npm run cy:open

发布

更新package version:

npm version <minor> or <major>...

构建:

npm build

发布:

npm publish --access public

网络原因导致连接registry服务器超时,可指定proxy

npm --proxy http://127.0.0.1:7890 publish

镜像站查询版本与手动同步:

npm镜像站

通过git log命令获取changelogs,用于填写GitHub Release内容:

git log --oneline --decorate

注意事项