封装常用的工具函数助力业务开发(含react hooks/vue hooks)

哈喽,我是柠檬酱👏

工欲善其事必先利其器,咱们来扒一扒开源库 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,
  };
}

往期文章

🥇🥇🥇 一网打尽主流的微前端框架生命周期/路由/资源系统:原理篇

一文带你梳理React面试题(2023年版本)

抽丝剥茧带你复习vue源码(2023年面试版本)

🥇🥇🥇一文带你打通微前端-qiankun/microapp/icestark/wujie全解析

🔥🔥🔥手把手带你集成低代码平台应用

🛬🛬🛬手把手带你集成阿里低代码引擎vue物料库

🐒🐒🐒猴子都能看懂的Rxjs教程(上篇)

🐟🐟🐟摸鱼时间到!两分钟实现掘金文字版权效果

项目实战:跨项目组件复用方案选择

5k字长文总结性能优化:构建你的前端知识体系

参考文档

  1. hooks Github
  2. hooks-vue Github
  3. react-use
相关推荐
小白学习日记34 分钟前
【复习】HTML常用标签<table>
前端·html
程序员大金37 分钟前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
john_hjy38 分钟前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀1 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会2 小时前
数组的概念
javascript