简介
整篇文章大概会涉及到以下内容:
- package.json 中一些常用重要字段用途讲解,比如
sideEffects
、publishConfig
、exports
、module
、main
、version
等。 - 如何发布一个优秀的npm包。
从 package.json 出发
name
name
字段在 npm 注册表中唯一标识一个包。
如果你希望发布的包属于某个组织或团队,可以使用 scoped packages。Scoped packages 的名称以 @scope/
开头,例如 @your-org/package-name
。
其中Scoped packages允许你创建一个命名空间,从而避免命名冲突。例如,如果你有一个名为 utils
的包,可以将其命名为 @your-org/utils
,这样即使其他开发者也有一个名为 utils
的包,也不会产生冲突。
version
version
字段表示该项目包的版本号。
主版本号
当新版本无法兼容基于前一版本的代码时,则提高主版本号;
次版本号
当新版本新增了功能和特性,但仍兼容前一版本的代码时,则提高次版本号;
修订号
当新版本仅仅修正了漏洞或者增强了效率,仍然兼容前一版本代码,则提高修订号;
^ 兼容某个大版本
^
意味着下载的包可能会是更高的次版本号或者修订版本号。(也就是主版本不能变,次版本、修订版本可以随意变)。
兼容某个大版本
如:^1.1.2 ,表示>=1.1.2 <2.0.0,可以是1.1.2,1.1.3,.....,1.1.n,1.2.n,.....,1.n.n
~ 兼容某个次版本
而~
意味着有可能会有更高的修订版本号。(也就是主版本、次版本不能变,修订版本可以随意变)。
兼容某个次版本
如:~1.1.2,表示>=1.1.2 <1.2.0,可以是1.1.2,1.1.3,1.1.4,.....,1.1.n
private
private
字段用于指示该包是否为私有包。如果将 private
字段设置为 true
,则该包不会被发布到 npm 公共注册表。这对于确保某些包仅用于内部开发或特定项目非常有用。
publishConfig
publishConfig
字段在 package.json
文件中用于指定发布包时的一些配置选项。这个字段可以帮助你自定义发布行为,例如指定发布到哪个注册表、设置访问权限等。
例如(这样就会发布到指定服务器上(私服)):
"publishConfig": {
"registry": "https://my.npmjs.org/",
}
但是需要注意的时,即使设置了private为false,且设置了publishConfig的registry为私服地址。发包后,仍会发布到私服,而不会发布到npm上。
typings
typings
字段用来指定TypeScript
的入口文件
files
files
配置是一个数组,用来描述当把npm
包作为依赖包安装时需要发布的文件列表。当npm
包发布时,files
指定的文件会被推送到npm
服务器中,如果指定的是文件夹,那么该文件夹下面所有的文件都会被提交。比如可以在该文件中这样写:
// package.json
"files": [
"lib",
"es"
],
如果有不想提交的文件,可以在项目根目录中新建一个.npmignore
文件,并在其中说明不需要提交的文件,防止垃圾文件推送到npm
上。这个文件的形式和.gitignore
类似。写在这个文件中的文件即便被写在files
属性里也会被排除在外。比如可以在该文件中这样写:
// .npmignore
node_modules
.vscode
build
.DS_Store
main
、module
main
字段用来指定加载的入口文件,在 browser
和 Node
环境中都可以使用。如果将项目发布为npm
包,那么当使用 require
导入npm
包时,返回的就是main
字段所列出的文件的module.exports
属性。如果不指定该字段,默认是项目根目录下的index.js
。如果没找到,就会报错。
module
字段可以定义 npm
包的 ESM
规范的入口文件,browser
环境和 node
环境均可使用。如果 npm
包导出的是 ESM
规范的包,使用 module
来定义入口文件。
Webpack
在进行项目构建时,有一个target
选项,默认为Web
,即构建Web
应用。如果需要编译一些同构项目,如node
项目,则只需将webpack.config.js
的target
选项设置为node
进行构建即可。如果在Node
环境中加载CommonJS
模块,或者ESM
,则只有main
字段有效。
webpack 在 target: web
的情况下 mainFields
字段默认为 ['browser', 'module', 'main']
。这样就意味着假如你使用 webpack 构建你的 Web 项目,无论你使用 ESM
还是 CJS
语法引入第三方包,本质上都是会优先查找 module
字段是否存在,之后才会去寻找 main
字段。
exports
exports
的优先级是高于任何入口字段的(module
、main
、browser
) 。
路径封装
exports
字段可以对于包中导出的路径进行封装。
比如以下代码:
{
// 表示该包仅存在默认导出,默认导出为 ./index.js
"exports": "./index.js"
}
// 上述的写法相当于
{
"exports": {
".": "./index.js"
}
}
定义了该字段后,该 Npm 包仅支持引入包自身,禁止引入其他子路径,相当于对于子路径的封装。
换句话说,仅仅只能引入 index.js
。比如我们引入了未在 exports
中定义的模块,会报错。
如果想要该包引入其他模块,可以增加其他模块配置,如下:
{
"exports": {
// . 表示引入包默认的导出文件路径, 比如 import lsx from 'lsx'
// 这里的 . 即表示未携带任何路径的 lsx,相当于默认导出 ./index.js 文件的内容
".": "./index.js",
// 同时额外定义一个可以被引入的子路径,下面的代码相当于导出./src/submodule.js文件的内容
// 可以通过 import lsxSub from 'lsx/submodule.js' 进行引入 /src/submodule.js 的文件
"./submodule.js": "./src/submodule.js"
}
}
条件导出
通常我们编写的 NPM 包支持被 ESM 和 CJS 两种方式同时引入,需要根据不同的引入方式来寻找不同的入口文件。
// package.json
{
"exports": {
// ESM 引入时的入口文件
"import": "./index-module.js",
// CJS 方式引入时寻找的路径
"require": "./index-require.cjs"
},
}
// 相当于
{
"exports": {
"import": {
".": "./index-module.js"
},
"require": {
".": "./index-require.cjs"
}
},
}
可以看到 exports
关键字中定义的 key 为 import
和 require
分别表示两种不同的模块引入方式使用该包时引入的不同文件路径。
关于条件判断的 Key 值,除了上述的 import
和 require
分别代表的 ESM 引入和 CJS 引入的方式,NodeJS 同样提供了以下的条件匹配:
"import"
- 当包通过ESM
或加载时匹配import()
,或者通过 ECMAScript 模块加载器的任何顶级导入或解析操作。"require"
- 当包通过CJS
加载时,匹配require()
。"default"
- 始终匹配的默认选项。可以是 CommonJS 或 ES 模块文件。这种情况应始终排在最后。 (他会匹配任意模块引入方式)
需要注意的是 exports
中的 key/value 顺序很重要,在发生条件匹配时较早的具有更高的优先级并优先于后面的配置。
当然上边我们提到的条件导出不仅仅适用于包的默认导出路径,同样也适用于子路径。比如:
{
"exports": {
".": "./index.js",
"./feature.js": {
"import": "./feature-node.js",
"default": "./feature.js"
}
}
}
注意:
如果引入的 Npm 包中定义了
exports
关键字来定义对应的入口文件导出,package.json中的module
、main
字段都是无效。此时自然
webpack
中的resolve.mainFields
字段也会失去它的效果,需要通过resolve.conditionNames
字段来定义对应的环境。也就是说,在引入的 Npm 包的 package.json 中如果存在
exports
关键字时,构建配置的resolve.mainFields
是无效的。如果未设置
resolve.conditionNames
字段,那么默认webpack
会按照你当前的运行环境以及引入方式从而去 npm 包中的exports
字段查找对应匹配的文件。
sideEffects
如果需要treeShaking,这是一个极为重要的配置。该配置用于告诉webpack,这个npm是不是有副作用的,这在减小业务包体积极为有用。比如开发组件库时,通常会把样式文件配置到这里,说明样式是存在副作用的时候。那么wenpack在打包时就不会剔除掉这些代码。
发布一个优秀的npm包
通常来讲,一个好的npm包中的package.json中的配置项,在上文中已经大都提到了。
接下来主要介绍一下发包的流程,以及一些注意事项。
- 配置包名、版本。
- 配置包的入口,即main、module。通常我们的包需要支持node和esmodule。
- 配置包的类型文件入口,即typings。
- 配置package.json中的files,发布指定文件夹到服务器。
- 配置npm私有服务器地址,即publishConfig。
- 配置打包脚本,通常会先编译文件再发包。
打包脚本,比如:
"compile:rollup": "rollup -c --bundleConfigAsCjs",
"push": "yarn compile:rollup && yarn publish"
注意事项:
- 合理配置dependencies和devDependencies,因为发包后,dependencies会被跟随包一起构建。如果配置不合理,无意会增大包体积。
- 如果包发布到私服,使用方需要配置.npmrc文件,配置私服地址,从私服拉取npm包。
- 配置sideEffects,不会你的包不支持treeShaking吧。
- 合理配置npm包的入口,通常入口文件很简单,只是简单的导出。
比如,以下代码是笔者在开发一个vue组件库时的一个入口文件:
export * from './types';
export * from './components/LevelReading/interface';
// 四线三格
export { default as EnglishLine } from './components/EnglishLine/index.vue';
// 分级阅读
export { default as LevelReading } from './components/LevelReading/index.vue';
// 单词卡
export { default as WordCard } from './components/WordCard/index.vue';
最后
文章的内容到这里就要画上句号了,感谢每一位可以看到结尾的小伙伴。
希望大家可以从文章中的内容有所收获,当然也欢迎每一位小伙伴在评论区留下自己的见解我们互相讨论。
如果我的文章帮助你解决了一些困惑或者学到了一些东西,也希望你能帮忙点个关注。