npm发包必知必会

4,025 阅读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 文件,且 nameversion 字段是必须的。

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 只规定了 mainbrowser 这两个字段,exportsnodejs 的规范,module 一般被构建工具所支持。

main

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

browser

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

exports

exports 需要 Node.js 版本不低于 v14.13.0v12.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-hellonpx 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 里面的包,这取决于包管理工具。npmv7 开始会自动安装,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,通常作为稳定发布版本的标签。其它标签可以根据开发流程自己选择,比如 legacybetanext 等等。

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".

相关文档链接