手撸手, 探究 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 中的 <script> 标签引入别的 js 文件时, 全局变量是不是会互相干扰.

node 中的包裹函数就规避了作用域污染, 任何模块中的全局变量, 经过了包裹器的包裹, 在其他模块中, 都成了局部变量。

参考:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容