npm发包必知必会

4,658 阅读9分钟

想必很多人都在 npm 上发过包,但是如果没有完整的查阅相关文档,肯定有很多细节不太了解,所以我在查阅了相关文档后总结了一些需要注意的知识点。

包和模块的关系

npm registry 存储了很多 JavaScript packages,他们中大多数都是 Node modules,或者包含 Node modules。

  • package 是被 package.json 文件描述的的文件或目录。

  • module 是在 node_modules 目录内能被 require() 函数引用的文件或目录。

    如下面代码所示,我们会说变量 req 指向 request 模块。

    var req = require('request')
    

一般来说我们发包就是为了导出一个或多个模块。

创建 package.json 文件

如果要发布一个包,里面必须得有package.json 文件,且 name 和 version 字段是必须的。

name

name 不能包含大写字母、URL不安全的字符,字符总数不超过214。

创建包时,可以用 npm view <name> 命令快速查看该 name 是否存在

verison

版本号必须能被 node-semver (Semantic Versioning)解析。

版本发布有几种级别: major, minor, patch, premajor, preminor, prepatch, or prerelease.

假设当前版本是 "2.0.1",如果我们要升级版本,不同级别的版本号对应如下:

version.png

其中 beta 是我们设置的 preid,我们一般会在发布时用此作为包的 dist-tag。

语义化版本标准

我们在发布时,版本号应该符合 Semantic Versioning 标准:

  1. 如果你的包已经基本完善了,觉得可以给大家用了,应该以 1.0.0 发布;
  2. 如果只是修复 bug 或者一些 minor change,应该以 Patch release 发布;
  3. 如果加了新功能但是向下兼容、没有 breaking change,应该以 Minor release 发布;
  4. 有不向下兼容的 breaking change,应该以 Major release 发布。

同时作为包的消费者时,我们可以指定我们能够接受的版本范围:

  • Patch releases: 1.0 or 1.0.x or ~1.0.4
  • Minor releases: 1 or 1.x or ^1.0.4
  • Major releases: * or x

我们使用 npm install 时默认接受的是 Minor 级别的发布,即默认接受向下兼容的补丁和新特性版本升级:

npm install vue                   # "vue": "^3.2.33"
npm install vue --save-prefix='^' # "vue": "^3.2.33"
npm install vue --save-prefix='~' # "vue": "~3.2.33"
npm install vue --save-exact      # "vue": "3.2.33"

files

指定哪些文件会被打包发布(后面详细描述)。

main、browser、exports、module

这四个字段都是用来定义包的入口,npm 只规定了 main 和 browser 这两个字段,exports 是 nodejs 的规范,module 一般被构建工具所支持。

main

main 规定了包的主要入口点,默认值为 index.js。

browser

browser 表示你的模块只能在浏览器端运行,比如依赖 window 对象,可以用 browser 字段替代 main 。

exports

exports 需要 Node.js 版本不低于 v14.13.0、v12.20.0 才支持。当版本支持该字段时,优先级使用该字段为包的入口定义字段。

exports 支持定义子路径导出和条件导出,同时封装包(防止使用未导出的模块作为入口)。

子路径导出

假设有个包的目录结构是这样的:

my-lib-tree.png

我们想要加载这三个模块可以这么写:

my-lib-main.png

但是如果我们定义了 exports:

my-lib-exports-json.png

就可以这样引入上面的模块:

my-lib-exports.png

封装包和文件路径导出

使用exports 字段会封装包,禁用文件路径导出,如果再像之前一样按文件路径引入模块就会报错:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './dist/hello' is not defined by "exports" in .../my-lib/package.json

想要公开包中的每个文件、继续支持按文件路径导入模块可以在 exports 里加上 "./*": "./*" 来禁用包封装,并且在导入文件模块时需要使用完整路径导入:

  • 如果直接 require('my-lib/dist/hello') 会报错 Error: Cannot find module;
  • 正确的引入方式为 require('my-lib/dist/hello.js')。
条件导出

条件导出提供了一种根据特定条件映射到不同路径的方法,比如想要为 require() 和 import 提供不同的模块导出可以这样写:

exports-condition.png

双包风险

但上面这种同时导出 CommonJS/ES 模块包可能会引发一些问题,比如双包风险(Dual package hazard):const pkgInstance = require('pkg')与import pkgInstance from 'pkg'是两个不同的模块,虽然在同一个应用中一般不会加载两个版本的模块,但是应用的依赖项加载不同版本的模块却很有可能发生,这时就有可能出现错误(除非你的包本身就没有状态)。

所以导出双包时,一般有两种方法来避免发生错误:

  1. 隔离状态:用 CommonJS 模块来管理状态,ES 模块直接导出状态对应的 CommonJS 模块。 dual-package-state.png

  2. 用 ES 模块包住 CommonJS 模块:通过打包工具生成 CommonJS 模块,再用 ES 模块导出所有 CommonJS 模块。 dual-package-wrapper.png

虽然这样可以解决双包带来的风险,但相信未来的趋势会是 ES module only。

module

一般被构建工具所支持的 ES 模块导出。

vue-module.png

根据 esbuild 对 module 字段的解释,以前有人提议用 module 字段导出 ES 模块,这个提议 node 并没有采用(而是用了 "type": "module" 来表示 ES 模块),但是主要的构建工具都支持了这个字段。

为什么要针对构建工具单独导出一个模块

拿 Vue3 举例,源码中定义很多常量,打包时会根据包的不同使用环境使用不同的值。

  • __DEV__:当构建用于开发环境的模块时会替换为 true ,构建用于生产环境的模块时会替换为 false ,构建提供给构建工具的模块时会替换为 (process.env.NODE_ENV !== 'production')。
  • __FEATURE_OPTIONS_API__:如果是提供给构建工具就会替换为常量 __VUE_OPTIONS_API__,否则会替换为 true 即默认开启 options api。在 @vitejs/plugin-vue 和 @vitejs/plugin-vue-jsx 插件中,如果发现用户没有自己定义常量值,就会给到默认值 true。

type

同 exports 一样,type 字段也是 nodejs 的规范,如果设置了 "type": "module" 意味着包内部的 .js 文件应该以 ES 模块加载。

到这里,我们基本知道了想要导出 ES 模块有三种方式:

  1. "type": "module" 配合 "main" 字段;
  2. 使用 exports 条件导出;
  3. 使用 module 字段(被构建工具支持)。

bin

定义包中的可执行文件对应的命令:

#!/usr/bin/env node
console.log('he11o')
{
  "bin": {
    "say-hello": "sayHello.js"
  }
}

如果 packageName 和命令相同可进行简写:

{
  "name": "sayHello",
  "bin": "sayHello.js"
}

如果全局安装了此包,say-hello 命令会添加到 bin 目录。

如果是在项目中安装了此包,可以在 "scripts" 里面直接使用 say-hello 命令,或者在项目路径中输入 npm exec say-hello 或 npx say-hello 执行此文件。

dependencies

指定项目的依赖包及其版本范围或链接地址,用户使用 npm install <package> 安装包时会自动安装其 dependencies 里面指定的包。

对于需要用户安装的依赖包,我们在使用 rollup 等打包工具进行打包时,需通过 external 选项指定为外部依赖。

devDependencies

用户使用 npm install <package> 安装包时会忽略 devDependencies 里面的包,一般用于存放一些用户不需要安装的依赖,比如测试、构建相关的包,或者需要将模块打包到我们的包文件中。

这种字段放在包里一般是没有意义的,因为一般我们不会把源码发包,用户也不会在 node_modules 中进行调试,如果有代码洁癖在发布时移除该字段,或者使用 pnpm workspace 管理项目。

peerDependencies

直译过来就是“同等的依赖”,当前 package 和安装当前 package 的应用需要引用同一个依赖包,因此需要应用提前安装好符合版本要求的依赖包。为了尽量避免和宿主环境的包版本冲突,依赖的版本范围应尽可能更广,不要使用 patch 版本作为 peerDependencies。

使用 npm install <package> 安装包时可能安装、也可能不安装 peerDependencies 里面的包,这取决于包管理工具。npm 从 v7 开始会自动安装,pnpm 会提示你需要手动安装。

CLI Commands

介绍几个与发包相关的命令。

npm publish

npm publish [<tarball>|<folder>] [--tag <tag>] [--access <public|restricted>]

folder

第一个参数默认为 . 即当前所在目录。并不是目录下所有文件都会发布到 npm ,我们可以手动配置(优先级从低到高):

  • .gitignore
  • .npmignore
  • The files in the package.json "files" field(Omitting the field will make it default to ["*"], which means it will include all files)

下面这些特殊文件会忽略上面的配置总是发布(README & LICENSE 不管大小写和后缀名):

  • package.json
  • README
  • CHANGELOG
  • LICENSE / LICENCE
  • The file in the "main" field

相反的,这些文件总是会忽略:

  • .git
  • CVS
  • .svn
  • .hg
  • .lock-wscript
  • .wafpickle-N
  • .*.swp
  • .DS_Store
  • ._*
  • npm-debug.log
  • .npmrc
  • node_modules
  • config.gypi
  • *.orig
  • package-lock.json

tag

如果不指定 tag 其默认值就是 latest,通常作为稳定发布版本的标签。其它标签可以根据开发流程自己选择,比如 legacy、 beta、next 等等。

access

第一次发布时设置包的查看、安装权限,后续发布不会影响。如果是个人账号发布,默认值是 public,如果是组织账号,默认值是 restricted。

如果想改变 access 可以使用 npm access 命令。

npm view (aliases: v, info, show)

查看某个包的发布信息,也可以用来判断想要发布的包名是否有冲突。

npm v vue
npm v vue --json
npm v vue versions
npm v vue dist-tags
npm v vue dist-tags.latest

npm dist-tag

查看、操作包的发布标签。

npm dist-tag add <pkg>@<version> [<tag>]
npm dist-tag rm <pkg> <tag>
npm dist-tag ls [<pkg>]

npm deprecate

废弃某个版本范围的包,如果没有指定版本则废弃整个包。

npm deprecate <pkg>[@<version>] <message>

当我们执行命令 npm deprecate <pkg> "deprecated by arman" 后,再使用 npm view <pkg> --json 产看包信息会发现下面多了一行:

{
  "deprecated": "deprecated by arman"
}

当我们再安装这个版本时控制台会出现一行警告:

npm WARN deprecated <pkg>@<version>: deprecated by arman

npm unpublish

从 registry 上移除指定版本范围的包,如果没有指定版本则会删除整个包。

npm unpublish [<@scope>/]<pkg>[@<version>]

包被删除后 24 小时内无法查看、安装,也无法创建使用该 name 的包,可以联系 npm 官方再次使用此包名。

根据 kik, left-pad, and npm 这篇 blog 所描述,被引用比较多的包不确定能否直接删除。

虽然这个包被删除了,但是如果要再次使用,之前的包名和版本组合是不能再使用了。比如之前发布了 v2.0.1,包删除后再发布是不能再用 v2.0.1 了。

npm ERR! 400 Bad Request - PUT https://registry.npmjs.org/arman-demo - Cannot publish over previously published version "2.0.1".

相关文档链接