rollup
是一个 JavaScript
模块打包器,可以将许多 JavaScript
库和应用程序打包成少量的捆绑包,从而提高了应用程序的性能。本文详细描述如何通过 rollup
实现多模块多依赖打包。
先看下项目根目录下的 package.json
文件夹:
// package.json
{
"private": true,
"workspaces": [
"packages/"
],
"scripts": {
"build": "node scripts/build.js",
"shared": "node scripts/shared.js"
},
"type": "module",
"name": "vue3",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"dependencies": {
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.1",
"execa": "^7.0.0",
"rollup": "^3.17.3",
"rollup-plugin-typescript2": "^0.34.1",
"typescript": "^4.9.5"
}
}
这里我们使用 workspaces
字段定义 npm
工作空间。它可以让你在同一项目中使用多个独立的 npm
包,这些包可以共享相同的依赖项和环境,使得管理依赖关系变得更加方便。
同时可以指定一个或多个目录,它们将包含在工作空间中。例子中,将 packages/
目录包含在工作空间中,意味着在该目录下的所有子目录都将被视为 npm
包,并共享相同的依赖项和环境。
使用工作空间可以大大简化具有多个独立模块或应用的项目的依赖关系管理。你可以在每个模块或应用中独立运行 npm
命令,而无需担心它们之间的依赖关系和环境影响。
需要注意的是,使用工作空间需要你的项目目录结构符合一定的规范,例如每个模块或应用都需要位于 packages/
目录的子目录中。此外,使用工作空间需要在命令行中运行 npm
命令时,需要使用 “--workspace
” 标志来指定要操作的工作空间,例如 npm install rollup -W
。
同时 “workspaces
” 只能在私有项目中使用。这里需要配置 private: true
安装项目依赖:
npm install @rollup/plugin-json @rollup/plugin-node-resolve execa rollup rollup-plugin-typescript2 typescript -W
既然 packages
目录下每个文件都属于一个独立 npm
模块,那么也会有一个属于自己的 package.json
以shared
为例:
// packages/shared/package.json
{
"name": "@vue/shared", // npm 包名称
"version": "1.0.0", // 版本号
"main": "./dist/shared.esm-bundler.js", // 入口文件
"license": "MIT", // 许可协议
"buildOptions": { // 构建可选项
"name": "VueShared", // global方式导出时的全局变量名
// 构建后输出的不同类型的模块规范
"formats": [
"esm-bundler", // ES Module 格式
"cjs", // CommonJS 格式
"global" // 全局变量格式
]
}
}
这段代码定义了一个名为 @vue/shared
的 npm
包,并且为了让它能够在不同的应用程序环境中使用,将它构建为了不同输出格式的库。
同时在 shared/src/index.ts
文件夹中补充如下测试代码:
// packages/shared/src/index.ts
export const isArray = Array.isArray
这里我们可以先简单的针对 shared
文件进行打包,在项目下新建一个 scripts/shared.js
文件夹来进行打包配置,并且补充如下代码
// scripts/shared.js
import { execa } from 'execa'
async function build(TARGET: string) {
await execa('rollup', ['-cw', '--environment', `TARGET:${TARGET}`], { stdio: 'inherit' })
}
build('shared').then(() => {
console.log('success')
})
上面代码通过 execa
去执行 rollup
的打包逻辑,这里的 −c
参数表示使用配置文件(默认是 rollup.config.js
),−−environment
参数用于指定环境变量, stdio: 'inherit'
表示将命令的标准输入、输出和错误流重定向到父进程的流(即 Node.js 进程的流)。
import { defineConfig } from 'rollup'
import { createRequire } from 'node:module'
import ts from 'rollup-plugin-typescript2'
import json from '@rollup/plugin-json'
import resolvePlugin from '@rollup/plugin-node-resolve' // 解析第三方插件
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url))
// 2.获取文件路径
const packagesDir = path.resolve(__dirname, 'packages')
// 2.1 获取需要打包的文件
const packageDir = path.resolve(packagesDir, process.env.TARGET)
const resolve = p => path.resolve(packageDir, p)
// 2.2 获取每个包的配置项
const pkg = require(resolve('package.json')) // 获取 json
const name = path.basename(packageDir) // reactivity
// 3、创建一个映射表
const outputConfigs = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: 'esm'
},
'cjs': {
file: resolve(`dist/${name}.cjs.js`),
format: 'cjs'
},
'global': {
file: resolve(`dist/${name}.global.js`),
format: 'iife'
}
}
const packageOptions = pkg.buildOptions || {}
// 1、创建一个打包配置
function createConfig(format, output) {
output.name = packageOptions.name
output.sourcemap = false
// 生成rollup配置
const config = {
input: resolve('src/index.ts'), // 输入
output, // 输出
plugins: [
json(),
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
}),
resolvePlugin()
]
}
return config
}
const packageFormats = packageOptions.formats
const packageConfigs = packageFormats.map(format => createConfig(format, outputConfigs[format]))
export default defineConfig(packageConfigs)
这段代码主要是读取预先定义好的打包配置(buildOptions
),然后针对不同的打包方式定义输出不同形式的 .js
文件,最后返回一个 rollup
配置项数组。执行 npm run shared
之后可以看到在 shared
文件夹下会有打包生成之后的代码。
在根目录下新建测试文件夹 example
并且补充测试文件 shared.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>sharedtitle>
head>
<body>
<div id="app">div>
<script src="../packages/shared/dist/shared.global.js">script>
<script>
let { isArray } = VueShared
console.log(isArray([])) // true
script>
body>
html>
我们在 packages
定义了很多 npm
包 那么如何在某个依赖里面去引用其他依赖呢?例如在 reactivity
里面通过 import xxx from '@vue/shared'
的方式去引用 shared
文件下的某个功能呢?这里直接引用看是否成功
可以看到直接引用这里是会报错的并且鼠标点进去也无法跳转到对应的文件,可以根据提示修改下项目的ts配置以修复报错:
// tsconfig.json
{
// 省略部分代码
"baseUrl": "./",
"moduleResolution": "node",
"paths": {
"@vue/*": ["packages/*/src"]
}
}
如果在 packages/
目录包含了多个 npm
包那么如何对每个文件经行打包呢,一种方式是手动将 scripts/shared.js
里面的 build('shared')
改为对应的模块逐个经行打包,另一种方式是遍历 packages
下的每个文件夹找到对应的 package.json
然后经行打包,很明显第二种方式更适合多模块打包,补充下相关代码:
// scripts/build.js
import { readdirSync, statSync } from 'fs'
import { execa } from 'execa'
// 1、获取打包目录
const dirs = readdirSync('packages')
.filter(dir => statSync(`packages/${dir}`).isDirectory()) // [ 'reactivity', 'shared' ]
async function build(TARGET) {
await execa('rollup', ['-c', '--environment', `TARGET:${TARGET}`], { stdio: 'inherit' })
}
function runParaller(dirs) {
let result = []
for (const dir of dirs) {
result.push(build(dir))
}
return Promise.all(result) // 存放打包的promise
}
// 2、进行打包
runParaller(dirs).then(() => {
console.log('success')
})
打包完之后在测试文件也会有对应的输出:
通过以上例子可以大体了解到开源库代码是如何组织并且打包的