npm 包管理与发布
大约 9 分钟约 2743 字
npm 包管理与发布
简介
npm(Node Package Manager)是 JavaScript 生态的包管理器,管理项目依赖、运行脚本和发布包。本篇介绍 npm 的高级用法,包括包发布、monorepo 管理、版本策略和私有包。
npm 是 Node.js 默认的包管理器,拥有超过 200 万个公开包。除了基础的依赖管理,npm 还提供了脚本运行(npm run)、包发布(npm publish)、版本管理(npm version)等核心能力。现代前端项目中,npm 不仅仅是安装依赖的工具,更是工程化体系的基础设施。
特点
包管理器对比
| 特性 | npm | pnpm | yarn |
|---|---|---|---|
| 安装速度 | 慢 | 最快(硬链接 + 符号链接) | 较快 |
| 磁盘占用 | 大(每项目完整复制) | 最小(全局存储 + 链接) | 较大 |
| 幽灵依赖 | 有(提升到顶层) | 无(严格隔离) | 有 |
| monorepo | workspaces | workspaces | workspaces |
| lockfile | package-lock.json | pnpm-lock.yaml | yarn.lock |
| 社区活跃度 | 最高 | 增长中 | 稳定 |
# 包管理器安装方式
# npm(Node.js 自带)
node -v && npm -v
# pnpm(推荐)
npm install -g pnpm
# yarn
npm install -g yarn
# pnpm 的核心优势:硬链接 + 符号链接
# 所有项目共享同一份全局存储(~/.pnpm-store)
# 项目 node_modules 中使用硬链接指向全局存储
# 大幅减少磁盘占用和安装时间包发布
创建和发布包
# 创建包目录
mkdir my-utils && cd my-utils
npm init -y
# 编写包代码// src/index.js
export function debounce(fn, delay = 300) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
export function throttle(fn, interval = 300) {
let lastTime = 0;
return function (...args) {
const now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, args);
}
};
}
export function deepClone(obj) {
return structuredClone(obj);
}TypeScript 包的完整配置
// package.json —— TypeScript 包的标准配置
{
"name": "@myorg/utils",
"version": "1.0.0",
"description": "常用工具函数库",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./debounce": {
"import": "./dist/debounce.mjs",
"require": "./dist/debounce.cjs"
}
},
"files": ["dist", "README.md"],
"sideEffects": false,
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts",
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
"prepublishOnly": "npm run build && npm run test",
"test": "vitest run",
"lint": "eslint src/",
"typecheck": "tsc --noEmit",
"release": "npm run build && npm publish"
},
"keywords": ["utils", "debounce", "throttle", "typescript"],
"author": "Your Name <email@example.com>",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/user/my-utils"
},
"homepage": "https://github.com/user/my-utils#readme",
"bugs": {
"url": "https://github.com/user/my-utils/issues"
},
"peerDependencies": {
"typescript": ">=5.0"
},
"devDependencies": {
"tsup": "^8.0.0",
"typescript": "^5.3.0",
"vitest": "^1.0.0",
"eslint": "^8.50.0"
}
}// tsup.config.ts —— 使用 tsup 构建(推荐)
import { defineConfig } from 'tsup'
export default defineConfig({
entry: ['src/index.ts'],
format: ['cjs', 'esm'],
dts: true,
splitting: false,
sourcemap: true,
clean: true,
minify: 'esbuild',
target: 'es2020',
outDir: 'dist',
})package.json 配置
{
"name": "@myorg/utils",
"version": "1.0.0",
"description": "常用工具函数库",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"],
"scripts": {
"build": "rollup -c",
"prepublishOnly": "npm run build",
"test": "vitest run",
"lint": "eslint src/"
},
"keywords": ["utils", "debounce", "throttle"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/user/my-utils"
},
"peerDependencies": {
"typescript": ">=5.0"
}
}发布流程
# 登录 npm
npm login
# 发布公开包
npm publish
# 发布带作用域的公开包
npm publish --access public
# 发布下个版本
npm version prerelease --preid=beta
npm publish --tag beta
# 安装 beta 版
npm install @myorg/utils@beta
# 废弃包(标记但不删除)
npm deprecate @myorg/utils@1.0.0 "请升级到 v2"
# 更新版本号
npm version patch # 1.0.0 → 1.0.1(bug修复)
npm version minor # 1.0.1 → 1.1.0(新功能)
npm version major # 1.1.0 → 2.0.0(破坏性变更)发布最佳实践
# 1. 发布前检查清单
npm run lint # 代码规范检查
npm run typecheck # 类型检查
npm run test # 单元测试
npm run build # 构建
npm pack --dry-run # 预览将要发布的文件(不实际创建 .tgz)
# 2. 使用 .npmignore 控制发布内容(比 files 字段更灵活)
# .npmignore
src/
test/
*.test.ts
tsconfig.json
tsup.config.ts
.eslintrc.js
vitest.config.ts
.github/
# 3. 查看包发布后的信息
npm info @myorg/utils
npm view @myorg/utils versions
npm view @myorg/utils dist-tags
# 4. 撤销发布(72 小时内)
npm unpublish @myorg/utils@1.0.0
# 5. 多 registry 发布
# 发布到 npm
npm publish
# 发布到 GitHub Packages
npm publish --registry=https://npm.pkg.github.com
# 6. 使用 npm link 本地调试
# 在包目录中
npm link
# 在使用该包的项目中
npm link @myorg/utils
# 调试完成后解除链接
npm unlink @myorg/utilsnpm Workspaces
Monorepo 管理
// 根目录 package.json
{
"name": "my-monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "npm run build --workspaces",
"test": "npm run test --workspaces",
"lint": "npm run lint --workspaces",
"clean": "npm run clean --workspaces"
}
}my-monorepo/
├── package.json
├── packages/
│ ├── ui/ → @myorg/ui
│ ├── utils/ → @myorg/utils
│ └── api-client/ → @myorg/api-client
└── tsconfig.json# 安装所有 workspace 依赖
npm install
# 在特定 workspace 安装依赖
npm install express -w @myorg/api-client
# workspace 间引用
# 在 @myorg/api-client 中:
# "dependencies": { "@myorg/utils": "workspace:*" }
# 运行特定 workspace 脚本
npm run build -w @myorg/uipnpm Workspaces(推荐)
# pnpm-workspace.yaml
packages:
- 'packages/*'
- 'apps/*'
- 'tools/*'// 根目录 package.json
{
"name": "my-monorepo",
"private": true,
"scripts": {
"dev": "pnpm --filter @myorg/web dev",
"build": "pnpm -r build",
"build:ui": "pnpm --filter @myorg/ui build",
"test": "pnpm -r test",
"lint": "pnpm -r lint",
"clean": "pnpm -r clean",
"changeset": "changeset",
"version": "changeset version",
"release": "pnpm build && changeset publish"
},
"devDependencies": {
"changeset": "^2.27.0",
"typescript": "^5.3.0"
}
}my-monorepo/
├── package.json
├── pnpm-workspace.yaml
├── packages/
│ ├── ui/ → @myorg/ui(组件库)
│ │ ├── package.json
│ │ ├── src/
│ │ └── tsconfig.json
│ ├── utils/ → @myorg/utils(工具函数)
│ │ ├── package.json
│ │ └── src/
│ └── api-client/ → @myorg/api-client(API 客户端)
│ ├── package.json
│ └── src/
├── apps/
│ ├── web/ → @myorg/web(Web 应用)
│ └── admin/ → @myorg/admin(管理后台)
└── tools/
├── eslint-config/ → @myorg/eslint-config
└── tsconfig/ → @myorg/tsconfig# pnpm 常用命令
pnpm install # 安装所有依赖
pnpm --filter @myorg/ui build # 只构建 ui 包
pnpm --filter @myorg/web dev # 只启动 web 应用
pnpm -r test # 递归运行所有包的 test
pnpm add lodash -w @myorg/web # 给 web 应用添加依赖
pnpm add @myorg/utils --filter @myorg/api-client # workspace 内部依赖
# workspace 协议
# "dependencies": { "@myorg/utils": "workspace:*" }
# 发布时自动替换为实际版本号版本策略
SemVer 语义化版本
版本格式: MAJOR.MINOR.PATCH
MAJOR: 破坏性变更(不兼容的 API 修改)
MINOR: 新增功能(向后兼容)
PATCH: Bug 修复(向后兼容)
前置标识:
1.0.0-alpha.1 内测版
1.0.0-beta.2 公测版
1.0.0-rc.1 候选版依赖版本范围
{
"dependencies": {
"express": "^4.18.0", // 兼容 4.x.x(>=4.18.0 <5.0.0)
"lodash": "~4.17.0", // 兼容 4.17.x(>=4.17.0 <4.18.0)
"react": "18.2.0", // 精确版本
"typescript": ">=5.0.0", // 大于等于
"axios": "*", // 任意版本(不推荐)
"dayjs": "latest" // 最新版本(不推荐)
}
}Changesets 版本管理
# 安装 changesets
pnpm add -Dw @changesets/cli
# 初始化
pnpm changeset init
# 创建 changeset(描述变更内容)
pnpm changeset
# 交互式选择:
# 1. 哪些包有变更
# 2. 变更级别:patch / minor / major
# 3. 变更描述
# 消费 changesets(更新版本号)
pnpm changeset version
# 自动更新 package.json 中的 version
# 自动更新 CHANGELOG.md
# 发布
pnpm changeset publish# .changeset/README.md(changeset 文件示例)
---
"@myorg/ui": minor
"@myorg/utils": patch
---
Button 组件新增 loading 状态
修复 debounce 函数在严格模式下的警告.npmrc 配置
私有 registry
# .npmrc
# 使用淘宝镜像
registry=https://registry.npmmirror.com
# 私有包使用内部 registry
@mycompany:registry=https://npm.mycompany.com/
# 认证
//npm.mycompany.com/:_authToken=${NPM_TOKEN}
# 严格 SSL
strict-ssl=true
# 忽略可选依赖
optional=false
# 缓存目录
cache=/tmp/.npm-cache团队级 .npmrc 配置
# .npmrc(放在项目根目录,提交到 git)
# 包管理器配置
save-exact=true # 安装时使用精确版本号(不带 ^)
engine-strict=true # 严格检查 Node.js 版本
audit=true # 安装时运行安全审计
fund=false # 禁止 npm fund 提示
# registry 配置
registry=https://registry.npmmirror.com
# 作用域包的 registry
@myorg:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_NPM_TOKEN}
# 网络配置
fetch-retries=3 # 请求重试次数
fetch-retry-mintimeout=10000 # 重试最小超时(毫秒)
fetch-retry-maxtimeout=60000 # 重试最大超时(毫秒)
network-timeout=300000 # 网络超时(毫秒)
# 安全配置
ignore-scripts=false # 是否跳过 postinstall 脚本(不推荐跳过)npm scripts 详解
{
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint . --ext .vue,.js,.ts,.tsx --fix",
"format": "prettier --write \"src/**/*.{js,ts,vue,css,scss}\"",
"typecheck": "vue-tsc --noEmit",
// 生命周期钩子
"prebuild": "echo '构建前执行'",
"postbuild": "echo '构建后执行'",
"prepublishOnly": "npm run lint && npm run test && npm run build"
}
}# npm scripts 生命周期
# pre<name> → <name> → post<name>
# npm run build 实际执行:prebuild → build → postbuild
# 特殊生命周期
# prepublishOnly: npm publish 前执行(推荐用于构建和测试)
# preinstall: npm install 前执行
# postinstall: npm install 后执行
# 并行执行脚本(npm 6+)
npm run lint & npm run typecheck # 使用 & 并行(bash)
npm-run-all lint typecheck # 使用 npm-run-all 工具
# 传递参数
npm run build -- --mode staging # -- 后面的参数传给 vite build
npm run test -- --grep "should work" # 传给 vitest优点
缺点
总结
npm 包发布核心:package.json 配置 name/version/main/exports/files,npm publish 发布到 registry。Monorepo 用 npm workspaces 管理多包。版本遵循 SemVer 规范,npm version 更新版本号。.npmrc 配置 registry 和认证。建议使用 pnpm 替代 npm 获得更快的安装速度和更少的磁盘占用。
关键知识点
- 先判断主题更偏浏览器原理、框架机制、工程化还是性能优化。
- 前端问题很多看似是页面问题,实际源头在构建、缓存、状态流或接口协作。
- 真正成熟的前端方案一定同时考虑首屏、交互、可维护性和线上诊断。
项目落地视角
- 把组件边界、状态归属、网络层规范和错误处理先定下来。
- 上线前检查包体积、缓存命中、接口失败路径和关键交互降级策略。
- 如果主题和性能有关,最好用 DevTools、Lighthouse 或埋点验证。
常见误区
- 只盯框架 API,不理解浏览器和运行时成本。
- 把状态、请求和 UI 更新混成一层,后期难维护。
- 线上问题出现时没有日志、埋点和性能基线可对照。
进阶路线
- 继续补齐 SSR、边缘渲染、设计系统和监控告警能力。
- 把主题和后端接口约定、CI/CD、缓存策略一起思考。
- 沉淀组件规范、页面模板和性能基线,减少团队差异。
适用场景
- 当你准备把《npm 包管理与发布》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合中后台应用、门户站点、组件库和实时交互页面。
- 当需求涉及状态流、路由、网络缓存、SSR/CSR 或性能治理时,这类主题很关键。
落地建议
- 先定义组件边界和状态归属,再落地 UI 细节。
- 对核心页面做首屏、体积、缓存和错误路径检查。
- 把安全、兼容性和可访问性纳入默认交付标准。
排错清单
- 先用浏览器 DevTools 看请求、性能面板和控制台错误。
- 检查依赖版本、构建配置、环境变量和静态资源路径。
- 如果是线上问题,优先确认缓存、CDN 和构建产物是否一致。
复盘问题
- 如果把《npm 包管理与发布》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《npm 包管理与发布》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《npm 包管理与发布》最大的收益和代价分别是什么?
