文章
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 whoami3. 创建项目目录
# 创建项目文件夹
mkdir my-first-package
cd my-first-package
# 初始化 package.json
npm init -y2.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-292.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发布成功后:
- 包可以在 npmjs.com/package/my-first-package 看到
- 任何人都可以
npm install my-first-package安装使用
三、进阶用法 #
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.jsonREADME.mdLICENSECHANGELOG.mdfiles字段指定的文件
默认忽略的文件:
.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
└── LICENSEpackage.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.jsonpackage.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.mdbin/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: 包名已被占用怎么办? #
解决方案:
- 使用作用域:
@your-username/package-name - 换个名字:加前缀或后缀,如
my-package-utils - 检查是否被恶意占用,可联系 npm 支持
# 检查包名
npm view package-name
# 检查作用域包
npm view @scope/package-nameQ2: 如何撤销已发布的版本? #
# 撤销特定版本(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:developersQ5: 如何自动化发布流程? #
使用 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:
- 去 npm 生成 Access Token(Automation 类型)
- 在 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-29API 文档 #
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 buildLicense #
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*继续阅读
返回文章列表