NodeJS-入门指南-一-

128 阅读36分钟

NodeJS 入门指南(一)

原文:Beginning Node.js

协议:CC BY-NC-SA 4.0

零、简介

毫无疑问,个人计算已经彻底改变了我们今天的生活和工作方式。网络进一步革新了我们使用应用的方式。当它被首次引入时,互联网被设计成以文档的形式呈现信息。后来,JavaScript 被加入进来,这是我们今天在网络上看到的创新的关键成分。Web 应用独立于平台,无缝更新,默认安全,随时随地可用。毫无疑问,如果不了解 Web 是如何工作的,那么很难开始开发人员的角色。

由于 web 的重要性以及 JavaScript 在 Web 开发中扮演的关键角色,您可以在一些开源 JavaScript 项目中找到大多数技术问题的解决方案。Node.js 允许您在服务器上使用所有这些创新的 JavaScript 项目,就像在客户端浏览器上一样。在服务器上使用 JavaScript 还可以减少在您更改编程语言和相关代码约定时需要在大脑中进行的上下文切换。这就是为什么你应该使用 Node.js 的情感方面。

这本书旨在温和地介绍 Node.js 和 JavaScript。除了基本的编程课程之外,没有软件开发经验。由于我们清楚地展示了创建 Node.js 背后的技术原因,如果您已经习惯于在另一种环境(如 C#或 Java)中编程,并且对围绕 Node.js 的所有大惊小怪感到好奇,那么这本书也很棒。这本书涵盖了 Node.js 软件开发的所有主要领域,从设置到部署,因此当您完成这本书时,您应该能够立即开始使用 Node.js,并准备好与世界共享您的项目。

一、设置 Node.js 开发环境

在本章中,我们将指导您在各种平台上安装 Node.js,并讨论如何设置 Node.js 开发环境。然后,我们将带您浏览 Node.js REPL(读取-评估-打印-循环),并向您展示如何运行 Node.js 应用。最后,我们提供了集成开发环境(IDEs) 的例子,可以帮助您更快地交付应用,让您的旅程更加愉快。

安装 Node.js

开发 Node.js 应用不再需要从源代码构建 Node.js。Node.js 现在提供了 Windows 和 Mac OS X 的安装程序,它可以像这些平台上的任何其他应用一样安装(图 1-1 )。你可以从http://nodejs.org/download/下载 Node.js 安装程序。

9781484201886_Fig01-01.jpg

图 1-1 。列出安装程序的 Node.js 下载页面

在下一节中,我们将指导您完成操作系统(OS)的重要步骤。您可以安全地跳过与您当前操作系统无关的部分。

在 Windows 上安装

Node.js 的网站列出了“Windows 二进制(.exe)”和“Windows Installer(。msi)。”你不希望使用 windows 二进制(.exe)进行开发,因为它不包含重要的东西,如 Node 包管理器(NPM) ,我们将在第四章中介绍。Node.js 为 32 位和 64 位 Windows 提供了单独的安装程序(.msi)。我们建议您根据您的平台安装。您可以像在 Windows 上启动任何其他安装程序一样启动安装程序(图 1-2 )。

9781484201886_Fig01-02.jpg

图 1-2 。Windows 上的 Node.js 安装向导

首次启动时,我们建议您使用默认选项安装到默认目录。尤其重要的是你要让安装程序添加到路径 ( 图 1-3 )

9781484201886_Fig01-03.jpg

图 1-3 。Windows 上 Node.js 安装程序的默认选项

安装后,卸载和重新安装 Node.js 极其容易。如果再次运行安装程序,会提示移除选项,如图图 1-4 所示。

9781484201886_Fig01-04.jpg

图 1-4 。Windows 的 Node.js 卸载程序

由于安装程序设置了系统路径,所以可以从命令提示符运行 Node.js(在 Windows 开始菜单中搜索“命令提示符”)。我们只需在 cmd ( 图 1-5 )中输入node就可以启动 Node.js。这将使您进入 REPL,我们将在下一节对此进行解释。

9781484201886_Fig01-05.jpg

图 1-5 。从命令行运行 Node.js

在 Mac OS X 上安装

http://nodejs.org/download/下载 Node.js 团队提供的 Mac OS X 安装程序。安装程序是一个可以从 Finder 启动的.pkg文件(图 1-6 )。

9781484201886_Fig01-06.jpg

图 1-6 。Mac OS X 的 Node.js 安装程序

开始时,坚持默认设置并为所有用户安装(图 1-7 )。

9781484201886_Fig01-07.jpg

图 1-7 。Node.js 为所有用户设置选项

一旦完成,安装程序会通知你它安装了两个二进制文件(nodenpm),如图图 1-8 所示。

9781484201886_Fig01-08.jpg

图 1-8 。已安装 Node.js 二进制文件

我们将在第四章的中详细介绍npm。你在 Node.js 中运行 JavaScript 的主可执行文件是node ( 图 1-9 )。对于 Mac OS X,您可以从终端启动node(使用 Mac OS X spotlight 搜索终端)。如果您在终端中执行node,它将启动 Node.js REPL,我们接下来将讨论它。

9781484201886_Fig01-09.jpg

图 1-9 。在 Mac OS X 上从命令行运行 Node.js

使用 REPL

Node.js 为您提供了一个 REPL(read-evaluate-print-loop),这样您就可以测试任意的 JavaScript,并试验和探索您试图解决的问题的解决方案。当您不带任何命令行参数运行node时,它会将您置于 REPL。要查看您可用的选项,如图 1-10 中的所示,键入.help并按回车键。

9781484201886_Fig01-10.jpg

图 1-10 。Node.js REPL 帮助

你可以在 REPL 中执行任意的 JavaScript 并立即看到它的结果,如图图 1-11 所示。

9781484201886_Fig01-11.jpg

图 1-11 。在 Node.js REPL 中执行 JavaScript

在每一步,REPL 打印最后执行的语句的结果。REPL 不会执行您的输入代码,直到所有的括号都被平衡。要执行多行,只需用括号将它们括起来。REPL 使用(…)来表示它在执行之前正在等待完整的代码。只需关闭括号并按下回车键,REPL 就可以计算输入的 JavaScript(见图 1-12 )。从块内部退出(...)无需执行您已经输入的内容,只需键入.break或按 Ctrl+C。

9781484201886_Fig01-12.jpg

图 1-12 。在 Node.js REPL 中执行多行

当您想要测试一些 JavaScript 并确保它按照您想要的方式运行时,REPL 非常有用。您可以通过键入.exit(或按 Ctrl+D)退出 REPL。

执行 Node.js 脚本

我们已经看到了如何通过在 REPL 中键入 JavaScript 来执行它。然而,你最常见的是编写 Node.js 程序(脚本文件)并使用 Node.js 执行它们。你可以在 Node.js 中执行一个 JavaScript 源文件,只需在命令行上将文件传递给node(图 1-13)。创建一个名为helloworld.js的新文件,包含一个简单的console.log,如清单 1-1 所示。

9781484201886_Fig01-13.jpg

图 1-13 。在 Node.js 中执行脚本文件

清单 1-1 。helloworld.js

console.log("hello world!");

然后从保存文件的同一个目录中通过在命令行上运行node helloworld.js来运行文件(在我们的例子中是C:\)。

注意,我们使用console.log的方式和我们做前端 web 开发时使用的方式一样。Node.js 的一个理念是,对于前端开发者来说应该是直观的。Node.js 团队试图在任何有意义的时候保持 API 与浏览器的一致性。

Node.js 只是像浏览器一样从上到下执行输入的 JavaScript。然而,通常将应用的主文件命名为app.js,这样人们就知道为了运行应用应该执行哪个文件。

建立集成开发环境

Node.js 非常棒,因为只需一个文本编辑器和终端就能轻松上手。(这并不意味着没有更全功能的开发环境。)Node.js 从 JetBrains(IntelliJ Idea、RubyMine 和 PyCharm 的创造者)的 WebStorm 以及微软的 Visual Studio 中获得了巨大的支持。WebStorm 可以在 Windows、Mac OS X 和 Linux 上使用,而 Visual Studio 只能在 Windows 上使用。

WebStorm Node.js 支持

WebStorm 自称是“最智能的 JavaScript IDE”。它基于 IntelliJ IDEA 平台,如果您是 Java、Ruby 或 Python 背景的人,可能很容易迁移到这个平台。从http://www.jetbrains.com/webstorm/就可以得到。

WebStorm 使用“项目”的概念工作当您启动 WebStorm 时,您会看到一个选项,创建一个新项目。对于这个例子,我们将创建一个简单的空项目(图 1-14 )。

9781484201886_Fig01-14.jpg

图 1-14 。在 WebStorm 中新建一个项目

打开项目窗口后,右键单击项目名称(如图图 1-15 所示)。添加一个新的 JavaScript 文件,并将该文件命名为“main”(也如图 1-15 所示)。

9781484201886_Fig01-15.jpg

图 1-15 。给 WebStorm 项目添加一个新文件

清除文件的内容并简单地放入一个console.log,如清单 1-2 所示。

清单 1-2

console.log("Hello WebStorm!");

因为我们已经安装了 Node.js,所以 WebStorm 足够聪明来解决这个问题。所以,如果你右击文件内的任何地方,WebStorm 会显示选项运行‘main . js’(图 1-16 )。

9781484201886_Fig01-16.jpg

图 1-16 。从 WebStorm 运行 Node.js 中的脚本文件

如果选择该选项,WebStorm 会启动 Node.js,将该文件作为参数传入,并显示输出,如图图 1-17 所示。

9781484201886_Fig01-17.jpg

图 1-17 。脚本执行导致网络风暴

当您要求 WebStorm 运行该文件时,它实际上创建了一个运行配置。您可以查看该运行配置,并通过使用运行image编辑配置对其进行进一步定制,如图图 1-18 所示。

9781484201886_Fig01-18.jpg

图 1-18 。在 WebStorm 中编辑运行配置

这将打开配置编辑器对话框,如图图 1-19 所示。您可以看到为您创建的配置,并根据需要进行编辑。

9781484201886_Fig01-19.jpg

图 1-19 。WebStorm 中的 Node.js 配置选项

WebStorm 比我们在这里展示的容量更大,我们在这里展示的目的是让您快速入门。WebStorm 与 Node.js 内置调试器有很好的集成,将在第十一章中探讨。

Visual Studio Node.js 支持

如果你来自一个.NET背景,你可能会很高兴听到 Visual Studio 有一流的 Node.js 支持。这种支持以“Node.js Tools for Visual Studio”的形式提供,适用于微软的 Visual Studio 2012 和 Visual Studio 2013。你可以从https://nodejstools.codeplex.com下载这些工具。安装这些工具再简单不过了。只需启动下载的.msi安装程序,点击完成。

现在,当您启动 Visual Studio 并创建一个新项目时,您会看到一个新的语言选项, JavaScript 。选择它并创建一个Blank Node.js Console App,指定其名称和位置,如图 1-20 中的所示。

9781484201886_Fig01-20.jpg

图 1-20 。创建一个新 NodeT3。使用 Visual Studio 的 js 项目

应用创建完成后,Visual Studio 打开 app.js ,如图图 1-21 所示。

9781484201886_Fig01-21.jpg

图 1-21 。使用 Visual Studio 创建的 Node.js 应用

此时不要担心 package.jsonnpm 。这些选项将在第四章中解释。现在让我们从 Visual Studio 运行这个简单的控制台应用。点击编辑器侧边栏添加调试断点,如图图 1-22 所示。

9781484201886_Fig01-22.jpg

图 1-22 。在 Visual Studio 中向文件添加调试断点

要在调试模式下运行该应用,请按 F5,Visual Studio 会将 app.js 传递给 Node.js,并在断点处暂停,如图 1-23 所示。Visual Studio 使用 Node.js 内置的 V8 调试器,我们将在第十一章中讨论。

9781484201886_Fig01-23.jpg

图 1-23 。在 Visual Studio 中激活断点 ??

Visual Studio 中的所有常见调试工具,如调用堆栈、局部变量和 watch,都可以很好地与 Node.js 一起工作。您甚至可以在 Node.js 的“内部”看到源代码。例如,图 1-24 中的调用堆栈中显示的 module.js 是 Node.js 的一部分,而不是我们的应用。

9781484201886_Fig01-24.jpg

图 1-24 。Visual Studio 显示局部变量和调用堆栈

按 F5 继续。然后将“Hello world”打印到控制台并退出(图 1-25 )。

9781484201886_Fig01-25.jpg

图 1-25 。从 Visual Studio 执行的 Node.js 应用

使用 Visual Studio 时需要注意的最后一件事是属性窗格。您可以在解决方案资源管理器中右键单击项目,选择属性,修改 Visual Studio 与node.exe的交互方式,如图图 1-26 所示。

9781484201886_Fig01-26.jpg

图 1-26 。Visual Studio 中的 Node.js 配置选项

摘要

Node.js 从一开始就获得了极好的社区支持。感谢安装程序,您不再需要从源代码编译 Node.js 来在您喜欢的平台上创建 Node.js 应用。在设置了 Node.js 之后,我们展示了一些 ide 示例,它们可以使使用 Node.js 变得更加容易,从而使您能够快速启动并运行。

在下一章中,我们将讨论为了成功使用 Node.js 你需要理解的重要 JavaScript 概念。

二、了解 Node.js

要理解 Node.js 是如何工作的,首先需要理解 JavaScript 的一些关键特性,这些特性使它非常适合服务器端开发。JavaScript 是一种简单的语言,但它也非常灵活。这种灵活性是它经受住时间考验的原因。一流的函数和闭包使它成为 web 应用的理想语言。

JavaScript 有一个不可靠的坏名声。然而,这种想法与事实相去甚远。实际上,JavaScript 的坏名声来自于 DOM 的不可靠性。DOM(文档对象模型)是浏览器供应商提供的使用 JavaScript 与浏览器交互的 API(应用编程接口)。不同浏览器厂商的 DOM 各不相同。然而,JavaScript 这种语言是定义良好的,可以跨浏览器和 Node.js 可靠地使用。在本章中,我们将讨论 JavaScript 的一些基础知识,然后讨论 Node.js 如何使用 JavaScript 为 web 应用提供高性能的平台。其他人抱怨 JavaScript 如何处理编程错误(它试图让无效代码工作)。然而,在这种情况下,开发人员确实应该受到责备,因为他们在使用高度动态的语言时需要小心。

变量

变量是在 JavaScript 中使用关键字var定义的。例如,下面的代码段创建了一个变量foo,并将其记录到控制台。(参见清单 2-1 。)正如您在上一章中看到的,您将使用node variable.js从您的控制台(Mac OS X 上的终端和 Windows 上的 cmd)运行这段代码。

**清单 2-1 。**变量. js

var foo = 123;
console.log(foo); // 123

JavaScript 运行时(浏览器或 Node.js)有机会定义一些我们可以在代码中使用的全局变量。其中之一是console对象,到目前为止我们一直在使用它。console对象包含一个成员函数(log),它接受任意数量的参数并将它们打印到控制台。我们将在使用全局对象时讨论更多的全局对象。正如您将看到的,JavaScript 包含了您期望一个好的编程语言拥有的大部分东西。

数字

JavaScript 中的所有数字都有相同的浮点数类型。算术运算(+、-、*、/、%)如您所料对数字起作用,如清单 2-2 所示。

清单 2-2 。 numbers.js

var foo = 3;
var bar = 5;
console.log(foo + 1); // 4
console.log(foo / bar); // 0.6
console.log(foo * bar); // 15
console.log(foo - bar); // -2;
console.log(foo % 2); // remainder: 1

布尔值

为布尔值定义了两个字面值:truefalse。您可以将这些赋值给变量,并按预期对它们应用布尔运算。(参见清单 2-3 。)

**清单 2-3 。**布尔型. js

var foo = true;
console.log(foo); // true

// Boolean operations (&&, ||, !) work as expected:
console.log(true && true); // true
console.log(true && false); // false
console.log(true || false); // true
console.log(false || false); // false
console.log(!true); // false
console.log(!false); // true

数组

您可以使用[]在 JavaScript 中非常容易地创建数组。数组有许多有用的功能,其中一些在清单 2-4 中显示。

清单 2-4 。 arrays.js

var foo = [];

foo.push(1); // add at the end
console.log(foo); // prints [1]

foo.unshift(2); // add to the top
console.log(foo); // prints [2,1]

// Arrays are zero index based:
console.log(foo[0]); // prints 2

对象文字

通过解释这几个基本类型,我们已经向您介绍了对象文字。在 JavaScript 中创建对象最常见的方式是使用对象符号{}。对象可以在运行时任意扩展。清单 2-5 给出了一个例子。

清单 2-5 。 objectLiterals1.js

var foo = {};
console.log(foo); // {}
foo.bar = 123; // extend foo
console.log(foo); // { bar: 123 }

不用在运行时扩展它,你可以通过使用清单 2-6 中的对象文字符号来定义对象的属性

清单 2-6 。 objectLiterals2.js

var foo = {
    bar: 123
};
console.log(foo); // { bar: 123 }

您还可以在对象文字中嵌套对象文字,如清单 2-7 所示。

清单 2-7 。 objectLiterals3.js

var foo = {
    bar: 123,
    bas: {
        bas1: 'some string',
        bas2: 345
    }
};
console.log(foo);

当然,你也可以在对象文字中包含数组,如清单 2-8 所示。

清单 2-8 。 objectLiterals4.js

var foo = {
    bar: 123,
    bas: [1, 2, 3]
};
console.log(foo);

而且,你也可以让这些数组本身包含对象文字,正如你在清单 2-9 中看到的。

清单 2-9 。 objectLiterals5.js

var foo = {
    bar: 123,
    bas: [{
        qux: 1
    },
    {
        qux: 2
    },
    {
        qux: 3
    }]
};
console.log(foo.bar); // 123
console.log(foo.bas[0].qux); // 1
console.log(foo.bas[2].qux); // 2

对象文字作为函数参数和返回值非常方便。

功能

JavaScript 中的函数非常强大。JavaScript 的大部分能力来自于它处理函数类型的方式。我们将在后面更复杂的例子中研究 JavaScript 中的函数。

职能 101

JavaScript 中的一个普通函数结构在清单 2-10 中定义。

清单 2-10 。。职能机构。射流研究…

function functionName() {
    // function body
    // optional return;
}

JavaScript 中的所有函数都返回值。在没有显式 return 语句的情况下,函数返回undefined。当您执行清单 2-11 中的代码时,您会在控制台上看到undefined

清单 2-11 。 functionReturn.js

function foo() { return 123; }
console.log(foo()); // 123

function bar() { }
console.log(bar()); // undefined

我们将在本章讨论默认值时更多地讨论undefined函数。

立即执行功能

定义一个函数后,您可以立即执行它。简单地将函数放在括号()中并调用它,如清单 2-12 所示。

清单 2-12 。 ief1.js

(function foo() {
    console.log('foo was executed!');
})();

使用立即执行函数的原因是为了创建一个新的变量范围。在 JavaScript 中,ifelsewhile不会创建新的变量范围。这个事实在清单 2-13 中得到了证明。

清单 2-13 。 ief2.js

var foo = 123;
if (true) {
    var foo = 456;
}
console.log(foo); // 456;

在 JavaScript 中创建新变量作用域的唯一推荐方法是使用函数。因此,为了创建一个新的变量范围,我们可以使用一个立即执行的函数,如清单 2-14 所示。

清单 2-14 。 ief3.js

var foo = 123;
if (true) {
    (function () { // create a new scope
        var foo = 456;
    })();
}
console.log(foo); // 123;

注意,我们选择避免不必要的函数命名。这被称为一个匿名函数,我们将在下面解释。

匿名函数

没有名字的函数叫做匿名函数。在 JavaScript 中,您可以将函数赋给变量。如果你打算用一个函数作为变量,你不需要给函数命名。清单 2-15 展示了两种内联定义函数的方法。这两种方法是等效的。

**清单 2-15 。**无名氏

var foo1 = function namedFunction() { // no use of name, just wasted characters
    console.log('foo1');
}
foo1(); // foo1

var foo2 = function () { // no function name given i.e. anonymous function
    console.log('foo2');
}
foo2(); // foo2

如果一个函数可以像该语言中的任何其他变量一样被处理,则称该语言具有一级函数。JavaScript 有一流的功能。

高阶函数

因为 JavaScript 允许我们将函数赋给变量,所以我们可以将函数传递给其他函数。以函数为自变量的函数称为高阶函数。高阶函数的一个非常常见的例子是setTimeout。这显示在清单 2-16 中。

清单 2-16 。 higherOrder1.js

setTimeout(function () {
    console.log('2000 milliseconds have passed since this demo started');
}, 2000);

如果您在 Node.js 中运行这个应用,您将在两秒钟后看到console.log消息,然后应用将退出。注意,我们提供了一个匿名函数作为setTimeout的第一个参数。这使得setTimeout成为一个高阶函数。

值得一提的是,没有什么可以阻止我们创建一个函数并将其传入。清单 2-17 中显示了一个例子。

清单 2-17 。 higherOrder2.js

function foo() {
    console.log('2000 milliseconds have passed since this demo started');
}
setTimeout(foo, 2000);

既然我们已经对对象文字和函数有了明确的理解,我们就可以研究闭包的概念了。

关闭

每当我们在一个函数中定义了另一个函数时,内部函数就可以访问外部函数中声明的变量。闭包最好用例子来解释。

在清单 2-18 中,你可以看到内部函数从外部作用域访问变量(variableInOuterFunction)。外部函数中的变量已被内部函数封闭(或绑定)。因此有了关闭 ?? 的说法。这个概念本身足够简单,也相当直观。

清单 2-18 。 closure1.js

function outerFunction(arg) {
    var variableInOuterFunction = arg;

    function bar() {
        console.log(variableInOuterFunction); // Access a variable from the outer scope
    }

    // Call the local function to demonstrate that it has access to arg
    bar();
}

outerFunction('hello closure!'); // logs hello closure!

现在最棒的部分是:内部函数可以从外部作用域访问变量,即使外部函数已经返回了。这是因为变量仍然绑定在内部函数中,不依赖于外部函数。清单 2-19 。显示了一个示例。

清单 2-19 。 closure2.js

function outerFunction(arg) {
    var variableInOuterFunction = arg;
    return function () {
        console.log(variableInOuterFunction);
    }
}

var innerFunction = outerFunction('hello closure!');

// Note the outerFunction has returned
innerFunction(); // logs hello closure!

现在我们已经了解了一流的函数和闭包,我们可以看看是什么让 JavaScript 成为服务器端编程的伟大语言。

了解 Node.js 性能

Node.js 专注于创建高性能的应用。在下一节中,我们将介绍 I/O 伸缩问题。然后我们展示传统上是如何解决的,接着是 Node.js 是如何解决的。

I/O 扩展问题

Node.js 致力于成为编写高性能 web 应用的最佳方式。为了理解它是如何实现的,我们需要了解 I/O 伸缩问题。让我们根据 CPU 周期来粗略估计一下从不同来源访问数据的速度(图 2-1 )。

9781484201886_Fig02-01.jpg

图 2-1 。比较常见的 I/O 源

您可以清楚地看到,磁盘和网络访问与访问 RAM 和 CPU 缓存中的数据完全不同。

大多数 web 应用依赖于从磁盘或其他网络来源(例如,数据库查询)读取数据。当收到一个 HTTP 请求,我们需要从数据库加载数据时,通常这个请求会等待磁盘读取或网络访问调用完成。

这些打开的连接和挂起的请求会消耗服务器资源(内存和 CPU)。为了使用同一个 web 服务器处理来自不同客户端的大量请求,我们遇到了 I/O 伸缩问题。

传统的 Web 服务器对每个请求使用一个进程

传统的服务器过去常常启动一个新的进程来处理每一个 web 请求。为每个请求旋转一个新的进程是一项开销很大的操作,在 CPU 和内存方面都是如此。这是 PHP 等技术最初创建时的工作方式。

这一概念的演示如图 2-2 所示。为了成功回复 HTTP 请求“A”,我们需要来自数据库的一些数据。这种读取可能需要很长时间。在整个读取期间,我们将有一个进程在空闲和等待数据库响应时占用 CPU 和内存。此外,进程启动缓慢,并且在 RAM 空间方面有很大的开销。这不能长期扩展,这就是为什么现代 web 应用使用线程池的原因。

9781484201886_Fig02-02.jpg

图 2-2 。使用进程的传统 web 服务器

使用线程池的传统 Web 服务器

现代服务器使用线程池中的一个线程来服务每个请求。因为我们已经创建了一些操作系统(OS)线程(因此有一个线程池),所以我们不需要为启动和停止 OS 进程付出代价(创建 OS 进程的成本很高,并且占用的内存比线程多得多)。当请求进来时,我们分配一个线程来处理这个请求。在处理请求的整个过程中,该线程都是为请求保留的,如图 2-3 所示。

9781484201886_Fig02-03.jpg

图 2-3 。使用线程池的传统 web 服务器

因为我们节省了每次创建新进程的开销,而且线程比进程轻,所以这种方法比原来的服务器设计好得多。几年前,大多数 web 服务器都使用这种方法,并且今天仍在使用。然而,这种方法并非没有缺点。同样,线程之间存在内存浪费。此外,操作系统需要在线程之间进行上下文切换(即使当它们空闲时),这导致了 CPU 资源的浪费。

Nginx 方式

我们已经看到,创建单独的进程和单独的线程来处理请求会导致操作系统资源的浪费。Node.js 的工作方式是只有一个线程处理请求。对于 Node.js 来说,单线程服务器的性能优于线程池服务器的想法并不新鲜。

Nginx 是一个单线程 web 服务器,可以处理大量并发请求。一个比较 Nginx 和 Apache 的简单基准,两者都服务于文件系统中的一个静态文件,如图 2-4 所示。

9781484201886_Fig02-04.jpg

图 2-4 。Nginx 与 Apache 每秒请求数和并发打开连接数的比较

如您所见,当并发连接数增加时,Nginx 每秒可以处理比 Apache 多得多的请求。更有趣的是内存消耗,如图图 2-5 。

9781484201886_Fig02-05.jpg

图 2-5 。Nginx 与 Apache 内存使用量和并发连接数

随着并发连接越来越多,Apache 需要管理更多的线程,因此消耗更多的内存,而 Nginx 保持在一个稳定的水平。

Node.js 性能秘密

JavaScript 中只有一个执行线程。这是网络浏览器传统的工作方式。如果您有一个长时间运行的操作(例如等待计时器完成或数据库查询返回),您必须使用回调继续操作。清单 2-20 。提供了一个简单的演示程序,它使用 JavaScript runtime setTimeout函数来模拟一个长时间运行的操作。您可以使用 Node.js 运行这段代码。

清单 2-20 。 simulateUserClick.js

function longRunningOperation(callback) {
    // simulate a 3 second operation
    setTimeout(callback, 3000);
}

function userClicked() {
    console.log('starting a long operation');
    longRunningOperation(function () {
        console.log('ending a long operation');
    });
}
// simulate a user action
userClicked();

这种模拟在 JavaScript 中是可能的,因为我们有一级函数和传递函数——回调是该语言中一种受良好支持的模式。当您将一流的函数与闭包的概念结合起来时,事情就变得有趣了。假设我们正在处理一个 web 请求,并且我们有一个长时间运行的操作,比如我们需要做的数据库查询。清单 2-21 中显示了一个模拟版本。

清单 2-21 。 simulateWebRequest.js

function longRunningOperation(callback) {
    // simulate a 3 second operation
    setTimeout(callback, 3000);
}

function webRequest(request) {
    console.log('starting a long operation for request:', request.id);
    longRunningOperation(function () {
        console.log('ending a long operation for request:', request.id);
    });
}
// simulate a web request
webRequest({ id: 1 });
// simulate a second web request
webRequest({ id: 2 });

在清单 2-21 中,由于闭包,我们可以在长时间运行的操作完成后访问正确的用户请求。我们刚刚毫不费力地在单线程上处理了两个请求。现在你应该明白下面这句话了:“Node.js 是高性能的,它使用 JavaScript 是因为 JavaScript 支持一流的函数和闭包。”

当有人告诉你你只有一个单线程来处理请求时,你应该立即想到的问题是,“但是我的计算机有一个四核 CPU。只使用单线程肯定会浪费资源。”答案是肯定的。然而,有一个很好的解决方法,我们将在第十三章讨论部署和可伸缩性时研究它。简单提示一下您将会看到什么:使用 Node.js,通过为每个 CPU 内核使用单独的 JavaScript 进程来使用所有 CPU 内核实际上非常简单。

还需要注意的是,Node.js 在 C 层管理一些线程(比如某些文件系统操作),但是所有的 JavaScript 都在一个线程中执行。这为您提供了 JavaScript 几乎完全拥有至少一个线程的性能优势。

更多 Node.js 内部信息

理解 Node.js 的内部工作原理并不十分重要,但是当您与同行讨论 Node.js 时,更多的讨论会让您更加了解这些术语。Node.js 的核心是一个事件循环

事件循环使任何 GUI 应用都能在任何操作系统上工作。当有事情发生时(例如,用户点击一个按钮),操作系统调用应用中的一个函数,然后应用执行这个函数中包含的逻辑直到完成。之后,您的应用准备好响应可能已经到达(并且在队列中)或者可能稍后到达(基于用户交互)的新事件。

线程饥饿

通常,在 GUI 应用中从一个事件调用一个函数的过程中,不会处理其他事件。因此,如果您在点击处理程序中执行长时间运行的任务,GUI 将变得没有响应。这是我遇到的每个电脑用户都曾经历过的事情。这种 CPU 资源可用性的缺乏被称为饥饿

Node.js 构建在与 GUI 程序相同的事件循环原则上。因此,它也会挨饿。为了更好地理解它,我们来看几个代码示例。清单 2-22 。展示了一小段使用console.timeconsole.timeEnd函数测量时间的代码。

清单 2-22 。 timeit.js

console.time('timer');
setTimeout(function(){
   console.timeEnd('timer');
},1000)

如果您运行这段代码,您应该会看到一个非常接近您预期的数字—换句话说,1000 毫秒。这个超时回调是从 Node.js 事件循环中调用的。

现在让我们写一些需要很长时间来执行的代码,例如,计算第 n 个斐波那契数的非优化方法,如清单 2-23 所示。

**清单 2-23 。**大型操作. js

console.time('timeit');
function fibonacci(n) {
    if (n < 2)
        return 1;
    elses
        return fibonacci(n - 2) + fibonacci(n - 1);
}
fibonacci(44); // modify this number based on your system performance
console.timeEnd('timeit'); // On my system it takes about 9000ms (i.e. 9 seconds)

现在我们有了一个可以从 Node.js 事件循环中引发的事件(set Timeout)和一个可以让 JavaScript 线程保持忙碌的函数(fibonacci)。我们现在可以在 Node.js 中演示饥饿。但是在这个超时完成之前,我们执行了一个占用大量 CPU 时间的函数,因此占用了 CPU 和 JavaScript 线程。由于这个函数被 JavaScript 线程占用,事件循环不能调用其他任何东西,因此超时被延迟,如清单 2-24 所示。

清单 2-24 。 starveit.js

// utility funcion
function fibonacci(n) {
    if (n < 2)
        return 1;
    else
        return fibonacci(n - 2) + fibonacci(n - 1);
}

// setup the timer
console.time('timer');
setTimeout(function () {
    console.timeEnd('timer'); // Prints much more than 1000ms
}, 1000)

// Start the long running operation
fibonacci(44);

这里的一个教训是,如果您有一个高 CPU 任务,需要在多客户机服务器环境中对客户机请求执行*,Node.js 不是最佳选择。然而,如果是这样的话,你将很难在任何平台上找到一个可扩展的软件解决方案。大多数高 CPU 任务应该脱机执行,通常使用物化视图、map reduce 等将任务卸载到数据库服务器。大多数 web 应用通过网络访问这些计算的结果,这就是 Node.js 的亮点——事件网络 I/O。*

现在,您已经理解了事件循环的含义以及 Node.js 的 JavaScript 部分是单线程的这一事实的含义,让我们再来看看为什么 Node.js 对 I/O 应用非常有用。

数据密集型应用

Node.js 非常适合数据密集型应用。正如我们所看到的,使用单线程意味着 Node.js 在用作 web 服务器时占用的内存非常少,并且可以处理更多的请求。考虑一个数据密集型应用的简单场景,该应用通过 HTTP 从数据库向客户端提供数据集。我们知道,与执行代码和/或从 RAM 中读取数据相比,收集响应客户端查询所需的数据需要很长时间。图 2-6 显示了一个传统的带有线程池的 web 服务器在响应两个请求时的样子。

9781484201886_Fig02-06.jpg

图 2-6 。传统服务器如何处理两个请求

Node.js 中相同的服务器如图图 2-7 所示。所有的工作都将在一个线程中进行,这导致了更少的内存消耗,并且由于缺少线程上下文切换,CPU 负载也更少。就实现而言,handleClientRequest是一个调用数据库的简单函数(使用回调)。当回调返回时,它使用用 JavaScript 闭包捕获的请求对象来完成请求。清单 2-25 中的伪代码显示了这一点。

9781484201886_Fig02-07.jpg

图 2-7 。Node.js 服务器如何处理两个请求

清单 2-25 。 handleClientRequest.js

function handleClientRequest(request) {
    makeDbCall(request.someInfo, function (result) {
        // The request corresponds to the correct db result because of closure
        request.complete(result);
    });
}

注意,对数据库的 HTTP 请求也是由事件循环管理的。拥有异步 IO 的优势以及 JavaScript + Node.js 非常适合数据密集型应用的原因现在应该很清楚了。

V8 JavaScript 引擎

值得一提的是,Node.js 内部的所有 JavaScript 都是由 V8 JavaScript 引擎执行的。V8 伴随着谷歌 Chrome 项目应运而生。V8 是 Chrome 的一部分,当你访问一个网页时,它运行 JavaScript。

任何做过网络开发的人都知道谷歌 Chrome 对网络来说有多神奇。浏览器使用统计非常清楚地反映了这一点。据 w3schools.org 称,访问他们网站的近 56%的互联网用户现在都在使用谷歌浏览器。这有很多原因,但 V8 和它的速度是一个非常重要的因素。除了速度之外,使用 V8 的另一个原因是谷歌的工程师使它易于集成到其他项目中,并且它是独立于平台的。

更多 JavaScript

现在我们已经理解了使用 Node.js 的动机,让我们更深入地研究 JavaScript,这样我们就可以编写可维护的应用。如果想成为 Node.js 开发人员,除了需要擅长 JavaScript 之外,擅长 JavaScript 的另一个原因是利用围绕 Node.js 和 JavaScript 的蓬勃发展的生态系统。GitHub 上项目数量最多的语言是 JavaScript。Node.js 是 GitHub 上最受欢迎的服务器端技术,如图 2-8 所示,也是第三大受欢迎的存储库。

9781484201886_Fig02-08.jpg

图 2-8 。GitHub 上最流行的存储库

一切都是参考

JavaScript 被设计得很简单,并且只需要有限的计算机资源。每当我们将一个变量赋给另一个变量时,JavaScript 都会复制对该变量的引用。要理解这意味着什么,请看一下清单 2-26 。

清单 2-26 。 reference1.js

var foo = { bas: 123 };
var bar = foo;
bar.bas = 456;
console.log(foo.bas); // 456

无论对象的大小如何,在函数调用中传递对象都是非常轻量级的,因为我们只复制对对象的引用,而不是对象的每个属性。要制作数据的真实副本(打破引用链接),你可以创建一个新的对象文字,如清单 2-27 所示。

清单 2-27 。 reference2.js

var foo = { bas: 123 };
var bar = { bas: foo.bas }; // copy

bar.bas = 456; // change copy
console.log(foo.bas); // 123, original is unchanged

我们可以使用相当多的第三方库来复制任意 JavaScript 对象的属性。(这是一个简单的函数,如果我们愿意,我们可以自己编写。)这些库在第四章中有所介绍。

默认值

JavaScript 中任何变量的默认值都是undefined。你可以在清单 2-28 中看到它被注销,你创建了一个变量,但没有给它赋值。

清单 2-28 。 default1.js

var foo;
console.log(foo); // undefined

类似地,变量上不存在的属性返回undefined ( 清单 2-29 )。

**清单 2-29 。**默认 2.js

var foo = { bar: 123 };
console.log(foo.bar); // 123
console.log(foo.bas); // undefined

精确相等

JavaScript 中需要注意的一点是=====之间的区别。当 JavaScript 试图抵抗编程错误时,==试图在两个变量之间进行类型强制。例如,它将一个字符串转换成一个数字,这样你就可以将它与一个数字进行比较,如清单 2-30 所示。

**清单 2-30 。**等于 1.js

console.log(5 == '5'); // true
console.log(5 === '5'); // false

然而,它做出的选择并不总是理想的。例如,在清单 2-31 中,第一条语句为假,因为“”和“0”都是字符串,显然不相等。然而,在第二种情况下,' 0 '和空字符串(')都是 false(换句话说,它们的行为类似 false ),因此相对于==是相等的。当你使用===时,这两种说法都是错误的。

**清单 2-31 。**等于 2.js

console.log('' == '0'); // false
console.log('' == 0); // true

console.log('' === '0'); // false
console.log('' === 0); // false

这里的提示是不要比较不同的类型。比较不同类型的变量(比如一个字符串和一个数字)是你无论如何都无法在静态类型语言中完成的事情(在静态类型语言中,你必须指定变量的类型)。如果你记住了这一点,你就可以放心地使用==。但是,建议您尽可能使用===

== vs. ===类似,还有不等式运算符!=!==,工作方式相同。换句话说!= does 类型强制,而!==是严格的。

null是一个特殊的 JavaScript 对象,用来表示一个空对象。这与undefined不同,后者被 JavaScript 用于不存在和未初始化的值。您不应该为undefined设置任何东西,因为按照惯例,undefined是您应该留给运行时的默认值。使用null的一个好时机是当你想明确地说某样东西不存在的时候,比如作为一个函数参数。你会在本章的错误处理部分看到它的用法。

真与假

JavaScript 中的一个重要概念是真值和假值。真值是那些在布尔运算中表现得像true的值,假值是那些在布尔运算中表现得像false的值。对于null / undefined,使用if / else / !通常比进行显式检查更容易。清单 2-32 显示了这些值的虚假性质的一个例子。

清单 2-32 。 truthyandfalsy.js

console.log(null == undefined); // true
console.log(null === undefined); // false

// Are these all falsy?
if (!false) {
    console.log('falsy');
}
if (!null) {
    console.log('falsy');
}
if (!undefined) {
    console.log('falsy');
}

其他重要的 falsy 值是0和空字符串('')。所有的对象文字和数组在 JavaScript 中都是真的。

显示模块模式

返回对象的函数是创建相似对象的好方法。这里的对象是指打包成一个漂亮的包的数据和功能,这是面向对象编程(OOP)的最基本形式。揭示模块模式的核心是 JavaScript 对闭包的支持和返回任意(函数+数据)对象文字的能力。清单 2-33 是一个简单的例子,展示了如何使用这个模式创建一个对象。

清单 2-33 。 revealingModules.js

function printableMessage() {
    var message = 'hello';
    function setMessage(newMessage) {
        if (!newMessage) throw new Error('cannot set empty message');
        message = newMessage;
    }
    function getMessage() {
        return message;
    }

    function printMessage() {
        console.log(message);
    }

    return {
        setMessage: setMessage,
        getMessage: getMessage,
        printMessage: printMessage
    };
}

// Pattern in use
var awesome1 = printableMessage();
awesome1.printMessage(); // hello

var awesome2 = printableMessage();
awesome2.setMessage('hi');
awesome2.printMessage(); // hi

// Since we get a new object everytime we call the module function
// awesome1 is unaffected by awesome2
awesome1.printMessage(); // hello

这个例子的优点在于它是一个易于理解的简单模式,因为它只使用了闭包、一级函数和对象文字——这些概念您已经很熟悉了,我们在本章开始时已经详细介绍过了。

理解这一点

JavaScript 关键字 this 在语言中有着非常特殊的地位。它是传递给函数的东西,取决于你如何调用它(有点像函数参数)。最简单的理解是,它指的是调用上下文。调用上下文是用于调用函数的前缀。清单 2-34 。演示其基本用法。

清单 2-34 。 this1.js

var foo = {
    bar: 123,
    bas: function () {
        console.log('inside this.bar is:', this.bar);
    }
}

console.log('foo.bar is: ', foo.bar); // foo.bar is: 123
foo.bas(); // inside this.bar is: 123

在函数bas内部,this引用foo,因为basfoo上被调用,因此是调用上下文。那么,如果我调用一个没有任何前缀的函数,调用上下文是什么呢?默认的调用上下文是 Node.js global变量,如清单 2-35 所示。

清单 2-35 。 this2.js

function foo() {
    console.log('is this called from globals? : ', this === global); // true
}
foo();

注意,如果我们在浏览器中运行它,global变量将是window而不是global

当然,由于 JavaScript 对一级函数有很好的支持,我们可以给任何对象附加一个函数并改变调用上下文,如清单 2-36 所示。

清单 2-36 。 this3.js

var foo = {
    bar: 123
};

function bas() {
    if (this === global)
        console.log('called from global');
    if (this === foo)
        console.log('called from foo');
}

// global context
bas(); // called from global

// from foo
foo.bas = bas;
foo.bas(); // called from foo

关于 JavaScript 中的this,你还需要知道最后一件事。如果用 JavaScript 操作符new调用一个函数,它会创建一个新的 JavaScript 对象,函数中的this会引用这个新创建的对象。再次,清单 2-37 提供了另一个简单的例子。

清单 2-37 。 this4.js

function foo() {
    this.foo = 123;
    console.log('Is this global?: ', this == global);
}

// without the new operator
foo(); // Is this global?: true
console.log(global.foo); // 123

// with the new operator
var newFoo = new foo(); // Is this global?: false
console.log(newFoo.foo); // 123

你可以看到我们在函数内部修改了this.foo,并且newFoo.foo被设置为那个值。

理解原型

一个常见的误解是 JavaScript 不是面向对象的。的确,直到最近 JavaScript 还没有关键字class。但是 JavaScript 中的函数比许多其他语言中的函数更强大,可以用来模仿传统的面向对象原则。秘方是new关键字(你已经见过了)和一个叫做prototype?? 的属性。JavaScript 中的每个对象都有一个到另一个对象的内部链接,这个对象叫做原型。在我们研究用 JavaScript 创建传统类之前,让我们更深入地了解一下 prototype。

当您读取一个对象的属性时(例如,foo.barfoo读取属性bar,JavaScript 检查这个属性是否存在于 foo 上。如果没有,JavaScript 检查属性是否存在于foo.__proto__上,依此类推,直到__proto__本身不存在。如果在任何级别找到一个值,则返回该值。否则,JavaScript 返回undefined(参见清单 2-38 )。

**清单 2-38 。**原型 1 。射流研究…

var foo = {};
foo.__proto__.bar= 123;
console.log(foo.bar); // 123

尽管这是可行的,JavaScript 中的__前缀通常用于用户代码不应该使用的属性(换句话说,私有/内部实现细节)。所以不要直接用__proto__。好消息是,当你在一个函数上使用new操作符创建一个对象时,__proto__ 被设置为该函数的.prototype成员,这可以用一段简单的代码来验证,如清单 2-39 所示。

**清单 2-39 。**原型 2.js

// Lets create a test function and set a member on its prototype
function foo() { };
foo.prototype.bar = 123;

// Lets create a object using `new`
// foo.prototype will be copied to bas.__proto__
var bas = new foo();

// Verify the prototype has been copied
console.log(bas.__proto__ === foo.prototype); // true
console.log(bas.bar); // 123

这很棒的原因是因为原型在由同一个函数创建的所有对象(让我们称这些实例)之间共享。这个事实显示在清单 2-40 中。

**清单 2-40 。**原型 3.js

// Lets create a test function and set a member on its prototype
function foo() { };
foo.prototype.bar = 123;

// Lets create two instances
var bas = new foo();
var qux = new foo();

// Show original value
console.log(bas.bar); // 123
console.log(qux.bar); // 123

// Modify prototype
foo.prototype.bar = 456;

// All instances changed
console.log(bas.bar); // 456
console.log(qux.bar); // 456

假设您需要创建 1000 个实例。你放在prototype上的所有功能都是共享的。因此第一课:prototype节省记忆。

Prototype 非常适合从对象中读取数据。但是,如果您在对象上设置了一个属性,您就断开了与原型的链接,因为(如前所述)只有在对象上不存在该属性时,才能访问原型。在一个对象上设置一个属性所导致的与原型属性的断开如清单 2-41 所示。

清单 2-41 。 prototype4.js

// Lets create a test function and set a member on its prototype
function foo() { };
foo.prototype.bar = 123;

// Lets create two instances
var bas = new foo();
var qux = new foo();

// Overwrite the prototype value for bas
bas.bar = 456;
console.log(bas.bar); // 456 i.e. prototype not accessed

// Other objects remain unaffected
console.log(qux.bar); // 123

你可以看到,当我们修改bas.barbas.__proto__.bar 不再被访问。因此,第二个教训:.prototype对你打算写给的资产没有好处。

问题变成了我们应该对需要写入的属性使用什么。回想一下我们对this的讨论,this指的是用new操作符调用函数时创建的对象。所以this是读/写属性的完美候选,您应该将它用于所有属性。但是功能在创建后一般不会改变。因此函数是继续.prototype的绝佳候选。这样,功能(函数/方法)在所有实例之间共享,属性属于单个对象。现在我们可以理解用 JavaScript 写一个类的模式,如清单 2-42 所示。

**清单 2-42 。**class . js

// Class definition
function someClass() {
    // Properties go here
    this.someProperty = 'some initial value';
}
// Member functions go here:
someClass.prototype.someMemberFunction = function () {
    /* Do something using this */
    this.someProperty = 'modified value';
}

// Creation
var instance = new someClass();

// Usage
console.log(instance.someProperty); // some initial value
instance.someMemberFunction();
console.log(instance.someProperty); // modified value

在成员函数中,我们可以使用this访问当前实例,即使所有实例共享同一个函数体。根据我们之前对this和调用上下文的讨论,原因应该很明显。这是因为我们在某个实例上调用了一个函数,换句话说,instance.someMemberFunction() 。这就是为什么在函数内部this会引用所使用的instance

这里与显示模块模式的主要区别在于,函数在所有实例之间共享,并且不会为每个新实例占用内存。这是因为功能只在.prototype上而不在this上。core Node.js 中的大多数类都是使用这种模式编写的。

错误处理

错误处理是任何应用的重要组成部分。错误可能是由于您的代码或者甚至是不在您的控件中的代码而发生的,例如,数据库失败。

JavaScript 有一个很好的异常处理机制,您可能已经从其他编程语言中熟悉了。要抛出异常,只需使用throw JavaScript 关键字。为了捕捉一个异常,你可以使用catch 关键字。对于无论是否捕获到异常都要运行的代码,可以使用finally 关键字。清单 2-43 。是演示这一点的一个简单示例。

**清单 2-43 。**错误 1.js

try {
    console.log('About to throw an error');
    throw new Error('Error thrown');
}
catch (e) {
    console.log('I will only execute if an error is thrown');
    console.log('Error caught: ', e.message);
}
finally {
    console.log('I will execute irrespective of an error thrown');
}

只有在抛出错误时,catch部分才会执行。尽管在try部分中抛出了任何错误,但是finally部分还是会执行。这种异常处理方法非常适合同步 JavaScript。但是,它在异步工作流下不起作用。清单 2-44 。证明了这个缺点。

清单 2-44 。 error2.js

try {
    setTimeout(function () {
        console.log('About to throw an error');
        throw new Error('Error thrown');
    }, 1000);
}
catch (e) {
    console.log('I will never execute!');
}

console.log('I am outside the try block');

它不起作用的原因是因为在执行对setTimeout的回调时,我们已经在 try/catch 块之外了。setTimeout 将调用稍后提供的函数,您可以在这个代码示例中看到这种情况,因为“我在 try 块之外”被执行了。Node.js 中未捕获异常的默认行为是退出进程,这就是我们的应用崩溃的原因。

正确的做法是处理回调中的错误*,如清单 2-45 所示。*

**清单 2-45 。**错误 3.js

setTimeout(function () {
    try {
        console.log('About to throw an error');
        throw new Error('Error thrown');
    }
    catch (e) {
        console.log('Error caught!');
    }
}, 1000);

这个方法在一个async函数中运行良好。但是现在我们有一个问题,就是找到一种方法来告诉外部代码这个错误。我们来看一个具体的例子。考虑一个简单的getConnection函数,它接受一个我们需要在成功连接后调用的callback,如清单 2-46 所示。

清单 2-46 。 error4.js

function getConnection(callback) {
    var connection;
    try {
        // Lets assume that the connection failed
        throw new Error('connection failed');

        // Notify callback that connection succeeded?
    }
    catch (error) {
        // Notify callback about error?
    }
}

我们需要通知回调关于成功和失败。这就是为什么 Node.js 有一个惯例,如果出现错误*,就用第一个参数error *调用回调。如果没有错误,我们将错误设置为null进行回调。因此,为 Node.js 生态系统设计的getConnection函数将类似于清单 2-47 中所示。

清单 2-47 。错误 5.js

function getConnection(callback) {
    var connection;
    try {
        // Lets assume that the connection failed
        throw new Error('connection failed');

        // Notify callback that connection succeeded?
        callback(null, connection);
    }
    catch (error) {
        // Notify callback about error?
        callback(error, null);
    }
}

// Usage
getConnection(function (error, connection) {
    if (error) {
        console.log('Error:', error.message);
    }
    else {
        console.log('Connection succeeded:', connection);
    }
});

将错误作为第一个参数可以确保错误检查的一致性。这是所有具有错误条件的 Node.js 函数遵循的约定。一个很好的例子是文件系统 API,我们将在第三章中介绍。还要注意,开发人员倾向于使用null的虚假特性来检查错误。

摘要

在本章中,我们讨论了成功使用 Node.js 所必需的重要 JavaScript 概念。现在,您应该对 JavaScript 和 Node.js 在创建数据密集型应用方面的优势以及为何其性能优于之前的技术有了深刻的理解。在下一章,我们将讨论更多 Node.js 特定的模式和实践来创建可维护的应用。

引用的作品

Ryan Dahl (2009) Node.js 来自 JSConf。

" ginx 诉 Apache "," ?? "

http://www.w3schools.com/browsers/browsers_stats.asp“浏览器统计”

https://github.com/search?o=desc&q=stars%3A%3E1&s=stars&type=Repositories“GitHub 知识库按星星搜索”

三、核心 Node.js

Node.js 附带了许多内置模块,这些模块提供了一组我们可以构建的核心特性。在这一章中,我们将展示 Node.js 的重要部分,每个认真的开发者都应该熟悉这些部分。Node.js 的伟大之处在于,对于普通开发人员来说,完全了解所有功能的确切方式是完全可能的。

为了成功地交付大型应用并在相当大的团队中工作,我们需要一种封装复杂性的方法。JavaScript 最初被设计成由 web 浏览器以一种简单的方式从上到下读取,并且使用<script>标签加载文件。随着越来越大的应用被用 JavaScript 编写,两个模块系统(AMD 和 CommonJS)被开发出来。它们使代码更易于管理和重用。存在两种模式,因为浏览器和服务器在模块加载延迟(网络请求与文件系统)方面提出了不同的挑战。在本章中,我们将讨论这些模式,并展示如何在浏览器中重用 Node.js 代码。

关于本章和其他章节中使用多个文件的代码示例,需要注意的是,示例的主入口点通常按照 Node.js 社区惯例被称为app.js。所以您应该能够以node app.js的身份运行一个样本。

基于 Node.js 文件的模块系统

Kevin Dongaoor 在 2009 年创建了 CommonJS,目标是为服务器上的 JavaScript 模块指定一个生态系统。Node.js 遵循 CommonJS 模块规范。以下是模块系统的几个要点:

  • 每个文件都是它自己的模块。
  • 每个文件都可以使用module变量访问当前的模块定义。
  • 当前模块的输出由module.exports变量决定。
  • 要导入一个模块,使用全局可用的require函数。

和往常一样,最好直接进入代码。让我们考虑一个简单的例子,我们希望与应用的不同部分共享文件foo.js中的函数。要从文件中导出函数,我们只需将它分配给module.exports ,如清单 3-1 所示。

清单 3-1 。intro/base/foo.js

module.exports = function () {
    console.log('a function in file foo');
};

为了从文件bar.js中使用这个函数,我们简单地使用全局可用的require函数导入foo,并将返回值存储在一个局部变量中,如清单 3-2 中的所示。

清单 3-2 。intro/base/bar.js

var foo = require('./foo');
foo(); // logs out : "a function in file foo"

Node.js 被设计得很简单,这一点在它的模块系统中有所体现。现在我们已经看到了一个简单的例子,让我们从require函数开始,更深入地研究各种细节。

Node.js 需要函数

Node.js require函数是将模块导入当前文件的主要方法。Node.js 中有三种模块:核心模块、*文件模块、*和外部 node_modules、,它们都使用require函数。我们目前正在讨论文件模块。

当我们使用相对路径进行require调用时——例如像require('./filename')require('../foldername/filename')——node . js 在一个新的范围内运行目标 JavaScript 文件,并返回该文件中module.exports的最终值。这是文件模块的基础。让我们看看这个设计的分支。

Node.js 很安全

许多编程环境中的模块并不安全,并且污染了全局范围。PHP 就是一个简单的例子。假设你有一个文件foo.php,它简单地定义了一个函数foo,如清单 3-3 所示。

清单 3-3 。foo.php

function foo($something){
        return $something;
}

如果你想在一个文件bar.php中重用这个函数,你可以简单地使用include函数包含foo.php,然后文件foo.php中的所有东西都成为bar.php的(全局)作用域的一部分。这允许您使用函数foo,如清单 3-4 中的所示。

清单 3-4 。在 PHP 中包含函数

include('foo.php');
foo();

这个设计有很多负面影响。例如,变量foo在当前文件中的含义可能会根据您导入的内容而改变。因此,如果两个文件foo1foo2有可能有同名的变量,那么您就不能安全地包含这两个文件。另外,所有的都被导入,所以在一个模块中不能只有局部变量。您可以在 PHP 中使用名称空间来解决这个问题,但是 Node.js 完全避免了名称空间污染的可能性。

使用require函数只给你一个module.exports变量,你需要在本地将结果赋给一个变量,以便在作用域内使用它,如清单 3-5 所示。

清单 3-5 。显示您控制名称的代码段

var yourChoiceOfLocalName = require('./foo');

没有偶然的全局范围—有显式名称和具有相似内部局部变量名的文件可以和平共存。

有条件地加载模块

require的行为就像 JavaScript 中的任何其他函数一样。它没有特殊的属性。这意味着你可以根据某些条件选择调用它,因此只有在你需要的时候才加载模块,如清单 3-6 所示。

清单 3-6 。延迟加载模块的代码片段

if(iReallyNeedThisModule){
     var foo = require('./foo');
}

这允许您基于您的需求,仅在第一次使用时延迟加载模块。

阻塞

require函数阻止进一步的代码执行,直到模块加载完毕。这意味着在模块被加载和执行之前,不会执行require调用之后的代码。这允许你避免提供不必要的回调,就像你需要为 Node.js 中的所有异步 I/O 做的那样,这在第二章中讨论过。(参见清单 3-7 。)

清单 3-7 。演示模块同步加载的代码片段

// Blocks execution till module is loaded
var foo = require('./foo');

// Continue execution after it is loaded
console.log('loaded foo');
foo();

缓存的

正如你从第二章中所知道的,从文件系统中读取数据比从 RAM 中读取要慢一个数量级。因此,在第一次对特定文件进行require调用后,module.exports被缓存。下一次调用解析为相同文件的require(换句话说,只要目标文件相同,传递给require调用的原始相对文件路径是什么并不重要),目标文件的module.exports变量从内存中返回,保持速度。清单 3-8 用一个简单的例子展示了这种速度差异。

清单 3-8 。intro/cache/bar . js

var t1 = new Date().getTime();
var foo1 = require('./foo');
console.log(new Date().getTime() - t1); // > 0

var t2 = new Date().getTime();
var foo2 = require('./foo');
console.log(new Date().getTime() - t2); // approx 0

共享状态

拥有某种在模块间共享状态的机制在各种环境中都很有用。由于模块被缓存,如果我们从模块foo.js返回一个对象foo,那么requirefoo.js的每个模块将获得相同的(可变的)对象。清单 3-9 用一个简单的例子展示了这个过程,在这个例子中我们导出了一个对象。该对象在app.js中被修改,如清单 3-10 所示。这个修改影响了bar.jsrequire返回的内容,如清单 3-11 所示。这允许您在模块之间共享内存中的对象,这对于使用模块进行配置是很有用的。清单 3-12 中显示了一个执行示例。

清单 3-9 。intro/shared/foo.js

module.exports = {
    something: 123
};

清单 3-10 。intro/shared/app.js

var foo = require('./foo');
console.log('initial something:', foo.something); // 123

// Now modify something:
foo.something = 456;

// Now load bar:
var bas = require('./bar');

清单 3-11 。intro/shared/bar.js

var foo = require('./foo');
console.log('in another module:', foo.something); // 456

清单 3-12 。intro/shared/app.js 的运行示例

$ node app.js
initial something: 123
in another module: 456

对象工厂

正如我们已经展示的,每次在 Node.js 进程中一个require调用解析到同一个文件时,都返回同一个对象。如果您希望每个require函数调用都有某种形式的新对象创建机制,那么您可以从返回新对象的源模块中导出一个函数。然后在你的目的地require模块并调用这个导入的函数来创建一个新的对象。清单 3-13 中显示了一个例子,我们导出一个函数,然后使用这个函数创建一个新对象,如清单 3-14 中的所示。

清单 3-13 。intro/factory/foo.js

module.exports = function () {
    return {
        something: 123
    };
};

清单 3-14 。intro/factory/app.js

var foo = require('./foo');

// create a new object
var obj = foo();

// use it
console.log(obj.something); // 123

请注意,您甚至可以一步完成此操作(换句话说,require('./foo')();)

Node.js 导出

现在我们对require有了更多的了解,让我们更深入地了解一下module.exports

模块.导出

如前所述,Node.js 中的每个文件都是一个模块。我们打算从模块中导出的项目应该附加到module.exports变量上。需要注意的是module.exports已经在每个文件中被定义为一个新的空对象。也就是说,module.exports = {}是隐性存在的。默认情况下,每个模块导出一个空对象,换句话说,{}。(参见清单 3-15 。)

清单 3-15 。intro/module.exports/app.js

console.log(module.exports); // {}

出口别名

到目前为止,我们只从一个模块中导出了一个对象。这可以很简单地通过将我们需要导出的对象分配给module.exports来完成。然而,从一个模块中导出多个变量是一个常见的需求。实现这一点的一个方法是创建一个新的对象文字并将其赋给module.exports,如清单 3-16 所示。

清单 3-16 。intro/exports/foo1.js

var a = function () {
    console.log('a called');
};

var b = function () {
    console.log('b called');
};

module.exports = {
    a: a,
    b: b
};

然而,这有点难以管理,因为模块返回的内容可能与模块包含的内容相差甚远。在清单 3-16 中,函数a的定义比我们实际将其导出到外部世界的时间要早得多。所以一个常见的惯例是简单地将我们想要导出的对象内联到module.exports,如清单 3-17 所示。这是可能的,因为module.exports被 Node.js 隐式设置为{},正如我们在前面的清单 3-15 中看到的。

清单 3-17 。intro/exports/foo2.js

module.exports.a = function () {
    console.log('a called');
};

module.exports.b = function () {
    console.log('b called');
};

然而,一直输入module.exports也变得很麻烦。因此 Node.js 通过为module.exports创建一个别名exports来帮助我们,所以不用每次都键入module.exports.something,你可以简单地使用exports.something。这显示在清单 3-18 中。

清单 3-18 。intro/exports/foo3.js

exports.a = function () {
    console.log('a called');
};

exports.b = function () {
    console.log('b called');
};

需要注意的是exports就像任何其他 JavaScript 变量一样;Node.js 只是为我们做了exports = module.exports。如果我们添加一些东西,例如,fooexports,也就是exports.foo = 123,我们实际上是在做module.exports.foo = 123,因为 JavaScript 变量是引用,正如在第二章中讨论的。

但是,如果你做了exports = 123,就断了对module.exports的引用;即exports不再指向module.exports。同样,它也不做module.exports = 123。所以,知道应该只使用exports别名给 attach stuff 而不直接给它赋值是很重要的。如果您想分配一个导出,使用module.exports =,就像我们在本节之前一直做的那样。

最后,你可以运行清单 3-19 中所示的代码样本来证明所有这些方法从消费(导入)的角度来看是等价的。

清单 3-19 。intro/export/app . js

var foo1 = require('./foo1');
foo1.a();
foo1.b();

var foo2 = require('./foo2');
foo2.a();
foo2.b();

var foo3 = require('./foo3');
foo3.a();
foo3.b();

模块最佳实践

现在我们已经了解了基于 Node.js 文件的模块系统背后的技术,让我们来看看社区遵循的一些最佳实践。Node.js 和 JavaScript 对编程错误具有很强的弹性,并努力保持灵活性,这就是为什么有各种各样的工作方式。但是,您应该遵循一些约定,我们强调了社区中常见的一些约定。

不要使用。js 扩展

最好是做require('./foo')而不是require('./foo.js'),尽管两者对 Node.js 都很好。

**原因:**对于基于浏览器的模块系统(比如 RequireJS,我们将在本章后面讨论),假设您没有提供.js扩展,因为我们无法查看服务器文件系统来理解您的意思。为了保持一致,避免添加。js 扩展在你所有的require调用中。

相对路径

在使用基于文件的模块时,需要使用相对路径(换句话说,用require('./foo')代替require('foo'))。

**原因:**非相对路径是为核心模块和 node_modules 预留的。我们在本章讨论核心模块,在下一章讨论 node_modules。

利用出口

当您想要导出多个内容时,尝试使用exports别名。

**原因:**它使输出的接近其定义。对于你导出的每一个东西都有一个本地变量也是一个惯例,这样你就可以很容易地在本地使用它。在一行中完成所有这些,如清单 3-20 所示。

清单 3-20 。创建一个局部变量并导出

var foo = exports.foo = /* whatever you want to export as `foo` from this module */ ;

导出整个文件夹

如果你有太多的模块放在一起,你一直在导入到其他文件中,尽量避免重复导入,如清单 3-21 所示。

清单 3-21 。避免重复巨大的导入块

var foo = require('../something/foo');
var bar = require('../something/bar');
var bas = require('../something/bas');
var qux = require('../something/qux');

相反,在something文件夹中创建一个单独的index.js。在index.js中,一次性导入所有模块,然后从该模块中导出,如清单 3-22 所示。

清单 3-22 。index.js 示例

exports.foo = require('./foo');
exports.bar = require('./bar');
exports.bas = require('./bas');
exports.qux = require('./qux');

现在,只要您需要所有这些东西,就可以简单地导入这个index.js:

var something = require('../something/index');

**理由:**更易维护。在导出方面,单个模块(单个文件)仍然较小——您不需要将所有内容都放在一个文件中,这样您就可以轻松地将它导入到其他地方。你只需要创建一个index.js文件。在导入方面,您需要编写(和维护)更少的require调用。

重要的全局

Node.js 提供了大量全局可用的实用变量。这些变量中有些是真正的全局变量(在所有模块之间共享),有些是局部全局变量(特定于当前模块的变量)。我们已经看到了几个真正的全局变量的例子,即require函数。我们已经看到了一些模块级隐式定义的变量— module(由module.exports使用)和exports。让我们检查几个更重要的全局变量。

控制台

console是可用的最有用的全局变量之一。因为从命令行启动和重新启动 Node.js 应用非常容易,所以当您需要调试应用时,控制台在快速显示应用中发生的情况方面起着重要作用。为了同样的目的,我们在整个例子中使用了console.logconsole有更多的 o 函数,我们将在第十一章中讨论。

计时器

我们之前在第二章中讨论 Node.js 事件循环时已经看到过setTimeout。它设置了一个在指定的延迟时间(毫秒)后调用的函数。请注意,此延迟是调用指定函数之前的最小间隔。它被调用的实际持续时间取决于 JavaScript 线程的可用性,正如我们在第二章的中关于线程饥饿的章节中看到的。它还取决于操作系统何时调度 Node.js 进程执行(通常这不是问题)。清单 3-23 中的显示了一个setTimeout的快速示例,它在 1000 毫秒(换句话说,一秒)后调用一个函数。

清单 3-23 。globals/timers/setTimeout.js

setTimeout(function () {
    console.log('timeout completed');
}, 1000);

setTimeout功能类似的是setInterval功能。setTimeout仅在指定的持续时间后执行一次回调函数*。但是setInterval之后重复调用回调,每次经过指定的持续时间。这显示在清单 3-24 中,我们每秒钟打印出second passed。与setTimeout类似,根据 JavaScript 线程的可用性,实际持续时间可能会超过指定值。*

清单 3-24 。globals/timers/setInterval.js

setInterval(function () {
    console.log('second passed');
}, 1000);

setTimeoutsetInterval都返回一个对象,该对象可用于使用clearTimeout / clearInterval函数清除超时/间隔。清单 3-25 演示了如何使用clearInterval在五秒钟内每秒调用一个函数,然后清除应用退出的间隔。

清单 3-25 。globals/timers/clear interval . js

var count = 0;
var intervalObject = setInterval(function () {
    count++;
    console.log(count, 'seconds passed');
    if (count == 5) {
        console.log('exiting');
        clearInterval(intervalObject);
    }
}, 1000);

_ _ 文件名和 _ _ 目录名

这些变量在每个文件中都可用,并为您提供当前模块的文件和目录的完整路径。完整路径意味着它们包括所有内容,直到该文件所在的当前驱动器的根目录。使用清单 3-26 中的代码来查看当你将文件移动到文件系统的不同位置并运行它时这些值的变化。

清单 3-26 。globals/fileAndDir/app.js

console.log(__dirname);
console.log(__filename);

过程

process是 Node.js 提供的最重要的全局变量之一。除了我们将在下一节讨论的一些有用的成员函数和属性之外,它还是一些关键事件的来源,我们将在第五章中更深入地研究这些事件。

命令行参数

由于 Node.js 没有传统 C/C++/JAVA/C#意义上的 main 函数,所以使用process对象来访问命令行参数。参数作为成员属性process.argv可用,它是一个数组。第一个元素是node(即 Node 可执行文件),第二个元素是传入 Node.js 以启动进程的 JavaScript 文件的名称,剩下的元素是命令行参数。作为一个例子,考虑一个简单的文件argv.js,它简单地将这些记录到控制台,如清单 3-27 中的所示。如果您以node argv.js foo bar bas的身份运行它,您将得到类似于清单 3-28 中所示的输出。

清单 3-27 。全局/流程/argv.js

// argv.js
console.log(process.argv);

清单 3-28 。argv.js 的示例输出

 ['node',
  '/path/to/file/on/your/filesystem/argv.js',
  'foo',
  'bar',
  'bas']

在 Node.js 中,有一些优秀的库可以以有意义的方式处理命令行参数。在下一章学习 NPM 时,我们将研究一个这样的库。

process.nextTick

process.nextTick是一个采用回调函数的简单函数。它用于将回调放入 Node.js 事件循环的下一个循环中。它被设计得非常高效,并且被许多 Node.js 核心库使用。它的用法简单到足以演示,清单 3-29 中给出了一个例子。该示例的输出如清单 3-30 中的所示。

清单 3-29 。globals/process/nexttick.js

// nexttick.js
process.nextTick(function () {
    console.log('next tick');
});
console.log('immediate');

清单 3-30 。nexttick.js 输出示例

immediate
next tick

如您所见,立即调用首先执行,而nextTick回调在事件循环的下一次运行中执行。您应该知道此函数的原因是,由于 Node.js 的异步特性,此函数通常会出现在调用堆栈中,因为这将是 Node.js 事件循环的起点。这个函数之前的都是 c,调用栈中这个函数之后的都是 JavaScript。

缓冲器

缓冲世界!纯 JavaScript 非常适合 Unicode 字符串。然而,为了处理 TCP 流和文件系统,开发人员添加了本地和快速支持来处理二进制数据。开发人员在 Node.js 中使用全球通用的Buffer类实现了这一点。

作为一名从事应用开发的 Node.js 开发人员,您与 buffer 的主要交互很可能是将Buffer实例转换为string,或者将字符串转换为Buffer实例。为了进行这两种转换,您需要告诉Buffer类每个字符在字节中的含义。这些信息被称为字符编码。Node.js 支持所有流行的编码格式,如 ASCII、UTF 8 和 UTF-16。

将字符串转换成缓冲区非常简单。你只需调用Buffer类构造函数(参见第二章中的原型讨论来回顾 JavaScript 中的类)传入一个字符串和一个编码。将一个Buffer实例转换成一个字符串也同样简单。您调用缓冲区实例的toString方法,传递一个编码方案。这两者都在清单 3-31 中进行了演示。

清单 3-31 。全局/缓冲区/缓冲区. js

// a string
var str = "Hello Buffer World!";

// From string to buffer
var buffer = new Buffer(str, 'utf-8');

// From buffer to string
var roundTrip = buffer.toString('utf-8');
console.log(roundTrip); // Hello

全球的

变量global是我们在 Node.js 中的全局名称空间的句柄,如果你熟悉前端 JavaScript 开发,这有点类似于window对象。我们见过的所有真正的全局变量(consolesetTimeoutprocess)都是global变量的成员。你甚至可以在全局变量中添加成员,使其随处可用,如清单 3-32 所示。这使得变量something随处可用的事实在清单 3-33 中得到证明。

清单 3-32 。globals/global/addToGlobal.js

global.something = 123;

清单 3-33 。全球/全球/app.js

console.log(console === global.console); // true
console.log(setTimeout === global.setTimeout); // true
console.log(process === global.process); // true

// Add something to global
require('./addToGlobal');
console.log(something); // 123

尽管您可以向 global 添加成员,但我们强烈建议不要这样做。原因是它使得知道一个特定的变量来自哪里变得极其困难。模块系统的设计使得大型代码库的分析和维护变得容易。到处都是全局变量是不可维护、不可伸缩或不可重用的。然而,知道这样做是有用的,更重要的是,作为库开发人员,您可以按照自己喜欢的方式扩展 Node.js。

核心模块

Node.js 的设计理念是提供一些经过实战检验的核心模块,并让社区在这些模块的基础上提供高级功能。在本节中,我们将研究几个重要的核心模块。

消耗核心模块

消费核心模块与消费自己编写的基于文件的模块非常相似。你还在用require功能。唯一的区别是,您只需为require函数指定模块的名称,而不是文件的相对路径。例如,要使用核心的path模块,您需要编写一个类似var path = require('path')的 require 语句。与基于文件的模块一样,不存在隐式的全局命名空间污染,您得到的是一个自己命名的局部变量来访问模块的内容。例如,在var path = require('path')中,我们将它存储在一个名为path的局部变量中。现在让我们检查几个核心模块,您应该了解这些模块才能成功使用 Node.js。

路径模块

使用require('path')加载该模块。path 模块导出函数,这些函数提供了使用文件系统时常见的有用的字符串转换。使用path模块的主要动机是消除处理文件系统路径时的不一致性。例如,path.join在 Mac OS X 等基于 UNIX 的系统上使用正斜杠/而在 Windows 系统上使用反斜杠` \ '。下面是一些更有用的函数的快速讨论和示例。

path.normalize(str)

这个函数修复了特定于操作系统的斜线。和..并删除重复的斜杠。展示这些特性的一个简单例子如清单 3-34 所示。

清单 3-34 。core/path/normalize.js

var path = require('path');

// Fixes up .. and .
// logs on Unix: /foo
// logs on Windows: \foo
console.log(path.normalize('/foo/bar/..'));

// Also removes duplicate '//' slashes
// logs on Unix: /foo/bar
// logs on Windows: \foo\bar
console.log(path.normalize('/foo//bar/bas/..'));

path.join([str1],[str2],…)

考虑到操作系统,该函数将任意数量的路径连接在一起。清单 3-35 中显示了一个示例。

清单 3-35 。核心/路径/连接. js

var path = require('path');

// logs on Unix: foo/bar/bas
// logs on Windows: foo\bar\bas
console.log(path.join('foo', '/bar', 'bas'));

dirname、basename 和 extname

这些函数是路径模块中最有用的三个函数。path.dirname给出特定路径字符串的目录部分(独立于操作系统),而path.basename给出文件的名称。path.extname给你文件扩展名。这些功能的一个例子如清单 3-36 所示。

清单 3-36 。核心/路径/目录 _ 基本 _ 扩展. js

var path = require('path');

var completePath = '/foo/bar/bas.html';

// Logs : /foo/bar
console.log(path.dirname(completePath));

// Logs : bas.html
console.log(path.basename(completePath));

// Logs : .html
console.log(path.extname(completePath));

现在你应该对如何使用path以及它的设计目标有所了解。Path 还有一些其他有用的函数,您可以使用 Node.js 官方文档(http://nodejs.org/api/path.html)在线探索。

fs 模块

fs模块提供了对文件系统的访问。使用require('fs')加载该模块。fs模块具有重命名文件、删除文件、读取文件和写入文件的功能。在清单 3-37 中显示了一个简单的写入和读取文件系统的例子。

清单 3-37 。core/fs/create.js

var fs = require('fs');

// write
fs.writeFileSync('test.txt', 'Hello fs!');

// read
console.log(fs.readFileSync('test.txt').toString());

关于fs模块的一个伟大的事情是它有异步和同步功能(使用-Sync后缀)来处理文件系统。例如,要删除一个文件,你可以使用unlinkunlinkSync。同步版本如清单 3-38 所示,相同代码的异步版本如清单 3-39 所示。

清单 3-38 。core/fs/deleteSync.js

var fs = require('fs');
try {
    fs.unlinkSync('./test.txt');
    console.log('test.txt successfully deleted');
}
catch (err) {
    console.log('Error:', err);
}

清单 3-39 。核心/fs/delete.js

var fs = require('fs');
fs.unlink('./test.txt', function (err) {
    if (err) {
        console.log('Error:', err);
    }
    else {
        console.log('test.txt successfully deleted');
    }
});

主要区别在于异步版本接受回调,如果有错误对象的话,就向其传递错误对象。我们在第二章中讨论了使用回调和错误参数的错误处理惯例。

我们在第二章中也看到,访问文件系统比访问 RAM 慢一个数量级。访问文件系统会同步阻塞 JavaScript 线程,直到请求完成。在繁忙的流程中,例如在 web 服务器场景中,最好尽可能使用异步函数。

关于fs模块的更多信息可以在 Node.js 官方文档(http://nodejs.org/api/fs.html)中在线找到。

操作系统模块

os模块提供了一些基本的(但至关重要的)操作系统相关的实用函数和属性。您可以使用require('os')调用来访问它。例如,如果我们想知道当前的系统内存使用情况,我们可以使用os.totalmem()os.freemem()函数。这些在清单 3-40 中进行了演示。

清单 3-40 。core/os/memory.js

var os = require('os');
var gigaByte = 1 / (Math.pow(1024, 3));
console.log('Total Memory', os.totalmem() * gigaByte, 'GBs');
console.log('Available Memory', os.freemem() * gigaByte, 'GBs');
console.log('Percent consumed', 100 * (1 - os.freemem() / os.totalmem()));

os模块提供的一个重要功能是关于可用 CPU 数量的信息,如清单 3-41 所示。

清单 3-41 。core/OS/CPU . js

var os = require('os');
console.log('This machine has', os.cpus().length, 'CPUs');

当我们讨论可伸缩性时,我们将在第十三章中学习如何利用这一事实。

实用程序模块

util模块包含许多有用的通用函数。您可以使用require('util')调用来访问util模块。要用时间戳将一些东西注销到控制台*,可以使用util.log函数,如清单 3-42 所示。*

清单 3-42 。core/util/log.js

var util = require('util');
util.log('sample message'); // 27 Apr 18:00:35 - sample message

另一个非常有用的特性是使用util.format函数进行字符串格式化。这个函数类似于 C/C++ printf函数。第一个参数是包含零个或多个占位符的字符串。然后,根据占位符的含义,使用剩余的参数替换每个占位符。流行的占位符是%s(用于字符串)和%d(用于数字)。这些在清单 3-43 中进行了演示。

清单 3-43 。核心/实用程序/格式. js

var util = require('util');
var name = 'nate';
var money = 33;

// prints: nate has 33 dollars
console.log(util.format('%s has %d dollars', name, money));

另外,util有几个函数来检查某个东西是否属于特定类型(isArrayisDateisError)。这些功能在清单 3-44 中进行了演示。

清单 3-44 。core/util/isType.js

var util = require('util');
console.log(util.isArray([])); // true
console.log(util.isArray({ length: 0 })); // false

console.log(util.isDate(new Date())); // true
console.log(util.isDate({})); // false

console.log(util.isError(new Error('This is an error'))); // true
console.log(util.isError({ message: 'I have a message' })); // false

在浏览器中重用 Node.js 代码

在我们学习如何在浏览器中重用 Node.js 代码之前,我们需要学习更多关于各种模块系统的知识。我们需要了解 AMD 的需求,以及它与 CommonJS 的区别。

AMD 简介

正如我们在本章开始时讨论的,Node.js 遵循 CommonJS 模块规范。当我们可以直接访问文件系统时,这个模块系统对服务器环境非常有用。我们第一次讨论了从 Node.js 中的文件系统加载模块是一个阻塞调用。考虑加载两个模块的简单情况,如清单 3-45 所示。

清单 3-45 。显示使用 CommonJS 加载两个模块的代码片段

var foo = require('./foo');
var bar = require('./bar');
// continue code here

在这个例子中,直到所有的foo.js都被加载后bar.js才被解析。事实上,Node.js 甚至不知道您将需要bar.js,直到foo.js被加载并且行require('./bar')被解析。在服务器环境中,这种行为是可以接受的,因为它被视为应用引导过程的一部分。在启动服务器时,您通常需要一些东西,然后这些东西会从内存中返回。

然而,如果在浏览器中使用相同的模块系统,每个require语句将需要触发一个到服务器的 HTTP 请求。这比文件系统访问调用慢一个数量级,并且不太可靠。加载大量模块会迅速降低用户在浏览器中的体验。解决方案是异步、并行和提前加载模块。为了支持这种异步加载,我们需要一种方式来声明这个文件将依赖于前面的./foo./bar,并使用回调来继续执行代码。已经有一个专门的规范叫做异步模块定义(AMD) 。AMD 格式的清单 3-45 中的相同示例显示在清单 3-46 中。

清单 3-46 。显示使用 AMD 加载两个模块的代码片段

define(['./foo', './bar'], function(foo, bar){
        // continue code here
});

define函数不是浏览器自带的。这些必须由第三方库提供。其中最受浏览器欢迎的是 RequireJS ( http://requirejs.org/)。

再次重申,浏览器与服务器启动有不同的延迟要求。这使得以异步方式加载模块需要不同的语法。require 调用的不同性质使得在浏览器中重用 Node.js 代码稍微复杂一些。在我们深入研究之前,让我们设置一个 RequireJS 引导应用。

设置要求 j

因为我们需要为 web 浏览器提供 HTML 和 JavaScript,所以我们需要创建一个基本的 web 服务器。我们将使用 Chrome 作为我们的首选浏览器,因为它可以在所有平台上使用,并且拥有出色的开发工具支持。这个示例的源代码可以在chapter3/amd/base文件夹中找到。

启动 Web 服务器

我们将使用server.js,这是一个非常基本的 HTTP web 服务器,我们将在第六章中自己编写。使用 Node.js ( node server.js)启动服务器。服务器将开始在端口 3000 上侦听来自浏览器的传入请求。如果你访问http://localhost:3000,服务器将尝试从与server.js相同的文件夹中提供index.html,如果它可用的话。

下载要求

可以从官方网站(http://requirejs.org/docs/download.html)下载 RequireJS。它是一个简单的 JavaScript 文件,可以包含在项目中。它已经存在于chapter3/amd/base文件夹中。

自举要求

在与server.js相同的文件夹中创建一个简单的index.html,内容如列表 3-47 所示。

清单 3-47 。amd/base/index.html

<html>
<script
    src="./require.js"
    data-main="./client/app">
</script>
<body>
    <p>Press Ctrl + Shift + J (Windows) or Cmd + Opt + J (MacOSX) to open up the console</p>
</body>
</html>

我们有一个简单的脚本标签来加载require.js。当 RequireJS 加载时,它查看加载了 RequireJS 的脚本标签上的data-main属性,并将其视为应用入口点。在我们的例子中,我们将data-main属性设置为./client/app,因此 RequireJS 将尝试加载http://localhost:3000/client/app.js

客户端应用入口点

当我们设置 RequireJS 来加载/client/app.js时,让我们创建一个client文件夹,并在该文件夹中创建一个app.js,它只是将一些东西记录到控制台,如清单 3-48 所示。

清单 3-48 。amd/base/client/app.js

console.log('Hello requirejs!');

现在,如果你打开浏览器http://localhost:3000并打开开发工具(按 F12),你应该看到记录到控制台的消息,如图 3-1 所示。

9781484201886_Fig03-01.jpg

图 3-1 。基本 AMD 样本

这是设置 RequireJS 的基础。该设置将用于本节的剩余演示。你只需要复制这个server.js + index.html + require.js + client/app.js组合,开始随心所欲的黑客攻击。

RequireJS 有更多的配置选项,我们鼓励您浏览在线提供的 API 文档。

玩 AMD

现在我们知道了如何启动一个 RequireJS 浏览器应用,让我们看看如何在模块中导入/导出变量。我们将创建三个模块:app.jsfoo.jsbar.js。我们将使用 AMD 的app.js中的foo.jsbar.js。该演示可在chapter3/amd/play文件夹中获得。

要从一个模块中导出某些东西,你可以简单地从define回调中返回它。例如,让我们创建一个导出简单函数的文件foo.js,如清单 3-49 中的所示。

清单 3-49 。amd/play/客户端/foo.js

define([], function () {
    var foo = function () {
        console.log('foo was called');
    };
    return foo; // function foo is exported
});

坦率地说,我们需要一个文件中的所有模块,文件的根包含一个对define的调用。要将app.js中的模块./foo./bar加载到同一个文件夹中,定义调用如清单 3-50 所示。

清单 3-50 。amd/play/client/app.js

define(['./foo', './bar'], function (foo, bar) {
        // use foo and bar here
});

define可以接受一个名为exports的特殊参数,其行为类似于 Node.js 中的exports变量。让我们使用这个语法创建模块bar.js,如清单 3-51 所示。

清单 3-51 。amd/play/client/bar.js

define(['exports'], function (exports) {
    var bar = exports.log = function () {
        console.log('bar.log was called');
    };
});

请注意,您只能使用exports来附加您想要导出的变量(例如,exports.log = /*something*/),),但是您不能将它分配给其他变量(exports = /*something*/),因为那样会破坏由 RequireJS 监控的exports变量的引用。这在概念上与 Node.js 中的exports变量非常相似。现在,让我们完成app.js并使用这两个模块,如清单 3-52 所示。

清单 3-52 。amd/play/client/app.js

define(['./foo', './bar'], function (foo, bar) {
    foo();
    bar.log();
});

如果您运行这个应用,您将得到如图 3-2 所示的期望结果。

9781484201886_Fig03-02.jpg

图 3-2 。app.js 中使用的 foo 和 bar

当我们查看 chrome 调试工具中的网络选项卡时,对模块使用这种替代(AMD)语法的真正好处变得显而易见,如图 3-3 中的所示。

9781484201886_Fig03-03.jpg

图 3-3 。基本 AMD 样本

可以看到foo.jsbar.js一下载完app.js就并行下载了,RequireJS 发现app.js因为调用define需要foo.jsbar.js才能发挥作用。

关于 AMD 的更多信息

以下是一些关于 AMD 的有用且有趣的事实,您应该了解这些事实以完善您的知识:

  • 模块被缓存。这与 Node.js 中缓存模块的方式类似,即每次都返回相同的对象。
  • 许多要定义的参数都是可选的,并且有各种方式来配置如何在 RequireJS 中扫描模块。
  • 您仍然可以使用一个require调用来有条件地加载特定的模块,这是 RequireJS 提供的另一个功能,如清单 3-53 所示。这个函数也是异步的,不同于require的 Node.js 版本。

清单 3-53 。展示如何在 AMD 中有条件地加载模块的代码片段

define(['./foo', './bar'], function(foo, bar){
        if(iReallyNeedThisModule){
                require(['./bas'], function(bas){
                        // continue code here.
                });
        }
});

这里的目标是给出如何使用 RequireJS 的快速概述,并理解浏览器不同于 Node.js。

将 Node.js 代码转换为浏览器代码

正如您所看到的,浏览器模块系统(AMD)和 Node.js 模块系统(CommonJS)之间存在显著的差异。然而,好消息是 Node.js 社区已经开发了许多工具来获取您的 CommonJS / Node.js 代码,并将其转换为与 AMD / RequireJS 兼容。最常用的(也是其他工具依赖的)是 Browserify ( http://browserify.org/)。

Browserify 是一个命令行工具,作为 NPM 模块提供。NPM 模块将在下一章详细讨论。现在,只要知道如果你按照第一章中的说明安装了 Node.js,你就已经有 npm 可用了。要在命令行工具上安装 Browserify,只需执行清单 3-54 中的命令。(注意:在 Mac OS X 上,你需要以 root 用户身份运行它(sudo npm install –g browserify)。

清单 3-54 。安装浏览器

npm install -g browserify

这将在全局范围内安装 Browserify(这个概念将在下一章中变得清晰),并使它在命令行上进一步可用。现在,如果您运行 browserify,您应该会看到如图图 3-4 所示的输出,表明安装成功。

9781484201886_Fig03-04.jpg

图 3-4 。在命令提示符下使用 browser ify

使用 browserify 最常见的方法是为 Node.js 模块指定一个入口点,并使用–o(--outfile)参数将该文件及其所有依赖文件转换为一个 AMD 兼容文件。和往常一样,让我们开始演示,获得一些实际操作经验。

浏览器验证演示

在本节中,我们将创建几个简单的 Node.js 模块,然后使用 Browserify 将它们转换为 AMD 语法并在浏览器中运行。这个例子的所有代码都在chapter3/amd/browserify文件夹中。

首先,我们将创建三个遵循 Node.js / CommonJS 模块规范的文件(代码在chapter3/amd/browserify/node文件夹中)。我们正在使用来自使用 CommonJS 的app.js ( 清单 3-57 )的foo.js ( 清单 3-55 )和bar.js ( 清单 3-56 )。您可以在 Node.js 中运行这段代码,看看它是否按预期工作。

清单 3-55 。amd/browserify/node/foo.js

module.exports = function () {
    console.log('foo was called');
}

清单 3-56 。amd/browserify/node/bar.js

exports.log = function () {
    console.log('bar.log was called');
}

清单 3-57 。amd/browserify/node/app.js

var foo = require('./foo');
var bar = require('./bar');

foo();
bar.log();

现在让我们转换这段代码,使它成为一个 AMD 兼容的模块。在命令行上,运行如清单 3-58 所示的命令。

清单 3-58 。将 app.js 转换为 AMD 模块的命令行参数

browserify app.js -o amdmodule.js

这会将app.js及其所有依赖项(foo.jsbar.js)转换成同一个文件夹中的单个 AMD 兼容模块amdmodule.js。最后一步,我们简单地从我们的客户端app.js ( 清单 3-59 )加载这个模块,以显示它可以在浏览器中工作。

清单 3-59 。amd/browserify/client/app.js

define(['../node/amdmodule'], function (amdmodule) {
});

现在如果我们启动服务器(server.js)并打开网页浏览器(http://localhost:3000),你会在 chrome 开发工具中看到console.log消息,如图图 3-5 所示。我们已经成功地将 Node.js 代码移植到浏览器中。

9781484201886_Fig03-05.jpg

图 3-5 。在浏览器中重用 Node.js/CommonJS 代码

需要注意的一点是,不可能将每个 Node.js 模块的都转换成浏览器模块。具体来说,依赖于只在服务器上可用的特性(如文件系统)的 Node.js 模块在浏览器中无法工作。

Browserify 有很多选项,也能够导航 NPM 包(node_modules)。您可以在http://browserify.org/在线了解 Browserify 的更多信息。

摘要

在本章中,我们讨论了一些重要的可维护性主题,为了成为一名成功的 Node.js 开发人员,您应该了解这些主题。我们仔细观察了require / module.exports的组合,让您对 Node.js 模块的原理及其简单性有了一个牢固的理解。然后我们讨论了几个核心的内置 Node.js 模块。(当我们了解事件、流和特定领域(如 TCP/HTTP)时,我们将更多地了解这些核心模块。)最后,我们讨论了 AMD 和 CommonJS 的区别,以及如何在浏览器中重用 Node.js 代码。

在下一章,我们将讨论 Node.js 的伟大之处之一——它的开源生态系统。开源 Node.js 项目包中有很多可用的包,我们将向您展示如何使用 NPM 来利用它们。