1. 项目概述一个前端开发者的“百宝箱”最近在整理自己的代码仓库翻到了一个我用了很久的私人项目——my-js。这名字听起来平平无奇甚至有点“简陋”但它对我来说意义远不止一个普通的工具库。它更像是我过去几年在前端开发路上一点一滴攒下来的“百宝箱”和“错题本”。这个项目没有发布到 npm也没有华丽的文档它纯粹是为我个人效率服务的里面塞满了各种在真实业务场景中被反复验证、提炼出来的 JavaScript/TypeScript 工具函数、组件逻辑片段、构建配置技巧以及一些特定问题的“优雅解”。我相信很多资深开发者都有类似的私人仓库。它的价值不在于技术有多前沿而在于“趁手”。每一个函数都带着当时解决问题的上下文每一行配置都可能是踩了几个小时坑换来的。今天我就把这个“百宝箱”打开和大家分享一下里面的核心设计思路、那些让我事半功倍的实用工具以及构建这样一个个人效率库的实操心得。无论你是想打造自己的工具集还是单纯想找一些可靠的代码片段来提升日常开发效率希望这篇分享都能给你带来启发。2. 核心设计哲学与架构思路2.1 为什么需要一个私人工具库在开始拆解my-js的具体内容之前我想先聊聊初衷。市面上优秀的工具库如 Lodash、Day.js、Axios 等数不胜数为什么还要自己造轮子原因主要有三点第一解决“最后一公里”的问题。通用库提供了强大的基础能力但在具体的业务场景中我们常常需要对其进行组合、封装或适配。比如项目中需要一个特定的日期格式化规则YYYY年MM月DD日 HH:mm:ss或者一个结合了防抖、参数序列化和错误处理的特定 API 请求函数。把这些高频使用的、业务定制的逻辑抽离成独立的工具函数放入my-js能极大减少重复代码和心智负担。第二沉淀最佳实践与避坑经验。在开发中我们总会遇到一些棘手的问题比如 Safari 浏览器下日期解析的兼容性、大数字精度丢失的处理、复杂对象的深拷贝性能瓶颈等。通过查阅资料、反复试验最终找到一个稳定可靠的解决方案。把这个方案及其背后的思考封装进my-js并加上详细的注释下次遇到同样问题就能直接“开箱即用”避免了重复踩坑。第三打造个性化的开发体验。每个人的编码习惯和偏好不同。my-js允许我按照自己最舒服的方式来设计函数签名、错误处理机制和代码风格。例如我偏好函数式编程那么我的工具函数会尽量追求纯函数和柯里化我讨厌冗长的try...catch就会封装一个统一的异步错误处理高阶函数。这个库让我和我的代码之间有了更高的默契度。2.2. 项目结构与组织逻辑my-js的结构并不复杂但清晰的组织是它能持续维护的关键。我的目录结构大致如下my-js/ ├── src/ │ ├── utils/ # 核心工具函数 │ │ ├── common.js/ts # 通用工具如类型判断、函数式工具 │ │ ├── format.js/ts # 格式化相关日期、数字、字符串 │ │ ├── validate.js/ts # 数据验证如邮箱、手机号、身份证 │ │ ├── storage.js/ts # 封装 localStorage/sessionStorage带过期时间和加密 │ │ └── url.js/ts # URL 参数解析、拼接、操作 │ ├── hooks/ # (如果是 React 项目) 自定义 React Hooks │ │ ├── useAsync.js/ts │ │ └── useLocalStorage.js/ts │ ├── constants/ # 常量定义 │ │ └── regexp.js/ts # 集中管理所有正则表达式 │ └── configs/ # 配置片段 │ ├── webpack.partial.js │ └── babel.config.partial.js ├── tests/ # 单元测试使用 Jest 或 Vitest ├── package.json └── README.md # 内部使用说明记录函数用途和示例组织逻辑的核心是“按领域划分而非按技术划分”。我不会把所有的“函数”都扔进一个functions.js文件。而是根据它们解决的问题域来分类。format目录下的所有函数都服务于“格式化”这个目标无论是日期、数字还是字符串。这样做的好处是当我在业务中需要格式化一个日期时我能非常确定地去src/utils/format/date.ts里寻找或添加函数上下文高度集中。另一个重要原则是“单一职责与高内聚”。每个文件甚至每个函数都尽可能只做一件事。src/utils/storage.ts只负责 Web Storage 的封装它内部可能会提供setItemWithExpiry、getItemWithExpiry、removeItem等几个方法但它们共同的目标是提供一个增强版的、统一的存储接口。这保证了代码的可维护性和可测试性。3. 实用工具函数深度解析下面我挑选几个my-js中极具代表性、使用频率最高的工具函数来详细解析它们的实现思路、边界案例处理和实战技巧。3.1. 日期处理超越Day.js的定制化封装虽然Day.js很轻量但在多时区项目或需要处理非标格式时依然需要封装。我创建了一个dateFormatter对象。// src/utils/format/date.ts import dayjs from dayjs; import utc from dayjs/plugin/utc; import timezone from dayjs/plugin/timezone; dayjs.extend(utc); dayjs.extend(timezone); /** * 日期格式化器 * 集中处理项目中所有日期显示逻辑保证格式统一 */ export const dateFormatter { /** * 标准日期时间格式 (YYYY-MM-DD HH:mm:ss) */ standard: (date: dayjs.ConfigType, timeZone?: string): string { const d timeZone ? dayjs(date).tz(timeZone) : dayjs(date); return d.isValid() ? d.format(YYYY-MM-DD HH:mm:ss) : Invalid Date; }, /** * 友好时间显示 (如“刚刚”、“5分钟前”、“昨天 15:30”) * 用于消息列表、评论时间等场景 */ friendly: (date: dayjs.ConfigType, now: dayjs.ConfigType new Date()): string { const d dayjs(date); const n dayjs(now); if (!d.isValid()) return 未知时间; const diffInSeconds n.diff(d, second); const diffInMinutes n.diff(d, minute); const diffInHours n.diff(d, hour); const diffInDays n.diff(d, day); if (diffInSeconds 60) return 刚刚; if (diffInMinutes 60) return ${diffInMinutes}分钟前; if (diffInHours 24) return ${diffInHours}小时前; if (diffInDays 1) return 昨天 ${d.format(HH:mm)}; if (diffInDays 2) return 前天 ${d.format(HH:mm)}; if (diffInDays 30) return ${diffInDays}天前; if (diffInDays 365) return d.format(MM-DD HH:mm); return d.format(YYYY-MM-DD); }, /** * 获取指定日期所在周的周一和周日日期 * 用于生成周报、周视图等场景 */ getWeekRange: (date: dayjs.ConfigType): { start: string; end: string } { const d dayjs(date); const weekStart d.startOf(week); // dayjs 默认一周从周日开始可根据 locale 调整 const weekEnd d.endOf(week); return { start: weekStart.format(YYYY-MM-DD), end: weekEnd.format(YYYY-MM-DD), }; }, }; 注意时区处理的坑。如果你的用户分布在全球服务端返回的往往是 UTC 时间戳或 ISO 字符串。在前端显示时必须明确指定目标时区。上面的standard函数提供了timeZone参数。更常见的做法是在应用初始化时统一设置dayjs.tz.setDefault(‘Asia/Shanghai’)。但切记涉及日期计算如加减天数时最好在 UTC 时间下进行以避免夏令时等边界问题。3.2. 存储封装支持过期与加密的localStorage原生localStorage简单易用但缺乏过期机制且存储明文数据不安全。我的增强版封装如下// src/utils/storage.ts import CryptoJS from crypto-js; // 一个常用的加密库 const SECRET_KEY import.meta.env.VITE_STORAGE_SECRET_KEY || my-default-secret; // 从环境变量读取 interface StorageItemT any { data: T; expiry: number | null; // 过期时间戳null 表示永不过期 } /** * 安全的存储工具 */ export const secureStorage { setItemT(key: string, value: T, ttl: number | null null): boolean { try { const item: StorageItemT { data: value, expiry: ttl ? Date.now() ttl * 1000 : null, // ttl 单位秒 }; const jsonString JSON.stringify(item); // 简单加密存储内容增加破解难度 const encrypted CryptoJS.AES.encrypt(jsonString, SECRET_KEY).toString(); localStorage.setItem(key, encrypted); return true; } catch (error) { console.error([secureStorage] Failed to set item ${key}:, error); // 当 localStorage 已满或不可用时静默失败或降级到内存存储需额外实现 return false; } }, getItemT(key: string): T | null { try { const encrypted localStorage.getItem(key); if (!encrypted) return null; const bytes CryptoJS.AES.decrypt(encrypted, SECRET_KEY); const decryptedString bytes.toString(CryptoJS.enc.Utf8); if (!decryptedString) { // 解密失败可能是密钥变更或数据被篡改清除之 this.removeItem(key); return null; } const item: StorageItemT JSON.parse(decryptedString); // 检查是否过期 if (item.expiry ! null Date.now() item.expiry) { this.removeItem(key); return null; } return item.data; } catch (error) { console.error([secureStorage] Failed to get item ${key}:, error); this.removeItem(key); // 读取失败也清除脏数据 return null; } }, removeItem(key: string): void { localStorage.removeItem(key); }, clear(): void { localStorage.clear(); }, }; 实操心得关于加密与性能。这里使用CryptoJS进行对称加密主要是为了防止用户轻易在浏览器控制台窥探存储内容例如 token、用户敏感标识。但这并非绝对安全前端代码和密钥是暴露的。因此绝对不要用localStorage存储真正的敏感信息如密码、支付密钥。此外加解密操作有性能开销对于频繁存取的大数据如大型列表缓存需谨慎或可以考虑仅对关键字段加密。ttl生存时间参数非常实用可以轻松实现“7天内免登录”这类功能。3.3. 函数式编程工具打造自己的lodash核心我借鉴了lodash和ramda的思想实现了一些更符合我个人习惯的函数式工具。// src/utils/common/fp.ts type Func (...args: any[]) any; /** * 函数柯里化 * param fn 需要柯里化的函数 * param arity 函数参数个数默认为 fn.length */ export function curryT extends Func(fn: T, arity: number fn.length): (...args: any[]) any { return function curried(...args: any[]) { if (args.length arity) { return fn.apply(this, args); } else { return function (...nextArgs: any[]) { return curried.apply(this, args.concat(nextArgs)); }; } }; } /** * 函数管道从左到右执行 * param fns 函数数组 * example pipe(add1, multiplyBy2)(3) // 8 */ export function pipe(...fns: Func[]): Func { return function (initialValue: any) { return fns.reduce((acc, fn) fn(acc), initialValue); }; } /** * 防抖函数 * param fn 目标函数 * param delay 延迟毫秒数 * param immediate 是否立即执行leading edge */ export function debounceT extends Func( fn: T, delay: number, immediate: boolean false ): (...args: ParametersT) void { let timeoutId: ReturnTypetypeof setTimeout | null null; return function (this: any, ...args: ParametersT) { const later () { timeoutId null; if (!immediate) fn.apply(this, args); }; const callNow immediate timeoutId null; if (timeoutId) clearTimeout(timeoutId); timeoutId setTimeout(later, delay); if (callNow) fn.apply(this, args); }; } /** * 异步函数重试机制 * param asyncFn 异步函数 * param maxRetries 最大重试次数 * param delayMs 重试延迟毫秒 * param shouldRetry 判断是否重试的函数默认对任何错误都重试 */ export async function retryAsyncT( asyncFn: () PromiseT, maxRetries: number 3, delayMs: number 1000, shouldRetry: (error: any) boolean () true ): PromiseT { let lastError: any; for (let i 0; i maxRetries; i) { try { return await asyncFn(); } catch (error) { lastError error; if (!shouldRetry(error) || i maxRetries - 1) { break; } console.warn([retryAsync] Attempt ${i 1} failed, retrying in ${delayMs}ms..., error); await new Promise((resolve) setTimeout(resolve, delayMs)); // 指数退避策略每次重试延迟加倍可选 // delayMs * 2; } } throw lastError; } 注意事项curry函数的局限性。上面实现的curry是一个简易版它依赖于函数的length属性。如果函数使用了默认参数或剩余参数...argsfn.length将不会计算这些参数导致柯里化行为与预期不符。在生产级工具库中需要更复杂的实现来处理这些情况。但对于个人库明确约定使用方式例如被柯里化的函数使用传统参数定义就能规避这个问题。retryAsync函数在网络请求不稳定时是救命稻草结合“指数退避”策略能更好地应对临时性网络故障。4. 自定义 React Hooks 集合对于 React 技术栈的项目my-js的hooks目录是我提炼业务逻辑的宝地。这里分享两个经典 Hook。4.1.useAsync优雅管理异步状态几乎每个 React 应用都要处理异步数据获取。手动管理loading、error、data状态非常繁琐且容易出错。useAsyncHook 将这些状态管理标准化。// src/hooks/useAsync.ts import { useState, useCallback, useEffect } from react; interface AsyncStateT { data: T | null; loading: boolean; error: Error | null; } type AsyncFunctionT (...args: any[]) PromiseT; export function useAsyncT( asyncFunction: AsyncFunctionT, immediate: boolean true, deps: any[] [] ) { const [state, setState] useStateAsyncStateT({ data: null, loading: immediate, // 如果立即执行初始状态为 loading error: null, }); const execute useCallback( async (...args: any[]) { setState((prev) ({ ...prev, loading: true, error: null })); try { const result await asyncFunction(...args); setState({ data: result, loading: false, error: null }); return result; // 返回结果方便链式调用 } catch (error) { setState({ data: null, loading: false, error: error as Error }); throw error; // 继续抛出错误让调用方可以 catch } }, [asyncFunction] // asyncFunction 作为依赖 ); useEffect(() { if (immediate) { execute(); } }, [execute, immediate, ...deps]); // 允许传入额外的依赖项当它们变化时重新执行 return { ...state, execute, // 提供手动触发执行的函数 setData: (data: T) setState((prev) ({ ...prev, data })), // 允许手动设置 data用于乐观更新 }; } // 使用示例 // function UserProfile({ userId }) { // const { data: user, loading, error, execute: refetch } useAsync( // () fetchUserApi(userId), // true, // 组件挂载时立即执行 // [userId] // userId 变化时重新执行 // ); // if (loading) return Spinner /; // if (error) return ErrorMessage error{error} /; // return div{user.name}/div; // } 核心优势关注点分离。这个 Hook 将数据获取的逻辑和 UI 渲染的状态完全解耦。UI 组件只需要消费data、loading、error三个状态无需关心useState和useEffect的细节。execute函数提供了手动重新获取数据的能力如点击刷新按钮setData则支持乐观更新等高级模式。4.2.useLocalStorage让状态持久化这是一个将 React 状态同步到localStorage的 Hook结合了之前封装的secureStorage。// src/hooks/useLocalStorage.ts import { useState, useEffect } from react; import { secureStorage } from ../utils/storage; export function useLocalStorageT( key: string, initialValue: T ): [T, (value: T | ((val: T) T)) void] { // 从 localStorage 读取初始值 const readValue (): T { try { const item secureStorage.getItemT(key); return item ! null ? item : initialValue; } catch (error) { console.warn([useLocalStorage] Error reading key ${key}:, error); return initialValue; } }; const [storedValue, setStoredValue] useStateT(readValue); // 定义一个 setter 函数同时更新 state 和 localStorage const setValue (value: T | ((val: T) T)) { try { // 允许值是一个函数类似于 useState 的 setter const valueToStore value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); secureStorage.setItem(key, valueToStore); } catch (error) { console.error([useLocalStorage] Error setting key ${key}:, error); } }; // 监听同源其他标签页的 storage 事件实现跨标签页状态同步 useEffect(() { const handleStorageChange (event: StorageEvent) { if (event.key key event.storageArea localStorage) { setStoredValue(readValue()); } }; window.addEventListener(storage, handleStorageChange); return () window.removeEventListener(storage, handleStorageChange); }, [key]); return [storedValue, setValue]; } // 使用示例 // function ThemeToggle() { // const [theme, setTheme] useLocalStoragelight | dark(app-theme, light); // const toggle () setTheme(prev prev light ? dark : light); // return button onClick{toggle}当前主题{theme}/button; // } 避坑技巧SSR 兼容性。这个 Hook 在服务端渲染SSR场景下会出问题因为localStorage是浏览器 API在 Node.js 环境中不存在。解决方案是在readValue函数中增加环境判断if (typeof window ‘undefined’) { return initialValue; }。此外storage事件仅在其他同源窗口修改localStorage时触发当前窗口自己的修改不会触发。这个特性正好用来实现多标签页间的状态同步体验非常棒。5. 构建配置与开发环境技巧my-js的configs目录存放着我从各个项目中总结出来的构建配置“秘籍”主要是 Webpack 和 Babel 的片段配置方便在新项目中快速复用。5.1. Webpack 性能优化片段// configs/webpack.partial.js const path require(path); const { BundleAnalyzerPlugin } require(webpack-bundle-analyzer); module.exports { // 解析配置 resolve: { extensions: [.js, .jsx, .ts, .tsx], alias: { : path.resolve(__dirname, ../src), // 路径别名 react: path.resolve(__dirname, ../node_modules/react), // 锁定React版本避免多实例 react-dom: path.resolve(__dirname, ../node_modules/react-dom), }, }, // 模块规则 module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: babel-loader, options: { cacheDirectory: true, // 开启缓存极大提升二次构建速度 }, }, }, { test: /\.(png|jpe?g|gif|svg|webp)$/i, type: asset, // webpack5 资源模块 parser: { dataUrlCondition: { maxSize: 8 * 1024, // 小于8kb的图片转base64 }, }, generator: { filename: static/images/[name].[hash:8][ext], // 输出路径和文件名 }, }, ], }, // 优化配置 optimization: { splitChunks: { chunks: all, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: vendors, priority: 10, // 优先级 }, commons: { name: commons, minChunks: 2, // 至少被两个入口引用 priority: 5, reuseExistingChunk: true, }, }, }, runtimeChunk: single, // 将运行时代码单独拆分利于长期缓存 }, plugins: [ // 包分析插件仅在需要时开启 new BundleAnalyzerPlugin({ analyzerMode: process.env.ANALYZE ? server : disabled, openAnalyzer: process.env.ANALYZE ? true : false, }), ], }; 实操心得cacheDirectory与splitChunks。babel-loader的cacheDirectory是我心中提升开发体验的性价比之王。它会把 Babel 编译结果缓存到文件系统下次构建时直接读取对于大型项目热更新速度能有数倍的提升。关于splitChunks关键在于cacheGroups的配置。将node_modules单独打包成vendors是常规操作因为第三方库变更不频繁可以充分利用浏览器缓存。进一步可以将一些大型库如react、lodash单独分包或者将多个路由页面公用的模块抽成commons这需要根据实际项目的chunk分析结果进行精细调整。5.2. Babel 配置与 Polyfill 策略现代前端项目往往需要兼容旧浏览器Babel 和 Polyfill 的配置是关键。// configs/babel.config.partial.js module.exports { presets: [ [ babel/preset-env, { // 根据 .browserslistrc 文件或此配置决定需要转译和 polyfill 的语法 targets: { browsers: [0.2%, not dead, not op_mini all], }, // 按需引入 polyfill推荐 usage useBuiltIns: usage, // 使用 core-js 3 版本 corejs: { version: 3, proposals: true }, // 避免将 ES Module 语法转为 CommonJS方便 webpack 做 tree-shaking modules: false, }, ], babel/preset-typescript, babel/preset-react, ], plugins: [ // 提案阶段的装饰器语法 [babel/plugin-proposal-decorators, { legacy: true }], // 类属性 [babel/plugin-proposal-class-properties, { loose: true }], // 运行时转换减少重复辅助代码注入 babel/plugin-transform-runtime, ], }; 注意事项useBuiltIns: ‘usage’的威力与陷阱。这个选项会让 Babel 根据你代码中实际使用的 ES6 API 和你配置的浏览器目标自动按需引入core-js的 polyfill。这比全量引入 (entry) 体积小得多。但有一个大坑Babel 默认只分析项目源码src/中的 API 使用情况。如果你的node_modules里的某个依赖也使用了新的 API例如使用了Array.prototype.includes而你的目标浏览器不支持Babel 不会为这部分代码引入 polyfill导致线上报错。解决方案是要么将不兼容的依赖也加入 Babel 转译范围 (exclude的反向操作)要么更稳妥地在入口文件顶部全量引入core-js/stable和regenerator-runtime/runtime。6. 维护、测试与迭代心得一个私人项目能长期保持活力离不开良好的维护习惯。6.1. 单元测试用 Jest 为工具函数上保险为工具函数编写单元测试不是“公司要求”而是对自己代码负责。我用 Jest 来为my-js的核心函数做测试。// tests/utils/format/date.test.ts import { dateFormatter } from ../../src/utils/format/date; describe(dateFormatter, () { describe(standard, () { it(should format a Date object correctly, () { const date new Date(2023-10-01T12:00:00Z); expect(dateFormatter.standard(date)).toBe(2023-10-01 12:00:00); }); it(should handle invalid date, () { expect(dateFormatter.standard(invalid)).toBe(Invalid Date); }); }); describe(friendly, () { const now new Date(2023-10-01T12:00:00Z); it(should return 刚刚 for seconds ago, () { const date new Date(2023-10-01T11:59:30Z); expect(dateFormatter.friendly(date, now)).toBe(刚刚); }); it(should return 昨天 for yesterday, () { const date new Date(2023-09-30T15:30:00Z); expect(dateFormatter.friendly(date, now)).toBe(昨天 15:30); }); // ... 更多边界测试用例 }); }); 测试心得测试“行为”而非“实现”。不要测试函数内部的私有变量或过于具体的实现逻辑比如某个中间变量是否被赋值。应该测试函数的输入输出是否符合预期以及边界条件如null、undefined、空字符串、非法输入是否被妥善处理。对于dateFormatter.friendly这种函数要重点测试时间间隔的各个临界点59秒、1分钟、59分钟、23小时等确保显示逻辑正确。6.2. 文档与注释写给未来的自己my-js的README.md不是对外宣传文档而是我的“使用备忘录”。我会为每个工具函数编写清晰的 JSDoc 注释并在README中记录最典型的用法和重要的注意事项。## 工具函数速查 ### secureStorage 封装了 localStorage支持设置过期时间(ttl)和简单加密。 **注意** 加密密钥来自环境变量 VITE_STORAGE_SECRET_KEY前端加密不绝对安全勿存敏感信息。 **示例** javascript // 存储10分钟后过期 secureStorage.setItem(user_token, abc123, 600); // 读取 const token secureStorage.getItem(user_token);useAsync管理异步操作状态的 React Hook。典型场景页面初始化加载数据、提交表单。注意deps参数用于控制useEffect的依赖如果asyncFunction依赖外部变量务必将其加入deps。示例(见上文)** 维护习惯定期“断舍离”。** 每隔一段时间比如半年我会回顾 my-js 中的代码。有些函数可能因为项目技术栈升级比如从 Vue 2 到 Vue 3而不再有用有些函数可能有了更优的实现比如用新的原生 API 替代。我会果断地删除或重构它们。同时也会把在新项目中遇到的通用问题和解法补充进来。让这个“百宝箱”始终保持精简和实用。 构建和维护 my-js 这样的私人工具库是一个持续学习和沉淀的过程。它最大的回报不是代码本身而是在这个过程中培养出的对代码质量的敏感度、对问题边界的思考习惯以及一套属于自己的高效开发工作流。当你再遇到一个似曾相识的问题时能从容地打开自己的仓库找到那段经过实战检验的代码那种感觉远比在搜索引擎里翻找要踏实和高效得多。