webpack 是一个现代的 JavaScript 应用程序的模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图表(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成少量的 bundle - 通常只有一个,由浏览器加载。
webpack作为一个模块打包工具,不仅可以打包js文件,还可以打包其他非js文件。下文仅针对js文件,从 CommonJS
模块和 ES6
模块两种处理模块的方式对 webpack 的打包机制进行解释。
以下过程均在webpack 2.5.1下测试
CommonJS 模块打包 webpack.config.js
1 2 3 4 5 6 7 8 9 10 const path = require ('path' )module .exports = { entry : './src/main.js' , output : { path : path.resolve (__dirname, 'dist' ), filename : 'bundle.js' } }
main.js
1 2 3 4 var test1 = require ('./test1.js' );test1 ();
test1.js
1 2 3 4 5 6 7 console .log ('test1 is loaded' );var test2 = require ('./test2.js' );module .exports = function ( ) { console .log ('this is in test1 exports function' ); };
test2.js
1 2 3 4 5 6 console .log ('test2 is loaded' );module .exports = function ( ) { console .log ('this is in test2 exports function' ); };
bundle.js (模块启动函数说明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 (function (modules ) { })( [ (function (module , exports , __webpack_require__ ) { }), (function (module , exports , __webpack_require__ ) { }), (function (module , exports ) { }) ] );
bundle.js
文件即为启动 webpack 命令打包后的文件。其对应了一个立即执行函数,modules
参数为一个数组,通过外部传入一个数组,数组中每一项对应了一个函数,被称之为模块启动函数。webpack 在打包时,将每一个被引用的js模块包装成这样的函数传入 modules
数组,形成一个相关联的模块列表传入 modules
。
bundle.js (打包机制说明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 (function (modules ) { var installedModules = {}; function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports ; } var module = installedModules[moduleId] = { i : moduleId, l : false , exports : {} }; modules[moduleId].call (module .exports , module , module .exports , __webpack_require__); module .l = true ; return module .exports ; } __webpack_require__.m = modules; __webpack_require__.c = installedModules; __webpack_require__.i = function (value ) { return value; }; __webpack_require__.d = function (exports , name, getter ) { if (!__webpack_require__.o (exports , name)) { Object .defineProperty (exports , name, { configurable : false , enumerable : true , get : getter }); } }; __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; }; __webpack_require__.o = function (object, property ) { return Object .prototype .hasOwnProperty .call (object, property); }; __webpack_require__.p = "" ; return __webpack_require__ (__webpack_require__.s = 1 ); })([]);
installedModules
对象对应了已加载过的模块列表,通过模块 id 作为其唯一属性存储模块的相关信息。每一个模块信息包含了该模块的模块 id,模块是否被加载过,模块 exports 对象。由下文的 _webpack_require_ 函数可知,当前模块被加载过后,再次引用该模块时,该模块不会被重新加载,仅仅是使用该模块的exports对象。
_webpack_require_ 函数是 webpack 处理模块关联的关键。其内部首先根据模块id去 `installedModules 列表中查看该 id 对应的模块是否被加载过,如果已加载过直接返回该模块的 exports 对象。如果未曾加载,则构建这样的一个模块对象,并去根据该模块 id 去调用模块启动函数。然后将该模块的是否加载标志位置为 true ,并返回该模块的 exports 对象。
m,c,i,d,n,o,p,s对应了 _webpack_require_ 的一些工具函数和变量,用于模块启动函数的一些操作。
bundle.js (模块启动函数说明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 [ (function (module , exports , __webpack_require__ ) { console .log ('test1 is loaded' ); var test2 = __webpack_require__ (2 ); module .exports = function ( ) { console .log ('this is in test1 exports function' ); }; }), (function (module , exports , __webpack_require__ ) { var test1 = __webpack_require__ (0 ); test1 (); }), (function (module , exports ) { console .log ('test2 is loaded' ); module .exports = function ( ) { console .log ('this is in test2 exports function' ); }; }) ]
在立即启动函数中最后一句中,通过 _webpack_require_ 函数调用了在modules数组中下标为1的模块启动函数,moduleId
为1对应的模块启动函数,即在 webpack.config.js
中配置的入口文件,在本例中即 main.js
对应的模块启动函数。在入口文件中又通过 _webpack_require_ 函数按照模块引用关系递归调用其他模块,并将模块内部的 exports 对象赋值给该模块对应的 exports 属性,用于模块的缓存。
ES6模块 注意:在 webpack2 中对于 ES6
的模块语法是开箱即用的,如果您还是 webpack1.*版本,请配置对应的 loader 处理 ES6
的模块语法。
webpack.config.js
1 2 3 4 5 6 7 8 9 10 const path = require ('path' )module .exports = { entry : './src/main.js' , output : { path : path.resolve (__dirname, 'dist' ), filename : 'bundle.js' } }
main.js
1 2 3 4 import {test1} from './test1.js' ;test1 ();
test1.js
1 2 3 4 5 6 7 8 9 10 11 console .log ('test1 is loaded' );import {test2} from './test2.js' ;function test1 ( ) { console .log ('this is in test1 export function' ); } export {test1};
test2.js
1 2 3 4 5 6 7 8 9 console .log ('test2 is loaded' );function test2 ( ) { console .log ('this is in test2 export function' ); } export {test2};
相比于 CommonJS
模块,webpack 对 ES6
模块的处理的不同在于对 ES6
模块包装后的模块启动函数。
bundle.js (模块启动函数说明)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 [ (function (module , __webpack_exports__, __webpack_require__ ) { "use strict" ; __webpack_require__.d (__webpack_exports__, "a" , function ( ) { return test1; }); var __WEBPACK_IMPORTED_MODULE_0__test2_js__ = __webpack_require__ (2 ); console .log ('test1 is loaded' ); function test1 ( ) { console .log ('this is in test1 export function' ); } }), (function (module , __webpack_exports__, __webpack_require__ ) { "use strict" ; Object .defineProperty (__webpack_exports__, "__esModule" , { value : true }); var __WEBPACK_IMPORTED_MODULE_0__test1_js__ = __webpack_require__ (0 ); __webpack_require__.i (__WEBPACK_IMPORTED_MODULE_0__test1_js__["a" ])(); }), (function (module , __webpack_exports__, __webpack_require__ ) { "use strict" ; console .log ('test2 is loaded' ); function test2 ( ) { console .log ('this is in test2 export function' ); } }) ]
ES6
的模块启动函数列表中同样将下标为1也就是 moduleId
为1的模块(即入口模块)作为启动模块,与 CommonJS
模块的模块启动函数不同,ES6
模块的启动函数参数中,_webpack_exports_ 对象替换了 exports 对象,不过与 exports 对象一样,他们都指向 module.exports 对象。在入口模块的模块启动函数中,首先定义了 module.exports 对象中的 __esModule 属性为true,然后按照引用关系依次递归调用其他模块。此处需要注意对 ES6 的模块处理中稍有不同的是,webpack 将模块导出的对象名作为一个a属性添加在了 module.exports 对象上。其后通过读取a属性的值进行相应的调用处理。
在本实验中又发现了一个比较有意思的事情,ES6
模块中 import 是静态的,无法用于按需加载,因此 import 语句必须放在模块的顶层。然而在本试验中,在 import 语句上方添加了打印语句,webpack 对此的处理是,不论 import 语句出现的位置,提前加载对应的模块,然后执行该文件中剩余的部分。
更新: ES6
中import命令只能出现在模块的顶层,出现在块级作用域中会报错。另外 import 命令具有提升的效果,会提升到整个模块的顶部,首先执行。
具体测试过程见 Github: webpack-bundle-study
参考文章:
webpack指南
从 Bundle 文件看 Webpack 模块机制