资讯专栏INFORMATION COLUMN

浅谈 JavaScript 模块化编程

wdzgege / 1998人阅读

摘要:与在模块化编程的世界中,有两个规范不得不提,它们分别是和。所有依赖于某个模块的代码全部移到模块加载语句的回调函数中去。的语句接受两个参数在回调函数中,可以通过变量引用模块。回调函数的返回值就是当前对象的导出值。

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模块化编程的世界中,有两个规范不得不提,它们分别是AMD和CMD。现在的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 采取的是通用事件机制,插件类型更丰富。

怎么看都像是在自夸啊= =,当然它有这个资格

参考文献

CommonJS官网

阮一峰博客

AMD Github

CMD Github

Sea.js

Require.js

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87540.html

相关文章

  • 浅谈JavaScript中的面向对象

    摘要:面向对象面向对象编程的全称是,简称,面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象编程的三个主要特征是封装继承多态。 面向对象 面向对象编程的全称是Object Oriented Programming,简称OOP,面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式。面向对象编程可以看做是使用一系列对象相互协作的软件设计,面向对象程序设计的目的是在编程中促...

    Magicer 评论0 收藏0
  • 如何设计大型网站的前端 JavaScript 框架

    摘要:前端单元测试,推荐淘宝开源的工具,简单易用,支持众多测试框架,也支持调试。这些也是设计前端框架时需要权衡的重要方面。最后,其实大型网站不一定要设计自己的前端框架,完全可以选用现有的框架。 有人在知乎上提问如何设计大型网站的前端 JavaScript 框架,有不少回答,其中得赞较多的两个回答如下: 相对大型的项目在前端 JS 方面有几个需要达成的目标: 1. 代码逻辑分层 ...

    Yuanf 评论0 收藏0
  • Learning PHP —— 设计模式 | Chap1:浅谈设计模式中的OOP

    摘要:而哈士奇区别于普通狗,又有新的特征逗比,爱捣乱为了保证类之间的松绑定,通常会继承抽象类,而且是浅继承只有一层子类。如果知道所有类都会共享一个公共的行为实现,就使用抽象类,并在其中实现该行为。 为什么使用OOP OOP是一个模块化的过程,目的是为了把复杂问题简单化,一个模块解决一个复杂问题的某一个方面,即一个类应当只有一个职责 OOP区别于顺序式编程与过程式编程,在于: 1.顺序编程...

    SunZhaopeng 评论0 收藏0
  • 浅谈JavaScript中的事件循环机制

    摘要:事件循环背景是一门单线程非阻塞的脚本语言,单线程意味着,代码在执行的任何时候,都只有一个主线程来处理所有的任务。在意识到该问题之际,新特性中的可以让成为一门多线程语言,但实际开发中使用存在着诸多限制。这个地方被称为执行栈。 事件循环(Event Loop) 背景 JavaScript是一门单线程非阻塞的脚本语言,单线程意味着,JavaScript代码在执行的任何时候,都只有一个主线程来...

    Pluser 评论0 收藏0
  • 2018 浅谈前端面试那些事

    摘要:声明的变量不得改变值,这意味着,一旦声明变量,就必须立即初始化,不能留到以后赋值。 虽然今年没有换工作的打算 但为了跟上时代的脚步 还是忍不住整理了一份最新前端知识点 知识点汇总 1.HTML HTML5新特性,语义化浏览器的标准模式和怪异模式xhtml和html的区别使用data-的好处meta标签canvasHTML废弃的标签IE6 bug,和一些定位写法css js放置位置和原因...

    LiuRhoRamen 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<