浅谈 JavaScript 模块化编程

JavaScript本身不是一种模块化言语,设想者在制造JavaScript之初应当也没有想到这么一个剧本言语的作用范畴会愈来愈大。之前一个页面的JS代码再多也不会多到哪儿去,而如今跟着愈来愈多的JavaScript库和框架的涌现,Single-page App的盛行以及Node.js的迅猛发展,假如我们还不对本身的JS代码举行一些模块化的构造的话,开辟历程会愈来愈难题,运转机能也会愈来愈低。因而,相识JS模块化编程是非常重要的。

简朴的模块

什么是模块?我以为将差别功用的函数放在一同,构成一个能完成某种或某些特定功用的团体就是一个模块,因而如许:

function add(a, b) {
  return a + b;
}

function divide(a, b) {
  return a / b;
}

云云简朴的两个函数就能够构成一个模块,这个模块能够举行一些数学运算。

固然没有人会这么写模块。仅仅是从“型”上来看,两个函数疏散在全局环境中,这也看不出模块的特性。模块存在于全局变量中,应当供应一个定名空间,成为模块内容的进口。那末我们能够将函数包裹在一个对象中:

var math = {
  add: function(a, b) {
    return a + b;
  },
  divide: function(a, b) {
    return a / b;
  }
}

如许看起来好像有模块的“型”了。然则如许还不完美,math中的一切成员都是对外暴露的,假如个中有一些变量不愿望被修正的话那就有风险了。为了防备天下被损坏,为了保护私有变量不被修正,我们能够运用闭包。

var math = (function() {
  var _flag = 0;

  return {
    add: function(a, b) {
      return a + b;
    },
    divide: function(a, b) {
      return a / b;
    }
  };
})();

外部代码只能接见返回的adddivide要领,内部的_flag变量是不能接见的。关于建立对象的一些要领的诠释,能够参考我的另一篇博文,内里有较细致的诠释。

应用自实行函数的特性,我们还能够很轻易地为模块增加要领:

var math = (function(module) {
  module.subtract = function(a, b) {
    return a - b;
  }
})(math);

模块在全局变量中的称号能够会与其他的模块发生争执,比方$标记,虽然运用轻易,但多个模块能够都邑用它作为本身的简写,比方jQuery。我们能够在模块的构造代码顶用$作为形参,将模块的全名变量作为参数传入,可起到防争执的结果。

var math = (function($) {
  // 这里的$指的就是Math
})(math);

模块的构建头脑就是经由过程如许的体式格局逐步演变而来,下面将经由过程引见一些JS模块化编程的范例来展现怎样构造,治理和编写模块。

AMDCMD

在JavaScript模块化编程的天下中,有两个范例不得不提,它们分别是AMDCMD。如今的JS库或框架,通常模块化的,平常都是遵照了这两个范例个中之一。

AMD(Asynchronous Module Definition)

CommonJS
在说AMD之前,先要提一下CommonJS。CommonJS是为了填补JavaScript范例库过少的瑕玷而发生的,因为JS没有模块机制(ES6引入了模块体系,但浏览器周全支撑预计另有好几年),CommonJS就协助JS完成模块的功用。如今很热点的Node.js就是CommonJS范例的一个完成。

CommonJS在模块中定义要领要借助一个全局变量exports,它用来天生当前模块的API:

/* math module */

exports.add = function(a, b) {
  return a + b;
};

要加载模块就要运用CommonJS的一个全局要领require()。加载之前完成的math模块像如许:

var math = require('math');

加载后math变量就是这个模块对象的一个援用,要挪用模块中的要领就像挪用一般对象的要领一样了:

var math = require('math');
math.add(1, 3);

总之,CommonJS就是一个模块加载器,能够轻易地对JavaScript代码举行模块化治理。但它也有瑕玷,它在设想之初并没有完整为浏览器环境斟酌,浏览器环境的特性是一切的资本,不斟酌当地缓存的要素,都须要从服务器端加载,加载的速率取决于收集速率,而CommonJS的模块加载历程是同步壅塞的。也就是说假如math模块体积很大,网速又不好的时刻,悉数顺序便会住手,守候模块加载完成。

跟着浏览器端JS资本的体积愈来愈巨大,壅塞给体验带来的不良影响也愈来愈严峻,终究从,在CommonJS社区中有了差别的声响,AMD范例诞生了。

AMD
它的特性就是异步加载,模块的加载不会影响其他代码的运转。一切依靠于某个模块的代码悉数移到模块加载语句的回调函数中去。AMD的require()语句吸收两个参数:

// require([module], callback)
require(['math'], function(math) {
  math.add(1, 3);
});

在回调函数中,能够经由过程math变量援用模块。

AMD范例也划定了模块的定义划定规矩,运用define()函数。

define(id?, dependencies?, factory);

它吸收三个参数:
id
这是一个可选参数,相当于模块的名字,加载器可经由过程id名加载对应的模块。假如没有供应id,加载器会将模块文件名作为默许id。

dependencies
可选,吸收一个数组参数,传入当前对象依靠的对象id。

factory
回调函数,在依靠模块加载完成后会挪用,它的参数是一切依靠模块的援用。回调函数的返回值就是当前对象的导出值。

用AMD范例完成一个简朴的模块能够如许:

define('foo', ['math'], function(math) {
  return {
    increase: function(x) {
      return math.add(x, 1);
    }
  };
});

假如省去id和dependencies参数的话,就是一个完整的匿名模块。factory的参数将为默许值requireexportsmodule加载器将完整经由过程文件途径的体式格局加载模块,同时假如有依靠模块的话可经由过程require要领加载。

define(function(require, exports, module) {
  var math = require('math');

  exports.increase = function(x) {
    return math(x, 1);
  };
});

AMD范例也许可对加载举行一些设置,设置选项不是必需的,但天真变动设置,会给开辟带来一些轻易。

baseUrl 以字符串情势划定根目次的途径,今后在加载模块时都邑以该途径为范例。在浏览器中,事情目次的途径就是运转剧本的网页地点的途径。

{
  baseUrl: './foo/bar'
}

path 能够指定需加载模块的途径,模块名与途径以键-值对的体式格局写在对象中。假如一个模块有多个可选地点,能够将这些地点写在一个数组中。

{
  path: {
    'foo': './bar'
  }
}

关于模块途径的设置项另有packagesmap

shim
关于某些没有根据AMD范例编写的模块,比方jQuery,来讲,要使它们能被加载器加载,须要用shim要领为其设置一些属性。在main模块中,用require.config()要领:

require.config({
  shim: {
    'jquery': {
      exports: '$'
    },
    'foo': {
      deps: [
        'bar',
        'jquery'
      ],
      exports: 'foo'
    }
  }
});

以后再用加载器加载就能够了。

现在完成了AMD范例的库有许多,比较著名的是Require.js

CMD(Common Module Definition)

CMD在许多处所和AMD有类似之处,在这里我只说二者的差别点。

起首,CMD范例和CommonJS范例是兼容的,比拟AMD,它简朴许多。遵照CMD范例的模块,能够在Node.js中运转。

define
与AMD范例差别的是CMD范例中不运用iddeps参数,只保存factory。个中:
1.factory吸收对象/字符串时,表明模块的接口就是对象/字符串。

define({ 'foo': 'bar' });

define('My name is classicemi.');

define.cmd
其值为一个空对象,用于推断页面中是不是有CMD模块加载器。

if (typeof define === 'function' && define.cmd) {
  // 运用CMD模块加载器编写代码
}

require
此函数一样用于猎取模块接口。如需异步加载模块,运用require.async要领。

define(function(require, exports, module) {
  require.async('math', function(math) {
    math.add(1, 2);
  });
});

我们能够发明,require(id)的写法和CommonJS一样是以同步体式格局加载模块。要像AMD范例一样异步加载模块则运用define.async要领。

exports
此要领用于模块对外供应接口。

define(function(require, exports, module) {
  // 对外供应foo属性
  exports.foo = 'bar';

  // 对外供应add要领
  exports.add = function(a, b) {
    return a + b;
  }
});

供应接口的另一个要领是直接return包括接口键值对的对象:

define(function(require, exports, module) {
  return {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

然则注重,不能用exports输出接口对象:

define(function(require, exports, module) {
  exports = {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

如许写是毛病的!
替换体式格局是如许写:

define(function(require, exports, module) {
  module.exports = {
    foo: 'bar',
    add: function(a, b) {
      return a + b;
    }
  }
});

之前毛病的原因是在factory内部,exports实际上是module.exports的一个援用,直接给exports赋值是不会转变module.exports的值的。

在module对象上,除了有上面提到的exports之外,另有一些别的属性和要领。
module.id
模块的标识。

define('math', [], function(require, exports, module) {
  // module.id 的值为 math
});

module.uri
模块的绝对途径,由模块体系剖析获得。

define(function(require, exports, module) {
  console.log(module.uri); // http://xxx.com/path/
});

module.dependencies
值为一个数组,返回本模块的依靠。

Require.js 和 Sea.js

之前在说AMD范例的时刻提到了Require.js。它是AMD范例的代表性产物。另一个Sea.js在前端界也是大名鼎鼎了,CMD范例实际上就是它的产出。它们之间的区分也很能表现AMD和CMD范例之间的区分。

AMD的依靠须要前置誊写

define(['foo', 'bar'], function(foo, bar) {
  foo.add(1, 2);
  bar.subtract(3, 4);
});

CMD的依靠就近誊写即可,不须要提早声明:
同步式:

define(function(require, exports, module) {
  var foo = require('foo');
  foo.add(1, 2);
  ...
  var bar = require('bar');
  bar.subtract(3, 4);
});

异步式:

define(function(require, exports, module) {
  ...
  require.async('math', function(math) {
    math.add(1, 2);
  });
  ...
});

虽然AMD也能够用和CMD类似的要领,但不是官方引荐的。

之前在引见CMD的API时,我们能够发明其API职责专注,比方同步加载和异步加载的API都分为requirerequire.async,而AMD的API比较多功用。

总而言之,援用玉伯的总结:
1. Require.js同时适用于浏览器端和服务器环境的模块加载。Sea.js则专注于浏览器端的模块加载完成。经由过程Node扩大也能够运转于Node环境中。
2. Require.js -> AMD,Sea.js -> CMD。
3. RequireJS 在尝试让第三方类库修正本身来支撑 RequireJS,现在只要少数社区采纳。Sea.js 不强推,采纳自立封装的体式格局来“海纳百川”,现在已有较成熟的封装战略。
4. Sea.js的调试东西比较完整,Require.js调试比较不轻易。
5. RequireJS 采用的是在源码中预留接口的情势,插件范例比较单一。Sea.js 采用的是通用事宜机制,插件范例更雄厚。

怎么看都像是在自诩啊= =,固然它有这个资历

参考文献

  1. CommonJS官网
  2. 阮一峰博客
  3. AMD Github
  4. CMD Github
  5. Sea.js
  6. Require.js
    原文作者:classicemi
    原文地址: https://segmentfault.com/a/1190000000492678
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞