JavaScript 模块化编程(二):规范

JavaScript 模块化编程(一):模块的写法
JavaScript 模块化编程(二):规范
JavaScript 模块化编程(三):实现一个RequireJS
JavaScript 模块化编程(四):结合Node源码分析CommonJs规范


常见的JavaScript 模块化规范有3种,CommonJS、AMD(异步模块定义)、CMD(公共模块定义)

其中
服务端 :NodeJS 服务:CommonJS规范,新版本的Node也可以启用ES6 Module功能
浏览器端:主要使用的是AMD规范和CMD规范,现在已经逐步被ES6 Module取代

当ES2015标准的出现后,ES6 在语言标准的层面上,实现了模块功能。这也让AMD、CMD逐渐被淘汰。相信ES6模块最终会一统天下


1.CommonJS规范

(1) 每一个文件都是一个模块,每一个模块都有一个独立的作用域,文件内的变量,函数都是私有的,其他文件不可使用(除非赋值到 global上)
(2)每个模块内部,module变量代表当前模块
(3)每个文件对外的接口是 module.exports 属性
(4) require用于引用其他模块,实际获得的是其他模块的module.exports这个属性

例子

//module_a.js
var x = 5;
var addX = function(value) {
  return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
//index.js
var example = require('./module_a.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6

CommonJS模块的特点

(1)所有代码都运行在模块作用域,不会污染全局作用域
(2)模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
(3)模块加载的顺序,按照其在代码中出现的顺序(即是同步加载)

require内部处理流程
require 实际是 指向当前模块的 module.require, module.require 又调用Node的 Module._load(此Module非彼module)

Module._load = function(request, parent, isMain) {
  // 1. 检查 Module._cache,是否缓存之中有指定模块
  // 2. 如果缓存之中没有,就创建一个新的Module实例
  // 3. 将它保存到缓存
  // 4. 使用 module.load() 加载指定的模块文件,
  //    读取文件内容之后,使用 module.compile() 执行文件代码
  // 5. 如果加载/解析过程报错,就从缓存删除该模块
  // 6. 返回该模块的 module.exports
};

其中 module.compile()执行如下:

Module.prototype._compile = function(content, filename) {
  // 1. 生成一个require函数,指向module.require
  // 2. 加载其他辅助方法到require
  // 3. 将文件内容放到一个函数之中,该函数可调用 require
  // 4. 执行该函数
};

2.AMD(Asynchromous Module Definition - 异步模块定义)

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出

CommonJS 采用的是同步加载机制,如果用于客户端,必定受到网络的限制。所以,CommonJS不适用于客户端。
而 AMD 采用的是模块异步加载方式,在需要执行到模块文件的时候,实现异步加载,回调执行。

require.js
首先下载最新require.js ,然后引入,data-main用于指定网页程序的主模块:

<script src="js/require.js" data-main="js/main"></script>

使用

定义模块
define(id?, dependencies?, factory)
加载模块
require([module], callback)

例子

//math.js
define(function() {
  var add = function(x, y) {
    return x + y
  }

 return  {
    add: add
  }
})

//main.js
require(['math'], function (math) {
    math.add(2, 3);
  });

当执行到这一段代码的时候, 浏览器会先 加载 math模块,在math模块加载成功后, 再执行后面的回调函数


3.CMD(Common Module Definition - 公共模块定义)

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出

SeaJS
使用

定义模块
define(factory)
加载模块
require(id)

区别

  1. 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible
  2. CMD 推崇依赖就近,AMD 推崇依赖前置,例:
//CMD
 define (function (require, exports, module) {
    var a = require('./a')  // 模块加载
    a.doSomething();
    var b = require('./b')  // 依赖可以就近书写
    b.doSomething();

    // 通过 exports 对外提供接口
    exports.doSomething = ...
    // 或者通过 module.exports 提供整个接口
    module.exports = ...
   })

代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。而AMD是依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,表现在require函数的调用结构上为:

//AMD
define(['./a','./b'],function(a,b){
   a.doSomething()
   b.doSomething()
}) 

代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组


4.UMD(Universal Module Definition - 通用模块定义)

Universal Module Definition。可以看成是AMD和CommonJS的一个合并方案。解决跨平台的解决方案。
步骤

1.先判断是否支持Node.js模块格式(CommonJS),存在则使用Node.js模块格式
2.再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块
3.前两个都不存在,则将模块公开到全局(window或global)

以一个calculator模块为例:

// if the module(calculator) has no dependencies, the above pattern can be simplified to
(function (name, context, definition) {
   if (typeof module != 'undefined' && module.exports){   //CommonJs
     module.exports = definition();
  }else if (typeof define == 'function' && define.amd){    //AMD
    define(name, definition);
   } else{  // Browser globals (context is window)
    context[name] = definition();
   }
}('calculator', this, function () {
  // your module here!
  return {
    sum: function(a, b) { return a + b; }
   };
});

5.ES6 Module

export命令用于规定模块的对外接口

//module_a.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};

使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块

// main.js
import {firstName, lastName, year} from './profile.js';

function setName(element) {
   element.textContent = firstName + ' ' + lastName;
}

export default命令,为模块指定默认输出。其他模块加载该模块时,import命令可以为该输出指定任意名字

详细见ES6 Module


6.回顾总结

1.CommonJS和AMD区别?
(1)CommonJS是适用于服务器端,Node就是采用的CommonJS模式。它是同步加载不同模块文件。之所以采用同步,是因为模块文件都存放在服务器的各个硬盘上,实际的加载时间就是硬盘的文件读取时间
(2)AMD是适用于浏览器端的一种模块加载方式。从名字可知,AMD采用的是异步加载方式。浏览器需要使用的js文件(忽略缓存)都存放在服务器端,从服务器端加载文件到浏览器受网速等各种因素的影响,如果采用同步加载方式,一旦js文件加载受阻,页面将处于阻塞状态

2.CommonJS和ES6 模块区别?
(1)CommonJS 模块是运行时加载,ES6 模块是编译时输出接口
这是因为CommonJS 的输出接口是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,使得编译时就能确定模块的依赖关系(“静态优化”)。
(2)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块输出的是module.exports这个对象,我们读取的也是这个对象,而不是模块内部某个变量。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(除非引用类型)。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。


参考

CommonJS规范
RequireJS和AMD规范
AMD 和 CMD 的区别有哪些?
Javascript 模块化管理的来世今生

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

推荐阅读更多精彩内容