简述 webpack
是一款现代化的前端打包工具,那么webpack是怎么将模块化代码能够在浏览器运行的?让我们来看一下
MVP 从一个最小webpack
实例开始:
src/index.js
1 console .log("Hello Webpack" );
我们直接使用命令行进行打包, 结果如下:
webpack –mode development
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 (() => { var __webpack_modules__ = ({ "./src/index.js" : (() => { eval ("console.log('Hello Webpack');\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?" ); }) }); var __webpack_exports__ = {}; __webpack_modules__["./src/index.js" ](); })() ;
webpack –mode development –devtool hidden-source-map
1 2 3 4 5 6 7 8 9 (() => { var __webpack_exports__ = {};console .log('Hello Webpack' ); })() ;
webpack –mode production
1 console .log("Hello Webpack" );
可以看到, 对于简单代码来说, 是否使用webpack打包区别不大。稍微注意一下,在默认的development
环境中引入了两个变量__webpack_exports__
和 __webpack_modules__
。顾名思义,是分别管理导出内容与模块列表的两个代码
__webpack_modules__
是一个key为代码(模块)路径,值为模块执行结果的一个对象。
我们来试试稍微复杂一点的例子:
使用import src/index.js
1 2 3 import {add} from './utils' console .log(add(1 , 2 ));
src/utils.js
1 2 3 export function add (a, b ) { return a + b; }
我们直接使用命令行进行打包, 结果如下:
webpack –mode development
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 (() => { "use strict" ; var __webpack_modules__ = ({ "./src/index.js" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { eval ("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ \"./src/utils.js\");\n\n\nconsole.log((0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2));\n\n\n//# sourceURL=webpack://webpack-demo/./src/index.js?" ); }), "./src/utils.js" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { eval ("__webpack_require__.r(__webpack_exports__);\n/* harmony export */ __webpack_require__.d(__webpack_exports__, {\n/* harmony export */ \"add\": () => (/* binding */ add)\n/* harmony export */ });\nfunction add(a, b) {\n return a + b;\n}\n\n\n//# sourceURL=webpack://webpack-demo/./src/utils.js?" ); }) }); var __webpack_module_cache__ = {}; function __webpack_require__ (moduleId ) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined ) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports : {} }; __webpack_modules__[moduleId](module , module .exports, __webpack_require__); return module .exports; } (() => { __webpack_require__.d = (exports , definition ) => { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports , key)) { Object .defineProperty(exports , key, { enumerable : true , get : definition[key] }); } } }; })(); (() => { __webpack_require__.o = (obj, prop ) => (Object .prototype.hasOwnProperty.call(obj, prop)) })(); (() => { __webpack_require__.r = (exports ) => { if (typeof Symbol !== 'undefined' && Symbol .toStringTag) { Object .defineProperty(exports , Symbol .toStringTag, { value : 'Module' }); } Object .defineProperty(exports , '__esModule' , { value : true }); }; })(); var __webpack_exports__ = __webpack_require__("./src/index.js" ); })() ;
webpack –mode development –devtool hidden-source-map
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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 (() => { "use strict" ; var __webpack_modules__ = ({ "./src/utils.js" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "add" : () => ( add) }); function add (a, b ) { return a + b; } }) }); var __webpack_module_cache__ = {}; function __webpack_require__ (moduleId ) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined ) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports : {} }; __webpack_modules__[moduleId](module , module .exports, __webpack_require__); return module .exports; } (() => { __webpack_require__.d = (exports , definition ) => { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports , key)) { Object .defineProperty(exports , key, { enumerable : true , get : definition[key] }); } } }; })(); (() => { __webpack_require__.o = (obj, prop ) => (Object .prototype.hasOwnProperty.call(obj, prop)) })(); (() => { __webpack_require__.r = (exports ) => { if (typeof Symbol !== 'undefined' && Symbol .toStringTag) { Object .defineProperty(exports , Symbol .toStringTag, { value : 'Module' }); } Object .defineProperty(exports , '__esModule' , { value : true }); }; })(); var __webpack_exports__ = {};(() => { __webpack_require__.r(__webpack_exports__); var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( "./src/utils.js" ); console .log((0 ,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1 , 2 ));})(); })() ;
(可以看到webpack --mode development --devtool hidden-source-map
这个命令执行的结果和直接development是一样的,但是代码可读性更加高。之后的文章将以这个命令的输出为准)
webpack –mode production
1 (()=> {"use strict" ;console .log(3 )})();
可以看到,webpack一旦发现了模块系统,那么就会增加很多中间代码(从注释 The module cache
到 变量 __webpack_exports__
)
首先webpack每块代码都是以(() => {})()
这种形式的闭包来处理的,防止污染外部空间。
然后每一段都有一段注释来告知下面这块代码的逻辑是要做什么
我们来一一看一下:
module cache and require function 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var __webpack_module_cache__ = {};function __webpack_require__ (moduleId ) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined ) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports : {} }; __webpack_modules__[moduleId](module , module .exports, __webpack_require__); return module .exports; }
定义了一个__webpack_module_cache__
用于缓存模块
定义了一个__webpack_require__
方法, 接受一个moduleId
, 从下面可以看到moduleId
是这个模块的路径(包括拓展名, 也即是__webpack_modules__
管理的key值)
先判断缓存中是否存在这个模块,即是否加载,如果加载直接返回导出的数据,如果没有则在缓存中创建一个空对象{exports: {}}
, 然后把module, module.exports, __webpack_require__
作为参数去执行__webpack_modules__
对应的方法
而__webpack_modules__
的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 var __webpack_modules__ = ({ "./src/utils.js" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "add" : () => (add) }); function add (a, b ) { return a + b; } }) });
可以看到,这里调用了一个__webpack_require__.r
和一个__webpack_require__.d
方法。目前我们不知道这两个方法是做什么用的。继续看下去。
webpack/runtime/define property getters 1 2 3 4 5 6 7 8 9 10 11 (() => { __webpack_require__.d = (exports , definition ) => { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports , key)) { Object .defineProperty(exports , key, { enumerable : true , get : definition[key] }); } } }; })();
d
是define
的缩写。可以看到这个方法的作用就是定义导出的值。
其目的就是遍历definition
对象将其一一填入exports
。需要注意的是使用__webpack_require__.d
的目的在于确保:
只能有一个key
存在,如果exports
中已经存在过了这个导出值,则不会重复导入
确保exports
中的属性只有getter
, 不能被外部设置
make namespace object 1 2 3 4 5 6 7 8 9 10 (() => { __webpack_require__.r = (exports ) => { if (typeof Symbol !== 'undefined' && Symbol .toStringTag) { Object .defineProperty(exports , Symbol .toStringTag, { value : 'Module' }); } Object .defineProperty(exports , '__esModule' , { value : true }); }; })();
这个方法完成了两个目的。
在exports
定义了Symbol.toStringTag
的值为Module
在exports
定义了__esModule
的值为true
目的在于完成导出模块的兼容性
我们试试换一种导出方式:src/utils.js
1 2 3 4 exports .add = function (a, b ) { return a + b; }
结果:
1 2 3 4 5 6 7 8 var __webpack_modules__ = ({ "./src/utils.js" : ((__unused_webpack_module, exports ) => { exports .add = function (a, b ) { return a + b; } }) });
可以看到输出简洁了很多。但是结果是一样的。都是在exports
中插入导出的方法, 只不过esmodule
的方式更加谨慎一点
那么前面的__unused_webpack_module
又是干嘛的呢?我们修改一下代码src/utils.js
1 2 3 module .exports = function add (a, b ) { return a + b; }
结果:
1 2 3 4 5 6 7 8 var __webpack_modules__ = ({ "./src/utils.js" : ((module ) => { module .exports = function add (a, b ) { return a + b; } }) });
一个主要细节在于esmodule
使用了__webpack_require__.d
来确保其代码的只读性,而commonjs没有:
esmodule和commonjs的模块导出可访问性区别 CommonJS
模块输出的是一个值的拷贝, ES6
模块输出的是值的引用
举个例子
commonjs
1 2 3 4 5 6 var a = 1 ;setTimeout (() => { a = 2 ; }, 0 ) exports .a = a;
生成代码:
1 2 3 4 5 6 7 8 ((module ) => { var a = 1 ; setTimeout (() => { a = 2 ; }, 0 ) module .exports.a = a; })
esmodule :
1 2 3 4 5 6 var a = 1 ;setTimeout (() => { a = 2 ; }, 0 ) export { a }
输出代码:
1 2 3 4 5 6 7 8 9 10 11 ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { "use strict" ; __webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "a" : () => ( a) }); var a = 1 ; setTimeout (() => { a = 2 ; }, 0 ) })
可以看到区别:
commonjs 输出的a: 1 -> 1
esmodule 输出的a: 1 -> 2
因为commonjs
内部实现是赋值,程序导出以后原来的a
和导出的a
的关系就没有了
而esmodule
输出的一个对象,内部的getter
会每次去拿最新的a
的值
那么到此我们的中间代码就看完了,顺便还介绍了一下webpack
的导出结果。完整的中间代码列表可以看这个文件
执行代码 在上面的示例中,我们得到以下代码:
1 2 3 4 5 6 7 var __webpack_exports__ = {};(() => { __webpack_require__.r(__webpack_exports__); var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils.js" ); console .log((0 ,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1 , 2 )); })();
该代码作为项目的入口代码, 完成了以下逻辑
通过 __webpack_require__.r
标记这个文件导出类型为esmodule
执行 __webpack_require__
并将导入的结果存放到临时变量 _utils__WEBPACK_IMPORTED_MODULE_0__
执行 (0,_utils__WEBPACK_IMPORTED_MODULE_0__.add)(1, 2)
并导出结果。这里的(0, ...)
是为了重置方法的this
指向
Comma operator
这个方法等价于1 2 const add = _utils__WEBPACK_IMPORTED_MODULE_0__.addadd(1 , 2 )
让我们来微调一下代码:
1 2 3 const add = require ('./utils' )console .log(add(1 , 2 ));
输出:
1 2 3 4 5 var __webpack_exports__ = {};(() => { const add = __webpack_require__("./src/utils.js" ) console .log(add(1 , 2 )); })();
可以看到, 其主要的区别就是__webpack_require__.r
, 其他的区别不是很大。
动态代码 修改部分代码:
src/index.js
1 2 3 import ('./utils' ).then(({add} ) => { console .log(add(1 ,2 )) })
生成代码:
webpack –mode development –devtool hidden-source-map
dist/main.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 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 (() => { var __webpack_modules__ = ({}); var __webpack_module_cache__ = {}; function __webpack_require__ (moduleId ) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined ) { return cachedModule.exports; } var module = __webpack_module_cache__[moduleId] = { exports : {} }; __webpack_modules__[moduleId](module , module .exports, __webpack_require__); return module .exports; } __webpack_require__.m = __webpack_modules__; (() => { __webpack_require__.d = (exports , definition ) => { for (var key in definition) { if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports , key)) { Object .defineProperty(exports , key, { enumerable : true , get : definition[key] }); } } }; })(); (() => { __webpack_require__.f = {}; __webpack_require__.e = (chunkId ) => { return Promise .all(Object .keys(__webpack_require__.f).reduce((promises, key ) => { __webpack_require__.f[key](chunkId, promises); return promises; }, [])); }; })(); (() => { __webpack_require__.u = (chunkId ) => { return "" + chunkId + ".js" ; }; })(); (() => { __webpack_require__.g = (function ( ) { if (typeof globalThis === 'object' ) return globalThis; try { return this || new Function ('return this' )(); } catch (e) { if (typeof window === 'object' ) return window ; } })(); })(); (() => { __webpack_require__.o = (obj, prop ) => (Object .prototype.hasOwnProperty.call(obj, prop)) })(); (() => { var inProgress = {}; var dataWebpackPrefix = "webpack-demo:" ; __webpack_require__.l = (url, done, key, chunkId ) => { if (inProgress[url]) { inProgress[url].push(done); return ; } var script, needAttach; if (key !== undefined ) { var scripts = document .getElementsByTagName("script" ); for (var i = 0 ; i < scripts.length; i++) { var s = scripts[i]; if (s.getAttribute("src" ) == url || s.getAttribute("data-webpack" ) == dataWebpackPrefix + key) { script = s; break ; } } } if (!script) { needAttach = true ; script = document .createElement('script' ); script.charset = 'utf-8' ; script.timeout = 120 ; if (__webpack_require__.nc) { script.setAttribute("nonce" , __webpack_require__.nc); } script.setAttribute("data-webpack" , dataWebpackPrefix + key); script.src = url; } inProgress[url] = [done]; var onScriptComplete = (prev, event ) => { script.onerror = script.onload = null ; clearTimeout (timeout); var doneFns = inProgress[url]; delete inProgress[url]; script.parentNode && script.parentNode.removeChild(script); doneFns && doneFns.forEach((fn ) => (fn(event))); if (prev) return prev(event); } ; var timeout = setTimeout (onScriptComplete.bind(null , undefined , { type : 'timeout' , target : script }), 120000 ); script.onerror = onScriptComplete.bind(null , script.onerror); script.onload = onScriptComplete.bind(null , script.onload); needAttach && document .head.appendChild(script); }; })(); (() => { __webpack_require__.r = (exports ) => { if (typeof Symbol !== 'undefined' && Symbol .toStringTag) { Object .defineProperty(exports , Symbol .toStringTag, { value : 'Module' }); } Object .defineProperty(exports , '__esModule' , { value : true }); }; })(); (() => { var scriptUrl; if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + "" ; var document = __webpack_require__.g.document; if (!scriptUrl && document ) { if (document .currentScript) scriptUrl = document .currentScript.src if (!scriptUrl) { var scripts = document .getElementsByTagName("script" ); if (scripts.length) scriptUrl = scripts[scripts.length - 1 ].src } } if (!scriptUrl) throw new Error ("Automatic publicPath is not supported in this browser" ); scriptUrl = scriptUrl.replace(/#.*$/ , "" ).replace(/\?.*$/ , "" ).replace(/\/[^\/]+$/ , "/" ); __webpack_require__.p = scriptUrl; })(); (() => { var installedChunks = { "main" : 0 }; __webpack_require__.f.j = (chunkId, promises ) => { var installedChunkData = __webpack_require__.o(installedChunks, chunkId) ? installedChunks[chunkId] : undefined ; if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push(installedChunkData[2 ]); } else { if (true ) { var promise = new Promise ((resolve, reject ) => (installedChunkData = installedChunks[chunkId] = [resolve, reject])); promises.push(installedChunkData[2 ] = promise); var url = __webpack_require__.p + __webpack_require__.u(chunkId); var error = new Error (); var loadingEnded = (event ) => { if (__webpack_require__.o(installedChunks, chunkId)) { installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0 ) installedChunks[chunkId] = undefined ; if (installedChunkData) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' ; error.name = 'ChunkLoadError' ; error.type = errorType; error.request = realSrc; installedChunkData[1 ](error); } } }; __webpack_require__.l(url, loadingEnded, "chunk-" + chunkId, chunkId); } else installedChunks[chunkId] = 0 ; } } }; var webpackJsonpCallback = (parentChunkLoadingFunction, data ) => { var [chunkIds, moreModules, runtime] = data; var moduleId, chunkId, i = 0 ; for (moduleId in moreModules) { if (__webpack_require__.o(moreModules, moduleId)) { __webpack_require__.m[moduleId] = moreModules[moduleId]; } } if (runtime) var result = runtime(__webpack_require__); if (parentChunkLoadingFunction) parentChunkLoadingFunction(data); for (;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (__webpack_require__.o(installedChunks, chunkId) && installedChunks[chunkId]) { installedChunks[chunkId][0 ](); } installedChunks[chunkIds[i]] = 0 ; } } var chunkLoadingGlobal = self["webpackChunkwebpack_demo" ] = self["webpackChunkwebpack_demo" ] || []; chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null , 0 )); chunkLoadingGlobal.push = webpackJsonpCallback.bind(null , chunkLoadingGlobal.push.bind(chunkLoadingGlobal)); })(); var __webpack_exports__ = {};__webpack_require__.e( "src_utils_js" ).then(__webpack_require__.bind(__webpack_require__, "./src/utils.js" )).then(({add} ) => { console .log(add(1 ,2 )) }) })() ;
dist/src/utils_js.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (self["webpackChunkwebpack_demo" ] = self["webpackChunkwebpack_demo" ] || []).push([["src_utils_js" ],{ "./src/utils.js" : ((__unused_webpack_module, __webpack_exports__, __webpack_require__ ) => { "use strict" ;__webpack_require__.r(__webpack_exports__); __webpack_require__.d(__webpack_exports__, { "add" : () => ( add) }); function add (a, b ) { return a + b; } }) }]);
相同的代码我们跳过,我们首先来看一下入口文件的执行代码:
1 2 3 4 5 6 var __webpack_exports__ = {};__webpack_require__.e("src_utils_js" ) .then(__webpack_require__.bind(__webpack_require__, "./src/utils.js" )) .then(({add} ) => { console .log(add(1 ,2 )) })
这个代码主要分成三部分:
第一部分执行__webpack_require__.e
第二部分生成一个__webpack_require__
方法并绑定参数
第三部分去执行实际逻辑。
我们来看下主要核心的中间代码__webpack_require__.e
:
1 2 3 4 5 6 7 8 9 10 11 12 (() => { __webpack_require__.f = {}; __webpack_require__.e = (chunkId ) => { return Promise .all( Object .keys(__webpack_require__.f).reduce((promises, key ) => { __webpack_require__.f[key](chunkId, promises); return promises; }, []) ); }; })();
简单了解一下reduce
这段代码很奇怪,看上去来说实际可以视为作为一个forEach
在使用。目的是试图去执行__webpack_require__.f
这个对象中的所有方法,最后返回一个总的Promise
。
至于执行的方法,目前只有一个__webpack_require__.f.j
,里面是一堆代码总之暂且放置不看,我们可以将其视为加载js文件即可(通过生成script的方式)。
我们可以将其视为加载好dist/src/utils_js.js
并将该文件里声明的对象的map添加到__webpack_modules__
即可。
此时使用__webpack_require__
去走之前的逻辑就可以正常调用模块了。
这样就实现了代码分割。
一些动态加载的小细节 1 2 3 4 5 var chunkLoadingGlobal = self["webpackChunkwebpack_demo" ] = self["webpackChunkwebpack_demo" ] || [];(self["webpackChunkwebpack_demo" ] = self["webpackChunkwebpack_demo" ] || []).push([["src_utils_js" ],{...})
通过这种命名空间方式解决了单页面多项目可能错误添加动态加载代码的问题。
1 chunkLoadingGlobal.push = webpackJsonpCallback.bind(null , chunkLoadingGlobal.push.bind(chunkLoadingGlobal));
重写数组的push
方法,在push
时做一些额外的操作 即执行chunkLoadingGlobal.push(arg1, arg2)
时。执行webpackJsonpCallback(chunkLoadingGlobal.push, arg1, arg2)
这种方式。老实说我没有想到这种写法的好处,但也算一种小技巧
总结 统一module方式 webpack 将两种形式导出方式进行了一定程度上的统一,即不论写法如何,都通过__webpack_require__
对模块进行引入,而对于导出的模块来说,都统一成module的样式。
区别在于esmodule
的default
导出和commonjs
的module.exports导出略有区别
esmodule
的default
导出在生成的代码中地位和一般的export
导出是一样的