(注意,当前文档还处于编辑更新过程,还未验证本文内容,参考内容会尽量给出原文链接。)
1. rollup简介
深入对比webapck、Parcel、Rollup打包工具的不同
1.1 rollup
比较常用的几个插件:@rollup/plugin-replace、@rollup/plugin-node-resolve、@rollup/plugin-commonjs、@rollup/plugin-babel、rollup-plugin-terser、rollup-plugin-typescript2
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.7、1.2.8、1.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.4、1.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
复制代码
上面列出的beta、csp、latest就是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文件。