一、打包工具出现之前
在打包工具出现之前,我们在 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 等。
本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler) 。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph) ,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。