简单理解Webpack原理

前言

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.jsapp/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中是否有此模块的缓存,如果有,直接返回缓存的模块,没有,就创建一个对象同时保存到缓存中,然后执行modulemodulesmoduleId对应的函数,执行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原理脑图
以上即是自己对webpack原理的简单理解,如有错误之处还望指正,如对您有帮助欢迎点赞👍支持!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一拳小和尚LXY

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值