1. 为什么需要webpack
在打包工具出现之前,在web网页中使用js代码如下所示:

上面html文件中,一般情况下,业务代码是需要依赖上面的第三方代码,根据js在浏览器的加载特性,所有的js代码是从上至下顺序加载的,因此,如果我们的业务代码依赖上面的那些库或者框架代码,那么顺序一定不能颠倒。如果颠倒,项目可能崩溃。
我们可以思考,如果我们将这些文件按照一些预定的顺序合并到一个文件里面,不就解决问题了吗。
再看一个html文档:

这个文档只加载了一个js文件,这个文件包含了11个文件,里面是项目的所有代码。这种方式虽然解决了我们上面加载多个js的问题,但可能会导致其他的问题。比如:作用域问题,文件太大的问题,可读性差,可维护性弱的问题。
作用域问题:我们使用过jquery, lodash,bootstrap的都知道,上面引入的外部库文件会分别在window对象上面绑定全局的变量,比如jquery可能会绑定 $, lodash可能会绑定 _ 等等。我们自己的业务文件,可能也会在全局上面绑定一些变量,这些变量会严重的污染window对象,使得window对象变得臃肿,这就是变量作用域问题。
文件太大问题:如果我们11个文件分散加载,那么页面的内容会随着文件的加载而逐渐显示内容,但是,如果我们将11个文件合并成1个文件,那这个脚本会带来网络瓶颈,用户得等待一段时间才能看到页面内容,会有短暂的白屏,用户体验非常差。
可读性差,可维护性弱的问题:如果我们将所有代码都合并在一个超大的文件里,对程序的可读性,可维护性带来很大的问题。
2. 如何解决作用域问题
早先前,使用 grunt 以及 gulp 两个工具来管理项目资源。这两个工具称为任务执行器,它们是将所有的项目文件拼接在一起,其实是利用了js的立即调用函数表达式。这样就解决了大型项目的作用域问题。

当脚本被封装在IIFE内部的时候,可以安全的拼接或者组合所有文件,而不必担心作用域冲突。那什么是IIFE:
新建app.js文件,在html中引入:
; (function () {
var myName = "前端";
})();
console.log(myName);
运行代码,结果如下:

说明当函数变成一个立即调用函数表达式的时候,表达式的变量是不能在外部访问的,也就是说不会污染window环境,也就解决了作用域问题。
如果想在window上去暴露一个变量或者内容该怎么做?
var result = (function () {
var myName = "前端";
return myName;
})();
console.log(result);
调整上方代码并运行结果如下:

这样我们既解决了作用域问题,又可以将我们想暴露的内容暴露在外部。但是如果我们想要去修改两段代码中的一段代码,那么我们得需重新编译这段代码,或者加入我们这段代码有10000行,但是我只改了一行,那么这个文件也会重新编译。
例如:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.js"></script>
</head>
<body>
<script>
const str = _.join(['前端', 'webpack5'], '-');
console.log(str); //前端-webpack5
</script>
</body>
</html>
上方代码,我们为了使用lodash的一个join方法,却吧lodash的整个库文件全都加载下来了,我们能不能将这个文件拆分成一个一个的方法模块,或者说实现一个方法的懒加载呢?
3. 如何解决代码拆分问题
首先得感谢nodejs,它使javascript模块诞生了。nodejs是一个JavaScript运行环境,可以在浏览器环境之外的计算机或者服务器上使用它。webpack其实就是运行在nodejs中的。既然我们不是在浏览器中运行javascript的,那么现在就没有了可以添加到我们浏览器的html文件以及我们编写的script标签了,那么nodejs是如何加载新的代码文件。commonjs的问世,引入了一个叫做require的机制。它允许在我们当前的文件中去加载和使用某个模块,导入需要的每个模块。这个开箱即用的功能帮助我们解决了代码拆分的问题。

例如:
新建math.js文件,添加两个方法,并将方法暴露出去
const add = (x, y) => {
return x + y;
}
const minus = (x, y) => {
return x - y;
}
module.exports = {
add,
minus
}
新建server.js,引入math模块,并使用其中的方法:
const math = require('./math.js');
console.log(math.add(4,5))
上述nodejs代码是不能直接在浏览器中运行的,只能在nodejs的环境里:

上面只使用了math的add方法就实现求和的功能,实现了模块化。
虽然commonjs是nodejs项目的绝佳模块拆分解决方案,但是浏览器是不支持这个模块化的。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="./commonjs/server.js"></script>
</body>
</html>
直接在html中引入server.js,并在浏览器打开,会发现nodejs的require方法失效了:

4. 如何让浏览器支持模块
究竟如何让浏览器支持模块呢?在早期,我们使用类似像browserify以及requirejs这样的打包工具编写能够在浏览器中运行commonjs的模块代码。

例如使用requirejs:
新建add.js和minus.js,分别添加如下代码:
const add = (x, y) => {
return x + y;
};
define([], function() {
return add
});
const minus = (x, y) => {
return x - y;
};
define([], function () {
return minus;
});
新建html文件,引入requirejs第三方依赖:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"
data-main="./requirejs/main.js">
</script>
</body>
</html>
新建main.js文件,使用require方法引入add和minus模块:
require(["./requirejs/add.js", "./requirejs/minus.js"], function (add, minus) {
console.log(add(4, 5));
});
script标签的data-main属性表示加载一个入口的js文件。在data-main中引入main.js。最后在浏览器运行html,结果如下:

这样我们会在浏览器上加载模块。
其实还有另外一个选择,模块正在成为ECMAScript的一个官方标准。例如:
新建add.js和minus.js,添加如下代码:
const add = (x, y) => {
return x + y;
};
export default add;
const minus = (x, y) => {
return x - y;
};
export default minus;
新建html文件,引入模块:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import add from './esm/add.js';
console.log(add(4,5));
</script>
</body>
</html>
运行得如下结果:

这样我们就使用ECMAScript的模块化实现定义和载入模块。但因为目前浏览器对它的支持还不是很完整。那是否有一种方式,不仅可以让我们编写模块而且还可以支持任何的模块格式?这个就是webpack了,它可以帮助我们打包javascript应用程序,并且同时支持es的模块化和commonjs,它还可以扩展,支持很多的静态资源打包。比如图片,字体文件,样式文件等等。
5. webpack与竞品
那么webpack是不是唯一的选择?当然不是,webpack也有竞品。

webapck vs PARCEL & rollup.js。
PARCEL号称零配置,用户一般无需做其他的配置,开箱即用。rollup.js用标准化的格式来编写代码,比如es6,通过减少无用的代码来尽可能的缩小代码包的体积,一般只能用来打包js。因此,如果想要构建一个简单的应用并且让它快速的运行起来,可以使用PARCEL。如果想要构建一个类库,只需导入很少的第三方的库,可以使用rollup.js。如果想要构建一个复杂的应用,并且想要集成很多的第三方的库,并且还要拆分代码,还要使用静态的资源文件,还要支持commonjs,esmodule等等,那只能是webpack了。
最后的黑马就是vite了。Vite 是一种新型前端构建工具,能够显著提升前端开发体验。vite的特点:
- 极速的服务启动:使用原生
ESM
文件,无需打包! - 轻量快速的热重载:无论应用程序大小如何,都始终极快的模块热重载(
HMR
) - 丰富的功能:对
TypeScript
、JSX
、CSS
等支持开箱即用。 - 优化的构建:可选 “多页应用” 或 “库” 模式的预配置
Rollup
构建 - 通用的插件:在开发和构建之间共享
Rollup-superset
插件接口。 - 完全类型化的API:灵活的 API 和完整 TypeScript 类型。