在鸿蒙应用开发过程中,为了提高代码复用性和可维护性,通常会将一些通用的功能封装到工具类中。本文将介绍几个常见的工具类及其方法,包括字符串、数组、对象处理、日期格式化等。
1. 字符串处理工具函数 String
1.1 根据指定的空格截取类型对字符串进行空格处理
typescript
/**
* 空格截取类型枚举,用于定义字符串中空格去除的方式。
*/
enum ESpaceTrimType {
ALL, // 去除所有空格(包括前、后、中间)
BEFORE, // 仅去除前面的空格
AFTER, // 仅去除后面的空格
BEFORE_AND_AFTER // 仅去除前后空格,保留中间空格
}
/**
* @param {string} str - 需要去除空格的原始字符串。
* @param {ESpaceTrimType} [trimType=ESpaceTrimType.BEFORE_AND_AFTER]
* 指定空格去除方式,默认为去除前后空格。
*
* @returns {string} 处理后的字符串,根据 trimType 移除了相应位置的空格。
*
* @example
* SpaceTrim(' Hello World ', ESpaceTrimType.ALL)
* // 返回 'HelloWorld'
*
* SpaceTrim(' Hello World ', ESpaceTrimType.BEFORE)
* // 返回 'Hello World '
*
* SpaceTrim(' Hello World ', ESpaceTrimType.AFTER)
* // 返回 ' Hello World'
*
* SpaceTrim(' Hello World ', ESpaceTrimType.BEFORE_AND_AFTER)
* // 返回 'Hello World'
*/
const SpaceTrim = (str: string, trimType: ESpaceTrimType = ESpaceTrimType.BEFORE_AND_AFTER): string => {
switch (trimType){
case ESpaceTrimType.ALL:
return str.replace(/\s+/g, '')
case ESpaceTrimType.BEFORE:
return str.replace(/(^\s*)/g, '')
case ESpaceTrimType.AFTER:
return str.replace(/(\s*$)/g, '')
case ESpaceTrimType.BEFORE_AND_AFTER:
return str.replace(/(^\s*)|(\s*$)/g, '')
}
}
1.2 对目标字符串或数字进行脱敏处理,保留前后位数,中间部分用星号(*)代替
typescript
/**
* 适用于手机号、身份证号、银行卡号等敏感信息的显示保护。
*
* @param {number | string} target - 需要脱敏的目标值,可以是数字或字符串形式的数字。
* @param {number} [prefixLength=3] - 保留的前缀字符数,默认为 3。
* @param {number} [suffixLength=4] - 保留的后缀字符数,默认为 4。
*
* @returns {string | number} 脱敏后的字符串;如果原始值为空或长度不足以脱敏,则返回原值。
*
* @example
* Desensitization('13800001111')
* // 返回 '138******1111'
*
* Desensitization(123456789, 2, 2)
* // 返回 '12****89'
*
* Desensitization('1234', 2, 3)
* // 返回 '1234' (因为总长度小于 prefixLength + suffixLength)
*/
const Desensitization = (target: number | string, prefixLength: number = 3, suffixLength: number = 4) => {
if (target) {
let targetStr = target.toString()
const prefix = targetStr.substring(0, prefixLength) // 前几位
const suffix = targetStr.substring(targetStr.length - suffixLength) // 后几位
if (targetStr.length > prefixLength + suffixLength) {
const maskedPart = '*'.repeat(targetStr.length - prefixLength - suffixLength) // 中间部分用 * 替换
return `${prefix}${maskedPart}${suffix}`
}
}
return target
}
1.3 生成一个模拟 UUID 的随机字符串(遵循 UUID v4 格式规范)
typescript
/**
* UUID v4 是基于随机数生成的唯一标识符,格式为:
* `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
* 其中:
* - 第3段以 `4` 开头(表示 UUID 版本 4)
* - 第4段以 `8`, `9`, `a`, 或 `b` 开头(符合 RFC 4122 规范)
*
* @returns {string} 返回一个模拟 UUID v4 的随机字符串。
*
* @example
* toAnyString()
* // 返回类似 '6a2e8f50-a219-4d7d-bf8c-0a9d3e1f7b5c'
*/
function toAnyString() {
const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
const r: number = (Math.random() * 16) | 0
const v: number = c === 'x' ? r : (r & 0x3) | 0x8
return v.toString()
})
return str
}
1.4 将字符串中每个单词的首字母转换为大写,其余字母保持小写
typescript
/**
* 适用于格式化标题、姓名、标签等需要首字母大写的场景。
*
* @param {string} str - 需要处理的原始字符串。
* @returns {string} 每个单词首字母大写的新字符串。
*
* @example
* firstUpperCase('hello world')
* // 返回 'Hello World'
*
* firstUpperCase('THIS IS A TEST')
* // 返回 'This Is A Test'
*/
function firstUpperCase(str: string) {
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
}
1.5 将十六进制颜色字符串(HEX)转换为 RGB 数值数组
typescript
/**
* 支持标准格式如 `#FF5500` 和简写格式如 `#FFF`。
* 转换后返回一个包含红、绿、蓝三个颜色通道数值的数组,每个数值范围为 0 到 255。
*
* @param {string} 【color】
* 十六进制颜色字符串。可以是 3 位(简写)或 6 位(标准),可选前缀 `#`。
* 示例:'#FF5500'、'FF5500'、'#ABC'、'ABC'
*
* @returns {number[]}
* 包含三个数字元素的数组,分别代表红(Red)、绿(Green)、蓝(Blue)通道的值。
* 示例:'#FF5500' → [255, 85, 0]、'#ABC' → [170, 187, 204]
*
* @throws {Error} 如果传入的颜色字符串不是 3 位或 6 位长度,则抛出错误。
*/
function getColorRGB(color: string): number[] {
// 去除可能的 # 并转为小写
let hex = color.replace(/^#/, '').toLowerCase();
// 检查是否为合法长度(3 或 6)
if (![3, 6].includes(hex.length)) {
throw new Error('Invalid hex color format. Expected 3 or 6 characters.');
}
// 如果是3位简写,扩展为6位(例如 'f' -> 'ff')
if (hex.length === 3) {
hex = hex.replace(/./g, match => match + match);
}
// 使用正则将字符串按每两位一组分割,并转换为十进制数值
const rgb = hex.match(/.{2}/g)?.map(channel => parseInt(`0x${channel}`)) || [];
return rgb;
}
1.6 将十六进制颜色值转换为 RGBA 字符串格式
typescript
/**
* @param {string} _color - 十六进制颜色字符串,如 '#FF5733' 或 '#F5A'
* @param {number} _opacity - 透明度(范围 0 到 1)
* @returns {string} 转换后的 RGBA 字符串,例如 'rgba(255, 87, 51, 0.5)'
* 如果输入无效,则返回原始字符串或空字符串
*/
const hexToRGBA = (_color: string, _opacity: number): string => {
let sColor = _color?.toLowerCase();
// 定义十六进制颜色的正则表达式,支持 3 位或 6 位格式
const reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
// 检查是否为有效的十六进制颜色格式
if (sColor && reg.test(sColor)) {
// 如果是简写的 3 位格式(如 #F5A),将其扩展为标准的 6 位格式(如 #FF55AA)
if (sColor.length === 4) {
let sColorNew = "#";
for (let i = 1; i < 4; i += 1) {
sColorNew += sColor.slice(i, i + 1).concat(sColor.slice(i, i + 1));
}
sColor = sColorNew;
}
// 处理标准的 6 位十六进制颜色值,提取 R、G、B 分量
const sColorChange: number[] = [];
for (let i = 1; i < 7; i += 2) {
// 将每个两位的十六进制值转换为十进制数
sColorChange.push(parseInt("0x" + sColor.slice(i, i + 2), 16));
}
// 构造并返回 RGBA 字符串
return `rgba(${sColorChange.join(", ")}, ${_opacity})`;
}
// 如果输入无效,返回原始颜色值
return sColor ?? '';
};
1.7 将对象参数转换为查询字符串并拼接到 URL 上
typescript
/**
* @param baseUrl - 基础 URL
* @param obj - 要转换为查询参数的对象
* @returns 拼接后的完整 URL
*
* 示例:
* const url = setObjToUrlParams("https://api.example.com", { a: 1, b: "hello" });
* // 结果: "https://api.example.com?a=1&b=hello"
*
* const urlWithParams = setObjToUrlParams("https://api.example.com?a=1", { b: "test" });
* // 结果: "https://api.example.com?a=1&b=test"
*/
function setObjToUrlParams(baseUrl: string, obj: object): string {
const params = Object.keys(obj).map(key => {
return `${key}=${encodeURIComponent(obj[key])}`;
}).join('&');
const separator = baseUrl.includes('?') ? '' : '?';
const cleanUrl = baseUrl.endsWith('?') || baseUrl.endsWith('&') ? baseUrl : baseUrl;
return `${cleanUrl}${separator}${params}`;
}
1.8 判断传入的字符串是否为 Base64 编码的图片数据 URI
typescript
/**
* 支持格式如:
* - data:image/png;base64,iVBORw0KG...
* - data:image/jpeg;base64,/9j/4AAQ...
*
* @param str - 需要判断的字符串
* @returns 如果是 Base64 格式的图片数据,返回 `true`;否则返回 `false`
*
* 示例:
* isBase64Image('data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...')
* // true
*
* isBase64Image('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z')
* // true
*
* isBase64Image('https://example.com/image.png')
* // false
*
* isBase64Image('not-a-base64-string')
* // false
*/
function isBase64Image(str: string): boolean {
// 判断字符串是否以 "data:image/" 开头并且包含 "base64"
const base64PrefixRegex = /^data:image/([a-zA-Z]*);base64,/;
return base64PrefixRegex.test(str);
}
1.9 移除 Base64 图片数据的前缀部分(如 "data:image/png;base64,")
typescript
/**
* @param base64String - 包含前缀的完整 Base64 图片字符串
* @returns 去除前缀后的纯 Base64 编码字符串
*
* ### 示例
* removeBase64Prefix('data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...')
* // 返回: 'iVBORw0KGgoAAAANSUhEUg...'
*
* removeBase64Prefix('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z')
* // 返回: '/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFQQMEDQsKDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwMDQwN/wAARCAABAAEDASIAAhEBAxEB/2gAMAwEAAhEDEQA/APf6ACiiigD//Z'
*
* removeBase64Prefix('iVBORw0KGgoAAAANSUhEUg...')
* // 返回: 'iVBORw0KGgoAAAANSUhEUg...'
*/
function removeBase64Prefix(base64String: string) {
const regex = /^data:image\/[^;]+;base64,/;
return base64String.replace(regex, "");
}
2. 数组|对象工具函数 Array | Object
2.1 根据指定的键对对象数组进行分组,返回一个以组合键为索引、对应元素数组为值的对象
typescript
/**
* @template T - 数组中每个元素的类型,必须是一个对象(object)
* @template K - 指定分组依据的键的类型,必须是 T 的 key
* @param {T[]} arr - 要分组的对象数组
* @param {K[]} keys - 用于分组的一个或多个键(属性名)
* @returns {Record<string, T[]>} 分组后的结果,键为组合键字符串,值为对应的元素数组
*
* @example
* const testData1 = [
* { name: 'Alice', age: 25, city: 'Beijing' },
* { name: 'Bob', age: 30, city: 'Shanghai' },
* { name: 'Charlie', age: 25, city: 'Beijing' }
* ];
*
* groupByKeys(testData1, ['city']);
* // 返回:
* // {
* // 'Beijing': [{ name: 'Alice', ... }, { name: 'Charlie', ... }],
* // 'Shanghai': [{ name: 'Bob', ... }]
* // }
*/
const groupByKeys = <T extends object, K extends keyof T>(arr: T[], keys: K[]): Record<string, T[]> => {
return arr.reduce<Record<string, T[]>>((acc, current) => {
const groupKey = keys.map((key) => Reflect.get(current, key) as T).join('-')
if (!acc[groupKey]) {
acc[groupKey] = []
}
acc[groupKey].push(current)
return acc
}, {} as Record<string, T[]>)
}
2.2 深拷贝函数,用于复制对象或数组及其嵌套结构,避免引用共享
typescript
/**
* @param {ESObject} obj - 要深拷贝的对象或数组
* @returns {ESObject} 返回一个与原对象结构相同但内存独立的新对象
*/
function deepCopy(obj: ESObject): ESObject {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let copy: ESObject;
if (Array.isArray(obj)) {
copy = [];
for (let i = 0; i < obj.length; i++) {
copy[i] = deepCopy(obj[i]);
}
} else {
copy = {};
for (let i = 0; i < obj.length(); i++) {
let key: ESObject = obj[i];
if (obj.hasOwnProperty(key)) {
copy[key] = deepCopy(obj[key]);
}
}
}
return copy;
}
2.3 对两个数组进行对比,删除数组 A 中与数组 B 相同的数据(根据指定的唯一标识字段)
typescript
/**
* @param dataA - 数组 A,从中移除匹配项
* @param dataB - 数组 B,包含需要匹配的项
* @param id - 用于比较的唯一标识字段名称(必须是对象中的键)
* @returns 过滤后的数组 A,不包含在数组 B 中出现过的数据
*
* @typeParam T - 泛型类型,表示数组中对象的类型
* @typeParam K - 泛型类型,表示用于比较的字段名,必须是 `T` 的键
*
* 示例:
* const arrayA = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
* const arrayB = [{ id: 2, name: 'Bob' }];
* const result = removeMatchingItemsById(arrayA, arrayB, 'id');
* // result => [{ id: 1, name: 'Alice' }]
*/
function removeMatchingItemsById<T extends Record<K, string | number>, K extends keyof T>(
dataA: T[],
dataB: T[],
id: K
): T[] {
if (!Array.isArray(dataA) || !Array.isArray(dataB)) {
throw new Error('dataA and dataB must be arrays');
}
const idsInB = new Set(dataB.map(item => item[id]));
return dataA.filter(item => !idsInB.has(item[id]));
}
3. 日期处理工具类 Date
3.1 获取当前时间戳(毫秒数)
typescript
/**
* 如果传入一个有效的 Date 对象,则返回该对象对应的时间戳;
* 否则返回当前系统时间的时间戳。
*
* @param {Date} [date] - 可选参数,一个有效的 Date 对象
* @returns {number} 表示时间戳的数字(单位:毫秒)
*
* @example
* getCurrentTimestamp(); // 返回当前时间戳,例如 1712345678901
* getCurrentTimestamp(new Date(2023, 0, 1)); // 返回 2023 年 1 月 1 日对应的时间戳
*/
function getCurrentTimestamp(date?: Date): number {
if (date instanceof Date && !isNaN(date.getTime())) {
return date.getTime();
}
return Date.now();
}
3.2 计算两个日期之间的天数差(向上取整)
typescript
/**
* @param {Date} startDate - 开始日期对象
* @param {Date} endDate - 结束日期对象
* @returns {number} 两个日期之间的天数差(向上取整)
*
* @example
* const start = new Date('2024-04-01');
* const end = new Date('2024-04-03');
* getDaysBetweenDates(start, end); // 返回 2
*/
function getDaysBetweenDates(startDate: Date, endDate: Date): number {
const diffTime = Math.abs(endDate.getTime() - startDate.getTime());
return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
}
3.3 计算两个日期之间的年数和剩余月份数,结果不进行四舍五入
typescript
interface YearsAndMonths {
years: number;
months: number;
}
/**
* @param {Date} startDate - 起始日期
* @param {Date} endDate - 结束日期
* @returns {YearsAndMonths} 返回包含年份差和剩余月份的对象
* @throws {Error} 如果传入的日期无效或结束日期早于开始日期
*
* @example
* const start = new Date('2020-05-15');
* const end = new Date('2023-07-20');
* CalculateYearsAndMonths(start, end); // 返回 { years: 3, months: 2 }
*/
const CalculateYearsAndMonths = (startDate: Date, endDate: Date) => {
const start = new Date(startDate);
const end = new Date(endDate);
// 校验是否为有效日期
if (isNaN(start.getTime()) || isNaN(end.getTime())) {
throw new Error('Invalid date input');
}
// 确保结束日期不早于开始日期
if (end < start) {
throw new Error('End date cannot be earlier than start date');
}
let years = end.getFullYear() - start.getFullYear();
let months = end.getMonth() - start.getMonth();
// 调整年份和月份:如果月份为负,则借位一年并加12个月
if (months < 0) {
years--;
months += 12;
}
return { years, months } as YearsAndMonths;
};
3.4 将时间格式化为指定字符串格式
typescript
interface TimeFormatObject {
y: number; // 年
m: number; // 月(1-12)
d: number; // 日
h: number; // 时
i: number; // 分
s: number; // 秒
a: number; // 星期几(0=周日,1=周一,... 6=周六)
}
/**
* 支持传入 Date 对象、时间戳(10位或13位)或日期字符串(如 '2024-01-01')
* 可自定义输出格式,支持年(y)、月(m)、日(d)、时(h)、分(i)、秒(s)、星期(a)等占位符
*
* 示例:
* parseTime(new Date(), '{y}-{m}-{d} {h}:{i}:{s}') => "2024-05-30 15:30:45"
* parseTime(1717029200, '{y}年{m}月{d}日 星期{a}') => "2024年5月30日 星期四"
*
* @param {Date | string | number} [time]
* 要格式化的时间,支持 Date / 时间戳 / 日期字符串
* @param {string} [cFormat]
*。 格式化模板,默认为 '{y}-{m}-{d} {h}:{i}:{s}'
* 支持字段:{y}年,{m}月,{d}日,{h}时,{i}分,{s}秒,{a}星期
* @returns {string} 格式化后的时间字符串,若时间无效则返回空字符串
*/
function parseTime<K extends keyof TimeFormatObject>(time: Date | string | number, cFormat?: string): string {
if (arguments.length === 0) {
return ''
}
if (time == null || time === 'null') {
return ''
}
let date: Date
if (time instanceof Date) {
date = time
} else {
if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
time = parseInt(time)
}
if (typeof time === 'number') {
const floorTime = Math.floor(time)
if (floorTime.toString().length === 10) {
time = time * 1000
}
}
date = new Date(time as number | string)
if (isNaN(date.getTime())) {
return ''
}
}
const format = cFormat || '{y}-{m}-{d} {h}:{i}:{s}'
const formatObj: TimeFormatObject = {
y: date.getFullYear(),
m: date.getMonth() + 1,
d: date.getDate(),
h: date.getHours(),
i: date.getMinutes(),
s: date.getSeconds(),
a: date.getDay()
}
const time_str = format.replace(/{(y|m|d|h|i|s|a)+}/g, (result, key: K) => {
const value = Reflect.get(formatObj, key) ?? ''
if (key === 'a') {
return ['日', '一', '二', '三', '四', '五', '六'][value as number] || ''
}
if (result.length > 0 && typeof value === 'number' && value < 10) {
return '0' + value
}
return value.toString()
})
return time_str
}
3.4.1 格式化时间戳为可读性更强的时间描述
parsedTime
需要用到上面封装的时间格式化工具函数
typescript
/**
* @param {number} 【time】 - 时间戳(秒或毫秒)。
* @param {string | undefined} 【option】
* 可自定义输出格式,支持年(y)、月(m)、日(d)、时(h)、分(i)、秒(s)、星期(a)等占位符
* @returns {string} - 格式化后的时间描述。
*
* 该函数首先检查时间戳的长度是否为10位(即是否是秒级时间戳),如果是,则将其转换为毫秒级时间戳。
* 然后创建一个 Date 对象,并计算当前时间和给定时间之间的差异(以秒为单位)。
* 根据时间差,返回不同的时间描述:
* - 如果时间差小于30秒,返回"刚刚"。
* - 如果时间差小于1小时,返回"X分钟前"。
* - 如果时间差小于24小时,返回"X小时前"。
* - 如果时间差小于2天,返回"1天前"。
* 如果提供了格式字符串,则使用 parseTime 函数按照指定格式返回时间。
* 否则,返回默认格式的时间字符串,包括月份、日期、小时和分钟。
*/
function formatTime(time: number, option?: string): string {
let parsedTime = time;
if (('' + parsedTime).length === 10) {
parsedTime = parseInt('' + parsedTime) * 1000;
} else {
parsedTime = +parsedTime;
}
const d = new Date(parsedTime);
const now = Date.now();
const diff = (now - d.getTime()) / 1000;
if (diff < 30) {
return '刚刚';
} else if (diff < 3600) {
// less 1 hour
return Math.ceil(diff / 60) + '分钟前';
} else if (diff < 3600 * 24) {
return Math.ceil(diff / 3600) + '小时前';
} else if (diff < 3600 * 24 * 2) {
return '1天前';
}
if (option) {
return parseTime(parsedTime, option);
} else {
return (
d.getMonth() +
1 +
'月' +
d.getDate() +
'日' +
d.getHours() +
'时' +
d.getMinutes() +
'分'
);
}
}
总结
以上是一些常用的工具类和方法,涵盖了字符串处理、日期格式化、数组、对象等方面。通过这些工具类的封装,可以大大提升开发效率并保持代码的一致性。在实际开发中,可以根据具体需求进一步扩展和完善这些工具类。