NPM包开发与发布完整指南

作为 JavaScript/Node.js 开发者,发布自己的 NPM 包是技术成长的重要一步。本文将从零开始,详细介绍如何开发、打包、测试并发布一个 NPM 包。

为什么需要发布 NPM 包?

  • 代码复用:将常用工具封装成包,多项目共享
  • 开源贡献:分享你的解决方案,帮助其他开发者
  • 技术积累:规范化代码组织,提升工程质量
  • 简历加分:维护公开项目,展示技术能力

目录结构规范

一个标准的 NPM 包项目结构:

1
2
3
4
5
6
7
8
9
10
11
12
my-package/
├── src/ # 源代码目录
│ ├── index.js # 主入口文件
│ └── utils.js # 辅助模块
├── dist/ # 构建产物(可选)
├── test/ # 测试文件
├── package.json # 包配置文件
├── README.md # 说明文档
├── LICENSE # 许可证文件
├── .gitignore # Git忽略配置
├── .npmignore # NPM忽略配置(可选)
└── CHANGELOG.md # 变更日志(推荐)

package.json 配置详解

这是包的核心配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
{
"name": "my-awesome-package",
"version": "1.0.0",
"description": "一个实用的工具库",
"main": "src/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"src",
"dist"
],
"scripts": {
"test": "jest",
"build": "rollup -c",
"prepublishOnly": "npm run build && npm run test"
},
"keywords": [
"utility",
"tools",
"helper"
],
"author": "FoleyDang",
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/foleydang/my-package.git"
},
"bugs": {
"url": "https://github.com/foleydang/my-package/issues"
},
"homepage": "https://github.com/foleydang/my-package#readme",
"dependencies": {},
"devDependencies": {
"jest": "^29.0.0",
"rollup": "^3.0.0"
},
"engines": {
"node": ">=14.0.0"
}
}

关键字段说明

字段 说明
name 包名,必须唯一,建议使用小写+连字符
version 版本号,遵循 SemVer 规范
main CommonJS 入口
module ES Module 入口
types TypeScript 类型声明文件
files 发布时包含的文件/目录
exports 现代包导出配置(推荐)

exports 配置(推荐)

现代 NPM 包推荐使用 exports 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"exports": {
".": {
"import": "./dist/index.esm.js",
"require": "./src/index.js",
"types": "./dist/index.d.ts"
},
"./utils": {
"import": "./dist/utils.esm.js",
"require": "./src/utils.js"
}
}
}

版本号规范(SemVer)

版本号格式:主版本.次版本.补丁版本(如 1.2.3

  • 主版本(Major):不兼容的 API 变化
  • 次版本(Minor):向后兼容的功能新增
  • 补丁版本(Patch):向后兼容的问题修复

常用命令:

1
2
3
4
5
6
7
8
9
10
11
# 补丁版本(1.0.0 -> 1.0.1)
npm version patch

# 次版本(1.0.0 -> 1.1.0)
npm version minor

# 主版本(1.0.0 -> 2.0.0)
npm version major

# 预发布版本
npm version prerelease --preid=beta # 1.0.0 -> 1.0.1-beta.0

包代码开发

入口文件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/index.js

/**
* 格式化日期
* @param {Date|string|number} date 日期对象/字符串/时间戳
* @param {string} format 格式模板
* @returns {string} 格式化后的日期字符串
*/
function formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const map = {
YYYY: d.getFullYear(),
MM: String(d.getMonth() + 1).padStart(2, '0'),
DD: String(d.getDate()).padStart(2, '0'),
HH: String(d.getHours()).padStart(2, '0'),
mm: String(d.getMinutes()).padStart(2, '0'),
ss: String(d.getSeconds()).padStart(2, '0')
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);
}

/**
* 深度克隆对象
* @param {*} obj 要克隆的对象
* @returns {*} 克隆后的对象
*/
function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}

module.exports = {
formatDate,
deepClone
};

ES Module 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// dist/index.esm.js

export function formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date);
const map = {
YYYY: d.getFullYear(),
MM: String(d.getMonth() + 1).padStart(2, '0'),
DD: String(d.getDate()).padStart(2, '0'),
HH: String(d.getHours()).padStart(2, '0'),
mm: String(d.getMinutes()).padStart(2, '0'),
ss: String(d.getSeconds()).padStart(2, '0')
};
return format.replace(/YYYY|MM|DD|HH|mm|ss/g, match => map[match]);
}

export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
const cloned = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}

使用 Rollup 构建

Rollup 是打包库的最佳选择,输出干净、高效。

安装依赖

1
npm install --save-dev rollup @rollup/plugin-node-resolve @rollup/plugin-commonjs @rollup/plugin-terser

rollup.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import terser from '@rollup/plugin-terser';

export default [
// ES Module 输出
{
input: 'src/index.js',
output: {
file: 'dist/index.esm.js',
format: 'esm',
sourcemap: true
},
plugins: [resolve(), commonjs()]
},
// CommonJS 输出
{
input: 'src/index.js',
output: {
file: 'dist/index.cjs.js',
format: 'cjs',
sourcemap: true
},
plugins: [resolve(), commonjs()]
},
// UMD 输出(浏览器兼容)
{
input: 'src/index.js',
output: {
file: 'dist/index.umd.js',
format: 'umd',
name: 'MyPackage',
sourcemap: true,
plugins: [terser()]
},
plugins: [resolve(), commonjs()]
}
];

编写测试

使用 Jest 编写单元测试:

1
npm install --save-dev jest

test/index.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
const { formatDate, deepClone } = require('../src/index');

describe('formatDate', () => {
test('格式化默认格式', () => {
const date = new Date('2026-05-09');
expect(formatDate(date)).toBe('2026-05-09');
});

test('自定义格式', () => {
const date = new Date('2026-05-09 11:30:00');
expect(formatDate(date, 'YYYY/MM/DD HH:mm:ss')).toBe('2026/05/09 11:30:00');
});

test('时间戳输入', () => {
const timestamp = 1746662400000; // 2026-05-09 00:00:00 UTC
expect(formatDate(timestamp, 'YYYY-MM-DD')).toBe('2026-05-09');
});
});

describe('deepClone', () => {
test('克隆简单对象', () => {
const obj = { a: 1, b: 'test' };
const cloned = deepClone(obj);
expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
});

test('克隆嵌套对象', () => {
const obj = { a: { b: { c: 1 } } };
const cloned = deepClone(obj);
cloned.a.b.c = 2;
expect(obj.a.b.c).toBe(1);
});

test('克隆数组', () => {
const arr = [1, { a: 2 }, [3, 4]];
const cloned = deepClone(arr);
expect(cloned).toEqual(arr);
cloned[1].a = 3;
expect(arr[1].a).toBe(2);
});
});

README.md 编写规范

README 是包的门面,必须清晰完整:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# my-awesome-package

> 一个实用的 JavaScript 工具库

## 安装

\`\`\`bash
npm install my-awesome-package
# 或
yarn add my-awesome-package
# 或
pnpm add my-awesome-package
\`\`\`

## 使用

\`\`\`javascript
// CommonJS
const { formatDate, deepClone } = require('my-awesome-package');

// ES Module
import { formatDate, deepClone } from 'my-awesome-package';

// 格式化日期
formatDate(new Date(), 'YYYY-MM-DD HH:mm:ss');
// 输出: '2026-05-09 11:30:00'

// 深度克隆
const cloned = deepClone({ a: { b: 1 } });
\`\`\`

## API 文档

### formatDate(date, format)

| 参数 | 类型 | 说明 |
|------|------|------|
| date | Date/string/number | 日期对象、字符串或时间戳 |
| format | string | 格式模板,默认 'YYYY-MM-DD' |

### deepClone(obj)

| 参数 | 类型 | 说明 |
|------|------|------|
| obj | any | 要克隆的对象 |

## 许可证

MIT

NPM 注册与登录

1. 注册账号

1
2
3
# 在官网注册:https://www.npmjs.com/signup
# 或使用命令行
npm adduser

按提示输入用户名、密码、邮箱,完成邮箱验证。

2. 登录验证

1
2
3
4
npm login

# 验证登录状态
npm whoami

3. 配置 2FA(推荐)

在 npmjs.com 站点设置页面启用双因素认证,保护账号安全。

本地测试发布

发布前先本地测试:

1
2
3
4
5
6
7
8
# 在包目录
npm link

# 在测试项目目录
npm link my-awesome-package

# 现在可以在测试项目中引用
const pkg = require('my-awesome-package');

使用 npm pack

1
2
3
4
5
# 打包成 .tgz 文件
npm pack

# 在测试项目中安装
npm install ./my-awesome-package-1.0.0.tgz

正式发布

发布流程

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 确保代码已提交
git status

# 2. 运行测试
npm test

# 3. 构建(如有)
npm run build

# 4. 发布
npm publish

# 发布成功后会收到邮件通知

发布特定标签

1
2
3
4
5
6
7
8
# 发布 beta 版本
npm publish --tag beta

# 发布 next 版本
npm publish --tag next

# 用户安装指定标签
npm install my-awesome-package@beta

发布 scoped 包

scoped 包(如 @username/package)默认为私有包,需要付费。

发布公开 scoped 包:

1
npm publish --access public

常见问题处理

包名已存在

  • 换一个包名
  • 使用 scoped 包:@yourname/package-name
  • 检查是否拼写错误

发布失败

1
2
3
4
5
6
7
8
# 检查登录状态
npm whoami

# 检查包配置
npm publish --dry-run

# 查看将要发布的文件
npm pack --dry-run

更新已发布的包

1
2
3
4
5
6
# 1. 修改代码
# 2. 更新版本号
npm version patch/minor/major

# 3. 发布
npm publish

删除已发布的包

1
2
3
4
5
6
7
8
# 撤销 24 小时内发布的版本
npm unpublish my-awesome-package@1.0.0

# 撤销整个包(谨慎操作)
npm unpublish my-awesome-package --force

# 弃用包(推荐,不删除)
npm deprecate my-awesome-package "此包已弃用,请使用 xxx 替代"

最佳实践

  1. 语义化版本:严格遵循 SemVer 规范
  2. 完善的测试:发布前确保测试通过
  3. 清晰的文档:README 必须包含安装、使用、API 说明
  4. 变更日志:维护 CHANGELOG.md 记录版本变化
  5. 类型声明:提供 TypeScript 类型文件(.d.ts)
  6. 持续集成:使用 GitHub Actions 自动测试发布

GitHub Actions 自动发布

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# .github/workflows/publish.yml
name: Publish to NPM

on:
push:
tags:
- 'v*'

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
registry-url: 'https://registry.npmjs.org'
- run: npm ci
- run: npm test
- run: npm run build
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

在 GitHub 项目设置 → Secrets 中添加 NPM_TOKEN(从 npmjs.com 获取)。

总结

发布 NPM 包的完整流程:

  1. 规划包的功能和结构
  2. 编写代码和测试
  3. 配置 package.json
  4. 编写 README 和 LICENSE
  5. 本地测试(npm link / npm pack)
  6. 注册登录 NPM
  7. 发布(npm publish)
  8. 维护更新(版本管理)

掌握了这些,你就可以将自己的代码分享给全世界了!

参考资料