上一篇文章《认识 webpack》 中说到, webpack 通过 babel 将 import 转换为 require(), 再将 require() 准换为其 __webpack_require__() 方法以实现“模块化”, 这篇文章就来分析一下 __webpack_require__() 实现“模块化”的方式。
注: 代码可能会被优化, 所以可能不同于 webpack 生成后的代码。
静态加载 即 import, 这是最常见, 也是最常用的加载方式。 因为 webpack 有 dynamic import 方式, 所以将这种模块加载方式命名为静态加载来与之对应。 下面开始对静态加载进行分析。
被 webpack 中 loader 处理的每一个模块, 最终会输出为一个或多个函数(如被 babel-loader 处理后的 ES Next 代码, 如果使用了 runtime plugin, 就会生成额外的 ES6 语法的 polyfill 函数), 这些函数被称为 webpack 处理后的模块, 大致代码如下:
1 2 3 4 5 6 7 8 (function (modules ) {})([ function ( ) {}, function ( ) {}, function ( ) {} ]);
__webpack_require__() 方法用于根据 moduleId 注册指定模块; 并且加载过的模块会被缓存, 以避免重复加载, 其中:
moduleId: 实际就是指定 module 的索引; 注册模块: 实际就是调用指定的 module 方法, 会从 entry 模块开始。 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 (modules ) { var installedModules = {}; function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) return installedModules[moduleId].exports; var module = (installedModules[moduleId] = { moduleId: moduleId, loaded: false , exports: {} }); modules[moduleId].call( module .exports, module , module .exports, __webpack_require__ ); module .loaded = true ; return module .exports; } return __webpack_require__(0 ); })([ function (module, exports, __webpack_require__ ) {}, function (module, exports, __webpack_require__ ) {}, function (module, exports, __webpack_require__ ) {} ]);
接着只需要设置 exports 值即可, webpack 支持 ES6 module 和 CommonJS 方式:
1 2 3 4 5 6 7 export test = 'test' export default 'test' module .exports = 'test' exports['test' ] = 'test'
上面这些方式分别会被转换为:
1 2 3 4 5 6 7 exports['default' ] = 'test' ; exports['test' ] = 'test' ; modules.exports = 'test' ; exports['test' ] = 'test' ;
可以看到 ES6 module 和 CommonJS 方式生成的 exports 是不同的。 那么 webapck 是怎么把这些方式统一起来方便使用的呢? 通过给 exports 添加额外的 __esModule 标记标识 ES6 module 模块:
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 var hasOwnProperty = function (object, property ) { return Object .prototype.hasOwnProperty.call(object, property); }; var defineProperty = function (exports, name, getter ) { if (!hasOwnProperty(exports, name)) { Object .defineProperty(exports, name, { enumerable: true , get : getter }); } }; function __webpack_require__() { } __webpack_require__.defineEsModule = function (exports ) { defineProperty(exports, '__esModule' , function ( ) { return true ; }); }; __webpack_require__.getDefaultExports = function (exports ) { const getter = exports && exports['__esModule' ] ? function getDefault ( ) { return exports['default' ]; } : function getModuleExports ( ) { return exprots; }; defineProperty(getter, 'a' , getter); return getter; };
所以在模块中, 我们就可以通过 getDefaultExports() 方法来获取导出的值。 下面是示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import test1Default, { test1 } from './test1.js' ;import test2 from './test2.js' ;import test3 from './test3.js' ;export default 'test1Default' ;export const test1 = 'test1' ;module .exports = 'test2' ;exports.test3 = 'test3' ;
最终会被转换为:
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 (function ( ) {})([ function (module, exports, __webpack_require__ ) { __webpack_require__.defineEsModule(exports); var _test1_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1 ); var _test2_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2 ); var _test2_js__WEBPACK_IMPORTED_MODULE_2___default = __webpack_require__.getDefaultExports( _test2_js__WEBPACK_IMPORTED_MODULE_2__ ); var _test3_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3 ); var _test3_js__WEBPACK_IMPORTED_MODULE_3___default = __webpack_require__.getDefaultExports( _test3_js__WEBPACK_IMPORTED_MODULE_3__ ); _test1_js__WEBPACK_IMPORTED_MODULE_1__['default' ]; _test1_js__WEBPACK_IMPORTED_MODULE_1__['test1' ]; _test2_js__WEBPACK_IMPORTED_MODULE_2___default(); _test3_js__WEBPACK_IMPORTED_MODULE_3__['test3' ]; }, function (module, exports, __webpack_require__ ) { __webpack_require__.defineEsModule(exports); __webpack_require__.defineProperty(exports, 'test1' , function ( ) { return test1; }); exports['default' ] = 'test1Default' ; var test1 = 'test1' ; }, function (module, exports, __webpack_require__ ) { module .exports = 'test2' ; }, function (module, exports, __webpack_require__ ) { exports.test3 = 'test' ; } ]);
完整代码如下:
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 (function (modules ) { var installedModules = {}; function __webpack_require__ (moduleId ) { if (installedModules[moduleId]) return installedModules[moduleId].exports; var module = (installedModules[moduleId] = { moduleId: moduleId, loaded: false , exports: {} }); modules[moduleId].call( module .exports, module , module .exports, __webpack_require__ ); module .loaded = true ; return module .exports; } var hasOwnProperty = function (object, property ) { return Object .prototype.hasOwnProperty.call(object, property); }; __webpack_require__.defineProperty = function (exports, name, getter ) { if (!hasOwnProperty(exports, name)) { Object .defineProperty(exports, name, { enumerable: true , get : getter }); } }; // define __esModule on exports __webpack_require__.defineEsModule = function(exports) { Object .defineProperty(exports, '__esModule' , { value : true }); }; __webpack_require__.getDefaultExports = function (exports ) { const getter = exports && exports['__esModule' ] ? function getDefault ( ) { return exports['default' ]; } : function getModuleExports ( ) { return exports; }; __webpack_require__.defineProperty(getter, 'a' , getter); return getter; }; return __webpack_require__(0 ); })([ function (module, exports, __webpack_require__ ) { __webpack_require__.defineEsModule(exports); var _test1_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1 ); var _test2_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2 ); var _test2_js__WEBPACK_IMPORTED_MODULE_2___default = __webpack_require__.getDefaultExports( _test2_js__WEBPACK_IMPORTED_MODULE_2__ ); var _test3_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(3 ); var _test3_js__WEBPACK_IMPORTED_MODULE_3___default = __webpack_require__.getDefaultExports( _test3_js__WEBPACK_IMPORTED_MODULE_3__ ); _test1_js__WEBPACK_IMPORTED_MODULE_1__['default' ]; _test1_js__WEBPACK_IMPORTED_MODULE_1__['test1' ]; _test2_js__WEBPACK_IMPORTED_MODULE_2___default(); _test3_js__WEBPACK_IMPORTED_MODULE_3__['test3' ]; }, function (module, exports, __webpack_require__ ) { __webpack_require__.defineEsModule(exports); __webpack_require__.defineProperty(exports, 'test1' , function ( ) { return test1; }); exports['default' ] = 'test1Default' ; var test1 = 'test1' ; }, function (module, exports, __webpack_require__ ) { module .exports = 'test2' ; }, function (module, exports, __webpack_require__ ) { exports.test3 = 'test' ; } ]);
动态加载 webpack 中的动态加载推荐采用 ECMASciprt 提案中的 import() 语法, 使用该方法后所有动态加载的代码会被打包到一个单独的 chunk 中, 然后通过 <script> 标签的方式在需要时按需加载, 我们先看一下这一部分的代码:
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 const installedChunks = {}function jsonpScriptSrc (chunkId ) { return __webpack__require__.p + '' + chunkId + '.js' } __webpack__require__.requireEnsure = function requireEnsure (chunkId ) { const promises = [] const installedChunkData = installedChunks[chunkId] if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push(installedChunkData[2 ]) } else { const promise = new Promise (function (resolve, reject ) { installedChunkData = installedChunks[chunkId] = [resolve, reject] }) promises.push(installedChunkData[2 ] = promise) const script = document .createElement('script' ) const onScriptComplete = function (evt ) { const chunk = installedChunks[chunkId] script.onload = script.onerror = null clearTimeout(timeout) if (chunk !== 0 ) { if (chunk) { const errorType = evt && (evt.type === 'load' ? 'missing' : evt.type); const realSrc = evt && evt.target && evt.target.src; const error = new Error ('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' ); error.type = errorType; error.request = realSrc; } installedChunks[chunkId] = undefined } var timeout = setTimeout(function ( ) { onScriptComplete({type : 'timeout' , target : script }) }, 120000 ) script.charset = 'utf-8' script.src = jsonpScriptSrc(chunkId) script.timeout = 120 script.onload = script.onerror = onScriptComplete document .head.appendChild(script) } } } return Promise .all(promises) } __webpack__require__.p = <output.publicPath >
上面的代码略长, 简单来说就是: 通过 <script> 标签加载指定的 chunk 文件, 并且在 installedChunk[chunkId] 中保存了此次的 promise, 并返回了一个 Promise.all(...)。 我们设想的当然是在 chunk 加载完后, 把此次的 promise 执行 resolve()。
webpack 中的方式是在 window 对象上挂载一个全局方法 webpackJsonpCallback, chunk 中是一个会自动调用该方法的立即执行函数:
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 webpackJsonpCallback (data ) { const chunkIds = data[0 ]; const moreModules = data[1 ]; const resolves = []; for (let i = 0 ; i < chunkIds.length; i++) { const chunkId = chunkIds[i]; if (installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0 ]); } installedChunks[chunkId] = 0 ; } for (const moduleId in moreModules) { if (__webpack__require__.o(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) resolves.shift()(); } const jsonArray = (window ['webpackJsonpTest' ] = window [ 'webpackJsonpTest' ] = []); const oldJsonpFunction = jsonArray.push.bind(jsonArray);const parentJsonpFunction = oldJsonpFunction;window ['webpackJsonpTest' ].webpackJsonpCallback = webpackJsonpCallback;jsonpArray = jsonpArray.slice();
chunk 中的代码:
1 2 3 4 5 6 7 8 9 (window ["webpackJsonpTest" ] = window ["webpackJsonpTest" ] || []).push([ [<chunk-name > ], { // 4 是 chunk splitting 前的 moduleId 4: function (module, exports) { exports['default'] = 'test' } } ])
在加载完 chunk 中的 module 后, 还要把该 module 注册到我们的 modules 中, 方便后序的使用:
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 __webpack__require__.t = function (value, mode ) { if (mode & 1 ) value = __webpack__require__(value); if (mode & 8 ) return value; if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value; const tmp = Object .create(null ); __webpack__require__.r(tmp); Object .defineProperty(tmp, 'default' , { enmerable : true , value }); if (mode & 2 && typeof value !== 'string' ) { for (const key in value) { __webpack__require__.d( tmp, key, function ( ) { return value[key]; }.bind(null , key) ); } } return tmp; };
再看一下 import() 最终会被转换哪种形式:
1 2 3 4 5 6 7 function (module, exports, __webpack_require__ ) { __webpack_require__.requireEnsure('<chunk-name>' ) .then(__webpack_require__.t.bind(null , '<moduleId>' , 7 )) .then(module => { }) }
最后, 动态加载部分的完整代码如下:
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 (function (modules ) { const installedModules = {}; const installedChunks = {}; function webpackJsonpCallback (data ) { const chunkIds = data[0 ]; const moreModules = data[1 ]; const resolves = []; for (let i = 0 ; i < chunkIds.length; i++) { const chunkId = chunkIds[i]; if (installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0 ]); } installedChunks[chunkId] = 0 ; } for (const moduleId in moreModules) { if (__webpack__require__.o(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) resolves.shift()(); } function jsonpScriptSrc (chunkId ) { return __webpack__require__.p + '' + chunkId + '.js' ; } function __webpack__require__ (moduleId ) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } const 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__.o = function hasOwnProperty (object, property ) { return Object .prototype.hasOwnProperty.call(object, property); }; __webpack__require__.d = function defineGetterProperty ( exports, name, getter ) { if (!__webpack__require__.o(exports, name)) { Object .defineProperty.call(null , exports, name, { enmerable: true , get : getter }); } }; __webpack__require__.r = function defineEsModule(exports) { Object .defineProperty(exports, '__esModule' , { value : true }); }; __webpack__require__.n = function getExportDefault (exports ) { const getter = exports.__esModule ? function ( ) { return exports.default; } : function ( ) { return exports; }; __webpack__require__.d(getter, 'a' , getter); return getter; }; __webpack__require__.t = function (value, mode ) { if (mode & 1 ) value = __webpack__require__(value); if (mode & 8 ) return value; if (mode & 4 && typeof value === 'object' && value && value.__esModule) return value; const tmp = Object .create(null ); __webpack__require__.r(tmp); Object .defineProperty(tmp, 'default' , { enmerable : true , value }); if (mode & 2 && typeof value !== 'string' ) { for (const key in value) { __webpack__require__.d( tmp, key, function ( ) { return value[key]; }.bind(null , key) ); } } return tmp; }; __webpack__require__.e = function requireEnsure (chunkId ) { const promises = []; let installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0 ) { if (installedChunkData) { promises.push(installedChunkData[2 ]); } else { const promise = new Promise (function (resolve, reject ) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push((installedChunkData[2 ] = promise)); const script = document .createElement('script' ); const onScriptComplete = function (evt ) { script.onerror = script.onload = null ; clearTimeout(timeout); const chunk = installedChunks[chunkId]; if (chunk !== 0 ) { if (chunk) { const errorType = evt && (evt.type === 'load' ? 'missing' : evt.type); const realSrc = evt && evt.target && evt.target.src; const error = new Error ( 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')' ); error.type = errorType; error.request = realSrc; } installedChunks[chunkId] = undefined ; } }; var timeout = setTimeout(function ( ) { onScriptComplete({ type : 'timeout' , target : script }); }, 120000 ); script.charset = 'utf-8' ; script.src = jsonpScriptSrc(chunkId); script.timeout = 120 ; script.onload = script.onerror = onScriptComplete; document .head.appendChild(script); } } return Promise .all(promises); }; __webpack__require__.p = '' ; let jsonpArray = (window ['webpackJsonpTest' ] = window [ 'webpackJsonpTest' ] = []); const oldJsonpFunction = window ['webpackJsonpTest' ].push.bind( window ['webpackJsonpTest' ] ); window ['webpackJsonpTest' ].push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); var parentJsonpFunction = oldJsonpFunction; return __webpack__require__(0 ); })([ function (module, __exports, __require ) { __require .e('chunks' ) .then(__require.t.bind(null , 14 , 7 )) .then(module => {}); } ]);
chunk 完整代码:
1 2 3 4 5 6 7 8 (window ['webpackJsonpTest' ] = window ['webpackJsonpTest' ] || []).push([ ['chunks' ], { 14 : function (module, exports ) { exports['default' ] = 'test' ; } } ]);