JavaScript模块化演变 CommonJs,AMD, CMD, UMD(一)

原文连接:https://www.jianshu.com/p/33d53cce8237javascript

原文系列2连接:https://www.jianshu.com/p/ad427d8879cb html

前端彻底手册: https://leohxj.gitbooks.io/front-end-database/content/javascript-oop/encapsulation.html前端

本文参考
Javascript模块化编程(一):模块的写法
Javascript模块化编程(二):AMD规范
Javascript模块化编程(三):require.js的用法java

随着网站逐渐变成"互联网应用程序",嵌入网页的Javascript代码愈来愈庞大,愈来愈复杂。网页愈来愈像桌面程序,须要一个团队分工协做、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。Javascript模块化编程,已经成为一个迫切的需求。理想状况下,开发者只须要实现核心的业务逻辑,其余均可以加载别人已经写好的模块。可是,Javascript不是一种模块化编程语言,它不支持"类"(class),更遑论"模块"(module)了。(正在制定中的ECMAScript标准第六版,将正式支持"类"和"模块",但还须要很长时间才能投入实用。)Javascript社区作了不少努力,在现有的运行环境中,实现"模块"的效果。本文总结了当前"Javascript模块化编程"的最佳实践,说明如何投入实用。虽然这不是初级教程,可是只要稍稍了解Javascript的基本语法,就能看懂。node

1、原始写法

模块就是实现特定功能的一组方法。只要把不一样的函数(以及记录状态的变量)简单地放在一块儿,就算是一个模块。git

  function m1(){
    //...
  }
  function m2(){
    //...
  }

上面的函数m1()和m2(),组成一个模块。使用的时候,直接调用就好了。这种作法的缺点很明显:"污染"了全局变量,没法保证不与其余模块发生变量名冲突,并且模块成员之间看不出直接关系。程序员

2、对象写法

为了解决上面的缺点,能够把模块写成一个对象,全部的模块成员都放到这个对象里面。github

 var module1 = new Object({
    _count : 0,
    m1 : function (){
      //...
    },
    m2 : function (){
      //...
    }
  });

上面的函数m1()和m2(),都封装在module1对象里。使用的时候,就是调用这个对象的属性。module1.m1();可是,这样的写法会暴露全部模块成员,内部状态能够被外部改写。好比,外部代码能够直接改变内部计数器的值。module1._count = 5;web

其实这种写法在ES5中又叫命名空间,参考 JS命名空间的使用express


var MYNAMESPACE = MYNAMESPACE || {};

MYNAMESPACE.person = function(name) {
    this.name = name;
};

MYNAMESPACE.person.prototype.getName = function() {
    return this.name;
};

// 使用方法
var p = new MYNAMESPACE.person("doc");
p.getName();
在没有相似 AMD 这样的模块化的格式和规范时,在前端的 JavaScript 中,命名空间是是一个很是棒的实践,用于避免全局变量污染以及用于组织代码模块的逻辑性,扩展性,可读性和可维护性。
 
一般选择一个顶级的应用命名空间而且这个命名空间(即对象)是惟一一个挂载到全局对象上的对象(在浏览器中是 window,在 Node.js 应用中是 global)。当选择了应用的顶级命名空间,嵌套命名空间可被用于一个模块化的架构。
例如,考虑下面:
var myMasterNS = myMasterNS || {};
myMasterNS.mySubNS = myMasterNS.mySubNS || {};
myMasterNS.mySubNS.someFunction = function(){
 //插入逻辑 
};

这里咱们声明了一个名为 myMasterNS 的简单全局对象,又分配了一个子命名空间对象做为原来命名空间的一个属性,这个例子中是 mySubNS。如今咱们可以在顶级命名空间下实现任何所需的功能,而且任何后面的子命名空间都不会污染全局做用域。请注意,这不是使用 JavaScript 实现命名空间的惟一模式。我在这个例子中选择它,是由于这种模式的简单和可读性。

3、当即执行函数写法

使用"当即执行函数"(Immediately-Invoked Function Expression,IIFE),能够达到不暴露私有成员的目的。

  var module1 = (function(){
    var _count = 0;
    var m1 = function(){
      //...
    };
    var m2 = function(){
      //...
    };
    return {
      m1 : m1,
      m2 : m2
    };
  })();

使用上面的写法,外部代码没法读取内部的_count变量。console.info(module1._count); //undefined
module1就是Javascript模块的基本写法。下面,再对这种写法进行加工。

4、放大模式

若是一个模块很大,必须分红几个部分,或者一个模块须要继承另外一个模块,这时就有必要采用"放大模式"(augmentation)。

  var module1 = (function (mod){
    mod.m3 = function () {
      //...
    };
    return mod;
  })(module1);

上面的代码为module1模块添加了一个新方法m3(),而后返回新的module1模块。

5、宽放大模式(Loose augmentation)

在浏览器环境中,模块的各个部分一般都是从网上获取的,有时没法知道哪一个部分会先加载。若是采用上一节的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"。

  var module1 = ( function (mod){
    //...
    return mod;
  })(window.module1 || {});

 

与"放大模式"相比,"宽放大模式"就是"当即执行函数"的参数能够是空对象。

6、输入全局变量

独立性是模块的重要特色,模块内部最好不与程序的其余部分直接交互。为了在模块内部调用全局变量,必须显式地将其余变量输入模块。

  var module1 = (function ($, YAHOO) {
    //...

  })(jQuery, YAHOO);

上面的module1模块须要使用jQuery库和YUI库,就把这两个库(实际上是两个模块)看成参数输入module1。这样作除了保证模块的独立性,还使得模块之间的依赖关系变得明显。这方面更多的讨论,参见Ben Cherry的著名文章《JavaScript Module Pattern: In-Depth》

7、模块的规范

先想想,为何模块很重要?由于有了模块,咱们就能够更方便地使用别人的代码,想要什么功能,就加载什么模块。可是,这样作有一个前提,那就是你们必须以一样的方式编写模块,不然你有你的写法,我有个人写法,岂不是乱了套!考虑到Javascript模块如今尚未官方规范,这一点就更重要了。目前,通行的Javascript模块规范共有两种:CommonJSAMD。我主要介绍AMD,可是要先从CommonJS讲起。

8、CommonJS

2009年,美国程序员Ryan Dahl创造了node.js项目,将javascript语言用于服务器端编程。这标志"Javascript模块化编程"正式诞生。由于老实说,在浏览器环境下,没有模块也不是特别大的问题,毕竟网页程序的复杂性有限;可是在服务器端,必定要有模块,与操做系统和其余应用程序互动,不然根本无法编程。

node.js的模块系统,就是参照CommonJS规范实现的。在CommonJS中,有一个全局性方法require(),用于加载模块。假定有一个数学模块math.js,就能够像下面这样加载。

 

var math = require('math');

 

而后,就能够调用模块提供的方法:

  var math = require('math');
  math.add(2,3); // 5

关于CommonJs后文详述

9、浏览器环境

有了服务器端模块之后,很天然地,你们就想要客户端模块。并且最好二者可以兼容,一个模块不用修改,在服务器和浏览器均可以运行。可是,因为一个重大的局限,使得CommonJS规范不适用于浏览器环境。仍是上一节的代码,若是在浏览器中运行,会有一个很大的问题,你能看出来吗?

  var math = require('math');

  math.add(2, 3);

第二行math.add(2, 3),在第一行require('math')以后运行,所以必须等math.js加载完成。也就是说,若是加载时间很长,整个应用就会停在那里等。这对服务器端不是一个问题,由于全部的模块都存放在本地硬盘,能够同步加载完成,等待时间就是硬盘的读取时间。可是,对于浏览器,这倒是一个大问题,由于模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。所以,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。

10、AMD

AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。全部依赖这个模块的语句,都定义在一个回调函数中,等到加载完成以后,这个回调函数才会运行。AMD也采用require()语句加载模块,可是不一样于CommonJS,它要求两个参数:

require([module], callback);

第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功以后的回调函数。若是将前面的代码改写成AMD形式,就是下面这样:

  require(['math'], function (math) {
    math.add(2, 3);
  });

math.add()与math模块加载不是同步的,浏览器不会发生假死。因此很显然,AMD比较适合浏览器环境。目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js。本系列的第三部分,将经过介绍require.js,进一步讲解AMD的用法,以及如何将模块化编程投入实战。

11、为何要用require.js?

最先的时候,全部Javascript代码都写在一个文件里面,只要加载这一个文件就够了。后来,代码愈来愈多,一个文件不够了,必须分红多个文件,依次加载。下面的网页代码,相信不少人都见过。

<script src="1.js"></script>
<script src="2.js"></script>
<script src="3.js"></script>
<script src="4.js"></script>
<script src="5.js"></script>
<script src="6.js"></script>

这段代码依次加载多个js文件。这样的写法有很大的缺点。首先,加载的时候,浏览器会中止网页渲染,加载文件越多,网页失去响应的时间就会越长;其次,因为js文件之间存在依赖关系,所以必须严格保证加载顺序(好比上例的1.js要在2.js的前面),依赖性最大的模块必定要放到最后加载,当依赖关系很复杂的时候,代码的编写和维护都会变得困难。require.js的诞生,就是为了解决这两个问题:
(1)实现js文件的异步加载,避免网页失去响应;
(2)管理模块之间的依赖性,便于代码的编写和维护。

12、require.js的加载

使用require.js的第一步,是先去官方网站下载最新版本。下载后,假定把它放在js子目录下面,就能够加载了。

<script src="js/require.js"></script>

有人可能会想到,加载这个文件,也可能形成网页失去响应。解决办法有两个,一个是把它放在网页底部加载,另外一个是写成下面这样:

<script src="js/require.js" defer async="true" ></script>

async属性代表这个文件须要异步加载,避免网页失去响应。IE不支持这个属性,只支持defer,因此把defer也写上。加载require.js之后,下一步就要加载咱们本身的代码了。假定咱们本身的代码文件是main.js,也放在js目录下面。那么,只须要写成下面这样就好了:

<script src="js/require.js" data-main="js/main"></script>

data-main属性的做用是,指定网页程序的主模块。在上例中,就是js目录下面的main.js,这个文件会第一个被require.js加载。因为require.js默认的文件后缀名是js,因此能够把main.js简写成main。

十3、require.js主模块的写法

上一节的main.js,我把它称为"主模块",意思是整个网页的入口代码。它有点像C语言的main()函数,全部代码都从这儿开始运行。下面就来看,怎么写main.js。若是咱们的代码不依赖任何其余模块,那么能够直接写入javascript代码。

  // main.js
  alert("加载成功!");

但这样的话,就不必使用require.js了。真正常见的状况是,主模块依赖于其余模块,这时就要使用AMD规范定义的的require()函数。

  // main.js
  require(['moduleA', 'moduleB', 'moduleC'], function (moduleA, moduleB, moduleC){
    // some code here
  });

require()函数接受两个参数。第一个参数是一个数组,表示所依赖的模块,上例就是['moduleA', 'moduleB', 'moduleC'],即主模块依赖这三个模块;第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用。加载的模块会以参数形式传入该函数,从而在回调函数内部就可使用这些模块。require()异步加载moduleA,moduleB和moduleC,浏览器不会失去响应;它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。

更多细节参考
http://www.requirejs.cn
【JavaScript】RequireJS模块化之HelloWorld

十4、CMD

参考
浅析JS模块规范:AMD,CMD,CommonJS
AMD 和 CMD 的区别有哪些?
AMD虽然实现了异步加载,可是开始就把全部依赖写出来是不符合书写的逻辑顺序的,能不能像commonJS那样用的时候再require,并且还支持异步加载后再执行呢?CMD (Common Module Definition), 是seajs推崇的规范,CMD则是依赖就近,用的时候再require。它写起来是这样的:

define(function(require, exports, module) {
   var clock = require('clock');
   clock.start();
});

AMD和CMD最大的区别是对依赖模块的执行时机处理不一样,而不是加载的时机或者方式不一样,两者皆为异步加载模块。

AMD依赖前置,js能够方便知道依赖模块是谁,当即加载;而CMD就近依赖,须要使用把模块变为字符串解析一遍才知道依赖了那些模块,这也是不少人诟病CMD的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到能够忽略。

十5、UMD

UMD是AMD和CommonJS的糅合。AMD模块以浏览器第一的原则发展,异步加载模块。CommonJS模块以服务器第一原则发展,选择同步加载,它的模块无需包装(unwrapped modules)。这迫令人们又想出另外一个更通用的模式UMD (Universal Module Definition)。但愿解决跨平台的解决方案。

UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式。在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。

(function (window, factory) {
    if (typeof exports === 'object') {
     
        module.exports = factory();
    } else if (typeof define === 'function' && define.amd) {
     
        define(factory);
    } else {
     
        window.eventUtil = factory();
    }
})(this, function () {
    //module ...
});

 

十6、JavaScript模块化编程探索

CommonJS团队定义了module格式来解决JavaScript做用域问题,这样确保了每个module都在本身的命名空间下执行。根据CommonJS的规范,每一个文件就是一个模块,有本身的做用域。在一个文件里面定义的变量、函数、类,都是私有的,对其余文件不可见。CommonJS规范规定,每一个模块内部,module变量表明当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,实际上是加载该模块的module.exports属性。CommonJS给出2个工具来实现模块之间的依赖:

  • require() 用于在当前做用域引入已有的模块
  • module object 用于从当前做用域导出一些东东

那就先搞一个Hello world的小栗子来试下吧!新建一个项目文件夹吧,虽然项目很小。。。起名commonjs,在里边新建2个JavaScript文件,分别命名为world.js和salute.js,代码以下:

// salute.js 打招呼
var MySalute = "Hello";
module.exports = MySalute;

/*注意上下是分别写在2个文件js文件里哦*/
// world.js
var MySalute = require("./salute");
var Result = MySalute + " world!";
console.log(Result);

 

而后无知的我有新建了一个demo.html,(想要在浏览器里打开看看是什么样子)内容以下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script type="text/javascript" src="world.js"></script>
</head>
<body>
    
</body>
</html>

 

结果在浏览器中打开,查看控制台大失所望,报了一个错误world.js:2 Uncaught ReferenceError: require is not defined
发现浏览器不兼容CommonJS的根本缘由,在于缺乏四个Node.js环境的变量:

  • module
  • exports
  • require
  • global

只要可以提供这四个变量,浏览器就能加载 CommonJS 模块,问题是能够解决的,可是好像并不怎么好玩,有兴趣的朋友能够去阮老师博客里逛逛啊,浏览器加载 CommonJS 模块的原理与实现,里面还讲了Browserify的原理。

这里使用命令行node world.js就能够了。

十7、Node.js中的模块和包

参考《Node.js开发指南 ByVoid》 Page34

模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具备必定规模的程序不可能只用一个文件,一般须要把各个功能拆分、封装,而后组合起来,模块正是为了实现这种方式而诞生的。在浏览器 JavaScript 中,脚本模块的拆分和组合一般使用 HTML 的script 标签来实现。Node.js 提供了 require 函数来调用其余模块,并且模块都是基于文件的,机制十分简单。

Node.js 的模块和包机制的实现参照了 CommonJS 的标准,但并未彻底遵循。不过二者的区别并不大,通常来讲你大可没必要担忧,只有当你试图制做一个除了支持 Node.js以外还要支持其余平台的模块或包的时候才须要仔细研究。一般,二者没有直接冲突的地方。咱们常常把 Node.js 的模块和包相提并论,由于模块和包是没有本质区别的,两个概念也时常混用。若是要辨析,那么能够把包理解成是实现了某个功能模块的集合,用于发布和维护。对使用者来讲,模块和包的区别是透明的,所以常常不做区分。本节中咱们会详细介绍:
1.什么是模块
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个Node.js 文件就是一个模块,这个文件多是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。在前面章节的例子中,咱们曾经用到了 var http = require('http'), 其中 http是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。咱们经过require 函数获取了这个模块,而后才能使用其中的对象。

2.建立模块
在 Node.js 中,建立一个模块很是简单,由于一个文件就是一个模块,咱们要关注的问题仅仅在于如何在其余文件中获取这个模块。Node.js 提供了 exports 和 require 两个对象,其中 exports 是模块公开的接口, require 用于从外部获取一个模块的接口,即所获取模块的 exports 对象。让咱们以一个例子来了解模块。建立一个 module.js 的文件,内容是:

//module.js
var name;
exports.setName = function(thyName) {
name = thyName;
};
exports.sayHello = function() {
console.log('Hello ' + name);
};

 

在同一目录下建立 getmodule.js,内容是:

//getmodule.js
var myModule = require('./module');
myModule.setName('BYVoid');
myModule.sayHello();

 

运行node getmodule.js,结果是:

Hello BYVoid

 

在以上示例中,module.js 经过 exports 对象把 setName 和 sayHello 做为模块的访问接口,在 getmodule.js 中经过 require('./module') 加载这个模块,而后就能够直接访问 module.js 中 exports 对象的成员函数了。这种接口封装方式比许多语言要简洁得多,同时也不失优雅,未引入违反语义的特性,符合传统的编程逻辑。在这个基础上,咱们能够构建大型的应用程序,npm 提供的上万个模块都是经过这种简单的方式搭建起来的。
这里有个疑问,能够参考exports 和 module.exports 的区别module.exports与exports??关于exports的总结

  • module.exports 初始值为一个空对象 {}
  • exports 是指向的 module.exports 的引用
  • require() 返回的是 module.exports 而不是 exports

咱们常常看到这样的写法:

exports = module.exports = somethings

 

上面的代码等价于:

module.exports = somethings
exports = module.exports

 

原理很简单,即 module.exports 指向新的对象时,exports 断开了与 module.exports 的引用,那么经过 exports = module.exports 让 exports 从新指向 module.exports 便可。

3.单次加载
上面这个例子有点相似于建立一个对象,但实际上和对象又有本质的区别,由于require 不会重复加载模块,也就是说不管调用多少次 require, 得到的模块都是同一个。咱们在 getmodule.js 的基础上稍做修改:

//loadmodule.js
var hello1 = require('./module');
hello1.setName('BYVoid');
var hello2 = require('./module');
hello2.setName('BYVoid 2');
hello1.sayHello();

运行后发现输出结果是 Hello BYVoid 2 ,这是由于变量 hello1 和 hello2 指向的是同一个实例,所以 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。

 

  1. 覆盖 exports

有时候咱们只是想把一个对象封装到模块中,例如:

//singleobject.js
function Hello() {
var name;
this.setName = function (thyName) {
name = thyName;
};
this.sayHello = function () {
console.log('Hello ' + name);
};
};
exports.Hello = Hello;

 

此时咱们在其余文件中须要经过 require('./singleobject').Hello 来获取Hello 对象,这略显冗余,能够用下面方法稍微简化:

//hello.js
function Hello() {
var name;
this.setName = function(thyName) {
name = thyName;
};
this.sayHello = function() {
console.log('Hello ' + name);
};
};
module.exports = Hello;

 

这样就能够直接得到这个对象了:

//gethello.js
var Hello = require('./hello');
hello = new Hello();
hello.setName('BYVoid');
hello.sayHello();

 

注意,模块接口的惟一变化是使用 module.exports = Hello 代替了 exports.Hello=Hello 。在外部引用该模块时,其接口对象就是要输出的 Hello 对象自己,而不是原先的exports 。事实上, exports 自己仅仅是一个普通的空对象,即 {} ,它专门用来声明接口,本质上是经过它为模块闭包的内部创建了一个有限的访问接口。由于它没有任何特殊的地方,因此能够用其余东西来代替,譬如咱们上面例子中的 Hello 对象。

注意,不能够经过对 exports 直接赋值代替对 module.exports 赋值。exports 实际上只是一个和 module.exports 指向同一个对象的变量,它自己会在模块执行结束后释放,但 module 不会,所以只能经过指定module.exports 来改变访问接口。

5.建立包
包是在模块基础上更深一步的抽象,Node.js 的包相似于 C/C++ 的函数库或者 Java/.Net的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合 CommonJS 规范的包应该具有如下特征:
 package.json 必须在包的顶层目录下;
 二进制文件应该在 bin 目录下;
 JavaScript 代码应该在 lib 目录下;
 文档应该在 doc 目录下;
 单元测试应该在 test 目录下。
Node.js 对包的要求并无这么严格,只要顶层目录下有 package.json,并符合一些规范便可。固然为了提升兼容性,咱们仍是建议你在制做包的时候,严格遵照 CommonJS 规范。

模块与文件是一一对应的。文件不只能够是 JavaScript 代码或二进制代码,还能够是一个文件夹。最简单的包,就是一个做为文件夹的模块。下面咱们来看一个例子,创建一个叫作 somepackage 的文件夹,在其中建立 index.js,内容以下:

//somepackage/index.js
exports.hello = function() {
console.log('Hello.');
};

 

而后在 somepackage 以外创建 getpackage.js,内容以下:

//getpackage.js
var somePackage = require('./somepackage');
somePackage.hello();

 

运行 node getpackage.js,控制台将输出结果 Hello. 。咱们使用这种方法能够把文件夹封装为一个模块,即所谓的包。包一般是一些模块的集合,在模块的基础上提供了更高层的抽象,至关于提供了一些固定接口的函数库。经过定制package.json,咱们能够建立更复杂、更完善、更符合规范的包用于发布。

在前面例子中的 somepackage 文件夹下,咱们建立一个叫作 package.json 的文件,内容以下所示:

{
"main" : "./lib/interface.js"
}

 

而后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以一样的方式再次调用这个包,依然能够正常使用。Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其做为包的接口模块,若是 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 做为包的接口。

package.json 是 CommonJS 规定的用来描述包的文件,彻底符合规范的 package.json 文件应该含有如下字段。
 name :包的名称,必须是惟一的,由小写英文字母、数字和下划线组成,不能包含空格。
 description :包的简要说明。
 version :符合语义化版本识别规范的版本字符串。
 keywords :关键字数组,一般用于搜索。
 maintainers :维护者数组,每一个元素要包含 name 、 email (可选)、 web (可选)字段。
 contributors :贡献者数组,格式与 maintainers 相同。包的做者应该是贡献者数组的第一个元素。
 bugs :提交bug的地址,能够是网址或者电子邮件地址。
 licenses :许可证数组,每一个元素要包含 type (许可证的名称)和 url (连接到许可证文本的地址)字段。
 repositories :仓库托管地址数组,每一个元素要包含 type (仓库的类型,如 git )、url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
 dependencies :包的依赖,一个关联数组,由包名称和版本号组成。
下面是一个彻底符合 CommonJS 规范的 package.json 示例:

{
    "name": "mypackage",
    "description": "Sample package for CommonJS. This package demonstrates the required
elements of a CommonJS package.",
    "version": "0.7.0",
    "keywords": ["package", "example"],
    "maintainers": [{
        "name": "Bill Smith",
        "email": "bills@example.com",
    }],
    "contributors": [{
        "name": "BYVoid",
        "web": "http://www.byvoid.com/"
    }],
    "bugs": {
        "mail": "dev@example.com",
        "web": "http://www.example.com/bugs"
    },
    "licenses": [{
        "type": "GPLv2",
        "url": "http://www.example.org/licenses/gpl.html"
    }],
    "repositories": [{
        "type": "git",
        "url": "http://github.com/BYVoid/mypackage.git"
    }],
    "dependencies": {
        "webkit": "1.2",
        "ssl": {
            "gnutls": ["1.0", "2.0"],
            "openssl": "0.9.8"
        }
    }
}
十6、Node.js模块加载机制

参考 《Node.js开发指南 ByVoid》Page 132
Node.js 的模块能够分为两大类,一类是核心模块,另外一类是文件模块。核心模块就是Node.js 标准 API 中提供的模块,如 fs 、 http 、 net 、 vm 等,这些都是由 Node.js 官方提供的模块,编译成了二进制代码。咱们能够直接经过 require 获取核心模块,例如require('fs') 。核心模块拥有最高的加载优先级,换言之若是有模块与其命名冲突,Node.js 老是会加载核心模块。文件模块则是存储为单独的文件(或文件夹)的模块,多是 JavaScript 代码、JSON 或编译好的 C/C++ 代码。文件模块的加载方法相对复杂,但十分灵活,尤为是和 npm 结合使用时。在不显式指定文件模块扩展名的时候,Node.js 会分别试图加上.js、.json 和 .node扩展名。.js 是 JavaScript 代码,.json 是 JSON 格式的文本,.node 是编译好的 C/C++ 代码。

文件模块的加载有两种方式,一种是按路径加载,一种是查找 node_modules 文件夹。若是 require 参数以“ / ”开头,那么就以绝对路径的方式查找模块名称,例如 require('/home/byvoid/module') 将会按照优先级依次尝试加载 /home/byvoid/module.js、/home/byvoid/module.json 和 /home/byvoid/module.node。若是 require 参数以“ ./ ”或“ ../ ”开头,那么则以相对路径的方式来查找模块,这种方式在应用中是最多见的。例如前面的例子中咱们用了 require('./hello') 来加载同一文件夹下的hello.js。

若是 require 参数不以“ / ”、“ ./ ”或“ ../ ”开头,而该模块又不是核心模块,那么就要经过查找 node_modules 加载模块了。咱们使用npm获取的包一般就是以这种方式加载的。在某个目录下执行命令 npm install express,你会发现出现了一个叫作node_modules的目录,里面的结构大概如图 6-1 所示。


 
图6-1 node_modules 目录结构

在 node_modules 目录的外面一层,咱们能够直接使用 require('express') 来代替require('./node_modules/express') 。这是Node.js模块加载的一个重要特性:经过查找 node_modules 目录来加载模块。当 require 遇到一个既不是核心模块,又不是以路径形式表示的模块名称时,会试图在当前目录下的 node_modules 目录中来查找是否是有这样一个模块。若是没有找到,则会在当前目录的上一层中的 node_modules 目录中继续查找,反复执行这一过程,直到遇到根目录为止。举个例子,咱们要在 /home/byvoid/develop/foo.js 中使用 require('bar.js') 命令,Node.js会依次查找:

  /home/byvoid/develop/node_modules/bar.js
  /home/byvoid/node_modules/bar.js
  /home/node_modules/bar.js
  /node_modules/bar.js

 

为何要这样作呢?由于一般一个工程内会有一些子目录,当子目录内的文件须要访问到工程共同依赖的模块时,就须要向父目录上溯了。好比说工程的目录结构以下:

|- project
|- app.js
|- models
|- ...
|- views
|- ...
|- controllers
|- index_controller.js
|- error_controller.js
|- ...
|- node_modules
|- express

 

咱们不只要在 project 目录下的 app.js 中使用 require('express') ,并且可能要在controllers 子目录下的index_controller.js 中也使用 require('express') ,这时就须要向父目录上溯一层才能找到 node_modules 中的 express 了。

做者:合肥懒皮 连接:https://www.jianshu.com/p/33d53cce8237 来源:简书 简书著做权归做者全部,任何形式的转载都请联系做者得到受权并注明出"web": "http://www.byvoid.com/"
相关文章
相关标签/搜索