手撸手, 探究 Node.js 中的模块机制

更多个人博客:(https://github.com/zenglinan/blog)

如果对你有帮助,欢迎star。

在 JavaScript 诞生之际, 人们将 JavaScript 当做网页的小脚本语言, JavaScript 里缺乏模块化的概念。 后来, CommonJS 规范给 JavaScript 提供了一系列参照, 包括了模块化, 二进制, Buffer, I/O流等。

Node.js 正是参照了 CommonJS, 实现了模块化。

在 Node.js 中, 每个 js 文件都看做一个模块, 每个模块上有两个核心模块对象

  • module
  • require

接下来, 分别说说他们是怎么回事

一. module 对象

我们先打印一下看看 module 对象里有什么

2

我们来一个个掰扯一下这些属性是什么意思

1. id

每个模块都有一个唯一确定的 id, 用于区分模块, id 一般为文件的绝对路径。也就是说, 模块和硬盘上的文件是相对应的。

什么? 你说显示的是 '.' , 不是绝对路径?

因为假如打印的模块正是 node xxx 对应的模块(文件), 就会用 '.' 代替。

2. exports

exports 是一个十分重要的属性, 他上面挂载了模块导出的内容, 我们可以通过如下的方式导出一个变量或方法

exports.a = 1
exports.fn = function(){}

或许之前你都是这样导出的:

// 这样
module.exports = function(){}
// 或是这样
module.exports = {
  get: function(){},
  a: 1
}

其实都可以的 ~ 因为 exports 实际上和 module.exports 指向的是同一个对象, 可以打印一下:

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

正因为如此, 我们不能直接给 exports 赋值

exports = function(){}

这会导致 exports 丢失对 module.exports 的引用

另外, 当我们 require 的时候, 实际上引入的就是模块的exports

// child
exports.a = 1

// main
console.log(require('./child'))  // {a: 1}

3. parent && children

我们在 main 模块里引入 child 模块, 并打印 module

const child = require('./child')
console.log('in main', module)

同时在被引入的 child 模块中也打印一下 module

module.exports = {
  a:1
}
console.log('in child', module)

这里可以看到, main 模块调用了 child, childmain 为父子模块关系.

3
4

这里我们可以发现: 为什么父模块的子模块的parent会显示为[Circular]呢? 父亲的儿子的父亲不应该就是父亲吗? 为什么显示 [Circular]

哈哈, 是不是有点绕口, 事实上, 这里不显示正是因为这是个循环引用的关系, 假如要显示父模块, 那父模块里面还有children呢, children 里面还可以显示 parent , 这就绕不出来了.

4. filename

顾名思义, 模块的文件名, 只不过是以绝对路径显示的 O(∩_∩)O

5. loaded

用于显示模块是否已经完成加载

6. paths

module.path 里保存了从当前目录, 一直找到根目录下, 所有的 node_modules 文件夹的路径

这个 path 属性的作用是:

当导入自定义模块时, 如果不指定路径, 用 require(xxx) 这种方式引入的话, Node 会去遍历当前执行文件的 module.paths 来查找这个模块.

如果找不到, 就会抛出错误.

Cannot find module 'xxx'

注意: module.paths 是一个数组, 查找模块的时候会从第一个路径找起, 也就是说:

假如多个路径下都有该模块, node 会优先使用 paths靠前的路径下的模块

接下来, 再来说说 reqire 对象

二. require

老规矩, 先打印看看

5

可以看到, require 实际上是一个函数对象

除了可以通过 require('xxx') 调用这个函数之外, 还可以访问这个函数对象上的一些属性.

下面我们来捋一捋这几个属性

1. resolve

require.resolve 也是一个方法, require.resolve('xxx')require('xxx') 类似, 只不过:

前者会去执行 xxx 模块, 后者只会解析不会执行.

require.resolve('xxx') 返回的是 xxx 模块的绝对路径.

2. main

方眼一看, 这个....怎么和上面的 module 这么像, 我们大胆推测下:

会不会 moduleresolve 对象上的一个属性

答案: 是的~

console.log(require.main === module)  // true

是不是感觉两个对象一下子充满了联系()

3. extensions

如果你有去打印一下, 你会发现每个 module 里都有这个属性, 而且长得一样一样的

这个属性实际上写明了 Node.js 支持的 require 的文件类型: .js .json .node

如果没有写明扩展名的话, Node.js 会依次尝试去寻找 .js > .json > .node 文件, 所以为了保证 Node.js 的加载速度, 当引入一个非 js 文件时, 应该注明扩展名.

4. cache

node 在加载执行模块的时候, 都会对模块进行缓存.

注意: require.resolve 的模块不会进行缓存.

cache 属性里保存了模块的缓存, 缓存的内容就是模块的 module 对象

仔细看, 里面居然还有 id: '.' 的模块, 还记得我们最开始说的什么吗?

node 执行的模块的 id 会显示为 '.'

对的, cache 不但会缓存子模块的 module 对象, 还会将模块本身的 module 对象也缓存了.

有了缓存之后, 如果我们重复 require 一个模块, 这个模块的内容只会被执行一次

// child
console.log(1)

// main
require('./child')  // 1
require('./child')  // 不显示任何内容

如果希望重新 require 时, 模块依旧会被执行, 可以手动清除缓存

不过, 最好的办法, 是将 child 模块包装成一个函数返回

// child
module.exports = function(){
  console.log(1)
}

// main
require('./child')()  // 1
require('./child')()  // 1

番外: 全局包裹函数

实际上, 在 Node.js 中, 每一个模块都用一个函数包裹起来了, node 在执行模块的时候, 并不会直接执行模块代码, 而是执行这个函数包裹器.

这个函数大概长这样:

(function(exports, require, module, __filename, __dirname) { 
  // 模块内部的代码
  // 返回值为 module.exports, 且返回值无法手动修改
})

看看这上面我们熟悉的参数, exports, require, module, 这也就解释了我们为什么可以使用这些未声明的全局变量了, 实际他们都是 node 传给包裹函数的参数

来看看两个新的全 (函) 局 (数) 变 (参) 量 (数) :

__dirname 返回当前路径 (不包含文件名)
__filename 返回当前文件路径 (包含文件名)

包裹函数的好处不限于此, 我们可以思考一下: 当我们用 JavaScript 中的

你可能感兴趣的:(手撸手, 探究 Node.js 中的模块机制)