wiseowner/blog

Webpack编译原理

garinghu opened this issue · 0 comments

从bundle文件分析webpack都做了什么

/******/ (function(modules) { // webpackBootstrap
/******/    // The module cache
/******/    var installedModules = {};
/******/
/******/    // The require function
/******/    function __webpack_require__(moduleId) {
/******/
/******/        // Check if module is in cache
/******/        if(installedModules[moduleId]) {
/******/            return installedModules[moduleId].exports;
/******/        }
/******/        // Create a new module (and put it into the cache)
/******/        var module = installedModules[moduleId] = {
/******/            i: moduleId,
/******/            l: false,
/******/            exports: {}
/******/        };
/******/
/******/        // Execute the module function
/******/        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/        console.log(module)
/******/        // Flag the module as loaded
/******/        module.l = true;
/******/
/******/        // Return the exports of the module
/******/        return module.exports;
/******/    }
/******/
/******/
/******/    // expose the modules object (__webpack_modules__)
/******/    __webpack_require__.m = modules;
/******/
/******/    // expose the module cache
/******/    __webpack_require__.c = installedModules;
/******/
/******/    // define getter function for harmony exports
/******/    __webpack_require__.d = function(exports, name, getter) {
/******/        if(!__webpack_require__.o(exports, name)) {
/******/            Object.defineProperty(exports, name, {
/******/                configurable: false,
/******/                enumerable: true,
/******/                get: getter
/******/            });
/******/        }
/******/    };
/******/
/******/    // getDefaultExport function for compatibility with non-harmony modules
/******/    __webpack_require__.n = function(module) {
/******/        var getter = module && module.__esModule ?
/******/            function getDefault() { return module['default']; } :
/******/            function getModuleExports() { return module; };
/******/        __webpack_require__.d(getter, 'a', getter);
/******/        return getter;
/******/    };
/******/
/******/    // Object.prototype.hasOwnProperty.call
/******/    __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/    // __webpack_public_path__
/******/    __webpack_require__.p = "";
/******/
/******/    // Load entry module and return exports
/******/    return __webpack_require__(__webpack_require__.s = 0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports, __webpack_require__) {
    let world = __webpack_require__(1);
    function sayHello(){
        console.log('hello')
    }
    world();
    // world.sayWorld();
    
    /***/ }),


    /* 1 */
    /***/ (function(module, exports) {
    
    function world(){
        console.log('world');
    }
    module.exports = world;
    
    /***/ })
    /******/ ]);

webpack构建的构建流程

webpack构建构建流程可以分为以下三个阶段

  • 初始化:启动构建,读取与合并配置参数,加载plugin,实例化compiler
  • 编译:从entry出发,针对每个module串行调用对应的loader去翻译文件的内容,再找到该module依赖的module,递归地进行编译处理(与其说module是文件,不如说是“依赖”比较准确)
  • 输出:将编译后的module组合成chunk,将chunk转换成文件,输出到文件系统中(一个chunk包含多个module)

如果只执行一次构建,则以上阶段将会按照顺序各执行一次,但在开启监听模式下,流程将如图所示

Alt text

这里解释一下几个重要概念

  • module:每个文件(打包前)为一个module,例如上面hello.js引用world.js,这两个文件都为module,只不过hello.js是入口文件而已
  • chunk:正常情况下,一个入口会对应一个出口,最后生成的文件即为一个chunk,当然代码分割产生对的文件也为一个chunk,可以通过引入webpack-bundle-analyzer插件分析最终产生的bundle的结构

编译阶段发生的事件

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

  • run:启动一次新的编译
  • compile:该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上compiler对象
  • compilation:当webpack以开发模式运行时,每当检测到文件的变化,便有一次compilcation被创建,一个compilation对象包含了当前的模块资源,编译生成资源,变化的文件等。compilation提供了很多事件回掉给插件进行扩展
  • make:一个新的compilation创建完毕,即将从entry开始读取文件,根据文件的类型和配置的loader对文件进行编译,编译完后再找出该文件依赖的文件,递归地编译和解析

在编译阶段中,最重要的事件是compilation,因为在compilation阶段调用了loader,完成了每个模块的转换操作。在compilation阶段又会发生很多子事件

  • build-module:使用对应的loader去转换一个模块

  • normal-module-loader:用loader转换完一个模块后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便webpack在后面对代码进行分析
    Alt text

  • seal:依赖模块通过loader转换完成,根据依赖关系生成chunk

由此可以看出webpack在编译的阶段的任务是根据打包前的代码生成module并且捋顺依赖关系,其中生成module用工厂模式实现
Alt text
Alt text

编译阶段的具体实现

这里引入了依赖(Dependency)的概念,每一个依赖都包含一个module,指向被依赖的module,这里可以理解成类似链表或有向图

从make事件开始

在创建 module 之前,Compiler 会触发 make,并调用 Compilation.addEntry 方法,通过 options 对象的 entry 字段找到我们的入口js文件。之后,在 addEntry 中调用私有方法 _addModuleChain ,这个方法主要做了两件事情。一是根据模块的类型获取对应的模块工厂并创建模块,二是构建模块。

  • 根据依赖模块的类型获取对应的模块工厂,用于后边创建模块。
var moduleFactory = this.dependencyFactories.get(dependency.constructor);
	if(!moduleFactory) {
		throw new Error("No dependency factory available for this dependency type: " + dependency.constructor.name);
	}
  • 使用模块工厂创建模块,并将创建出来的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;
		}
  • 对module进行build了。包括调用loader处理源文件,使用acorn生成AST
this.buildModule(module, function(err) {
			if(err) {
				return errorAndCallback(err);
			}

			if(this.profile) {
				var afterBuilding = +new Date();
				module.profile.building = afterBuilding - afterFactory;
			}
            
        //这里module已经build完了,依赖也收集好了,开始处理依赖的module
			moduleReady.call(this);
		}.bind(this));

webpack分析依赖是基于ast(抽象语法树)的,其中通过调用acorn解析经loader处理后的源文件http://esprima.org/demo/parse.html#

通过遍历ast即可找到下一个需要构建的模块,形成依赖

Parser.prototype.parse = function parse(source, initialState) {
   var ast;
   if(!ast) {
       // acorn以es6的语法进行解析
       ast = acorn.parse(source, {
           ranges: true,
           locations: true,
           ecmaVersion: 6,
           sourceType: "module"
       });
   }
     ...
 };
  • 对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。
Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) {
    // 根据依赖数组(dependencies)创建依赖模块对象
    var factories = [];
    for(var i = 0; i < dependencies.length; i++) {
        var factory = _this.dependencyFactories.get(dependencies[i][0].constructor);
        factories[i] = [factory, dependencies[i]];
    }
        ...
      // 与当前模块构建步骤相同
  }