瞅瞅JavaScript模块标准

模块是每门语言构建复杂系统的必备特性,JavaScript自然也不例外。JavaScript当前流行的模块化标准有CommonJS、AMD、CMD、ES6等等,本文对这些标准做了简单梳理,努力做到应用时不懵逼,不乱用。

模块

现如今几乎每门语言都有自己的模块化解决方案,这是随着软件工程越来越复杂的必然产物。贴几个流行语言的模块化介绍大家感受下:

所有语言的模块化解决方案都是为了实现将复杂的程序拆分成独立的几个模块,每个模块写明自己的依赖、输出自己的能力。模块化让复杂代码变得容易维护、方便复用。

概览

JavaScript标准众多,缕清这几个标准的发展史有助于大家选择采用哪种方案来写代码。

  1. CommonJS应该是最早在民间自发产生的服务端模块化标准,一开始是叫ServerJS,后来改名了。
  2. 服务端JS有了模块化标准之后,浏览器JS表示我也必须有,于是基于CommonJS标准产生了AMD,和CommonJS相比最大的不同就是依赖的异步加载。
  3. CMD是类似AMD的对于浏览器JS模块化标准,源自Sea.js。
  4. ES6则是集大成者,其统一了同步和异步的模块化标准,试图让JS模块化标准从分裂走向统一,并取得了不小的成绩。

标准定制一般都是和实现相辅相成的,那么JS这些有名的模块化标准主要都有哪些实现呢?

CommonJS AMD CMD ES6
Node.js/RingoJS RequireJS/curl.js SeaJS ES6

每个标准都在JS世界的不同领域中得到广泛的应用,对这些标准进行初步的了解是有必要的。

CommonJS

为了方便,直接使用Node.js的模块化实现来说明CommonJS标准。下面给出按照CommonJS标准写的demo,随后其他标准的demo也会实现一样的功能。

// math.js
const { PI } = Math;
exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;
console.log(module);

// main.js
var area = require('./math').area;
var result = area(3);
console.log(result);

CommonJS模块定义了三个变量,moduleexportsrequire

module

通过console.log(module),我们可以打印出module的结构如下:

Module {
  id: '.',                                                      // 模块Id,一般都是文件的绝对路径
  exports: { area: [Function], circumference: [Function] },     // 模块对外输出的变量
  parent: null,                                                 // 调用该模块的模块,如果直接执行就是null
  filename: '/path/to/demo/math.js',                            // 带绝对路径的文件名
  loaded: false,                                                // 模块是否加载完成
  children: [],                                                 // 模块的依赖
  paths:                                                        // 模块依赖的搜索路径
   [ '/path/to/demo/node_modules',
     '/path/to/node_modules',
     '/path/node_modules',
     '/node_modules' ] }

exports

module对象中是有字段exports的,exports实际上就是module.exports

var exports = module.exports;

因此导出变量有两种方式:

exports.area = (r) => PI * r ^ 2;
exports.circumference = (r) => 2 * PI * r;

// 或者也可以如下
module.exports.area = (r) => PI * r ^ 2;
module.exports.circumference = (r) => 2 * PI * r;

因为exportsmodule.exports的引用,在导出的时候我们就要格外小心了。

exports.area = (r) => PI * r ^ 2;
module.exports = (r) => 2 * PI * r; // 将module.exports对象覆盖,area这个变量就不会被导出。

exports = (r) => 2 * PI * r; // exports就不再是module.exports的引用了,会导致后面的circumference导出无效。
exports.circumference = (r) => 2 * PI * r;

require

require的参数是模块id,require实现的功能就是根据模块id去找到对应的依赖模块。模块id的变数主要在两个方面,一个是后缀名,一个是路径。

首先来说后缀名,一般默认是js的,所以我们在依赖的以后一般不需要添加后缀名。而且找不到的话,Node.js还会尝试添加.json.node后缀去查找。

var area = require('./math').area;

// 和上面是一样的
var area = require('./math.js').area;

再来说路径,绝对路径和相对路径就不多说,比较好理解。

var area = require('/math').area;   // 在指定的绝对路径查找模块
var area = require('./math').area;  // 在相对与当前目录的路径查找模块

还有如果不是以"."、".."或者"/"开头的话,那就会先去核心模块路径找,找不到再按照module.paths指定的路径找。

var area = require('math').area;

AMD

同样的,本节采用RequireJS来说明AMD标准。先上一个例子。

// math.js
define('app/math', function () {
    const { PI } = Math;
    return {
        area: function (r) {
          return PI * r ^2;math.js
        },
        circumference: function (r) {
          return 2 * PI * r;
        }
    };
});

// main1.js
define(['app/math', 'print'], function (math, print) {
    print(math.area(3));
});

// main2.js
define(function (require) {
    var math = require('./math');
    var print = require('print');
    print(math.area(3));
});

define

AMD使用define这个api来定义一个模块,其语法比较简单。

define(id?, dependencies?, factory);

模块id和依赖都是可选参数,只有构造函数是必须的。

id

AMD的模块id和CommonJSmodule对象中的id作用是一样的,用来唯一的指定模块,一般是模块的绝对路径。虽然define函数将这个id暴露给使用者,但一般也是不填的,一些优化工具会自动生成绝对路径作为id参数传给define函数。id的定义也和CommonJS类似,相对路径、绝对路径、js后缀可以省略等等。详细的可以查看AMD模块id的格式

dependencies

factory函数中使用到的依赖需要先在这里指明,比如示例代码,需要指明app/mathprint,然后将他们作为factory的参数传给函数体使用。AMD协议保证在factory函数执行之前,能将所有的依赖都准备好。

除了指明依赖之外,dependencies还有一种写法。这种写法是为了方便复用按照CommonJS规范写的模块,足见AMD规范的良苦用心。

define(function(require, exports, module) {
    var a = require("a");
    exports.foo = function () {
        return a.bar();
    };
});

RequireJS中依赖的查找路径是通过配置文件来指定的baseUrlpathsbundles等,这一点和Node.js是完全不一样的。

AMD这个标准有个比较明显的缺陷就是所有的依赖都必须要先执行,这个从其接口的设计上就能看出来。如果依赖比较多的话,这个事情就比较坑爹了。

factory

这个参数名字比较有意思,叫工厂函数,当某块被依赖的时候,这个工厂函数就会被执行,而且即便被依赖多次,也只会执行一次。在factory中需要导出变量的时候,直接return就可以了,当然也可以使用CommonJS规范的exports。

相比较而言,AMD标准还是比较复杂的。

CMD

CMD虽然没有CommonJSAMD出名,但是SeaJS在国内还是比较出名,这里也捎带提及CMD规范,不多说,来demo代码先。

// math.js
define(function(require, exports, module) {
    const { PI } = Math;
    exports.area = function (r) {
        return PI * r ^2;math.js
    },
    exports.circumference = function (r) {
        return 2 * PI * r;
    }
});

// main1.js
define(function(require, exports, module) {
    var area = require('./math').area;
    var print = require('print');
    print(area(3));
});

上面的示例和AMD的示例虽然比较像,但是实际上CMD的规范和AMD还是不太一样的,有自己的一些特色。

define

模块定义和虽然和AMD一样用的是define函数,但是只支持factory一个参数。

define(factory);

factory和AMD也是类似的,可以是函数,也可以是一个object。

require && require.async

CMD除了有同步的require接口,还有异步接口require.async,这样就解决了我们之前提到的AMD需要先把所有依赖都加载好才能执行factory的弊端。

define(function(require) {
    // 同步接口示例
    var a = require('./a');
    a.doSomething();

    // 异步接口示例
    require.async('./b', function(b) {
        b.doSomething();
    });
})

exports

这个就比较类似CommonJS的exports了,是用来输出API或者对象的。

module

这个也比较类似CommonJS的module对象,不过相比于Node.js的module对象要简单的多,只包括

module.uri                  // 模块完整解析出来的uri
module.dependencies         // 所有的依赖
module.exports              // 导出的能力

从上面的简单描述可以看出,CMD想同时解决AMD和CommonJS能解决的问题,基于AMD和CommonJS的设计做了简化优化,同时设计了异步require的接口等。关于CMD的大量细节可以查看SeaJS官网

ES6

一直以来JavaScript语言本身是没有内置的模块系统,ES6终结了这个局面。虽然ES6的普及还需要好多年,但ES6完全兼容ES5的所有特性。ES6的写法可以通过转换工具转成ES5来执行,是时候好好学习ES6了。

让我们来看看用ES6实现上面的示例是什么样的?

// math.js
const { PI } = Math;
export function area(r) {
    return PI * r ^ 2;
}
export function circumference(r) {
    return 2 * PI * r;
}

// main.js
import { area, circumference } from './math';
console.log(area(3));

export

ES6的模块是严格要求一个模块一个文件,一个文件一个模块的。每个模块可以只导出一个变量,也可以导出多个变量。

一个模块导出多次使用命名的导出(named exports)。

export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}

一个模块只导出一次使用默认导出(default exports),非常方便。

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
export default function () {}

import

ES6的import和之前标准的require是比较不一样的,被导出变量是原有变量的只读视图。这意味着虽然变量被导出了,但是它还是和内部变量保持关联,被导出变量的变化,会导致内部变量也跟着变化。也许这正是ES6重新取了import这个名字而没有使用require的原因。这一点和require是完全不一样的,require变量导出之后就生成了一个新的变量,和原始的内部变量就脱离关系了。有个demo能比较好的说明这个问题。

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

模块是ES6语言的一项重大特性,里面的细节比较多,详细描述怕是篇幅太长了,需要详细了解ES6模块语法的同学请移步ES Modules

总结

本文简单描述了CommonJS、AMD、CMD以及ES6的模块标准,仔细研究各个标准的细节可以一窥JavaScript模块化标准的发展历程。JavaScript语言早期作为网站的一种脚本语言,不需要模块化这种特性,但随着node.js的出现,js的工程越来越复杂,模块化也越来越重要。CommonJS、AMD和CMD是在语言不支持的情况下发展出来的第三方模块化解决方案,ES6正是基于这些解决方案提出了语言内置的模块标准,希望ES6能尽快的推广起来,这样JSer就能轻松许多啦。

参考文献

微信一键关注
微信一键关注
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容