欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

webpack源码解析(使用介绍)

程序员文章站 2022-03-13 11:54:46
webpack 源码解析 序言 项目上在使用webpack,感叹真是神器,既然是神器,就想探知究竟。 总览 webpack整体是一个插件架构,所有的功能都以插件的方式集成在...

webpack 源码解析

序言

项目上在使用webpack,感叹真是神器,既然是神器,就想探知究竟。

总览

webpack整体是一个插件架构,所有的功能都以插件的方式集成在构建流程中,通过发布订阅事件来触发各个插件执行。webpack核心使用Tapable 来实现插件(plugins)的binding和applying.

先整体来看一下webpack事件流:通过在Tapable中打日志获得

         method                       event-name
--------------------------------------------------
applyPluginsBailResult           |   entry-option
applyPlugins                     |   after-plugins
applyPlugins                     |   after-resolvers
applyPlugins                     |   environment
applyPlugins                     |   after-environment
applyPluginsAsync                |   before-run
applyPluginsAsyncSeries          |   run
applyPlugins                     |   normal-module-factory
applyPlugins                     |   context-module-factory
applyPlugins                     |   compile
applyPlugins                     |   this-compilation
applyPlugins                     |   compilation
applyPluginsParallel             |   make
applyPluginsAsyncWaterfall       |   before-resolve
applyPluginsWaterfall            |   factory
applyPluginsWaterfall            |   resolver
applyPlugins                     |   resolve
applyPlugins                     |   resolve-step
applyPluginsParallelBailResult   |   file
applyPluginsParallelBailResult   |   directory
applyPlugins                     |   resolve-step
applyPluginsParallelBailResult   |   result
applyPluginsAsyncWaterfall       |   after-resolve
applyPluginsBailResult           |   create-module
applyPluginsWaterfall            |   module
applyPlugins                     |   build-module
applyPlugins                     |   normal-module-loader
applyPluginsBailResult           |   program
applyPluginsBailResult           |   statement
applyPluginsBailResult           |   evaluate MemberExpression
applyPluginsBailResult           |   evaluate Identifier document.write
applyPluginsBailResult           |   call document.write
applyPluginsBailResult           |   expression document.write
applyPluginsBailResult           |   expression document
applyPlugins                     |   succeed-module
applyPlugins                     |   seal
applyPlugins                     |   optimize
applyPlugins                     |   optimize-modules
applyPlugins                     |   after-optimize-modules
applyPlugins                     |   optimize-chunks
applyPlugins                     |   after-optimize-chunks
applyPluginsAsyncSeries          |   optimize-tree
applyPlugins                     |   after-optimize-tree
applyPluginsBailResult           |   should-record
applyPlugins                     |   revive-modules
applyPlugins                     |   optimize-module-order
applyPlugins                     |   before-module-ids
applyPlugins                     |   optimize-module-ids
applyPlugins                     |   after-optimize-module-ids
applyPlugins                     |   record-modules
applyPlugins                     |   revive-chunks
applyPlugins                     |   optimize-chunk-order
applyPlugins                     |   before-chunk-ids
applyPlugins                     |   optimize-chunk-ids
applyPlugins                     |   after-optimize-chunk-ids
applyPlugins                     |   record-chunks
applyPlugins                     |   before-hash
applyPlugins                     |   hash
applyPlugins                     |   hash
applyPlugins                     |   hash
applyPlugins                     |   hash
applyPlugins                     |   hash-for-chunk
applyPlugins                     |   chunk-hash
applyPlugins                     |   after-hash
applyPlugins                     |   before-chunk-assets
applyPluginsWaterfall            |   global-hash-paths
applyPluginsBailResult           |   global-hash
applyPluginsWaterfall            |   bootstrap
applyPluginsWaterfall            |   local-vars
applyPluginsWaterfall            |   require
applyPluginsWaterfall            |   module-obj
applyPluginsWaterfall            |   module-require
applyPluginsWaterfall            |   require-extensions
applyPluginsWaterfall            |   asset-path
applyPluginsWaterfall            |   startup
applyPluginsWaterfall            |   module-require
applyPluginsWaterfall            |   render
applyPluginsWaterfall            |   module
applyPluginsWaterfall            |   render
applyPluginsWaterfall            |   package
applyPluginsWaterfall            |   modules
applyPluginsWaterfall            |   render-with-entry
applyPluginsWaterfall            |   asset-path
applyPlugins                     |   chunk-asset
applyPlugins                     |   additional-chunk-assets
applyPlugins                     |   record
applyPluginsAsyncSeries          |   additional-assets
applyPluginsAsyncSeries          |   optimize-chunk-assets
applyPlugins                     |   after-optimize-chunk-assets
applyPluginsAsyncSeries          |   optimize-assets
applyPlugins                     |   after-optimize-assets
applyPluginsAsyncSeries          |   after-compile
applyPluginsBailResult           |   should-emit
applyPluginsAsyncSeries          |   emit
applyPluginsWaterfall            |   asset-path
applyPluginsAsyncSeries          |   after-emit
applyPlugins                     |   done

其中有几个关键节段对应的事件分别是:

entry-option 初始化option

run 开始编译

make 从entry开始递归的分析依赖,对每个依赖模块进行build

before-resolve - after-resolve 对其中一个模块位置进行解析

build-module 开始构建 (build) 这个module,这里将使用文件对应的loader加载

normal-module-loader 对用loader加载完成的module(是一段js代码)进行编译,用 acorn 编译,生成ast抽象语法树。

program 开始对ast进行遍历,当遇到require等一些调用表达式时,触发call require事件的handler执行,收集依赖,并。如:AMDRequireDependenciesBlockParserPlugin等

seal 所有依赖build完成,下面将开始对chunk进行优化,比如合并,抽取公共模块,加hash

bootstrap 生成启动代码

emit 把各个chunk输出到结果文件

webpack的关键实体

模块,依赖,模块工厂

webpack源码解析(使用介绍)

模块

Module是webpack的中的核心实体,要加载的一切和所有的依赖都是Module,总之一切都是Module。它有很多子类:RawModule,NormalModule ,MultiModule,ContextModule,DelegatedModule,DllModule,ExternalModule 等

依赖

每一个依赖(Dependency)的实体都包含一个module字段,指向被依赖的Module. 这样通过Module的dependencies数组成员就能找出该模块所依赖的其它模块。 webpack使用不同的Dependency子类,如AMDRequireDependency ,AMDDefineDependency ,AMDRequireArrayDependency,CommonJsRequireDependency,SystemImportDependency来表式不同的模块加载规范, 通过对应的DependencyParserPlugin来加载 AMD或CMD的模块。 后面会专门讲不同DependencyParserPlugin的实现方式 。

依赖模版Template

每个依赖都有相应Template,用来生成加载该依赖模块的js代码。

模块工厂

使用工厂模式创建不同的Module,有四个主要的子类: NormalModuleFactory,ContextModuleFactory , DllModuleFactory,MultiModuleFactory.

调用过程

webpack的实际入口是Compiler类的run方法, 在run方法里调用compile方法开始编译。在编译的时候会使用一个核心对象:Compilation.

核心对象Compilation

该对象负责组织整个编译过程,包含了每个构建环节所对应的方法,如:addEntry ,buildModule,processModuleDependencies,summarizeDependencies,createModuleAssets,createHash等等。

主要的成员

参考源码:

        this.compiler = compiler;       //Compiler对象的引用
        this.resolvers = compiler.resolvers;   //模块解析器
        this.inputFileSystem = compiler.inputFileSystem;

        var options = this.options = compiler.options;
        this.outputOptions = options && options.output;
        this.bail = options && options.bail;
        this.profile = options && options.profile;

        this.mainTemplate = new MainTemplate(this.outputOptions);   //这里Template是用来生成js结果文件的。
        this.chunkTemplate = new ChunkTemplate(this.outputOptions, this.mainTemplate);
        this.hotUpdateChunkTemplate = new HotUpdateChunkTemplate(this.outputOptions);
        this.moduleTemplate = new ModuleTemplate(this.outputOptions);

        this.entries = [];              //入口
        this.preparedChunks = [];        //预先加载的chunk
        this.chunks = [];                                //所有的chunk
        this.namedChunks = {};                        //每个都对应一个名子,可以通过namedChunks[name]获取chunk
        this.modules = [];              //所有module
        this._modules = {};
        this.cache = null;
        this.records = null;
        this.nextFreeModuleId = 0;
        this.nextFreeChunkId = 0;
        this.nextFreeModuleIndex = 0;
        this.nextFreeModuleIndex2 = 0;
        this.additionalChunkAssets = [];
        this.assets = {};                                //保存所有生成的文件
        this.errors = [];
        this.warnings = [];
        this.children = [];            // 保存子Compilation对象,子Compilation对象依赖它的上级Compilation对象生成的结果,所以要等父Compilation编译完成才能开始。
        this.dependencyFactories = new ArrayMap();   //保存Dependency和ModuleFactory的对应关系,方便创建该依赖对应的Module
        this.dependencyTemplates = new ArrayMap();   //保存Dependency和Template对应关系,方便生成加载此模块的代码
程序核心处理流程注释

SingleEntryPlugin,MultiEntryPlugin 两个插件中注册了对make事件的监听,当Compiler执行make时,触发对 Compilation.addEntry 方法的调用. 在addEntry方法内调用私有方法_addModuleChain :

Compilation.prototype._addModuleChain = function process(context, dependency, onModule, callback) {
    var start = this.profile && +new Date();

    var errorAndCallback = this.bail ? function errorAndCallback(err) {
        callback(err);
    } : function errorAndCallback(err) {
        err.dependencies = [dependency];
        this.errors.push(err);
        callback();
    }.bind(this);

    if(typeof dependency !== "object" || dependency === null || !dependency.constructor) {
        throw new Error("Parameter 'dependency' must be a Dependency");
    }

    //根据依赖模块的类型获取对应的模块工厂,用于后边创建模块。
    var moduleFactory = this.dependencyFactories.get(dependency.constructor);
    if(!moduleFactory) {
        throw new Error("No dependency factory available for this dependency type: " + dependency.constructor.name);
    }

    //使用模块工厂创建模块,并将创建出来的module作为参数传给回调方法:就是下边`function(err, module)`的参数
    moduleFactory.create(context, dependency, function(err, module) {
        if(err) {
            return errorAndCallback(new EntryModuleNotFoundError(err));
        }

        if(this.profile) {
            if(!module.profile) {
                module.profile = {};
            }
            var afterFactory = +new Date();
            module.profile.factory = afterFactory - start;
        }

        var result = this.addModule(module);

        //result表示该module是否第一次创建
        if(!result) {
            //不是第一次创建
            module = this.getModule(module);

            onModule(module);

            if(this.profile) {
                var afterBuilding = +new Date();
                module.profile.building = afterBuilding - afterFactory;
            }

            return callback(null, module);
        }

        //如果module已缓存过,且不需要rebuild。result是一个Module对象,直接返回该缓存的module
        if(result instanceof Module) {
            if(this.profile) {
                result.profile = module.profile;
            }

            module = result;

            onModule(module);

            moduleReady.call(this);
            return;
        }



        onModule(module);

        //下面要对module进行build了。包括调用loader处理源文件,使用acorn生成AST,将遍历AST,遇到requirt等依赖时,创建依赖(Dependency)加入依赖数组.
        this.buildModule(module, function(err) {
            if(err) {
                return errorAndCallback(err);
            }

            if(this.profile) {
                var afterBuilding = +new Date();
                module.profile.building = afterBuilding - afterFactory;
            }

        //OK,这里module已经build完了,依赖也收集好了,开始处理依赖的module
            moduleReady.call(this);
        }.bind(this));

        function moduleReady() {
            this.processModuleDependencies(module, function(err) {
                if(err) {
                    return callback(err);
                }

                return callback(null, module);
            });
        }
    }.bind(this));
};
递归处理依赖

经过上面buildModule后,程序调用processModuleDependencies开始递归处理依赖的module.:

Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
    var _this = this;
    var start = _this.profile && +new Date();

    var factories = [];
    for(var i = 0; i < dependencies.length; i++) {
        var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
        if(!factory) {
            return callback(new Error("No module factory available for dependency type: " + dependencies[i][0].constructor.name));
        }
        factories[i] = [factory, dependencies[i]];
    }

    //遍历每个依赖模块
    async.forEach(factories, function(item, callback) {

        //这下面跟上面处理_addModuleChain方法类似

        var dependencies = item[1];
        var criticalDependencies = dependencies.filter(function(d) {
            return !!d.critical;
        });
        if(criticalDependencies.length > 0) {
            _this.warnings.push(new CriticalDependenciesWarning(module, criticalDependencies));
        }

        var errorAndCallback = function errorAndCallback(err) {
            err.dependencies = dependencies;
            err.origin = module;
            module.dependenciesErrors.push(err);
            _this.errors.push(err);
            if(bail) {
                callback(err);
            } else {
                callback();
            }
        };
        var warningAndCallback = function warningAndCallback(err) {
            err.dependencies = dependencies;
            err.origin = module;
            module.dependenciesWarnings.push(err);
            _this.warnings.push(err);
            callback();
        };

        var factory = item[0];

        //创建Module
        factory.create(module.context, dependencies[0], function(err, dependentModule) {
            function isOptional() {
                return dependencies.filter(function(d) {
                    return !d.optional;
                }).length === 0;
            }

            function errorOrWarningAndCallback(err) {
                if(isOptional()) {
                    return warningAndCallback(err);
                } else {
                    return errorAndCallback(err);
                }
            }
            if(err) {
                return errorOrWarningAndCallback(new ModuleNotFoundError(module, err));
            }
            if(!dependentModule) {
                return process.nextTick(callback);
            }
            if(_this.profile) {
                if(!dependentModule.profile) {
                    dependentModule.profile = {};
                }
                var afterFactory = +new Date();
                dependentModule.profile.factory = afterFactory - start;
            }

            dependentModule.issuer = module.identifier();
            var newModule = _this.addModule(dependentModule, cacheGroup);

            if(!newModule) { // from cache
                dependentModule = _this.getModule(dependentModule);

                if(dependentModule.optional) {
                    dependentModule.optional = isOptional();
                }

                dependencies.forEach(function(dep) {
                    dep.module = dependentModule;
                    dependentModule.addReason(module, dep);
                });

                if(_this.profile) {
                    if(!module.profile) {
                        module.profile = {};
                    }
                    var time = +new Date() - start;
                    if(!module.profile.dependencies || time > module.profile.dependencies) {
                        module.profile.dependencies = time;
                    }
                }

                return process.nextTick(callback);
            }

            if(newModule instanceof Module) {
                if(_this.profile) {
                    newModule.profile = dependentModule.profile;
                }

                newModule.optional = isOptional();
                newModule.issuer = dependentModule.issuer;
                dependentModule = newModule;

                dependencies.forEach(function(dep) {
                    dep.module = dependentModule;
                    dependentModule.addReason(module, dep);
                });

                if(_this.profile) {
                    var afterBuilding = +new Date();
                    module.profile.building = afterBuilding - afterFactory;
                }

                if(recursive) {
                    return process.nextTick(_this.processModuleDependencies.bind(_this, dependentModule, callback));
                } else {
                    return process.nextTick(callback);
                }
            }

            dependentModule.optional = isOptional();

            dependencies.forEach(function(dep) {
                dep.module = dependentModule;
                dependentModule.addReason(module, dep);
            });

            //build模块
            _this.buildModule(dependentModule, function(err) {
                if(err) {
                    return errorOrWarningAndCallback(err);
                }

                if(_this.profile) {
                    var afterBuilding = +new Date();
                    dependentModule.profile.building = afterBuilding - afterFactory;
                }

                //循环处理此模块的依赖
                if(recursive) {
                    _this.processModuleDependencies(dependentModule, callback);
                } else {
                    return callback();
                }
            });

        });
    }, function(err) {
        if(err) {
            return callback(err);
        }

        return callback();
    });
};

所有模块build完成,开始封装

调用seal方法封装,要逐次对每个module和chunk进行整理,生成编译后的源码,合并,拆分,生成hash。 webpack会根据不同的插件,如MinChunkSizePlugin,LimitChunkCountPlugin 将不同的module整理到不同的chunk里,每个chunk最终对应一个输出文件。此时所有的module仍然保存的是编译前的 原始文件内容。webpack需求将源代码里的require()调用替换成webpack模块加载代码,说白了就是生成最终编译后的代码。

通过Template生成结果代码

生成结果js的调用入口,是compilation类里的createChunkAssets方法:

    //如果是入口,则使用MainTemplate生成结果,否则使用ChunkTemplate.
    if(chunk.entry) {
        source = this.mainTemplate.render(this.hash, chunk, this.moduleTemplate, this.dependencyTemplates);
    } else {
        source = this.chunkTemplate.render(chunk, this.moduleTemplate, this.dependencyTemplates);
    }

Template是用来生成结果代码的。webpack中Template有四个子类:

MainTemplate.js 用于生成项目入口文件

ChunkTemplate.js 用于生成异步加载的js代码

ModuleTemplate.js 用于生成某个模块的代码

HotUpdateChunkTemplate.js

在MainTemplate和ChunkTemplate需要根据依赖的模块,逐个调用ModuleTemplate的render方法。下面分析ModuleTemplate是如何生成每个模块的结果代码的:

ModuleTemplate.prototype.render = function(module, dependencyTemplates, chunk) {
    var moduleSource = module.source(dependencyTemplates, this.outputOptions, this.requestShortener);
    moduleSource = this.applyPluginsWaterfall("module", moduleSource, module, chunk, dependencyTemplates);
    moduleSource = this.applyPluginsWaterfall("render", moduleSource, module, chunk, dependencyTemplates);
    return this.applyPluginsWaterfall("package", moduleSource, module, chunk, dependencyTemplates);
};

第一行module.source()方法即是生成该模块结果代码的方法。source是一个抽象方法,在Module的不同子类里会重写该方法。在子类NormalModule的source方法里,必须把源代码中的require()引入的模块代码替换成 webpack的模块加载代码,完成此功能的代码就是这句:

    //还记得dependencyTemplates是什么吗?就是保存Dependency和Template对应关系,下面这句从获取不同的Dependency.Template实例 
    //如AMDDefineDependency.Template ,AMDRequireContextDependency.Template ,CommonJsRequireDependency.Template 

    var template = dependencyTemplates.get(dep.constructor);
    if(!template) throw new Error("No template for dependency: " + dep.constructor.name);

    //source是一个ReplaceSource,可利用dep参数的range属性定位require调用在源码中的位置,从而实现替换。
    //range: 根据paser:acorn的文档说明,保存了AST节点在源码中的起始位置和结束位置[ start , end ]
    template.apply(dep, source, outputOptions, requestShortener, dependencyTemplates);

比如最终会生成类似以下的代码:

//原始文件内容是:    var kidsico =  require('assets/img/kids.gif') , closeico = require('assets/img/close.gif');
  var kidsico =  __webpack_require__(32) , closeico = __webpack_require__(33); 

最后输出到结果文件

webpack会在Compiler的emitAssets方法里把compilation.assets里的结果写到输出文件里,在此前会先创建输出目录。所有当你要开发一些自定义的 插件要输出一些结果时,把文件放入compilation.assets里即可。

使用acorn生成AST,并遍历AST收集依赖

webpack使用acorn解析每一个经loader处理过的source,并且成AST,然后遍历所有节点,当遇到require调用时,会分析是AMD的还是CMD的调用,或者是require.ensure . 我们不再分析AST的遍历过程了。

对loader的加载和调用

webpack官网对 loader 已经介绍的非常详细了,不再多说。你只需要记住:

webpack在build模块时 (`调用doBuild方法`),要先调用相应的loader对resource进行加工,生成一段js代码后交给acorn解析生成AST.所以不管是css文件,还是jpg文件,还是html模版,
最终经过loader处理会变成一个module:一段js代码。

比如:url-loader,根据loader配置生成一段dataURL或者使用调用loadercontext的emitFile方法向assets添加一个文件。

经典插件

html-webpack-plugin

在HtmlWebpackPlugin里通过 var childCompiler = compilation.createChildCompiler(compilerName, outputOptions)创建了childCompiler, 然后调用childCompiler.compile方法进行编译, 使得HtmlWebpackPlugin也可以使用webpack的loader机制,如html-loader,handlebar-loader等等来处理template.最后从compilation对象中取出chunk和css注入到html 的head或者body里。

extract-text-webpack-plugin

extract-text-webpack-plugin 被用来抽取css样式到独立的文件,方便页面引用,因此必须配合css-loader使用。 ExtractTextPlugin.extract("style-loader", "css-loader?sourceMap!cssnext-loader") 这样的loader配置,第一个before参数style-loader会被省略掉不参与loader处理,真正 起作用的是第二个参数css-loader?sourceMap!cssnext-loader ,所以配置成ExtractTextPlugin.extract("css-loader?sourceMap!cssnext-loader")也可以。

首先在资源build的时候,使用ExtractTextPlugin的loader将创建一个childCompiler(类似html-webpack-plugin)对css(或者sass,)文件重新进行编译,将编译结果记录在module的meta数组里。原来的位置替换成一行注释:// removed by extract-text-webpack-plugin 编译完成后,在优化chunk的时候 ( optimize-tree 事件触发 )将每个module的meta数组取出来生成独立的css文件。

CommonsChunkPlugin

这个插件用来提取公共的module到独立的chunk文件里。如果只有一个entry是没必须用这个插件 。当有多个entry,可能每个entry有一些公共依赖的module。此时CommonsChunkPlugin会把这些公共的module提取 到独立的文件中。https://webpack.github.io/docs/list-of-plugins.html#commonschunkplugin有详细的介绍

UglifyJsPlugin

在optimize-chunk-assets时,将每个chunk逐一uglify一把,然后再输出结果文件。

← 遇到一个jdk建立SSL连接时的坑:Reverse DNS lookup during SSL handshake

webpack 源码解析→