node的模块机制

1,433

CommonJS承担了拯救者的角色

在ES2015规范之前,JavaScript这门语言本身没有提供组织代码的方式, Node.js用CommonJS模块规范填补了这个空白,在这篇文章里面, 我们将会讨论node.js的模块机制以及如何在项目中组织模块

模块是什么

模块是代码结构的基本构建单元,模块允许你组织你自己的代码,隐藏私有数据,通过module.exports暴露公共接口,每当你调用require的时候,你就在加载一个模块

这是一个很简单的使用CommonJS的例子

// multiply.js
function multiply(a, b) {
    return a * b
}
module.exports = multiply

这个multiply.js文件就是一个模块,要想使用它,我们只需要require它即可

    const multiply = require('multiply')
    console.log(multiply(2, 3)) // 6

在背后,multiply.js被node.js用下面的方式包装了

(function (exports, require, module, __filename, __dirname) {
 function multiply(a, b) {
    return a * b
}

  module.exports = add
})

这就是为什么你可以在你的代码中获取到像require和module这些全局变量,模块机制也保证了变量的作用域限制在本地而不会暴露到全局

Node的模块实现

在Node中引入模块,需要经历如下3个步骤。
路径分析
文件定位
编译执行

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

核心模块在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最 快的。
文件模块则是在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。

node的模块加载机制是在第一次调用requrie之后缓存模块,之后这个模块的调用都是从缓存中读取,也就是每当你使用`require('a-module'), 你将会得到a-module的同一个实例,这保证了模块是单例的,同一个模块不管require多少次,在应用中都保持一个状态

你可以加载原生模块,路径引用来自你的文件系统或者已经安装的模块,如果传给require函数的标识符不是一个原生的模块也不是一个文件的引用('./../'),node.js将会查找已经安装的模块,他会遍历你的文件目录里面的node_modules文件夹,来查找引用的模块

处理模块加载的是node的核心模块是module.js,可以在node仓库的lib/module.js找到它,其中最重要的是函数_load_complie

Module._load

这个函数检查当前的模块是不是存在缓存中,如果在缓存中,则返回导出的对象

如果模块是原生的,会传入filename做参数,调用NativeModule.require()方法,返回结果

否则,这个函数为文件创建一个新模块并且将其保存在缓存中,返回导出对象,流程如下

Module._compile

编译函数在隔离的作用域或者沙箱里面运行文件,对这个文件暴露了require,module, exports这些帮助变量

如何在项目中组织模块

在应用中,我们在创建模块的时候需要处理好内聚和耦合的平衡,最理想的场景就是实现高内聚低耦合

一个模块要想实现高内聚就需要专注一个功能,低耦合意味着模块不应该有全局或共享的状态,他们应该仅通过传递参数来通讯,这样即使模块被替换也不需要改动太多的代码库

建议像下面这样单独暴露命名的函数和常量

const COUNT_NUMBER = 0

function count () { /* ... */ }

module.exports = {
  COUNT_NUMBER,
  count
}

而不是暴露一个含有各种属性的对象

const COUNT_NUMBER = 0

function count () { /* ... */ }
let obj = {COUNT_NUMBER, count}
module.exports = obj

如何处理模块

有两种主要的方式写Node模块

一种是硬编码依赖,通过调用require,显式地加载一个模块到另外一个模块,这种是最常见的使用方法,这种方式我们用node来管理模块的生命周期,直接易懂,而且也方便调试

第二种是依赖注入的方式

这种方式在node的环境中很少使用,但它是一个很有用的概念,依赖注入模式可以降低模块间的解耦,这种模式不是显式为模块定义依赖,而是从外面接收,因此很容易用一个有同样接口的模块来替代

我们用工厂模式创建一个依赖注入的模块,来说明下这种方式

class Car {
    constructor (options) {
    this.engine = options.engine
  }

  start () {
    this.engine.start()
  }
}
function create (options) {
  return new Car(options)
}
module.exports = create

当我们使用create这个方法的时候,只要传递的options有engine属性,并且engine属性实现了start方法就可以了,换句话说,不管你是啥车,自行车也可以,只要有引擎,并且引擎有启动的方法,我就可以生产一台车,哪怕有一天你的车升级了,可以在水上开,空中飞也没关系,这个create模块我依然不需要修改

往期推荐

nodejs进阶系列

1 npm的使用和最佳实践
2 手把手教你用npm发布包

vue系列

和尤雨溪一起进阶vue
vue项目实现缓存的最佳方案