通用工具模块封装:防抖节流、参数校验公共工具类
项目写久了会发现一个问题:各个页面都在写重复的工具方法。输入框要防抖,每个页面自己写一遍;手机号校验,每个页面复制一段正则;判断空对象、格式化日期,到处都是类似的代码。改一个小地方要翻遍整个项目,漏改一处就出bug。
正确的做法是把这些通用逻辑抽成独立的工具模块,全项目统一调用。本文基于鸿蒙ArkTS,讲清楚防抖、节流、参数校验这些常用工具怎么封装,代码可以直接复制到项目里用。
01 为什么要封装通用工具
先说说到处复制粘贴的坏处。
**代码重复,维护困难。**同一个手机号校验,10个页面写了10份。哪天正则要改,改了9处漏了1处,那个页面就出问题。
**风格不统一。**A同学写的防抖是300毫秒,B同学写的是500毫秒,同一个项目里输入框体验不一致。
**新人上手慢。**新同学不知道项目里已经有这些工具,又自己写一套,越积越多。
抽成统一的工具模块之后,所有页面调用同一个方法,改一处全项目生效。新人来了看一眼工具目录就知道有哪些能力,不用到处找代码。
工具模块的原则很简单:纯函数、无状态、可复用。输入确定,输出就确定,不依赖页面上下文,不修改外部状态。这样的函数放哪里都能跑,不会出副作用。
02 防抖Debounce实现
防抖是最常用的工具之一。搜索框输入的时候,每敲一个字就发一次请求,太浪费了。防抖的思路是:停止输入一段时间之后,才真正执行一次。比如输入完500毫秒之后再搜,中间连续输入就一直重置计时。
// 防抖:停止触发wait毫秒后才执行
function debounce(func: Function, wait: number = 300): Function {
let timerId: number = -1;
return (...args: any[]) => {
// 每次调用都清掉之前的定时器
if (timerId !== -1) {
clearTimeout(timerId);
}
// 重新计时
timerId = setTimeout(() => {
func.apply(this, args);
timerId = -1;
}, wait);
};
}
用的时候这样写:
@Entry
@Component
struct SearchPage {
@State keyword: string = '';
// 搜索请求,防抖500毫秒
private searchApi = debounce((kw: string) => {
console.info('发起搜索:', kw);
// 这里调接口
}, 500);
build() {
Column() {
TextInput({ placeholder: '搜索' })
.onChange((value: string) => {
this.keyword = value;
this.searchApi(value); // 连续输入不会重复请求
})
}
}
}
防抖的关键点:每次触发都清掉上一个定时器,重新开始计时。只有停下来不操作了,时间到了才真正执行。
除了搜索框,按钮防连点也可以用防抖。用户快速点两次提交按钮,只生效第一次,后面的都被过滤掉。
03 节流Throttle实现
节流和防抖不一样。防抖是"等停下来再执行",节流是"每隔一段时间最多执行一次"。比如页面滚动事件,滚动的时候持续触发,节流可以保证每200毫秒最多处理一次,不会卡。
// 节流:每隔wait毫秒最多执行一次
function throttle(func: Function, wait: number = 200): Function {
let lastTime: number = 0;
let timerId: number = -1;
return (...args: any[]) => {
const now = Date.now();
const remaining = wait - (now - lastTime);
if (remaining <= 0) {
// 距离上次执行已经超过间隔,直接执行
if (timerId !== -1) {
clearTimeout(timerId);
timerId = -1;
}
lastTime = now;
func.apply(this, args);
} else if (timerId === -1) {
// 还在等待期,设个定时器在最后执行一次
timerId = setTimeout(() => {
lastTime = Date.now();
timerId = -1;
func.apply(this, args);
}, remaining);
}
};
}
防抖和节流的区别:
-
防抖:等你停下来再执行,适合搜索、输入框
-
节流:固定频率执行,适合滚动、拖拽、窗口大小变化
比如页面滚动加载更多,用节流保证每200毫秒最多算一次位置,不然滚动一次触发几十次计算,性能直接掉下来。
04 参数校验工具类
表单提交的时候,手机号、邮箱、身份证、必填项,这些校验逻辑每个页面都要写。抽成统一的校验工具类,调用一行就行。
class ValidateUtil {
// 必填校验
static required(value: string, label: string = '该项'): string | null {
if (value === null || value === undefined || value.trim() === '') {
return `${label}不能为空`;
}
return null;
}
// 手机号校验
static phone(value: string): boolean {
const reg = /^1[3-9]\d{9}$/;
return reg.test(value);
}
// 邮箱校验
static email(value: string): boolean {
const reg = /^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
return reg.test(value);
}
// 身份证校验
static idCard(value: string): boolean {
const reg = /^\d{17}[\dXx]$/;
return reg.test(value);
}
// 数字范围校验
static numberRange(value: number, min: number, max: number): boolean {
return value >= min && value <= max;
}
// 长度校验
static length(value: string, min: number, max: number): boolean {
return value.length >= min && value.length <= max;
}
}
表单提交的时候这样用:
function submitForm(phone: string, email: string, age: number): string | null {
// 依次校验,有错误直接返回
const phoneError = ValidateUtil.required(phone, '手机号');
if (phoneError) return phoneError;
if (!ValidateUtil.phone(phone)) return '手机号格式不正确';
const emailError = ValidateUtil.required(email, '邮箱');
if (emailError) return emailError;
if (!ValidateUtil.email(email)) return '邮箱格式不正确';
if (!ValidateUtil.numberRange(age, 18, 100)) return '年龄需在18-100岁之间';
// 全部通过,提交
console.info('校验通过,提交表单');
return null;
}
校验逻辑集中在一个类里,以后要加新的校验规则,比如车牌号、银行卡号,直接在ValidateUtil里加方法就行。所有页面自动用最新的校验规则,不用一个个改。
05 其他常用工具
除了防抖节流和校验,还有几个工具方法几乎每个项目都用得上。
**空值判断。**判断一个对象是不是空的,数组是不是空的,字符串是不是全空格。
class ObjectUtil {
// 判断是否为空(null/undefined/空字符串/空数组/空对象)
static isEmpty(value: any): boolean {
if (value === null || value === undefined) return true;
if (typeof value === 'string') return value.trim() === '';
if (Array.isArray(value)) return value.length === 0;
if (typeof value === 'object') return Object.keys(value).length === 0;
return false;
}
// 判断是否不为空
static isNotEmpty(value: any): boolean {
return !this.isEmpty(value);
}
}
**日期格式化。**把时间戳转成"2024-01-15 14:30"这种格式。
class DateUtil {
// 时间戳转格式化字符串
static format(timestamp: number, format: string = 'YYYY-MM-DD HH:mm:ss'): string {
const date = new Date(timestamp);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds);
}
// 相对时间:"刚刚"、"5分钟前"、"昨天"
static fromNow(timestamp: number): string {
const diff = Date.now() - timestamp;
const minute = 60 * 1000;
const hour = 60 * minute;
const day = 24 * hour;
if (diff < minute) return '刚刚';
if (diff < hour) return `${Math.floor(diff / minute)}分钟前`;
if (diff < day) return `${Math.floor(diff / hour)}小时前`;
return this.format(timestamp, 'YYYY-MM-DD');
}
}
**深拷贝。**复制一个对象,修改副本不影响原对象。
function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') return obj;
return JSON.parse(JSON.stringify(obj));
}
这些小工具看起来简单,但到处都用得到。统一封装之后,不用每个页面自己写一遍。
06 完整工具模块组织
工具函数多了,要按类型分文件管理,不要全塞在一个文件里。项目结构大概是这样:
common/utils/
├── DebounceUtil.ets # 防抖节流
├── ValidateUtil.ets # 参数校验
├── ObjectUtil.ets # 对象/空值判断
├── DateUtil.ets # 日期格式化
├── StringUtil.ets # 字符串处理
├── DeviceUtil.ets # 设备信息
└── Index.ets # 统一导出
Index.ets里统一导出,外面import的时候只从一个入口进:
// common/utils/Index.ets
export { debounce, throttle } from './DebounceUtil';
export { ValidateUtil } from './ValidateUtil';
export { ObjectUtil } from './ObjectUtil';
export { DateUtil } from './DateUtil';
export { deepClone } from './ObjectUtil';
// 外面使用:
// import { debounce, ValidateUtil, DateUtil } from '../common/utils';
这样做的好处是:外面调用的人不用知道具体哪个文件里有什么,统一从Index.ets导出就行。以后工具类内部怎么拆分、怎么重命名,外面调用的代码都不用改。
落地注意事项
**工具函数要纯。**不要在工具函数里改全局变量、不要发网络请求、不要操作UI。纯函数输入输出,这样才好测试,不会出副作用。
**命名要清晰。**防抖就叫debounce,校验就叫validatePhone,不要起什么a、b、test这种名字。看函数名就知道干什么的,不用读实现。
**不要过度封装。**只有两三个地方用到的逻辑,不用急着抽工具类。等真的重复到第三处了,再抽出来。过早封装反而增加复杂度。
**加注释和用例。**复杂一点的工具函数,写清楚参数是什么、返回值是什么、用的时候要注意什么。不然过两个月自己都忘了这个函数是干嘛的。
小结
通用工具模块的核心就是:把重复的逻辑抽出来,全项目统一调用。防抖节流解决高频触发的性能问题,参数校验解决表单重复代码的问题,日期、空值判断这些小工具把常用逻辑集中管理。
实际项目里,工具模块不用一开始就做全。写代码的时候发现某个逻辑在第三个地方重复了,就把它抽出来。时间久了,自然就形成一套完整的工具库。新同学来了看一眼工具目录,就知道项目里有哪些现成能力可以用。
更多推荐




所有评论(0)