CodeL
以前端为翼,以 AI 为脑,向全栈而行
2026-03-31

npm 包开发完全指南

npm 包开发完全指南:从零发布你的第一个包 一篇搞定:npm 包是什么、怎么写、怎么发布、怎么用 一、核心概念 1.1 什么是 npm 包? npm 包 = 可以被别人复用的代码模块 打个比方: npm 就像是一个「代...

npm 包开发完全指南:从零发布你的第一个包 #

一篇搞定:npm 包是什么、怎么写、怎么发布、怎么用


一、核心概念 #

1.1 什么是 npm 包? #

npm 包 = 可以被别人复用的代码模块

打个比方:

  • npm 就像是一个「代码超市」(仓库)
  • npm 包就是超市里卖的「商品」
  • npm install 就是「买东西」
  • npm publish 就是「上架商品」

你写了一段工具代码,觉得别人也能用,就把它打包发到 npm 上。全世界的人都可以通过 npm install 你的包名 下载使用。

1.2 为什么需要发布 npm 包? #

场景 说明
代码复用 多个项目用同一套代码,发成包直接装
团队协作 公司内部共享组件库、工具函数
开源贡献 分享给社区,积累影响力
版本管理 语义化版本,方便升级和回退
依赖管理 自动处理依赖关系,不用手动下载

1.3 核心名词速查 #

名词 含义 示例
npm Node.js 包管理器 npm install
package.json 包的「身份证」,描述包的信息 名称、版本、依赖
node_modules 依赖包存放目录 ./node_modules/
semver 语义化版本号 1.2.3 = 主版本.次版本.修订号
registry 包的仓库地址 https://registry.npmjs.org
scope 作用域,区分包归属 @babel/core 的 scope 是 @babel

二、基础使用:从零创建一个 npm 包 #

2.1 准备工作 #

1. 注册 npm 账号

npmjs.com 注册一个账号(免费)。

2. 本地登录

# 登录 npm
npm login
 
# 输入用户名、密码、邮箱
# 验证登录状态
npm whoami

3. 创建项目目录

# 创建项目文件夹
mkdir my-first-package
cd my-first-package
 
# 初始化 package.json
npm init -y

2.2 package.json 核心字段 #

运行 npm init -y 后,会生成一个 package.json

{
  "name": "my-first-package",     // 包名(必须唯一)
  "version": "1.0.0",              // 版本号
  "description": "我的第一个包",    // 描述
  "main": "index.js",              // 入口文件
  "scripts": {                     // 脚本命令
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],                  // 搜索关键词
  "author": "",                    // 作者
  "license": "ISC"                 // 开源协议
}

重要字段详解:

字段 必填 说明
name 包名,发布后全局唯一,只能小写字母、数字、中划线
version 版本号,遵循 semver 规范
main CommonJS 入口,默认 index.js
module ESM 入口,打包工具使用
types TypeScript 类型定义文件
files 发布时包含的文件(白名单)
keywords 搜索关键词,帮助别人找到你的包
repository 代码仓库地址
bugs 问题反馈地址
homepage 项目主页

2.3 编写包代码 #

创建入口文件 index.js

// index.js - 包的入口文件
 
/**
 * 格式化日期
 * @param {Date|string|number} date - 日期
 * @param {string} format - 格式,默认 'YYYY-MM-DD'
 * @returns {string} 格式化后的日期字符串
 */
function formatDate(date, format = 'YYYY-MM-DD') {
  const d = new Date(date);
  
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
  const hours = String(d.getHours()).padStart(2, '0');
  const minutes = String(d.getMinutes()).padStart(2, '0');
  const seconds = String(d.getSeconds()).padStart(2, '0');
 
  return format
    .replace('YYYY', year)
    .replace('MM', month)
    .replace('DD', day)
    .replace('HH', hours)
    .replace('mm', minutes)
    .replace('ss', seconds);
}
 
/**
 * 生成随机字符串
 * @param {number} length - 长度,默认 8
 * @returns {string} 随机字符串
 */
function randomString(length = 8) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}
 
/**
 * 防抖函数
 * @param {Function} fn - 要防抖的函数
 * @param {number} delay - 延迟时间(毫秒)
 * @returns {Function} 防抖后的函数
 */
function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
/**
 * 深拷贝
 * @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;
}
 
// 导出(CommonJS 方式)
module.exports = {
  formatDate,
  randomString,
  debounce,
  deepClone
};

2.4 本地测试 #

方法一:npm link(推荐)

# 在包目录下,创建软链接
npm link
 
# 在其他项目中使用
cd ~/other-project
npm link my-first-package
 
# 现在可以在代码中引用了
const { formatDate, debounce } = require('my-first-package');

方法二:直接引用路径

// 在测试文件中直接引用本地路径
const myPackage = require('/path/to/my-first-package');
 
console.log(myPackage.formatDate(new Date()));
// 输出:2026-03-29

2.5 发布到 npm #

# 1. 确保已登录
npm whoami
 
# 2. 检查包名是否可用
npm search my-first-package
# 或者
npm view my-first-package
# 404 说明可用
 
# 3. 发布
npm publish
 
# 看到类似输出说明成功:
# + my-first-package@1.0.0

发布成功后:


三、进阶用法 #

3.1 支持 ESM 和 CommonJS 双模块 #

现代包应该同时支持 ESM(ES Modules)和 CommonJS。

1. 创建 ESM 版本 index.mjs

// index.mjs - ESM 版本
 
export function formatDate(date, format = 'YYYY-MM-DD') {
  const d = new Date(date);
  const year = d.getFullYear();
  const month = String(d.getMonth() + 1).padStart(2, '0');
  const day = String(d.getDate()).padStart(2, '0');
 
  return format
    .replace('YYYY', year)
    .replace('MM', month)
    .replace('DD', day);
}
 
export function randomString(length = 8) {
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += chars.charAt(Math.floor(Math.random() * chars.length));
  }
  return result;
}
 
export function debounce(fn, delay = 300) {
  let timer = null;
  return function (...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}
 
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;
}

2. 更新 package.json:

{
  "name": "my-first-package",
  "version": "1.1.0",
  "description": "一个实用的工具函数库",
  "main": "index.js",
  "module": "index.mjs",
  "exports": {
    ".": {
      "import": "./index.mjs",
      "require": "./index.js",
      "default": "./index.js"
    }
  },
  "type": "commonjs",
  "files": [
    "index.js",
    "index.mjs"
  ],
  "keywords": [
    "utils",
    "format",
    "debounce",
    "deep-clone"
  ],
  "author": "your-name",
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/your-name/my-first-package.git"
  }
}

exports 字段详解:

{
  "exports": {
    ".": {
      "import": "./index.mjs",    // ESM 入口
      "require": "./index.js",     // CommonJS 入口
      "default": "./index.js"      // 兜底
    },
    "./utils": "./utils.js",       // 子路径导出
    "./package.json": "./package.json"
  }
}

3.2 添加 TypeScript 类型定义 #

1. 创建类型文件 index.d.ts

// index.d.ts - TypeScript 类型定义
 
/**
 * 格式化日期
 */
export function formatDate(
  date: Date | string | number,
  format?: string
): string;
 
/**
 * 生成随机字符串
 */
export function randomString(length?: number): string;
 
/**
 * 防抖函数
 */
export function debounce<T extends (...args: any[]) => any>(
  fn: T,
  delay?: number
): (...args: Parameters<T>) => void;
 
/**
 * 深拷贝
 */
export function deepClone<T>(obj: T): T;

2. 更新 package.json:

{
  "name": "my-first-package",
  "version": "1.2.0",
  "main": "index.js",
  "module": "index.mjs",
  "types": "index.d.ts",
  "files": [
    "index.js",
    "index.mjs",
    "index.d.ts"
  ]
}

3.3 版本管理(semver) #

npm 使用语义化版本号:主版本.次版本.修订号(Major.Minor.Patch)

版本类型 含义 何时升级 npm 命令
Patch 修订号 修复 bug,不影响 API npm version patch
Minor 次版本 新增功能,向后兼容 npm version minor
Major 主版本 破坏性变更,不兼容旧版本 npm version major

示例:

# 当前版本 1.0.0
 
# 修复 bug → 1.0.1
npm version patch
 
# 新增功能 → 1.1.0
npm version minor
 
# 重大更新 → 2.0.0
npm version major

版本范围语法:

语法 含义 示例
^1.2.3 兼容次版本更新 >=1.2.3 <2.0.0
~1.2.3 兼容修订号更新 >=1.2.3 <1.3.0
>=1.2.3 大于等于 1.2.3 及以上
1.2.3 - 2.0.0 范围 >=1.2.3 <=2.0.0
latest 最新版本 当前最新发布版
* 任意版本 不推荐使用

3.4 作用域包(Scoped Packages) #

作用域包用于区分归属,避免命名冲突。

命名格式: @scope/package-name

# 创建作用域包
npm init --scope=@your-username
 
# 发布作用域包(默认私有,需要付费)
npm publish
 
# 发布为公开包(免费)
npm publish --access public

示例:

{
  "name": "@your-username/my-utils",
  "version": "1.0.0"
}
// 使用
import { formatDate } from '@your-username/my-utils';

3.5 .npmignore 和 files 字段 #

控制发布时包含哪些文件。

方法一:files 字段(白名单,推荐)

{
  "files": [
    "index.js",
    "index.mjs",
    "index.d.ts",
    "lib/",
    "README.md",
    "LICENSE"
  ]
}

方法二:.npmignore(黑名单)

# .npmignore
src/
test/
.git/
.eslintrc
.prettierrc
*.test.js
*.spec.js

默认包含的文件:

  • package.json
  • README.md
  • LICENSE
  • CHANGELOG.md
  • files 字段指定的文件

默认忽略的文件:

  • .git/
  • node_modules/
  • .npmignore 中列出的文件

四、实战场景 #

场景 1:发布一个工具函数库 #

目录结构:

my-utils/
├── src/
│   ├── date.js        # 日期相关
│   ├── string.js      # 字符串相关
│   ├── object.js      # 对象相关
│   └── index.js       # 统一导出
├── dist/
│   ├── index.js       # 编译后的 CommonJS
│   └── index.mjs      # 编译后的 ESM
├── test/
│   └── index.test.js
├── index.d.ts         # 类型定义
├── package.json
├── README.md
└── LICENSE

package.json:

{
  "name": "@liuxiaodeng/my-utils",
  "version": "1.0.0",
  "description": "前端常用工具函数库",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./index.d.ts"
    }
  },
  "files": [
    "dist",
    "index.d.ts",
    "README.md",
    "LICENSE"
  ],
  "scripts": {
    "build": "rollup -c",
    "test": "vitest",
    "prepublishOnly": "npm run build && npm test"
  },
  "keywords": [
    "utils",
    "helper",
    "format",
    "validate"
  ],
  "author": "liuxiaodeng",
  "license": "MIT",
  "devDependencies": {
    "rollup": "^4.0.0",
    "vitest": "^1.0.0"
  }
}

场景 2:发布一个 React 组件库 #

目录结构:

my-ui-components/
├── src/
│   ├── components/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   ├── Button.module.css
│   │   │   └── index.ts
│   │   ├── Input/
│   │   └── index.ts
│   └── index.ts
├── dist/
├── package.json
└── tsconfig.json

package.json:

{
  "name": "@liuxiaodeng/ui-components",
  "version": "1.0.0",
  "description": "React UI 组件库",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./styles.css": "./dist/styles.css"
  },
  "files": [
    "dist"
  ],
  "sideEffects": [
    "**/*.css"
  ],
  "peerDependencies": {
    "react": ">=17.0.0",
    "react-dom": ">=17.0.0"
  },
  "devDependencies": {
    "react": "^18.0.0",
    "typescript": "^5.0.0"
  }
}

关键点:

字段 作用
peerDependencies 用户项目中必须安装的依赖
sideEffects 标记哪些文件有副作用,帮助 tree-shaking

场景 3:发布一个 CLI 工具 #

目录结构:

my-cli/
├── src/
│   ├── commands/
│   │   ├── init.js
│   │   └── build.js
│   └── index.js
├── bin/
│   └── my-cli.js
├── package.json
└── README.md

bin/my-cli.js:

#!/usr/bin/env node
 
const { program } = require('commander');
const packageJson = require('../package.json');
 
program
  .name('my-cli')
  .description('一个实用的 CLI 工具')
  .version(packageJson.version);
 
program
  .command('init')
  .description('初始化项目')
  .argument('[name]', '项目名称', 'my-project')
  .option('-t, --template <template>', '模板名称', 'default')
  .action((name, options) => {
    console.log(`创建项目: ${name}`);
    console.log(`使用模板: ${options.template}`);
  });
 
program
  .command('build')
  .description('构建项目')
  .option('-w, --watch', '监听文件变化', false)
  .action((options) => {
    console.log('构建中...');
    if (options.watch) {
      console.log('监听模式已开启');
    }
  });
 
program.parse();

package.json:

{
  "name": "@liuxiaodeng/my-cli",
  "version": "1.0.0",
  "description": "一个实用的 CLI 工具",
  "bin": {
    "my-cli": "./bin/my-cli.js"
  },
  "files": [
    "bin",
    "src"
  ],
  "dependencies": {
    "commander": "^12.0.0"
  },
  "engines": {
    "node": ">=16.0.0"
  }
}

使用方式:

# 全局安装
npm install -g @liuxiaodeng/my-cli
 
# 使用
my-cli init my-project
my-cli build --watch
 
# 或者 npx 直接运行
npx @liuxiaodeng/my-cli init my-project

场景 4:公司私有包 #

使用私有 registry:

# 设置 registry
npm config set registry https://npm.your-company.com
 
# 或者使用 .npmrc
@your-company:registry=https://npm.your-company.com
registry=https://registry.npmjs.org

使用 Verdaccio 搭建私有仓库:

# 安装
npm install -g verdaccio
 
# 启动
verdaccio
 
# 默认地址:http://localhost:4873
 
# 添加用户
npm adduser --registry http://localhost:4873
 
# 发布
npm publish --registry http://localhost:4873

五、常见问题 #

Q1: 包名已被占用怎么办? #

解决方案:

  1. 使用作用域:@your-username/package-name
  2. 换个名字:加前缀或后缀,如 my-package-utils
  3. 检查是否被恶意占用,可联系 npm 支持
# 检查包名
npm view package-name
 
# 检查作用域包
npm view @scope/package-name

Q2: 如何撤销已发布的版本? #

# 撤销特定版本(24小时内可撤销)
npm unpublish my-package@1.0.0
 
# 撤销整个包(谨慎使用!)
npm unpublish my-package --force
 
# 弃用包(不删除,但标记弃用)
npm deprecate my-package "这个包已弃用,请使用 new-package"

Q3: 如何发布 beta/alpha 版本? #

# 发布预发布版本
npm version 1.0.0-beta.1
npm publish --tag beta
 
# 用户安装
npm install my-package@beta

常用 tag:

tag 含义
latest 最新稳定版(默认)
beta 测试版
alpha 内测版
next 下一版本预览
canary 每日构建版

Q4: 发布时提示权限不足? #

# 确认登录状态
npm whoami
 
# 重新登录
npm logout
npm login
 
# 如果是组织包,确保有发布权限
npm team ls @org:developers

Q5: 如何自动化发布流程? #

使用 GitHub Actions:

# .github/workflows/publish.yml
name: Publish to npm
 
on:
  release:
    types: [created]
 
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
      
      - run: npm ci
      - run: npm run build
      - run: npm test
      
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

需要先在 GitHub 仓库设置中添加 NPM_TOKEN secret:

  1. 去 npm 生成 Access Token(Automation 类型)
  2. 在 GitHub 仓库 → Settings → Secrets → 添加 NPM_TOKEN

Q6: 如何检查包的大小? #

# 安装
npm install -g pkg-size
 
# 检查
pkg-size my-package
 
# 或使用 npm 自带
npm pack
# 查看生成的 .tgz 文件大小

Q7: 如何处理依赖安全漏洞? #

# 检查漏洞
npm audit
 
# 自动修复
npm audit fix
 
# 强制修复(可能有破坏性变更)
npm audit fix --force
 
# 查看详情
npm audit --json

六、发布最佳实践 #

6.1 README 模板 #

# my-package
 
> 一句话描述这个包是干什么的
 
## 安装
 
```bash
npm install my-package
# 或
yarn add my-package
# 或
pnpm add my-package

快速开始 #

import { formatDate } from 'my-package';
 
console.log(formatDate(new Date()));
// 输出:2026-03-29

API 文档 #

formatDate(date, format?) #

格式化日期。

参数 类型 默认值 说明
date Date | string | number - 日期
format string 'YYYY-MM-DD' 格式

randomString(length?) #

生成随机字符串。

...

开发 #

# 克隆仓库
git clone https://github.com/your-name/my-package.git
 
# 安装依赖
npm install
 
# 运行测试
npm test
 
# 构建
npm run build

License #

MIT © Your Name

 
### 6.2 版本发布检查清单
 
| 检查项 | 说明 |
|--------|------|
| ✅ 更新版本号 | `npm version patch/minor/major` |
| ✅ 更新 CHANGELOG.md | 记录变更内容 |
| ✅ 运行测试 | `npm test` |
| ✅ 构建产物 | `npm run build` |
| ✅ 更新文档 | README、API 文档 |
| ✅ 检查依赖漏洞 | `npm audit` |
| ✅ 提交 Git | `git push` |
| ✅ 打 tag | `git tag v1.0.0` |
| ✅ 发布 | `npm publish` |
 
### 6.3 常用命令速查表
 
| 命令 | 说明 |
|------|------|
| `npm init` | 交互式创建 package.json |
| `npm init -y` | 快速创建,使用默认值 |
| `npm login` | 登录 npm |
| `npm whoami` | 查看当前登录用户 |
| `npm publish` | 发布包 |
| `npm publish --access public` | 发布公开作用域包 |
| `npm unpublish pkg@version` | 撤销特定版本 |
| `npm deprecate pkg "message"` | 弃用包 |
| `npm version patch/minor/major` | 升级版本号 |
| `npm link` | 本地软链接(测试用) |
| `npm pack` | 打包成 .tgz(预览发布内容) |
| `npm view pkg` | 查看包信息 |
| `npm search keyword` | 搜索包 |
| `npm outdated` | 检查过时依赖 |
| `npm audit` | 安全审计 |
 
---
 
## 七、总结速记
 
| 阶段 | 核心命令 | 说明 |
|------|----------|------|
| **准备** | `npm login` | 登录账号 |
| **创建** | `npm init` | 初始化 package.json |
| **开发** | `npm link` | 本地测试 |
| **发布** | `npm publish` | 推送到 npm |
| **更新** | `npm version` | 升级版本号后再 publish |
| **撤销** | `npm unpublish` | 24小时内可撤销 |
 
**记住这几点:**
 
1. **包名唯一** → 先 `npm view` 检查
2. **语义化版本** → Patch 修复、Minor 功能、Major 大改
3. **files 字段** → 控制发布内容
4. **exports 字段** → 支持双模块(ESM + CJS)
5. **README 完整** → 帮助别人使用你的包
 
---
 
## 附录:推荐资源
 
| 资源 | 链接 | 说明 |
|------|------|------|
| npm 官方文档 | [docs.npmjs.com](https://docs.npmjs.com/) | 权威文档 |
| semver 规范 | [semver.org](https://semver.org/) | 版本号规范 |
| npm trends | [npmtrends.com](https://npmtrends.com/) | 包下载量对比 |
| bundlephobia | [bundlephobia.com](https://bundlephobia.com/) | 包大小分析 |
| package.json 生成器 | [packagejson.xyz](https://www.packagejson.xyz/) | 可视化配置 |
 
---
 
*最后更新:2026-03-29*