前端模块化(三):CommonJS规范

1 概述

CommonJS对模块的定义十分简单,主要分为模块定义、模块引用和模块标识。Nodejs的模块系统就遵循了CommonJS规范。但Node在实现中并非完全按照CommonJS规范实现,而是对模块规范进行了一定的取舍。下面,我们结合Node来深入了解CommonJS规范。

2 模块定义

CommonJS规范规定,一个文件就是一个模块,用module变量代表当前模块。 Node在其内部提供一个Module的构建函数。所有模块都是Module的实例。实例代码如下:

function Module(id, parent) {

  this.id = id;

  this.exports = {};

  this.parent = parent;

  this.filename = null;

  this.loaded = false;

  this.children = [];

}

module.exports = Module;

var module = new Module(filename, parent);

每个模块内部,都有一个module对象,代表当前模块。它的属性如下:

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 初始值为一个空对象{},表示模块对外输出的接口。

2.1 module.exports属性

module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。

例如,我们在moduleA.js文件中定义funA方法,并用module.exports变量把该方法暴露出,实例代码如下:

//moduleA.js

module.exports.funcA= function(){

  console.log('This is moduleA!');

} 

然后,在moduleB模块中加载引入moduleA模块,便可以使用funA方法了,示例代码如下:

//moduleB.js

var a = require('./moduleA');

a.funcA();//打印'This is moduleA!'

2.2 exports变量

为了方便,Node为每个模块提供一个exports变量,指向module.exports。在模块内部大概是这样的:

var exports = module.exports={};

在对外输出模块接口时,可以向exports对象添加方法。

//a.js

var funA=function(){

console.log('This is module a!');

};

exports.funA=funA;//等同于module.exports.funA=funA;

exports 赋值其实是给 module.exports 这个空对象添加myName属性而已,为什么是给exports添加属性,而不直接exports= funA呢?

因为, exports 是指向的 module.exports 的引用。如果直接是给exports赋值而不是添加属性的话,exports 就不再指向module.exports 了。当exports 被改变的时候,module.exports将不会被改变,而模块导出的时候,真正导出的执行是module.exports,而不是exports。例如,将a.js改为:

//a.js

var funA=function(){

console.log('This is module a!');

};

exports =funA;

这样是无效的。因为,前面是通过给 exports 添加属性,而现在对 exports 指向的内存做了修改,exports 和 module.exports 不再指向同一块内存,即 module.exports 指向的那块内存并没有做任何改变,仍然为一个空对象 {},所以funA方法输出无效。

如果觉得module.exports和exports难以分清的话,个人建议可以全部使用module.exports来应对所有的情况,并尽量减少犯错的机会。

 

3 模块引用

require函数的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。当我们用require()获取module时,Node会根据module.id找到对应的module,并返回module. exports,这样就实现了模块的输出。

require函数使用一个参数,参数值可以带有完整路径的模块的文件名,也可以为模块名。

假如,有三个文件:一个是a.js(存放路径:home/a.js),一个是b.js(存放路径:home/user/b.js), 一个是c.js(存放路径:home/user/c.js)。我们在a.js文件中引用三个模块,实例代码如下:

var httpModule=require('HTTP');//用 “模块名”加载服务模块http

var b=require('./user/b');//用“相对路径”加载文件b.js

var b=require('../ home/user/c');//用“绝对路径”加载文件c.js

 

4 模块标识

模块标识就是传递给require方法的参数,必须符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径,默认文件名后缀.js。在Node实现中,正是基于这样一个标识符进行模块查找的,如果没有发现指定模块会报错。

根据参数的不同格式,require命令去不同路径寻找模块文件。加载规则如下:

(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,require('/home/marco/foo.js')将加载/home/marco/foo.js。

(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,require('./circle')将加载当前脚本同一目录的circle.js。

(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。

举例来说,脚本/home/user/projects/foo.js执行了require('bar.js')命令,Node会依次搜索以下文件。

/usr/local/lib/node/bar.js

/home/user/projects/node_modules/bar.js

/home/user/node_modules/bar.js

/home/node_modules/bar.js

/node_modules/bar.js

这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。

(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如require('example-module/path/to/file'),则将先找到example-module的位置,然后再以它为参数,找到后续路径。

(5)如果指定的模块文件没有发现,Node会尝试为文件名添加.js、.json、.node后,再去搜索。.js件会以文本格式的JavaScript脚本文件解析,.json文件会以JSON格式的文本文件解析,.node文件会以编译后的二进制文件解析。

(6)如果想得到require命令加载的确切文件名,使用require.resolve()方法。

CommonJS是同步的,意味着你想调用模块里的方法,必须先用require加载模块。这对服务器端的Nodejs来说不是问题,因为模块的JS文件都在本地硬盘上,CPU的读取时间非常快,同步不是问题。但如果是浏览器环境,要从服务器加载模块。模块的加载将取决于网速,如果采用同步,网络情绪不稳定时,页面可能卡住,这就必须采用异步模式。所以,就有了 AMD解决方案。下一篇我们开始介绍模块化规范的AMD规范;

 

参考链接

[1] https://en.wikipedia.org/wiki/CommonJS

[2] https://nodejs.org/api/modules.html#modules_modules

[3] http://javascript.ruanyifeng.com/nodejs/module.html#toc2

 

 

 

原文地址:https://www.cnblogs.com/huiguo/p/7967241.html