CommonJS & AMD & CMD浅析

作者 Kylewh 日期 2017-01-03
JS
CommonJS & AMD & CMD浅析

为什么要使用模块化?

  • 网站更多转型向网络应用
  • 网站规模的增加导致代码的复杂度增加
  • 性能优化,需要更少的HTTP请求

模块化的作用?

  • 解决命名冲突问题
  • 文件之间依赖管理
  • 代码间的解耦,提高复用性。

CMD、AMD、CommonJS 规范分别指什么?有哪些应用?

现代模块实现的基石:

var Module = (function($){
var $body = $("body"); // we can use jQuery now!
var foo = function(){
console.log($body); // 特权方法
}
// Revelation Pattern
return {
foo: foo
}
})(jQuery)
Module.foo();

以上的形式只实现了封装性,却不能保证模块之间的加载执行过程。(顺序)

CommonJS规范

由NodeJS社区的繁荣而兴起,它提供了非常简单的模块输出与引入的方式:

//math.js
//定义与输出
function increment(num) {
return num++;
}
function decrement(num) {
return num--;
}
module.exports = {
increment: increment,
decrement: decrement
}
//引入
const MATH = require('math');
var num = 0;
console.log(MATH.increment(num)); //1;
console.log(MATH.increment(num)); //-1;

特点:

  • 通过exports或者module.exports来暴露模块对象
  • 所有代码都运行在模块作用域,不会污染全局作用域。
  • 模块可以多次加载,但是只会在第一次加载时运行并且缓存,以之后加载直接读取缓存结果。要想让模块再次运行,必须清除缓存
  • 模块加载的顺序,按照其在代码中出现的顺序。

  • 同步/阻塞式加载

//来一个timeout
const time = 2;
(function(time){
var start = +new Date();
while(start + second*1000 > new Date()) { // do nothing}
})(time);
console.log( time + 'second executed');
// main.js
require('./timeout'); // sync load
console.log('done!');

main.js执行结果:

//两秒后
//输出: 2 second execcuted
//输出: done!

But! 这一切都是在服务端NodeJS环境下执行的,而在浏览器环境下,同步式加载而导致的长时间等待和卡顿是会让用户抓狂的,而且浏览器端代码的引入执行是通过script动态添加的方式,由于script标签的异步加载特性,无法保证各代码文件的加载顺序,从而引起报错。

AMD/CMD规范的出现解决了针对浏览器环境的模块加载问题。

Asynchronous Module Definition - AMD

见名知意,模块可以异步加载,采用回调方式进行依赖加载->执行代码,保证了模块的运行正常,
最典型的应用便是RequireJS。

策略: 预加载,预执行,返回对象作为模块。

//定义
define( 'ID', ['dependencies arry'], factory function)
//example
//carousel.js
define('carousel', ['jquery'],
function($){
let Carousel = (function(){
function _Carousel () {
this.$ct = $ct;
// do sth........
}
return {
init: function($ct) {
new _Carousel($ct);
}
}
})($ct)
});
//index.js
//引入
define('index', ['jquery', 'carousel'],
function($, Carousel){
Carousel.init($('.carousel_ct'));
});

关于AMD的执行机制,如何验证?

DEMO

//main.js
requirejs.config({
baseurl: '.',
path: {
}
});
requirejs(['index']);
//index.js
define('index', ['require', 'math', 'timeout'], function(require){
console.log('index.js executed ');
var math = require('math');
var timeout = require('timeout');
math.increment(0);
math.decrement(0);
timeout(2);
});
//math.js
define('math', [], function(){
console.log('math.js required & executed ');
var math = (function(){
function increment (num) {
console.log('increment executed...');
num++;
console.log('result is ' + num);
return num;
}
function decrement (num) {
console.log('decrement executed...');
num--;
console.log('result is ' + num);
return num;
}
return {
increment: increment,
decrement: decrement
};
})();
return math;
});
//timeout.js
define('timeout', [], function(){
console.log('timeout.js required & executed ');
return function (time) {
var start = +new Date();
console.log('start: ' + new Date());
while(start + time*1000 > new Date()) {
}
console.log('finished: ' + new Date());
console.log(time + ' second passed, we are finished');
};
});

执行结果:(实际代码封装了render模块,console.log全部替换为render,将结果渲染到页面上)

我们可以清楚的看见,所有dependencies都进行了预加载&执行,这是与接下来要介绍的CMD规范最大的不同之处。在语法层面,与CommonJS语法互通,AMD为CommonJS留下了grammar sugar, 上面index.js中的var math = require('math');写法便是典型的CommonJS写法,即依赖就近(虽然依旧在头部早早引入了依赖);以返回一个对象作为模块对象。

Common Module Definition - CMD

CMD规范由来源于玉伯开发的Sea.js,一个文件便是一个模块。

策略: 按需加载

//定义
//log.js
define(function(require, exports, module) {
exports.log = function () { //暴露接口
console.log('I am a exported module');
};
module.id = 'log';
});
//引入
//index.js
define(function(require, exports) {
//require获取接口
var log = require('log');
});
//I am a exported module

加载 & 执行机制观测:

使用同样的代码结构,换为CMD语法,执行AMD例子中的index.js, 为了观察到require和执行的顺序,我们将index.js的代码顺序修改一下:

DEMO

define(function (require, exports, module) {
console.log('index.js executed ');
var math = require('../js/math');
math.increment(0);
math.decrement(0);
var timeout = require('../js/timeout');
timeout(2);
});

执行结果如下:

跟我们的代码顺序完全一致,而不是像AMD一样预先加载&立刻执行。

以上两个范例代码: github

性能分析

  • 对于AMD,策略为一劳永逸,在规模较大,模块较多时,首屏加载性能可能稍差,但是后续的运行会非常顺畅。
  • 对于CMD,策略为按需取物,即时执行,与AMD同等环境下,首屏加载性能较快,在后续运行中可能会有卡顿现象。

主流?

  • AMD
    • requireJS 过去式
  • CMD
    • seaJS 过去式
    • Browserify 小而美
    • Webpack 大而全

requireJS应用DEMO

  • 应用requireJS,打包优化 (辣鸡uglify,毁我青春
  • TAB支持可见区域自动切换 (已做debounce处理,下次试试throttle,underscoreJS的源码真心得好好看看
  • 首屏自动轮播,支持前后按钮点击跳转,缩略导航图点击跳转。(未作animate.stop()处理,目测animate自带debounce
  • jsonp请求数据,点击按钮,可无限请求。 (小坑: github会对http请求做保守拦截,将请求地址协议改为https可行,只是会报警内容混合,视觉上无碍
  • 时光轴采用懒加载模式 (封装了LoadTrigger函数,对于事件触发引起回调在数据结构上本质属于data consumer与data producer的关系,只在乎谁push谁。所以对这种模式封装(虽然我封装的很不抽象),传入触发事件类型以及回调和参数即可
  • 大于设置scrolltop尺寸右下角浮现gotop按钮 (个人认为还是手动在html内添加元素加上对应classname更加符合解耦的原则,用过的框架大多也是这么设计,然而我还是偷懒用js添加node…)

code

preview

PITFALL ALERT!

requireJS的optimizer里内置的uglifyJS不支持ES6语法,即时使用babel官网提供针对requireJS的build方案也不好使,建议大家直接写ES5吧,毕竟requireJS已经消逝了….勿踩坑。

总结

  • CommonJS:

    • 代表: NodeJS
    • 特点: 模块具有独立作用域,运行代码不会污染全局。 模块的引入会产生缓存,加载顺序遵循引入顺序,阻塞式加载(这就是为什么需要AMD,CMD的原因)。
    • 语法:
      // In a.js
      const num = 100
      const decrement = (num) => num--
      moudle.exports = {
      num: a,
      decrement: decrement
      }
    // In b.js
    const { num, decrement } = require('./a') //./a.js
    console.log( decrement(num) ) // 99
  • AMD:

    • 代表: requireJS
    • 特点:预加载,预执行,首屏性能较差。
    • 语法:
      // In a.js
      define('a', [/* nothing */], function(){
      const num = 100
      const decrement = (num) => num--
      return {
      num: num,
      decrement: decrement
      }
      })
    // In b.js
    define('b', ['require', 'a'], function(require){
    const { num, decrement } = require('a')
    console.log(decrement(num)) // 99
    })
  • CMD:

    • 代表: sea.js
    • 特点: 按需加载&执行
    • 语法:
      //In a.js
      define(function(exports, module) {
      const num = 100
      const decrement = (num) => num--
      // or return { num: num , decrement: decrement}
      module.exports = {
      num: num,
      decrement: decrement
      }
      })
    // In b.js
    define(function(require){
    const { num, decrement } = require('./a')
    // we can also use async require
    /*
    require.async('./a', function({ num, decrement }) {
    console.log(decrement(num)) //99
    })
    */
    console.log( decrement(num) ) // 99
    })