# 模块化的演化过程
# 1、文件划分方式
将每个功能及其相关状态数据各自单独放到不同的 JS
文件中,约定每个文件就是一个独立的模块。使用某个模块就将这个模块引入到页面中,一个 script
便签对应一个模块,然后直接调用模块中的成员(变量、函数)。
<!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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
缺点
- 模块直接在全局工作,大量模块成员污染全局作用域。
- 没有私有空间,所有模块内的成员都可以在外部被访问或者修改。
- 一旦模块增多,容易产生命名冲突。
- 无法管理模块与模块之间的依赖关系。
- 在维护的过程中也很难分辨每个成员所属的模块。
# 2、命名空间方式
具体做法就是将每个模块包裹成一个全局对象的形式实现。解决了命名冲突的问题,但是其他问题还在。
//a.js
window.moduleA={
methodA:function(){}
}
//b.js
window.moduleB={
methodA:function(){}
}
2
3
4
5
6
7
8
# 3、LIFE函数
具体做法就是将每个模块成员都放在一个立即执行函数所形成的私有作用域中,对于需要暴露给外部的成员,通过挂到全局对象的方式上实现。 这种方式带来了私有成员的概念,私有成员只能在模块内部通过闭包的形式访问。这解决了前面所提到的全局作用域污染和命名冲突的问题。
// a.js
;(function () {
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
}
window.moduleA = {
method1: method1
}
})()
2
3
4
5
6
7
8
9
10
11
12
13
# 4、LIFE依赖参数
这使得每一个模块之间的依赖关系变得更加明显。
// module-a.js
;(function ($) { // 通过参数明显表明这个模块的依赖
var name = 'module-a'
function method1 () {
console.log(name + '#method1')
$('body').animate({ margin: '200px' })
}
window.moduleA = {
method1: method1
}
})(jQuery)
2
3
4
5
6
7
8
9
10
11
12
13
这四个阶段确实解决了很多在前端领域实现模块化的问题,但是仍然存在一些没有解决的问题。最明显的就是 模块加载的问题 没有解决。上面额方式都是通过直接在 script
标签中引入模块,这意味着模块的加载不受代码的控制。更为理想的方式就是在页面中引入一个 JS
入口文件,其余用到的模块通过代码控制,按需加载。
模块化规范的出现的两点需求:
- 一个统一的模块化标准。
- 一个可以自动加载模块的基础库。
# 5、AMD(异步模块定义规范)
在 AMD
规范中约定每个模块通过 define()
函数定义,这个函数默认可以接收两个参数,第一个参数是一个数组,用于声明此模块的依赖项;第二个参数是一个函数,参数与前面的依赖项一一对应,每一项分别对应依赖项模块的导出成员,这个函数的作用就是为当前模块提供一个私有空间。如果在当前模块中需要向外部导出成员,可以通过 return
的方式实现。
// AMD规范定义一个模块
difine(['jquery','./a.js'],function($,moduleA){
return {
start:function(){
$("#btn").animate({left:'200px'})
}
}
})
2
3
4
5
6
7
8
Require.js
基于 AMD
推出了 Require.js
库,是一个非常强大的模块加载器。它还提供了一个 require()
函数用于自动加载模块。区别在于 require()
只能用来载入模块,而 define()
还可以定义模块。当 Require.js
需要加载一个模块时,内部就会自动创建 script
标签去请求并执行相应模块的代码。
require(["./a.js"],function(moduleA){
moduleA.start()
})
2
3
目前绝大多数第三方库都支持 AMD
规范,但是它使用起来相对复杂,而且 当项目中模块划分过于细致时,就会出现同一个页面对 js
文件的请求次数过多的情况,从而导致效率降低。
Sea.js
淘宝的推出的,只不过它实现的是另外一个标准,叫作 CMD
,这个标准类似于 CommonJS
,在使用上基本和 Require.js
相同,可以算上是重复的轮子。Sea.js
后来也被 Require.js
兼容了。
目前前端模块化规范的最佳实践方式也基本实现了统一。
- 在
Node.js
环境中,我们遵循CommonJS
规范来组织模块。 - 在浏览器环境中,我们遵循
ES Modules
规范。
模块打包工具出现的原因?
- 首先,我们所使用的
ES Modules
模块系统本身就存在环境兼容问题。 - 其次,模块化的方式划分出来的模块文件过多,而前端应用又运行在浏览器中,每一个文件都需要单独从服务器请求回来。零散的模块文件必然会导致浏览器的频繁发送网络请求,影响应用的工作效率。
- 最后,在前端应用开发过程中不仅仅只有 JavaScript 代码需要模块化,HTML 和 CSS 这些资源文件也会面临需要被模块化的问题。
模块打包工具解决的问题?
- 它需要具备编译代码的能力,也就是将我们开发阶段编写的那些包含新特性的代码转换为能够兼容大多数环境的代码,解决我们所面临的环境兼容问题。(ES6->ES5)
- 能够将散落的模块再打包到一起 ,这样就解决了浏览器频繁请求模块文件的问题。注意,只是在开发阶段才需要模块化的文件划分,因为它能够帮我们更好地组织代码。
- 它需要支持不同种类的前端模块类型,也就是说可以将开发过程中涉及的样式、图片、字体等所有资源文件都作为模块使用,这样我们就拥有了一个统一的模块化方案,所有资源文件的加载都可以通过代码控制,与业务代码统一维护,更为合理。(
file-loader、css-loader
)。
webpack解决的问题
- 能编译新特性的代码
- 能够将散落的模块打包到一起。
- 可以支持不同类型的前端模块类型。
- 代码拆分
- 按需加载
将应用中所有的模块按照我们的需要分块打包,这样一来,就不用担心全部代码打包到一起,产生单个文件过大,导致加载慢的问题。可以把应用初次加载所必需的模块打包到一起,其他的模块再单独打包,等到应用工作过程中实际需要用到某个模块,再异步加载该模块,实现增量加载,或者叫作渐进式加载。