webpack作为前端打包工具受到大多数的前端开发者的青睐,在使用webpack的过程我们通过webpack自带的模块化功能实现了项目代码的模块化,方便了我们管理和维护,那么webpack是怎么实现各个模块之间的划分和加载的呢?
在了解模块加载之前,我们需要首先看下webpack是怎么将一个个文件划分为模块的。我们有一个入口文件index.js以及模块a.js、b.js,代码如下
// index.js
import A from './a.js'
function hello () {
console.log(A)
}
hello()
export default hello
// a.js
export default {
a: 'a'
}
// b.js
export const B = function () {
console.log('B')
}
在经过了webpack的各种编译之后,会产生一个bundle.js的文件,我们来看下里面的大体结构。
(function(modules){
......
......
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for(var i = 0; i < jsonpArray.length; i++)
webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})(
{
"./src/a.js": (function(module, __webpack_exports__, __webpack_require__){
.......
.......
}),
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__){
.......
.......
})
}
)
我们可以看到webpack最终打包出来的文件其实一个立即执行函数,函数的参数是一个modules对象。webpack将一个文件作为一个模块,用文件的相对路径来作为module的key,把文件的内容用一个闭包包裹作为module的值。将modules传入上面的函数之后,执行到return webpack_require(webpack_require.s = “./src/main.js”); 部分,加载我们的入口文件main.js。所以我们可以看出webpack进行模块加载的关键就是__webpack_require__函数了。
前面有提到__webpack_require__函数是webpack中模块加载的关键函数,下面看下它的作用是什么。
// The module cache
var installedModules = {};
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// Promise = chunk loading, 0 = chunk loaded
var installedChunks = {
0: 0
};
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] = {
// id
i: moduleId,
// 是否记载的标志变量
l: false,
// module的值
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查找该模块是否已经加载过了,若之前加载过了,则使用之前的缓存,若未加载,则会定义一个包含i、l、exports的module变量,并将该变量缓存到installedModules中去,接下来就执行我们上面提到的modules里对应key的函数,module, module.exports, __webpack_require__作为该函数的参数,我们拿main.js来举例接着来看modules里面的函数做了什么
"./src/main.js": (function(module, __webpack_exports__, __webpack_require__){
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a.js */ "./src/a.js");
})
function hello() {
console.log(_a_js__WEBPACK_IMPORTED_MODULE_0__["default"]);
}
__webpack_exports__["default"] = (hello)
}
在webpack编译出的main.js文件中,我们用来引入模块的import已经被转换成了__webpack_require__。在该函数里面主要做的事情就是给之前我们定义的module.exports赋值,若导出模块使用的是export default的话,就会给module.exports定义一个default属性,若是使用export的话,则会给module.exports定义一个导出的key的属性。
在执行完这个函数之后,再在上面的__webpack_require__函数中把赋值module.exports的导出,最后就完成了对应模块的定义和导入。
在上面的函数中,webpack有使用到一些自定义的工具函数和属性,如:webpack_require.r等,我们来看一下这些工具函数的的大致用途。
属性名 | 类型 | 用途 |
---|---|---|
webpack_require.e | Function | 用于异步加载,后面的内容会提到 |
webpack_require.m | Array | modules |
webpack_require.c | Array | 已经加载过的module的cache |
webpack_require.d | Function | 给module的exports定义属性 |
webpack_require.r | Function | 给给module的exports定义__esModule属性 |
webpack_require.o | Function | 作用等同于hasOwnProperty |
webpack的异步加载的过程和普通模块加载的过程有些许不同,webpack 在编译时,会静态地解析代码中的异步加载相关的代码,同时将模块添加到一个分开的 chunk 当中,这时候就不能使用我们同步加载模块的方式了。我们通过编译过后的独立chunk的代码来看下webpack的异步加载是怎么实现的。
// 原代码
() => import('./src/c.js')
// 编译后代码
function () {
return __webpack_require__.e(/*! import() */ 1).then(__webpack_require__.bind(null, /*! ./src/c.js */ "./src/c.js"));
}
我们使用了import()进行异步加载后,webpack将其编译成了__webpack_require__.e去加载chunkId为0的chunk。然后在通过__webpack_require__加载对应的模块,我们先看下__webpack_require__.e是怎么加载chunk的。
__webpack_require__.e = function requireEnsure(chunkId) {
var promises = [];
// JSONP chunk loading for javascript
var installedChunkData = installedChunks[chunkId];
if(installedChunkData !== 0) { // 0 means "already installed".
// a Promise means "currently loading".
if(installedChunkData) {
promises.push(installedChunkData[2]);
} else {
// setup Promise in chunk cache
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
promises.push(installedChunkData[2] = promise);
// start chunk loading
var script = document.createElement('script');
var onScriptComplete;
script.charset = 'utf-8';
script.timeout = 120;
if (__webpack_require__.nc) {
script.setAttribute("nonce", __webpack_require__.nc);
}
script.src = jsonpScriptSrc(chunkId);
onScriptComplete = function (event) {
// avoid mem leaks in IE.
script.onerror = script.onload = null;
clearTimeout(timeout);
var chunk = installedChunks[chunkId];
if(chunk !== 0) {
if(chunk) {
var errorType = event && (event.type === 'load' ? 'missing' : event.type);
var realSrc = event && event.target && event.target.src;
var error = new Error('Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')');
error.type = errorType;
error.request = realSrc;
chunk[1](error);
}
installedChunks[chunkId] = undefined;
}
};
var timeout = setTimeout(function(){
onScriptComplete({ type: 'timeout', target: script });
}, 120000);
script.onerror = script.onload = onScriptComplete;
document.head.appendChild(script);
}
}
return Promise.all(promises);
};
简单的说就是,动态地添加script标签,去加载chunkId对应的js,加载完成之后放入promise中,等待resolve,等到调用了resolve才标志着chunk加载完成,接着我们看下chunk1里面的js主要内容。
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[1],{
"./src/c.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
function c() {
console.log("c");
}
__webpack_exports__["default"] = (c)
})
}])
可以认为,在chunk里面其实主要就是执行了(window[“webpackJsonp”] = window[“webpackJsonp”] || []).push代码,这个push和一般的数组的不同,这个是自定义的push函数,我们回到最上面的主chunk代码中。
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
我们发现有这么一段代码,初始化了window[“webpackJsonp”]这个变量,并将该变量的push属性变成了webpackJsonpCallback。
function webpackJsonpCallback(data) {
var chunkIds = data[0];
var moreModules = data[1];
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId, chunkId, i = 0, resolves = [];
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(data);
while(resolves.length) {
resolves.shift()();
}
};
webpackJsonpCallback的主要工作就是把我们push的参数(chunkId,对应的module)注入到全局当中,最后执行resolve,表示模块已经注入成功,这个时候,我们使用__webpack_require__就可以导入分离出来的chunk中的module了。