模块化是前端工程化中最重要的一部分,因为目前的前端项目中,单个文件里面的代码已经多到非常臃肿的地步,不利于开发和管理。而模块化开发是目前比较主流的解决方法,它通过将文件代码安装不同的功能来划分为不同的模块区域文件,以此来提高开发效率,降低维护成本
模块化本质上是一种思想,并不是一种具体的实现,需要的用其他的方式来实现代码的模块化
模块化演变流程
第一阶段:文件划分方式
不同模块写在不同的js文件中,使用的时候在html页面用不同的script标签引进来
早期模块化是完全依靠约定,代码量提升后就难以维护
var name = 'modules-a'
function method1 () {
console.log(name + 'modules-1')
}
function method2 () {
console.log(name + 'modules-2')
}
<script src="./modules-a.js">
</script><script src="./modules-b.js">
</script>
// 命名冲突
method1()
// 里面的数据可以被外部修改
name = 'foo'
缺点:
- 全部js中的变量、函数都是同一个全局作用域,污染全局作用域
- 里面的变量可以被外部随意修改
- 会发生命名冲突问题
- 无法管理模块之间的依赖关系
第二阶段:命名空间方式
在第一阶段的基础上,规定每一个文件只导出一个全局对象,文件里面的变量都挂载到这个全局对象上
单个文件里面导出一个对象,其他变量包在这个对象中,把作用域限定在这个对象中
这种方式可以解决命名冲突问题,但是还是没有私有空间,内部变量可以被修改
var moduleA = {
name: 'modules-a',
method1: function () {
console.log(this.name + 'modules-1')
},
method2: function () {
console.log(this.name + 'modules-2')
}
}
<script src="./modules-a.js"></script>
moduleA.method1()
// 里面的数据可以被外部修改
moduleA.name = 'foo'
缺点:
- 里面的变量可以被外部随意修改
- 无法管理模块之间的依赖关系
第三阶段:立即执行函数
将文件里面变量和函数都包在一个立即执行函数里面,把变量和函数限制在这个内部私有作用域,把需要暴露的成员,挂载在window全局对象上,这样可以把内部变量限制在私有作用域内,外部无法访问修改
;(function () {
var name = 'modules-a'
function method1 () {
console.log(name + 'modules-1')
}
function method2 () {
console.log(name + 'modules-2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})()
<script src="./modules-a.js"></script>
// 访问不到 undefined
console.log(moduleA.name)
缺点:
- 无法管理模块之间的依赖关系
第四个阶段:通过立即执行函数传入依赖
在立即执行函数调用时,把这个模块里面需要的依赖传入到参数里面,这样可以清晰地知道需要的依赖
;(function () {
var name = 'modules-a'
function method1 () {
console.log(name + 'modules-1')
}
function method2 () {
console.log(name + 'modules-2')
}
window.moduleA = {
method1: method1,
method2: method2
}
})()
<script src="./modules-a.js"></script>
// 访问不到 undefined
console.log(moduleA.name)
之前的方式都是依赖约定来实现的,并且模块的导入都是通过script标签引入的,模块的加载不受代码的控制,随着时间会变得越难维护,代码和html之间对于模块的加载没有办法确保可以相互对应起来,为此应该需要通过代码来维护
合适的模式是:模块化标准 和 模块加载器
模块化规范
CommonJS规范:node.js的标准
-
一个文件就是一个模块
-
每个模块都有单独的作用域
-
通过 module.exports 导出成员
-
通过 require 函数载入模块
var math = require('math') var path = require('path') // 需要等待模块require后才执行 console.log(path.join(__dirname, 'src')) math.add(2, 3)
CommonJs是以同步模式加载模块,node是在启动时加载模块,运行时不再加载模块,这种规范方式不适合用在浏览器中,避免页面刷新时同步加载模块卡顿
AMD(Asynchronous Module Definition):为浏览器设计的标准
Require.js库,实现了AMD规范
- 模块需要通过define来定义,通过return导出需要的成员
- 载入模块需要用require
大部分第三方库都实现了AMD规范
优点:
- 适合浏览器环境异步加载模块
- 可以并行加载多个模块
缺点:
AMD规范使用起来相对复杂,需要额外增加代码
不能按需加载,要在提前加载所有依赖后,才能回调
// 异步 导入
require(['math'], function (math) {
math.add(2, 3)
})
// 不需要等待加载
console.log('hello')
// 第一个参数为 模块标识 可选
define('myModule', ['module1', 'module2'], function (module1, module2) {
function foo () {}
return {
foo: foo
}
}
CMD(Common Modules Definition)和sea.js:通用模块规范,为浏览器设计的标准
和AMD规范相似,也是通过define定义模块,require导入模块
优点:
- 适合浏览器环境异步加载模块
- 模块按需加载
缺点:
- 模块加载受代码逻辑影响,不直观
- 依赖SPM打包
CMD和AMD区别:
CMD依赖是延时执行;AMD依赖的模块需要提前执行
CMD推崇依赖就近,用到再按需加载;AMD推崇依赖前置,定义时要先声明依赖
// CMD是按需加载的
define(function (require, exports, module) {
console.log('不用依赖就先执行的')
const math = require('math')
math.add(2, 3)
var status = true
if (status) {
// 动态引入
var module2 = require('module2')
}
// 导出
exports.foo = function () {
return 'hello'
}
}
ES6 模块
目前最佳的模块化方案是
浏览器环境:ES Modules 规范
node环境:CommonJS 规范
ES Modules 是 ES6的标准,一开始的浏览器都不支持这个规范,需要借助webpack等打包工具来实现,后期部分浏览器支持了ES Modules,可以在浏览器中直接使用
ES Modules是从语言规范上设计的标准,目前最主流的浏览器模块化规范