摘要:引言通过前面几张的铺垫下面开始分析源码核心流程大体上可以分为初始化编译输出三个阶段下面开始分析初始化这个阶段整体流程做了什么启动构建,读取与合并配置参数,加载,实例化。推荐源码之源码之机制源码之简介源码之机制参考源码
引言
通过前面几张的铺垫,下面开始分析webpack源码核心流程,大体上可以分为初始化,编译,输出三个阶段,下面开始分析
初始化这个阶段整体流程做了什么? 启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler。详细分析
//通过yargs获得shell中的参数 yargs.parse(process.argv.slice(2), (err, argv, output) => { //把webpack.config.js中的参数和shell参数整合到options对象上 let options; options = require("./convert-argv")(argv); function processOptions(options) { const firstOptions = [].concat(options)[0]; const webpack = require("webpack"); let compiler; //通过webpack方法创建compile对象,Compiler 负责文件监听和启动编译。 //Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例。 compiler = webpack(options); if (firstOptions.watch || options.watch) { compiler.watch(watchOptions, compilerCallback); //启动一次新的编译。 } else compiler.run(compilerCallback); } processOptions(options); });
说明 从源码中摘取了初始化的的第一步,做了简化,当运行webpack命令的的时候,运行的是webpack-cli下webpack.js,其内容是一个自执行函数,上面是执行的第一步,进行参数的解析合并处理,并创建compiler实例,然后启动编译运行run方法,其中关键步骤 compiler = webpack(options); 详细展开如下所示
const webpack = (options, callback) => { //参数合法性校验 const webpackOptionsValidationErrors = validateSchema( webpackOptionsSchema, options ); let compiler; if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === "object") { options = new WebpackOptionsDefaulter().process(options); //创建compiler对象 compiler = new Compiler(options.context); compiler.options = options; new NodeEnvironmentPlugin().apply(compiler); //注册配置文件中的插件,依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } //开始应用 Node.js 风格的文件系统到 compiler 对象,以方便后续的文件寻找和读取。 compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); //注册内部插件 compiler.options = new WebpackOptionsApply().process(options, compiler); } return compiler; };
说明 注册插件过程不在展开,webpack内置插件真的很多啊
编译这个阶段整体流程做了什么? 从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。详细分析
this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); });
说明 从执行run方法开始,开始执行编译流程,run方法触发了before-run、run两个事件,然后通过readRecords读取文件,通过compile进行打包,该方法中实例化了一个Compilation类
compile(callback) { const params = this.newCompilationParams(); this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); this.hooks.compile.call(params); // 每编译一次都会创建一个compilation对象(比如watch 文件时,一改动就会执行),但是compile只会创建一次 const compilation = this.newCompilation(params); // make事件触发了 事件会触发SingleEntryPlugin监听函数,调用compilation.addEntry方法 this.hooks.make.callAsync(compilation, err => { if (err) return callback(err); }); }); }
说明 打包时触发before-compile、compile、make等事件,同时创建非常重要的compilation对象,内部有声明了很多钩子,初始化模板等等
this.hooks = { buildModule: new SyncHook(["module"]), seal: new SyncHook([]), optimize: new SyncHook([]), }; //拼接最终生成代码的主模板会用到 this.mainTemplate = new MainTemplate(this.outputOptions); //拼接最终生成代码的chunk模板会用到 this.chunkTemplate = new ChunkTemplate(this.outputOptions); //拼接最终生成代码的热更新模板会用到 this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate()
//监听comple的make hooks事件,通过内部的 SingleEntryPlugin 从入口文件开始执行编译 compiler.hooks.make.tapAsync( "SingleEntryPlugin", (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); compilation.addEntry(context, dep, name, callback); } );
说明 监听compile的make hooks事件,通过内部的 SingleEntryPlugin 从入口文件开始执行编译,调用compilation.addEntry方法,根据模块的类型获取对应的模块工厂并创建模块,开始构建模块
doBuild(options, compilation, resolver, fs, callback) { const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); //调用loader处理模块 runLoaders( { resource: this.resource, loaders: this.loaders, context: loaderContext, readResource: fs.readFile.bind(fs) }, (err, result) => { const resourceBuffer = result.resourceBuffer; const source = result.result[0]; const sourceMap = result.result.length >= 1 ? result.result[1] : null; const extraInfo = result.result.length >= 2 ? result.result[2] : null; this._source = this.createSource( this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); //loader处理完之后 得到_source 然后ast接着处理 this._ast = typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined ? extraInfo.webpackAST : null; return callback(); } ); }
说明 SingleEntryPlugin这个内存插件主要作用是从entry读取文件,根据文件类型和配置的 Loader 执行runLoaders,然后将loader处理后的文件通过acorn抽象成抽象语法树AST,遍历AST,构建该模块的所有依赖。
输出这个阶段整体流程做了什么? 把编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统。详细分析
//所有依赖build完成,开始对chunk进行优化(抽取公共模块、加hash等) compilation.seal(err => { if (err) return callback(err); this.hooks.afterCompile.callAsync(compilation, err => { if (err) return callback(err); return callback(null, compilation); }); });
说明 compilation.seal主要是对chunk进行优化,生成编译后的源码,比较重要,详细展开如下所示
//代码生成前面优化 this.hooks.optimize.call(); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { this.hooks.beforeHash.call(); this.createHash(); this.hooks.afterHash.call(); if (shouldRecord) this.hooks.recordHash.call(this.records); this.hooks.beforeModuleAssets.call(); this.createModuleAssets(); if (this.hooks.shouldGenerateChunkAssets.call() !== false) { this.hooks.beforeChunkAssets.call(); //生成最终打包输出的chunk资源,根据template文件,详细步骤如下所示 this.createChunkAssets(); } }); -------------------------------------- //取出最后文件需要的模板 const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; //通过模板最终生成webpack_require格式的内容,他这个是内部封装的拼接渲染逻辑,也没用什么ejs,handlebar等这些模板工具 source = fileManifest.render(); //生成的资源保存在compilation.assets,方便下一步emitAssets步骤中,把文件输出到硬盘 this.assets[file] = source;
//把处理好的assets输出到output的path中 emitAssets(compilation, callback) { let outputPath; const emitFiles = err => { if (err) return callback(err); asyncLib.forEach( compilation.assets, (source, file, callback) => { const writeOut = err => { //输出打包后的文件到配置中指定的目录下 this.outputFileSystem.writeFile(targetPath, content, callback); }; writeOut(); } ); }; this.hooks.emit.callAsync(compilation, err => { if (err) return callback(err); outputPath = compilation.getPath(this.outputPath); this.outputFileSystem.mkdirp(outputPath, emitFiles); }); }总结
如果多带带看这篇文章的话,理解起来会比较困难,推荐一下与之相关的系列铺垫文章,上面是我对webpack源码运行流程的总结, 整个流程已经跑通了,不过还有蛮多点值得深入挖掘的。清明在家宅了3天,过得好快,明天公司组织去奥森公园寻宝行动,期待ing 。
推荐
webpack源码之tapable
webpack源码之plugin机制
webpack源码之ast简介
webpack源码之loader机制
参考源码
webpack: "4.4.1"
webpack-cli: "2.0.13"
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/94025.html
摘要:流程划分纵观整个打包过程,可以流程划分为四块。核心类关系图功能实现模块通过将源码解析为树并拆分,以及直至基础模块。通过的依赖和切割文件构建出含有和包含关系的对象。通过模版完成主入口文件的写入,模版完成切割文件的写入。 前言 插件plugin,webpack重要的组成部分。它以事件流的方式让用户可以直接接触到webpack的整个编译过程。plugin在编译的关键地方触发对应的事件,极大的...
摘要:源码分析安装好包,根据上述方法,我们运行如下命令初始化在构造函数处打上断点,可以看到继承自,上面定义了一个函数。因为函数定义在原型上,并通过在构造函数中赋值。 Webpack源码阅读之Tapable webpack采用Tapable来进行流程控制,在这套体系上,内部近百个插件有条不紊,还能支持外部开发自定义插件来扩展功能,所以在阅读webpack源码前先了解Tapable的机制是很有必...
摘要:用于对模块的源代码进行转换。甚至允许你直接在模块中文件和区别之前一篇文章中介绍了机制和今天研究的对象他们两者在一起极大的拓展了的功能。原理说明上面是源码中执行关键步骤递归的方式执行执行机流程似于中间件机制参考源码参考文档 loader概念 loader是用来加载处理各种形式的资源,本质上是一个函数, 接受文件作为参数,返回转化后的结构。 loader 用于对模块的源代码进行转换。loa...
摘要:下面一步步拆解上述流程。切换至分支检测本地和暂存区是否还有未提交的文件检测本地分支是否有误检测本地分支是否落后远程分支发布发布检测到在分支上没有冲突后,立即执行。 背景 最近一直在着手做一个与业务强相关的组件库,一直在思考要从哪里下手,怎么来设计这个组件库,因为业务上一直在使用ElementUI(以下简称Element),于是想参考了一下Element组件库的设计,看看Element构...
阅读 3567·2020-12-03 17:42
阅读 2718·2019-08-30 15:54
阅读 2193·2019-08-30 15:44
阅读 535·2019-08-30 14:08
阅读 934·2019-08-30 14:00
阅读 1082·2019-08-30 13:46
阅读 2763·2019-08-29 18:33
阅读 2724·2019-08-29 14:11