2014.11.01 / 前端开发 / 周 涛
31995

前言

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

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
  • Promises/B
  • Promises/D
  • Unit Testing/1.0

在模块化方面,它实现了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浏览器)

转载于:https://www.cnblogs.com/susanws/p/5385924.html

拥抱模块化的javascript相关推荐

  1. cxf springmvc_拥抱模块化Java平台:Java 10上的Apache CXF

    cxf springmvc 自Java 9发布最终将Project Jigsaw交付给大众以来,已经过去了整整一年的时间. 这是一段漫长的旅程,但是在那里,所以发生了什么变化? 这是一个很好的问题,答 ...

  2. java cxf_拥抱模块化Java平台:Java 10上的Apache CXF

    java cxf Java 9版本终于将Project Jigsaw交付给大众已经过去了一年的时间. 这是一段漫长的旅程,但是在那里,所以发生了什么变化? 这是一个很好的问题,答案并不明显和直接. 总 ...

  3. 拥抱模块化Java平台:Java 10上的Apache CXF

    Java 9版本终于将Project Jigsaw交付给大众已经过去了一年多的时间. 这是一段漫长的旅程,但是在那里,所以发生了什么变化? 这是一个很好的问题,答案并不明显和直接. 总的来说, 拼图项 ...

  4. 拥抱haXe之javascript 也玩mvc

    haXe是高级语言的高级语言,可以将一个haXe程序转换为其它的高级语言,如JS.haXe有很多优点,比如,强类型.泛型.内联.宏.动态语言特性.命名空间--,用haXe写程序比用JS写程序舒服得多. ...

  5. edge浏览器怎么设置activex_Microsoft Edge拥抱HTML5和JavaScript,不再支持IE上的ActiveX技术...

    ie系列浏览器_从IE到Chromium Edge:微软浏览器的浮沉史 我们知道,2015年的时候微软放弃了20岁的IE浏览器产品,发布了一款全新的浏览器edge,edge是windows 10的默认 ...

  6. JavaScript模块化开发的演进历程

    写在前面的话 js模块化历程记录了js模块化思想的诞生与变迁 历史不是过去,历史正在上演,一切终究都会成为历史 拥抱变化,面向未来 延伸阅读 - JavaScript诞生(这也解释了JS为何一开始没有 ...

  7. 使用SeaJS实现模块化JavaScript开发

    2019独角兽企业重金招聘Python工程师标准>>> 前言 SeaJS是一个遵循CommonJS规范的JavaScript模块加载框架,可以实现JavaScript的模块化开发及加 ...

  8. 使用SeaJS实现模块化JavaScript开发【转】

    前言 SeaJS是一个遵循CommonJS规范的JavaScript模块加载框架,可以实现JavaScript的模块化开发及加载机制.与jQuery等JavaScript框架不同,SeaJS不会扩展封 ...

  9. Web前端进阶之JavaScript模块化编程知识

    JavaScript是前端三要素之一,也是很多初学Web前端的人遭遇的第一条拦路虎.很多同学表示JavaScript涵盖的知识点太多太复杂.应用也是五花八门完全摸不着头脑.但只要我们一点一点由基础到进 ...

  10. JavaScript模块化编程知识分享!

    JavaScript是前端三要素之一,也是很多初学Web前端的人遭遇的第一条拦路虎.很多同学表示JavaScript涵盖的知识点太多太复杂.应用也是五花八门完全摸不着头脑.但只要我们一点一点由基础到进 ...

最新文章

  1. 路由协议有类和无类的区别
  2. Day20 Ajax
  3. Daily scrum[2013.12.07]
  4. 现代制造工程课堂笔记06-集成电路制造工程
  5. Akka的字数统计MapReduce
  6. python视频提取音频_python脚本实现mp4中的音频提取并保存在原目录
  7. 在linux下如何进行mysql命令行 创建数据库
  8. php80端口改成8080,jetty,_JETTY 8080端口改为80端口无法访问,jetty - phpStudy
  9. 【aviator】aviator 报错 EOF while reading string at index
  10. Java队列——线程池创建的例子
  11. python中︿是什么意思_Python learning notes-0003-注释、变量、简单类型、运算符,学习,笔记...
  12. Linux_Qt:-1: error: cannot find xxx/lib: file format not recognized
  13. ptp输出内容包含什么_PTP 无线传输
  14. pd.concat实现数据合并
  15. Visual Studio Code下c语言环境的安装与运行
  16. 上树建站教程:新手单页网站制作教程上集
  17. 洛谷[P3374] [P3368]树状数组(线段树解法)
  18. 在win10更新的时候出现0x80240004错误代码怎么解决。
  19. NOI2002银河英雄传说
  20. 第六届360前端星计划_前端代码的自我修养

热门文章

  1. eth0: ERROR while getting interface flags: No such device
  2. linux shell 学习时遇到的一些问题([: 11: y: unexpected operator)
  3. BNU 斩(超级大水题)
  4. android快速查询,android常用adb快捷命令一览表
  5. 【算法原理+洛谷P6114+HDU6761】Lyndon分解
  6. 十大排序算法——计数排序(C语言)
  7. WordPress插件:WP-China-Yes解决国内访问官网慢的方法
  8. matlab 结构体遍历,matlab 遍历结构体struc的成员
  9. 翻译: 构建基于卡尔曼滤波器的 IMU 用速度数据改进 IMU 姿态估计
  10. hive 指定字段插入数据_Hive插入数据的几种常用方法