从零发布一个npm包:从dry-run到npx消费的标准流程
我一直觉得一个前端工程师真正“入门”的标志不是会用多少框架而是能把一个项目从“自己用”变成“别人也能用”。npm package 就是最常见的交付形态。这篇文章完整记录了我发布一个 npm 包的全过程从npm publish --dry-run预演到最终别人用npx 包名一条命令跑起来中间踩过的坑、想通的原理、验证方法都给你扒一遍。如果你正准备发布自己的第一个 npm 包或者已经发过但总觉得流程不透明这篇应该能帮你省下不少时间。1. 发布前的准备工作很多坑其实从这里开始1.1 账号、registry 与本地环境三件套发布 npm 包的第一步不是写代码而是先确认你的环境“能发”。听起来简单但我在帮同事排查发布问题时十个里有八个卡在账号登录和镜像源上。先搞定账号。去 npm 官网注册一个账号注册完在本地终端执行npm login按提示输入用户名、密码、邮箱。登录成功后可以用npm whoami验证能输出你的用户名就说明本地已经持有了有效的发布凭证。注意npm 的登录态默认存在用户目录下的.npmrc文件里里面保存的是//registry.npmjs.org/:_authTokenxxx这样的 token千万别把这个文件提交到 git 仓库。然后检查 registry 配置。国内开发者为了方便下载依赖通常会把镜像源切到淘宝、腾讯云之类的地方。下载依赖没问题但你要是直接npm publishnpm 会把包推到你当前配置的 registry 上用镜像源发布大概率会遇到 403 或者一个莫名其妙的“不存在”错误。所以我发布包前一定会执行npm config get registry确保输出是https://registry.npmjs.org/。如果你想方便切换多套源推荐装一个nrmnpm install -g nrm nrm ls nrm use npm这里有个小心得日常开发用镜像源没问题但发布相关操作我永远切回官方源。别嫌麻烦发布失败返工更麻烦。最后是 Node.js 和 npm 版本。现在 npm 已经内置在 Node 安装包中建议至少 Node 16 以上npm 7 以上。老版本 npm 对peerDependencies、postinstall脚本的处理逻辑和现在差别很大发布出去的包在新环境里可能出现依赖解析问题。检查一下node -v npm -v如果你用的 Windows发布过程中还可能遇到 PowerShell 执行策略问题时常报“npm.ps1 无法加载因为在此系统上禁止运行脚本”。这个问题我放到第 5 节统一写但你在发布前最好先心里有数这不是 npm 坏了是 PowerShell 出于安全策略默认不运行脚本文件。1.2 package.json 里的字段决定别人能不能用上你的包很多人以为 package.json 只是随便填几个字段但实际上它就是 npm 包的“门面”。npm publish上传的核心元信息几乎全部来自这里。先看name和version。name必须唯一不能和 npm 上已有包重名命名规则上不能有大写字母、不能有空格可以用-或_分隔。发布前用下面命令确认没有冲突npm view 你的包名如果返回 404 或者空信息说明名字基本可用如果返回了一大段 JSON说明已经有人用了换一个。version必须符合语义化版本规范例如1.0.0、2.1.3-beta.1。第一次发布我习惯从0.1.0或1.0.0开始看项目野心。没事不要去乱动线上依赖的版本号。然后是main和module字段。main是 CommonJS 入口指向编译后的dist/index.js之类的文件module是 ESM 入口指向dist/index.mjs如果包要同时支持require和import这两个字段很有必要。exports字段更进阶一点可以限制外部只能访问包内的哪些子路径避免用户直接引用未编译的源码文件。如果发布的是 CLI 工具关键字段是bin{ bin: { my-cli: ./bin/index.js } }bin的意思是把一个命令名映射到某个脚本文件。npm 安装包含后会自动在node_modules/.bin目录生成软链接window 下还会生成.cmd和.ps1辅助脚本这样用户才能直接在终端敲命令。如果只配置了main而没配bin用户用npx 你的包时会提示找不到命令这个坑我在第 4 节详细说。files字段用来声明发布时包含哪些文件是一个白名单数组。比如{ files: [dist, bin, README.md, LICENSE, package.json] }如果你不写filesnpm 默认会发布除了.gitignore、node_modules以外的所有文件这样很容易把测试用例、源码、配置文件全部带上造成包体积失控。反之files写少了用户安装后会发现找不到入口文件。我建议发布前用npm pack --dry-run检查实际内容后面会讲。还有几个容易被忽略但很影响体验的字段keywords给别人搜你的包时用的尽量贴 3-5 个精准词。description包的一句话说明npm 搜索页面会显示。license必须写MIT 或是 Apache-2.0 都行否则合规上很麻烦。repository填仓库地址用户报 bug 时能跳转到 GitHub Issues。engines声明兼容的 Node 版本避免用户用旧 Node 跑你的包直接崩。一个我个人的小建议是README 一定要写至少写清楚“这是什么”“怎么安装”“怎么用”“配置项有哪些”。很多新手发布包后不开 README别人搜到只觉得是一个空壳子看不到文档就不敢用。1.3 本地目录结构发布前要养成的“洁癖”除了 package.json目录结构也是发布质量的关键。很多包发布后体积巨大问题是把不需要的生产文件全部带上去了。我在自己的包里一般会分清楚源码目录src、构建产物目录dist或lib、CLI 入口目录bin、测试目录test、文档docs。files白名单里只写最终需要的东西源码和测试能不带就不带。这么做的好处有两个一是用户安装包的时候下载体积小、速度快二是别人在node_modules里看你包的时候不会看到一堆无关文件体验会清爽很多。如果包里有需要编译的原生模块或预处理步骤记得保证发布前已经把构建产物生成到正确目录。prepublishOnly脚本是最常用的地方{ scripts: { build: tsup src/index.ts --format cjs,esm --dts, prepublishOnly: npm run build npm test } }prepublishOnly会在npm publish命令触发后、实际发布前自动执行适合做构建、测试等校验。这个脚本会在 dry-run 时也执行所以我下一篇会强调dry-run 不是无害的它比你想象的更接近真实发布。2. 先别急着全量发布用 dry-run 先“预演”一遍2.1 dry-run 到底做了什么它和真实发布差在哪我第一次发布包的时候直接在终端敲了npm publish结果发上去才发现 README 没写、目录带了一堆node_modules垃圾文件最后只能 deprecate。后来学乖了发布前必跑npm publish --dry-run--dry-run是 npm 提供的一个“试运行”模式它会完整执行发布流程中除实际上传以外的所有步骤。换句话说npm 会读取 package.json、整理文件列表、计算包大小、执行生命周期脚本但最后不会真的把包推送到 registry。输出内容里能看到包名和版本文件总数和总大小实际要发布的文件列表生命周期的执行日志更严格一点可以同时指定--json让输出变成结构化数据npm publish --dry-run --json这样在 CI 里想自动校验发布产物时就很方便可以用node解析 JSON 去判断有没有把不该发的文件打进去。不过要记住dry-run 和真实发布的区别只有一个会不会真的上传。它并不能替你规避所有问题比如 npm 服务端对某些包名、版本号、依赖关系的校验只有在真实发布时才会暴露出来。所以 dry-run 是做“预演”不是做“保证”。2.2 用 npm pack 和 dry-run 的组合拳检查包内容npm pack和npm publish --dry-run很像但npm pack会真的在本地生成一个.tgz文件。这个 tarball 就是你最终要传到 npm 服务器上的原始内容。我常用的组合是npm pack生成类似my-package-1.0.0.tgz的文件然后解压或者用tar -tvf看里面的列表。如果你机器上没有 tar也可以用npm pack --dry-run --json看files数组。日常我会把npm pack第一遍跑出来检查有没有把node_modules、src等目录打进去再决定要不要微调files字段。有一次我的包引用了dist里的一个子目录但files只写了dist结果 tarball 里确实有整个dist这个没问题。但另一次我图省事把入口文件路径写成了./lib/index.js而lib目录恰好被.npmignore忽略了最后 pack 出来是空的。这种问题靠npm publish --dry-run扫一遍就能提前发现。另外如果你把 README.md、LICENSE 漏了npm 在发布时会自动帮你补上默认的 readme但 LICENSE 不会。所以检查 tarball 时要记得看LICENSE文件在不在。2.3 dry-run 会执行脚本别把副作用写在 prepublishOnly 里上文提到 dry-run 会执行部分生命周期脚本这里要展开讲。npm 在 publish 时执行的脚本顺序大致是prepublishOnlyprepackpreparepostpack真正 publishpostpublish--dry-run会执行从prepublishOnly到postpack理论上不会执行postpublish。这意味着如果prepublishOnly里有诸如“创建生产环境数据库表”这种副作用操作跑 dry-run 时一样会执行。我自己有一次在prepublishOnly里写了自动生成 changelog 并提交 git 的逻辑结果每次 dry-run 都会多出一个提交记录非常烦人。正确做法是把纯校验逻辑build、test、lint放在prepublishOnly把需要真实发布后才做的操作比如发通知放在postpublish。而如果你希望本地调试时也能触发检查可以另写一个precheck脚本手动执行。注意prepare比较特殊它不仅在 publish 时运行在本地npm install时也会运行。如果在prepare里放重构建或者长耗时任务用户在安装你的包时就会被卡住。我一般只在prepare里放“从 git 安装仓库时自动构建”的场景发布场景交给prepublishOnly。3. 上架发布从 0.0.1 到稳定版本版本策略比你想的重要3.1 语义化版本与 npm version 自动改号发布不只是敲一条命令那么简单。版本号是让别人知道你改了什么的关键盲目npm publish只会让用户觉得你的包“乱来”。语义化版本SemVer的基本规则是主版本号.次版本号.修订号。修复 bug不动已有 API递增修订号比如1.0.0 - 1.0.1增加功能但保持向后兼容递增次版本号比如1.0.1 - 1.1.0有破坏性变更比如删了某个 API、改了配置格式递增主版本号比如1.1.0 - 2.0.0还有一个辅助规则预发布版本用-分隔加标识符比如2.0.0-beta.1、2.0.0-rc.1。这种版本不会默认被latest标签指向普通用户只有显式安装pkgbeta的人才会用到。手动改 version 容易出错我推荐直接用官方命令npm version patch npm version minor npm version major这三个命令会分别递增修订号、次版本号、主版本号同时自动更新 package.json 里的 version 字段并且如果当前目录是 git 仓库还会自动打一个同名 tag。如果你不想要这个行为可以加--no-git-tag-versionnpm version patch --no-git-tag-version发布前我还会把这次改动写进 CHANGELOG.md。变化大的写一整段变化小的至少写一行。别小看这个动作几个月后你再回来看git log和 registry 历史很多细节都会忘掉。3.2 首次发布--access、--tag 和 --registry 的配合使用正式发布的命令是npm publish --access public--access public主要针对 scoped 包也就是包名形如your-scope/your-package的包。这种包默认是私有发布的不传--access public会报权限错误。无 scope 的普通包不用传也能发布但传了也无妨。如果是预发布版本建议加一个 tagnpm publish --tag beta--tag的作用是给这个版本打一个分发标签。默认标签是latest普通用户安装时默认拿latest指向的版本。如果发测试版时不打beta标签用户执行npm install your-package会直接装到测试版这是很多人被气疯的原因。而体验者只需要npm install your-packagebeta npm install your-package1.0.0-beta.1就可以拿到预发布版本。等测试没问题了再把稳定版发布成latestnpm publish --tag latest发布完成后用npm view your-package查看版本和 tag 列表或者npm info your-package这能确认你的包已经成功上了官方 registry。在真实项目里多人协作或 CI 环境通常不会每次手动npm login更合适的做法是配置一个 npm token。登录网页版 npm 后在 Access Tokens 页面生成一个Publish权限的 token然后在环境里设置npm config set //registry.npmjs.org/:_authToken${NPM_TOKEN}或者用.npmrc文件放到项目目录里但这个文件不要提交进 git最好加到.gitignore。3.3 发布错了unpublish 和 deprecate 的取舍有时候发布完发现版本号错了、包里有 bug、甚至忘了打包就传上去了第一反应往往是“撤下来”。npm unpublish能做到但它有严格限制仅能在发布后 24 小时内执行而且如果你被依赖到了别人项目直接安装失败影响范围很大。更保守、更推荐的做法是npm deprecatenpm deprecate your-package1.0.0 这个版本有严重 bug请升级到 1.0.1deprecate不会删除包但会在用户安装或更新时显示警告这样既不会破坏正在使用旧版本的人也能提醒用户升级。我发现很多开源项目维护者都倾向于 deprecate 而不是 unpublish原因很简单包一旦被发布它就可能已经被别人锁进了 package-lock.json。强行 unpublish 会让别人的构建直接失败这种事情在社区里伤害性很大。如果你是极早期实验包想彻底消失可以试试 unpublish但记住必须在 24 小时内而且需要满足条件。24 小时后没有自助渠道需要联系官方处理周期长且不可控。反正我自己已经不干这种事了错了就 deprecate 发新版本这对维护者自己和用户都是最稳妥的。4. 从“我发”到“别人用”用 npx 和 npm link 把本地包玩透4.1 本地验证npm link 与 npx . 的差别发布前如果不动手本地跑一遍很容易出现“发都发了结果 command not found”的尴尬。本地验证最好的两个工具是npm link和npx .。npm link的作用是把当前目录的包链接到全局node_modules里同时如果你的包配置了bin还会把对应命令链接到全局可执行路径。这样你在任意目录打开终端敲入命令都能直接运行当前的开发版本。cd your-package npm link然后在另一个测试项目里直接敲你在bin里配置的命令名。比如你配置了my-cli: ./bin/index.js那终端直接输入my-cli就会执行。npx .则是直接执行当前目录包里的bin命令不需要全局链接。它在实际发布前非常有用因为很多包发布完用户都是通过npx来使用的npx .能让你模拟这个消费场景npx .如果你的 CLI 支持参数可以npx . --help从我的经验看先跑npm run build再npx . --help再npm pack --dry-run基本能在本地把八成问题干掉包括“bin 命令不存在”“运行入口打错”“依赖没打进files”这些经典问题。4.2 npx 是怎么找到你刚发布的包的当你发完包用户使用npx your-packagenpx 的查找顺序大致是先看当前项目的node_modules/.bin里有没有这个命令。再看全局配置的node_modules/.bin里有没有。如果都没有npx 就会去 npm registry 查找your-package临时下载并执行。在 npm 7 之后npx其实被整合进了npm exec所以你还可以这样写npm exec your-package两者效果几乎一样。很多人有一个误区以为npx your-package和npx your-command是同一个概念。其实 npx 优先解释为“包名”如果包名和命令名不一致可以通过包名来执行命令例如npx -p your-package your-command-p指定要安装的包名后面的第一个参数指定要运行的命令名。刚发布完立刻在别的项目敲npx your-package可能会报找不到。这不是你包有问题常见原因是 npm 的本地缓存或 CDN 延迟。解决方式有两种npm cache clean --force npx your-package --registryhttps://registry.npmjs.org/用--registry强制走官方源很多时候能绕开镜像源同步延迟。如果你发布的是 scoped 包比如my-scope/my-tool用户需要npx my-scope/my-tool这种包如果没配置bin字段npx 直接执行时依然会提示 command not found。所以发布 CLI 包前一定要确定 package.json 里有bin而且脚本顶部写了 shebang#!/usr/bin/env node这个头部很重要它告诉系统用 Node 执行这个文件只写普通 JS 文件不写这个头运行时会报“无法将该项目识别为 cmdlet、函数、脚本文件或可运行程序的名称”之类的话。4.3 bin 机制细节为什么用户安装你的包后可以不用 npx其实除了npx用户还可以直接全局安装你的 CLI 包npm install -g your-package然后直接在终端输入命令。这时候bin字段里的命令会出现在全局的二进制目录下比如 Linux 的/usr/local/bin或 Windows 的 npm 全局目录。npx临时安装的好处是不会污染用户的项目依赖用完即走。但也正因为是临时安装如果你的包依赖很多、体积很大首次执行会比较慢。想测试真正的发布效果建议用npm install到一个干净的测试项目里再跑。我在本地做消费端测试的流程是mkdir /tmp/test-package cd /tmp/test-package npm init -y npm install ../your-package ./node_modules/.bin/your-command --version npx your-command --version先安装本地打包好的tgz验证依赖完整再用远程包名执行验证发布链路。这两步都通过才敢跟别人说“我这个包现在可以用 npx 直接用”。5. 常见问题与排查技巧实录5.1 本地工具链报错不能运行 npm/npx 的各种姿势我见过太多人在 Windows 上卡在脚本执行策略上包括热词里那些报错。“npm : 无法加载文件 ... npm.ps1因为在此系统上禁止运行脚本”这类错误本质上是 PowerShell 默认不允许执行未经签名的.ps1脚本。解决办法是在当前 PowerShell 会话里临时放开Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned或者给整个用户放开谨慎处理Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser如果不想碰 PowerShell最简单的方式是改用 Windows 自带的 cmd命令提示符在 cmd 里执行npm -v、npx -v通常不会有这个限制。还有一类“无法将‘npm’或‘npx’项识别为 cmdlet、函数、脚本文件或可运行程序的名称”的报错通常是因为 Node 没安装好或者安装后PATH环境变量里没有加入 Node 的安装路径。例如C:\Program Files\nodejs\你需要去“环境变量”面板检查一下Path把 Node 安装目录加进去然后重新打开终端。如果你用的是 nvm-windows 或 fnm 这类版本管理工具也要检查当前激活的 Node 版本是否正确。有时候你确实安装了 Node但 nvm 当前版本是空所以npm命令找不到。5.2 发布过程报错403、404、EINTEGRITY 与镜像源发布时最经典的问题就是 403 Forbidden。403 You must be logged in to publish packages说明没有登录或 token 失效执行npm login或重新配置 token。403 You cannot publish over the previously published versions说明你想发布的版本已经存在了或者因为某种原因和之前某个版本号冲突修改版本号再发布。404 Not Found有时候不是你的包不存在而是你用了镜像源镜像源还没同步到官方源。此时应该检查npm config get registry官方源必须是 registry.npmjs.org。EINTEGRITY本地node_modules缓存损坏删除node_modules和package-lock.json再重装依赖。EBUSYWindows 下文件被占用比如编辑器打开着 node_modules 里的某个文件或者杀毒软件在扫描。把相关程序关掉重试。如果你在发布的时候还跑着npm install也容易造成文件占用冲突。所以发布前我建议先别开 IDE 自动保存和终端里的 watch 模式先全部暂停等发布完成再说。5.3 用户安装后跑不起来最常见的是 bin 配置和 shebang 问题发布完之后找一个全新的项目测试。用户运行npx your-package时如果提示“command not found”第一件事就是检查 package.json 的bin字段是否存在。有bin字段但命令名写错也会找不到。要记住bin的 key 就是命令名value 是运行脚本路径别把两者搞反。还有刚才说的 shebangbin指向的那个 JS 文件第一行必须是#!/usr/bin/env node并且文件要有可执行权限。如果你在 Linux 或 Mac 上发布别忘了chmod x bin/index.jsWindows 缺失这个权限通常不影响但 Linux/Mac 用户就会踩坑。很多用.js写的 CLI 包没加这个头部本地跑没问题因为你会用node bin/index.js但用户跑的是my-cli系统直接试图以 shell 脚本方式执行它于是报错。另一个常见问题是依赖没打全。如果你的包引用了dependencies里没声明的包恰好本地又能跑因为全局或父级目录有同名的依赖发布出去用户就会遇到Cannot find module xxx。这就是为什么发布前最好在全新目录里测试安装而不要只在项目根目录测试。5.4 独家避坑技巧发布前检查清单最后把我每次发布前固定执行的检查清单分享出来照着抄能省掉 80% 的低级错误npm run build确认构建产物是最新的。npm run test确认测试通过。npm version确认版本号已经递增到目标版本。npm config get registry确认是官方源。npm publish --dry-run检查文件列表、体积和生命周期脚本。npm pack生成 tarball解压或tar -tvf查看内容重点看是否包含dist、bin、README.md、LICENSE。在临时目录npm install 生成的tgz并执行npx 你的包 --help。npm login确认登录态。最后执行npm publish --access public。如果这次发布的是预发布版记得加--tag beta别污染latest标签。如果是在 CI 里发布环境变量里配置好NPM_TOKEN并且用npm publish --dry-run在流水线里先做验证再进入正式发布阶段。还有一个细节npm pack生成的.tgz文件不要随手提交到 git 仓库或发布到源上它只是本地检验用的记得清理或加入.gitignore。发布完会影响 package-lock 的完整性测试时我通常会在临时目录使用避免污染真实项目。最后分享一点我的个人体会发布 npm 包这件事第一次永远会手忙脚乱但只要你把 dry-run、npm pack 和临时目录消费测试这三个动作变成肌肉记忆后面几乎不会翻车。我最开始发包时直接npm publish结果把node_modules都打上去包体积十几 MB后来被迫 deprecate那种尴尬的经历希望能帮你们避开。如果你正准备发布自己的第一个包我的建议很简单先把bin入口写好跑通npx .再认真填 package.json 的files然后 dry-run 两次最后再发布。让用户执行一条npx 你的包名就能用上你的作品这种成就感真的值得体验一次。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →