哈喽,我是柠檬酱👏
工欲善其事必先利其器,咱们来扒一扒开源库 qiankun/micro app/icestark/wujie/ahooks/hooks等 用到的一些工具函数
数据类型判断
基本数据类型除了null都可以通过typeof判断
Object.toString.call可以判断所有的数据类型
判断所有的数据类型
typescript
type AllTypes =
| 'Number'
| 'Object'
| 'BigInt'
| 'Boolean'
| 'Undefined'
| 'Null'
| 'Array'
| 'Symbol'
| 'Math'
| 'JSON'
| 'Date'
| 'RegExp'
| 'Error'
| 'Window'
| 'HTMLDocument';
const checkTypes = (type: AllTypes) => <T>(value: unknown): value is T => Object.prototype.toString.call(value).slice(8, -1) === type;
是否为字符串
csharp
function isString (target: unknown): target is string {
return typeof target === 'string'
}
是否为null
csharp
function isNull (target: unknown): target is null {
return target === null
}
是否为undefined
javascript
function isUndefined (target: unknown): target is undefined {
return target === undefined
}
是否为布尔值
vbnet
function isBoolean (target: unknown): target is boolean {
return typeof target === 'boolean'
}
是否为数字类型
csharp
function isNumber (target: unknown): target is Number {
return typeof target === 'number'
}
是否为函数
vbnet
function isFunction (target: unknown): target is Function {
return typeof target === 'function'
}
是否为对象
vbnet
function isObject (target: unknown): target is Object {
return typeof target === 'object'
}
是否为promise
typescript
const toString = Object.prototype.toString
function isPromise (target: unknown): target is Promise<unknown> {
return toString.call(target) === '[object Promise]'
}
是否为plainObject
plainObject和普通对象有什么区别?
通过对象字面量或new Object创建的对象叫plainObject
typeof null === object但是它不是plainObject
typescript
// micro app中isPlainObject判断
const toString = Object.prototype.toString
function isPlainObject <T = Record<PropertyKey, unknown>> (target: unknown): target is T {
return toString.call(target) === '[object Object]'
}
// react中plainObject判断
function isPlainObject(target){
if(target ===null || typeof target !== object){
return false
}
}
是否为构造函数
拆解下面的正则表达式:
matlab
/^function\s+[A-Z]/.test(targetStr)
- \s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符,记忆:s代表space,空格
- +为量词表示{1,},至少出现1次
- [A-Z]表示大写字母A到Z
arduino
/^class\s+/.test(targetStr)
这段正则表明以class开头命名,至少有一个空格的字符串
typescript
function isConstructor (target: unknown): boolean {
// 构造函数的前提:它本身是一个函数
if (isFunction(target)) {
// 函数转字符串
const targetStr = target.toString()
return (
target.prototype?.constructor === target &&
Object.getOwnPropertyNames(target.prototype).length > 1
) ||
/^function\s+[A-Z]/.test(targetStr) ||
/^class\s+/.test(targetStr)
}
// 不是函数返回false
return false
}
浏览器检测
检测浏览器类型通过navigator.userAgent判断即可
是否为谷歌浏览器
javascript
function isChomre():boolean{
return /Chrome/.test(navigator.userAgent)
}
是否为safari浏览器
javascript
function isSafari (): boolean {
return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent)
}
是否为火狐浏览器
arduino
function isFireFox (): boolean {
return navigator.userAgent.indexOf('Firefox') > -1
}
文件类型判断
是否为内联js
php
function isInlineScript (address: string): boolean {
return address.startsWith('inline-')
}
shadowDOM判断
csharp
function isShadowRoot (target: unknown): target is ShadowRoot {
return typeof ShadowRoot !== 'undefined' && target instanceof ShadowRoot
}
<link>/<style>/<script>/<iframe>/<div>/<img>标签都是通过元素的tagName判断
link标签
csharp
function isLinkElement (target: unknown): target is HTMLLinkElement {
return (target as HTMLLinkElement)?.tagName?.toUpperCase() === 'LINK'
}
style标签
csharp
function isStyleElement (target: unknown): target is HTMLStyleElement {
return (target as HTMLStyleElement)?.tagName?.toUpperCase() === 'STYLE'
}
script标签
csharp
function isScriptElement (target: unknown): target is HTMLScriptElement {
return (target as HTMLScriptElement)?.tagName?.toUpperCase() === 'SCRIPT'
}
iframe标签
csharp
function isIFrameElement (target: unknown): target is HTMLIFrameElement {
return (target as HTMLIFrameElement)?.tagName?.toUpperCase() === 'IFRAME'
}
是否为div元素
csharp
function isDivElement (target: unknown): target is HTMLDivElement {
return (target as HTMLDivElement)?.tagName?.toUpperCase() === 'DIV'
}
是否为img元素
csharp
function isImageElement (target: unknown): target is HTMLImageElement {
return (target as HTMLImageElement)?.tagName?.toUpperCase() === 'IMG'
}
是否为URL
php
function isURL (target: unknown): target is URL {
return target instanceof URL || !!(target as URL)?.href
}
是否支持scope css
javascript
// qiankun写法
export type FrameworkConfiguration = QiankunSpecialOpts & ImportEntryOpts & StartOpts;
function isEnableScopedCSS(sandbox: FrameworkConfiguration['sandbox']) {
if (typeof sandbox !== 'object') {
return false;
}
if (sandbox.strictStyleIsolation) {
return false;
}
return !!sandbox.experimentalStyleIsolation;
}
URL处理
给url增加协议名
globalThis是通用的写法(兼容浏览器和Node),浏览器环境使用window.location.protocol即可
typescript
function addProtocol (url: string): string {
return url.startsWith('//') ? `${globalThis.location.protocol}${url}` : url
}
查询对象转字符串
用途:http请求处理
csharp
function stringifyQuery (queryObject: LocationQueryObject): string {
let result = ''
for (const key in queryObject) {
const value = queryObject[key]
if (isNull(value)) {
result += (result.length ? '&' : '') + key
} else {
const valueList: LocationQueryValue[] = isArray(value) ? value : [value]
valueList.forEach(value => {
if (!isUndefined(value)) {
result += (result.length ? '&' : '') + key
if (!isNull(value)) result += '=' + value
}
})
}
}
return result
}
去空格
typescript
function trim (str: string): string {
return str ? str.replace(/^\s+|\s+$/g, '') : ''
}
nextTick
javascript
function nextTick(cb: () => any): void {
Promise.resolve().then(cb);
}
sleep
面试官:实现一个sleep函数,要求等待指定的时机再执行
javascript
// ms为毫秒数
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
执行钩子函数
plugins为多个钩子函数(数组),遍历plugins,保留plugin[hookName]存在的且为函数类型的,将传参arg传入单个plugin依次执行
scss
function execHooks(plugins: Array<plugin>, hookName: string, ...args: Array<any>): void {
try {
if (plugins && plugins.length > 0) {
plugins
.map((plugin) => plugin[hookName])
.filter((hook) => isFunction(hook))
.forEach((hook) => hook(...args));
}
} catch (e) {
error(e);
}
}
requestIdleCallback
兼容处理
javascript
// wujie写法
const requestIdleCallback = window.requestIdleCallback || ((cb: Function) => setTimeout(cb, 1));
函数柯里化
[f1, f2, f3, f4] => f4(f3(f2(f1)))
typescript
function compose(fnList: Array<Function>): (...args: Array<string>) => string {
return function (code: string, ...args: Array<any>) {
return fnList.reduce((newCode, fn) => (isFunction(fn) ? fn(newCode, ...args) : newCode), code || "");
};
}
事件触发器
csharp
function eventTrigger(el: HTMLElement | Window | Document, eventName: string, detail?: any) {
let event;
if (typeof window.CustomEvent === "function") {
event = new CustomEvent(eventName, { detail });
} else {
event = document.createEvent("CustomEvent");
event.initCustomEvent(eventName, true, false, detail);
}
el.dispatchEvent(event);
}
元素查找
从数组list中查找element是否存在
javascript
function find(list, element) {
if (!Array.isArray(list)) {
return false;
}
return list.filter((item) => item === element).length > 0;
}
获取元素
javascript
function getContainer(container: string | HTMLElement): HTMLElement | null {
return typeof container === 'string' ? document.querySelector(container) : container;
}
页面元素如下图
ini
const asyncForEach = async(arr, callback)=> {
for (let idx = 0; idx < arr.length; ++idx) {
await callback(arr[idx], idx);
}
};
日志处理
告警日志处理
typescript
function warn(msg: string, data?: any): void {
console?.warn(`[wujie warn]: ${msg}`, data);
}
错误日志处理
typescript
function error(msg: string, data?: any): void {
console?.error(`[wujie error]: ${msg}`, data);
}
全局错误处理
分别绑定错误处理事件监听、移除错误处理事件监听
javascript
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull): void {
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
}
export function removeGlobalUncaughtErrorHandler(errorHandler: (...args: any[]) => any) {
window.removeEventListener('error', errorHandler);
window.removeEventListener('unhandledrejection', errorHandler);
}
react专用
HOC
高阶组件是一个函数,传入的参数是组件,返回值也是组件
React.cloneElement是一个组件增强方法,支持拓展props和state
javascript
function renderComponent(Component: any, props = {}): React.ReactElement {
return React.isValidElement(Component) ? (
React.cloneElement(Component, props)
) : (
<Component {...props} />
);
}
hooks
下面代码片段来自阿里的ahooks库,具体使用参考 ahooks.js.org/zh-CN/hooks...
useLatest
返回当前最新值的 Hook,可以避免闭包问题
useLatest返回的永远是最新值
csharp
import { useRef } from 'react';
function useLatest<T>(value: T) {
const ref = useRef(value);
ref.current = value;
return ref;
}
useMount
在组件挂载阶段执行的函数
javascript
import { useEffect } from 'react';
const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
const useMount = (fn: () => void) => {
if (isDev) {
if (typeof fn !== 'function') {
console.error(
`useMount: parameter \`fn\` expected to be a function, but got "${typeof fn}".`,
);
}
}
useEffect(() => {
fn?.();
}, []);
};
useUnmount
在组件卸载时执行的函数
javascript
import { useEffect } from 'react';
const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
const useUnmount = (fn: () => void) => {
if (isDev) {
if (typeof fn !== 'function') {
console.error(`useUnmount expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
useEffect(
() => () => {
fnRef.current();
},
[],
);
};
useDebounce
防抖
typescript
import {useState,useEffect,useMemo} from "react";
import { debounce } from 'lodash';
const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
export interface DebounceOptions {
wait?: number;
leading?: boolean;
trailing?: boolean;
maxWait?: number;
}
function useDebounceFn<T extends noop>(fn: T, options?: DebounceOptions) {
if (isDev) {
if (typeof fn !== 'function') {
console.error(`useDebounceFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const debounced = useMemo(
() =>
debounce(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
debounced.cancel();
});
return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}
export function useDebounce<T>(value: T, options?: DebounceOptions) {
const [debounced, setDebounced] = useState(value);
const { run } = useDebounceFn(() => {
setDebounced(value);
}, options);
useEffect(() => {
run();
}, [value]);
return debounced;
}
useThrottle
节流
scss
import { useEffect, useState, useMemo } from 'react';
interface ThrottleOptions {
wait?: number;
leading?: boolean;
trailing?: boolean;
}
function useThrottleFn<T extends noop>(fn: T, options?: ThrottleOptions) {
if (isDev) {
if (typeof fn !== 'function') {
console.error(`useThrottleFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useLatest(fn);
const wait = options?.wait ?? 1000;
const throttled = useMemo(
() =>
throttle(
(...args: Parameters<T>): ReturnType<T> => {
return fnRef.current(...args);
},
wait,
options,
),
[],
);
useUnmount(() => {
throttled.cancel();
});
return {
run: throttled,
cancel: throttled.cancel,
flush: throttled.flush,
};
}
function useThrottle<T>(value: T, options?: ThrottleOptions) {
const [throttled, setThrottled] = useState(value);
const { run } = useThrottleFn(() => {
setThrottled(value);
}, options);
useEffect(() => {
run();
}, [value]);
return throttled;
}
useToggle
toggle在两个状态之间切换
typescript
import { useState, useMemo } from 'react';
function useToggle<D, R>(defaultValue: D = false as unknown as D, reverseValue?: R) {
// 默认值,初始为false
const [state, setState] = useState<D | R>(defaultValue);
const actions = useMemo(() => {
// 默认值取反值
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R;
// 切换函数
const toggle = () => setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue));
// 设置值函数
const set = (value: D | R) => setState(value);
// 取默认值
const setLeft = () => setState(defaultValue);
// 取反值
const setRight = () => setState(reverseValueOrigin);
return {
toggle,
set,
setLeft,
setRight,
};
}, []);
return [state, actions];
}
useCookieState
将state存储在cookie中,作用就是刷新页面数据不丢失
ini
import Cookies from 'js-cookie';
import { useState, useRef, useMemo } from 'react';
const isDev = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test';
function useMemoizedFn<T extends noop>(fn: T) {
if (isDev) {
if (typeof fn !== 'function') {
console.error(`useMemoizedFn expected parameter is a function, got ${typeof fn}`);
}
}
const fnRef = useRef<T>(fn);
fnRef.current = useMemo(() => fn, [fn]);
const memoizedFn = useRef<PickFunction<T>>();
if (!memoizedFn.current) {
memoizedFn.current = function (this, ...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current as T;
}
function useCookieState(cookieKey: string, options: Options = {}) {
const [state, setState] = useState<State>(() => {
// cookie根据key拿到值
const cookieValue = Cookies.get(cookieKey);
if (typeof cookieValue === 'string') return cookieValue;
if (typeof options.defaultValue === 'function') {
return options.defaultValue();
}
return options.defaultValue;
});
const updateState = useMemoizedFn(
(
newValue: State | ((prevState: State) => State),
newOptions: Cookies.CookieAttributes = {},
) => {
const { defaultValue, ...restOptions } = { ...options, ...newOptions };
const value = typeof newValue === 'function' ? newValue(state) : newValue;
setState(value);
if (value === undefined) {
Cookies.remove(cookieKey);
} else {
Cookies.set(cookieKey, value, restOptions);
}
},
);
return [state, updateState] as const;
}
useLocalStorageState
localStorage的hooks版本
ini
const isFunction = (value: unknown): value is (...args: any) => any =>
typeof value === 'function';
const isUndef = (value: unknown): value is undefined => typeof value === 'undefined';
const isBrowser = !!(
typeof window !== 'undefined' &&
window.document &&
window.document.createElement
);
const useLocalStorageState = createUseStorageState(() => (isBrowser ? localStorage : undefined));
function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
const {
onError = (e) => {
console.error(e);
},
} = options;
try {
storage = getStorage();
} catch (err) {
onError(err);
}
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value);
}
return JSON.stringify(value);
};
const deserializer = (value: string): T => {
if (options.deserializer) {
return options.deserializer(value);
}
return JSON.parse(value);
};
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
}
const [state, setState] = useState(getStoredValue);
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
const updateState = (value?: SetState<T>) => {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);
if (isUndef(currentState)) {
storage?.removeItem(key);
} else {
try {
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
}
};
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
useSessionStorageState
sessionStoage的hooks版本
scss
function createUseStorageState(getStorage: () => Storage | undefined) {
function useStorageState<T>(key: string, options: Options<T> = {}) {
let storage: Storage | undefined;
const {
onError = (e) => {
console.error(e);
},
} = options;
try {
storage = getStorage();
} catch (err) {
onError(err);
}
const serializer = (value: T) => {
if (options.serializer) {
return options.serializer(value);
}
return JSON.stringify(value);
};
const deserializer = (value: string): T => {
if (options.deserializer) {
return options.deserializer(value);
}
return JSON.parse(value);
};
function getStoredValue() {
try {
const raw = storage?.getItem(key);
if (raw) {
return deserializer(raw);
}
} catch (e) {
onError(e);
}
if (isFunction(options.defaultValue)) {
return options.defaultValue();
}
return options.defaultValue;
}
const [state, setState] = useState(getStoredValue);
useUpdateEffect(() => {
setState(getStoredValue());
}, [key]);
const updateState = (value?: SetState<T>) => {
const currentState = isFunction(value) ? value(state) : value;
setState(currentState);
if (isUndef(currentState)) {
storage?.removeItem(key);
} else {
try {
storage?.setItem(key, serializer(currentState));
} catch (e) {
console.error(e);
}
}
};
return [state, useMemoizedFn(updateState)] as const;
}
return useStorageState;
}
const useSessionStorageState = createUseStorageState(() =>
isBrowser ? sessionStorage : undefined,
);
useDrag & useDrop
处理拖拽
useDrag
ini
import { useRef, useEffect } from 'react';
const useEffectWithTarget = createEffectWithTarget(useEffect);
const useDrag = <T>(data: T, target: BasicTarget, options: Options = {}) => {
// useLatest上面已经定义过了,引入即可
const optionsRef = useLatest(options);
const dataRef = useLatest(data);
const imageElementRef = useRef<Element>();
const { dragImage } = optionsRef.current;
// useMount已定义过,引入即可
useMount(() => {
if (dragImage?.image) {
const { image } = dragImage;
if (isString(image)) {
const imageElement = new Image();
imageElement.src = image;
imageElementRef.current = imageElement;
} else {
imageElementRef.current = image;
}
}
});
useEffectWithTarget(
() => {
const targetElement = getTargetElement(target);
if (!targetElement?.addEventListener) {
return;
}
const onDragStart = (event: React.DragEvent) => {
optionsRef.current.onDragStart?.(event);
event.dataTransfer.setData('custom', JSON.stringify(dataRef.current));
if (dragImage?.image && imageElementRef.current) {
const { offsetX = 0, offsetY = 0 } = dragImage;
event.dataTransfer.setDragImage(imageElementRef.current, offsetX, offsetY);
}
};
const onDragEnd = (event: React.DragEvent) => {
optionsRef.current.onDragEnd?.(event);
};
targetElement.setAttribute('draggable', 'true');
targetElement.addEventListener('dragstart', onDragStart as any);
targetElement.addEventListener('dragend', onDragEnd as any);
return () => {
targetElement.removeEventListener('dragstart', onDragStart as any);
targetElement.removeEventListener('dragend', onDragEnd as any);
};
},
[],
target,
);
};
useDrop
ini
import { useRef } from 'react';
const useDrop = (target: BasicTarget, options: Options = {}) => {
const optionsRef = useLatest(options);
// https://stackoverflow.com/a/26459269
const dragEnterTarget = useRef<any>();
useEffectWithTarget(
() => {
const targetElement = getTargetElement(target);
if (!targetElement?.addEventListener) {
return;
}
const onData = (
dataTransfer: DataTransfer,
event: React.DragEvent | React.ClipboardEvent,
) => {
const uri = dataTransfer.getData('text/uri-list');
const dom = dataTransfer.getData('custom');
if (dom && optionsRef.current.onDom) {
let data = dom;
try {
data = JSON.parse(dom);
} catch (e) {
data = dom;
}
optionsRef.current.onDom(data, event as React.DragEvent);
return;
}
if (uri && optionsRef.current.onUri) {
optionsRef.current.onUri(uri, event as React.DragEvent);
return;
}
if (dataTransfer.files && dataTransfer.files.length && optionsRef.current.onFiles) {
optionsRef.current.onFiles(Array.from(dataTransfer.files), event as React.DragEvent);
return;
}
if (dataTransfer.items && dataTransfer.items.length && optionsRef.current.onText) {
dataTransfer.items[0].getAsString((text) => {
optionsRef.current.onText!(text, event as React.ClipboardEvent);
});
}
};
const onDragEnter = (event: React.DragEvent) => {
event.preventDefault();
event.stopPropagation();
dragEnterTarget.current = event.target;
optionsRef.current.onDragEnter?.(event);
};
const onDragOver = (event: React.DragEvent) => {
event.preventDefault();
optionsRef.current.onDragOver?.(event);
};
const onDragLeave = (event: React.DragEvent) => {
if (event.target === dragEnterTarget.current) {
optionsRef.current.onDragLeave?.(event);
}
};
const onDrop = (event: React.DragEvent) => {
event.preventDefault();
onData(event.dataTransfer, event);
optionsRef.current.onDrop?.(event);
};
const onPaste = (event: React.ClipboardEvent) => {
onData(event.clipboardData, event);
optionsRef.current.onPaste?.(event);
};
targetElement.addEventListener('dragenter', onDragEnter as any);
targetElement.addEventListener('dragover', onDragOver as any);
targetElement.addEventListener('dragleave', onDragLeave as any);
targetElement.addEventListener('drop', onDrop as any);
targetElement.addEventListener('paste', onPaste as any);
return () => {
targetElement.removeEventListener('dragenter', onDragEnter as any);
targetElement.removeEventListener('dragover', onDragOver as any);
targetElement.removeEventListener('dragleave', onDragLeave as any);
targetElement.removeEventListener('drop', onDrop as any);
targetElement.removeEventListener('paste', onPaste as any);
};
},
[],
target,
);
};
useVirtualList
提供虚拟列表能力
ini
const useVirtualList = <T = any>(list: T[], options: Options<T>) => {
const { containerTarget, wrapperTarget, itemHeight, overscan = 5 } = options;
const itemHeightRef = useLatest(itemHeight);
const size = useSize(containerTarget);
const scrollTriggerByScrollToFunc = useRef(false);
const [targetList, setTargetList] = useState<{ index: number; data: T }[]>([]);
const [wrapperStyle, setWrapperStyle] = useState<CSSProperties>({});
const getVisibleCount = (containerHeight: number, fromIndex: number) => {
if (isNumber(itemHeightRef.current)) {
return Math.ceil(containerHeight / itemHeightRef.current);
}
let sum = 0;
let endIndex = 0;
for (let i = fromIndex; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
endIndex = i;
if (sum >= containerHeight) {
break;
}
}
return endIndex - fromIndex;
};
const getOffset = (scrollTop: number) => {
if (isNumber(itemHeightRef.current)) {
return Math.floor(scrollTop / itemHeightRef.current) + 1;
}
let sum = 0;
let offset = 0;
for (let i = 0; i < list.length; i++) {
const height = itemHeightRef.current(i, list[i]);
sum += height;
if (sum >= scrollTop) {
offset = i;
break;
}
}
return offset + 1;
};
// 获取上部高度
const getDistanceTop = (index: number) => {
if (isNumber(itemHeightRef.current)) {
const height = index * itemHeightRef.current;
return height;
}
const height = list
.slice(0, index)
.reduce((sum, _, i) => sum + (itemHeightRef.current as ItemHeight<T>)(i, list[i]), 0);
return height;
};
const totalHeight = useMemo(() => {
if (isNumber(itemHeightRef.current)) {
return list.length * itemHeightRef.current;
}
return list.reduce(
(sum, _, index) => sum + (itemHeightRef.current as ItemHeight<T>)(index, list[index]),
0,
);
}, [list]);
const calculateRange = () => {
const container = getTargetElement(containerTarget);
if (container) {
const { scrollTop, clientHeight } = container;
const offset = getOffset(scrollTop);
const visibleCount = getVisibleCount(clientHeight, offset);
const start = Math.max(0, offset - overscan);
const end = Math.min(list.length, offset + visibleCount + overscan);
const offsetTop = getDistanceTop(start);
setWrapperStyle({
height: totalHeight - offsetTop + 'px',
marginTop: offsetTop + 'px',
});
setTargetList(
list.slice(start, end).map((ele, index) => ({
data: ele,
index: index + start,
})),
);
}
};
useUpdateEffect(() => {
const wrapper = getTargetElement(wrapperTarget) as HTMLElement;
if (wrapper) {
Object.keys(wrapperStyle).forEach((key) => (wrapper.style[key] = wrapperStyle[key]));
}
}, [wrapperStyle]);
useEffect(() => {
if (!size?.width || !size?.height) {
return;
}
calculateRange();
}, [size?.width, size?.height, list]);
useEventListener(
'scroll',
(e) => {
if (scrollTriggerByScrollToFunc.current) {
scrollTriggerByScrollToFunc.current = false;
return;
}
e.preventDefault();
calculateRange();
},
{
target: containerTarget,
},
);
const scrollTo = (index: number) => {
const container = getTargetElement(containerTarget);
if (container) {
scrollTriggerByScrollToFunc.current = true;
container.scrollTop = getDistanceTop(index);
calculateRange();
}
};
return [targetList, useMemoizedFn(scrollTo)] as const;
};
更多hooks请看参考文档第一条
vue hooks
useRequest
我去掉了ts部分方便不会ts的同学理解,用法参考dewfall123.github.io/ahooks-vue/...
ini
function useRequest(service,options) {
let contextConfig = {};
if (getCurrentInstance()) {
contextConfig = inject(RequestConfig, {});
}
// options选项合并
const finalOptions = { ...DefaultOptions, ...contextConfig, ...options };
const {
// 请求方法
requestMethod,
// loading状态
defaultLoading,
// boolean值,manual为true时需要手动调用run函数才会触发请求
manual,
throwOnError,
onSuccess,
onError,
onFinally,
formatResult,
initialData,
// 默认参数
defaultParams,
// 可以延迟 `loading` 变成 `true` 的时间,有效防止闪烁
loadingDelay,
debounceInterval,
// 设置为false时,loading 不会在第一时间变成`true`,要等到`debounceInterval`ms 后,即函数真正执行时
loadingWhenDebounceStart,
// 节流模式,单位毫秒
throttleInterval,
// 轮询间隔,单位毫秒
pollingInterval,
// pollingWhenHidden设置为false时,在屏幕不可见时,暂时暂停定时任务
pollingWhenHidden,
// pollingSinceLastFinished设置为false时,每隔`pollingInterval`ms 都会执行一次请求,而不是等上次请求结束
pollingSinceLastFinished,
// 在屏幕重新获取焦点或重新显示时,是否重新发起请求。默认为 `false`,即不会重新发起请求。
refreshOnWindowFocus,
// watch 到 refreshDeps 变化,会触发 service 重新执行
refreshDeps,
ready,
} = finalOptions;
let promiseService: (...args) => Promise;
if (['string', 'object'].includes(service)) {
promiseService = () => requestMethod(service);
} else {
promiseService = (...args) =>
new Promise((resolve, reject) => {
const returnedService = service(...args);
let fn = returnedService;
if (! returnedService.then) {
if (!['string', 'object'].includes(returnedService)) {
throw new Error(
'If sevice is a function, it must return a String, Object or Promise',
);
}
fn = requestMethod(returnedService);
}
fn.then(resolve).catch(reject);
});
}
const loading = ref(defaultLoading);
const data = ref(initialData);
const error = ref();
const params = ref(defaultParams);
const lastSuccessParams = ref(defaultParams);
let count = 0;
let unmountedFlag = false;
if (getCurrentInstance()) {
onUnmounted(() => {
unmountedFlag = true;
});
}
let isVisible = ref(true);
if (getCurrentInstance()) {
isVisible = useDocumentVisibility({
// 页面聚焦时请求一次
onVisible() {
if (refreshOnWindowFocus) {
refresh();
}
},
}).isVisible;
}
let loadingDelayTimer: any;
let pollingSinceFinishedTimer: any;
function _run(...args) {
// 只要 ready=false 不执行
if (!ready.value) {
return Promise.resolve();
}
if (pollingSinceFinishedTimer) {
clearTimeout(pollingSinceFinishedTimer);
}
if (loadingDelayTimer) {
clearTimeout(loadingDelayTimer);
}
// 设置loading
if (loadingDelay) {
loadingDelayTimer = setTimeout(() => {
loading.value = true;
}, loadingDelay);
} else {
loading.value = true;
}
count++;
const curCount = count;
params.value = cloneDeep(args);
// 抛弃该次请求结果
const shouldAbandon = () => unmountedFlag || curCount !== count;
return promiseService(...args)
.then(res => {
if (shouldAbandon()) {
return;
}
const formattedResult = formatResult(res);
data.value = formattedResult;
// fix #21
error.value = undefined;
lastSuccessParams.value = cloneDeep(args);
onSuccess(formattedResult, args);
return formattedResult;
})
.catch(err => {
if (shouldAbandon()) {
return;
}
console.error(err);
error.value = err;
onError(err, args);
if (throwOnError) {
throw err;
}
})
.finally(() => {
if (shouldAbandon()) {
return;
}
if (loadingDelayTimer) {
clearTimeout(loadingDelayTimer);
}
// 在请求结束时轮询
if (pollingInterval && pollingSinceLastFinished) {
// 当时页面不可见,等页面可见再查询
if (pollingWhenHidden && !isVisible.value) {
pollingSinceFinishedTimer = setInterval(() => {
// 需要恢复查询
if (!(pollingWhenHidden && !isVisible.value)) {
clearInterval(pollingSinceFinishedTimer);
_run(...args);
}
}, pollingInterval);
} else {
pollingSinceFinishedTimer = setTimeout(() => {
_run(...args);
}, pollingInterval);
}
}
loading.value = false;
onFinally();
});
}
const cancel = () => {
if (pollingTimer) {
clearInterval(pollingTimer);
}
if (pollingSinceFinishedTimer) {
clearTimeout(pollingSinceFinishedTimer);
}
if (loadingDelayTimer) {
clearTimeout(loadingDelayTimer);
}
count++;
loading.value = false;
};
let run = _run;
if (debounceInterval) {
const debounceRun = debounce(_run, debounceInterval);
run = (...args) => {
// 在debounce等待阶段把loading设置为true,比loadingDelay优先级高
if (loadingWhenDebounceStart) {
loading.value = true;
}
return Promise.resolve(debounceRun(...args)!);
};
}
if (throttleInterval) {
const throttleRun = throttle(_run, throttleInterval);
run = (...args) => {
return Promise.resolve(throttleRun(...args)!);
};
}
let pollingTimer: any;
// 每隔x时间开始轮询,不管上次什么时候结束
if (pollingInterval && !pollingSinceLastFinished) {
run = (...args) => {
if (pollingTimer) {
clearInterval(pollingTimer);
}
pollingTimer = setInterval(() => {
if (pollingWhenHidden && !isVisible.value) {
return;
}
_run(...args);
}, pollingInterval);
return _run(...args);
};
}
function refresh() {
return run(...params.value);
}
// 自动执行
if (!manual) {
// ready 变为true 自动发起请求,会带上参数 options.defaultParams
watch(
ready,
() => {
if (ready.value) {
run(...defaultParams);
}
},
{
immediate: true,
},
);
}
// refreshDeps
watch(refreshDeps, refresh);
return {
// 是否正在加载
loading,
// 抛出异常
error,
// 返回的数据,默认为undefined
data,
// 手动触发
run,
// 参数数组
params,
// 只有执行成功才赋值的参数
lastSuccessParams,
// 取消当前请求,如果有轮询就停止
cancel,
// 使用上一次的params重新执行service
refresh,
};
}
useAxios
用于axios请求的hook,基于useRequest封装
csharp
function useAxios(params,options) {
return useRequest(params, {
...options,
requestMethod,
});
}
useToggle
用于在两个状态值间切换的 Hook
上面react已经有ts版本的了,vue就用js版本的
ini
function useToggle(defaultValue,reverseValue) {
const state = ref(defaultValue);
const setState = (value) => {
state.value = value;
};
const reverseValueOrigin = (reverseValue === undefined
? !defaultValue
: reverseValue);
// 切换返回值
const toggle = (value) => {
if (value === undefined) {
value = state.value === defaultValue ? reverseValueOrigin : defaultValue;
}
setState(value);
};
// 设置默认值
const setLeft = () => {
setState(defaultValue);
};
// 设置取反值
const setRight = () => {
setState(reverseValueOrigin);
};
return {
state,
toggle,
setLeft,
setRight,
};
}
useBoolean
跟上面useToggle的区别就是只接受传入boolean值
ini
const useBoolean = (defaultValue = false) => {
const { state, toggle } = useToggle(defaultValue);
const setTrue = () => toggle(true);
const setFalse = () => toggle(false);
const toggleBoolean = () => toggle();
return {
state,
toggle: toggleBoolean,
setTrue,
setFalse,
};
};
useHover
一个用于追踪 dom 元素是否有鼠标悬停的 Hook
通过监听鼠标移入移出事件mouseenter/mouseleave实现
scss
function useHover(target, options) {
const { state, setFalse, setTrue } = useBoolean(false);
const { onEnter, onLeave } = options ?? {};
function onMouseEnter() {
setTrue();
onEnter && onEnter();
}
function onMouseLeave() {
setFalse && setFalse();
onLeave && onLeave();
}
safeOnMounted(() => {
const targetElement = getTargetElement(target);
if (!targetElement) {
return;
}
targetElement.addEventListener('mouseenter', onMouseEnter);
targetElement.addEventListener('mouseleave', onMouseLeave);
});
onUnmounted(() => {
// 等价于document.querySelector
const targetElement = getTargetElement(target);
if (targetElement) {
targetElement.removeEventListener('mouseenter', onMouseEnter);
targetElement.removeEventListener('mouseleave', onMouseLeave);
}
});
return state;
}
useKeyPress
用于管理键盘事件的hook,比如键盘点击enter键,点击shift键等
ini
const defaultEvents = ['keydown'];
function useKeyPress(
keyFilter,
eventHandler,
option,
) {
const { events = defaultEvents, target } = option;
const isKeyEvent = genKeyFormater(keyFilter);
safeOnMounted(() => {
const handlers = [];
// 类似于document.getElementById,区别是获取不到target时默认值为window
const el = getTargetElement(target, window)!;
for (const eventName of events) {
const handler = (event) => {
if (isKeyEvent(event)) {
return eventHandler(event);
}
};
handlers.push(handler);
el.addEventListener(eventName, handler);
}
onUnmounted(() => {
for (const eventName of events) {
const handler = handlers.shift()!;
el.removeEventListener(eventName, handler);
}
});
});
}
/**
* 键盘输入预处理方法
* @param [keyFilter] 当前键
* @returns () => Boolean
*/
export function genKeyFormater(keyFilter) {
const type = isType(keyFilter);
if (type === 'function') {
return keyFilter;
}
if (type === 'string' || type === 'number') {
return (event) => genFilterKey(event, keyFilter);
}
if (type === 'array') {
return (event) =>
keyFilter.some((item) => genFilterKey(event, item));
}
return keyFilter ? () => true : () => false;
}
useTable
封装了表格常用的功能的hook,支持分页、排序、搜索
ini
function useTable<T>(data,options){
const { page, sort, search } = merge(
{},
defaultParams,
options,
);
const dataRef = isRef(data) ? data : (computed(() => data);
const page = ref(pageParams);
const sort = ref(sortParams);
const search = ref(searchParams);
const throlltedSearchText = useThrottle(() => search.value.text, {
wait: 500,
});
const filtedList = computed(() => {
let list = [...dataRef.value];
// 查询
const searchText = throlltedSearchText.value.trim();
if (searchText) {
list = list.filter(i => {
let iStr = '';
if (search.value.keys?.length) {
iStr = search.value.keys.map(key => i[key]).join(' ');
} else {
iStr = JSON.stringify(i);
}
if (search.value.isReg) {
try {
const matched = Boolean(iStr.match(new RegExp(searchText)));
return matched;
} catch {
}
}
return iStr
.toLocaleLowerCase()
.includes(searchText.toLocaleLowerCase());
});
}
// 排序处理
if (sort.value.key) {
list.sort((a, b) => {
const key = sort.value.key;
const sortResult = sort.value.compareFn(
a,
b,
);
return sort.value.direction === 'ascend' ? sortResult : sortResult * -1;
});
}
return list;
});
const total = computed(() => filtedList.value.length);
const pagedData = computed(() => {
let list = [...filtedList.value];
// page slice
const start = (page.value.index - 1) * page.value.size;
const end = start + page.value.size;
list = list.slice(start, end);
return list;
});
watch([data], () => {
search.value.text = '';
page.value.index = 1;
});
watch(
() => page.value.size,
() => {
page.value.index = 1;
},
{
immediate: false,
},
);
watch(
() => filtedList.value,
() => {
page.value.index = 1;
},
);
return {
data,
page,
search,
sort,
pagedData,
total,
};
}
往期文章
🥇🥇🥇 一网打尽主流的微前端框架生命周期/路由/资源系统:原理篇
🥇🥇🥇一文带你打通微前端-qiankun/microapp/icestark/wujie全解析