首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >写了几年前端,这 10 个小函数从没被我抛弃过

写了几年前端,这 10 个小函数从没被我抛弃过

作者头像
前端达人
发布2026-06-24 20:41:32
发布2026-06-24 20:41:32
1340
举报
文章被收录于专栏:前端达人前端达人

每次新建项目,我都会做同一件事——

打开旧项目,找到 utils 文件夹,把里面那一堆小函数全部复制过来。

不是懒,是因为踩过太多坑之后,我终于搞清楚了一件事:有些代码,永远都会用到。

搜索框的抖动、日期显示的格式、JSON 解析时崩溃的报错、下载文件时莫名失败……这些问题我处理了一遍又一遍,最终决定:不再重复写了,统一放进工具库,每个新项目直接带走。

今天把这 10 个函数全部分享出来,每一个提供 TypeScript + 原生 JS 双版本,完整可粘贴,拿走即用,同时附上真实业务场景。

先建好 utils 目录结构

在写任何业务代码之前,我会先把工具函数库的骨架搭好:

代码语言:javascript
复制
src/
└── utils/
    ├── debounce.ts / debounce.js
    ├── formatDate.ts / formatDate.js
    ├── classNames.ts / classNames.js
    ├── jsonHelpers.ts / jsonHelpers.js
    ├── object.ts / object.js
    ├── clipboard.ts / clipboard.js
    ├── string.ts / string.js
    ├── sleep.ts / sleep.js
    ├── array.ts / array.js
    ├── download.ts / download.js
    └── index.ts / index.js   // 统一导出

1. 防抖(Debounce)

用得最多的一个,没有之一。搜索框、滚动监听、窗口 resize,任何"用户操作会高频触发"的场景都需要它。

TypeScript 版本

代码语言:javascript
复制
// utils/debounce.ts
exportfunction debounce<T extends (...args: unknown[]) => void>(
  func: T,
  delay: number = 300
): (...args: Parameters<T>) => void {
let timeout: ReturnType<typeof setTimeout> | null = null;

returnfunction (...args: Parameters<T>): void {
    if (timeout !== null) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func(...args);
      timeout = null;
    }, delay);
  };
}

JavaScript 版本

代码语言:javascript
复制
// utils/debounce.js
exportfunction debounce(func, delay = 300) {
let timeout = null;

returnfunction (...args) {
    if (timeout !== null) {
      clearTimeout(timeout);
    }
    timeout = setTimeout(() => {
      func(...args);
      timeout = null;
    }, delay);
  };
}

业务场景

代码语言:javascript
复制
// ① 电商后台:商品搜索,用户停止输入 400ms 后再发请求
//    接口调用量降低 80%,服务器压力大幅减少
const handleProductSearch = debounce((keyword: string) => {
  fetchProductList({ keyword, page: 1 });
}, 400);

// ② 数据大屏:监听窗口 resize,停止拖拽后才重绘图表
//    避免 resize 过程中图表一直重新渲染,导致页面卡死
const handleResize = debounce(() => {
  chart.resize();
}, 200);
window.addEventListener("resize", handleResize);

// ③ 表单校验:用户输入手机号时实时校验格式
//    停止输入 500ms 后才触发,不要每输入一个数字就校验
const validatePhone = debounce((value: string) => {
const isValid = /^1[3-9]\d{9}$/.test(value);
  setPhoneError(isValid ? "" : "请输入正确的手机号");
}, 500);

2. 日期格式化(formatDate)

每个项目都要显示日期,每次都要想格式——这个函数一劳永逸。

TypeScript 版本

代码语言:javascript
复制
// utils/formatDate.ts
type DateFormatOptions = {
  locale?: string;
  showTime?: boolean;
  separator?: string;
};

exportfunction formatDate(
  date: string | number | Date,
  options: DateFormatOptions = {}
): string {
const { locale = "zh-CN", showTime = false, separator } = options;
const d = newDate(date);

if (isNaN(d.getTime())) return"日期无效";

if (separator) {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    const base = [y, m, day].join(separator);
    if (!showTime) return base;
    const h = String(d.getHours()).padStart(2, "0");
    const min = String(d.getMinutes()).padStart(2, "0");
    const s = String(d.getSeconds()).padStart(2, "0");
    return`${base} ${h}:${min}:${s}`;
  }

const dateOpts: Intl.DateTimeFormatOptions = {
    year: "numeric",
    month: "short",
    day: "numeric",
  };
if (showTime) {
    dateOpts.hour = "2-digit";
    dateOpts.minute = "2-digit";
    dateOpts.second = "2-digit";
  }

return d.toLocaleDateString(locale, dateOpts);
}

JavaScript 版本

代码语言:javascript
复制
// utils/formatDate.js
exportfunction formatDate(date, options = {}) {
const { locale = "zh-CN", showTime = false, separator } = options;
const d = newDate(date);

if (isNaN(d.getTime())) return"日期无效";

if (separator) {
    const y = d.getFullYear();
    const m = String(d.getMonth() + 1).padStart(2, "0");
    const day = String(d.getDate()).padStart(2, "0");
    const base = [y, m, day].join(separator);
    if (!showTime) return base;
    const h = String(d.getHours()).padStart(2, "0");
    const min = String(d.getMinutes()).padStart(2, "0");
    const s = String(d.getSeconds()).padStart(2, "0");
    return`${base} ${h}:${min}:${s}`;
  }

const dateOpts = { year: "numeric", month: "short", day: "numeric" };
if (showTime) {
    dateOpts.hour = "2-digit";
    dateOpts.minute = "2-digit";
    dateOpts.second = "2-digit";
  }

return d.toLocaleDateString(locale, dateOpts);
}

业务场景

代码语言:javascript
复制
// ① 订单列表:显示下单时间
formatDate("2024-04-22T14:30:00", { showTime: true })
// => "2024年4月22日 14:30:00"

// ② 导出文件名:生成带日期的文件名,避免覆盖
const filename = `订单报表-${formatDate(new Date(), { separator: "-" })}.xlsx`;
// => "订单报表-2024-04-22.xlsx"

// ③ 对接国际化:英文环境切换 locale
formatDate("2024-04-22", { locale: "en-US" })
// => "Apr 22, 2024"

3. 条件类名(classNames)

写过 React 的都懂这个痛——动态拼接 className 太丑。

TypeScript 版本

代码语言:javascript
复制
// utils/classNames.ts
type ClassValue = string | number | boolean | undefined | null;

export function classNames(...args: ClassValue[]): string {
  return args
    .filter((arg): arg is string | number => Boolean(arg))
    .map(String)
    .join(" ");
}

JavaScript 版本

代码语言:javascript
复制
// utils/classNames.js
export function classNames(...args) {
  return args
    .filter(Boolean)
    .map(String)
    .join(" ");
}

业务场景

代码语言:javascript
复制
// ① 按钮状态:根据多个条件组合类名
<button
  className={classNames(
    "btn",
    "btn-md",
    isPrimary && "btn-primary",
    isLoading && "btn-loading",
    isDisabled && "btn-disabled opacity-50 cursor-not-allowed"
  )}
  disabled={isDisabled}
>
  {isLoading ? "提交中..." : "立即提交"}
</button>

// ② 表格行高亮:选中行 + 斑马纹
rows.map((row, index) => (
  <tr
    key={row.id}
    className={classNames(
      "table-row",
      index % 2 === 0 && "bg-gray-50",
      selectedIds.includes(row.id) && "bg-blue-100 font-semibold"
    )}
  />
))

// ③ 导航菜单:当前激活项高亮
<NavItem className={classNames("nav-item", isActive && "nav-item--active")} />

4. 安全解析 JSON(safeJsonParse)

JSON.parse 解析失败直接抛异常,整个程序崩了。读 localStorage 时尤其危险——用户的数据可能是任何格式。

TypeScript 版本

代码语言:javascript
复制
// utils/jsonHelpers.ts
exportfunction safeJsonParse<T = unknown>(
  str: string | null | undefined,
  fallback: T = undefined as unknown as T
): T {
if (str === null || str === undefined || str.trim() === "") {
    return fallback;
  }
try {
    returnJSON.parse(str) as T;
  } catch {
    console.warn("[safeJsonParse] 解析失败,已返回 fallback:", str);
    return fallback;
  }
}

// 反向:安全序列化,避免循环引用崩溃
exportfunction safeJsonStringify(value: unknown, fallback = "{}"): string {
try {
    returnJSON.stringify(value);
  } catch {
    return fallback;
  }
}

JavaScript 版本

代码语言:javascript
复制
// utils/jsonHelpers.js
exportfunction safeJsonParse(str, fallback = undefined) {
if (str === null || str === undefined || str.trim() === "") {
    return fallback;
  }
try {
    returnJSON.parse(str);
  } catch {
    console.warn("[safeJsonParse] 解析失败,已返回 fallback:", str);
    return fallback;
  }
}

exportfunction safeJsonStringify(value, fallback = "{}") {
try {
    returnJSON.stringify(value);
  } catch {
    return fallback;
  }
}

业务场景

代码语言:javascript
复制
// ① 读取本地用户配置,解析失败给默认值,不崩溃
interface UserSettings {
  theme: "light" | "dark";
  language: string;
  pageSize: number;
}
const settings = safeJsonParse<UserSettings>(
  localStorage.getItem("user_settings"),
  { theme: "light", language: "zh-CN", pageSize: 20 }
);

// ② 处理接口返回的 JSON 字符串字段
//    有些后端会把对象序列化成字符串塞进字段里
const extra = safeJsonParse<Record<string, string>>(order.extraInfo, {});

// ③ 保存前防止循环引用报错(比如含 DOM 引用的对象)
const cache = safeJsonStringify(complexObject);
localStorage.setItem("cache", cache);

5. 判断空对象(isEmpty)

检查表单是否有错误、判断数据是否为空,比 Object.keys(obj).length === 0 更安全。

TypeScript 版本

代码语言:javascript
复制
// utils/object.ts
exportfunction isEmpty(
  value: Record<string, unknown> | null | undefined | unknown[]
): boolean {
if (value === null || value === undefined) returntrue;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === "object") {
    returnObject.keys(value).length === 0;
  }
returnfalse;
}

// 深度克隆(简单对象,无循环引用)
exportfunction deepClone<T>(obj: T): T {
returnJSON.parse(JSON.stringify(obj));
}

// 删除对象中值为 undefined / null / '' 的字段(清理请求参数常用)
exportfunction removeEmpty<T extends Record<string, unknown>>(obj: T): Partial<T> {
returnObject.fromEntries(
    Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== "")
  ) as Partial<T>;
}

JavaScript 版本

代码语言:javascript
复制
// utils/object.js
exportfunction isEmpty(value) {
if (value === null || value === undefined) returntrue;
if (Array.isArray(value)) return value.length === 0;
if (typeof value === "object") {
    returnObject.keys(value).length === 0;
  }
returnfalse;
}

exportfunction deepClone(obj) {
returnJSON.parse(JSON.stringify(obj));
}

exportfunction removeEmpty(obj) {
returnObject.fromEntries(
    Object.entries(obj).filter(([, v]) => v !== undefined && v !== null && v !== "")
  );
}

业务场景

代码语言:javascript
复制
// ① 表单提交前校验:有错误不允许提交
const formErrors = validateForm(formData);
if (isEmpty(formErrors)) {
await submitOrder(formData);
} else {
  showErrors(formErrors);
}

// ② 接口请求:清理掉值为空的查询参数,避免传 undefined 给后端
const rawParams = { keyword: searchText, status: selectedStatus, page: currentPage };
const cleanParams = removeEmpty(rawParams);
// keyword 为空时不传这个字段,后端不报参数错误

// ③ 编辑页面:判断是否有未保存的改动
const originalData = deepClone(fetchedFormData);  // 保存原始数据副本
const hasUnsavedChanges = JSON.stringify(formData) !== JSON.stringify(originalData);
if (hasUnsavedChanges) {
  showLeaveConfirm(); // 离开页面前弹确认框
}

6. 复制到剪贴板(copyToClipboard)

"复制链接""复制代码""复制订单号"——每个产品都会有这个需求。

TypeScript 版本

代码语言:javascript
复制
// utils/clipboard.ts
exportasyncfunction copyToClipboard(text: string): Promise<boolean> {
// 优先使用现代 API
if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text);
      returntrue;
    } catch {
      // fallthrough to legacy
    }
  }

// 降级兼容:使用 execCommand(已废弃但覆盖面广)
try {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0;";
    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    const success = document.execCommand("copy");
    document.body.removeChild(textarea);
    return success;
  } catch {
    returnfalse;
  }
}

JavaScript 版本

代码语言:javascript
复制
// utils/clipboard.js
exportasyncfunction copyToClipboard(text) {
if (navigator.clipboard && window.isSecureContext) {
    try {
      await navigator.clipboard.writeText(text);
      returntrue;
    } catch {
      // fallthrough to legacy
    }
  }

try {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.cssText = "position:fixed;top:-9999px;left:-9999px;opacity:0;";
    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    const success = document.execCommand("copy");
    document.body.removeChild(textarea);
    return success;
  } catch {
    returnfalse;
  }
}

业务场景

代码语言:javascript
复制
// ① 邀请好友:一键复制邀请链接
const handleCopyInvite = async () => {
const link = `https://example.com/register?ref=${userId}`;
const ok = await copyToClipboard(link);
  message.success(ok ? "邀请链接已复制!去分享给好友吧 🎉" : "复制失败,请手动复制");
};

// ② 代码展示块:复制代码按钮
const CodeBlock = ({ code }: { code: string }) => {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
    const ok = await copyToClipboard(code);
    if (ok) {
      setCopied(true);
      setTimeout(() => setCopied(false), 2000); // 2秒后恢复
    }
  };
return (
    <div className="code-block">
      <pre>{code}</pre>
      <button onClick={handleCopy}>{copied ? "✅ 已复制" : "复制"}</button>
    </div>
  );
};

// ③ 订单详情:复制订单号
<span onClick={() => copyToClipboard(order.orderNo).then(ok => ok && toast("已复制"))}>
  {order.orderNo} 📋
</span>

🔔 注意navigator.clipboard 只在 HTTPS 或 localhost 下可用。这个版本做了降级处理,HTTP 环境也能工作。

7. 首字母大写(capitalize)& 字符串工具

TypeScript 版本

代码语言:javascript
复制
// utils/string.ts

// 首字母大写
exportfunction capitalize(str: string): string {
if (!str) return"";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

// 驼峰转中划线(camelCase → kebab-case)
exportfunction camelToKebab(str: string): string {
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
}

// 截断长文本,超出显示省略号
exportfunction truncate(str: string, maxLength: number, ellipsis = "..."): string {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
}

// 手机号脱敏:138****8888
exportfunction maskPhone(phone: string): string {
return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
}

// 金额格式化:12345.6 => ¥12,345.60
exportfunction formatCurrency(
  amount: number,
  currency = "CNY",
  locale = "zh-CN"
): string {
returnnewIntl.NumberFormat(locale, {
    style: "currency",
    currency,
    minimumFractionDigits: 2,
  }).format(amount);
}

JavaScript 版本

代码语言:javascript
复制
// utils/string.js
exportfunction capitalize(str) {
if (!str) return"";
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
}

exportfunction camelToKebab(str) {
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
}

exportfunction truncate(str, maxLength, ellipsis = "...") {
if (str.length <= maxLength) return str;
return str.slice(0, maxLength - ellipsis.length) + ellipsis;
}

exportfunction maskPhone(phone) {
return phone.replace(/(\d{3})\d{4}(\d{4})/, "$1****$2");
}

exportfunction formatCurrency(amount, currency = "CNY", locale = "zh-CN") {
returnnewIntl.NumberFormat(locale, {
    style: "currency",
    currency,
    minimumFractionDigits: 2,
  }).format(amount);
}

业务场景

代码语言:javascript
复制
// ① 状态枚举展示(后端返回小写,前端展示首字母大写)
capitalize("pending")  // => "Pending"
capitalize("active")   // => "Active"

// ② 订单列表:超长商品名截断显示
truncate("2024年春季新款女士羊绒大衣长款双面呢加厚保暖外套", 20)
// => "2024年春季新款女士羊绒大衣长款双..."

// ③ 用户信息脱敏展示
maskPhone("13812348888")   // => "138****8888"

// ④ 价格展示
formatCurrency(12345.6)    // => "¥12,345.60"
formatCurrency(99, "USD", "en-US")  // => "$99.00"

8. 延时工具(sleep)& 重试工具

TypeScript 版本

代码语言:javascript
复制
// utils/sleep.ts

// 基础延时
exportfunction sleep(ms: number): Promise<void> {
returnnewPromise((resolve) => setTimeout(resolve, ms));
}

// 带重试的异步函数包装器
exportasyncfunction withRetry<T>(
  fn: () => Promise<T>,
  retries: number = 3,
  delay: number = 1000
): Promise<T> {
let lastError: Error = newError("Unknown error");
for (let i = 0; i < retries; i++) {
    try {
      returnawait fn();
    } catch (err) {
      lastError = err instanceofError ? err : newError(String(err));
      if (i < retries - 1) {
        console.warn(`[withRetry] 第 ${i + 1} 次失败,${delay}ms 后重试...`);
        await sleep(delay * (i + 1)); // 指数退避
      }
    }
  }
throw lastError;
}

JavaScript 版本

代码语言:javascript
复制
// utils/sleep.js
exportfunction sleep(ms) {
returnnewPromise((resolve) => setTimeout(resolve, ms));
}

exportasyncfunction withRetry(fn, retries = 3, delay = 1000) {
let lastError = newError("Unknown error");
for (let i = 0; i < retries; i++) {
    try {
      returnawait fn();
    } catch (err) {
      lastError = err instanceofError ? err : newError(String(err));
      if (i < retries - 1) {
        console.warn(`[withRetry] 第 ${i + 1} 次失败,${delay}ms 后重试...`);
        await sleep(delay * (i + 1));
      }
    }
  }
throw lastError;
}

业务场景

代码语言:javascript
复制
// ① 骨架屏最少展示时间,避免加载太快导致的闪烁
setLoading(true);
const [data] = awaitPromise.all([
  fetchOrderList(),
  sleep(600),   // 保证 loading 至少显示 600ms,体验更稳
]);
setLoading(false);

// ② 轮询接口(如支付结果查询)
asyncfunction pollPaymentResult(orderId: string) {
for (let i = 0; i < 10; i++) {
    const result = await checkPaymentStatus(orderId);
    if (result.status === "paid") return result;
    await sleep(2000); // 每隔 2 秒查一次
  }
thrownewError("支付结果查询超时");
}

// ③ 不稳定接口自动重试,失败时指数退避
const data = await withRetry(
() => fetchWeatherData(cityCode),
3,    // 最多重试 3 次
500   // 首次延迟 500ms,之后 1000ms、1500ms
);

9. 数组工具(uniqueArray + 更多)

TypeScript 版本

代码语言:javascript
复制
// utils/array.ts

// 数组去重(基本类型)
exportfunction uniqueArray<T>(arr: T[]): T[] {
return [...new Set(arr)];
}

// 按对象字段去重
exportfunction uniqueBy<T>(arr: T[], key: keyof T): T[] {
const seen = new Set();
return arr.filter((item) => {
    const val = item[key];
    if (seen.has(val)) returnfalse;
    seen.add(val);
    returntrue;
  });
}

// 数组分块(大列表分批处理)
exportfunction chunk<T>(arr: T[], size: number): T[][] {
const result: T[][] = [];
for (let i = 0; i < arr.length; i += size) {
    result.push(arr.slice(i, i + size));
  }
return result;
}

// 数组扁平化(兼容性更好)
exportfunction flatDeep<T>(arr: (T | T[])[], depth = 1): T[] {
return arr.flat(depth) as T[];
}

// 按字段排序
exportfunction sortBy<T>(arr: T[], key: keyof T, order: "asc" | "desc" = "asc"): T[] {
return [...arr].sort((a, b) => {
    const va = a[key];
    const vb = b[key];
    if (va < vb) return order === "asc" ? -1 : 1;
    if (va > vb) return order === "asc" ? 1 : -1;
    return0;
  });
}

JavaScript 版本

代码语言:javascript
复制
// utils/array.js
exportfunction uniqueArray(arr) {
return [...new Set(arr)];
}

exportfunction uniqueBy(arr, key) {
const seen = newSet();
return arr.filter((item) => {
    const val = item[key];
    if (seen.has(val)) returnfalse;
    seen.add(val);
    returntrue;
  });
}

exportfunction chunk(arr, size) {
const result = [];
for (let i = 0; i < arr.length; i += size) {
    result.push(arr.slice(i, i + size));
  }
return result;
}

exportfunction sortBy(arr, key, order = "asc") {
return [...arr].sort((a, b) => {
    if (a[key] < b[key]) return order === "asc" ? -1 : 1;
    if (a[key] > b[key]) return order === "asc" ? 1 : -1;
    return0;
  });
}

业务场景

代码语言:javascript
复制
// ① 合并多个接口的标签列表,去掉重复 ID
const allTagIds = uniqueArray([...userTags, ...hotTags, ...defaultTags]);

// ② 接口返回可能有重复数据,按 id 字段去重
const uniqueOrders = uniqueBy(orderList, "orderId");

// ③ 批量上传:1000 张图片分批处理,每批 20 张,避免并发太高
const batches = chunk(imageList, 20);
for (const batch of batches) {
awaitPromise.all(batch.map(uploadImage));
await sleep(200); // 批次间稍微等一下
}

// ④ 商品列表按价格升序排列
const sortedProducts = sortBy(productList, "price", "asc");

10. 触发文件下载(downloadFile)

不用打开新窗口,纯前端触发任意格式文件的下载。

TypeScript 版本

代码语言:javascript
复制
// utils/download.ts

// 通过 URL 下载文件
exportfunction downloadFile(url: string, filename: string): void {
const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.style.display = "none";
document.body.appendChild(a);
  a.click();

// 延迟移除,Safari 下立即移除会导致下载中断
  setTimeout(() =>document.body.removeChild(a), 100);
}

// 把 Blob 数据下载为文件(适用于前端生成内容,如 CSV)
exportfunction downloadBlob(blob: Blob, filename: string): void {
const url = URL.createObjectURL(blob);
  downloadFile(url, filename);
// 释放内存
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

// 把字符串内容下载为文件
exportfunction downloadText(
  content: string,
  filename: string,
  mimeType = "text/plain;charset=utf-8"
): void {
const blob = new Blob([content], { type: mimeType });
  downloadBlob(blob, filename);
}

JavaScript 版本

代码语言:javascript
复制
// utils/download.js
exportfunction downloadFile(url, filename) {
const a = document.createElement("a");
  a.href = url;
  a.download = filename;
  a.style.display = "none";
document.body.appendChild(a);
  a.click();
  setTimeout(() =>document.body.removeChild(a), 100);
}

exportfunction downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
  downloadFile(url, filename);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

exportfunction downloadText(content, filename, mimeType = "text/plain;charset=utf-8") {
const blob = new Blob([content], { type: mimeType });
  downloadBlob(blob, filename);
}

业务场景

代码语言:javascript
复制
// ① 报表导出:后端返回二进制流,前端触发 xlsx 下载
const handleExport = async () => {
const blob = await exportOrdersAsBlob({ month: "2024-04" });
  downloadBlob(blob, `订单报表-2024年4月-${Date.now()}.xlsx`);
};

// ② 前端生成 CSV 并下载(无需后端)
const handleExportCsv = () => {
const headers = ["订单号", "商品名称", "金额", "状态"];
const rows = orderList.map(o => [o.orderId, o.productName, o.amount, o.status]);
const csv = [headers, ...rows].map(row => row.join(",")).join("\n");
  downloadText(" " + csv, "订单数据.csv", "text/csv;charset=utf-8"); //   解决 Excel 乱码
};

// ③ 下载后端静态资源(如合同 PDF、发票)
<Button onClick={() => downloadFile(contract.url, `合同-${contract.id}.pdf`)}>
  下载合同
</Button>

统一导出(index.ts / index.js)

TypeScript

代码语言:javascript
复制
// utils/index.ts
export { debounce } from"./debounce";
export { formatDate } from"./formatDate";
export { classNames } from"./classNames";
export { safeJsonParse, safeJsonStringify } from"./jsonHelpers";
export { isEmpty, deepClone, removeEmpty } from"./object";
export { copyToClipboard } from"./clipboard";
export { capitalize, camelToKebab, truncate, maskPhone, formatCurrency } from"./string";
export { sleep, withRetry } from"./sleep";
export { uniqueArray, uniqueBy, chunk, sortBy } from"./array";
export { downloadFile, downloadBlob, downloadText } from"./download";

JavaScript

代码语言:javascript
复制
// utils/index.js
export { debounce } from"./debounce.js";
export { formatDate } from"./formatDate.js";
export { classNames } from"./classNames.js";
export { safeJsonParse, safeJsonStringify } from"./jsonHelpers.js";
export { isEmpty, deepClone, removeEmpty } from"./object.js";
export { copyToClipboard } from"./clipboard.js";
export { capitalize, camelToKebab, truncate, maskPhone, formatCurrency } from"./string.js";
export { sleep, withRetry } from"./sleep.js";
export { uniqueArray, uniqueBy, chunk, sortBy } from"./array.js";
export { downloadFile, downloadBlob, downloadText } from"./download.js";

之后在任意文件里一行导入:

代码语言:javascript
复制
import { debounce, formatDate, copyToClipboard, maskPhone } from "@/utils";

新项目初始化的正确顺序

有了这套工具库,我的新项目启动流程是这样的:

代码语言:javascript
复制
新建项目(Vite / Next.js / CRA)
          │
          ▼
  配置 ESLint / Prettier / tsconfig
          │
          ▼
  ★ 搭建 utils/ 目录,粘贴这套工具函数  ← 这一步从不跳过
          │
          ▼
  封装 request(axios/fetch + 拦截器)
          │
          ▼
  接入路由 / 状态管理
          │
          ▼
  开始写业务组件和页面

以前我总是直接跳到"写业务",结果到后期 JSON.parse 到处抛异常,防抖逻辑三处重写,下载文件每次都现查。现在反过来——工具库先行,之后省的时间是搭建它花费时间的十倍。

最后说一句

这 10 个函数,没有一个是高深的,每一个你都能在 5 分钟内自己写出来。

但「能写」和「每次都写」是两回事。

工程的价值不在于你能解决多难的问题,而在于你有没有把解决过的问题,变成下次不需要再解决的问题

把这套工具库加进你的 starter kit,下个项目开工的第一天,你会感谢今天做的这件事。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先建好 utils 目录结构
  • 1. 防抖(Debounce)
  • 2. 日期格式化(formatDate)
  • 3. 条件类名(classNames)
  • 4. 安全解析 JSON(safeJsonParse)
  • 5. 判断空对象(isEmpty)
  • 6. 复制到剪贴板(copyToClipboard)
  • 7. 首字母大写(capitalize)& 字符串工具
  • 8. 延时工具(sleep)& 重试工具
  • 9. 数组工具(uniqueArray + 更多)
  • 10. 触发文件下载(downloadFile)
  • 统一导出(index.ts / index.js)
  • 新项目初始化的正确顺序
  • 最后说一句
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档