webpack完整的一次初始化、编译、输出的逻辑前几篇文章有了介绍,作为目前最受欢迎的打包工具其内部的实例逻辑必然是复杂,一些细节点我觉得暂不必深究,而且webpack还有一些其他的点值得去探究,例如:
这些点之后会较为深入的学习了解,本文主要分析最基本的输出文件的结构和逻辑,主要想要了解点如下:
为了研究输出文件的最基本的结构和逻辑,实例如下:
// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
// 执行 show 函数
show('Webpack');
入口文件main.js中只存在一个show.js的模块,按照这个实例去输出最后的文件。
最后输出文件结构是一个IIFE函数(立即执行函数),基本结构如下:
(function(modules) {
// 相关逻辑
})(模块数组);
function(modules) { // webpackBootstrap 启动函数
// 已加载模块缓存
var installedModules = {};
// webpack实现的require函数用于加载模块
function __webpack_require__(moduleId) {
// 已加载模块直接从缓存中取
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建新模块并cache
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;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
// getDefaultExport function for compatibility with non-harmony modules
__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;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
/**
* - 入口模块的导入执行
* - 定义入口文件模块的下标位置__wepack_require__.s
* - 暴露相关模块指定内容
**/
return __webpack_require__(__webpack_require__.s = 0);
}
最基本的实例立即执行函数中的逻辑总结为:
模块数组参数是指所有的模块对应的函数的集合,数据结构是数组类型。
本部分基于的实例涉及到:main.js入口文件、show.js逻辑文件,所以对应对应这这边的模块数组如下:
([
/* 0 */
(function(module, exports, __webpack_require__) {
const show = __webpack_require__(1);
show('Webpack');
}),
/* 1 */
(function(module, exports) {
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
// 通过 CommonJS 规范导出
module.exports = show;
})
]);
模块数组作为立即执行函数的入参传入,即modules参数对应的值,其中0表示入口文件main.js的逻辑,1表示逻辑文件show.js的具体逻辑。这里需要注意两点:
在分析输出文件的结构和逻辑时的实例是:
总结下:
- 使用CommonJs规范编写,webpack输出文件中会自实现module.exports和require函数来实现模块的加载和输出
- main.js中使用webpack自定义的require来加载、show.js中使用webpack中的定义module.exports来暴露内容
下面分析不同模块规范的都是基于目前的实例main.js和show.js来,分别使用不同模块规范来重写输出和加载。
import show from './show.js';
show('Webpack');
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
export default show;
在当前情况下比较输出文件,webpackBootstrap部分即立即执行函数逻辑保持不变,相对于CommonJs规范不同有:
// main.js模块
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
var __WEBPACK_IMPORTED_MODULE_0__show_js__ = __webpack_require__(1);
Object(__WEBPACK_IMPORTED_MODULE_0__show_js__["a" /* default */])('Webpack');
}
// show.js模块
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
__webpack_exports__["a"] = (show);
}
webpack因为是Node.js平台下编写的工具,所以其配置文件等是按照CommonJs规范写的,对于ES模块就需要兼容处理,使用ES模块最后输出文件中不同在于:
如果暴露内容是采用export,相关的show模块的内容与export default相同,只是加载时取值时需解构一层。如果暴露内容是采用export {},则show模块逻辑存在差异,具体如下:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.d(__webpack_exports__, "a", function() { return show; });
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
}
这里值得注意的是_webpack_require_.d方法的执行逻辑,具体如下:
__webpack_require__.d = function(exports, name, getter) {
// hasOwnProperty判断
if(!__webpack_require__.o(exports, name)) {
// 定义相关属性
Object.defineProperty(exports, name, {
configurable: false,
enumerable: true,
get: getter
});
}
};
const show = require('./show.js');
show('Webpack');
define(function() {
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
return show;
});
AMD规范下show模块的兼容处理代码如下:
function(module, exports, __webpack_require__) {
// 依赖模块 + 加载后的依赖模块对象
var __WEBPACK_AMD_DEFINE_ARRAY__,
__WEBPACK_AMD_DEFINE_RESULT__;
// 定义__WEBPACK_AMD_DEFINE_RESULT__函数并执行
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [],
__WEBPACK_AMD_DEFINE_RESULT__ = function() {
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
return show;
}.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__),
// 将define中暴露的内容赋值给module.exports
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__)
)
}
// 通过 CommonJS 规范导入 show 函数
const show = require('./show.js');
show('Webpack');
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
export default show;
在这种情况下看看输出文件的不同,首先说说不变的地方:
立即执行函数中内容没有任何变化,即不同的模块系统保持相同
变动的地方是模块数组中,实例涉及到两个模块main.js和show.js,主要是show.js模块职工ES模块的兼容处理,实际上ES模块输出模块内容也存在3种形式:
export default 内容
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
function show(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
}
__webpack_exports__["default"] = (show);
}
与CommonJs规范下输出不同点有2处:
实际上__webpack_exports实际上就是webpack自己实现的module.exports。需要注意的是,这里输出的内容的default,那对应main.js使用CommonJs规范加载模块只能调用default名称了,即:
const { default: show } = require('./show.js');
show('Webpack');
export 内容
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
const show = function(content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
__webpack_exports__["show"] = show;
}
与export default不同的是,这里直接输出show名称,即加载该模块直接可使用暴露的名称。
export { 内容 }
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
__webpack_require__.d(__webpack_exports__, "show", function() { return show; });
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
}
在ES模块规范时候就分析过__webpack_require__.d方法,该方法实际上该函数将getter定义到指定对象上。此时使用CommonJs规范加载该模块,就无法直接调用,需要解构一层,即:
const { show } = require('./show.js');
show('Webpack');
import show from './show.js';
show('Webpack');
function show (content) {
window.document.getElementById('app').innerText = 'Hello,' + content;
};
module.exports = show;
在这种情况下比较输出文件的变化:
因为show模块是通过CommonJs规范方式暴露内容的,而加载是使用ES模块形式来的,所以必然存在兼容的逻辑,具体如下:
function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// 定义__esModule属性
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
// 加载show模块
var __WEBPACK_IMPORTED_MODULE_0__show_js__ = __webpack_require__(1);
var __WEBPACK_IMPORTED_MODULE_0__show_js___default = __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__show_js__);
__WEBPACK_IMPORTED_MODULE_0__show_js___default()('Webpack');
需要理解的是_webpack_require_.n的逻辑处理,该函数定义于立即执行函数中,具体逻辑如下:
__webpack_require__.n = function(module) {
// 判断是否是ES模块
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
// 在getter函数上定义a属性
__webpack_require__.d(getter, 'a', getter);
return getter;
};
因为show模块的module中没有定义__esModule属性,则相应的getter则是:
function getModuleExports() { return module; }
在当前情况下,即module为show模块的exports对象,即show函数。
webpack通过自己实现的require和module来兼容支持AMD、CommonJs、ES模块规范,不论是什么模块规范,webpackBootstrap启动函数的逻辑没有变更,唯一的变更就是模块是按照哪种的模块规范去暴露内容和加载依赖模块的,本文主要分析AMD、CommonJs、ES之间的互相兼容,主要分为:
而作为不同规范定义被加载的模块,实际的处理是不同的:
webpack支持通过import(*)的形式按需加载,按需加载会将加载的模块从输出文件中分割出来生成单独一个chunk,那么按需加载输出文件会有什么差异?
需要改造入口文件main.js,在main.js中按需加载show.js,代码如下:
import('./show.js').then((show) => {
show('Webpack');
});
依据webpack的配置,对于按需加载或加载外部资源来说,output.publicPath的设置是重要的,否则会找不到指定的文件,所以需要在配置文件中配置publicPath。
分析当前生成的输出文件,按照位置来分存在3处不同:
webpackBootstrap是webpack的启动函数,当没有按需加载时,该函数定义了require函数以及相关方法。存在按需加载,新增了:
webpackJsonp函数挂载在window对象下,其他都地方都可调用。
var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
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;
}
// moreModules对应就是按需加载的模块
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()();
}
};
该属性类似于installedModules功能,installedChunks这里记录所有chunk加载的情况,是加载中还是已加载等。
import()的具体实现,即按需加载的核心逻辑。
__webpack_require__.e = function requireEnsure(chunkId) {
var installedChunkData = installedChunks[chunkId];
// chunk已加载
if(installedChunkData === 0) {
return new Promise(function(resolve) { resolve(); });
}
// chunk正在加载中
if(installedChunkData) {
return installedChunkData[2];
}
// 未加载chunk的promise
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
installedChunkData[2] = promise;
// 通过