
你有没有经历过这种场景项目里有个 utils 文件夹里面的 isMobile 函数有三四种实现有的用正则有的拿 navigator.userAgent 去判断还有的是三年前从旧项目里直接 copy 过来的。当你想抽个公共模块统一一下时发现这些实现之间还有各种微妙的差异根本不敢随便动。这种“重复造轮子”的混乱状态我忍了很长时间最后下定决心干脆自己从零搭一套可复用的前端通用工具库把常用的函数、组合式 API、通用组件都规范化让以后的新项目能直接装上用不用再到处翻历史代码。这篇文章我会基于 Vue3 JavaScript配合 Vite 的库模式从 0 开始搭建这样一个工具库完整展示项目的目录结构、核心代码实现、打包配置、单元测试和发布流程。全程用 JS 而不是 TypeScript主要照顾到团队里很多工程还是纯 JS 技术栈不想为了一个工具库额外引入类型编译成本。适合那些想把手头零散的 utils 方法沉淀成正式 npm 包的开发者也适合想搞懂 Vite 库模式到底怎么用的人。读完你不仅能跑通整个流程还能避开我在实际搭建中踩过的几个坑。1. 先想清楚通用工具库究竟要解决谁的什么问题1.1 工具库解决的是“重复成本”而非“炫技”很多同学一提到自建工具库第一反应是“我要把 lodash 重新写一遍”其实不是这么回事。工具库的核心价值是降低重复成本这个成本包括三部分写代码的时间、找代码的时间、维护代码的时间。我见过一个团队内部有三个不同的项目都写了 formatDate 函数每个函数的输出格式还不完全一样有的支持传入连字符有的只支持斜杠。后来产品要求统一日期展示格式负责的老兄花了整整一个下午去改三套实现改完还不敢保证边界情况全部覆盖。如果当初有一个统一维护的工具函数这个问题就是改一个文件、发一个新版、所有项目升级依赖这么简单。但自建工具库也不是毫无成本你需要配置构建、测试、文档、发布还要持续维护。所以正确的姿势是先判断“值不值得做”。我的判断标准很简单只要存在一个函数或组件连续出现在三个以上项目里并且它们的行为基本一致就值得沉淀进工具库。如果只是当前项目一处用到就先忍住别抽等第二次需要时再抽也不迟。1.2 什么代码适合放进通用工具库适合放进通用工具库的代码有这几个共同特征无业务上下文、依赖外部环境少、可以独立测试、结果可预期。以纯函数为例日期格式化、千分位转换、URL 参数解析、uuid 生成都非常适合。组合式 API 方面useWindowSize、useLocalStorage、useEventListener 这类也是天然的工具库素材。通用组件就更多了比如可定制的高亮文本、懒加载图片、防重复点击按钮等。不适合的典型例子是封装了特定后端接口的 request比如带了某个接口的鉴权头、绑定业务权限的指令、只服务于当前页面布局的巨型组件。这些代码一旦脱离当前业务环境就废了硬塞进工具库只会让维护者为了抽象这些业务逻辑而头疼。还有一个容易忽略的判断维度团队成员的使用习惯。如果你建了一个工具库但其他项目的人根本不用那就是在自娱自乐。所以我在动手之前专门拉了一个清单把日常代码里重复出现的小函数都列出来先统计使用频率再决定首批放哪些工具。这一步看起来简单实际决定了工具库后续的活跃度。另外说一下为什么选 Vue3 JS 而不直接上 TS。我自己项目里有相当一部分是遗留的 JS 代码如果工具库用 TS 写虽然能提供类型声明但构建链路会复杂一截而且会让不愿意碰 TS 的同事产生距离感。Vue3 的组合式 API 本身对 JS 的友好度很高很多逻辑根本不需要类型系统就能表达得很清晰。当然如果你的团队全员 TS那直接用 TS 写会更好——工具库的选型永远跟着团队现状走不是跟着技术潮流走。2. 工程化骨架用 Vite 的库模式搭出可发布的包2.1 初始化项目与基础依赖新建一个工具库项目最省事的方式是用 Vite 的脚手架保留 Vue3 模板。我用的命令是这样的npm create vitelatest my-utils -- --template vue模板带上的 React 之类的东西都可以删掉我们只需要一个干净的 Vite Vue3 环境。因为这个工具库不是业务应用所以入口不再是 index.html而是一个仓库入口文件这点后面会重点说。依赖安装上除了 vue 和 vite我还会加这几个运行时无关的工具npm install -D vitest vue/test-utils eslint prettier husky lint-staged这里稍微解释一下为什么需要这些。vitest 用来跑单元测试因为它是 Vite 生态的测试框架可以复用同一个 vite.config不用单独配一套vue/test-utils 是 Vue 组件测试的官方工具库测组件必用eslint 和 prettier 统一代码风格husky 和 lint-staged 负责在提交前拦截不规范代码。对一个公共工具库来说质量门禁是必须的因为代码一旦发布出去被多个项目依赖任何低级错误都会被无限放大。2.2 Vite 的 lib 模式配置解析Vite 默认构建的是应用会从 index.html 出发生成一堆静态资源。工具库要的是“库模式”——从一个 JS 入口开始把内容编译成可被其他代码 import 的包。配置核心就落在build.lib上。我最终的 vite.config.js 是这样写的import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], build: { lib: { entry: src/index.js, name: MyUtils, formats: [es, cjs, umd], fileName: (format) format es ? my-utils.esm.js : my-utils.${format}.js }, rollupOptions: { external: [vue], output: { globals: { vue: Vue } } } }, test: { environment: jsdom } })这里面的关键点值得好好说说。entry指向的是包入口工具库所有对外暴露的能力都从这个文件导出。formats我选了 es、cjs、umd 三种ESM 适合现代打包器可以触发 tree-shakingCJS 是为了兼容旧版 Node 环境和老式构建器UMD 则是给那些直接在 script 标签里引用的人用的虽然现在已经很少见但加上总没坏处。rollupOptions.external: [vue]是一个特别重要的配置。它告诉 Vite“vue 这个依赖不要打包进我的产物里。”因为工具库里的组件、组合式 API 都依赖 Vue但 Vue 应该由使用方项目提供而不是工具库重复打包一份。如果这里不排除发布出去的包体积会大很多而且可能导致使用方项目里同时存在两份 Vue 实例触发各种奇奇怪怪的问题。这点我在后面第 6 部分还会专门强调。2.3 目录结构设计JS 工具、组合式函数、组件分开维护工具库不是把所有代码堆在一个文件里就完事目录结构直接决定了后续的可维护性。我采用的是一套简单清晰的分层模式my-utils/ ├── src/ │ ├── index.js # 入口统一导出 │ ├── funcs/ # 纯函数工具 │ │ ├── formatDate.js │ │ ├── debounce.js │ │ └── ... │ ├── composables/ # 组合式 API │ │ ├── useWindowSize.js │ │ ├── useLocalStorage.js │ │ └── ... │ └── components/ # 通用 Vue 组件 │ ├── HighlightText/ │ │ ├── index.js │ │ └── HighlightText.vue │ └── ... ├── test/ # Vitest 测试用例 ├── dist/ # 构建产物gitignore ├── package.json ├── vite.config.js └── ...为什么把纯函数、组合式 API、组件分成三个目录因为它们的用法和构建路径完全不同。纯函数是普通 JS 模块可以直接被任何环境加载组合式 API 依赖 Vue 的运行时但不需要渲染组件则必须有 Vue 的渲染器配合。分开后后续做按需引入时非常方便使用者可以精确地只加载一个模块。入口文件src/index.js的作用是把所有能力汇总到一处方便全量引用。同时我也在 package.json 里配好了子路径导出让使用者既能import { debounce } from my-utils全量引入也能import debounce from my-utils/funcs/debounce精准引入这其实就是按需加载的另一种手段后面详细讲。这样一个骨架搭完你就有了一个干净的起点。接下来最核心的事情就是往里面填充真正有用的代码。3. 通用函数、组合式 API、组件三类能力的实现与设计思路3.1 纯函数以 formatDate 和 debounce 为例纯函数是工具库的基本盘。写这些函数的核心原则是输入可预期、输出稳定、不做意外的事。我拿两个最常见的例子展开。第一个是formatDate。很多项目都用 date-fns 或 dayjs但为了一个两个格式化的需求去引整个库其实有点重。自己写一个针对常用格式的轻量函数就够了// src/funcs/formatDate.js export function formatDate(date, fmt YYYY-MM-DD HH:mm:ss) { const d date instanceof Date ? date : new Date(date) if (Number.isNaN(d.getTime())) { throw new TypeError(formatDate received an invalid date) } const pad (n) String(n).padStart(2, 0) const map { YYYY: d.getFullYear(), MM: pad(d.getMonth() 1), DD: pad(d.getDate()), HH: pad(d.getHours()), mm: pad(d.getMinutes()), ss: pad(d.getSeconds()) } return fmt.replace(/YYYY|MM|DD|HH|mm|ss/g, (match) String(map[match])) }我特意没有做很复杂的令牌解析只覆盖了最常见的年月日时分秒为什么这么克制因为工具库的代码要尽可能容易被后续的人读懂功能越多、边界情况越多潜在的风险就越大。当实际需求超出这个范围时直接按 dayjs 或者 date-fns 更合理不要试图自己架一个完整的日期引擎。一个容易被忽略的细节入参定义成date instanceof Date ? date : new Date(date)。这样字符串日期、时间戳都能兼容并且当传入非法值时会显式抛错而不是返回一个又难看又难排查的NaN/NaN/NaN。显式抛错是工具代码里值得推崇的做法——宁可让调用方在开发阶段就发现错误也不要把错误吞掉。第二个例子是debounce这个函数几乎每个项目都自己写过一版但很多版本在“取消”和“立即执行”这两个能力上是缺失的。工具库里的 debounce 应该更完整// src/funcs/debounce.js export function debounce(fn, wait 300, immediate false) { if (typeof fn ! function) { throw new TypeError(expected a function as first argument) } let timer null let result null const debounced function (...args) { return new Promise((resolve) { const later () { timer null if (!immediate) { result fn.apply(this, args) resolve(result) } } const callNow immediate !timer clearTimeout(timer) timer setTimeout(later, wait) if (callNow) { result fn.apply(this, args) resolve(result) } }) } debounced.cancel () { clearTimeout(timer) timer null } return debounced }这里有两个设计点是我从实际项目里总结出来的。第一用 Promise 包一层让调用方可以用await debounced()拿到函数返回结果这对手写版 debounce 来说是不常见的但实际在某些需要等待结果入库的场景特别好用。第二保留.cancel方法这样在组件卸载时能优雅地清理掉还未触发的任务避免在异步回调里触发对已卸载组件的操作。3.2 组合式 API封装一个 useWindowSizeVue3 的组合式 API 非常适合做工具库因为它能把响应式状态、副作用生命周期都管理起来。我用useWindowSize来示范一个标准写法// src/composables/useWindowSize.js import { ref, onMounted, onUnmounted } from vue export function useWindowSize() { const width ref(window.innerWidth) const height ref(window.innerHeight) const update () { width.value window.innerWidth height.value window.innerHeight } onMounted(() { window.addEventListener(resize, update) }) onUnmounted(() { window.removeEventListener(resize, update) }) return { width, height } }为什么用ref而不是reactive因为返回的是一个双成员对象用 ref 包裹后使用方可以优雅地解构const { width, height } useWindowSize()同时这两个值在模板里可以直接渲染不会丢失响应性。而如果用 reactive 包装解构后响应性就断了。监听事件一定要放在onMounted里吗不一定如果你的工具库只会在组件实例中调用那么 onMounted 是安全的。但如果你希望这个函数也能在普通 JS 环境没有组件上下文下使用那就需要做一些兼容处理比如直接用const update () {...}然后立即初始化再返回一个手动销毁的方法。我这里的写法是为绝大多数 Vue 单文件组件场景设计的简单可靠。需要额外说明的是组件的onUnmounted保证了 resize 监听器在组件销毁时被移除这能避免大多数内存泄漏问题。再多说一点工具型的 composable 最重要的是“副作用可控”。类似useWindowSize这种监听一个全局事件就必须在合适时机解绑绝不能让使用方背着内存泄漏的债。3.3 通用组件从最简单的高亮文本组件开始通用组件是工具库里比较容易“膨胀”的部分因为 UI 设计千差万别强行做成通用往往两头不讨好。我建议从没有视觉绑定的功能型组件开始比如高亮文本。它负责接收关键词把匹配到的片段用mark或其他标签包起来至于样式直接由使用方的项目控制。用 Vue3 的 SFC 和 JS 实现如下!-- src/components/HighlightText/HighlightText.vue -- template span classmy-utils-highlight-text template v-for(part, index) in parts :keyindex {{ part.text }}slot v-ifpart.match namehighlight :partpart{{ part.text }}/slot /template /span /template script setup import { computed } from vue const props defineProps({ text: { type: String, required: true }, keyword: { type: String, default: }, caseSensitive: { type: Boolean, default: false } }) const parts computed(() { if (!props.keyword || !props.text) { return [{ text: props.text || , match: false }] } const flags props.caseSensitive ? g : gi const regex new RegExp((${escapeRegExp(props.keyword)}), flags) return props.text .split(regex) .filter((segment) segment ! undefined) .map((segment, index) ({ text: segment, match: regex.test(segment), index })) }) function escapeRegExp(str) { return str.replace(/[.*?^${}()|[\]\\]/g, \\$) } /script组件可以配合插件式注册实现全量引入// src/components/HighlightText/index.js import HighlightText from ./HighlightText.vue export default HighlightText然后在主入口里注册// src/index.js export { default as HighlightText } from ./components/HighlightText/index.js组件本身不绑定任何样式由使用方项目用样式覆盖这是保持通用性的关键。如果你在组件里写死了背景色和字体到每个项目里都会有人抱怨“颜色对不上”。这里的关键词匹配我特意用正则做了转义避免用户输入*或.等正则特殊字符导致匹配行为异常。高亮文本组件规模不大但它演示了工具组件最基本的设计思路props 传入数据、不依赖业务状态、支持插槽做扩展、无内置样式。后续要扩展其他组件时都可以参考这个模式。4. 质量保障ESLint Prettier Vitest 让工具库少踩坑4.1 ESLint 与 Prettier 配置要点工具库代码会被很多人依赖代码风格不统一会直接导致合并冲突和 review 效率低下。我在项目里集成了 ESLint 和 Prettier配置比较简单但够用。// .eslintrc.cjs module.exports { root: true, env: { browser: true, node: true, es2022: true }, extends: [ eslint:recommended, plugin:vue/vue3-essential, prettier ], parserOptions: { ecmaVersion: 2022, sourceType: module }, rules: { vue/multi-word-component-names: off } }vue/multi-word-component-names这个规则默认要求组件名必须是多个单词但对于工具库来说很多通用组件就是HighlightText、DebounceButton这种两词名字也有个别像Button这种基础组件所以关掉比较省心。Prettier 简单配置一个.prettierrc{ semi: false, singleQuote: true, trailingComma: none, printWidth: 100 }因为主要是 JS 文件行宽设 100 足够。把 ESLint、Prettier、Vite 同时跑通的好处是代码风格不再需要人工 review机器帮你兜底。4.2 给工具库补上单元测试Vitest工具库是纯逻辑居多不测一下说不过去。Vitest 和 Vite 深度集成跑测试直接用vitest命令就行。我写几个典型测试// test/formatDate.test.js import { describe, it, expect } from vitest import { formatDate } from ../src/funcs/formatDate describe(formatDate, () { it(formats date correctly, () { const date new Date(2024, 0, 15, 9, 30, 45) expect(formatDate(date)).toBe(2024-01-15 09:30:45) }) it(throws on invalid date, () { expect(() formatDate(not-a-date)).toThrow(TypeError) }) })debounce 的测试需要依赖 fake timersVitest 同样支持// test/debounce.test.js import { describe, it, expect, vi } from vitest import { debounce } from ../src/funcs/debounce describe(debounce, () { it(waits until wait time passes before invoking, () { vi.useFakeTimers() const fn vi.fn() const debounced debounce(fn, 200) debounced() vi.advanceTimersByTime(100) expect(fn).not.toHaveBeenCalled() vi.advanceTimersByTime(100) expect(fn).toHaveBeenCalledTimes(1) vi.useRealTimers() }) })组合式 API 的测试稍微复杂一点需要提供一个宿主环境。Vitest 开了environment: jsdom后我们可以用vue/test-utils挂载一个小组件来验证// test/useWindowSize.test.js import { describe, it, expect, vi } from vitest import { mount } from vue/test-utils import { defineComponent, h } from vue import { useWindowSize } from ../src/composables/useWindowSize describe(useWindowSize, () { it(returns window dimensions, async () { const Comp defineComponent({ setup() { const size useWindowSize() return () h(div, ${size.width.value}x${size.height.value}) } }) window.innerWidth 1024 window.innerHeight 768 const wrapper mount(Comp) expect(wrapper.text()).toBe(1024x768) window.innerWidth 1280 window.dispatchEvent(new Event(resize)) await wrapper.vm.$nextTick() expect(wrapper.text()).toBe(1280x768) wrapper.unmount() }) })这种测试的意义是保证核心函数的行为回归可控。工具库一旦发布任何一次升级都可能影响很多项目没有测试谁敢随便升版本所以我个人的建议是工具库里的每个函数至少一个主流程测试加一个边界测试每个 composable 至少测挂载和销毁组件至少测 props 渲染。4.3 lint-staged husky 拦截不规范的提交有了 ESLint 和测试还需要把它们卡在提交前否则总有人跳过检查。简单搭一个 husky 钩子npx husky init然后配置lint-staged在 package.json 里{ lint-staged: { *.{js,vue}: [eslint --fix, prettier --write] } }同时.husky/pre-commit里写npx lint-staged这样每次提交只会检查暂存区里的文件速度很快也不会逼着大家把所有测试跑一遍。更严谨点的团队可以在 pre-push 阶段跑全量测试但工具库代码量不大所以我在 commit 前直接跑了npm test npm run lint也行。质量门禁的意义不在于设置多少关卡而在于让团队所有人都保持一致的习惯。5. 打包与发布从本地工具库到 npm 公共包5.1 多格式产物与 Tree ShakingVite 库模式在build后会在 dist 目录下生成三个文件my-utils.esm.js、my-utils.cjs.js、my-utils.umd.js。ESM 文件内部使用 ES Module 语法现代打包器Vite、webpack 5、Rollup在解析 import 时可以静态分析导出成员从而剔除未使用的代码这就是 tree-shaking 的基础。要让 tree-shaking 真正生效package.json 里必须明确sideEffects: false意思是这个包里的模块都没有副作用。但组件文件可能包含样式副作用所以如果你有 CSS需要单独处理。我的做法是把组件样式内联到组件里用style scoped这样 Vite 在编译后会把 CSS 抽离成单独的 CSS 文件在 package.json 里通过sideEffects指定 CSS 文件有副作用其他模块没有。目前我们先不引入 CSS保持sideEffects: false是最省心的。5.2 package.json 的关键字段构建产物出来后发布配置是重中之重。我最终的 package.json 关键字段长这样{ name: my-utils, version: 0.1.0, description: A collection of Vue3 JS utilities, composables and components, type: module, main: ./dist/my-utils.cjs.js, module: ./dist/my-utils.esm.js, unpkg: ./dist/my-utils.umd.js, exports: { .: { import: ./dist/my-utils.esm.js, require: ./dist/my-utils.cjs.js }, ./funcs/*: ./dist/funcs/*.js, ./composables/*: ./dist/composables/*.js, ./components/*: ./dist/components/*.js }, files: [dist, README.md], peerDependencies: { vue: 3.2.0 }, sideEffects: false, scripts: { build: vite build, test: vitest run, lint: eslint src test --fix } }几个字段解释一下。main指向 CJS 产物是为了兼容还没有切换到 ESM 的旧 Node 环境。module指向 ESM 产物现代打包器会优先读取。exports字段是 Node.js 12 和现代打包器的新标准它比 main/module 更精细能限制外部访问包内未声明的路径防止别人import my-utils/src/xxx这种奇怪的引用方式。files决定发布时哪些文件会被打进 npm 包这里只包含 dist 和 README能有效避免把源码、测试文件全部发布出去既缩小包体积也避免暴露未编译的内部细节。peerDependencies用来声明这个包依赖的宿主版本——Vue 3.2 及以上但不会把 Vue 安装进来。在发布前建议先本地验证一下npm pack这会生成my-utils-0.1.0.tgz你可以解开看看里面有什么再或者用一个临时项目npm install /path/to/my-utils-0.1.0.tgz实际引用一下确保发布到 npm 后不会出现“装回来发现缺文件”的尴尬。5.3 版本号与更新日志别让发布变成灾难工具库的版本管理我强烈建议走语义化版本SemVer也就是MAJOR.MINOR.PATCH。新增一个向后兼容的函数升 MINOR修复原有函数的 bug升 PATCH如果你改了函数入参、调整了组件 props 的必填项这类破坏性变化一定要升 MAJOR。配合 conventional commits可以在每次提交时自动生成 CHANGELOG。比如feat: add useLocalStorage会被识别为 feature 更新fix: correct formatDate timezone offset会被识别为 bug 修复。我在项目里用了standard-version这个工具来根据提交记录自动生成版本号和 changelog执行npx standard-version就会自动修改 version、生成 CHANGELOG.md、打上 git tag非常省事。不过版本生成的粒度还是要自己控制。工具库的发布频率不宜太高我一般会在一次功能迭代完成后集中发布一版而不是每次 commit 都发。多个项目共用一个工具库的好处也体现在这里——只有你的版本规划做得足够清晰下游项目才能放心地跟进升级。6. 实战中绕不开的坑记住了能省下半天时间6.1 Vue 外部化后别忘了开发依赖里的 vue前面说了打包时要把 Vue 设为 external但很多新手会掉进另一个坑既然 vue 是 peerDependencies是不是项目里就不用装 vue 了不是的。你本地开发工具库时仍然需要 vue 作为 devDependency因为组件和测试都要实际运行。peerDependencies 解决的是“使用方要自己装 vue”而 devDependencies 解决的是“你在开发这个库时需要 vue 可用”。我见过一个例子某位同事把 vue 从 devDependencies 里删掉认为发布时会自动带上结果本地跑测试直接报“Cannot find module ‘vue’”。正确的习惯是vue 同时出现在 peerDependencies 和 devDependencies 里peer 里写宽松范围如3.2.0dev 里写一个明确的版本保证本地开发环境可控。6.2 Tree Shaking 对组件并没有想象中那么灵组件跟纯函数不一样。如果你在主入口 index.js 里一次性导出了 10 个组件然后使用方通过import { Button } from my-utils这样的方式按需引用现代打包器确实可以通过 tree-shaking 把没用到的组件代码丢掉。但这里有个前提组件内部不能被任何模块级副作用触发比如组件文件顶部直接插入全局 CSS 或执行console.log。更让人头疼的是如果使用方项目用 webpack 4 这类对 tree-shaking 支持不完善的老构建器一个入口导出很多组件反而可能导致全部被打进 bundle 里。这时候你就需要子路径导出让使用方可以import Button from my-utils/components/Button这样的精确引用绕开 tree-shaking 的机制限制。这也是我在 package.json 里配置 exports 子路径的初衷——为老旧项目留一条后路。6.3 发布前务必先本地联调最好用 npm link 或 yalc我最后一次发布的教训是“发布一时爽打完脸就肿”。那次我更新了一个组件里的 prop 默认值本地构建和测试都通过了直接npm publish结果有个项目升级后组件的样式全乱了。原因是我漏改了另一处相关代码但本地没有实际业务场景测试也没有覆盖到所有 CSS 组合。从那以后我养成了发布前先用npm link或者yalc把包链接到实际项目里做冒烟验证的习惯。npm link的用法很简单在工具库目录里执行npm link再在目标项目里执行npm link my-utils这样目标项目引用的就是本地源码产物改动后的效果能立即看到。但 npm link 会改变 node_modules 的符号链接结构偶尔会触发 vite 缓存问题所以我个人更喜欢 yalc——它会把本地发布文件“推”到目标项目的 node_modules 里更接近真实依赖情况也不会因为符号链接导致某些工具无法解析。说到底工具库开发的完整闭环是“编码 - 测试 - 构建 - 本地联调 - 发布”。前四步走得越扎实最后一步就越稳。每当我偷懒跳过本地联调时几乎都会在下一次发布后收到一个“有问题”的反馈。所以现在我的出包流程里npm pack和本地冒烟这步已经成了肌肉记忆。从我这次搭建工具库的经历来看最耗时间的不是写那几十个函数而是让整个项目变得干净、可信、可维护的过程。工具库的本质是给未来的自己和其他项目“预写”代码如果你的包发布一次就要让下游项目改一次配置那这个工具库就失去了意义。如果你也打算动手我的建议是从一个最小的函数库开始先跑通打包发布流程再逐步加入组合式 API 和组件别一开始就想面面俱到。最后再分享一个小技巧每次构建之后进 dist 目录随便打开一个产物文件看看里面有没有意外的残留代码比如过期的注释、调试用的 console 语句——有一次我就在产物里发现了一段被误加到纯函数里的 console.warn还好发的是预发布版本没有影响线上项目。这种细节点滴积累起来就是工具库和其他人的随手分享之间最大的区别。