前端封装工具类,本质是把某一类通用逻辑(如日期、存储、请求、校验、DOM、URL、设备判断等)集中到一个独立模块里,对外暴露清晰、稳定、可复用的 API。
和"公共方法"相比,公共方法 更泛指所有可复用的函数;工具类 通常指按功能领域组织的一组工具函数或静态方法集合,比如 DateUtils、StorageUtils、RequestUtils。
现代前端更推荐:模块化 + 命名导出 + 纯函数优先 ,而不是传统 OOP 的 class 一把梭。下面以 Vue3 + TypeScript 项目为例,讲清楚怎么封装工具类。
一、先选封装形式
| 形式 | 写法 | 适用场景 | 推荐度 |
|---|---|---|---|
| 模块函数 | export function formatDate() {} |
绝大多数工具类 | 最高 |
| 对象字面量 | export const storage = { get, set } |
一组相关方法,如存储 | 高 |
| 类 + 静态方法 | class DateUtils { static format() {} } |
需要命名空间、链式调用 | 中 |
| 组合式函数 | export function usePagination() {} |
带响应式状态,Vue3 | 高 |
| 高阶函数 | debounce(fn, delay) |
防抖、节流、缓存 | 高 |
建议 :优先用模块函数和对象字面量,class 只在确实需要时用。
二、目录怎么组织
推荐按功能领域拆文件,统一放 src/utils:
text
src/
├── utils/
│ ├── index.ts # 统一出口
│ ├── date.ts # 日期工具
│ ├── string.ts # 字符串工具
│ ├── array.ts # 数组工具
│ ├── object.ts # 对象工具
│ ├── storage.ts # 本地存储
│ ├── request.ts # 请求封装
│ ├── validate.ts # 校验工具
│ ├── url.ts # URL 工具
│ ├── dom.ts # DOM 工具
│ ├── env.ts # 环境判断
│ ├── debounce.ts # 防抖
│ └── throttle.ts # 节流
├── hooks/ # Vue3 组合式函数
│ └── usePagination.ts
└── services/ # 接口服务
每个文件只负责一个领域,避免一个 utils.ts 塞几百个函数。
三、封装原则
- 单一职责:一个文件或一个函数只做一件事。
- 纯函数优先:相同输入得到相同输出,不依赖外部状态。
- 无副作用:除非是存储、请求、DOM 操作等本身就有副作用的工具。
- 输入输出明确:参数和返回值清晰,有 TypeScript 类型。
- 默认参数:降低调用成本。
- 错误处理:内部处理或抛出明确错误。
- 不污染全局 :不要挂到
window。 - 按需导出:支持 Tree-shaking。
- 可测试:容易写单元测试。
- 不耦合业务:工具类不应依赖具体页面或 store。
四、具体示例
1. 日期工具类
ts
// utils/date.ts
export function formatDate(
date: Date | string | number,
format = 'YYYY-MM-DD HH:mm:ss'
): string {
const d = new Date(date)
const map: Record<string, number> = {
YYYY: d.getFullYear(),
MM: d.getMonth() + 1,
DD: d.getDate(),
HH: d.getHours(),
mm: d.getMinutes(),
ss: d.getSeconds()
}
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, key => {
return String(map[key]).padStart(2, '0')
})
}
export function isToday(date: Date | string | number): boolean {
const d = new Date(date)
const now = new Date()
return (
d.getFullYear() === now.getFullYear() &&
d.getMonth() === now.getMonth() &&
d.getDate() === now.getDate()
)
}
使用:
ts
import { formatDate, isToday } from '@/utils/date'
formatDate(new Date(), 'YYYY-MM-DD') // 2026-10-15
2. 存储工具类
ts
// utils/storage.ts
const PREFIX = 'app_'
export const storage = {
set(key: string, value: any) {
localStorage.setItem(PREFIX + key, JSON.stringify(value))
},
get<T = any>(key: string): T | null {
const raw = localStorage.getItem(PREFIX + key)
if (!raw) return null
try {
return JSON.parse(raw) as T
} catch {
return null
}
},
remove(key: string) {
localStorage.removeItem(PREFIX + key)
},
clear() {
Object.keys(localStorage)
.filter(k => k.startsWith(PREFIX))
.forEach(k => localStorage.removeItem(k))
}
}
使用:
ts
import { storage } from '@/utils/storage'
storage.set('token', 'abc')
const token = storage.get<string>('token')
3. 请求工具类
ts
// utils/request.ts
import axios from 'axios'
import { storage } from './storage'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
request.interceptors.request.use(config => {
const token = storage.get<string>('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
request.interceptors.response.use(
res => res.data,
err => {
// 统一错误处理
return Promise.reject(err)
}
)
export default request
4. 校验工具类
ts
// utils/validate.ts
export const isPhone = (value: string): boolean => /^1[3-9]\d{9}$/.test(value)
export const isEmail = (value: string): boolean =>
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
export const isIdCard = (value: string): boolean =>
/(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(value)
5. 防抖与节流
ts
// utils/debounce.ts
export function debounce<T extends (...args: any[]) => void>(
fn: T,
delay = 300
) {
let timer: ReturnType<typeof setTimeout> | null = null
return function (this: any, ...args: Parameters<T>) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, args), delay)
}
}
ts
// utils/throttle.ts
export function throttle<T extends (...args: any[]) => void>(
fn: T,
interval = 300
) {
let last = 0
return function (this: any, ...args: Parameters<T>) {
const now = Date.now()
if (now - last >= interval) {
last = now
fn.apply(this, args)
}
}
}
6. 环境判断工具类
ts
// utils/env.ts
export const isIOS = () => /iPhone|iPad|iPod/i.test(navigator.userAgent)
export const isAndroid = () => /Android/i.test(navigator.userAgent)
export const isWechat = () => /MicroMessenger/i.test(navigator.userAgent)
export const isMobile = () => isIOS() || isAndroid()
7. URL 工具类
ts
// utils/url.ts
export function getQuery(name: string): string | null {
return new URLSearchParams(location.search).get(name)
}
export function buildQuery(params: Record<string, any>): string {
return Object.entries(params)
.filter(([, v]) => v !== undefined && v !== null)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&')
}
五、统一出口与按需导入
在 utils/index.ts 统一导出:
ts
export * from './date'
export * from './storage'
export * from './validate'
export * from './debounce'
export * from './throttle'
export * from './url'
export * from './env'
使用:
ts
import { formatDate, storage, isPhone } from '@/utils'
也可以按需导入:
ts
import { formatDate } from '@/utils/date'
建议:大型项目优先按需导入,减少打包体积;小型项目可以用统一出口。
六、TypeScript 类型支持
工具类一定要写类型:
ts
export function formatDate(
date: Date | string | number,
format?: string
): string
泛型:
ts
export function deepClone<T>(value: T): T
返回类型明确,调用方有提示,减少错误。
七、带响应式的工具:组合式函数
如果工具类需要响应式状态,应封装成 Vue3 的 composables 或 hooks:
ts
// hooks/usePagination.ts
import { ref } from 'vue'
export function usePagination(fetchFn: (params: any) => Promise<any>) {
const page = ref(1)
const pageSize = ref(10)
const total = ref(0)
const list = ref([])
const loading = ref(false)
async function load() {
loading.value = true
try {
const res = await fetchFn({ page: page.value, pageSize: pageSize.value })
list.value = res.data.list
total.value = res.data.total
} finally {
loading.value = false
}
}
return { page, pageSize, total, list, loading, load }
}
使用:
ts
const { list, loading, load } = usePagination(api.getUserList)
八、测试与文档
- 单元测试:用 Vitest 测试纯函数。
- JSDoc 注释:说明参数、返回值、示例。
- README :在
utils目录下写说明。 - 类型提示:TypeScript 本身就是最好的文档。
示例注释:
ts
/**
* 格式化日期
* @param date 日期对象、时间戳或字符串
* @param format 格式,默认 YYYY-MM-DD HH:mm:ss
* @returns 格式化后的字符串
* @example
* formatDate(new Date(), 'YYYY-MM-DD') // '2026-10-15'
*/
九、常见坑
- 把业务逻辑塞进工具类,导致工具类依赖具体业务。
- 直接挂到
window,污染全局。 - 不处理异常,调用方容易崩溃。
- 参数过多,没有默认值。
- 不写类型,调用方不知道传什么。
- 循环依赖:
a.ts引b.ts,b.ts又引a.ts。 - 过度封装,简单逻辑也包一层。
- 不写测试,改一处崩多处。
- 命名混乱,
utils1、common、helper混用。
十、总结
前端封装工具类的核心:
按领域拆模块,纯函数优先,类型明确,按需导出,写好测试和文档。
一个合格的工具类应该:
- 别人不看源码就会用
- 输入输出清晰
- 不依赖具体页面
- 容易测试
- 不会互相影响
- 能按需加载
从项目中最常重复的逻辑开始封装,逐步沉淀成自己的工具库。
如果逻辑带响应式,就封装成 Vue3 的组合式函数;如果纯逻辑,就放 utils 里按模块导出。