阅读 2424

看看webpack都打出了些什么

最近在看webpack的原理,觉得可以分为两个方面来完成:

  • 了解webpack打包出来的文件。
  • 了解webpack流程并且自己写loader和plugin。

当然看源码是可以的,但是有点事倍功半并且没有必要,个人觉得完成以上两部分就可以对webpack有不错的了解了。本文主要关于webpack打包出来的文件的内容【希望能够提出不对或者可以补充的地方,感觉说的不是很清晰,欢迎指tu正cao】。

配置以及待打包文件如下:

// webpack.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
    entry: {
        bundle1: path.resolve(__dirname, 'src/index1.js'),
        bundle2: path.resolve(__dirname, 'src/index2.js')
    },
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    plugins: [
        new webpack.optimize.CommonsChunkPlugin({
            name: 'manifest'
        })
    ]
};
复制代码
// index1.js
const test1 = require('./test1');
const test3 = require('./test3')
console.log(test1);
console.log(test3);

// test1.js
const str = 'test1 is loaded';
module.exports = str;

// test3.js
const str = 'test3 is loaded';
module.exports = str;

// index2.js
setTimeout(function() {
    require.ensure([], function() {
        const test2 = require('./test2');
        console.log(test2);
    });
}, 5000);

// test2.js

const str = 'test2 is async loaded';
module.exports = str;
复制代码

module和chunk

首先了解module和chunk的概念:

  • module 其实就是打包前,import 或者 require 的js 文件,如test1.js 与 index1.js。
  • chunk 是打包后的文件,即 bundle1.js、bundle2.js、0.js和manifest.js文件,这里需要注意 一个 chunk 可能包含若干 module

三个核心的方法

打包结果文件(简化版本):

// manifest.js
(function(modules) { // webpackBootstrap
	// install a JSONP callback for chunk loading
	var parentJsonpFunction = window["webpackJsonp"];
	window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
		// add "moreModules" to the modules object,
		// then flag all "chunkIds" as loaded and fire callback
		var moduleId, chunkId, i = 0, resolves = [], result;
		for(;i < chunkIds.length; i++) {
			chunkId = chunkIds[i];
			if(installedChunks[chunkId]) {
				resolves.push(installedChunks[chunkId][0]);
			}
			installedChunks[chunkId] = 0;
		}
		for(moduleId in moreModules) {
			if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
				modules[moduleId] = moreModules[moduleId];
			}
		}
		if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
		while(resolves.length) {
			resolves.shift()();
		}
		if(executeModules) {
			for(i=0; i < executeModules.length; i++) {
				result = __webpack_require__(__webpack_require__.s = executeModules[i]);
			}
		}
		return result;
	};
	// The module cache
	var installedModules = {};
	// objects to store loaded and loading chunks
	var installedChunks = {
		3: 0
	};
	// 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__);
		// Flag the module as loaded
		module.l = true;
		// Return the exports of the module
		return module.exports;
	}
	// This file contains only the entry chunk.
	// The chunk loading function for additional chunks
	__webpack_require__.e = function requireEnsure(chunkId) {
		var installedChunkData = installedChunks[chunkId];
		if(installedChunkData === 0) {
			return new Promise(function(resolve) { resolve(); });
		}
		// a Promise means "currently loading".
		if(installedChunkData) {
			return installedChunkData[2];
		}
		// setup Promise in chunk cache
		var promise = new Promise(function(resolve, reject) {
			installedChunkData = installedChunks[chunkId] = [resolve, reject];
		});
		installedChunkData[2] = promise;
		// start chunk loading
		var head = document.getElementsByTagName('head')[0];
		var script = document.createElement('script');
		script.type = 'text/javascript';
		script.charset = 'utf-8';
		script.async = true;
		script.timeout = 120000;
		if (__webpack_require__.nc) {
			script.setAttribute("nonce", __webpack_require__.nc);
		}
		script.src = __webpack_require__.p + "" + chunkId + ".js";
		var timeout = setTimeout(onScriptComplete, 120000);
		script.onerror = script.onload = onScriptComplete;
		function onScriptComplete() {
			// avoid mem leaks in IE.
			script.onerror = script.onload = null;
			clearTimeout(timeout);
			var chunk = installedChunks[chunkId];
			if(chunk !== 0) {
				if(chunk) {
					chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
				}
				installedChunks[chunkId] = undefined;
			}
		};
		head.appendChild(script);
		return promise;
	};
})
([]);
复制代码

manifest.js 先运行注入了一些方法,下面三个是最核心的方法:

  • webpackJsonp
  • webpack_require
  • webpack_require.e

这里只大致的说下大致的作用以及重点的部分。

webpackJsonp方法,接受三个参数chunkIds, moreModules, executeModules。这里要分清楚chunk id和module id,chunk id指的是一个打包后文件的标示,而module id是每个打包前的module的唯一标示也就是id。这里需要分别用来表示各个chunk和module以及在之后的缓存过程中使用到。

chunkIds指的是这个chunk文件加载后需要被加载的chunk id的数组,所以默认会有自身chunk的id。如果有这个chunk会用到的module打包到的chunk需要被预加载的话,对应的chunk的id也会在chunkIds中。

moreModules指的是这个chunk加载后带来的module的数组,其中的每个module被以函数的形式包裹实现作用域上的隔离,其实和node的模块加载的机制很像。

以bundle1.js为例

webpackJsonp([1],[
/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const test1 = __webpack_require__(1);
const test3 = __webpack_require__(2)

console.log(test1);
console.log(test3);

/***/ }),
/* 1 */
/***/ (function(module, exports) {

const str = 'test1 is loaded';

module.exports = str;

/***/ }),
/* 2 */
/***/ (function(module, exports) {

const str = 'test3 is loaded';

module.exports = str;

/***/ })
],[0]);
复制代码

chunkIds是[1],moreModules是中间的数组参数,executeModules是[0](这里要分清楚,[1]中的1是chunk id,而[0]中的0是module id 指的是需要被执行的module的id这里指的就是打包之前的index.js文件)这里运行了bundle1.js相当于执行了打包之前的index1.js文件。

moreModules数组中的元素举个栗子:

/* 0 */
/***/ (function(module, exports, __webpack_require__) {

const test1 = __webpack_require__(1);
const test3 = __webpack_require__(2)

console.log(test1);
console.log(test3);

/***/ })
复制代码

每个module接收三个参数,第三个参数可选(取决于该module是否依赖其他module,稍后说明)其中的module和exports是在开发时的模块导出中经常遇到的。我们在导出一个模块的时候的操作:

module.exports = balabala;
复制代码

Javascript的函数的参数传递是按值来传递的。在函数执行的stack中当变量的类型是对象(引用类型值)时储存的是这个对象的地址,真正的对象存储在这个地址指向的堆(heap)中。函数的参数在按值传递情况下当参数类型是对象的时候传递的是对象的地址,这样操作的时候指向的是heap中的同一个对象。

所以也就是变相的将module中export的内容挂载到了module.export对象上了。在打包后的代码可以看到,我们只要执行moreModules数组中对应的元素的函数,就能够变相的将这个module想要export的内容挂载到输入到函数的module的export对象上。

在介绍完module的接收的三个参数后。我们可以看到函数内部当需要引用某个module的时候,会调用__webpack_require__方法参入对应的module的id。

// 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__);
	// Flag the module as loaded
	module.l = true;
	// Return the exports of the module
	return module.exports;
}
复制代码

__webpack_require__方法比较简单,其实就是传入moduleId首先判断下installedModules中是否有缓存(也就是之前加载过),有的话直接返回输出的内容,没有的话,就执行modules[moduleId].call(module.exports, module, module.exports, webpack_require);将module输出的内容挂载到module.exports对象上,同时缓存到installedModules中,结果就是:

每个module只会在最开始依赖到的时候加载一次,之后会从installedModules直接获取不在加载。如果module依赖的module继续依赖其他module的话,上述的过程会递归的执行下去,但是加载过的依赖值会加载一次。

这里可以看到如果依赖的module被打包到独立的chunk,并且这个chunk还没有被执行的话,这个时候modules[moduleId]就是undefined了。我觉得这个时候就和webpackJsonp的第一个参数有关了,也就是这个chunk依赖的chunk的id需要在chunkIds参数中。这里牵扯到另一个概念“循环依赖”的处理,这个打算之后再另一篇文章中专门介绍。

简单来说__webpack_require__的作用就是加载执行对应的module,并且缓存起来。

在了解了__webpack_require__后,回头看下每个chunk都有的IIFE的对应的webpackJsonp方法,webpackJsonp做的事情其实简单来说:

  • 标记了每个chunk是否加载过了。
  • 缓存了每个加载的chunk带来的module到modules对象中。
  • 将需要异步加载的chunk的回调(promise的resolve)统一收集并且执行。(这部分具体的过程感觉理解的还不是很透彻)
  • 按顺序加载executeModules中的module,加载的过程就已经执行了对应的module,返回最后的module的执行结果。

这里还需要注意**webpack_require.e**方法,这个方法对应的是require.ensure方法,这个方法的作用是加载chunk,也就是对应的module会被打包成独立的chunk。在执行require.ensure回调中的方法的时候之前会下载对应的chunk,从而实现chunk的按需加载。

从__webpack_require__.e的代码中可以看到,ta大致的思想是:判断对应的chunk是否已经加载过了,如果已经加载过了,就return一个resolve了的promise,然后执行对应的回调函数的内容。如果chunk没有加载过则用动态添加script标签的方式加载对应的chunk(所以这里的方法名叫做webpackJsonp也是有道理的,异步加载的方式和jsonp的方法有类似的地方)。然后标注chunk已加载,这里还对如果chunk下载失败的情况下抛错警告chunk加载失败。

总结

至此算是大致的介绍了webpack打包后文件的内容,可以看到webpack打包后的文件,自己实现了一套模块加载的机制,这样方便实现比如代码分割等功能。

在了解了webpack的打包之后文件的结构,我们并知道webpack的打包过程依靠不同 loader 和 plugin 组合来进行。loader负责将不同类型的文件进行转换成js类型的数据,plugin在打包过程中注册对应的事件,webpack在对应的阶段执行不同插件来实现对文件的处理。

在知道了这些之后,实现loader和plugin来真正的使用webpack。

内部分享的时候的 Slide

参考资料: