~ Node.js 模块作用域

和函数作用域类似,在自定义模块中定义的变量、方法等成员,只能在当前模块内被访问,

这种模块级别的访问限制,叫做模块作用域。

// 模块作用域的好处:防止了全局变量污染的问题。


在 Node.js 中,你不可能在最外层定义变量,因为所有的用户代码都属于当前模块,而模块本身就不是最外层上下文。


以下变量,也可以直接使用,虽然看起来像全局变量,但其实并不是,它们的作用域,仅存在于模块范围内:


[1] __dirname    当前模块的目录名称

console.log(__dirname);  // /Users/admin/webpiece/Project/test/test
 
// 等同于
let path = require('path');
console.log(path.dirname(__filename));

[2] __filename    当前模块的文件名( 当前模块文件的绝对路径 )

console.log(__filename);  // /Users/admin/webpiece/Project/test/test/app.js

模块化规范 - CommonJS

模块成员的引入与导出Node.js 中,模块的加载机制被称为 CommonJS 规范。

    前端标准的模块化规范:1. AMD - requirejs      2. CMD - seajs
    服务器端的模块化规范:CommonJS - Node.js

CommonJS 规定了模块的特性和各模块之间如何相互依赖

① 每个模块内部,module 变量代表当前模块。
② module 变量是一个对象,它的 exports 属性( 即 module.exports )是对外的接口。
③ 加载某个模块,其实是加载该模块的 module.exports 属性,require() 方法用于加载模块。

实际上,Node.js 并非完全遵循 CommonJS 规范,而是对模块化规范进行了一定的取舍,也增加了少许自身需要的特性。


CommonJS 对模块的定义十分简单,主要分为 3 个部分:模块引用、模块定义和模块标识

 

 

 

 

 

 

 

 

 

在程序开发过程中,随着代码越写越多,单个文件中的代码会越来越长,维护就变得越来越复杂。

为了编写可维护的代码,我们把很多函数分组,分别放在不同的文件中,

这样,每个文件包含的代码就相对较少,很多编程语言都采用这种代码组织方式。

在 Node.js 中,一个 .js 文件就称之为一个模块( module )。使用模块化有什么好处呢?

  • 最大的好处是大大提高了代码的可维护性。                                   // 命名冲突、文件依赖
  • 其次,编写代码不必从零开始,当一个模块编写完毕,就可以在其他地方被引用( 我们在编写程序的时候,也经常引用其他模块,包括 Node 中内置的核心模块和来自第三方的自定义模块 )。
  • 使用模块,还可以避免命名冲突,相同名字的函数和变量可以分别存在不同的模块中,简单来说就是,不同模块内部各自使用的变量名和函数名互不冲突。

// 总结如下:
    传统的非模块化开发的缺点:命名冲突、文件依赖

    前端标准的模块化规范:1. AMD - requirejs      2. CMD - seajs
    服务器端的模块化规范:CommonJS - Node.js

    模块化的相关规则:
    1. 如何定义模块:一个js文件就是一个模块,模块内部的成员是相互独立的;
    2. 模块成员的引入和导出

模块成员的引入和导出

Node 中,模块分为两类:一类是 Node 提供的模块,称为核心模块;另一类是用户编写的模块,称为文件模块。

  • 核心模块,在 Node.js 源代码的编译过程中,编译进了二进制执行文件。

在 Node.js 进程启动时,部分核心模块就被直接加载进内存中,所以,这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以,它的加载速度是最快的;

  • 文件模块,则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

[引入模块 — require(id) 方法 ]                           // 模块的加载(文件扩展名,了解)?!

在 JS 中,加载模块使用 script 标签即可;在 nodejs 中,如何在一个模块中,加载另一个模块呢?

// 使用 require 方法进行引入,文件的扩展名(通常为.js .json .node)可省略
    require('./math');    // require 方法,通过路径分析进行模块的路径查找,从而找到这个'包'所在位置

[ 导出模块 ] 如何在一个模块中访问另外一个模块中定义的变量呢?


方式 1 - 全局对象?!最容易想到的是:把一个模块中定义的变量复制到 global 中,然后,另一个模块访问全局环境。


//a.js
var a = 100;
global.a = a;

//b.js
require('./a');
console.log(global.a);//100

这种方法虽然简单,但由于会污染全局环境,不推荐使用。


方式 2 - module 对象:常用的方法是使用 nodejs 提供的模块对象 module,该对象保存了当前模块相关的一些信息。

  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块。
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 用于指定一个模块所导出的内容,即可以通过 require() 访问的内容。

function Module(id, parent) {
    this.id = id;
    this.exports = {};
    this.parent = parent;
    if (parent && parent.children) {
        parent.children.push(this);
    }
    this.filename = null;
    this.loaded = false;
    this.children = [];
}

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

//a.js
var a = 100;
module.exports.a = a;

//b.js
var result = require('./a');
console.log(result);//'{ a: 100 }'

[ exports 变量 ] 为了方便,Node 为每个模块提供一个 exports 变量,指向 module.exports。造成的结果是,在对外输出模块接口时,可以向 exports 对象添加方法。

console.log(module.exports === exports);//true

// 不能直接将 exports 变量指向一个值,因为这样等于切断了exportsmodule.exports的联系。


[ 编译模块(了解即可)编译和执行是模块实现的最后一个阶段,定位到具体的文件后,Node 会新建一个模块对象,然后根据路径载入并编译。不同的文件扩展名,其载入方法也有所不同:

  • js 文件 — 通过 fs 模块同步读取文件后编译执行;
  • node 文件 — 这是用 C/C++ 编写的扩展文件,通过 dlopen() 方法加载最后编译生成的文件;
  • json 文件 — 通过 fs 模块同步读取文件后,用 JSON.parse() 解析返回结果;
  • 其余扩展名文件 — 它们都被当做 .js 文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在 Module._cache 对象上,以提高二次引入的性能 ...


Node.js 模块化规范 — CommonJS

Node 中,模块的加载机制被称为 CommonJS 规范。实际上,Node 在实现中并非完全遵循 CommonJS 规范,而是对模块规范进行了一定的取舍,同时也增加了少许自身需要的特性。

CommonJS 规范的提出,主要是为了弥补当前 JS 没有标准的缺陷,使其具备开发大型应用的基础能力,而不是停留在小脚本程序的阶段。CommonJS 对模块的定义十分简单,主要分为 3 个部分:模块引用、模块定义和模块标识。


[ 方式一:exports ]  

  • 模块引用:在规范中,存在require()方法,这个方法接受模块标识,以此引入一个模块的API到当前上下文中。
var math = require('math');
  • 模块定义:在模块中,上下文提供 require() 方法来引入外部模块。
    • 对应引入功能,上下文提供了exports对象用于导出当前模块的方法或变量,并且它是唯一导出的出口。
    • 在模块中,还存在一个 module 对象,它代表模块自身,而 exports 是 module 的属性。

在 Node 中,一个文件就是一个模块,将方法挂载在 exports 对象上作为属性即可定义导出的方式。

// math.js
exports.add = function () {
    var sum = 0, i = 0,args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
};
// 在另一个文件中,我们通过require()方法引入模块后,就能调用定义的属性或方法了

// program.js
var math = require('./math');
exports.increment = function (val) {
    return math.add(val, 1);
};
  • 模块标识:模块标识其实就是传递给require()方法的参数,它必须是符合小驼峰命名的字符串,或者以.、..开头的相对路径,或者绝对路径。它可以没有文件名后缀.js。

[ 方式二:module.exports]  表示模块对外输出的值,推荐(可以应对任何情况)。

'use strict';

var s = 'Hello';
function greet(name) {
    console.log(s + ', ' + name + '!');
}
module.exports = greet;
'use strict';

// 引入hello模块:
var greet = require('./hello');
var s = 'webpiece';
greet(s); // Hello, webpiece!
  • 要在模块中对外输出(暴露)变量,用:
module.exports = variable;     // 输出的变量可以是任意对象、函数、数组等等。
  • 一个模块要引入其他模块输出的对象,用:
var foo = require('other_module');      // 引入的对象具体是什么,取决于引入模块输出的对象