
1. 项目概述一个技能库的诞生与价值最近在整理个人技术栈和项目经验时我意识到一个问题很多零散但实用的代码片段、工具函数、配置模板往往散落在不同的项目里或者干脆只存在于某个已经模糊的记忆中。当需要快速搭建新项目、解决一个似曾相识的问题时又得重新搜索、调试效率很低。我相信这也是很多开发者尤其是独立开发者或小团队负责人的共同痛点。于是我决定启动一个名为mao-skill的项目。这个名字很直白“mao”是我的个人标识“skill”就是技能、技巧。它的核心定位就是一个个人或小团队专用的、高度定制化的源代码技能库。它不是另一个lodash或axios那样庞大、通用的开源库而是一个“私房菜谱”里面装的是我或我们团队在长期开发实践中沉淀下来的、经过实战检验的、最能解决我们自身特定问题的代码“积木”。这个库的价值不在于它有多高的 star 数而在于它能极大地提升我个人的开发效率和代码质量。通过系统性地整理、抽象和封装这些“技能”我可以实现“一次编写处处复用”减少重复劳动保证解决方案的一致性并且能持续迭代优化。对于看到这个项目的你而言它的价值可能在于提供了一个构建个人技术资产库的思路、一些可直接借鉴的实用代码或者一种提升工程化思维的方法。2. 核心设计思路如何构建一个“活”的技能库创建一个代码仓库很容易但要让这个技能库真正“活”起来被持续使用和更新而不是变成另一个被遗忘的“垃圾堆”就需要精心的设计。我的核心思路围绕三个关键词模块化、场景化、可维护。2.1 模块化原子能力与组合艺术mao-skill拒绝将所有代码扔进一个巨大的utils.js文件。我采用彻底的模块化设计每个独立的“技能”都是一个自包含的模块。这里的“模块”可以是一个独立的函数、一个 React/Vue 组件、一个完整的 Node.js 脚本、一个 Webpack 配置片段甚至是一套 Dockerfile 模板。模块划分的原则是“单一职责”和“高内聚低耦合”。例如一个处理“日期格式化”的函数模块就只关心日期转换这一件事它不应该同时去处理网络请求。一个“防抖/节流”的 Hooks以 React 为例就只封装这个行为逻辑而不掺杂具体的业务 API 调用。这样做的好处显而易见易于查找和引用我需要日期功能就去date目录下找一目了然。易于测试每个模块功能纯粹可以编写非常聚焦的单元测试。易于更新和替换当有更好的实现方案时我可以单独替换这个模块而不会牵一发而动全身。在项目结构上我通常会按功能域进行一级分类例如/src /browser # 浏览器端相关技能DOM操作、事件、存储等 /node # Node.js 后端相关技能文件处理、进程、日志等 /framework # 框架相关React Hooks, Vue Composables, 通用组件 /algorithm # 常用的算法与数据结构实现 /network # 网络请求相关封装、拦截器、错误处理 /tool # 构建工具、代码质量工具配置片段 /config # 各类配置文件模板.eslintrc, .prettierrc, docker-compose.yml等2.2 场景化从抽象代码到具体解决方案单纯的工具函数是冰冷的。为了让技能库更“好用”我特别强调场景化。每个模块不仅提供代码更会通过清晰的文档和示例说明它诞生的背景、解决的具体问题以及适用的边界条件。例如我有一个叫做useAsyncData的 React Hook。在文档里我不会只写“这是一个用于异步数据获取的 Hook”。我会这样描述场景在管理后台中80%的页面都需要加载列表数据并处理加载中、成功、失败三种状态。重复编写useState和useEffect来管理这些状态非常繁琐且容易出错。解决方案useAsyncData封装了完整的异步状态管理逻辑你只需要传入一个异步函数如 API 调用它会返回{ data, loading, error, execute }对象。示例附上一个完整的、可运行的代码示例展示如何在真实组件中使用它来获取用户列表边界与注意此 Hook 默认在组件挂载时立即执行可通过参数禁用错误处理为简单控制台打印生产环境建议结合全局错误监控接入。这种场景化的描述能让使用者包括未来的我自己快速理解这个模块的“用武之地”降低学习和接入成本。它连接了抽象的代码能力和具体的业务需求。2.3 可维护性文档、测试与版本约定一个缺乏维护的技能库很快就会腐化。我为自己设定了三条铁律代码即文档文档必更新每个模块都必须有对应的README.md或 JSDoc 注释。文档需包含功能描述、API 说明、使用示例、参数详解、返回值说明、注意事项。任何代码修改必须同步更新文档。我甚至会将一些复杂的逻辑流程图或决策树画在文档里。测试是信心的基石对于核心的工具函数和组件必须编写单元测试。使用 Jest、Vitest 等框架确保模块在各种边界条件下的行为符合预期。这不仅能防止回归也是对外部使用者或未来的自己的一种质量承诺。测试覆盖率不是唯一目标但关键路径必须覆盖。简单的版本与变更管理虽然mao-skill可能不对外发布到 npm但我内部仍会使用CHANGELOG.md和语义化版本SemVer的思想来管理变更。例如修复一个 bug 就递增修订号0.0.1 - 0.0.2新增一个向后兼容的功能就递增次版本号0.0.2 - 0.1.0。这有助于追踪变化并在引用时明确依赖的版本。3. 核心模块解析与实现要点下面我挑选mao-skill中几个具有代表性的模块类别深入解析其设计思路和实现中的关键细节。这些模块覆盖了前端开发的常见痛点。3.1 网络请求层的深度封装几乎每个应用都需要与后端 API 交互。与其在每个项目中重复编写fetch或axios调用不如在技能库中沉淀一套强大的网络请求层。核心设计目标统一配置基础 URL、超时时间、请求头如认证 Token等集中管理。统一拦截在请求发出前、响应返回后注入通用逻辑如加载状态管理、Token 刷新、错误统一提示。简化调用提供类似get,post,put,delete的语义化方法支持 TypeScript 类型提示。错误处理标准化将网络错误、业务逻辑错误、认证错误等进行分类处理避免到处写try-catch。实现要点与避坑指南我选择以axios为基础进行封装因为它功能全面、拦截器机制完善。// 示例src/network/request.js import axios from axios; // 1. 创建实例统一配置 const service axios.create({ baseURL: process.env.VITE_API_BASE_URL, // 从环境变量读取 timeout: 15000, }); // 2. 请求拦截器 - 注入认证信息 service.interceptors.request.use( (config) { const token localStorage.getItem(access_token); if (token) { config.headers.Authorization Bearer ${token}; } // 可以在这里统一添加其他header如语言标识 config.headers[Accept-Language] getCurrentLanguage(); return config; }, (error) { return Promise.reject(error); } ); // 3. 响应拦截器 - 统一处理错误和数据结构 service.interceptors.response.use( (response) { // 假设后端统一返回格式为 { code: 0, data: {}, message: success } const res response.data; if (res.code 0) { return res.data; // 直接返回业务数据剥离外层包装 } else { // 业务逻辑错误如参数错误、权限不足 // 此处可触发一个全局的消息提示 showMessage(res.message || 业务错误, error); return Promise.reject(new Error(res.message || Error)); } }, (error) { // 网络错误或HTTP状态码错误4xx, 5xx if (error.response) { switch (error.response.status) { case 401: // Token过期跳转登录页或尝试刷新Token handleUnauthorized(); break; case 403: showMessage(无权访问, error); break; case 500: showMessage(服务器内部错误, error); break; default: showMessage(请求错误: ${error.response.status}, error); } } else if (error.request) { // 请求发出但没有收到响应网络断开、超时 showMessage(网络连接异常请检查网络, error); } else { // 请求配置出错 showMessage(请求配置错误, error); } return Promise.reject(error); } ); // 4. 封装便捷的请求方法 export const get (url, params, config {}) service.get(url, { params, ...config }); export const post (url, data, config {}) service.post(url, data, config); // ... 其他 put, delete 等方法 export default service;实操心得不要过度封装拦截器里的逻辑要保持简洁。像“Token刷新”这种涉及异步状态和重试的复杂逻辑最好抽离成独立的函数或类在拦截器中调用避免拦截器代码膨胀。区分错误类型网络层错误如超时和业务层错误如“用户名已存在”的处理方式通常不同。在拦截器中做好分类方便上层业务代码做差异化处理。类型安全如果使用 TypeScript可以为get、post等方法添加泛型以精确推断返回数据的类型例如getUserProfile(/api/user)。3.2 状态管理辅助 Hook以 React 为例React Hooks 极大地提升了函数组件的表达能力。在mao-skill中我封装了一系列基于 Hooks 的“状态管理增强包”用于解决特定模式下的重复代码问题。典型模块useLocalStorage这是一个将状态同步到localStorage的 Hook用于持久化用户偏好设置如主题、表格分页大小。// 示例src/framework/react/useLocalStorage.js import { useState, useEffect } from react; function useLocalStorage(key, initialValue) { // 1. 初始化状态尝试从 localStorage 读取 const [storedValue, setStoredValue] useState(() { if (typeof window undefined) { // 服务端渲染SSR环境直接返回初始值 return initialValue; } try { const item window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (error) { console.error(Error reading localStorage key ${key}:, error); return initialValue; } }); // 2. 返回一个包装过的 setter在更新状态的同时写入 localStorage const setValue (value) { try { // 允许值是一个函数类似于 useState 的 setState const valueToStore value instanceof Function ? value(storedValue) : value; setStoredValue(valueToStore); if (typeof window ! undefined) { window.localStorage.setItem(key, JSON.stringify(valueToStore)); } } catch (error) { console.error(Error setting localStorage key ${key}:, error); } }; // 3. 可选监听其他标签页对同一 key 的修改 useEffect(() { if (typeof window undefined) return; const handleStorageChange (e) { if (e.key key e.storageArea window.localStorage) { try { setStoredValue(e.newValue ? JSON.parse(e.newValue) : initialValue); } catch (parseError) { console.error(Error parsing new value for key ${key}:, parseError); } } }; window.addEventListener(storage, handleStorageChange); return () window.removeEventListener(storage, handleStorageChange); }, [key, initialValue]); return [storedValue, setValue]; } export default useLocalStorage;使用示例function ThemeToggle() { const [theme, setTheme] useLocalStorage(app-theme, light); const toggleTheme () setTheme(prev prev light ? dark : light); return button onClick{toggleTheme}当前主题{theme}/button; } // 刷新页面后主题状态依然保持注意事项序列化localStorage只能存字符串所以要用JSON.stringify和JSON.parse。这意味着你存储的值必须是可序列化的不能存函数、DOM 元素等。错误处理localStorage的操作可能会因为用户禁用、存储空间满等原因失败必须用try-catch包裹避免整个应用崩溃。SSR 兼容在 Next.js 等 SSR 框架中window对象在服务端不存在。初始化时必须做环境判断否则会报错。性能监听storage事件步骤3是一个可选的高级功能用于实现多标签页状态同步。如果不需要此功能可以移除相关代码以简化 Hook。3.3 工具函数日期与字符串处理日期和字符串处理是业务开发中的高频需求。与其每次遇到都去搜索“JavaScript 格式化日期”不如在技能库中准备好一套趁手的工具。日期处理模块设计 我通常会引入一个轻量级的库如dayjs作为基础然后封装我们业务中最常用的格式。// 示例src/browser/date.js import dayjs from dayjs; import relativeTime from dayjs/plugin/relativeTime; import dayjs/locale/zh-cn; // 配置 dayjs 插件和语言 dayjs.extend(relativeTime); dayjs.locale(zh-cn); /** * 格式化日期时间 * param {string|Date|dayjs.Dayjs} date - 输入日期 * param {string} format - 格式字符串默认 YYYY-MM-DD HH:mm:ss * returns {string} 格式化后的字符串 */ export function formatDateTime(date, format YYYY-MM-DD HH:mm:ss) { return dayjs(date).format(format); } /** * 格式化日期仅日期部分 * param {string|Date|dayjs.Dayjs} date - 输入日期 * returns {string} 格式为 YYYY-MM-DD */ export function formatDate(date) { return dayjs(date).format(YYYY-MM-DD); } /** * 获取相对时间例如3小时前2天前 * param {string|Date|dayjs.Dayjs} date - 输入日期 * returns {string} 相对时间描述 */ export function fromNow(date) { return dayjs(date).fromNow(); } /** * 计算两个日期之间的天数差 * param {string|Date|dayjs.Dayjs} date1 - 日期1 * param {string|Date|dayjs.Dayjs} date2 - 日期2 * returns {number} 天数差date2 - date1可正可负 */ export function diffInDays(date1, date2) { return dayjs(date2).diff(dayjs(date1), day); }字符串工具模块示例// 示例src/browser/string.js /** * 将字符串转换为驼峰命名camelCase * param {string} str - 输入字符串例如 hello-world 或 hello_world * returns {string} 驼峰命名字符串例如 helloWorld */ export function toCamelCase(str) { return str.replace(/[-_\s](.)?/g, (_, c) c ? c.toUpperCase() : ); } /** * 安全地截断字符串并在末尾添加省略号 * param {string} str - 输入字符串 * param {number} length - 最大允许长度 * param {string} suffix - 后缀默认为 ... * returns {string} 截断后的字符串 */ export function truncate(str, length, suffix ...) { if (str.length length) return str; return str.substring(0, length - suffix.length) suffix; } /** * 生成一个随机的、包含数字和字母的字符串可用于ID等 * param {number} length - 字符串长度 * returns {string} 随机字符串 */ export function generateRandomString(length 8) { const chars ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789; let result ; for (let i 0; i length; i) { result chars.charAt(Math.floor(Math.random() * chars.length)); } return result; }实操心得依赖管理对于日期处理直接使用原生Date对象会面临很多时区、格式化等坑。引入一个像dayjs这样体积小、API 友好的库是明智的选择。但务必在技能库文档中说明此依赖并注明版本。函数纯净性工具函数应该是纯函数相同的输入永远得到相同的输出且不产生副作用。这保证了它们的可靠性和可测试性。文档与类型每个工具函数都必须有清晰的 JSDoc 注释说明参数、返回值和示例。如果使用 TypeScript要定义明确的接口类型。4. 技能库的工程化与持续集成一个仅供个人使用的代码库也需要基本的工程化保障以确保代码质量和使用的便捷性。4.1 项目初始化与构建配置我使用pnpm作为包管理器因其高效和节省磁盘空间并配置了TypeScript、ESLint、Prettier和Jest。关键配置文件示例package.json脚本部分{ scripts: { dev: vitest, // 使用 Vitest 进行测试和开发监听 build: tsc vite build, // 构建为多种格式UMD, ESM, CJS lint: eslint src --ext .js,.jsx,.ts,.tsx, lint:fix: eslint src --ext .js,.jsx,.ts,.tsx --fix, format: prettier --write \src/**/*.{js,jsx,ts,tsx,json,css,md}\, test: vitest run, test:coverage: vitest run --coverage, prepublishOnly: npm run lint npm run test npm run build } }vite.config.ts或rollup.config.js用于将源码打包成可供其他项目直接引用的库格式如dist目录下的index.js,index.esm.js等。需要配置好外部依赖如react,dayjs避免将它们打包进最终的 bundle。4.2 文档生成与示例项目代码写得好更要让人会用。我使用TypeDoc或VitePress来为技能库生成静态文档网站。TypeDoc非常适合为 TypeScript 项目自动生成 API 文档。它会根据源码中的注释生成详细的类、接口、函数说明。VitePress更灵活可以编写更丰富的教程、指南和示例。我通常会建立一个docs目录用 VitePress 搭建一个迷你站点包含“快速开始”、“模块指南”、“在线示例”等章节。此外在项目根目录下创建一个examples文件夹里面放几个小型的、可运行的示例项目例如一个使用mao-skill的简单 React 应用或 Node.js 脚本。这是最直观的“使用说明书”。4.3 私有 npm 仓库与自动化为了让技能库能在不同项目间无缝使用最好的方式是将其发布为 npm 包。如果代码涉密或不想公开可以搭建私有 npm 仓库如 Verdaccio或使用 GitHub Packages、GitLab Package Registry 等。结合 GitHub Actions 或 GitLab CI可以实现自动化流程代码推送触发自动运行lint和test。打 Tag 触发发布当推送一个类似v1.0.0的 git tag 时自动运行构建命令并将打包好的库发布到私有仓库。生成更新日志使用standard-version或release-it等工具根据Conventional Commits规范自动生成CHANGELOG.md并升级版本号。这套自动化流程将维护成本降到最低让我可以专注于编写和整理新的“技能”。5. 常见问题与实战经验在建设和使用mao-skill的过程中我踩过不少坑也总结了一些经验。5.1 如何决定一个功能是否应该放入技能库这是一个关键问题避免技能库变成杂物间。我的判断标准是“三次原则”重复出现同一个或类似的代码在三个或以上不同的项目或地方出现过。逻辑独立这段代码有清晰的输入和输出不严重依赖特定项目的业务上下文。经过验证这段代码已经在生产环境或重要项目中稳定运行过排除了明显的 bug。如果满足这三点它就是一个很好的候选值得被抽象、完善后放入技能库。5.2 技能库的版本管理与项目依赖当技能库更新后如何同步到所有使用它的项目中这需要谨慎处理。语义化版本严格遵守 SemVer。破坏性更新API 变更升级主版本号新增功能升级次版本号Bug 修复升级修订号。项目中的依赖声明在业务项目的package.json中使用版本范围而非固定版本。例如mao-skill: ^1.2.0表示允许自动升级到1.x.x的最新版只要主版本号不变。对于破坏性更新主版本升级需要我手动去项目里评估和修改。建立更新日志习惯每次发布新版本务必在CHANGELOG.md中清晰写明新增、变更、修复的内容以及可能的迁移指南特别是主版本升级时。5.3 处理技能库与业务逻辑的耦合有时一个“技能”会不可避免地需要一些业务配置比如请求拦截器里需要知道如何获取当前用户的 Token。处理不好技能库就会和具体业务强耦合。解决方案是“依赖注入”或“配置化”不要将具体的业务逻辑如localStorage.getItem(token)硬编码在技能库模块里。改为接收一个函数或配置对象作为参数。例如创建请求实例时传入一个getToken函数。在技能库中提供一个默认的、简单的实现但允许使用者覆盖它。// 技能库中的设计 export function createRequestClient(options {}) { const { getToken () localStorage.getItem(token), baseURL } options; const instance axios.create({ baseURL }); instance.interceptors.request.use(config { const token getToken(); if (token) config.headers.Authorization Bearer ${token}; return config; }); return instance; } // 在具体业务项目中使用 import { createRequestClient } from mao-skill; const request createRequestClient({ baseURL: https://api.myapp.com, getToken: () store.state.user.token // 注入业务特定的获取Token方式 });5.4 技能库的“保鲜”与重构技术栈在迭代最佳实践在变化。技能库不能一成不变。定期回顾每季度或每半年花点时间通读一遍技能库的代码看看是否有过时的 API比如基于componentWillReceiveProps的代码、是否有新的语言特性如可选链?.、空值合并??可以简化现有代码、是否有更好的第三方库可以替代现有实现。渐进式重构发现需要改进的地方不要一次性推翻重来。可以创建一个新的、更优的模块如useLocalStorageV2在文档中标记旧模块为“已弃用”并说明迁移到新模块的方法。给旧项目留出迁移时间。收集反馈如果在团队内共享鼓励同事提出改进建议或提交 Pull Request。一个人的视野总是有限的集体的智慧能让技能库更健壮。构建和维护mao-skill这样的个人技能库是一个典型的“磨刀不误砍柴工”的过程。初期需要投入时间进行抽象和封装但长期来看它带来的开发效率提升、代码质量保证和知识沉淀的价值是巨大的。它不仅仅是一个代码仓库更是一个开发者成长路径的映射和核心竞争力的组成部分。当你面对一个新需求能从容地从自己的工具箱里挑选出合适的“武器”时那种感觉是非常棒的。