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

2018-11-30  本文已影响17人  卓三阳

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 模块化管理的来世今生

上一篇下一篇

猜你喜欢

热点阅读