自制XSF工具库实战:模块化配置、按需引入与示例 项目标题里的“自制”两个字往往比库本身更能说明问题。但凡一个人动手写自己的库通常不是市面上没有可用方案而是现成方案在某几个具体场景里总是差一口气要么体积太大要么依赖链太深要么某个边界行为怎么调都别扭。XSF 就是在这种背景下长出来的东西——一个体量克制、模块边界清晰、能按需取用的自制工具库。它不试图做成“全家桶”而是把日常开发里反复出现的字符串清洗、时间格式化、表单校验、请求重试、内存缓存这几类琐碎活儿收拢到一处让调用方少写点重复代码。这篇内容会把 XSF 从“它是什么”讲到“怎么下载”、再到“工程里怎么配置”、最后落到“五个能直接跑起来的简易示例”中间穿插我自己在接入和维护过程中遇到的具体问题。适合手里正攒着自己的工具集、或者准备把一个散落在各项目里的 util 目录整理成正式库的人看也适合刚接触模块化工程配置、想弄明白 exports 字段到底怎么回事的读者。1. XSF 这个自制库到底解决了什么问题1.1 三个重复劳动的瞬间先说清楚动机否则后面讲下载和配置都会显得没头没尾。XSF 的诞生不是因为有什么高深的技术追求纯粹是被三件反复发生的小事磨出来的。第一件事是字符串处理。几乎每个项目里都有一份叫utils.js或者helpers.ts的文件里面躺着几个功能高度相似但实现略有差异的函数有的负责去掉首尾空白有的负责把全角字符转半角有的负责把手机号中间四位打码。这些函数在不同项目里被复制粘贴了七八遍改一个 bug 要改七八个地方还经常漏掉。第二件事是时间格式化。原生日期对象的方法用起来啰嗦社区方案又往往带着完整的时区数据库一个只需要把时间戳转成“2024-06-01 12:30”的小需求最后可能引入几百 KB 的依赖。第三件事是请求封装。超时、重试、取消这三个需求在任何有网络请求的项目里都会出现但每次都要从头写一遍状态判断写完还得自己测。这三件事有一个共同特征逻辑不复杂但细节多容易写错且高度可复用。XSF 的定位就是把这类“不复杂但容易写错”的东西沉淀下来做成一个可以跨项目直接取用的库。它的名字是内部代号取的是“小型、够用”的意思没有特别的含义读者完全可以在自己的项目里换成任何你喜欢的名字。1.2 模块划分与能力边界XSF 目前的模块划分是六个每个模块单独导出一个入口互不依赖。这个“互不依赖”是刻意设计的后面讲打包体积的时候会看到它的价值。模块入口路径主要能力体积量级字符串xsf/string清洗、脱敏、模板填充、全半角转换约 3 KB日期xsf/date格式化、相对时间、时区偏移换算约 4 KB校验xsf/validate组合式规则、异步校验、错误聚合约 5 KB请求xsf/request超时、重试、取消、统一错误结构约 6 KB缓存xsf/cacheTTL 过期、LRU 淘汰、容量上限约 4 KB日志xsf/logger分级输出、环境开关、格式化约 2 KB表里的体积是压缩后未再 gzip 的粗略量级实际数字会随构建配置浮动但相对关系是稳定的请求模块最重因为它要处理状态机日志模块最轻因为它本质上只做字符串拼接和条件判断。能力边界同样要说清楚。XSF 不做 UI 组件不做状态管理不做路由也不做任何与框架强绑定的东西。它只处理纯数据层面的通用逻辑这意味着它可以同时用在浏览器、Node 服务端、甚至某些边缘运行环境里。这条边界是我在做过一次失败尝试之后才划定的早期版本里我加过几个和特定框架生命周期绑定的辅助函数结果每次框架升级大版本库就得跟着改维护成本远超收益。砍掉之后整个库的升级频率从每月一次降到了半年一次。1.3 为什么不做成“大而全”的框架这个问题我被问过不止一次既然都自己写库了为什么不干脆做成一个完整的开发框架把请求、状态、路由、组件全包了理由很实在维护成本随功能数量呈超线性增长。一个只有六个纯函数模块的库我需要关注的是输入输出契约和边界条件一旦引入状态管理和路由我需要关注的是生命周期、并发时序、内存泄漏、服务端渲染兼容性每一项都是持续投入。以我一个人的精力做成大而全的结果大概率是每个部分都半吊子最后连最基础的字符串处理都没人敢用。另一个理由是替换成本。纯函数模块的替换成本极低调用方换掉一个函数只需要改一行引用而框架级的东西一旦被业务代码深度绑定想换就是伤筋动骨。把库做小反而让它在项目里的存活时间更长。这个判断在后面讲版本号策略时还会再提一次因为它直接决定了 XSF 的版本演进方式。2. 把 XSF 拿到本地四种下载路径与适用场景2.1 包管理器直装最省事也最容易埋雷常规做法是在项目根目录执行安装命令指定版本号而非使用默认的最新版本标签。npm install xsf-toolkit1.4.2 --save-exact这里的--save-exact是有意加的。默认安装会把版本写成带脱字符的形式^1.4.2意味着下次别人安装时可能拿到 1.5.x。对于一个还在快速迭代的自制库来说次版本号里出现行为变更的概率不低锁定精确版本能避免“我这里好好的同事那里报错”这类最消耗时间的排查。安装完成后先别急着写代码做两件事。第一件是确认实际落地的版本npm ls xsf-toolkit会打印出依赖树里的真实版本如果显示的不是 1.4.2说明有别的依赖把它提升了或者 lock 文件没更新。第二件是看一眼安装体积du -sh node_modules/xsf-toolkit。如果这个数字远超预期比如超过 2 MB大概率是包里误带了源码、测试文件或者 sourcemap这类问题在自制库里非常常见因为发布脚本往往没配好files字段。包管理器安装的优势是版本管理和依赖解析自动化劣势是你对包里到底有什么缺乏控制。所以在正式接入之前我习惯先看一眼包的目录结构确认里面只有构建产物和必要的声明文件没有惊喜。2.2 源码包手工落地断网与内网环境的常规操作有些环境不允许访问公共包仓库比如隔离的构建机、某些客户现场的内网机器。这时候走源码包路线。先在能联网的机器上打包npm pack这条命令会在当前目录生成一个xsf-toolkit-1.4.2.tgz文件本质是一个 tar 归档。把它拷到目标机器然后npm install ./xsf-toolkit-1.4.2.tgz安装本地 tgz 和装远程包在行为上几乎一致区别只是来源。为了让这个过程可追溯我会额外做一步校验在打包机器上计算出文件的哈希值和目标机器上的做比对。# 打包机器 sha256sum xsf-toolkit-1.4.2.tgz # 目标机器 sha256sum xsf-toolkit-1.4.2.tgz两个值一致才能确认传输过程没有损坏。这一步听起来多余但在跨网络传输大文件时静默损坏导致的“安装成功但运行报错”极难排查因为错误信息通常指向模块内部某个奇怪的位置而不是指向文件损坏。还有一种更轻的方式直接把构建产物目录拷贝进项目作为本地目录依赖引入。{ dependencies: { xsf-toolkit: file:./vendor/xsf-toolkit } }这种方式适合项目数量少、更新频率低的情况。缺点很明显库更新后需要手动同步每个项目的 vendor 目录容易版本漂移。我一般只在一次性交付的项目里用长期维护的项目还是走包管理器。2.3 私有源与版本锁定团队规模稍微大一点之后公共源就不好用了发布要走审批、包名可能被占、内部版本不想公开。这时候搭一个私有源是常规选择。配置方式是在项目根目录建一个.npmrcregistryhttps://registry.example-internal.dev/ //registry.example-internal.dev/:_authToken${XSF_TOKEN} always-authtrue这里把 token 写成环境变量引用而不是硬编码是因为.npmrc经常会不小心被提交进版本库。硬编码的 token 一旦进了提交历史清理起来非常麻烦需要改 token、清历史、通知所有协作者重新拉取。私有源的另一个好处是可以做版本冻结。对于交付类项目我会在私有源上保留一个只读的稳定版本通道所有线上项目只能从这个通道取包新功能走另一个通道。这样即使新版本有问题影响范围也可控。2.4 下载完成后的完整性校验清单不管走哪条路径装完之后我都会跑一遍这个清单。它的作用是尽早暴露问题避免等到写业务代码时才发现库根本没装对。检查项命令或方法异常表现版本是否正确npm ls xsf-toolkit版本与预期不符入口文件是否存在检查包内dist目录目录为空或只有源码类型声明是否齐全查找.d.ts文件只有.js没有声明导出映射是否生效尝试引用子路径入口提示找不到模块体积是否合理查看目录大小明显偏大含测试文件第三项和第四项是最容易出问题的。很多自制库在本地开发时一切正常因为它走的是源码路径而发布之后消费者拿到的是构建产物导出映射一旦写错就会出现“本地能跑、装完就崩”的情况。第 5 章会专门讲这类问题的排查链路。3. 配置环节让 XSF 在你的工程里“看起来像原生代码”3.1 目录骨架与包描述文件的关键字段配置的第一步是把包的描述文件写对。这是整个接入过程中最容易被敷衍、也最容易出问题的一环。下面是一个可用的最小配置字段按重要性排列{ name: xsf-toolkit, version: 1.4.2, type: module, main: ./dist/index.cjs, module: ./dist/index.mjs, types: ./dist/index.d.ts, exports: { .: { types: ./dist/index.d.ts, import: ./dist/index.mjs, require: ./dist/index.cjs }, ./string: { types: ./dist/string/index.d.ts, import: ./dist/string/index.mjs, require: ./dist/string/index.cjs }, ./date: { types: ./dist/date/index.d.ts, import: ./dist/date/index.mjs, require: ./dist/date/index.cjs } }, files: [dist], sideEffects: false }几个字段值得单独说明。type: module声明这个包默认按 ES 模块解析配合exports里的条件映射可以同时支持import和require两种引用方式。exports里每个条件块内types必须放在第一位——解析器是按顺序匹配的如果import写在前面类型声明就可能被跳过消费方会看到一堆隐式 any。files字段决定发布时哪些文件进包。只写dist意味着源码、测试、配置文件都不会被发布。这个字段是控制包体积最有效的手段比事后清理靠谱得多。sideEffects: false是给打包工具的提示告诉它这个包里的模块没有副作用可以安全地做 tree-shaking。这条声明是有前提的如果你的某个模块在导入时就执行了注册、打补丁之类的操作就不能这么写。XSF 全部是纯函数导出所以可以放心声明。3.2 构建产物的三种形态与导出映射导出映射写对之后还有一件事要确认构建工具真的产出了三种形态。形态文件后缀使用方关键要求ESM.mjs现代打包器、浏览器原生模块保留 import 语句CJS.cjs旧版 Node、部分工具链需要正确的 exports 赋值类型声明.d.ts编辑器、类型检查与源码结构一一对应ESM 和 CJS 并存的意义在于兼容性。有些老项目还在用require有些新项目已经全面 ESM一个包如果只提供一种总会有一半人用不了。代价是构建配置更复杂需要在打包工具里配两个输出目标。这里有个容易忽略的细节CJS 产物的导出方式。ESM 里写export function parseDate() {}构建成 CJS 后应该变成exports.parseDate ...而不是把整个模块对象挂到module.exports上。前者支持具名导入后者只能整体引入。如果构建配置没处理好消费方写const { parseDate } require(xsf-toolkit/date)会拿到 undefined。类型声明文件的位置也要和导出映射对齐。上面的例子里./string子路径对应的是dist/string/index.d.ts如果实际构建产出的是dist/string.d.ts映射就失效了。这种错位在本地开发时不容易发现因为编辑器可能通过其他途径找到了声明文件但换一台机器就暴露了。3.3 路径别名与类型声明的对齐项目里通常会配路径别名让引用看起来更干净。别名配置有一个坑打包工具认的别名和类型检查认的别名是两套配置必须同时改。// tsconfig.json { compilerOptions: { baseUrl: ., paths: { xsf/*: [node_modules/xsf-toolkit/dist/*] } } }// vite.config.js import { defineConfig } from vite; import path from node:path; export default defineConfig({ resolve: { alias: { xsf/string: path.resolve(__dirname, node_modules/xsf-toolkit/dist/string/index.mjs) } } });两边不一致时的典型症状是编辑器里不报错构建时报“找不到模块”或者反过来构建通过但编辑器满屏红波浪线。原因是 TS 只读pathsVite 只读resolve.alias两者各玩各的。我的做法是尽量少配别名。XSF 的模块路径本身就很短xsf-toolkit/string已经足够清晰加一层xsf/string的收益有限反而多了一处需要同步维护的配置。只有当项目里引用频次非常高、路径又特别长的时候别名才划算。3.4 环境变量与运行时分流的配置细节XSF 的日志模块和请求模块都有环境相关的行为开发环境下日志级别更详细生产环境下请求重试次数更保守。这些行为通过配置项传入而不是库内部读环境变量。import { createLogger } from xsf-toolkit/logger; const logger createLogger({ level: import.meta.env.DEV ? debug : warn, prefix: [app] });这样设计的原因是可测试性。如果库内部直接读process.env.NODE_ENV单元测试就没法覆盖不同环境下的分支只能靠改全局变量来模拟测试之间还会互相污染。把环境判断交给调用方库本身保持纯粹测试就好写得多。请求模块的配置同理import { createClient } from xsf-toolkit/request; const client createClient({ timeout: 8000, retry: { times: 2, delay: 300, backoff: 2 }, onError: (err) logger.error(request failed, err) });backoff: 2表示每次重试等待时间翻倍即 300ms、600ms。这个参数在小规模重试时够用重试次数超过三次就该考虑是不是接口本身有问题而不是继续加大等待时间。4. 跑起来才算数五个从简到繁的示例4.1 示例一三行代码完成字符串清洗先从最简单的开始验证安装和引用链路是否通畅。import { clean, mask } from xsf-toolkit/string; const raw Hello ; console.log(clean(raw)); // Hello,WORLD console.log(mask(13800001234)); // 138****1234clean做三件事去首尾空白、全角转半角、中文标点转英文标点。这三步的顺序有讲究。如果先转标点再去空白全角空格会被转成普通空格但此时已经过了去空白阶段结果里会残留空格。所以正确顺序是先处理字符宽度和标点最后去空白。这个顺序在文档里不一定写但用的时候如果发现结果多出空格大概率就是顺序理解错了。mask的第二个参数可以指定保留位数和掩码字符console.log(mask(13800001234, { keep: 3, char: # })); // 138####1234默认保留前三位后四位是因为这个组合覆盖了绝大多数手机号和证件号的展示需求。如果你的场景需要保留中间几位直接传参即可不用改库。4.2 示例二时间格式化与时区处理时间模块是使用频率最高的部分也是最容易出错的。import { format, fromNow, toUTC } from xsf-toolkit/date; const ts 1717214400000; console.log(format(ts, YYYY-MM-DD HH:mm:ss)); console.log(format(ts, YYYY年M月D日)); console.log(fromNow(Date.now() - 3600 * 1000)); // 1小时前格式化令牌用的是常见的大小写区分规则MM是月份mm是分钟HH是 24 小时制hh是 12 小时制。这个规则不是 XSF 独创的而是沿用了社区里最广泛的那套约定目的是降低切换成本。如果你之前用过别的格式化库基本可以无缝迁移。时区处理要特别小心。format默认按运行环境的本地时区输出这在浏览器里通常没问题但在服务端就有风险同一份代码在开发机东八区和服务器UTC上会输出不同的字符串。// 明确指定时区偏移避免环境差异 console.log(format(ts, YYYY-MM-DD HH:mm, { offset: 8 })); // 或者先转成 UTC 再格式化 console.log(format(toUTC(ts), YYYY-MM-DD HH:mm));我在一个报表项目里踩过这个坑本地测试时日期都是对的部署到服务器后所有报表的日期整体偏了一天。排查了半天才意识到服务器是 UTC 时区凌晨的数据被算到了前一天。从那以后凡是涉及日期边界的地方我一律显式传offset不依赖环境默认值。4.3 示例三表单校验的组合式写法校验模块的设计思路是组合而非继承。规则是独立的函数通过数组组合成校验链。import { validate, rules } from xsf-toolkit/validate; const schema { username: [ rules.required(用户名不能为空), rules.minLength(3, 至少 3 个字符), rules.pattern(/^[a-zA-Z0-9_]$/, 只能包含字母、数字和下划线) ], phone: [ rules.required(手机号不能为空), rules.pattern(/^1[3-9]\d{9}$/, 手机号格式不正确) ] }; const result validate({ username: ab, phone: 123 }, schema); console.log(result.valid); // false console.log(result.errors); // { username: 至少 3 个字符, phone: 手机号格式不正确 }几个设计上的取舍值得说明。第一校验遇到第一个错误就停不会继续跑后面的规则。这样做的好处是错误信息干净用户一次只需要改一个问题坏处是没法一次性列出所有问题。如果你需要全量校验可以在规则数组外面包一层配置const result validate(data, schema, { all: true });第二异步校验通过返回 Promise 的规则函数实现validate会自动识别并等待。第三错误信息的结构是“字段名 → 第一条错误”而不是数组。这个选择是基于实际使用反馈做的绝大多数表单在字段旁边只显示一条错误返回数组反而每次都要取第一个元素平白多一层代码。4.4 示例四带重试与超时的请求封装请求模块封装的是一个带状态机的调用过程不是简单的 fetch 包装。import { createClient } from xsf-toolkit/request; const client createClient({ timeout: 5000, retry: { times: 2, delay: 200, backoff: 2 }, retryOn: (err) err.status 500 || err.status 429 }); const res await client.get(/api/list, { params: { page: 1 } });retryOn这个配置是关键。默认的重试策略只对网络错误和 5xx 生效不会对 4xx 重试——因为 4xx 是客户端问题重试一百次结果也一样只会加重服务端负担。429 是例外它表示限流等一会儿再试是有意义的。取消请求的用法const controller client.createAbort(); try { await client.get(/api/slow, { signal: controller.signal }); } catch (err) { if (err.name AbortError) { console.log(已取消); } } // 需要时取消 controller.abort();这里有个细节重试过程中如果发生取消正在等待的重试也要一并终止。实现上是在每次重试前检查 signal 状态如果已取消就直接抛出 AbortError不再进入下一轮。这个逻辑如果漏了用户点了取消按钮之后请求还会在后台继续跑两轮看起来像没生效。4.5 示例五内存缓存的过期与容量控制缓存模块实现的是带 TTL 和容量上限的内存缓存适合缓存接口结果、计算结果这类场景。import { createCache } from xsf-toolkit/cache; const cache createCache({ ttl: 60 * 1000, max: 100 }); cache.set(user:1, { name: A }); console.log(cache.get(user:1)); // { name: A } // 60 秒后自动失效 setTimeout(() { console.log(cache.get(user:1)); // undefined }, 61 * 1000);max: 100表示最多存 100 条超出时按最近最少使用淘汰。这里有个实现细节容易出问题TTL 到期和容量淘汰是两套逻辑如果只在读取时检查过期那么长期不读的键会一直占着位置最终把缓存撑满触发淘汰时淘汰的可能是刚写入的新键。XSF 的做法是在写入时顺带清理一批已过期的键清理是惰性的、分批的不会阻塞主流程。还有一个常见误用把大对象塞进缓存。内存缓存的所有数据都在堆上缓存一个几 MB 的响应体一百条就是几百 MB很容易触发内存告警。我的经验是缓存值控制在几十 KB 以内超过这个量级应该考虑用外部缓存而不是进程内缓存。5. 我踩过的七个坑与现场排查链路5.1 安装成功但运行时报“找不到模块”这是最高频的问题报错信息通常是Cannot find module xsf-toolkit/string。排查链路是有顺序的按这个顺序走基本能在五分钟内定位第一步确认包真的存在。去node_modules/xsf-toolkit看一眼如果目录不存在说明安装根本没成功可能是源配置错了或者网络问题先解决安装。第二步确认目标文件存在。按exports里./string对应的路径去找比如dist/string/index.mjs如果这个文件不在说明构建产物不全是发布环节的问题。第三步检查exports字段是否包含这个子路径。有些库只写了根入口.没写子路径那么xsf-toolkit/string就会被拒绝而xsf-toolkit本身能正常引用。第四步检查大小写。文件系统在 Linux 上区分大小写本地 macOS 不区分所以本地跑得好好的代码在 CI 上会挂这类问题占了相当比例。还有一种情况是缓存导致的假象包已经更新了但构建工具的缓存还指向旧路径。rm -rf node_modules/.vite node_modules/.cache清完之后重跑问题常常就消失了。5.2 类型声明存在却提示 any症状是编辑器里所有来自 XSF 的函数都是any但dist目录里明明有.d.ts文件。根因通常有两个。第一个是exports里的types位置不对放在了import后面。类型解析器按顺序匹配遇到第一个符合条件的就停如果import在前面且匹配成功类型声明那一条就永远不会被读到。修复办法是把types提到每个条件块的第一行。第二个是tsconfig里的moduleResolution设置太旧。设成node时不会读exports字段只认main和types。改成bundler或node16才能正确解析条件导出。{ compilerOptions: { moduleResolution: bundler } }改完之后如果还是 any重启一下编辑器的语言服务。TS 的语言服务有缓存配置变更后不一定立刻生效这个细节坑过我不止一次。5.3 打包体积翻倍的真正原因现象是接入 XSF 之后构建产物的体积涨了远超预期明明只用了字符串模块的三个函数。先确认sideEffects: false写了没有。没有这条声明打包工具不敢做 tree-shaking会把整个包都打进去。再确认引用方式是具名导入而不是整体导入// 可以摇掉未使用的导出 import { clean } from xsf-toolkit/string; // 摇不掉整个模块都会进包 import string from xsf-toolkit/string;第三点最隐蔽检查构建产物里是不是同时打进了 ESM 和 CJS 两份。有些打包配置在解析时优先命中了main而不是module导致 CJS 版本被引入而 CJS 的静态分析能力弱tree-shaking 效果大打折扣。解决办法是在构建配置里显式指定优先字段// 以 Vite 为例 export default { resolve: { mainFields: [module, main] } };我遇到过一次体积涨了三倍的情况最后查出来是某个间接依赖同时引入了 ESM 和 CJS 两份 XSF原因是两个库用了不同的解析配置。这类问题只能靠产物分析工具看出来肉眼看依赖列表是发现不了的。5.4 循环依赖引发的初始化顺序问题这个坑最难受因为报错信息完全不指向真正的原因。症状是某个函数在启动时报 undefined但代码里明明导入了。原因是模块之间存在循环引用A 模块导入 BB 又导入 A。在 ESM 里循环引用不一定报错但被循环引用的一方在初始化阶段可能还没完成赋值拿到的是 undefined。XSF 的模块设计上刻意避免了相互依赖六个模块之间零耦合。但消费方的代码可能引入循环比如业务模块 A 导入了xsf/request而传给createClient的onError回调里又引用了业务模块 A 的某个方法。这种循环发生在业务层库本身没法控制。排查方法是看构建工具的警告。大多数打包器在遇到循环依赖时会打印警告只是很容易被淹没在其他输出里。定位到具体的循环之后常见的解法是把共享部分抽到第三个模块或者把回调改成延迟引用onError: (err) { // 延迟到调用时再解析避免初始化阶段的循环 import(./handler.js).then((m) m.handleError(err)); }这个改法的代价是错误处理变成异步的如果你需要同步处理那就只能老老实实拆模块。6. 把 XSF 当作长期资产版本、测试与扩展6.1 版本号怎么定才不折腾自己自制库最常见的死法不是没人用而是版本号乱到自己都记不清哪个版本改了什么。XSF 遵循的规则很简单修 bug 升补丁位加功能升次版本改行为升主版本。关键在于什么叫“改行为”。我的判断标准是如果调用方需要改代码才能保持原有输出就算改行为。举个例子把一个函数的默认参数从false改成true虽然函数签名没变但输出变了这就得升主版本。主版本升得勤不一定是坏事但每次升主版本都要写迁移说明说明里要写清楚“改了什么、为什么改、怎么改”。这份说明的价值在半年后体现得最明显——那时候你自己都忘了当初为什么那么改。版本号还有一条实践不要用0.x太久。0.x阶段意味着 API 随时可能变消费方不敢用一旦你决定让库进入可用状态直接发1.0.0把承诺明确下来。这个心理门槛对维护者自己是好事它会逼着你在发布前把 API 想清楚。6.2 最小可用的测试用例集自制库的测试不需要追求覆盖率数字但有几类必须覆盖。第一类是边界值。字符串模块的clean要测空字符串、纯空白、超长字符串日期模块的format要测闰年、月末、跨时区。这类测试写起来枯燥但能拦住绝大多数线上问题。第二类是错误路径。校验失败时返回什么结构、请求超时时抛什么类型的错误这些必须有断言否则调用方没法可靠地处理异常。第三类是回归用例。每修一个 bug就补一个能复现该 bug 的测试这条规则执行下来测试集自然会长成一份问题清单。测试工具用什么不重要重要的是能一条命令跑完npm testCI 里跑同一套。我见过不少自制库的测试只在本地手工跑跑的时候还得改一堆配置结果就是没人跑。6.3 新增模块的扩展流程想往 XSF 里加新模块按这个顺序走能少返工。第一步先在一个实际项目里用它写成业务代码的一部分。用上两三个项目之后如果这段代码几乎没改过说明它的接口稳定了可以提出来。第二步把它整理成独立文件确保不依赖业务上下文所有外部依赖通过参数传入。第三步补测试覆盖边界和错误路径。第四步加导出映射exports里加一条子路径构建配置里加一个入口。第五步发一个次版本在迁移说明里写清楚适用场景和典型用法。这个流程的核心是“先验证再入库”。直接从想法出发写模块写出来的接口大概率要返工而返工的代价在库里比在业务代码里高得多——业务代码改了就改了库的接口改了要通知所有使用方。扩展时还要注意一件事新模块不要引入新的运行时依赖。XSF 目前是零依赖的这条约束让它的安装体积可控、安全审计简单、也不会因为某个间接依赖出问题而被迫发版。如果某个功能非依赖不可优先考虑自己实现一小段而不是引入一个包。最后分享一个实际使用中的小技巧。我习惯在项目里建一个统一的出口文件把 XSF 的引用集中在一处// src/lib/xsf.js export { clean, mask } from xsf-toolkit/string; export { format, fromNow } from xsf-toolkit/date; export { validate, rules } from xsf-toolkit/validate;业务代码只从这个文件引用不直接引xsf-toolkit。这样做的好处是将来如果要换库、或者要包一层自己的默认配置只需要改这一个文件不用全项目搜索替换。这个习惯坚持了两年中途换过一次时间格式化方案改动量只有二十行。