前言
webpack
是一个代码编译打包工具,有入口,出口、loader和插件等,大多数前端开发人员能够熟练的使用webpack
管理我们的代码,但我们可能还没有尝试理解过webpack
编译的原理,让我们怀着好奇(`ヘ´)=3
的心态通过阅读编译后的源码来尝试理解一下webpack
编译的基本原理吧ヾ(◍°∇°◍)ノ゙
。
以下面的代码为例:
index.html
<!--index.html-->
<html>
<head>
<title>Hello World</title>
<script src='./main.js' />
</head>
<body>
<div id='app'></div>
</body>
</html
./app/main.js
let message=require('./module1.js');
let app = document.getElementById("app");
app.innerHTML += `<span class="title">${message}</span>`;
./app/module1.js
let message = "Hello World"
module.exports = message;
webpack配置如下:
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode:'development',
/*编译配置*/
devtool: 'eval-source-map',
/* 入口文件 */
entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
output: {
path: __dirname + "/dist",//打包后的文件存放的地方
filename: "[name].js"//打包后输出文件的文件名
},
/*开发服务配置*/
devServer: {
contentBase: "./dist",//本地服务器所加载的页面所在的目录
historyApiFallback: true,
inline: true
},
plugins:[
new HtmlWebpackPlugin({ // 打包输出HTML
title: 'Hello World app',
minify: { // 压缩HTML文件
removeComments: true, // 移除HTML中的注释
collapseWhitespace: true, // 删除空白符与换行符
minifyCSS: true// 压缩内联css
},
filename: 'index.html',
template: 'index.html'
}),
],
/*配置loader*/
module: {
rules: [
{
//babel-loader转换指定文件到浏览器能识别的js
test: /(\.jsx|\.js)$/,
use: [
{
loader: "babel-loader",
},
{
loader:"force-strict-loader",
options:{
sourceMap:true
}
}
],
exclude: /node_modules/ //不处理的位置
},
]
}
};
上面的代码会将./app/main.js
和app/module1.js
打包成./dist/main.js
,并在index.html
文件中引入。
编译后代码
接下来我们阅读编译之后的./dist/main.js
代码,初步理解打包编译原理。为了更加便于阅读和理解,我们对代码进行了简化,代码如下。
(
function (modules) {
//已加载的模块的缓存
let installedModule = {};
//CommonJS模块加载实现的核心方法
function __webpack_require__(moduleId) {
if (installedModule[moduleId]) {
//模块加载过,直接返回缓存中的模块
return installedModule[moduleId];
}
let module = installedModule[moduleId] = {
id: moduleId,
loaded: false, //是否已加载完成
exports: {} //此文件模块对外的接口
}
//调用指定模块的加载函数,这个函数会执行模块代码,并为module.exports赋值
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
module.loaded = true; //加载完成
//返回此模块的导出值
return module.exports;
}
//加载入口文件
return __webpack_require__(entry);
}
)({
'./app/main.js': function (module, __webpack_exports__, __webpack_require__) {
//module1的CommonJs导入语句会编译成调用__webpack_require__加载函数的形式,
//返回module1的导出值,即moduleId='./app/module1.js'的module对象的expots属性
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./module1 */
"./app/module1.js");
//读取文件内容
let app = document.getElementById("app");
app.innerHTML += `<span class="title">${_module1__WEBPACK_IMPORTED_MODULE_0__}</span>`;
},
'./app/module1.js': function (module, __webpack_exports__, __webpack_require__) {
let message = "Hello World"
//修改本身module对象的exports值,作为导出值
module.exports = message;
}
});
首先webpack把每个文件编译成一个自执行的函数,形成一个独立的作用域范围。这个函数接收一个modules数组对象,这个数组对象是一个对应js文件的键值对。
几个需要注意的关键点:
entry
:入口文件installedModule
:模块缓存对象__webpack_require__
:CommonJs模块系统的模拟实现
简化代码
为了使代码和执行流程更加直观,进一步对代码进行简化。
//入口文件
let entry = './app/main.js';
let installedModule={};//已加载的模块的缓存
//CommonJS模块加载实现
function __webpack_require__(moduleId){
if(installedModule[moduleId]){
//模块加载过,直接返回缓存中的模块
return installedModule[moduleId];
}
//构造一个新的module对象
let module=installedModule[moduleId]={
id:moduleId,
loaded:false,//是否已加载完成
exports:{} //此文件模块对外的接口
}
//调用指定模块的加载函数,这个函数会执行模块代码,并为module.exports赋值
modules[moduleId].call(module.exports,module,module.exports,__webpack_require__);
module.loaded=true;//加载完成
//返回此模块的导出值
return module.exports;
}
//webpack编译后生成每个js=文件对应的一个对象,是一个{文件名:执行函数}的键值对,
//在函数中执行本身的函数和递归调用__webpack_require__来加载它所依赖的其他模块
let modules={
'./app/main.js':function ( module, __webpack_exports__ , __webpack_require__){
//module1的CommonJs导入语句会编译成调用__webpack_require__加载函数的形式,
//返回module1的导出值,即moduleId='./app/module1.js'的module对象的expots属性
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./module1 */ "./app/module1.js");
let app = document.getElementById("app");
app.innerHTML += `<span class="title">${_module1__WEBPACK_IMPORTED_MODULE_0__}</span>`;
},
'./app/module1.js':function ( module, __webpack_exports__ , __webpack_require__){
let message="Hello World"
//修改本身module对象的exports值,作为导出值
module.exports=message;
}
};
// 加载入口文件
__webpack_require__(entry);
参考给出的注释,上面的代码就很容易理解了,这里简单再解释一下。
- js文件编译后会生成一个如
modules
的对象,这个对象是所有js文件编译后的源码的集合,使用键值对的形式表示,键为文件路径,值为一个执行源码的函数; - 每个js文件对应一个module对象,每个对象有一个
exports
属性,这个exports
属性就是用来导出文件内容的对外接口;
{
id:moduleId,
loaded:false,//是否已加载完成
exports:{} //此文件模块对外的接口
}
installedModule
是一个module
对象的集合,通过键值对的方式保存了所有的module
的缓存。- 当我们通过
__webpack_require__(entry)
加载入口文件时,我们首先检查installedModule
中是否有此模块的缓存,如果有,直接返回缓存的模块,没有,就创建一个对象同时保存到缓存中,然后执行module
与modules
中moduleId
对应的函数,执行module
源码,并通过修改module.exports
属性导出此模块中的值。 - 如果js文件中有对其他文件的依赖,即require引用其他文件,转译为形如
var _module1__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./app/module1.js");
的语句,继续调用__webpack_require__
函数引入模块,__webpack_require__
函数返回当前引入模块的exports
属性。
webpack帮我们做的是就是把我们编写的源代码编译成了上述代码的形式,理解了编译后的代码,我们就大致理解webpack在编译时需要做如下几件事:
- 读取js文件内容
- 遇到
require
行转换成__webpack_require__
函数的形式 - 将文件内容封装成一个对象,键为文件路径,值为一个执行编译后js源码的函数
上面👆的流程只是js文件的大致编译流程,真正的项目中会有多个loader用于编译不同类型的文件,如vue-loader
用来编译.vue
文件,css-loader
用来编译css
相关代码和文件,ts-loader
用来编译.ts
文件。webpack
会根据modules
中配置的loader
去编译不同类型的文件。
最后
本文参考下面的文章👇,加入自己的理解和代码整理。同时也整理了一份下面这篇博文中webpack原理相关的脑图,方便记忆和理解流程,供大家参考。
参考文章:面试官:webpack原理都不会?
以上即是自己对webpack原理的简单理解,如有错误之处还望指正,如对您有帮助欢迎点赞👍支持!