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

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

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

计划

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. 成组vs成对|H1是受保护的|U检验

    生物统计与实验设计 样本均值的分布推导 概率密度曲线上每点x取值概率是不相等的.标准化是转化为无量纲的表面误差,该分布是误差分布,置信区间是可接受该误差是随机误差的误差区间.上面的部分是该估计参数与平 ...

  2. abrupt|promising

    N-VAR 典礼;(宗教)仪式A ritual is a religious service or other ceremony which involves a series of actions ...

  3. 苹果联合创始人高调宣布弃用Facebook是什么梗?

    这段时间,扎克伯格非常郁闷.泄密丑闻不仅让Facebook股价大跌.引来审查等,还被众多互联网.科技大佬批判.孤立.如,"钢铁侠"马斯克就直接删除了SpaceX 和特斯拉的 Fac ...

  4. linux上hosts文件如何配置

    linux上hosts文件如何配置 一.什么是host Hosts是一个没有扩展名的系统文件,其基本作用就是将一些常用的网址域名与其对应的IP地址建立一个关联“数据库”,当用户在浏览器中输入一个需要登 ...

  5. python2下经典爬虫(第一卷)

    python2.7的爬虫个人认为比较经典在此我将会用书中的网站http://example.webscraping.com作为案例 爬虫第一步:进行背景调研 了解网站的结构资源在网站的robots.t ...

  6. 杂谈php之什么是cgi,fastcgi,fpm,cli,mod

    杂谈PHP极少关注的问题 本话题来自于我使用PHP进行网页爬虫的一次经历.对于一个web开发者来说,PHP解释器本身却知之甚小,实在是惭愧呐! 首先这个话题要从几个提问开始. PHP是什么? 外文名: ...

  7. zepto.js和jquery.js函数比较有什么优点?

    1.Zepto.js 是专门为现代智能手机浏览器退出的 Javascript 框架, 拥有和jQuery相似的语法, 但是和jQuery相比下来, 他有很多优点, 大小方面 , 压缩后的 zepto. ...

  8. [Python_scrapy图片爬取下载]

    welcome to myblog Dome地址 爬取某个车站的图片 item.py 中 1.申明item 的fields class PhotoItem(scrapy.Item): # define ...

  9. Java里观察者模式(订阅发布模式)

    创建主题(Subject)接口 创建订阅者(Observer)接口 实现主题 实现观察者 测试 总结 在公司开发项目,如果碰到一些在特定条件下触发某些逻辑操作的功能的实现基本上都是用的定时器 比如用户 ...

  10. TDA2050功率放大器研究

    音频功率放大模块(以下简称功放)用于处理模拟信号,将功率较低的输入信号进行线性放大,输出大功率的信号以驱动换能器.通常,电子发烧友自己设计功放,与各类音源和喇叭匹配,以得到满意的音响效果.在测试中,实 ...