npm:从零开始,开发一个软件包

把本地文件夹变成别人能 npm install 的包,要做三件事:写出可引用的代码、用 package.json 标明入口和身份、再发到 registry。

发布到公共源后,代码会出现在 npmjs.com,使用者用包名安装。发之前先在本地确认 tarball 里有什么,不要只靠源码目录里能跑就直接 npm publish

初始化项目

关键有两点:

  • 输出的 JavaScript 文件,也就是代码主体。常见是 lib/index.jsdist/index.js
  • package.json 指明入口:CommonJS 用 main;现行 Node.js 还认 exports(有 exports 时以它为准)。

初始化就是生成 package.json。要发布的话,nameversion 必填。可以手动创建,也可以 npm initnpm 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"
  }
}
  • nameversion:包的唯一标识,发布后同一版本号不能再用。
  • mainrequire("my-hello") 加载的文件。
  • files:打进 tarball 的白名单(数组或 glob),不是「随便写一个文件夹名」。未写则默认几乎全打进去,再减去 ignore 规则。
  • bin:命令名到可执行文件的映射。全局安装链到全局 bin,本地安装链到 node_modules/.bin
  • keywords:字符串数组,便于 npm 搜索。
  • scriptsnpm run 执行的命令。
  • license:开源常用 MITISC
  • homepagerepository:主页和 Git 地址。

image-20201103231118837

开发

复杂程度可以差很多:

  • 简单:在 lib 下写一个文件,例如 console.log('Hello World!'),或 module.exports 一个函数。
  • 复杂:用 webpack、Vue 等把编译结果输出到 libdist,并在 mainexportsfiles 里指向编译产物,不要把 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"

相对路径和 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 unlinknpm uninstall 的别名,去掉 -g 或包名时不会清掉全局那一环。

npm pack 测试

相对路径安装已经接近真实安装,但发布前仍应单独看 tarballfiles.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 addusernpm 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 网站上改团队权限。

相关问题

场景一: 正在开发的包装进业务项目后,构建报错,例如:

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_modulesnpm 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 registryhttps://registry.npmjs.org

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-09-02 00:58:49

results matching ""

    No results matching ""