← 全部材料 npm
NPM PACKAGE · 14 个高频点

别人凭什么敢用你的包

这块知识的特点是:会用的人很多,能讲清楚"为什么这么配"的人很少。比如 exports 字段为什么把 types 写在最前面、peerDependencies 到底解决什么问题——这些都不是配置题,是设计题。下面每节都从一个具体后果倒推。

01从后果倒推配置每个字段都先问"写错了会怎样",这比记字段名有用得多。
02关注升级路径包一旦有人用,破坏性变更的代价就很高。多数设计问题都在这。
03发布前先 dry-runnpm pack --dry-run 是唯一能让你看见"用户到底拿到什么"的手段。
1

package.json 里四个入口字段★

改错一个,用户就装了个跑不起来的东西
main 给 CJS/Node 用,module 给打包器(非标准但事实标准),browser 指定浏览器环境替换,types 指类型声明。exports 是新的统一方案,一旦写了它就接管全部。
写错会怎样只写了 main 打包器只能拿到 CJS,tree-shaking 大打折扣 只写了 module Node 直接 require 会失败 漏了 types 用户装了但全是 any,issue 第一句就是"没类型" 写了 exports 其他所有入口字段对它都失效 ← 最容易踩
面试怎么答main 指向 CJS 入口供 Node 使用,module 指向 ESM 入口供打包器使用,browser 做浏览器环境替换,types 指向类型声明。exports 是新的条件导出方案,优先级最高,一旦定义就会覆盖 main 和 module 的行为,所以里面的条件必须写全。
2

exports 的条件导出与顺序坑★

顺序错了,类型就不生效
exports 的条件是从上往下匹配,命中第一个就停。所以 types 必须写在最前面,否则可能被 import 先接走,TypeScript 就找不到类型了。
◐ 可交互:同一个包,四类调用方各拿到哪份文件
调用方
package.json 里有什么
三个常见错误1. types 写在 import 后面 → TS 拿不到类型,因为匹配顺序是先命中的赢 2. 忘了暴露 ./package.json → 有些工具(打包器、插件)会读它,读不到直接报错 3. 没写 ./* 通配,但包里有子路径 → exports 一旦存在,未声明的子路径<b>全部不可访问</b> 这是刻意的:把内部结构藏起来
面试怎么答exports 的条件按书写顺序自上而下匹配,命中即停,所以 types 必须放在最前面。它一旦定义就是封闭的,未声明的子路径不可访问,这既是封装也是升级踩坑的来源。要记得暴露 package.json 并给 default 兜底。
3

语义化版本与依赖类型★

^ 和 ~ 差一格,后果差很远
^1.2.3 允许升 minor 和 patch(1.x.x),~1.2.3 只允许升 patch(1.2.x)。peerDependencies 是"你得自己装,我不带",插件类包必须用它。

三种依赖的区别

  • dependencies:运行时真的要用,会被装进来
  • devDependencies:只在开发和构建时用
  • peerDependencies:声明"我用你,但由用户提供"

为什么插件必须用 peer

  • React 组件库如果自己带一份 React
  • 用户项目里就有两份 React 实例
  • hooks 报错、context 读不到
  • 所以必须 peer,让用户提供
版本范围的实际含义1.2.3 精确,不变 ~1.2.3 同 minor,只升 patch 1.2.x ^1.2.3 同 major,升 minor+patch 1.x.x ^0.2.3 0.x 时特殊:只升 patch 0.2.x * 任意,等于放弃控制
面试怎么答插入符允许 minor 和 patch 升级,波浪号只允许 patch,0.x 版本下插入符等价于波浪号。peerDependencies 用于声明由宿主提供的依赖,插件和组件库必须用它避免出现多份实例。破坏性变更必须升 major。
4

lockfile 与 npm ci★

为什么服务器上不能用 npm install
lockfile 锁的是整棵依赖树的精确版本。npm install 会按 package.json 的范围重新解析、可能更新 lockfile;npm ci 只按 lockfile 装,lockfile 和 package.json 不一致就直接失败。
两者的行为差异npm install 解析 + 安装 + <b>可能改写 lockfile</b> 本地开发用它,因为要装新包 npm ci 删除 node_modules,严格按 lockfile 装 lockfile 与 package.json 不匹配 → 直接报错 CI/CD 必须用它:快、可复现、不会偷偷升级
面试怎么答lockfile 记录的是整棵依赖树的精确版本和完整性校验。npm install 会重新解析并可能更新 lockfile,npm ci 严格按 lockfile 安装并且在不一致时直接失败,所以 CI 环境应该用 npm ci 保证可复现。lockfile 必须提交到仓库。
5

双格式产物怎么打★

CJS、ESM、类型声明,一个不能少
同时给 CJS 和 ESM 是最省事的兼容方案。可以用 tsup 或 unbuild 一条命令出三份产物,也可以用 tsc 打两次 + 后处理。
一个容易被忽略的点:双包陷阱同一份代码被打成两份,如果模块里有<b>模块级状态</b>, 两份产物各自持有一份,状态就分裂了: import { counter } from 'my-lib' // 拿 ESM 那份 const { counter } = require('my-lib') // 拿 CJS 那份 ← 两个 counter 不是同一个 <em>解法:有状态的包只发一种格式,或用 globalThis 兜住。</em>
面试怎么答双格式产物用 tsup 或 unbuild 一次生成 ESM、CJS 和类型声明,配合 exports 条件导出。要注意双包陷阱:模块级状态在两份产物里是两份,会导致状态分裂,有状态的包最好只发一种格式。类型声明里的路径引用也要保证发布后可用。
6

sideEffects 与它的代价★

这个字段是承诺,不是优化开关
写 "sideEffects": false 等于告诉打包器"删掉没用到的模块是安全的"。但如果包里有纯副作用导入(CSS、polyfill、全局注册),它们会被一起删掉。
面试怎么答sideEffects 声明模块是否含副作用,告诉打包器可以安全地删除未被使用的模块。设成 false 时如果有纯副作用导入(如 CSS、polyfill)会被误删,所以要改成数组列出这些文件。它只在消费方使用 ESM 且开启压缩时才有意义。
7

发布前的检查清单★

publish 是不可逆的
发布前必须做的三件事:确认 files 字段(决定哪些文件进包)、本地 npm pack 看一下、确认版本号没重复。已经发布的版本号不能再复用。
发布流程npm pack --dry-run 先看包里到底有什么(最重要的一步) npm version patch 改版本号并打 git tag npm publish 发布 npm view my-lib 确认线上版本 # 发现发错了 npm unpublish my-lib@1.0.1 ← 24 小时内可以,但会留下痕迹 npm deprecate my-lib@1.0.1 "有严重 bug,请升级到 1.0.2" ← 更推荐
面试怎么答发布前先跑 npm pack --dry-run 确认包内容,用 files 白名单控制发布范围,改版本号用 npm version 让它同时更新 git tag。发错了优先用 npm deprecate 提示用户升级,而不是 unpublish,因为后者有时间限制且已安装的用户不受影响。
8

dist-tag 与预发布

latest 不是唯一的选择
npm publish 默认打 latest 标签。想发 beta 就用 --tag beta,用户要装就必须写 my-lib@beta,不会影响用默认版本的人。
面试怎么答dist-tag 是版本之外的一层指针,默认发布打 latest。预发布用 --tag beta 或 next,只有显式写 @beta 的用户才会装到,不影响普通用户。正式发布后用 dist-tag add 把 latest 指向新版本。
9

scope、私有源与 monorepo

一个仓库多个包的常见做法
scope 是包名的命名空间(@myorg/ui),也是私有包的前提。workspaces 让一个仓库管理多个包,互相引用时走软链接而不是从 registry 下载。
workspaces 解决了什么以前:改了 utils,要发版 → 再去 ui 里升级 utils → 才能调试 现在:packages/ui 里 import '@myorg/utils' 直接指向 packages/utils 的源码,改完立刻生效 <em>注意:本地能跑不代表发布后能跑。 发布前要用 npm pack 验证真实依赖是否完整。</em>
面试怎么答scope 是包名命名空间,也是发布私有包的前提。workspaces 让一个仓库管理多个包,包之间引用走软链接,改完即时生效。要注意本地能跑不等于发布后能跑,发布前应该用 npm pack 验证依赖完整性。
10

供应链安全★

你装的不只是一个包
安装一个包等于执行它的 postinstall 脚本,并且信任它以及它所有间接依赖的作者。npm audit 只是最低限度,真正的防线是锁版本、审依赖、开 2FA。
几个真实的防线npm ci 严格按 lockfile,避免安装时被塞入新版本 npm audit 已知漏洞扫描(只是下限,别当全部) npm audit signatures (npm 9+)校验包的来源签名 package.json: "overrides": { "lodash": "4.17.21" } 强制统一间接依赖版本 发布侧: 账号开 2FA 用 provenance(来源证明),让别人能验证产物确实由 CI 构建
面试怎么答npm 安装依赖时可能执行 postinstall 脚本,所以供应链安全的核心是控制依赖。实践上要提交 lockfile、CI 用 npm ci、定期跑 npm audit、用 overrides 统一间接依赖版本,发布侧开 2FA 并使用 provenance。减少依赖数量本身就是最有效的防御。
11

包怎么设计:API 面越小越好

一个包的价值在于别人用起来不用想
好包的共同点:API 面小、错误信息能直接指导修复、默认行为合理。这三件事比"功能多"重要得多。

该避免的设计

  • 默认导出整个对象,用户只能全引入
  • 错误只抛 new Error(失败)
  • 配置项有二十个必填
  • 为了一个小功能引入三个依赖

更好的做法

  • 具名导出,天然可 tree-shake
  • 错误里带上下文和修复建议
  • 提供合理默认值,按需覆盖
  • 优先零依赖,把选择权留给用户
面试怎么答好的包设计是 API 面尽量小、全部使用具名导出以便 tree-shaking、错误信息带上实际值和修复建议、提供合理默认值。是否零依赖要看场景,工具库适合零依赖,复杂功能硬做零依赖反而质量更差。
12

版本发布自动化

changesets 解决的是"改了什么"
手动改版本号的问题是:没人记得该升 major 还是 patch。changesets 让每次 PR 附带一个变更说明,发布时自动汇总、自动升版本、自动写 CHANGELOG。
开发时每次改动加一个 changeset 文件,写明 patch/minor/major
→
合并后CI 汇总所有 changeset,开一个版本发布 PR
→
发布时合并那个 PR,自动改版本号 + 写 CHANGELOG + 发布
一个 changeset 长这样--- "@myorg/utils": minor --- 新增 debounce 的 immediate 选项。 <em>关键点:版本决策发生在写代码的时候,那时最清楚这次改动的影响。</em>
面试怎么答changesets 让版本决策在写代码时就记录:每个 PR 附带一个说明本次改动是 patch、minor 还是 major 的文件,发布时自动汇总生成版本号和 CHANGELOG。monorepo 场景尤其必要,发布应该走 CI 而不是本地手动执行。
13

README 决定了有没有人用

技术选型时,多数人只看前 30 行
README 的结构比文笔重要:一句话说明它解决什么问题 → 安装 → 最小可运行示例 → 常见场景 → 限制。别一上来就贴 API 大全。
一个能用的骨架# my-lib 一句话:把 X 转成 Y,支持 Z 场景。 ## 安装 npm i my-lib ## 30 秒上手 (一段能直接复制运行的代码,不要有省略号) ## 常见场景 - 场景 A:... - 场景 B:... ## 和同类库的区别 (诚实说明,包括你不擅长的地方) ## 限制 (写清楚,减少无效 issue)
面试怎么答README 前 30 行决定别人要不要继续看,结构应该是一句话定位、安装、可直接运行的最小示例、常见场景、与同类库的区别和限制说明。写清限制反而能建立信任并减少无效 issue。
14

速答清单

考前十分钟只看这页
01main / module / browser / types 分别是什么?
main 给 CJS 和 Node,module 给打包器,browser 做浏览器替换,types 指类型声明。
02exports 字段要注意什么?
按顺序匹配命中即停,types 必须放最前;一旦定义就是封闭的,未声明的子路径访问不到。
03^ 和 ~ 的区别?
^1.2.3 允许升 minor 和 patch,~1.2.3 只允许 patch。0.x 时 ^ 等价于 ~。
04为什么要用 peerDependencies?
插件和组件库不能让用户装两份宿主库,否则出现多实例,hooks 和 context 会失效。
05npm ci 和 install 的区别?
ci 严格按 lockfile 装,不一致就失败,可复现;install 会重新解析并可能改 lockfile。
06sideEffects: false 有什么风险?
纯副作用导入(CSS、polyfill)会被误删。应该改成数组列出这些文件。
07发布前最重要的一件事?
npm pack --dry-run,先看包里到底有什么再发。files 是白名单。
08发错了怎么办?
优先 npm deprecate 并马上发修复版。unpublish 有 24 小时限制且对已安装用户无效。
09dist-tag 是什么?
版本之外的推荐指针,默认 latest。预发布用 --tag beta 或 next,不影响普通用户。
10双包陷阱是什么?
同一份代码打成 CJS 和 ESM 两份,模块级状态会各存一份导致分裂。有状态的包只发一种格式。