前言

我们再一次被计算机的名词、概念笼罩。

Backbone、Emberjs、Spinejs、Batmanjs 等MVC框架侵袭而来。
CommonJS、AMD、NodeJS、RequireJS、SeaJS、Curljs 等模块化的JavaScript概念及库扑面而来。

模块化JavaScript的概念尤为突出,似乎有赶超07年Ajax风潮之趋势。

写函数(过程式)
2005年以前,JavaScript没人重视,只作为表单验证等少量应用。那时一个网页上写不了几行JS代码,1000行算很复杂了。这时组织代码的方式是过程式,几十行的代码甚至连一个函数都不用写。稍多的需要提取抽象出一个函数,更复杂一些则需要更多函数,函数间互相调用。

写类(面向对象)
2006年,Ajax席卷全球。JavaScript被重视了,越来越多的后端逻辑放到了前端。网页中的JS代码量急剧增加。这时写函数方式组织大量代码显得力不从心。有时调试一个小功能,从一个函数可能会跳到第N个函数去。这时写类的方式出现了,Prototype 率先流行开来。用它组织代码,写出的都是一个个类,每个类都是Class.create创建的。又有YUI、Ext等重量级框架。虽然它们的写类方式各不同,但它们的设计思路却都是要满足大量JavaScript代码的开发。

写模块(现在,未来?)
2009年,Nodejs诞生!这个服务器端的JavaScript采用模块化的写法很快征服了浏览器端的JSer。牛人们纷纷仿效,各种写模块的规范也是层出不穷。CommonJS想统一前后端的写法,AMD则认为自己是适合浏览器端的。好吧,无论写模块的风格是啥样,写模块化的JavaScript却已开始流行了。你,准备好了吗?

模块化的JavaScript是神马? 这是我们发明了又一个银弹吗?无论是啥,就当学习吧。至于适不适合项目中使用,各自斟酌。

写到这也没说什么是“模块”。其实在计算机领域,模块化的概念被推崇了近四十年。软件总体结构体现模块化思想,即把软件划分为一些独立命名的部件,每个部件称为一个模块,当把所有模块组装在一起的时候,便可获得问题的一个解。模块化以分治法为依据,但是否意味着我们把软件无限制的细分下去?事实上当分割过细,模块总数增多,每个模块的成本确实减少了,但模块接口所需代价随之增加。

要确保模块的合理分割则须了解信息隐藏,内聚度及耦合度。

信息隐藏
模块应设计的使其所包含的信息(过程和数据)对于那些不需要用到它的模块不可见。每个模块只完成一个独立的功能,然后提供该功能的接口。模块间通过接口访问。JavaScript中可以用函数去隐藏,封装,而后返回接口对象。如下是一个提供事件管理的模块event。

 
1
2
3
4
5
6
7
8
Event = function() {
    // do more
    return {
        bind: function() {},
        unbind: function() {},
        trigger: function() {}
    };
}();

函数内为了实现想要的接口bind、unbind、trigger可能需要写很多很多代码,但这些代码(过程和数据)对于其它模块来说不必公开,外部只要能访问接口bind,unbind,trigger即可。
信息隐藏对于模块设计好处十分明显,它不仅支持模块的并行开发,而且还可减少测试或后期维护工作量。如日后要修改代码,模块的隐藏部分可随意更改,前提是接口不变。如事件模块开始实现时为了兼容旧版本IE及标准浏览器,写了很多IE Special代码,有一天旧版本IE消失了(猴年马月),只需从容删去即可。

内聚度
内聚是来自结构化设计的一个概念,简单说内聚测量了单个模块内各个元素的联系程度。最不希望出现的内聚就是偶然性内聚,即将完全无关的抽象塞进同一个模块或类中。最希望出现的内聚是功能性内聚,即一个模块或类的各元素一同工作,提供某种清晰界定的行为。
内聚度指模块内部实现,它是信息隐藏和局部化概念的自然扩展,它标志着一个模块内部各成分彼此结合的紧密程度。好处也很明显,当把相关的任务分组后去阅读就容易多了。设计时应该尽可能的提高模块内聚度,从而获得较高的模块独立性。

耦合度
耦合也是来自结构化设计,Stevens、Myers和Constantine将耦合定义为「一个模块与另一个模块之间建立起的关联强度的测量。强耦合使系统变得复杂,因为如果模块与其它模块高度相连,它就难以独立的被理解、变化和修正」
内聚度是指特定模块内部实现的一种度量,耦合度则是指模块之间的关联程度的度量。耦合度取决于模块之间接口的复杂性,进入或调用模块的位置等。与内聚度相反,在设计时应尽量追求松散耦合的系统。

JavaScript中模块“写法”

在JavaScript模块到底是什么,能用代码具体展现一下吗?其实上面已经写了一段事件模块代码

这能代表“模块”吗?这就是一个JS对象啊,以为有多么深奥。

是的,JavaScript中模块多数时候被实现为一个对象。这么看来,多数时候我们都写过“模块”(但没有在整个项目中应用模块化思想)。或许每个人写模块的方式(风格)还不同。比如上面的事件模块是一个匿名函数执行,匿名函数中封装了很多代码,最后通过return返回给Event变量,这个Event就是事件模块的接口。

又如jQuery,它也是一个匿名函数执行,但它并不返回接口对象。而是将自己暴露给window对象。

 
1
2
3
4
5
(function(window){
    // ..
    // exports
    window.jQuery = window.$ = jQuery;
})(window);

再如SeaJS,它一开始就将接口公开了

 
1
2
3
4
/**
* Base namespace for the framework.
*/
this.seajs = { _seajs: this.seajs };

后续是很多的匿名函数执行给变量seajs添加很多工具方法。注意,这里的this在浏览器环境指window对象,如果是定位在浏览器中,这个this也可以去掉。就象Ext。

 
1
2
3
4
5
6
7
Ext = {
    /**
     * The version of the framework
     * @type String
     */
    version : '3.1.0'
};

我们已经看到了四种方式写模块(把jQuery,SeaJS,Ext看成模块,呃很大的模块)。哪一种更好呢? 哪一种更适合在浏览器端呢?纯从代码风格上说,是萝卜白菜各有所爱。只要我们运用了“模块化”的思想来开发就行了。

但如果有一种统一的语法格式来写模块岂不是更好,就不会出现各用各的风格来写模块而使代码乱糟糟。

这就是目前的现状,开发者强烈需要一种统一的风格来写模块(最好是语言内置了)。这时一些组织出现了,最具代表的如CommonJS,AMD。此外ECMAScript也开始着手模块的标准化写法。

无论它们提供什么样的写法,我们需要的仅仅是:

  • 将一些零散代码封装成一个有用的单元(encapsulate)
  • 导出模块的接口API(exports)
  • 方便友好引用其它模块(dependency)

服务器端的JSer是幸运的,它有Node.js,Node.js遵循了一个称为CommonJS的规范。CommonJS其中就有对写模块的标准化。当然模块化只是其中的一部分而已。

具体来说Node.js实现了:

在模块化方面,它实现了Modules/1.0(已经更新到1.1.1),以下是node中是写模块的一个示例。

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
MATH.JS
exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};
 
INCREMENT.JS
var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};
 
MAIN.JS
var inc = require('increment').increment;
var a = 1;
inc(a); // 2

这就写了一个math、increment、main模块。math提供了add方法来实现数字相加。increment模块依赖于math模块,它提供increment方法实现相加。main获取到increment方法,执行相加操作。

以上代码示例可以看到:

  • node要求一个js文件对应一个模块。可以把该文件中的代码想象成是包在一个匿名函数中,所有代码都在匿名函数中,其它模块不可访问除exports外的私有变量
  • 使用exports导出API
  • 使用require加载其它模块

CommonJS module基本要求如下:

  • 标示符require,为一个函数,它仅有一个参数为字符串,该字符串须遵守Module Identifiers的6点规定
  • require方法返回指定的模块API
  • 如果存在依赖的其它模块,那么依次加载
  • require不能返回,则抛异常
  • 仅能使用标示符exports导出API

Modules/1.1较1.0仅增加了标示符module,require函数增加了main和paths属性。而仔细比对1.1与1.1.1后发现除了格式调整了下几乎没有变化。

Node.js模块格式在浏览器中的尝试

前面提到Node.js有一套简洁的格式写模块,它遵循的就是 Moudles。
浏览器里的JavaScript呢? 尽管语言本身暂不支持模块(ES6打算支持),但可以用现有的API包装一个写法出来。
毫无疑问,首先想到的是Node.js的Modules格式,它是最好的效仿对象。因为前后端有一个统一的方式写JS模块岂不乐哉!
但一开始就碰到一些难题:
服务器端JS模块文件就在本地,浏览器端则需要通过网络请求。
服务器端可以很容易的实现同步或异步请求模块,浏览器端则问题多多。
如下。

 
1
2
3
4
var event = require("event");
event.bind(el, 'click', function() {
    // todo
});

这段代码中require如果是异步执行的,则event.bind的执行有可能会出错。
那实现同步的require不就行了吗?的确可以使用 XHR 实现同步载入模块JS文件。但XHR的缺点也是明显的,它不能跨域,这点让人很难接受,因为有些场景需要模块部署在不同的服务器。
那只能通过script tag来实现模块加载了!但script tag默认就是异步的,要实现Node.js的一模一样风格(Modules)很难,几乎是不可能。

这时,“救世主”出现了:Modules/Wrappings ,顾名思义包裹的模块。该规范约定如下:

  • 定义模块用module变量,它有一个方法declare。
  • declare接受一个函数类型的参数,如称为factory。
  • factory有三个参数分别为require、exports、module。
  • factory使用返回值和exports导出API。
  • factory如果是对象类型,则将该对象作为模块输出。

描述有拗口,代码却很简单,使用了一个function包裹模块(Node.js模块则无需包裹)。

 
1
2
3
4
5
6
7
8
9
10
11
一个基本的模块定义
module.declare(function(require, exports, module)
{
    exports.foo = "bar";
});
 
直接使用对象作为模块
module.declare(
{
    foo: "bar"
});

Modules/Wrappings的出现使得浏览器中实现它变得可能,包裹的函数作为回调。即使用script tag作为模块加载器,script完全下载后去回调,回调中进行模块定义。

好了,截止目前我们已经看到了两种风格的模块定义:Modules 和 Modules/Wrappings。

CommonJS Modules有1.0、1.1、1.1.1三个版本:
Node.js、SproutCore实现了 Modules 1.0
SeaJS、AvocadoDB、CouchDB等实现了Modules 1.1.1
SeaJS、FlyScript实现了Modules/Wrappings

注意:
SeaJS未实现全部的 Modules 1.1.1,如require函数的main,paths属性在SeaJS中没有。但SeaJS给require添加了async、resolve、load、constructor。
SeaJS没有使用 Modules/Wrappings 中的module.declare定义模块,而是使用define函数(看起来象AMD中的define,实则不然)。

AMD:浏览器中的模块规范

前面提到,为实现与Node.js相同方式的模块写法,大牛们做了很多努力。

但浏览器环境不同于服务器端,它的模块有一个HTTP请求过程(而Node.js的模块文件就在本地),这个请求过程多数使用script tag,script 默认的异步性导致很难实现与Node.js一模一样的模块格式。

Modules/Wrappings 使得实现变为现实。虽然和Node.js的模块写法不完全一致,但也有很多相似之处,使得熟悉Node.js的程序员有一些亲切感。

但Node.js终究是服务器端的JavaScript,没有必要把这些条条框框放到浏览器JavaScript环境中。

这时AMD 诞生了,它的全称为异步模块定义。从名称上看便知它是适合script tag的。也可以说AMD是专门为浏览器中JavaScript环境设计的规范。它吸取了CommonJS的一些优点,但又不照搬它的格式。开始AMD作为CommonJS的transport format 存在,因无法与CommonJS开发者达成一致而独立出来。它有自己的wiki 和讨论组 。

AMD设计出一个简洁的写模块API:
define(id?, dependencies?, factory);
其中:
id: 模块标识,可以省略。
dependencies: 所依赖的模块,可以省略。
factory: 模块的实现,或者一个JavaScript对象。
特别指出,id遵循CommonJS Module Identifiers 。dependencies元素的顺序和factory参数一一对应。

以下是使用AMD模式开发的简单三层结构(基础库/UI层/应用层),用于展示模块的五种写法:

  1. 定义无依赖的模块(base.js)
  2. 定义有依赖的模块(ui.js,page.js)
  3. 定义数据对象模块(data.js)
  4. 具名模块
  5. 包装模块
 
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
BASE.JS
 
define(function() {
    return {
        mix: function(source, target) {
        }
    };
});
 
UI.JS
define(['base'], function(base) {
    return {
        show: function() {
            // todo with module base
        }
    }
});
 
PAGE.JS
define(['data', 'ui'], function(data, ui) {
    // init here
});
 
DATA.JS
define({
    users: [],
    members: []
});

以上同时演示了define的前三种用法。细心的会发现,还有两种没有出现:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
具名模块
 
define('index', ['data','base'], function(data, base) {
    // todo
});
 
包装模块
 
define(function(require, exports, module) {
    var base = require('base');
    exports.show = function() {
        // todo with module base
    }
});

如果不考虑多了一层函数外,格式和Node.js是一样的:使用require获取依赖模块,使用exports导出API。

除了define外,AMD还保留一个关键字require。require 作为规范保留的全局标识符,可以实现为 module loader,也可以不实现。

目前,实现AMD的库有RequireJS 、curl 、Dojo 、bdLoad、JSLocalnet 、Nodules 等。也有很多库支持AMD规范,即将自己作为一个模块存在,如MooTools 、jQuery 、qwery 、bonzo 甚至还有 firebug 。

UMD:各种模块格式的糅合

UMD是AMD 和CommonJS的糅合,前面花了很长的篇幅介绍了两大类模块规范,CommonJS(Modules/Modules/Wrappings)及AMD。

我们知道Modules/Wrappings是出于对Node.js模块格式的偏好而包装下使其在浏览器中得以实现。而Modules/Wrappings的格式通过某些工具(如r.js)也能运行在Node.js中。事实上,这两种格式同时有效且都被广泛使用。

AMD以浏览器为第一(browser-first)的原则发展,选择异步加载模块。它的模块支持对象(objects)、函数(functions)、构造器(constructors)、字符串(strings)、JSON等各种类型的模块。因此在浏览器中它非常灵活。

CommonJS module以服务器端为第一(server-first)的原则发展,选择同步加载模块。它的模块是无需包装的(unwrapped modules)且贴近于ES.next/Harmony的模块格式。但它仅支持对象类型(objects)模块。这迫使一些人又想出另一个更通用格式 UMD(Universal Module Definition)。希望提供一个前后端跨平台的解决方案。

UMD的实现很简单,先判断是否支持Node.js模块格式(exports是否存在),存在则使用Node.js模块格式。接着判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。前两个都不存在,则将模块公开到全局(window或global)。下面是一个示例:

下面是一个示例:

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
EVENTUTIL.JS
(function (root, factory) {
    if (typeof exports === 'object') {
        module.exports = factory();
        
    } else if (typeof define === 'function' && define.amd) {
        define(factory);
        
    } else {
        root.eventUtil = factory();
    }
})(this, function() {
    // module
    return {
        addEvent: function(el, type, handle) {
            //...
        },
        removeEvent: function(el, type, handle) {
            
        },
    };
});

虽然UMD八字还没有一撇,有些开源库却开始支持UMD了,如大名鼎鼎的《JavaScript设计模式》作者Dustin Diaz开发的qwery。代码如下:

 
1
2
3
4
5
6
7
8
9
!function(name,definition){
  if(typeof module != 'function') module.exports = definition()
  else if (typeof define == 'function' && typeof define.amd == 'object') define(definition)
  else this(name) = definition()
}('query',function(){
    var doc =document
    , html = doc.documentElement
    // ...
})

ECMAScript6:未来的JS模块化

ECMAScript的下一个版本Harmony已经考虑到了模块化的需求。

 
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
34
35
36
37
38
39
40
41
42
43
使用module关键字来定义一个模块
module math {
    export function sum(x, y) {
        return x + y;
    }
    export var pi = 3.141593;
}
 
使用import关键字来加载外部模块
// we can import in script code, not just inside a module
import {sum, pi} from math;
alert("2π = " + sum(pi, pi));
 
引入所有API
// import everything
import * from math;
alert("2π = " + sum(pi, pi));
 
局部重命名
import { draw: drawShape } from shape;
import { draw: drawGun } from cowboy;
 
嵌套模块
module widgets {
    export module button { ... }
    export module alert { ... }
    export module textarea { ... }
    ...
}
  
import { messageBox, confirmDialog } from widgets.alert;
 
从服务器上请求的模块
<script type=”harmony”>
// loading from a URL
module JSON at 'http://json.org/modules/json2.js';
  
alert(JSON.stringify({'hi': ‘world'}));
动态载入一个模块
Loader.load('http://json.org/modules/json2.js', function(JSON) {
    alert(JSON.stringify([0, {a: true}]));
});

ES6 modules还需要很长时间来规范化,可谓任重而道远。且它有个问题,即新的语法关键字不能向下兼容(如低版本IE浏览器)。

拥抱模块化的JavaScript的更多相关文章

  1. 浅谈模块化的JavaScript

    模块化JavaScript之风早已席卷而来, CommonJS . AMD . NodeJS .RequireJS . SeaJS . curljs  等模块化的JavaScript概念及库扑面而来, ...

  2. 模块化的JavaScript开发的优势在哪里

    如今模块化的 JavaScript 的开发越来越火热,无论是模块加载器还是优秀的 JavaScript 模块,都是层出不穷.既然这么火,肯定是有存在的理由,肯定是解决了某些实际问题.很多没接触过模块化 ...

  3. 模块化的JavaScript

    我们再一次被计算机的名词,概念笼罩. backbone.emberjs.spinejs.batmanjs 等MVC框架侵袭而来. CommonJS.AMD.NodeJS.RequireJS.SeaJS ...

  4. 使用r.js来打包模块化的javascript文件

    前面的话 r.js(下载)是requireJS的优化(Optimizer)工具,可以实现前端文件的压缩与合并,在requireJS异步按需加载的基础上进一步提供前端优化,减小前端文件大小.减少对服务器 ...

  5. JS模块化,Javascript 模块化管理的历史

    模块管理这个概念其实在前几年前端度过了刀耕火种年代之后就一直被提起. 直接回想起来的就是 cmd amd commonJS 这三大模块管理的印象.接下来,我们来详细聊聊. 一.什么是模块化开发 为了让 ...

  6. 放弃 Electron,拥抱 WebView2!JavaScript 快速开发独立 EXE 程序

    Electron 不错,但也不是完美的. Electron 带来了很多优秀的桌面软件,但并不一定总是适合我们的需求. 多个选择总是好事! 我使用 Electron 遇到的一些麻烦 1.Electron ...

  7. Javascript模块化开发,使用模块化脚本加载工具RequireJS,提高你代码的速度和质量。

    随着前端JavaScript代码越来越重,如何组织JavaScript代码变得非常重要,好的组织方式,可以让别人和自己很好的理解代码,也便于维护和测试.模块化是一种非常好的代码组织方式,本文试着对Ja ...

  8. 【javascript激增的思考01】模块化编程

    前言 之前我做过一个web app(原来可以这么叫啦),在一个页面上有很多小窗口,每个小窗口都是独立的应用,比如: ① 我们一个小窗口数据来源是腾讯微博,需要形成腾讯微博app小窗口 ② 我们一个小窗 ...

  9. JavaScript之模块化编程

    前言 模块是任何大型应用程序架构中不可缺少的一部分,模块可以使我们清晰地分离和组织项目中的代码单元.在项目开发中,通过移除依赖,松耦合可以使应用程序的可维护性更强.与其他传统编程语言不同,在当前Jav ...

随机推荐

  1. ORA-00368 ORA-00353 ORA-00312

    在昨天客户突然打电话过来,说系统进不去了,经过咨询发现是Oracle数据库没启动起来,经过一番折腾,最终弄好了. 解决方法还是在网络上的一般方法,最磨人的是Oracle的一个redo日志文件出现问题, ...

  2. Android线程计时器实现

    cocos2dx的计时器很好用,但当app进入后台,其计时器会pause掉,如果想要一个稳恒计时器就得自己去实现完成了,在Cocos2d-x for ios中我们可以利用NSTimer类并结合objc ...

  3. python中类的继承

    python中类的继承 在python中面向对象编程中实现继承,以下面一个实例进行说明. class SchoolMenber(): # __init__类似于c++中的构造函数 # __init__ ...

  4. hdu 1159 Palindrome(回文串) 动态规划

    题意:输入一个字符串,至少插入几个字符可以变成回文串(左右对称的字符串) 分析:f[x][y]代表x与y个字符间至少插入f[x][y]个字符可以变成回文串,可以利用动态规划的思想,求解 状态转化方程: ...

  5. .net 码农转战 iOS - 初探

    好久没写博客了,之前还打算把毕业设计中涉及到的两个算法拿出来说说(脸型分析 + 声音分析),博文都写了一半了,后来实在太忙了,那篇随笔也就沉在草稿列表中没动过. 我原先是专职 .net 开发的,在公司 ...

  6. 关于ASSERT(断言)的作用

    程序一般分为Debug 版本和Release 版本,Debug 版本用于内部调试,Release 版本发行给用户使用.断言assert 是仅在Debug 版本起作用的宏,它用于检查“不应该”发生的情况 ...

  7. 35个jquery技巧[转]

    人人都会的35个Jquery小技巧 2015-10-28 WEB开发者 收集的35个 jQuery 小技巧/代码片段,可以帮你快速开发. 1. 禁止右键点击 $(document).ready(fun ...

  8. Eclipse修改字体大小

    1.MyEclipse|Window|General|Appearance|Colors and Fonts->点击Text Font->Edit

  9. jvm内存GC详解

    一.相关概念  a. 基本回收算法 1. 引用计数(Reference Counting)  比较古老的回收算法.原理是此对象有一个引用,即增加一个计数,删除一个引用则减少一个计数.垃圾回收时,只用收 ...

  10. GWT用frame调用JSP

    Frame htmlFrame = new Frame("../OurHome/modules/core/mainIndex.jsp?merchantId="+merchantId ...