A dozen of utils for Front-End Development
writeImage(element: HTMLImageElement | null | string): Promise<void>
复制图片到剪贴板
writeText(text: string): Promise<void>
复制文本到剪切板
render<P>(element: ReactElement<P>): Promise<string>
渲染React
组件,返回HTML字符串。
cleanup(): void
清理函数,需要在调用render()
函数后调用。
useDisableContextMenu(target: ContextMenuTarget = defaultContextMenuTarget): void
在target
函数返回的元素上禁用右键菜单,默认的target
是() => document
例1:在id
是test
的元素上禁用右键菜单
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
。返回ref
,ref.current
是一个自增数字,每次deps
变化,ref.current
加1
。用法见测试
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>
)
}
scrollToTop(element: Element | null | undefined): void
元素滚动条滚动到顶部,对老旧浏览器做了兼容,见浏览器兼容性。
strip(html: string): string
从字符串中去除 HTML 标签并返回纯文本内容。
import { dom } from '@d-matrix/utils';
dom.strip('测试<em>高亮</em>测试'); // '测试高亮测试'
rangeOfYears(start: number, end: number = new Date().getFullYear()): number[]
创建start
和end
之间的年份数组。
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年,type
传YearOptionKind.Numbers
,返回[2023, 2022, 2021]
数字数组;type
传YearOptionKind.Objects
,返回如下的对象数组
[
{ value: 2023, label: '2023年' },
{ value: 2022, label: '2022年' },
{ value: 2021, label: '2021年' },
]
更多用法,见测试用例
dayOfWeek(num: number, lang: keyof typeof i18n = 'zh'): string
返回星期几, lang
仅支持zh
和en
, num
必须为正整数,否则报错
dayOfWeek(0) // "日"
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'
moveMulti<T extends unknown>(arr: T[], indexes: number[], start: number): T[]
移动多个元素到数组中指定的位置,用法,见测试用例
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
进行文件下载
isBrowserEnv(): boolean
是否是浏览器环境
isWebSocket(): boolean
是否支持WebSocket
isSharedWorker(): boolean
是否支持SharedWorker
sleep(ms?: number): Promise<unknown>
使用setTimeout
与Promise
实现,暂停执行ms
毫秒
await sleep(3000); // 暂停3秒
console.log('continue'); // 继续执行
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
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;
}[];
};
removeZeroValueKeys = <T extends Record<string, any>>(obj: T, zeroValues = ZeroValues): T
移除零值的键, 默认的零值是:undefined
、null
, ''
, NaN
, []
, {}
removeZeroValueKeys({ a: '', b: 'abc', c: undefined, d: null, e: NaN, f: -1, g: [], h: {} })
// { b: 'abc', f: -1 }
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 }]
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
镜像站查询版本与手动同步:
通过git log
命令获取changelogs,用于填写GitHub Release内容:
git log --oneline --decorate