05.webpack的模块化原理
webpack中mode配置
在使用webpack打包的过程中,如果不设置mode属性,那么每次执行npm run build的时候总会抛出一个警告,用来提示我们设置mode属性:
WARNING in configuration
The 'mode' option has not been set, webpack will fallback to 'production' for this value.
Set 'mode' option to 'development' or 'production' to enable defaults for each environment.
You can also set it to 'none' to disable any default behavior.
Learn more: https://webpack.js.org/configuration/mode/
以上这段话的意思是:在webpack.config.js配置文件中你还没有对mode也就是打包的模式做一个配置,但是webpack会默认应用当然的mode为production生产模式,除了production之外你还可以设置模式为development开发模式或者none不设置。
1. production 生产模式
mode设置为production生产模式之后,webpack会自动为我们开启很多默认的优化选项,并且会将DefinePlugin也就是配置全局常量的插件中process.env.NODE_ENV的值设置为production,同时为模块和chunk开启确定性的混淆名称,也就是会将代码在打包的时候进行混淆和压缩。设置之后默认开启的优化选项如下:
// webpack.production.config.js
module.exports = {
+ mode: 'production', // 开启此选项等于设置了下面这些配置
- performance: {
- hints: 'warning'
- },
- output: {
- pathinfo: false
- },
- optimization: {
- namedModules: false,
- namedChunks: false,
- nodeEnv: 'production',
- flagIncludedChunks: true,
- occurrenceOrder: true,
- sideEffects: true,
- usedExports: true,
- concatenateModules: true,
- splitChunks: {
- hidePathInfo: true,
- minSize: 30000,
- maxAsyncRequests: 5,
- maxInitialRequests: 3,
- },
- noEmitOnErrors: true,
- checkWasmTypes: true,
- minimize: true,
- },
- plugins: [
- new TerserPlugin(/* ... */),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
- new webpack.optimize.ModuleConcatenationPlugin(),
- new webpack.NoEmitOnErrorsPlugin()
- ]
}
2. development 开发模式
mode设置为development开发模式之后,webpack也会为我们开启很多默认的优化配置选项,并且会将DefinePlugin也就是配置全局常量的插件中process.env.NODE_ENV的值设置为development,但是打包后的代码中的变量及函数名称都是有效名称。
// webpack.development.config.js
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- cache: true,
- performance: {
- hints: false
- },
- output: {
- pathinfo: true
- },
- optimization: {
- namedModules: true,
- namedChunks: true,
- nodeEnv: 'development',
- flagIncludedChunks: false,
- occurrenceOrder: false,
- sideEffects: false,
- usedExports: false,
- concatenateModules: false,
- splitChunks: {
- hidePathInfo: false,
- minSize: 10000,
- maxAsyncRequests: Infinity,
- maxInitialRequests: Infinity,
- },
- noEmitOnErrors: false,
- checkWasmTypes: false,
- minimize: false,
- },
- plugins: [
- new webpack.NamedModulesPlugin(),
- new webpack.NamedChunksPlugin(),
- new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
3. none 不使用任何默认优化选项
// webpack.custom.config.js
module.exports = {
+ mode: 'none',
- performance: {
- hints: false
- },
- optimization: {
- flagIncludedChunks: false,
- occurrenceOrder: false,
- sideEffects: false,
- usedExports: false,
- concatenateModules: false,
- splitChunks: {
- hidePathInfo: false,
- minSize: 10000,
- maxAsyncRequests: Infinity,
- maxInitialRequests: Infinity,
- },
- noEmitOnErrors: false,
- checkWasmTypes: false,
- minimize: false,
- },
- plugins: []
}
webpack的模块化原理
webpack在打包代码的时候允许我们写的代码里面使用各种类型的模块化,最常用的是ES6 Module和CommonJS两个模块化标准,浏览器首先是不支持CommonJS标准的,并且低版本的浏览器也是不支持ES6 Module的,在高版本的浏览器中要支持ES6 Module也需要配置script标签的type=module才可以,那么思考一个问题:webpack是如何做到将打包之前浏览器不支持的模块化写法经过打包之后可以让大多数浏览器都支持的呢?所以就需要探讨下webpack的模块化实现原理:
- webpack是如何实现CommonJs模块化的?
- webpack是如何实现ES Module模块化的?
- webpack是如何实现在CommonJs模块中加载ES Module的?
- webpack是如何实现在ES Module中加载CommonJs模块的?
分析源码前的配置
-
设置mode为development避免混淆名称
在分析webpack打包后的文件bundle.js之前我们先将mode模式设置为development,因为这样可以保证打包之后的代码中模块名和变量名不会经过混淆丑化,便于我们对比打包前后的代码。 -
设置devtool为source-map避免将源代码转化为eval函数执行的字符串
mode设置为development代表着开启了很多优化配置选项,而其中有一条就是将devtool的值设置为eval,该项配置的作用就是将打包前的源代码在打包之后转化为一个代码字符串被eval()函数执行,而这对于调试和阅读源码是非常不友好的,所以我们需要将devtool的值先设置为source-map。
module.exports = {
mode:"development",
devtool:"source-map",
}
webpack实现CommonJs模块化源码分析
在utils.js中基于CommonJs语法导出两个函数:
function sum (a,b) {
return a+b;
}
function mul (a,b){
return a*b;
}
module.exports = {
sum,
mul
}
在项目入口文件main.js中导入:
const {sum,mul} = require('./js/CommonJS.js');
console.log(sum(10,20));
console.log(mul(10,20));
执行npm run build打包,虽然浏览器不支持require和module.exports语法,但是打包之后的代码是可以在浏览器中正确执行的,webpack在实现CommonJs模块化的时候,主要内部做了以下工作:
1. 定义__webpack_modules__对象
var __webpack_modules__ = {
"./src/js/CommonJS.js": (function(module) {
function sum(a, b) {
return a + b;
}
function mul(a, b) {
return a * b;
}
module.exports = {
sum,
mul
}
})
};
要点1:立即执行函数IEEF
bundle.js文件中最外层是一个立即执行函数,代表此文件只要被浏览器加载之后就会立即执行里面的代码,webpack在实现模块化原理的时候在很多地方使用了立即执行函数,只不过写法不同,主要有三种写法:
// 第一种写法:两个括号包裹
(function(...args){})(arg1,arg2);
// 第二种写法:一个大括号包裹
(function(...args){}(arg1,arg2));
// 第三种写法:将函数变为一个表达式,js引擎也会直接将该函数执行
!function(...args){}(arg1,arg2);
要点2:将要打包的模块分别要键值对进行映射
以上代码表示以模块的相对于根目录的路径为对象key值,以一个函数为value,这个函数接收一个module对象作为参数,函数体就是当前模块要导出的变量、函数等,最后在函数的最底部给module对象上添加了一个exports属性,并将要导出的变量依次添加在exports属性指向的对象中。
2. 定义缓存对象__webpack_module_cache__
模块缓存对象__webpack_module_cache__最主要的作用就是将已经通过下面的__webpack_require__函数加载过的模块返回的值添加到自己对象中,下次再通过__webpack_require__函数加载模块的时候就直接返回结果,避免模块的重复加载。
3. 定义用于加载模块核心函数__webpack_require__
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
/*
判断模块缓存对象中是否存在当前要加载的模块:
如果已经加载,则直接从__webpack_module_cache__对象中取出值返回
如果值为undefined表示没有加载,则继续执行后面代码
*/
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
/*
核心步骤:对象的连续赋值
1. 声明module变量并赋值为{exports: {}}
2. 给缓存对象中添加一个属性,属性名为唯一的模块ID也就是模块路径,属性值为{exports: {}}
重点在于将module和__webpack_module_cache__[moduleId]指向了同一个对象,也就是同一个内存地址,所以其中任意一个操作改变了对象中exports属性的值,另外一个会感知到。
*/
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
/*
核心步骤:加载和执行模块中代码
1. 读取模块代码:通过__webpack_modules__[moduleId]可以读取到一个函数,这个函数中包裹着模块中的代码。
2. 执行模块代码:执行上一步中读取到的函数并执行,执行的同时传入三个参数module, module.exports, __webpack_require__,这里暂时只用到第一个module对象,其余两个涉及到模块的交叉引用的时候才会用到。
3. 执行模块代码完成之后,就会为module对象中的exports参数指定一个对象作为值,对象里面存放着模块要导出的变量名或者说接口名。
*/
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/* 将上一步执行后的module.exports = {sum:fn,mul:fn}导出 */
return module.exports;
}
4. 启动执行函数
/*
!function(){}()是将函数变为表达式的写法,等于是一个立即执行函数
上面的代码都是函数或者变量的定义,这里才是真正加载模块的逻辑开始的地方,原理很简单就是执行加载模块核心函数__webpack_require__并将模块的路径也就是moduleID传入,并得到__webpack_require__函数的返回结果也就是一个导出接口的对象,如下:
{
sum,
mul,
}
*/
! function() {
/* 解构对象*/
const {
sum,
mul
} = __webpack_require__( "./src/js/CommonJS.js");
/* 执行函数 */
console.log(sum(10, 20));
console.log(mul(10, 20));
}();
webpack实现ES Module模块化源码分析
在utils.js中基于ES Module语法导出两个函数:
function sum (a,b) {
return a+b;
}
function mul (a,b){
return a*b;
}
export {
sum,
mul
}
在项目入口文件main.js中导入:
import {sum,mul} from "./js/ESModule.js";
console.log(sum(10,20));
console.log(mul(10,20));
1. 开启严格模式
webpack在对ES Module的模块进行打包的时候,在打包之后生成的bundle.js文件中还是由一个立即执行函数包裹,但是不同的是由于ES Module规定其内部默认开启严格模式,所以打包之后的立即执行函数最顶端会声明"use strict"代表当前采用严格模式。
2. 定义__webpack_modules__对象
同CommonJS处理方法,将模块的路径当做key,将一个函数当做value,webpack处理ES Module和CommonJS模块的区别就在于这个函数内部的逻辑不一样:
var __webpack_modules__ = ({
"./src/js/ESModule.js":
/**
* @param __unused_webpack_module :对应调用时的module,值为{exports:{}}
* @param __webpack_exports__ :对应调用时的module.exports,值为{}
* @param __webpack_require__ :对应加载模块的核心函数,函数也是一个对象,上面挂载o、r、d三个方法
*
* */
function(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
/*
调用__webpack_require__函数对象上的r方法
将最终module.exports导出的{}标记为一个ES Module
*/
__webpack_require__.r(__webpack_exports__);
/*
调用__webpack_require__函数对象上的d方法
将最终module.exports导出的{}做一层代理,代理过后在外部调用这个对象上的属性的时候,就会执行该属性对应的getter方法,getter方法的返回值才是最终读取该属性的值,也就是下面定义好的要导出的接口sum、mul等。
*/
__webpack_require__.d(__webpack_exports__, {
"sum": function() {
return sum;
},
"mul": function() {
return mul;
},
});
// 这是原本模块中要导出的接口
function sum(a, b) {
return a + b;
}
function mul(a, b) {
return a * b;
}
})
};
3. 定义缓存对象__webpack_module_cache__
var __webpack_module_cache__ = {};
4. 定义用于加载模块核心函数__webpack_require__
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对应的函数,并依次传入三个参数:
1. module:{exports:{}}
2. module.exports:空对象{}
3. __webpack_require__:当前函数本身
执行此函数的过程中做了两件事:
1. 为最终导出对象打一个ES Module的标记
2. 将要导出的接口依次添加到导出的对象上,然后做了一层代理
*/
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
/* 将经过上一步处理后的 module.exports对象返回 */
return module.exports;
}
5. 给__webpack_require__函数对象添加d方法
d方法的作用是对
/* 立即执行函数的表达式写法 */
! function() {
/**
* @param exports 是最终module.exports导出的{}
* @param definition 是一个对象,对象中的每一个key都是模块中需要导出的变量名,变量值就是对应的变量值
*
* */
__webpack_require__.d = function(exports, definition) {
for (var key in definition) {
/*
o函数就是用来判断当前对象是否存在某个属性的
如果definition对象中存在key并且最终module.exports导出的对象不包含key
那么就对definition对象中的所有key值做一层代理
*/
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
/* 依次将导出的接口添加到最终要导出的对象exports上,这是比较核心的代码 */
Object.defineProperty(exports, key, {
// 调用defineProperty方法定义属性时候不显式声明就默认为false,就会导致无法被迭代
enumerable: true,
// 在访问key属性的时候,调用getter函数,对应的值就是一个个的函数或者js值
get: definition[key]
});
}
}
};
}();
6. 给__webpack_require__函数对象添加r方法
r方法的本质是webpack对当前加载的模块做一个标记,记录当前加载的模块是一个ES Module。
r方法不返回任何值,它只是将传入的exports对象做一层标记,经过这个方法处理后的对象会被标记为一个ES Module,具体的实现就是调用toString的时候返回Module或者访问对象的__esModule属性会返回true。
! function() {
/**
* @param exports 执行加载函数__webpack_require__时传入的空对象,这个对象最终经过处理之后存放的就是要导出给外部的变量
*
* */
__webpack_require__.r = function(exports) {
/*
如果执行此代码的环境支持Symbol,就将exports对象上的Symbol.toStringTag的内置属性值定义为'Module',这样做的意义在于将一个对象调用toString方法的时候就会优先返回Module告诉这是一个ES Module
*/
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: 'Module'
});
}
/*
如果执行此代码的环境不支持Symbol,那么就直接将exports对象添加一个'__esModule'属性并且将其值设置为true,作用一样都是记录这是一个ES Module
*/
Object.defineProperty(exports, '__esModule', {
value: true
});
};
}();
7. 给__webpack_require__函数对象添加o方法
o方法的作用是一个辅助函数,用于检测对象中是否存在某个属性,如果存在返回true,否则返回false。
其实Object.prototype.hasOwnProperty.call(obj, prop)这种写法的另外一个写法就是:obj.hasOwnProperty(prop);本质都是一样用来检测当前对象是否包含属性prop的。
! function() {
/**
*
* @param obj 要检测的对象
* @param prop 要检测的属性
*
* */
__webpack_require__.o = function(obj, prop) {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
}();
8. 入口启动函数
var __webpack_exports__ = {};
! function() {
/* 给当前要加载的模块标记为ES Module */
__webpack_require__.r(__webpack_exports__);
/* 执行模块加载函数__webpack_require__ */
var _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/js/ESModule.js");
/*
(0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 20)
这种写法就等于:
_js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum(10, 20)
*/
console.log((0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.sum)(10, 20));
console.log((0, _js_ESModule_js__WEBPACK_IMPORTED_MODULE_0__.mul)(10, 20));
}();
9. 最终呈现
经过上述操作之后,一个ES Module经过webpack处理之后最终的呈现如下:
import * as demo from "./utils.js";
console.log(demo);
{
c: 100
mul: ƒ mul(a,b)
sum: ƒ sum(a,b)
__esModule: true // 经过r方法打上的标记
Symbol(Symbol.toStringTag): "Module" // 经过r方法打上的标记
get c: ƒ () // 经过d方法实现的getter代理
get mul: ƒ () // 经过d方法实现的getter代理
get sum: ƒ () // 经过d方法实现的getter代理
}
webpack是实现在CommonJs模块中加载ES Module
- 搞清楚互相加载的到底是如何实现的
- CommonJS语法到底是怎样的?
- ES Module和CommonJS的比较
- 前端模块化
- 再看看Vue Press博客是如何搭建的