前言
说到 npm 包的发布,大伙儿都觉得很简单,不就是 npm publish
命令就可以发布了嘛。是的,最后发布包用到了 npm publish
命令,但是仅靠这一个命令,并不能发布一个高质量,易用的 npm 包,无论你是发布一个开源的 npm 包,还是公司内部的 npm 包,都需要了解发布 npm 包的必知必会,才能更好的提高发布 npm 包的效率。
本篇是结合笔者发布公司内部的脚手架所经历的过程,梳理了以下几点,如果你正准备想要发布 npm 包的话,那本篇值得你去看一看。
准备工作
在你准备大显身手,想要发布一个给其他人使用的 npm 包之前,还需要做一些关于包的准备工作。
package.json 配置
package.json 文件大家很熟悉了,这里重点说说几个关于 npm 包使用的字段:name
、main
、type
、types
、files
、exports
。
name 是指定发布 npm 包的名称,可以通过 npm view @packageName
查看 registry 上是否已经有同名的包了。
现代很多 npm 包都开始放入某个命名空间下,比如 rollup,它的插件的包名基本都是 @rollup/xxx 这种形式。所以初始化生成 package.json 文件的时候,可以这么做:
sh
npm init -y --scope=@my-org
这样就能得到一个命名空间的 package.json:
指定命名空间可以很大程度上避免 npm 包命名冲突的问题。
main 字段是指定入口文件,而type:module
指定包为 ESM 规范,如果你是基于 Node 环境构建的包,又想要使用 ESM 规范,则可以将 .js
后缀修改为 .mjs
。注意一定不要混用模块规范,但有时候可能又无法避免,后续会提到该问题。
types 字段指定该 npm 包的类型定义文件,就是为 TypeScript 项目导入包的类型而准备,一般都是以 *.d.ts
文件作为类型定义文件,生成类型定义文件,需要在 tsconfig.json
文件中配置 declaration: true
和 declarationDir: lib
,然后可以通过构建工具生成,也可以通过 tsc 命令生成类型定义文件。
files 字段指定作为 npm 包的文件,通过该字段可以排除不需要的文件夹或者文件,但是像 package.json
,README.md
,LICENSE
,main
字段指定的文件都是默认包含在 npm 包中的。
以 vite 包为例,这是 vite 包中包含的文件,然后下载 vite,打开 node_modules 找到 vite,可以看到最终发布的包中包含的文件就是 files 所指定的:
这样去除 src 源代码目录,减小了 npm 包的体积。
exports 字段则是指定对外暴露的文件,默认情况下,对外暴露的就是 main 字段指定的入口文件,如果希望暴露其他文件,则可以指定该字段
像是 vite 这种脚手架的 npm 包,还会有一个 bin 字段,用来指定脚手架的命令入口文件。
模块化规范
随着现代浏览器的普及,ESM 规范正在逐渐被社区接受,而以往的 CommonJS 规范依然还存在于那些老的 npm 包中,所以如果你需要开发一个 npm 包的话,建议使用 ESM 规范,ESM规范的代码,引擎可以进行静态分析,天然支持 tree-shaking,但是如果你的包使用了 CommonJS 规范的包,则需要进行将 CommonJS 转换为 ESM,这点可以通过构建工具,诸如 Rollup、Webpack 来完成。
开发中
决定好你要开发的 npm 包的类型以及使用的模块化规范,就可以着手进行开发了。
当然关于 npm 包的设计流程不能少,得好好计划你的实现思路,比如包的运行架构,技术架构,功能架构,还包括版本控制,代码风格等等,都需要在前提的设计流程中体现。
本文主要不是介绍这些,因此就一笔带过了,直接进入代码开发环节。
包依赖
开发过程中,会依赖很多第三方库,如果是平时的业务开发,普遍的做法要么是这些依赖和业务代码一起打包发布,要么是抽离出来,放到 cdn 上面引用。
对于一个 npm 包来说,做法与上面不一样。npm 包只关注自己写的代码,这些第三方的依赖通通可以单独抽离出来,也就是 Webpack 或者 Rollup 中的 external 的概念。这么做不是会导致使用包的时候找不到依赖吗?当时我也有这样的疑惑,但是当你下载一个你发布的包,打开看看包的内容就理解了。
每一个 npm 包下面也存在 node_modules 目录,它其实就是这个包的第三方依赖。注意,这里依赖是使用 pnpm 安装依赖,如果是使用 npm 来安装依赖的话,所有的依赖都是扁平的,这就会导致幽灵依赖。好了,扯远了,感兴趣的同学可以去下面的参考链接学习。总之,这么做的话,可以将自己的代码体积尽可能的减小,调试的时候,npm 包最终的产物也不会包含其他不相关的内容。
这里以 Rollup 为例,来看看提取第三方依赖的配置:
js
export default {
external: [Object.keys(pkg.dependencies), ...Object.keys(pkg.devDependencies)]
}
当然,也可以选择将第三方的依赖打包到自己的代码中,但是这种情况,更适用于第三方依赖比较简单,打包进去不会影响到原本的代码。
代码开发完成以后,就进入调试阶段了。
测试 npm 包
调试阶段,如果是使用 pnpm 作为包管理工具的话,那就比较简单了,可以直接在包的源码下面新建一个 playground 目录,在 pnpm-workspace.yaml
文件下配置 playground 工作空间:
这样就可以在 playground 目录下新建测试项目,在测试项目的 package.json
文件中引入 npm 包:
这里 umi 和 @umijs/plugins 理解为将要发布的 npm 包。配置好依赖以后,下载依赖,pnpm 会自动软连接到 npm 包的位置,此时就可以进行调试了。
当然也可以选择使用 yalc 的工具,这个工具,其实等于在本地创建了一个 npm 仓库,使用 yalc publish
将npm 包发布到本地的仓库,使用 yalc add
添加本地仓库中的依赖,这样就可以在本地调试 npm 包了。yalc 也可以看作 npm link 的高级版本。
包的构建与发布
所以一切准备完成以后,就可以进行构建了,构建也有两种风格,一种是 bundle,另一种是 bundleless。
Bundle 从项目入口出发,递归解析所有依赖,合并到一个文件上进行输出。
Bundle 模式更多是为了应对 HTTP 1.x的限制,比如并发链接在 Chrome 上面最多只有6-8个。一个完整的应用除了 JS 文件,可能包含其他类型的文件,比如图片、CSS文件、JSON文件、SVG文件等。为了使应用尽快在浏览器中呈现,把 JS 相关内容打包成一个文件,可以更快的在浏览器中呈现资源。但是这种方式的弊端也随之而来,合并成一个 JS 文件的体积过于庞大,从而导致 JS 执行时间过长,就产生了首屏时间这样的指标来衡量应用。
Bundleless 只做平行输出,将 TS 文件转换为 JS 文件,将 JS 文件转换为兼容性的 JS 文件,其他文件不做处理,直接输出。
这也就解释了上面为什么把第三方的依赖全部单独抽出来,而不是打包进原有的代码中去。
构建完成以后,就可以 npm publish
进行发布了。这里建议可以将构建和发布的流程交给 CI、CD 工具,避免了手动操作产生的错误,保证 npm 包的稳定性。
后话
发布一个 npm 包的大致流程就是以上这些,本篇简单描述了 npm 包从准备阶段,到最后发布到 registry 上的流程,当然还有很多细节没有提到或者有所疏漏,但是已足够你完成一个 npm 包的发布了。由于准备匆忙,可能有所错误,也欢迎大家指出。
除此之外,npm 包的维护更新也很重要,这是一个持续长久的过程,也是一个不断迭代改进 npm 包的过程,显然这才是重中之重,值得仔细推敲琢磨。