解决什么问题
想弄清楚webpack的由来首先要知道webpack需要解决面临什么问题,任何工具的出现都是因为解决什么问题的必要。 随着前端项目的日益复杂,一些问题就会出现: 例如:
- 如果整个项目都放在一个文件里,不容易管理,开发和维护
- 全局变量命名冲突问题
- 依赖加载顺序问题和管理问题
模块化的发展
1. Stage 1 - 文件划分方式
最早我们会基于文件划分的方式实现模块化,也就是 Web 最原始的模块系统。使用某个模块将这个模块引入到页面中,一个 script 标签对应一个模块,然后直接调用模块中的成员(变量 / 函数)。
└─ stage-1
├── module-a.js
├── module-b.js
└── index.html
// module-a.js
function foo () {
console.log('moduleA#foo')
}
// module-b.js
var data = 'something'
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Stage 1</title>
</head>
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
// 直接使用全局成员
foo() // 可能存在命名冲突
console.log(data)
data = 'other' // 数据可能会被修改
</script>
</body>
</html>
缺点:
- 模块直接在全局工作,大量模块成员污染全局作用域;
- 没有私有空间,所有模块内的成员都可以在模块外部被访问或者修改;
- 一旦模块增多,容易产生命名冲突;
- 无法管理模块与模块之间的依赖关系;
- 在维护的过程中也很难分辨每个成员所属的模块。
2. Stage 2 – 命名空间方式
后来,我们约定每个模块只暴露一个全局对象,所有模块成员都挂载到这个全局对象中,这种方式就好像是为模块内的成员添加了“命名空间”,所以我们又称之为命名空间方式。
// module-a.js
window.moduleA = {
method1: function () {
console.log('moduleA#method1')
}
}
// module-b.js
window.moduleB = {
data: 'something'
method1: function () {
console.log('moduleB#method1')
}
}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Stage 2</title>
</head>
<body>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
// 模块成员依然可以被修改
moduleA.data = 'foo'
</script>
</body>
</html>
这种命名空间的方式只是解决了命名冲突的问题,但是其它问题依旧存在。
3. Stage 3 – IIFE
使用立即执行函数表达式(IIFE,Immediately-Invoked Function Expression)为模块提供私有空间。具体做法是将每个模块成员都放在一个立即执行函数所形成的私有作用域中,对于需要暴露给外部的成员,通过挂到全局对象上的方式实现。
// module-a.js
;(function () {
var name = 'module-a'
function method1 () {
name += 'hahha1'
console.log(name + '#method1')
}
window.moduleA = {
method1: method1
}
})()
// module-b.js
;(function () {
var name = 'module-b'
function method1 () {
name += 'hahha2'
console.log(name + '#method2')
}
window.moduleB = {
method1: method1
}
})()
这种方式带来了私有成员的概念,我们无法在模块外部修改变量name,只能在模块成员内通过闭包的形式访问,而且虽然模块都有name这个变量却不存在命名冲突问题。这就解决了前面所提到的全局作用域污染和命名冲突的问题。但是这确没有解决模块依赖关系。
- Stage 4 - IIFE 依赖参数
在 IIFE 的基础之上,我们还可以利用 IIFE 参数作为依赖声明使用,这使得每一个模块之间的依赖关系变得更加明显。
// module-a.js
;(function ($) { // 通过参数明显表明这个模块的依赖
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
window.moduleA = {
method1: method1
}
})(jQuery)
以上 4 个阶段是早期的开发者在没有工具和规范的情况下对模块化的落地方式,这些方式确实解决了很多在前端领域实现模块化的问题,但是仍然存在一些没有解决的问题。
Evolution最明显的问题就是:模块的加载。我们都是通过 script 标签的方式直接在页面中引入的这些模块,这意味着模块的加载并不受代码的控制,很难维护。试想一下,如果你的代码需要用到某个模块,如果 HTML 中忘记引入这个模块,又或是代码中移除了某个模块的使用,而 HTML 还忘记删除该模块的引用,都会引起很多问题和不必要的麻烦。
更为理想的方式应该是在页面中引入一个 JS 入口文件,其余用到的模块可以通过代码控制,按需加载进来。
- 模块化规范的出现
除了模块加载的问题以外,目前这几种通过约定实现模块化的方式,不同的开发者在实施的过程中会出现一些细微的差别,因此,为了统一不同开发者、不同项目之间的差异,我们就需要制定一个行业标准去规范模块化的实现方式。
再结合我们刚刚提到的模块加载的问题,我们现在的需求就是两点: 一个统一的模块化标准规范 一个可以自动加载模块的基础库
随着技术的发展,JavaScript 的标准逐渐走向完善,对前端模块化规范的最佳实践方式也基本实现了统一。 在 Node.js 环境中,我们遵循 CommonJS 规范来组织模块。 在浏览器环境中,我们遵循 ES Modules 规范。
- 模块打包工具的出现
模块化可以帮助我们更好地解决复杂应用开发过程中的代码组织问题,但是随着模块化思想的引入,我们的前端应用又会产生了一些新的问题,比如:
- 首先,我们所使用的 ES Modules 模块系统本身就存在环境兼容问题。尽管现如今主流浏览器的最新版本都支持这 特性,但是目前还无法保证用户的浏览器使用情况。所以我们还需要解决兼容问题。
- 其次,模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。
- 最后,谈一下在实现 JS 模块化的基础上的发散。随着应用日益复杂,在前端应用开发过程中不仅仅只有 JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。而且从宏观角度来看,这些文件也都应该看作前端应用中的一个模块,只不过这些模块的种类和用途跟 JavaScript 不同。