一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情
相信每个前端开发者都听说过webpack。作为前端开发最重要的构建工具,它极大地提高了我们的开发效率。
虽然网上有很多关于Webpack的教程,但由于Webpack本身的复杂性,很多初学者在阅读这些教程后仍然无法理解Webpack。
所以本文试着写一个更容易理解的教程,并制作一些动画。帮助更容易地理解它。
webpack做了什么?
Webpack的核心功能是将不同的JavaScript模块合并在一起。
假设我们有两个JavaScript文件:
add.js :
exports.default = function (a, b) {
return a + b
}
index.js :
var add = require('./add.js').default
console.log(add(1, 2))
它们使用CommonJS标准模块语法。如果我们直接使用nodejs来执行这段代码,它可以正常工作:
$ node index.js3
但是,如果它们被直接引用到HTML文件中,浏览器将无法正确执行以下代码:
<script src="./add.js"></script>
<script src="./index.js"></script>
这主要是因为CommonJS标准不是Web API的一部分。浏览器无法理解exports对象和require函数,因此无法正确执行上述代码。 为了解决这个问题,我们可以使用Webpack来打包代码。Webpack需要做的是将上述代码转换成浏览器可以理解的新版本,并且不更改原始的执行逻辑。 理解一个特性的最好方法是自己实现它。让我们看看相关功能是如何实现的。
实现Exports
如果我们想加载一个模块,我们只需要两个步骤:
- 读取文件的内容
- 然后将字符串转换为可执行代码
在读取文件内容时,我们可以使用类似
fs.readfileSync()的API。然后我们可以使用eval函数将文件中的字符串作为代码执行。所以我们可以把add.js转换为:
var exports = {}
eval('exports.default = function(a,b) {return a + b}')
浏览器可以理解这些代码:
我们可以用动画来表示这个过程:
然而,上面的实现有一个轻微的缺点,即如果在模块中声明了一个变量,那么它在eval之后将成为一个全局变量,从而污染全局命名空间。
为了解决这个问题,我们用立即调用的函数表达式来封装作用域:
var exports = {}
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
这就是Webpack处理导出的方式。
实现Require
那么如何编写require函数呢?require函数需要做的事情非常简单,即取出exports中的内容。
我们可以简化这个问题,假设我们只需要在add中加载内容。require函数可以这样写:
function require(file) {
var exports = {};
(function (exports, code) {
eval(code)
})(exports, 'exports.default = function(a,b){return a + b}')
return exports
}
如果我们为代码转换过程设置动画,它应该如下所示:
如果需要加载多个模块,那么我们应该将所有模块的文件名和代码字符串组织到一个键值表中,然后我们可以根据参数加载不同的模块:
let moduleList = {
"index.js": `
var add = require('add.js').default
console.log(add(1 , 2))
`,
"add.js": `exports.default = function(a,b){return a + b}`,
}
function require(file) {
var exports = {};
(function (exports, code) {
eval(code);
})(exports, moduleList[file]);
return exports;
}
require("index.js");
最后,为了避免引入变量moduleList,我们可以将上述代码编写为立即调用的函数表达式:
(function (list) {
function require(file) {
var exports = {};
(function (exports, code) {
eval(code);
})(exports, list[file]);
return exports;
}
require("index.js");
})({
"index.js": `
var add = require('add.js').default
console.log(add(1 , 2))
`,
"add.js": `exports.default = function(a,b){return a + b}`,
});
这里我们只是简单的实现require函数。
总过程
除了解决exports 和 require问题,Webpack还将做很多事情。
在打包过程中,Webpack还处理模块之间的依赖关系。它生成的依赖关系图如下所示:
{
"./src/index.js": {
"deps": { "./add.js": "./src/add.js" },
"code": "....."
},
"./src/add.js": {
"deps": {},
"code": "......"
}
}
此外,Webpack需要将ES6语法转换为ES5语法。 综上所述,Webpack打包的过程大致如下:
- 分析模块之间的依赖关系
- 将ES6转换为ES5
- 替代exports 和 require 我们可以用动画来表示这个过程: