npm 依赖包开发实战:package.json、CJS/ESM 与私有库发布
1. 先想清楚自己的 npm 依赖包到底该不该抽写包这件事我第一次动念头是因为一段日期格式化代码在三个项目里出现了三份而且三份还不太一样——A 项目修了跨年 bugB 项目没修C 项目干脆抄的是旧版本。那一刻我才意识到代码复制的成本不在写的时候而在改的时候。npm 依赖包的本质不是代码打包而是把一份能力的唯一真相放到一个有版本号的地方谁用谁装改一处全体生效。这篇文章聊的就是从零开始做一个 npm 依赖包的完整链路怎么设计目录、怎么写 package.json、怎么在本地调试到爽、怎么打包瘦身、最后怎么推到自己团队的私有镜像库。整套流程我前后完整跑过七八个包有小到只有三个函数的工具库也有带 React 组件的 UI 库和命令行工具坑踩得不算少所以下面讲的东西基本都是实操验证过的不是文档翻译。适合谁看如果你已经在写业务代码被换个项目就得重写一遍工具函数折磨过或者你们团队已经有内部组件库的需求但一直卡在怎么发、发给谁、怎么更新这三个问题上那这篇基本能覆盖你百分之九十的场景。纯新手也能看我会把每个参数的来龙去脉讲清楚不会只甩一段配置让你抄——因为抄配置的人遇到报错是没法自救的。1.1 抽包的临界点三个信号同时出现再动手很多人抽包抽早了一个只有五行代码的debounce也要单独发一个包结果项目里多了一堆xxx-utils这种包名装依赖的时候自己都记不清哪个是哪个。我的经验是同时满足下面三个条件才值得抽成独立的 npm 依赖包。跨项目复用次数稳定在三处以上。两处还能忍三处以上说明这不是偶然。有独立的演进节奏。也就是这个能力会单独迭代而不是跟着某个业务项目一起发版。如果它永远跟着业务走那它就是业务代码不该独立。值得写测试。能被抽出来的能力通常边界清晰、输入输出明确天然适合写单元测试。凡是没法写测试的代码抽出来只是把耦合藏得更深。反过来说如果是团队协作层面需要统一规范的东西比如请求封装、埋点上报、错误上报其实也符合上述三条——它们的演进确实是独立的而且一旦出错影响面极大更需要独立版本控制。1.2 私有镜像库解决的到底是什么问题有人会问现在公共仓库那么方便为什么还要自建镜像库我在团队里推私有库的时候被问过不下十次答案其实很朴素内部代码不能进公共仓库。不只是合规问题还有命名冲突——你想用的包名早就被人占了想发也发不上去。第二个理由是速度。私有镜像库通常部署在内网装包走内网链路几百毫秒就下来了比绕一圈公共源稳定得多。第三个理由是可追溯。私有库能锁版本、能强制走审核、能知道谁在什么时候发了哪个版本出了问题能顺着版本号找到提交。这一点在人多的大团队里价值极高因为这个包是谁发的、改了啥往往是排障的第一现场。至于镜像库选型如果只是团队内部用Verdaccio 是最省事的选择——一个进程、一个存储目录、一个配置文件Docker 一条命令就能跑起来后面我会给具体配置。规模再大一些Nexus 或者云厂商的制品仓库也能用但本文聚焦在小团队自建的场景。1.3 包的形态决定你的构建方式同样是 npm 依赖包形态不同构建复杂度差了一个数量级。动手前先对号入座能少走很多弯路。包形态典型内容构建复杂度关键注意点纯工具库函数、常量、类型定义低双格式输出、tree-shakingUI 组件库React/Vue 组件、样式高样式产物、peerDependencies 必须外置CLI 工具命令行入口、模板文件中bin 字段、可执行权限、跨平台路径类型定义包纯 .d.ts低types 字段、命名空间声明我建议第一次做包的人从纯工具库入手因为它的构建链路最短能让你把main/module/types/exports这四个字段彻底搞明白。等你对这四个字段的优先级和作用范围有直觉了再做组件库就只是多了一层样式处理而已。2. 项目初始化与目录结构设计初始化这一步看着简单实际上决定了后面所有的调试体验。我见过太多人用npm init随便生成一个 package.json然后一路改到面目全非最后自己都不记得哪个字段是干嘛的。所以这一节我想把关键字段一个一个拆开讲讲它们在实际装包时到底起什么作用。2.1 package.json 里每个字段到底管什么先给你一份我常用的小型工具库模板然后再逐条解释。{ name: your-scope/awesome-utils, version: 0.1.0, description: 内部工具函数集合, 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 }, ./package.json: ./package.json }, files: [dist], sideEffects: false, engines: { node: 18 }, scripts: { build: tsup, dev: tsup --watch, test: vitest run, prepublishOnly: npm run test npm run build }, publishConfig: { registry: http://your-registry.internal:4873 }, peerDependencies: {}, devDependencies: { tsup: ^8.0.0, typescript: ^5.4.0, vitest: ^1.6.0 } }name是包的唯一标识带 scope 的形式your-scope/xxx在私有库场景下强烈推荐因为 scope 天然对应团队或业务线能避免同名冲突也能配合镜像库做权限划分。main/module/types是老三样分别对应 Node 的 CommonJS 入口、打包工具优先读取的 ESM 入口、以及类型声明入口。注意module不是 Node 官方字段是打包工具社区的约定Node 自己不认识它所以真正跨环境生效的是exports。exports是现代的入口映射表条件顺序非常关键types必须放在最前面import和require分别对应 ESM 和 CJS 场景。顺序写反了TypeScript 会找不到类型漏掉./package.json这条映射某些工具读 package.json 时会直接报错找不到模块——这个坑我踩过排查了半小时才反应过来。files是白名单只有写进去的目录才会被打进 tarball。这个字段比.npmignore更可靠因为.npmignore是黑名单你永远不知道哪个新加的临时目录会被误打包进去。sideEffects: false告诉打包工具这个包里的模块没有副作用允许它安全地做 tree-shaking这是工具库能否被摇掉无关代码的关键。publishConfig.registry是我最喜欢的一个字段它把发到哪个源这件事写死在包里避免某次手抖推到公共源上去。团队协作时这个字段能救命。2.2 双入口构建CJS 与 ESM 怎么和平共处现在的 Node 生态正处于 CJS 和 ESM 的过渡期你的包如果只出 CJS用 ESM 写的新项目引入时会有点别扭只出 ESM老项目require又会直接失败。所以最稳的做法是同时输出两种格式让消费方自己挑。手工配 rollup 当然可以但配置量不小我更推荐 tsup它就是给库作者准备的配置几乎为零。// tsup.config.ts import { defineConfig } from tsup export default defineConfig({ entry: [src/index.ts], format: [cjs, esm], dts: true, clean: true, sourcemap: true, splitting: false, treeshake: true, target: node18, outExtension({ format }) { return { js: format esm ? .mjs : .cjs } } })这里有个细节值得说为什么要把扩展名显式区分为.cjs和.mjs而不是统一用.js因为在type: module的包里.js会被 Node 当作 ESM 解释那么 CJS 产物就必须用.cjs后缀才能被正确加载。如果偷懒统一输出.js你在本地测试时可能一切正常但用户装完之后require直接崩这种问题最难排查因为它不在你的环境里复现。构建完你会在 dist 下看到四个文件index.cjs、index.mjs、对应的两个.map以及index.d.ts。这四个文件的命名必须和exports里的路径严格一致我一般会在prepublishOnly里加一步校验或者干脆用npm pack打开 tarball 看一眼内容对不对——本节后面会专门讲这个动作。2.3 类型声明产出的两条路径类型声明有两种产出方式一种是tsc单独跑一遍生成.d.ts另一种是让 tsup 用dts: true顺带产出。我选后者因为它省了一步配置也不用担心 CJS 和 ESM 各自的声明文件路径问题。但有个前提tsconfig.json必须配合好。{ compilerOptions: { target: ES2020, module: ESNext, moduleResolution: Bundler, declaration: true, strict: true, skipLibCheck: true, isolatedModules: true, verbatimModuleSyntax: true, outDir: dist }, include: [src] }isolatedModules和verbatimModuleSyntax这两个开关我建议都打开。前者保证每个文件都能被单独编译和打包工具的编译模型一致后者强制类型导入必须写import type避免把纯类型导入编译成真实的运行时 import——这类问题在双格式输出时会很致命因为生成的 CJS 里会多出一句莫名其妙的require。还有一点如果你的包导出的是接口和类型记得在入口文件里显式export type别只写在实现文件里。类型能不能被外部消费取决于入口有没有把它导出这和运行时导出是两套体系很多人第一次做包会在这里困惑。3. 本地调试三种打法各有各的适用场景开发 npm 依赖包最容易让人崩溃的环节就是本地调试。代码明明写对了装到业务项目里就是不生效或者生效的是上古版本。这不是玄学是模块解析机制在作怪。这一节我把三种常用手法讲透包括它们各自的失效原因。3.1 npm link最经典也最容易翻车npm link的原理其实很简单它在全局的 node_modules 目录下建一个软链接指向你的包目录然后你在业务项目里执行npm link your-package-name业务项目的 node_modules 里就会出现一个指向全局软链的链接。等于说业务项目直接读到了你本地的源码目录。# 在包目录里 npm link # 在业务项目里 npm link your-scope/awesome-utils改完代码包目录里跑一下tsup --watch业务项目的热更新就能感知到变化。听起来完美但坑在于依赖重复实例化。假设你的包依赖了lodash业务项目也依赖lodash用 link 之后你的包会去自己的 node_modules 找 lodash业务项目也去自己的 node_modules 找同一个库在内存里出现了两份。对于普通工具函数无所谓但如果这个库维护了某种单例状态比如缓存、事件总线两份实例就意味着状态不共享你会看到传进去的数据读不出来这种诡异现象。组件库场景下这个坑会放大——React 出现两份Hooks 直接报错。解决办法是所有框架级别的依赖一律放进peerDependencies让包自己不安装只用业务项目那一份。这也是我想强调的一条原则库不该拥有宿主环境的核心依赖。另一个常见问题是 link 之后修改了 package.json 的 entry 路径但链接不会自动刷新需要重新 link。我的做法是写一个脚本串起来# scripts/dev-link.sh set -e npm run build npm link cd ../business-app npm link your-scope/awesome-utils echo linked. run dev server now.注意Windows 上npm link创建的符号链接在某些情况下需要管理员权限如果报 EPERM先确认终端权限再执行。另外 link 出来的包不遵循.npmignore也就是说你发布时会被排除的文件在 link 状态下依然可见——这会造成本地好用发布后报错的错觉务必用npm pack复核。3.2 file: 协议加 watch更接近真实安装的调试方式link 是软链装出来的结果和真实安装差得远。想更接近真实场景可以用file:协议。{ dependencies: { your-scope/awesome-utils: file:../awesome-utils } }file:在 npm 7 之后的行为是建立符号链接而不是复制在 pnpm 和 yarn 里行为略有差异但基本等价。它的好处是走的是完整的安装流程files字段、exports映射、类型声明这些全部按真实规则解析很多在 link 下不会暴露的问题在这里会立刻现形。代价就是每次改完包都要重新npm install一次体验不如 link 顺滑。折中方案是用 monorepo。如果业务项目和包本身能放在同一个 workspace 里pnpm workspace 会自动建立本地链接并且会跟随构建产物更新是目前我认为体验最好的方案。# pnpm-workspace.yaml packages: - packages/* - apps/*在这个结构下apps/web引用packages/utils改完 utils 的源码跑一次 watch 构建web 那边立刻生效同时依赖解析又完全遵循真实规则。唯一需要注意的是 workspace 下不要用file:写死路径直接写your-scope/awesome-utils: workspace:*让包管理器处理链接关系。3.3 本地调试台与单元测试把验证成本压到最低不管是 link 还是 file都依赖于业务项目存在这个前提。但很多时候你只是想验证一个函数算得对不对为此启一个完整业务项目太浪费。所以我习惯在包里自带一个playground目录用最轻量的方式跑起来。// playground/index.ts import { formatDate, debounce } from ../src/index console.log(formatDate(new Date(), YYYY-MM-DD HH:mm:ss)) console.log(debounce(() console.log(fired), 300))配合tsx playground/index.ts直接跑一秒钟就能看到结果。这个目录千万不要写进files白名单否则会被一起发出去。单元测试用 vitest配置基本可以照抄// vitest.config.ts import { defineConfig } from vitest/config export default defineConfig({ test: { include: [src/**/*.test.ts], coverage: { provider: v8, thresholds: { lines: 80, functions: 80 } } } })覆盖率阈值我一般卡在 80% 而不是 100%因为工具库里总有几个分支是防御性代码为了凑覆盖率写测试反而浪费时间。但边界条件必须覆盖——日期库的跨月跨年、字符串处理的全角半角、数值计算的浮点精度这些都是历史 bug 高发区。提示测试用例不要只测正常输入一定要有一组异常输入的用例。我在一个字符串截断函数上就翻过车正常中文没问题遇到 emoji 会把它截成半个字符导致渲染出乱码。这个 bug 上线两周才发现因为没人想到拿 emoji 测。4. 打包发布从 tarball 预演到推上镜像库发布这一步是最容易出事故的环节因为一旦发出去版本号就收不回来了。我的习惯是发布前必须做一次完整预演把所有问题挡在真正发布之前。4.1 用 npm pack 做发布预演npm pack会生成一个.tgz文件内容就是你真正会推上去的东西。配合--dry-run可以只列出文件清单不打实际包。npm pack --dry-run输出类似这样npm notice Tarball Contents npm notice 1.2kB dist/index.cjs npm notice 0.9kB dist/index.mjs npm notice 4.1kB dist/index.d.ts npm notice 0.6kB package.json npm notice 1.1kB README.md npm notice Tarball Details npm notice name: your-scope/awesome-utils npm notice version: 0.1.0 npm notice total files: 5看到这个清单你要检查三件事。第一dist里的产物是否齐全有没有漏掉声明文件。第二有没有混进源码、测试、playground 这些不该出现的目录。第三README.md在不在里面——镜像库的详情页就是读 README 渲染的没有它页面就是一片空白。如果发现有不该打包的内容检查顺序是先看files白名单是不是写漏了再看有没有.npmignore在和它打架。记住files的优先级高于.npmignore两者同时存在时以files为准所以别在同一个项目里维护两套规则选一个就够了。还有一个细节package.json里的private: true一定要在正式发布时去掉否则npm publish会直接拒绝执行。这个字段在开发初期加上是好习惯能防止误发但容易在发布时忘记。4.2 版本号怎么定语义化不是形式主义版本号的意义在于让消费者能用版本号判断升级风险。major.minor.patch这套规则看着教条但它真能减少沟通成本。patch修 bug不改行为。用的人可以放心升。minor加功能向后兼容。用的人升了不会挂。major破坏性变更。用的人必须读迁移说明。npm version patch -m fix: 修正跨年日期计算 npm version minor -m feat: 新增 formatRelative 方法 npm version major -m feat!: 重命名导出移除旧 APInpm version会自动改 package.json、打 git tag、生成一次提交。我强烈建议配合 conventional commits 的写法这样后面用工具自动生成 CHANGELOG 会轻松很多。关于 0.x 版本很多人的理解有偏差。按照语义化的原始约定0.x.y表示 API 还在不稳定阶段0.1.0到0.2.0之间是可以直接破坏兼容的。所以如果你的包已经有人在用就该尽快升到1.0.0让版本的语义变得清晰。团队内部包我一般直接发1.0.0然后老老实实按规则走。4.3 把包推到自己的镜像库先用 Docker 把 Verdaccio 起起来这是自建 npm 镜像库最省事的方案。# docker-compose.yml version: 3 services: verdaccio: image: verdaccio/verdaccio:5 container_name: verdaccio ports: - 4873:4873 volumes: - ./storage:/verdaccio/storage - ./conf:/verdaccio/conf配置文件需要补上访问控制不然任何人都能往上发# conf/config.yaml storage: /verdaccio/storage auth: htpasswd: file: /verdaccio/conf/htpasswd max_users: 100 uplinks: npmjs: url: https://registry.npmjs.org/ packages: your-scope/*: access: $authenticated publish: $authenticated proxy: npmjs **: access: $all publish: $authenticated proxy: npmjs这里your-scope/*这条规则的意思是自己的 scope 只允许登录用户访问和发布同时当本地缓存里没有某个版本时会去上游公共源代理拉取。这样一个镜像库同时承担了私有包托管和公共包缓存两个角色内网装包速度会明显提升。服务起来之后先在本机配置源并登录npm config set your-scope:registry http://your-registry.internal:4873 npm adduser --registry http://your-registry.internal:4873 npm whoami --registry http://your-registry.internal:4873npm adduser会让你输入用户名、密码、邮箱成功后凭证会写进用户目录的.npmrc。注意这里用your-scope:registry的写法只把 scope 指向私有源其他包还是走原来的源这样最安全不会影响你其他项目的正常安装。然后发布npm publish因为包里的publishConfig.registry已经指定了目标源直接npm publish就会推到私有库不需要额外加参数。这个设计的好处是发布动作本身不携带环境信息任何人克隆下来发布结果都一样。注意发布前先跑一次npm run prepublishOnly里配的校验流程。我见过最惨的一次事故是忘了构建就直接npm publish推上去的是上一次的 dist 产物业务方升级之后发现 bug 还在白白浪费了一轮排查时间。把test build挂在prepublishOnly上就是防止这种低级失误。5. 常见报错与排查技巧实录前面讲的是顺风局这一节讲逆风局。下面这些问题我在不同机器、不同项目上反复遇到过整理成速查表遇到的时候可以直接对号入座。5.1 Windows 上 npm 命令直接报禁止运行脚本这是 Windows 用户最常见的拦路虎报错长这样npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。原因不是 npm 坏了而是 PowerShell 的执行策略ExecutionPolicy把.ps1脚本全禁了。npm 在 Windows 上通过一个 PowerShell 包装脚本调用被策略挡住就走不下去了。解决办法是放宽当前用户的执行策略不需要动系统全局Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSignedRemoteSigned的含义是本地写的脚本可以直接跑从网络下载的脚本必须有签名。这个级别比Unrestricted安全日常开发够用。如果公司机器有组策略限制改不了还有两个绕行方案。一是改用 CMD 而不是 PowerShell 执行命令CMD 不受执行策略影响。二是改用 Git Bash 之类的 shell同样能规避。我个人在 Windows 上长期用 Git Bash体验比 PowerShell 顺畅很多。顺带说一个相关报错npm : 无法将npm项识别为 cmdlet、函数、脚本文件或可运行程序的名称这个是 PATH 没配好npm 的可执行目录没进环境变量。通常是 Node 安装目录下的nodejs根目录没加进 PATH。加完之后记得重开终端因为环境变量是在终端启动时读取的改完不重启是不生效的。5.2 依赖冲突与 peerDependencies 的报错处理ERESOLVE这类报错基本都指向依赖树里出现了互相矛盾的版本要求。npm ERR! ERESOLVE unable to resolve dependency tree处理顺序建议是先看报错里提示的冲突双方是谁通常是一个包要求react^17另一个要求react^18。如果只是本地调试可以用--legacy-peer-deps临时绕过但这只是遮住问题不是解决。真正要做的是回到你自己的包检查框架类依赖有没有错误地放进dependencies。记住这条原则宿主提供的东西放 peerDependencies自己独占的东西放 dependencies。React、Vue、lodash 这种宿主大概率也有的一律 peer。你自己写的专用工具放 dependencies。peerDependenciesMeta可以用来标注某个 peer 依赖是可选的避免用户在没装的时候收到一堆警告{ peerDependencies: { react: 17 }, peerDependenciesMeta: { react: { optional: true } } }5.3 装完之后找不到模块或者类型丢失这两种情况几乎都出在exports字段上。如果运行时提示Cannot find module先确认导出路径是不是真的存在。exports里写的./dist/index.mjs必须和构建产物文件名完全一致多一个./少一个./都可能出问题。如果是 TypeScript 报找不到类型声明第一反应检查types条件有没有排在exports的第一位。条件匹配是从上往下走的如果import排在types前面TypeScript 在解析时先命中了运行时代码路径就不会再去找声明文件了。这个顺序问题非常隐蔽因为在某些编辑器版本下会表现成代码能跳转但类型是 any。还有一种情况是消费方 tsconfig 的moduleResolution太老比如还在用node。这种情况下它根本不认识exports字段会退回读main。所以文档里最好注明一句需要 TypeScript 5.0 和 moduleResolution: bundler/node16。5.4 问题速查表报错现象大概率原因处理方式npm.ps1 禁止运行脚本PowerShell 执行策略限制设置 CurrentUser 为 RemoteSigned无法将 npm 识别为命令PATH 未配置把 Node 安装目录加入 PATH 并重开终端ERESOLVE依赖树冲突peer 依赖版本互相矛盾检查依赖归类必要时用 overridesCannot find moduleexports 映射路径错误对照构建产物核对路径与扩展名类型声明丢失types 条件顺序靠后把 types 提到 exports 首位EPUBLISHCONFLICT版本号已存在提升版本号或撤回旧版本本地生效、发布后失效link 掩盖了 files 白名单问题用 npm pack 预演复核样式丢失组件库CSS 未作为独立入口导出增加./style.css映射并在文档说明表格里最后一条值得单独说一句。组件库的样式是最容易被忽略的部分很多人只映射了 JS 入口忘了把 CSS 也暴露出去结果业务方引入之后组件能渲染但完全没有样式排查半天以为是构建配置问题。做法很简单在exports里加一条./style.css: ./dist/style.css并在 README 里明确写清楚需要手动引入。6. 把发布流程自动化以及几条踩坑心得包能发出去只是第一步能不能长期维护才是真正的考验。我做过一个内部工具包半年之后回头看版本号跳得毫无规律CHANGELOG 一片空白连我自己都得翻 git log 才知道某次改动做的是什么。所以这一节聊聊怎么让流程自动跑起来以及几个我付过学费的教训。6.1 自动化发布的三个关键动作第一个动作是把校验挂到发布前置钩子。prepublishOnly是最靠得住的关卡它保证无论谁执行发布都会先跑测试和构建。{ scripts: { prepublishOnly: npm run lint npm run test npm run build } }第二个动作是在 CI 里做发布而不是在本地。CI 环境干净、凭证统一管理、每次发布都有记录。用 GitHub Actions 的话大致这样# .github/workflows/release.yml name: release on: push: tags: [v*] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 registry-url: http://your-registry.internal:4873 - run: npm ci - run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}注意这里的npm ci而不是npm install。ci严格按 lockfile 安装不会顺手改版本在发布流程里更可控。本地开发用install自动化流程用ci这是我一直在用的分工。第三个动作是用 git tag 和版本号绑定。npm version会自动打 tagCI 监听 tag 触发发布这样版本号、tag、发布记录三者天然对齐谁在什么时候发了什么都没争议。6.2 我踩过的几个坑希望你不用再踩第一个坑是files写成dist/带斜杠。大部分情况下没问题但在某些 npm 版本上会导致目录匹配异常打包出来的 tarball 里少文件。后来我统一改成不带斜杠的dist再没出过问题。这类小细节文档里不会写只能靠踩。第二个坑是把测试文件放进了 src 目录然后忘了排除。结果测试代码被打进包里业务方装完之后依赖树里多了一堆 devDependencies 的引用告警。解决方式是构建时用 tsup 的entry精确指定入口或者把测试挪到平行的test/目录从物理上隔离。第三个坑是改动了导出结构却没有升 major。我把一个具名导出改名觉得反正只有两个项目在用通知一声就行结果第三个项目没通知到构建直接失败。从那之后我的原则是只要导出符号的集合发生变化一律升 major不管用的人有多少。版本号是给不认识你的人看的不是给你自己看的。第四个坑是私有源的凭证过期。发布突然报 401排查半天发现是.npmrc里的 token 到期了。建议把 token 的有效期设长一点并且在 CI 里用 secret 管理而不是写死在配置里。写死在.npmrc并提交进仓库等于把凭证公开了。6.3 关于长期维护的一点真实体会一个包发出去之后你会慢慢发现真正的成本不在写代码而在拒绝需求。总有人希望你加一个只有他们用得上的特殊参数加着加着这个包就变成了一个大杂烩API 越来越难看懂最后没人敢动。我的做法是给每个包定一条明确边界写在 README 的第一段里这个包解决什么问题不解决什么问题。凡是越界的需求一律建议对方自己再抽一个包。听起来有点不近人情但两年之后回头看坚持这条原则的包都还活着什么都往里塞的包基本都死了。另外一点是文档的最小可用标准一段能跑的安装命令、一个真实可复制的使用示例、一份变更记录。就这三样。README 写得很华丽但示例跑不起来是比没有文档更糟糕的状态因为读者会怀疑整个包的质量。最后分享一个小技巧——我在每个包的playground目录里放了一个smoke.ts它只做一件事从dist目录而不是src导入所有导出符号挨个调用一遍打印结果。发布前跑一次能挡住绝大多数源码能跑、产物有问题的低级事故。这个脚本只有二十行但它帮我省下的排查时间远远超过写它的成本。包这个东西本质上是你和未来使用它的人之间的一份契约构建产物就是这份契约的实物——发之前摸一摸它是热的还是凉的心里会踏实很多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →