模块化的好处: 为了更有效的组织代码,提高重用性,增大开发效率,我们会把项目拆分成不同模块,每个模块,职责单一,多人协同,高效运行,易于维护。
模块的发展历程
- js 不像其他高级语言有模块系统,标准库较少和更缺乏包管理系统。
- js 起初只有全局对象的形式,通过一个个小函数来实现不同的模块功能
- 渐渐发展,通过构建对象的形式,来武装不同的功能
- 继续发展,通过立即执行函数和闭包的形式来分离一个又一个的小组件
- 当对象多起来的时候,又开始通过命名空间,来实现分级管理
- 最终,历经十多年,社区渐渐发展壮大,commonJS规范的提出成了javascript 历史上最重要的里程碑。
- Best Wishes : hope javascript can run everywhere!
CommonJS规范
CommonJS只是一个规范,不是文件或者框架,这点一定要清楚。它的官方网址:http://www.commonjs.org/ ,CommonJS中大部分只是草案或者说是理论,在近几年的发展中已有显著成效,这些规范包括:
- 模块
- 二进制
- I/O流
- 进程环境
- 文件系统
- 套接字
- 单元测试
- web服务器网关
- 包管理等等涵盖了方方面面。
在后端nodejs显然已经是CommonJS规范的最佳实践。
nodejs中的require的内部机制
我们知道,在nodejs 中每个文件就是一个独立的模块。在模块之间不会造成变量污染和命名冲突等问题。通过exports 或者 module.exports 输出功能或者说提供API,我们通过require 来引入一个模块,很像前端通过script标签来引入js文件。但是它的内部细节又如何呢,在朴灵的那本《深入浅出》中有关javascript的模块编译那块已经做了一些讲解,那么我们现在去繁留简来深入体验一把:
require 函数的源码可以这样理解,当然我们简化了不少东西,现在我们只把它抽离出来 :
// 模拟require的实现 function _require(path) { // 定义一个Module对象 var Module = function() { this.exports = {}; } // 引入nodejs 文件模块 下面是nodejs中原生的require方法 var fs = require('fs'); // 同步读取该文件 var sourceCode = fs.readFileSync(path, 'utf8'); // 头尾拼接包装成新的字符串 var packSourceCode = '(function(module,exports){ ' + sourceCode + ' return module.exports; })'; // 字符串转换成函数 var packFunc = eval(packSourceCode); // 实例化一个Module 里面有一个exports属性 var module = new Module(); // 把module 和 它内部的module.exports都作为参数传进去 // 并得到挂在到module.exports 或 exports上的功能 var res = packFunc(module, module.exports); // 最终我们拿到了path代表的文件模块提供的API return res; }
说明: 上面举了一个小栗子来模拟nodejs中的require的实现,当然源码不会这样写,源码涉及的东西会更复杂些。上面只是一些帮助理解的小demo。
我们自己写的逻辑功能全在path所代表的文件模块中,在内部把所有功能挂在到module.exports 或者exports对象上,最终return 他们得以暴露。
我们可以看出 module.exports 和 exports 指向的是同一对象
- 在require一个模块时大致经历了这几步:
- 路径分析
- 文件定位
- 编译执行
一个小的 require 模块引入 案例
在 util.js文件模块中,我们这样写
// 定义一个计算器对象 var Calculator = function() {}; Calculator.prototype = { add: function(x, y) { return x + y; }, subtract: function(x, y) { return x - y; }, multiply: function(x, y) { return x * y; }, divide: function(x, y) { return x / y; } } // 虽然exports 和 module.exports 指向同一个对象 // 但是此处不能使用 exports = new Calculator(); 具体原因,下一篇博客分析 module.exports = new Calculator(); // 暴露API对象
在 index.js 文件中,我们这样写:
var cal = require('./util'); var res = cal.add(1, 2); console.log(res); // 3
在 CLI 中,我们执行index模块,将会输出3
$ node index 3
nodejs中的模块分类和加载顺序
nodejs 中核心模块和文件模块的引用方式:
- 核心模块: require(‘核心模块名’);
- 文件模块: require(‘路径+文件名’); 路径可以用
./
代表的相对路径或者绝对路径,其中文件名可以省略后缀名。 - 自定义模块:特殊的文件模块,可能是一个文件或者包的形式
模块加载顺序 :
- 优先从缓存加载
- 核心模块:如http、fs、path等 (优先查找核心模块)
- 文件模块: ./或../开始的相对路径文件模块,以/开始的绝对路径文件
- 自定义模块:查找最费时
目录和包查找原则
比如有如下的模块路径: 查找规则是沿路径向上逐级递归,直到根目录的node_modules目录:
├─/node_modules/ └─/home/node_modules/ └─/user/test/node_modules/
这就是自定义模块加载速度最慢的原因了。
当我们require 的标识符 不包含扩展名node 会按照 .js .json .node 的次序补足扩展名 ,依次尝试。
如果在require过程中,没有查找到对应文件,却得到一个目录,此时 node 会将当前目录当作一个包来处理。
- 此时,node 会查找目录下的package.json文件,通过JSON.parse() 解析包描述对象,从中拿到main属性指定的文件名进行定位
- 如果main属性指定错误,或者没有package.json文件,那么node会将index作为默认的文件名,去依次查找index.js , index.json , index.node
- 在目录分析中没有定位到任何模块,那么它会遍历自己的上一级目录进行查找,如果还没找到,抛出查找失败的异常。