@sop-cli/utils
v0.11.1
Published
Business-specific utility functions, complement to lodash-es
Downloads
538
Maintainers
Readme
@sop-cli/utils
业务特定工具函数库,是对 lodash-es 的补充。TypeScript 严格类型、ESM/CJS/IIFE 多格式、支持 Monorepo 与 NPM 发布。
utils 定位
@sop-cli/utils 专注于业务特定的工具函数(树形转换、数据脱敏、金融计算、URL 处理等)。
对于通用工具函数(数组操作、对象操作、函数工具等),建议使用成熟的 lodash-es。
分层约定:领域金额 / 流水进
@sop-cli/utils/finance,通用数字留@sop-cli/utils/number,UI class 辅助留@sop-cli/utils/browser。
📚 文档导航
架构设计
- 🏗️ 架构决策:utils vs lodash-es - 为什么选择补充而非重复造轮子
- 📋 审核与重构方案 - 初始代码审查和完整重构计划
开发规范
- 🏛️ 封装原则(强制遵守) - 9大核心原则详解
最佳实践
- 🎯 核心理念 - DRY 原则的正确理解与最佳架构
- 💡 职责隔离 - 工具函数与业务解耦
- 🧪 单元测试 - 复杂工具编写测试用例
- 🧹 定期清理 - 避免 utils 变成“杂物堆”
- 🎯 场景选型 - ES6 vs lodash-es 的选择策略
- 👥 协作规范 - 多人开发的标准化流程
重构记录
- ✅ 重构完成总结 - DRY 原则精简重构最终报告
- 🎯 单一职责重构 - 基于 SRP 的模块化重构
- 📝 重构执行清单 - 详细的执行步骤和验证清单
- 📦 重构完成报告 - 第一阶段重构总结
- 🔧 初始重构方案 - 完整的类型系统和模块重构方案
优化指南
变更历史
- 📅 变更记录 - ITIL 规范的版本变更记录
🏛️ 封装原则(强制遵守)
1. 分层原则(核心)
严格拆分三层,职责隔离,互不侵入:
- 底层依赖层:lodash-es、原生 API、第三方轻量工具库(只引入,不修改)
- 通用适配层:对底层库二次导出、别名、简单参数兼容(无复杂逻辑)
- 业务工具层:项目专属逻辑,仅依赖上层,不反向侵入底层
// ❌ 错误:业务层直接侵入底层
import _ from 'lodash-es';
export function processUserData(users) {
return _.chain(users).filter(...).map(...).value(); // 业务逻辑混入
}
// ✅ 正确:分层清晰
// lodash.ts - 通用适配层
export { groupBy, sortBy } from 'lodash-es';
// user-utils.ts - 业务工具层
import { groupBy, sortBy } from './lodash';
export function groupUsersByDept(users) {
return groupBy(users, 'department');
}2. DRY 原则落地细则
- ✅ 相同逻辑只实现一次,全项目统一调用,禁止组件内重复写相同工具函数
- ✅ 函数复用粒度适中:通用逻辑抽 utils,页面独有逻辑留在组件 / hook
- ❌ 禁止在多个业务文件中重复引入、重复封装同一个第三方方法
// ❌ 错误:多处重复封装
// file-a.ts
const formatMoney = (val) => `¥${val.toFixed(2)}`;
// file-b.ts
const formatMoney = (val) => `¥${val.toFixed(2)}`; // 重复!
// ✅ 正确:统一抽取到 utils/number/format.ts
export function formatCurrency(amount: number): string {
return `¥${amount.toFixed(2)}`;
}3. 单一职责原则
- ✅ 一个函数只做一件事,函数名语义化,功能不耦合
- ✅ 一个模块只归类一类能力:类型判断、日期、存储、业务校验分开
// ❌ 错误:功能耦合
export function formatAndCheckDate(dateStr: string): string {
if (!isValid(dateStr)) throw new Error('Invalid date');
return formatDate(dateStr, 'YYYY-MM-DD');
}
// ✅ 正确:拆分为两个纯函数
export function isValidDate(dateStr: string): boolean {
return dayjs(dateStr).isValid();
}
export function formatDate(dateStr: string, fmt = 'YYYY-MM-DD'): string {
return dayjs(dateStr).format(fmt);
}4. 最小侵入 & 轻量化原则
- ✅ 不修改第三方库源码,只做导出、转发、简单适配
- ✅ 工具函数保持代码简短,避免过度设计、冗余分支
- ✅ 体积敏感场景(小程序 / 轻量 H5):优先原生 ES6 API,仅引入必要 lodash 方法
// ❌ 错误:过度封装
export function isEmpty(val: any): boolean {
if (val === null || val === undefined) return true;
if (Array.isArray(val)) return val.length === 0;
if (typeof val === 'string') return val.trim().length === 0;
if (typeof val === 'object') return Object.keys(val).length === 0;
return false;
}
// ✅ 正确:直接使用 lodash(已优化 Tree-shaking)
export { isEmpty } from 'lodash-es';
// ✅ 或者:简单场景用原生 API
export function isArrayEmpty(arr: unknown[]): boolean {
return arr.length === 0;
}5. 类型安全原则(TS 项目必守)
- ✅ 所有工具函数显式声明入参、返回值类型,不使用
any - ✅ 复杂结构使用 interface/type 定义
- ✅ 对入参做空值、类型校验,提前拦截非法入参
// ❌ 错误:缺少类型声明
export function sum(arr) {
return arr.reduce((a, b) => a + b, 0);
}
// ✅ 正确:完整类型声明 + 边界校验
export function sum(numbers: readonly number[]): number {
if (!Array.isArray(numbers)) {
throw new TypeError('Expected an array of numbers');
}
return numbers.reduce((acc, num) => {
if (typeof num !== 'number' || !Number.isFinite(num)) {
throw new TypeError(`Invalid number: ${num}`);
}
return acc + num;
}, 0);
}6. 边界兼容原则
- ✅ 主动处理
null/undefined/ 空字符串 / 空数组等边界值 - ✅ 对外抛出异常统一、提示友好,不静默报错
- ✅ 兼容项目多环境(开发 / 生产 / 测试)
// ❌ 错误:未处理边界情况
export function getUserName(user: User): string {
return user.profile.name.toUpperCase(); // 可能崩溃
}
// ✅ 正确:全面边界处理
export function getUserName(user: User | null | undefined): string {
if (!user?.profile?.name) {
console.warn('[getUserName] User name is missing');
return 'Unknown';
}
return user.profile.name.toUpperCase();
}7. 可扩展 & 可替换原则
- ✅ 统一入口导出,未来替换底层库(如 lodash → es-toolkit)只需改一处
- ✅ 预留扩展参数,不写死硬编码(如日期格式、脱敏规则)
// ❌ 错误:硬编码
export function maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); // 规则写死
}
// ✅ 正确:参数化配置
export interface MaskOptions {
visibleStart?: number;
visibleEnd?: number;
maskChar?: string;
}
export function maskPhone(
phone: string,
options: MaskOptions = {}
): string {
const { visibleStart = 3, visibleEnd = 4, maskChar = '*' } = options;
// ... 灵活实现
}8. 命名规范原则
- ✅ 函数:动宾结构
getXXX/formatXXX/checkXXX/useXXX(Hook) - ✅ 模块:按功能命名
date.ts/storage.ts/validate.ts/lodash.ts - ✅ 全局统一命名风格,禁止拼音、缩写歧义
// ❌ 错误:命名不规范
export function rq(url) { /* ... */ } // 缩写歧义
export function formatDateStr(d) { /* ... */ } // 参数名不明确
export function shoujihaoma_minguo(phone) { /* ... */ } // 拼音
// ✅ 正确:语义化命名
export function requestApi(url: string): Promise<Response> { /* ... */ }
export function formatDate(date: Date | string, pattern?: string): string { /* ... */ }
export function maskPhoneNumber(phone: string): string { /* ... */ }9. 无副作用原则
- ✅ 纯工具函数优先设计为纯函数:相同入参必然相同返回,不修改入参、不操作全局变量
- ✅ 涉及存储、请求、DOM 等副作用的函数单独归类,明确标注
// ✅ 纯函数:无副作用
export function add(a: number, b: number): number {
return a + b; // 相同输入永远得到相同输出
}
export function sortArray(arr: readonly number[]): number[] {
return [...arr].sort((a, b) => a - b); // 不修改原数组
}
// ⚠️ 副作用函数:明确标注
export function saveToLocalStorage(key: string, value: unknown): void {
localStorage.setItem(key, JSON.stringify(value)); // 修改外部状态
}
export async function fetchUserData(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`); // 网络请求
return response.json();
}💡 最佳实践
🎯 核心理念:DRY 原则的正确理解
DRY 原则 ≠ 不建 utils,而是建统一 utils 消灭重复代码
很多团队误以为 "不要重复造轮子" 就意味着不应该有自己的工具库,这是错误的理解。
❌ 错误理解
// 误区:因为有 lodash-es,所以不需要自己的 utils
// 结果:每个开发者在各自组件中重复实现相同的业务逻辑
// component-a.tsx
const formatMoney = (val) => `¥${val.toFixed(2)}`;
const maskPhone = (phone) => phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
// component-b.tsx
const formatMoney = (val) => `¥${val.toFixed(2)}`; // 重复!
const maskPhone = (phone) => phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2'); // 重复!
// component-c.tsx
const formatCurrency = (amount) => `¥${amount.toFixed(2)}`; // 又一个重复!✅ 正确理解
// 正确:建立统一的 utils 层,消灭项目内的重复代码
// utils/number/format.ts - 统一实现
export function formatCurrency(amount: number): string {
return `¥${amount.toFixed(2)}`;
}
// utils/string/mask.ts - 统一实现
export function maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 所有组件统一调用
import { formatCurrency, maskPhone } from '@sop-cli/utils';核心价值:
- 🎯 统一标准 - 全项目使用同一套实现,避免行为不一致
- 🎯 易于维护 - 修改逻辑只需改一处,所有调用方自动生效
- 🎯 减少 Bug - 消除因重复实现导致的细微差异和错误
- 🎯 提升效率 - 新人直接使用现有工具,无需重新实现
🏗️ 最佳架构:lodash-es + 轻量业务工具层
┌─────────────────────────────────────────────┐
│ 你的业务代码(Components) │
└──────────────┬──────────────────────────────┘
│ 导入
┌───────┴────────┐
│ │
┌──────▼──────┐ ┌─────▼──────────┐
│ lodash-es │ │ @sop-cli/utils │
│ (通用底层) │ │ (业务专属) │
└─────────────┘ └────────────────┘1️⃣ 通用底层能力 → 直接用 lodash-es
原则:绝不手写造轮,不重复造 lodash 已实现的功能
// ✅ 正确:直接使用 lodash-es
import {
debounce, // 防抖
throttle, // 节流
cloneDeep, // 深拷贝
groupBy, // 分组
omit, // 排除属性
pick, // 选择属性
uniqBy, // 去重
sortBy, // 排序
merge, // 深度合并
get, // 安全访问
set // 安全设置
} from 'lodash-es';
// ❌ 错误:自己实现 lodash 已有的功能
export function myDebounce(fn, delay) { /* ... */ } // 不要!
export function myCloneDeep(obj) { /* ... */ } // 不要!
export function myGroupBy(arr, key) { /* ... */ } // 不要!判断标准:
- ✅ lodash-es 是否已有成熟实现?
- ✅ 是否需要处理边界情况(循环引用、null 安全等)?
- ✅ 是否有性能优化(Tree-shaking、缓存等)?
答案都是 YES → 直接用 lodash-es
2️⃣ 业务专属能力 → 基于 ES6+TS 自己封装
原则:做到统一、无重复、类型安全
// ✅ 正确:业务特定的工具函数
// 树形转换(lodash 没有)
export function arrayToTree<T>(items: readonly T[], options?: TreeOptions): T[] {
// 业务特定的树形结构转换逻辑
}
// 数据脱敏(lodash 没有)
export function maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2');
}
// 金融计算(lodash 没有)
export function yuanToFen(yuan: string | number): number {
const num = Number(yuan);
return Math.round(num * 100);
}
// URL 参数清理(lodash 没有)
export function cleanQueryParams(params: Record<string, any>): Record<string, any> {
return Object.fromEntries(
Object.entries(params).filter(([_, v]) => v != null && v !== '')
);
}判断标准:
- ✅ 是否是业务特定的逻辑(树形转换、脱敏、金融计算)?
- ✅ 是否需要结合项目规范(特定的日期格式、脱敏规则)?
- ✅ 是否需要 TypeScript 强类型支持?
答案都是 YES → 封装到 @sop-cli/utils
🛡️ 四大核心原则
工具库坚持以下四大原则,确保代码质量:
1. 分层原则
- 底层依赖层:lodash-es、原生 API
- 通用适配层:二次导出、简单适配
- 业务工具层:项目专属逻辑
2. 单一职责
- 一个函数只做一件事
- 一个模块只归类一类能力
- 功能不耦合,易于测试
3. 类型安全
- 显式声明入参、返回值类型
- 不使用
any - 对入参做空值、类型校验
4. 纯函数设计
- 相同入参必然相同返回
- 不修改入参、不操作全局变量
- 无副作用,易于测试
📋 工程化保障
统一入口导出
// src/index.ts - 统一出口
export * from './array';
export * from './object';
export * from './string';
export * from './number';
// ... 所有模块
// 使用方只需记住一个包名
import { arrayToTree, maskPhone } from '@sop-cli/utils';规范命名
- 函数:动宾结构
getXXX/formatXXX/checkXXX - 模块:按功能命名
date.ts/storage.ts/validate.ts - 禁止拼音、缩写歧义
定期清理
- 每季度执行健康检查
- 删除 6 个月未使用的废弃函数
- 合并重复函数,避免 utils 臃肿腐化
1. 职责隔离:工具函数与业务解耦
核心原则:工具函数禁止耦合业务组件状态、UI 逻辑
// ❌ 错误:工具函数耦合 UI 逻辑
export function formatUserList(users: User[]) {
// 直接操作 DOM
const container = document.getElementById('user-list');
container.innerHTML = users.map(u => `<div>${u.name}</div>`).join('');
// 耦合组件状态
setState({ loading: false });
}
// ✅ 正确:纯数据处理,与 UI 无关
export function formatUserNames(users: User[]): string[] {
return users.map(user => user.name).filter(Boolean);
}
// UI 层调用
const names = formatUserNames(users);
renderUserList(names); // UI 逻辑在组件中处理检查清单:
- [ ] 函数是否访问了
document、window、localStorage? - [ ] 函数是否调用了 React/Vue 的状态更新方法?
- [ ] 函数是否依赖特定组件的 props 或 state?
- [ ] 函数的返回值是否只依赖输入参数?
2. 单元测试:保证迭代质量
复杂工具必须编写单元测试(Jest/Vitest)
// utils/number/calculate.ts
export function round(value: number | string, decimals = 2): number {
const num = toNumber(value);
const multiplier = Math.pow(10, decimals);
return Math.round(num * multiplier) / multiplier;
}
// utils/number/calculate.test.ts
import { describe, it, expect } from 'vitest';
import { round } from './calculate';
describe('round', () => {
it('should round to 2 decimal places by default', () => {
expect(round(3.14159)).toBe(3.14);
expect(round(2.675)).toBe(2.68);
});
it('should round to specified decimal places', () => {
expect(round(3.14159, 3)).toBe(3.142);
expect(round(3.14159, 0)).toBe(3);
});
it('should handle string input', () => {
expect(round('3.14159')).toBe(3.14);
expect(round('invalid')).toBe(0);
});
it('should handle edge cases', () => {
expect(round(NaN)).toBe(0);
expect(round(Infinity)).toBe(0);
expect(round(null)).toBe(0);
expect(round(undefined)).toBe(0);
});
});测试覆盖要求:
- ✅ 正常场景:典型输入的正确输出
- ✅ 边界场景:null、undefined、空值、极值
- ✅ 异常场景:非法输入的类型和格式
- ✅ 覆盖率目标:核心工具函数 ≥ 80%
3. 定期清理:避免 "杂物堆"
每季度执行一次工具库健康检查
清理清单
- [ ] 删除废弃函数:查找 6 个月未使用的函数
- [ ] 合并重复函数:识别功能相似的函数并合并
- [ ] 更新文档:确保 JSDoc 与实际代码一致
- [ ] 重构复杂函数:简化超过 50 行的函数
- [ ] 检查依赖:移除未使用的第三方库
检测方法
# 查找未使用的导出函数(需要配置 ESLint)
pnpm run lint -- --rule 'unused-imports/no-unused-vars: error'
# 统计函数复杂度
pnpm run complexity # 使用 eslint-complexity
# 检查重复代码
pnpm run duplicate # 使用 jscpd合并示例
// ❌ 清理前:多个相似函数
export function maskPhone(phone: string): string { /* ... */ }
export function maskEmail(email: string): string { /* ... */ }
export function maskIdCard(id: string): string { /* ... */ }
// ✅ 清理后:统一的脱敏函数
export interface MaskOptions {
type: 'phone' | 'email' | 'idCard';
visibleStart?: number;
visibleEnd?: number;
}
export function maskSensitiveData(data: string, options: MaskOptions): string {
// 统一实现
}4. 场景化选型:ES6 vs lodash-es
超轻量场景:优先原生 ES6 API
适用场景: 小程序、轻量 H5、性能敏感模块
// ✅ 简单数组操作 - 用原生 ES6
const doubled = arr.map(x => x * 2); // 替代 _.map
const filtered = arr.filter(x => x > 0); // 替代 _.filter
const found = arr.find(x => x.id === 1); // 替代 _.find
const flat = arr.flat(); // 替代 _.flatten
const unique = [...new Set(arr)]; // 替代 _.uniq
// ✅ 简单对象操作 - 用原生 ES6
const keys = Object.keys(obj); // 替代 _.keys
const values = Object.values(obj); // 替代 _.values
const entries = Object.entries(obj); // 替代 _.toPairs
const merged = { ...obj1, ...obj2 }; // 替代 _.assign (浅合并)
// ✅ 简单字符串操作 - 用原生 ES6
const trimmed = str.trim(); // 替代 _.trim
const upper = str.toUpperCase(); // 替代 _.toUpper
const includes = str.includes('test'); // 替代 _.includes判断标准:
- 操作是否简单(一行代码可完成)?
- 是否需要兼容旧浏览器(IE11)?
- 包体积是否敏感(< 50KB)?
大数据 & 深对象场景:优先 lodash-es
适用场景: 复杂数据处理、深度嵌套对象、性能优化
// ✅ 深度克隆 - lodash 处理循环引用
import { cloneDeep } from 'lodash-es';
const cloned = cloneDeep(complexObject); // 手写递归易出错
// ✅ 深度合并 - 保留嵌套结构
import { merge } from 'lodash-es';
const merged = merge({}, defaults, overrides); // 递归合并所有层级
// ✅ 链式调用 - 大数据流式处理
import { chain } from 'lodash-es';
const result = chain(users)
.filter(u => u.active)
.sortBy('name')
.groupBy('department')
.value();
// ✅ 防抖节流 - 成熟的事件处理
import { debounce, throttle } from 'lodash-es';
const debouncedSearch = debounce(searchApi, 300);
const throttledScroll = throttle(handleScroll, 100);
// ✅ 路径访问 - 安全访问深层嵌套
import { get, set } from 'lodash-es';
const name = get(user, 'profile.address.city', 'Unknown');
set(user, 'profile.status', 'active');判断标准:
- 是否需要深度操作(cloneDeep、merge)?
- 是否需要链式处理大量数据?
- 是否需要成熟的事件控制(debounce、throttle)?
- 是否需要安全的路径访问(get、set)?
5. 多人协作规范
5.1 模块化归类
新增工具函数必须按模块归类,不随意新增文件
// ✅ 正确:归入现有模块
// src/string/mask.ts
export function maskPhone(phone: string): string { /* ... */ }
export function maskEmail(email: string): string { /* ... */ }
// ❌ 错误:随意新建文件
// src/string/mask-phone.ts // 不应该
// src/string/mask-email.ts // 不应该模块归属规则:
| 功能类型 | 归属模块 | 示例 |
|---------|---------|------|
| 数组转换 | array/ | arrayToTree, sortBy |
| 对象清理 | object/ | cleanParams, omitKeys |
| 字符串脱敏 | string/ | maskPhone, trimAll |
| 数字格式化 | number/ | formatCurrency, yuanToFen |
| URL 处理 | network/ | parseQuery, getUrlParam |
| 类型判断 | is/ | isArray, isEmpty |
| JSON 安全 | json/ | safeJsonParse |
| 通用工具 | common/ | uuid, sleep |
5.2 JSDoc 注释规范
公共函数必须写 JSDoc 注释(用途、入参、返回值、示例)
/**
* 将平铺数组转换为树形结构
*
* @param items - 平铺数组,每个元素需包含 id 和 parentId
* @param options - 配置选项
* @param options.idField - ID 字段名,默认为 'id'
* @param options.parentIdField - 父ID 字段名,默认为 'parentId'
* @param options.childrenField - 子节点字段名,默认为 'children'
* @returns 树形结构数组
*
* @example
* ```typescript
* const flat = [
* { id: 1, parentId: null, name: 'Root' },
* { id: 2, parentId: 1, name: 'Child' }
* ];
* const tree = arrayToTree(flat);
* // [{ id: 1, children: [{ id: 2 }] }]
* ```
*
* @throws {TypeError} 当 items 不是数组时抛出
*/
export function arrayToTree<T extends Record<string, any>>(
items: readonly T[],
options: TreeOptions = {}
): T[] {
// ... 实现
}JSDoc 必需字段:
- ✅
@description- 功能描述(第一行) - ✅
@param- 每个参数的说明和类型 - ✅
@returns- 返回值说明 - ✅
@example- 至少一个使用示例 - ✅
@throws- 可能抛出的异常(如有)
5.3 复用优先原则
通用能力优先查现有 utils,确认无重复再新增
新增函数前的检查流程:
graph TD
A[需要新功能] --> B{搜索现有 utils}
B -->|找到类似函数| C[评估是否可直接使用]
B -->|未找到| D[确认真的需求]
C -->|可以| E[直接使用]
C -->|不可以| F[评估是否可扩展]
F -->|可以扩展| G[扩展现有函数]
F -->|不可扩展| H[新增函数]
D --> H
H --> I[选择正确模块]
I --> J[编写 JSDoc]
J --> K[编写单元测试]
K --> L[提交 PR]搜索方法:
# 1. 代码搜索
grep -r "function.*format" src/
grep -r "export.*mask" src/
# 2. IDE 全局搜索
# VS Code: Cmd+Shift+F (Mac) / Ctrl+Shift+F (Windows)
# 3. 查看模块索引
cat src/number/index.ts
cat src/string/index.ts5.4 禁止事项
❌ 严禁在 utils 中编写以下逻辑:
// ❌ 禁止:业务页面逻辑
export function loadUserPage() {
const userId = getUrlParam('id');
const user = await fetchUser(userId);
renderUserProfile(user); // 渲染逻辑不应在 utils
}
// ❌ 禁止:组件渲染逻辑
export function createUserCard(user: User) {
return (
<div className="user-card">
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
); // JSX 不应在 utils
}
// ❌ 禁止:状态管理逻辑
export function updateUserStore(user: User) {
store.dispatch({ type: 'UPDATE_USER', payload: user }); // Redux 逻辑不应在 utils
}
// ❌ 禁止:路由跳转逻辑
export function navigateToUserDetail(userId: string) {
router.push(`/users/${userId}`); // 路由逻辑不应在 utils
}✅ 正确做法:
// ✅ utils 只提供纯数据处理
export function formatUserData(user: User): FormattedUser {
return {
displayName: `${user.firstName} ${user.lastName}`,
maskedEmail: maskEmail(user.email),
createdAt: formatDate(user.createdAt)
};
}
// ✅ 业务逻辑在组件/hook 中
function UserProfile({ userId }: Props) {
const user = useFetchUser(userId); // Hook 处理请求
const formatted = formatUserData(user); // utils 处理格式化
return (
<div>
<h3>{formatted.displayName}</h3>
<p>{formatted.maskedEmail}</p>
</div>
); // 组件处理渲染
}特性
- ✅ 业务特定功能:树形转换、数据脱敏、金融计算、URL 处理、参数清理
- ✅ TypeScript 类型守卫:isArray, isString, isNumber 等
- ✅ 同构存储适配器:浏览器 + Node.js 自动降级
- ✅ 按模块按需导入,天然 Tree-Shaking
- ✅ 完整
.d.ts类型声明与 JSDoc - ✅ 零运行时依赖(lodash-es 为 peer dependency)
安装
pnpm add @sop-cli/utils
# 建议同时安装 lodash-es(用于通用工具函数)
pnpm add lodash-es使用
业务特定功能 - 从 utils 导入
import {
arrayToTree,
maskPhone,
cleanQueryParams,
yuanToFen
} from '@sop-cli/utils';通用工具函数 - 从 lodash-es 导入
import {
debounce,
cloneDeep,
omit,
pick,
groupBy,
uniqBy
} from 'lodash-es';
// Tree-shaking 友好的按需导入
import debounce from 'lodash-es/debounce';
import cloneDeep from 'lodash-es/cloneDeep';完整示例
import { arrayToTree, maskPhone } from '@sop-cli/utils';
import { groupBy, sortBy } from 'lodash-es';
// 业务特定:树形转换
const tree = arrayToTree(flatList, {
id: 'userId',
parentId: 'managerId'
});
// 通用:分组排序
const grouped = groupBy(users, 'department');
const sorted = sortBy(users, 'name');
// 业务特定:数据脱敏
const safePhone = maskPhone('13812345678'); // '138****5678'目录结构
src/
├── array/ # arrayToTree, treeToArray, unique, sortBy
├── object/ # cleanQueryParams, cleanBodyParams
├── string/ # maskPhone, maskIdCard, trimAll
├── network/ # queryString, parseQuery, processUrl, getUrlParam
├── number/ # formatCurrency, yuanToFen, toNumber, dodRatio
├── is/ # isArray, isString, isNumber, isEmpty...
├── json/ # safeJsonParse, safeJsonStringify
├── common/ # uuid, sleep
├── storage/ # StorageAdapter (local/session + Node 内存降级)
├── types/ # 公共类型导出
└── internal/ # 内部实现(不对外导出)注意: function 模块已废弃,请使用 lodash-es 的 debounce/throttle。
API 速览
@sop-cli/utils(业务特定)
| 模块 | 方法 |
|------|------|
| array | arrayToTree treeToArray unique sortBy |
| object | cleanQueryParams cleanBodyParams cleanParamsByStrategy |
| string | maskPhone maskIdCard trimAll |
| number | formatCurrency formatPercent formatWan yuanToFen fenToYuan toNumber dodRatio |
| network | queryString parseQuery getUrlParam processUrl |
| storage | createLocalStorage createSessionStorage StorageAdapter |
| is | isArray isObject isString isNumber isEmpty isDate isPromise... |
| json | safeJsonParse safeJsonStringify |
| common | uuid sleep |
lodash-es(通用工具,需单独安装)
| 类别 | 常用函数 |
|------|---------|
| Array | uniqBy groupBy keyBy sortBy chunk flatten |
| Object | pick omit cloneDeep merge defaults |
| Function | debounce throttle memoize curry |
| String | capitalize camelCase kebabCase trim |
| Math | sum max min round random |
构建
pnpm --filter @sop-cli/utils build产物:dist/index.es.js | dist/index.cjs | dist/index.iife.js | dist/**/*.d.ts
License
MIT
