自定义一些常见的hook,方便在工作中使用,以下内容都是本人工作中的使用经验,不足之处,欢迎指正
useBooleans
ts
import { useState } from 'react'
/**
* @description 切换true/false的公共hook
* @param {Boolean} initValue 默认值
*/
export interface UseBooleansActions {
toggle: () => void // 切换值
set: (value: boolean) => void // 设置值
setTrue: () => void // 设置为true
setFalse: () => void // 设置值为false
reset: () => void // 重置为初始值
}
function useBooleans(initValue = false): [boolean, UseBooleansActions] {
const [value, setValue] = useState<boolean>(initValue)
/** 切换值 */
const toggle = () => {
setValue((pre) => !pre)
}
/** 设置值 */
const set = (value: boolean) => {
setValue(value)
}
/** 设置为true */
const setTrue = () => {
setValue(true)
}
/** 设置为false */
const setFalse = () => {
setValue(false)
}
/** 重置为初始值 */
const reset = () => {
setValue(initValue)
}
return [value, { toggle, set, setTrue, setFalse, reset }]
}
export default useBooleans
// 使用
const [booleanValue, { toggle, setTrue, setFalse, reset, set }] = useBooleans(true)
useToggle
ts
import { useMemo, useState } from 'react'
// 定义操作方法的接口
interface Actions<T, U> {
toggle: () => void
set: (value: T | U) => void
setLeft: () => void // 设置为默认值
setRight: () => void // 设置为取反值
}
function useToggle<T = boolean>(
defaultValue?: T,
reverseValue?: T,
): [T, Actions<T, T>]
function useToggle<T, U>(
defaultValue: T,
reverseValue: U,
): [T | U, Actions<T | U, T | U>]
function useToggle<D, R>(
defaultValue: D = false as unknown as D,
reverseValue?: R,
) {
// 状态管理:支持默认值和取反值
const [state, setState] = useState<D | R>(defaultValue)
// 计算实际的取反值(若未提供则使用布尔取反)
const reverseValueOrigin =
reverseValue === undefined ? !defaultValue : reverseValue
// 缓存操作方法避免重复创建
const actions = useMemo(() => {
// 核心切换逻辑:在默认值和取反值之间切换
const toggle = () =>
setState(
(s) =>
(s === defaultValue ? reverseValueOrigin : defaultValue) as D | R,
)
const set = (value: D | R) => setState(value)
const setLeft = () => setState(defaultValue)
const setRight = () => setState(reverseValueOrigin as D | R)
return { toggle, set, setLeft, setRight }
}, [defaultValue, reverseValueOrigin])
return [state, actions]
}
export default useToggle
// 使用
const [toggleValue, { toggle: toggle1, set: set1, setLeft, setRight }] =
useToggle<'left', 'right'>('left', 'right')
useExcel
基于xlsx的一些常见的导入导出功能封装
ts
import { getTypeOf, isAvailableArr } from '@/utils'
import { message } from 'antd'
import type {
BookType,
ColInfo,
Range,
RowInfo,
Sheet2JSONOpts,
WorkBook,
} from 'xlsx'
import * as XLSX from 'xlsx'
// 导出文件配置
export type IExportConfig = {
name?: string // 导出文件名称
bookType?: BookType // 导出文件类型
sheetName?: string // sheet名称
errorMsg?: string // 错误提示
headers?: Record<string, string> // 自定义表头,导出的文件里面只有在定义中的字段,并且如果数据为空的话,只生成一个表头。示例:{id: 'ID', name: '链接名称', site_type: '官网类型'}
merges?: Range[] // 单元格合并
colInfo?: ColInfo[] // 列属性
rowInfo?: RowInfo[] // 行属性
}
// 多sheet导出
export type IExtraSheetConfig = {
name?: string // 导出文件名称
bookType?: BookType // 导出文件类型
sheets: ({ json: any[] } & Omit<IExportConfig, 'name' | 'bookType'>)[]
}
/**
* @desc 自定义导出文件hook
*/
const useExcel = () => {
/**
* @desc 一维数组导出Excel文件
*
* 此函数将一个一维数组转换为Excel文件并下载
* 支持自定义表头、单元格合并、列属性和行属性
*
* @param {any[]} json - 要导出的数据数组
* @param {IExportConfig} config - 导出配置项
* @param {string} [config.name='导出'] - 导出文件名称
* @param {BookType} [config.bookType='xlsx'] - 导出文件类型
* @param {string} [config.sheetName='Sheet1'] - 工作表名称
* @param {Record<string, string>} [config.headers] - 自定义表头映射
* @param {Range[]} [config.merges] - 单元格合并范围
* @param {ColInfo[]} [config.colInfo] - 列属性配置
* @param {RowInfo[]} [config.rowInfo] - 行属性配置
* @returns {void}
*/
function exportJson2Excel<T = any>(json: T[], config?: IExportConfig) {
const {
name = '导出',
sheetName = 'Sheet1',
bookType = 'xlsx',
headers,
merges,
colInfo,
rowInfo,
} = config || {}
let lists = [...json]
if (
headers &&
getTypeOf(headers) === 'Object' &&
isAvailableArr(Object.keys(headers))
) {
// 没有数据的时候用header去生成一个空表头
if (!isAvailableArr(lists)) {
const headersField = Object.values(headers)
const headerObj: Record<string, any> = {}
headersField.forEach((f) => {
headerObj[f] = null
})
lists = [headerObj as T]
} else {
// 有数据的时候根据header去生成
const headerFields = Object.entries(headers)
lists = json.map((j) => {
const obj: Record<string, any> = {}
for (const [key, value] of headerFields) {
obj[value] = j[key as keyof T] ?? null
}
return obj
}) as T[]
}
}
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.json_to_sheet(lists)
if (merges) {
ws['!merges'] = merges
}
if (colInfo) {
ws['!cols'] = colInfo
}
if (rowInfo) {
ws['!rows'] = rowInfo
}
XLSX.utils.book_append_sheet(wb, ws, sheetName)
XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
}
/**
* @desc 一维数组多sheet导出Excel文件
*
* 此函数将多个一维数组分别放在不同的工作表中导出为一个Excel文件
* 支持自定义表头、单元格合并、列属性和行属性
*
* @param {IExtraSheetConfig} params - 多sheet导出配置项
* @param {string} [params.name='导出'] - 导出文件名称
* @param {BookType} [params.bookType='xlsx'] - 导出文件类型
* @param {Array} params.sheets - 工作表配置数组
* @param {any[]} params.sheets[].json - 要导出的数据数组
* @param {string} [params.sheets[].sheetName] - 工作表名称
* @param {Record<string, string>} [params.sheets[].headers] - 自定义表头映射
* @param {Range[]} [params.sheets[].merges] - 单元格合并范围
* @param {ColInfo[]} [params.sheets[].colInfo] - 列属性配置
* @param {RowInfo[]} [params.sheets[].rowInfo] - 行属性配置
* @returns {void}
*/
function exportJson2ExcelSheets<T = any>(params: IExtraSheetConfig) {
const { name = '导出', bookType = 'xlsx', sheets } = params || {}
const wb = XLSX.utils.book_new()
if (isAvailableArr(sheets)) {
sheets?.forEach((s) => {
const { json, headers, merges, colInfo, rowInfo, sheetName } = s || {}
let lists = [...json]
if (
headers &&
getTypeOf(headers) === 'Object' &&
isAvailableArr(Object.keys(headers))
) {
// 没有数据的时候用header去生成一个空表头
if (!isAvailableArr(lists)) {
const headersField = Object.values(headers)
const headerObj: Record<string, any> = {}
headersField.forEach((f) => {
headerObj[f] = null
})
lists = [headerObj as T]
} else {
// 有数据的时候根据header去生成
const headerFields = Object.entries(headers)
lists = json.map((j) => {
const obj: Record<string, any> = {}
for (const [key, value] of headerFields) {
obj[value] = j[key] ?? null
}
return obj
}) as T[]
}
}
const ws = XLSX.utils.json_to_sheet(lists)
if (merges) {
ws['!merges'] = merges
}
if (colInfo) {
ws['!cols'] = colInfo
}
if (rowInfo) {
ws['!rows'] = rowInfo
}
XLSX.utils.book_append_sheet(wb, ws, sheetName)
})
} else {
const ws = XLSX.utils.json_to_sheet([])
XLSX.utils.book_append_sheet(wb, ws)
}
XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
}
/**
* @desc 二维数组导出Excel文件
*
* 此函数将一个二维数组转换为Excel文件并下载
* 支持单元格合并、列属性和行属性
*
* @param {any[][]} aoas - 要导出的二维数组
* @param {IExportConfig} config - 导出配置项
* @param {string} [config.name='导出'] - 导出文件名称
* @param {BookType} [config.bookType='xlsx'] - 导出文件类型
* @param {string} [config.sheetName='Sheet1'] - 工作表名称
* @param {Range[]} [config.merges] - 单元格合并范围
* @param {ColInfo[]} [config.colInfo] - 列属性配置
* @param {RowInfo[]} [config.rowInfo] - 行属性配置
* @returns {void}
*/
function exportAoa2Excel<T = any>(aoas: T[][], config?: IExportConfig) {
const {
name = '导出',
sheetName = 'Sheet1',
bookType = 'xlsx',
merges,
colInfo,
rowInfo,
} = config || {}
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.aoa_to_sheet(aoas)
if (merges) {
ws['!merges'] = merges
}
if (colInfo) {
ws['!cols'] = colInfo
}
if (rowInfo) {
ws['!rows'] = rowInfo
}
XLSX.utils.book_append_sheet(wb, ws, sheetName)
XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
}
/**
* @desc 从本地文件读取Excel工作簿
*
* 此函数从File或Blob对象读取Excel文件并返回工作簿对象
*
* @param {File | Blob} file - 要读取的Excel文件
* @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
*/
function readWorkbookFromLocalFile(
file: File | Blob,
): Promise<WorkBook | false> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = function (e) {
const data = (e.target as any)?.result
const workbook = XLSX.read(data, {
type: 'binary',
raw: true,
cellNF: true,
})
resolve(workbook)
}
reader.onerror = function () {
resolve(false)
}
})
}
/**
* @desc 从本地Excel文件读取数据为JSON格式
*
* 此函数从File或Blob对象读取Excel文件并返回第一个工作表的数据为JSON数组
*
* @param {File | Blob} file - 要读取的Excel文件
* @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
* @returns {Promise<any[] | false>} 返回JSON数组或false(读取失败时)
*/
function readFileToJson<T = any>(
file: File | Blob,
options?: Sheet2JSONOpts,
): Promise<T[] | false> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = function (e) {
const data = (e.target as any)?.result
const workbook = XLSX.read(data, {
type: 'binary',
raw: true,
cellNF: true,
})
const json = XLSX.utils.sheet_to_json<T>(
workbook.Sheets[workbook.SheetNames[0]],
options,
)
resolve(json)
}
reader.onerror = function () {
resolve(false)
}
})
}
/**
* @desc 从本地Excel文件读取多个工作表的数据为JSON格式
*
* 此函数从File或Blob对象读取Excel文件并返回所有工作表的数据为JSON对象
*
* @param {File | Blob} file - 要读取的Excel文件
* @param {Sheet2JSONOpts} [options] - 工作表转JSON的选项
* @returns {Promise<Record<string, any[]> | false>} 返回包含所有工作表数据的对象或false(读取失败时)
*/
function readFileToJsons(
file: File | Blob,
options?: Sheet2JSONOpts,
): Promise<Record<string, any[]> | false> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = function (e) {
const data = e.target?.result
const workbook = XLSX.read(data, {
type: 'binary',
raw: true,
cellNF: true,
})
const sheetNames = workbook.SheetNames
if (!sheetNames?.length) {
resolve(false)
}
const jsons = sheetNames.reduce(
(pre: Record<string, any[]>, cur: string) => {
const json = XLSX.utils.sheet_to_json(workbook.Sheets[cur], options)
pre[cur] = json
return pre
},
{},
)
resolve(jsons)
}
reader.onerror = function () {
resolve(false)
}
})
}
/**
* @desc 文件流导出Excel文件
*
* 此函数从File或Blob对象读取Excel文件流并导出为Excel文件
*
* @param {File | Blob} file - 要导出的Excel文件流
* @param {IExportConfig} config - 导出配置项
* @param {string} [config.name='导出'] - 导出文件名称
* @param {BookType} [config.bookType='xlsx'] - 导出文件类型
* @param {string} [config.errorMsg='下载失败'] - 错误提示信息
* @returns {Promise<void>}
*/
async function exportBuffer2Excel(file: File | Blob, config?: IExportConfig) {
const {
name = '导出',
bookType = 'xlsx',
errorMsg = '下载失败',
} = config || {}
const wb: WorkBook | false = await readWorkbookFromLocalFile(file)
if (wb) {
XLSX.writeFile(wb, `${name}.${bookType}`, { bookType })
} else {
message.error(errorMsg)
}
}
/**
* @desc 从URL导出Excel文件
*
* 此函数从URL获取Excel文件并导出
*
* @param {string} url - Excel文件的URL地址
* @param {IExportConfig} config - 导出配置项
* @param {string} [config.name='导出'] - 导出文件名称
* @param {BookType} [config.bookType='xlsx'] - 导出文件类型
* @param {string} [config.errorMsg='下载失败'] - 错误提示信息
* @returns {Promise<void>}
*/
async function exportUrl2Excel(url: string, config?: IExportConfig) {
fetch(url)
.then((response) => response.blob())
.then((blob) => {
exportBuffer2Excel(new Blob([blob]), config)
})
.catch((error) => {
throw error
})
}
/**
* @desc 从工作表中读取指定列的数据
*
* 此函数从Excel文件的指定工作表中读取指定列的数据
*
* @param {File | Blob} file - Excel文件
* @param {number} [sheetIndex=0] - 工作表索引
* @param {string[]} [columns=['A']] - 要读取的列数组(如['A', 'B'])
* @param {Record<string, string>} [fieldsMap={}] - 列名到字段名的映射
* @returns {Promise<any[] | false>} 返回包含指定列数据的数组或false(读取失败时)
*/
function readColumnFromSheet(
file: File | Blob,
sheetIndex: number = 0,
columns: string[] = ['A'],
fieldsMap: Record<string, string> = {},
): Promise<any[] | false> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = function (e) {
const data = (e.target as any)?.result
const workbook = XLSX.read(data, {
type: 'binary',
raw: true,
cellNF: true,
})
const sheetName = workbook.SheetNames[sheetIndex]
const worksheet = workbook.Sheets[sheetName]
// 获取 A 列的所有数据,跳过第一行(标题行)
const columnData = []
const range = XLSX.utils.decode_range(worksheet['!ref'] as string)
// 从第2行开始(索引为1,跳过标题行)
for (let row = range.s.r + 1; row <= range.e.r; row++) {
const res: Record<string, any> = {}
columns.forEach((col: string) => {
const columnIndex = XLSX.utils.decode_col(col) // 将列字母转换为索引
const cellAddress = XLSX.utils.encode_cell({
r: row,
c: columnIndex,
}) // A列是第0列
const cell = worksheet[cellAddress]
const field = fieldsMap[col] ?? col
res[field] = cell ? cell.v : ''
})
columnData.push(res)
}
resolve(columnData)
}
reader.onerror = function () {
resolve(false)
}
})
}
/**
* @desc 从工作表中获取指定列的数据
*
* 此函数从Excel工作表中获取指定列的所有数据(跳过标题行)
*
* @param {XLSX.WorkSheet} worksheet - Excel工作表对象
* @param {string} column - 要获取数据的列(如'A'、'B')
* @returns {any[]} 返回指定列的数据数组
*/
function getColumnData(worksheet: XLSX.WorkSheet, column: string) {
const columnData = []
const range = XLSX.utils.decode_range(worksheet['!ref'] as string)
const columnIndex = XLSX.utils.decode_col(column) // 将列字母转换为索引
for (let row = range.s.r + 1; row <= range.e.r; row++) {
const cellAddress = XLSX.utils.encode_cell({ r: row, c: columnIndex })
const cell = worksheet[cellAddress]
columnData.push(cell ? cell.v : null)
}
return columnData
}
/**
* @desc 从本地文件读取Excel工作簿
*
* 此函数从File或Blob对象读取Excel文件并返回工作簿对象
*
* @param {File | Blob} file - 要读取的Excel文件
* @param {XLSX.ParsingOptions} [options] - 解析选项
* @returns {Promise<WorkBook | false>} 返回工作簿对象或false(读取失败时)
*/
function readWorkbookFromFile(
file: File | Blob,
options: XLSX.ParsingOptions = {
type: 'binary',
raw: true,
cellNF: true,
},
): Promise<WorkBook | false> {
return new Promise((resolve) => {
const reader = new FileReader()
reader.readAsBinaryString(file)
reader.onload = function (e) {
const data = (e.target as any)?.result
const workbook = XLSX.read(data, options)
resolve(workbook)
}
})
}
/**
* @desc 从URL读取Excel工作簿
*
* 此函数从URL读取Excel文件并返回工作簿对象
*
* @param {string} url - Excel文件的URL地址
* @param {XLSX.ParsingOptions} [options] - 解析选项
* @returns {WorkBook} 返回工作簿对象
*/
function readWorkbookFromUrl(
url: string,
options: XLSX.ParsingOptions = {
type: 'string',
raw: true,
cellNF: true,
},
): WorkBook {
const workbook = XLSX.readFile(url, options)
return workbook
}
/**
* @desc 从URL读取Excel工作簿
*
* 此函数从URL读取Excel文件并返回工作簿对象
*
* @param {string} url - Excel文件的URL地址
* @param {XLSX.ParsingOptions} [options] - 解析选项
* @returns {WorkBook} 返回工作簿对象
*/
async function readWorkbookFromUrl1(url: string): Promise<WorkBook | false> {
try {
const res = await fetch(url)
const blob = await res.blob()
const wb = readWorkbookFromFile(new Blob([blob]))
return Promise.resolve(wb)
} catch {
return Promise.resolve(false)
}
}
/**
* @desc 保存工作簿为Excel文件
*
* 此函数将工作簿对象保存为Excel文件
*
* @param {WorkBook} workbook - 要保存的工作簿对象
* @param {string} fileName - 文件名
* @param {XLSX.WritingOptions} [options] - 写入选项
* @returns {void}
*/
function saveWbToExcel(
workbook: WorkBook,
fileName: string,
options?: XLSX.WritingOptions,
) {
XLSX.writeFile(workbook, fileName, options)
}
return {
exportJson2Excel,
exportAoa2Excel,
exportBuffer2Excel,
exportJson2ExcelSheets,
exportUrl2Excel,
readWorkbookFromLocalFile,
readFileToJson,
readFileToJsons,
readColumnFromSheet,
getColumnData,
readWorkbookFromFile,
readWorkbookFromUrl,
readWorkbookFromUrl1,
saveWbToExcel,
}
}
export default useExcel
// 使用
const {
exportJson2Excel,
readFileToJson,
readFileToJsons,
readColumnFromSheet,
readWorkbookFromUrl,
readWorkbookFromUrl1,
readWorkbookFromFile,
saveWbToExcel,
} = useExcel()
useSet
ts
import { useMemo, useState } from 'react'
// 定义操作方法的接口
interface Actions<T> {
add: (key: T) => void // 新增一条数据
set: (map: Set<T>) => void // 设置全部数据
remove: (key: T) => void // 删除某条数据
clear: () => void // 清空数据
reset: () => void // 重置为默认值
}
function useSet<T>(defaultValue: Set<T> = new Set()): [Set<T>, Actions<T>] {
const [sets, setSets] = useState<Set<T>>(defaultValue)
// 缓存操作方法避免重复创建
const actions = useMemo(() => {
// 新增数据
const add = (value: T) => {
const newSet = new Set(sets)
newSet.add(value)
setSets(newSet)
}
// 设置全部数据
const set = (sets: Set<T>) => {
setSets(sets)
}
// 删除数据
const remove = (key: T) => {
const newSet = new Set(sets)
newSet.delete(key)
setSets(newSet)
}
// 清空数据
const clear = () => {
const newSet = new Set<T>()
setSets(newSet)
}
// 重置为默认值
const reset = () => {
setSets(defaultValue)
}
return { add, set, remove, clear, reset }
}, [defaultValue, sets])
return [sets, actions] as const
}
export default useSet
useMap
ts
import { useMemo, useState } from 'react'
// 定义操作方法的接口
interface Actions<T, U> {
add: (key: T, value: U) => void // 新增一条数据
get: (key: T) => U | undefined // 获取一条数据
set: (map: Map<T, U>) => void // 设置全部数据
remove: (key: T) => void // 删除某条数据
clear: () => void // 清空数据
reset: () => void // 重置为默认值
}
function useMap<T, U>(
defaultValue: Map<T, U> = new Map(),
): [Map<T, U>, Actions<T, U>] {
const [map, setMap] = useState<Map<T, U>>(defaultValue)
// 缓存操作方法避免重复创建
const actions = useMemo(() => {
// 新增数据
const add = (key: T, value: U) => {
const newMap = new Map(map)
newMap.set(key, value)
setMap(newMap)
}
// 获取数据
const get = (key: T) => {
return map.get(key)
}
// 设置全部数据
const set = (map: Map<T, U>) => {
setMap(map)
}
// 删除数据
const remove = (key: T) => {
const newMap = new Map(map)
newMap.delete(key)
setMap(newMap)
}
// 清空数据
const clear = () => {
const newMap = new Map(map)
newMap.clear()
setMap(newMap)
}
// 重置为默认值
const reset = () => {
setMap(defaultValue)
}
return { add, get, set, remove, clear, reset }
}, [defaultValue, map])
return [map, actions] as const
}
export default useMap
usePrevious
ts
import { useRef } from 'react'
type ShouldUpdateFn<T> = (prev: T | undefined, next: T) => boolean
const shouldUpdate = <T>(prev: T | undefined, next: T) => !Object.is(prev, next)
export default function usePrevious<T>(
state: T,
shouldUp: ShouldUpdateFn<T> = shouldUpdate,
): T | undefined {
const curRef = useRef<T>() // 当前值
const preRef = useRef<T>() // 上一个值
if (shouldUp(curRef.current, state)) {
preRef.current = curRef.current
curRef.current = state
}
return preRef.current
}
useLatest
ts
import { RefObject, useRef } from 'react'
// 获取某个state的最新值的hook
export default function useLatest<S>(value: S): RefObject<S> {
// useRef 保存能保证每次获取到的都是最新的值
const curRef = useRef<S>(value)
curRef.current = value
return curRef
}
useSafeState
ts
import { useUnmount } from 'ahooks'
import { Dispatch, SetStateAction, useRef, useState } from 'react'
// 主要是实现 setRafState 方法,在外部调用 setRafState 方法时,会取消上一次的 setState 回调函数,并执行 requestAnimationFrame 来控制 setState 的执行时机
export default function useSafeState<S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>] {
const [state, setState] = useState(initialState)
const reqId = useRef(0)
const setSafeState = (state: SetStateAction<S>) => {
cancelAnimationFrame(reqId.current)
reqId.current = requestAnimationFrame(() => {
// 在回调执行真正的 setState
setState(state)
})
}
useUnmount(() => {
cancelAnimationFrame(reqId.current)
})
return [state, setSafeState] as const
}
useSetState
ts
import { getTypeOf } from '@/utils'
import { SetStateAction, useCallback, useRef, useState } from 'react'
// 类似于以前的setState,有一个回调函数,回调函数的参数是最新的值
export default function useSetState<S>(
initialState: S | (() => S),
): [S, (state: SetStateAction<S>, cb?: (state: S) => void) => void] {
const [state, set] = useState(initialState)
const curRef = useRef<S>(state)
curRef.current = state
const setState = useCallback(
(state: SetStateAction<S>, cb?: (state: S) => void) => {
set((pre) => {
const newState =
getTypeOf(state) === 'Function'
? (state as (preState: S) => S)(pre)
: (state as S)
curRef.current = newState
return newState
})
cb?.(curRef.current)
},
[],
)
return [state, setState] as const
}
useMergeStates
ts
import { getTypeOf } from '@/utils'
import { Dispatch, SetStateAction, useCallback, useState } from 'react'
// 合并state属性,使用的时候不用整体重新赋值
export default function useMergeStates<S extends Record<string, any>>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<Partial<S>>>] {
const [state, set] = useState(initialState)
const setState = useCallback((patch: SetStateAction<Partial<S>>) => {
set((pre) => {
const newState =
getTypeOf(patch) === 'Function'
? (patch as (preState: Partial<S>) => S)(pre)
: (patch as S)
return newState ? { ...pre, ...newState } : pre
})
}, [])
return [state, setState] as const
}
useGetState
ts
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'
// 在useState的基础上增加获取方法
export default function useGetState<S>(
initialState: S | (() => S),
): [S, Dispatch<SetStateAction<S>>, () => S] {
const [state, setState] = useState(initialState)
const curRef = useRef(state)
curRef.current = state
const getState = useCallback(() => curRef.current, [])
return [state, setState, getState] as const
}
useReactive
ts
import { useRef } from 'react'
import useToggle from './useToggle'
// 响应式对象
export default function useReactive<S extends object>(initialState: S): S {
const [, { toggle }] = useToggle(false)
const curRef = useRef(initialState)
const proxy = new Proxy(curRef.current, {
set(target, key, value) {
const ret = Reflect.set(target, key, value)
toggle() // 属性赋值时触发回调
return ret
},
get(target, key) {
const ret = Reflect.get(target, key)
return ret
},
defineProperty(target, key) {
const ret = Reflect.deleteProperty(target, key)
toggle() // 属性删除时触发回调
return ret
},
})
return proxy
}
useOptions
ts
import { DefaultOptionType } from 'antd/lib/select'
import { useEffect, useState } from 'react'
// 账号列表项
export type UseOptionsProps<T = DefaultOptionType, R = any> = {
labelField?: keyof R // 要取的作为label的字段
labelFormat?: (record: R) => any // label字段自定义格式化函数
valueField?: keyof R // 要取的作为value的字段
valueFormat?: (record: R) => any // value字段自定义格式化函数
resField?: string[] // 要取的返回值字段(比如res、list、resutl等)
dataFn: (...args: any) => Promise<any> // 要请求的接口
fnParams?: any // 请求的接口参数
definedFormat?: (data: R[]) => T[] // 自定义格式化数据函数
}
/**
* @desc 自定义获取任意接口并格式化为下拉options的hook
* @param T 最终返回的数据的类型
* @param R 接口返回的数据的类型
*/
function useOptions<T = DefaultOptionType, R = any>(
props: UseOptionsProps<T, R>,
): [T[], R[], () => void] {
const {
labelField,
valueField,
labelFormat,
valueFormat,
fnParams,
dataFn,
resField,
definedFormat,
} = props || {}
const [options, setOptions] = useState<T[]>([]) // 下拉选项
const [datas, setDatas] = useState<R[]>([]) // 原始数据
/** 格式化为通用下拉选项结构 */
const formatToCommonOptions = (list: R[]): T[] => {
return list?.map((item: R) => {
const label = labelFormat
? labelFormat?.(item)
: labelField
? item?.[labelField]
: item?.['name' as keyof R] || '' // label取值
const value = valueFormat
? valueFormat?.(item)
: valueField
? item?.[valueField]
: item?.['id' as keyof R] || '' // value取值
return { label, value } as T
})
}
// 获取数据
const getDatas = async () => {
try {
const res = fnParams ? await dataFn(fnParams) : await dataFn()
const list: R[] =
resField && resField?.length
? resField.reduce((pre, cur) => pre?.[cur], res)
: res
if (list && Array.isArray(list)) {
setDatas(list) // 保存原始数据
setOptions(
definedFormat ? definedFormat(list) : formatToCommonOptions(list),
) // 保存格式化数据
}
} catch {
//
}
}
useEffect(() => {
getDatas()
}, [])
return [options, datas, getDatas]
}
export default useOptions
useSse
ts
import { useEffect, useRef, useState } from 'react'
// 定时器的类型
type TimerType = ReturnType<typeof setTimeout> | undefined
/**
* @description SSE暴露的方法
*/
export interface SSEMethods {
close: () => void
reconnect: () => void
}
/**
* SSE连接状态枚举
*/
export enum SSEReadyState {
CONNECTING, // 连接中
OPEN, // 已连接
CLOSED, // 已关闭
}
/**
* @description 初始化参数
*/
export type EventSourceInit = {
withCredentials?: boolean
headers?: Record<string, string>
payload?: string
}
/**
* @description SSE连接需要的参数
* @param T 当前SSE连接的数据结构
*/
export interface SSEProps {
events?: string[] // 事件名称
retryInterval?: number // 重试间隔
heartbeatTimeout?: number // 心跳超时时间
init?: EventSourceInit // 初始化参数
onMessage?: (event: MessageEvent) => void // 消息回调
autoConnect?: boolean // 是否自动连接
maxRetries?: number // 最大重试次数
}
/**
* @description SSE连接返回结构
* @param T 当前SSE连接的数据结构
*/
export interface SSEReturn<T> {
data: T | null // 当前SSE连接的数据结构
error: Error | null // 错误
readyState: SSEReadyState // 连接状态
methods: SSEMethods // 暴露的方法
retryCount: number // 当前重试次数
}
/**
* @description SSE连接状态枚举
*/
export const READY_STATE_MAP = {
[SSEReadyState.CONNECTING]: '连接中...',
[SSEReadyState.OPEN]: '已连接',
[SSEReadyState.CLOSED]: '已关闭',
}
/**
* @description SSE连接状态枚举
*/
export const READY_STATE_TYPE_MAP = {
[SSEReadyState.CONNECTING]: 'warning',
[SSEReadyState.OPEN]: 'success',
[SSEReadyState.CLOSED]: 'error',
}
/**
* @description 通用的SSE连接HOOK
* @param {SSEProps} 连接参数
*/
// 连接地址
const useSse = <T,>(url: string, options: SSEProps): SSEReturn<T> => {
const {
autoConnect,
maxRetries,
retryInterval = 5 * 1000,
heartbeatTimeout,
init,
events,
onMessage,
} = options || {}
const [readyState, setReadyState] = useState<SSEReadyState>(
SSEReadyState.CONNECTING,
) // 当前连接状态
const [data, setData] = useState<T | null>(null) // 当前SSE数据
const [error, setError] = useState<Error | null>(null) // 错误
const sseRef = useRef<EventSource | null>(null) // SSE实例
const retryTimer = useRef<TimerType | null>(null) // 重试的定时器
const heartbeatTimer = useRef<TimerType | null>(null) // 心跳的定时器
const retryCount = useRef<number>(0) // 当前重试次数
// 初始化SSE
const initSSE = () => {
try {
const sseInstance = new EventSource(url, init)
sseRef.current = sseInstance
// sse连接打开事件
sseInstance.onopen = () => {
console.log('SSE连接已打开')
setReadyState(SSEReadyState.OPEN)
retryCount.current = 0 // 重置重试次数
startHeatbeat() // 开始心跳
bindEvents()
}
sseInstance.onerror = (e) => {
setError(new Error(`SSE Error: ${e.type}`))
setReadyState(SSEReadyState.CLOSED)
if (autoConnect !== false) reconnect()
}
sseInstance.onmessage = handleMessage
} catch (error) {
setError(error as Error)
setReadyState(SSEReadyState.CLOSED)
if (autoConnect !== false) reconnect()
}
}
// 消息处理事件
const handleMessage = (event: MessageEvent) => {
const parsedData = JSON.parse(event.data) as T
setData(parsedData)
onMessage?.(event)
}
// 重置重试的定时器
const resetRetryTimer = () => {
if (retryTimer.current) {
clearTimeout(retryTimer.current)
retryTimer.current = null
}
}
// 重置心跳的定时器
const resetHeartbeatTimer = () => {
if (heartbeatTimer.current) {
clearTimeout(heartbeatTimer.current)
heartbeatTimer.current = null
}
}
// 重连SSE
const reconnect = () => {
// 超过最大重试次数就不重试了
if (maxRetries && retryCount.current >= maxRetries) {
setReadyState(SSEReadyState.CLOSED)
return
}
resetRetryTimer()
resetHeartbeatTimer()
retryTimer.current = setTimeout(() => {
setReadyState(SSEReadyState.CONNECTING)
retryCount.current += 1 // 重试次数+1
initSSE()
}, retryInterval * (retryCount.current + 1))
}
// 开始心跳
const startHeatbeat = () => {
if (heartbeatTimeout) {
resetHeartbeatTimer()
heartbeatTimer.current = setTimeout(() => {
setReadyState(SSEReadyState.CLOSED)
disconnectSSE()
initSSE()
}, heartbeatTimeout)
}
}
// 绑定自定义事件
const bindEvents = () => {
if (sseRef && events?.length) {
events?.forEach((eventName) => {
;(sseRef as unknown as EventSource).addEventListener(
eventName,
handleMessage,
)
})
}
}
// 解绑自定义事件
const unbindEvents = () => {
if (sseRef && events?.length) {
events?.forEach((eventName) => {
;(sseRef as unknown as EventSource).removeEventListener(
eventName,
handleMessage,
)
})
}
}
useEffect(() => {
// 如果是自动连接
if (autoConnect !== false) {
initSSE()
}
return close
}, [])
// 断开SSE连接
const disconnectSSE = () => {
const sseInstance = sseRef.current
if (sseInstance) {
sseInstance.close()
sseRef.current = null
}
}
// 关闭
const close = () => {
disconnectSSE()
setReadyState(SSEReadyState.CLOSED)
resetRetryTimer()
resetHeartbeatTimer()
retryCount.current = 0
setData(null)
unbindEvents()
}
const methods: SSEMethods = {
close,
reconnect: () => {
close()
initSSE()
},
}
return { data, readyState, retryCount: retryCount.current, error, methods }
}
export default useSse
useWebSocket
ts
import { HeartBeatType } from '@/enums/common'
import { useCallback, useMemo, useRef, useState } from 'react'
// 心跳类型枚举
enum HeartBeatType {
Ping = 'heartBeatPing', // 心跳发送的消息
Pong = 'heartBeatPong', // 心跳接收的消息
}
type TimerType = ReturnType<typeof setTimeout> | undefined // 定时器的类型
// useWebSocket的参数类型
export interface IWebSocketProps {
url?: string // socket连接地址
heartBeatInterval?: number // 心跳检测间隔
heartBeatData?: HeartBeatType // 心跳检测发送数据
heartBeatSendData?: any // 心跳检测实际发送数据
maxReconnectAttempts?: number // 最大重连次数
reconnectInterval?: number // 重连间隔
}
// 发送数据的类型
export type SendData = string | ArrayBufferLike | Blob | ArrayBufferView
/**
* @description WebSocket hook封装
*/
export default function useWebSocket(options: IWebSocketProps) {
const {
url,
heartBeatInterval = 60 * 1000,
heartBeatData = HeartBeatType.Ping,
heartBeatSendData,
maxReconnectAttempts = 10,
reconnectInterval = 5 * 1000,
} = options
const socket = useRef<WebSocket>() // socket实例
const [lastMessage, setLastMessage] = useState<MessageEvent>() // socket消息
const reconnectAttempts = useRef<number>(0) // 已经尝试重连了的次数
const error = useRef<Event>() // error
const curUrlRef = useRef<string>() // curUrlRef
let heartBeatTimer: TimerType = undefined // 心跳检测的定时器
let reconnectTimer: TimerType = undefined // 重连的定时器
// 是否已连接
const isConnected = useMemo(
() => socket.current?.readyState === WebSocket.OPEN,
[socket],
)
// 连接
const connect = useCallback(
(curUrl: string) => {
// 如果已存在 WebSocket 实例,则先关闭它
if (socket) {
socket.current?.close()
}
if (curUrl && !curUrlRef.current) {
curUrlRef.current = curUrl
}
const socketInstance = new WebSocket(curUrl || url!)
socket.current = socketInstance
socketInstance.onopen = onopen
socketInstance.onmessage = onmessage
socketInstance.onclose = onclose
socketInstance.onerror = onerror
},
[url],
)
// 接收消息
const onmessage = (data: any) => {
setLastMessage(data)
}
// 连接成功
const onopen = () => {
console.log('连接成功')
// 重置重连次数
reconnectAttempts.current = 0
// 开始心跳检测
checkHealthStart()
}
// 连接断开
const onclose = () => {
console.log('连接断开')
// 结束心跳检测
checkHealthEnd()
}
// 连接出错
const onerror = (e: Event) => {
console.log('连接出错')
error.current = e
// 结束心跳检测
checkHealthEnd()
// 重连
reconnect()
}
// 断开连接
const disconnect = () => {
console.log('连接断开')
socket.current?.close()
// 结束心跳检测
checkHealthEnd()
}
// 发送消息
const sendMessage = (data: SendData) => {
console.log('即将发送的数据', data)
if (socket.current?.readyState === WebSocket.OPEN) {
console.log('真实发送的数据', data)
socket.current?.send(data)
}
}
// 心跳检测开始
const checkHealthStart = () => {
console.log('心跳检测开始')
checkHealthEnd()
heartBeatTimer = setTimeout(() => {
if (socket.current?.readyState === WebSocket.OPEN) {
sendMessage(
heartBeatSendData ||
JSON.stringify({
type: 'ping',
data: heartBeatData,
}),
)
checkHealthStart()
}
}, heartBeatInterval)
}
// 心跳检测结束
const checkHealthEnd = () => {
console.log('心跳检测结束')
clearTimeout(heartBeatTimer)
heartBeatTimer = undefined
}
// 重连
const reconnect = () => {
console.log('开始重连')
// 没有连接并且没有达到重连次数
if (
socket.current?.readyState !== WebSocket.OPEN &&
reconnectAttempts.current <= maxReconnectAttempts
) {
// 使用递增的延迟来避免频繁重连
reconnectTimer = setTimeout(() => {
connect(curUrlRef.current || url!)
}, reconnectInterval * reconnectAttempts.current + 1)
// 增加重连尝试次数
reconnectAttempts.current += 1
} else {
clearTimeout(reconnectTimer)
reconnectTimer = undefined
}
}
// 取消重连
const cancelReconnect = () => {
clearTimeout(reconnectTimer)
reconnectTimer = undefined
}
return {
socket,
lastMessage,
isConnected,
error,
connect,
disconnect,
reconnect,
checkHealthStart,
sendMessage,
cancelReconnect,
}
}
useLocalforage
ts
import localforage from 'localforage'
/**
* @desc localForage 是一个 JavaScript 库,通过简单类似 localStorage API 的异步存储来改进你的 Web 应用程序的离线体验。它能存储多种类型的数据,而不仅仅是字符串。
localForage 有一个优雅降级策略,若浏览器不支持 IndexedDB 或 WebSQL,则使用 localStorage。在所有主流浏览器中都可用:Chrome,Firefox,IE 和 Safari(包括 Safari Mobile)。
localForage 提供回调 API 同时也支持 ES6 Promises API,你可以自行选择。
* @return getItem 获取某个key的值
* @return setItem 设置某个key的值
* @return removeItem 移除某个key的值
* @return clearItems 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
* @return itemsLength 获取离线仓库中的 key 的数量(即数据仓库的"长度")
* @return key 根据 key 的索引获取其名
* @return keys 获取数据仓库中所有的 key,包含所有 key 名的数组
*/
const useLocalforage = <T = any>(): [
getItem: (key: string) => Promise<T | null>, // 获取某个key的值
setItem: (key: string, value: T) => Promise<T>, // 设置某个key的值
removeItem: (key: string) => Promise<boolean>, // 移除某个key的值
clearItems: () => Promise<boolean>, // 移除所有key的值,此方法将会删除离线仓库中的所有值。谨慎使用此方法。
itemsLength: () => Promise<number>, // 获取离线仓库中的 key 的数量(即数据仓库的"长度")
key: (index: number) => Promise<string | null>, // 根据 key 的索引获取其名
keys: () => Promise<string[]>, // 获取数据仓库中所有的 key,包含所有 key 名的数组
] => {
/**
* @desc 获取某个key的值
* @param {string} key 要获取的key
* @return {Promise<T | null>} 返回一个Promise,值的类型是T或者是null
*/
const getItem = (key: string): Promise<T | null> => {
return localforage
.getItem<T>(key)
.then((value) => Promise.resolve(value))
.catch((err) => Promise.reject(err))
}
/**
* @desc 设置某个key的值
* @param {string} key 要设置的key
* @param {T} value 要设置的key的值
* @return {Promise<T>} 返回一个Promise,值的类型是T
*/
const setItem = (key: string, value: T): Promise<T> => {
return localforage
.setItem<T>(key, value)
.then((value) => Promise.resolve(value))
.catch((err) => Promise.reject(err))
}
/**
* @desc 从离线仓库中删除 key 对应的值
* @param {string} key 要删除的key
* @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
*/
const removeItem = (key: string): Promise<boolean> => {
return localforage
.removeItem(key)
.then(() => Promise.resolve(true))
.catch(() => Promise.resolve(false))
}
/**
* @desc 从数据库中删除所有的 key,重置数据库.将会删除离线仓库中的所有值。谨慎使用此方法
* @return {Promise<Boolean>} 返回一个Promise,值的类型是Boolean,为了保证不阻塞代码执行,移除时报错会返回Promise<false>,各位大佬可以根据返回值来判断是否移除成功
*/
const clearItems = (): Promise<boolean> => {
return localforage
.clear()
.then(() => Promise.resolve(true))
.catch(() => Promise.resolve(false))
}
/**
* @desc 获取离线仓库中的 key 的数量(即数据仓库的"长度")
* @return {Promise<number>} 返回一个Promise,值的类型是number
*/
const itemsLength = (): Promise<number> => {
return localforage
.length()
.then((numberOfKeys) => Promise.resolve(numberOfKeys))
.catch((err) => Promise.reject(err))
}
/**
* @desc 根据 key 的索引获取其名
* @param {number} index 要删除的key
* @return {Promise<string>} 返回一个Promise,key名,值的类型是string
*/
const key = (index: number): Promise<string | null> => {
return localforage
.key(index)
.then((keyName) => Promise.resolve(keyName))
.catch((err) => Promise.reject(err))
}
/**
* @desc 获取数据仓库中所有的key名数组
* @return {Promise<string[]>} 返回一个Promise,key名数组,值的类型是string[]
*/
const keys = (): Promise<string[]> => {
return localforage
.keys()
.then((keys) => Promise.resolve(keys))
.catch((err) => Promise.reject(err))
}
return [getItem, setItem, removeItem, clearItems, itemsLength, key, keys]
}
export default useLocalforage