Skip to content

Latest commit

 

History

History
732 lines (501 loc) · 41.3 KB

File metadata and controls

732 lines (501 loc) · 41.3 KB

二、Node 模块系统

作为开发人员,您可以使用核心 Node 功能解决许多复杂的问题。然而,Node 真正的优势之一是它的开发者社区和丰富的第三方模块。Node 的包管理器npm负责跟踪所有这些模块。npm FAQ 页面开玩笑地说npm不是“Node 包管理器”的首字母缩写,而是一个递归的反义词。npm不是首字母缩写不管它的意思是什么,npm是一个命令行工具,从 Node 版本 0.6.3 开始,它与 Node 环境捆绑在一起。

npm所做的——而且做得非常好——是管理 Node 模块及其依赖关系。在编写本报告时,官方登记册中有 47 000 多个包裹。您可以在注册中心的网站https://npmjs.org/上浏览所有可用的软件包。除了每个单独的模块,该网站还显示了各种排名,包括哪些模块最受欢迎,哪些模块最受依赖。如果您更愿意亲自使用命令行,您可以使用npm search命令搜索注册表,该命令允许您基于一个或多个关键字搜索软件包。例如,npm search可以用来定位名称或描述中包含database一词的所有模块(参见清单 2-1 )。第一次运行这个命令时,预计会有短暂的延迟,因为npm会构建一个本地索引。

清单 2-1 。使用npm searchnpm注册表中定位模块

$ npm search database

安装软件包

为了使用一个模块,你必须在你的机器上安装它。这通常就像下载几个 JavaScript 源文件一样简单(有些模块还需要下载或编译二进制文件)。要安装软件包,请键入npm install,后跟软件包名称。例如,commander模块提供了实现命令行接口的方法。要安装最新版本的commander,发出清单 2-2 中的命令。

清单 2-2 。使用npm安装最新版本的commander

$ npm install commander

如果您对安装软件包的最新版本不感兴趣,您可以指定一个版本号。Node 模块跟随一个专业小调补丁版本控制方案。例如,要安装commander版本 1.0.0 ,使用清单 2-3 中所示的命令。@字符用于将包名和版本分开。

清单 2-3 。安装commander的 1.0.0 版本

$ npm install commander@1.0.0

对主要版本号的更改可以表明模块已经以非向后兼容的方式进行了更改(称为重大更改)。即使对次要版本的更改也可能会意外引入重大更改。因此,您通常会希望安装某个版本的最新补丁——npm支持使用x通配符。清单 2-4 中的命令安装了commander1.0 版本的最新补丁。(注意,x通配符也可以用来代替主要版本和次要版本。)

清单 2-4 。安装最新的commander 1.0 补丁

$ npm install commander@1.0.x

您还可以使用关系版本范围描述符来选择版本。关系版本范围描述符选择与一组给定标准相匹配的最新版本。npm支持的各种关系版本范围描述符在表 2-1 中列出。

表 2-1 。关系版本范围描述符

|

关系版本范围描述符

|

版本标准

| | --- | --- | | =版本 | 与版本完全匹配。 | | >版本 | 大于版本。 | | > =版本 | 大于或等于版本。 | | | 小于版本。 | | < =版本 | 低于或等于版本。 | | *版本 | 大于或等于版本,但小于下一个主要版本。 | | * | 最新版本。 | | "" | 最新版本。 | | 版本1–版本 2 | 大于等于版本 1 ,小于等于版本 2 。 | | 范围 1 ||范围 2 | 匹配范围 1 和范围 2 指定的版本。 |

根据表 2-1 ,列表2-5中的所有命令都是有效的npm命令。

清单 2-5 。使用关系版本范围描述符的各种npm install命令

$ npm install commander@"=1.1.0"
$ npm install commander@">1.0.0"
$ npm install commander@"∼1.1.0"
$ npm install commander@"*"
$ npm install commander@""
$ npm install commander@">=1.0.0 <1.1.0"
$ npm install commander@"1.0.0 - 1.1.0"
$ npm install commander@"<=1.0.0 || >=1.1.0"

从 URL 安装

此外,npm允许直接从gitURL 安装软件包。这些 URL 必须采用清单 2-6 中所示的形式之一。在清单中,commit-ish表示一个标签、SHA 或分支,可以作为参数提供给git checkout。注意,例子中的链接并没有指向任何特定的git项目。

image 注意使用 Node 不需要了解git和 GitHub。然而,大多数 Node 模块使用 GitHub 生态系统进行源代码控制和错误跟踪。虽然 GitHub 及其使用已经超出了本书的范围,但是熟悉它是非常可取的。

清单 2-6git支持的 URL 格式npm

`git://github.com/user/project.git#commit-ish`
git+ssh://user@hostname:project.git#commit-ish
git+ssh://user@hostname/project.git#commit-ish
git+http://user@hostname/project/blah.git#commit-ish
git+https://user@hostname/project/blah.git#commit-ish

软件包也可以从 tarball URLs 安装。例如,要安装 GitHub 库的主分支,使用清单 2-7 中所示的语法。虽然这个 URL 没有指向实际的存储库,但是您可以通过下载commander模块:https://github.com/visionmedia/commander.js/tarball/master进行试验。

清单 2-7 。从 GitHub 库安装 Tarball

$ npm install https://github.com/user/project/tarball/master

包装位置

当软件包被安装时,它们被保存在本地机器的某个地方。通常,这个位置是当前目录中名为node_modules的子目录。要确定位置,使用命令npm root。您也可以使用npm ls命令查看所有已安装的模块。安装commander模块后,您可以使用npm ls验证它是否存在。出于此示例的目的,请安装版本 1.3.2。清单 2-8 显示commander版本 1.3.2 已安装。另外,请注意安装了一个名为keypress的模块。树形结构表明commander依赖于keypress模块。由于npm能够识别这种依赖性,它会自动安装任何需要的模块。

清单 2-8 。使用npm ls列出所有当前安装的软件包

$ npm ls
/home/colin/npm-test
└─┬ commander@1.3.2
     └── keypress@0.1.0

也可以通过浏览node_modules子目录来查看已安装的模块。在这个例子中,commander安装在node_modules/commander,而keypress安装在node_modules/commander/node_modules/keypress。如果keypress有任何依赖项,它们将被安装在keypress目录下的另一个node_modules子目录中。

全球软件包

如前所述,包是包含在程序中的库。这些被称为本地包,必须安装在使用它们的每个项目中。另一种类型的软件包,称为全局软件包,只需要安装在一个位置。尽管全局包通常不包含代码库,但它们可以。根据经验,全局包通常包含命令行工具,它们应该包含在PATH环境变量中。

要全局安装包,只需发出带有-g--global选项的npm install。事实上,您可以通过在大多数npm命令中添加-g选项来处理全局包。例如,您可以通过发出命令npm ls -g来查看已安装的全局包。您也可以使用npm root -g命令定位全局node_modules文件夹。

链接包

使用npm,您可以创建到本地包的链接。当您链接到一个包时,它可以像一个全局包一样被引用。如果您正在开发一个模块,并且希望另一个项目引用该模块的本地副本,这将非常有用。如果您想部署您的模块而不将它发布到公共的npm注册中心,链接也是有用的。

包链接是一个两步过程。第一步,创建链接,是通过切换到您想要使其可链接的项目的目录来完成的。清单 2-9 展示了如何创建一个到你的模块的链接,假设你的模块位于foo-module中。执行npm link命令后,验证该链接是使用npm ls -g创建的。

清单 2-9 。使用npm link创建链接

$ cd foo-module
$ npm link

模块链接的第二步,实际上是引用链接,非常类似于包安装。首先,切换到将导入链接模块的项目的目录。接下来,发出另一个npm link命令。但是,这一次您还必须指定链接模块的名称。该程序的一个例子如清单 2-10 所示。在这个例子中,清单 2-9 中的foo-module链接是从第二个模块bar-module引用的。

清单 2-10 。使用npm link引用现有链接

$ cd bar-module
$ npm link foo-module

解除包的链接

移除链接模块的过程与创建链接模块的过程非常相似。要从应用中删除链接的模块,使用npm unlink命令,后跟名称。清单 2-11 显示了从bar-module中移除链接的foo-module的命令。

清单 2-11 。使用npm unlink删除对链接的引用

$ cd bar-module
$ npm unlink foo-module

类似地,要从您的系统中删除一个链接,切换到链接模块的目录,并发出npm unlink命令。清单 2-12 展示了如何移除foo-module链接。

清单 2-12 。使用npm unlink移除链接的模块

$ cd foo-module
$ npm unlink

更新软件包

因为任何被积极开发的包最终都会发布一个新版本,所以你的拷贝会变得过时。要确定你的副本是否过期,在你的项目目录中运行npm outdated(见清单 2-13 )。在示例中,假设安装了commander的过时版本 1.0.0,npm表示最新版本是 2.0.0,但您的副本只有 1.0.0。清单 2-13 检查所有的本地包。您可以通过指定它们的名称来检查单个包,并且可以通过指定-g选项来处理全局包。

清单 2-13 。使用npm outdated显示过期的包

$ npm outdated
npm http GET https://registry.npmjs.org/commander
npm http 304 https://registry.npmjs.org/commander
commander@2.0.0 node_modules/commander current=1.0.0

要更新任何过期的本地包,使用npm update命令。与outdated非常相似,update在默认情况下适用于所有本地包。同样,您可以通过指定它们的名称来定位单个模块。您也可以使用-g选项更新全局包。在清单 2-14 的中,npm使用-g选项更新自己。

清单 2-14 。使用npm update更新npm

$ npm update npm -g

卸载软件包

要删除一个包,使用npm uninstallnpm rm命令(这两个命令可以互换使用),并指定一个或多个要删除的包。您也可以通过提供-g选项来删除全局包。清单 2-15 显示了如何使用npm rm移除commander模块。

清单 2-15 。使用npm rm卸载commander

$ npm rm commander

require()功能

如前一节所示,使用npm管理 Node 包。然而,要将模块导入到程序中,需要使用require()函数。require()接受单个参数,即指定要加载的模块的字符串。如果指定的模块路径存在,require()返回一个可用于与模块接口的对象。如果找不到该模块,就会引发异常。清单 2-16 显示了如何使用require()函数将commander模块导入到程序中。

清单 2-16 。使用require()功能

var commander = require("commander")

核心模块

核心模块是编译成 Node 二进制的模块。require()赋予它们最高的优先级,这意味着在模块命名冲突的情况下,加载核心模块。例如,Node 包含一个名为http的核心模块,顾名思义,它提供了使用超文本传输协议(HTTP) 的功能。无论如何,对require("http")的调用总是会加载核心http模块。顺便提一下,核心模块位于 Node 源代码的lib目录中。

文件模块

文件模块是从文件系统加载的非核心模块。可以使用绝对路径、相对路径或从node_modules目录指定它们。以斜杠(/)开头的模块名被视为绝对路径。例如,在清单 2-17 中,一个文件模块foo使用绝对路径加载。

清单 2-17 。使用绝对路径导入文件模块

require("/some/path/foo");

image 注意Windows 等一些操作系统使用不区分大小写的文件系统。这允许你写require("commander")require("COMMANDER")require("CoMmAnDeR")。然而,在像 Linux 这样区分大小写的文件系统上,最后两个调用会失败。因此,无论使用什么操作系统,都应该区分大小写。

Node 还支持 Windows 样式的文件路径。在 Windows 上,Node 允许交换使用斜杠和反斜杠字符(/\)。为了一致性,也为了避免转义反斜杠字符,本书主要使用 Unix 风格的路径。然而,请注意在清单 2-18 中显示的所有路径在 Windows 上都是有效的。

清单 2-18 。在 Windows 上有效的模块路径示例

require("/some/path/foo");
require("C:/some/path/foo");
require("C:\\some\\path\\foo");
require("\\some/path\\foo");

以一两个点(...)开头的模块路径被解释为相对路径——也就是说,它们被认为是相对于调用require()的文件的。清单 2-19 显示了相对模块路径的三个例子。在第一个示例中,foo从与调用脚本相同的目录中加载。在第二个中,foo位于调用脚本的父目录中。在第三个示例中,foo位于调用脚本目录的子目录sub中。

清单 2-19 。使用相对路径的模块导入示例

require("./foo");
require("../foo");
require("./sub/foo");

如果模块路径不对应于核心模块、绝对路径或相对路径,那么 Node 开始在node_modules文件夹中搜索。Node 从调用脚本的父目录开始,并追加/node_modules。如果没有找到该模块,Node 在目录树中向上移动一级,追加/node_modules,然后再次搜索。重复这种模式,直到找到模块或到达目录结构的根。清单 2-20 中的例子假设一个项目位于/some/path中,并按顺序显示了将被搜索的各种node_modules目录。

清单 2-20node_modules目录的搜索顺序示例

/some/path/node_modules
/some/node_modules
/node_modules

文件扩展名处理

如果require()没有找到完全匹配,它会尝试添加.js.json.node文件扩展名。如第一章所述,.js文件被解释为 JavaScript 源代码,.json文件被解析为 JSON 源代码,.node文件被视为编译后的附加模块。如果 Node 仍然找不到匹配,就会抛出一个错误。

还可以使用内置的require.extensions对象以编程方式添加对附加文件扩展名的支持。最初,这个对象包含三个键,.js.json.node。每个键映射到一个函数,该函数定义了require()如何导入该类型的文件。通过扩展require.extensions,可以自定义require()的行为。例如,清单 2-21 扩展了require.extensions,使得.javascript文件被视为.js文件。

清单 2-21 。扩展require.extensions对象以支持额外的文件类型

require.extensions[".javascript"] = require.extensions[".js"];

您甚至可以添加自定义处理程序。在清单 2-22.javascript文件使require()将导入文件的数据打印到控制台。

清单 2-22 。向require.extensions对象添加自定义处理程序

require.extensions[".javascript"] = function() {
 console.log(arguments);
};

image 注意虽然这个特性最近被弃用,但是模块系统 API 被锁定,所以require.extensions不太可能完全消失。官方文档推荐将非 JavaScript 模块包装在另一个 Node 程序中,或者先验地编译成 JavaScript。

解析模块位置

如果您只对了解包的位置感兴趣,可以使用require.resolve()函数,它使用与require()相同的机制来定位模块。然而,resolve()并没有真正加载模块,而是只返回模块的路径。如果传递给resolve()的模块名是核心模块,则返回该模块的名称。如果模块是文件模块,resolve()返回模块的文件名。如果 Node 找不到指定的模块,则会引发错误。清单 2-23 中的例子显示了resolve()在 REPL 环境中的用法。

清单 2-23 。使用require.resolve()定位http模块

> require.resolve("http");
'http'

模块缓存

成功加载的文件模块缓存在require.cache对象中。同一模块的后续导入将返回缓存的对象。一个警告是,解析的模块路径必须完全相同。这是因为模块通过其解析的路径进行缓存。因此,缓存成为导入模块和调用脚本的功能。假设你的程序依赖于两个模块,foobar。第一个模块foo没有依赖关系,但是bar依赖foo。产生的依赖层次结构如清单 2-24 所示。假设foo驻留在node_modules目录中,它被加载两次。第一次加载发生在foo解析到your-project/node_modules/foo目录时。当从bar引用foo并解析为your-project/node_modules/foo/node_modules时,发生第二次加载。

清单 2-24 。一个依赖层次结构,其中foo被多次引用

your-project
├── foo@1.0.0
└─┬ bar@2.0.0
     └── foo@1.0.0

package.json文件

在前面的部分中,您看到了npm识别包之间的依赖关系并相应地安装模块。但是npm如何理解模块依赖 ies 的概念呢?事实证明,所有相关信息都存储在名为package.json的配置文件中,该文件必须位于项目的根目录下。正如文件扩展名所暗示的,文件必须包含有效的 JSON 数据。从技术上来说,你不需要提供一个package.json,但是如果没有的话,npm将无法访问你的代码。

package.json中的 JSON 数据应该符合特定的模式。最低限度,你必须为你的包指定一个名字和版本。没有这些字段,npm将无法处理您的包裹。最简单的package.json文件如清单 2-25 所示。包的名称由name字段指定。该名称应该在npm注册表中唯一地标识您的包。通过使用npm,该名称成为 URL、命令行参数和目录名的一部分。因此,名称不能以点或下划线开头,也不能包含空格或任何其他非 URL 安全字符。最佳实践还规定,名称应该简短且具有描述性,并且不包含“js”或“node”,因为它们是隐含的。此外,如果您计划向公众发布您的包,请验证该名称在npm注册表中是否可用。

清单 2-25 。最小的package.json文件

{
  "name": "package-name",
  "version": "0.0.0"
}

包的版本在version字段中指定。当与名称结合时,版本为包提供了真正唯一的标识符。版本号指定了主版本号、次版本号和补丁号,用点分隔(npm允许版本以v字符开头)。您还可以通过在修补程序编号后附加一个标记来指定内部版本号。有两种类型的标签,预发布和发布后。后发布标签增加版本号,而预发布标签减少版本号。发布后标签是一个连字符后跟一个数字。所有其他标签都是预发布标签。清单 2-26 中的例子展示了版本标记的作用。几个带标签的版本和一个不带标签的版本(0.1.2)按降序排列。

清单 2-26 。几个带标签的版本和一个不带标签的版本按降序排列

0.1.2-7
0.1.2-7-beta
0.1.2-6
0.1.2
0.1.2beta

描述和关键字

description字段用于提供您的包的文本描述。类似地,使用keywords字段提供一组关键字来进一步描述您的包。关键字和描述帮助人们发现你的包,因为它们是由npm search命令搜索的。清单 2-27 显示了包含descriptionkeywords字段的package.json摘录。

清单 2-27 。在package.json文件中指定描述和关键字

"description": "This is a description of the module",
"keywords": [
  "foo",
  "bar",
  "baz"
]

作者和撰稿人

项目的主要作者在author字段中指定。该字段只能包含一个条目。然而,第二个字段contributors可以包含对项目做出贡献的人员的数组。有两种方法可以指定一个人。第一个是包含nameemailurl字段的对象。清单 2-28 中显示了这种语法的一个例子。该示例指定了一个主要作者和两个额外的投稿人。

清单 2-28 。在package.json文件中指定作者和贡献者

"author": {
  "name": "Colin Ihrig",
  "email": "colin@domain.com",
  "url": "http://www.cjihrig.com"
},
"contributors": [
  {
    "name": "Jim Contributor",
    "email": "jim@domain.com",
    "url": "http://www.domain.com"
  },
  {
    "name": "Sue Contributor",
    "email": "sue@domain.com",
    "url": "http://www.domain.com"
  }
]

或者,表示人的对象可以写成字符串。在一个字符串中,一个人由名字指定,然后由尖括号内的电子邮件地址指定,后面是圆括号内的 URL。在清单 2-28 中显示的对象语法已经在清单 2-29 中使用字符串重写。

清单 2-29 。将作者和贡献者指定为字符串而不是对象

"author": "Colin Ihrig <colin@domain.com> (http://www.cjihrig.com)",
"contributors": [
  "Jim Contributor <jim@domain.com> (http://www.domain.com)",
  "Sue Contributor <sue@domain.com> (http://www.domain.com)"
]

主入口点

由于包可以由许多文件组成,Node 需要某种方法来标识它的主入口点。像大多数其他配置选项一样,这是在package.json文件中处理的。在main字段中,您可以告诉 Node 在使用require()导入您的模块时加载哪个文件。假设您的模块名为foo,但是它的主入口点位于一个名为bar.js的文件中,该文件位于src子目录中。您的package.json文件应该包含清单 2-30 中的main字段。

清单 2-30 。指定包的主入口点

"main": "./src/bar.js"

preferGlobal设置

有些包是打算全局安装的,但是没有办法实际执行这个意图。然而,如果用户通过包含preferGlobal字段并将其设置为true来本地安装您的模块,您至少可以生成一个警告。同样,这将而不是阻止用户执行本地安装。

依赖性

包依赖关系在package.json文件的dependencies字段中指定。这个字段是一个将包名映射到版本字符串的对象。版本字符串可以是npm理解的任何版本表达式,包括 git 和 tarball URLs。清单 2-31 显示了一个仅依赖于commander的包的dependencies字段的例子。

清单 2-31 。一个简单的dependencies字段

"dependencies": {
  "commander": "1.1.x"
}

注意commander的版本字符串使用了清单 2-31 中的x通配符。在指定模块依赖关系时,使用这种语法通常被认为是最佳实践,因为主版本和次版本更新可能表示不兼容的更改,而补丁更改通常仅表示错误修复。保持软件包更新是好的,但是只有在彻底测试之后才这样做。例如,如果在清单 2-31 中使用的版本字符串是>= 1.1.0,那么在更新到版本 1.2.0 后,程序中可能会神秘地出现 bug。为了在安装新的软件包时自动更新dependencies字段,在npm install命令后添加--save标志。因此,要在安装期间将commander添加到package.json文件中,发出命令npm install commander --save

发展依赖性

许多包都有仅用于测试和开发的依赖项。这些包不应包含在dependencies字段中。相反,将它们放在单独的devDependencies字段中。例如,mocha包是 Node 社区中常用的一个流行的测试框架。使用mocha进行测试的包应该在devDependencies字段中列出,如清单 2-32 所示。

清单 2-32 。将mocha列为发展依赖

"devDependencies": {
  "mocha": "∼1.8.1"
}

开发依赖性也可以自动添加到package.json文件中。为此,将--save-dev标志附加到npm install命令上。命令npm install mocha --save-dev就是一个例子。

可选依赖项

可选依赖项是您希望使用但不需要的包,例如,提高加密性能的模块。如果可以的话,一定要使用它。如果由于某种原因它不可用,您的应用可以依靠一个较慢的替代方案。通常,如果依赖项不可用,npm将会失败。对于可选的依赖项,npm将继续执行,尽管它们不存在。与devDependencies一样,可选的依赖项列在一个单独的optionalDependencies字段中。通过将--save-optional标志指定给npm install,可选的依赖项也可以在安装过程中自动添加到package.json文件中。

如果您选择使用可选的依赖项,您的程序仍然必须考虑到包不存在的情况。这是通过在try...catchif语句中包装对模块的引用来实现的。在清单 2-33 的例子中,commander被假定为一个可选的依赖项。由于require()函数在commander不存在时抛出异常,所以它被包装在try...catch语句中。在程序的后面,在使用之前检查commander是否有定义的值。

清单 2-33 。引用可选依赖项时使用防御性编程

var commander;

try {
  commander = require("commander");
} catch (exception) {
  commander = null;
}

if (commander) {
  // do something with commander
}

发动机

engines 字段用于指定模块使用的nodenpm的版本。引擎版本控制类似于用于依赖关系的方案。然而,最佳实践会有所不同,这取决于您是在开发独立的应用还是可重用的模块。应用应该使用保守的版本控制来确保新发布的依赖项不会引入错误。另一方面,可重用模块应该使用积极的版本控制,以确保尽可能地使用最新版本的 Node。清单 2-34 中的例子包括一个engines字段。在这个例子中,node字段使用积极的版本控制,总是选择最新的版本。同时,npm版本字符串比较保守,只允许补丁更新。

清单 2-34 。在package.json文件中定义支持的引擎版本

"engines": {
  "node": ">=0.10.12",
  "npm": "1.2.x"
}

剧本

当存在时,scripts字段包含npm命令到脚本命令的映射。脚本命令可以是任何可执行命令,在外部 shell 进程中运行。两个最常见的命令是startteststart命令启动您的应用,而test运行您的应用的一个或多个测试脚本。在清单 2-35 的例子中,start命令导致node执行文件server.jstest命令显示没有指定测试。在真实的应用中,test可能会调用mocha或其他一些测试框架。

清单 2-35 。在package.json文件中指定一个scripts字段

"scripts": {
  "start": "node server.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}

image 注意尽可能避免使用特定于平台的命令。例如,使用 Makefile 是 Unix 系统上的常见做法,但是 Windows 没有make命令。

要执行starttest命令,只需将命令名传递给npm清单 2-36 ,基于清单 2-35 中的scripts字段,显示了test命令的输出。您可以从输出中看到npm将非零退出代码视为错误并中止命令。

清单 2-36 。启动npm test命令

$ npm test

> example@0.0.0 test /home/node/example
> echo "Error: no test specified" && exit 1

\"Error: no test specified\"
npm ERR! Test failed.  See above for more details.
npm ERR! not ok code 0

请注意,您不能简单地添加任意命令并从npm调用它们。例如,发出命令npm foo将不起作用,即使您已经在scripts字段中定义了foo。还有一些命令充当钩子,在某些事件发生时执行。例如,installpostinstall命令是在使用npm install安装包之后执行的。scripts字段(见清单 2-37 )使用这些命令显示软件包安装后的消息。要获得可用脚本命令的完整列表,请发出命令npm help scripts

清单 2-37 。一些 npm 挂钩

"scripts": {
  "install": "echo \"Thank you for installing!\"",
  "postinstall": "echo \"You're welcome!\""
}

附加字段

package.json文件中通常可以找到许多其他字段。例如,您可以在homepage字段中列出项目的主页,在license字段中列出软件许可类型,在repository字段中列出项目源代码所在的存储库。如果您计划将您的模块发布到npm注册中心,那么repository字段尤其有用,因为您的模块的npm页面将包含到您的存储库的链接。此外,通过包含一个repository字段,用户可以使用命令npm repo module-name快速导航到存储库(其中module-name是您的模块的npm名称)。

只要没有命名冲突,您甚至可以添加自己的特定于应用的字段。有关package.json文件的更多信息,请发出命令npm help json

生成 package.json 文件

虽然一个package.json文件的语法并不复杂,但是它可能会很乏味并且容易出错。最困难的部分可能是记住你的包的依赖项和它们的版本。为了帮助缓解这个问题,Node 提供了npm init,这是一个命令行向导,提示您输入关键字段的值,并自动生成一个package.json文件。如果您已经有了一个package.json文件,npm init会维护它的所有信息,只添加新信息。

例如,假设您有一个名为foo-module的项目目录。在那个目录里面是foo.js,你的模块的主要入口点。您的模块只有一个依赖项,commander,它是在开发过程中安装的。此外,您还有一个测试脚本test.js,它测试您的模块。现在是创建package.json文件的时候了。发出命令npm init,逐步完成清单 2-38 中所示的向导。

清单 2-38 。使用npm init 生成一个package.json文件

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sane defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (foo-module)
version: (0.0.0) 1.0.0
description: An awesome new Node module.
entry point: (foo.js)
test command: test.js
git repository:
keywords: node, awesome, foo
author: Colin Ihrig <cjihrig@domain.com>
license: (BSD)
About to write to /home/colin/foo-module/package.json:

{
  "name": "foo-module",
  "version": "1.0.0",
  "description": "An awesome new Node module.",
  "main": "foo.js",
  "dependencies": {
    "commander": "∼1.1.1"
  },
  "devDependencies": {},
  "scripts": {
    "test": "test.js"
  },
  "repository": "",
  "keywords": [
    "node",
    "awesome",
    "foo"
  ],
  "author": "Colin Ihrig <cjihrig@domain.com>",
  "license": "BSD"
}

Is this ok? (yes)
npm WARN package.json foo-module@1.0.0 No README.md file found!

请注意,一些值,包括名称foo-module,都用括号括起来了。这些值都是npm的猜测。你可以按下Enter键来接受它们。如果您想使用自己的值,只需在按下Enter前输入即可。对于某些字段,如descriptionnpm就不提供猜测了。在这些情况下,您可以提供一个值或将该字段留空,如git repository字段所示。在向导的最后,npm显示生成的 JSON 数据。此时,要么接受建议的数据并生成package.json文件,要么中止整个过程。

最后,npm提供了一条警告消息,表明没有找到README.md文件。README.md是一个可选的推荐文件,它提供了关于你的模块的文档。.md文件扩展名表示该文件包含 降价数据。Markdown 是一种标记语言,很容易转换为 HTML,但比 HTML 更容易阅读,它是 Node 文档的天然选择,因为 GitHub 能够显示 Markdown,并且大多数 Node 项目都托管在 GitHub 上。在你的项目根目录中总是包含一个README.md文件是一个好的惯例。如果存在,文件名使用readmeFilename域在package.json文件中指定。清单 2-39 中的例子显示了一个降价文件。GitHub 上呈现的相同降价显示在图 2-1 中。关于 Markdown 语法的其他信息在网上随处可见。

清单 2-39 。使用降价语法

#Level One Heading
This test is *italicized*, while this text is **bold**.

##Level Two Heading
By combining the two, this text is ***bold and italicized***.

9781430258605_Fig02-01.jpg

图 2-1 。GitHub 上呈现的清单 2-39 的降价

一个完整的例子

这可能是查看包含依赖项的 Node 程序的完整示例的好时机。在这个例子中,我们将创建一个 Hello World 风格的程序,它将彩色文本打印到控制台。为了创建彩色文本,程序将导入一个名为colors的第三方模块。示例程序的源代码如清单 2-40 所示。将源代码添加到名为colors-test.js的文件中并保存。第一行代码使用require()函数导入colors模块。第二行将消息"Hello Node!"打印到控制台。附加到控制台消息的.rainbow使字符串中的字符以各种颜色打印出来。

清单 2-40 。使用colors模块打印彩虹文本

var colors = require("colors");

console.log("Hello Node!".rainbow);

由于colors不是核心模块,运行程序前需要安装。为此,发出命令npm install colors。安装完成后,发出命令node colors-test执行程序。您应该会在控制台上看到一条彩色的消息。如果你是团队的一员,其他人将需要运行你的代码。对于这么小的程序,只有一个依赖项,您的团队成员可以简单地将您的代码从源代码控制中签出并安装colors。然而,这种方法对于具有数十甚至数百个依赖项的大型程序来说并不真正可行。如果你想让其他人运行你的重要程序,你必须提供一个package.json文件。要生成package.json,运行npm init。逐步执行向导,根据需要输入值。(该项目的示例package.json文件如清单 2-41 所示。)您的程序现在可以只安装您的源代码、package.json文件和npm

清单 2-41 。彩虹文本程序的package.json文件

{
  "name": "colors-test",
  "version": "1.0.0",
  "description": "An example program using the colors module.",
  "main": "colors-test.js",
  "dependencies": {
    "colors": "∼0.6.0-1"
  },
  "devDependencies": {},
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "repository": "",
  "keywords": [
    "colors",
    "example"
  ],
  "author": "Colin Ihrig <cjihrig@domain.com>",
  "license": "BSD"
}

image 注意很多开发者并不将node_modules文件夹检入源代码控制。因为这个文件夹可以使用npm重新生成,所以在源代码管理中排除它可以节省空间。然而,应用开发人员应该考虑提交他们的node_modules文件夹,以避免在依赖关系引入不兼容的变更时可能出现的神秘错误。不幸的是,当应用加载到不同的机器或操作系统上时,这会带来问题。另一种方法是使用npm shrinkwrap实用程序锁定已知有效的确切模块版本。shrinkwrap不仅锁定顶层依赖关系的版本,还锁定所有依赖关系的版本(这不能通过package.json文件完成)。不用将node_modules签入源代码控制,只需运行npm shrinkwrap,并签入结果npm-shrinkwrap.json文件(与package.json在同一个目录下)。另一方面,模块开发者不应该提交他们的依赖或者使用shrinkwrap。相反,他们应该努力确保他们的代码尽可能地跨版本兼容。

模块创作

到目前为止,这一章的重点是使用现有的模块。这一节解释了模块实际上是如何产生的。在 Node 中,模块和文件是一一对应的。这意味着一个文件是一个可以使用require()导入到其他文件中的模块。为了演示这个概念,在同一个目录中创建两个文件,foo.jsbar.jsfoo.js的内容如清单 2-42 所示。该文件导入第二个文件bar.js,其内容如清单 2-43 所示。在foo.js 内部,require()的返回值保存在变量bar中,打印到控制台。

清单 2-42foo.js的内容,导入文件bar.js

var bar = require("./bar");

console.log(bar);

bar.js内部,定义了一个名为bar()的函数。该模块包含两个打印语句,一个在模块级,另一个在bar()函数 中。

清单 2-43 。在清单 2-42 中导入的bar.js的内容

function bar() {
  console.log("Inside of bar() function");
}

console.log("Inside of bar module");

要运行这个示例,发出命令node foo.js。结果输出如清单 2-44 所示。对foo.js中的require()的调用导入了bar.js,这导致第一条消息被打印出来。接下来,打印bar变量,显示一个空对象。基于这个例子,有两个问题需要回答。第一,空的对象到底是什么?第二,如何从bar.js外部调用bar()函数。

清单 2-44 。运行清单 2-42 中的代码的输出

$ node foo.js
Inside of bar module
{}

module物体

Node 在每个代表当前模块的文件中提供了一个自由变量modulemodule是包含名为exports的属性的对象,默认为空对象。exports的值由require()函数返回,定义了一个模块的公共接口。由于exports清单 2-43 中从未被修改,这解释了在清单 2-44 中看到的空对象。

为了使bar()函数在bar.js之外可用,我们有两种选择。首先,bar可以被分配给bar.js内部的module.exports(如清单 2-45 所示)。请注意,exports对象已经被一个函数覆盖。

清单 2-45 。重写bar.js以导出bar()

module.exports = function bar() {
  console.log("Inside of bar() function");
}

console.log("Inside of bar module");

foo.js然后可以访问bar()功能,如清单 2-46 所示。因为bar变量现在指向一个函数,所以可以直接调用它。

清单 2-46 。重写foo.js以从清单 2-45 中访问bar()

var bar = require("./bar");

console.log(bar);
bar();

这种方法的缺点是bar模块只能导出bar()函数。第二种选择是简单地将bar()函数附加到现有的exports对象上,如清单 2-47 所示。这种技术允许模块导出任意数量的方法和属性。为了适应这种变化,foo.js将访问bar()函数作为bar.bar()

清单 2-47 。通过扩充现有的exports对象导出bar()

module.exports.bar = function bar() {
  console.log("Inside of bar() function");
}

console.log("Inside of bar module");

module对象提供了其他几个不常用的属性。这些属性总结在表 2-2 中。

表 2-2 。模块对象的附加属性

|

财产

|

描述

| | --- | --- | | id | 模块的标识符。通常这是模块的完全解析文件名。 | | filename | 模块的完全解析文件名。 | | loaded | 表示模块状态的布尔值。如果模块已经完成加载,这将是true。否则就是false。 | | parent | 一个对象,表示加载当前模块的模块。 | | children | 表示由当前模块导入的模块的对象数组。 |

发布到npm

为了将您的模块发布到npm,您必须首先创建一个npm用户帐户。清单 2-48 展示了建立一个npm账户所需的命令。前三个命令用于关联您的个人信息。最后一个命令npm adduser,将提示您输入用户名并创建一个npm账户(假设用户名可用)。帐户创建后,用户发布的模块可以在https://npmjs.org/∼username查看。

清单 2-48 。创建 npm 用户帐户

npm set init.author.name "John Doe"
npm set init.author.email "john@domain.com"
npm set init.author.url "http://www.johnspage.com"
npm adduser

在设置了一个npm帐户之后,您必须为您的模块创建一个package.json文件。本章已经介绍了这样做的过程。最后,发出命令npm publish来基于package.json文件创建一个npm条目。

摘要

这一章已经涵盖了大量的材料——这是必须的。开发 Node 应用的很大一部分是使用npm和第三方包。从本章开始,你应该已经很好地掌握了npmrequire()函数、package.json文件和模块创作。虽然整个软件包系统不能在一章中全面介绍,但是你现在应该知道足够的知识来完成本书的其余部分。通过阅读在线文档来填补知识上的空白。