webpack 历险记

1,217 阅读8分钟
原文链接: zhuanlan.zhihu.com

基本概念

1.chunk: 被打包出来的文件,装载了module

2.module: 具有一定功能的模块

3.loader: 是处理各个类型资源的转化器

4.plugin: 可以触及整个打包流程,去实现一些特殊的功能


打包结果

一个模块


我们先来看下一个打包后的文件内容 :

(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] = {
			exports: {},
			id: moduleId,
			loaded: false
		};

		// Execute the module function
		modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

		// Flag the module as loaded
		module.loaded = 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;

	// __webpack_public_path__
	__webpack_require__.p = "";

	// Load entry module and return exports
	return __webpack_require__(0);
})
([
/* 0 */
 function(module, exports) {

	alert(1);

 }
]);

这是一个立即执行函数,接受一个 `modules ` 参数,是一个数组,存放所有的模块,这些模块按照一定顺序排列,其索引作为其标示。


然后函数开始执行,`installedModules` 用来存放已经加载过的模块,用来作为缓存,然后定义了一个函数 `__webpack_require__` 下面就调用了这个函数,并传入参数0。


我们看下这个函数


先从缓存拿模块,没有的话,定义一个module,结构为

 {
 exports: {},
 id: moduleId,// id 就是modules数组的索引值
 loaded: false
}

然后执行 modules[moduleId] 这个模块,也就是上面的第一个函数,把`module`, `module.exports`, `__webpack_require__`,这3个参数传入,这样我们的模块就能通过`exports`导出内容,使用`require` 去加载模块。这样我们就执行了模块,拿到结果。不管是es6还是jsx,最终都会被相应的loader转化成接受这3个参数的一个函数。


多个模块


现在只有一个模块,如果我在入口文件引入了另一个文件呢?那么变化的只有`modules`参数,看下:

[function (module, exports, __webpack_require__) {
    var module1=__webpack_require__(1);
    alert(module1.value);
}, function (module, exports) {
    var value = 1;
    exports.value = value;
}]

在第一个模块执行的时候,会去require第2个模块。

公共提取


下面我们来看下公共提取工的作机制,如果使用 CommonsChunkPlugin 插件来提取公共模块,那么打包后的文件会被拆分,那么是这个机制又是怎么样的呢?


比如我们的2个入口文件 index.js 和 index1.js, 都用到来一个模块,这个模块被打到了common.js 文件,看下这个文件内容

(function (modules) { // webpackBootstrap
    // install a JSONP callback for chunk loading
    var parentJsonpFunction = window["webpackJsonp"];
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
        // add "moreModules" to the modules object,
        // then flag all "chunkIds" as loaded and fire callback
        var moduleId, chunkId, i = 0, callbacks = [];
        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];
            if (installedChunks[chunkId])
                callbacks.push.apply(callbacks, installedChunks[chunkId]);
            installedChunks[chunkId] = 0;
        }
        for (moduleId in moreModules) {
            modules[moduleId] = moreModules[moduleId];
        }
        if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
        while (callbacks.length)
            callbacks.shift().call(null, __webpack_require__);
        if (moreModules[0]) {
            installedModules[0] = 0;
            return __webpack_require__(0);
        }
    };
    // The module cache
    var installedModules = {};
    // object to store loaded and loading chunks
    // "0" means "already loaded"
    // Array means "loading", array contains callbacks
    var installedChunks = {
        2: 0
    };
    // The require function
    function __webpack_require__(moduleId) {
        // 已经介绍过 ,这里先省略
    }
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = function requireEnsure(chunkId, callback) {
        // 按需加载需要,这里先省略
    };
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = modules;
    // expose the module cache
    __webpack_require__.c = installedModules;
    // __webpack_public_path__
    __webpack_require__.p = "";
})([, function (module, exports) {

    var value = 1;
    exports.value = value;

}]);

然后是index.js

webpackJsonp([0],[function(module, exports, __webpack_require__) {
    var module1=__webpack_require__(1);
    alert(module1.value);
 }]);

index1.js

webpackJsonp([1],[function(module, exports, __webpack_require__) {
    var module1=__webpack_require__(1);
    alert(module1.value);
}]);

当我们的代码执行的时候,先加载common.js文件,我们先自己看下modules参数,这个数组其实有2个值,第一个值是空的,我们的公用模块是第2个,第一个值为2个入口文件的初始模块留坑。


这里主要定义了`webpackJsonp` 函数,并没有实际运行什么模块代码。

当加载index.js文件的时候,开始调用`webpackJsonp`,他接受2个参数 `chunkIds`和`moreModules`,


首先处理`chunkIds` 相关,这个在下一节按需加载分析,这里最后调用了`__webpack_require__(0)`,去加载入口模块,然后在这个模块调用了`__webpack_require__(1)`,这里的处理逻辑就清晰了,2个入口文件根据其chunkId的分别是chunk0和chunk1,他们的入口模块id都是0,公用的模块的id是1,这样就能保证2个chunk文件中能通过模块id找到被独立出来的common模块了。


按需加载


我们可以通过 `require.ensure` 来实现按需加载,这个函数就是上文省略的


__webpack_require__.e = function requireEnsure(chunkId, callback) {
        // "0" is the signal for "already loaded"
        if (installedChunks[chunkId] === 0)
            return callback.call(null, __webpack_require__);
        // an array means "currently loading".
        if (installedChunks[chunkId] !== undefined) {
            installedChunks[chunkId].push(callback);
        } else {
            // start chunk loading
            installedChunks[chunkId] = [callback];
            var head = document.getElementsByTagName('head')[0];
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.charset = 'utf-8';
            script.async = true;
            script.src = __webpack_require__.p + "" + chunkId + "." + ({ "0": "index"}[chunkId] || chunkId) + ".js";
            head.appendChild(script);
        }
    };

我们来回忆上上文中有一个installedChunks 数组来保存chunks的加载情况,0标示已经加载,数组标示正在加载。

这里的代码切割逻辑和上文的公共提取类似,会把要被异步加载的模块单独成文件,内容像这样:

webpackJsonp([1],[
/* 0 */,
/* 1 */
 function(module, exports) {
	var value = 1;
    exports.value = value;
 }
]);

我们的主文件中的modules则变成了这样:

[function(module, exports, __webpack_require__) {
    __webpack_require__.e(1, function(require) {
      var module1 = __webpack_require__(1);
	  alert(module1.value)
    });
}])];

ps:这个模块需要用到的module1现在还没有,在另一个未被加载的文件里面。

文件开始执行,调用`__webpack_require__(0)`,然后是` __webpack_require__.e`,这个函数先检测这个chunk是否被加载,没有的话,先把模块,也就是上面的回调函数放到installedChunks,注意其chunkid是1(ps:chunkid和moduleid不要搞混),然后会通过script标签的形式去加载这chunk文件,然后`webpackJsonp`被调用,然后看按需加载的`webpackJsonp`函数,和上面的有所区别:

window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules) {
	var moduleId, chunkId, i = 0, callbacks = [];
	for(;i < chunkIds.length; i++) {
		chunkId = chunkIds[i];
		if(installedChunks[chunkId])
			callbacks.push.apply(callbacks, installedChunks[chunkId]);
		installedChunks[chunkId] = 0;
	}
	for(moduleId in moreModules) {
		modules[moduleId] = moreModules[moduleId];
	}
	if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);
	while(callbacks.length)
		callbacks.shift().call(null, __webpack_require__);
}

这个函数内部先是拿到了上面注册的chunk1的模块的callback,然后注册这个chunk的module,最后执行callbeck,因为module1被加载了,所以代码终于可以跑起来了。

打包流程


上面我们看了生成的结果文件,那么现在我们来看根据一个配置文件,来把我们的资源文件进行打包构建的具体流程。


&amp;amp;amp;amp;lt;img src="https://pic1.zhimg.com/v2-18bc1cb51e185a78950a7cf311692370_b.jpg" data-rawwidth="4436" data-rawheight="4244" class="origin_image zh-lightbox-thumb" width="4436" data-original="https://pic1.zhimg.com/v2-18bc1cb51e185a78950a7cf311692370_r.jpg"&amp;amp;amp;amp;gt;

tapable

webpack的整体流程是基于插件架构的,而提供整个插件机制的是 tapable,他通过订阅和发布事件来注册和执行各个插件,在 这里,我们可以看到webpack内部使用了大量的插件。下文的Compiler就是tapable的实例。22


Compiler


Compiler 是 webpack 的主要引擎,webpack 通过实例化 compiler,然后调用它的 run 方法来使用它。

既然他是主流程,那么我们来看下他的主要事件流,就能大致了解整个打包流程.(来自webpack2)

  • entry-option 参数处理

  • after-plugins 设置插件的初始配置后
  • after-resolvers 设置解析器后

  • environment 环境配置
  • after-environment 环境配置完成

  • before-run 编译之前
  • run 开始编译

  • watch-run 监视后开始编译之前
  • normal-module-factory 创建 NormalModuleFactory 后

  • context-module-factory 创建 ContextModuleFactory 后
  • before-compile 编译参数创建完成

  • compile 创建新编译之前
  • this-compilation 发射 compilation 事件之前

  • compilation 编译创建完成
  • make 从entry开始递归分析依赖,构建模块

  • after-compile 编译之后
  • should-emit 此时可以返回 true/false ,来判断是否生成文件

  • emit 生成文件之前
  • after-emit 生成文件结束

  • done 整体流程结束

Compilation


Compilation实例继承于compiler,这个对象主要负责编译流程。在编译阶段,模块被加载,封闭,优化,分块,哈希和重建等。以下是这个对象的主要方法

  • templatesPlugin

  • addModule

  • getModule

  • findModule
  • buildModule

  • processModuleDependencies

  • addModuleDependencies

  • _addModuleChain
  • addEntry

  • prefetch

  • rebuildModule

  • seal
  • sortModules

  • addChunk

  • processDependenciesBlockForChunk

  • removeChunkFromDependencies
  • applyModuleIds

  • applyChunkIds

  • sortItems

  • summarizeDependencies
  • createHash

  • modifyHash

  • createModuleAssets

  • createChunkAssets
  • getPath

  • getStats

  • createChildCompiler

流程大概是调用 addEntry 找到入口文件,然后调用_addModuleChain ,获得相应 moduleFactory ,开始构建模块,构建模块流程比较复杂,其中的步骤有

* 调用各 loader 处理模块

* 使用 acorn 解析js文件,得到 AST 然后拿到依赖模块,调用addDependency方法添加

* 不断重复以上流程,来递归到构建模块


流程中处理的的核心对象就是module


以下是一次构建流程中一个module对象的key值:

['dependencies','blocks','variables','context','reasons','debugId','lastId','id','index','index2','chunks','warnings','dependenciesWarnings','errors','dependenciesErrors','request','userRequest','rawRequest','parser','resource','loaders','fileDependencies','contextDependencies','error','_source','meta','assets','built','_cachedSource','issuer','optional','building','buildTimestamp','cacheable' ]

其中几个关键的

  • dependencies 模块依赖

  • context 模块文件的上下文

  • chunks 此模块所在的chunks

  • request 加载此模块的完整请求,包括loader及其参数
  • resource 该模块对应的资源文件

  • loaders 解析该模块的loader

  • fileDependencies 文件依赖,包括自己

  • issuer 模块调用方
  • _source 模块内容

输出文件


等到构建结束以后,就要输出文件了。webpack会对构建结果整理分析,在将结果拆分,合并。

主要流程是

  • 对chunk进行排序

  • 遍历chunk,为每个chunk整理其module,为module添加chunk

  • 进行各种优化

  • 调用`createChunkAssets` 方法,这里会根据每个 chunk 的类型找到相应到文件 template 进行 render,最后在根据用户的配置来输出最终的文件,也就是上面打包结果章节的内的文件。


loader


loader就是一个函数,接受字符串内容,返回转化后的内容,一个loader只做一件事,多个loader串行执行调用来完成资源转化。比如一个最简单的loader,raw-loader内容如下:

module.exports = function(content) {
	this.cacheable && this.cacheable();
	this.value = content;
	return "module.exports = " + JSON.stringify(content);
}

plugin

webpack给予插件触及webpac各个流程的能力,他痛殴一个apply方法来安装到webpack中,写法如:

function MyPlugin(options) {
  // Configure your plugin with options...
}

MyPlugin.prototype.apply = function(compiler) {
  compiler.plugin("compile", function(params) {
    console.log("The compiler is starting to compile...");
  });

  compiler.plugin("compilation", function(compilation) {
    console.log("The compiler is starting a new compilation...");

    compilation.plugin("optimize", function() {
      console.log("The compilation is starting to optimize files...");
    });
  });

  compiler.plugin("emit", function(compilation, callback) {
    console.log("The compilation is going to emit files...");
    callback();
  });
};

module.exports = MyPlugin;