rollup打包、npm发包学习

371 阅读7分钟

(注意,当前文档还处于编辑更新过程,还未验证本文内容,参考内容会尽量给出原文链接。)

1. rollup简介

rollup简介

webpack、rollup、gulp对比

深入对比webapck、Parcel、Rollup打包工具的不同

1.1 rollup

rollup插件github地址

比较常用的几个插件:@rollup/plugin-replace、@rollup/plugin-node-resolve、@rollup/plugin-commonjs、@rollup/plugin-babel、rollup-plugin-terser、rollup-plugin-typescript2

rollup选项大列表

1.1.1 rollup打包范围

Rollup集成第三方工具: package.json中的dependencies、devDependencies等依赖决定了node_modules中哪些模块会被打包到bundle,详情可参考链接。但rollup并不能"开箱即用"地处理NPM包依赖关系,需要添加一些配置,@rollup/plugin-node-resolve可以让rollup查找到外部模块(安装在node_modules中的模块),前置依赖external决定在打包的bundle中不会包含哪些外部模块,同时由于大多数npm包都是CommonJS,因此需要使用@rollup/plugin-commonjs将CommonJS转成ES2015;此外还有怎么使用babel和Gulp。

注意事项:与传统的CommonJS和AMD这一类非标准化的解决方案不同,Rollup使用的是ES6版本JavaScript中的模块标准。但大多数NPM包暴露的都是CommonJS模块,因此,我们需要使用@rollup/plugin-commonjs来将CommonJS转换为ES2015。同时在使用Babel时(@rollup/plugin-babel插件加载Babel),要将modules设置为false(即@babel/preset-env中的modules),否则Babel将在rollup有机会执行其操作之前将模块转换为CommonJS,从而导致失败。

1.1.2 tree shaking

tree-shaking可以用来消除无效代码tree shaking是依赖ES Module的模块特性来工作的,rollup中treeshake默认值是true(即默认使用)。如前面所述的配置 rollup使用@rollup/plugin-node-resolve、@rollup/plugin-commonjs可以将node_modules下的包转成ES2015,@babel/preset-env的modules设置为false将保留ES modules,这些是rollup能够使用tree shaking的前提条件。

2. npm发包预备知识

参考自:前端工程化之npm知识储备

2.1 版本号(semver规范)

关于版本号,强烈推荐学习一下语义化版本2.0.0

可以看一下版本号

主版本号.次版本号.修订号 = major.minor.patch

range matching semantics:>、<、^1.2.3、~1.2.3等的含义

带有预发布关键词的版本号:

预测版alpha(α)、测试版beta(β)、最终测试版rc(release candidate),如1.2.4-alpha.1,1.2.3-beta.2。

该类版本号的作用有两个:(参考自node-semver

(1) 带有预发布关键词的版本更新往往很快、很频繁,包含很多并不适合公用的breaking changes。

因此带有预发布关键词的版本,只允许与有相同 major.minor.patch、相同预发布关键词的版本进行比较(我对原文的理解,如有错误请指出),如 1.2.1-alpha.1 可以与 1.2.1-alpha.x 进行比较,但不能与 1.2.1-beta.x、1.2.2-alpha.x 进行比较,更多例子如下:

>=1.2.7 <1.3.0能够匹配到1.2.71.2.81.2.99,但匹配不到1.2.8-aplpha.1
>1.2.3-alpha.3 仅仅能够匹配 1.2.3-aplpha.x(x>3)的版本,如1.2.3.alpha.4,不能匹配1.2.41.2.4-aplpha.4,当然也不能匹配到1.2.4-beta.4等。
~1.2.3-beta.2 能够匹配1.2.3-beta.4,但不能匹配到1.2.4-beta.2(它有不一样的[major,minor,patch])。
^1.2.3-beta.2 能够匹配1.2.3-beta.4,但不能匹配到1.2.4-beta.2(它有不一样的[major,minor,patch])。

(2) 正如其含义一样,往往alpha代表预测版、beta代表测试版、rc代表最终测试版。

此外,如果不想让此次更新正式发布,还可以创建预发布版本,如下:

# 当前版本号为 1.2.3
npm version prepatch  # 版本号变为 1.2.4-0,也就是 1.2.4 版本的第一个预发布版本
npm version preminor  # 版本号变为 1.3.0-0,也就是 1.3.0 版本的第一个预发布版本
npm version premajor  # 版本号变为 2.0.0-0,也就是 2.0.0 版本的第一个预发布版本
npm version prerelease  # 版本号变为 2.0.0-1,也就是使预发布版本号加一

2.2 项目版本号管理

方案一,使用npm自带的npm version:

对应package.json中的version字段,可以使用npm version指令自动更新version。注意,在git环境中执行 npm version修改完版本号后,还会默认执行 git add -> git commit -> git tag操作,可以使用 npm version xxx -m 'XXX' 来指定git commit时的message,如XXX为feat: upgrade ...。如果git工作区有未提交的修改 npm version会执行失败,可以使用--no-git-tag-version参数不让npm version指令影响git仓库,也可以在npm设置中禁止。(原文可见链接,建议看原文)

方案二,使用node-semver管理版本(推荐,但未代码验证):

开发者可以使用包node-semver来进行版本比较(如semver.gt(v1, v2))、根据当前版本号生成下一个版本号(如semver.inc(currentVersion, 'patch'))。

(1)开发者可以使用exec来获取包的所有远程版本(这里的exec也可以换成包execa。 ):

const {exec} = require('child-process-promise');
const getOnlineVersion = async(packageName) => {
    const {stdout} = await exec(`npm view ${packageName} versions`);
    return stdout;
}

(2)在开发过程中,开发者可以先使用(1)中方法获取包的所有远程版本号,计算出最大版本号,再使用node-semver生成最大版本号的下一个版本号,如semver.inc(currentMaxVersion, 'patch')或semver.inc(currentMaxVersion, 'prepatch', 'alpha')。

方案三:听说changesets也挺好用的,等上手后再补充这部分内容。

2.3 模块tag管理(链接

(原作者写的太好了,这里将原内容摘抄到这里)

不经常发布包的同学可能对模块 tag 概念不是很清楚。以vue为例,首先执行npm dist-tag ls vue查看vue包的tag

beta: 2.6.0-beta.3
csp: 1.0.28-csp
latest: 2.6.10
复制代码

上面列出的betacsplatest就是tag。每个tag对应了一个版本。

tag到底有什么用呢?

tag类似于git里面分支的概念,发布者可以在指定的tag上发布版本,而使用者可以选择指定的tag来安装包。不同的标签下的版本之间互不影响,这在发布者发布预发布版本包和使用者尝鲜预发布版本包的同时,不影响到正式版本

在发布包的时候执行npm publish默认会打上latest这个tag,实际上是执行了npm publish --tag latest。而在安装包的时候执行npm install xxx则会默认下载latest这个tag下面的最新版本,实际上是执行了npm install xxx@latest。当然,我们也可以自定义tag

# 当前版本为1.0.1
npm version prerelease  # 1.0.2-0
npm publish --tag beta
npm dist-tag ls xxx  # # beta: 1.0.2-0
npm install xxx@beta  # 下载beta版本 1.0.2-0
复制代码

prerelease版本已经稳定了,可以将prerelease版本设置为稳定版本:

npm dist-tag add xxx@1.0.2-0 latest
npm dist-tag ls xxx  # latest: 1.0.2-0

3. 使用pnpm workspace构建monorepo工程

参考自:pnpm+workspace+changesets构建你的monorepo工程(强烈推荐去看原文,这里的内容摘抄自原文)。

monorepo是把多个工程放到一个git仓库中进行管理,因此他们可以共享同一套构建流程、代码规范也可以做到统一,特别是如果存在模块间的相互引用的情况,查看代码、修改bug、调试等会更加方便。

在现有的众多选项中,pnpm workspaces是一个不错的选项。(lerna也是一个不错的选项,但lerna现在已经不再持续维护了。)

3.1 pnpm workspace

pnpm是新一代的包管理工具,可以实现节约磁盘空间并提升安装速度、创建非扁平化的node_modules文件夹。关于pnpm的详细介绍可见 pnpm项目初衷pnpm是凭什么对npm和yarn降维打击的都2022年了,pnpm快到碗里来

使用pnpm worksapce的方式也很简单,对于如下工程目录结构的项目(子包都放在packages目录下如pkg1、pkg2),工程根目录下的pnpm-workspace.yaml配置文件中指定工作空间的目录:

.
├── README.md
├── package.json
├── packages
│   ├── pkg1
│   │   ├── package.json
│   │   ├── src
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   └── pkg2
│       ├── package.json
│       ├── src
│       │   └── index.ts
│       └── tsconfig.json
├── pnpm-workspace.yaml
└── tsconfig.root.json
└── package.json

其中的pnpm-workspace.yaml的内容如下:
packages:
  - 'packages/*'

(1) 为了防止根目录被发布出去,需要设置工程根目录下package.json配置文件的private字段为true.

(2) pnpm安装依赖包分以下几种情况:

对于全局的公共依赖包,如rollup、typescript等,可以使用 pnpm 的-w参数,将依赖包安装到工程的根目录下,作为所有package的公共依赖,如:pnpm install react -w,对于开发时依赖可以添加-D参数安装到package.json中的devDependencies中,如pnpm install rollup -wD

针对某个package单独安装指定依赖时,可以使用pnpm的--filter参数,如pnpm add axios --filter @qftjs/monorepo1,注意--filter后跟着的是package下package.json的name字段,用法可参考filter文档

模块间的相互依赖,当pkg1作为pkg2的依赖进行安装时,可以使用pnpm的 workspace: 协议,$ pnpm install @qftjs/monorepo2 -r --filter @qftjs/monorepo1,此时我们查看 pkg1 的 package.json,可以看到 dependencies 字段中多了对 @qftjs/monorepo2 的引用,以 workspace: 开头,后面跟着具体的版本号。

{
  "name": "@qftjs/monorepo1",
  "version": "1.0.0",
  "dependencies": {
    "@qftjs/monorepo2": "workspace:^1.0.0",
    "axios": "^0.27.2"
  }
}

在设置依赖版本的时候推荐用 workspace:*,这样就可以保持依赖的版本是工作空间里最新版本,不需要每次手动更新依赖版本。当 pnpm publish 的时候,会自动将 package.json 中的 workspace 修正为对应的版本号。

(3)只允许pnpm,在项目根目录的package.json中使用preinstall

{
  "scripts": {
    "preinstall": "npx only-allow pnpm"
  }
}

preinstall 脚本会在 install 之前执行,现在,只要有人运行 npm install 或 yarn install,就会调用 only-allow 去限制只允许使用 pnpm 安装依赖

3.2 代码规范检查(husky、lint-staged、eslint)

参考自构建前端代码预提交检查,这里着重讲解在项目中怎么配置出对代码进行检查的功能,即开发者在git commit前检查js或ts的的代码问题(防止🐶woof)。

核心配置:

安装:npm install --save-dev eslint babel-eslint @ecomfe/eslint-config husky lint-staged

package.json文件中:
"lint-staged": {
    "*.{ts}": "eslint"
}

"husky": {
    "hooks": {
        "pre-commit": "lint-staged"
    }
}

ESLint的配置文件.eslintrc.js中:
module.exports = {
    extends: [
        'eslint-config-standard'
        '@ecomfe/eslint-config/typescript',
    ]
}

整体流程:

husky在你git commit的时候,调用pre-commit钩子,执行lint-staged,lint-staged触发ESLint检查所有 git add存到git暂存区的文件,若有不符合规则且无法自动修复的,停止此次提交,若通过ESLint检查就成功commit。

名词解释:

(1)husky

官方描述Modern native Git hooks made easy(让git hooks变得简单)。可以解决git hooks共享问题(你在.git/hooks中创建了一些hooks,希望分享给队友,但.git/hooks文件夹并不会提交到远端,无奈只能拷贝,参考自 husky原理解析及在代码Lint中的应用)。

关于githook,可参考手摸手教你使用最新版husky(v7.0.1)让代码更优雅规范官网githooks)。

使用示例如前面的核心配置中,当用户在git commit的时候,调用pre-commit钩子,执行lint-staged,执行eslint的规则进行检查,如果有不符合规则且服务自动修复的,会停止此次提交。

(2) lint-staged

lint-staged是一个在git暂存文件上运行linters的工具。也就是说当在package.json中配置成{"lint-staged": {"*": "your-cmd"}},如果您做了 git add file1 file2,lint-staged将会运行命令your-cmd file1 file2(参考自lint-staged使用教程) 。

(3)ESLint

Lint是一种静态代码分析工具,用于标记代码中某些编码错误、风格问题和不具结构(易导致bug)的代码。简单理解就是一个代码检查器,检查目标代码是否符合语法和规定的风格习惯。ESLint是基于ECMAScript/JavaScript语法的Lint。新手对ESLint的了解可参考前端科普系列(5): ESLint-守住优雅的护城河ESLint使用教程

个人感觉继承配置extends在ESLint中比较实用,可以很方便地继承已有配置的全部特性,包括rules、env、extends等,如在本文核心配置的.eslintrc.js中的使用。

3.3 配置@microsoft/api-extractor把所有的.d.ts合并成一个

参考自:使用rollup和api-extractor打包utils库

API Extractor是由微软提供的针对Typescript的API分析工具,它的功能如下:

  • 将所有TS类型定义导出到一个.d.ts文件(最有用)。
  • 从项目入口遍历所有export的类型,生成api报告。
  • 生成文档米哦书模型(xxx.api.json)可以通过微软提供的api-documenter进一步转换成Markdown文档。

本项目面临的问题:

前面使用rollup打包生成.js文件,借助插件@rollup/plugin-typescript生成.d.ts后缀的类型定义文件,但其生成的类型定义往往分散在项目不同的文件下,别人使用我们的包时需要从node_modules下引用这些不同的.d.ts模块,很不方便。

api-extractor主要用来解决上面的问题,它可以将所有类型定义从一个入口获取到,最后汇总到一个.d.ts文件。