大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。

内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。

分享不易,希望能够得到大家的支持和关注。

计划

rollup系列打算一章一章的放出,内容更精简更专一更易于理解

目前打算分为以下几章:

TL;DR

书接上文,我们知道rollup.rollup对配置中的入口进行了解析、依赖挂载、数据化这些操作,最终返回了一个chunks,然后返回了一些方法:

rollup() {
const chunks = await graph.build();
return {
generate,
// ...
}
}
这其中利用了闭包的原理,以便后续方法可以访问到rollup结果

这期我们就深入generate方法,来看看它的内心世界

还是老套路,在看代码前,先大白话说下整个过程,rollup.generate()主要分为以下几步:

  1. 配置标准化、创建插件驱动器
  2. chunks、assets收集
  3. preserveModules模式处理
  4. 预渲染
  5. chunk优化
  6. 源码render
  7. 产出过滤、排序

最近看到这么一句话:

'将者,智、信、仁、勇、严也'

指的是将者的素养,顺序代表着每个能力的重要性:

智: 智略、谋略

信:信义、信用

仁:仁义、声誉

勇:勇武、果断

严:铁律、公证

时至今日,仍然奏效,哪怕是放到it领域。虽然不能直接拿过来,但内涵都是一样的。

想要做好it这一行,先要自身硬(智),然后是产出质量(信),同事间的默契合作(仁),对事情的判断(勇)和对团队的要求以及奖惩制度(严)。

注意点

所有的注释都在这里,可自行阅读

!!!版本 => 笔者阅读的rollup版本为: 1.32.0

!!!提示 => 标有TODO为具体实现细节,会视情况分析。

!!!注意 => 每一个子标题都是父标题(函数)内部实现

!!!强调 => rollup中模块(文件)的id就是文件地址,所以类似resolveID这种就是解析文件地址的意思,我们可以返回我们想返回的文件id(也就是地址,相对路径、决定路径)来让rollup加载

rollup是一个核心,只做最基础的事情,比如提供默认模块(文件)加载机制, 比如打包成不同风格的内容,我们的插件中提供了加载文件路径,解析文件内容(处理ts,sass等)等操作,是一种插拔式的设计,和webpack类似

插拔式是一种非常灵活且可长期迭代更新的设计,这也是一个中大型框架的核心,人多力量大嘛~

主要通用模块以及含义

  1. Graph: 全局唯一的图,包含入口以及各种依赖的相互关系,操作方法,缓存等。是rollup的核心
  2. PathTracker: 无副作用模块依赖路径追踪
  3. PluginDriver: 插件驱动器,调用插件和提供插件环境上下文等
  4. FileEmitter: 资源操作器
  5. GlobalScope: 全局作用局,相对的还有局部的
  6. ModuleLoader: 模块加载器
  7. 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篇的更多相关文章

  1. 原来rollup这么简单之 rollup.rollup篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...

  2. 原来rollup这么简单之 rollup.watch篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...

  3. 原来rollup这么简单之 tree shaking篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 分享不易,希望能够得到大家的支持和关注. 计划 rollup系列打算 ...

  4. 原来rollup这么简单之插件篇

    大家好,我是小雨小雨,致力于分享有趣的.实用的技术文章. 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步. 大家的支持是我创作的动力. 计划 rollup系列打算一章一章的放出 ...

  5. 基于PHP——简单的WSDL的创建(WSDL篇)

    1.建立WSDL文件      建立WSDL的工具很多,eclipse.zendstudio.vs都可以,我个人建议自己写,熟悉结构,另外自动工具对xml schame类型支持在类型中可能会报错. 下 ...

  6. 史上最简单的SpringCloud教程 | 第三篇: 服务消费者(Feign)

    转载请标明出处: https://www.fangzhipeng.com/springcloud/2017/07/12/sc03-feign/ 本文出自方志朋的博客 最新Finchley版本请访问: ...

  7. [转]【ROLLUP】Oracle分组函数之ROLLUP魅力

    原创:http://blog.itpub.net/519536/viewspace-610995 本文通过演示给出Oracle ROLLUP分组函数的用法,体验一下Oracle在统计查询领域中的函数魅 ...

  8. 写简单游戏,学编程语言-python篇:大鱼吃小鱼

    很常见的游戏之一,实现原理并不复杂,并且参考了几个相关的代码.这边主要还是以学习编程语言和学习编程思路为重点记录一下吧.最近时间有点吃紧,只能匆忙记录一下.用pygame做的大鱼吃小鱼的游戏截图如下: ...

  9. 写简单游戏,学编程语言-python篇:传说哥大战剧毒术士

    上篇写的动画示例其实算不上一个游戏,顶多算是利用pygame进行的图形操作,今天着手实现一个小游戏:传说哥大战剧毒术士.名字很玄乎,其实就是最简单的一个射击游戏.好了废话不多说,先上截图吧: 一.初始 ...

随机推荐

  1. JavaScript秒转换成天-小时-分钟-秒

    根据时间秒转换成天-小时-分钟-秒 // 秒转换成day.hour.minutes.seconds formatSecond(second: number) { const days = Math.f ...

  2. idea常见需求

    1.给class加注释模板 /** *@ClassName ${NAME} *@Description TODO *@Author xxx *@Date ${DATE} ${TIME} *@Versi ...

  3. 喜欢B站的用户为何认同感超强?

    前几天视频弹幕网站哔哩哔哩(以下简称"B站"),正式登陆纳斯达克,股票代码"BILI".当日其CEOC兼董事长陈睿在B站的账号上传了上市视频. 打开视频,满眼& ...

  4. [CF1009F] Dominant Indices (+dsu on tree详解)

    这道题用到了dsu(Disjoint Set Union) on tree,树上启发式合并. 先看了CF的官方英文题解,又看了看zwz大佬的题解,差不多理解了dsu on tree的算法. 但是时间复 ...

  5. 引入 JPEGCodec;JPEGImageEncoder; 图片处理;MyEclipse编译时报错处理

    在Eclipse中处理图片,需要引入两个包: import com.sun.image.codec.jpeg.JPEGCodec; import com.sun.image.codec.jpeg.JP ...

  6. AQS总结

    前言 AQS(Abstract Queued Synchronizer)是JUC并发包中的核心基础组件,作者是大名鼎鼎的Doug Lea.通过AQS可以实现大部分的同步需求. 宏观架构 AQS包括一个 ...

  7. java design pattern - adapter pattern

    场景 适配器模式 总结 参考资料 场景 在编程中我们经常会遇到驴头不对马嘴的情况,比如以前是继承A的接口的对象,现在外部调用的时候需要该对象已B接口的形式来调用 ,这个时候我们可以让对象同时集成A和B ...

  8. HTTP Continuation or non-HTTP traffic

    发现一个 HTTP      Continuation or non-HTTP traffic的数据包,之前没有碰到过.不懂其意义,一看长度,显示1460,与TCP segment of a reas ...

  9. Jprofile解析dump文件使用详解

    1 Jprofile简介 官网 下载对应的系统版本即可 性能查看工具JProfiler,可用于查看java执行效率,查看线程状态,查看内存占用与内存对象,还可以分析dump日志. 2 功能简介 选择a ...

  10. 自动清理IIS log 日志脚本

    系统环境:windows server 2012 r2 IIS 版本:IIS8 操作实现清理IIS log File 脚本如下: @echo off ::自动清理IIS Log file set lo ...