理解JS中的模块规范(CommonJS,AMD,CMD)

  随着互联网的飞速发展,前端开发愈来愈复杂。本文将从实际项目中遇到的问题出发,讲述模块化能解决哪些问题,以及如何使用 Sea.js 进行前端的模块化开发。css

恼人的命名冲突

  咱们从一个简单的习惯出发。我作项目时,经常会将一些通用的、底层的功能抽象出来,独立成一个个函数,好比前端

function each(arr) { // 实现代码
} function log(str) { // 实现代码
}

  并像模像样地把这些函数统一放在 util.js 里。须要用到时,引入该文件就行。这一切工做得很好,同事也很感激我提供了这么便利的工具包。直到团队愈来愈大,开始有人抱怨。node

小杨:我想定义一个 each 方法遍历对象,但页头的 util.js 里已经定义了一个,个人只能叫 eachObject 了,好无奈。git

小高:我自定义了一个 log 方法,为何小明写的代码就出问题了呢?谁来帮帮我。程序员

  抱怨愈来愈多。团队通过一番激烈的讨论,决定参照 Java 的方式,引入命名空间来解决。因而 util.js 里的代码变成了github

var org = {}; org.CoolSite = {}; org.CoolSite.Utils = {}; org.CoolSite.Utils.each = function (arr) { // 实现代码
}; org.CoolSite.Utils.log = function (str) { // 实现代码
};

  不要认为上面的代码是为了写这篇文章而故意捏造的。将命名空间的概念在前端中发扬光大,首推 Yahoo! 的 YUI2 项目。下面是一段真实代码,来自 Yahoo! 的一个开源项目。后端

if (org.cometd.Utils.isString(response)) { return org.cometd.JSON.fromJSON(response); } if (org.cometd.Utils.isArray(response)) { return response; }

  经过命名空间,的确能极大缓解冲突。但往往看到上面的代码,都忍不住充满同情。为了调用一个简单的方法,须要记住如此长的命名空间,这增长了记忆负担,同时剥夺了很多编码的乐趣。api

  做为前端业界的标杆,YUI 团队下定决心解决这一问题。在 YUI3 项目中,引入了一种新的命名空间机制。浏览器

YUI().use('node', function (Y) { // Node 模块已加载好 // 下面能够经过 Y 来调用
  var foo = Y.one('#foo'); });

  YUI3 经过沙箱机制,很好的解决了命名空间过长的问题。然而,也带来了新问题。性能优化

YUI().use('a', 'b', function (Y) { Y.foo(); // foo 方法到底是模块 a 仍是 b 提供的? // 若是模块 a 和 b 都提供 foo 方法,如何避免冲突?
});

  看似简单的命名冲突,实际解决起来并不简单。如何更优雅地解决?咱们按下暂且不表,先来看另外一个常见问题。

烦琐的文件依赖

  继续上面的故事。基于 util.js,我开始开发 UI 层通用组件,这样项目组同事就不用重复造轮子了。其中有一个最被你们喜欢的组件是 dialog.js,使用方式很简单。

<script src="util.js"></script>
<script src="dialog.js"></script>
<script> org.CoolSite.Dialog.init({ /* 传入配置 */ }); </script>

  但是不管我怎么写文档,以及多么郑重地发邮件宣告,时不时总会有同事来询问为何 dialog.js 有问题。经过一番排查,发现致使错误的缘由常常是

<script src="dialog.js"></script>
<script> org.CoolSite.Dialog.init({ /* 传入配置 */ }); </script>

  在 dialog.js 前没有引入 util.js,所以 dialog.js 没法正常工做。一样不要觉得我上面的故事是虚构的,在我待过的公司里,至今依旧有相似的脚本报错,特别是在各类快速制做的营销页面中。

  上面的文件依赖还在可控范围内。当项目愈来愈复杂,众多文件之间的依赖常常会让人抓狂。下面这些问题,我相信天天都在真实地发生着。

  1. 通用组更新了前端基础类库,却很难推进全站升级。
  2. 业务组想用某个新的通用组件,但发现没法简单经过几行代码搞定。
  3. 一个老产品要上新功能,最后评估只能基于老的类库继续开发。
  4. 公司整合业务,某两个产品线要合并。结果发现前端代码冲突。
  5. ……

  以上不少问题都是由于文件依赖没有很好的管理起来。在前端页面里,大部分脚本的依赖目前依旧是经过人肉的方式保证。当团队比较小时,这不会有什么问题。当团队愈来愈大,公司业务愈来愈复杂后,依赖问题若是不解决,就会成为大问题。

  文件的依赖,目前在绝大部分类库框架里,好比国外的 YUI3 框架、国内的 KISSY 等类库,目前是经过配置的方式来解决。

YUI.add('my-module', function (Y) { // ...
}, '0.0.1', { requires: ['node', 'event'] });

  上面的代码,经过 requires 等方式来指定当前模块的依赖。这很大程度上能够解决依赖问题,但不够优雅。当模块不少,依赖很复杂时,烦琐的配置会带来很多隐患。

  命名冲突和文件依赖,是前端开发过程当中的两个经典问题。下来咱们看如何经过模块化开发来解决。为了方便描述,咱们使用 Sea.js 来做为模块化开发框架。

使用 Sea.js 来解决

  Sea.js 是一个成熟的开源项目,核心目标是给前端开发提供简单、极致的模块化开发体验。这里很少作介绍,有兴趣的能够访问 seajs.org 查看官方文档。

  使用 Sea.js,在书写文件时,须要遵照 CMD (Common Module Definition)模块定义规范。一个文件就是一个模块。

  前面例子中的 util.js 变成

define(function(require, exports) { exports.each = function (arr) { // 实现代码
 }; exports.log = function (str) { // 实现代码
 }; });

  经过 exports 就能够向外提供接口。这样,dialog.js 的代码变成

define(function(require, exports) { var util = require('./util.js'); exports.init = function() { // 实现代码
 }; });

  关键部分到了!咱们经过 require('./util.js') 就能够拿到 util.js 中经过 exports 暴露的接口这里的require 能够认为是 Sea.js 给 JavaScript 语言增长的一个语法关键字,经过 require 能够获取其余模块提供的接口。

  这其实一点也不神奇。做为前端工程师,对 CSS 代码必定也不陌生。

@import url("base.css"); #id { ... } .class { ... }

  Sea.js 增长的 require 语法关键字,就如 CSS 文件中的 @import 同样,给咱们的源码赋予了依赖引入功能。

  若是你是后端开发工程师,更不会陌生。Java、Python、C# 等等语言,都有 includeimport 等功能。JavaScript 语言自己也有相似功能,但目前还处于草案阶段,须要等到 ES6 标准获得主流浏览器支持后才能使用。

  这样,在页面中使用 dialog.js 将变得很是简单。

<script src="sea.js"></script>
<script> seajs.use('dialog', function(Dialog) { Dialog.init(/* 传入配置 */); }); </script>

  首先要在页面中引入 sea.js 文件,这通常经过页头全局把控,也方便更新维护。想在页面中使用某个组件时,只要经过 seajs.use 方法调用。

  好好琢磨以上代码,我相信你已经看到了 Sea.js 带来的两大好处:

一、经过 exports 暴露接口。这意味着不须要命名空间了,更不须要全局变量。这是一种完全的命名冲突解决方案。

二、经过 require 引入依赖。这可让依赖内置,开发者只需关心当前模块的依赖,其余事情 Sea.js 都会自动处理好。对模块开发者来讲,这是一种很好的关注度分离,能让程序员更多地享受编码的乐趣。

小结

  除了解决命名冲突和依赖管理,使用 Sea.js 进行模块化开发还能够带来不少好处:

一、模块的版本管理。经过别名等配置,配合构建工具,能够比较轻松地实现模块的版本管理。

二、提升可维护性。模块化可让每一个文件的职责单一,很是有利于代码的维护。Sea.js 还提供了 nocache、debug 等插件,拥有在线调试等功能,能比较明显地提高效率。

三、前端性能优化。Sea.js 经过异步加载模块,这对页面性能很是有益。Sea.js 还提供了 combo、flush 等插件,配合服务端,能够很好地对页面性能进行调优。

四、跨环境共享模块。CMD 模块定义规范与 Node.js 的模块规范很是相近。经过 Sea.js 的 Node.js 版本,能够很方便实现模块的跨服务器和浏览器共享。

  模块化开发并非新鲜事物,但在 Web 领域,前端开发是新生岗位,一直处于比较原始的刀耕火种时代。直到最近两三年,随着 Dojo、YUI三、Node.js 等社区的推广和流行,前端的模块化开发理念才逐步深刻人心。

  前端的模块化构建可分为两大类。一类是以 Dojo、YUI三、国内的 KISSY 等类库为表明的大教堂模式。在大教堂模式下,全部组件都是颗粒化、模块化的,各组件之间层层分级、环环相扣。另外一类是以 jQuery、RequireJS、国内的 Sea.js、OzJS 等类库为基础的集市模式。在集市模式下,全部组件彼此独立、职责单一,各组件经过组合松耦合在一块儿,协同完成开发。

  这两类模块化构建方式各有应用场景。从长远来看,小而美更具有宽容性和竞争力,更能造成有活力的生态圈。

  总之,模块化能给前端开发带来不少好处。若是你尚未尝试,不妨从试用 Sea.js 开始。

模块话的原理和必要性:

  若是你听过js模块化这个东西,那么你就应该听过或CommonJS或AMD甚至是CMD这些规范咯,我也听过,但以前也真的是听听而已。如今就看看吧,这些规范究竟是啥东西,干吗的。

1、CommonJS

  CommonJS就是为JS的表现来制定规范,由于js没有模块的功能因此CommonJS应运而生,它但愿js能够在任何地方运行,不仅是浏览器中。

  CommonJS能有必定的影响力,我以为绝对离不开Node的人气,不过喔,Node,CommonJS,浏览器甚至是W3C之间有什么关系呢,我找到了个贴切的图:

  |--------------------浏览器----- ------------------|         |--------------------------CommonJS----------------------------------|

  |  BOM  |       | DOM |        | ECMAScript |         | FS |           | TCP |         | Stream |        | Buffer |          |........|

  |---------W3C----------|        |-----------------------------------------------Node--------------------------------------------------|

  CommonJS定义的模块分为:{模块引用(require)} {模块定义(exports)} {模块标识(module)}

  require()用来引入外部模块;exports对象用于导出当前模块的方法或变量,惟一的导出口;module对象就表明模块自己。

  好比说咱们就能够这样用了:

//sum.js
exports.sum = function(){...作加操做..}; //calculate.js
var math = require('sum'); exports.add = function(n){ return math.sum(val,n); };

  虽然说Node遵循CommonJS的规范,可是相比也是作了一些取舍,填了一些新东西的。不过,说了CommonJS也说了Node,那么我以为也得先了解下NPM了。NPM做为Node的包管理器,不是为了帮助Node解决依赖包的安装问题嘛,那它确定也要遵循CommonJS规范啦,它遵循包规范(仍是理论)的。

2、AMD

  CommonJS是主要为了JS在后端的表现制定的,他是不适合前端的,为何这么说呢?

  这须要分析一下浏览器端的js和服务器端js都主要作了哪些事,有什么不一样了:

----------------------------------------------服务器端JS   |    浏览器端JS--------------------------------------------------

                                   相同的代码须要屡次执行  |    代码须要从一个服务器端分发到多个客户端执行

                                     CPU和内存资源是瓶颈   |    带宽是瓶颈

                                         加载时从磁盘中加载   |    加载时须要经过网络加载

---------------------------------------------------------------------------------------------------------------------------------------

  因而乎,AMD(异步模块定义)出现了,它就主要为前端JS的表现制定规范。

  AMD就只有一个接口:define(id?,dependencies?,factory);

  它要在声明模块的时候制定全部的依赖(dep),而且还要当作形参传到factory中,像这样:

define(['dep1','dep2'],function(dep1,dep2){...});

  要是没什么依赖,就定义简单的模块,下面这样就能够啦:

define(function(){ var exports = {}; exports.method = function(){...}; return exports; });

  这里有define,把东西包装起来啦,那Node实现中怎么没看到有define关键字呢,它也要把东西包装起来呀,其实吧,只是Node隐式包装了而已.....

  RequireJS就是实现了AMD规范的呢。

  这有AMD的WIKI中文版,讲了不少蛮详细的东西,用到的时候能够查看:AMD的WIKI中文版

3、CMD

  大名远扬的玉伯写了seajs,就是遵循他提出的CMD规范,与AMD蛮相近的,不过用起来感受更加方便些,最重要的是中文版。

define(function(require,exports,module){...});
相关文章
相关标签/搜索