01. 为什么需要 Webpack

215 阅读3分钟

一、打包工具出现之前

在打包工具出现之前,我们在 Web 中使用 JavaScript 的方式:

1. 通过 script 标签引入脚本来存放每个功能

<!-- 引入外部的 JavaScript 文件 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.core.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/twitter-bootstrap/5.0.2/js/bootstrap.min.js"></script>

<!-- 引入自己的 JavaScript 文件 -->
<script src="./scripts/common.js"></script>
<script src="./scripts/user.js"></script>
<script src="./scripts/authentication.js"></script>
<script src="./scripts/product.js"></script>
<script src="./scripts/ .js"></script>
<script src="./scripts/payment.js"></script>
<script src="./scripts/checkout.js"></script>
<script src="./scripts/shipping.js"></script>

存在的问题:

  • 加载太多脚本会导致网络瓶颈
  • 改变脚本加载顺序可能导致项目崩溃

2. 引入包含所有功能的大型 js 文件

<!-- 合并所有 js 文件到一个 bundle.js 中并引入 -->
<script src="./scripts/bundle.js"></script>

存在的问题:

  • 作用域混乱
  • 文件太大,加载慢
  • 可读性与可维护性差

二、如何解决作用域问题

1. 使用立即调用函数表达式(IIFE)

假设存在 3 个文件:

// 文件 1
;(function () {
  var a = 1
  console.log('内部 a1:', a)
})()

// 文件 2
;(function () {
  var a = 2
  console.log('内部 a2:', a)
})()

// 文件 3
console.log('外部 a:', a)

// 内部 a1: 1
// 内部 a2: 2
// Uncaught ReferenceError: a is not defined

上面代码,由于文件 1 与文件 2 使用了 IIFE,即使两个文件中有相同变量名,代码执行也不会报错;且在文件 3 中,无法访问文件 1 与文件 2 中的变量。

IIFE 解决了作用域的问题,防止了变量的全局污染。

但这也导致了不能在一个文件中访问另一个文件的变量的问题,如何解决:

// 文件 1
var a1 = (function () {
  var a = 1
  return a
})()

// 文件 2
window.a2 = (function () {
  var a = 2
  return a
})()

// 文件 3
console.log('a1:', a1)
console.log('a2:', a2)

// a1: 1
// a2: 2

上面代码,文件 1 与 文件 2 分别将内部变量 return 出来放到全局变量或者 window 身上,文件 3 中就可以访问这两个文件中的变量。

2. 存在的问题

当我们要使用以 IIFE 的方式封装的文件时,就要将整个文件全部引入,即使我们只需要用到其中的一个方法。

以 lodash 为例,lodash 中封装了很多有用的方法,如果我们只需要用到 lodash 中的一个方法,也必须要加载整个 lodash 文件。

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>JS Bin</title>
  <script src="https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>
<body>
<script>
  const str = _.join(['aaa', 'bbb'], '-')
  console.log(str)    // aaa-bbb
</script>
</body>
</html>

三、如何解决代码拆分问题

1. CommonJS

在 nodejs 中,使用 CommonJS 的 require 机制,可以在某个文件中引入其他文件暴露出来的模块。

// math.js
const add = (x, y) => {
  return x + y
}
const minus = (x, y) => {
  return x - y
}
module.exports = { add, minus }

// calc.js
var math = require('./math.js')
var sum = math.add(1, 2)
console.log(sum)    // 3

然而,CommonJS 模块是为 Node.js 打包 JavaScript 代码的原始方式,在浏览器中并不支持。

2. Browserify 和 RequireJS

在早期,我们应用 Browserify 和 RequireJS 等打包工具编写能够在浏览器中运行的 CommonJS 模块。

以 RequireJS 为例:

// add.js
const add = (x, y) => {
  return x + y
}
define([], function() {
  return add
})

// minus.js
const minus = (x, y) => {
  return x - y
}
define([], function() {
  return minus
})

// main.js
require(['./requirejs/add.js', './requirejs/minus.js'], function(add, minus) {
  console.log(add(4, 5))    // 9
})
<!-- index.html -->
<script 
    src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.js"
    data-main="./requirejs/main.js"
></script>

上面的 data-main 属性指定了入口 js 文件。

3. ESModule

ECMAScript 标准的官方功能,但目前浏览器支持不够完整。

// add.js
const add = (x, y) => {
  return x + y
}
export default add

// minus.js
const minus = (x, y) => {
  return x - y
}
export default minus
<!-- index.html -->
<script type="module">
    import add from './add.js'
    console.log(add(4, 5))    // 9
</script>

上面的 type="module" 用于声明这个脚本是一个模块,只能在模块内部使用 import 和export 语句,不声明会报错。

五、Webpack 搞定这一切

Webpack 是一个工具,可以打包你的 JavaScript 应用程序(支持 ESModule 和 CommonJS),可以扩展为支持许多不同的静态资源,例如:images、fonts 和 stylesheets 等。

image.png

本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler) 。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph) ,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle