前言

随着项目越来越复杂,我们写的代码也越来越多了。代码越来越难维护,全局的命名污染,依赖不明确等等。
为了解决此问题,衍生有许多模块化规范。前端模块化发展历程: 无模块化 ==> CommonJS规范 ==> AMD规范 ==> CMD规范 ==> ES6模块化

模块化是个啥?

在ES5中,只有全局作用域和函数作用域,一旦在函数体中声明变量没有添加 var,就会造成全局污染。在ES6中有了letconst,这样就有了块作用域。
模块化:指解决一个复杂的问题时自顶向下把系统划分成若干模块的过程,有多种属性,分别反映其内部特性;将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起;块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;
模块化组成:数据(内部属性),操作数据的行为(内部函数)。
ES5中怎么实现模块化?

  • 原始写法:无模块可言,会污染全局作用域,看不出依赖关系。
    1
    2
    3
    4
    5
    6
    function foo(){
    //doSomethings
    }
    function bar(){
    //doSomethings
    }
  • 对象写法:对象会暴露所有模块成员,内部状态可以被外部改写。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var myModule = new Object({
    count:0,
    foo:function(){
    //doSomethings
    },
    bar:function(){
    //doSomethings
    }
    })
  • 立即执行函数(IIFE):避免暴露私有成员,数据是私有的, 外部只能通过暴露的方法操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var myModule = (function(){
    var count = 0
    var increaseCount = function(){
    count++
    //doSomethings
    },
    var decreaseCount = function(){
    count--
    //doSomethings
    }
    return {increaseCount,decreaseCount} //ES6简写,ES5 ==> {increaseCount:increaseCount,...}
    })()
  • **IIFE的增强(依赖引入)
    1
    2
    3
    4
    5
    6
    7
    8
    var myModule = (function($){
    var _$body = $("body")
    var logBody = function(){
    console.log(_$body)
    //doSomethings
    }
    return {logBody}
    })(jQuery)

    无模块化

    比如以下代码:
    1
    2
    3
    4
    <script src="jquery.js"></script>
    <script src="bootstrap.min.js"></script>
    <script src="main.js"></script>
    <script src="do.js"></script>
    简单粗暴引入文件即可,但是顺序不能错,因为后面的代码依赖着前面的代码。而且代码变量的命名可能冲突,可能在main.js里面声明了变量foodo.js文件里面又声明了,或者直接用了该变量,又或者修改了该变量。
    无模块化带来的问题:
  1. 污染全局作用域
  2. 依赖关系不明显
  3. 维护成本高

CommonJS规范

该规范最初是用在服务器端的node的,它有四个重要的环境变量为模块化的实现提供支持:moduleexportsrequireglobal
实际使用时,用module.exports定义当前模块对外输出的接口(不推荐直接用exports),用require加载模块(同步)。注意这是同步加载,在浏览器同步加载是会阻塞的,所以在浏览器不用此规范。
优点:解决了依赖、全局变量污染的问题;CommonJS用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,CommonJS不适合浏览器端模块
module.exportsexports的区别:module.exports导出一个对象,exports可以导出多个对象。不过 module.exports.foo == exports.foo也就说exports.属性会自动挂载到没有命名冲突的module.exports.属性,但是不要给exports赋值,一旦有了新值,它就不再绑定到module.exports;

1
2
3
module.exports = {foo: 'bar'}  //正确
module.exports.foo = 'bar' //正确
exports = {foo: 'bar'} //error 这种方式是错误的,相当于重新定义了exports
1
2
3
4
5
6
// 这是add.js文件
const c = 5
const add = function(a,b){
return a + b + c
}
module.exports = {Add:add,c}
1
2
3
4
5
6
// 这是main.js文件
const addFn = require('./add')
console.log(addFn.Add(5,9),addFn.c)//19,5
//ES6解构赋值,导入
const {Add,c} = require('./add')
console.log(Add(5,9),c)//19,5

AMD规范

AMD是”Asynchronous Module Definition”的缩写,意思就是”异步模块定义”。
它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行
AMD也采用require()语句加载模块,但是不同于CommonJS;
基本语法:

  1. 定义暴露模块: define([依赖模块名], function(){return 模块对象});
  2. 引入模块: require(['模块1', '模块2', '模块3'], function(m1, m2){//使用模块对象})
  3. 指定引用路径:require.config()
    require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写;
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //页面引入  打印I am LiSi;I am 18 years old
    <script data-main="js/main.js" src="https://cdn.bootcdn.net/ajax/libs/require.js/2.3.6/require.min.js"></script>
    js/logFn.js文件
    define(function(){
    var logName = function(name){
    console.log(`I am ${name}`)
    }
    var logAge = age => {
    console.log(`I am ${age} years old`)
    }
    return {logName,logAge}
    })
    js/main.js文件
    require(['logFn'],function(logFn){
    logFn.logName('LiSi')
    logFn.logAge(18)
    })
    优点:适合在浏览器环境中异步加载模块、并行加载多个模块;
    缺点:不能按需加载、开发成本大;

CMD规范

AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。
在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
说白了就是AMD引入依赖要先执行依赖再跳出执行主程序,而CMD则需要的时候引入,执行完主程序再回来执行依赖;CMD是按需加载,就近原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//页面引入 
//module1 show() this is module1 msg
//异步引入依赖模块1 this is module1 msg
<script src="https://cdn.bootcdn.net/ajax/libs/seajs/3.0.3/sea.js"></script>
<script type="text/javascript">
seajs.use('./js/main.js')
</script>
js/module1.js文件
define(function (require, exports, module) {
//内部变量数据
var data = 'this is module1'
var msg = 'this is module1 msg'
//内部函数
function show() {
console.log('module1 show() ' + data)
}
//向外暴露
exports.show = show
exports.msg = msg
})
js/main.js文件
define(function (require, exports, module) {
//引入依赖模块(同步)
var module1 = require('./module1')
function show() {
console.log('module1 show() ' + module1.msg)
}
show()
//引入依赖模块(异步)
require.async('./module1', function () {
console.log('异步引入依赖模块1 ' + module1.msg)
})
})

ES6模块化

在ES6中,我们可以使用 import 关键字引入模块,通过 exprot 关键字导出模块,功能较之于前几个方案更为强大,也是我们所推崇的;
但是由于ES6在一些浏览器中无法执行,所以,我们只能通过babel将不被支持的import编译为当前受到广泛支持的 require。
基本用法:

  1. export 命令用于规定模块的对外接口;
  2. import 命令用于输入其他模块提供的功能。
  3. export default 暴露一个对象 export暴露多个对象
    es6在导出的时候有一个默认导出,export default,使用它导出后,在import的时候,不需要加上{},模块名字可以随意起。该名字实际上就是个对象,包含导出模块里面的函数或者变量。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //add.js
    export const add = (a,b) =>{
    return a + b
    }
    import {add} from 'add.js'
    //add.js
    export default const add = (a,b) =>{
    return a + b
    }
    import add from 'add.js'

CommonJS规范和 ES6的区别

  1. CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  2. CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。