requriejs

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

2017-03-21  本文已影响181人  newDasiykoo

学习ES6和Webpack过程中,研究了一下CommonJS,AMD, CMD, 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的值。

特征:

//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,它要求两个参数:

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

特征:

//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在调用const a = require('./a.js')后,解析路径后只是将a.js文件中的代码通过<scpript>插入到页面中,并给予唯一的id。只有当真正调用a时,才会去执行对应id的那段js代码。所以叫延迟执行。

ES6

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

语法

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

//直接在希望导出的 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>这样的东西之后也会和大家见面。

总结:

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

上一篇 下一篇

猜你喜欢

热点阅读