webpack现代化前端应用的基石
- 现阶段的大型应用就要求前端必须要有独立项目,独立的项目需要进行工程化获得足够的效率
- 具有复杂数据状态的应用开发过程就必须要有合适的框架,采用数据驱动的方式增加可维护性
- 复杂项目结构必须进行模块化管理,提高公共部分的可复用性,增加团队的并行协作能力
- 重复规律性的工作必须采用自动化工具,提高工作效率,避免人为错误
webpack与模块化开发
前端发展到前后端分离之后:前端项目作为一个独立的个体,已经具备了很高的复杂性,相对管理就变得很难,而在解决这一问题上,出现了前端模块化。将不同功能的代码划分为不同的模块单独维护。
webpack的本质是一个模块打包工具(万物皆可模块:万物皆可打包)
webpack解决了如何在前端项目中更加高效的管理和维护项目中的每一个资源
模块化的演进过程
1.Stage 1 - 文件划分方式
最早期实现模块化,是将每个功能及其状态的数据各自单独放到不同的js文件中,约定每个文件都是一个独立的模块。使用某个模块就将某个模块引入到的页面中,一个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 () {
console.log(name + '#method1')
}
window.moduleA = {
method1: method1
}
})()
// module-b.js
;(function () {
var name = 'module-b'
function method1 () {
console.log(name + '#method1')
}
window.moduleB = {
method1: method1
}
})()
这种方式带来了私有成员的概念,私有成员只能在模块成员内通过闭包的形式访问,这就解决了前面所提到的全局作用域污染和命名冲突的问题。
4. 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)
模块加载的问题
以上四种方式确实解决了一些问题,但是仍然有一些问题并未解决,并且被忽略了
<!DOCTYPE html>
<html>
<head>
<title>Evolution</title>
</head>
<body>
<script src="https://unpkg.com/jquery"></script>
<script src="module-a.js"></script>
<script src="module-b.js"></script>
<script>
moduleA.method1()
moduleB.method1()
</script>
</body>
</html>
最明显的问题就是:模块的加载。
我们都是通过 script 标签的方式直接在页面中引入的这些模块,这意味着模块的加载并不受代码的控制,时间久了维护起来会十分麻烦。试想一下,如果你的代码需要用到某个模块,如果 HTML 中忘记引入这个模块,又或是代码中移除了某个模块的使用,而 HTML 还忘记删除该模块的引用,都会引起很多问题和不必要的麻烦。
更为理想的方式应该是在页面中引入一个 JS 入口文件,其余用到的模块可以通过代码控制,按需加载进来。
模块化规范的出现
- 一个统一的模块化标准规范
- 一个可以自动加载模块的基础库
1.Node.js中的CommonJS规范:
一个文件就是一个模块,每个模块都有单独的作用域,通过module.exports导出成员,再通过require函数载入模块。注意:CommonJS仅支持在node环境中。
CommonJS约定是以同步的方式加载模块,因为Node.js执行机制是在启动时加载模块,执行过程中只是使用模块,所以这种方式没有问题。但是在浏览器端使用同步加载模式,会引起大量的同步模式请求,导致应用效率地下。
2.浏览器端的AMD(异步模块定义规范):
规范出现的同时,推出了一个非常出名的库,叫做Require.js,它实现了AMD模块化规范,本身也是一个非常强大的模块加载器
目前绝大多数第三方库都支持 AMD 规范,但是它使用起来相对复杂,而且当项目中模块划分过于细致时,就会出现同一个页面对 js 文件的请求次数过多的情况,从而导致效率降低。AMD 规范为前端模块化提供了一个标准,但这只是一种妥协的实现方式
模块化的标准规范
- 在Node.js环境中,我们遵循Common.js规范来组织模块
- 在浏览器环境中,我们遵循ESModules规范
1.模块打包工具的出现
模块化思想的进一步引入和发展,前端应用有产生了一些新的问题,比如:
- 使用ES Modules模块标准,存在兼容性性问题。尽管主流浏览器的最新版本都支持这一特性,但是我们无法保证用户所使用的浏览器情况。
- 模块化分出的模块文件过多,前端应用又运行在浏览器环境中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器频繁发送网络请求,影响应用的实际体验
- 随着前端应用的增大,不仅仅是js需要模块化,html和css也面临相同的问题
所以,我们理想的打包工具需要实现一些功能:
- 第一,它需要具备编译代码的能力,也就是将我们开发阶段编写的那些包含新特性的代码转换为能够兼容大多数环境的代码,解决我们所面临的环境兼容问题。
- 第二,能够将散落的模块再打包到一起,这样就解决了浏览器频繁请求模块文件的问题。这里需要注意,只是在开发阶段才需要模块化的文件划分,因为它能够帮我们更好地组织代码,到了实际运行阶段,这种划分就没有必要了。
- 第三,它需要支持不同种类的前端模块类型,也就是说可以将开发过程中涉及的样式、图片、字体等所有资源文件都作为模块使用,这样我们就拥有了一个统一的模块化方案,所有资源文件的加载都可以通过代码控制,与业务代码统一维护,更为合理。