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

大家好,我是小雨小雨,致力于分享有趣的、实用的技术文章。 内容分为翻译和原创,如果有问题,欢迎随时评论或私信,希望和大家一起进步。 分享不易,希望能够得到大家的支持和关注。 计划 rollup系列打算一章一章的放出,内容更精简更专一更易于理解 目前打算分为以下几章: rollup.rollup <==== 当前文章 rollup.generate + rollup.write rollup.watch tree shaking plugins TL;DR 在进入枯燥的代码解析之前,先大白话说下整个过程,rollup.rollup()主要分为以下几步: 配置收集、标准化 文件分析 源码编译,生成ast 模块生成 依赖解析 过滤净化 产出chunks 按照这个思路来看其实很简单,但是具体的细节却是百般复杂的。 不过我们也不必纠结于具体的某些实现,毕竟条条大路通罗马,我们可以吸纳并改进或学习一些没见过的代码技巧或优化方法,在我看来,这才是良好的阅读源码的方式。:) 注意点 所有的注释都在这里,可自行阅读 !!!版本 => 笔者阅读的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等)的构造基类 主流程解析 1.调用getInputOptions标准化input配置参数 const inputOptions = getInputOptions(rawInputOptions); 1.1. 调用mergeOptions,设置默认的input和output配置,并返回input配置 和 使用非法配置属性的错误信息 let { inputOptions, optionError } = mergeOptions({ config: rawInputOptions }); 1.2. 调用options钩子函数,以在input配合完全标准化之前进行自定义修改 inputOptions = inputOptions.plugins!.reduce(applyOptionHook, inputOptions); 1.3. 标准化插件操作:为返回对象中没有name属性的插件设置默认的插件名 => at position 当前插件在所有插件中索引值 inputOptions.plugins = normalizePlugins(inputOptions.plugins!, ANONYMOUS_PLUGIN_PREFIX); 1.4. 对不兼容内嵌动态引入模块或保留模块两种情况的配置,进行警告报错 // 将动态导入的依赖(import | require.ensure() | other)内嵌到一个chunk而不创建独立的包,相关的代码逻辑如下 if (inputOptions.inlineDynamicImports) { // preserveModules: 尽可能的保留模块,而不是混合起来,创建更少的chunks,默认为false,不开启 if (inputOptions.preserveModules) // 如果开启了,就与内嵌冲突了 return error({ code: 'INVALID_OPTION', message: `"preserveModules" does not support the "inlineDynamicImports" option.` }); // 其他判断,具体参考代码仓库:index.ts } else if (inputOptions.preserveModules) { // 又对 以原始文件命名,不综合打包 的功能进行排异处理 if (inputOptions.manualChunks) return error({ code: 'INVALID_OPTION', message: '"preserveModules" does not support the "manualChunks" option.' }); // 其他判断,具体参考代码仓库:index.ts } 1.5. 返回处理后的input配置 return inputOptions; 2.是否开启性能检测,检测inputOptions.perf属性,如果未设置没那么检测函数为空 initialiseTimers(inputOptions); 3.创建图,参数为input配置和watch,watch当前不考虑 const graph = new Graph(inputOptions, curWatcher); 3.1. 初始化警告函数,对已经提示过得警告进行缓存 this.onwarn = (options.onwarn as WarningHandler) || makeOnwarn(); 3.2. 给当前图挂载路径追踪系统,无构造函数,只有属性和更改属性的方法 this.deoptimizationTracker = new PathTracker(); 3.3. 初始化当前图的唯一模块缓存容器,可以将上个打包结果的cache属性赋给下一次打包,提升打包速度 => this.cachedModules = new Map(); 3.4. 读取传递的上次build结果中的模块和插件。插件缓存参考 =>,下文中解释。 if (options.cache) { if (options.cache.modules) for (const module of options.cache.modules) this.cachedModules.set(module.id, module); } if (options.cache !== false) { this.pluginCache = (options.cache && options.cache.plugins) || Object.create(null); for (const name in this.pluginCache) { const cache = this.pluginCache[name]; for (const key of Object.keys(cache)) cache[key][0]++; } } 3.5. treeshake信息挂载。 if (options.treeshake !== false) { this.treeshakingOptions = options.treeshake && options.treeshake !== true ? { annotations: options.treeshake.annotations !== false, moduleSideEffects: options.treeshake.moduleSideEffects, propertyReadSideEffects: options.treeshake.propertyReadSideEffects !== false, pureExternalModules: options.treeshake.pureExternalModules, tryCatchDeoptimization: options.treeshake.tryCatchDeoptimization !== false, unknownGlobalSideEffects: options.treeshake.unknownGlobalSideEffects !== false } : { annotations: true, moduleSideEffects: true, propertyReadSideEffects: true, tryCatchDeoptimization: true, unknownGlobalSideEffects: true }; if (typeof this.treeshakingOptions.pureExternalModules !== 'undefined') { this.warnDeprecation( `The "treeshake.pureExternalModules" option is deprecated. The "treeshake.moduleSideEffects" option should be used instead. "treeshake.pureExternalModules: true" is equivalent to "treeshake.moduleSideEffects: 'no-external'"`, false ); } } 3.6. 初始化代码解析器,具体参数和插件参考Graph.ts this.contextParse = (code: string, options: acorn.Options = {}) => this.acornParser.parse(code, { ...defaultAcornOptions, ...options, ...this.acornOptions }) as any; 3.7. 插件驱动器 this.pluginDriver = new PluginDriver( this, options.plugins!, this.pluginCache, // 处理软连文件的时候,是否以为软连所在地址作为上下文,false为是,true为不是。 options.preserveSymlinks === true, watcher ); 3.7.1. 弃用api警告,参数挂载 3.7.2. 实例化FileEmitter并且将实例所携带方法设置到插件驱动器上 // basePluginDriver为PluginDriver的第六个参数,代表graph的'根'插件驱动器 this.fileEmitter = new FileEmitter(graph, basePluginDriver && basePluginDriver.fileEmitter); this.emitFile = this.fileEmitter.emitFile; this.getFileName = this.fileEmitter.getFileName; this.finaliseAssets = this.fileEmitter.assertAssetsFinalized; this.setOutputBundle = this.fileEmitter.setOutputBundle; 3.7.3. 插件拼接 this.plugins = userPlugins.concat( basePluginDriver ? basePluginDriver.plugins : [getRollupDefaultPlugin(preserveSymlinks)] ); 3.7.4. 缓存插件们的上下文环境,之后执行插件的的时候会通过index获取并注入到插件内 // 利用map给每个插件注入plugin特有的context,并缓存 this.pluginContexts = this.plugins.map( getPluginContexts(pluginCache, graph, this.fileEmitter, watcher) ); 3.7.5. input和output设置的插件冲突的时候,报错 if (basePluginDriver) { for (const plugin of userPlugins) { for (const hook of basePluginDriver.previousHooks) { if (hook in plugin) { graph.warn(errInputHookInOutputPlugin(plugin.name, hook)); } } } } 3.8. 监听模式的设定 if (watcher) { const handleChange = (id: string) => this.pluginDriver.hookSeqSync('watchChange', [id]); watcher.on('change', handleChange); watcher.once('restart', () => { watcher.removeListener('change', handleChange); }); } 3.9. 全局上下文 this.scope = new GlobalScope(); 3.10. 设置模块的全局上下文,默认为false this.context = String(options.context); // 用户是否自定义了上下文环境 const optionsModuleContext = options.moduleContext; if (typeof optionsModuleContext === 'function') { this.getModuleContext = id => optionsModuleContext(id) || this.context; } else if (typeof optionsModuleContext === 'object') { const moduleContext = new Map(); for (const key in optionsModuleContext) { moduleContext.set(resolve(key), optionsModuleContext[key]); } this.getModuleContext = id => moduleContext.get(id) || this.context; } else { this.getModuleContext = () => this.context; } 3.11. 初始化moduleLoader,用于模块(文件)的解析和加载 // 模块(文件)解析加载,内部调用的resolveID和load等钩子,让使用者拥有更多的操作能力 this.moduleLoader = new ModuleLoader( this, this.moduleById, this.pluginDriver, options.external!, (typeof options.manualChunks === 'function' && options.manualChunks) as GetManualChunk | null, (this.treeshakingOptions ? this.treeshakingOptions.moduleSideEffects : null)!, (this.treeshakingOptions ? this.treeshakingOptions.pureExternalModules : false)! ); 4.执行buildStart钩子函数,打包获取chunks,以供后续生成和写入使用 try { // buildStart钩子函数触发 await graph.pluginDriver.hookParallel('buildStart', [inputOptions]); // 这一步通过id,深度分析拓扑关系,去除无用块,进而生成我们的chunks // build的逻辑详见下文 chunks = await graph.build( // 这个chunks是闭包,所以generate和write可以用到 inputOptions.input as string | string[] | Record, inputOptions.manualChunks, inputOptions.inlineDynamicImports! ); } catch (err) { const watchFiles = Object.keys(graph.watchFiles); if (watchFiles.length > 0) { err.watchFiles = watchFiles; } await graph.pluginDriver.hookParallel('buildEnd', [err]); throw err; } 5.返回一个对象,包括缓存,监听文件和generate、write两个方法 return { cache, watchFiles, generate, write } graph.build逻辑解析 build方法通过id,深度分析拓扑关系,去除无用块,进而生成我们的chunks 接受三个参数:入口、提取公共块规则(manualChunks)、是否内嵌动态导入模块 build是很单一的方法,就是产出我们的chunks。他返回一个promise对象供之后的使用。 return Promise.all([ 入口模块, // 代码为: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true) 用户定义公共模块 // 这块没有返回值,只是将公共模块缓存到模块加载器上,处理结果由入口模块代理返回。巧妙的处理方式,一举两得 ]).then((入口模块的返回) => { // 模块的依赖关系处理 return chunks; }); 入口模块: this.moduleLoader.addEntryModules(normalizeEntryModules(entryModules), true) normalizeEntryModules对入口进行标准化处理,返回统一的格式: UnresolvedModule { fileName: string | null; id: string; name: string | null; } addEntryModules对模块进行加载、去重,再排序操作,最后返回模块,公共chunks。其中,在加载过程中会将处理过的模块缓存到ModuleLoaders的modulesById(Map对象)上。部分代码如下: // 模块加载部分 private fetchModule( id: string, importer: string, moduleSideEffects: boolean, syntheticNamedExports: boolean, isEntry: boolean ): Promise { // 主流程如下: // 获取缓存,提升效率: const existingModule = this.modulesById.get(id); if (existingModule instanceof Module) { existingModule.isEntryPoint = existingModule.isEntryPoint || isEntry; return Promise.resolve(existingModule); } // 新建模块: const module: Module = new Module( this.graph, id, moduleSideEffects, syntheticNamedExports, isEntry ); // 缓存,以备优化 this.modulesById.set(id, module); // 为每一个入库模块设置已监听 this.graph.watchFiles[id] = true; // 调用用户定义的manualChunk方法,获取公共chunks别名,比如: // 比如 manualChunkAlias(id){ // if (xxx) { // return 'vendor'; // } // } const manualChunkAlias = this.getManualChunk(id); // 缓存到 manualChunkModules if (typeof manualChunkAlias === 'string') { this.addModuleToManualChunk(manualChunkAlias, module); } // 调用load钩子函数并返回处理结果,其中第二个数组参数为传到钩子函数的的参数 return Promise.resolve(this.pluginDriver.hookFirst('load', [id])) .cache() .then(source => { // 统一格式: sourceDescription return { code: souce, // ... } }) .then(sourceDescription => { // 返回钩子函数transform处理后的代码,比如jsx解析结果,ts解析结果 // 参考: https://github.com/rollup/plugins/blob/e7a9e4a516d398cbbd1fa2b605610517d9161525/packages/wasm/src/index.js return transform(this.graph, sourceDescription, module); }) .then(source => { // 代码编译结果挂在到当前解析的入口模块上 module.setSource(source); // 模块id与模块绑定 this.modulesById.set(id, module); // 处理模块的依赖们,将导出的模块也挂载到module上 // !!! 注意: fetchAllDependencies中创建的模块是通过ExternalModule类创建的,有别的入口模块的 return this.fetchAllDependencies(module).then(() => { for (const name in module.exports) { if (name !== 'default') { module.exportsAll[name] = module.id; } } for (const source of module.exportAllSources) { const id = module.resolvedIds[source].id; const exportAllModule = this.modulesById.get(id); if (exportAllModule instanceof ExternalModule) continue; for (const name in exportAllModule!.exportsAll) { if (name in module.exportsAll) { this.graph.warn(errNamespaceConflict(name, module, exportAllModule!)); } else { module.exportsAll[name] = exportAllModule!.exportsAll[name]; } } } // 返回这些处理后的module对象,从id(文件路径) 转换到 一个近乎具有文件完整信息的对象。 return module; }) } // 去重 let moduleIndex = firstEntryModuleIndex; for (const entryModule of entryModules) { // 是否为用户定义,默认是 entryModule.isUserDefinedEntryPoint = entryModule.isUserDefinedEntryPoint || isUserDefined; const existingIndexModule = this.indexedEntryModules.find( indexedModule => indexedModule.module.id === entryModule.id ); // 根据moduleIndex进行入口去重 if (!existingIndexModule) { this.indexedEntryModules.push({ module: entryModule, index: moduleIndex }); } else { existingIndexModule.index = Math.min(existingIndexModule.index, moduleIndex); } moduleIndex++; } // 排序 this.indexedEntryModules.sort(({ index: indexA }, { index: indexB }) => indexA > indexB ? 1 : -1 ); 模块的依赖关系处理 部分 已经加载处理过的模块会缓存到moduleById上,所以直接遍历之,再根据所属模块类进行分类 // moduleById是 id => module 的存储, 是所有合法的入口模块 for (const module of this.moduleById.values()) { if (module instanceof Module) { this.modules.push(module); } else { this.externalModules.push(module); } } 获取所有入口,找到正确的、移除无用的依赖,并过滤出真正作为入口的模块 // this.link(entryModules)方法的内部 // 找到所有的依赖 for (const module of this.modules) { module.linkDependencies(); } // 返回所有的入口启动模块(也就是非外部模块),和那些依赖了一圈结果成死循环的模块相对路径 const { orderedModules, cyclePaths } = analyseModuleExecution(entryModules); // 对那些死循环路径进行警告 for (const cyclePath of cyclePaths) { this.warn({ code: 'CIRCULAR_DEPENDENCY', cycle: cyclePath, importer: cyclePath[0], message: `Circular dependency: ${cyclePath.join(' -> ')}` }); } // 过滤出真正的入口启动模块,赋值给modules this.modules = orderedModules; // ast语法的进一步解析 // TODO: 视情况详细补充 for (const module of this.modules) { module.bindReferences(); } 剩余部分 // 引入所有的导出,设定相关关系 // TODO: 视情况详细补充 for (const module of entryModules) { module.includeAllExports(); } // 根据用户的treeshaking配置,给引入的环境设置上下文环境 this.includeMarked(this.modules); // 检查所有没使用的模块,进行提示警告 for (const externalModule of this.externalModules) externalModule.warnUnusedImports(); // 给每个入口模块添加hash,以备后续整合到一个chunk里 if (!this.preserveModules && !inlineDynamicImports) { assignChunkColouringHashes(entryModules, manualChunkModulesByAlias); } let chunks: Chunk[] = []; // 为每个模块都创建chunk if (this.preserveModules) { // 遍历入口模块 for (const module of this.modules) { // 新建chunk实例对象 const chunk = new Chunk(this, [module]); // 是入口模块,并且非空 if (module.isEntryPoint || !chunk.isEmpty) { chunk.entryModules = [module]; } chunks.push(chunk); } } else { // 创建尽可能少的chunk const chunkModules: { [entryHashSum: string]: Module[] } = {}; for (const module of this.modules) { // 将之前设置的hash值转换为string const entryPointsHashStr = Uint8ArrayToHexString(module.entryPointsHash); const curChunk = chunkModules[entryPointsHashStr]; // 有的话,添加module,没有的话创建并添加,相同的hash值会添加到一起 if (curChunk) { curChunk.push(module); } else { chunkModules[entryPointsHashStr] = [module]; } } // 将同一hash值的chunks们排序后,添加到chunks中 for (const entryHashSum in chunkModules) { const chunkModulesOrdered = chunkModules[entryHashSum]; // 根据之前的设定的index排序,这个应该代表引入的顺序,或者执行的先后顺序 sortByExecutionOrder(chunkModulesOrdered); // 用排序后的chunkModulesOrdered新建chunk const chunk = new Chunk(this, chunkModulesOrdered); chunks.push(chunk); } } // 将依赖挂载到每个chunk上 for (const chunk of chunks) { chunk.link(); } 以上就是rollup.rollup的主流程分析,具体细节参考代码库注释 部分功能的具体解析 插件缓存能力解析,为开发者们提供了插件上的缓存能力,利用cacheKey可以共享相同插件的不同实例间的数据 function createPluginCache(cache: SerializablePluginCache): PluginCache { // 利用闭包将cache缓存 return { has(id: string) { const item = cache[id]; if (!item) return false; item[0] = 0; // 如果访问了,那么重置访问过期次数,猜测:就是说明用户有意向主动去使用 return true; }, get(id: string) { const item = cache[id]; if (!item) return undefined; item[0] = 0; // 如果访问了,那么重置访问过期次数 return item[1]; }, set(id: string, value: any) { cache[id] = [0, value]; }, delete(id: string) { return delete cache[id]; } }; } 可以看到rollup利用对象加数组的结构来为插件提供缓存能力,即: { test: [0, '内容'] } 数组的第一项是当前访问的计数器,和缓存的过期次数挂钩,再加上js的闭包能力简单实用的提供了插件上的缓存能力 总结 到目前为止,再一次加深了职能单一和依赖注入重要性,比如模块加载器,插件驱动器,还有Graph。还有rollup的(数据)模块化,webpack也类似,vue也类似,都是将具象的内容转换为抽象的数据,再不断挂载相关的依赖的其他抽象数据,当然这其中需要符合某些规范,比如estree规范。 鄙人一直对构建很感兴趣,我的github有接近一半都是和构建有关的,所以这次从rollup入口,开始揭开构建世界的那一层层雾霾,还我们一个清晰地世界。:) rollup系列不会参考别人的分享(目前也没找到有人分析rollup。。),完全自食其力一行一行的阅读,所以难免会有些地方不是很正确。 没办法,阅读别人的代码,有些地方就像猜女人的心思,太tm难了,所以有不对的地方希望大佬们多多指点,互相学习。 image 还是那句话,创作不易,希望得到大家的支持,与君共勉,咱们下期见! image

本文章由javascript技术分享原创和收集

发表评论 (审核通过后显示评论):