React Hook

异步操作相关

useEffectAsync

允许异步返回清除函数

export function useEffectAsync(effect:()=>Promise<()=>void>,deps: DependencyList){
    useEffect(()=>{
        let closed = false
        let closeFunc: (()=>void)
        effect().then((f)=>{
            if(closed){
                f()
            }else{
                closeFunc = f
            }
        })
        return ()=>{
            closeFunc?.()
            closed = true
        }
    },deps)
}

useLoading

为一个包装一个异步函数,使其同时只能运行一个,并获得一个是否正在运行的状态变量

export function useLoading<AT extends Array<any>, RT>(func: (...args: AT) => Promise<RT>): [
    (...args: AT) => Promise<RT | undefined>,
    boolean,
] {
    const [loading, setLoading] = useState(false)
    const loadingRef = useRef<boolean>(false)
    const f = useCallback(async (...args: AT) => {
        if (loadingRef.current) {
            return undefined
        }
        setLoading(loadingRef.current = true)
        try {
            const v = await func(...args)
            setLoading(loadingRef.current = false)
            return v
        } catch (e) {
            setLoading(loadingRef.current = false)
            notification.error({
                message: "发生错误",
                description: String(e)
            })
            throw e
        }

    }, [func])

    return [f, loading]
}

useAsyncTimer

每隔一段时间执行一次异步函数

export function useAsyncTimer(interval: number, func: () => Promise<any>) {
    useEffect(() => {
        let timer: any = undefined
        let running: boolean = true
        const run = async () => {
            timer = undefined
            try {
                await func()
            } catch { }
            if (running) {
                timer = setTimeout(run, interval)
            }
        }
        timer = setTimeout(run, interval)
        return () => {
            running = false
            timer !== undefined && clearTimeout(timer)
        }
    }, [interval, func])
}

useInitLoading

异步加载一次数据

export function useInitLoading<RT>(func: () => Promise<RT>): RT | undefined {
    const inited = useRef<RT | undefined | null>(null)
    const [_, setFlag] = useState(0)
    if (inited.current === null) {
        func().then(v => {
            inited.current = v
            setFlag(f => f + 1)
        })
        inited.current = undefined
    }
    return inited.current
}

自定义

useTheme

获取页面亮暗主题

import { useState, useEffect, useMemo } from "react"

export type ThemeName = 'light' | 'dark'
export type ThemeType = {
    themeName: ThemeName
    isDarkMode: boolean
    isLightMode: boolean
}

export function useTheme(): ThemeType {
    const [themeName, setThemeName] = useState<ThemeName>('light')
    useEffect(() => {
        setThemeName(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
        window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => setThemeName(e.matches ? 'dark' : "light"))
    }, [])
    return useMemo(() => ({
        themeName,
        isDarkMode: themeName === 'dark',
        isLightMode: themeName === 'light',
    }), [themeName])
}

实用性

useLastValue

获取上一次渲染时的值

export function useLastValue<T, U = undefined>(nowValue: T, firstValue: U = undefined as U): T | U {
    const valueRef = useRef<T | U>(firstValue);
    const lastValue = valueRef.current;
    valueRef.current = nowValue;
    return lastValue;
}

useEffectAndValueRef

为了性能有时候不希望刷新副作用,此函数在副作用函数的基础上添加了每次刷新(无条件)都更新的ref

export function useEffectAndValueRef<T extends Object>(callback: (v: T) => (void | (() => void)), refValue: T, deps: DependencyList = []) {
    const ref = useRef<T>({} as T);
    Object.assign(ref.current, refValue);
    useEffect(() => {
        return callback(ref.current)
    }, deps)
}

useNotUndefinedValue

获取最近一个不为undefined的值

export function useNotUndefinedValue<T>(value: T | undefined): T|undefined {
    const valueRef = useRef<T | undefined>(value)
    useEffect(()=>{
        if(value !== undefined){
            valueRef.current = value
        }
    },[value])
    return valueRef.current === undefined ? value : valueRef.current
}

usePropValue

用于创建可监听变化的属性

import { useEffect, useState } from "react"

export class EavesdropperProp<T> {
    private onChangeEventList: (() => void)[] = []

    constructor(
        private value: T,
    ) { }
    get() {
        return this.value
    }
    set(value: T) {
        this.value = value
        onPropChange(this.onChangeEventList)
        return value
    }

    useValue() {
        return usePropValue(this.value, this.onChangeEventList)
    }
}

export function usePropValue<T>(prop: T, onChangeEventList: (() => void)[]): T {
    const [_, setFlag] = useState(0)

    useEffect(() => {
        const cb = () => {
            setFlag(f => f + 1)
        }
        onChangeEventList.push(cb)
        return () => {
            onChangeEventList.splice(onChangeEventList.findIndex(c => c === cb), 1)
        }
    }, [onChangeEventList])

    return prop
}

export function onPropChange(onChangeEventList: (() => void)[]) {
    for (const cb of onChangeEventList) {
        cb()
    }
}

TypeScript

实用函数

frameCall

每帧调用一次

export function frameCall(fn: (delta: number) => any) {
    let lastTime = Date.now()
    let cid = 0

    function frame() {
        const now = Date.now()
        const delta = (now - lastTime)
        lastTime = now

        if (!fn(delta)) {
            cid = requestAnimationFrame(frame)
        }
    }

    cid = requestAnimationFrame(frame)
    return () => {
        cancelAnimationFrame(cid)
    }
}

downloadFileData

export function downloadFileData(data: BlobPart[], fileName: string) {
    const blob = new Blob(data);
    const blobUrl = window.URL.createObjectURL(blob);
    const link = document.createElement('a');
    link.download = fileName;
    link.style.display = 'none';
    link.href = blobUrl;
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

异步

unAsync.ts

工具类

PromisePool

承诺池

从承诺池创建承诺,这些承诺会在特定情况下兑现。

承诺池中创建的承诺会在以下情况被兑现:

  • 达到超时时间

  • 收到特定信号

export class PromisePool<T, R> {
    private readonly pool = new Map<R, [(arg: T) => void, NodeJS.Timeout, Promise<T | undefined> | null][]>
    private readonly promiseDestroy: Map<Promise<T | undefined>, (arg?: T) => void> = new Map()

    constructor(
        private readonly defaulTimeout: number,
    ) {
    }

    /**
     * 创建承诺
     */
    getPromise(name: R, timeout?: number) {
        let timeoutFunc: (arg?: T) => void
        let timer: NodeJS.Timeout
        let item: [(arg: T) => void, NodeJS.Timeout, Promise<T | undefined> | null]

        const p = new Promise<T | undefined>(ok => {
            if (!this.pool.has(name)) {
                this.pool.set(name, [])
            }
            const list = this.pool.get(name)!
            timer = setTimeout(timeoutFunc = (arg?: T) => {
                list.splice(list.indexOf(item), 1)
                ok(arg)
                this.promiseDestroy.delete(item[2]!)
            }, timeout ?? this.defaulTimeout)
            item = [ok, timer, null]
            list.push(item)
        })
        item![2] = p
        this.promiseDestroy.set(p, () => {
            clearTimeout(timer)
            timeoutFunc()
        })
        return p
    }


    public call(name: R, arg: T) {
        const list = this.pool.get(name)
        if (list) {
            list.forEach(item => {
                item[0](arg)
                clearTimeout(item[1])
                this.promiseDestroy.delete(item[2]!)
            })
            this.pool.delete(name)
        }
    }

    public callOne(promise: Promise<T | undefined>, arg?: T) {
        this.promiseDestroy.get(promise)?.(arg)
    }

}

格式化

formatTime

import { format } from "date-fns";
import { zhCN } from "date-fns/locale";

export function formatTime(time?: Date): string {
    return time ? format(time, 'yyyy-MM-dd HH:mm:ss', {
        locale: zhCN,
    }) : "";
}

formatDeltaTime

export function formatDeltaTime(time: number): string {
    const negative = time < 0;
    time = Math.abs(time);
    return `${(() => {
        const seconds = Math.floor(time / 1000);
        if (seconds < 10) {
            return "几秒"
        }
        const minutes = Math.floor(seconds / 60);
        if (minutes < 1) {
            return `${seconds}秒`
        }
        const hours = Math.floor(minutes / 60);
        if (hours < 1) {
            return `${minutes}分钟`
        }
        const days = Math.floor(hours / 24);
        if (days < 1) {
            return `${hours}小时`
        }
        const months = Math.floor(days / 30.4);
        if (months < 1) {
            return `${days}天`
        }
        const years = Math.floor(months / 12);
        if (years < 1) {
            return `${months}个月`
        }
        return `${years}年${months % 12}个月`
    })()}${negative ? "后" : "前"}`
}

formatPrivacyEmail

export function formatPrivacyEmail(email: string): string {
    const [username, domain] = email.split('@');

    const showLength = Math.min(
        username.length,
        3,
        Math.floor(username.length / 2)
    )

    const tarr = []
    for (let i = 0; i < username.length - showLength; i++) {
        tarr.push('*')
    }

    return `${username.slice(0, showLength)}${tarr.join('')}@${domain}`;
}

getBearingDescription

获取方位角的文字描述

export const getBearingDescription = (bearing: number): string => {
    const degrees = bearing / Math.PI * 180

    if (degrees >= 337.5 || degrees < 22.5) return '北';
    if (degrees >= 22.5 && degrees < 67.5) return '东北';
    if (degrees >= 67.5 && degrees < 112.5) return '东';
    if (degrees >= 112.5 && degrees < 157.5) return '东南';
    if (degrees >= 157.5 && degrees < 202.5) return '南';
    if (degrees >= 202.5 && degrees < 247.5) return '西南';
    if (degrees >= 247.5 && degrees < 292.5) return '西';
    return '西北';
};

dithering

异步请求消抖

将在请求成功时执行callback

保证同一时间只有一个promise在执行

保证最后一次传入的参数一定会被执行并获得结果

export function dithering<TA extends any[], TR>(
    func: (...args: TA) => Promise<TR>, 
    callback: (result: TR) => void
): (...args: TA) => void
{
    let newArgs: TA | null = null
    let running = false

    const run = async()=>{
        try{
            running = true
            let arg: TA | null = null
            while(newArgs !== arg){
                arg = newArgs
                const result = await func(...arg!)
                callback(result)
            }
        }finally{
            running = false
        }
        
    }

    return (...args: TA) => {
        newArgs = args
        if(!running){
            run()
        }
    }
}

onceFunction

export function onceFunction<TR>(func: ()=>TR):()=>TR{
    let v : TR;
    let called = false
    return ()=>{
        if(called){
            return v
        }else{
            return v = func()
        }
    }
}

算法

calculateDistance

计算两个经纬度坐标点之间的距离

export const calculateDistance = (
    lon1: number,
    lat1: number,
    lon2: number,
    lat2: number,
): number => {
    /**
     * 将角度转换为弧度
     * @param degrees 角度值
     * @returns 弧度值
     */
    const toRadians = (degrees: number): number => {
        return degrees * (Math.PI / 180);
    };

    // 地球平均半径(千米)
    const R = 6371.0;

    // 将经纬度转换为弧度
    const lat1Rad = toRadians(lat1);
    const lon1Rad = toRadians(lon1);
    const lat2Rad = toRadians(lat2);
    const lon2Rad = toRadians(lon2);

    // Haversine 公式
    const dlon = lon2Rad - lon1Rad;
    const dlat = lat2Rad - lat1Rad;
    const a =
        Math.sin(dlat / 2) ** 2 +
        Math.cos(lat1Rad) * Math.cos(lat2Rad) * Math.sin(dlon / 2) ** 2;
    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    // 计算最终距离
    return R * c;
};

NextJS

SystemError

import {ZodError} from "zod";

export default class SystemError {
    constructor(private message: string = "未知系统错误") {
    }

    static toSystemError(error: any): SystemError {
        switch (error.constructor) {
            case String:
                if (error[0] == '{') {
                    const msg = /message: "(.*?)"/.exec(error);
                    if (msg) {
                        return new SystemError(msg[1]);
                    } else {
                        if (/message:/.test(error)) {
                            return new SystemError()
                        }
                    }
                }
                return new SystemError(error);
            case SystemError:
                return error;
            case ZodError:
                return new SystemError("参数错误:" + error.errors.map((e: any) => e.message).join(","));
            default:
                return error.message ? this.toSystemError(error.message) : new SystemError(undefined);
        }
    }

    getMessage(): string {
        return this.message;
    }
}

R

import SystemError from "./SystemError";
import { notification } from "antd";

export type RType<T> = {
    success: true,
    data: T,
} | {
    success: false;
    message: string;
}

const redirecterr = "正在重定向···"

export default class R {
    static readonly PermissionDenied = R.fail("权限不足")
    static readonly NotFound = R.fail("未找到")
    static readonly OK = R.fail("成功")

    static success<T>(data: T): RType<T> {
        return {
            success: true,
            data: data,
        }
    }

    static fail(message: string): RType<any> {
        return {
            success: false,
            message: message,
        }
    }

    static async run<T>(promise: Promise<T>): Promise<RType<T>> {
        try {
            const res = await promise;
            return R.success(res)
        } catch (e: any) {
            if (e.message === "NEXT_REDIRECT") {
                throw e;
            }
            const msg = SystemError.toSystemError(e).getMessage()
            return R.fail(msg)
        }
    }

    static runFunc<T>(func: ()=>Promise<T>):Promise<RType<T>> {
        return this.run(func()) 
    }

    static async runNoReturn<T>(promise: Promise<T>): Promise<RType<void>> {
        const res = await R.run(promise);

        delete (res as any).data
        return res as RType<void>
    }

    static async waitRun<T>(promise: Promise<RType<T>>, showMessage = true): Promise<T> {
        let errmsg = "";
        try {
            const res = await promise;
            if (res) {
                if (res.success) {
                    return res.data;
                }
                errmsg = res.message;
            } else {
                // 空的res,服务器发送了异常,这个异常应该被忽略
                // 已知的,这个异常可能是被重定向
                // throw new SystemError("正在重定向···")
                errmsg = redirecterr
            }
        } catch (e) {
            console.error(e);
            errmsg = "网络错误,请稍后再试"
        }
        if (showMessage && redirecterr !== errmsg) {
            notification.error({
                message: errmsg,
            })
        }
        throw new SystemError(errmsg);
    }
}

SearchParams

import SystemError from "./SystemError";

export type SearchParams = { [key: string]: string | string[] | undefined }

export function getSearchValue(search: SearchParams, key: string): string {
    const v = getSearchValueOptional(search, key)
    if (v === undefined) throw new SystemError("未提供所需参数")
    return v
}

export function getSearchValueOptional(search: SearchParams, key: string): string | undefined {
    const value = search[key]
    if (Array.isArray(value)) {
        return value[0]
    } else {
        return value
    }
}