简介ztools 是一套面向 JavaScript 前端开发者的轻量级工具集适合希望提升异步编程、视图渲染与代码解耦能力的初中级开发者。它围绕三个核心模块展开兼容旧版 IE 的 ES6 Promise 实现帮助在低版本浏览器中编写现代异步代码Plato 简易逻辑模板引擎用于以简洁语法完成数据填充与视图构建Eidos 依赖注入类封装通过运行时管理对象依赖降低组件耦合提升可测试性与复用性。资源包共 17 个文件以 11 个 js 源码为主另含 3 个 html 演示页、1 个 md 说明文档及 json、gitignore 等配置压缩包约 13KB结构紧凑便于快速阅读源码。目前已有 311 人学习下载。读者可借此理解 Promise polyfill 的实现思路、模板引擎的解析逻辑与依赖注入在前端类中的落地方式并参考演示页面与说明文档快速上手适合作为前端基础能力进阶与源码学习的实践素材。1. 前端工具集到底解决什么问题从一次构建翻车说起前端工具集这个词听起来像大而全的框架实际落地时往往是一堆小脚本、小函数、小配置的集合。我见过太多团队把utils.js写成垃圾桶日期格式化、深拷贝、防抖节流、URL 参数解析全塞进去最后没人敢删任何一行。ztools 这类前端工具集的核心价值不是“功能多”而是边界清晰、按需引入、可测试。它适合三类人刚接手老项目需要快速补齐基础能力的开发者、想统一团队代码风格的技术负责人、以及厌倦了每次从零写工具函数的中高级前端。这一章先讲清楚它和 lodash、underscore 的区别以及为什么你不需要另一个“大而全”。2. 拆解 ztools 的模块划分与按需引入机制2.1 为什么 tree-shaking 友好的导出方式决定生死很多工具集翻车的第一个原因就是导出方式不对。如果你用export default { debounce, throttle, deepClone }打包器无法静态分析哪些函数被使用最终整个工具集都会进 bundle。正确做法是每个函数独立文件、独立命名导出并在package.json里声明sideEffects: false。// src/debounce.js /** * 防抖函数 * param {Function} fn - 需要防抖的函数 * param {number} delay - 延迟毫秒数默认 300 * param {boolean} immediate - 是否立即执行一次 * returns {Function} 包装后的函数 */ export function debounce(fn, delay 300, immediate false) { let timer null; return function (...args) { // 立即执行分支首次触发直接调用后续在延迟内不再触发 if (immediate !timer) { fn.apply(this, args); } if (timer) clearTimeout(timer); timer setTimeout(() { timer null; // 非立即执行分支延迟结束后调用 if (!immediate) fn.apply(this, args); }, delay); }; }逻辑说明immediate参数控制首次是否立即执行这是搜索框和按钮防重复点击的常见需求差异。参数delay默认 300ms 是经验值低于 200ms 用户无感知高于 500ms 会显得卡顿。注意timer置 null 的时机否则 immediate 模式只会执行一次。2.2 按需引入的三种落地方式与体积对比引入方式写法打包体积示例适用场景全量引入import ztools from ztools约 28KB gzip快速原型不关心体积命名引入import { debounce } from ztools约 1.2KB gzip现代打包器推荐路径引入import debounce from ztools/debounce约 0.8KB gzip老构建工具兜底方案命名引入依赖打包器的 tree-shakingWebpack 4 和 Vite 默认支持。路径引入最稳但写起来啰嗦。我一般推荐命名引入同时在 CI 里加一条 bundle 体积检查超过阈值就报警。2.3 用 rollup 打包出 esm 和 cjs 双格式# 安装必要依赖 npm i -D rollup rollup/plugin-node-resolve rollup/plugin-terser # rollup.config.mjs// rollup.config.mjs import resolve from rollup/plugin-node-resolve; import terser from rollup/plugin-terser; export default { input: src/index.js, output: [ { file: dist/ztools.esm.js, format: esm, // 给现代打包器用保留 tree-shaking sourcemap: true }, { file: dist/ztools.cjs.js, format: cjs, // 给 Node 端或老构建用 exports: named, sourcemap: true } ], plugins: [resolve(), terser()] };逻辑说明format: esm输出 ES 模块保留export语句打包器可以静态分析。format: cjs输出 CommonJS给 Node 环境或 Jest 测试用。exports: named避免default导出警告。terser做压缩但 sourcemap 要保留线上排查问题时能映射回源码。3. 核心工具函数的实现细节与参数调优3.1 深拷贝structuredClone 不是万能药很多工具集还在用JSON.parse(JSON.stringify())做深拷贝遇到 Date、RegExp、undefined、循环引用直接翻车。现代浏览器有structuredClone但它不支持函数和 DOM 节点。我一般写一个带降级方案的 deepClone。export function deepClone(target, map new WeakMap()) { // 基本类型直接返回 if (target null || typeof target ! object) return target; // 处理循环引用 if (map.has(target)) return map.get(target); // 处理 Date if (target instanceof Date) return new Date(target); // 处理 RegExp if (target instanceof RegExp) return new RegExp(target.source, target.flags); // 处理数组和普通对象 const clone Array.isArray(target) ? [] : {}; map.set(target, clone); Reflect.ownKeys(target).forEach(key { clone[key] deepClone(target[key], map); }); return clone; }逻辑说明WeakMap记录已拷贝对象解决循环引用导致的栈溢出。Reflect.ownKeys能拿到 Symbol 键比Object.keys更全。参数map外部不用传递归时自动传递。注意这个实现不处理 Map、Set、ArrayBuffer如果需要得单独加分支。3.2 日期格式化时区问题是最大的坑export function formatDate(date, fmt YYYY-MM-DD HH:mm:ss) { const d date instanceof Date ? date : new Date(date); if (isNaN(d.getTime())) throw new Error(Invalid date); const o { M: d.getMonth() 1, // 月份从 0 开始必须加 1 D: d.getDate(), H: d.getHours(), m: d.getMinutes(), s: d.getSeconds(), q: Math.floor((d.getMonth() 3) / 3), // 季度 S: d.getMilliseconds() }; if (/(Y)/.test(fmt)) { fmt fmt.replace(RegExp.$1, (d.getFullYear() ).substr(4 - RegExp.$1.length)); } for (const k in o) { if (new RegExp(( k )).test(fmt)) { fmt fmt.replace(RegExp.$1, RegExp.$1.length 1 ? o[k] : (00 o[k]).substr(( o[k]).length)); } } return fmt; }逻辑说明getMonth()返回 0-11必须加 1。RegExp.$1是上一个正则匹配的捕获组这里用来判断格式串里有没有对应占位符。substr(4 - RegExp.$1.length)处理YY和YYYY两种年份格式。注意这个函数输出的是本地时间如果后端返回 UTC 时间字符串需要先转换时区。3.3 请求重试与超时控制AbortController 的正确用法export async function fetchWithRetry(url, options {}, retries 3, timeout 5000) { const controller new AbortController(); const timer setTimeout(() controller.abort(), timeout); try { const res await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(HTTP ${res.status}); return await res.json(); } catch (err) { clearTimeout(timer); if (retries 0 err.name ! AbortError) { // 指数退避每次重试间隔翻倍 await new Promise(r setTimeout(r, (4 - retries) * 500)); return fetchWithRetry(url, options, retries - 1, timeout); } throw err; } }逻辑说明AbortController用于超时中断setTimeout触发abort()后 fetch 会抛出AbortError。重试次数retries默认 3 次指数退避间隔为 500ms、1000ms、1500ms。注意AbortError不重试因为超时说明服务端可能已经挂了继续重试只会加重负担。4. 避坑与排查工具集维护中最容易翻车的 5 个点4.1 现象打包后体积没变小反而变大原因package.json里没写sideEffects: false或者写了但被babel转成了 CommonJS。解决检查sideEffects字段确保babel配置里modules: false让打包器自己处理模块语法。4.2 现象Node 端引入报错 “Cannot use import statement outside a module”原因只打包了 ESM 格式Node 默认走 CJS。解决在package.json里配置exports字段区分import和require入口。{ name: ztools, exports: { .: { import: ./dist/ztools.esm.js, require: ./dist/ztools.cjs.js } } }4.3 现象防抖函数在 Vue 组件里失效原因组件重新渲染时创建了新的防抖函数旧的定时器还在。解决在setup或data里只创建一次或者用useMemo缓存。React 里用useRef存防抖后的函数。4.4 现象深拷贝后 Date 变成字符串原因用了JSON.parse(JSON.stringify())。解决换成本文的deepClone或者用structuredClone并手动处理不支持的类型。4.5 现象单元测试覆盖率 100% 但线上还是出问题原因测试用例只覆盖了正常路径没测边界值。解决每个工具函数至少加三条用例——正常输入、空值/异常输入、边界值如delay0、retries0。5. 进阶把工具集变成团队规范的一部分工具集写完之后真正的挑战是让团队用起来、不重复造轮子。我一般做三件事第一在 CI 里加一条 ESLint 规则禁止直接写JSON.parse(JSON.stringify())和手写防抖引导使用工具集。第二给每个函数写 JSDoc 注释VS Code 里悬停能看到参数说明和示例。第三定期跑一次npm outdated和 bundle 分析把没人用的函数标记为 deprecated下个大版本删掉。// .eslintrc.js 片段 module.exports { rules: { no-restricted-syntax: [ error, { selector: CallExpression[callee.object.nameJSON][callee.property.nameparse], message: 请使用 ztools 的 deepClone避免 Date/RegExp 丢失 } ] } };逻辑说明no-restricted-syntax用 AST 选择器匹配JSON.parse调用报错信息引导到工具集。这个规则比no-restricted-properties更灵活能精确匹配调用表达式。验证方法在 CI 里加一条npm run build npx bundlesize设置 gzip 后不超过 5KB。超过就失败强制 review 新增函数是否必要。我自己的习惯是每加一个函数先问三个问题——现有函数能不能组合出来浏览器原生 API 能不能替代三个月后还有人用吗三个问题有一个答不上来就不加。希望帮到你。本文还有配套的精品资源点击获取