Node.js 中循环 require

关于循环 require 的骚走位

前言

以前一直没关心这个问题,按正常的方式来 require。但今天遇到了,收获了,疑问了。感触良多,思考不少。

起因

作为学习的一种方式,我准备搭建一个在线测试 Ng 匹配规则的平台,同时肩负静态文件提供(方便写日志的时候贴链接,防止外链失效或者比较慢)。后端 Koa,路由用的 koa-router。在路由设置这里,我的目录是这样的:

  • router
    • index.js
    • ngServer.js

最开始我想着,可以一个 route 导出一个配置,有多少个 router,app.js 中就引用多少次,但这样有个问题,太 low 了,app.js 看着也太乱了,还可能把我的路由功能直接暴露出来,而且不利于维护(加一个路由,得好几个地方加代码)。我想写不同的配置文件,然后 index 中全部引入,再导出给 app.js 引入来使用,这样的方式,app 只用引入一次路由。

问题

首先看一下 koa-router 暴露出来的 API:

ff

能用的基本都是实例方法,也没有暴露出相应方法可以从其他文件加载方法,类似 #addRouter (obj) 之类的。虽然可以写兼容函数来实现,但总感觉这么多人用的东西,会没有好的解决办法?

我的错,写到这里时我已经发现了,use 这个函数就是我所谓的 addRouter。。。自黑尴尬脸。但我今天要说的不是这个,而是 require 的循环引用。

什么是循环引用

简单地说,就是 A 还没执行完毕时,呼唤(require)了 B,B 在执行时又呼唤了 A,而且可能有调用 A 某些方法的欲望,这样你侬我侬的情景。我们可能理解起来觉得应该会出错,毕竟别人还没准备好,你就召唤别人了,这么心急。

对人可能是这么评价,但是对 Node 来说,它有自己的实现方式。下面打个例子:

A 模块中:

1
2
3
4
5
6
7
8
9
console.log(' 开始加载 A 模块');
var b = require('./b.js');
console.log('in a, b is ', b);

exports.func = function() {
console.log('调用 A 模块成功');
};

console.log('A 模块加载完毕');

B 模块中:

1
2
3
4
5
6
7
8
console.log(' 开始加载 B 模块');
var A = require('./a.js');
console.log('in b, a is ', A);

exports.callAmodule = function() {
A.func();
}
console.log('B 模块加载完毕');

运行以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = require('./a.js');
var b = require('./b.js');
b.callAmodule();




// 结果:
开始加载 A 模块
开始加载 B 模块
in b, a is {}
B 模块加载完毕
in a, b is { callAmodule: [Function] }
A 模块加载完毕
调用 A 模块成功

来解释一下:require A 的时候,执行 console.log (‘ 开始加载 A 模块’);然后在 A 中 require B,这时停止 A 的加载,开始 B 的加载;然后输出 ““开始加载 B 模块””。这时遇到去加载引用它的父模块 A 了。但由于 A 模块是部分加载,Node 会直接跳过再次加载 A 的过程,运行到 “in b, a is”。这时的 A 并没有 module.exports 或者 exports 导出属性,所以此时的 A 是空对象 {}。然后 B 导出一个使用了 A 方法的方法。执行 “B 模块加载完成”。这时 B 模块加载完成,继续回到 A 中,继续加载。

这里有个小问题,为什么 B 中的 callAmodule 方法可以成功调用 A 的 func?这里就要搞清楚 module.exports 和 exports.method 的区别了。因为 A 中使用的使 exports.func 来导出属性,相当于 module 模块 exports 属性又增添了一个属性,就 module.exports 来说,它的指向并没有改变。换而言之,如果使用

1
2
3
module.exports = { // 这里相当于将module.exports重新指向一个新对象(不清楚的可以看一下javascript引用类型)
func: function() {}
}

这样的话,在 B 中的 callAmodule 中使用的 A 就是 A 最开始的 module.exports 空对象了,就会导致调用出错。