npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sop-cli/utils

v0.11.1

Published

Business-specific utility functions, complement to lodash-es

Downloads

538

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

📚 文档导航

架构设计

开发规范

最佳实践

  • 🎯 核心理念 - DRY 原则的正确理解与最佳架构
  • 💡 职责隔离 - 工具函数与业务解耦
  • 🧪 单元测试 - 复杂工具编写测试用例
  • 🧹 定期清理 - 避免 utils 变成“杂物堆”
  • 🎯 场景选型 - ES6 vs lodash-es 的选择策略
  • 👥 协作规范 - 多人开发的标准化流程

重构记录

优化指南

变更历史

🏛️ 封装原则(强制遵守)

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 逻辑在组件中处理

检查清单:

  • [ ] 函数是否访问了 documentwindowlocalStorage
  • [ ] 函数是否调用了 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.ts

5.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