谈谈JS的那些文件模块系统

学习ES6和Webpack过程中,研究了一下CommonJS,AMD, CMD, ES6这些模块系统到底有什么区别。

先提结论:

  • 相同点是,所有的文件模块系统,都采用单例模式。
  • 模块加载方面:CJS采用同步阻塞机制,而AMD采用异步回调机制,CMD可通过调用不同的require方法,采用不同的机制。
  • 模块执行方面:CJS,AMD都采用预先执行,在引用模块的同时执行模块代码;CMD则采用延迟执行,只有当模块中的变量或方法真正被执行时, 模块代码才被执行。
  • 个人觉得服务器端使用CJS和CMD都可以,但比较偏爱CJS简洁的语法。浏览器端从性能方面考虑还是比较推荐使用AMD,尽量避免在代码运行过程中去做资源请求。
  • 特别推荐使用webpack模块打包工具配上ES6的模块文件系统语法,在本地进行模块编译后上线。

正文:

JS本来是没有模块的语言,随着页面交互越来越复杂,为了避免全局变量泛滥以及方便多人合作开发等,我们开始运用对象,模拟类,闭包等来实现模块的效果,例如:

  • 用对象将方法和变量封装起来。
var module1 = {
  _count : 0,
  m1 : function (){
    //...
  },
  m2 : function (){
    //...
  }
};
  • 用闭包封装方法,可得到外部无法读取的私有变量
var module1 = (function(){
  var _count = 0;
  var m1 = function(){
    //...
  };
  var m2 = function(){
    //...
  };
  return {
    m1 : m1,
    m2 : m2
  };
})();
  • 沙箱模式,声明模块依赖
Sandbox.modules = {};
Sandbox.modules.dom = function(box) {
    box.getElement = function() {};
    box.getStyle = function() {console.log('getStyle')};
    box.foo = 'bar';
};
new Sandbox(['dom', 'ajax'], function(box) {     //声明依赖
    box.getElement();
})

思路:1. 将全局属性方法的初始化函数放在Sandbox构造函数的属性modules中;2. 在构造函数内部获取所需的属性,赋值给this; 3. 运行callback(this)。具体实现代码可在这篇文章里搜‘沙箱模式’查看

虽然模块化编程能较好的帮助我们处理单个文件内的代码结构。但随着单页面应用的出现,单个js文件已经无法满足复杂交互的要求了。
而单纯依赖html<script>引用的多文件间没有依赖关系,需要根据依赖严格注意在页面中的引用顺序,即使这样,还是很容易出现错误。因此,协调不同文件之间依赖关系模块文件系统就出现了。

CommonJS

CommonJS 是以在浏览器环境之外构建 JavaScript 生态系统为目标而产生的项目,比如在服务器和桌面环境中。Node.js 就是CommonJS规范的最常见的实现。规范其中也定义了适合服务器端使用的同步阻塞文件模块系统:

语法:

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

//a.js 定义模块
module.exports = 'I am from a.js!';
//main.js 引用模块
const a = require('./a.js');
console.log(a);  //输出'I am from a.js!'

为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令:

var exports = module.exports;

这让我们可以很方便的在对外输出模块接口时,向exports对象添加方法。最终输出的是exports对象。

//a.js 定义模块
exports.doStuff = function() {
  console.log('doing stuff...');
}
//main.js 引用模块
const a = require('./a.js');
a.doStuff();  //输出'doing stuff...'

这就解释了如果希望输出的是非对象类型,不能直接赋值给exports,而是要赋值给module.export。这是因为exports实际上是指向module.export的,修改它的值只会修改它的指针指向别的对象或数据,而不能修改module.export的值。

特征:

  • 同步阻塞:CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。这是由于服务器端的文件是存在于本地的,加载时间比较短。但在浏览器端需要较长时间向服务器端请求文件的情况下,这种文件模块系统并不合适。
  • 单例模式: 虽然CommonJS允许在同一个文件对同一个模块多次引用,但获取到的是同一个对象(类,函数)。可以看看下面这个例子:
//counter.js 定义一个计数器函数,有一个内部变量i
var i = 0
function count() {
    return ++i;
}
module.exports = count;
//main.js引用counter.js
var counter1 = require('./counter')
var counter2 = require('./counter')
var counter3 = require('./counter')
console.log(counter1())
console.log(counter2())
console.log(counter3())

运行结果:三次引用counter.js都执行一次,分别输出1,2,3;说明三次引用的是同一个函数,操作的是同一个变量。


WX20170321-163625.png

AMD

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。相对于CommonJS的同步阻塞模块系统,更适合用于浏览器端。最常用的实现是require.js。

它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。

语法:

//name.js 定义模块
define([], function(){
  return 'daisy';
});
//age.js 定义模块
define([], function(){
  return 23;
});
//main.js 引用模块
require(['./name', './age'], function(name, age) {
    console.log(`Hi! I am ${name}, I am ${age} years old.`);  //Hi! I am daisy, I am 23 years old.
});

AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

  • 第一个参数[module],是一个数组,里面的成员就是要加载的模块;
  • 第二个参数callback,则是加载成功之后的回调函数。回调函数的参数依次是按照数组顺序依赖的模块内容。

同样的,在define()语句定义模块时,第一个参数数组也可以用于引用其他模块,在回调函数的参数重输出。

特征:

  • 异步加载: 模块代码采用回调函数的形式执行,页面不需要停下等待加载完成,因此是浏览器端常用的模块文件系统。
  • 与CommonJS一样,引用时采用单例模式。
//counter.js 
define([], function(){
  let i = 0;
  return function() {
    return ++i;
  }
});
//main.js 
require(['./counter'], function(counter) {
    console.log(counter());   //1
    console.log(counter());   //2
    console.log(counter());   //3
});
require(['./counter'], function(counter) {
    console.log(counter());   //4
    console.log(counter());   //5
    console.log(counter());   //6
});

CMD

CMD是个人比较不熟悉的,但感觉上更像是AMD和CJS的结合延伸。CMD 推崇 as lazy as possible,sea.js是CMD常用的实现。

语法:

define({ "foo": "bar" });
define('I am a template. My name is {{name}}.');
define(function() {
      // 模块代码
}) ;
//也可以接受依赖
define('hello', ['jquery'], function(require, exports, module) {
  // 模块代码
});
//引用
define(function(require, exports) {
  // 同步获取模块 a 的接口
  const a = require('./a');  
  // 调用模块 a 的方法
  a.doSomething();

 // 异步获取模块b的接口
  require.async('./b', function(b) {
    b.doSomething();
  });
// 异步加载多个模块,在加载完成时,执行回调
  require.async(['./c', './d'], function(c, d) {
    c.doSomething();
    d.doSomething();
  });
});

特征:

  • 与AMD预先加载模块不同的是,CMD提倡就近加载:在需要用到该模块的地方才调用require()require.async()获取模块接口,尽量避免预先加载不需要的模块。
  • 对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同。

对于提前执行延迟执行的理解:AMD在最初加载模块文件时,立刻执行模块中的方法,并将返回值赋值给回调函数的参数;而CMD在调用const a = require('./a.js')后,解析路径后只是将a.js文件中的代码通过<scpript>插入到页面中,并给予唯一的id。只有当真正调用a时,才会去执行对应id的那段js代码。所以叫延迟执行。

  • 同时支持同步异步两种模块加载方式。

ES6

千呼万唤ES6终于推出了模块文件系统。新一代的标准的设计理念是兼容现有的 CommonJS 和 AMD 模块。但目前因为还没有浏览器支持这个功能,我们能够看到的只是模块到处和引用的语法。个人感觉依赖ES6变量结构语法,这套语法用起来会比之前所有文件系统的语法更加顺手。

语法

一个 ES6 的模块是一个包含了 JS 代码的文件。ES6 里没有所谓的 module 关键字。一个模块看起来就和一个普通的脚本文件一样,除了以下两个区别:

  • ES6 的模块自动开启严格模式,即使你没有写 'use strict'。
  • 你可以在模块中使用 import 和 export。
//直接在希望导出的 function、class、var、let 或 const 前添加export关键字
export function detectCats(canvas, options) {
  var kittydar = new Kittydar(options);
  return kittydar.detectCats(canvas);
}
export class Kittydar {
  ... several methods doing image processing ...
}
// This helper function isn't exported.
function resizeCanvas() {
  ...
}

//直接导出对象集合:
export {detectCats, Kittydar};

// no `export` keyword required here
function detectCats(canvas, options) { ... }
class Kittydar { ... }

在另外一个文件中,我们可以导入这个模块并且使用 detectCats() 函数:

//引用整个模块导出对象
import kitty from "kittydar.js";
kitty.detectCats();

//直接引用模块中的某个方法或变量
import {detectCats} from "kittydar.js";

function go() {
    var canvas = document.getElementById("catpix");
    var cats = detectCats(canvas);
    drawRectangles(canvas, cats);
}

//同时引用模块中的多个方法或变量
import {detectCats, Kittydar} from "kittydar.js";

特征:

由于在浏览器端仍没有具体的实现,很难去判断它的特性。Jon Coppeard 正在火狐浏览器上实现 ES6 的模块。之后包括 JavaScript Loader 规范在内的工作已经在进行中。HTML 中类似 <script type=module>这样的东西之后也会和大家见面。

总结:

  • 相同点是,所有的文件模块系统,都采用单例模式。
  • 模块加载方面:CJS采用同步阻塞机制,而AMD采用异步回调机制,CMD可通过调用不同的require方法,采用不同的机制。
  • 模块执行方面:CJS,AMD都采用预先执行,在引用模块的同时执行模块代码;CMD则采用延迟执行,只有当模块中的变量或方法真正被执行时, 模块代码才被执行。
  • 个人觉得服务器端使用CJS和CMD都可以,但比较偏爱CJS简洁的语法。浏览器端从性能方面考虑还是比较推荐使用AMD,尽量避免在代码运行过程中去做资源请求。
  • 特别推荐使用webpack模块打包工具配上ES6的模块文件系统语法,在本地进行模块编译后上线。

参考:
CommonJS规范
阮一峰AMD
AMD和CMD的区别from玉伯
关于AMD 是提前执行,CMD 是延迟执行
ES6文件系统模块

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

推荐阅读更多精彩内容