前言
常见的打包工具比较多,比较主流的有webpack、vite、rollup等。它们都在不同的应用场景,以及各自的优缺点。
- webpack:是最主流的打包工具,它是那种大而全的工具。并且将一切都视为模块化,并通过递归依赖关系将它们打包成最终的输出文件。但它是基于 node 进行处理导出性能不够快,并且因为大而全所以配置相对复杂。
- rollup:相比于传统的模块打包工具,在处理模块时可能会引入额外的代码,导致最终文件体积较大。而 rollup 是基于静态分析的,它能够在编译阶段对模块进行更彻底的分析,这使得 Rollup 能够更有效地消除未使用的代码,并生成更紧凑、更高效的输出。
- vite:虽然已经有了如 webpack 和 rollup 等工具,但是它们在某些方面还是有局限性,比如构建速度和开发体验等方面。同时由于浏览器对 ES6 模块的原生支持程度不断提高,使得可以采用原生 ES 模块(ESM)来编写前端应用。
由上面可以看出相较于 webpack 和 vite,它们其实是为了处理项目的工程化及自动化为目的。而 rollup 则是着眼于工具打包的,所以如果开发一个工具框架 rollup 是个更好的选择。
rollup 的使用
相比于其他的工具 rollup 的本身功能较少,以及 api 也更为简单。并且支持常见的文件打包格式,如:amd, cjs, esm, umd 等
基础的配置
基本的使用很简单,只需要引入待打包的文件,然后配置输出格式及文件名即可。
- 控制台执行
rollup -c即可进行打包;或在 package.json 内配置对应的 scripts - 在进行开发是可添加
-w既rollup -w -c用于监听入口文件的变化
// rollup.config.js
export default {
input: "src/main.js",
output: {
file: "bundle.js",
format: "cjs",
},
};
常见的插件
但实际使用中往往不只是一个独立文件,而是通过先定义入口文件。然后再引入更多不同的文件资源及其它 npm 包等。并且在开发工具时为了能有更好的严谨性,我们会使用 TS 作为协助。但正式由于 rollup 的单纯性,它本身并不支持这些能力。所以就需要通过插件去辅助了。
- @rollup/plugin-node-resolve 当我们引入其他 npm 包时,rollup 默认是不知道如何处理这些依赖的,所以就需要这个插件去给他解析相应路径。
- @rollup/plugin-commonjs 因为有一大部分 npm 包是以 cjs 方式打包的,而 rollup 只支持 es6 的 import/export 导出方式,所以就需要改插件进行转换。
- @rollup/plugin-babel 基于 Babel 可以做 js 的向下兼容处理。需配合 @babel/core 和 @babel/preset-env 等
- @rollup/plugin-alias 有时为了方便导入不去写长长的引用路径地址,可以用 alias 做路径解析配置。
- rollup-plugin-typescript2 由于 rollup 默认只支持 js ,所以需要有 ts 转换插件的支持才行。需配合 typescrit
- 更多插件可查看:github.com/rollup/plug…
当我们引入插件后就需要调整相关配置,这样就可以借些 cjs 依赖以及打包 ts 代码了。
// rollup.config.js
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
const ts = require('rollup-plugin-typescript2')
export default {
input: "src/main.js",
output: {
file: "bundle.js",
format: "cjs",
},
// 相关拓展 rollup 功能的插件
plugins: [
nodeResolve(),
commonjs(),
ts(),
],
};
external 属性
虽然 nodeResolve 插件帮我们解决了引入 npm 包的问题,但有时我们更希望外部的 npm 不直接被打包进来,而是保持 package 的引用关系。可以通过调整 external 配置,告诉 rollup 哪些 npm 在打包时要排除,只保持引用而已。
// rollup.config.js
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
const ts = require('rollup-plugin-typescript2')
export default {
input: "src/main.js",
output: {
file: "bundle.js",
format: "cjs",
},
plugins: [
nodeResolve(),
commonjs(),
ts(),
],
// 告诉 rollup 不要直接打包内容,而是保持引用
external: ['xxx-plugins']
};
Pnpm+Monorepo
通过上面的内容,我们已经可以对自己工具进行打包了。但是由于想做的是通用的打包框架,而不是每个工具都建个仓库并拷贝一个配置代码。所以我们就需要借助其他的工具。
Monorepo
Monorepo 是一种单仓库多模块的开发模式,它允许在一个代码库中管理多个项目、组件或服务,提供更好的代码共享和重用性。 由于希望的是通用框架,所以我们希望可以把不同的工具代码都放在一个仓库里。因为它们可能有公用的代码,并且相互之间也可能有引用关系。所以很合适使用 Monorepo 这种管理方式。
Pnpm
因为是个通用框架所以多个工具之间会依赖相同的 npm 包。如果按照以往方式,可能要在每个工具文件下都安装相同的依赖包。那么各自都会有 node_modules 目录这样既浪费空间并且重复安装也更慢。 而 pnpm 通过使用硬连接的方式节约磁盘空间利用率,采用虚拟存储目录和软连接解决幽灵依赖等。它与 Monorepo 结合能提供更好的协作,并且它天生支持了 Monorepo 不需要我们在安装其他工具了。
workspace 配置
由于希望多个模块之间既是独立的又可以相互引用,就需要定义一个工作空间来管理它们。所以需要创建一个** pnpm-workspace.yaml **文件。
- 通过 **packages **配置去定义子模块目录,而后它下面的所有子目录,都会被视为一个独立模块
packages:
- 'packages/*'
基本使用
pnpm 的命令绝大部分都是和 npm 相似的,更多了解可以去 pnpm.io 查看。但是关于 Monorepo 下的方式是 npm 没有的。
pnpm add pkg -w [-D]把包安装在根目录pnpm -F subname add/rm pkg把包安装在**子目录 **subname指的是 package.json 中的name名称
pnpm -F subname scriptname执行子目录内 package.json 下scripts命令pnpm add @xxx/xx@^ --workspace关联 packages 中其他模块
目录结构
以 packages 作为子模块目录,仅包含各自的代码及配置文件内容。
- 使用 Monorepo 后全局只有一个 node_modules 在根目录下
.
├── packages
│ ├─ module-a
│ │ ├─ src # 模块 a 的源码
│ │ ├─ package.json # 仅模块 a 的依赖
│ │ ├─ rollup.config.js # 继承根 tsconfig.json 和自有配置
│ │ └─ tsconfig.json # 继承根 tsconfig.json 和自有配置
│ │
│ └─ module-b
│ ├─ src # 模块 b 的源码
│ ├─ package.json # 仅模块 a 的依赖
│ ├─ rollup.config.js # 继承根 tsconfig.json 和自有配置
│ └─ tsconfig.json # 继承根 tsconfig.json 和自有配置
│
├─ rollup.config.js # 通用配置文件
├─ tsconfig.base.json # 配置文件用于全局继承
├─ node_modules # 整个项目只有一个外层 node_modules
└─ package.json # 包含整个项目公用依赖
脚本拆分
rollup.config.js
- 根目录下的 rollup.config.js 文件,由于需要通用所以改为 **函数形式 **方便配置及拓展。
// rollup.config.js
const { nodeResolve } = require('@rollup/plugin-node-resolve')
const commonjs = require('@rollup/plugin-commonjs')
const ts = require('rollup-plugin-typescript2')
exports.createConfig = function createConfig({ pkg, external = [] }) {
const _pkg = JSON.parse(fs.readFileSync(pkg))
return {
input: "src/main.js",
output: {
format: "cjs",
file: _pkg.main,
sourcemap: true
},
plugins: [
nodeResolve(),
commonjs(),
ts(),
],
// 告诉 rollup 不要直接打包内容,而是保持引用
external: Object.keys(_pkg.dependencies || {}).concat(Object.keys(_pkg.peerDependencies || {})).concat(external)
}
};
- 子目录下的 rollup.config.js 文件,通过调用根目录配置的并传入自己的配置
const path = require('path');
const { createConfig } = require('../../rollup.config')
module.exports = createConfig({
pkg: path.resolve('./package.json')
})
tsconfig.base.json
- 由于需要使用 typescript 进行开发,所以还需要在根目录添加 tsconfig.base.json 的配置内容
// tsconfig.base.json
{
"compilerOptions": {
"target": "ES6",
"module": "ES6",
"moduleResolution": "node",
"lib": [
"ES6"
],
"removeComments": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
},
"exclude": [
"node_modules",
"dist",
"example"
]
}
- 子目录下的 tsconfig.json 文件,通过继承根目录的配置。并可以传入自己的配置
{
"extends": "../../tsconfig.base.json",
"include": [
"src",
"types"
]
}
package.json
- 在子目录的 package.json 中配置相关的 scripts 命令
- 在根目录通过
pnpm -F 子模块名称 dev执行子模块内的命令 - 子目录下直接执行
pnpm dev即可
- 在根目录通过
// ...其他配置
"scripts": {
"dev": "rollup -w -c",
"build": "rm -rf dist && rollup -c",
"lint": "eslint src --ext .ts,.js",
"lint:ts": "tsc --noEmit",
"prepublish": "pnpm lint && pnpm lint:ts && pnpm build"
},
总结
通过上述内容我们完成了一个通用工具打包框架的搭建。支持按照各自模块不同的配置规则,进行差异内容打包。同时使用 Monorepo+pnpm 提高依赖内容的复用率,同时还降低了空间占用。并且还了解 rollup 的基本使用、Monorepo 的概念、pnpm 相较于 npm 的天然优势等。