十三、附录 A:NodeJS 启动

在 web 开发中,希望有某种后端服务器。可用的服务器和语言列表非常广泛。但是有一个服务器产生了非同寻常的兴奋,它就是 Node.js,它让你可以使用相同的 JavaScript 进行前端和后端开发,并允许真正有趣的可能性。

这本书的目的是介绍脸书的前端用户界面框架,称为 ReactJS。本附录的目的是提供足够的后端来运行经过身份验证的服务器,虽然有很多好的选择,但 Node.js 在没有要求您处理新语言的附录的情况下也能工作。本附录中所做的基本工作涵盖的领域与您可能使用另一种服务器和后端语言的领域相当。

在本附录中,我们将涵盖以下主题:

  • Node.js 如何从嵌入中得到启示
  • 像 JavaScript 一样,Node.js 是如何拥有雷区的
  • 移植实用程序

但是我们先来看看 Node.js 和 INTERCAL。

Node.js 和 INSERT

insert,恰当地命名为没有可发音首字母缩略词的编译语言,是由普林斯顿学生唐·伍兹和吉姆·里昂于 1972 年首次宣布的。一种旨在讽刺编程语言中各种趋势和时尚的语言的原型例子,它可能更为人所知,因为它是一种语言的原型例子,不是为了容易使用,而是为了故意和不必要的难以使用。其你好,世界!代码包含完整、重复的应激损伤诱导 16 行;其传奇的 ROT-13 加密器/解密器(一个简单的用于 Perl 或 Unix shell 命令的单行代码)在alt.folklore.computers上被描述为“四页完全无法破译的代码。”嵌入最初是在 EBCDIC 中的穿孔卡片上发布的,EBCDIC 是一种被称为加密标准的字符编码。

被讽刺的一个趋势是 Edgser Dikjstra 的《走向被认为有害的陈述》,这部作品不仅仅是开创性的,可以说是计算机科学史上最重要的一篇文章。例如,Wags 说过,程序员就是被告知“去死吧”而被冒犯的人不是靠“地狱”而是靠Go to。一种嵌入变体(C-interpe)的前提是Go to语句确实被正确地认为是有害的,它们希望尽可能远离Go to语句——比软弱的 IF-THEN-ELSE 语句和while循环更远。他们提出了一个比 IF-THEN-ELSE 和 T5 循环(T6)更激进的脱离 T4 声明的方案。而Go to语句说“如果执行到达代码的这个点,转到代码的那个区域”,come from反义词说“如果执行到达代码的另一个区域,在代码的这个点切换并拾取东西。”

可能提出的建议如下:Node.js 的天才之处在于,我们在本章中为其提供了一个启动,它向后弯曲以基于come from语句或与它们非常相似的语句进行流量控制。现在,Node.js 也是一个可以用 JavaScript 编程的服务器,这一点没什么可轻视的,但是它已经完全盖过了所有其他可以用 JavaScript 编程的服务器。它的天才之处不仅仅来自于 JavaScript,而是来自于一个开发环境,当你能够以come from作为流量控制的主要工具来解决问题时,这个开发环境是最有效的。通常首选的术语是异步回调函数,而不是come from,但是当您意识到 Node.js 作为come from编程在默认情况下表现出色,并且比其竞争对手高出一个数量级的活生生的例子很有趣时,您将最好地使用 Node.js。

一个有 C 语言形成经验的人来到 Perl 可能会被告知,“除非你想到散列,否则你不会真正想到 Perl”,或者一个老派的 Java 程序员可能会被告知,“除非你想到闭包,否则你不会真正想到 JavaScript。”同样,来自任何其他主流网络服务器的人可能会被告知,“除非你想到的是come from风格的异步回调函数,否则你不会真正想到 Node.js。”

类型

自从匿名函数出现在 Lisp 中以来,它就一直很辉煌,并且是 JavaScript 中的一大特色。但是在处理 Node.js 回调时,可以考虑使用非嵌套的命名函数来替代匿名内部函数的深度嵌套层。

从技术上讲,在 Node.js 中使用异步回调函数是严格可选的。然而,强烈建议您,除非您正在使用 Node.js 的学习工具,例如优秀的“向您学习 Node.js,赢得更多胜利!”(这个标题明显暗指学习你一个哈斯克尔为伟大的好作为一个优秀的前任】,Node.js 学习工具是在 http://nodeschool.io 推广的工具——你应该记得 Knuth 的两条规则:

  • 规则 1(适用于所有程序员):不优化
  • 规则 2(仅限高级程序员):稍后优化

在 Node.js 的上下文中,这变成了:

  • 规则 1(适用于所有 Node.js 黑客):不要在异步方法可行的地方使用同步方法
  • 规则 2(仅适用于高级 Node.js 黑客):稍后添加任何同步功能

对于同步实现的代码示例,非 Node.js 人员可能看到的方式是,我们可以读取并打印一个文件,如/etc/passwd(在 Windows 上,不同的完整路径将是合适的;您可以使用记事本或您最喜欢的编辑器创建并保存一个):

var fs = require('fs');
var contents = fs.readFileSync('/etc/passwd');
console.log(contents);

通过come from异步回调功能实现:

var fs = require('fs'); 
fs.readFile('/etc/passwd', 'utf8', function(err, data) {
  if (err) {
    console.log('There was an error:');
    console.log(err);
    return;
  }
  console.log(data);
});

console.log()是否阻塞的问题与我们无关。

这是一个稍微复杂一点的你好,世界!程序为 Node,或者也许是一个刚刚结束的程序你好,世界!在 Node.js 中,简单如下:

console.log('Hello, world!');

让我们详细评论一下异步示例。

导入包的标准方式是调用require(),将结果保存在要用来访问包的变量中。fs包是自动附带 Node.js 的少数包之一,但是 Node.js 附带了通过Node 包管理器 ( npm )提供的一整套包,这个包管理器可能对于使用 Linux 包管理器的人来说看起来很熟悉。有了 npm,你可以搜索例如 Express.js,这将在本章中简单介绍。Express.js 在 Node.js 社区中很受欢迎,与 Node.js 配合得很好,有点像 Ruby 的 Rails 或 Python 的 Django。对 Express.js 的搜索可以是这样的:

npm search express

一旦您确定了您想要的软件包名称(或者您认为您想要的名称),您就可以安装它:

npm install express

在前面的代码中,fs.readFile()函数调用设置了come from行为。像其他异步调用一样,它有两个必需的参数:一个提供给fs.readFile()的基本参数(可能是一个数组)和一个回调函数。当调用这个函数时——而不是在读取文件时阻塞一段昂贵的时间——程序注册了一个读取具有指定参数的文件的请求,然后单个 Node.js 将它留在原地,并处理其他请求。这一点非常重要。程序不是在等待时阻塞什么也不做,而是服务于其他需求,在文件操作返回文件结果后(在此期间一直忙着塞进其他请求),然后come from无论在哪里,都服务于提供的回调函数。很难意外得到正确使用的 Node.js 来阻止,除非用一些会烧坏 CPU 的东西,而且以目前的 CPU 速度,一个无关痛痒的请求阻止 CPU 到成为问题的程度是很少见的(有兴趣的人可以使用 Node.js 通过诸如http://bitcoinjs.org/提供的工具来挖掘比特币,但是大概很少有人担心他们的 Node.js 服务器尽可能的高性能会让它在一旁挖掘比特币)。有一个旨在利用多核的集群模块,默认情况下,Node.js 在一个内核上的一个单线程进程中运行。但是,如果您怀疑您的用例是否极端到需要类似集群的东西来执行 Node 的默认性能结构,那么您可能还不需要集群。

Node 的工作方式(至少不使用集群)具有避免并发问题的额外优势,因为它是单线程的,事实上不是并发的。这是一件非常好的事情。并发性是一个棘手的问题。有非常精通并发的程序员,但是总的来说,并发应该被认为是有害的——一种潘多拉魔盒,长期困扰着大多数普通程序员。就并发性而言,可能有理由将使用不可变数据的纯函数式语言视为一个单独的案例,但在这里,我们将坚持说,Node.js 在默认情况下可以处理大量请求,而不需要开发人员处理棘手的并发性问题,这很好。

fs.readFile()的第二个参数是可选的,fs.readFile()允许有点不寻常。正常的异步调用看起来像标识符(数据,回调)。在这种情况下,第二个可选参数值得仔细研究。

参数是用什么编码从字节数组中生成字符串,给出的参数是正常的默认编码'utf-8',尽管这里有一点的诱惑,在这种情况下是在'ascii'上。这尤其是因为 Unix /etc/passwd文件比UTF-[anything]早了几十年。但我们会做好网民,用好'utf-8'

在我们的例子中,使用 UTF-8 和 ASCII 编码的行为可能是相似的。事实上,如果/etc/passwd像多年来许多/etc/passwd文件一样只包含 ASCII 字符,并且可能只支持 ASCII 字符,那么输出将是相同的。但是,如果不指定某种编码,它们中的任何一个都将是不同的野兽。没有进一步的改变,回调将以字节给出,而不是普通意义上的任何 JavaScript 字符串。这里,我们有一个关于 Node.js 的重要线索

浏览器之间一直在为最快的 JavaScript 引擎进行军备竞赛,Node.js 采用了谷歌 Chrome 的 V8 引擎(Node.js 的 forks 可能会使用更新的东西),并以某些方式对其进行了扩展,以便它能够作为通用运行时环境运行,包括很好地适应作为网络服务器。这包括添加客户端 web 浏览器 JavaScript 中不存在的几个扩展。将套接字作为服务器来处理的能力就是一个例子,正如前面所讨论的那样,这种能力与输入/输出一起,是沿着异步模型出色地发展起来的。

另一个差距与二进制数据有关。在撰写本书时,标准浏览器 JavaScript 并没有真正提供处理二进制数据的直接方法。虽然在下面的代码中可能有明显的幕后处理二进制数据的工作,但没有明确的方法说“我想要 128(八位字节)字节的交替的 1 和 0,从 1 开始”:

<img id="portrait" />
<script>
(document.getElementById('portrait').src =
'http://cjsh.naimg/portrait.png');
</script>

Node.js 扩展了 V8 处理适当的二进制数据的能力,而久经考验的真正的 JSON 由新的二进制友好的 BSON 补充。二进制数据的处理是低级的,对于它的 C 类特性来说可能太低级了。例如,当一个 C 程序员完全从使用malloc()(“内存分配”)切换到使用calloc()(“清除的内存分配”)时,生产力的提高和挫败感的降低就来了。malloc()函数用内存前一个主人留下的碎片分配一个原始内存块,如果你不能正确初始化内存的任何部分,就会产生奇怪而邪恶的魔法效果。

calloc()函数分配一个原始的内存块,并用零填充任何先前的内容。还记得皮特·亨特说过的话吗,“我宁愿被预测,也不愿被正确预测?”不再直接使用malloc()而使用calloc()是 C 程序员选择可预测而不是正确的主要方式。然而,出于错误的优化考虑(不擦除分配的字节内存会快几分之一秒),据我所知,Node.js 只提供了 C malloc()的等价物,没有任何提供的calloc()等价物。幸运的是,Node.js JavaScript(或者说 C 语言)非常强大,移植calloc()功能是一个简单的练习。只需制作一个包装器,处理 Node.js 字节分配处理的所有内容,然后将所有位都设置为 0,并在分配字节时专门使用这个包装器。

回到立即高亮显示的代码,至少在 Node.js 思维中,从文件或网络中读取不会返回字符串。它返回二进制字节。现在,这些字节可以很容易地通过给定的编码进行转换,如果你提供你想要的编码,fs.readFile()将会给你一个合适的字符串,而不仅仅是字节。但是让我们看一些类似于之前的代码。Node.js 和很多好的环境一样,提供了一个读-评估-打印-循环 ( REPL )来尝试(调用 Node 可执行文件而没有任何后续参数会激活 REPL)。来自 REPL:

> fs.readFile('/etc/passwd', function(err, data){console.log(data)});
undefined
> <Buffer 23 23 0a 23 20 55 73 65 72 20 44 61 74 61 62 61 73 65 0a 23 20 0a 23 20 4e 6f 74 65 20 74 68 61 74 20 74 68 69 73 20 66 69 6c 65 20 69 73 20 63 6f 6e 73 ...>

文件中的各种字节用十六进制代码表示。

回到上一个代码示例,function(err, data)回调签名是编程契约的正常回调签名。回调最终应该被调用,而且可能调用得非常快。这应该用“truthy”err来完成,在这种情况下,回调应该有选择地采取步骤来响应错误反馈中包含的任何信息,而不需要进一步返回,或者用 null err 来完成,在这种情况下,函数的前提条件得到满足,回调应该采取任何适当的动作来接收所请求的数据。前面的代码说明了这种模式:检查是否为空err,通过记录诊断信息对其进行操作,如果 err 为空,则打印文件内容。

警告–node . js 及其生态系统很热,热到让你严重烧伤!

当我是一名教师助理时,我被告知的一个非显而易见的建议是不要告诉学生某件事是“容易的”原因回想起来有些明显:如果你告诉人们某件事很容易,一个看不到解决方案的人最终可能会感到(甚至更加)愚蠢,因为他们不仅不知道如何解决问题,而且他们愚蠢到无法理解的问题也是一个容易的问题!

有一些问题不仅仅困扰来自 Python/Django 的人,如果你改变了什么,它会立即重新加载源代码。对于 Node.js,默认行为是,如果您进行了一次更改,旧版本将继续保持活动状态,直到时间结束或您手动停止并重新启动服务器。这种不恰当的行为不仅惹恼了皮托尼斯塔斯;这也激怒了提供各种变通方法的原生 Node.js 用户。StackOverflow 的问题“Node.js 中文件的自动重新加载”,在撰写本文时,已有超过 200 个 Auto 和 19 个答案;编辑将用户导向保姆脚本,Node 管理器,主页位于http://tinyurl.com/reactjs-node-supervisor。这个问题给新用户提供了一个很好的机会,让他们觉得自己很愚蠢,因为他们认为自己已经解决了这个问题,但是旧的、有缺陷的行为完全没有改变。而且很容易忘记弹起服务器;我做过很多次了。我想传达的信息是,“不,你不傻,因为 Node.js 的这种行为咬了你的背;只是 Node.js 的设计者认为没有理由在这里提供适当的行为。一定要试着处理它,也许从 Node 主管或其他解决方案那里得到一点帮助,但是请不要觉得自己很愚蠢。你不是有问题的人;问题出在 Node.js 的默认行为上。”

经过一番辩论后,这一部分被保留了下来,正是因为我不想给人一种“这很容易”的印象我在做事情的时候反复切手,我不想磨平困难,让你相信:让 Node.js 及其生态系统正常运行是一件简单的事情,如果对你来说也不简单,你就不知道自己在做什么。如果您在使用 Node.js 时没有遇到令人讨厌的困难,那就太好了。如果你这样做了,我希望你不要一走了之就觉得“我很笨”。我一定是有问题。”如果你在与 Node.js 打交道时遇到了令人讨厌的惊喜,那你就不傻了。这不是你!是 Node.js 及其生态系统!

接下来,我们将探索一个示例项目,一个远程的快速和脏的基于本地存储的持久性的等价物,它在第 11 章JavaScript 中的演示函数式反应式编程中有所涉及,并有一个真实的示例第四部分——添加一个草稿栏并把它放在一起。这是一个成功,但这是太难了,一路上有太多的失误。我有时会比较 Python 和 JavaScript 但是,与 Python 的 Django 相比,为什么 JavaScript 的 Node.js 真的很恶心,这可能值得花点时间来研究一下。

多年的经验之后,我对 Django 的第一次体验是一种感觉,它是一个出色的动力工具,出于某种奇怪的原因,它意外地没有被包含在 Python 的标准库中。事实上,这有一个很好的理由,一个既不需要批评 Python 也不需要批评 Django 的理由:正如 Python 的“仁慈的终身独裁者”所观察到的那样,当标准库“死了”时,而不是当它还在增长时,你就把它放进了标准库中。Django 还在成长,还在变好。因此,Django 不属于 Python 的标准库,不管它有多好。

在许多情况下,有一个最少惊讶的原则,一旦你开始熟悉 Python,它不会经常给你不愉快的惊喜。Django 确实带来了一些惊喜,比如它的模板系统,这在 ASP 和 JSP 时代是一个引人注目的命题。(现在它已经有了 15 分钟的名气,甚至 Python/Django 开发人员也是从用更强大的东西替换模板系统开始的。对 ReactJS 来说,从本质上反其道而行之是完全正确的选择。)但矛盾的是,Django 和 ReactJS 都提供了模板,反映了《新黑客词典》中定义的火星技术的天才:

【TMRC】一种有远见的品质,使人能够忽略标准的方法,提出一个完全出乎意料的新算法。从一个从未有人想到过的另类角度来研究一个问题,但回想起来,这是完全有意义的。对比一下格罗克和禅宗。

使用 Node.js 的工作感觉一点也不像使用 Python,甚至不像使用 ReactJS。它更令人沮丧,更困难,有更多没有意义的事情。

这里有一个例子:在最初研究的时候,我打算使用 passport.js 来卸载身份验证的脏活。我原本打算使用脸书身份验证,但指令涉及在脸书开发人员网站上创建一些东西,并从脸书应用中提取信息。甚至在浏览了脸书开发者网站并询问“passport.js 说要从我在脸书开发者网站上的应用中获取 XYZ 信息”之后,我也完全没有得到任何及时的回答。

缩小我的野心,我决定使用 passport.js 中最基本的正确身份验证——用户名和密码——直到我了解到作为用户名和密码支持提供的东西几乎完全没有用。

之所以没用是因为就像创建、读取、更新、销毁 ( CRUD )一样,提供了一个基本职责的枚举——任何用于处理数据和记录的严肃而完整的工具(无论是 SQL 数据库、NoSQL 数据库的任何条带、保存在编程环境中的腌制数据、编辑器, 或者电子邮件客户端)—主流帐户管理中需要涵盖一组基本的基础,无论是用户名/密码还是任何更新、更温和的替代方案,都可以让用户跟踪另一个登录和密码。 虽然个别网站可能会选择退出某些功能,但提供帐户管理 CRUD 的重复和基本功能包括以下内容:

  • 允许用户创建新帐户
  • 允许用户使用现有帐户登录
  • 在没有通过电子邮件向用户发送未加密密码的情况下,替换丢失的密码
  • 可能是网站管理成员的一组扩展功能,例如帐户审核(如果需要)、锁定和解锁帐户以及帐户删除

passport.js 的功能涵盖了这些基础中的唯一一个,那就是使用一个已经存在的帐户登录,这个帐户是通过某种我无法确定的方式创建的,并且通过了成功或失败的身份验证。几十年前,字节杂志 4 月 1 日的一期刊登了一则关于 CRUD 的更病态的不完整的消息,当时有人宣传了一项关于只写存储器的非常好的交易。

现在我们可能会顺便注意到,严格来说,支持 100%的 CRUD 并不是唯一可能的方法。几年前,写一次,读很多次 ( WORM )磁盘驱动器花了一些时间在聚光灯下。虽然可能没有配备真正的 WORM 驱动器的现代笔记本电脑,但 ClojureScript 在提供 WORM 数据方面付出了巨大的努力。在这种情况下,WORM 意味着数据被设计为排除更新(尽管您可以足够容易地制作修改过的副本),而 Deletion 被保留给垃圾收集:出于对 CRUD 的完全支持,ClojureScript 的 WORM 数据只允许未受影响的数据创建和读取。ClojureScript 反映了一个经过深思熟虑的决定,即在完全支持 CRUD 会容易得多的情况下提供 WORM 数据。这一决定现在值得明显的尊重。

现在,缺乏完全 CRUD 支持的等价物并不是身份验证的世界末日,因为至少还有一个团体更恰当地使用了“身份验证 CRUD”。Stormpath 为 Node、Python、Java 和 REST 做广告。他们的一个开发人员重写了我的身份验证代码,让我使用他们的系统。虽然这可能只是对一个可能涵盖他们产品的作者很好,但是 Stormpath 包含甚至不是一个适当的集成挑战;真的很简单。现在需要声明的是,Stormpath 不是开源的,而是一个免费增值的 SaaS。功能齐全、免费且“无需信用卡”的开发人员层每月有 100,000 次 API 调用的配额,他们估计用户登录大约使用三次 API 调用。他们肯定有盈利的动机,但是如果你有足够的流量让你需要付费的服务,你不应该关心你必须支付给他们的琐事。这个系统给人一种有点年轻的总体印象,人们在解决剩下的问题,但确实有人礼貌地告诉他们一些不成熟的事情,这个问题很快得到了解决。

另一个基本困难围绕着数据库。有一个很好的例子说明 MongoDB 很重要,再加上 mongoose 的“从 Node.js 访问 MongoDB”包,值得学习。在本章的准备过程中,顶级搜索教程证明了如何创建一个模式并在其中保存一些东西,但是对于如何有效地接近已经存在所有必要模式的数据库留下了猜测。随后的工作发现了一个现有的堆栈溢出解决方案,该解决方案似乎将数据库 CRUD 与已经存在的模式和数据库一起覆盖。在放弃之前,我可能差一点就被解雇了,但我几乎从一开始就打算使用 mongose/MongoDB 进行数据库工作,而且我还没有达到熟练程度。

另一个看似完美的数据库,也可能是可赎回的,是 HTML5 键值存储的服务器端实现。这种方法的主要优势是大多数优秀的前端开发人员都非常了解的应用编程接口。就此而言,这也是一个大多数不太好的前端开发人员都足够了解的应用编程接口。但是使用node-localstorage包,虽然不提供dictionary-syntax访问(您可能想要使用localStorage.setItem(key, value)localStorage.getItem(key),但不使用localStorage[key]),但是实现了完整的 localStorage 语义,包括默认的 5 MB 配额。为什么呢?服务器端 JavaScript 开发人员是否需要自我保护?

对于客户端数据库功能,每个网站 5 MB 的配额对于让开发人员使用它来说确实是一个慷慨而有用的喘息空间。您可以设置一个更低的配额,并且仍然为开发人员提供比跛行和 cookie 管理不可估量的改进。5 MB 的限制并不能很快适应大数据客户端处理,但是有一个非常慷慨的津贴,足智多谋的开发人员可以用它来做很多事情。另一方面,5 MB 在最近任何时候购买的大多数磁盘中都不是特别大的一部分。这意味着,如果你和一个网站对磁盘空间的合理使用意见不一,或者如果一个网站只是贪图便宜,它并不会让你付出太多,你也不会面临硬盘被淹没的危险,除非你的硬盘已经太满了。如果平衡少一点或多一点,我们可能会更好,但总的来说,这是解决客户端环境内在紧张关系的一个不错的解决方案。

然而,可以温和地指出,当您为服务器编写代码时,您不需要任何额外的保护来使您的数据库超过可容忍的 5 MB 大小。大多数开发人员既不需要也不希望工具充当保姆,保护他们不存储超过 5 MB 的服务器端数据。此外,这个 5 MB 配额是客户端的黄金平衡,在 Node.js 服务器上有点傻。

此外,对于一个面向多个用户的数据库(如本附录所述),可能会有点痛苦地指出,它不是每个用户帐户 5 MB,除非您为每个帐户上的列表创建一个单独的数据库。它在所有用户帐户之间共享 5 MB。这可能会变得痛苦,如果你去病毒!

文档说明配额是可定制的,但是一周前给开发人员的一封询问如何更改配额的电子邮件没有得到回复,同样的问题还有一个堆栈溢出问题。我能找到的唯一答案是在 GitHub CoffeeScript 源代码中,它被列为构造函数的可选第二个整数参数。这很简单,您可以指定一个等于磁盘或分区大小的配额。但是,除了移植一个没有意义的特性之外,该工具的作者也完全没有遵循一个非常标准的惯例,即对于一个变量或函数,将 0 解释为“无限制”,其中一个整数用于指定相关资源使用的最大限制。处理这一缺陷的最好方法可能是指定配额为无穷大:

if (typeof localStorage === 'undefined' || localStorage === null)
  {      
  var LocalStorage = require('node-localstorage').LocalStorage;
  localStorage = new LocalStorage(__dirname + '/localStorage',
    Infinity);
  }

在我的研究中,类似的业余粗糙感不断出现在浮出水面的元素中。Express.js 比 Node.js 水平更高,但就搬起石头砸自己的脚的方式而言,Node.js 比我最近使用的任何其他技术都更接近于提供 C 的方式。c 代表那些喜欢在搬起石头砸自己的脚之前先装子弹的。

一个示例项目——我们的 Pragmatometer 的服务器

让我们朝着一个简单的项目努力。我们将创建一个通用的服务器后端,可以为本书最后几章中介绍的 Pragmatometer 项目的修改提供服务,该项目通过在 HTML5 localStorage 中本地保存和恢复一些 JSON 字符串来处理持久性。我们将在一个可以提供静态内容的服务器上工作,就像已经开发的一样,提供一个保存或恢复字符串和识别密钥的应用编程接口,并处理基本的身份验证和帐户管理。客户端编程应该不会比以前更有趣了,基本上是通过将保存到本地存储交换到我们的远程 Node.js 服务器。

我们将使用几种技术。例如,与 Stormpath 相比,在 Express.js 中工作最受关注。Stormpath 似乎没有因为发明了一些全新的、原创的或令人惊叹的东西,或者因为在身份验证机制上的突破而受到表扬。他们可能会因为解决了一个众所周知的问题而受到表扬,以至于你的一大堆繁忙的工作将被取消。添加 Stormpath 既小又不显眼。大多数用户不会将它作为一个平台来构建一些伟大的作品。因此,我们将非常重视 Express.js(并让我们的客户与 Express.js 对话),这是一个可以合作的平台。在框架的网站上,Express.js 被宣传为“Node.js 的一个快速的、非个性化的网络框架”,他们几乎实现了他们所吹嘘的东西。

我们将需要建立一个服务器,但也需要为第 8 章到第 11 章的 Pragmatometer 项目改变客户端。有save()restore()功能,它们将被改变和扩展。

通过npm install express安装 Express.js。然后使用express [the directory name for your project]创建一个快递项目。你会有一个充实的框架。您可以将包添加到package.json文件,并运行npm install来填充您的本地副本。将会有一个公共的或者静态的目录,你可以使用,并且routes/index.js以一种熟悉其他框架的人可能感觉像家一样的方式处理路由。

客户端准备

第八章到第十一章js/目录下的所有内容都被移到public/javascripts中。变更的全部细节将在网站上公布。在这里,我们将save()restore()功能从(客户端)特定于本地存储调整为保留本地存储,以略微提高速度,但从远程服务器恢复并保存到远程服务器。在这种情况下,服务器是用 Express.js 构建的 Node.js 服务器,但是它本质上可以是任何服务于相同的、简单的和隐式 API 的服务器。

通常,对于 ReactJS,对象的状态是在setInitialState()调用中设置的。理论上,我们可以通过加载 Ajax 调用的同步等价物来保留相关的语义,但是也可以填充一个存根,然后提供一个回调来真正启动事情。Ajax 调用成功返回后,用于填充对象状态的函数是populate_state():

  var populate_state = function(state, new_data) {
    for (field in new_data) {
      if (new_data.hasOwnProperty(field)) {
        state[field] = new_data[field];
      }
    }
  state.initialized = true;
  }

restore()函数有点复杂,但那是因为它是为了构建感知的响应层而编写的。它发出一个 Ajax 调用,将状态设置为已初始化并将state.initialized标记为false。它还可以从 JSON 恢复(如果有保存的话)。它检查本地存储是否可用,如果不可用,则进行适度的降级,这可能是历史的,因为 ReactJS 据称只适用于提供本地存储的浏览器(IE8 及更高版本)。尽管如此,它还是提供了一个我们可以如何进行适度降级的例子:

  var restore = function(identifier, default_value, state, callback) {
    populate_state(state, default_value);
    state.initialized = false;
    var complete = function(jqxhr) {
      if (jqxhr.responseText === 'undefined' || jqxhr.responseText.length &&
        jqxhr.responseText[0] === '<') {
        // We deliberately do nothing.
      } else {
        populate_state(state, JSON.parse(jqxhr.responseText));
      }
      callback();
      state.initialized = true;
    }
    jQuery.ajax('/restore', {
      'complete': complete,
      'data': {
        'identifier': identifier,
        'userid': userid
      },
      'method': 'POST'
    });
    if (Modernizr.localstorage) {
      if (localStorage[identifier] === null || localStorage[identifier]
        === undefined) {
        return default_value;
      } else {
        return JSON.parse(localStorage[identifier]);
      }
    } else {
      return default_value;
    }
  }

作为范围的限制,无论是前面代码中的restore()函数的实现,还是下面代码中的save()函数,都不能解决面对失败的 Ajax 调用的弹性。解决这一问题的一种方法是检查失败并继续重试,重试之间的延迟呈指数级增加,以成为一个好网民,而不会给网络增加持续的大量流量。这种模式(大致)在高层次上被 Gmail 遵循,在低层次上被烘焙成 TCP/IP。对于我们的实现,任何失败的 Ajax 调用可能没有传达的信息都应该在键值存储中重新可用,除非有后续的更新,在这种情况下,这两个更改通常都会被保存。

save()函数稍微简单一点,但它代表了硬币的另一面:进行一个 Ajax 调用来保存/恢复,并保存到 localStorage 和从 local storage 恢复,作为一个近似值,然后才可用:

  var save = function(identifier, data) {
    if (Modernizr.localstorage) {
      localStorage[identifier] = JSON.stringify(data);
    }
    jQuery.ajax('/save', {
      'data': {
        'data': JSON.stringify(data),
        'identifier': identifier,
        'userid': userid
      },
      'method': 'POST'
    });
  }

当我们从本地存储中提取数据时,我们试图阻止用户输入数据。这是因为在不可预测的比赛条件下,当来自 Ajax 调用的数据返回时,这些数据会遭到破坏。换句话说,在从 Ajax 恢复数据之前,用户无法添加任何输入(即使已经从 localStorage 恢复了一个值)。这尤其意味着提交按钮被禁用,目前,回调功能被赋予restore()的唯一应用是启用已被禁用的提交按钮。对于日历来说,render()方法有一个禁用的提交按钮(您可以更加纯粹,禁用所有输入字段,但是禁用提交按钮足以防止用户数据被比赛条件破坏):

    render: function() {
      var result = [this.render_basic_entry(
        this.state.entry_being_added)];
      if (this.state.entry_being_added &&
        this.state.entry_being_added.hasOwnProperty('repeats') &&
        this.state.entry_being_added.repeats) {
        result.push(this.render_entry_additionals(
          this.state.entry_being_added));
      }
      return (<div id="Calendar">
        <h1>Calendar</h1>
        {this.render_upcoming()}<form onSubmit={
        this.handle_submit}>{result}
        <input type="submit" value="Save" id="submit-calendar"
          disabled="disabled" /></form></div>);
    },

日历的getInitialState功能只安排一个数据的裸存根同步到位。Ajax 调用返回后,会给它一个更合适的值,并重新启用禁用的保存按钮,因为这里不再考虑竞争条件:

    getInitialState: function() {
      default_value = {
        entries: [],
        entry_being_added: this.new_entry()
        };
      restore('Calendar', default_value,
        default_value, function()
        {
        jQuery('#submit-calendar').prop('disabled', false);
        });

客户端有几个比较详细,但也不是特别难。例如,我们添加了一个注销链接(CSS 位于右上角),以及从键值存储中擦除帐户数据的 JavaScript 行为(不调用通常的preventDefault()方法,因为我们不想阻止默认行为):

  jQuery('#logout').click(function() {
    if (Modernizr.localstorage) {
      localStorage.removeItem('Calendar');
      localStorage.removeItem('Todo');
      localStorage.removeItem('Scratch');
    }
  });

服务器端

当我们需要包时,我们应该将它们添加到我们的package.json文件中。这样做的一种方式是倒退。执行 npm 安装 XYZ,然后在package.json文件的“依赖项”下添加一行,指定“XYZ”:~ 1 . 2 . 3”并记录安装的版本号。目前包含的依赖项如下:

{
  "name": "Pragmatometer",
  "version": "0.0.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "body-parser": "~1.13.2",
    "connect-flash": "~0.1.1",
    "debug": "~2.2.0",
    "ejs": "~2.3.2",
    "express": "~4.12.4",
    "express-stormpath": "~1.0.5",
    "jade": "~1.9.2",
    "morgan": "~1.5.3",
    "serve-favicon": "~2.2.1",
    "stormpath": "~0.10.0"
  }
}

我们在https://stormpath.com/创建一个账户,可能是一个免费的开发者账户(除非你知道你需要更多),并在app.js中指定各种细节。该设置使用类似 HTML 的 EJS,而不是类似 Markdown 的 Jade 进行查看:

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.engine('html', require('ejs').renderFile);
app.set('view engine', 'ejs');

app.use(logger('dev'));
// uncomment after placing your favicon in /public
app.use(favicon(__dirname + '/publimg/favicon.ico'));
app.use('/public', express.static(path.join(__dirname, 'public')));

// Authentication middleware.
app.use(stormpath.init(app, {
  apiKeyId: '[Deleted]',
  apiKeySecret: '[Deleted]',
  application:
    'https://api.stormpath.com/v1/applications/[Deleted]',
  secretKey: '[Deleted]',
  sessionDuration: 365 * 24 * 60 * 60 * 1000
  }));

app.use('/users', users);

除了一个之外,所有标记为[Deleted]的物品都是你在风暴路径上设置的物品。有些人建议在制作自己的秘密钥匙时要尽量聪明;不要!在 Mac、Unix、Linux 或 Cygwin (Cygwin 可从http://cygwin.org免费获得,在 Windows 下运行)下,调出命令提示符,键入以下行:

python -c "import binascii; print binascii.hexlify(open('/dev/random').read(1024))"

这将为您带来一千字节的加密强随机数据,这些数据被编码为易于复制和粘贴。

这里有一个关于卫生的注意事项:建议的做法是非常小心地使用您的密钥,特别是不要将其包含在版本控制中。取而代之的是,把它放在你的主目录下的一个点文件目录中,拥有不允许任何人使用它的权限。

大概其中工作量最大的一个文件就是routes/index.js。我们引入了几个依赖项,包括一个能够从 Ajax 获取数据的主体解析器,它将 POST JSON 保存在请求的主体中:

var body_parser = require('body-parser');
var json_parser = body_parser.json();
var express = require('express');
var stormpath = require('express-stormpath');

我们包括 localStorage,指定 Infinity 作为我们的配额,然后为键中的字符提供一个消毒程序。这种特殊的消毒剂保持字母数字字符不变,与应用的其他部分一起足以确保它不会发生密钥冲突。它还确保字符在排除冒号的白名单上。这允许我们创建名称类似于username:component-name的密钥,在冒号上执行字符串分割,并且总是在第零个槽中获得用户名,在第一个槽中获得组件名:

var sanitize = function(raw) { 
  var workbench = []; 
  for(var index = 0; index < raw.length; ++index) {
    if ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.'
      .indexOf(raw[index]) !== -1) {
      workbench.push(raw[index])
    }
  }
  return workbench.join('');
}

路由器的工作方式对于以前几乎在任何环境中见过路由器的用户来说都应该是熟悉的。虽然路由和非路由功能将混合使用,但路由器会被创建并连接到前两条路由:

var router = express.Router();

router.get('/', stormpath.loginRequired, function(request, response) {
  response.render('index.ejs');
});

router.post('/', stormpath.loginRequired, function(request, response) {
  response.render('index.ejs');
});

所包含的stormpath.loginRequired,一旦包含了 Stormpath,就真的是你获得一个视图以进行登录保护所需要的全部。我们接着定义两个非视图函数:用于特定用户的save()restore()键的函数:

var save = function(userid, identifier, value) {
  localStorage.setItem(sanitize(userid) + ':' + sanitize(identifier), value);
  return true;
}

var restore = function(userid, identifier) {
  var value = localStorage.getItem(sanitize(userid) + ':' + sanitize(identifier));
  if (value) {
    return value;
  } else {
    return 'undefined';
  }
}

我们添加了路由来服务 POST Ajax 请求。如果我们想增加对 GET 或其他动词的支持,可以调用router.get()等:

router.post('/restore', json_parser, function(request, response, next) {
  var result = restore(request.user.href, request.body.identifier);
  response.type('application/json');
  response.send(result);
});

router.post('/save', function(request, response, next) {
  var success_or_failure = save_identifier(request.user.href,
  request.query.identifier,
    request.query.data);
  response.type('application/json');
  response.send(String(success_or_failure));
  });

然后有一个样板行,我们保持不变:

module.exports = router;

我们还对静态数据使用了 Express.js 的层次结构;修改后的index.ejs来自不同于我们早期 js/的地方:

    <script
      src="//cdnjs.cloudflare.com/ajax/libs/react/0.13.1/react-with-addons.js">
    </script>
    <script
      src="//cdnjs.cloudflare.com/ajax/libs/showdown/1.0.2/showdown.js">
    </script>
    <script src="//cdn.ckeditor.com/4.4.7/basic/ckeditor.js"></script>
    <script src="//code.jquery.com/jquery-1.9.1.js"></script>
    <script src="/public/javascripts/json2.js"></script>
    <script src="/public/javascripts/modernizr.js"></script>
    <script src="/public/javascripts/site.js"></script>

就这样!我们在电子资源包中提供全部细节。现在我们已经提供了一个带有帐户管理的服务器端键值存储。

总结

在考虑这个附录时,我考虑的问题之一是“JavaScript 加 Node.js 还是 Python 加 Django?”这本书的重点是 ReactJS 的前端,而后端的重点只是为了有足够的东西来支持前端。我自然认为 Python 是如此容易,即使对新来者也是如此,Django 也是如此容易(同样,即使对新来者也是如此),以至于即使引入了一种新的语言,带有身份验证的基本键值存储也应该是一个易于阅读和编写的附录。然而,作者当时认为我会走 JavaScript 加 Node.js 的高道路,这是每个人都想加入的组合,从那以后,我一直在为他不提供 Python 加 Django 附录的决定买单。

当然,捆绑包中提供的代码是免费提供的,您可以从中获取任何里程,而不会违反 Packt Publishing 的许可。但是,实现带有账户管理的键值存储的基本任务可能是本科生的作业水平。从任何意义上来说,它都不能证明任何服务器所提供的奇迹的顶峰。

现在,Node.js 确实提供了奇峰。但是,这里没有探讨这些,因为目标是提供足够的“Node.js plus Express.js”来创建第 8 章到第 11 章中介绍的 Pragmatometer 项目的基于服务器的改编。此外,鉴于所有项目的热情程度和纯粹的工作时间,可能需要一年、两年或三年时间来大幅缓和在撰写本书时对不成熟生态系统的任何评论。5 年后,说“2015 Node.js 生态系统有几个雷区”可能真的有意义。2020 Node.js 生态系统有多个天堂。”

但是,像 passport.js 那样发布一个简单的动画,在passport.authenticate('twitter')passport.authenticate('google')passport.authenticate('facebook')等之间轻松滑动,然后让用户搜索并详细询问 passport.js 处理用户名-密码认证的方法,允许用户创建一个新的帐户,这是不可能的。这是非常不合适的,而且在 Node.js 生态系统中不止一次发生过。从找到一个拥有光滑网站的 Node.js 工具到激活“你好,世界!”功能水平达到了 50%的成功率。它代表了一个比我在整个网络历史上看到的任何东西都大的鸿沟。

我可以看到人们在想,不完全是因为我如何让待办事项列表中的每个项目都有多种状态,所以我很聪明,而是因为我很实际,总的来说,这本书大大减少了读者了解 ReactJS 的工作量。然而,如果人们告诉我我很聪明,我会感到困惑,因为我想到了制作一个经过身份验证的键值存储,如本附录所述。这一成就根本不是因为我设法让一些技术作为一个经过认证的键值存储工作——这是一项与本科生作业不相上下的任务——而是因为它是在一个与嵌入持续存在的环境中完成的。

人们不必要地搬起石头砸自己的脚,因为他们一直在使用 JavaScript 作为一个整体,而让 JavaScript 成为一种受人尊敬的语言的关键是道格拉斯·克洛克福特所说的话,本质上:

“JavaScript 作为一种语言,有一些真的好的部分,也有一些真的不好的部分。这里是好的部分。忘了那里还有别的东西吧。”

也许热门的 Node.js 生态系统会长出自己的“道格拉斯·克洛克福特”,他会说:

Node.js 生态系统是一个编码的狂野西部,但也有一些真正的瑰宝可以发现。这是一张路线图。以下是几乎不惜任何代价都要避免的领域。这里有一些在任何语言或环境中都可以找到的收入最高的地区。

也许其他人可以把这些话当作一个挑战,跟随克罗克福德的脚步,为 Node.js 及其生态系统写下好的部分和/或更好的部分。我会买一本!