Hello World
Spiga

Node.js中相同模块是否会被加载多次?

2011-12-27 15:19 by 老赵, 1501 visits

JavaScript的包管理一直是个软肋,我很难想象,连这一基础功能都没有做好的语言,现在居然会如此流行。在我看来,其实JavaScript流行的最主要元素还是把持了浏览器,而Web应用在这几年掀起了一阵腥风血雨。任意一门语言,只要能像JavaScript般被标准采纳,被所有浏览器接受,它都能“成功”,真是所谓宿命。

当然,既然它流行了,既然人们想要用它做大事了,就要开始为它制定一些模块的约定。这几天我为Jscex实现AMD规范的时候,便深刻体会到模块化的优势。Node.js也使用了CommonJS模块机制,最近在InfoQ上有一篇文章讨论了这方面的问题。这篇文章提到Node.js在载入模块时,如果之前该模块已经加载过则不会有重复开销,因为模块加载有缓存机制。这篇文章是我初审的,当时也正好在思考Jscex在Node.js使用时的模块化问题,但研究了它的规则之后,我发现在某些情况下还是可能加载多次。现在我们便来分析这个问题。

当我们使用require方法加载另一模块的时候,Node.js会去查询一系列的目录。我们可以从module.paths中得到这些路径,例如:

[ '/Users/jeffz/Projects/node-test/node_modules',
  '/Users/jeffz/Projects/node_modules',
  '/Users/jeffz/node_modules',
  '/Users/node_modules',
  '/node_modules']

这里是我在运行/User/jeffz/Projects/node-test目录下一个模块时得到的结果。可见,Node.js会从当前模块所在目录的node_modules(这里怎么不遵守Unix习惯,而使用了下划线呢?)开始找起,如果没找到再会去找上级目录的node_modules,直到根目录为止。当然,实际情况下还会有NODE_PATH环境变量标识的目录等等。当模块的位置确定之后,Node.js便会查看这个位置的模块是否已经被加载,如果已加载,则直接返回。

简单地说,Node.js是根据模块所在路径来缓存模块的。

这么看来,“相同模块是否会被加载多次”这个问题,其实就演变成了“相同模块是否会出现在不同路径里”。简单想来这似乎不太可能,因为如果我们要使用某个模块的时候,它的位置总是确定的。例如,使用npm安装的模块,总是会出现在当前目录的node_modules里,加载时总是会找到相同的路径。那么,在“间接”依赖相同模块的情况下呢?

例如我们想要使用Express框架,于是使用npm来安装,便会得到:

$ npm install express
express@2.5.2 ./node_modules/express 
├── mkdirp@0.0.7
├── qs@0.4.0
├── mime@1.2.4
└── connect@1.8.5

可见,Express依赖了其他一些模块,它们都存放在express模块自己的目录里面,例如./node_modules/express/node_modules/mime。好,假如我们项目自身也要使用mime项目,我们自然也可以使用npm来安装:

$ npm install mime
mime@1.2.4 ./node_modules/mime 

于是我们最终得到的是这样的结构:

./node_modules
├── mime
└── express
    └── node_modules
        ├── mkdirp
        ├── qs
        ├── mime
        └── connect

请注意,这里的mime模块便出现在两个位置上,它们名称版本都一致,完全是一个模块。那么试想,如果我们在自己的代码里加载的mime模块,以及express内部加载的mime模块是同一个吗?显然不是,可见,在这里相同的模块被重复加载了两次,产生了两个模块“实例”。

这种重复加载在一般情况下不会有太大问题,最多内存占用大一点而已,不会影响程序的正确性。但是,我们也可以轻易设想到一些意外的情况。例如,在Jscex中,每个Task对象我都会给定一个ID,不断增长。要实现这点我们需要维护一个“种子”,全局唯一。之前这个种子定义在闭包内部,但由于Jscex模块会被加载多次,这样从不同模块“实例”生成的Task对象,它们的ID便有可能重复。当然,解决这个问题也并不困难,只需要将种子定义在根对象上即可,不同的模块“实例”共享相同的根对象。

还有个问题可能就显得隐蔽些了,我们可以通过一个简单的实验来观察结果。我们先来定义一个jeffz-a模块,其中暴露出一个MyType类型:

module.exports.MyType = function () { }

然后将其发布到npm上。然后再写一个jeffz-b模块,依赖jeffz-a,并将jeffz-a中定义的MyType类型直接暴露出去:

module.exports.MyType = require("jeffz-a").MyType;

接着将jeffz-b也发布置npm上。再重新写一个测试模块,使用npm安装jeffz-a和jeffz-b,最终目录会是这样的:

./node_modules
├── jeffz-a
└── jeffz-b
    └── node_modules
        └── jeffz-a

在测试模块内,我们来测试实例与类型之间的关系:

var a = require("jeffz-a");
var b = require("jeffz-b");

console.log(new a.MyType() instanceof a.MyType); // true
console.log(new b.MyType() instanceof b.MyType); // true

console.log(new a.MyType() instanceof b.MyType); // false
console.log(new b.MyType() instanceof a.MyType); // false

从表面上看,jeffz-b和jeffz-a暴露出的应该是相同的MyType类型,它们的对象通过instanceof相互判断应该都返回true,但实际上由于jeffz-b中的jeffz-a,与我们直接加载的jeffz-a模块是不同的实例,因此MyType类型自然也不是同一个了。

这对于Jscex的影响在于,Jscex的异步模块在取消时,原本是通过判断异常对象是否为CanceledError类型来决定Task的状态为cancelled还是faulted。但由于Node.js可能会将相同的模块加载为多个实例,因此即便抛出的的确是某个实例的CancelledError,也无法通过另一个实例内部的判断。因此,目前Jscex的判断方式修改为检查异常对象的isCancellation字段,简单地解决了这个问题。

当然,Node.js这种“重复加载”的影响也并非完全是负面的,至少它天然的解决了多版本共存的问题。例如,express v2.5.2依赖mime v1.2.4,但我们程序自身又想使用mime v1.2.5。此时,express内部自然使用mime v1.2.4,而我们自己的程序使用的便是mime v1.2.5。

有些情况下您可能也想避免这种重复加载,这就必须手动地删除模块内部被间接依赖的模块,将其移动到模块查询路径的公用部分上了。就目前看来,这些操作必须手动进行,因为npm在安装模块时不会关心依赖的模块是否已经安装过了(例如在NODE_PATH环境变量标识的路径里),它一定会重新下载所有依赖的模块。可惜如果您使用的是托管形式的Node.js服务,则很有可能无法做到这一点。

因此,我们在编写Node.js模块的时候,便事先考虑下它会被重复加载时的情况吧。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

24 条回复

  1. GhostHorse
    111.174.107.*
    链接

    GhostHorse 2011-12-28 01:28:41

    沙发也抢了,花了我一年多遍历老赵的文章和共享的ppt,从好多都看不懂,现在才能看懂一点点了,能说说怎么对那么多语言感兴趣, 那些语言的什么 吸引了你

  2. 老赵
    admin
    链接

    老赵 2011-12-28 05:40:42

    @GhostHorse

    因为我的工作是编程,各种语言都能做出差不多的东西,但编程体验对我很重要,学习多种语言就知道怎么可以改进自己编程生活。对于不能改进很难改进的情况,比如Java,就十分深恶痛绝。

  3. mcpssx
    59.175.192.*
    链接

    mcpssx 2011-12-28 07:34:25

    java特性再少,也比javascipt多啊,不知道老赵对javascript有何看法?

  4. mcpssx
    59.175.192.*
    链接

    mcpssx 2011-12-28 07:54:30

    如果老赵实在喜欢C#,jvm上也有类似C#的语言,比如slab,

    using java.lang;
    using java.util;
    
    var list = new ArrayList<String> { "a", "aa", "aaa" };
    
    foreach (var s in list.where(p => p.length() > 1));    
    

    但是反过来,jvm上的fantom,scala,clojure在.net实现的成熟度就差远了,lucene,hadoop都是java的,java6还自带了javascript引擎,jvm上server side javacript框架一堆了,而.net上连一个成熟的javascript实现都没有。

    实在喜欢C#语法的,建议不如改投java的slab门下.

  5. 老赵
    admin
    链接

    老赵 2011-12-28 09:23:05

    @mcpssx

    太神奇了,根本没提微软你都能插嘴,果然人至践则无敌啊。

    我在很多地方说过了,JavaScript是个好语言,虽然有一些大规模开发上的缺陷,但总体而言比Java好太多了。当然这句话没指望你能理解,你在这方面就是个初中生水平,只会谈语言特性的数量多少。理解能力更差,从不关心我一直说的是谈什么,逻辑不清,自说自话。

    至于其他方面,你连客观事实都不遵守,更没啥好说的了。别问我哪些,没空陪你耍流氓。

  6. mcpssx
    183.94.0.*
    链接

    mcpssx 2011-12-28 13:07:11

    javascript比java好太多,就更说明问题。

    在server端,因为java6本来就自带了javascript引擎,基于rhino的server side javascript框架有好几个了,其他的也大都是基于开源v8或则spidermonkey。没有一个是用微软的js引擎的,嗯,也许10几年前的asp的jscript算一个,可惜微软早就放弃了。

    在client端,js确实很有前途,html5啊,qt,gtk+啊都支持,flash as3也是扩展js的,看来微软的wpf恐怕又要退到幕后去了,学wpf恐怕很快就没什么用了。

    其实我觉得老赵你推jscex,推广javascript,我觉得比微软专有技术的wpf什么靠谱多了。

  7. 老赵
    admin
    链接

    老赵 2011-12-29 02:07:16

    @mcpssx

    哈哈,要么“说明问题”,要么“更说明问题了”,果然是神一般的思路啊。别用你的逻辑来解释我的行为,让我觉得好恶心啊。WPF现在的价值显然是越来越高,真正实现Everywhere了,我只后悔我居然没学过。当然,照样没指望你能理解这点。无论我推广什么,再说一遍,别用你的逻辑来解释我的行为,让我觉得好恶心。

    还是那句话,果然人至贱则无敌,什么话题都能被你扯到微软身上去,果然痛恨微软到骨子里了。我很想知道您的脸皮怎么练到那么厚的,用来干事业估计早成蒙牛这样的伟大企业了,何必只在我这里装疯卖傻。

  8. 银光小子
    113.116.7.*
    链接

    银光小子 2011-12-29 13:12:39

    这次我感觉老赵太凶了...... 人家都没攻击骂人啥的 赵哥已经喷人家好几次了 呵呵.......

  9. 老赵
    admin
    链接

    老赵 2011-12-29 13:17:06

    @银光小子

    你还要我每次都花很长时间跟他耍够流氓了才停啊,到时候停不停的下来还是问题,流氓永远有流氓逻辑的。我的方式一般是要争论就争论到底,但也有份黑名单,黑名单上面的人物我不招惹,但它们出现基本就直接骂了,比如这里的某某某和博客园的某某某和某某某,嘿嘿。

  10. mathgl
    113.12.169.*
    链接

    mathgl 2011-12-30 05:47:07

    此人牛x。任何事情都可以和微软绑在一起..莫不是买了其股票跌了?

  11. ToEverBody
    210.75.15.*
    链接

    ToEverBody 2011-12-30 07:16:19

    老赵 我出现了 现在我突然发现了 C#的美好 我觉得学习C# 跟你学习 OhYeah

  12. mcpssx
    59.175.192.*
    链接

    mcpssx 2011-12-31 03:27:09

    从infoq中知乎关于京东采用.net的讨论来看,反对.net的看来占了大多数。 京东采用.net的讨论 我没有知乎的账号, 不知道老赵在里面是不是也骂那些人?

  13. waynebaby
    210.22.108.*
    链接

    waynebaby 2011-12-31 04:17:15

    那帮傻子光跟他们讲道理就拉黑我们的

  14. 老赵
    admin
    链接

    老赵 2011-12-31 14:30:40

    @mcpssx

    嗯上面说了,讲道理容易被拉黑啊,他们跟你一样都是业界高级分析师,牛人风范,方舟子总知道吧,最近也在装疯卖傻。我很少骂人的,没有那么多人像你一样能进黑名单,你不妨得意一番。知乎上我当然不骂人,自然有人反对他们的观点,自己去看看吧,别只看到你想看的内容。

    至于知乎,你还真别装,之前你还举过知乎上面的例子,现在又说没有知乎帐号?而且你想要知乎帐号么?我邀请你啊,留个邮件,只有我看得到。你不是热爱讨论么,不是喜欢来我这里“兼听则明”么,怎么连知乎帐号都没有?如果不留以后拜托不要没事来装模做样装疯卖傻,之前让你去知乎讨论,搞了半天你也到现在没去。

    总而言之,你还真喜欢每天都来秀点下限,抽自己几下耳光啊。

  15. mathgl
    171.105.49.*
    链接

    mathgl 2012-01-01 15:39:51

    看知乎,如果不看全部答案,不需要帐号....

  16. jsyzthz
    222.128.9.*
    链接

    jsyzthz 2012-01-03 03:32:38

    老赵能推荐个国外的ASP.NET虚拟主机空间啊,便宜点的。空间大小20-30M就OK了,支持access的

  17. 老赵
    admin
    链接

    老赵 2012-01-03 09:46:51

    @mathgl

    不清楚,反正我还在等“兼听则明”的大神留邮箱。

  18. 链接

    Gabriel Zhang 2012-01-05 09:57:00

    md文件用什么软件打开呢

  19. 老赵
    admin
    链接

    老赵 2012-01-05 13:38:42

    @Gabriel Zhang

    就是Markdown格式了,用某些Markdown编辑器或浏览器可以看到转化后的结果,比如我现在用Mou编辑md文件,就像我博客这样实时预览的,很不错。

  20. oztoto
    114.244.81.*
    链接

    oztoto 2012-01-07 01:21:11

    请问老赵,VS2010企业版个人买要多少钱啊,我怎么在微软的主页上就没看到售价呢?

  21. Raindy Long
    218.19.51.*
    链接

    Raindy Long 2012-01-07 02:49:53

    试着玩了一下jscex,确实挺有趣的,因为自己不是专门做研发的,所以暂时说不出个所以然来,免得说错了不好意思呢,以后玩多了有感觉再来SOSO。

    8年前学和用javascript的时候,那时候它还没有现在的红火,现在早已忘得差不多了,BS自己没有KEEP住不断的学习,老赵对技术的美的追求让我重新有了学习的冲动啊,呵呵。

    睡觉前运行了一下quick-start.js,醒来发现“Infinity”输出了,不过这也是预料中的结果。

  22. Cat Chen
    106.187.35.*
    链接

    Cat Chen 2012-01-09 05:55:30

    这说明了你不能把你的依赖项暴露出去,你的 exports 里面不能带有任何 require 进来的类型,除非你准备把那个类型也 exports 出去,否则别人在使用你的库的时候再 require 一个同名库就不一样了。

    这个原则到底怎么执行好,best practice 是什么,估计还要一段时间去发现。暂时来说,最好还是不要 exports 任何 require 的东西,但这对 Deferred 库来说是个问题。

  23. 老赵
    admin
    链接

    老赵 2012-01-09 13:45:02

    @Cat Chen

    有些情况暴露些require里的东西也是很常见的,平时写程序,例如依赖第三方某个库,也可能直接就把其中的类型暴露出去了啊。

  24. jovi
    116.12.234.*
    链接

    jovi 2012-01-13 04:17:12

    上面的吵架好黄好暴力.... 一个技术贴怎么吵成这样.....

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我