简介

到目前为止,这个系列已经涵盖:所谓的 “狂野西部”症和用于实现更好的组织 JavaScript 代码库的命名空间策略。我们会在最后一节提到 JavaScript 模块以及模块格式和规范。这一节将深入讲解 JavaScript 模块。

假如把一个方法理解为功能上独立的单元,那么模块就该被认为是功能上相似的单元的一个独立的分组。JavaScript 模块近似等同于服务端框架中的组件。继续这个经典的类比,类应该遵循单一的职责,按照SOLID原则。同样的,一个模块也应该只承担单一的职责。就此而言,单一职责意味着一站式地处理了相关的任务。

回到前一节中我们关于错误处理模块的讨论,错误处理模块可能包含很多功能上不同的单元(比如函数),但是都应该是与处理错误相关的。而且,模块不应该包含任何在功能上对于处理错误无关的代码,此外,需要能够处理给定应用所需要处理的所有错误。

当完成了错误处理模块,就应该在你的应用中被使用,或者被用到的其他模块中。更进一步,通过一个类似 Pub/Sub 的事件驱动模式实现与应用中其他模块松散地耦合,将会包含在下一个节中。

现在,我们近距离的看一下真正 JavaScript 模块的实现。

通常的 JavaScript 模块

使用原生 JavaScript 实现模块是相对容易的,但是已经存在很多的模式随便你使用。每种模式可以说是各有利弊。这里会包含这些模式中的几个,但没有顾及那个最适合你的选择。多数时候,会取决于你的偏好以及哪个在你指定的场景下工作的最好。对于哪个模块实现是最好的,网上没有什么明确的意见。所以,如果不确定可以随便去搜索一下。

我们现在开始。

JavaScript 模块可以像 JavaScript 对象一样的简单,对象的原型就是功能上独立的单元,也就是函数。请注意没有闭包,这个模块实例可能会污染全局命名空间。

实例:POJO 模块模式

var myModule = {
    propertyEx: "This is a property on myModule",    
    functionEx: function(){ 
        //在这里插入代码 
    } 
}; 

为了创建闭包,并且确保所有的变量和函数都是这个模块局部的,通常业内会用一个 IIFE(立即执行的函数表达式)包裹这个模块,这与前面提到的对象方法很类似。

实例:Scoped 模块模式

var myModule = (function () { 
    var module;
    module.varProperty = "This is a property on myModule";
    module.funcProperty = function(){ 
        //在这里插入代码
    };
    return module; 
})(); 

另一种模式是模块模式,连同所谓的暴露模块模式。暴露模块模式只是模块模式的一个特例,实现了一种形式的私有成员,这样仅仅暴漏了一个公共接口。下面是暴露模块模式的的一个实例。

实例:暴露模块模式

var myRevealingModule = (function () {
var privateVar = "Alex Castrounis",
    publicVar  = "Hi!";

function privateFunction() {
    console.log( "Name:" + privateVar );
}

function publicSetName( strName ) {
    privateVar = strName;
}

function publicGetName() {
    privateFunction();
}

return {
    setName: publicSetName,
    greeting: publicVar,
    getName: publicGetName
}; 

})(); 

我要说的最后一个普遍的 JavaScript 模式是原型模式。和前面描述的模块模式类似,但是使用了 JavaScript 原型。下面是一个带有私有成员基于原型模式的模块的一个实例。

实例:原型模式

var myPrototypeModule = (function (){
var privateVar = "Alex Castrounis",
    count = 0;

function PrototypeModule(name){
    this.name = name;
}

function privateFunction() {
    console.log( "Name:" + privateVar );
    count++;
}

PrototypeModule.prototype.setName = function(strName){
    this.name = strName;
};

PrototypeModule.prototype.getName = function(){
    privateFunction();
};

return PrototypeModule;     

})();

再仔细的看一下不同的 JavaScript 模块模式和实现,包括全局引入,我强烈的建议读一下 Ben Cherry 的这篇文章。另外,如果对这个问题非常感兴趣,Addy Osmani 的 JavaScript 设计模式也是一本必读的书。

模块格式和规范

尽管,像前面给出的每个实例那样实现普通的 JavaScript 模块没有什么技术上的错误,不过有一些问题还是非常值得考虑的,那些往往会导致不一致,缺乏标准化。还记得这个系列第一节中关于“狂野西部”症么?另一个值得考虑的是原生 JavaScript 在处理异步和并行模块载入时,模块之间的依赖和优化等等时显得很无力。

出于这些考虑,大量的模块格式和规范应运而生,旨在提升以下几个方面:标准和一致,封装,异步和并行载入脚本,提升应用性能,提高可读性和代码整洁,避免出现 HTML 中 script 标签,并且单独维护。有三个最大的参与者分别是 AMD(异步模块定义),CommonJS 以及 ECMAScript 6 Harmony 模块规范。

不过,请记住这个系列的意图只是对一些现存的方式做了概述,构建的思考以及最佳实践。网上会有非常多相关的文章。

现在我们深层次的分别看一下这些选择。可以查阅这一节末尾的“资源”部分,有更多的资源的详细地涵盖了这些主题。

异步模块定义 API(AMD)

异步模块定义,或者 AMD 是一个关于如何定义模块以及模块间依赖的规范,使得这些模块可以被浏览器异步载入。James Burke 的 Require.js 文件和模块加载器就是一个基于 AMD 规范的非常通用的框架。它可能是最受欢迎的,但它不是当前环境下唯一的 AMD 实现。

Require.js 的可配性和可定制性都很高,并且完成度很高,应用广泛。它使用了 AMD 语法,集中于 define 和 require 函数。对于 Require.js 和 AMD 最多的抱怨就是相当繁琐的配置和启动,并且用到的语法也不是很简单。此外,在设计上,Require.js 更像是一个客户端(基于浏览器)模块实现和架构。

你可能不同意上面的观点,但无论哪种方式,AMD 和 Require.js 肯定有积极的一面。第一点是 Require.js 在它想要做的地方表现的相当优秀。假定你已经完成了所有的初始设置,你会很放心模块和它们的依赖会异步的载入,可以满足任何所需要的规则,按照各个模块的依赖和你的配置。

使用 Require.js 的另一个优势就是它拥有内建的控制器,可被用于合并、压缩脚本(通过 UglifyJS 或者 Google 的 Closure Compiler),当然还有内联和优化 CSS 文件,通过 @imports 命令和移除注释。

CommonJS

CommonJS 是另外一个被广泛应用的模块规范。它试图制定一个模块格式可以在浏览器和服务端通用,尽管它最普遍的实现是服务端的框架,像 Node.js。Browserify 是一个很流行的框架,试图在客户端实现 CommonJS 模块。你能够实现一个全栈的 CommonJS 模块方案,通过把客户端的 Browserify(或者类似的)和服务端的 Node.js 结合在一起就行。

和 AMD 不同的是,CommonJS 更多是基于exports和 require 两个函数。需要注意,AMD 和 CommonJS 全都把模块的依赖表示成字符串。

对 CommonJS 规范最多的抱怨是依赖于服务端工具以及构建过程,服务驱动很重,跨域问题以及缺乏传输格式。尽管如此,CommonJS对很多人来说都是受益的,原因如下:相对容易去安装和配置,简洁的语法和接口,容易阅读,变化多,并且可以遍及全栈来实现。也就能够更好的复用多个依赖。

ECMAScript 6 Harmony

ECMAScript 是一个脚本语言规范,通过 ECMA-262 标准和 ISO/IEC 16262 定义。ECMAScript 语言规范的维护和标准化由 ECMA 国际来负责。

ECMAScript 标准的当前版本是 5.1,虽然这个标准的下一个版本 ECMAScript 6(也被称为 Harmony 和 ES.next)已经问世。ECMAScript 是 JavaScript 语言自身基于的规范。

每当 ECMAScript 规范推出新的特性和更新,JavaScript 语言自身和引擎都会相应的解释和执行它的变化(至少是理论上)。

使用 JavaScript 实现的原生模块最明显的好处当然就是原生!根据 Axel Rauschmayer2 指出,ES6 更加坚实,得益于静态解析和模块结构,实现改进的周期性处理依赖关系,单个模块规范的标准化,消除全局变量,并且降低或者消除基于对象命名空间的依赖。

ES6 模块通过声明导入和导出语法构造而成,还包括编程载入 API 用来载入模块和管理依赖。

与 AMD 和 CommonJS 不同的是,ES6 使用了几个非常关键的函数(define,require 以及 exports),它的模块是关键字驱动的。import 和 export 关键字是最基本的成员。为导出模块声明一个名字,就是一个具名 export。

除了具名 exports,ES6 定义了默认 export,用来表示每个模块最关键的一个导出值。可以是你想要的任何值(函数,对象等等),但是默认 export 应该是给定模块的关键导出项。注意,除了具名 export 外模块还可以拥有默认 export。

ES6 模块的其他特性包括对于同步和异步加载的支持,以及一个可编程和可配置的模块装载 API。

注意,ES6 模块还没有被所有主流浏览器实现,可能需要使用 transpiler 来实现。这个链接是一个非常好的资源,用于判定浏览器对于 ES6 和 ES7 的兼容性。

总结

在这一节,我们详细讲述了多种原生 JavaScript 的模块实现方式,也主要探索了三个主要的模块格式和规范。当然也包括了即将到来的原生 ECMAScript 模块规范,但只是一个雏形,我强烈建议去选择自己最有感觉的模块模式或者规范。一旦决定了一个模块规范,并且确保在你的项目中的统一性。统一是可维护性的一个关键要素。

下一节中,我们会讨论用于最小化模块间耦合的模式,这对于一个应用的可扩展性,可重用性,可靠性和可维护性都至关重要。

敬请期待!

章节

原文:How to Write Highly Scalable and Maintainable JavaScript: Modules

0
推荐阅读