rollup 原理解析
使用
我们以 node api 的形式使用 rollup。
首先是配置 rollup.config.ts
import path from 'path'
import { RollupOptions } from 'rollup'
import typescript from '@rollup/plugin-typescript'
import babel from '@rollup/plugin-babel'
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import { DEFAULT_EXTENSIONS } from '@babel/core'
import pkg from './package.json'
const paths = {
input: path.join(__dirname, '/src/index.ts'),
output: path.join(__dirname, '/lib'),
}
// rollup 配置项
const rollupConfig: RollupOptions = {
input: paths.input,
output: [
{
file: path.join(paths.output, 'index.js'),
format: 'cjs', // 输出 commonjs 规范的代码
name: pkg.name,
},
{
file: path.join(paths.output, 'index.esm.js'),
format: 'es', // 输出 es 规范的代码
name: pkg.name,
},
],
external: [
// ...Object.keys(pkg.dependencies),
/@babel\/runtime/,
], // 指出应将哪些模块视为外部模块,如 Peer dependencies 中的依赖
plugins: [
// ts 的功能只在于类型检查和编译出声明文件,编译交给 babel 来做
typescript({
tsconfig: './tsconfig.json',
}),
// 配合 commnjs 解析第三方模块
resolve(),
// 使得 rollup 支持 commonjs 规范,识别 commonjs 规范的依赖
commonjs(),
babel({
babelHelpers: 'runtime',
// 只转换源代码,不运行外部依赖
exclude: 'node_modules/**',
// babel 默认不支持 ts 需要手动添加
extensions: [...DEFAULT_EXTENSIONS, '.ts'],
}),
],
}
export default rollupConfig
使用 node api 打包
import { rollup } from 'rollup'
import rollupConfig from './rollup.config'
const build = async () => {
const inputOptions = {
input: rollupConfig.input,
external: rollupConfig.external,
plugins: rollupConfig.plugins,
}
const outOptions = rollupConfig.output
let bundle
try {
bundle = await rollup(inputOptions)
// 写入需要遍历输出配置
if (Array.isArray(outOptions)) {
outOptions.forEach(async outOption => {
await bundle.write(outOption)
})
}
} catch (e) {
if (e instanceof Error) {
log.error(e.message)
}
}
if (bundle !== null) {
// closes the bundle
await bundle.close()
log.progress('Rollup built successfully')
}
}
我们重点关注两个内容,以及详细步骤:
- rollup 方法(使用 input、plugins 等参数)创建了一个 bundle 对象
- bundle 对象是个什么,是个编译对象么?
- plugins 在整个编译过程的生命周期是什么样的
- bundle.write 方法(使用 output 参数,包括输出格式、名称等)将最终资源写入到系统中
- 通过 format(cjs / es),是如何输出对应的格式规范的
打包效果
源码部分:
// index.js
import { name } from './bar'
const main = () => {
console.log('first', name)
}
const data = {
barName: name
}
export { data, main }
// bar.js
const name = 'bar'
const noop = () => {}
export { name }
打包后结果(es 格式):
- 两个文件合并成了一个文件
- 去掉依赖之间的 import export ,变成直接使用
- 去掉了没有被使用的代码(bar.js 中的 noop)
// index.esm.js
const name = 'bar';
const main = () => {
console.log('first', name);
};
const data = {
barName: name
};
export { data, main };
源码解析
入口在 src/rollup/rollup.ts
,看一看它做了哪些事情(指 rollupInternal
)
- getInputOptions 方法将用户配置改成内部用的数据格式
- new Graph() 实例化了一个图数据结构
- catchUnfinishedHookActions
- 返回一个对象(bundle),包含 write、close 等方法
getInputOptions
内部最终会生成这样的一个 option
const options: NormalizedInputOptions & InputOptions = {
acorn: getAcorn(config), // acorn 配置
acornInjectPlugins: getAcornInjectPlugins(config), // 注入 acorn 插件,比如 acorn-jsx
cache: getCache(config), // cache 缓存配置
context, // 上下文,比如 windows
experimentalCacheExpiry: config.experimentalCacheExpiry ?? 10,
external: getIdMatcher(config.external), // 将 external 组装成方法,方便使用
inlineDynamicImports: getInlineDynamicImports(config, onwarn, strictDeprecations),// 废弃了不关注,改到 output 中
input: getInput(config), // 将 input 组成一个数组
makeAbsoluteExternalsRelative: config.makeAbsoluteExternalsRelative ?? true, // 是否将绝对路径转为相对路径
manualChunks: getManualChunks(config, onwarn, strictDeprecations), // 废弃了,不关注,改到 output 中
maxParallelFileReads: getMaxParallelFileReads(config), // 文件读取最大并发数,默认 20
moduleContext: getModuleContext(config, context), // 更 context 差不多
onwarn, // 警告监听
perf: config.perf || false, // 是否收集性能数据
plugins: ensureArray(config.plugins), // 插件
preserveEntrySignatures: getPreserveEntrySignatures(config, unsetOptions), // strict 时,将导出名跟导入时的命名一样,只导出必要的
preserveModules: getPreserveModules(config, onwarn, strictDeprecations), // 废弃了,不关注,改到 output 中
preserveSymlinks: config.preserveSymlinks || false, // false 时,解析文件时遵循符号链接。
shimMissingExports: config.shimMissingExports || false,
strictDeprecations: config.strictDeprecations || false, // 使用了标记为弃用的方法,true 直接 error,false 报 warning,默认 false
treeshake: getTreeshake(config, onwarn, strictDeprecations) // treeshake 配置,默认为 true,相当于未指定,会使用默认的 treeshake 配置, 还可以使用三个内置配置: “smallest” “recommended” “safest”
}
这里内部使用的配置还是比较复杂的,但也可以了解到一些不常用的配置。
我们仍旧只看一些重点配置关注它们的走向:input,plugins。
new Graph()
(生成图实例,那猜测将会从入口开始的解析依赖关系组成一幅有向图。)
关注构造函数(忽略 cache 和 watch 相关的代码):
pluginDriver = new PluginDriver
生成一个 插件驱动实例,插件驱动器中 实例化了一个 FileEmitter ,为插件提供文件输出能力。- hookParallel 钩子触发器,调用有所有插件的钩子
acornParser = acorn.Parser...
一个 acorn 解析器实例moduleLoader = new ModuleLoader
生成一个模块加载器实例,初始化了一个读取队列。
类之间的关系如下图
至此就结束了,没有生成有向图?在后面呢
catchUnfinishedHookActions / graph.build()
这个方法很有意思,直译为“捕获未完成的钩子动作”。
emptyEventLoopPromise
注册了一个 beforeExit 事件,一旦触发,就会 reject。(会在插件返回的 promise 一直没有被 resolve 时发生)。
emptyEventLoopPromise 会跟 callback 竞争(race),如果 beforeExit 先触发了,说明 callback 没有执行完,有些问题,就会抛出异常。
async function catchUnfinishedHookActions (pluginDriver, callback) {
let handleEmptyEventLoop
const emptyEventLoopPromise = new Promise((_, reject) => {
handleEmptyEventLoop = () => {
const unfulfilledActions = pluginDriver.getUnfulfilledHookActions();
reject(
new Error(`Unexpected early exit...`)
)
}
process.once('beforeExit', handleEmptyEventLoop)
})
const result = await Promise.race([callback(), emptyEventLoopPromise])
process.off('beforeExit', handleEmptyEventLoop)
return result
}
跑远了,我们重点关注 callback 做了哪些事情(不关注异常问题):graph.build()
这里分别在 build 前后触发了插件的 buildStart、buildEnd 钩子。我们重点看 build 方法。
generateModuleGraph()
从入口模块 ast 解析,并根据依赖关系生成一幅模块图
从 modulesById 中向 modules 添加 module
sortModules()
- 分析模块,输出循环依赖、和排序好的依赖(是后序排序的,而非逆后序排序,逆后序会在后面步骤进行)
- 模块绑定来源(
bindReferences()
)
includeStatements()
- 判断是否有 sideEffect(从 Program 递归调用各个节点的 hasSideEffect,各个节点中,rollup 内置了 ast node type 的类,并描述了如何查找 sideEffect,如何 render 等等,值得一题的是 render 中使用了 MagicString 来处理编译后的代码)
- treeshaking
return
最后返回一个对象
bundle = {
close () {},
write () {
// ...
handleGenerateWrite(...)
},
// ...
}
bundle.write()
实际上调用了 handleGenerateWrite 方法,里面分 3 个步骤
核心步骤是 bundle.generate()
getOutputOptionsAndPluginDriver
添加了一个 outputPluginDriver,来做编译后的资源输出
bundle.generate()
新建了一个 bundle 实例,并调用 bundle.generate()。看看 generate 里面做了哪些事情:
创建了一个
outputBundle
空对象,最后输出generateChunks()
创建了一个 chunk 实例
createAddons()
在预渲染之前创建额外添加的内容:banner、 footer、 intro、 outro
getGenerateCodeSnippets()
生成代码片段,处理一些细节,比如是否是否加 封号,使用 let 还是 var 等等
prerenderChunks(); addFinalizedChunksToBundle()
预渲染块,以及最终渲染的内容添加到 bundle 上。这里的逻辑比较复杂,直接关注一些点:
- generateExports 去掉原先代码的 exports,改为 直接声明(因为是要把代码合并的,如最开始的示例)
- modules 会被 reverse(),在上面 build 阶段是图的后序排序,这里 reverse 后就是拓扑排序(逆后序)了
- es 格式的,可以省去了 exports 等等代码的添加
- 使用了 MagicString 来处理字符串的拼接删除等操作
- 定义了 AST node 类型对应的类,并使用他们的 render 方法
输出
outputBundle
对象,包含了编译后的字符串
{
code: "use strict;...",
fileName: "index.js",
modules: [] // 前面排序好的 modules
type: "chunk",
// ...
}
writeOutputFile
将资源写入到系统中
总结
施工中...