npm:从零开始,开发一个软件包
把本地文件夹变成别人能 npm install 的包,要做三件事:写出可引用的代码、用 package.json 标明入口和身份、再发到 registry。
发布到公共源后,代码会出现在 npmjs.com,使用者用包名安装。发之前先在本地确认 tarball 里有什么,不要只靠源码目录里能跑就直接 npm publish。
初始化项目
关键有两点:
- 输出的 JavaScript 文件,也就是代码主体。常见是
lib/index.js或dist/index.js。 package.json指明入口:CommonJS 用main;现行 Node.js 还认exports(有exports时以它为准)。
初始化就是生成 package.json。要发布的话,name 和 version 必填。可以手动创建,也可以 npm init;npm init -y 用默认值。
{
"name": "my-hello",
"version": "1.0.0",
"description": "示例包",
"author": "",
"main": "lib/index.js",
"files": ["lib"],
"bin": {},
"keywords": ["hello"],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"license": "MIT",
"homepage": "",
"repository": {
"type": "git",
"url": "git+https://github.com/user/my-hello.git"
}
}
name、version:包的唯一标识,发布后同一版本号不能再用。main:require("my-hello")加载的文件。files:打进 tarball 的白名单(数组或 glob),不是「随便写一个文件夹名」。未写则默认几乎全打进去,再减去 ignore 规则。bin:命令名到可执行文件的映射。全局安装链到全局 bin,本地安装链到node_modules/.bin。keywords:字符串数组,便于 npm 搜索。scripts:npm run执行的命令。license:开源常用MIT、ISC。homepage、repository:主页和 Git 地址。

开发
复杂程度可以差很多:
- 简单:在
lib下写一个文件,例如console.log('Hello World!'),或module.exports一个函数。 - 复杂:用 webpack、Vue 等把编译结果输出到
lib或dist,并在main、exports、files里指向编译产物,不要把src、测试、配置误打进包。 - 加上 ESLint 等校验。
- 写测试,例如 Mocha、Chai;现行也常用 Node 内置
node:test。 - 用 GitLab CI、GitHub Actions 等做发布流水线。
- 根目录放
README.md,写清安装和使用。
入口文件最小例子:
// lib/index.js
module.exports = function hello(name) {
return 'Hello, ' + name;
};
测试
本地验证分三层,由浅到深:目录能不能被装上、开发时改一句立刻在业务项目里看到、打包结果像不像将要发布的那份。
相对路径安装
进入 测试项目 目录,用相对路径安装正在开发的包。装完后看 node_modules 里有没有对应目录。
cd [测试项目目录]
npm install [包的相对路径]
例如 npm install ../my-hello。npm 会按发布规则打一份再装(走 file:),并在测试项目的 package.json 里写下类似 "my-hello": "file:../my-hello"。
npm link 测试
相对路径和 tarball 都要记住路径。开发期改一句就想在业务项目里看到,可以用 npm link 做全局符号链接。它 不会 按 files 打包,链的是整个项目目录,和真实发布仍有差别;联调方便,发版前仍要用上一节的 npm pack 验一次。
先到 npm 包项目:
cd [npm 包项目]
npm link
典型输出(Unix,具体前缀随 Node 安装位置变化):
/usr/local/lib/node_modules/[包名] -> /Users/${whoami}/Documents/[插件项目路径]/[包名]
意思是把包项目链到 当前 Node 版本 的全局 node_modules。Windows 上全局目录常见为 %AppData%\npm\node_modules;用了 nvm 则在该版本自己的前缀下。
再到业务项目:
cd [需要使用该包的项目目录]
npm link [package-name]
.../node_modules/[package-name] -> .../lib/node_modules/[package-name] -> [包项目真实路径]
之后改包项目里的文件,业务项目里链过去的那份会立刻看到(若业务侧有构建缓存,需要重新编译)。若包需要先 build 才生成 lib,改源码后仍要先 build。
测完拆掉链接。在 业务项目:
cd [业务项目]
npm unlink [package-name]
在 包项目 去掉全局链接:
cd [npm 包项目]
npm unlink -g
npm unlink 是 npm uninstall 的别名,去掉 -g 或包名时不会清掉全局那一环。
npm pack 测试
相对路径安装已经接近真实安装,但发布前仍应单独看 tarball:files、.npmignore、.gitignore 会改掉最终内容,源码目录能 require 到的文件,使用者未必拿得到。
在 npm 包项目 根目录:
cd [npm 包项目]
npm pack --dry-run
只列出将打进包的文件,不写磁盘。检查是否漏了 lib、是否误带了 src、测试、.env。
确认名单后真正打包:
npm pack
当前目录生成 my-hello-1.0.0.tgz(名字是 name-version.tgz)。用归档工具查看内容:
tar -tf my-hello-1.0.0.tgz
tarball 里路径带一层 package/。再到测试项目里安装 这个文件,而不是源码目录:
cd [测试项目目录]
npm install [包项目的绝对或相对路径]/my-hello-1.0.0.tgz
这样加载到的就是使用者 npm install my-hello 时同一套文件。改完源码要再测,重新 npm pack 再装一次;旧 tarball 不会自动跟着变。
npm publish --dry-run 会走打包并打印将要发布的清单,但不上传。适合在 npm pack 之后再确认一遍账号无关的内容。
发布
先确认 registry 是官方源。若 npm config get registry 指向 https://registry.npmmirror.com(或旧的淘宝、cnpm 源),npm publish 会发到镜像而不是 npmjs.com,常见报错见文末。
npm config set registry https://registry.npmjs.org
账号在 npmjs.com 注册。命令行:
npm login
npm whoami
npm adduser 与 npm login 在现行 CLI 里是同一条登录流程。开启 2FA 时,发布或改权限会提示一次性密码,也可 --otp=123456。
发布:
npm publish
范围包(@scope/name)默认按受限包处理,要公开需:
npm publish --access public
或在 package.json 写 "publishConfig": { "access": "public" }。"private": true 的包会直接拒绝发布。
每次发布必须换版本号。同一 name@version 用过之后,即使 unpublish 也不能再占用。升版本:
npm version patch # 1.0.0 -> 1.0.1 缺陷修复
npm version minor # 1.0.1 -> 1.1.0 向后兼容的功能
npm version major # 1.1.0 -> 2.0.0 不兼容变更
这些命令会改 package.json(有 git 仓库时还会提交并打 tag)。然后再 npm publish。
撤销发布受 unpublish 政策 约束,不是随时都能删:
- 首次发布起 72 小时内,且公共 registry 上没有别的包依赖它,可以 unpublish。
- 超过 72 小时,还要同时满足:无依赖、近一周下载不足 300、只有一名维护者。
- 某个
name@version一旦用过就作废。整包删光后,24 小时内不能再用同名发新版本。 - 删不掉或不该删时,用
npm deprecate <pkg> "<说明>"提示不要再用,安装仍能成功。
npm unpublish <package-name>@<version>
npm unpublish <package-name> --force
--force 才允许一次拿掉整包所有版本。即使页面上搜不到,短时间内仍会有缓存或直链。
使用
npm install [npm包名]
CommonJS:
const hello = require('[npm包名]');
ESM(包已提供 ESM 入口,或本项目 "type": "module"):
import hello from '[npm包名]';
import { named } from '[npm包名]';
权限转让
把包交给另一个 npm 用户时,先加对方为 owner,再删掉自己(确认对方已经能发布后再 rm):
npm owner add <their-username> <package-name>
npm owner rm <your-username> <package-name>
若写入操作开了 2FA,加上 --otp:
# 123456 换成身份验证器里的当前码
npm owner add <their-username> <package-name> --otp=123456
npm owner rm <your-username> <package-name> --otp=123456
npm owner ls <package-name> 可查看当前所有者。组织包还可以在 npm 网站上改团队权限。
相关问题
npm link 失败
场景一: 正在开发的包装进业务项目后,构建报错,例如:
Module build failed: TypeError: Invalid PostCSS Plugin found at: plugins[0]
原因:包项目里的 PostCSS 配置引用了若干插件,业务项目没装同一套,或 npm link 让两套 node_modules 里各有一份插件(同类问题也见于 React 被装了两份)。
处理:改包项目的配置,不要把宿主才该有的插件写进会连带生效的配置;或让业务构建忽略包目录外的配置。发版前用 npm pack 安装 tarball,往往能避开 link 特有的「两份依赖」。
场景二: Node 版本不一致。
nvm 一类工具会给 每个 Node 版本 单独准备一份全局 node_modules。npm link 写在你执行命令时那个版本的全局目录里,换版本后链接「消失」且 不一定报错。
处理:包项目和业务项目都 nvm use 到同一版本。即便 node -v 看起来已经相同,也再指定一次同一版本。
场景三: 业务项目又执行了 npm install 装其他包,有时会拆掉 node_modules 里的链接。需要再跑一次 npm link [package-name]。
npm publish 报错
[no_perms] Private mode enable, only admin can publish this module
原因:当前 registry 是镜像(npmmirror、旧淘宝源、cnpm),不是 npmjs 公共源;登录态和要写入的源对不上。
处理:切回官方源后再登录、发布。
npm config set registry https://registry.npmjs.org
npm login
npm whoami
npm publish
也可以用 nrm 在源之间切换,发布前确认 npm config get registry 为 https://registry.npmjs.org。