【51CTO.com快译】众所周知,Node.js是一个基于Chrome V8引擎的服务器端JavaScript运行环境。它采用了一种事件驱动的、非阻塞式的I/O模式,运行起来既轻量级又高效。诚然,我们可以使用单个js文件,来编写出应用程序所涉及到的全部内容,但这样既不灵活,又不够模块化。而Node.js的出现,让模块化代码的编写变得非常简便。因此,对于Node.js的核心,我们需要理解和掌握的一个重要概念便是:依赖关系的管理。本文将和您一起探讨依赖项管理的各种模式,以及Nodejs是如何加载依赖项的。

在深入探讨细节之前,让我们首先弄清楚什么是模块。简而言之,模块是一段代码。为了共享和重用,我们需要将代码进行分组放置。通过模块,我们可以将复杂的应用程序分解到小块代码中。同时,模块也能够帮助我们理解程序代码的意图,并发现或修复各种错误。如果您想深入了解有关JavaScript模块系统的相关知识,请参见--https://hexquote.com/javascript-module-system/。

自2009年以来,CommonJS便实现了Javascript的模块化规范。它规范了模块的特性和各个模块之间的相互依赖性。由于每个文件都被当做一个模块(通常,module变量代表了当前模块),而且有自己的作用域,因此每个文件里面的变量、函数、以及类,都是私有的,且对于其他模块是不可见的。而模块的exports属性便是对外的接口。只有通过exports导出的属性,才能被其他模块识别和加载。而Node是基于CommonJs规范来实现模块的同步与加载。也就是说,我们可以通过在模块中调用require()方法,来接收模块的标识,并根据node的模块引入规则,引入其他模块,进而调用对应的属性和方法。

在此,我假设您已经掌握了Nodejs的上述基础知识。当然,如果您是一名Node.js的新手,则可以通过查看Node.js的相关简介(https://dzone.com/articles/nodejs-introduction),来了解更多背景信息。

设置应用

让我们从最简单的开始。假设我已经为某个项目创建了一个目录。通过运用npm init命令对其初始化后,我们将创建app.js和appMsg.js,两个JavaScript文件。下图展示了本项目的目录结构,我们将其作为管理的起点。如果您感兴趣的话,可以从文末给出的git存储库链接中,下载该项目的最终源代码。

默认情况下,这两个.js文件均为空。让我们通过如下更改,来更新appMsgs.js文件。

上面的代码段展示了module.exports关键字的用法。此语法用于公开给定文件(此处为appMsgs.js)中的属性或对象,以便能够在另一个文件(如本例中的app.js)中被直接使用到。

在该系统中,每个文件都可以访问到名为module.exports的文件。因此,我们在appMsgs.js文件中公开了一些项目,以方便观察app.js是如何使用(require)某些属性的。

显然,require关键字可以方便我们引用某个文件。也就是说,当我们执行require时,它将返回一个代表着模块化代码段的对象。因此,我们可以将其分配给一个appMsgs变量,然后在console.log的语句中简单地使用该属性。当代码被执行时,我们将看到如下输出:

该require通过执行JavaScript,构造出一个具有某种功能函数的对象,作为返回。它们既可能是一个类构造函数,又可以是其中包含了许多元素、或一些简单属性的对象。针对不同的模式,我们既可以导出多个对象,又可以只导出那些复杂的对象。可见,通过require和module.exports,我们可以创建出模块化的应用程序。

值得注意的是,应用程序所需的功能函数只会仅加载代码一次。也就是说,无论执行了什么代码,它们都不会被执行第二次。那么,如果别的程序也要通过require来获取对象的话,它将只能获得该对象的缓存版本。

下面,让我们来看看导出的方式。

如上面代码段所示,我对前面的代码进行了更改。现在,我不再公布对象了,而是导出了一个功能函数(function)。该函数在每次被调用时,都需要执行该代码。

下面,让我们来看看如何在app.js文件中使用它:

更新app.js文件

除了调用某个属性,我们还可以像执行函数一样去执行它。因此,这里的区别主要是,每当我们执行该代码时,函数内部的代码都会被重新执行(re-executed)。

下面是我们重新运行该代码段的输出:

至此,我们已经看到了module.exports的两种模式,及其两者的区别。还有一个常见的模式是,将其用作构造器方法(constructor method)。下面,让我们再来看一个例子:

下面是更改过的app.js文件:

从本质上讲,这与您在JavaScript中创建伪类(pseudo-class),并且创建它的各种实例(instances)是一致的。

下面是更改后的输出:

接着,让我们接着讨论此类模式的另一个示例。如下代码段所示,我创建了一个名为userRepo.js的新文件。

下面是更改后的app.js文件。

下图是该更改被执行后的结果:

当然,针对单个文件都去使用require的情况并不常见。接下来,让我们再讨论另一种模式--文件夹的依赖性。

文件夹依赖性

为了弄清Node.js是如何查找依赖性的,让我们重温一下前面例子中的JavaScript代码:

var appMsgs = require(“ ./appMsgs”)

Node不但会查找appMsgs.js文件,而且会查找作为目录的appMsgs,并取出它的值。

我创建了一个名为logger的文件夹,并在其中创建了一个index.js文件,其内容如下面的代码段所示:

下面是require此模块的app.js文件:

可见,在本例中,我们可以写出这样的JavaScript代码:

var logger = require(“./logger/index.js”)

上述较长的路径形式肯定是正确的。但是,我们其实只需写出如下的JavaScript代码即可:

var logger = require(“./logger”)

由于没有logger.js,而只有logger目录,因此在默认情况下,Node将加载index.js作为logger的起点。我们可以通过如下命令,来验证其输出结果:

在此,您可能心生疑虑:我们为什么如此费尽周折地创建文件夹和index.js呢?其背后的原因在于:您可能会将一些复杂的依赖项放在一起,而这些依赖项也可能还有其他的依赖项。而对于需要logger的调用者(caller)而言,它们不需要知道其他依赖项的存在。

这便是一种封装形式(encapsulation)。我们完全可以在多个文件中,构建更为复杂的代码段;而在使用者(consumer)角度,它们只需使用一个文件足矣。可见,文件夹是管理此类依赖性关系的更好方法。

Node程序包管理器(NPM)

第三类值得我们探讨的依赖性管理是NPM。顾名思义,NPM是Node.js程序包的管理和分发工具,它相当于后端的Maven。它可以让Javascript开发者更加轻松的共享和共用代码段。

通常,我们可以使用如下npm命令,来安装依赖项:

npm install underscore;

如下代码段所示,我们也可以简单地在app.js中require它:

如您所见,我们可以通过underscore的软件包来使用各项功能。同理,当需要用到此类模块时,我们并没有指定文件的路径,而只需使用其名称即可。Node.js将会从您的应用程序的node_modules文件夹中,自动加载到其对应的模块。

下面是代码执行后的输出结果:

小结

综上所述,我们讨论了Node.js是如何管理其依赖性关系的。您可以从此Git存储库—https://github.com/jawadhasan/nodedependency处,下载上述示例的源代码。

原文标题:Node.js – Dependency Management,作者: Jawad Hasan Shani

【51CTO译稿,合作站点转载请注明原文译者和出处为51CTO.com】