原来rollup这么简单之 rollup.generate + rollup.write篇
大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
分享不易,希望能够得到大家的支持和关注。
计划
rollup系列打算一章一章的放出,内容更精简更专一更易于理解
目前打算分为以下几章:
- rollup.rollup
- rollup.generate + rollup.write <==== 当前文章
- rollup.watch
- tree shaking
- plugins
TL;DR
书接上文,我们知道rollup.rollup对配置中的入口进行了解析、依赖挂载、数据化这些操作,最终返回了一个chunks,然后返回了一些方法:
rollup() {
const chunks = await graph.build();
return {
generate,
// ...
}
}
这其中利用了闭包的原理,以便后续方法可以访问到rollup结果
这期我们就深入generate方法,来看看它的内心世界
还是老套路,在看代码前,先大白话说下整个过程,rollup.generate()主要分为以下几步:
- 配置标准化、创建插件驱动器
- chunks、assets收集
- preserveModules模式处理
- 预渲染
- chunk优化
- 源码render
- 产出过滤、排序
最近看到这么一句话:
'将者,智、信、仁、勇、严也'
指的是将者的素养,顺序代表着每个能力的重要性:
智: 智略、谋略
信:信义、信用
仁:仁义、声誉
勇:勇武、果断
严:铁律、公证
时至今日,仍然奏效,哪怕是放到it领域。虽然不能直接拿过来,但内涵都是一样的。
想要做好it这一行,先要自身硬(智),然后是产出质量(信),同事间的默契合作(仁),对事情的判断(勇)和对团队的要求以及奖惩制度(严)。
注意点
所有的注释都在这里,可自行阅读
!!!版本 => 笔者阅读的rollup版本为: 1.32.0
!!!提示 => 标有TODO为具体实现细节,会视情况分析。
!!!注意 => 每一个子标题都是父标题(函数)内部实现
!!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载
rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似
插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~
主要通用模块以及含义
- Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心
- PathTracker: 无副作用模块依赖路径追踪
- PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等
- FileEmitter: 资源操作器
- GlobalScope: 全局作用局,相对的还有局部的
- ModuleLoader: 模块加载器
- NodeBase: ast各语法(ArrayExpression、AwaitExpression等)的构造基类
主流程解析
- generate方法:
调用封装好的内置私有方法,返回promise,一个一个的来,先来看getOutputOptionsAndPluginDriver
;
generate: ((rawOutputOptions: GenericConfigObject) => {
// 过滤output配置选项,并创建output的插件驱动器
const { outputOptions, outputPluginDriver } = getOutputOptionsAndPluginDriver(
rawOutputOptions
);
const promise = generate(outputOptions, false, outputPluginDriver).then(result =>
createOutput(result)
);
// 丢弃老版本字段
Object.defineProperty(promise, 'code', throwAsyncGenerateError);
Object.defineProperty(promise, 'map', throwAsyncGenerateError);
return promise;
})
- getOutputOptionsAndPluginDriver:
该方法通过output配置生成标准化配置和output插件驱动器
PluginDriver类暴露了createOutputPluginDriver方法
class PluginDriver {
// ...
public createOutputPluginDriver(plugins: Plugin[]): PluginDriver {
return new PluginDriver(
this.graph,
plugins,
this.pluginCache,
this.preserveSymlinks,
this.watcher,
this
);
}
// ...
}
引用该方法,创建output的插件驱动器: graph.pluginDriver.createOutputPluginDriver
const outputPluginDriver = graph.pluginDriver.createOutputPluginDriver(
// 统一化插件
normalizePlugins(rawOutputOptions.plugins, ANONYMOUS_OUTPUT_PLUGIN_PREFIX)
);
生成标准output配置更简单了,调用之前在rollup.rollup方法中用到的,用来提取input配置的mergeOptions(参考mergeOptions.ts)方法,获取处理后的配置,调用outputOptions
钩子函数,该钩子可以读取到即将传递给generate/write的配置,进行更改,但是rollup更推荐在renderStart中进行更改等操作。之后进行一些列校验判断最终返回ourputOptions
function normalizeOutputOptions(
inputOptions: GenericConfigObject,
rawOutputOptions: GenericConfigObject,
hasMultipleChunks: boolean,
outputPluginDriver: PluginDriver
): OutputOptions {
const mergedOptions = mergeOptions({
config: {
output: {
...rawOutputOptions,
// 可以用output里的覆盖
...(rawOutputOptions.output as object),
// 不过input里的output优先级最高,但是不是每个地方都返回,有的不会使用
...(inputOptions.output as object)
}
}
});
// 如果merge过程中出错了
if (mergedOptions.optionError) throw new Error(mergedOptions.optionError);
// 返回的是数组,但是rollup不支持数组,所以获取第一项,目前也只会有一项
const mergedOutputOptions = mergedOptions.outputOptions[0];
const outputOptionsReducer = (outputOptions: OutputOptions, result: OutputOptions) =>
result || outputOptions;
// 触发钩子函数
const outputOptions = outputPluginDriver.hookReduceArg0Sync(
'outputOptions',
[mergedOutputOptions],
outputOptionsReducer,
pluginContext => {
const emitError = () => pluginContext.error(errCannotEmitFromOptionsHook());
return {
...pluginContext,
emitFile: emitError,
setAssetSource: emitError
};
}
);
// 检查经过插件处理过的output配置
checkOutputOptions(outputOptions);
// output.file 和 output.dir是互斥的
if (typeof outputOptions.file === 'string') {
if (typeof outputOptions.dir === 'string')
return error({
code: 'INVALID_OPTION',
message:
'You must set either "output.file" for a single-file build or "output.dir" when generating multiple chunks.'
});
if (inputOptions.preserveModules) {
return error({
code: 'INVALID_OPTION',
message:
'You must set "output.dir" instead of "output.file" when using the "preserveModules" option.'
});
}
if (typeof inputOptions.input === 'object' && !Array.isArray(inputOptions.input))
return error({
code: 'INVALID_OPTION',
message: 'You must set "output.dir" instead of "output.file" when providing named inputs.'
});
}
if (hasMultipleChunks) {
if (outputOptions.format === 'umd' || outputOptions.format === 'iife')
return error({
code: 'INVALID_OPTION',
message: 'UMD and IIFE output formats are not supported for code-splitting builds.'
});
if (typeof outputOptions.file === 'string')
return error({
code: 'INVALID_OPTION',
message:
'You must set "output.dir" instead of "output.file" when generating multiple chunks.'
});
}
return outputOptions;
}
- generate内部的
generate
方法
获取到标准化之后的output配合和插件驱动器后,到了内置的generate方法了,该方法接受三个参数,其中第二个参数标识是否写入,也就是说该方法同时用于generate和下一篇write中。
首先获取用户定义的资源名,没有的话取默认值
const assetFileNames = outputOptions.assetFileNames || 'assets/[name]-[hash][extname]';
获取chunks的目录交集,也就是公共的根目录
const inputBase = commondir(getAbsoluteEntryModulePaths(chunks));
getAbsoluteEntryModulePaths获取所有绝对路径的chunks id,commondir参考的node-commondir模块,原理是先获取第一个文件的路径,进行split转成数组(设为a),然后遍历剩余所有文件id,进行比对,找到不相等的那个索引,然后重新赋值给a,进行下一次循环,直到结束,就得到了公共的目录。
function commondir(files: string[]) {
if (files.length === 0) return '/';
if (files.length === 1) return path.dirname(files[0]);
const commonSegments = files.slice(1).reduce((commonSegments, file) => {
const pathSegements = file.split(/\/+|\\+/);
let i;
for (
i = 0;
commonSegments[i] === pathSegements[i] &&
i < Math.min(commonSegments.length, pathSegements.length);
i++
);
return commonSegments.slice(0, i);
}, files[0].split(/\/+|\\+/));
// Windows correctly handles paths with forward-slashes
return commonSegments.length > 1 ? commonSegments.join('/') : '/';
}
创建一个包含所有chunks和assets信息的对象
const outputBundleWithPlaceholders: OutputBundleWithPlaceholders = Object.create(null);
调用插件驱动器上的setOutputBundle将output设置到上面创建的outputBundleWithPlaceholders
上。
outputPluginDriver.setOutputBundle(outputBundleWithPlaceholders, assetFileNames);
setOutputBundle在FileEmitter类上实现,在插件驱动器类(PluginDriver)上实例化,并将公共方法赋给插件驱动器。
reserveFileNameInBundle方法为outputBundleWithPlaceholders上挂载文件chunks。
finalizeAsset方法只处理资源,将资源格式化后,添加到outputBundleWithPlaceholders上。格式为:
{
fileName,
get isAsset(): true {
graph.warnDeprecation(
'Accessing "isAsset" on files in the bundle is deprecated, please use "type === \'asset\'" instead',
false
);
return true;
},
source,
type: 'asset'
};
class FileEmitter {
// ...
setOutputBundle = (
outputBundle: OutputBundleWithPlaceholders,
assetFileNames: string
): void => {
this.output = {
// 打包出来的命名
assetFileNames,
// 新建的空对象 => Object.create(null)
bundle: outputBundle
};
// filesByReferenceId是通过rollup.rollup中emitChunks的时候设置的,代表已使用的chunks
// 处理文件
for (const emittedFile of this.filesByReferenceId.values()) {
if (emittedFile.fileName) {
// 文件名挂在到this.output上,作为key,值为: FILE_PLACEHOLDER
reserveFileNameInBundle(emittedFile.fileName, this.output.bundle, this.graph);
}
}
// 遍历set 处理资源
for (const [referenceId, consumedFile] of this.filesByReferenceId.entries()) {
// 插件中定义了source的情况
if (consumedFile.type === 'asset' && consumedFile.source !== undefined) {
// 给this.output上绑定资源
this.finalizeAsset(consumedFile, consumedFile.source, referenceId, this.output);
}
}
};
// ...
}
调用renderStart
钩子函数,用来访问output和input配置,可能大家看到了很多调用钩子函数的方法,比如hookParallel、hookSeq等等,这些都是用来触发插件里提供的钩子函数,不过是执行方式不同,有的是并行的,有的是串行的,有的只能执行通过一个等等,这会单独抽出来说。
await outputPluginDriver.hookParallel('renderStart', [outputOptions, inputOptions]);
执行footer banner intro outro钩子函数,内部就是执行这几个钩子函数,默认值为option[footer|banner|intro|outro],最后返回字符串结果待拼接。
const addons = await createAddons(outputOptions, outputPluginDriver);
处理preserveModules模式,也就是是否尽可能少的打包,而不是每个模块都是一个chunk
如果是尽可能少的打包的话,就将chunks的导出多挂载到chunks的exportNames属性上,供之后使用
如果每个模块都是一个chunk的话,推导出导出模式
for (const chunk of chunks) {
// 尽可能少的打包模块
// 设置chunk的exportNames
if (!inputOptions.preserveModules) chunk.generateInternalExports(outputOptions);
// 尽可能多的打包模块
if (inputOptions.preserveModules || (chunk.facadeModule && chunk.facadeModule.isEntryPoint))
// 根据导出,去推断chunk的导出模式
chunk.exportMode = getExportMode(chunk, outputOptions, chunk.facadeModule!.id);
}
预渲染chunks。
使用magic-string模块进行source管理,初始化render配置,对依赖进行解析,添加到当前chunks的dependencies属性上,按照执行顺序对依赖们进行排序,处理准备动态引入的模块,设置唯一标志符(?)
for (const chunk of chunks) {
chunk.preRender(outputOptions, inputBase);
}
优化chunks
if (!optimized && inputOptions.experimentalOptimizeChunks) {
optimizeChunks(chunks, outputOptions, inputOptions.chunkGroupingSize!, inputBase);
optimized = true;
}
将chunkId赋到上文创建的outputBundleWithPlaceholders上
assignChunkIds(
chunks,
inputOptions,
outputOptions,
inputBase,
addons,
outputBundleWithPlaceholders,
outputPluginDriver
);
设置好chunks的对象,也就是将chunks依照id设置到outputBundleWithPlaceholders上,这时候outputBundleWithPlaceholders上已经有完整的chunk信息了
outputBundle = assignChunksToBundle(chunks, outputBundleWithPlaceholders);
语法树解析生成code操作,最后返回outputBundle。
await Promise.all(
chunks.map(chunk => {
const outputChunk = outputBundleWithPlaceholders[chunk.id!] as OutputChunk;
return chunk
.render(outputOptions, addons, outputChunk, outputPluginDriver)
.then(rendered => {
// 引用类型,outputBundleWithPlaceholders上的也变化了,所以outputBundle也变化了,最后返回outputBundle
outputChunk.code = rendered.code;
outputChunk.map = rendered.map;
return outputPluginDriver.hookParallel('ongenerate', [
{ bundle: outputChunk, ...outputOptions },
outputChunk
]);
});
})
);
return outputBundle;
- generate内部的
createOutput
方法
createOutput接受generate的返回值,并对生成的OutputBundle进行过滤和排序
function createOutput(outputBundle: Record<string, OutputChunk | OutputAsset | {}>): RollupOutput {
return {
output: (Object.keys(outputBundle)
.map(fileName => outputBundle[fileName])
.filter(outputFile => Object.keys(outputFile).length > 0) as (
| OutputChunk
| OutputAsset
)[]).sort((outputFileA, outputFileB) => {
const fileTypeA = getSortingFileType(outputFileA);
const fileTypeB = getSortingFileType(outputFileB);
if (fileTypeA === fileTypeB) return 0;
return fileTypeA < fileTypeB ? -1 : 1;
}) as [OutputChunk, ...(OutputChunk | OutputAsset)[]]
};
}
- rollup.write
write方法和generate方法几乎一致,只不过是generate方法的第二个参数为true,供generateBundle钩子函数中使用,已表明当前是wirte还是generate阶段。
之后是获取当前的chunks数,多出口的时候会检测配置的file和sourcemapFile进而抛出错误提示
let chunkCount = 0; //计数
for (const fileName of Object.keys(bundle)) {
const file = bundle[fileName];
if (file.type === 'asset') continue;
chunkCount++;
if (chunkCount > 1) break;
}
if (chunkCount > 1) {
// sourcemapFile配置
if (outputOptions.sourcemapFile)
return error({
code: 'INVALID_OPTION',
message: '"output.sourcemapFile" is only supported for single-file builds.'
});
// file字段
if (typeof outputOptions.file === 'string')
return error({
code: 'INVALID_OPTION',
message:
'When building multiple chunks, the "output.dir" option must be used, not "output.file".' +
(typeof inputOptions.input !== 'string' ||
inputOptions.inlineDynamicImports === true
? ''
: ' To inline dynamic imports, set the "inlineDynamicImports" option.')
});
}
之后调用写入方法: writeOutputFile
await Promise.all(
Object.keys(bundle).map(chunkId =>
writeOutputFile(result, bundle[chunkId], outputOptions, outputPluginDriver)
)
);
writeOutputFile方法就很直观了,解析路径
const fileName = resolve(outputOptions.dir || dirname(outputOptions.file!), outputFile.fileName);
根据chunk类型进行不同的处理,assets直接获取代码即可,chunks的话还需根据sourcemap选项将sourcemp追加到代码之后。
if (outputFile.type === 'asset') {
source = outputFile.source;
} else {
source = outputFile.code;
if (outputOptions.sourcemap && outputFile.map) {
let url: string;
if (outputOptions.sourcemap === 'inline') {
url = outputFile.map.toUrl();
} else {
url = `${basename(outputFile.fileName)}.map`;
writeSourceMapPromise = writeFile(`${fileName}.map`, outputFile.map.toString());
}
if (outputOptions.sourcemap !== 'hidden') {
source += `//# ${SOURCEMAPPING_URL}=${url}\n`;
}
}
}
最后调用fs模块进行文件创建和内容写入即可
function writeFile(dest: string, data: string | Buffer) {
return new Promise<void>((fulfil, reject) => {
mkdirpath(dest);
fs.writeFile(dest, data, err => {
if (err) {
reject(err);
} else {
fulfil();
}
});
});
}
以上就是代码流程的解析部分,具体细节参考代码库注释
部分功能的具体解析
- 略
总结
随着深入阅读发现rollup细节操作很多,很复杂,需要话更多的时间去打磨,暂时先分析了下主流程,具体的实现细节比如优化chunks、prerender等之后视情况再说吧。
不过也学到了一些东西,rollup将所有的ast类型分成了一个个的类,一个类专门处理一个ast类型,调用的时候只需要遍历ast body,获取每一项的类型,然后动态调用就可以了,很使用。对于ast没有画面感的同学可以看这里 => ast在线解析
rollup从构建到打包,经历了三个大步骤:
加载、解析 => 分析(依赖分析、引用次数、无用模块分析、类型分析等) => 生成
看似简单,实则庞杂。为rollup点个赞吧。
原来rollup这么简单之 rollup.generate + rollup.write篇的更多相关文章
- 原来rollup这么简单之 rollup.rollup篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...
- 原来rollup这么简单之 rollup.watch篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...
- 原来rollup这么简单之 tree shaking篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...
- 原来rollup这么简单之插件篇
大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...
- 基于PHP——简单的WSDL的创建(WSDL篇)
1.建立WSDL文件 建立WSDL的工具很多,eclipse.zendstudio.vs都可以,我个人建议自己写,熟悉结构,另外自动工具对xml schame类型支持在类型中可能会报错. 下 ...
- 史上最简单的SpringCloud教程 | 第三篇: 服务消费者(Feign)
转载请标明出处: https://www.fangzhipeng.com/springcloud/2017/07/12/sc03-feign/ 本文出自方志朋的博客 最新Finchley版本请访问: ...
- [转]【ROLLUP】Oracle分组函数之ROLLUP魅力
原创:http://blog.itpub.net/519536/viewspace-610995 本文通过演示给出Oracle ROLLUP分组函数的用法,体验一下Oracle在统计查询领域中的函数魅 ...
- 写简单游戏,学编程语言-python篇:大鱼吃小鱼
很常见的游戏之一,实现原理并不复杂,并且参考了几个相关的代码.这边主要还是以学习编程语言和学习编程思路为重点记录一下吧.最近时间有点吃紧,只能匆忙记录一下.用pygame做的大鱼吃小鱼的游戏截图如下: ...
- 写简单游戏,学编程语言-python篇:传说哥大战剧毒术士
上篇写的动画示例其实算不上一个游戏,顶多算是利用pygame进行的图形操作,今天着手实现一个小游戏:传说哥大战剧毒术士.名字很玄乎,其实就是最简单的一个射击游戏.好了废话不多说,先上截图吧: 一.初始 ...
随机推荐
- nevertheless|magnificent |prosperous|
ADV 然而;不过You use nevertheless when saying something that contrasts with what has just been said. Mos ...
- Java捕获并处理线程失败抛出的异常
使用 UncaughtExceptionHandler 示例代码如下: Thread.UncaughtExceptionHandler handler = new Thread.UncaughtExc ...
- VRRP笔记三:配置keepalived为实现haproxy高可用的双主模型配置文件示例:
说明:其基本实现思想为创建两个虚拟路由器,并以两个节点互为主从. ! Configuration File for keepalived global_defs { notification_emai ...
- 云服务器离线安装MariaDB安装步骤和解决办法
前面我写了tomcat的安装那么接下来我们来安装云服务的数据库服务 第一步:下载安装包 https://downloads.mariadb.org/ 按照上图所示操作就能完成在线安装,但由于国内的网络 ...
- Spring中的事件处理
文章目录 Spring中的事件处理 Spring 的核心是 ApplicationContext,它负责管理 beans 的完整生命周期.当加载 beans 时,ApplicationContext ...
- 前进中的人工智能——聚焦Faculty Summit 2015人工智能主题研讨会
Summit 2015人工智能主题研讨会" title="前进中的人工智能--聚焦Faculty Summit 2015人工智能主题研讨会"> 在近几年上映的科幻大 ...
- requests库入门-16-Session和Cookie
分类专栏: Python接口自动化测试之requests库入门 作者 | Anthony_tester ,300w+访问量博主,Oracle测试开发工程师. 地址 | https://blog.csd ...
- 网络爬虫url跳转代码
from bs4 import BeautifulSoup from urllib.request import urlopen import re import random base_url = ...
- 全平台Markdown笔记软件——Notable
简介 The markdown-based note-taking app that doesn't suck. 一款简介.跨平台的本地笔记软件,github地址:https://github.com ...
- Design Patterns 25
尽管将一个系统分割成许多对象通常可以增加其可服用性, 但是对象间相互连接的激增又会降低其可复用性了. 大量的连接使得一个对象不可能在没有改变其他对象的支持下工作, 系统表现为一个不可分割的整体, 所以 ...