大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。
内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。
分享不易,希望能够得到大家的支持和关注。
rollup系列打算一章一章的放出,内容更精简更专一更易于理解
目前打算分为一下几章:
rollup.rollup
rollup.generate + rollup.write <==== 当前文章
rollup.watch
插件的实现
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方法