模块化和组件化是当前JavaScript在编写稍大型工程项目时候解决单独文件过大问题和相似逻辑解耦的优秀的解决方案之一。在项目代码中,一般将代码合理拆分到不通的Js文件中,每一个文件就是一个模块,而文件路径就是模块名。


模块化代码在 nodejs 中有以下特性:


  1. Node.js是通过模块的形式进行组织与调用的,在编写每个模块时,都有requireexportsmodule 三个预先定义好的变量可供使用。
  1. 所以系统自带了很多模块
  1. 同时也提供了新模块的扩展机制


1.1 require


require函数用于在当前模块中加载和使用别的模块,传入一个模块名(路径),反回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头)。另外,模块名中的.js扩展名可以省略。以下是一个例子。


var foo1 = require('./foo');
var foo2 = require('./foo.js');
var foo3 = require('/home/user/foo');
var foo4 = require('/home/user/foo.js');
// foo1至foo4中保存的是同一个模块的导出对象。


另外,可以使用以下方式加载和使用一个JSON文件。


var data = require('./data.json');


1.2 exports


exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。


exports.hello = function () {
    console.log('Hello World!');
};


1.3 module


通过module对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。


module.exports = function () {
    console.log('Hello World!');
};



※ Node.js 官方实现require 的方式:


function require(...) {
  var module = { exports: {} };
  ((module, exports) => {
    // Your module code here. In this example, define a function.
    function some_func() {};
    exports = some_func;
    // At this point, exports is no longer a shortcut to module.exports, and
    // this module will still export an empty default object.
    module.exports = some_func;
    // At this point, the module will now export some_func, instead of the
    // default object.
  })(module, module.exports);
  return module.exports;
}


如果 a.js require 了 b.js, 那么在 b 中定义全局变量 t = 111 能否在 a 中直接打印出来?


① 每个 .js 能独立一个环境只是因为 node 帮你在外层包了一圈自执行, 所以你使用 t = 111 定义全局变量在其他地方当然能拿到. 情况如下:


// b.js
(function (exports, require, module, __filename, __dirname) {
  t = 111;
})();

// a.js
(function (exports, require, module, __filename, __dirname) {
  // ...
  console.log(t); // 111
})();


a.js 和 b.js 两个文件互相 require 是否会死循环? 双方是否能导出变量? 如何从设计上避免这种问题?


② 不会, 先执行的导出空对象, 通过导出工厂函数让对方从函数去拿比较好避免. 模块在导出的只是 var module = { exports: {} };中的 exports, 以从 a.js 启动为例, a.js 还没执行完 exports 就是 {} 在 b.js 的开头拿到的就是 {} 而已.


另外还有非常基础和常见的问题, 比如 module.exports 和 exports 的区别这里也能一并解决了 exports 只是 module.exports 的一个引用. 没看懂可以在细看我以前发的帖子.


再晋级一点, 众所周知, node 的模块机制是基于 CommonJS 规范的. 对于从前端转 node 的同学, 如果面试官想问的难一点会考验关于 CommonJS 的一些问题. 比如比较 AMD, CMD, CommonJS 三者的区别, 包括询问关于 node 中 require 的实现原理等.


1.4 module.exports 与 exports


  1. 默认exportsmodule.exports指向相同的对象的引用,并且此对象是一个空对象{}
  1. 对他们添加属性,不会破坏他们的一致性


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

// 修改exports
module.exports.a = 100;
console.log(module.exports === exports);  // true
console.log(exports);  // { a: 100 }

// 修改exports
exports.b = {
	a: 100
};
console.log(module.exports === exports); // true
console.log(exports);  // { a: 100, b: { a: 100 } }


  1. 对他们直接使用赋值号,则会破坏他们的引用关系


console.log(module.exports === exports); // true
module.exports = {c:100}; 
console.log(exports); // {}
console.log(module.exports); // {c:100}
console.log(module.exports === exports); // false

// 直接修改exports
console.log(module.exports === exports); // false
exports = {
	c:100
};
console.log(exports); // {c:100}
console.log(module.exports); // {}
console.log(module.exports === exports); // false


  1. 导出以module.exports为准


1.5 系统自带模块


可以通过 process.moduleLoadList 打印的 NativeModule 可以查看到相关的模块信息。主要系统包括:


在 V8.9.3中,主要的系统模块包括:

[ 'assert', 'buffer', 'console', 'dns', 'domain', 'events', 'fs', 'module', 'net', 'os', 'path', 'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'timers', 'tty', 'url', 'util', 'vm' ]


其中最能代表node.js的最初思想的是net, 'events'这两个模块。


1.5.1 exports 和 module.exports


先看一个例子:


// module circle.js
const { PI } = Math;

exports.area = (r) => PI * r ** 2;

exports.circumference = (r) => 2 * PI * r;



第二个例子:


// square.js
module.exports = class Square {
  constructor(width) {
    this.width = width;
  }

  area() {
    return this.width ** 2;
  }
};
// use square module
const Square  = require('./square.js');
const mySquare = new Square(2);
console.log(`mySquare 的面积是 ${mySquare.area()}`);



1.5.2 主模块



1.5.3 包管理器的技巧

http://nodejs.cn/api/modules.html#modules_addenda_package_manager_tips


1.5.4 缓存



1.5.5 模块缓存的注意事项:



1.5.6 核心模块



1.5.7 循环

在模块之间的互相加载时,当 main.js 加载 a.js 时,a.js 又加载 b.js。 此时,b.js 会尝试去加载 a.js, 这样就造成了无限循环。

为了防止无限的循环,Nodejs 提供了一个解决策略。


会返 a.jsexports 对象的未完成的副本给 b.js 模块。 然后 b.js 完成加载,并将 exports 对象提供给 a.js 模块。


DEMO:


//a.js:
console.log('a 开始');
exports.done = false;
const b = require('./b.js');
console.log('在 a 中,b.done = %j', b.done);
exports.done = true;
console.log('a 结束');

//b.js:
console.log('b 开始');
exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %j', a.done);
exports.done = true;
console.log('b 结束');

//main.js:
console.log('main 开始');
const a = require('./a.js');
const b = require('./b.js');
console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);


分析过程:



$ node main.js
main 开始
a 开始
b 开始
在 b 中,a.done = false
b 结束
在 a 中,b.done = true
a 结束
在 main 中,a.done=true,b.done=true



1.5.8 文件模块(系统自带的模块)



1.5.9 目录作为模块

如果把程序和依赖库放在统一个文件夹下,提供一个单一的入口指向它。把目录传给 require() 作为一个参数,即为 目录作为模块 引用。



{ 
  "name" : "some-library",
  "main" : "./lib/some-library.js" 
}


如果这是在 ./some-library 目录中,则 require('./some-library') 会试图加载 ./some-library/lib/some-library.js



1.5.10 node_modules 目录加载

传入require()的路径不是一个核心模块,Nodejs 从父目录开始,尝试从父目录的node_modules中加载模块。

如果在'/home/ry/projects/foo.js' 文件里调用了 require('bar.js'),则 Node.js 会按以下顺序查找:



通过在模块名后包含一个路径后缀,可以请求特定的文件或分布式的子模块。 例如,require('example-module/path/to/file') 会把 path/to/file 解析成相对于 example-module 的位置。 后缀路径同样遵循模块的解析语法。


1.5.11 从全局目录加载


如果 NODE_PATH 环境变量被设为一个以冒号分割的绝对路径列表,则当在其他地方找不到模块时 Node.js 会搜索这些路径。


注意:在 Windows 系统中,NODE_PATH 是以分号间隔的。


在当前的模块解析算法运行之前,NODE_PATH 最初是创建来支持从不同路径加载模块的。


虽然 NODE_PATH 仍然被支持,但现在不太需要,因为 Node.js 生态系统已制定了一套存放依赖模块的约定。 有时当人们没意识到 NODE_PATH 必须被设置时,依赖 NODE_PATH 的部署会出现意料之外的行为。 有时一个模块的依赖会改变,导致在搜索 NODE_PATH 时加载了不同的版本(甚至不同的模块)。


此外,Node.js 还会搜索以下位置:


  1. $HOME/.node_modules
  1. $HOME/.node_libraries
  1. $PREFIX/lib/node


其中 $HOME 是用户的主目录,$PREFIX 是 Node.js 里配置的 node_prefix


这些主要是历史原因。


注意:强烈建议将所有的依赖放在本地的 node_modules 目录。 这样将会更快地加载,且更可靠。


1.5.12 [Module Scope] __dirname



DEMO: 运行 /Users/demo/example.js


console.log(__dirname);
// Prints: /Users/demo
console.log(path.dirname(__filename));
// Prints: /Users/demo


1.5.12 [Module Scope] __filename



DEMO: 运行/Users/demo/example.js


console.log(__filename);
// Prints: /Users/demo/example.js
console.log(__dirname);
// Prints: /Users/demo


给定两个模块: a 和 b, 其中 b 是 a 的一个依赖。


文件目录结构如下:



使用__filename===>



1.5.13 [Module Scope] exports / module



1.5.14 require()


用于引入模块



DEMO:


// modules
> require.resolve.paths('aaa')
[ '/Users/zhengao/repl/node_modules',
  '/Users/zhengao/node_modules',
  '/Users/node_modules',
  '/node_modules',
  '/Users/zhengao/.node_modules',
  '/Users/zhengao/.node_libraries',
  '/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node',
  '/Users/zhengao/.node_modules',
  '/Users/zhengao/.node_libraries',
  '/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node' ]
 // 核心模块
 > require.resolve.paths('http')
null


1.5.15 module对象



> module
Module {
  id: '<repl>',
  exports: {},
  parent: undefined,
  filename: null,
  loaded: false,
  children: [],
  paths:  [ 
	 '/Users/zhengao/repl/node_modules',
     '/Users/zhengao/node_modules',
     '/Users/node_modules',
     '/node_modules',
     '/Users/zhengao/.node_modules',
     '/Users/zhengao/.node_libraries',
     '/Users/zhengao/.nvm/versions/node/v8.9.3/lib/node' 
  ]
}



DEMO:


// 许多人希望他们的模块成为某个类的实例, 需要将期望导出的对象赋值给 module.exports
//a.js
const EventEmitter = require('events');
module.exports = new EventEmitter();
// 处理一些工作,并在一段时间后从模块自身触发 'ready' 事件。
setTimeout(() => {
  module.exports.emit('ready');
}, 1000);
// 然后,在另一个文件中可以这么做:
// b.js
const a = require('./a.js');
a.on('ready', () => {
  console.log('模块 a 已准备好');
});


DEMO:


// 注意,对 module.exports 的赋值必须立即完成。 不能在任何回调中完成。否则无效
// x.js:
setTimeout(() => {
  module.exports = { a: 'hello' };
}, 0);

// y.js:
const x = require('./x');
console.log(x.a);



module.exports.hello = true; // 从对模块的引用中导出
exports = { hello: false };  // 不导出,只在模块内有效

//当 module.exports 属性被一个新的对象完全替代时,也会重新赋值 exports,例如:

module.exports = exports = function Constructor() {
  // ... 及其他
};

//为了解释这个行为,想象对 require() 的假设实现,它跟 require() 的实际实现相当类似:
function require(/* ... */) {
  const module = { exports: {} };
  ((module, exports) => {
    // 模块代码在这。在这个例子中,定义了一个函数。
    function someFunc() {}
    exports = someFunc;
    // 此时,exports 不再是一个 module.exports 的快捷方式,
    // 且这个模块依然导出一个空的默认对象。
    module.exports = someFunc;
    // 此时,该模块导出 someFunc,而不是默认对象。
  })(module, module.exports);
  return module.exports;
}`



1.6 Q&A


Q1: 比较AMD,CMD和 CommonJS 三者区别

A:


背景: 网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等......开发者不得不使用软件工程的方法,管理网页的业务逻辑。因为有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块。



CommonJS规范是诞生比较早的。NodeJS就采用了CommonJS。CommonJS 是一种同步的模块化规范,是这样加载模块:


var clock = require('clock');
clock.start();


这种写法适合服务端,因为在服务器读取模块都是在本地磁盘,加载速度很快。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。


因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。


这就是AMD规范诞生的背景。比如上面的例子中clock的调用必须等待clock.js请求成功,加载完毕。



AMD,即 (Asynchronous Module Definition),这种规范是异步的加载模块,requireJs应用了这一规范。先定义所有依赖,然后在加载完成后的回调函数中执行:


require([module], callback);


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


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


math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。


目前,主要有两个Javascript库实现了AMD规范:require.jscurl.js


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的一点,牺牲性能来带来开发的便利性,实际上解析模块用的时间短到可以忽略。





Q2:Node.js 中 require()的实现


A:





Q3: 什么时候使用 exports ,什么时候使用 module.exports


A:用一句话来说明就是,require方能看到的只有module.exports这个对象,它是看不到exports对象的,而我们在编写模块时用到的exports对象实际上只是对module.exports的引用。


var module = {
    exports:{
        name:"我是module的exports属性"
    }
};
//exports是对module.exports的引用,也就是exports现在指向的内存地址和module.exports指向的内存地址是一样的
var exports = module.exports;  

console.log(module.exports);    //  { name: '我是module的exports属性' }
console.log(exports);   //  { name: '我是module的exports属性' }

exports.name = "我想改一下名字";

console.log(module.exports);    //  { name: '我想改一下名字' }
console.log(exports);   //  { name: '我想改一下名字' }
//看到没,引用的结果就是a和b都操作同一内存地址下的数据


//这个时候我在某个文件定义了一个想导出的模块
var Circle = {
    name:"我是一个圆",
    func:function(x){
        return x*x*3.14;
    }
};

exports = Circle;  // 看清楚了,Circle这个Object在内存中指向了新的地址,所以exports也指向了这个新的地址,和原来的地址没有半毛钱关系了

console.log(module.exports);    //  { name: '我想改一下名字' }
console.log(exports);   // { name: '我是一个圆', func: [Function] }



1.7 模块化的基本要求



1.8 模块化的代码规范


在 Node.js 中使用 CommonJS 使用模块规范