从零到一:npm 包发布完整指南

从零到一:npm 包发布完整指南

一篇全面的 npm 包开发与发布教程,带你掌握从创建到维护的完整流程

📋 目录


前言

npm (Node Package Manager) 是 JavaScript 生态系统中最大的包管理器。通过发布 npm 包,你可以:

  • 🌍 分享代码:将你的工具库分享给全世界的开发者
  • 🔄 代码复用:在多个项目中复用相同的代码
  • 🤝 开源贡献:参与开源社区,获得反馈和贡献
  • 📈 技术影响力:建立个人技术品牌

本文将详细介绍如何创建、开发和发布一个 npm 包。


环境准备

1. 安装 Node.js 和 npm

首先确保你的系统已安装 Node.js,npm 会随 Node.js 一起安装。

1
2
3
4
5
6
7
# 检查 Node.js 版本
node -v
# 输出示例: v18.16.0

# 检查 npm 版本
npm -v
# 输出示例: 9.5.1

推荐版本

  • Node.js: v16.x 或更高
  • npm: v8.x 或更高

2. 注册 npm 账号

访问 npm 官网 注册账号,或使用命令行注册:

1
npm adduser

按提示输入:

  • Username(用户名)
  • Password(密码)
  • Email(邮箱,需验证)

3. 登录 npm

1
npm login

验证登录状态:

1
2
npm whoami
# 输出你的用户名

创建 npm 包

1. 初始化项目

创建项目目录并初始化:

1
2
3
4
5
6
# 创建项目目录
mkdir my-awesome-package
cd my-awesome-package

# 初始化 npm 项目
npm init

或使用快速初始化(使用默认值):

1
npm init -y

2. 项目结构

推荐的项目结构:

my-awesome-package/
├── src/                  # 源代码目录
│   └── index.js         # 主入口文件
├── dist/                # 构建输出目录(如果需要)
├── test/                # 测试文件
│   └── index.test.js
├── .gitignore          # Git 忽略文件
├── .npmignore          # npm 发布忽略文件
├── README.md           # 项目说明文档
├── LICENSE             # 开源协议
└── package.json        # 包配置文件

3. 创建必要文件

创建 .gitignore

1
2
3
4
5
echo "node_modules/
dist/
.DS_Store
*.log
.env" > .gitignore

创建 .npmignore

1
2
3
4
5
6
echo "src/
test/
.gitignore
.eslintrc.js
.prettierrc
*.test.js" > .npmignore

配置 package.json

package.json 是 npm 包的核心配置文件,以下是详细配置说明:

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
{
"name": "my-awesome-package",
"version": "1.0.0",
"description": "一个很棒的 npm 包",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "rollup -c",
"test": "jest",
"prepublishOnly": "npm run build && npm test"
},
"keywords": [
"javascript",
"utility",
"helper"
],
"author": "Your Name <[email protected]>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/username/my-awesome-package.git"
},
"bugs": {
"url": "https://github.com/username/my-awesome-package/issues"
},
"homepage": "https://github.com/username/my-awesome-package#readme",
"files": [
"dist",
"README.md",
"LICENSE"
],
"engines": {
"node": ">=14.0.0"
},
"devDependencies": {
"rollup": "^3.20.0",
"jest": "^29.5.0"
},
"dependencies": {}
}

重要字段说明

字段 说明 必填
name 包名称,必须唯一
version 版本号,遵循 SemVer
description 包的简短描述
main CommonJS 入口文件
module ES Module 入口文件 推荐
types TypeScript 类型定义文件 推荐
keywords 关键词,用于搜索
author 作者信息
license 开源协议
repository 代码仓库地址 推荐
files 要发布的文件列表 推荐

编写代码

示例 1:简单工具函数库

src/index.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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 格式化日期
* @param {Date} date - 日期对象
* @param {string} format - 格式字符串
* @returns {string} 格式化后的日期
*/
export function formatDate(date, format = 'YYYY-MM-DD') {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day);
}

/**
* 深拷贝对象
* @param {any} obj - 要拷贝的对象
* @returns {any} 拷贝后的对象
*/
export function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (obj instanceof Date) {
return new Date(obj.getTime());
}

if (obj instanceof Array) {
return obj.map(item => deepClone(item));
}

const clonedObj = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

/**
* 防抖函数
* @param {Function} func - 要防抖的函数
* @param {number} wait - 等待时间(毫秒)
* @returns {Function} 防抖后的函数
*/
export function debounce(func, wait = 300) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}

示例 2:TypeScript 项目

src/index.ts

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
/**
* 格式化日期
*/
export function formatDate(date: Date, format: string = 'YYYY-MM-DD'): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day);
}

/**
* 深拷贝对象
*/
export function deepClone<T>(obj: T): T {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (obj instanceof Date) {
return new Date(obj.getTime()) as any;
}

if (obj instanceof Array) {
return obj.map(item => deepClone(item)) as any;
}

const clonedObj: any = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}

return clonedObj;
}

/**
* 防抖函数类型
*/
type DebouncedFunction<T extends (...args: any[]) => any> = {
(...args: Parameters<T>): void;
cancel: () => void;
};

/**
* 防抖函数
*/
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number = 300
): DebouncedFunction<T> {
let timeout: NodeJS.Timeout | null;

const debounced = function(...args: Parameters<T>) {
const later = () => {
timeout = null;
func(...args);
};

if (timeout) clearTimeout(timeout);
timeout = setTimeout(later, wait);
};

debounced.cancel = () => {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
};

return debounced as DebouncedFunction<T>;
}

tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test"]
}

本地测试

在你的包目录中:

1
2
# 在包目录中创建全局链接
npm link

在测试项目中:

1
2
# 链接到本地包
npm link my-awesome-package

使用完毕后解除链接:

1
2
3
4
5
# 在测试项目中
npm unlink my-awesome-package

# 在包目录中
npm unlink

方法 2:本地安装

1
2
# 在测试项目中
npm install /path/to/my-awesome-package

方法 3:使用测试框架

安装 Jest

1
npm install --save-dev jest @types/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
import { formatDate, deepClone, debounce } from '../src/index';

describe('formatDate', () => {
test('should format date correctly', () => {
const date = new Date('2024-01-15');
expect(formatDate(date)).toBe('2024-01-15');
});
});

describe('deepClone', () => {
test('should deep clone object', () => {
const obj = { a: 1, b: { c: 2 } };
const cloned = deepClone(obj);

expect(cloned).toEqual(obj);
expect(cloned).not.toBe(obj);
expect(cloned.b).not.toBe(obj.b);
});
});

describe('debounce', () => {
test('should debounce function calls', (done) => {
let count = 0;
const debouncedFn = debounce(() => count++, 100);

debouncedFn();
debouncedFn();
debouncedFn();

setTimeout(() => {
expect(count).toBe(1);
done();
}, 150);
});
});

运行测试:

1
npm test

发布到 npm

1. 预发布检查

在发布前,确保:

  • ✅ 代码已测试通过
  • ✅ README.md 文档完善
  • ✅ package.json 配置正确
  • ✅ 版本号符合规范
  • ✅ .npmignore 配置正确

2. 构建项目

如果需要编译或打包:

1
npm run build

3. 发布包

首次发布

1
npm publish

发布作用域包(scoped package)

1
2
3
4
5
# 公开发布
npm publish --access public

# 私有发布(需要付费账号)
npm publish --access restricted

4. 验证发布

访问 npm 官网查看你的包:

https://www.npmjs.com/package/my-awesome-package

或使用命令查看:

1
npm view my-awesome-package

5. 安装测试

在其他项目中安装测试:

1
npm install my-awesome-package

版本管理

语义化版本(SemVer)

版本号格式:主版本号.次版本号.修订号(如 1.2.3

  • 主版本号:不兼容的 API 修改
  • 次版本号:向下兼容的功能性新增
  • 修订号:向下兼容的问题修正

版本更新命令

1
2
3
4
5
6
7
8
# 修订号 +1 (1.0.0 -> 1.0.1)
npm version patch

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

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

发布新版本

1
2
3
4
5
6
7
8
# 1. 更新版本号
npm version patch

# 2. 推送到 Git(如果有)
git push && git push --tags

# 3. 发布到 npm
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

最佳实践

1. 文档规范

README.md 模板

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
# My Awesome Package

> 一句话描述你的包

[![npm version](https://badge.fury.io/js/my-awesome-package.svg)](https://www.npmjs.com/package/my-awesome-package)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

## 安装

\`\`\`bash
npm install my-awesome-package
\`\`\`

## 快速开始

\`\`\`javascript
import { formatDate } from 'my-awesome-package';

const date = new Date();
console.log(formatDate(date)); // 2024-01-15
\`\`\`

## API 文档

### formatDate(date, format)

格式化日期

**参数:**
- `date` (Date): 日期对象
- `format` (string): 格式字符串,默认 'YYYY-MM-DD'

**返回值:**
- (string): 格式化后的日期字符串

**示例:**
\`\`\`javascript
formatDate(new Date(), 'YYYY-MM-DD'); // '2024-01-15'
\`\`\`

## 许可证

MIT © [Your Name]
\`\`\`

### 2. 选择合适的开源协议

常见开源协议:

- **MIT**:最宽松,允许商用、修改、分发
- **Apache 2.0**:类似 MIT,但提供专利保护
- **GPL**:要求衍生作品也开源
- **ISC**:类似 MIT,更简洁

### 3. 添加 CI/CD

**GitHub Actions 示例**

`.github/workflows/publish.yml`

```yaml
name: Publish Package

on:
release:
types: [created]

jobs:
build:
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 }}

4. 包大小优化

1
2
3
4
5
6
# 查看包大小
npm pack --dry-run

# 分析包内容
npm pack
tar -xvzf *.tgz

优化建议:

  • 使用 .npmignore 排除不必要的文件
  • 压缩代码(使用 Rollup、Webpack 等)
  • 按需加载(提供 ESM 版本)

5. 发布前检查清单

1
2
3
4
5
# 安装发布前检查工具
npm install -g publint

# 检查包配置
publint

常见问题

1. 包名已被占用

错误信息:

npm ERR! 403 Forbidden - PUT https://registry.npmjs.org/my-package - You do not have permission to publish "my-package". Are you logged in as the correct user?

解决方案:

  • 更换包名
  • 使用作用域包:@username/package-name
1
2
3
4
5
6
7
# 修改 package.json
{
"name": "@myusername/my-package"
}

# 发布作用域包
npm publish --access public

2. 版本号已存在

错误信息:

npm ERR! 403 Forbidden - PUT https://registry.npmjs.org/my-package - You cannot publish over the previously published versions

解决方案:

1
2
3
4
5
# 更新版本号
npm version patch

# 重新发布
npm publish

3. 未登录或登录过期

错误信息:

npm ERR! need auth This command requires you to be logged in.

解决方案:

1
2
3
4
5
# 重新登录
npm login

# 验证登录状态
npm whoami

4. 发布被拒绝(需要邮箱验证)

解决方案:

  1. 检查注册邮箱
  2. 点击验证链接
  3. 重新发布

5. 撤销已发布的包

1
2
3
4
5
# 撤销特定版本(发布后 72 小时内)
npm unpublish [email protected]

# 撤销整个包(慎用!)
npm unpublish my-package --force

⚠️ 注意:撤销包可能影响依赖该包的其他项目,请谨慎操作。

6. 更新包信息

1
2
3
4
5
# 废弃某个版本
npm deprecate [email protected] "This version has bugs, please upgrade to 1.0.1"

# 废弃所有版本
npm deprecate my-package "*" "Package no longer maintained"

进阶技巧

1. 发布 TypeScript 包

package.json 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"name": "my-ts-package",
"version": "1.0.0",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc && rollup -c",
"prepublishOnly": "npm run build"
}
}

2. Monorepo 发布

使用 Lerna 或 pnpm 管理多包项目:

1
2
3
4
5
# 使用 Lerna
npx lerna publish

# 使用 pnpm
pnpm -r publish

3. 自动化发布

使用 semantic-release

1
2
3
4
5
6
7
8
9
10
11
12
npm install --save-dev semantic-release

# .releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/npm",
"@semantic-release/github"
]
}

4. 生成 CHANGELOG

1
2
3
4
5
# 使用 conventional-changelog
npm install -g conventional-changelog-cli

# 生成 CHANGELOG
conventional-changelog -p angular -i CHANGELOG.md -s

总结

发布 npm 包的完整流程:

  1. 环境准备:安装 Node.js、注册 npm 账号
  2. 创建项目:初始化项目、配置 package.json
  3. 编写代码:实现功能、编写测试
  4. 本地测试:使用 npm link 或测试框架
  5. 发布包:npm publish
  6. 版本管理:遵循语义化版本规范
  7. 持续维护:更新文档、修复 bug、发布新版本

推荐工具

  • 开发工具:TypeScript、ESLint、Prettier
  • 构建工具:Rollup、esbuild、tsup
  • 测试工具:Jest、Vitest
  • 文档工具:TypeDoc、VitePress
  • 发布工具:semantic-release、np

学习资源


关于作者

如果这篇文章对你有帮助,欢迎:

  • ⭐ Star 我的 GitHub 项目
  • 📝 关注我的技术博客
  • 💬 留言交流你的想法

Happy Coding! 🚀


作者

hujinbin

发布于

2022-06-25

更新于

2026-06-15

许可协议

评论

:D 一言句子获取中...