JustYeh的前端博客

JavaScript模块化开发

2019-07-02

模块化js

通俗易懂的介绍了模块化概念,以及使用模块化编程的必要性。详细介绍了实现模块化开发的演变过程。

一、为什么会有模块化

1.当一个项目开发的越来越复杂的时候,会遇到一些问题,比如:

命名冲突

当项目由团队进行协作开发的时候,不同开发人员的变量和函数命名可能相同;即使是一个开发,当开发周期比较长的时候,也有可能会忘记之前使用了什么变量,从而导致重复命名,导致命名冲突。

文件依赖

代码重用时,引入js文件的数目可能少了,或者引入的顺序不对,比如使用boostrap的时候,需要引入jQuery,并且jQuery的文件必须要比boostrap的js文件先引入。

2.当使用模块化开发的时候可以避免以上的问题,并且让开发的效率变高,以及方便后期的维护:

提升开发效率

代码方便重用,别人开发的模块直接拿过来就可以使用,不需要重复开发法类似的功能。

方便后期维护

代码方便重用,别人开发的模块直接拿过来就可以使用,不需要重复开发法类似的功能。

所以总结来说,在生产角度,模块化开发是一种生产方式,这种方式生产效率高,维护成本低。从软件开发角度来说,模块化开发是一种开发模式,写代码的一种方式,开发效率高,方便后期维护。

二、模块化开发的演变过程

1. 全局函数
function m1(){
    //...
}
function m2(){
    //...
}

在早期的开发过程中就是将重复的代码封装到函数中,再将一系列的函数放到一个文件中,这种情况下全局函数的方式只能认为的认为它们属于一个模块,但是程序并不能区分哪些函数是同一个模块,如果仅仅从代码的角度来说,这没有任何模块的概念。

存在的问题:

  • 污染了全局变量,无法保证不与其他模块发生变量名冲突。
  • 模块成员之间看不出直接关系。
2. 对象封装-命名空间
var module1 = new Object({
    _count : 0,
    m1 : function (){
        //...
    },
    m2 : function (){
        //...
    }
});

通过添加命名空间的形式从某种程度上解决了变量命名冲突的问题,但是并不能从根本上解决命名冲突。 不过此时从代码级别可以明显区分出哪些函数属于同一个模块。

存在的问题:

  • 暴露了所有的模块成员,内部状态可以被外部改写,不安全。
  • 命名空间越来越长。
3. 立即执行函数写法

使用"立即执行函数"(Immediately-Invoked Function Expression,IIFE),可以达到不暴露私有成员的目的。

var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();

特点:

  • 利用此种方式将函数包装成一个独立的作用域,私有空间的变量和函数不会影响到全局作用域。
  • 以返回值的方式得到模块的公共成员,公开公有方法,隐藏私有空间内部的属性、元素,比如注册方法中可能会记录日志。
  • 可以有选择的对外暴露自身成员。
  • 从某种意义上来说,解决了变量命名冲突的问题。
4. 放大模式
// 原有方法
var module1 = (function(){
var _count = 0;
var m1 = function(){
//...
};
var m2 = function(){
//...
};
return {
m1 : m1,
m2 : m2
};
})();

// 新增需求
var module1 = (function (mod){
mod.m3 = function () {
//...
};
return mod;
})(module1);

特点:

  • 利用此种方式,有利于对庞大的模块的子模块划分。
  • 实现了开闭原则:对新增开发,对修改关闭。对于已有文件尽量不要修改,通过添加新文件的方式添加新功能。
5. 宽放大模式

在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

var module1 = ( function (mod){
//...
return mod;
})(window.module1 || {});

与"放大模式"相比,"宽放大模式"就是"立即执行函数"的参数可以是空对象。

总结:在什么场景下使用模块化开发:

  • 业务复杂
  • 重用逻辑非常多
  • 扩展性要求较高

三、模块化规范

CommonJS

服务器端规范主要是CommonJS,node.js用的就是CommonJS规范:

定义模块

根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。

模块输出

模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象

加载模块

加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象

看个例子:

//模块定义 myModel.js
var name = 'Byron';
function printName(){
    console.log(name);
}
function printFullName(firstName){
    console.log(firstName + name);
}
module.exports = {
    printName: printName,
    printFullName: printFullName
}

// 加载模块
var nameModule = require('./myModel.js');
nameModule.printName();

客户端规范

客户端规范主要有:AMD(异步模块定义,推崇依赖前置)、CMD(通用模块定义,推崇依赖就近)。AMD规范的实现主要有RequireJs,CMD规范的主要实现有SeaJs。SeaJs在国内用的比较多,但SeaJs已经停止维护了,就多做多的介绍。RequireJs是使用最广泛的模块化的实现方案,关于RequireJs的介绍,请参考本站文章:RequireJS参考手册

AMD与CMD区别

下面的回答来自SeaJs的作者玉伯

  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.

  • CMD 推崇依赖就近,AMD 推崇依赖前置。看代码:

    // CMD
    define(function(require, exports, module) {
      var a = require('./a')
      a.doSomething()
      // 此处略去 100 行
      var b = require('./b') // 依赖可以就近书写
      b.doSomething()
      // ... 
    })
    

// AMD 默认推荐的是 define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() ... }) ``` 虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。

  • AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。