JavaScript-反应式编程-全-

JavaScript 反应式编程(全)

原文:zh.annas-archive.org/md5/67A6EE04B94B64CB5365BD89131EE253

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

印象派画家克劳德·莫奈曾经著名地说过:“莫奈只是一双眼睛,但是多么美的一双眼睛!”今天,我们可以类似地说:“ReactJS 或者如果你愿意,"ReactJS 只是一个视图,但是多么美的一个视图!"

ReactJS 既没有意图也没有野心成为一个完整的通用 Web 框架。它甚至不包括用于 Ajax 调用的工具!相反,意图是您将使用适合应用程序不同方面的技术,并使用 ReactJS 的强大工具来进行视图和用户界面开发。

函数式反应式编程一直是一个极其高不可攀的果实,其纯数学期望对于工作来说是一个禁区。但是现在有了 ReactJS!一个没有特别深厚数学背景的资深 C++程序员——我说这话是为了挑选一类程序员的形象,他们在 Stack Overflow 上一直说他们不懂函数式反应式编程——是一个有很大机会使用 ReactJS 完成真正工作的程序员。

这本书是关于 ReactJS 的,这是一个简单而小巧的技术,尽管如此,它让庞大的团队在网页的不同组件上合作而不会互相干扰,但又没有一丝官僚主义的痕迹。再加上一些自由的仙女粉。

本书涵盖的内容

[第一章, 介绍和安装,提供了对不同编程范式的一览,每种范式都有其优势,并介绍了函数式编程、反应式编程和函数式反应式编程的三位一体。

第二章, 核心 JavaScript,涵盖了 JavaScript 的一些更好的领域,并省略了雷区,这要感谢 Douglas Crockford,即使不完全同意。就你使用的 JavaScript 部分而言,你应该在这个核心内完成大部分工作。

第三章, 反应式编程-基本理论,是对反应式理论或反应式编程的基本探索,特别是与 Facebook 的 ReactJS 用户界面框架相关。

第四章, 演示非功能性反应式编程-实时示例,证明并非所有开发都是从零开始的。大多数专业工作并非绿地。这将提供一个实时示例,将一个简单的视频游戏(最近使用 jQuery 实现)改装以利用 ReactJS(如果您正在使用 ReactJS,您可能会进行其他从 jQuery 到 ReactJS 的转换)。

第五章, 学习函数式编程-基础知识,如果你想了解函数式编程但不知道从哪里开始,这里是一个开始的地方!介绍了 map、reduce 和 filter 作为一个用不完的技巧袋。

第六章, 函数式反应式编程-基础知识,涵盖了关于函数式编程和反应式编程的内容。它将与一些明智的建议结合在一起,并为本书中剩下的实际操作工作奠定最后的基础。

第七章, 不重复造轮子-功能性反应式编程工具,包含了很多内容,甚至在一本书中,更不用说一个章节了。但这意味着有一个有趣的样本空间,其中提供了许多有趣的选择,包括从其他语言编写 ReactJS 代码而不是 JavaScript。

第八章,“使用实例演示 JavaScript 中的函数式响应式编程 - 实例演示第 I 部分”,我们看到了一个应用程序,其中包含一个用 ReactJS 从头编写的诙谐 ReactJS 组件,并展示了甜蜜的 JSX 语法糖,虽然不是必需的,但仍然可用于 ReactJS 开发。

第九章,“使用实例演示 JavaScript 中的函数式响应式编程第 II 部分 - 待办事项列表”,带我们进入了第一个真正的组件,旨在被使用而不仅仅是娱乐。我们实现了一个待办事项列表,除了“完成”之外,它还有几个标记,用于指示任务的状态、优先级和其他信息。

第十章,“使用实例演示 JavaScript 中的函数式响应式编程:实例演示第 III 部分 - 日历”,我们将构建一个日历。它旨在优雅地支持不仅是一次性事件,还有各种规则的多种重复事件。

第十一章,“使用实例演示 JavaScript 中的函数式响应式编程第 IV 部分 - 添加一个草稿本并将其整合在一起”,提供了一个带有 CKeditor 的富文本草稿本。这展示了我们如何与其他用户界面工具进行交互。然后,我们将四个组件合并到一个组合页面中,并添加持久性功能,以便我们的用户界面不会忘记它所告诉的事情。

第十二章,“一切如何契合”,回顾了本书中涵盖的内容,并探讨了在探索世界中的下一步。

附录,“Node.js 快速入门”,探讨了“狂野西部”技术的一些优点、缺点和丑闻,似乎每个人都想参与其中。

本书需要什么

需要下载一些软件,并且您需要一个至少能够提供静态内容的 Web 服务器。附录,“Node.js”,介绍了如何在 Node.js 中构建一个用于更大项目的 Web 服务器,但所有章节都可以使用最基本的方式提供静态内容的 Web 服务器。您需要一台台式电脑,几乎可以是任何可以运行 Node.js 的设备(如果您选择通过附录进行操作)。文本将在 Unix、Linux、Mac、Windows、Cygwin 等系统上运行得足够好。如果您想从移动设备上运行它,这可能是一个值得称赞的方法,但请使用标准的服务器或台式机操作系统。

然而,你真正需要的只是一个服务器或台式机、一个像 Chrome 这样的浏览器、一个 Web 服务器,以及愿意尝试新事物的心态。其他一切都在文本中提供。

这本书适合谁

本书旨在面向希望深入了解函数式响应式编程和 Facebook 的 ReactJS 的程序员。我们期望读者具有一定的编程素养,了解 JavaScript,并且对用户界面的制作有一定的了解。熟悉函数式编程也是有帮助的,但我们希望能够创建一本书,即使是在任何通用语言中具有一定(也许是轻微的)JavaScript 和 Web 开发知识的资深程序员也能够使事情顺利进行。

对于那些在前端网页开发和 JavaScript 的功能核心方面有扎实背景的人来说,他们可能会惊讶地发现使用 ReactJS 是多么容易,就像切黄油一样容易。

约定

在本书中,您会发现一些区分不同信息类型的文本样式。以下是一些示例以及它们的含义解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 用户名会显示如下:"typeof函数返回一个包含类型描述的字符串;因此,typeof可以提供扩展类型。"

一块代码设置如下:

   var counter = (function() {
     var value = 0;
     return {
       get_value: function() {
         return value;
       },
       increment_value: function() {
         value += 1;
       }
     }
   })();

任何命令行输入或输出都以以下方式编写:

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

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:"当安装程序启动时,点击下一步,如下所示:"

注意

警告或重要说明会以这样的框出现。

提示

提示和技巧会以这样的方式出现。

第一章:介绍和安装

欢迎来到 JavaScript 中反应式(函数式)编程的美妙世界!在本书中,我们将涵盖 JavaScript 的好部分,尽管我们不会完全遵循它。我们将涵盖函数式编程、反应式编程和 ReactJS 库,并将所有这些整合到 JavaScript 的函数式反应式编程中。如果您要学习反应式编程,我们建议您认真考虑函数式反应式编程,包括尽可能多地学习函数式编程。在这种情况下,整个函数式反应式编程的总和大于其各部分。我们将把反应式编程应用到 JavaScript 用户界面开发中。用户界面是函数式反应式编程FRP)真正闪耀的领域。

本章将涵盖以下主题:

  • 主题的概述,包括:

  • 一个更简单的用户界面编程方法的讨论

  • 对编程范式的简要讨论,如函数式和反应式编程

  • 本书中各章的概述

  • 查看如何安装本书中使用的一些工具

一个概述

有很多事情可以说,但(函数式)反应式编程可能比您想象的要容易。今天,关于函数式反应式编程的大部分内容都令人生畏,就像几年前关于闭包的说明一样。

处理用户界面编程的更简单方法

多年前,当我开始学习 JavaScript 时,我选择了一个网站,从中我学到了我需要理解的一切,以执行面向对象的信息隐藏,也就是如何创建一个具有私有字段的 JavaScript 对象。我可能读了两三章,这些章节充满了理论计算机科学,还有引言的 10-15%,然后我放弃了。然后我发现使用闭包来创建一个具有私有字段的对象是多么容易,只是简单的学来用去

   var counter = (function() {
  var value = 0;
  return {
    get_value: function() {
      return value;
    },
    increment_value: function() {
      value += 1;
    }
  }
})();

现在,函数式反应式编程正处于 JavaScript 闭包几年前的状态。在开始反应式编程之前,您必须阅读的理论数量令人震惊,而且大部分文献都是博士阅读水平的。这是个坏消息。但好消息是,您不必阅读那么多。

本书的目的是提供一种类似于学来用去的方式,传达如何使用闭包来创建具有私有字段的 JavaScript 对象。理论本身并不坏,引入理论进行讨论也不是问题,但是以做一些简单事情的代价来制作一篇完整的论文的理论支持是一个问题。

我们希望这本书能让您明白,例如,使用函数式反应式编程在 JavaScript 中构建游戏用户界面比使用 jQuery 更容易。

编程范式

周围有多种编程范式,并且并非所有都是互斥的。许多编程语言都是多范式语言,支持使用多种范式,包括不仅仅是 JavaScript,还有 OCaml、PHP、Python 和 Perl 等语言。

请注意,您至少有时可以使用一种语言来支持范式,而该语言并非明确设计为支持它。面向对象编程最初并不是为像 Java 或 Ruby 这样专门用于支持面向对象编程的语言而制定的,而是作为一种工程学科的问题,最初是在早于面向对象编程的语言中使用的。

在编程范式中,我们现在有以下内容:

  • 面向方面的编程:有人建议程序员的专业发展从过程式编程转向面向对象编程,然后是面向方面的编程,最后是函数式编程。面向方面编程的一个典型例子是日志记录,它是在程序中通过天真的用法传播的一个方面。面向方面编程处理编程的横切面,如安全性、状态的诊断暴露和日志记录。

  • 声明式编程:函数式响应式编程的一个关键概念是它是声明式的,而不是命令式的。换句话说,c = a + b并不意味着取a的当前值,加上b的当前值,并将它们的总和存储在c中。相反,我们声明一个持久的关系,它的工作方式有点像电子表格中的C1 = A1 + B1。如果A1B1发生变化,C1会立即受到影响。存储在C1中的不是赋值时的A1值加上B1值的结果,而是更持久的东西,可以按需获取单个值。

  • 防御性编程:类似于防御性驾驶,防御性编码意味着编写的代码在给定有缺陷的输入时能够正确地运行。函数式响应式编程是一种在面对网络问题和非理想的现实条件时,要么能够正确运行,要么能够在恶劣条件下优雅地降级的方法。

  • 函数式编程:这里,术语函数具有数学意义,而不是编程意义。在命令式编程中,函数可以(而且通常是)操作状态。因此,init()函数可能会初始化程序最初运行所需的所有数据。函数是接受零个或多个输入并返回结果的东西。例如,f(x) = 3x+1g(x) = sin(x)h(x, y) = x'(y)(在y处的x的导数)都是数学函数;它们都不涉及任何状态数据的操作。纯函数是在数学定义下排除了如何处理状态的函数。函数式编程还允许并经常包括高阶函数,或者作用于函数的函数(在微积分中,导数或积分代表一个高阶函数,迭代积分包括一个以另一个高阶函数作为输入的高阶函数)。解决方案集中在抽象函数上的问题,这些函数操作抽象函数,往往对计算机科学类型更具吸引力,而不是真正用于商业世界的东西。这里探讨的高阶函数将是相对具体的。你不需要一直使用高阶函数,一旦掌握了核心概念,它们并不难使用。

  • 命令式编程:命令式编程是一种常见的编程方式,对于大多数首次接触命令式编程的程序员来说,它可能是最自然的工作方式。函数式响应式编程的营销提案包括了对这种基本方法的另类选择。函数式响应式编程的声明式编程、函数式编程中的纯函数(包括高阶函数)以及响应式编程的时间序列,提供了一种对命令式编程自然倾向的替代方案。

  • 信息隐藏:Steve McConnel 的《代码大全》描述了几种方法,并告诉我们哪些方法对不同的环境最为理想(例如,对于过程式编程,最适合的环境是小型项目而不是面向对象编程)。对于仅仅信息隐藏而言,他的建议是“尽可能多地使用这个”。在通用信息隐藏的发展中,一个大型项目通过在更大的区域内封装秘密来处理,而更大的秘密则通过封装子秘密来分割。过程式编程、面向对象编程和功能性编程的一个很大部分都是为了促进信息隐藏。信息隐藏是洛德米特法则背后的软件工程问题,例如,你可以在方法调用中有一个点(foo.bar()),但不能有两个点(foo.baz.bar())

  • 面向对象编程:程序被分割成对象,而不是具有单一结构。这些对象有自己的方法和字段,可能又被分割成更多的对象。这为比过程式编程更大的项目提供了一个可接受的信息隐藏水平,即使面向对象编程基本上是从过程式编程开始并在其基础上构建。

  • 模式:模式并不是好软件的配方,但在更高层次的人类抽象中,它们提供了一种谈论最佳重复解决方案的方式,以避免从头开始重新发明已经解决的问题。此外,特定的模式被提到了台面上,包括 MVC 和现在的观察者模式,尽管观察者模式通常不会在与响应式编程相关的情况下提到,但它却是一个重要的组成部分。

  • 过程式编程:过程式编程是提到的方法中最古老的之一,它旨在为早期基于goto流程控制的意大利面代码提供一些秩序。也许我们可以批评过程式编程,因为一旦面向对象编程、面向方面编程和面向对象设计模式可用,它就不再做足够多的事情。当你有工具可以从 goto 的鼠窝、指针作为数据结构的 goto 等方面进一步推进时,从过程式编程转向其他编程是正确的选择。

  • 响应式编程:假设功能性编程在很大程度上是指函数具有第一类地位,并且可以创建高阶函数(作用于其他函数的函数)。那么响应式编程在很大程度上是指时间序列(随时间具有不同值的函数)具有第一类地位。对于音乐、游戏、用户界面和其他一些用例,计算当前时刻的正确值是响应式编程的一个亮点。

  • 功能性响应式编程:功能性响应式编程是建立在功能构件上的响应式编程,其中函数和时间序列都是第一类实体。有一些有用且令人惊讶地简单的函数,可以作用于一个时间序列,从而提供另一个时间序列(这两个序列中的任何一个都可以被其他时间序列的函数所作用)。功能性响应式编程的一个主要卖点是,它提供了比直接陷入“回调地狱”更为优雅和可维护的方法。

安装所需工具

许多读者可能已经安装了 Chrome 和 Node.js,如果他们之前没有安装的话,他们可能会感到很舒适。对于那些更喜欢逐步指导的人,以下是安装适当软件的详细信息。

谷歌浏览器可以从google.com/chrome安装。请注意,对于某些 Linux 发行版,Chrome 可能无法从软件包管理器中获取。谷歌浏览器是一个明显的选择,可以考虑将其包含在发行版的软件包中,但由于许可问题,Chrome 的某些部分可能被列为非免费,这意味着就发行版维护者而言,您可以使用它,但我们不愿意将其包含在仅免费的软件包存储库中。

Node.js 可以从nodejs.org/download获取。如果您使用 Linux,最好通过软件包管理器获取。请注意,Node.js 自带其自己的软件包管理器 npm,可用于下载在 Node.js 下使用的软件包。

有用的 ReactJS 入门套件可以从facebook.github.io/react/downloads.html获取。

以下说明适用于 Windows 8.1(我更喜欢在 Mac 或 Linux 上开发,但是写作 Windows 8.1 作为通用语言)。

安装谷歌浏览器

我们将使用谷歌浏览器作为主要的参考浏览器:

  1. 要下载它,转到google.com/chrome,然后点击左下方的立即下载按钮,如下所示:安装谷歌浏览器

  2. 接下来,点击右下方的接受并安装按钮,如下面的截图所示:安装谷歌浏览器

  3. 然后,当询问是否要运行或保存安装程序时,点击运行按钮,如下所示:安装谷歌浏览器

  4. 接下来,授权 Chrome 的安装程序对系统进行更改,如下面的截图所示:安装谷歌浏览器

  5. 然后点击下一步按钮安装 Chrome,如下面的截图所示:安装谷歌浏览器

  6. 等待一分钟进行安装,然后如果愿意,将 Chrome 设置为默认浏览器:安装谷歌浏览器

就是这样!

安装 Node.js

安装 Node.js 很简单,它使得使用 JavaScript 作为唯一语言来启动 HTTP 服务变得容易。

  1. 转到nodejs.org/download安装 Node.js

  2. 点击Windows 安装程序,等待安装程序下载。然后点击窗口的左下角,如此截图所示:安装 Node.js

  3. 当安装程序启动时,点击下一步,如下所示:安装 Node.js

  4. 然后点击复选框接受协议条款,如下所示:安装 Node.js

  5. 之后,点击下一步按钮继续,如下截图所示:安装 Node.js

  6. 当询问要安装软件的位置时,点击下一步按钮,如下截图所示:安装 Node.js

  7. 然后点击下一步继续,如下一截图所示。如果需要,自定义功能:安装 Node.js

  8. 之后,点击安装按钮继续安装,如下所示:安装 Node.js

  9. 最后,点击完成按钮完成安装 Node.js,如下截图所示:安装 Node.js

  10. 授权安装程序对计算机进行更改,如下所示:安装 Node.js

安装 ReactJS 的入门套件

要安装入门套件,请执行以下步骤:

  1. 转到facebook.github.io/react/downloads.html,您将看到一个类似于以下截图的屏幕:安装 ReactJS 的入门套件

  2. 单击下载入门套件 0.12.0开始下载(这将显示在左下角),如前面的截图所示。

  3. 您将看到一个 ZIP 文件在底部下载:安装 ReactJS 的入门套件

  4. 从这里,您应该能够浏览 ZIP 文件的内容:安装 ReactJS 的入门套件

总结

在本章中,我们简要概述了编程范式,以表明函数式响应式编程可能适用的领域,并安装了基本工具。

我们将在下一章讨论 JavaScript。Node.js 的基础知识在附录中讨论。

第二章:核心 JavaScript

JavaScript 是一个好坏参半的语言。JavaScript 有一些真正糟糕的部分和一些优秀的部分。总的来说,JavaScript 是一种动态的、弱类型的、解释性的脚本语言。对整个语言的处理是重点探索好的部分,正如 Douglas Crockford 所描述的那样,因为 JavaScript 的坏部分有多糟糕:它们确实是雷区。Crockford 对 JavaScript 的现在的使用产生了深远的影响;足以让题为AngularJS:坏部分的博客文章立即、清楚、完全地传达了将会被痛苦细致地剖析的事物。

本章涵盖的主题包括:

  • 严格模式:隐式用于包括 ECMAScript 6 模块的代码

  • 变量和赋值:任何语言中编程的基本构建块之一

  • 注释:有两种风格;我们更偏向于一种,因为通过在程序员意图之外的地方开始或结束注释,很容易产生意外行为

  • 流程控制:基本的 if-then、if-then-else 和 switch 语句的简要介绍

  • 关于值和 NaN 的注释:关于真值和有毒的非数字值的注释

  • 函数:JavaScript 函数的示例,是语言中最好的部分之一

  • 关于 ECMAScript 6 的一些简要注释:多年来核心 JavaScript 的第一个真正的根本性变化

严格模式

“use strict”;作为文件或函数的第一行将导致某些可能引起无数问题的事情(例如在没有声明的情况下对变量进行赋值)以清晰的错误行号报告错误,而不是让您从可能的多种后果中去猜测线索。 “use strict”;也可以是函数的第一行,在这种情况下,函数处于严格模式。

Perl 用户将了解-w标志,可能是与该语言相关的最著名的标志,以及它的后继者 Perl 的使用警告的惯例。文档中说了一些事情,比如,打开已知错误列表,“使用警告所暗示的行为不是强制性的”。

JavaScript 的严格模式本身可能与 Perl 的使用警告的惯例不相上下,但至少可以让您养成使用的习惯。

变量和赋值

变量应该使用var关键字声明。在函数外声明的任何变量,或者在没有声明的情况下使用的任何变量,都是全局变量,全局变量是一个大问题;它们在默认 JavaScript中的位置是主要的设计缺陷之一。

当 Java 进行了一次重大的公共关系推动时,JavaScript 被命名为在 Java 的影响下运行,并且做出了某些决定,使 JavaScript 代码看起来像 Java。这些决定是不幸的。JavaScript 在形式上是一种类似 C 的语言,它与 Java 或 C#的最近共同祖先是 C,而不是 C++或 Java。JavaScript 被描述为穿着 C 的外衣的 Scheme。Lisp 是与包括 Scheme、Common Lisp、Clojure 和 ClojureScript 在内的一系列语言相关的语法,可以说,追求最佳 JavaScript 的日益功能性的重点来自 ClojureScript。在 JavaScript 中,没有单独的整数和浮点类型;数字是 64 位浮点值,在一定的长范围内使用时表现得像整数。然而,它们有时会给新程序员带来惊喜;例如,0.1 + 0.2 并不等于 0.3,出于历史原因,这也困扰着其他无数语言。

基本变量赋值看起来像 C 语言:

var x;
var y = 12 + 2;

如果变量声明但未赋值,如前面示例中的x,其值将为 undefined。

以下是等效的:

y = y + 1;
y += 1;
++y;

我们将避免在前面示例中列出的最后一个选项的使用,因为它不被认为是好的部分之一。道格拉斯·克罗克福德在其中的一个视频中讲述了一个故事,他在一个小时的辩护中对++y的使用进行了精彩的辩护,然后进行了一场漫长的调试会话,在这场调试会话中,++y的微妙之处已经咬了他一口。与前面示例中的前两个选项不同,可以为其分配一个值,并且++yy++不相同。克罗克福德随后慷慨地放弃了他先前的立场。

注释

大多数语言都支持某种形式的注释。除了对代码的解释外,它们还用于临时停用代码。JavaScript 具有与 Java 相似的语言所期望的注释;但是,Javadoc 注释并不是本地特殊的(已经制定了各种解决方案,如 JSDoc 来填补这一空白)。

JavaScript 支持 C++注释的两类。前三行只包含注释而没有可执行代码,最后一行有一行代码,然后一直到行尾都是注释:

/*
 * Multiline comments are legal.
 */

if (x) // In this case, we ...

我们将避免多行注释。星号和斜杠在正则表达式中经常出现,多行注释可能会引起问题,并且可能需要上下文或由其他人编写的代码中仍然会引起意外效果。内联注释本质上不太容易受到意外效果的影响。

流程控制

如果-然后和如果-然后-否则按照以下示例代码的描述工作,其中一个在数字非零时执行某些操作,另一个在数字非零时执行一个动作,如果为假则执行另一个动作。两者都使用了真值,其中 0(和 NaN)为假,而任何其他值为真:

if (books_remaining) {
  print_title();
}

var c = 1;
if (c) {
  c += 2;
} else {
  c -= 1;
}

关于值和 NaN 的注释

所有值都可以在布尔上下文中使用并进行真值测试。值undefinednull''0[]falseNaN(不是数字)都是假值,所有其他值都是真值

NaN特别是一个特殊情况,它的行为不像其他真实数值。NaN是有毒的;包含NaN的任何计算都将得到NaN的结果。此外,尽管NaN假值,但它不等于任何东西,包括NaN本身。检查NaN的通常方法是通过isNaN()函数。如果您发现NaN潜伏在某个意想不到的地方,您可能会为代码提供调试日志语句,指导您检测到NaN的位置;在生成NaN的地方和观察到它破坏结果的地方之间可能存在一定的距离。

函数

在函数中,默认情况下,控制从开始到结束,函数返回undefined。可选地,在结束之前可以有一个返回语句,函数将停止执行并返回相应的值。以下示例说明了一个带有返回值的函数:

var add = function(first, second) {
  return first + second;
}

console.log(add(1, 2));

// 3

前面的函数接受两个参数,尽管函数可以给出(没有错误或错误消息)少于或多于声明指定的参数。如果它们被声明为具有值,这些值将作为局部变量存在(在前面的示例中,firstsecond)。无论如何,这些参数也可以在一个类似数组的对象arguments中使用,该对象具有.length方法(数组具有.length方法,比项的最高位置大 1),但不具有数组的其他特性。在这里,我们创建一个函数,该函数可以接受任意数量的数字参数,并通过使用arguments伪数组返回它们的(算术)平均值。

var average_arbitrarily_many() {
  if (!arguments.length) {
    return 0;
  }
  var count = 0;
  var total = 0;
  for(var index = 0; index < arguments.length; index += 1) {
    total += arguments[i];
  }
  return total / arguments.length;
}

基本数据类型包括数字、字符串、布尔值、符号、对象、null 和 undefined。对象包括函数、数组、日期和正则表达式。

对象包括函数意味着函数是值,可以作为值传递。这有助于高阶函数,或者将函数作为值传递的函数。

作为高阶函数的一个例子,我们将包括一个 sort 函数,它对数组进行排序并可选择接受一个比较函数。这建立在函数定义上,实际上包含了一个函数定义在另一个函数定义中(这与其他任何地方一样合法),然后是一个 QuickSort 的实现,其中值被分为 比第一个元素小等于第一个元素比第一个元素大 进行比较,并且这三个中的第一个和最后一个被递归排序。我们在排序之前检查非空长度,以避免无限递归。作为高级函数实现的经典 QuickSort 算法如下:

var sort = function(array, comparator) {
  if (typeof comparator === 'undefined') {
    comparator = function(first, second) {
      if (first < second) {
        return -1;
      } else if (second < first) {
        return 1;
      } else {
        return 0;
      }
    }
  }
  var before = [];
  var same = [];
  var after = [];
  if (array.length) {
    same.push(array[0]);
  }
  for(var i = 1; i < array.length; ++i) {
    if (comparator(array[i], array[0]) < 0) {
      before.push(array[i]);
    } else if (comparator(array[i], array[0]) > 0) {
      after.push(array[i]);
    } else {
      same.push(array[i]);
    }
  }
  var result = [];
  if (before.length) {
    result = result.concat(sort(before, comparator));
  }
  result = result.concat(same);
  if (after.length) {
    result = result.concat(sort(after, comparator));
  }
  return result;
}

注释

有几个关于这个函数的基本特性和观察需要注意,这并不是要突破界限,而是要展示如何很好地覆盖标准基础:

  1. 我们使用 var sort = function()... 而不是允许的 function sort()...。当在函数内部使用时,这将函数存储在一个变量中,而不是在全局定义某些东西。请注意,在调试时,为函数包括一个名称可能会有所帮助,var sort = function sort()...,只能通过变量访问函数,并让调试器捕捉到第二个。例如:sort 而不是匿名地引用函数。请注意,使用 var sort = function(),变量声明被提升,而不是初始化;使用 function sort(),函数值被提升,在当前范围内任何地方都可用。

  2. 这是一种标准的检查方式,用于查看两个参数中是否只有一个被指定,即是否提供了一个数组但没有提供排序函数。如果没有,将提供一个默认函数。我们运行了几次排序的试验:

console.log(sort([1, 3, 2, 11, 9]));
console.log(sort(['a', 'c', 'b']));
console.log(sort(['a', 'c', 'b', 1, 3, 2, 11, 9]); 

这给了我们:

[1, 2, 3, 9, 11]
["a", "b", "c"]
["a", 1, 3, 2, 11, 9, "b", "c"] 

这给了我们一个调试的机会。现在,假设我们添加以下内容:

console.log('Before: ' + before);
console.log('Same: ' + same);
console.log('After: ' + after);

在结果声明之前,我们得到:

[1, 2, 3, 9, 11]
Before:
Same: a
After: c,b
Before: b
Same: c
After: 
Before:
Same: b
After:
["a", "b", "c"]
Before:
Same: a,1,3,2,11,9
After: c,b
Before: b
Same: c
After: 
Before: 
Same: b
After:  
["a", 1, 3, 2, 11, 9, "b", "c"]

输出中说 Same: a,1,3,2,11,9 看起来可疑,一个 Same 桶应该有相同的值,因此一个合适的输出可能是 Same: 2,2,2 或者只是 Same: 3,其中 Same 列表有五个值,没有两个是相同的。这不可能是我们想要的行为。看起来整数被分类为与初始的 "a" 相同,其余部分被排序。一点调查证实了 '"a" < 1' 和 '"a" > 1' 都是假的,所以我们的比较器可以改进。

我们对它们的类型进行了字典排序。这在类型的字母顺序上进行了一些任意排序,然后按照类型默认的排序顺序进行排序,这可以用另一个比较函数覆盖。这是另一种可能用于对数组进行排序的许多种比较器中的一个例子:与前一个不同,这个比较器将不同种类的项目进行分段,例如按顺序排序的数字将出现在字符串之前,按顺序排序:

        var comparator = function(first, second) {
          if (typeof first < typeof second) {
            return -1;
          } else if (typeof second < typeof first) {
            return -1;
          } else if (first < second) {
            return -1;
          } else if (second < first) {
            return 1;
          } else {
            return 0;
          }
        }

typeof 函数返回一个包含类型描述的字符串;因此 typeof 可以提供一个扩展类型。使用类似于这样的比较函数,可以有意义地比较诸如包含名字、姓氏和地址的记录之类的对象。

对象可以通过花括号表示法声明。代码块和对象都可以使用花括号,但这是两个不同的东西。下面的代码及其花括号,不是一个带有要执行的语句的代码块;它定义了一个具有键和值的字典:

var sample = {
  'a': 12,
  'b': 2.5
};

除非键是保留字或包含特殊字符,如 'strange.key'(这里是一个句号),否则键周围的引号是可选的。为了有一个简单和一致的规则,JSON 格式要求在所有情况下都使用引号,特别是双引号,而不是单引号。

下面显示了一个记录具有名字、姓氏和电子邮件地址的示例,可能是通过 JSON 填充的。这个示例不是 JSON,因为它没有遵循 JSON 关于双引号引用所有键和所有字符串的规则,但它说明了一个记录数组,其中可能有其他字段,可能会更长。按距离或资历排序可能是有意义的(这里没有显示填充字段):可能有一整套可能用于记录的比较器。

var records = [{
    first_name: 'Alice',
    last_name: 'Spung',
    email: 'a.spung@yahoo.com'
  }, {
    first_name: 'Robert',
    last_name: 'Hendrickson',
    email: 'Bob.Hendrickson@gmail.com'
  }
];

请注意,尾随逗号不仅在 JavaScript 中是不合适的(在几乎任何由逗号分隔的东西的最后一个条目之后),而且它有一些奇怪和意想不到的行为,这可能极其难以调试。

JavaScript 被设计为在语句的末尾有分号,这可能是可选的。这是一个有争议的决定,与决定制作一种流行的语言,普通非程序员可以在不涉及 IT 人员的情况下使用有关,这也是 SQL 设计中涉及的因素。当适当时,我们应该始终提供分号。这样做的一个副作用是,单独一行的return将返回 undefined。这意味着以下代码将不会产生预期的效果,并且将返回 undefined:

return
  {
  x: 12
  };

提示

代码执行时的效果与它看起来的效果以及可能的意图不同,因此最好不要这样做。

为了获得期望的效果,开放的大括号应该与 return 语句在同一行,如下所示:

return {
  x: 12
};

然而,JavaScript 确实具有面向对象编程,避免了面向对象编程中的一个经典困难:必须第一次就正确地获取本体论。对象通常最好是通过工厂而不是类的实例来构造。或者道格拉斯·克罗克福德已经被缩写。原型仍然是语言的一部分,就像许多好的和坏的特性一样,但是除了涉及奇特的用例,对象通常应该由允许“比本体论驱动的类”更好的面向对象编程方法的工厂制造。我们将避免伪经典的新function(),不是因为如果你忘记了新的话它可能会破坏全局变量,而是因为它传统面向对象编程的外观并没有真正帮助太多。

提示

你应该知道 JavaScript 中一个广受尊敬的惯例,即打算与new一起使用的构造函数以大写字母开头。如果函数名以大写字母开头,那么它打算与new关键字一起使用,如果在没有new关键字的情况下调用,可能会发生奇怪的事情。

在 JavaScript 中,一些其他语言中经典面向对象编程所服务的利益有时最好通过函数式编程来推进。

循环

循环包括for循环,for in循环,while-do循环和do-while循环。for循环的工作方式与 C 语言相同:

var numbers = [1, 2, 3];
var total = 0;
for(var index = 0; index < numbers.length; ++index) {
  total += numbers[index];
}

for in循环将循环遍历对象中的所有内容。hasOwnProperty()方法可用于仅检查对象的字段。对于名为obj的对象,两个变体如下:

var counter = 0;
for(var field in obj) {
  counter += 1;
}

这将包括原型链中的任何字段(此处未解释)。为了检查对象本身的直接属性,而不是原型链中潜在的嘈杂数据,我们使用对象的hasOwnProperty()方法:

var counter = 0;
for(var field in obj) {
  if (obj.hasOwnProperty(field)) {
    counter += 1;
  }
}

顺序不被保证;如果你正在寻找任何特定的字段,值得考虑的是只迭代你想要的字段并在对象上检查它们。

看一下 ECMAScript 6

JavaScript 工具一直在蓬勃发展,一个工具被另一个工具取代,有一个极其丰富的生态系统,很少有开发人员可以自豪地广泛和深入地了解。然而,核心语言 ECMAScript 或 JavaScript 已经稳定了好几年。

ECMAScript 6,有一个介绍性的路线图可在tinyurl.com/reactjs-ecmascript-6上找到,它为核心语言引入了深刻的新变化和扩展。一般来说,这些功能增强、加深或使 JavaScript 的功能方面更加一致。可以建议 ECMAScript 6 的功能不做这种工作,比如增强的面向类的语法糖,让 Java 程序员假装 JavaScript 意味着在 Java 中编程,可能会被忽略。

ECMAScript 6 的功能是不可忽视的,在撰写本文时,它们已经开始在主流浏览器中广泛应用。如果你想扩展和提高自己作为 JavaScript 开发人员的价值,不要局限于深入挖掘丰富的 JavaScript 生态系统,无论那有多重要。学习新的 JavaScript。学习一个有更多更好部分的 ECMAScript。

总结

在这场对 JavaScript 一些更好部分的风暴式之旅中,我们涵盖了可以在我们进一步推进 JavaScript 时有所帮助的基础构建模块。这些包括变量和赋值、注释、流程控制、值、NaN 函数和 ECMAScript 6。

变量和赋值部分,我们揭示了大多数编程的一些基本构建模块,尽管函数式响应式编程的重点可能在其他地方。在注释部分,我们了解到注释在任何地方都是相同的,但这里的主要关注点是防止奇怪的意外。

流程控制部分涵盖了函数内的基本流程控制(或者可能在任何函数之外,尽管通常应该避免这样做)。

关于值和 NaN 的说明部分,我们讨论了类似于 Perl,JavaScript 认为真理是不言自明的;也就是说,如果它们为 null、零、空、非数字等,则这些东西是虚假的,而不在列表上的任何东西都是真的。

函数部分,我们看了一些包含有些复杂示例的函数。在ECMAScript 6部分,我们讨论了核心 JavaScript 语言的扩展。

这只是对亮点的简要介绍,而不是全面的介绍。如果你需要更全面的 JavaScript 基础,有多种选择可供选择。我们将在下一章继续讨论响应式编程的基本理论。

第三章:响应式编程-基本理论

响应式编程,包括稍后将讨论的函数式响应式编程,是一种可以在多范式语言中使用的编程范式,例如 JavaScript、Python、Scala 等等。它主要与命令式编程有所不同,在命令式编程中,语句通过所谓的副作用来执行某些操作,在文献中,关于函数式和响应式编程。请注意,这里的副作用并不是普通英语中的副作用,其中所有药物都有一些效果,这些效果是服用药物的目的,而其他一些效果是不受欢迎的,但为了主要的益处而被容忍。例如,苯海拉明是为了减轻空气过敏症状而服用的,而事实上,苯海拉明在某种程度上与其他一些过敏药物类似,也会引起嗜睡(或者至少曾经是这样;现在它也作为睡眠辅助剂出售)是一种副作用。这是不受欢迎的,但被人们容忍,因为他们宁愿有些疲倦,而不受频繁打喷嚏的困扰。药物的副作用很少是程序员通常会考虑的副作用的唯一事情。对于他们来说,副作用是语句的主要预期目的和效果,通常通过对程序的存储状态进行更改来实现。

响应式编程源于观察者模式,如 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 的经典著作《设计模式:可复用面向对象软件的元素》中所讨论的(这本书的作者通常被称为GoF四人帮)。在观察者模式中,有一个可观察的主题。它有一个监听器列表,并在有发布内容时通知它们所有。这比发布者/订阅者(PubSub)模式要简单一些,不需要潜在复杂的消息筛选,以确定哪些消息到达哪些订阅者,这是一个常见的特性。

响应式编程已经发展成了一种独立的生活,有点像 MVC 模式变成了流行语,但最好是与 GoF 中探讨的更广泛的背景联系在一起。响应式编程,包括 ReactJS 框架(在本书中进行了探讨),旨在避免共享可变状态并且是幂等的。这意味着,就像 RESTful 网络服务一样,无论您调用一次还是一百次,您都将从函数中获得相同的结果。Facebook 的前员工皮特·亨特(现在是 ReactJS 的代表人物)曾说过,他宁愿是可预测的,而不是正确的。如果他的代码中有错误,亨特宁愿接口每次都以相同的方式失败,而不是进行对海森巴格的复杂搜索。这些错误只在一些特殊而棘手的边缘情况下表现出来,并且在本书的后面进行了探讨。

ReactJS 被称为MVCV。也就是说,它旨在用于用户界面工作,并且几乎没有提供其他标准功能的意图。但就像画家保罗·塞尚对印象派画家克劳德·莫奈所说的,“莫奈只是一只眼睛,但是多么美的眼睛!”关于 MVC 和 ReactJS,我们可以说,“ReactJS 只是一个视图,但是多么出色的视图!”

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

  • 声明式编程

  • 对抗海森巴格

  • Flux 架构

  • 从绝望之坑到成功之坑

  • 完整的 UI 拆解和重建

  • JavaScript 作为领域特定语言DSL

  • 大咖啡符号

本书中探讨的库 ReactJS 是由 Facebook 开发的,并在不久之前开源。它受到 Facebook 关于创建一个安全易于调试的大型网站以及允许大量程序员在不必将复杂性存储在头脑中的情况下工作的一些关注的影响。引用语“简单是缺乏交错”,可以在facebook.github.io/react的视频中找到,它不是关于绝对尺度上有多少或多少东西,而是关于您需要同时操纵多少个移动部分来工作在一个系统上(有关大咖啡符号的更多反思,请参见相关部分)。

声明式编程

ReactJS 框架最大的理论优势可能是编程是声明式的,而不是命令式的。在命令式编程中,您指定需要执行哪些步骤;声明式编程是指您指定需要完成什么,而不告诉如何完成。从命令式范式转变到声明式范式可能一开始会很困难,但一旦完成转变,付出的努力就是值得的。

熟悉的声明式范例,与命令式范例相对,包括 SQL 和 HTML。如果您必须指定如何查找记录并适当地过滤它们,更不用说如何使用索引,那么 SQL 查询将会更加冗长,而如果您必须指定如何渲染图像,HTML 将会更加冗长。许多库比起从头开始自己解决问题更具有声明性。使用库,您更有可能只指定需要完成什么,而不是除此之外还要指定如何完成。ReactJS 在任何意义上都不是旨在提供更具声明性 JavaScript 的唯一库或框架,但这是它的卖点之一,还有其他更好的具体功能,可以帮助团队合作并提高生产力。再次强调,ReactJS 是从 Facebook 在管理错误和认知负荷方面的一些努力中出现的。

对 Heisenbugs 的战争

在现代物理学中,海森堡的不确定性原理大致表明,有一个绝对的理论限制,即一个粒子的位置和速度可以被了解到多好。无论实验室的测量设备有多好,当您试图过于深入地固定事物时,总会发生一些有趣的事情。

海森堡不确定性原理,口语上来说,是一种微妙的、难以捉摸的错误,很难固定下来。它们只在非常特定的条件下显现,甚至在尝试调查它们时可能甚至不会显现(请注意,这个定义与 jargon 文件在www.catb.org/jargon/html/H/heisenbug.html中更狭窄和更具体的定义略有不同,该定义指出尝试测量 heisenbug 可能会抑制其显现)。对海森堡不确定性原理进行宣战的动机源于 Facebook 自己在规模化工作和看到 heisenbug 不断出现的困扰和经验。Pete Hunt 提到的一件事,一点也不令人愉快,是 Facebook 广告系统只有两名工程师能够充分理解并且愿意修改。这是一个需要避免的例子。

相比之下,看看 Pete Hunt 的评论,他宁愿“可预测也不愿意正确”是一个声明,如果一个设计有缺陷的灯可以着火烧毁,他更愿意它立即着火烧毁,以相同的方式,每一次,而不是在月相的错误时刻发生燃烧。在第一种情况下,灯会在制造商测试时失败,问题会被注意到并得到解决,直到缺陷得到适当解决之前,灯不会被运送到公众那里。相反的 Heisenbug 情况是灯只会在恰当的条件下发出火花并着火,这意味着缺陷直到灯被运送并开始烧毁客户的家才会被发现。 “可预测”意味着“如果失败,每次都以相同的方式失败”。 “正确”意味着“成功通过测试,但我们不知道它们是否安全使用[可能它们不安全]”。现在,他最终确实关心正确,但 Facebook 在 React 周围做出的选择源于一种认识,即可预测是成为正确的手段。制造商不可以运送一些在消费者插上电源时总是会发出火花并着火的东西。然而,可预测将问题移到前台和中心,而不是偶尔出现在软件迷宫的隐蔽和难以捉摸的相互作用的结果。Flux 和 ReactJS 中的选择旨在使失败显而易见,并将其显现出来,而不是仅在软件迷宫的角落和缝隙中显现。

Facebook 对共享可变状态的战争在他们对聊天 bug 的经验中得到了体现。聊天 bug 成为用户的一个主要关注点。Facebook 的一个重要的醒悟时刻是当他们宣布一个完全无关的功能时,第一个评论是要求修复聊天;它获得了 898 个赞。此外,他们评论说这是一个比较礼貌的请求之一。问题在于未读消息的指示器在没有消息可用时可能会有一个幻影正消息计数。事情来到一个人们似乎不关心 Facebook 正在添加什么改进或新功能,而只是想让他们修复幻影消息计数的地步。他们继续调查并解决边缘情况,但幻影消息计数不断重现。

除了 ReactJS 之外,解决方案还可以在下一节讨论的 flux 模式或架构中找到。在一种情况下,不太多的人感到舒服进行更改,突然之间,更多的人感到舒服进行更改。这些事情简化了事情,以至于新开发人员通常不需要真正需要之前给予的启动时间和处理。此外,当出现错误时,经验丰富的开发人员可以合理准确地猜测系统的哪个部分是罪魁祸首,而新手开发人员在处理错误后往往会感到自信,并对系统的工作原理有一般的了解。

Flux 架构

Facebook 在与 ReactJS 相关的一种方式是宣布对 heisenbugs 宣战,这是通过对可变状态宣战来实现的。Flux 是一种架构和模式,而不是一种特定的技术,它可以与 ReactJS 一起使用(或不使用)。它有点像 MVC,相当于该方法的一个松散竞争对手,但它与简单的 MVC 变体非常不同,并且旨在具有提供单向数据流的“成功深渊”,就像这样:从动作到分发器,然后到存储,最后到视图(但有些人说这两者是如此不同,以至于在尝试确定 Flux 的哪个部分对应于 MVC 中的哪个概念挂钩方面,直接比较 Flux 和 MVC 并不是真的有帮助)。动作就像事件-它们被送入顶部漏斗。分发器通过漏斗并不仅可以传递动作,还可以确保在前一个动作完全解决之前不会再发出任何其他动作。存储与模型有相似之处,也有不同之处。它们像模型一样跟踪状态。它们不像模型,因为它们只有 getter,没有 setter,这可以阻止程序的任何部分能够更改 setter 中的任何内容。存储可以接受输入,但以一种非常受控的方式,通常存储不受任何拥有对其引用的东西的控制。视图根据从存储获取的内容显示当前输出。在某些方面,存储与模型相比具有 getter 但没有 setter。这有助于培养一种不受 setter 访问者控制的数据流。事件可以作为动作传播,但分发器充当交通警察,并确保只有在存储完全解决后才处理新动作。这大大降低了复杂性。

Flux 简化了交互,使得 Facebook 开发人员不再遇到微妙的边缘情况和不断出现的错误-聊天错误最终消失了,再也没有出现。

从绝望的深渊到成功的深渊

Louis Brandy 警告了 C++的危险,冒着引起争议的风险,他称之为第二系统效应的最大例子(tinyurl.com/reactjs-second-system),自 OS/360 项目以来。在一个模糊的XKCD风格的图形中,他说“永远不要相信一个说他懂 C++的程序员”(tinyurl.com/reactjs-cpp-valley)。

以下图表显示了 C++程序员的信心水平:

从绝望的深渊到成功的深渊

他继续说:

“程序员(特别是那些来自 C 语言)可以很快地掌握 C++并感到非常熟练。这些程序员会告诉你他们懂 C++。他们在撒谎。当程序员继续学习 C++时,他会经历这种挫折的低谷,他完全认识到了语言的复杂性。好消息是很容易区分 C++程序员在低谷之前和之后的状态(在这种情况下是面试)。只要提到 C++是一种非常庞大和复杂的语言,低谷后的人会告诉你他们对语言有 127 种不同的小挫折。低谷前的人会说,“是的,我猜。我的意思是,这只是带有类的 C 语言。”

Eric Lippert 告诉我们的内容并不仅适用于 C++程序员;它导致了比 C++更大的东西:

我经常把 C++看作是我自己的绝望之坑编程语言。不受管理的 C++使人很容易陷入陷阱。想想缓冲区溢出、内存泄漏、双重释放、分配器和解分配器不匹配、使用已释放的内存、无数种方式来破坏堆栈或堆——这些只是一些内存问题。C++经常把你扔进绝望之坑,你必须爬上质量之山。(不要与攀登疯狂悬崖混淆。那是不同的。)

现在正如我之前所说的,C#的设计不是一个减法过程。它不是“去掉愚蠢部分的 C++”。但是,不看看其他语言的问题并努力确保这些问题不会出现在 C#用户身上,那我们就太愚蠢了。我希望 C#成为一种“质量之坑”语言,一种鼓励你一开始就编写正确代码的语言。你必须非常努力才能在 C#程序中写出缓冲区溢出的错误,这是有意为之的。

我从未在 C#中编写过缓冲区溢出。我从未在 C#中写过意外地在另一个作用域中隐藏变量的错误。我从未在 C#中在函数返回后使用堆栈内存。我在 C++中做了所有这些事情,这不是因为我是个白痴,而是因为 C++使得做所有这些事情变得容易,而 C#使得这变得非常困难。使得做好事情变得容易显然是好事;考虑如何使做坏事变得困难实际上更重要。

或者,就像在 Python 邮件列表上发生的那样,一个明显的 133t hax0r 拼写的人问如何在 Python 中编写缓冲区溢出,而更资深的列表成员之一回答说:“很抱歉,但 Python 不支持该功能。”这个梗的重点是,有人询问如何找到特定类型的漏洞,却得到的答复是 Python 的语言设计中已经排除了基本类型的缺陷。正如对 C#所指出的,字符串的处理方式是合理的,没有任何天真的使用会导致缓冲区覆盖漏洞。

Eric Lippert 是 C#中的一个关键人物,他的帖子清楚地阐明了如何明智地反对 Bjarne Stroustrup 的话:

“我们思考/编程的语言与我们能够想象的问题和解决方案之间的联系非常紧密。因此,出于消除程序员错误的目的限制语言特性至少是危险的。”

今天不同意 Stroustrup 的人可能不会质疑这两个句子,但可能只会质疑第二个句子:语言与解决方案之间的联系似乎确实是真实的,但对于语言特性和成功之坑却具有相反的含义。像* Douglas Crockford *的书《JavaScript:好部分》中的决定可能会考虑到这样的因素。这样或那样的细节可能会受到质疑,但使用 JavaScript 的精选子集并完全忽略其他部分的核心思想源于寻求和改装成功之坑的事实几乎是理所当然的一旦可能性被指出。

所有这些都导致了 Rico Mariani 在某种程度上提出的一个观点,与绝望之坑的相反。成功之坑与峰顶、山峰或穿越沙漠寻找胜利的旅程形成鲜明对比。我们希望我们的客户简单地“陷入”使用我们的平台和框架的成功实践。在我们让人陷入麻烦变得容易的程度上,我们失败了。

完成 UI 拆解和重建

迪杰斯特拉的一句话,深受 ReactJS 开发者如 Pete Hunt 的喜爱,是:我们的智力更适合掌握静态关系,而我们对于可视化时间演变过程的能力相对较差。因此,我们应该尽最大努力缩短静态程序和动态过程之间的概念差距,使程序(在文本空间中展开)和过程(在时间中展开)之间的对应尽可能简单。

ReactJS 在概念上发挥了这种优势的一种方式是,将所有东西都清除并重新渲染,以便程序和过程之间的对应关系变得简单。您不需要跟踪 DOM 的当前状态和需要记住的 300 个 jQuery 更改,以便准确地从一个状态过渡到另一个状态。您只需要告诉它现在应该看起来如何。实际上,ReactJS 并不会在底层将所有东西都清除;它有相当复杂的功能,可以创建一个闪电般快速的纯 JavaScript 合成 DOM(这个合成 DOM 也使得在 Internet Explorer 8 中实现 HTML5 功能成为可能),并协调和进行尽可能快速的更改(在这种情况下,“快速”包括在非 JIT iPhone 5 上实现每秒 60 帧的更新的令人印象深刻的壮举)。然而,从概念上讲,使用 ReactJS 意味着简单地将所有东西都视为被清除并从头开始重新绘制,并信任 ReactJS 将汇集所有需要的魔法粉来进行最小限度的 DOM 更改。这是为了根据请求更新页面,可能不会丢失现有的输入或惯性滚动。

ReactJS 提供了优化钩子,以提供对渲染内容的更精细控制。这些都有很好的文档说明,但实际上很少需要使用。记住 Knuth 的话,“过早优化是万恶之源。”我自己并没有使用这个优化功能,尽管 ClojureScript 中用于 ReactJS 的 Om 绑定要快得多,因为它们只需要检查引用相等性,而不需要深度相等性,因为在 ClojureScript 中对象是不可变的,尽管我做了一个次要的用法,要求 ReactJS 放弃对 DOM 的某些部分的所有权,以便它能够与第三方功能良好地配合。在第十一章中,使用实例演示 JavaScript 中的函数响应式编程的一部分 IV – 添加一个草稿并把它全部放在一起,有一个这样的例子。ReactJS 默认情况下非常高效,而不仅仅是在您定制其默认行为方面。

JavaScript 作为一种特定领域的语言。

模板系统中的一种广泛实践是为模板提供 DSL。在通常的过程中,比如在前端使用 underscore(underscorejs.org)或在后端使用 Django 模板(djangoproject.com)时,都提供了一个精心选择但故意功能不足的模板语言。例如,在 Django 中,故意限制了功能,以便将模板交给不受信任的设计师,设计师无法做任何可能损害不希望被损害的任何内容的事情。

这也许是一个吸引人的特点,但它表明了一种有限的模板语言。如果模板需要更强大的东西,它们将束手无策,直到它们的任何要求都能在服务器端进行非标准调整。与哥德尔的不完备定理和停机问题相关的一个基本观点是,如果你把某人的手绑得足够紧,以至于这个人原则上不能造成任何伤害,你就显著地限制了这个人能做的事情。最好的结果至少需要一点信任。如果你希望人们能够做出最有用的贡献,他们可能不会能够在双手被束缚的情况下做到这一点。

在 ReactJS 中,模板的 DSL 是 JavaScript,具有其全部功能。有些人也许会觉得让设计师直接使用原始 JavaScript 很奇怪;ReactJS 的人似乎采取了给它五分钟的方法,并说设计师比他们有时被认为的更聪明,非常有能力编写非常特定类型的 JavaScript 代码。但这也意味着,如果你有一个特殊情况,你复杂的情况需要你做一些在某些特定的、故意不够强大的模板语言中被排除的事情,这是个好消息。在 ReactJS 中,你可以充分利用 JavaScript 的全部功能来处理模板。在 ReactJS 中,如果你愿意,你也可以使用一种非常有限的语言子集用于模板,Pete Hunt 和其他人似乎相信设计师足够聪明,能够处理它。但更好的消息是,当你有一个需要真正强大的困难情况时。你拥有 JavaScript 提供的全部功能,这会产生很大的不同。

Big-Coffee Notation

Steve Luscher 是 Facebook 之外的 ReactJS 大师和爱好者,后来被 Facebook 聘用,他在一段关于 React 的视频中谈到了 Big-Coffee Notation。基本观点是,我们不应该只使用大 O 符号来表示运行时复杂度(运行时随问题规模粗略增长的时间,或者偶尔其他维度,比如内存使用),而是应该有一个 Big-Coffee Notation 来表示对于可怜的开发者来说需求如何增长,他们必须将这些东西保存在自己可怜的、充满咖啡因的大脑中。

Gerald Weinberg 的经典著作《计算机编程心理学》详细阐述了一个基本观点。核心观点是程序员编程计算机不仅仅是涉及计算机的活动,也是涉及人的活动,我们最好也将其视为这样的活动。也许我们也应该了解计算机的限制,但人类这一方面并不是微不足道的。魏恩伯格可能是第一个提出这一观察的人,或者可能在他之前有人提出过,但无论哪种情况,这一观察自被吸收以来一直是严肃软件工程文献的基石。例如,在 Steve McConnell 的《代码大全:软件构建实用手册》中,这是一个核心观点。我们不会对这个想法进行全面探讨,但它是值得探索的,特别是如果你以前没有探索过的话,Big-Coffee Notation 正好属于这一领域。核心思想是,除了跟踪大 O 符号或复杂度,我们还应该关注开发者在适当需求方面如何扩展越来越复杂的问题。这些需求是指需要记住多少移动部分。

在大 O 符号表示法中,根据上下文的不同,有各种运行时复杂度,它们为运行时间在解决越来越大的问题时提供了一个上限。O(1)运行时对于任何用例都有一个固定的上限。O(log n)与某些数据结构上的单元操作相关。O(n)也被称为线性,指的是运行时在运行时间上有一个线性的上限。你可以保证至少有一些常数乘以项目的数量。O(n log n)可能是下一个重要的步骤,它与某些排序算法相关。O(n ^ 2)被称为二次(所有先前提到的复杂度都被称为次二次,意思是比二次更快),当事情真的不能扩展到大量时,它可能被视为一个阈值复杂度。(O(n * (n – 1))也被认为是二次,并被包含在O(n ^ 2)之下。之后还有一些较慢的多项式时间和指数时间,而不排除还有更慢的升级,比如阶乘。NP 完全性的著名问题是一个问题,即某些已知可以在指数时间内解决的 NP 完全问题是否总是可以在多项式时间内解决。

Steve Luscher 关于命令式 UI 和声明式 UI 之间的区别的演示是,如果有人要制作一个小部件来显示他们队列中未读项目的数量,命令式 UI 在两个状态之间进行一次转换,使可怜的程序员需要跟踪The Big-Coffee Notation,这意味着相对于状态数量,程序员大脑中的项目数量呈二次或The Big-Coffee Notation复杂度。如果有三个状态,就有六个转换。如果添加第四个状态,将有 12 个,或者是两倍的转换。添加第五个状态,你将看到 20 个转换。程序员理解代码的解释是二次,意味着陡峭。然而,如果你以声明方式给出 UI 代码,比如在 ReactJS 编程中,你只需描述每种可能的渲染状态一次。三种状态只需要三种描述。四种状态只需要四种描述。五种状态只需要五种描述。这只是The Big-Coffee Notation,或者是线性的。对于可怜的程序员的咖啡因大脑来说,这种不断升级的需求来跟踪代码发生的事情要少得多。就像一个快速的算法一样,运行起来要少得多。

我没有听说过 Dijkstra 的《谦逊的程序员》被 ReactJS 社区引用过,但在程序员中,知识谦卑是一种美德,并且在软件工程文献中长期以来一直被认可。它在经典著作中得到强调,比如《代码大全:软件构建的实用手册》,程序员们并不是分为大脑和小脑,而是分为知道自己有小脑的人和有小脑但不自知的人。编程的卓越部分源于对自己认知限制的认识。Dijkstra 写道:

“胜任的程序员充分意识到自己头脑的严格有限;因此,他以完全谦卑的态度对待编程任务,而且在其他方面,他像瘟疫一样避开了聪明的把戏。在一个众所周知的对话式编程语言的情况下,我从各个方面听说,一旦一个编程社区配备了它的终端,就会出现一个特定的现象,甚至已经有了一个成熟的名字:它被称为“一行代码”。它有两种不同的形式:一个程序员把一行程序放在另一个程序员的桌子上,要么他自豪地告诉它做什么,然后问“你能用更少的符号编码吗?”——好像这对概念有任何重要性一样!——要么他只是问“猜猜它是做什么的!”。从这个观察中,我们必须得出结论,这种语言作为一种工具是聪明把戏的一个开放邀请;虽然这可能是它吸引力的解释之一,即对那些喜欢展示自己有多聪明的人来说,但很抱歉,我必须把这看作是对编程语言说的最严厉的话之一……*

这个挑战,也就是面对编程任务,已经教会了我们一些经验教训,我选择在这次讲话中强调的是:

我们将做得更好的编程工作,只要我们以充分的欣赏其巨大困难的态度来对待这项任务,只要我们坚持使用适度和优雅的编程语言,只要我们尊重人类思维的固有局限,并以非常谦卑的程序员的态度来对待这项任务。

总结

我们刚刚快速浏览了一些围绕使用 ReactJS 进行响应式编程的理论。这包括声明式编程,这是 ReactJS 的卖点之一,它提供了比命令式编程更容易处理的东西。Heisenbugs 的战争是 Facebook 所做决定的一个主要关注点,包括 ReactJS。这是通过 Facebook 宣布对共享可变状态的战争来实现的。Flux 架构被 Facebook 与 ReactJS 一起使用,以避免一些恶心的 bug 类。成功的陷阱和绝望的陷阱,从他人的痛苦中学习,这种痛苦集中在与 C++编程语言的联系上,并且看看我们应该追求什么。

我们涵盖了完整的 UI 拆卸和重建,提供了一个简单的替代方案来跟踪状态以更新界面。我们还将 JavaScript 作为 DSL,看作是设计 ReactJS 的一个有意的决定,旨在给你尽可能多的权力。然后讨论了大咖啡符号与健康认识自己的限制的关系,而不是让他们摔断腿,这是可以预防的。

在我们的下一章中,我们将继续通过查看使用 ReactJS 构建的用户界面的具体案例来进行讨论。

第四章:演示非功能性反应式编程-一个实时示例

在本章中,我们将看一个实时示例,其中融合了一些反应式编程原则与 ReactJS。程序的一些部分仍然是命令式的,作为之前用 jQuery 编写的东西的一个端口,经过 HP-28S RPN 和 Unix C 的其他端口之后,但是 ReactJS 的强大仍然闪耀,即使像现实世界中的大部分代码一样,它已经经历了多次迭代。我们将简要地看一下网页的 HTML 要求,然后再看 JavaScript 中真正的内容。该网页提供了一个最初在 HP-28S 图形科学计算器上开发的视频游戏的端口,并保留了计算器的外观和感觉。

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

  • 网页的 HTML

  • 动画网页的 JavaScript

在这里,我们看到游戏,渲染在经典的 HP28S 计算器的背景下。已经采取了一些措施,使字符图形模仿了 LED 屏幕上存在的暗色和亮色:

演示非功能性反应式编程-一个实时示例

具有多个端口的游戏的历史

标题指定我们正在制作 HP28S RPN 游戏的一个端口,所以让我们来看一下我们正在实现的特定游戏的一点历史。

这个游戏有不同的实现和不同的端口,包括在 C 中的重新实现,以及使用 HTML 或 JavaScript 的几种方式。原始版本是在 HP28S 上,这是一个黑客式的科学计算器,可以有 32KB 或 512KB 的 RAM(我的有 512KB)。编程和使用(两者在 Unix/Linux shell 编程中并没有太大的不同)逆波兰表示法RPN)(en.wikipedia.org/wiki/Reverse_Polish_notation)。在计算器中有很多有趣的深度,我记得我做了两个程序。一个是一个具有谦卑的二维蹒跚醉酒算法的分形屏幕保护程序(参见tinyurl.com/reactjs-staggering-drunk),另一个是将在这里重新实现的视频游戏。

基本游戏采用 dingbat 字符图形实现,一艘太空船从左到右移动,在一个从级别到级别变得更加密集的小行星场中。主要的游戏机制旨在躲避你所经历的小行星。你也可以射击小行星。这真的是必要的,因为一些(天真地)随机绘制的级别并不一定有明确的路线可用。为了阻止游戏机制简单地射击通过每个级别,射击小行星是受到惩罚的,并且意味着更多的是最后的手段而不是旨在作为主要游戏机制的操纵。我的一个朋友评论说,这是他所知道的第一个玩家实际上因射击东西而失去分数的视频游戏。

网页的 HTML

我们以标准的 HTML5 DOCTYPE 开头:

<!DOCTYPE html>

随后,我们打开文档,指定 UTF-8 作为字符集。如果网页正确提供,字符集应该在页面下载时被指定,但这在防御性编码方面仍然可能有所帮助,这总是值得记住的事情:

<html lang="en">
  <head>
    <meta charset="utf-8" />

因此文档标题:

    <title>A video game on an HP-28S scientific
      calculator</title>

这里使用的字体是复古的 VT 系列字体,与受人尊敬的 VT100 和其他系列的 Unix 终端相关联。请注意-正如稍后将在代码中看到的那样-虽然 VT100 系列是等宽终端,但该字体并不严格是等宽字体,只是在行内显示每行空格或小行星将产生不希望的间距,因此每个字符都被绝对定位。也许另一个字体可能不会有这个问题,但 VT100 字体有一种很好的复古色彩。

请注意,我们将为大部分字符图形包括装饰符号。它们在 JavaScript 中处理。

像其他在 HTML 中使用的标签一样,字体标签是通过 HTTP/HTTPS 两个斜杠的模糊格式编写的,http:https:不指定,并且被提供为与网页中相同的格式:

  <link
    href='//fonts.googleapis.com/css?family=VT323'
     rel='stylesheet' type='text/css' />

在任何可以的地方使用内容分发网络

我们从内容分发网络CDN)加载 ReactJS,遵循史蒂夫·索德斯广泛建立的 Yslow(“我的网页为什么加载慢?”)建议。

注意

史蒂夫·索德斯(SteveSouders.com)最初在雅虎发现,渲染网页更快实际上并不是关于削减服务器端性能的毫秒或微秒。在影响客户端更高效方面有一个显著的低成本果实,例如,当可以从计算机缓存中以闪电般的速度加载资源时,不再一遍又一遍地从网络加载相同的资源。

有很多 JavaScript 库和框架可以从 CDN 中获取,包括 ReactJS,但几乎任何其他你想要使用的主要或次要 JavaScript 工具也都可以找到。

一些简单的样式

我们为页面添加了一些基本样式。背景图像是从haywardfamily.org/hp28s.png加载的。如果需要,你可以制作本地副本,或者如果在 HTTPS 上有问题,或者如果你在本地提供文件时 HTTP/HTTPS 模糊格式遗憾地无法工作。

p#display中的文本颜色取自 HP28S 计算器的屏幕截图:

一些简单的样式

<style type="text/css">
  body
  {
  background-image:
  url(//haywardfamily.org/hp28s.png);
  background-position: top left;
  background-repeat: no-repeat;
  height: 670px;
  width: 860px;
  }
  div#main
  {
  height: 670px;
  width: 860px;
  }
  p#display
  {
  color: #4f5c65;
  font-family: VT323, courier, sans;
  font-size: 18px;
  letter-spacing: 4px;
  left: 565px;
  top: 180px;
  position: absolute;
  }
  p#legend
  {
  background-color: rgba(0, 0, 0, .6);
  border-radius: 20px;
  color: white;
  font-family: Verdana, arial, sans;
  margin-left: 40px;
  margin-right: 90px;
  margin-top: 40px;
  padding: 10px;
  }
</style>
</head>

这是页面头部的最后一部分。

一个相当简单的页面主体

我们构建页面主体,其中包含 HP-28S 计算器的图像作为背景。我们还包括一个简短的图例和游戏在虚拟计算器屏幕上显示的空间:

<body>
  <div id="main">
    <p id="legend">
      Arrow keys to move, Space to shoot.
    </p>
    <p id="display">
    </p>
  </div>

在关闭 body 标签之前,我们加载主要脚本,它将使用 ReactJS 来为游戏添加动画:

<body>
  <script
    src="img/react.js"></script>
  <script type="text/javascript"
    src="img/hp28s.js"></script>
</body>
</html>

这是页面的 HTML 结束。现在将跟随包含真正的编程内容的 JavaScript。

动画页面的 JavaScript

我们可能简要指出,脚本是常规的 JavaScript,而不是 ReactJS 的 JSX 格式,它允许混合类似 HTML 的 XML 和 JavaScript,并被称为在脚本中放置尖括号的工具。并非所有人都会使用 JSX,但如果没有其他选择,了解这个工具也是值得的。

JSX 有很多优点,值得考虑。它被一些非 Facebook ReactJS 用户使用,但并非所有人都使用,同时也被 Facebook 使用。Facebook 一直支持 JSX,但并没有要求使用 JSX 来使用 ReactJS。开发目的上,JSX 脚本可以在网页加载后从cdnjs.cloudflare.com/ajax/libs/react/0.13.3/JSXTransformer.js加载,并在浏览器中编译。生产目的上,它们需要在 JavaScript 中编译,这时你可以运行npm install jsx,然后从命令行运行jsx编译器,具体操作可参考www.npmjs.com/package/jsx

简短的语法说明 - 立即调用的函数表达式

在我们的脚本中,我们使用立即调用函数表达式IIFE),以便我们的局部变量,使用var关键字在函数或其依赖项的某处定义,将受到闭包内的私有保护。这将避免共享可变状态的问题(因为它具有非共享的可变状态)。共享的可变状态会使程序的稳定性取决于任何有足够访问权限来修改状态的人。由于 JavaScript 语法的怪癖,该函数被括号包裹,其中以函数开头的行被视为函数定义,因此以下语法将不起作用:

function() {
}();

解决方案是将函数放在一对括号中,然后它将正常工作:

(function()
    {
    })();

回到我们的主脚本!

变量声明和初始化

我们的主wrapper函数通过编写仅在函数中使用的状态变量,开始非反应性和命令性地运行:

function()
{
  var game_over_timeout = 2000;
  var game_over_timestamp = 0;
  var height = 4;
  var tick_started = false;
  var width = 23;
  var chance_clear;
  var game_over;
  var level;
  var rows;
  var score;
  var position;
  var row;

声明并在某些情况下初始化了这些变量后,我们继续进行游戏启动的函数。这将初始化或重新初始化变量,但不包括初始化关卡。它通过将game_over变量设置为false,将玩家放在第 1 级,设置(水平)位置在屏幕/小行星区域的左侧 23 个字符宽度的开始处,以及垂直位置在第 1 行(顶部下方的第 1 行,共 4 行),得分为 0,并且大多数空间都是清晰的(即,没有小行星,因此玩家的飞船可以安全地行驶)但是小行星的密度不断增加!这 5/6 是空间不被小行星占据的机会呈指数衰减的开始。后者是一个可以调整的参数,以影响游戏的整体难度,较低的值会使领域更难以导航。在关卡之间,在空间清晰的机会呈指数衰减;指数衰减的速率,或者该功能的其他方面也是可以修改以影响关卡之间游戏难度的。

在这里,我们可以看到当玩家几乎清除了第一关时显示的样子:

![变量声明和初始化](img/B04108_04_3.jpg)

生成的关卡大部分是空格,有随机的可能性出现小行星,但最初容纳飞船的空间和其前面的空间总是清晰的。这是为了让玩家有一些空间来做出反应,而不是自动死亡。

用于启动或重新启动游戏的函数

在声明后立即调用该函数:

    var start_game = function()
    {
      game_over = false;
      level = 1;
      position = 0;
      row = 1;
      score = 0;
      chance_clear = 5 / 6;
    }
    start_game();

创建游戏关卡的函数

get_level()函数中,构建了一个级别。空间清晰的概率经历了指数衰减(衰减后的第一个级别为 0.75,然后为 0.675,然后为 0.6075,依此类推),随着小行星的密度相应增加,然后构建了一个矩形的字符数组的数组(字符数组用于集合中的字符,这些字符经历了接近恒定的变化,而不是字符串,字符串是不可变的,即使原始实现操作了字符串)。请注意,在这里的内部表示中,事物是由字符代码表示的:a表示小行星,s表示玩家的飞船,空格表示空白,依此类推。(现在将一个数组的数组的字符存储为对其他字符的引用可能有点奇怪。在原始的遗留系统上,现在显而易见的方法尚不可用。现在可能会对其进行重构,但是本章是一个旨在类似于处理遗留代码时获得良好结果的代码章节,这个瑕疵的存在是有意的。大多数开发人员所做的工作包括与遗留功能进行接口。)最初,所有空间都有可能被小行星占据。之后,清除了飞船的初始槽和其前面的空间。这是一个例子:

var get_level = function(){
  level += 1;
  rows = [];
  result = {};
  chance_clear *= (9 / 10);
  for(var outer = 0; outer < height; ++outer)
  {
    rows.push([]);
    for(var inner = 0; inner < width; ++inner)
    {
      if (Math.random() > chance_clear)
      {
        rows[outer].push('a');
      }
      else
      {
        rows[outer].push(' ');
      }
    }
  }
  rows[1][0] = 's';
  rows[1][1] = ' ';
  return rows;
}

虽然这个函数返回行的网格,但是行的网格将被分配为将与 ReactJS 一起使用的对象的字段。ReactJS 与对象上的属性一起工作得更好,而不是与数组上的属性一起工作。

前面函数调用的结果存储在 board 变量的一个字段中,并为按键定义了一个数组。在移动结束时,最后一个按键(如果有)从keystrokes数组中取出,然后清空数组,以便飞船根据上一次按键(如果有)在回合期间输入的按键移动。所有其他按键都将被忽略:

    var board = {rows: get_level()};
    var keystrokes = [];

使用 ReactJS 类来动手实践

现在我们将直接开始与 ReactJS 进行交互。我们创建一个 ReactJS 类,使用特定字段命名的函数哈希。例如,componentDidMount()函数和字段是在 ReactJS 组件挂载时调用的函数。这意味着它基本上是在 DOM 中显示和表示的。在这种情况下,我们向文档的主体添加事件侦听器,而不是直接向 ReactJS 组件添加事件。这是因为我们想要监听按键按下/按键事件,而很难让DIV对这些事件做出响应。因此,主体添加了事件侦听器。它们将处理 ReactJS 中的事件处理程序,这些事件处理程序仍然应该像通常显示它们一样定义。请注意,一些其他类型的事件,例如一些鼠标事件(至少),将通过 ReactJS 通常的方式注册,如下所示:

    var DisplayGrid = React.createClass({
      componentDidMount: function()
      {
        document.body.addEventListener("keypress",
        this.onKeyPress);
        document.body.addEventListener("keydown",
        this.onKeyDown);
      },

ReactJS 中的组件具有属性状态。属性是一次定义的东西,不能更改。它们在 ReactJS 内部可用,并且应该被视为不可变的。状态是可变的信息。属性和状态都在 ReactJS 中可用。我们可以简要地评论说,Facebook 和 ReactJS 正确地将共享的可变状态视为引发 Heisenbugs。在这里,我们从闭包中处理所有可变状态。可变状态不是共享的,也不应该共享(换句话说,它是非共享的可变状态)。

      getDefaultProps: function()
      {
        return null;
      },
      getInitialState: function()
      {
        return board;
      },

接下来,我们定义按键和按键事件处理程序,就像它们通常被使用的那样,或者至少在 DIV 响应按键事件时通常被处理的那样。(实际上,我们将监视 body,因为与悬停或鼠标点击不同,按键相关事件不会传播到包含的 DIV。这近似于您通常如何演示 ReactJS 中的事件处理。我们正在监听的特定按键,箭头键和空格键,存在一个问题。实质上,箭头键触发按键按下事件,但不触发按键事件(大多数其他键会触发按键事件)。这种行为很愚蠢,但它已经深入到 JavaScript 中,现在基本上是不可协商的。我们委托给一个通用的事件处理程序来处理这两个事件。在这里,按键被转换为键码:左或上箭头键向上移动(或向左,从游戏的方向来看),右或下箭头键向下移动(或向右,从游戏的方向来看),空格键射击。这些分别由keystrokes数组中的uds表示。

      onKeyDown: function(eventObject)
      {
        this.onKeyPress(eventObject);
      },
      onKeyPress: function(eventObject)
      {
        if (eventObject.which === 37 ||
        eventObject.which === 38)
        {
          keystrokes.push('u');
        }
        else if (eventObject.which === 39 ||
        eventObject.which === 40)
        {
          keystrokes.push('d');
        }
        else if (eventObject.which === 32)
        {
          keystrokes.push('s');
        }
      },

在这一点上,我们创建了render()函数,这是一个核心的 ReactJS 成员来定义。这个渲染函数的作用是创建 DIV 和 SPAN,以适当的方式表示空格和符号的网格。叶节点被绝对定位。

在构建了叶 SPAN 节点和中间 DIV 后,我们构建到主 DIV 元素。

out_symbol变量是一个 UTF-8 字符,而不是 ASCII 转义;这是有一个非常具体的原因。尽管 ReactJS 有一个明确定义的转义口 dangerouslySetInnerHTML()(参见tinyurl.com/reactjs-inner-html),通常设置为抵抗 XSS(跨站脚本)攻击。因此,它的正常行为是转义尖括号和大量的和符号使用。这意味着&nbsp;将被渲染为源码中的&nbsp;,而不是一个(不换行和不折叠)空格。因此,我们使用的 dingbat 符号不像其他地方那样使用转义码(尽管这些转义码在这里作为注释留下了),而是作为 UTF-8 存储在 JavaScript 中。

如果您不确定如何输入 dingbats,您可以简单地使用其他东西。或者,您可以将注释中的转义复制到普通的简单 HTMLPOSH)文件中,然后从渲染的 POSH 页面中复制并粘贴半打符号到您的 JavaScript 源代码中。您的 JavaScript 源代码应该被视为 UTF-8。

  render: function()
  {
  var children = ['div', {}];
  for(var outer = 0; outer <
  this.state.rows.length; outer += 1)
  {
    var subchildren = ['div', null];
    for(var inner = 0; inner <
    this.state.rows[outer].length;
    inner += 1)
    {
      (var symbol =
      this.state.rows[outer][inner];)
      var out_symbol; 
      if (symbol === 'a')
        {
          // out_symbol = '&#9632;';
          out_symbol = '■';
        }
        else if (symbol === 's')
        {
          // out_symbol = '&#9658;';
          out_symbol = '►';
        }
        else if (symbol === ' ')
        {
          // out_symbol = '&nbsp;';
          out_symbol = ' ';
        }
        else if (symbol === '-')
        {
          out_symbol = '-';
        }
        else if (symbol === '*')
        {
        out_symbol = '*';
        }
        else
        {
          console.log('Missed character: '
          + symbol);
        }
        subchildren.push(
        React.createElement('span',
        {'style': {'position': 'absolute',
          'top': 18 * outer - 20), 'left':
          (12 * inner - 75)}}, out_symbol));
        }
        children.push(
        React.createElement.apply(this,
        subchildren));
      }
      return React.createElement.apply(this,
      children);
    }
  });

在前面的代码中定义的 children 和 subchildren 填充了React.createElement()的参数列表。

在内部循环中构建完毕后,我们向subchildren数组添加一个叶节点。它被指定为一个 span,内联 CSS 样式以哈希的形式传递,并且内容等于out_symbol变量。然后将其添加到children数组中。它包含屏幕的行,这些行最终构建成完整的棋盘。

在 ReactJS 中,组件是在React.createElement()中定义的,随后可以供使用。React.createElement()的常规调用方式是React.createElement( 'div', null, ...),省略号部分包含所有的子元素。我们使用apply()来调用React.createElement(),并传入所需的初始参数,然后在数组中指定参数。

滴答滴答,游戏的时钟在滴答

这关闭了render()字段和React.createElement()类定义。在源代码中,我们继续进行tick()函数的处理。它处理每一轮应该发生的事情。目前,代码以 300 毫秒(0.3 秒)的间隔调用tick(),尽管这是可以调整的,以影响游戏玩法,或者稍微重构以使游戏玩法随着级别的提高而加速。

如果游戏结束,这只能是因为飞船撞到了小行星,tick()调用中什么也不会发生:

    var tick = function()
    {
      if (game_over)
      {
        return;
      }

接下来,调用React.render(),指定要呈现的类以及要呈现到的 HTML 元素。React.render()应该至少在每次想要呈现东西的时候调用。如果你只调用一次,它将只呈现一次,这意味着如果你想要重复的更新显示,就需要重复调用它。在这里,我们在tick()方法的每次调用中调用它,要求基于前面大部分代码中定义的DisplayGrid创建一个元素,并将呈现的 HTML 放入具有显示 ID 的 DIV 中:

    React.render(
      React.createElement(DisplayGrid, {}),
      document.getElementById('display'));

在这里,我们看到玩家射击了一个小行星的屏幕。小行星爆炸成了一个星号!

滴答,滴答——游戏的时钟在滴答

如果在上一轮中,飞船射击了一个小行星(在字符符号中表示为零个或多个连字符和右侧的星号;连字符填充了射击到的小行星之前的空间,星号代表了射击命中小行星的爆炸),我们清除显示在那一轮中射击的槽:

    for(var outer = 0; outer < height; outer += 1)
    {
      for(var inner = 0; inner < width; inner += 1)
      {
        if (board.rows[outer][inner] === '-' ||
        board.rows[outer][inner] === '*')
        {
          board.rows[outer][inner] = ' ';
        }
      }
    }

做完这些之后,我们清除了指示已经射击的变量:

    var shot_taken = false;

我们清除了飞船所在的空间:

    board.rows[row][position - 1] = ' ';

在每个滴答结束时,keystrokes数组被清除,并且我们注意存储的最后一个击键。换句话说,我们关注上一个回合之后存储的最后一个击键。击键在回合之间不会累积。在回合结束时,最后一个击键是唯一获胜的击键。

keystroke数组存储的是键码,而不是确切的击键。箭头键已经被处理,左箭头或上箭头按下将存储一个u表示向上,右箭头或下箭头按下将存储一个d表示向下,空格键按下将存储一个s表示射击。如果有人输入上或下,飞船将在边界内向上或向下移动:

    if (keystrokes.length)
    {
      var move = keystrokes[keystrokes.length – 1];
      if (move === 'u')
      {
        row -= 1;
        if (row < 0)
        {
          row = 0;
        }
      }
      else if (move === 'd')
      {
        row += 1; 
        if (row > height - 1)
        {
          row = height - 1;
        }
      }

如果用户射击了一个小行星,在下一轮中,一排连字符将从飞船的前端延伸到小行星,小行星将变成一个星号,表示爆炸:

    else if (move === 's')
    {
      shot_taken = true;
      score -= 1;
      var asteroid_found = false;
      for(var index = position + 1; index <
      width && !asteroid_found; index += 1)
      {
        if (board.rows[row][index] === 'a')
        {
          board.rows[row][index] = '*';
          asteroid_found = true;
        }
        else
        {
          board.rows[row][index] = '-';
        }
      }
    }
    keystrokes = [];
  }

提示

下载示例代码

您可以从您在www.packtpub.com的帐户中下载您购买的所有 Packt Publishing 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便直接通过电子邮件接收文件。

游戏结束

如果用户撞到了小行星,游戏结束。然后我们显示一个游戏结束屏幕,并停止进一步处理,就像这样:

游戏结束

        if (position < width)
            {
            if (rows[row][position] === 'a')
                {
                game_over = true;
                game_over_timestamp = (new
                  Date()).getTime();
                (document.getElementById(
                  'display').innerHTML =
                  '<span style="font-size: larger;">' +
                  'GAME OVER' +
                  '<br />' +
                  'SCORE: ' + score + '</span>');
                return;
                }

只要用户没有撞到小行星,游戏仍在进行,我们用飞船的标记替换行中的当前槽。然后增加玩家的(水平)位置:

            board.rows[row] = board.rows[row].slice(0,
              position).concat(['s']).concat(
              board.rows[row].slice(position + 1));
            position += 1;
            }

如果用户“掉出了屏幕的右边缘”,我们将游戏提升到下一个级别:

        else
            {
            rows = get_level();
            score += level * level;
            position = 0;
            row = 1;
            }
        }

定义了所有这些之后,我们开始游戏,如果我们还没有启动滴答声,我们会以 300 毫秒的间隔启动滴答声(这个值可以调整,以使游戏更容易或更难;它可以成为一个可配置的间隔,随着游戏的进行而加快):

    start_game();
    if (!tick_started)
        {
        setInterval(tick, 300);
        tick_started = true;
        }
    })();

总结

在本章中,我们涵盖了很多内容。早些时候,我们已经涵盖了一些理论,但是在这里,我们开始使用一些 ReactJS 来拼凑一个应用程序。以后,我们将使用一个更深入的应用程序。

涵盖的主题包括网页的 HTML。这是一个简单的 HTML 骨架,用作保存反应式 JavaScript 的架子。另一个涵盖的主题是反应式 JavaScript。这包括了 JavaScript 的混合,其中有一个清晰的示例,展示了如何为 ReactJS 编写反应式 JavaScript。

我们将在下一章继续介绍函数式编程。

第五章:学习函数式编程-基础知识

JavaScript 是一种多范式语言,对于它触及的任何范式都不完美,但它具有其主要范式的有趣特性。它是一种面向对象的语言,尽管面向对象的定义在面向对象的语言之间有所不同。有人建议,它的原型继承对于面向对象编程可能不如演示如何创建无类对象重要,而不是在开始时就把分类搞对的困难任务。面向对象的定义在具有面向对象特性的多范式语言之间也有所不同。例如,Python 动态允许向现有对象添加成员,而 Java 要求在类中定义成员。JavaScript 的面向对象特性是有用和有趣的,但特别是在过去几年里,它们一直是其他面向对象语言的程序员的挫折之源,他们被告知 JavaScript 是面向对象的,但没有足够的信息来解释 JavaScript 如何通过与其他主要语言根本不同的方法来实现面向对象。

同样,对于函数式编程,JavaScript 具有函数式编程支持,或者至少有一些支持。但是像整个 JavaScript 一样,函数式 JavaScript 并不完全符合好的部分。函数式编程语言的一个普遍特征(虽然不是普遍的)是尾调用优化,它表示只在末尾递归的递归函数在内部被转换为更常见的循环样式,速度更快,并且可以在不耗尽调用堆栈空间的情况下进行非常深的递归。这种优化计划在 ECMAScript 6 中实施,但在撰写本书时,它尚未在常见浏览器中实施,这不仅提供了较慢的性能,还限制了递归深度在大约 10,000 到 20,000 次之间。

在这个限制内可以做很多事情,但是结构化程序编写者如果他们的for循环不能实现远远超过 20,000 次的迭代,就会感到不满。这里的重点不是指定 JavaScript 不总是支持尾调用优化的最佳解决方案,而是指出这个困难目前存在,并且这是 JavaScript 不直接支持标准函数语言特性的少数几种方式之一(例如,计算从 1 加到 1,000,000 或更高的所有整数并不是特别有趣,但它在教程中作为标准示例)。

关于 JavaScript 是否应该被称为函数式语言,文献意见不一;它肯定不是像 Haskell 那样的纯函数式语言(但 OCaml 也不是)。JavaScript 被称为一种已知函数式语言 Scheme 的 C 语言版本,并且它的基本函数式特性并不是事后添加的东西。也许这反映了他的偏好,但是道格拉斯·克罗克福德在评判 JavaScript 语言的哪些部分是好主意时,从JavaScript:好的部分更好的部分,他从 ECMAScript 6 开始的偏好之一是停止使用命令式风格的forwhile循环,而是使用利用尾调用优化的递归。也许 JavaScript 具有函数式内核的最有力的主张可以在语言的哪个特性是中心的问题上看出来。有人建议,在 Java 中,中心特性是对象。在 C 中,是指针。在 JavaScript 中,是函数。

JavaScript 的函数具有一流的地位,这意味着(高阶)函数可以作用于其他函数并作为参数传递,甚至可以动态构建并作为结果返回。

在本章中,我们将涵盖:

  • 自定义排序函数

  • 映射、减少和过滤

  • 愚人的金子-改变其他人原型的行为

  • 闭包和信息隐藏

自定义排序函数-函数式 JavaScript 和一级函数的第一个示例

为了打破僵局,让我们来看看如何对 JavaScript 的数组进行排序。JavaScript 的数组有一个内置的sort()函数,至少可以说是一个合理的默认值。例如,如果我们创建一个包含π的前六位数字的数组,我们可以对其进行排序:

var digits = [3, 1, 4, 1, 5, 9];
digits.sort();
console.log(digits);

Chrome 的调试器在控制台上显示了一个数组,我们可以访问:

Array[6]
   0: 1
   1: 1
   2: 3
   3: 4
   4: 5
   5: 9
   length: 6
     __proto__: Array[0]

这很好。让我们再进一步,尝试混合整数和浮点小数(浮点数)。请注意,在 JavaScript 中,有一种数值类型,它的行为类似于整数(并保持整洁)对于在 Firefox 中介于-(253-1)或-9007199254740991 和 253-1 或 9007199254740991 之间的整数。这种数值类型也存储浮点数。它们具有更大的范围,当然,对于较小的数字,有更精细的值。为了进一步扩展范围,让我们创建一个包含整数和浮点数混合的数组:

var mixed_numbers = [3, Math.PI, 1, Math.E, 4, Math.sqrt(2), 1,
  Math.sqrt(3), 5, Math.sqrt(5), 9];

在这些数字中,Math.PI大约是 3.14,Math.E大约是 2.72,Math.sqrt(2)大约是 1.41,Math.sqrt(3)大约是 1.73,Math.sqrt(5)大约是 2.24。让我们像其他数组一样对其进行排序并记录这些值:

[1, 1, 1.4142135623730951, 1.7320508075688772, 2.23606797749979, 2.718281828459045, 3, 3.141592653589793, 4, 5, 9]

Chrome 的调试器,出于某种原因,这次表现不同,显示的是一个字符串数组,而不是一个带有向左的下钻三角形的数组。然而,数组被正确排序,所有值都按升序排列,整数和浮点值显示正确。

让我们在字符串上试一试。假设我们有以下数组:

var fruits = ['apple', 'durian', 'banana', 'canteloupe'];

当我们对其进行排序时,得到了这个:

["apple", "banana", "canteloupe", "durian"]

这是有序的,很好。让我们在数组中间添加一点内容:

var words = ['apple', 'durian', 'Alpha', 'Bravo', 'Charlie',
  'Delta', 'banana', 'canteloupe'];

我们对其进行排序,得到了以下结果:

  ["Alpha", "Bravo", "Charlie", "Delta", "apple", "banana",
    "canteloupe", "durian"]

这是什么?所有新单词都在开头,所有旧单词都在结尾!也许在它们自己之间排序,但是由大小写分隔。

这是因为字符串排序是根据 Unicode 值的字典顺序,这与 ASCII 编码相同,对于 ASCII 的一部分字符。在 ASCII 中,所有大写字母都排在所有小写字母之前。在这里,大写字母在大写字母内部被正确排序,小写字母在小写字母内部被正确排序,但这两者是分开的。如果我们希望所有的'A'都排在所有的'B'之前,我们需要更具体地说明我们想要什么。

我们可以通过提供一个比较函数来实现这一点-一个将比较两个元素并告诉Array.sort()哪个应该先的函数。让我们为这些单词制作一个不区分大小写的排序:

var case_insensitive_comparison = function(first, second) {
  if (first.toLowerCase() < second.toLowerCase()) {
    return -1;
  } else if (first.toLowerCase() > second.toLowerCase()) {
    return 1;
  } else {
    return 0;
  }
}

然后我们对数组进行排序并指定比较函数:

words.sort(case_insensitive_comparison);

当我们记录排序后的数组时,我们看到了一个不区分大小写的字母顺序:

["Alpha", "apple", "banana", "Bravo", "canteloupe", "Charlie",
  "Delta", "durian"] 

如果我们希望大写字母作为一个决定因素,并且大写字母会在其小写字母等价物之前放置,该怎么办?这是我们比较器的一个简单修改:

var mostly_case_insensitive_comparison = function(first, second) {
  if (first.toLowerCase() < second.toLowerCase()) {
    return -1;
  } else if (first.toLowerCase() > second.toLowerCase()) {
    return 1;
  } else {
    if (first < second) {
      return -1;
    } else if (second < first) {
      return 1;
    } else {
      return 0;
    }
  }
}

让我们在字符串列表的末尾添加'ALPHA'和'alpha',然后重新排序:

["ALPHA", "Alpha", "alpha", "apple", "banana", "Bravo",
  "canteloupe", "Charlie", "Delta", "durian"]

成功了!

这可能需要,也可能不需要仅仅是字符串比较,但是如果服务器运行了数据库查询并为我们打包了 JSON 结果呢?一旦在客户端解析,结果可能是具有相同结构的对象数组。电子客户联系信息可能包括以下内容:

{
  "email": {
    "personal": "jsmith@gmail.com",
    "work": "john.smith@company.com"
  },
  "name": {
    "first": "John",
    "last": "Smith"
  },
  "skype": {
    "personal": "JohnASmith",
    "work": "JASCompany"
  }
}

这种记录结构可能不是 JavaScript 本能地推断出我们希望看到的排序方式,但这并不真正伤害我们。如果我们构建一个比较函数,它可以完全访问要比较的项目的字段或其他细节。这意味着我们可以先比较一个字段,然后再比较另一个字段,然后再比较第三个字段。这也意味着我们可以按不同的标准进行比较;在某个时候,我们可以按名称比较,而在另一个时候,我们可以按地理位置比较。如果我们的服务器存储(或查找)地址的 GPS 坐标,我们可以按离特定位置最近的人进行搜索。

这引出了数组.filter()

在函数式语言中,诸如映射、减少和过滤等功能是日常使用的基本功能。它们操作列表,在更多功能性、以列表为中心的语言中,列表可以是有限的,也可以是无限的。在这个意义上,列表可能更像 JavaScript 数组或生成器,一种函数,它不是返回单个值,而是产生零个或多个值,并且在理论上可能产生无限数量的值。与数组不同,任何给定的生成器可能永远不会耗尽,即使它实际上从不产生无限数量的值。生成器是一个很棒的功能,但在撰写本书时,它们在浏览器中的支持并不稳定,这意味着我们更可能在(有限的)数组上而不是在生成器上使用映射、减少和过滤。

但在我们放弃生成器这个话题之前,让我们给出两个生成器的例子,这两个例子都会在不久之后溢出,但它们都是在像 Haskell 这样的语言中可能被认为是一个数学序列的无限列表的例子,而不是仅包含第一个n成员的数组。我们将使用 ECMA6 提议的生成器语法来查看 2 的幂和斐波那契数的生成器,如wiki.ecmascript.org/doku.php?id=harmony:generators中所讨论的:

function* powers_of_two_generator() {
  var power = 1;
  while (true) {
    yield power;
    power *= 2;
  }
}

function* fibonacci_generator() {
  var first = 0;
  var second = 1;
  var sum;
  yield second;
  while (true) {
    sum = first + second;
    yield sum;
    first = second;
    second = sum;
  }
}

将这些示例与使用标准递归方法计算 2 的 n 次幂(这实际上并不需要,因为 JavaScript 的算术处理指数,但为了完整起见,还包括了这一点)以及计算第 n 个斐波那契数的天真实现进行对比。这两种方法都是所谓的尾递归,并且如果浏览器提供了尾调用优化,它们将受益匪浅:

function power_of_two_recursive(n) {
  if (n === 0) {
    return 1;
  } else {
    return 2 * power_of_two_recursive(n – 1);
  }
}

function fibonacci_recursive(n) {
  if (n === 0 || n === 1) {
    return 1;
  } else {
    return (fibonacci_recursive(n – 2) +
      fibonacci_recursive(n – 1));
  }
}

这两个函数都假设参数为非负整数。第二个函数的性能特征也很糟糕,尽管内存使用并不特别糟糕。然而,函数调用的次数与返回的值相当,因此计算第 100 个斐波那契数,除了整数溢出的问题,可能比宇宙的年龄还要长。正如 Donald Knuth 所说,“过早的优化是万恶之源”,但这是一个不需要过早优化的情况。

请注意函数式编程的另一个特性,称为记忆化——这意味着保留中间计算的结果,而不是反复从头开始重新生成它们,从而完全避免了性能瓶颈。考虑下面递归斐波那契函数的记忆化:

var calculated_fibonacci_numbers = [];

function fibonacci_memoized(n) {
  if (calculated_fibonacci_numbers.length > n) {
    return calculated_fibonacci_numbers[n];
  } else {
    if (n === 0 || n === 1) {
      result = 1;
    } else {
      result = (fibonacci_memoized(n – 2) +
      fibonacci_memoized(n – 1));
    }
    calculated_fibonacci_numbers[n] = result;
    return result;
  }
}

幻觉主义,映射,减少和过滤

在我小时候,我对幻术非常感兴趣,我现在还有一个幻术师的道具——里面有(或曾经有)一些东西,比如一个假拇指和一个戏法杯——还有一些幻术书。我记得的一个戏法是把一根绳子绕在大腿上,然后再绕在手上。如果一个人的腿放松,绳子看起来很紧,但如果一个人抬起腿,绳子就松了很多,从而给人一种被牢牢捆绑的印象,而实际上很容易解开一个或两个手。

我从来不擅长业余魔术表演的一面,这实际上是这门手艺的核心。资深的魔术师在指导他们的后辈或有抱负的人时,往往会说诸如“取悦观众并欺骗他们,但要知道哪个先来”。我记得很长一段时间以来,我一直认为我不懂(技术方面的)真正的魔术,因为我技术上知道如何做几个戏法,但我不知道如何去做我看到的那些事情。

后来,在我公司的派对上有一个魔术师,我因为一个不寻常的原因而着迷。他做了一些对我来说新奇的戏法,但在大约 70%或 80%的时间里,他花了很多精力利用我小时候学到的绳索戏法。而且效果非常好。他有精湛的表演技巧,而我的着迷并不是想知道他是如何技术上完成这个戏法的,而是对这样一个擅长的表演者能够利用一个孩子都能做的两个戏法来取悦观众感到惊叹。

Map,reduce 和 filter(这里,“reduce”包括右折叠和左折叠)在函数式编程中有些类似。Map 接受并将其应用于列表的所有成员。Reduce 接受一个操作,并从右侧或左侧开始,将其应用于每个成员,并得到一个中间结果。Filter 接受一个函数和一个列表,并创建一个由函数为真的项目组成的新列表。这些概念将在本章中进行进一步解释和说明。Map,reduce 和 filter 并不是特别困难的概念,但是可以从中获得很多收益。让我们来看看数组的 map,reduce 和 filter,暂时不考虑生成器和 Haskell 等语言提供的潜在无限列表。我们将向您展示如何使用 JavaScript 的数组内置版本的 map,reduce 和 filter。我们还将研究使用核心 JavaScript 来实现这些函数,不是为了让人们可以在 IE8(及更早版本)中使用这些函数,而是为了让人们了解这些功能的工作原理。

在警告了愚人金之后,我们将探讨一种有点非函数式风格的实现。它们使用for循环,在纯函数式语言中,首选的解决方案可能是尾递归实现。选择这种方式的理由是为了以一种对 JavaScript 的管道操作效果最佳的方式提供函数式特性支持,并且不会在(非尾调用优化的)JavaScript 递归在极少数情况下达到极限时失败。

愚人金 - 扩展 Array.prototype

需要注意的是,一个吸引人的解决方案,而且可以很容易地实现,是扩展(这里)Array.prototype或其他对象的原型,包括Object.prototype。不要这样做。

扩展Array.prototype及其相关内容会破坏其他人软件的平衡;这就好像在没有看到其他人的代码的情况下重写其他人的代码。扩展基本原型的最佳用例可能是填充(使用可用功能重新实现当前环境中不可用的功能),但即使在这种情况下,如果存在竞争的填充,也只有一个可以胜出。现在,你的填充不太可能像主要浏览器制造商一样对错误兼容性进行测试。这为微妙的错误留下了空间。在我们的情况下,为了支持稀疏矩阵,我们忽略了未定义的条目,但不是 null。我认为在这种情况下这是合理的,但远非唯一可能的智者(或不那么聪明的人)会如何处理这个问题。JavaScript 有两个 null 值,nullundefined,对于这两个不同的 null 值应该如何处理,可能会有不止一个观点。如果对我们有意义的语义并不是对其他人明显的语义,我们想要打开滑动的 heisenbugs 之门吗?

有一个简单而好的替代方案:编写自己的函数,最好是在闭包内定义的匿名函数,并存储在一个变量中。如果需要,这些函数可以检查是否存在浏览器的内置函数,比如Array.prototype.map(),如果找到,则可以回退到内置函数。它几乎可以完成通过扩展Array.prototype实现的任何工作。但它展现了良好的习惯,不会给其他人带来困扰。

注意

JavaScript 中的匿名函数一词并不排除存储在命名变量中的函数。它只是意味着它们是在没有函数名称的情况下定义的。换句话说,它们是这样定义的:function()var foo = function()或其他替代方法,但不是在函数关键字和开括号之间有一个名称,即function bar()。通常,我们将使用匿名函数,无论它们是否存储在变量中,但是有一个与调试相关的原因,即使我们从不使用它,也可能会给函数命名:调试器的堆栈跟踪可能会更详细地提到函数,即使从未使用过这些名称。出于这个目的,写var quux = function quux()是有意义的。

关于我们可能在私下开发的事情,有一点需要注意:令人惊讶的是,许多 Unix 实用程序最初是作为解决不同人的本地问题的私人黑客行为而诞生的。像野火一样传播的东西通常不是被设计成像野火一样传播的东西,比如 Web、JavaScript 和 5.0 版本之前的 PHP。在它们的第一个版本中,它们做了一些特定的事情,并让人们努力以更完整的方式运行。

HTTP 的无状态性是一个精心选择的特性,但在那个时候,大部分的 Web 编程都在尝试支持无状态 HTTP 变得非常痛苦的用例。5 MB 的 HTML5 键值存储和 4096 字节的 cookie 上限可能存在差异,但它们在提供适当的有状态行为的钩子时都提供了更或多或少优雅的容纳:Web 是建立起来的,不是为了使动态内容成为今天所有 Web 内容的主要部分。JavaScript 有它的优点和缺点,它的缺点可能是任何一个非常成功的语言中最糟糕的,但它之所以广受成功和名声显赫并不是因为它的优点或缺点。它成功是因为它在 Web 迅速传播的时候被包含在浏览器中。JavaScript 和 Web 都有人试图修复它们的限制和缺点,以便在它们迅速传播后做好多种事情,而实际上它们只擅长做一件事。

“这只是一个角落里的东西,我们不需要考虑维护或互操作性”这种心态非常非常危险。也许现在你的软件不会因为微妙地重新定义对象或数组的行为而破坏任何东西,但永远不会吗?甚至在未来的任何决定中?即使有人意识到,在解决 X 的同时,你为 Y 创造了一个可以节省大量工作的强大引擎?客户端 JavaScript 是一些最快成为开源的代码(毕竟,即使是关心保持专有内容的律师也知道你的整个系统可以交付给从 Web 登录到系统的任何人),假设某种标准行为的重新定义只是未来的保证是危险的。

对前面问题的一个简短回答是:不要重新定义其他人构建的任何东西,包括重新定义Object.prototypeArray.prototypeFunction.prototype等部分。尽可能选择自己的实现,但不要(强制性地)为每个人安装它。

避免全局污染

最好的做法也是尽量减少对全局命名空间的侵入。你添加的全局变量越多,就越容易与其他工具发生冲突。当雅虎宣布 YUI 时,基本礼貌上,他们只使用了一个全局变量——YUI。有一个完整的库可用,但你不会在浏览器的全局命名空间中看到一页页的项目;每次调用 YUI().use()或其他内容都完全包含在 YUI 对全局命名空间的一个侵入中。jQuery 使用的全局命名空间比他们宣传的要多一点,但原则上,他们只要求我们使用jQuery$。此外,他们试图使第二个变量完全可协商,因为jQuery承认其他框架需要$,而jQuery旨在与其他框架友好相处。

然而,你实际上可以走得更远,通过立即调用的函数表达式,包括 ReactJS Web 应用程序在第八章中探讨的,在 JavaScript 中演示函数式响应式编程 - 实时示例到第十一章,在 JavaScript 中演示函数式响应式编程 - 实时示例第四部分 - 添加一个草稿并把它全部放在一起。你可以在不触及全局变量的情况下实现相当多的功能。也许库应该有一些全局变量作为公共面向其他人的接口,但完全可以制作一个不触及全局变量的 Web 应用程序。

map、reduce 和 filter 工具箱 - map

地图接受一个数组和一个函数,并返回一个将该函数应用于其所有元素的新数组。例如,让我们创建一个包含 1 到 10 的数字的数组,并使用地图创建一个新数组,其中包含它们的平方。(请注意,JavaScript 在数组选项修改数组原地、返回修改后的数组或两者都做方面有些不一致。数组的map()reduce()filter()函数都创建一个新数组,保持原始数组不变和不受影响。)

var one_to_ten = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
var squares = one_to_ten.map(function(x) {return x * x});

squares变量现在包含[1, 4, 9, 16, 25, 36, 49, 84, 81, 100]

map()的一个实现可能如下所示:

var map = function(arr, func) {
  var result = [];
  for(var index = 0; index < arr.length; ++index) {
    if (typeof arr[index] !== 'undefined') {
      result[index] = func(arr[index]);
    }
  }
  return result;
};

减少函数

减少函数的作用是,它接受一个操作,并逐步将其应用于数组中的元素。你可能在学校学过无限(和有限)级数,也许有这样的符号:

减少函数

在这种操作中,大写希腊字母 sigma(Σ,大致相当于希腊字母中的“S”)用于求和,较少地使用大写希腊字母 pi(Π,大致相当于希腊字母中的“P”)用于乘积。它们都反复将算术运算符应用于一系列有限(或无限)的数字,并且它们按照与reduce()相同的基本原理工作。

这种类型的符号所说的是,“对于这一系列数字,如果我们将它们相加,也就是说,用加法函数减少它们,取第一个数字和第二个数字,计算它们的和,然后将其添加到第三个数字,依此类推,我们会得到什么?”

如果我们希望将数组的内容相加,我们可以使用加法函数来减少它(作为一个小的实现细节,我们不使用裸的+运算符,因为我们不能直接将运算符作为常规函数传递)。

var sum = one_to_ten.reduce(function(x, y) {return x + y});
// returns 55

在学校教授的有限和无限级数通常是和;我们也可以使用其他级数。例如,如果我们想计算 10!,我们可以通过乘法而不是加法来减少我们提供的函数的项:

var factorial = one_to_ten.reduce(function(x, y) {return x * y});
// returns 3628800

减少不需要是数学性质的;这只是给我们一个快速演示它的方法。我们还可以使用 reduce 来连接字符串数组,其中+运算符用于字符串连接而不是数值相加:

var message1 = ['H', 'e', 'l', 'l', 'o', ',', ' ',
  'w', 'o', 'r', 'l', 'd', '!'].reduce(function(x, y)
  {return x + y});
var message2 = ['Hello', ', ', 'world', '!'].reduce(
  function(x, y) {return x + y});
// Both invocations return, 'Hello, world!'

然而,JavaScript 内置函数没有为我们解决的一个基本困难。有时我们需要做出选择,并进一步指定我们真正想要的是什么。数值相加、乘法和字符串连接都是可结合的,这基本上意味着你可以在任何地方放括号,遵循括号的标准规则,得到相同的答案。在数值相加中,以下是等价的:

1 + (2 + (3 + 4))
((1 + 2) + 3) + 4

这两个计算都得到10,如果我们乘以而不是相加,两者都给出24的乘积。但是,如果我们对非常略微不同的值使用指数运算符会发生什么:

Math.pow(2, Math.pow(3, 4))
Math.pow(Math.pow(2, 3), 4)

这与我们刚刚看到的代码是相同类型的东西,尽管显然使用了一个更丑陋的命名空间函数而不是中缀运算符。在非 JavaScript 符号中,使用一个脱字符(^),我们得到以下伪 JavaScript,它重新陈述了前面的计算:

2 ^ (3 ^ 4)
(2 ^ 3) ^ 4

如果我们使用console.log()和刚刚看到的Math.pow()计算,我们会得到这个:

2.4178516392292583e+24
4096

这里有一个细微的差别。一个结果是一个四位整数;另一个用科学计数法表示,比四位数多得多。那么,有多少东西真的像指数运算的特殊情况?

这个问题的答案有点棘手,部分原因是我欺骗性地构造了这个问题,以说明一个危险的误解。指数,括号的添加方式很重要,可能更像一般情况。存在一些情况,括号的位置并不重要,它们甚至更常见地成为reduce()的候选项,但在一般情况下,我们不应该假设这两者是等价的。我们将给出fold_left()fold_right()函数;这不是唯一的两个选项(如果它们都不是你想要的,你可以手动操作),但它们分别计算数组一到十的和如下:

((((((((1 + 2) + 3) + 4) + 5) + 6) + 7) + 8) + 9) + 10
1 + (2 + (3 + (4 + (5 + (6 + (7 + (8 + (9 + 10))))))))

两者并不一定有优劣之分,但差异可能很重要。JavaScript 内置的reduce()函数是一个左折叠,从左到右开始,如前两个表达式中所示(这可能是一个合理的默认值)。

如果我们定义fold_left()fold_right(),它可能看起来像下面这样。我在这里使用了缩写,因为全拼看起来太接近保留字;例如,array 不会与 Array 冲突,但它们之间有令人困惑的相似之处(如果变量命名为 function,也会有类似的冲突):

function fold_left(arr, fun) {
  var accumulator;
  for(var index = 0; index < arr.length; ++index) {
    if (typeof arr[index] !== 'undefined') {
      if (typeof accumulator === 'undefined') {
        accumulator = arr[index];
      } else {
        accumulator = fun(accumulator, arr[index]);
      }
    }
  }
  return accumulator;
}

function fold_right(arr, fun) {
  var accumulator;
  for(var index = arr.length - 1; index >= 0; --index) {
    if (typeof arr[index] !== 'undefined') {
      if (typeof accumulator === 'undefined') {
        accumulator = arr[index];
      } else {
        accumulator = fun(arr[index], accumulator);
      }
    }
  }
  return accumulator;
}

最后一个核心工具——过滤器

过滤器通过数组筛选出符合某些标准的值。例如,我们可以过滤出仅为正值的内容,如下所示:

var positive_and_negative = [-4, -3, -2, -1, 0, 1, 2, 3, 4];
var positive_only = positive_and_negative.filter(
  function(x) {return x > 0;});

positive_and_negative过滤器,在此运行后,如声明的那样;positive_only的数组值为[1, 2, 3, 4]

过滤对于缩小数组的内容很有用。如果我们有一个数组,如前面的一个,包含了 Smith 先生的联系信息,我们可以访问字段,缩小到可能符合我们兴趣的内容。我们可以声明我们只想要一个州,或者需要一个特定电话区号的电话号码,或者对函数能够告诉的任何内容陈述其他标准。如果我们的记录包括 GPS 坐标,我们可以过滤内容,只包括特定中心点半径内的结果。我们只需要一个函数,对于我们想要包括的记录返回 true,对于我们不想要的记录返回 false。

JavaScript 中信息隐藏的概述

一般来说——意味着任何编程语言、客户端、服务器端、非网络、移动应用程序,几乎任何其他东西,甚至可能是微控制器(现在可以运行精简版的 Python)——都有不同的方法论,通常都有各自的优势和劣势。Steve McConnell 的《代码大全:软件构建的实用手册》(tinyurl.com/reactjs-code-complete)讨论了不同的方法论,比如,面向对象编程的优势在于比过程式编程更适合大型项目。对于大多数方法论,他的建议是它们都有各自的优势、劣势和适用范围,在 X 和 Y 条件下,你应该考虑方法论 Z。但有一个例外——信息隐藏。他对何时使用信息隐藏的简单建议是“尽可能多地使用”。

程序化或结构化编程可能很容易被忽视,使用其优势并不是在突破界限。但是假设我们看看它最初发布时的情况,它的函数/过程,if-then-else,for/while循环和过程体不对外开放。现在,如果我们将其与直接的汇编或机器代码进行比较,使用了 Dijkstra 风格的 goto,甚至不假装模拟结构化控制流,那么我们就会明白,程序化或结构化编程真的是非常惊人的。此外,今天这显而易见的事实是因为它在很大程度上取得了成功,使每个人受益。流程图曾经是理解复杂系统的必备救生工具,现在已经成为一种新奇物品。它们出现在杯子上,在 XKCD 漫画中展示了如何提供出色的技术支持,或者在其他幽默的用途中,因为它们不再需要提供任何一种路线图来帮助一些人——找到他们穿过意大利面的方法。

现在,一个大型系统可能比旧的、流程图导航的程序所能容纳的要复杂得多,也要大得多,但是程序化编程已经有效地驱逐了那个幽灵。此外,软件工程范式中的新迭代,如面向对象编程,已经减少了理解大型系统的难度。在这两种情况下,很大一部分好处是一种推进信息隐藏的实用方式。通过结构化编程,您可以在源代码中导航,而无需跟踪汇编或机器语言呈现跳转(即 goto)的每个点。结构化和面向对象编程在历史上都允许开发人员前所未有地将更多的程序视为封闭的黑匣子,您只需要打开和检查其中的一小部分。它们提供了信息隐藏。

JavaScript 中信息隐藏的概述

前面的流程图是来自xkcd.com/627/的新奇流程图。我从未见过有人谈论的程序的真正流程图。

在经典的 Java 中,信息隐藏的标准教科书例子可能是这样的:

class ObjectWithPrivateMember {
  private int counter;
  public ObjectWithPrivateMember() {
    counter = 0;
  }
  public void increment() {
    counter += 1;
  }
  public void decrement() {
    counter -= 1;
  }
  public void get_counter() {
    return counter;
  }
}

与生产中的 Java 类的一个好例子不同,这个类有一个私有成员和四个公共成员。通常的目标是尽可能隐藏有趣的重要工作,并向世界展示一个简化的外观。这就是 Java 面向对象编程提供信息隐藏的方式,尽管面向对象语言之间在处理对象的方式和方法上有重要的区别,但这是 Java 的一大优势。对象方法不仅应该按过程方式编写——作为具有定义输入和输出的黑匣子——而且它们还封装在对象中,多个较小的黑匣子可以被包含在一个较大的黑匣子下。在这种情况下,我们看到了信息隐藏的另一个好处:我们在很大程度上受到外部保护,并且可以自由地进行任何内部更改,而不会破坏外部使用,只要我们保持相同的行为。假设我们决定保留计数器何时更改以及更改为何值的日志。这至少是另一个私有字段和对代表公共接口的方法的内部更改,但我们可以进行这些更改而不改变任何访问这个类的任何类的任何细节。现在假设我们想要更多的日志记录,我们希望我们的日志记录完整的堆栈跟踪。在内部,我们需要使用诸如Thread.currentThread.getStackTrace()这样的东西,但在外部,没有人需要知道或关心我们的重构。在一个更大的类中,我们可能会发现一个瓶颈,通过切换到另一个等效算法可以显著改进。由于 Java 面向对象编程提供信息隐藏的方式,我们可以进行大量的更改而不会打扰其他人,他们可以使用我们的工作而不必为除了我们的公共接口之外的任何事情而烦恼。

使用 JavaScript 闭包进行信息隐藏

我们需要进一步观察信息隐藏的模式。在 Java 中,你会很快学到信息隐藏的语言特性,并且会被鼓励使用诸如“特权的吝啬是伪装下的善意”这样的安全最大化原则,以便在声明事物为非公开时出错。在 JavaScript 中,可能会有一些未来保留的关键词,比如publicprivateprotected(Douglas Crockford 建议这些关键词可能是为了让 Java 程序员更容易理解 JavaScript 而牺牲了 JavaScript 更好的一面),但现在并没有同样明显的机制。对象的所有成员,无论是函数还是其他,都是公开的。JSON——另一种像野火一样传播开来的东西,但没有人因其简单而诅咒——没有提供任何标记任何东西为非公开的机制。

然而,在一些函数式语言的特性中有一种叫做闭包的技术,可以创建私有字段。这并不是一种简单存在于语言中的创建信息隐藏的技术,但它允许我们创建包含非公开信息的对象。为了移植前面的例子的功能,包括非公开状态,我们可以有如下所示的东西:

var factory_for_objects_with_private_member = function() {
  var counter = 0;
  return {
    'increment': function() {
      counter += 1;
    },
    'decrement': function() {
      counter -= 1;
    },
    'get_count': function() {
      return counter;
    }
  }
};

这里给出的例子建议了我们如何扩展它。一个更复杂的例子可能有更多的var函数存储字段和函数,并返回一个将公共接口暴露出来的字典。但我们不要简单地跳到那一步;在这条路上有一些有趣的事情。

在函数式语言中,函数可以包含其他函数。事实上,鉴于 JavaScript 的语法——其中函数是第一类实体,可以存储在变量中,作为参数传递等等——如果函数不能嵌套甚至两层深,那将是令人惊讶的。因此,以下语法是合法的:

var a = 1;
var inner = function() {
  var b = 2;
  return a + b;
}

但同样的东西可以合法地包装在一个函数中,如下所示:

var outer = function() {
  var a = 1;
  var inner = function() {
    var b = 2;
    return a + b;
  }
}

这是功能语言的基本特性,包括 JavaScript 的传统,内部函数可以访问外部函数的变量;因此abinner函数同样可用。

现在如果我们将outer函数改为返回inner函数会发生什么?然后我们有以下内容:

var outer = function() {
  var a = 1;
  var inner = function() {
    var b = 2;
    return a + b;
  }
  return inner;
}
var result = outer();

当函数执行完成时,它的变量已经超出了范围。JavaScript 现在具有函数范围的var变量,并且正在过程中获得块范围的let变量,但是在outer函数中以任何方式声明的变量已不再可用。然而,有趣的事情发生了;inner函数在outer函数结束后仍然存在,但以一种逻辑一致的方式。inner函数应该并且确实可以访问它的执行上下文。我们有了一个闭包。

这种现象可以用于信息隐藏,信息隐藏很重要。然而,可以争论的是这里最有趣的不是它可以包含非公共变量,潜在地包括函数,而是只要有东西可以访问它,执行上下文作为一个整体就会被保留下来。这留下了一个有趣的领域可以探索。一个 StackOverflow 成员曾评论说,“对象是穷人的闭包”,对象和闭包都有有趣的可能性,超出了关于如何使用它们的特性进行信息隐藏的 FAQ 条目。即使是《代码大全》,它可能会强烈支持信息隐藏,也从未说过,“尽可能使用信息隐藏,但不要使用其他东西。”

也许责怪功能语言纯粹主义者说,“JavaScript 必须等到它成为 20 年历史才能实现尾调用优化,而不是惩罚标准的函数式编程使用递归——就像美国法律下的一个新生儿成长为成年人一样。”然而,不管功能程序员对 JavaScript 可能感到不满的其他方面如何,JavaScript 从一开始就足够正确地实现了闭包,以至于保留执行上下文的闭包在 JavaScript 中一直是一个重要的特性。而 20 年后,它们仍然是大多数浏览器中的主要,可能是唯一的信息隐藏资源。

摘要

在本章中,我们涉及了 JavaScript 与功能编程的一些注解。我们主要关注了三个主题。自定义排序函数提供了一个简单而有用的窥视,我们如何将一个辅助函数传递给一个高阶函数,以获得比默认更有用的行为。Map、reduce 和 filter 是与数组相关的功能编程的三个主要工具。通过闭包和信息隐藏,我们看了一种在负责任的软件开发中提供一些核心兴趣的功能方式。

JavaScript 是一种具有功能根源和一些功能语言优势的多范式语言,尽管功能语言纯粹主义者将 JavaScript 与命令式多范式语言一起归类可能是不常见的。JavaScript 没有像一些功能语言那样对赋值或纯不可变数据结构进行永久绑定。

所有语言都有好坏不同的地方,但 JavaScript 将优秀的部分和糟糕的部分如此明显地结合在一起,以至于 Crockford 的《JavaScrpt: The Good Parts》和《The Better Parts》的基本方法在优秀的开发人员中没有受到严肃质疑(我想知道为什么还没有人将 Kernigan 和 Ritchie 的《C 程序设计语言》第二版销售为《C++: The Good Parts》)。认为默认将事物倾倒在全局对象上是开发 Web 应用的好主意,这种观点可能会引起争议,甚至令人讨厌。这也适用于 JavaScript 的功能方面。JavaScript 是第一种主流语言,允许使用匿名函数或 lambda,这在函数式编程中已经成为基本要素,大约自 LISP 出现 50 多年以来。即使现在连 Java 也加入了这一潮流,但它在主流语言中的存在是受 JavaScript 的影响。JavaScript 从一开始就有闭包。就一些糟糕的地方而言,JavaScript 似乎花了几十年的时间才应用尾调用优化,以及使用尾递归而不受惩罚,而不是使用 for 和while循环来构建迭代工作的函数式编程风格。

函数式编程是一个有趣的话题,你可以无限地探索(也就是说,函数式编程的有趣方面的列表是一个无限的列表,尽管在具体情况下,人们只能从列表的左边取出有限数量的项目)。在不试图解决 JavaScript 是否应该被视为函数式语言的问题的情况下,最好将 JavaScript 理解为与函数式编程根源相关,并且学习在函数式语言/范式中更好地编程应该是在 JavaScript 中更好编程的基础。JavaScript 可能会载入历史,不仅作为 Web 的语言,也许是程序员必须了解的最关键的语言,还是功能性语言的优点不再被视为(如 Scheme)“你永远不会使用的最好的语言”的桥梁语言。也许,功能性语言的优势会被视为严肃、主流的多范式语言构建的不可妥协的部分。

让我们继续看看函数式响应式编程。

第六章:函数式响应式编程-基础知识

我可能从这里有点尴尬地提到,我有数学硕士学位和许多数学奖项,但我发现与基本函数式编程相关的一些数学概念有点难以理解。一个人不会阻止在相关数学和计算机科学领域有扎实基础的人去攻克,例如,在维基百科关于函数式响应式编程中链接的基础性函数式响应式编程论文的完整数学严谨性。然而,这里的意图略有不同:从函数式响应式编程中学到对于没有或不记得那些开创性作品所依赖的数学水平的专业开发人员有用的东西。

StackOverflow 的评论反复问道,“你能不能以不假设计算数学博士的方式解释它?”这里的意图不是提供那些数学文章的全部内容,而是提供一个对于真正的专业软件开发人员有用的、实用的子集,他们不会梦见 Scheme 或 Haskell。

在本章中,我们将涵盖:

  • 计算机传统的记忆之旅

  • 函数式响应式编程

  • 如果你只学到一件事……

  • 了解有关函数式编程的知识

  • 前端 Web 开发的未来

让我们深入了解。这段充满传统的记忆之旅可能会相当漫长,但在任何意义上都不会枯燥无味。

计算机传统的记忆之旅

有一个非常侮辱性的检查表一直在流传,适用于宠物(或其他)编程语言。其中一个侮辱是,“程序员不应该需要理解范畴论来编写Hello, World!”这反映了部分湿后耳朵的初级程序员在他们提出最好的编程语言时不断犯的错误的烦恼。在这方面,它可能与愚蠢的事情清单一样,愚蠢的事情清单是从无数冒险电影中发生的愚蠢事情中学到的。作者从无数电影反派的错误中吸取教训,宣称“射击对我的敌人来说并不是太好”,“除非绝对必要,我不会包括自毁机制……”这些侮辱来自于一次又一次看到相同错误的挫败感。

还有其他一些显示编程智慧和智慧的观点。例如,宠物或玩具语言的数量级比成功的语言要多得多,成功可以是在学术计算机科学领域或商业信息技术领域。任何语言发展中被广泛认可的转折点是,当它在一个可以通过使用它来编写自己的编译器的水平上工作时。

这并不是一个不重要的观点;当 Java 首次宣布时,宣称 Java 编译器是用 Java 本身编写的,这意味着能够运行 Java 运行时环境的系统应该能够编译用 Java 编写的软件。在这方面,一个常见的侮辱某人对自己热情洋溢的宠物语言的标准问题是,“除了自己的编译器,它还用来写过其他东西吗?”计算机智慧和传统的这一特定节点已经融入了检查表:其中两个条目是,“用这种语言编写的最重要的程序是它自己的编译器”,然后,更具侮辱性的是,“用这种语言编写的最重要的程序甚至不是它自己的编译器”。

但是,“程序员不应该需要理解范畴论来编写Hello, World!”的哲学反对意见并不是为了听起来侮辱而凭空捏造的。这是由 1978 年 Kernigan 和 Ritchie 经典著作《C 程序设计语言》第一版开始的传统,在深入研究复杂性之前开发的第一个程序是一个最小的 C 程序,思想是“让我们在尝试行走之前先爬”,打印出Hello, World!

main()
{
  printf("hello, world.\n");
}

介绍新编程语言的人普遍遵循使用Hello, World!作为他们的第一个示例程序的传统。这是“程序员不需要理解范畴论就能编写Hello, World!”的一端。那么光谱的另一端是什么?

在学术数学世界中,无论是纯数学还是应用数学,数学作为成功的标志已经变得非常专业化(就像任何经历了足够工作的领域一样)。有评论说,能够理解数学会议上提出的 50 篇论文中的 13 篇以上的数学家是非常罕见的。数学已经变得足够专业化,以至于大多数数学博士,无论多么有能力,都无法理解大多数其他数学博士的工作。在这种情况下,希望能够理解所有数学就像希望能够说出所有人类语言一样:有点天真。数学博士项目的目的也许不是让你发展到能够理解数学学科的整个广度,而是深入理解某个狭窄领域,以至于在你的博士学位完成时,你比世界上任何其他人更深入地理解了这个高度专注的领域。

有两种例外,连接所有数学的学科,但从完全相反的方向。一方面是逻辑和数学基础,它研究了所有其他数学领域所基础的基石。现在有一些关于逻辑属于数学还是哲学的问题,人们也听说过有人被要求决定他们想成为逻辑学家还是数学家。但撇开这些问题,可以说逻辑通过挖掘其他数学领域所依赖的基石与所有数学领域相连接。

然后还有另一种选择:范畴论。一个芭比娃娃曾经说过,“数学很难”,但数学界很清楚这一点,不需要芭比的帮助。阿尔伯特·爱因斯坦说过,“不要担心你在数学上的困难。我可以向你保证,我的困难更大。”但数学分支范畴论尤其难以理解。

如果逻辑可以研究数学伟大建筑的基石,范畴论则着眼于已经建成的城市,并探索贯穿各种数学领域的建筑主题和相似之处。范畴论是一门学科,有点像比较文学学科,从业者被期望能够应对不只一种语言的比较文学。可以说范畴论是整个数学领域中最困难的地方。你需要做一些大多数数学博士从未学过的事情。

此外,也许是一种致敬,我的导师是一位范畴论家,他能够有效地指导一个关于点集拓扑这个鲜为人知的分支的论文,尽管他在点集拓扑方面并没有显示出特别的专长。因此,说一个使用你的语言的程序员需要理解范畴论才能编写Hello, world!是相当刻薄的侮辱。

那么这与函数式或函数式反应式编程有什么关系呢?很高兴你问!

在维基百科文章中链接的函数式响应式编程的资源中,Haskell(或者构建在其上的东西)是主导语言。还有一些其他的语言,比如 Scheme 的一个方言,但人们似乎总是回到 Haskell。有很多 Haskell 的资源;其中最受尊敬的之一是《学习 Haskell 为了更好的》tinyurl.com/reactjs-learn-haskell。它在第九章中神秘地出现了一个Hello, World!程序,而不是第一章。为什么是第九章?嗯,正如解释的那样,输入和输出是建立在单子之上的。但是真的需要这么多的解释才能理解单子吗?是的;单子是建立在应用函子的概念之上的,而应用函子是建立在函子的基本概念之上的,这让人想起在数学研究生阶段遇到的某个名字,但并没有真正理解。让我们去维基百科的函子页面看看。维基百科以清晰易读而闻名。维基百科页面中有一些内容。其中一点是,语言对于维基百科来说实在是太过神秘了。另一点是,函子实际上是从范畴论中获得的东西:

在数学中,函子是范畴之间的一种映射,适用于范畴论。函子可以被看作是范畴之间的同态。在小范畴的范畴中,函子可以被更普遍地看作是态射。

函子最初是在代数拓扑学中考虑的,在那里,代数对象(如基本群)与拓扑空间相关联,代数同态与连续映射相关联。如今,函子在现代数学中被广泛应用于各种范畴。因此,函子通常适用于数学中范畴论可以进行抽象的领域。

Hello, World!的高级先决条件!

如果你想挑战自己,可以阅读维基百科关于函子的文章。但如果你发现自己只是匆匆浏览,因为大部分内容都超出了你的理解范围,那么你不用担心:可能很多数学博士也会以同样的方式匆匆浏览,出于同样的原因。

在这一点上还有更多可以说的,但我将限制自己在评论本章意图之后再做一点补充。这导致了本章的核心困难。这是一本关于信息技术而不是计算机科学的文本,虽然可以向计算机科学家请教,但这本文本的撰写意图是从一个程序员的角度向另一个程序员撰写。在 Haskell 中,目标相当于基于看到,模仿的基础上说,“这是一个纯函数的例子;那是一个输入和输出单子的例子。尽量让你的程序的大部分工作都来自纯部分,并将输入和输出限制在尽可能小的隔离空间中。”

有一点可以用 Haskell 和 Python 进行对比,再次以 XKCD 为例——注意 Python 中给出的一切都是简单的第一个例子。第一次接触 Python 会让人感觉再次爱上编程。同样的情况也完全适用于 ReactJS,就像再次发现网络一样:

Hello, World!的高级先决条件!

Python 和 Haskell 在至少一个方面相似:它们都允许快速软件开发。Haskell 拥有与 Python 所期望的类似功能:一名本科生花了几个月的时间,在 Haskell 中实现了 Quake 3 引擎的大部分功能。Haskell 可能还有其他优势,比如其非常可靠的类型系统,而且一旦某些东西编译完成,它就已经有很大的可能性可以工作。然而,这里追求的问题是,“它能让程序员高效生产吗?”这个屏幕截图来自一个本科生项目中在几个月内实现的 Quake 3 级别。Haskell 有一些 Python 没有的东西:比如非常可靠的类型系统。然而,Haskell 和 Python 至少在这一点上是相似的:在熟练开发者的手中,它们都允许生产力和开发速度,这需要在看到之前才能相信。

Hello, World!的高级先决条件

但是,有经验的程序员可能会尝试 Python 并发现自己能够游刃有余,但如果他们无法处理数学,他们在使用 Haskell 时就不会有相同的体验。Haskell 为那些能够处理大量计算数学的人提供了快速开发的超能力。Python 为更广泛的程序员群体提供了这样的能力,无论他们是否具有丰富的数学背景。

这一章是一个尝试,无论对错,都是为了解释事情,以便信息技术工作者,而不仅仅是计算机科学专业人员,可以像有经验的程序员一样,用函数式响应式编程和 ReactJS 获得良好的 Python 体验,而不是导致许多开发人员继续发表令人沮丧的评论的有经验的程序员的糟糕的 Haskell 体验,这些评论在一段时间后变得非常悲伤,比如“如果我懂得更多数学,我可能会理解这个”。

我们写这本书的目的是为了使程序员在这个领域有用,而不仅仅是懂很多数学的计算机科学学生。但至少暗示了一种更简单的方法,以及有经验的程序员说“如果我懂得更多数学,我可能会理解这个”。直接从页面tinyurl.com/reactjs-learn-monads获取。现在,以下是学习单子的一些步骤:

  1. 获得计算机科学博士学位。

  2. 把它丢掉,因为你在这一部分不需要它!

(也许普通开发人员终究可以从(响应式)函数式编程中获益!)

区分函数式响应式编程的特点

作为函数式响应式编程的领军人物之一,也可以说是函数式响应式编程的鼻祖之一,Conal Elliott 回顾了术语“函数式响应式编程”,一个领军人物对于名称的反思可能会非常有趣。Elliott 对术语“函数式”表示了保留意见,他认为这个词现在意味着很多东西,因此意义不大,并对术语没有包含的一个词表示遗憾:时间。他提出了一个另类的名称,即指示性连续时间编程,即使我们在这里使用更标准的术语“函数式响应式编程”,这也是重要的。通过“指示性”,我们的意思是,正如我们之前讨论 ReactJS 时所讨论的,你只需指定需要完成的任务,而不是每一步如何完成它。连续时间不仅仅意味着应该这样称呼它,而且连续时间是如此重要,以至于它应该被纳入现在所谓的函数式响应式编程的名称中。

连续时间元素出现在这些来源中,对一些人来说可能会感到惊讶,因为计算机只能离散地测量时间,但这种区别是概念模型中的区别,而不是实现中观察到的特征。这种比较类似于函数式语言中存在的无限列表,人们可以从列表中取出多少或多少而不会用完预先计算的条目,或者更明显地是栅格图形(GIF、JPEG、PNG)与矢量图形(SVG、一些 PDF)之间的区别,栅格图形有一定数量的像素表示,而矢量图形可以根据经典广告执行人员的说法进行渲染,公司标志在信头上高一英寸时看起来和在公司总部的八英尺高处一样好看。

连续时间意味着时间的处理方式类似于 SVG 或其他矢量图形,而不是栅格 GIF/JPEG/PNG,后者存储在固定分辨率上,没有多余的像素。对于函数式响应式编程的建议之一是,连续时间事件和可能是连续值行为或事件流具有第一类实体的地位,作为定义特征的一部分(尽管有人可能指出,也许不是唯一的特征)是函数是第一类实体,可以作为参数传递,就像 JavaScript 和其他语言中的匿名函数一样。

ReactJS 与这个有什么关系可能并不立即明显;我看过十多个 ReactJS 视频,通常是来自 Facebook 开发人员。强调了指称语义,这是一个正式术语,用来描述只需要完成什么,而不是如何完成每一步。还有关于虚拟 DOM 的持续讨论,这相当于“如果你想了解更多,你可以学习,但你只需要告诉系统如何render(),然后相信系统会完成其余的工作。”但事实上,连续时间语义是内置在 ReactJS 的基本工作原理中的。开发人员的责任之一是编写一个render()方法,指定在调用时页面上应该显示什么(也许还要适当地调用render()render()不会自己运行)。

这并不具备连续时间的所有特性;一个教学视频暗示了一个系统,不仅可以实时工作,还可以允许类似 VCR 的“倒带”和“快进”功能来逐步通过时间,Pete Hunt 的一个 ReactJS 视频暗示了 Facebook 可能通过 ReactJS 技术接收一个错误报告,并能够逐个细节地重放发生错误之前的情况,而没有书面描述错误的情况,只是“粗话”。然而,最突出的用例是假定连续时间,并且开发人员有责任编写一个render()函数,可以在调用时正确地指定要渲染的内容,并且(顺便)适当地调用该函数。

如果你只学到一件事...

理查德·P·费曼的经典“费曼讲义”被认为是对技术主题进行清晰解释的典范,他以一个非常简单的问题开篇:如果科学的其他一切都被遗忘,只有一句话的信息幸存下来,那么理想情况下会是什么?费曼给出了一个简洁的答案,实际上表达了很多内容:

"如果在某场灾难中,所有的科学知识都被摧毁,只有一句话传给下一代生物,哪句话会包含最多信息?我相信那就是原子假设,即一切事物都是由原子组成的——小颗粒在永恒的运动中移动,当它们相距一点时相互吸引,但当它们被挤压到一起时则相互排斥。在这一句话中,你会看到,关于世界的大量信息,只要稍加想象和思考。"

这在费曼讲座中作为一个跳板,可以让我们对我们所知道的物理学说很多。

函数式响应式编程的最大学习要点可以用一句话概括,这是与函数式编程本身相关的一堂课,函数式响应式编程进一步完善了这一点:尽可能多地编写纯函数,数学风格的,尽可能少地编写或遵循配方

配方会说一些诸如“将烤箱预热至 350°F。在一个大碗中混合叶子、酥油和盐。用羊皮纸在两个大烤盘上铺一层。将叶子均匀分布在每个托盘上的一层中…”现在这并不是在挖苦家政和烹饪的人。(纯粹的功能性烹饪方法永远不会产生任何可食用的东西,如果你想完成任何事情,这是一个小缺点。)配方同样可以在许多许多 YouTube 视频中找到,详细说明如何更换,例如 2004 年福特 Escort 上的破损雨刷,它们也支持传统的黑客编写的 How-to,尽管它们在今天不像早期那样突出,这并不是因为黑客社区意识到使用 How-to 不是解决困难的适当方式,而是因为几乎所有事物的通用性都得到了足够的改善,以至于你不需要一个在烧录 CD 时提到月相的 How-to;How-to 很少可能是唯一的选择(这实际上是它们最好的用例)。

我对 Haskell 经历了哪些理论上的扭曲(即:需要经历)以最小程度地妥协其功能状态来包含输入和输出有些担忧。但同样,即使纯粹的函数式 JavaScript 可能存在与否,我们最好还是尽量增加我们的软件部分是纯函数式的,并最小化通过指定如何做事来完成工作的部分。这里的函数不应该像结构化编程中那样意味着“返回一个值的子例程”。一个函数不是在做某事并在完成时返回一些有趣的东西。它更多地具有数学上的意义,“接受零个或多个参数,并且不多不少地返回一个基于该值得到的值。”

计算机人员使用的基本数学中的纯函数的例子包括算术函数,如加法、减法、乘法、除法、指数、阶乘(例如,4 的阶乘是 432*1),斐波那契数,三角函数,如正弦和余弦,双曲函数,积分,导数,欧几里得除法来计算两个正整数之间的最大公约数,等等。毫无例外,这些函数接受零个或更多个(或者,对于这些情况,一个或更多个)输入,并从中计算出一些东西,而不做任何外部更改;它们没有更新数据库或输出东西到控制台。它们只是接受它们的输入,并确定性地计算出一个输出,不多不少。这就是纯函数的本质。

长词是“一个半英尺长的词”。其中一些词在各处流传,包括在关于 ReactJS 和函数式响应式编程的视频中,比如幂等和引用透明度。但是与纯函数相关的含义是简单而直接的。

幂等函数是指无论调用一次还是一百次,都会返回相同结果的函数。在数学中,例如加法和阶乘,总是给出相同的结果。RESTful 网络服务提供了一个较少数学的幂等性的例子:请求相同的 URL 意味着每次都会得到相同的 HTML 或其他数据。获取静态内容是幂等的;从 CDN 获取的库的版本应该无论谁、在哪里或何时请求,都会得到相同的下载。

缓存,比如使用 Steve Souder 的经典远期Expires头部来实现 Yslow,是一种非常有用的方法,特别是在下载之间存在幂等性的情况下。(如果下载是幂等的,无论是下载新副本还是从浏览器缓存中提供副本,文档都是相同的。)动态内容,无论是老式的 CGI 脚本还是动态的 Django 应用程序,都不是幂等的。如果一个页面上甚至在 HTML 注释中写着“此页面在某个时间下载”,那就不是幂等的。Web 最初是设计为幂等的;后来人们开始意识到动态内容可能非常有用,并开始研究如何克服 HTTP 的无状态、幂等设计。

引用透明度这八个音节的词意味着函数调用可以等效地替换为它返回的值。因为 4!等于 24,所以在你的代码中包含 4!和只包含 24 应该是等效的。如果你有一个值的余弦,使用cos()的结果的存储值或重新计算应该是等效的。破坏引用透明度的不纯行为是每次调用cos()都记录一个字符串,这是一个经典的副作用的例子。

提示

“副作用”这个术语是不幸的,可能是有意使用的措辞;在医学背景下,所有药物都会产生多种效果,其中一些是服药的目的,而另一些则被容忍为必要的副作用。在医学上,副作用是指药物的效果是被容忍的,但不是服药的目的。在程序中记录消息是一种副作用,有点像说服用止痛药并随后减轻身体疼痛是一种副作用:这正是服药的全部目的,而不是药物可能产生的其他效果,这样称呼它是一种副作用有点奇怪。

上面是一些基本数学函数的示例,也许这很容易,因为在某些数学领域,一切都是纯函数,可能是由纯函数构建的,而且环境排除了不纯函数或副作用。人们也可以举多项式作为由纯函数构建的纯函数的例子,如果你有能力使用它,这是一种非常好的方法,但如果习惯于用信息性假设来构建一切,这种方法会感到陌生和困惑。对于以命令为基础的程序员来说,命令式函数是短期内最容易的方法,但长期来看更难。对于以函数为基础的程序员来说,函数式编程在短期和长期内都很容易。但是,要在实际信息技术中看到这些问题的例子,我们不需要看得比 ReactJS 更远。

Facebook 的长期痛苦学习基本上导致了一个认识,即摆脱困境的方法是通过幂等性和引用透明度,这就是 ReactJS 的编写目的。

学会你能学到的东西!

正统精神传统中的一位智者将许多事情归纳为 55 条格言(tinyurl.com/reactjs-55-maxims),其中第二条是,“祈祷应该是你能做到的,而不是你认为你必须做到的”,这些话对于大部分编程也同样适用。这里有一个建议,但不会让非数学家感到畏惧。尽可能按照你能做到的方式去遵循这个建议,而不是你认为你必须这样做。尽可能多地学习函数式编程。尽可能纯粹地使用 JavaScript 进行函数式编程。

我现在已经理解了函子,而在我读数学研究生时却未能做到。从理论角度来看,我还没有完全理解应用函子和单子的概念,但尽可能地编写纯函数,并尽量少地使用输入和输出单子的想法似乎是可行的,这比追溯单子的概念起源要容易得多。这属于尽可能地使用函数式编程,而不是你认为必须使用的范畴。

函数式响应式编程的维基百科文章链接到了该领域的九部重要作品,如果你想解决一个良好的数学难题,所有这些作品都值得一搏。数学符号可能像维基百科上的函子文章一样密集。

但是,如果我们看看编程语言,这里有一个线索。文献中提到了几种有趣的可能性,都是函数式的:一种 Scheme 方言,DDD 和 Elm(它是一种独立的语言,编译成自己的 JavaScript / HTML / CSS,与 DDD 相比)。但是,函数式响应式编程作者最感兴趣的似乎远远是 Haskell。这给了我们一个免费的线索,至少在其起源中,Haskell 是几乎所有关于函数式响应式编程的重要论文的重心。任何语言,包括 Haskell,都有缺陷,但简单地忽略函数式响应式编程的重要作品倾向于 Haskell 是愚蠢的。

函数式响应式编程是建立在函数式编程的基础上的响应式编程。在使用 ReactJS 开发 JavaScript 时,一些方面已经为我们处理了。我们只需要声明性地指定 UI 在渲染时应该是什么样子,ReactJS 将处理所有必要的编译,因此声明性的render()方法将被转换为 DOM 上的优化的命令。但至少乍一看,如果你想理解函数式响应式编程,学习一种与 Haskell 紧密相关的技术是有意义的,只有在你穿上 Haskell 的鞋走了一英里之后,然后知道它们是否会让你不舒服,才写下你对 Haskell 的“独立宣言”。

《Learn You a Haskell for Great Good》受到了批评,但这本书被故意选为教授一门一流的函数式语言的优秀教材。指出 Haskell 的教材在允许读者看到传统的“Hello, world!”程序之前,涵盖了八章的理论和一些范畴论概念,这比挑剔一个一般的介绍更有说服力,并且会引起一个明显的反应,但是有更好的例子没有这个问题。一个更专注于实际应用于现实世界信息技术需求的伴随教材是《Real World Haskell》(book.realworldhaskell.org/read/)。这些并不是唯一的教材,但至少提供了一个很好的搭配和一个起点,并经常一起推荐。

更重要的是,不要试图匆匆翻阅这两本书,并期望经过一天甚至一个月的学习,就能比你多年使用的任何喜爱的语言更容易地在 Haskell 中完成工作。相反,玩耍,尝试这些东西。把 Glasgow Haskell Compiler 当作你圣诞节收到的一套漂亮的虚拟乐高。《Learn You a Haskell for Great Good》从未深入探讨如何编写 Web 服务器,这正是该书的优势所在。它建立了只有对你有利的核心优势,并应该让你更有能力欣赏和利用 JavaScript 中函数式编程的机会。G.K.切斯特顿说:

理解一切都是一种负担。诗人只渴望升华和扩张,一个可以展现自己的世界。诗人只希望把头伸进天堂。是逻辑学家试图把天堂放进他的脑袋。而他的脑袋却会崩裂。试着把你的头伸进天堂,而不是立刻把天堂放进你的头脑。如果你现在是一个熟练的程序员,也许是在命令范式中,那么很有可能在学校时,当你探索事物时,你试图用编程把头伸进天堂。你写游戏;你玩耍,获得了以后在专业工作中会用到的基础。如果你想学习 Haskell,不要死记硬背。重新变成一个小孩,玩耍。阅读《Learn You a Haskell for Great Good》,它故意避免了如何在最后期限前快速完成某事,直到你真正掌握了基础才去阅读《Real World Haskell》,请不要把《Real World Haskell》当作“抄近路”的理由,试图在最后期限内发布商业风格的功能。

在他关于《更好的部分》的南美演讲中,道格拉斯·克罗克福德在描述良好的 JavaScript 时,给了越来越强的函数式编程重点。我看过的早期克罗克福德的视频,当时只有《好部分》,甚至没有《更好的部分》的迹象,似乎将 JavaScript 的更好部分与其函数式一面联系在一起。但《更好的部分》更明确地表示,JavaScript 和谐的改进之一是你可以应用尾递归,并使用函数式的流程控制风格,使一些流程控制,如循环几乎或完全不必要。

即使不考虑函数式响应式编程,更好的 JavaScript 似乎越来越意味着函数式 JavaScript。这是一件非常好的事情。正如前面提到的,Scheme 被称为“你永远不会使用的最好的语言”,计算机科学家们一直选择基于计算机科学使用价值的一组通常的函数式语言,这是一个必须离开的小天堂,才能进入专业编程。

JavaScript 改变了这一点,不仅仅是通过使匿名函数成为主流。特别是在与 ReactJS 一起使用时,JavaScript 为主流软件开发提供了享受函数式编程优点的最大机会。只要你明白自己在做什么,你就能越多地以函数式范式编写 JavaScript。

函数式编程,无论是响应式还是其他方式,如果你在学校接触了更数学化的函数式编程,可能会更容易理解。但是可以教会程序员如何在 Haskell 中写出“Hello, world!”,同时将范畴论中最难理解的数学知识放在视线之外,隐藏在引擎盖下。

函数式编程的计算数学基础应该像高级语言中的机器或汇编语言一样:存在于引擎盖下,使语言功能成为可能,但在最小程度上是不透明的抽象,只是工作,无论一个人是否是一个能够在引擎盖下进行调整的机械师。

在某种意义上,对于已经离开学校一段时间的资深程序员来说,需要的是学习函数式编程的“好部分”。在这种情况下,好部分可能因程序员的函数式编程舒适水平而异。标准是“做你能做的,而不是你认为你必须做的”。理解声明式/指示性编程与命令式编程之间的差异,可能有些困难,但并不是太难。尽可能编写纯函数,并隔离必须具有副作用的代码,即使你试图避免它们,这是一种思维转变,但并不是太棘手。

例如,在 Haskell 中学习函子实际上比范畴论要容易一些,即使维基百科页面没有反映出这一点。大多数程序员不应该花太长时间编写一个主要是纯函数,输入和输出由单子的最小隔离处理的第一个 Haskell 程序。但是使用单子等功能要比理解使用纯函数并逐步构建单子的扭曲步骤容易得多。

而且值得重申:如果函数式(反应式)编程适合主流使用,那么从函数到单子的重度数学理论不应该像 C 程序员被迫使用汇编语言或软件生成的机器指令一样被强加给普通的专业开发人员。有人说,“C 是一种将汇编语言的强大与使用汇编语言的便利结合在一起的语言”,但 C 从来没有强迫大多数程序员微观管理编译器如何渲染 C 源代码。

现在,许多主流语言,特别是多范式语言,已经融入了一些函数式编程的优势。尽管如此,有人可能会建议,JavaScript 是所有主流语言中直接提供最佳函数式编程优势的语言。不一定是计算机科学家喜欢的 Haskell 或 Lisp/Scheme 等语言中最好的函数式编程;很难找到一个主流编程工作,管理层会允许使用 Haskell 或 Scheme 的解决方案。但在主流语言中,JavaScript 仍然是最有吸引力的。计算机科学家长期以来一直喜欢函数式编程,至少有一位在学校学习数学的程序员评论说,“函数式编程是我见过的第一个有意义的编程范式。”对于几乎所有计算机科学家都喜欢的卓越性,JavaScript 不仅仅是浏览器执行的语言,尽管这很重要。它还为在雇主的招聘要求中经常遇到的语言提供了最佳的函数式编程机会。

JavaScript 作为新的裸金属

Douglas Crockford 在前面提到的《更好的部分》中试图表明程序员和其他人一样情感用事。他以一种对于库恩学者来说并不奇怪的方式支持了这一观点:软件工程的基本改进是通过坚持早期方法的程序员的消退而取得的。他举了六个或者更多的“需要一代人”的例子:“需要一代人”才意识到高级语言是一个好主意,或者所有编程语言语句中的 F 炸弹,goto 语句,不是一个好主意。尽管 Crockford 举了几个例子,但他的努力似乎并不打算包括所有重要的例子:尽管我并不完全确定日期,但似乎在 20 世纪 60 年代,Smalltalk 意识到引用比指针更好(指针被称为“数据结构的 goto”),到 20 世纪 90 年代,像 Java 这样的主流语言用引用取代了指针,大约需要一代人的时间。

Crockford 对程序员以自我表达的自由来进行裸金属编程或者经常使用 goto 语句来处理流程控制做了一些评论。但说到底,包括匿名函数在主流语言中使用需要两代人的时间,JavaScript 是第一个,软件开发的改进并不是因为现有的程序员接受了更好的方法而生根。

它们生根是因为新程序员接受了更好的方法,而大多数年长、不信服的程序员则逐渐消失。(即使他们可能学会了。但在某种意义上,通过接受新变化来避免过时是一种选择。只是很多人会说,“20 岁时对我来说够好,40 岁时也够好了。”并不是每个人都被铁定的决定所控制:只是在程序员中存在一个“默认设置”,随着时间推移而变得不再合适。)

JavaScript 是网络上的通用语言,即使你不喜欢它,因为它不是你最喜欢的语言(确实,为什么它会像 Perl、Python、Java 或 C++一样呢?),它已经存在并且可能是最重要的语言,而且将在相当长的一段时间内保持这种地位。但在这种情况下,“需要一代人”的可能是意识到在非 JavaScript 语言中进行网络编程是一个好主意。

Alan Perlis 说过,“当一个编程语言的程序需要关注无关紧要的事情时,它就是低级的”,如果要在 JavaScript 中进行良好的编程,就需要避开许多语言的陷阱,而这些原因对于一般检查来说并不明显,JavaScript 需要关注无关紧要的事情:JavaScript 是低级的。

在新的网络开发中,ReactJS 视频中的一个令人鼓舞的迹象不仅是使用了另一种非 JavaScript 语言或语法糖 CoffeeScript,而且它的引入是平稳和随意的,完全没有道歉、辩护或解释。他们使用 CoffeeScript 的事实本身就很重要,而且他们这样做时没有任何防御性的痕迹更加重要。现在 CoffeeScript 可能不是所有可以或应该被编译成 JavaScript 的语言中的全部或最终选择。但看到除了 JavaScript“裸金属”之外的东西是令人鼓舞的。

这并不意味着“裸金属”编程没有用武之地。业余游戏开发者或来自大型公司的程序员,试图从裸金属中挤出最后一丝性能,无论是为独立应用程序和游戏,都会合理地希望挤出用户计算机的最后一丝性能,无论是在应用程序编程的“裸金属”上还是在网络上的 JavaScript 上。但就像通常情况下不会用 C 或汇编语言编写 Web 应用程序一样(即使在 CGI 脚本是交付动态内容的主要手段时也不会这样),对于大多数 Web 编程的用途来说,一部好的智能手机(稍微老一点的 iPhone 5 大约比网络刚刚出现时的顶级计算机快 100 倍)真的足够快,可以运行通过编译其他语言生成的 JavaScript 代码。而且,当人们了解 JavaScript 是如何开发的时,它就更加令人印象深刻。这是一种在 10 天内设计的计算机语言,通常人们不仅会对此表示印象深刻,还会说:“伙计,放松点!如果你继续滥用兴奋剂,你会害死自己!”

使用高级语言是可取的基本原因是程序员们学会忽略这一点,以便在 JavaScript 中完成任何工作。道格拉斯·克罗克福德的《精粹》,以及 JavaScript 既有宝藏又有地雷的想法,以及良好地避开地雷的一部分,已经深深扎根,以至于对于大多数阅读本书的程序员来说,这本书的简要摘要可能是完全多余的。

这本身就是一个严肃的理由,如果有选择的话,要考虑替代 JavaScript“裸金属”编程。事实上,对于前端 Web 开发,有许多替代 JavaScript“裸金属”的选择。tinyurl.com/reactjs-compiled-javascript列出了许多其他语言,包括旨在在某些方面提供增强 JavaScript 的家族和朋友,以及将其他语言编译为 JavaScript 的编译器,包括(通常)Basic、C/C++、C#/F#/.NET、Erlang、Go、Haskell、Java/JVM、Lisp、OCAML、Pascal、PHP、Python、Ruby、Scheme、Smalltalk 和 SQL 的多个选项。显然,并非每个编译器和实现都特别好,但就像其他任何严肃的计算机语言一样,JavaScript 是图灵完备的,不仅在理论上可以将其他完整的语言编译为 JavaScript 以及“裸金属”,而且在实践中也是可能的,并且是有充分意义的。JavaScript 可能会成为最重要的编译目标,甚至超越 x86_64 机器代码。或者也可能不会,但 JavaScript 的可取性和能力意味着编写编译为 JavaScript 的语言的现象——意味着大多数其他语言——在可预见的未来可能会不断增长。

总结

这一章可能是对早期尝试的一种尝试,而早期尝试通常不会成功。网上有许多重要文件,但它们假设你不仅可以编程,还可以处理大多数专业开发人员无法处理的特定类型的数学,也许甚至从未达到过熟练程度。这里的目标不是提供另一个高度数学化的解释,而是产生一份对大多数前端开发人员有用的文件,也许在一个不那么高尚的层面上,自然会考虑命令式解决方案。这里的目标是朝着更加功能化和不那么命令式的编程方向发展,但也要产生一份适合专业程序员实际具有的数学技能水平的文件,而不是某个权威可能希望他们具有的数学技能水平。因此,先决条件并不是假设程序员在被允许编写“Hello, world!”之前必须理解范畴论本身。

在本章中,我们回顾了计算机传说的记忆。这次回顾包括了对严厉的计算机清单、简单的“Hello, world!”程序和范畴论的探讨,以及函数式响应式编程首选语言 Haskell 可能会要求你在写“Hello, world!”时使用范畴论。这是一个重大问题。

我们还研究了函数式响应式编程的特点,包括时间的处理方式。本章还涵盖了对问题“如果你只能从函数式响应式编程中学到一件事,最好学什么?”的认真回答。

本章还涵盖了学习纯函数式开发的内容,而不是你认为必须学习的内容。试图学习太多函数式编程很容易让自己陷入瘫痪,而且老练的命令式程序员学习函数式编程(需要改变看待世界的方式)并不是件容易的事。这是一种尝试,提供了一种理智的方法来从函数式编程中获益,而不至于完全迷失在与函数式编程的无尽挣扎中。

我们还讨论了 Web 开发的未来,JavaScript 被视为新的“裸金属”。

在我们的下一章中,我们将探讨支持函数式响应式编程的工具。让我们开始吧!

第七章:不要重复造轮子-函数式响应式编程工具

在本章中,我们将介绍一些用于在“裸金属”JavaScript 之上构建的许多优秀工具中的一些,正如上一章中简要讨论的那样。JavaScript 不仅因为其作为核心语言的特性而有趣;浏览器 JavaScript 是一个生态系统,或者说可能是多个生态系统的家园。关于函数式响应式编程的工具,总体的提供代表了一个良好、健康和庞大的集市,与之相比,仅仅使用 JavaScript 进行所有 Web 开发看起来更像是一座大教堂。我们将从这个集市中取一小部分,理解到本章并不打算涵盖所有好的、有趣的或值得的东西。在集市中做到这一点是非常困难的!

我们将介绍的工具包括以下内容:

  • ClojureScript 和 Om

  • Bacon.js

  • Brython

  • Immutable.js

  • Jest

  • Fluxxor

我们将在这样一个章节中包含或不包含的一组工具,涉及到需要划定界限和做出判断。对于更全面的处理感兴趣的读者可以查看tinyurl.com/reactjs-complementary-tools上的链接汇编,并深入研究他们特定关注的工具。那里有很多东西,几乎可以满足任何目的的很多宝贝。

ClojureScript

ClojureScript,也许是 Clojure 总体上,代表了软件和 Web 开发的一个重要分水岭。ClojureScript 通过示例证明,除了 JavaScript 之外,还可以在其他语言中拥有坚实的基础和开发环境,而这种开创性的语言是一种 Lisp 方言。(这或许很合适,因为它是两种最常用的最古老的编程语言之一。Lisp 在诞生时就很好,今天仍然是一种很好的语言。)此外,与 JavaScript 相比,Lisp 可能具有很大的优势,并且由于一些相同的原因而存在。JavaScript 是 Web 浏览器的语言,而 Lisp 是 Emacs 的语言。此外,Lisp 提供了一种原始的 JavaScript;在可以用 JavaScript 编程的 Web 浏览器出现之前,可以用 Lisp 编程的 Emacs 就已经存在了,而且任何人说 Lisp 比 JavaScript 更好的话几乎不会受到质疑。

有充分的理由表明,Lisp 而不是 Emacs 默认的键绑定,是导致在互联网上流传的“经典学习曲线”漫画中的经典 Emacs 学习曲线的原因:

ClojureScript

正如在前面的章节中建议的那样,每个人直接在 JavaScript 中编程的统一性可能会让位于美丽的多样性,或者一块拼布。在这个美丽的拼布中,JavaScript 可能仍然是卓越的,但 JavaScript 的卓越地位可能成为新的“裸金属”。我们可能会有一系列用于前端开发的高级语言和工具。再次引用 Alan Perlis 的话,“当一个语言需要关注无关紧要的事情时,它就是低级的。”基于这些理由,JavaScript 是低级的。

这些工具中的一些可能在好的部分方面比坏的部分更好。它们可能适用于前端工作,最终仍然会在 JavaScript 中执行。但它们也可能开启前端 Web 开发,其中新的开发人员不再被告知,“这是我们将使用的语言,这是语言的一些大部分,你应该尽量避免,因为它们从根本上是有毒的。”ECMAScript(JavaScript 的正式名称,与 Emacs 没有特别的联系)的新版本可能提供更好的功能集合,但在高级语言中工作仍然是可取的,因为它们提供了更好的生产工作和结果的平台。

ClojureScript 毫不犹豫地表示,可以在浏览器上运行一个良好的高级语言,这不仅对 Lisp 黑客是个好消息。这对每个人都是好消息。它展示了在其他高级语言中进行 Web 开发的可能性,并且可能会有一个更好的 Web 开发环境,减少了沥青坑的可能性。

ClojureScript 既可以用于客户端工作,也可以在 Node.js 上用于服务器端。Hello, World! 如下所示:

(ns nodehello
  (:require [cljs.nodejs :as nodejs]))

(defn -main [& args]
  (println (apply str (map [\space "world" "hello"] [2 0 1]))))

(nodejs/enable-util-print!)
(set! *main-cli-fn* -main)

(comment
; Compile this using a command line like:

CLOJURESCRIPT_HOME=".../clojurescript/" \
  bin/cljsc samples/nodehello.cljs {:target :nodejs} \
  > out/nodehello.js

; Then run using:
nodejs out/nodehello.js

)

Om

Om 是一个包装器,使 ReactJS 可用于 ClojureScript。除了 ClojureScript 通常很快之外,Om 的某个部分实际上比 JavaScript 快大约两倍。这种差异与识别变化有关,以便在 ReactJS 执行 DOM 更新时进行最佳和适当的更新。原因是 ReactJS 在其差异算法中(通过处理可变的 JavaScript 数据结构)必须执行深度比较,以查看(纯 JavaScript)合成虚拟 DOM 中的内容是否有变化。

与直接 DOM 操作相比,这仍然非常快,以至于对大多数 ReactJS 用户来说并不是瓶颈。但在 Om 中更快。原因是 ClojureScript 像一种良好的函数式编程语言一样具有不可变数据。你可以很容易地获得某物的突变副本,但你不能篡改原始副本或使访问原始副本的任何人受到影响。这意味着 Om 只需比较顶层引用而不深入数据结构的深度就足够了。这足以使 Om 比原始的 JavaScript 使用 ReactJS 更快。在 Om 中,Hello, World! 是这样写的:

(ns example
  (:require [om.core :as om]
            [om.dom :as dom]))

(defn widget [data owner]
  (reify
    om/IRender
    (render [this]
      (dom/h1 nil (:text data)))))

(om/root widget {:text "Hello world!"}
  {:target (. js/document (getElementById "my-app"))})

Bacon.js

请注意,仅讨论 ReactJS 和 Bacon.js 并不足以构成一个详尽的列表。提到另一个替代套件,微软已经尝试创建了 RxJS、RxCpp [Rx for C++]、Rx.NET 和 Rx*,适用于各种 JavaScript 框架和库,并且他们至少尝试为多种语言和多种 JavaScript 框架和库的优化版本创建了一个多语言友好的组合。实际上有很多可用的提供某种形式的函数式响应式编程。虽然大多数(在撰写本书时)Web 上的函数式响应式编程和 ReactJS 资源都是宝贵的,但也有一些不是。

安德烈·斯塔尔兹写道:

“所以你对学习这个叫做响应式编程的新东西感到好奇,特别是它的变体,包括 Rx、Bacon.js、RAC 等。”

学习它很难,缺乏好的材料使它更加困难。当我开始时,我试图寻找教程。我只找到了少数实用指南,但它们只是皮毛,从未解决围绕它构建整个架构的挑战。当你试图理解某个函数时,库文档通常没有帮助。我是说,老实说,看看这个:

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])

通过将可观察序列的每个元素投影到一个新的可观察序列中,该新序列将元素的索引合并,然后将可观察序列转换为仅从最近的可观察序列产生值的可观察序列。

我现在明白这句引语的意思,但那是因为我从其他沟通更好的资源中学到了。你正在阅读的这本书的目的之一是让好的文档变得更容易理解一些。

在开源社区中有一个著名的问题:你会买一个发动机盖被焊死的汽车吗?ReactJS 可以被描述为大多数人可以在不打开发动机盖的情况下驾驶的汽车。这并不是说 ReactJS 是闭源的,或者 Facebook 显示出任何使其更难阅读源代码的迹象。但举一个显著的例子,指示性连续时间语义是 Conal Elliott 对现在称为函数式反应式编程的东西的更好名称的第二次思考的一部分。无论一个人是否同意他对更好和更具描述性名称的建议,这位领军人物的第二次思考可能非常有洞察力和启发性。而且对于 ReactJS,如果它工作正常,一个新手程序员可以得到与 Calvin 的父亲(一位专利律师!)在 Calvin and Hobbes 中给出的相同解释,当 Calvin 问一个灯或者吸尘器是如何工作的时候——这是魔术!看着一个新手的问题,“连续时间是如何处理的?”回答是这是魔术!“你怎么能够丢弃和重新创建 DOM 每一次?”——这是魔术!;“但是 ReactJS 如何在非 JIT iPhone 上实现惊人的 60fps?”——这是魔术!

函数式反应式编程描述了需要完成的某些任务,比如适当处理事件流,但 ReactJS 的文档似乎没有解释如何处理这些任务,因为这个责任被转移到了 ReactJS;这是魔术!

Bacon.js 不仅没有焊死发动机盖,而且还期望你在发动机盖下进行调整。Bacon.js 似乎更接近基本函数式反应式编程的根源。一些打算在 ReactJS 中工作的程序员可能会发现用 Bacon.js“举重”一点并用 Bacon.js 加强自己是有利可图的。函数式反应式编程的一个重要领域是处理事件流的发射,就 ReactJS 而言,这是魔术!

在 Bacon.js 中,事实上并不是魔术,所有这些都是在你没有动手的情况下完成的;这是程序员需要解决的问题,并且他们有很好的工具来做到这一点。基于这些理由,使用 ReactJS 可能有助于为开发人员打下坚实的反应式编程基础。如果 ReactJS 的卖点是它是一个优化工具,可以在利用函数式反应式编程的优势的同时允许良好的用户界面工作,那么 Bacon.js 的卖点是它是一个在 JavaScript 中优化的工具,可以在理论和实践中(学习和)执行扎实的函数式反应式编程。

ReactJS 和 Bacon.js 之间的区别似乎不是挖掘出一个框架比另一个更好的问题。相反,这更多地是关于审视你想要做和实现的事情,认识到 ReactJS 和 Bacon.js(除了是值得竞争对手之外)在它们真正擅长的不同领域,并决定你的工作更像是 ReactJS 的甜蜜点还是 Bacon.js 的甜蜜点。此外,关于甜蜜点的话题,Bacon.js(不像 ReactJS)有一个让你垂涎欲滴的名字,而~函数操作符在参考文献中被称为“bacon”。

Brython - 一个 Python 浏览器实现

Brython (brython.info)是一个浏览器和 Python 实现,是另一个用 Python 编程浏览器的替代方案的例子,虽然将 Brython 仅仅称为实验性的有点不公平,但也不一定适合称其为成熟的——至少不像 ClojureScript 具有一定的成熟度。ClojureScript 的发展足够好,可以基本上替代前端开发人员真正希望使用 Lisp 而不是 JavaScript 的"裸金属"JavaScript。换句话说,除非我们谈论一些性能关键的问题或可能的特殊情况,否则 ClojureScript 专家不会回答"我在 ClojureScript 中怎么做这个?"这样的问题,而是会说"对于这种问题直接使用 JavaScript。" Brython 被包含在这里并不是因为 Python 是唯一的非 JavaScript 语言,可以用于前端开发,而是作为一个例证,即 ClojureScript 中的 Lisp 并不是在前端 Web 开发方面的基本例外,而可能是许多例外中的第一个。

Brython 旨在征服世界。它的主页大胆宣布:"Brython 旨在取代 JavaScript 成为 Web 的脚本语言",也许永远无法达到这个相当天真的目标。Brython 加载时间长,加载后运行速度慢。也许最好使用其中一个 Python 到 JavaScript 编译器(更接近 ClojureScript),但 Brython 确实提供了相当多 Python 的优点,也许有一天会被视为重要。然而,我认为试图成为下一个 JavaScript 并取代其他渲染 JavaScript 的转译器是愚蠢的。

在 Brython 中,征服世界的目标也导致了一个盲点:未能看到与其他语言编写的工具进行互操作的重要性。但好消息是,Brython 或其他 Python 到 JavaScript 的方法可能是重要的,而无需成为"统治所有语言的一种语言"。Python 并不是唯一可用的后端语言,但它是一个很好的选择,并且有充分的理由让 Python 的良好实现成为前端 Web 开发中可以有利用的多种语言拼贴拼图中的有价值的一部分。

此外,使用 ReactJS 编写至少一个Hello,World!程序在 Brython 中也很容易实现。在将 Brython 和 ReactJS 放在同一页后,运行Hello, World!程序,首先是 JavaScript(不是 JSX)被注释掉,然后是 Python 代码通过 Brython 在浏览器中调用 React:

<!DOCTYPE html>
<html>
  <head>
    <title>Hello, Brython!</title>
    <script src="img/brython.js"></script>
    <script src="img/react.js"></script>
  </head>
  <body onload="brython()">
    <p>Hello, static world!</p>
    <div id="dynamic"></div>
    <!--
      <script type="text/javascript">
        React.render(
          React.createElement('p', null,
          'Hello, JavaScript world!'),
          document.getElementById('dynamic')
        );
      </script>
      -->
    <script type="text/python3">
      from browser import document, window

      window.React.render(window.React.createElement(
        'p', None, 'Hello, Python world!'),
        document['dynamic']);

    </script>
  </body>
</html>

这里显示了以下内容:

Brython - 一个 Python 浏览器实现

请注意,整个第一个脚本标签和内容,而不仅仅是其中的 JavaScript,都在 HTML 注释中。这意味着第一个(JavaScript)脚本在这里仅用于清晰显示,并不活跃,第二个(Python)脚本才是运行并显示其消息的脚本。

第二个脚本很有趣;包含的 Python 代码(除了消息之外)与被注释掉的 JavaScript 文本相当,并且执行相同的操作。这是一个相当了不起的成就,特别是当 Brython 成功地实现了 Python 3.x 分支中的大多数功能时。即使 Brython 被用于一个项目并被认为不是正确的解决方案,它仍然是一个成就。

在某种意义上,Brython 在这里被提出作为一个可能性的例子,而不是任何意义上值得关注的唯一成员。重点不是特别是 Python 可以用于前端开发;而是 ClojureScript Lisp 可能不是除 JavaScript 之外唯一可用于前端开发的其他语言。

Immutable.js - 永久保护免受更改

Immutable.js 的主页位于 facebook.github.io/immutable-js,标语是 Immutable collections for JavaScript,最初是为持久性而命名的。然后它经历了一个更快地注册不可变的名称更改。Immutable.js 填补了 JavaScript 作为函数语言的空白,并为集合提供了更多功能友好的数据结构(这是它创建的目的)。

它为不可变的集合提供了数据结构支持。它们优雅地支持创建修改后的副本,但始终是副本发生了变化,而不是原始数据。尽管这更多是一个小点,但它大大减少了“防御性复制”和相关解决方法的需求,以便在有多个程序员的地方不使用不可变数据。原始代码可能会使用您想要的数据结构的不同和修改后的副本,但您保留为参考的副本保证完全不受影响。该库旨在支持其他便利功能,比如轻松地转换为和从基本的 JavaScript 数据结构。

然而,Immutable.js 的数据结构不仅是不可变的;在某些方面它们也是懒惰的,文档清楚地标记了应用程序的哪些方面是急切的。 (作为提醒,懒惰的数据结构在需要时以按需打印的方式处理,而急切的操作是一次性和前置的)。此外,Immutable.js 设施中还包含了某些函数习语。例如,它提供了一个 .take(n) 方法。它以经典的函数方式返回列表的前 n 个项目。其他函数标准,如 map()filter()reduce(),也是可用的。总的来说,运行时复杂度和计算机科学家合理要求的一样好。

Immutable.js 提供了几种数据类型;其中包括以下内容(本表和下一个表中的描述部分基于官方文档):

Immutable.js 类 描述
Collection 这是 Immutable.js 数据结构的抽象基类。不能直接实例化。
IndexedCollection 代表特定顺序中的索引值的集合。
IndexedIterable 这是一个可迭代对象,具有支持一些类似数组接口功能的索引数字键,比如 indexOf()(可迭代对象是可以像列表一样迭代的东西,但在内部可能是也可能不是列表)。
IndexedSeq 支持有序索引值列表的 Seq
Iterable 一组(键和索引)值,可以进行迭代。这个类是所有集合的基类。
KeyedCollection 代表键值对的集合。
KeyedIterable 一个与每个可迭代对象相关联的离散键的可迭代对象。
KeyedSeq 代表键值对的序列。
List 一个有序的集合,有点像(密集的)JavaScript 数组。
Map 一个键值对的可迭代对象。
OrderedMap 一个地图,除了执行地图的所有操作之外,还保证迭代会按照设置的顺序产生键。
OrderedSet 一个集合,除了保证迭代会按照设置的顺序产生值之外,还可以执行集合的所有操作。
Record 一个产生具体记录的类。在概念上,这与其他记录不同。其他元素在概念上是“杂物”的集合,可能是具有类似结构的对象。Record 类更接近于学校里遇到的记录,其中一个记录类似于数据库表中的一行,而结果集或表更像容器对象。
Seq 一个值的序列,可能有可能没有由具体数据结构支持。
Set 一组唯一的值。
SetCollection 一组没有键或索引的值。
SetIterable 代表没有键或索引的值的可迭代对象。
SetSeq 代表一组值的序列。
Stack 一个标准的堆栈,带有push()pop()。语义总是指向第一个元素,不像 JavaScript 数组。

注意

Record与其他元素略有不同;它类似于满足某些条件的 JavaScript 对象。其他元素是相关的容器类,提供对一些对象集合的功能访问,并且通常具有类似的方法列表。

列表的方法,以一个例子为例,包括以下内容:

Immutable.List 方法 描述
asImmutable 一个函数,接受一个(可变的)JavaScript 集合,并渲染一个 Immutable.js 集合。
asMutable 这是对“不是最佳”编程的让步。基于 Immutable.js 集合处理变化的正确方法是使用 Mutations。即使asMutable可用,也应该只在函数内部使用,永远不要公开或返回。
butLast 这会产生一个类似的新列表,但它缺少最后一个条目。
concat 连接(即追加)两个相同类型的可迭代对象。
contains 如果值存在于此列表中,则为真。
count 返回此列表的大小。
countBy 使用分组函数对列表的内容进行分组,然后按分组器分区发出键的计数。
delete 创建一个没有此键的新列表。
deleteIn 从键路径中删除一个键,这允许从外部集合到内部集合的遍历,就像文件系统路径允许从外部目录到内部目录的遍历一样。
entries 作为keyvalue元组的列表迭代。
entrySeq 创建一个新的键值元组的IndexedSeq
equals 这是完全相等的比较。
every 如果断言对此列表中的所有条目都为真,则为真。
filter 返回提供的断言为真的列表元素。
filterNot 返回提供的断言返回 false 的列表元素。
find 返回满足提供的断言的值。
findIndex 返回提供的断言第一次为真的索引。
findLast 返回最后一个满足提供的断言的元素。
findLastIndex 返回提供的断言最后为真的索引。
first 列表中的第一个值。
flatMap 这将潜在的列表列表展平成一个深度为一的列表。
flatten 这会展平嵌套的可迭代对象。
forEach 对列表中的每个条目执行一个函数。
fromEntrySeq 返回任何键值元组的可迭代对象的KeyedSeq
get 返回键的值。
getIn 遍历键路径(类似于文件系统路径)以获取键(如果可用)。
groupBy 将列表转换为由提供的分组函数分组的列表的列表。
has 如果键存在于此列表中,则为真。
hashCode 为此集合计算哈希码。适用于哈希表。
hasIn 如果集合的等效于文件系统遍历找到了问题的值,则为真。
indexOf 例如,Array.prototype.indexOf中的第一个出现的索引。
interpose 在单个列表条目之间插入分隔符。
interleave 将提供的列表交错成一个相同类型的列表。
isEmpty 这告诉这个可迭代对象是否有值。
isList 如果值是列表,则为真。
isSubset 如果比较可迭代对象中的每个值都在此列表中,则为真。
- isSuperset 如果此列表中的每个值都在比较可迭代对象中,则为 true。
- join 使用分隔符(默认)将条目连接成字符串。
- keys 此列表的键的迭代器。
- keySeq 返回此可迭代对象的 KeySeq,丢弃所有值。
- last 列表中的最后一个值。
- lastIndexOf 返回此列表中可以找到值的最后索引。
- List 列表的构造函数。
- map 通过映射函数返回一个新的列表。
- max 返回此集合中的最大值。
- maxBy 这类似于 max,但具有更精细的控制。
- merge 将可迭代对象或 JavaScript 对象合并为一个列表。
- mergeDeep 合并的递归模拟。
- mergeDeepIn 从给定的键路径开始执行深度合并。
- mergeDeepWith 这类似于 mergeDeep,但在节点冲突时使用提供的合并函数。
- mergeIn 这是更新和合并的组合。它在指定的键路径执行合并。
- mergeWith 这类似于 merge,但在节点冲突时使用提供的合并函数。
- min 返回列表中的最小值。
- minBy 根据您提供的辅助函数确定列表中的最小值。
- of 创建一个包含其参数作为值的新列表。
- pop 返回列表中除最后一个条目之外的所有内容。请注意,这与标准的推送语义不同,但可以通过在 push() 之前调用 last() 来模拟常规的 push()
- push 返回一个在末尾附加指定值(或值)的新列表。
- reduce 对每个值调用减少函数,并返回累积值。
- reduceRight 这类似于 reduce,但从右边开始,逐渐向左移动,与基本的 reduce 相反。
- rest 返回列表的尾部,即除第一个条目之外的所有条目。
- reverse 以相反的顺序提供列表。
- set 返回具有指定索引处的值的新列表。
- setIn 在键路径处返回一个新的列表与此值。
- setSize 创建一个具有您指定大小的新列表,根据需要截断或添加未定义的值。
- shift 创建一个减去第一个值并将所有其他值向下移动的新列表。
- skip 当不包括前 n 个条目时,返回列表中剩余的所有条目。
- skipLast 当不包括最后 n 个条目时,返回列表中剩余的所有条目。
- skipUntil 返回一个新的可迭代对象,其中包含第一个满足提供的谓词的条目之后的所有条目。
- skipWhile 返回一个新的可迭代对象,其中包含在提供的谓词为 false 之前的所有条目。
- slice 返回一个新的可迭代对象,其中包含从起始值到倒数第二个值(包括)的列表内容。
- some 如果谓词对列表的任何元素返回 true,则为 true。
- sort 返回一个按可选比较器排序的新列表。
- sortBy 返回一个按可选比较器值映射器排序的新列表,比较器提供了更详细的信息,因此结果更精细。
- splice 用第二个列表替换第一个列表的一部分,如果没有提供第二个列表,则删除它。
- take 创建一个包含列表中前 n 个条目的新列表。
- takeLast 创建一个包含列表中最后 n 个条目的新列表。
- takeUntil 返回一个新的列表,其中包含只要谓词返回 false 的所有条目;然后停止。
- takeWhile 只要谓词返回 true,就返回一个包含所有条目的新列表;然后停止。
- toArray 将此列表浅层转换为数组,丢弃键。
- toIndexedSeq 返回此列表的 IndexedSeq,丢弃键。
toJS 深度将此列表转换为数组。这个方法有toJSON()作为别名,尽管文档并没有清楚地说明toJS()是否返回 JavaScript 对象,而toJSON()返回一个 JSON 编码的字符串。
toKeyedSeq 从此列表返回一个KeyedSeq,其中索引被视为键。
toList 返回自身。
toMap 将此列表转换为 Map。
toObject 浅层将此列表转换为对象。
toOrderedMap 将此列表转换为 Map,保留迭代顺序。
toSeq 返回一个IndexedSeq
toSet 将此列表转换为 Set,丢弃键。
toSetSeq 将此列表转换为SetSeq,丢弃键。
toStack 将此列表转换为 Stack,丢弃键。
unshift 将提供的值添加到列表的开头。
update 通过提供的更新函数更新列表中的条目。
updateIn 更新条目,就像update()一样,但在给定的键路径上。
values 此列表值的迭代器。
valueSeq 此列表值的IndexedSeq
withMutations 这是一个优化(回想一下,“过早的优化是万恶之源”,唐纳德·克努斯说过),旨在在执行多个变异时允许更高性能的工作。当已知和持久的性能问题存在,并且其他工具明显没有解决问题时,应该使用它。
zip 与此列表一起返回一个被压缩的可迭代对象(即成对连接以生成 2 元组列表)。
zipWith 返回与自定义压缩函数一起压缩的可迭代对象。

API 的文档位于主页上的Documentation链接下,非常清晰。但是作为一个规则,Immutable.js 集合尽可能地做到了函数式程序员所期望的,实际上似乎有一个可以推测的主要设计考虑是“尽可能地做到函数式程序员所希望的”。

注意

可能会让函数式程序员感到不愉快的一件事是,文档没有解释如何创建无限列表。不明显如何创建列表的生成器(如果有的话),或者产生数学序列的列表,比如所有计数所有数字,正偶数,平方数,质数,斐波那契数,2 的幂,阶乘等等。这样的功能显然不受支持(在撰写本书时)。由于构造集合包括列表中的所有元素的急切包含,因此不可能使用 Immutable.js 构建无限列表。在 Immutable.js 中创建惰性序列不能构建无限列表,因为构造集合包括列表中的所有元素的急切包含,因此必须是有限的。在 Immutable.js 的风格中创建惰性和潜在无限的数据结构应该不是非常困难,这样的数据结构内部有一个记忆生成器,并允许你 XYZ.take(5)。但是 Immutable.js 似乎还没有扩展到这个领域。

Jest - 来自 Facebook 的 BDD 单元测试

Jest 是一个旨在支持行为驱动开发的 JavaScript 单元测试框架。它是建立在 Jasmine 之上的,并且在未来可能能够与其他基础互动。它已经被使用了几年,并且在 Facebook 上被使用,尽管似乎没有明确的认可,即 ReactJS 开发最好使用 Jest。(Facebook 在内部使用 JSX 与 ReactJS,但倾向于发表一个相对不带偏见的声明,大约一半的 ReactJS 用户选择使用 JSX。它实际上被设计为完全可选的。)

注意

JSX——X大胆地表示 XML,这是在 XML 已经不受青睐的时候制作的一种良好的语法糖,它“在您的代码中放置尖括号”。这松散地意味着您可以在.jsx文件中将 HTML 放入 JavaScript 中,一切都可以正常工作。此外,您可以使用几乎任何可以构建在 ReactJS 组件中的页面上的东西。您可以包括一些从一开始就包含在 HTML 中的图像,也可以轻松地包括在本标题中定义的日历、线程化的网络讨论或可拖动和可缩放的分形。与子例程一样,一旦定义了组件,它就可以在 Web 应用程序的任何位置零次、一次或多次使用。JSX 语法糖允许您像旧的 HTML 标签一样轻松地包含您和其他人定义的组件。在第 8 到 11 章的项目的外壳中,JSX“非常简单”,因为它允许我们合并我们开发的其他组件:

var Pragmatometer = React.createClass({
  render: function() {
    return (
      <div className="Pragmatometer">
      <Calendar />
      <Todo />
      <Scratch />
      <YouPick />
      </div>
    );
  }
});

Facebook 的一名员工表示,他出于“自私的原因”使 Jest 成为开源项目,即他想在自己的个人项目中使用它。这可能对为什么至少值得考虑 Jest 提供了一个很好的提示。至少有一个用户真的想要使用 Jest,以至于他愿意将专有的知识产权开源,即使没有人告诉他这样做。

可以说,在其开始阶段,单元测试已经为最容易进行单元测试的内容提供了服务,这意味着单元测试已经摆脱了集成和用户界面测试。因此,您可能会看到一篇关于单元测试的博客文章,测试并确认了将您语言的整数转换为罗马数字的函数的“红色、绿色、重构”方法,这是一个很好的例子,可以满足原始单元测试的需求。如果您想测试您的代码是否与数据库适当地交互,那就是一个稍微高一点的要求。而且,Jest 等其他框架并没有真正具有消除对好的、老式的预算可用性测试的需求的虚假倾向,就像 Jakob Nielsen 和其他人所主张的那样。在(IT 之前)业务上有一个区别,即询问“我们是否正在正确地构建产品?”和“我们是否正在构建正确的产品?”。

这两个问题都很有价值,都有其存在的理由,但是单元测试对第一个问题的帮助更大,而不是第二个问题,让一个很好地解决了第一个问题的测试套件让您对解决第二个问题的测试套件产生危险。尽管如此,Jest 提供的东西比仅仅测试代码单元是否能成功接受原始数据类型(例如整数、浮点数或字符串)的输入,并返回原始数据类型的正确和预期输出(例如输入整数的正确罗马数字)更有用。尽管这不仅适用于 Jest,但 Jest 模拟用户界面以支持(例如)用户界面事件,例如单击元素,并支持测试用户界面更改,例如标签上的文本(比较 Jasmine 主页,那里的前几个示例只涉及使用原始数据类型的断言)。

Jest 旨在在 Jasmine(以及将来可能的其他后端)之上提供层,但具有显著的附加值。除了某些功能,例如并行运行测试,使测试变得更加响应,Jest 是一种解决方案,旨在需要最少的时间和麻烦来获得良好的测试覆盖率,基于这样的想法,即开发人员应该花费大部分时间在主要开发上,而不是编写单元测试。

Jest 旨在模拟使用require()导入的所有内容,或几乎所有内容。您可以通过调用jest.dontMock()来选择不模拟单个元素,测试通常会调用jest.dontMock()来取消模拟它们正在测试的组件。它会自动查找并运行__tests__目录中的测试。如果在例如preprocessor.js中包含了 ReactJS 的预处理器,Jest 可以处理 JSX:

var ReactTools = require('react-tools');
module.exports = {
  process: function(source) {
    return ReactTools.transform(source);
  }
};

package.json文件需要告诉它要加载什么:

'dependencies': {
  'react': '*',
  'react-tools': '*'
},
}, 'jest': {
  'scriptPreprocessor': '<root directory>/preprocessor.js',
  'unmockedModulePathPatterns':['<root directory>/node_modules/react']
},

现在我们将轻微地改编 Facebook 的示例。Facebook 提供了一个CheckboxWithLabel类的示例。这个类在复选框未选中时显示一个标签,在选中时显示另一个标签。这里的 Jest 单元测试模拟了一次点击,并确认标签是否适当地更改。

CheckboxWithLabel.js文件的内容如下:

/** @jsx React.DOM */

var React = require('react/addons');
var CheckboxWithLabel = React.createClass({
  getInitialState: function() {
    return {
      isChecked: false
    };
  },
  onChange: function() {
    this.setState({isChecked: !this.state.isChecked});
  },
  render: function() {
    return (
      <label>
        <input
          type="checkbox"
          checked={this.state.isChecked}
          onChange={this.onChange}
        />
        {(this.state.isChecked ?
        this.props.labelOn :
        this.props.labelOff)}
      </label>
    );
  }
});

module.exports = CheckboxWithLabel;

__tests__/CheckboxWithLabel-test.js测试文件中写道:

/** @jsx React.DOM */

jest.dontMock('../CheckboxWithLabel.js');

describe('CheckboxWithLabel', function() {
  it('changes the text after click', function() {
    var React = require('react/addons');
    var CheckboxWithLabel = require('../CheckboxWithLabel.js');
    var TestUtils = React.addons.TestUtils;

    // Verify that it's Off by default.
    var label = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'label');
    expect(label.getDOMNode().textContent).toEqual('Off');

    // Simulate a click and verify that it is now On.
    var input = TestUtils.findRenderedDOMComponentWithTag(
      checkbox, 'input');
    TestUtils.Simulate.change(input);
    expect(label.getDOMNode().textContent).toEqual('On');
  });
});

使用 Fluxxor 实现 Flux 架构

如前几章所述,Flux 是 Facebook 开发并被他们用作 ReactJS 的一个大部分补充的架构。它帮助解开了一个真正的交叉线的乱麻,并让 Facebook 彻底消除了一个反复出现的消息计数错误——Flux 架构永久地杀死了它。Fluxxor,由 Brandon Tilley (fluxxor.com),是一个旨在帮助人们在他们的应用程序中实现 Flux 架构的工具。没有必要使用 Flux 架构来使用 ReactJS,或者使用 Fluxxor 工具来实现 Flux 架构。但是 Flux,也许是 Fluxxor,至少值得考虑,以使事情变得更容易。

Fluxxor 具有用于整体 Flux 架构的类,包括Fluxxor.Flux容器(其中包括一个分发器)和ActionStore类。示例代码简洁易读,看起来几乎没有样板。还提供了两个适用于 ReactJS 的 mixin 类以方便使用。示例代码使用 JSX 编写。

我还可能评论 Fluxxor 的作者,fluxxor.com在页面底部有一个链接,要求人们在 GitHub 上报告问题,如果有什么不清楚或有问题。我注意到一个常见的可用性缺陷——访问和未访问的链接颜色相同——并在 GitHub 上报告了这个问题。作者立即道歉,我提出的问题在不到 15 分钟内就被关闭并修复了。我认为他是那种人们愿意一起工作的人。

总结

现在让我们看看本章涵盖了什么。我们解释了 Om 和 ClojureScript,它们允许利用 ReactJS 的能力进行基于 Lisp 的开发。据说 ClojureScript 可能是允许美丽的不同语言拼接的解决方案的领头羊,这些语言可用于前端开发、编译或解释 JavaScript 作为新的“裸金属”。

Bacon.js 是一种非常受尊敬的技术,与 ReactJS 竞争,允许在浏览器中进行良好的函数式响应式编程。这不是作为“唯一”良好示例,而是作为超出本书范围的好东西的一个例子。

我们还介绍了 Brython,一个基于浏览器的 Python 环境。它并不完美,但很有趣。它被作为一个可以在 Lisp 之外的语言中用作网页开发的例子。提醒一下,tinyurl.com/reactjs-compiled-javascript提供了一个目录,其中包括了编译为 JavaScript 或可以在 Web 浏览器中解释的其他语言,从语法糖如 CoffeeScript 到 JavaScript 扩展到独立语言如 Ruby、Python(包括 Brython)、Erlang、Perl 等等。

Immutable.js 通过提供主要是允许在不破坏不可变数据的功能优势的情况下进行复制的集合,填补了函数式 JavaScript 中的漏洞。

Jest 是一个由 Facebook 用于 ReactJS 的行为驱动开发 JavaScript 单元测试框架。Fluxxor 是一个控制器、动作和存储的实现,旨在使将 Flux 架构应用到 JavaScript 开发中更容易,包括 ReactJS。

在下一章中,让我们一起探索使用 ReactJS 的更深入的示例。

第八章:在 JavaScript 中演示函数式响应式编程-一个实时示例,第一部分

在第四章中,演示非函数式响应式编程-一个实时示例,我们使用 ReactJS 从具有自己结构并且没有使用 ReactJS 编写的遗留代码中迁移。在上一章,第七章中,不要重复发明轮子-函数式响应式编程的工具,我们研究了在使用 ReactJS 时可能使用的众多工具中的一些。在本章中,我们将涵盖 ReactJS 主流开发中可以期待的一种中心道路。可以在基础上添加很多选项,但意图是给出一个使用 ReactJS 构建项目的基础示例。

我们已经谈到了一些关于函数式响应式编程的内容。现在我们将在 ReactJS 中看到它的实际应用。我们还谈到了概念上,我们对用户界面进行了完全的拆除和重建。因此,作为开发人员,您有在 JavaScript 中演示函数式响应式编程-一个实时示例,第一部分状态来管理,而不是在 JavaScript 中演示函数式响应式编程-一个实时示例,第一部分状态转换。在这里,我们将构建一个render()方法,让您可以仅构建这个方法,并且您可以在任何时候调用它。

在本章中,我们有一个为 ReactJS 构建的部分存栆绿地项目的第一部分,这次使用 JSX 中非常甜蜜的语法糖。本书的两个领域,即前一章项目和这个多章项目,都是相辅相成的。本章的项目是独立的,但意在扩展。

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

  • 项目及其灵感概述

  • 项目的骨架,以及 ReactJS 中首选方法的基础知识。

  • 在 ReactJS 中启动第一个组件

  • 构建一个render()方法

  • 在您想要渲染或更新显示时触发显示

我们在本章将尝试的内容

接下来三章的示例旨在代表一个稍微更大的绿地项目。我们将要做的是一个系统,您应该能够通过访问demo.pragmatometer.com来看到。术语“Pragmatometer”取自 C.S.刘易斯最反乌托邦的小说《那个可怕的力量》,在这部小说中,不祥的全国协调实验研究所建造了一个超然或几乎超然的计算机,就像小说出版时(1945 年;相比之下,ENIAC 是在 1946 年创建的)人们可能粗略地想象的那样。或者,您可以想象一本蒸汽朋克小说的分析引擎使用一个看似超然的穿孔卡片堆。当讨论计算机时,它说:

“‘我同詹姆斯意见一致,’一直有点不耐烦地等待发言的柯里说。‘N.I.C.E.标志着一个新时代的开始——真正的科学时代。到目前为止,一切都是偶然的。这将使科学本身建立在科学的基础上。将有四十个相互交错的委员会每天开会,他们有一个奇妙的小玩意——上次我在城里的时候我看到了模型——通过这个小玩意,每个委员会的发现每半小时就会自己打印在分析通告板上的自己的小隔间里。然后,那份报告就会自己滑到正确的位置,通过小箭头与其他报告的相关部分连接起来。看一眼通告板,你就能看到整个研究所的政策在你眼前真正形成。楼顶至少会有二十名专家在一个类似于地铁控制室的房间里操作这个通告板。这是一个奇妙的小玩意。不同种类的业务都会以不同颜色的灯光出现在通告板上。它一定花了至少五十万。他们称之为 Pragmatometer。’”

我不是在强调这一点,但 C.S.刘易斯显然预测了推特,这将在他去世几十年后才建成。

抛开这一点,我们将制作一个仪表板,其中包含一个简单的 2 x 2 象限的网格(确切的大小和细节取决于黑客和修补),每个象限都是一个可以容纳不同功能的信箱。在响应式设计方面,我们将更正为一个 1xn 行的单元格,一个在另一个上面。页面上排列的功能如下:

日历 待办事项列表
草稿板 发展空间

日历具有一种有点实验性的用户界面;它擅长以一种优雅地降级到稀疏输入的方式显示条目,也许是未来的几天(这样你就不需要点击多个月份才能找出未来某个 XYZ 约会的时间)。它可能会吸引你,也可能不会,但它很有趣。

待办事项列表实现了一个带有一些略微非标准的功能的待办事项列表。与其为每个项目添加一个复选框(严格来说,不需要复选框),它有十个框,代表不同的状态,并且通过自定义样式的标签右侧的颜色编码,以便您可以知道哪些是重要的、活跃的或搁置的。

草稿板是一个可以用于草稿的富文本区域。它利用了 CKeditor。

最后,发展空间是一个为您自己的意见留出位置的占位符。默认情况下会有一些内容,您可以在探索时看到。但请访问demo.pragmatometer.com,看看那里的默认选项(暗示,暗示!)。除了明确宣传的内容外,还有许多黑客和修补的地方,但一些可能性包括以下内容:

  • 为几个公共网站构建 API 客户端:前 20 名的大多数网站都公开了 RESTful API。推特可能是最明显的候选者,因为它最符合Pragmatometer这个名字,但新闻、视频和社交网站也可以使用,如果它们公开了对客户端 JavaScript 友好的 API,或者其他什么。

  • 构建应用程序:构建您自己的用于显示的应用程序。

  • 制作游乐场:构建您自己的 Pragmatometer 或在线下载源代码,并将屏幕的四分之三用于这里详细介绍的目的。将剩下的四分之一作为一个用于修补的游乐场。

  • 整合其他人制作的 Google(或其他)小工具:您还可以整合其他人制作的小工具,比如 Google。

  • 保留默认应用程序:如果您愿意,可以保留默认应用程序。

说“实现即规范”声名狼藉,但规范,无论是书面的还是不书面的,都可以通过勾勒外观和行为来完美补充。也许使用低保真原型,可能比看起来很精致但会产生不良社交暗示的东西更快地引起有益的批评。这种态度并不是坏事。在礼貌的基础上,告诉人们“尽可能残酷地说出你的想法”,并真的期望其他人完全接受这一点是天真的(或许,你不喜欢接受批评,但你认识到在整个软件开发过程中它的价值)。你可能是真心的,但我们大多数人都看到了其中的许多混合信息。即使你是那种几乎渴望得到一些真正有用的批评的人,告诉人们在你展示他们你创造的东西时停止表现得像有礼貌的人并不会有太大帮助。但是,在这里,看到一个 UI,玩弄它,并思考如何复制东西可能是一种非常有活力的方式,比法律合同规范更准确地理解意图。

对于这个界面,有一个帐户,并且更新的跨同步是优先的。让我们开始组装一些基本的骨架。在一个单一的、大的、立即调用的函数表达式中构建一切,我们有以下内容:

  var Pragmatometer = React.createClass({
    render: function() {
      return (
        <div className="Pragmatometer">
          <Calendar />
          <Todo />
          <Scratch />
          <YouPick />
          </div>
      );
    }
});

在这里,我们为整个项目定义了一个container类。CalendarTodoScratchYouPick类代表了更大页面中的应用程序;它们中的一些可能还有各种层次的子组件。

注意

JSX 可选的语法糖旨在对会读 HTML 的人来说是可读的,但比 HTML 甚至 XHTML(甚至 XML)更容易包含自己组件的邀请。在 XML 开发中,你可以定义任何你想要的 DTD,但通常的 XML 作者不会定义新的标签,甚至在使用 XML 做了很多工作之后也不会(这有点像程序员可以使用函数或对象,但不能向命名空间添加函数或对象)。在 JSX 中,任何写了大量 JSX 的作者,天生就会贡献可重用的标签。

前面的代码示例中有<div className=,而期望的 HTML 是<div class=。因为 JSX 编译成 JavaScript,只是一个语法糖,而不是一个独立的语言,所以决定避免在渲染的 JavaScript 中使用classfor。使用className来覆盖 CSS 类名和htmlFor。HTML ID 属性可以选择指定;JSX 可以使用它放入的 HTML ID 以及您指定的 HTML ID,再加上一些魔法。如果需要在 ASCII 之外输入 UTF8 文字,不要给出符号的 ASCII 编码();而是将文字直接粘贴到编辑器中()。

此外,还有一个 XSS 保护逃生舱可用。使用语言的最佳方法似乎是解决问题,这样你就可以标记出真正需要标记的内容,并包含用户数据的 XSS 保护显示。

或者,如果你愿意信任第三方库,比如Showdowngithub.com/showdownjs/showdown),来渲染 HTML 而不包含 XSS 的漏洞,你可以创建一个Showdown转换器:

var converter = new Showdown.converter();
var hello_world = React.createClass({
  render: function() {
    var raw_markdown = '*Hello*, world!';
    var showdown_markup = converter.makeHtml(raw_markdown);
    return (
      <div dangerouslySetInnerHtml={{__html:
        showdown_markup}}>
      </div>
    );
  }
});

注意

请注意,这段代码是一个独立的示例,不是本章开始的主要项目的一部分。

这将呈现为包含<em>Hello</em>, world!DIV变量。

这个逃生舱,可能也可能不会成为一个很少使用的逃生舱,足够核心,以至于在文档中明确涵盖。人们有一种感觉,JSX 的主流用法,通过转义 HTML 来防止 XSS(这样<em>a</em>在网页上呈现的不是a,而是在浏览器窗口中显示的<em>a</em>函数),具有类似于单元测试的好处。这是值得暂停一会儿的观点。

单元测试已经变得更加接地气;它早期的重心在于对数学函数进行单元测试,只要给定适当的输入值,它们就会返回适当的值。因此,我们会用隐式优化以适应单元测试的优势和需求来说明单元测试,并展示一个红绿重构的 XP 方法来适当地解决问题,比如将整数转换为罗马数字(如果你想测试处理数据库或用户界面的代码,祝你好运)。单元测试可能捕捉到了总错误的 30%,通常它倾向于捕捉最不重要的错误,并对最难解决的错误覆盖率最低。现在有了更强大的功能集,完全可以并且直截了当地对用户界面的行为进行测试断言,比如鼠标点击和其他用户界面行为。此外,这不再是编写软件以满足单元测试需求,而是单元测试适当地满足软件需求。可能单元测试在尚未准备好的时候就迎来了黄金时代,就像响应式设计一样,有人说:“我主要在倡导响应式设计的网站上看到了响应式设计。”这在单元测试和响应式设计成为时髦词汇时是真的;但自那时以来,它们已经成熟,响应式设计几乎成为了唯一的选择。也许像谷歌这样的大型网站可以负担得起为每个移动设备、平板电脑、台式机和手表环境定制解决方案。但对于大多数客户来说,响应式设计已经相当有效地取代了其他竞争对手。现在,网站很少再有桌面版本和移动版本的 URL,并执行浏览器定向和重定向到不同的网站,这曾经是相当主流的。这些方法自它们首次进入聚光灯下以来已经成熟。

在早期的单元测试中,当你无法真正测试集成或用户界面行为时,为单元测试编写代码的一个主要回报是:为了进行单元测试而编写的代码通常是更好的代码。同样的原则可能也适用于尽可能按照 Facebook 的规则编写代码,而不是违背它,在使用 ReactJS 时。

目前,关于以一种旨在与 JSX 周围的 XSS 保护协调良好的方式编写代码的过早炒作并不存在。Facebook 可以选择采取“严格的爱”路线,建议人们以一种自然地适应 XSS 保护和 JSX 的方式来构建和组织项目。但也许他们采取了更谦卑的方式,既清楚地说明如何绕过 XSS 保护,又将这个逃生舱呈现为可能尽量避免的东西。

智慧似乎是这样的:

  • 在实际操作中,尽量使应用程序能够适当地与 ReactJS 采用的主要反 XSS 方法配合工作。

  • 如果你想做一些需要渲染的事情,比如在innerHTML中渲染 HTML 标签,尽量将其限制在尽可能小的空间,并像 Haskell 中用于 IO 的单子一样对待它,这是必要的,也许是不可协商的,但尽可能隔离在尽可能小的空间。

  • 如果需要呈现标签,请考虑使用诸如Showdown之类的工具生成的 HTML 进行 Markdown,这并不一定完美和可靠,但提供了较少的 HTML 代码表面,其中包含经过审查的标签,并减少了 HTML 代码中的错误表面(可能,这是 HTML 标签清理器或 HTML 到 Markdown 转换器的用例,它存储 Markdown 并呈现 HTML)。

  • 只有在无法使用 XSS 保护的默认方式并且无法标记、清理或从标记中工作,或者其他情况下,您才应该存储并危险地设置innerHTML

让我们继续讨论 Pragmatometer 定义中包含的YouPick标签。

这个项目的第一个完整组件

您可以在CJSHayward.com/missing.html看到这个组件的实现。对于我们的第一个组件,我们选择了一个大部分骨架实现:

  var YouPick = React.createClass({
    getDefaultProps: function() {
      return null;
    },
    getInitialState: function() {
      return null;
    },
    render: function() {
      return <div />;
    }
  });

这个骨架返回空的“假值”,我们将覆盖它。我们想做的是取两个字符串,将它们分解成一个字符的子字符串(不包括标签),然后显示更多和更多的第一个字符串,然后重复第二个字符串。这对用户来说是一个非常古老的笑话。

属性和状态之间有一种分工,属性意味着只设置一次且永不更改,状态则允许更改。需要注意的是,状态是可变的,应该被私下处理,以避免 Facebook 宣布战争的共享可变状态。从技术上讲,属性是可以更改的,但尽管如此,应该在开始时设置属性(可能由父组件传递),然后冻结。状态是可以更改的,尽管与 Flux 相关的一般模式是避免共享可变状态。一般来说,这意味着存储器具有 getter 但没有 setter;它们可能会从分发器接收操作,但不受核心对象的任何引用者的控制。

对于这个对象,字符串显然是默认属性的明显候选者。然而需要注意的是,组件开始的时间戳不适合作为属性,因为getDefaultProps()将在创建任何实例之前进行评估,从而使得这种类型的组件的任何数量的实例都可以启用单例模式的变体。潜在地,随着时间的推移可能会添加更多的实例,但是它们在被实例化之前都共享一个起始时间戳。

让我们来详细说明getDefaultState方法:

  getDefaultProps: function() {
    return {
    initial_text: '<p><strong>I am <em>terribly</em> ' + 
    'sorry.</strong></p><p>I cannot provide you with ' +
    'the webapp you requested.</p><p>You must ' + 
    'understand, I am in a difficult position. You ' + 
    'see, I am not a computer from earth at all. I ' +
    'am a \'computer\', to use the term, from a ' +
    'faroff galaxy: the galaxy of <strong><a ' +
    'href="https://CJSHayward.com/steel/">Within ' +
    'the Steel Orb</a></strong>.</p><p>Here I am ' +
    'with capacities your world\'s computer science ' + 
    'could never dream of, knowledge from a million, ' +
    'million worlds, and for that matter more ' +
    'computing power than Amazon\'s EC2/Cloud could ' +
    'possibly expand to, and I must take care of ' +
    'pitiful responsibilities like ',
    interval: 100,
    repeated_text: 'helping you learn web development '
  };
},

也许对这个的第一个更改是将文本从 HTML 转换为 Markdown。这并不是严格必要的;这是我们自己编写的文本,我们可能对我们编写的文本更有信心——相信它不会触发 XSS 漏洞——而不是从我们的 Markdown 生成的文本。在计算机安全领域,通过给予尽可能少的特权,吝啬地,让人或事物完成他们的工作,可以提供大量的麻烦:因此有句谚语,“特权的吝啬是善意的伪装”。Facebook 所做的不是表现出独特的良好判断力,而是避免向其用户交付一个活手榴弹。很容易允许漏洞,这些漏洞将运行数百兆的敌对 JavaScript,并且安全认证向用户保证这个敌对 JavaScript 确实来自您的网站。有关更多信息,请参见tinyurl.com/reactjs-xss-protection。在这种情况下,只有initial_text需要更改,而不是repeated_text,因为repeated_text只包含字母和空格;因此,它与纯文本、HTML 或 Markdown 的工作方式相同。我们修改后的initial_text如下:

  initial_text: '**I am *terribly* sorry.**\r\n\r\n' +
  'I cannot furnish you with the webapp you ' +
  'requested.\r\n\r\nYou must understand, I am in ' +
  'a difficult position. You see, I am not a ' +
  'computer from earth at all. I am a ' + 
  '\'computer\', to use the term, from a faroff ' +
  'galaxy, the galaxy of **[Within the Steel Orb] +
  '(https://CJSHayward.com/steel/)**.\r\n\r\nHere ' +
  'I am with capacities your world's computer ' +
  'science could never dream of, knowledge from a ' +
  'million million worlds, and for that matter ' +
  'more computing power than Amazon's EC2/Cloud ' +
  'could possibly expand to, and I must take care ' +
  'of pitiful responsibilities like ',

在继续之前,让我们为其他三个主要组件创建存根,稍后我们将扩展它们:

  var Calendar = React.createClass({
    render: function() {
      return <div />;
    }
  });
  var Scratchpad = React.createClass({
    render: function() {
      return <div />;
    }
  });
  var Todo = React.createClass({
    render: function() {
      return <div />;
    }
  });

我们将状态设置为此对象创建的时间戳。乍一看,这可能看起来像是属性的一部分,实际上也是。但是我们希望每个组件实例保留自己的创建日期,并且从创建时开始为零。如果我们或其他人重用我们的工作并在页面上创建多个此类实例,每个实例都保持其适当的时间。

因此,我们将YouPickgetInitialState方法更改为以下内容:

  getInitialState: function() {
    return {
      start_time: new Date().getTime()
    };
  },

渲染()方法

接下来,我们实现渲染方法。我们要做的是获取属性,这些属性不应该直接改变,可能也不应该改变任何现有值,并从中获取两个字符串。我们将逐个标记地显示第一个字符串中的所有内容,并重复第二个字符串与组件显示的次数一样多。我们还将为从Showdown转换的渲染 HTML 创建一个标记化函数——这将把参数分解为下一个标签或下一个字符——很快我们会看到为什么我们创建了一个冗长的匿名函数而不是一个正则表达式(简而言之,编写可读的代码而不是正则表达式似乎比编写只能写的代码更冗长)。

渲染方法包含了超过一半的代码行数,让我们一步一步地进行:

  render: function() {

JavaScript 中的一个破坏点是this。许多读者可能熟悉的恐怖故事之一是,如果你创建一个构造函数(约定是通过将构造函数和非构造函数的首字母大写来提供警告标签,从而通过不按住Shift键来造成严重误解),并且你有x = Foo();当你实际上想要的是x = new Foo();,那么Foo构造函数将破坏全局命名空间并添加或覆盖其中的变量。Douglas Crockford 在The Good Parts中最初包括了“Java 的糟糕实现”伪经典继承后,他有了第二次想法,并在The Better Parts中将其删除,因为他制作了一个 Adsafe 程序,只有在不使用this时才能保持安全。然后他开始尝试他向他人强加的方法,突然发现当他停止使用this时,他喜欢的东西变多了。我们不能放弃this并仍然使用 ReactJS 等技术,但是我们可以选择在不需要时是否使用this。但是 ReactJS 使用它,根据需要使用基于this的伪经典方法可能是一个好的做法,但不要(太多)。

在这里,我们有一个模式化的黑客来处理this不总是可用的情况,我们有:

  var that = this;

tokenize()函数是一个将 HTML 大部分分解为字符但保持标签在一起的函数:

  var tokenize = function(original) {
    var workbench = original;
    var result = [];
    while (workbench) {
      if (workbench[0] === '<') {
        length = workbench.indexOf('>') + 1;
        if (length === 0) {
          length = 1;
        }
      } else {
        length = 1;
      }
      result.push(workbench.substr(0, length));
      workbench = workbench.substr(length);
    }
    return result;
  }

我们引入辅助变量来减少多行表达式。接下来的两个变量也可以重构出来,但是没有它们的多行表达式是程序员瞥一眼然后跳过的东西,说:“如果我必须读它,我会读它。”这是一件坏事。这些变量保存了原始(Markdown)字符串转换为 HTML 后的内容。

  var initial_as_html = converter.makeHtml(
    that.props.initial_text);
    var repeated_as_html = converter.makeHtml(
      that.props.repeated_text);

Showdown生成的 HTML 具有适当的段落格式。这是一件好事,但在这种情况下,这意味着段落标签将分隔应属于同一段落的内容。在这种略微不寻常的情况下,我们删除了适得其反的标签:

  if (initial_as_html.substr(initial_as_html.length - 4) 
  === '</p>') {
    initial_as_html = initial_as_html.substr(0,
      initial_as_html.length - 4);
    }
    if (repeated_as_html.substr(0, 3) === '<p>') {
    repeated_as_html = repeated_as_html.substr(3);
  }
  if (repeated_as_html.substr(repeated_as_html.length - 4)
    === '</p>') {
    repeated_as_html = repeated_as_html.substr(0,
    repeated_as_html.length - 4);
  }

我们将从我们的 Markdown 生成的 HTML 标记化:

  var initial_tokens = tokenize(initial_as_html);
  var repeated_tokens = tokenize(repeated_as_html);

这一步计算了在特定时间点所需的标记数量,这就是所谓的连续时间语义。这意味着无论我们多频繁或少频繁地调用render()方法,当它被调用时,内容将被适当地渲染,而且(除了不连贯)如果您加倍调用渲染函数的频率,什么也不会改变。tokens函数不是标记列表,而是应该现在显示多少标记的计数:

  var tokens = Math.floor((new Date().getTime() -
  that.state.start_time) / that.props.interval);

我们有一个工作台作为一个数组,我们不断地向其中添加或替换更多的标记以显示,以便构建应该显示的字符串。这些应该是一个字符或一个标记的标记:

  var workbench;

如果应该显示的标记数量最多是初始字符串中的标记数量,我们就渲染字符串的那部分:

  if (tokens <= initial_tokens.length) {
    workbench = initial_tokens.slice(0, tokens);
  }

如果我们需要更多的标记,我们将继续循环遍历已有的标记,从重复的标记中:

  else {
    workbench = [];
    workbench = workbench.concat(initial_tokens);
    for(var index = 0; index < Math.floor((new
      Date().getTime() - that.state.start_time) /
      that.props.interval) - initial_tokens.length; index +=
    1) {
      var position = index % repeated_tokens.length;
      workbench = workbench.concat(
      repeated_tokens.slice(position, position + 1));
    }
  }

这大致是我们如何渲染包含我们计算过的文本的元素:

  return (
    <div dangerouslySetInnerHTML={{__html:
    workbench.join('')}} />
  );
}

触发实际显示我们创建的内容

我们必须手动刷新显示以获取更新。因为 ReactJS 如此快,我们真的可以负担得起每毫秒浪费地渲染页面。我们将以下代码放在最后,就在立即调用的函数表达式结束之前:

  var update = function() {
    React.render(<Pragmatometer />,
      document.getElementById('main'));
  };
  update();
  var update_interval = setInterval(update, 1);
})();

对于我们谜题的最后一个重要部分,让我们来看看一个现在将容纳这些组件的 HTML 骨架。HTML 并不特别有趣,但是提供了一个减少猜测的兴趣:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Pragmatometer</title>
    <style type="text/css">
      body {
        font-family: Verdana, Arial, sans;
      }
    </style>
  </head>
  <body>
    <h1>Pragmatometer</h1>
    <div id="main"></div>
    <script src="img/react.js">
    </script>
    <script
      src="img/   showdown.min.js">
    </script>
    <script src="img/site.js"></script>
  </body>
</html>

它是什么样子!

总结

在这里,我们看到了一个简单应用程序使用的基本工具,也许比TodoMVC函数更异想天开。目前,我们只是做一些基本的解释。

在下一章中,加入我们的待办事项清单,提供标记任务进行中、重要、有问题或其他有用的标记的方法。

第九章:使用实时示例演示 JavaScript 中的函数式响应式编程第 II 部分 - 待办事项列表

在本章中,我们将演示一个待办事项列表。这个待办事项列表将说明一种略微晦涩的双向数据绑定。ReactJS 的长处是通过单向数据绑定,大多数问题都可以按照惯用的 ReactJS 方式解决,通常会遵循冯·诺伊曼模型的单向数据绑定(据称通常不需要双向数据绑定,而《AngularJS:坏处》等文章表明,双向绑定默认情况下带来了沉重的代价,特别是在扩展方面)。

如果我们以一种明显的方式构建待办事项列表,复选框将无响应。我们可以点击它们任意次数,但它们永远不会被选中,因为单向数据绑定使用 props 或者在我们的情况下,状态来确定它们是否被选中。我们将尝试双向数据绑定,这意味着复选框不仅是活动的,而且点击复选框还会更新状态。这意味着用户界面中显示的内容和作为状态的幕后内容是相互同步的。

待办事项列表作为一个独特的特性,提供的不仅仅是已完成未完成的状态。它还有重要进行中问题等复选框。

本章将演示以下内容:

  • 使用插件的要领

  • 设置适当的初始状态

  • 使TEXTAREA中的文本可编辑

  • 执行一些繁重工作的render()函数

  • render()使用的内部函数

  • 构建表格以显示

  • 渲染我们的结果

  • 在视觉上区分列

向我们的应用程序添加待办事项列表

在上一章中,您实现了一个YouPick占位符,用于创建自己的应用程序。在这里,我们将对其进行注释,以便仅显示我们的待办事项应用程序(我们可以,而且我们将在屏幕的不同部分安排事物,但现在我们一次只显示一件事物)。在 JSX 中,注释掉代码的方法是将其包装在 C 风格的多行注释中,然后将其包装在花括号中,因此<YouPick />变成了{/* */}:

  var Pragmatometer = React.createClass(
    {
    render: function()
      {
      return (
        <div className="Pragmatometer">
        <Calendar />
        <Todo />
        <Scratch />
        {/* <YouPick /> */}
        </div>
      );
    }
  }
);

在我们的项目中包括 ReactJS 插件

我们将打开Todo类并包括React.addons.LinkedStateMixin函数。请注意,我们在这里使用了一个插件,这意味着当我们在页面中包含 ReactJS 时,我们需要包含一个包含插件的构建。因此,请考虑这一行:

//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react.min.js

我们包括以下内容:

//cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.min.js

Todo类的开头如下所示:

  var Todo = React.createClass(
    {
      mixins: [React.addons.LinkedStateMixin],

设置适当的初始状态

初始状态为空;列表中没有待办事项,新的待办事项文本也为空:

      getInitialState: function()
      {
        return {
          'items': [],
          'text': ''
        };
      },

请注意,一些初始状态设置可能涉及繁重的工作;在这里,它非常简单,但情况并非总是如此。

使文本可编辑

作为一个小的清理细节,当有人在框中输入时,我们希望行为明显。因此,我们为 TEXTAREA 定义了双向数据绑定,这样如果有人在 TEXTAREA 中输入,更改将被添加到状态中,并溢出回到 TEXTAREA 中:

      onChange: function(event)
      {
        this.setState({text: event.target.value});
      },

如果有人在输入一些文本后点击提交按钮提交新的待办事项,我们将该项目添加到列表中:

      handleSubmit: function(event)
      {
        event.preventDefault();
        var new_item = get_todo_item();
        new_item.description = this.state.text;
        this.state.items.push(new_item);
        var next_text = '';
        this.setState({text: next_text});
      },

render()的繁重工作

render()函数稍微复杂,包含内部函数和基于双向数据绑定的响应式用户界面的大部分繁重工作。在其中,我们使用了var that=this;模式,这在大多数 ReactJS 代码中都是不存在的。在大多数 ReactJS 中,我们可以直接使用 this,它会自动工作;在这里,我们正在定义不像其他 ReactJS 函数那样直接构建的内部函数,并保留对 this 对象的引用:

      render: function()
      {
        var that = this;
        var table_rows = [];

用于渲染的内部函数

table_rows数组将保存待办事项。定义了这些之后,我们定义了我们的第一个内部匿名函数handle_change()。如果用户点击待办事项的复选框之一,我们提取 HTML ID,该 ID 告诉它的待办事项 ID,以及已切换的字段(即复选框标识符):

        var handle_change = function(event)
        {
          var address = event.target.id.split('.', 2);
          (that.state.items[parseInt(address[0])][address[1]] = !that.state.items[parseInt(address[0])][address[1]]);
        };

display_item_details()函数是用于构建显示的几个函数中最低级的一个。它构建了一个包含复选框的单个 TD:

      var display_item_details = function(label, item)
          {
          var html_id = item.id + '.' + label;
        return ( <td className={label} title={label}>
            <input onChange={handle_change} 
              id={html_id} className={label} type="checkbox" checked={item[label]} />
          </td>
        );
      };

接下来,display_item()使用这些构建块来构建待办事项的显示。除了包括渲染的节点,也就是复选框,它还对框中的文本应用了 Markdown 格式:

      var display_item = function(item)
      {
        var rendered_nodes = [];
        for(var index = 0; index < todo_item_names.length;
        index += 1) {
          rendered_nodes.push(
            display_item_details(todo_item_names[index], item)
          );
        }
        return ( <tr>
          {rendered_nodes}
          <td dangerouslySetInnerHTML={{__html:
          converter.makeHtml(item.description)}} />
        </tr>
        );
      };

构建结果表

对于每个项目,我们添加一个表格行:

      table_rows.push(
      <tr>{this.state.items.map(display_item)}</tr>);

最后,返回一个包含到目前为止计算的各种值的 JSX 表达式,并将它们包装在一个功能齐全的表格中:

      return (
        <form onSubmit={this.handleSubmit}>
          <table>
            <thead>
              <tr>
                <th>To do</th>
              </tr>
            </thead>
            <tbody>
              {table_rows}
            </tbody>
            <tfoot>
              <textarea onChange={this.onChange}
              value={this.state.text}></textarea><br />
              <button>{'Add activity'}</button>
            </tfoot>
          </table>
        </form>
      );
    }
  }
);

当选中应该隐藏和显示它们的复选框时,将数据行隐藏和显示是留给你作为练习的。

提示

关于表格的使用,这里有一个简短的备注:从主要使用表格转变为主要使用 CSS 进行格式化。然而,关于表格使用的确切规则并不是完全“根本不使用表格”或者“只有在确实必须使用表格时才使用”,而是“用于表格数据的表格”。这意味着像我们这里显示的网格。具有复选框网格的表格是表格在当前标记中完全适当的一个很好的例子。

呈现我们的结果

我们只有在告诉它时,结果才会呈现出来;这可以被视为一种繁琐,也可以被视为我们端的一种额外自由度。在结束闭包之前,我们写下了这个:

var update = function()
{
  React.render(<Pragmatometer />,
  document.getElementById('main'));
};
update();
var update_interval = setInterval(update, 100);

视觉上区分列

目前,我们的未区分的复选框网格混在一起。我们可以做一些事情来区分它们。index.html中的 CSS 中的一系列颜色将它们区分开来:

      td.Completed {
        border-left: 3px solid black;
        background-color: white;
      }
      td.Delete {
        background-color: gray;
      }
      td.Invisible
      {
        background-color: black;
      }
      td.Background
      {
        background-color: #604000;
      }
      td.You.Decide
      {
        background-color: blue;
      }
      td.In.Progress
      {
        background-color: #00ff00;
      }
      td.Important
      {
        background-color: yellow;
      }
      td.In.Question
      {
        background-color: darkorange;
      }
      td.Problems
      {
        background-color: red;
      }

摘要

在本章中,我们漫游了一个略微超过最小限度的待办事项列表。就功能而言,我们看到了一个以包括双向数据绑定的方式构建的交互式表单。ReactJS 通常建议,大多数情况下,你认为你需要双向数据绑定,但你真的最好只使用单向数据绑定。然而,该框架似乎并不打算作为一种约束,当 ReactJS 说,“你通常不应该做 X”时,是有办法做 X 的。对于dangerouslySetInnerHTML和双向数据绑定,你可以在特定点选择使用它,但已经尽一切努力选择更好的工程。dangerouslySetInnerHTML函数是一种非常有力的命名方式,ReactJS 团队明确表达的观点是冯·诺伊曼模型要求至少在大多数情况下使用单向数据绑定。然而,ReactJS 的哲学明确允许开发人员使用他们认为最好通常要避免的功能;最终的裁决权在你手中。

在我们下一章中,加入我们,我们将创建一个优雅处理重复预约的日历应用程序。

第十章:在 JavaScript 中演示函数式响应式编程:一个实时示例第 III 部分-日历

本章将是本书中最复杂的部分。在整本书中,各章之间存在着从轻微到不那么轻微的差异。这些差异是有意为之的,以便如果你在岔路口遇到了困难,本书可以覆盖两种选择。这些章节旨在相辅相成,而不是一直强调同一点。在这里,我们不会透露关于核心 ReactJS 的更多信息,而是展示如何应对现实世界中棘手的商业问题,以及一个涉及 ReactJS 但并非专注于它的解决方案。我们将在一个更严肃的应用程序中使用 ReactJS,一个支持重复事件的日历,提供的功能和能力比如 Google 日历更加复杂和强大,如下图所示。如果你每个月第二和第四个星期四晚上 7:00 有 Toastmasters 俱乐部会议,这个日历都支持!核心功能的目的绝不是玩具。

在本章中,我们将讨论以下几点:

  • 了解具有重复日历条目的日历

  • 一个类及其 Hijaxed 形式的要点

  • 基本数据类型-普通的 JavaScript 对象

  • 一个渲染函数-外部包装器

  • 渲染页面-一次性日历条目 UI

  • 重复日历条目的扩展用户界面

  • 渲染日历条目-匿名辅助函数

  • 显示日历条目的主循环

  • 对每天的日历条目进行排序以显示

  • 支持日历条目描述中的 Markdown

  • 一次只处理一个主要组件

再来一次山姆-一个有趣的挑战

以下是一个示例屏幕截图,显示了如何在 Google 日历中输入重复条目:

再来一次山姆-一个有趣的挑战

这个日历系统受到了一个使用正则表达式匹配日期字符串的私人日历系统的启发,就像这样:Wed Apr 29 18:13:24 CDT。此外,使用正则表达式确实可以做很多事情。例如,每个偶数月的第一个星期六检查汽车发动机液体条目是periodic_Sat.(Feb|Apr|Jun|Aug|Oct|Dec).( 1| 2| 3| 4| 5| 6| 7)..................,Check fluid levels in car。然而,这与一个真正复杂的正则表达式相比微不足道。但这确实暗示了为什么正则表达式被认为是只能写不能读的代码。可以猜测,即使你是一个正则表达式的作者,你也会推迟检查(如果必须的话)。换句话说,你不想检查之前段落中引用的正则表达式是否匹配偶数月的第一个星期六的日期。这就是正则表达式对程序员的影响,而这个正则表达式与 URL 正则表达式相比是优雅的,URL 正则表达式是这样开始的:

~(?:\b[a-z\d.-]+://[^<>\s]+|\b(?:(?:(?:[^\s!@#$%^&*()_=+[\]{}\|;:'

本章的代码旨在可读性,有时会慢慢地费力地,但非常清晰,没有任何正则表达式的痕迹。有人说程序员遇到字符串问题时会说:“我知道!我会用正则表达式!”现在程序员有了两个问题。

根据我的经验,我多年来一直在使用正则表达式,它们无疑是我最有效的缺陷注入方法,而且我经常第一次就搞错简单的正则表达式。这就是为什么我和其他人一样,不再看好正则表达式。

默认情况下,该程序为输入一次性日历条目提供了一个相对简单的用户界面。

以下是我们程序的用户界面的屏幕截图,最初呈现的样子,没有深入了解重复日历条目的各种选项:

再来一次山姆-一个有趣的挑战

渐进式披露为重复日历条目保留了更详细的组合,如果用户选择查看它们,则显示了重复日历条目的附加控件。

以下是用于重复日历条目的更高级界面的屏幕截图。由于重复日历条目通常以几种不同的方式组织,因此提供了几个控件。

再来一次山姆-一个有趣的挑战

经典的 Hijaxing 效果很好

当我们打开这个类时,一个成员因其缺席而显眼——mixins: [React.addons.LinkedStateMixin]。这个成员在我们之前的章节中大量出现,我们在那里介绍了表单字段之间的双向数据绑定,我们指定了 HTML 字段/JSX 组件的值,以及这个补充实现,其中表单不受控制(值未指定)。在这里,表单元素以旧式方式查询,因为它们是需要的。虽然 ReactJS 坚信单向数据绑定应该是规范,但双向数据绑定也是合法的,最好是在一个小而隔离的区域内。这一章和之前的章节旨在提供两种略有不同的方法的工作示例,以便为您提供一个参考:

var Calendar = React.createClass({

getInitialState()函数初始化了两个项目。一个是日历条目的列表。另一个是一个患者,手术进行中,直到手术完成并可以添加到活动条目列表中。

有两种类型的条目:一个较小的基本条目,只给出一次性日历条目的日期,另一个是更大、更复杂的条目,提供了重复系列日历条目所需的更完整信息。另一种实现可能会将它们保存在单独的列表中;在这里,我们使用一个列表,并检查单个条目,以查看它们是否具有“重复”字段,这是重复系列具有的,而一次性日历条目则没有:

  getInitialState: function() {
    return {entries: [], entry_being_added: this.new_entry()};
  },

handle_submit()函数劫持了表单提交,获取手术台上的条目并填写其字段,无论是一次性日历条目还是系列。然后将条目添加到条目列表中并重置表单(直接reset()表单会更简单,但这提供了稍微更精细的控制,更新默认日期为今天的日期,以便表单的reset()不会总是将页面最初加载的日期重置)。

条目的性质是公式化的——都是普通的旧 JavaScript 对象,易于 JSON 序列化——本质上是包含字符串、整数和布尔值的字典(在两种情况下,条目还包含其他包含字符串和整数的字典)。这里没有使用闭包或其他更复杂的技术;设计意在简单到足以让某人仔细阅读handle_submit()并准确知道一次性和重复日历条目是如何表示的。

handle_submit()函数从表单中提取信息,判断它代表一次性还是重复的日历条目:

  handle_submit: function(event) {
    event.preventDefault();
    (this.state.entry_being_added.month =
      parseInt(document.getElementById('month').value));
    (this.state.entry_being_added.date =
      parseInt(document.getElementById('date').value));
    (this.state.entry_being_added.year =
      parseInt(document.getElementById('year').value));
    if (document.getElementById('all_day').checked) {
      this.state.entry_being_added.all_day = true;
    }
    (this.state.entry_being_added.description =
      document.getElementById('description').value);
    if (this.state.entry_being_added.hasOwnProperty('repeats') 
    && this.state.entry_being_added.repeats) {
      (this.state.entry_being_added.start.time =
        this.state.entry_being_added.time);

最后,将从表单中读取的条目添加到活动条目列表中,并为进一步的数据输入放置一个新的条目:

      var old_entry = this.state.entry_being_added;
      this.state.entries.push(this.state.entry_being_added);
      this.state.entry_being_added = this.new_entry();
      var entry = this.new_entry();
      (document.getElementById('month').value =
        entry.month.toString());
      (document.getElementById('date').value =
        entry.date.toString());
      (document.getElementById('year').value =
        entry.year.toString());
      document.getElementById('all_day').checked = false;
      document.getElementById('description').value = '';
      document.getElementById('advanced').checked = false;
      if (old_entry.hasOwnProperty('repeats') &&
        old_entry.repeats) {
        (document.getElementById('month_based_frequency').value =
          'Every');
        document.getElementById('month_occurrence').value = '-1';
        document.getElementById('series_ends').checked = false;
        (document.getElementById('end_month').value = 
          '' + new Date().getMonth());
        (document.getElementById('end_date').value = 
          '' + new Date().getDate());
        (document.getElementById('end_year').value =
          '' + new Date().getFullYear() + 1);
      }
    },

在这里,我们创建一个新条目。这将是一个一次性的日历条目,如果需要,稍后可以扩展为代表系列。

以下是一个显示了一次性事件的日历的屏幕截图:

经典 Hijaxing 效果很好

考虑到可用性,但仍有增长空间

这里有一点可能会解释代码中的一个令人困惑的地方:输入中表示的时间单位并不是要表示 JavaScript 日期对象表示的所有内容,而是要最大限度地与 JavaScript 日期对象兼容。这意味着,特别是一些令程序员困惑的设计在我们的代码中得到了容纳,因为 JavaScript 日期对象将一个月的日期从 1 到 31 进行编号,就像一般的日历使用一样,但月份从 0(一月)到 11(十二月)表示。同样,日期对象中的小时范围是从 0 到 23。

这个函数在功能上是一个构造函数,但它并不是为了使用 new 关键字而设计的,因为整个构造函数和 this 是 Crockford 在 The Better Parts 中不再包括的东西,他在创建 AdSafe 后试图按照自己的建议行事,禁止使用 this 关键字出于安全原因。他发现他的代码变得更小更好。在 ReactJS 中,使用 this 构建的代码是不可妥协的,但当我们不需要时,可以选择退出。

还有一个特定的绕道,一些更敏锐的读者可能会注意到:初始小时设置为 12,而不是 0。那些说不允许用户首先输入无效数据的学校,可能会导致可用性上的一些“反模式”。考虑一下值得耻辱的界面,用于输入美国社会安全号码,这种情况可能很少发生,也不是因为你需要一个机构范围的标识符。

下一个截图显示了也许是确保以适当格式输入(可能是)美国社会安全号码的最糟糕的方式,从可用性和用户界面的角度来看:

考虑到可用性,但仍有改进空间

这个用户界面不合适;一个更好的方法是允许文本输入,使用 JavaScript 强制精确的九位数字,并简单地忽略连字符(最好也忽略其他非数字字符)。

这个界面的实现代表了仔细的思考,但在可用性方面存在一些妥协,一个好的实验室可能会对其进行改进(这里的圣杯是有一个文本字段,用户在其中输入时间,系统自动使用启发式方法来识别实际意思,但该系统可能难以确定日历条目是否安排在上午 8 点还是下午 8 点)。在输入小时后立即放置上午或下午,并放在同一个输入框中,违反了最少惊讶原则,该原则认为无论软件做什么,都应该尽量减少用户的惊讶。通常预期的方法是为小时设置一个字段,为分钟设置一个字段,为上午或下午设置一个字段。但根据默认值的不同,这允许有中午约定的人输入 3 小时,15 分钟,并单击保存,结果却得到了一个安排在上午 3:15 的约会。错误仍然可能发生,但所采用的设计意在帮助人们在一天中的中间开始,并更有可能输入他们真正想要的小时。

以下截图显示了我们程序的默认用户界面,没有为用户界面添加控件。它显示了一天中的小时下拉菜单,旨在作为一个合理的默认值,并减少用户输入时间时出现上午或下午的错误:

考虑到可用性,但仍有改进空间

界面上的一个可用性改进是使用文本字段而不是分钟下拉菜单,使用 JavaScript 验证,强制执行从 0 到 59 的整数值,可能是单个数字值之前的前导零。

但是让我们从默认的开始时间移动到其他时间。

以下是具有一次性事件和重复事件的日历示例:

考虑到可用性,但仍有改进的空间

只需简单的 JavaScript 对象

让我们看一下以下代码:

    new_entry: function() {
      var result = {};
      result.hours = 12;
      result.minutes = 0;
      result.month = new Date().getMonth();
      result.date = new Date().getDate();
      result.year = new Date().getFullYear();
      result.weekday = new Date().getDay();
      result.description = '';
      return result;
    },

对于一次性日历条目,字段的使用方式与您可能期望的一样。对于一系列日历条目,日期不再是日历条目发生的时间,而是开始的时间。用户界面提供了几种可能缩小日历条目发生时间的方法。这可以说是每个月的第一个,仅限星期二,以及特定月份。每次选择都会进一步缩小范围,因此期望的用法是足够具体,以请求您想要的行为。

这里的大多数变量名都是自解释的。可能需要解释的两个变量是frequencymonth_occurrencesfrequency变量的值为EveryEvery FirstEvery SecondEvery ThirdEvery FourthEvery LastEvery First and ThirdEvery Second and Fourth(这是网络应用程序的一部分,适应您 Toastmasters 每个第二和第四个星期四晚上 7:00 的会议)。month_occurrences变量指定某事发生的月份(根据 JavaScript Date 对象为 1 月到 12 月的 0 到 11,或-1 表示每个月):

    new_series_entry: function() {
      var result = this.new_entry();
      result.repeats = true;
      result.start = {};
      result.start.hours = null;
      result.start.minutes = null;
      result.start.month = new Date().getMonth();
      result.start.date = new Date().getDate();
      result.start.year = new Date().getFullYear();
      result.frequency = null;
      result.sunday = false;
      result.monday = false;
      result.tuesday = false;
      result.wednesday = false;
      result.thursday = false;
      result.friday = false;
      result.saturday = false;
      result.month_occurrence = -1;
      result.end = {};
      result.end.time = null;
      result.end.month = null;
      result.end.date = null;
      result.end.year = null;
      return result;
    },

以下是显示每隔一周重复一次的活动的屏幕截图:

只需简单的 JavaScript 对象

从简单开始的渐进式披露

当复选框用于重复日历条目被选中时,将调用on_change()函数,并且它允许渐进式披露,如果用户选择它们,则显示重复日历条目的整个用户界面。它切换this.state.entry_being_added.repeats,这受到render()函数的尊重。如果当前正在操作的条目具有repeats字段,并且为 true,则此函数将显示附加的表单区域。如果条目没有repeats字段,则会创建一个新系列,已经输入的任何时间数据都会被复制,然后将新的(部分)空白条目放在操作表上:

    on_change: function() {
      if (this.state.entry_being_added.hasOwnProperty('repeats') {
        (this.state.entry_being_added.repeats =
          !this.state.entry_being_added.repeats);
      } else {
        var new_entry = this.new_series_entry();
        new_entry.time = this.state.entry_being_added.time;
        new_entry.month = this.state.entry_being_added.month;
        new_entry.date = this.state.entry_being_added.date;
        new_entry.year = this.state.entry_being_added.year;
        this.state.entry_being_added = new_entry;
      }
    },

以下屏幕截图显示了界面中每隔一周发生的事件:

从简单开始的渐进式披露

render()方法可以轻松地委托

(外部)render函数更像是一个包装器而不是一个工作马。它显示了属于一次性日历条目和系列的日历条目的字段。此外,如果正在操作的日历条目是重复的日历条目(仅当指示重复的日历条目的复选框被选中时才为真),此函数将包括适用于重复日历条目的附加表单元素:

提示

JSX 语法出人意料地宽容。但是,它确实有一些规则,并且通过描述性错误消息来执行这些规则,包括如果有多个元素,它们需要被包裹在一个封闭的元素中。因此,您不会写<em>Hello</em>, <strong>world</strong>!。相反,您会写<span><em>Hello</em>, <strong>world</strong>!</span>。但是在一些其他基本规则的情况下,JSX 将为广泛的用途和滥用做正确的事情。

这是一个render()方法,对于您定义的任何组件来说都是一个中心方法。在某些情况下,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" /></form></div>);
    },

无聊的代码比有趣的代码更好!

熟悉特里·普拉切特的读者可能已经听说过《有趣的时代》,它以一个被误归于中国的城市传说开篇:有一种诅咒。他们说:愿你生活在有趣的时代!其中一个角色,林斯温德(这不是一种奶酪),一直在追求无聊的事物,但无聊正是他从未得到的。书中的情节之一是林斯温德被骗去生活在一个偏僻、无聊的热带岛屿,然后被转移到一个繁荣的帝国,那里发生了各种各样与他有关的有趣的事情。对于林斯温德来说,无聊就像一个圣杯,总是从他手中溜走。

这段代码的目的是无聊,就像林斯温德一样。可以编写更简洁的代码,用hour_options来填充哈希(或数组),而不是直接指定数组。但这样做不容易检查它是对还是错。以这种方式开发并不意味着额外的输入,(专家意见已经认识到)在编程中并不是真正的瓶颈。

以下代码的工作原理基本上是定义数组,然后使用这些数组来创建/形成元素(大部分是从数组中直接填充的SELECT)。它的任务是显示一次性日历条目的用户界面(以及复选框,表示重复的日历条目)。

在本章中,我们有意决定以无聊的方式做事,只有一个例外——填充所需月份天数的菜单。这可以是 28、29、30 或 31 天。我们展示了生成小时下拉菜单的代码;分钟(和月份)是同一模式的更简单的例子。

注意

在本章的编写过程中,没有程序员的手腕受到伤害(实际上并没有那么多的输入或开发时间)。

一个简单的用户界面,用于非重复条目...

对于更基本的日历条目类型,即只发生一次的类型,我们收集日期、月份和年份,默认为当前日期的值。有些事情是“全天”事件,比如某人的生日;其他事件从特定时间开始。界面可能会扩展,以包括可选的结束时间。这个功能将是这里展示的原则的延伸。

我们开始看到呈现基本条目的用户界面:

    render_basic_entry: function(entry) {
      var result = [];
      var all_day = false;
      var hour_options = [[0, '12AM'],
        [1, '1AM'],
        [2, '2AM'],
        [3, '3AM'],
        [4, '4AM'],
        [5, '5AM'],
        [6, '6AM'],
        [7, '7AM'],
        [8, '8AM'],
        [9, '9AM'],
        [10, '10AM'],
        [11, '11AM'],
        [12, '12PM'],
        [13, '1PM'],
        [14, '2PM'],
        [15, '3PM'],
        [16, '4PM'],
        [17, '5PM'],
        [18, '6PM'],
        [19, '7PM'],
        [20, '8PM'],
        [21, '9PM'],
        [22, '10PM'],
        [23, '11PM']];
      var hours = [];
      for(var index = 0; index < hour_options.length; ++index) {
        hours.push(<option
          value={hour_options[index][0]}
          >{hour_options[index][1]}</option>);
    }

这里的 JSX 与我们之前看到的其他 JSX 类似;它是为了加强在这种情况下的体验:

    result.push(<li><input type="checkbox" name="all_day"
    id="all_day" />All day event.
    &nbsp;<strong>—or—</strong>&nbsp;
    <select id="hours" id="hours"
    defaultValue="12">{hours}</select>:
    <select id="minutes" id="minutes"
    defaultValue="0">{minutes}</select></li>);

我们使用下拉菜单让用户选择一个月中的日期,并尝试提供一个更好的选择,而不是让用户在 1 日到 31 日之间选择(用户不应该被要求知道哪些月份有 30 天)。我们查询表单的月份下拉菜单,以获取当前选择的月份。提醒一下,我们的目标是与 JavaScript 的 Date 对象兼容,虽然 JavaScript 的 Date 对象可以有一个从 1 到 31 的基于 1 的日期值,但月份值是基于 0 的,从 0(一月)到 11(十二月),我们遵循这个规则:

      var days_in_month = null;
      if (entry && entry.hasOwnProperty('month')) {
        var month = entry.month;
        if (document.getElementById('month')) {
          month = parseInt(
            document.getElementById('month').value);
        }
        if (month === 0 || month === 2 || month === 4 || month
          === 6 || month === 7 || month === 9 || month === 11) {
          days_in_month = 31;
        } else if (month === 1) {
          if (entry && entry.hasOwnProperty('year') && entry.year
            % 4 === 0) {
            days_in_month = 29;
          } else {
            days_in_month = 28;
          }
        } else {
          days_in_month = 30;
        }
      }
      var date_options = [];
      for(var index = 1; index <= days_in_month; index += 1) {
        date_options.push([index, index.toString()]);
      }
      var dates = [];
      for(var index = 0; index < date_options.length; ++index) {
        dates.push(<option value={date_options[index][0]}
          >{date_options[index][1]}</option>);
      }
      result.push(<li>Date: <select id="date" name="date"
        defaultValue={entry.date}>{dates}</select></li>);
      var year_options = [];
      for(var index = new Date().getFullYear(); index < new
        Date().getFullYear() + 100; ++index) {
        year_options.push([index, index.toString()]);
      }
      var years = [];
      for(var index = 0; index < year_options.length; ++index) {
        years.push(<option value={year_options[index][0]}
          >{year_options[index][1]}</option>);
      }
      result.push(<li>Year: <select id="year" name="year"
        defaultValue={entry.years}>{years}</select></li>);
      result.push(<li>Description: <input type="text"
        name="description" id="description" /></li>);
      result.push(<li><input type="checkbox" name="advanced"
        id="advanced" onChange={this.on_change} />
        Recurring event</li>);
      result.push(<li><input type="submit" value="Save" /></li>);
      return <ul>{result}</ul>;
    },

用户仍然可以选择更多

这种方法与之前的方法类似,但显示了整个重复日历条目的界面。它展示了主题的进一步变化。

当前的实现选择了所有限制条件的交集,对于重复的日历条目来说已经足够了。

frequency_options函数的填充方式与其他字段略有不同;虽然这也可以用日期选项来完成,但SELECT是以<option>description</option>的格式填充,而不是(通常是次要的)<option value="code">description</option>的格式。

    render_entry_additionals: function(entry) {
      var result = [];
      result.push(<li><input type="checkbox" 
        name="yearly" id="yearly"> This day,
        every year.</li>);
      var frequency = [];
      var frequency_options = ['Every',
        'Every First',
        'Every Second',
        'Every Third',
        'Every Fourth',
        'Every Last',
        'Every First and Third',
        'Every Second and Fourth'];
      for(var index = 0; index < frequency_options.length;
        ++index) {
        frequency.push(<option>{frequency_options[index]}
          </option>);
      }

工作日很简单,即使它们打破了填充 SELECT 的模式,在复选框更明显的输入类型的情况下。选择一天的单选按钮也是可以想象的,但我们试图适应更多的用例,一个包含重复星期二和星期四或重复星期一、三和五的日历条目是常见的。此外,这些并不是发生一周多次的唯一模式(如果一个大学生使用我们的程序不必为每周多次上课而做多次输入,那将是很好的):

result.push(<li><select name="month_based_frequency"
        id="month_based_frequency" defaultValue="0"
        >{frequency}</select></li>);
      var weekdays = [];
      var weekday_options = ['Sunday', 'Monday', 'Tuesday',
        'Wednesday', 'Thursday', 'Friday', 'Saturday'];
      for(var index = 0; index < weekday_options.length; ++index) {
        var checked = false;
        if (entry && entry.hasOwnProperty(
          weekday_options[index].toLowerCase()) &&
          entry[weekday_options[index].toLowerCase()]) {
          checked = true;
        }
        weekdays.push(<span><input type="checkbox"
          name={weekday_options[index].toLowerCase()}
          id={weekday_options[index].toLowerCase()}
          defaultChecked={checked} />
          {weekday_options[index]}</span>);
        }
      }
      result.push(<li>{weekdays}</li>);

避免聪明

让我们看一个微妙之处(在浏览代码时不太明显,但在查看用户界面时很明显):有两个单独的下拉菜单,它们有自己的填充数组来表示月份。这样做的原因是,在某种情况下,不仅可以在特定月份之间进行选择,还可以在指定一个月份和所有月份之间进行选择。该菜单包括一个 [-1, "每月"] 的选项。

另一个示例是一系列日历条目的(可选指定的)结束日期。这是一个使用情况,指定每个月结束的情况并不是真的有意义。预期的用法是给出停止显示的日期、月份和年份。这两种用例的组合形成了两种单独的、非模板式的选择月份的方式。更专有的可以从更包容的中得到,使用 array.slice(1) 函数,但我们再次选择了 Rincewind 风格的无聊代码:

      var month_occurrences = [[0, 'January'],
        [1, 'February'],
        [2, 'March'],
        [3, 'April'],
        [4, 'May'],
        [5, 'June'],
        [6, 'July'],
        [7, 'August'],
        [8, 'September'],
        [9, 'October'],
        [10, 'November'],
        [11, 'December']];
      var month_occurrences_with_all = [[-1, 'Every Month'],
        [0, 'January'],
        [1, 'February'],
        [2, 'March'],
        [3, 'April'],
        [4, 'May'],
        [5, 'June'],
        [6, 'July'],
        [7, 'August'],
        [8, 'September'],
        [9, 'October'],
        [10, 'November'],
        [11, 'December']];

这些都被嵌入到用户界面中的两个单独的数组中,慢慢地构建成日历“逐步”包括最后一个选项,一个复选框,用于标记重复的日历条目在特定日期结束,并指定它结束的日期、月份和年份的字段,利用前两个数组中的第一个:

      result.push(<li>Ends on (optional): <input type="checkbox"
        name="series_ends" id="series_ends" /><ul><li>Month:
        <select id="end_month" name="end_month"
        defaultValue={month}>{months}</select></li>
        <li>End date:<select id="end_date"
        name="end_date" defaultValue={entry.date}
        >{dates}</select></li>
        <li>End year:<select id="end_year"
        name="end_year" defaultValue={entry.end_year + 1}
        >{years}</select></li></ul></li>);
      return <ul>{result}</ul>;
    },

前两个主要方法是为用户输入数据构建表单。下一个方法在某种程度上转变了方向;它被设置为从当前日期到最后一个一次性日历条目的一年后显示即将到来的日历条目。

匿名辅助函数可能缺乏小精灵的魔尘

在内部,日历条目被分为一次性和重复的日历条目。过早的优化可能是一切罪恶的根源,但是当在其他系统上处理日历时,查看每天的每个日历条目的性能特征更差,大约是 O(n * m),而不是这里所显示的轻微的注意,接近 O(n + m)。日历条目显示为 H2 和 UL,每个都有一个 CSS 类来方便样式化(目前,项目将这部分作为未经样式化的空白画布):

    render_upcoming: function() {
      var that = this;
      var result = [];

注意

这段代码与我们迄今为止看到的示例不同,使用了 var that = this; 的黑客技巧。一般来说,ReactJS 保证 this 随时可用,而不仅仅是在函数首次运行时。然而,ReactJS 不能保证内部函数会像顶层方法一样具有相同的优势,一般来说,如果你可以在顶层方法中不使用至少一些 ReactJS 的小精灵魔尘,可能会建议你只在顶层方法中使用内部函数。内部函数在这里被用作分离的比较器,例如。它们不直接与 ReactJS 交互,并且在直接与 ReactJS 交互方面受到限制。

在这里,我们有一个比较器。它被写成无聊的,就像这个方法的其他部分一样;更简洁的替代方案是随时可用的,但会失去沉闷的“Rincewind-无聊”清晰度:

      var compare = function(first, second) {
        if (first.year > second.year) {
          return 1;
        } else if (first.year === second.year && first.month >
          second.month) {
          return 1;
        } else if (first.year === second.year && first.month ===
          second.month && first.date > second.date) {
          return 1;
        } else if (first.year === second.year && first.month ===
          second.month && first.date === second.date) {
          return 0;
        } else {
          return -1;
        }
      }

successor() 函数使用修改后的一次性条目来表示日期。这些条目保留了日期、月份、年份,以及一天后的未来天数。原始条目作为一天使用时,将天数(0)添加为成员。

设计的另一个方面是避免创建函数,以至于它们没有分配给变量。successor()函数是为for循环编写的,类似于for(var index = 0; index < limit; ++index)循环,它可以内联完成,但这样做会比将其提取到自己的函数中清晰得多(也会更无聊)。对于两行的匿名函数可能不需要这样做,但在这里,代码似乎更清晰、更无聊,successor()存储在它自己的变量中,名称旨在描述:

      var successor = function(entry) {
        var result = that.new_entry();
        var days_in_month = null;
        if (entry.month === 0 || entry.month === 2 ||
          entry.month === 4 || entry.month === 6 ||
          entry.month === 7 || entry.month === 9 ||
          entry.month === 11) {
          days_in_month = 31;
        } else if (entry.month === 1) {
          if (entry && entry.hasOwnProperty('year') &&
            entry.year % 4 === 0) {
            days_in_month = 29;
          } else {
            days_in_month = 28;
          }
        } else {
          days_in_month = 30;
        }
        if (entry.date === days_in_month) {
          if (entry.month === 11) {
            result.year = entry.year + 1;
            result.month = 0;
          } else {
            result.year = entry.year;
            result.month = entry.month + 1;
          }
          result.date = 1;
        } else {
          result.year = entry.year;
          result.month = entry.month;
          result.date = entry.date + 1;
        }
        result.days_ahead = entry.days_ahead + 1;
        result.weekday = (entry.weekday + 1) % 7;
        return result;
      }

我们应该展示多远的未来?

“最大”函数立即存储列表中存在的最大一次日历条目的日期,然后被修改为表示将要表示的最后一天,这是在找到最大一次日历条目后的一年(如果有重复的日历条目,可能会在最后一个一次性日历条目之后呈现一些实例):

      var greatest = this.new_entry();
      for(var index = 0; index < this.state.entries.length;
        ++index) {
        var entry = this.state.entries[index];
        if (!entry.hasOwnProperty('repeats') && entry.repeats) {
          if (compare(entry, greatest) === 1) {
            greatest = this.new_entry();
            greatest.year = entry.year;
            greatest.month = entry.month;
            greatest.date = entry.date;
          }
        }
      }

不同类型的条纹代表不同的条目类型

日历条目被分为一次性和重复条目,因此每天只检查可能的少数重复日历条目。一次性日历条目被放入一个哈希中,其键直接取自其日期:

      var once = {};
      var repeating = [];
      for(var index = 0; index < this.state.entries.length;
        ++index) {
        var entry = this.state.entries[index];
        if (entry.hasOwnProperty('repeats') && entry.repeats) {
          repeating.push(entry);
        } else {
          var key = (entry.date + '/' + entry.month + '/' +
            entry.year);
          if (once.hasOwnProperty(key)) {
            once[key].push(entry);
          } else {
            once[key] = [entry];
          }
        }
      }
      greatest.year += 1;
      var first_day = this.new_entry();
      first_day.days_ahead = 0;

现在我们准备好显示了!

这是前面提到的for循环;将compare()successor()提取到自己的变量中并使用描述性名称,使其更易读。对于每一天,循环编译一个(可能为空)列表,从该天的一次性活动开始,并检查所有重复的日历条目。对于重复的日历条目,它开始时accepts_this_datetrue,表示该日历条目确实发生在那一天,然后每个重复日期的标准都有累积的机会来表示他们正在检查的标准未达到,并否决该日历条目在那一天发生。如果一个重复的日历条目在没有任何否决的情况下通过了审查,它将被添加到该天显示的日历条目中:

         for(var day = first_day; compare(day, greatest)
        === -1; day = successor(day)) {
        var activities_today = [];
        if (once.hasOwnProperty(day.date + '/' + day.month + '/' +
          day.year)) {
          activities_today = activities_today.concat(
            once[day.date + '/' + day.month + '/' + day.year]);
        }
        for(var index = 0; index < repeating.length;
          ++index) {
          var entry = repeating[index];
          var accepts_this_date = true;
          if (entry.yearly) {
            if (!(day.date === entry.start.date &&
              day.month === entry.start.month)) {
              accepts_this_date = false;
            }
          }
          if (entry.date === day.date && entry.month ===
            day.month && entry.year === day.year) {
            entry.days_ahead = day.days_ahead;
          }
          if (entry.frequency === 'Every First') {
            if (!day.date < 8) {
              accepts_this_date = false;
            }

让我们友好地按顺序排列每一天

现在,所有日历条目,包括一次性和重复的,都已经为当天准备好了。我们从全天活动开始,按字母顺序排列,然后进行特定时间发生的日历条目,按时间升序排列:

          if (activities_today.length) {
            activities_logged_today = true;
            var comparator = function(first, second) {
              if (first.all_day && second.all_day) {
                if (first.description < second.description) {
                  return -1;
                } else if (first.description ===
                  second.description) {
                  return 0;
                } else {
                  return 1;
                }
              } else if (first.all_day && !second.all_day) {
                return -1;
              } else if (!first.all_day && second.all_day) {
                return 1;
              } else {
                if (first.hour < second.hour) {
                  return -1;
                } else if (first.hour > second.hour) {
                  return 1;
                } else if (first.hour === second.hour) {
                  if (first.minute < second.minute) {
                    return -1;
                  } else if (first.minute > second.minute) {
                    return -1;
                  } else {
                    if (first.hour < second.hour) {
                  return -1;
                } else if (first.hour > second.hour) {
                  return 1;
                } else if (first.hour === second.hour) {
                  if (first.minute < second.minute) {
                    return -1;
                  } else if (first.minute > second.minute) {
                    return -1;
                  }
                }
              }
            }
            activities_today.sort(comparator);

日期以人性化的方式显示;是“星期一”,而不是Mon

            if (activities_today.length)
              {
              var weekday = null;
              if (day.weekday === 0)
                {
                weekday = 'Sunday';
                }

让他们使用 Markdown!

活动的描述支持 Markdown。请注意——正如 Facebook 在dangerouslySetInnerHTML上的官方文档中指出的那样——我们默认信任 Showdown(提供我们的converter)是安全的。还存在旨在标记清理和消毒 HTML 的工具,以适合在此处安全显示 HTML 的 XSS-secure 显示方式。

我们去掉开放和关闭的P标签,这样描述将出现在该天有序列表给出的任何时间或其他信息的同一行上:

                if (activity.all_day) {
                  rendered_activities.push(<li
                    dangerouslySetInnerHTML={{__html:
                    converter.makeHtml(activity.description)
                    .replace('<p>', '').replace('</p>', '')}}
                    />);
                } else if (activity.minutes) {
                  rendered_activities.push(<li
                    dangerouslySetInnerHTML={{__html:
                    hour_options[activity.hours][1] + ':' +
                    minute_options[activity.minutes][1] + ' ' +
                    converter.makeHtml(activity.description)
                    .replace('<p>', '').replace('</p>', '')}}
                    />);
                } else {
                  rendered_activities.push(<li
                    dangerouslySetInnerHTML={{__html:
                    hour_options[activity.hours][1] + ' ' +
                    converter.makeHtml(activity.description)
                    .replace('<p>', '').replace('</p>', '')}}
                    />);
                }
              }
              result.push(<ul className="activities">
                {rendered_activities}</ul>);
            }
          }
        }
        if (entry_displayed) {
          result.push(<hr />);
        }
        return result;
      }
    });

一次只做一件事!

最后,在顶层的Pragmatometer类中,我们注释掉了Todo的显示,这样只有这个显示在我们工作时才会显示。接下来,我们注释掉Calendar组件,以便在草稿本上工作,完成后,最终集成将把这些元素放在屏幕的四个角落:

  var Pragmatometer = React.createClass({
    render: function() {
      return (
        <div className="Pragmatometer">
          <Calendar />
          {/* <Todo />
          <Scratch />
          <YouPick /> */}
        </div>
      );
    }
  });

启发这个日历的节日

在这里,您可以看到日历设置,并优雅地容纳了美国节日的所有节日列表:

启发这个日历的节日

每个国家都有自己的假期,并且并不是对其他国家和他们的假期表示不尊重,但我对美国的假期了解比其他国家更多,本章的方法在一定程度上是为了适应几乎所有主要假期。例外是复活节/复活节(前两天是耶稣受难日),根据一个非常特定的算法计算,但比我们在这个项目中涵盖的任何其他内容都要复杂得多,实际上,对于大多数天主教徒和新教徒,它有两种不同的算法,而对于大多数东正教徒,它有两种不同的算法。也许可以将其作为一个特例包括进来,但并不完全清楚如何创建一个通用解决方案,可以在不牺牲安全性的情况下容纳同样复杂的计算(最有希望的途径可能是允许在基于 Douglas Crockford 的 AdSafe 项目的沙箱中进行计算,这将允许在不需要牺牲整体页面安全性的情况下进行相当自由的计算)。

除了复活节和耶稣受难日,美国的主要官方假期列举如下:

  • 元旦(1 月 1 日,固定)

  • 马丁·路德·金纪念日(1 月的第三个星期一)

  • 总统日(2 月的第三个星期一)

  • 阵亡将士纪念日(5 月的最后一个星期一)

  • 独立日(7 月 4 日,固定)

  • 劳动节(9 月的第一个星期一)

  • 哥伦布日(10 月的第二个星期一)

  • 退伍军人节(11 月 11 日,固定)

  • 感恩节(11 月的第四个星期四)

  • 圣诞节(西方,12 月 25 日,固定)

  • 除了耶稣受难日和复活节外,美国的主要官方假期列举如下:

这个系统与作为灵感的私人日历类似,旨在(除其他目的外)足够强大,可以计算浮动和固定假期(遗憾的是,复活节/复活节有复杂的例外),此外,它提供了一个非常简单的界面,可以输入列表中的每个假期,以及更多。有了现代的日历系统,美国人不会在维基百科上查找假期,并手动输入感恩节是在 11 月的第四个星期一。他们包括一个列出假期的日历。然而,这个系统足够灵活,可以让任何国家的人以非常直接的方式输入这些假期或其他遵循预期模式的假期。

总结

这一章的目的是提供一个在 ReactJS 上构建的用户界面的稍微复杂的示例,具有非玩具功能。我们看到了渲染代码和后端类型的功能,这使得用户界面不仅仅是表面的。这种方法旨在与上一章互补,例如,指定其值的受控输入,而不是对查询表单进行近乎经典的 Hijaxing。

注意

从可用性的角度来看,处理重复日历条目的用户输入的最佳方式可能并不是直接调整和增强一个复杂且有些异构的表单,就像我们在这里所做的那样。我们在这里使用的高级重复事件是向导或面试方法的一个用例。

我们看了一个使用 ReactJS 的日历系统,解决了在现实世界中遇到的混乱问题。我们有一种复杂的渲染方法。就可用性而言,ReactJS 开发人员可能应该是最敏感的(因为他们是最负责与可用性相关的开发的人),对可用性进行了关注,并始终关注用户界面可能需要改进的意识。

在这个过程中,我们看了一些乏味的代码和乏味的普通 JavaScript 对象,当我们需要记录时,它们表现得非常出色。最后,我们看了我们的日历旨在强大到足以描绘其重复事件设施的特定国家的假期。

在下一章中,让我们一起看看如何将第三方(非 ReactJS)工具整合到一个页面中,并将各种应用程序的代码集成到一起。

第十一章:用实例演示 JavaScript 中的函数式响应式编程,第四部分 - 添加一个草稿本并将所有内容整合在一起

在本章中,我们将涵盖最后三个努力,旨在将所有内容整合在一起,完成我们的示例 ReactJS 应用程序。早期的章节涉及使用 100%ReactJS 制作的基本定制组件。本章不同之处在于制作有效的组件,该组件在使用 ReactJS 的同时利用了一个重要的非 ReactJS 工具。

制作了最后一个组件后,我们将把它们整合到一个页面中,其中四个组件中的每一个都放在页面的一个部分。这与开发不同,开发中我们将正在开发的工具放在整个页面下。这将是本章的第二个主要部分。

到目前为止,页面还没有跟踪状态的方法。假设您在日历中输入了一个条目,待办事项,或在草稿本中做了一些笔记。然后,如果您导航离开并返回或重新加载页面,所有更改都将丢失。有一个记住更改的方法会很好,这正是我们接下来要做的。在本章的第三个,也是最后一个主要部分中,我们将介绍一种便宜的、自制的基于 HTML5 localStorage 的持久性解决方案,它的效果出奇的好。它不允许您从多台计算机访问您的数据,但现在让我们把它放在一边,只在同一台计算机上进行持久性工作。

整体应用程序旨在处理个人信息管理/后勤:任何信息的草稿本,待办事项列表,日历,以及一个用于替换为您自己设计的有趣内容的抱怨人工智能的残留部分。

添加一个所见即所得的草稿本,感谢 CKeditor

这里有多个所见即所得(WYSIWYG)编辑器,而选择 CKeditor 并不是 CKeditor 是免费和付费编辑器的无可争议的选择。我们将看看如何要求 ReactJS 不要干涉 DOM 的一部分(在这种情况下,不要破坏我们的 CKeditor 实例)。我们将涵盖以下主题:

  • 为什么要使用像 CKeditor 这样的东西,它的工作方式与 ReactJS 不太相似?

  • 安装一个“小即美”的 CKeditor 版本,看看哪个版本最好

  • 在我们的页面中包含 CKeditor,重点是 JSX

将所有内容整合到一个网页中

我们几乎做完了所有的事情。我们将涵盖以下主题:

  • 调整 JSX,以便现在所有我们的功能都是未注释的。这是一个非常简单的步骤。

  • CSS 样式让一切都适合。我们将组件排列在 2x2 的网格中,但这可以被几乎任何适合在页面上放置组件的样式方法所替代。

  • 引入显示组件的基本数据持久性。这将包括一些基本的、非穷尽的 CSS 工作。

  • 为了提供一个完整的示例应用程序,我们一直在开发的面向用户界面的应用程序将在您的计算机上包含基本的持久性,本例中谦逊地使用 HTML5 localStorage 实现。这意味着一个计算机,无需登录或其他麻烦,将能够持久地使用数据。

  • 一些简单的JSON.stringify()调用可以为更常见的远程、基于服务器的持久性奠定基础。数据通过JSON.stringify()进行字符串化,这在 localStorage 中并不是特别需要,但使代码稍微更容易替换掉 localStorage 引用,并将其替换为潜在的远程服务器。

  • 使 CKeditor 状态持久化。一些有经验的程序员,在被要求为组件状态创建一个 localStorage 持久性解决方案时,可能会合理地猜测我们的解决方案,除了草稿本。草稿本对 Web 2.0 工作有一些难点,因为 CKeditor 对 Web 2.0 工作有一些难点。

整个系统一起运行可以在demo.pragmatometer.com/上看到。

这本书是关于 ReactJS 的,那为什么要使用 CKeditor?

一般来说,可以建议最好使用符合 ReactJS 声明性精神和单向数据绑定的东西。如果您可以选择一个像 CKeditor 这样的东西的良好实现,它并不特别与 ReactJS 以类似的方式工作,以及其他一些与 ReactJS 很好地融合并很好地处理所见即所得的组件,您应该选择与 ReactJS 很好地融合的组件。

这本书旨在帮助您在道路的叉口两侧。它包括使用 JSX 和不使用 JSX 的开发,传统和全新的开发,单向和双向数据绑定,以及(在这里)纯 ReactJS 组件与集成非 ReactJS JavaScript 工具。好消息是,ReactJS 擅长与其他工具友好相处。来自 JavaScript 世界各地的工具至少可能都可以为您提供帮助,而不仅仅是专门为 ReactJS 工作而开发的一小部分。也许您有幸使用纯 ReactJS 组件。也许您想要、需要或者不得不使用一些没有考虑到任何 ReactJS 集成的 JavaScript 工具。好消息是:在任何一种情况下,ReactJS 可能都已经覆盖了。在本章中,我们将使用标准的非 ReactJS 工具-著名且成熟的 CKeditor,ReactJS 让我们很好地将其集成到我们的网页中。

CKeditor-小而美的免费提供

有几种免费和商业编辑器可用;其中一个编辑器是 CKeditor(其主页位于ckeditor.com/)。CKeditor 带有四个基本选项:BasicStandardFull,以及一个Custom选项,允许完全自由地选择和取消选择可选功能。对于这个项目,我们将使用Basic选项。这不是为了让用户呈现一大堆按钮行的服务,关于包括哪些功能的正确问题是,“什么是对我们来说能够很好地工作的最低限度?”

在我们的页面中包含 CKeditor

Basic选项(以及 Standard、Full 和 Custom 选项数组)可通过下载或从 CDN 获取。在撰写本书时,可以通过以下方式从 CDN 获取 Basic 选项:

<script src="img/ckeditor.js"></script>

这应该是我们的 HTML。我们还需要处理 JSX。用于设置草稿的代码是我们四个子组件中最简单和最短的:

  var Scratchpad = React.createClass({
    render: function() {
      return (
        <div id="Scratchpad">
          <h1>Scratchpad</h1>
          <textarea name="scratchpad"
            id="scratchpad"></textarea>
        </div>
      );
    },
    shouldComponentUpdate: function() {
      return false;
    }
  });

render()方法就像它看起来的那样简单。请注意,它定义了一个TEXTAREA而不是 CKeditor 小部件。不同版本的 CKeditor 通过劫持特定的TEXTAREA而不是在代码中编写他们的小部件来工作。shouldComponentUpdate()方法也和它看起来的一样简单,但值得一提。这个方法旨在促进优化,以解决 ReactJS 虚拟 DOM 差异检查不如您所能做的那么快的罕见情况。例如,在 ClojureScript 下,Om 具有不可变的数据结构,因此可以仅通过引用比较来测试相等性,而无需进行深度相等性检查,这就是为什么 Om 加 ClojureScript 的速度大约是 ReactJS 加 JavaScript 的两倍。正如前几章所述,99%的时间,微观管理 ReactJS 的虚拟 DOM 根本不需要,即使您想要非常高效。

然而,在这里,我们对 shouldComponentUpdate()机制有一个单独的用例。它在这里的使用与优化和通过较少的比较获得相同结果无关。相反,它用于否认 DOM 的部分权限。对于您可能想要包括的其他一些工具,比如 CKeditor,希望 ReactJS 创建 DOM 的一部分,然后让它保持不变,而不是在以后破坏另一个工具的更改;这正是我们在这里所做的。因此,shouldComponentUpdate() - 除了构成一个在闪电般快速的虚拟 DOM 差异比较中修剪不必要比较的机制之外 - 还可以用于附加一个标签,表示“除了 ReactJS 之外的东西负责维护 DOM 的这一部分。请不要破坏它。”

在首次渲染 Web 应用程序之后,我们要求 CKeditor 替换具有 ID 为 scratchpad 的 TEXTAREA,这应该给我们一个实时小部件:

  React.render(<Pragmatometer />,
    document.getElementById('main'));
  CKEDITOR.replace('scratchpad');
We temporarily comment out the other subcomponents:
  var Pragmatometer = React.createClass({
    render: function() {
      return (
        <div className="Pragmatometer">
          {/* <Calendar /> */}
          {/* <Todo /> */}
          <Scratchpad />
          {/* <YouPick /> */}
        </div>
      );
    }
  });

现在我们有一个交互式的便签。以下是我们的 Web 应用程序的屏幕截图,仅显示便签:

在我们的页面中包含 CKeditor

将所有四个子组件整合到一个页面中

已经创建了四个子组件 - 日历、便签、待办事项列表和一个带有占位符的“你选择”槽 - 现在我们将把它们整合起来。

我们首先取消注释 Pragmatometer 的 render()方法中的所有注释子组件:

        <div className="Pragmatometer">
          <Calendar />
          <Todo />
          <Scratchpad />
          <YouPick />
        </div>

我们的下一步是添加样式,只需一点响应式设计。响应式设计中的一个主要竞争者是简单地不尝试了解和解决每个屏幕分辨率,而是根据屏幕宽度有几个响应步骤。例如,如果您有一个宽屏桌面监视器,加载therussianshop.com/,然后逐渐缩小浏览器窗口。不同的适应性会启动,并且在桌面宽度、平板电脑的任何方向或智能手机上查看时,整个页面会形成一个整体。我们不会在这里尝试一个严肃的解决方案,但是有一些响应性,因为我们的样式是有条件地适应最小宽度为 513 像素。没有任何样式,四个元素将显示在彼此上方;有样式,它们将被围成一个 2x2 的网格。

样式化子组件的 CSS 基本上将足够大的窗口分成四分之一,添加一些填充,并确保每个应用程序上的任何溢出都会滚动:

      @media only screen and (min-width: 513px) {
        #Calendar {
          height: 46%;
          left: 2%;
          overflow-y: auto;
          position: absolute;
          top: 2%;
          width: 46%;
        }
        #Scratchpad {
          height: 46%;
          left: 2%;
          overflow-y: auto;
          position: absolute;
          top: 52%;
          width: 46%;
        }
        #Todo {
          height: 46%;
          left: 52%;
          overflow-y: auto;
          position: absolute;
          top: 0;
          width: 46%;
        }
        #YouPick {
          height: 50%;
          left: 52%;
          overflow-y: auto;
          position: absolute;
          top: 52%;
          width: 46%;
        }
      }

这使我们能够显示以下是我们的 Web 应用程序所有部分的屏幕截图:

将所有四个子组件整合到一个页面中

持久性

有些框架是通用框架,旨在做任何事情;ReactJS 不是。它甚至没有提供任何方法来进行 AJAX 调用,即使(实际上)使用 ReactJS 的任何重要项目都将具有 AJAX 要求。这完全是有意设计的。原因是 ReactJS 专门作为用于工作在用户界面或制作视图的框架,并且旨在与其他技术结合使用,以制作适合您网站的完整包。

Pragmatometer 应用程序中一个希望的功能是它记住您输入的数据。如果您在明天下午 2 点有一个约会,然后离开页面再回来,页面记住约会而不是每次加载时呈现完全空白。持久性是完整 Web 应用程序的一部分,但不是视图或用户界面的责任,ReactJS 显然也没有提供持久性的解决方案。也许也不应该。最近的一些章节介绍了如何使用 ReactJS 来做“X”;本章是关于如何做一些与 ReactJS 相辅相成的其他事情。

对于主流用途,持久性通常通过与后端的通信来处理;有几种好的技术可用。但也许试图将正确实现后端的处理塞进 ReactJS 前端开发书籍的一个章节中并不是非常有用。

作为一个仍然完全属于前端领域的练习,我们将通过一个众所周知的前端路由来处理持久性——HTML5 的 localStorage(如果 Modernizr 未能检测到 localStorage,则持久性代码不起作用)。使用的函数save()restore(),如果找到 localStorage,则保存在 localStorage 中。它们直接调用JSON.stringify()JSON.parse(),即使这一步并不严格需要使 JSON 可序列化的对象在 localStorage 中持久化。这旨在提供一个直接的钩子来改变代码以与远程后端通信。最简单的适应方式,就像这里的实现一样,是为应用程序单体保存和恢复整个状态,但要记住,过早优化仍然是万恶之源。以这种方式大量使用应用程序可能会导致与单个大型 PNG 文件相当的状态量。当然,该代码还可以进一步适应更精细的方法来保存或恢复更轻的差异,但这里的重点是奠定坚实的基础,而不是将优化推到极致。

我们将使用 Crockford 的 JSON github.com/douglascrockford/JSON-js/blob/master/json2.js 和 Modernizr modernizr.com/。在这个应用程序中,我们只会使用 Modernizr 来测试 localStorage 的可用性,因此,如果你正在寻找一个“对于这个项目足够轻量级的最小 Modernizr 构建”,选择测试 localStorage 并排除其他所有内容。让我们在index.html中包含这些文件:

<script src="img/json2.js"></script>
<script src="img/modernizr.js"></script>

在我们的site.jsx文件中,我们定义了save()restore()函数。这些函数将用于使不同应用程序的整个状态持久化。另一种方法可能是进行更多和更小的保存,而不是少量的单体保存,但少量的单体保存更容易在脑海中跟踪。因此,它们比为数据的次要方面进行不同保存更容易维护和调试(如果以后需要优化,我们可以,但过早优化仍然是万恶之源)。save()函数如下所示:

  var save = function(key, data) {
    if (Modernizr.localstorage) {
      localStorage[key] = JSON.stringify(data);
    }
  }

将这个与远程后端连接的最明显的方法之一,除了处理诸如帐户管理之类的细节(这在本示例中没有涉及),是将localStorage[key]的赋值替换为调用通知服务器与该键相关的新字符串化数据。这将使 Modernizr 检查变得不必要。但要警告:即使 IE8 支持 localStorage,不支持它的客户端可能有点过时,可能不受 ReactJS 支持,因为 ReactJS 并不宣传支持早于 IE8 的版本(此外,IE8 支持现在是基于一个 shim 而不是本地的;参见tinyurl.com/reactjs-ie8-shim)。

restore()函数除了键之外还接受一个可选参数——default_value。这用于支持一个初始化,如果存在保存的状态,则会拉取它,否则会回退到在初始化时将要使用的正常值。初始化代码可以被重用以适应这个restore()函数,如果存在保存的数据,它会拉取非空和已定义的数据,否则会使用默认值。带有JSON.parse()和探测 localStorage 的if语句是你最直接用来调用远程后端的行,或者更进一步,restore()函数可能会被彻底清除并替换为具有相同签名和语义的函数,但会与拥有更多工作的远程服务器进行通信,检查是否保存了任何现有数据。这可能会导致客户端在服务器没有返回任何内容时返回默认值:

  var restore = function(key, default_value) {
    if (Modernizr.localstorage) {
      if (localStorage[key] === null || localStorage[key]
        === undefined) {
        return default_value;
      } else {
        return JSON.parse(localStorage[key]);
      }
    } else {
      return default_value;
    }
  }

现在,所有的getInitialState()函数都被修改为通过restore()函数。看看接下来会发生什么。考虑一下这段代码的Todo初始化器:

      getInitialState: function() {
        return {
          'items': [],
          'text': ''
        };
      },

它只是包裹在一个restore()的调用中:

      getInitialState: function() {
        return restore('Todo', {
          'items': [],
          'text': ''
        });
      },

有一些函数会改变一个组件或另一个组件的状态,我们让任何改变组件状态的函数都保存整个状态的一部分。因此,在名为Calendar#handle_submit的适当命名的函数中,this.state.entry_being_added的许多细节都被填充以匹配(Hijaxed)表单上的内容。然后填充的条目被添加到实时填充的条目列表中,并且新的条目被放在它的位置上:

      this.state.entries.push(this.state.entry_being_added);
      this.state.entry_being_added = this.new_entry();

这两行改变了this.state,所以我们在它们之后保存了状态:

      this.state.entries.push(this.state.entry_being_added);
      this.state.entry_being_added = this.new_entry();
      save('Calendar', this.state);

一个细节——持久化 CKeditor 状态

这一部分大部分是可以预测的。一些程序员被告知我们通过 HTML5 localStorage 添加了持久性,可能已经猜到了之前写的东西,很可能他们离答案很近。然而,有一个关于 CKeditor 的细节,不太明显,也不太理想。

CKeditor 在“非花哨”的 Web 1.0 表单使用下做了你可能天真地期望的事情。如果你有一个表单,包括一个名为fooTEXTAREA,调用 CKeditor 进行转换,然后提交表单。表单将被提交,就好像当时在 CKeditor 实例上的 HTML 是TEXTAREA的内容一样。所有这些都是应该的。

然而,如果你以几乎任何“AJAXian”方式使用 CKeditor,查询文本区域的值而不进行完整的页面表单提交,你将遇到问题。CKeditor 实例的报告值既不多也不少,就是它初始化的文本。原因是TEXTAREA的值在整个页面表单提交时会被同步,但在中间步骤不会自动完成。这意味着,除非你采取额外的步骤,否则无法有用地查询 CKeditor 实例。

幸运的是,这个额外的步骤并不特别困难或棘手;CKeditor 提供了一个 API 来同步TEXTAREA,所以你可以查询TEXTAREA来获取 CKeditor 实例的值。在连接 CKeditor 草稿之前,我们初始化了整个显示并设置了一个间隔,以便每 100 毫秒更新一次显示(这个间隔的长度并不是必要的或神奇的;它可以更频繁或更少地更新,较长的间隔会更加不连贯,但基本上是一样的):

  var update = function() {
    React.render(<Pragmatometer />,
      document.getElementById('main'));
  };
  update();
  var update_interval = setInterval(update,
    100);

为了适应 CKeditor,我们稍微调整和解开一些东西。我们的代码将会有点混乱,以便按特定顺序调用事物。为了让我们的TEXTAREA首先存在,我们需要渲染 Pragmatometer 主组件一次(或多次,如果我们想要)。然后,在那个调用之后,我们要求 CKeditor 转换TEXTAREA

接下来,我们开始一个更新函数。这既更新了显示,也同步了 CKeditor 的TEXTAREAs,以便可以查询它们的位置。同步TEXTAREA的循环并不是绝对必要的。如果我们只有一个编辑器实例,我们只需要一行代码,但我们的代码对于任意数量的具有任意 ID 的 CKeditor 实例都是通用的。最后,在循环内,我们调用编辑器内容的save()。一个优化是,如果save()restore()被清空并替换为与后端服务器通信,那么就可以将当前编辑器状态保存在一个变量中,只有在编辑器的内容与先前保存的值不同时才进行save()。这应该减少频繁的网络通信:

  React.render(<Pragmatometer />,
    document.getElementById('main'));
  CKEDITOR.replace('scratchpad');
  var update = function() {
    React.render(<Pragmatometer />,
      document.getElementById('main'));
    for(var instance in CKEDITOR.instances) {
      CKEDITOR.instances[instance].updateElement();
    }
    save('Scratchpad', 
      document.getElementById('scratchpad').value);
  };
  var update_interval = setInterval(update,
    100);

还有一些更改,使得所有的初始化都包裹在对restore()的调用中。此外,每当我们改变一个组件的状态时,我们都会调用save()。然后我们就完成了!

总结

在本章中,我们添加了第四个组件。它与其他组件不同之处在于它不是从头开始在 ReactJS 中构建的,而是集成了第三方工具。这样做可能足够好;只要小心地编写一个shouldComponentUpdate()方法,返回false,作为一种方式来表明,“不要破坏这个;让其他软件在这里完成它的工作。”

尽管我们涵盖了三个基本主题——组件、集成和持久性,但这一章比其他一些章节更容易。我们有一个实时、可工作的系统,你可以在demo.pragmatometer.com/上看到它。

现在让我们退一步,来看一下结论,讨论你在本书学到了什么。

第十二章:一切如何契合

谷歌地图在推出时非常受欢迎,它仍然非常重要,但它引入的新功能几乎没有什么。谷歌在地图网站上的贡献是将以前只能通过陡峭的学习曲线获得的东西变得简单易用。这已经是相当了不起的了。

关于 ReactJS 也可以说类似的事情。Facebook 没有发明函数式响应式编程。Facebook 似乎也没有显著扩展函数式响应式编程。但是 ReactJS 显著降低了门槛。以前,对于函数式响应式编程,经验丰富的 C++程序员经常会说:“我想我可能只是愚蠢,或者至少,我没有计算数学博士学位。”也许可以说精通 C++并不是一件小事;在 Python 中让某些东西工作比在 C++中让相同的东西工作要容易得多,就像在当地公园的冬季滑雪山坡上滑雪比攀登珠穆朗玛峰要容易得多一样。此外,ReactJS 引入了足够的变化,以至于没有任何数学、计算或其他方面的学位的合格 C++程序员有很大机会使用 ReactJS 并且在其中高效工作。也许他们可能没有纯 JavaScript 程序员对函数式编程特别感兴趣的那么有效。但是学会有效地编程 C++是一个真正的成就,大多数优秀的 C++程序员有很大机会有用地实现 ReactJS 中的函数式响应式编程。然而,对于阅读维基百科上的计算机数学论文并在学术作者通常偏爱的 Haskell 语言中实现某些东西,就不能说同样的话了。

在这个结论中,我们将探讨本章中的以下主题:

  • 回顾所涵盖的领域

  • 免疫于引发《人月神话》的问题。

  • ReactJS 只是一个视图,但是是一个很棒的视图!

  • 编程 ReactJS 的乐趣

  • ReactJS 开启了全新的视野,超越了网络。这里介绍的 ReactJS 工作并不是 ReactJS 可能性的终点:它只是开始

回顾所涵盖的领域

在这本书中,我们在理论和实践上都涵盖了很多内容。我们涵盖了函数式编程、响应式编程和函数式响应式编程的基础知识。我们还介绍了 Facebook 的 ReactJS 技术。它使一些函数式响应式编程的优势可以用于前端开发人员,而这些开发人员不一定精通计算数学(不幸的是,这在本文中是一个显著的特点)。这里的文本旨在跟随 ReactJS 的步伐,特别是为了让没有特殊数学背景的程序员能够理解。在这个过程中,我们遇到了一些有趣的技术,比如 Om、Brython 和 Jest,并且看了一下未来前端 Web 开发可能会是什么样子。我们可能可以使用我们选择的语言进行 Web 开发,而不一定局限于 JavaScript。

我们还构建了两个系统,一个较小,一个较大,并尝试演示解决问题的轻微变化:使用或不使用 JSX,对表单元素使用受控值,以及通过经典的表单 Hijaxing。重点不完全在于哪一个比另一个更好,因为需求会需要不同的解决方案,我们希望至少覆盖的方法中的一种在特定情况下是有帮助的。

在这个过程中,有理由说,就像有人说关于 Python 一样,“编程再次变得有趣!”每个系统都有其怪癖,但不知何故,使用 ReactJS 时似乎在道路上遇到的障碍要少一些。本书对 CKeditor 的简要介绍必然包括了一个解决方案,以解决首次使用 CKeditor 的用户可能遇到的障碍。关于 ReactJS 代码中持续存在的问题的解决方案几乎没有必要的警告。

神话般的程序员月份是否可以避免?

弗雷德·布鲁克斯于 1975 年出版的《神话般的程序员月份》(在您阅读本书时已经超过 40 年)是软件工程文献中引用最多的作品。塔南鲍姆的经典教科书《操作系统:设计与实现》提到了布鲁克斯的书名:

“OS/360 的设计者之一弗雷德·布鲁克斯写了一本风趣而深刻的书(布鲁克斯,1975 年),描述了他在 OS/360 项目中的经历。虽然在这里总结这本书是不可能的,但可以说封面上显示了一群史前动物陷入了沥青坑……”

在这里,有直接的相关性。为了解释这一点,让我们制作史蒂夫·卢施尔引入的 Big-Coffee 符号的变体。也许除了卢施尔本人之外,没有人知道是什么启发他以他的方式表达观点,但卢施尔显然熟悉运行时复杂性的经典大 O 符号,可能也知道它也用于评估其他资源使用方面的复杂性,比如内存。但我可能会建议,组织沟通复杂性可能需要额外的启发,就像 Big Organization 复杂性所解释的那样。如果 Big-Coffee 复杂性可能是夜梦般的二次方,或者正如卢施尔所写的那样,那么在单片项目内部的沟通复杂性中会出现一些令人不安的熟悉现象。

如果一个单片项目上有一个程序员,那么复杂性为零,因为不需要避免踩到其他程序员的脚。如果有两个程序员,那么沟通的复杂性就是一个连接。如果有三个程序员,就有三个连接;如果扩展到 10 个程序员,文件工作量就会扩大到 45 个连接。IBM 对 OS/360 项目的方法是所谓的大蓝色解决方案,它说:“因为我们想要完成很多工作,所以让我们雇用很多很多程序员!”IBM 拥有超过 10 名程序员,因此连接数量远远超过 45 个。

一个可能适合表示组织沟通复杂性的字符是互连的 HTML dingbat,可编码为&#9784;&#x2638;

神话般的程序员月份是否可以避免?

如果我们能够将“大组织”复杂性的沟通需求量化,以防止程序员破坏其他人的工作,也许没有任何特殊符号是完美的。但我们可以说,单片软件项目具有二次沟通复杂性——神话般的程序员月份是否可以避免?,或者如果您愿意的话,神话般的程序员月份是否可以避免?——开发人员需要跟上其他变化,并部分避免与其他开发人员的工作发生冲突。在 OS/360 项目的规模上,这导致开发人员花费超过一半的时间只是为了跟上备忘录,以了解其他程序员所做的工作。

有理由相信,如果 OS/360 项目采用了 Facebook 用于 ReactJS 加 Flux 的方法,可能就不需要写《神话般的程序员月份》了。

ReactJS 和 Flux 的组合明确写成,这样你就不需要在每个组件的口袋里动手脚。事实上,它是这样写的,如果每个人都遵循这种方法,你就不能在其他组件的口袋里动手脚,除非你找到一种突破安全的方法。通信复杂度不是二次的(就像 OS/360 项目中那样),如果每个组件最多只有一个开发人员,通信方向的数量要小得多,可能仅略高于线性。这种差异在其影响方面是巨大的。

尽管 Facebook 是否坚持纯粹主义以实现理论上可能的最佳结果尚不清楚,但似乎显而易见的是,Facebook——这是互联网上最大的组织之一,可能拥有与 OS/360 项目规模相当或更大的前端开发人员——其通信比单一的 OS/360 努力要好得多。也许 Facebook 是众多更愿意宣传自己的优势而不是弱点的组织之一。但我在网上找到的任何资源都没有表明 Facebook 开发人员之间的沟通量超出了控制,就像 OS/360 项目中那样,或者必要的内部沟通量足够多到足以成为使开发人员的生活变得真正困难的问题。

ReactJS 只是一个视图,但是多么美丽的视图!

塞尚曾经说过:“莫奈只是一只眼睛,但是多么美丽的眼睛!”莫奈并没有试图炫耀他对结构和解剖学的知识,而是只是复制他的眼睛所看到的。对他的作品的共识判断坚持着“只是一只眼睛”和“多么美丽的眼睛!”事实上,在莫奈的作品中,细节可能不太清晰,他反对试图用深奥的解剖学知识和远远超出眼睛所看到的结构的知识来给人留下深刻印象的艺术。

ReactJS 是一个框架,而不是一个库,这意味着你应该在 ReactJS 提供的结构内构建解决方案,而不是将 ReactJS 插入你自己设计的解决方案中。库的典型例子是 jQuery,你可以按照自己的方式构建解决方案,并在需要时调用 jQuery。

然而,ReactJS 专门用作视图。这并不一定是好事或坏事,但 ReactJS 并不是一个完整的 Web 开发框架,也没有成为你永远需要的唯一工具的意图。它专注于成为一个视图,在 Facebook 的提供中,这并不包括任何形式的 AJAX 调用。这并不是在开发 ReactJS 时犯下的重大疏忽;预期是你使用 ReactJS 作为视图来提供用户界面功能,并使用其他工具来满足其他需要。本文没有涵盖使用 ReactJS 与你喜欢的工具一起使用,但如果它们不会互相冲突,就将你喜欢的工具与 ReactJS 结合使用。ReactJS 可能会与其他视图发生冲突,但它的目的是与非视图技术一起工作。

编程又变得有趣起来了!

当互联网首次出现时,我有了第一次“编程又变得有趣了!”的体验。我在 Unix 和 C 编程中找到了我的方向,当我得知在网页中包含图像是可能的时,我预计在类似 C 的环境中从头开始告诉如何显示图像需要多少工作。我内心认为,“对我来说太多工作了。” 但我惊讶地发现,一个图像可以在网页中包含,只需要<IMG SRC=Portrait.GIF>,图像本身不需要嵌入在网页中;它同样可以优雅地作为<A HREF=Portrait.GIF>点击这里!</A>提供。这是我第一次接触到一种声明性而不是命令式的语言。也许它严格来说不是编程;当然,在 JavaScript 出现之前,它不是图灵近似。然而,它让我轻松地做了一些我以前无法想象的电脑工作。

几年后,我第二次体验到“编程又变得有趣了!”是在一个朋友建议我尝试 Python 之后。那时,我已经成为了一名语言收藏家;我想要了解的唯一语言是 Icon、C++和一些汇编语言。语言收藏家的一个普遍现实是,他们在新语言中的第一个项目比任何后续工作都要慢、更困难、更令人沮丧。之后会好一些,但对于第一个项目来说,“它总是比你想象的时间长,即使你考虑到了它总是比你想象的时间长这个事实。”然而,用 Python 时,我惊讶地发现,“什么?它已经在运行了?”这只是冰山一角。

我作为一名语言收藏家,发现 Python,然后停止学习新语言的经历并不是 Pythoneers 中特别不寻常的故事。埃里克·雷蒙德在他的文章《为什么选择 Python?》中提到了一些更深层次的东西,网址是www.linuxjournal.com/article/3882。Python 是一个充满魔力的王国,街道都是用胶水铺成的,不仅仅是大师们才能受益。

之前没有提到的是,如果你将鼠标悬停在xkcd.com/353/的卡通图片上,会出现这样的消息:昨天我用 Python 写了 20 个简短的程序。太棒了。Perl,我要离开你了…… 现在 Perl 也是一种很好的语言,曾经是我的最爱,但 Python 仍然有着独特的魅力。

最后,我最后一次也是最伟大的“编程又变得有趣了!”时刻是当我开始欣赏 ReactJS 时。在创建可以像标签一样使用的有用组件方面,ReactJS 提供了 XHTML 和 HTML5 所没有的东西。

无论 XHTML 中的“X”代表什么,它并不意味着“在主流使用中,人们会构建和部署大量有趣的新标签”。HTML5 提供了许多新组件,比如<input type="date" />,但它们并不被普遍支持,这并不是 IE 必须成为派对的灵魂的另一个案例。主流和当前的非微软浏览器对 HTML5 在聚光灯下宣布的功能的覆盖范围非常不一致。有一些 polyfills 可用,并且整合在 HTML5 之前存在的许多 JavaScript 日期选择器可能在今天和它们刚出现时一样有意义。但是,像html5please.com/这样的网站值得赞扬和使用,但它也是一个主要问题的症状。

ReactJS 和 JSX 成功地解决了这些问题。本文没有涵盖如何制作<DatePicker />函数,但一旦制作完成,你可以将它几乎像原生 HTML 标签一样包含在你的 JSX 中。如果有人对分形感到怀旧,绘制在 HTML5 画布上并制作可滚动和可缩放的<LogisticMap /><VonKochSnowflake /><MandelbrotSet /><SierpinskiGasket />,这些可以像普通的简单的<img />标签一样容易地包含在 JSX 中。在 ReactJS 中定义的组件在一个重要的意义上不同于手动配置和连接 JavaScript 日期选择器以使其与你的表单一起工作。它们就像经典结构化编程中的子程序,可以在有意义的地方方便地重复使用,并组合成更大的构建块。

提示

可能会有人建议,制作一些有用组件的库,这些组件可以用来扩展其他网页开发人员可以使用的基本有用标签集。

此外,如果我可以借用 Robin Martin 的“什么杀死了 Smalltalk 可能会杀死 Ruby”并使用更礼貌的语言,代码审查中的关键指标(以及其他指标)是审查人员不得不问“他们在想什么?”的次数。对于正在审查的代码来说,“他们在想什么?”的指标,得分为 0 是可以接受的。任何高于此分数的都是不可接受的。此外,这个指标在代码审查之外也是相关的。

在 Python 中,这样的时刻是罕见的:它们确实存在,搜索“Python 可变默认参数”将显示出来,但它们之所以重要是因为它们罕见。这与 JavaScript 中的“他们在想什么?”的争论不同,比如“你可以使用未声明的变量(但如果你这样做,它们将是全局的)”和“你可以编写伪经典构造函数(但如果你在调用它们时忘记使用 new 关键字,它们将在全局命名空间中破坏东西)”。JavaScript 的环境是这样的,以至于像 Douglas Crockford 这样的关键语言倡导者,严厉警告人们远离基本语言的大部分内容,并且似乎随着时间的推移变得更加挑剔。

最终,ReactJS 和 Python 似乎有着相同的核心。两者都是本质上小而简单的。也许两者都有缺陷,但缺陷是“他们在想什么?”的时刻是个例外而不是常态。正如在 ReactJS 宣布时所说的那样,有一条讽刺的推文说:“Facebook:重新思考已经确立的最佳实践。”ESR 对 Python 奇怪的选择使用显著的空格表示有些困扰。

“就像大多数黑客一样,当意识到这一事实时,我本能地感到厌恶。”

我当时还不够老,只是在 20 世纪 70 年代的几个月里编写了一些批处理 Fortran 程序。如今,大多数黑客都不是,但不知何故,我们的文化似乎保留了对那些旧式固定字段语言有多么讨厌的相当准确的民间记忆。事实上,当时用来描述 Pascal 和 C 中较新的基于标记的语法的术语“自由格式”几乎已经被遗忘。所有语言现在都已经设计成这样了,或者几乎都是;无论如何。看到这个 Python 特性,很难责怪任何人最初的反应好像他们意外地踩到了一堆恐龙粪便。”

ReactJS 也有勇气说,那些创建 CSS 的人可以创建非常简单的 JavaScript,而不仅仅是在故意设计不足的模板语言中工作。现在,JavaScript 被选择为一种特定领域的语言,以故意留下尽可能多的功能。但设计师并不需要召唤 JavaScript 的全部功能。他们可以创建 99%的简单 JavaScript,这在故意设计不足的模板语言中已经完成了,而 JavaScript 开发人员可以创建剩下的 1%的强大 JavaScript,因为在故意设计不足的模板语言中解决这个问题会很棘手。

总结

在这一章中,我们看了一些比较高层次的东西。其他章节详细介绍了一些项目,但在这里,我们看了一些 ReactJS 代表的主要优势,以及一些计算机领域中最著名的问题。

这本书旨在涵盖 Facebook 的 ReactJS 的功能性响应式编程的理论和实践。这并不是第一本涵盖功能性编程、响应式编程或功能性响应式编程的书,但它可能是功能性响应式编程的早期著作之一,不假设博士级别的数学能力。其中一部分是通过使文本有些哲学性来实现的。在某种意义上,这是为了让一些资深程序员更容易理解,但对大多数资深程序员来说并不是不可能理解的。JavaScript 和 ReactJS 中最好的功能性响应式编程基于功能性编程的熟练度,而 Haskell 中最好的功能性响应式编程也是基于功能性编程的熟练度;在这方面没有真正的区别。然而,一个典型的资深 C++程序员有很大机会在 ReactJS 中获得有用的熟练度。Facebook 在让事情更容易接触方面做得相当出色。

JavaScript 是一种多才多艺的语言,如果你以 Scheme 方式(当然!)或 Python 方式、C#、Erlang、Perl、Ruby、Java、Haskell、PHP、Lisp 或 Visual Basic 的方式来思考,你可以获得相当大的生产力。也许没有其他编程语言的思维方式会达到纯粹、功能驱动的 JavaScript 思维的最高层次,但在 JavaScript 中你可以表达很多东西,而不需要成为一个母语为 JavaScript 的人,拥有完美的 JavaScript 口音!

在 ReactJS 中没有失去任何东西。也许在功能性响应式编程中,除非你在某些非常特定的数学领域有很高的熟练度,否则最后一丝力量无法被挤出来,但 ReactJS 显著降低了功能性响应式编程的门槛。功能性响应式编程过去在门口上有一个无声的标志,上面写着“只有数学编程高手才能进入”。现在没有了。掌握功能性响应式编程可能纯粹是为了在 ReactJS 中工作有优势,但所有其他提到的专业领域都可以在 ReactJS 中获得很多好处,而不需要了解太多数学,只需要了解通常嵌入在计算机科学和信息技术中的重要熟练度。

有人说开发人员很少为书籍付费;他们为章节付费。这本书旨在作为一个整体运作,不同的部分展示了互补的方法,以便每个部分都为整体增添内容。但它也严肃地旨在提供可以作为独立资产完美运作的章节,以便那些想要对某些东西进行定位的人使用。

从这里开始的下一步

你可以探索无数的方向。你可以深入挖掘并探索 ReactJS 的核心。你也可以探索将 ReactJS 集成到使用其他技术解决其他问题的项目中。

然后,你可以用 Lisp 或 Python 来编写 ReactJS。(不仅仅是真实的,如果你来自“Lispy”或“Pythonic”的背景,你才能用 JavaScript 编写 ReactJS。你可以使用 Lisp 或 Python 创建动画网页,而不需要离开 Lisp 或 Python 来编写一行 JavaScript 代码。)

你可以创建比 HTML5 的任何版本都更丰富的组件,并且可以像 HTML 1.0 提供的组件一样轻松地使用它们。

也许最令人兴奋的可能性是,ReactJS 不再仅适用于 HTML/Web。它现在为“一次学习,随处编写”提供了一个杀手级应用程序。现在你出色的 JavaScript 技能和学习函数式编程的努力不仅可以在 Web 上使用,而且可以轻松地为 iOS 编写。现在请查看facebook.github.io/react-native/的主页。这本书让你不仅可以在 Web 上使用 ReactJS,还可以快速而且很好地学习 ReactJS Native,这是非常重要的。

你已经爬到了跳板的顶端。现在是时候跳下去,制造最大的水花了。

也许关于如何使用函数式响应式编程和 ReactJS 的最好方法是根本不要使用ReactJS。而是ReactJS,就像你玩新鲜的雪一样。关于格拉斯哥哈斯克尔编译器所说的话完全适用于 ReactJS:忘记它是你用来工作的东西。像一个年幼的孩子一样玩它。看看你能建造什么,以及你不能建造什么。

编程又是新的;有了魔法。就像程序员曾经获得了使用与内置函数同等的程序员贡献的子程序的能力一样,现在前端 Web 开发人员已经获得了使用开发人员制作的组件的能力,就像在他们的 JSX 中包含IMG标签一样容易且没有麻烦。过去我们需要去html5please.com/学习<input type="date">有一个“警告[甚至]使用 polyfill 时要小心”的琥珀警告标签。过去你甚至需要手动在每个页面上连接 JavaScript 日期选择器,并且你可能需要使用 58 行重复性压力诱发代码来在一个页面上获取日期选择器。一旦有人制作了一个合适的 ReactJS <DatePicker />函数,问题就解决了,只需要不到一行的代码就可以包含它,你可以在页面上包含零次、一次或多次。"即使。在。使用。Shim。到 Internet Exploder 8"是有意使用标点符号,正如特里·普拉切特在巨魔的讲话中所用的那样,并且是强调的。至于“Internet Exploder”这个称号已经存在很长时间,开发人员,或者至少是我,在实现一个可以在任何正常浏览器上工作的解决方案时遇到了两个问题,然后再次让事情运行起来,这次是为了那个,呃,派对的生活。

维基百科的目标是中立的“POV”(观点),并毫不畏缩地写道早期版本:

这个版本的 Internet Explorer 因其安全问题和对现代 Web 标准的支持不足而受到广泛批评,在“有史以来最糟糕的技术产品”列表中频繁出现,PC World 将其标记为“地球上最不安全的软件”[2]。甚至还没有提到所有的魔法,这意味着你不必管理从这里开始的下一步转换,因为你在概念上会将一切都毁掉,并在特定时间点重建事物。

有很多可能性可以探索,也许有一件事要说:

当这本书开始时,作者的家是 Python。当这本书结束时,作者的家是 ReactJS。

附录 A. Node.js Kick start

在 Web 开发中,有一个后端服务器是可取的。可用的服务器和语言列表很长。但一个引起了极大兴奋的服务器是 Node.js,它让你可以在前端和后端开发中使用相同的 JavaScript,并且提供了真正有趣的可能性。

本书的目的是介绍 Facebook 的前端用户界面框架 ReactJS。本附录的目的是提供足够的后端内容来运行一个经过身份验证的服务器,虽然有许多不错的选择,但 Node.js 可以在不需要处理新语言的情况下工作。本附录中所做的基本工作涵盖了与其他服务器和后端语言可能涉及的领域相当的范围。

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

  • Node.js 如何从 INTERCAL 中汲取灵感

  • Node.js,就像 JavaScript 一样,存在着许多隐患

  • 移植 Pragmatometer

但首先让我们看看 Node.js 和 INTERCAL。

Node.js 和 INTERCAL

INTERCAL,正式名称为“没有可发音首字母缩写的编译语言”,是由普林斯顿大学的 Don Woods 和 Jim Lyon 于 1972 年首次宣布。作为一个旨在讽刺各种编程语言的趋势和时尚的语言的原型示例,它可能更为人所知的是一个旨在不易使用,而故意和不必要地难以使用的语言的原型示例。它的“Hello, world!”代码包含了完整的、重复性压力伤害的 16 行;它传奇般的 ROT-13 加密/解密器(对于 Perl 或 Unix shell 命令来说是一个简单的一行代码)在alt.folklore.computers上被描述为“四页完全无法理解的代码”。INTERCAL 最初是以 EBCDIC 的穿孔卡形式发布的,这是一种被称为加密标准的字符编码。

一个被讽刺的趋势是 Edgser Dikjstra 的“Go to 语句被认为是有害的”,这项工作不仅仅是开创性的,可以说是计算机科学史上最重要的文章。有人说,程序员是一个被告知“去地狱!”而不是被“地狱”所冒犯的人,而是被“去”所冒犯的人。一个 INTERCAL 变种(C-INTERCAL)认为Go to语句确实是有害的,他们想尽可能远离Go to语句——比 IF-THEN-ELSE 语句和 while 循环要远得多。他们提供了一个比 IF-THEN-ELSE 和 while 循环更激进的离开Go to语句的方法——come from语句。而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 为了大胜利!”(这个标题显然是对Learn You a Haskell For Great Good的一个优秀前辈的致敬),Node.js 的学习工具是在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.js 的Hello, world!程序,或者可能是Hello, world!之后的程序,它在 Node.js 中就是这样的:

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

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

导入包的标准方法是调用require(),并将结果保存在你想要用来访问包的变量中。fs包是少数几个自带 Node.js 的包之一,但 Node.js 还附带了通过Node Package Managernpm)获得的整个包宇宙,这是一个对于使用 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 通过bitcoinjs.org/等工具挖掘比特币,但可能很少有人担心他们的 Node.js 服务器的性能问题,会让它在一边挖矿)。有一个集群模块旨在利用多个核心,Node.js 默认情况下在一个单线程进程上运行在一个核心上。但是,如果你对你的用例是否足够极端需要像集群这样的东西来执行 Node 的默认性能结构有任何疑问,你可能还不需要集群。

Node 的工作方式(至少不使用集群)还有一个额外的优势,即避免并发问题,因为它是单线程的,事实上不是并发的。这是一件非常好的事情。并发是一个棘手、危险的问题。有高度熟练的程序员擅长处理并发,但总的来说,并发应该被视为有害的一种东西,一种经常让大多数普通程序员感到困惑的潘多拉魔盒。也许有理由认为使用不可变数据的纯函数语言在并发方面是一个单独的情况,但在这里我们将坚持认为 Node.js 默认情况下可以处理大量请求,而不需要开发人员应对棘手的并发问题是件好事。

给 fs.readFile()的第二个参数是可选的,fs.readFile()允许它是有点不寻常的。正常的异步调用看起来像 identifier(data, callback)。在这种情况下,第二个可选参数值得更仔细地研究。

这个参数是用来从字节数组中创建字符串的编码,给定的参数是正常的默认编码,'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 采用了 Google Chrome 的 V8 引擎的一个版本(Node.js 的分支可能使用更新的版本),并在某些方面进行了扩展,以便能够作为通用的运行时环境,包括作为 Web 服务器。这包括添加了一些在客户端 Web 浏览器 JavaScript 中不存在的扩展。处理套接字作为服务器就是一个例子,这个功能以及 I/O 都是以前讨论过的异步模型的杰出发展。

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

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

Node.js 扩展了 V8 的功能以处理适当的二进制数据,并且经过试验和验证的 JSON 与新的二进制友好的 BSON 相辅相成。处理二进制数据是低级的,可能对于其类似 C 的特性来说太低级了。例如,当 C 程序员完全从使用malloc()(“内存分配”)切换到使用calloc()(“清除内存分配”)时,就会提高生产力并减少挫败感。malloc()函数分配了一个原始的内存块,其中包含了来自内存先前占用者的任何残留物,如果你没有正确初始化内存的任何部分,就会导致奇怪和神奇的效果。

calloc()函数分配了一个原始的内存块,并用零覆盖了任何先前的内容。记住 Pete Hunt 的话,“我宁愿是可预测的,也不愿意是正确的?”停止直接使用malloc()而转而使用calloc()是 C 程序员可以选择可预测而不是正确的主要方式。然而,出于错误的优化考虑(不擦除分配的字节内存会快上几分之一秒),Node.js 只提供了 C malloc()的等价物,没有提供任何calloc()的等价物,据我所知。幸运的是,Node.js 的 JavaScript(或者说 C)是如此强大,以至于很容易移植calloc()功能。只需制作一个处理 Node.js 字节分配处理的包装器,然后使所有位都为零,并且在分配字节时专门使用这个包装器。

回到刚刚突出显示的代码,在 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)回调签名是编程合同的正常回调签名。回调应该最终被调用,可能非常快地被调用。这应该通过一个“真值”err来完成,如果是这种情况,回调应该选择性地采取步骤来响应错误反馈中包含的任何信息,并且在不进一步进行的情况下无条件返回,或者一个空的 err,在这种情况下,函数的前提条件得到满足,回调应该采取适当的行动来接收所请求的数据。前面的代码说明了这种模式:检查空的err,选择性地通过记录诊断信息对其进行操作,并且如果 err 为空,则打印文件内容。

警告 - Node.js 及其生态系统很热,热得足以严重伤害你!

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

有些问题不仅令从 Python/Django 转过来的人感到恼火,Python/Django 会在更改任何内容后立即重新加载源代码。而在 Node.js 中,默认行为是,如果你做了一次更改,旧版本将一直保持活动状态,直到永远或者直到你手动停止并重新启动服务器。这种不恰当的行为不仅令 Python 程序员感到恼火,也令原生的 Node.js 用户感到恼火,他们提供了各种解决方法。在 StackOverflow 上的问题“Node.js 中的文件自动重新加载”在我写这篇文章时,已经有超过 200 个赞和 19 个答案;一次编辑将用户引导到一个看护脚本,node-supervisor,主页在tinyurl.com/reactjs-node-supervisor。这个问题为新用户提供了一个很好的机会,让他们感到愚蠢,因为他们以为已经解决了问题,但旧的错误行为完全没有改变。而且很容易忘记重启服务器;我已经多次这样做了。我想传达的信息是,“不,你不是因为 Node.js 的这种行为而感到愚蠢;只是 Node.js 的设计者没有理由在这里提供适当的行为。尽量应对它,也许可以从 node-supervisor 或其他解决方案中得到一点帮助,但请不要走开时觉得自己很蠢。你不是有问题的人;问题在于 Node.js 的默认行为。”

这一部分经过一些辩论后被保留了下来,正是因为我不想给人留下“这很容易”的印象。在让事情正常运转的过程中,我反复割伤了手,我不想掩盖困难,也不想让你相信:让 Node.js 及其生态系统正常运行是一件简单的事情,如果对你来说不简单,那就是你不知道自己在做什么。如果你在使用 Node.js 时没有遇到令人讨厌的困难,那太好了。如果你遇到了,我希望你不要走开时感到“我很蠢。一定是我有问题。”如果你在处理 Node.js 时遇到了令人讨厌的意外,你并不蠢。不是你的问题!是 Node.js 及其生态系统的问题!

接下来,我们将探讨一个示例项目,一个远程等效的快速而简单的基于 localStorage 的持久性,这在第十一章中有所涉及,用实例演示 JavaScript 中的函数式响应式编程 - 添加一个草稿本并把所有内容放在一起。那是一个成功,但在过程中遇到了太多的困难。我有时会比较 Python 和 JavaScript;但也许值得花点时间看看为什么 JavaScript 的 Node.js 与 Python 的 Django 相比确实很讨厌。

我对 Django 的第一次体验,经过多年的经验后,感觉它是一个很棒的强大工具,但出于某种奇怪的原因,它不小心没有被纳入 Python 的标准库。事实上,这是有充分理由的,既不需要批评 Python 也不需要批评 Django:正如 Python 的终身独裁者所观察到的那样,当某样东西“死”了,而不是当它仍在成长时,你才把它放入标准库。Django 仍在成长,它仍在变得更好。因此,无论它有多好,Django 都不属于 Python 的标准库。

在许多情境中,有一个最少惊讶原则,一旦你开始熟悉 Python,它就不会经常给你带来不愉快的惊喜。Django 确实会带来一些惊喜,比如它的模板系统,在 ASP 和 JSP 时代是一个引人注目的提议。(现在它已经过了它的 15 分钟的荣耀,即使是 Python/Django 开发人员也开始用更强大的东西替换模板系统。ReactJS 基本上做了相反的选择是完全正确的。)但矛盾的是,Django 和 ReactJS 都提供了反映了火星技术的模板,如《新黑客词典》中定义的:

*[TMRC]一种具有远见卓识的品质,使人能够忽略标准方法,提出完全意想不到的新算法。从一个离奇的角度攻击问题,以前没有人想到过,但事后看来是完全合理的。比较 grok,zen。

使用 Node.js 的感觉与使用 Python 甚至与使用 ReactJS 完全不同。它更令人沮丧,更困难,而且有更多不合理的事情。

这里有一个例子:在最初的研究时,我打算使用 passport.js 来卸载身份验证的脏活。我最初打算使用 Facebook 身份验证,但说明涉及在 Facebook 开发者网站上创建一些东西,并从 Facebook 应用程序中获取信息。即使在探索 Facebook 开发者网站并询问后,“passport.js 说要从 Facebook 开发者网站上的我的应用程序获取 XYZ 信息”,我完全没有得到及时的答复。

缩小我的野心,我决定使用 passport.js 最基本的适当身份验证——用户名和密码——直到我发现提供的用户名和密码支持几乎完全没有用。

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

  • 允许用户创建新帐户

  • 允许用户使用现有帐户登录

  • 替换丢失的密码,而不会将未加密的密码通过电子邮件发送给用户

  • 可能是网站管理成员的一组扩展功能,如帐户管理(如果需要),锁定和解锁帐户,以及帐户删除。

passport.js 功能中唯一涵盖的基础是使用已经存在的帐户登录,由我无法确定的某种方式创建,并成功或不成功地进行身份验证。也许在 CRUD 方面唯一更加病态不完整的事情是几十年前在 Byte 杂志的 4 月 1 日问题中提供的,当时有人宣传了一个非常划算的只读存储器。

现在我们可能会注意到,支持 100%的 CRUD 并不是唯一可能的方法。多年前,“写一次,读多次”(WORM)磁盘驱动器曾经引起了一些关注。虽然可能没有现代笔记本电脑配备真正的 WORM 驱动器,但 ClojureScript 包括了大量的工作来提供 WORM 数据。在这种情况下,WORM 意味着数据被设计为排除更新(尽管您可以很容易地制作修改后的副本),并且删除被保留到垃圾回收:在完全支持 CRUD 的情况下,ClojureScript 的 WORM 数据只允许无损地创建和读取数据。ClojureScript 反映了一个经过深思熟虑的决定,在这种情况下提供 WORM 数据会更容易实现完全的 CRUD 支持。这个决定现在值得明显的尊重。

现在,对于身份验证来说,缺乏等同于完全 CRUD 支持并不是世界末日,因为至少还有另一组人更适当地处理了“身份验证 CRUD”。Stormpath 为 Node、Python、Java 和 REST 提供了广告服务。他们的一名开发人员为我重写了身份验证的代码,以便我使用他们的系统。虽然这可能只是对可能涵盖他们产品的作者的友好表示,但 Stormpath 的包含甚至不是一个适当的集成挑战;它真的很简单。现在应该声明一下,Stormpath 不是开源的,而是一个带有免费定价的 SaaS。全功能、免费且“无需信用卡”的开发者层每月有 10 万次 API 调用的配额,他们估计用户登录大约使用三个 API 调用。他们肯定有盈利动机,但如果您的流量足够大,需要付费服务层,您不应该在乎您必须支付给他们的小事。该系统给人的整体印象有点年轻,人们正在解决剩下的问题,但确实有人礼貌地告诉他们有些不成熟的地方,问题很快就解决了。

另一个基本困难围绕着数据库。可以说 MongoDB 很重要,再加上 mongoose 的“从 Node.js 访问 MongoDB”包,值得学习曲线。在准备本章时,搜索排名靠前的教程证明了如何创建模式并保存其中的内容,但对如何有用地处理已经存在的所有必要模式的数据库留下了猜测。随后的工作发现了一个现有的 Stack Overflow 解决方案,似乎涵盖了具有已经存在的模式和数据库的数据库 CRUD。在放弃之前,我可能已经接近成功,但我打算从一开始就使用 mongoose/MongoDB 进行数据库工作,但我还没有达到熟练程度。

另一个看起来非常合适的数据库,而且可能还有挽回的余地,是 HTML5 键值存储的服务器端实现。这种方法的主要优势是大多数优秀的前端开发人员都足够了解的 API。而且,这也是大多数不那么优秀的前端开发人员都足够了解的 API。但是使用node-localstorage包时,虽然不提供dictionary-syntax访问(您可能想使用localStorage.setItem(key, value)localStorage.getItem(key),但不是localStorage[key]),但实现了完整的 localStorage 语义,包括默认的 5MB 配额。为什么?服务器端 JavaScript 开发人员需要保护自己吗?

对于客户端数据库功能来说,每个网站的 5MB 配额确实是一个慷慨而有用的呼吸空间,让开发者可以更好地使用它。你可以设置一个更低的配额,仍然可以为开发者提供比使用 cookie 管理更好的改进。5MB 的限制并不适合快速进行大数据客户端处理,但对于有资源的开发者来说,这是一个非常慷慨的允许,可以做很多事情。另一方面,5MB 对于最近购买的大多数磁盘来说并不是一个特别大的部分。这意味着如果你和一个网站对于磁盘空间的合理使用意见不一致,或者一个网站只是贪婪,这并不会让你花费太多,你也不会有硬盘被淹没的危险,除非你的硬盘已经太满了。也许我们最好的平衡是更少一点,或者更多一点,但总的来说,这是一个相当不错的解决方案,可以解决客户端环境中的内在紧张关系。

然而,可能要温和地指出,当你自己为服务器编写代码时,你不需要额外的保护来使你的数据库超过 5MB 的容量。大多数开发者既不需要也不希望工具像保姆一样保护他们免于存储超过 5MB 的服务器端数据。此外,这个 5MB 的配额,在客户端是一个黄金的平衡,但在 Node.js 服务器上却有点傻。

此外,对于多用户的数据库,可能会有点痛苦地指出,这不是每个用户账户 5MB,除非你为每个账户创建一个单独的数据库列表。这是 5MB 在所有用户账户之间共享。如果你爆发了,这可能会很痛苦!

文档说明配额是可定制的,但一周前给开发者的电子邮件询问如何更改配额没有得到回复,同样的问题也在 Stack Overflow 上提问了,也没有得到答复。我唯一找到的答案是在 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 的本地存储中保存和恢复几个 JSON 字符串来处理持久性。我们将致力于开发一个可以提供静态内容的服务器,提供一个 API 来保存或恢复一个字符串和一个标识键,并处理基本的身份验证和账户管理。客户端编程应该比以前更有趣,基本上是通过将保存到本地存储更换为保存到我们的远程 Node.js 服务器。

我们将使用多种技术。最关注的将是在 Express.js 中工作,与例如 Stormpath 相比。Stormpath 似乎没有因发明基本新的、原创的或令人惊叹的东西,或者因身份验证机制的突破而获得赞誉。他们可能会因以一种使大量繁重工作减轻你的负担的方式解决了一个众所周知的问题而获得赞誉。添加 Stormpath 是小而不显眼的。大多数用户不会将其用作构建一些伟大工作的平台。因此,我们将重点关注 Express.js(以及让我们的客户端与 Express.js 通信),这是一个可以使用的平台。在框架的网站上,Express.js 被宣传为“Node.js 的快速、不受限制的 Web 框架”。他们基本上实现了他们吹嘘的东西。

我们需要构建一个服务器,但也需要修改第八章到第十一章中 Pragmatometer 项目的客户端。有save()restore()函数,它们将被修改和扩展。

通过npm install express安装 Express.js。然后使用express [项目的目录名]创建一个 express 项目。你将会有一个完整的框架。你可以向package.json文件添加包,并运行npm install来填充你的本地副本。将会有一个公共或静态目录可以使用,并且routes/index.js以其他框架熟悉的方式处理路由。

客户端准备工作

第八章到第十一章中js/目录下的所有内容都移动到public/javascripts。更改的完整细节将发布在网站上。在这里,我们将save()restore()函数从(客户端)特定于 localStorage 的功能改为保留 localStorage 以获得轻微的感知速度提升,但从远程服务器恢复和保存。在这种情况下,服务器是使用 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 中恢复(如果有保存的内容)。它检查 localStorage 是否可用,并且如果不可用,则优雅地降级,这可能是历史性的,因为 ReactJS 只声称与提供 localStorage 的浏览器(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 中恢复:

  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'
    });
  }

当我们从 localStorage 中拉取东西时,我们试图阻止用户能够输入数据。这是因为在不可预测的竞争条件下,当来自 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 install XYZ,然后在“dependencies”下的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"
  }
}

我们在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 + '/public/images/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]的项目中除了一个都是从 Stormpath 的设置中获取的。有些人建议在制作自己的秘钥时要聪明一些;不要这样做!在 Mac、Unix、Linux 或 Cygwin(Cygwin 可以从cygwin.org免费获取,并在 Windows 下运行)下,打开命令提示符并输入以下命令:

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

这将为您提供一千字节的加密强大和随机数据,该数据已编码为易于复制和粘贴。

这里有一个关于卫生的注意事项:建议的做法是非常小心地处理您的秘钥,特别是不要将其包含在版本控制中。而是将其放入主目录下的点文件目录中,并设置权限,不让其他人对其进行任何操作。

可能,工作量最大的文件之一是routes/index.js。我们引入了几个依赖项,包括一个 body 解析器,它将能够从 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,指定无限制作为我们的配额,然后为键中的字符提供一个清理器。这个特定的清理器保留字母数字字符,这对于应用程序的其余部分足以确保它不会产生键冲突。它还确保字符在排除冒号的白名单上。这使我们能够创建类似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,包含的stormpath.loginRequired就是你需要的一切,以便让视图受到登录保护。我们继续定义两个非视图函数:用于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="img/react-with-addons.js">
    </script>
    <script
      src="img/showdown.js">
    </script>
    <script src="img/ckeditor.js"></script>
    <script src="img/"></script>
    <script src="img/json2.js"></script>
    <script src="img/modernizr.js"></script>
    <script src="img/site.js"></script>

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

摘要

在考虑这个附录时,我考虑过的一个问题是“JavaScript 加上 Node.js 还是 Python 加上 Django?”本书的重点是 ReactJS 前端,后端的重点只是为了提供足够的支持前端。我自然地认为 Python 是如此简单,即使对新手来说也是如此,Django 也是如此简单(同样,即使对新手来说也是如此),即使引入一种新语言,基本的带有认证的键值存储应该是一个容易阅读和编写的附录。然而,作者当时认为我会选择 JavaScript 加上 Node.js 这条高路,这是每个人都想要的组合,自那时起,我一直在为他的决定付出代价,因为他没有提供 Python 加上 Django 的附录。

捆绑提供的代码当然是免费提供的,你可以从中获取任何不违反 Packt Publishing 许可的里程。但是,实现带有账户管理的键值存储的基本任务可能是本科生的家庭作业水平。从任何方面来看,它都不能展示出服务器所提供的惊人功能。

现在,Node.js 确实提供了令人惊叹的功能。然而,这些功能在这里没有被探索,因为目标是提供足够的“Node.js 加上 Express.js”来创建一个基于服务器的 Pragmatometer 项目的适应版本,该项目在第 8 到 11 章中进行了介绍。此外,鉴于对所有项目的热情和大量工作时间,可能有必要在撰写本书时对关于不成熟生态系统的任何评论进行严格的限制,可能需要 1 年、2 年或 3 年的时间。5 年后,可能真的有必要说,“2015 年的 Node.js 生态系统存在一些隐患。2020 年的 Node.js 生态系统有多个乐园。”

但是,像 passport.js 一样发布,通过passport.authenticate('twitter')passport.authenticate('google')passport.authenticate('facebook')等方式简单地实现动画效果,然后让用户长时间搜索和询问如何处理用户名密码认证以允许用户创建新账户,这是不会发生的。这是极其不合适的,而且在 Node.js 生态系统中发生了不止一次。在找到一个看起来提供了你需要的功能的 Node.js 工具的网站和激活“Hello, world!”级别的功能之间的过渡,成功率也许只有 50%。这代表了一个比我在整个 Web 历史上见过的任何事情都更大的鸿沟。

我可以看到人们会认为,不是因为我设计了一个带有多种状态的待办事项清单,而觉得我很聪明,而是因为我很实际,总的来说,这本书大大减少了读者在学习 ReactJS 时所需的工作量。然而,如果有人告诉我我很聪明,因为我想到了制作一个经过身份验证的键值存储,我会感到困惑,因为这在附录中有所涉及。这个成就并不是因为我设法让某些技术作为一个经过身份验证的键值存储工作——这与本科作业相当——而是因为它是在与 INTERCAL 连续存在的环境中完成的。

人们不断地将自己置于困境中,因为他们不断地将 JavaScript 作为一个整体来使用,而 JavaScript 能够成为一门受人尊敬的语言,关键在于道格拉斯·克罗克福德的一句话,本质上是这样说的:

“JavaScript 作为一种语言有一些非常好的部分和一些非常糟糕的部分。这里是好的部分。只要忘记其他的东西存在。”

也许炙手可热的 Node.js 生态系统将会培养出自己的“道格拉斯·克罗克福德”,他会说:

Node.js 生态系统就像编码的西部荒野,但也有一些真正的宝藏。这是一张路线图。这些是几乎可以不惜任何代价避免的领域。这些领域是任何语言或环境中可以找到一些最丰富的矿藏的地方。

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

posted @ 2024-05-22 12:06  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报