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
}
}