想必很多人都在 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"
,如果我们要升级版本,不同级别的版本号对应如下:
其中 beta
是我们设置的 preid
,我们一般会在发布时用此作为包的 dist-tag
。
语义化版本标准
我们在发布时,版本号应该符合 Semantic Versioning
标准:
- 如果你的包已经基本完善了,觉得可以给大家用了,应该以
1.0.0
发布; - 如果只是修复
bug
或者一些minor change
,应该以Patch release
发布; - 如果加了新功能但是向下兼容、没有
breaking change
,应该以Minor release
发布; - 有不向下兼容的
breaking change
,应该以Major release
发布。
同时作为包的消费者时,我们可以指定我们能够接受的版本范围:
- Patch releases:
1.0
or1.0.x
or~1.0.4
- Minor releases:
1
or1.x
or^1.0.4
- Major releases:
*
orx
我们使用 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
支持定义子路径导出和条件导出,同时封装包(防止使用未导出的模块作为入口)。
子路径导出
假设有个包的目录结构是这样的:
我们想要加载这三个模块可以这么写:
但是如果我们定义了 exports
:
就可以这样引入上面的模块:
封装包和文件路径导出
使用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
提供不同的模块导出可以这样写:
双包风险
但上面这种同时导出 CommonJS/ES 模块包可能会引发一些问题,比如双包风险(Dual package hazard):const pkgInstance = require('pkg')
与import pkgInstance from 'pkg'
是两个不同的模块,虽然在同一个应用中一般不会加载两个版本的模块,但是应用的依赖项加载不同版本的模块却很有可能发生,这时就有可能出现错误(除非你的包本身就没有状态)。
所以导出双包时,一般有两种方法来避免发生错误:
-
隔离状态:用 CommonJS 模块来管理状态,ES 模块直接导出状态对应的 CommonJS 模块。
-
用 ES 模块包住 CommonJS 模块:通过打包工具生成 CommonJS 模块,再用 ES 模块导出所有 CommonJS 模块。
虽然这样可以解决双包带来的风险,但相信未来的趋势会是 ES module only。
module
一般被构建工具所支持的 ES 模块导出。
根据 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 模块有三种方式:
"type": "module"
配合"main"
字段;- 使用
exports
条件导出; - 使用
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".