HTML5-数据服务秘籍-全-

HTML5 数据服务秘籍(全)

原文:zh.annas-archive.org/md5/1753B09CD35CEC6FE2CC3F9B8DA85828

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

HTML5 无处不在,从个人电脑到平板电脑、智能手机,甚至现代电视机。网络是最普遍的应用平台和信息媒介。最近,HTML5 已成为已建立的操作系统(如 Microsoft Windows 8、Firefox OS 和 Google Chrome OS)中的一等公民。

开放性是网络的重要方面之一。HTML5 是反对私有和专有解决方案的主要方式之一,这些解决方案强制使用特定技术。在过去几年中,真正的革命正在发生。JavaScript 已经成为 Web 应用程序开发的领先位置,无论是服务器端还是客户端。

过去,获取一半完成的脚本和编写不良的 JavaScript 非常常见,因此该语言声誉不佳。HTML5 功能已经可用,但广泛未被使用。有很多网络应用程序在重新发明轮子,而 HTML5 已经有他们需要的功能。

这本书将帮助您快速学习现代 HTML5 功能。在书的结尾,您应该对浏览器和服务器中的 JavaScript 有扎实的理解。除此之外,您还将创建使用新 HTML5 技术的酷小应用程序,并学会如何调整现有应用程序以使用这些新功能。

本书涵盖的内容

第一章,“文本数据的显示”,涵盖了在 HTML5 中显示文本所需的知识。这包括格式化数字、显示数学公式和测量单位。此外,还有关于显示表格数据和呈现 Markdown 的部分,展示了一些日常开发功能。

第二章,“图形数据的显示”,首先介绍了使用 Flot 图表库创建图表,以及更现代的数据驱动D3.js。本章还涵盖了显示带有路线和标记的地图。

第三章,“动画数据显示”,探讨了动画和交互式可视化的创建。本章大部分可视化基于D3.js,但也有一些从头开始或使用通知 API 等技术的示例。

第四章,“使用 HTML5 输入组件”,首先介绍了简单文本输入元素的使用,然后转向 HTML5 添加的新输入类型。它还涵盖了新属性的使用,以及使用地理位置或拖放区域的更高级输入。

第五章,“自定义输入组件”,延续了前一章的主题,重点是创建自定义控件,添加新功能或模仿桌面应用程序中可用的组件。本章解释了如何创建菜单、对话框、列表选择和富文本输入等控件。

第六章,“数据验证”,介绍了 HTML5 处理表单验证的方式。本章将涵盖文本和数字的验证,内置的电子邮件和数字验证。此外,它还涵盖了使用 Node.js 进行服务器端验证,并展示了如何结合客户端和服务器端验证。

第七章,“数据序列化”,深入探讨了从客户端 JavaScript 创建 JSON、base64 和 XML,以及从这些格式创建 JavaScript 对象的逆向过程。

第八章,“与服务器通信”,让您开始使用 Node.js 并创建 REST API。本章还包含了如何从纯 JavaScript 发出 HTTP 调用、如何处理二进制文件以及通信安全的详细信息。

第九章,“客户端模板”,介绍了流行的客户端模板语言 Handlebars,EJS 和 Jade 的使用。它涵盖并比较了这些语言的基本用法,以及它们更高级的功能,如部分,过滤器和混合。

第十章,“数据绑定框架”,让您开始使用两种不同类型的 Web 框架。一方面,我们有 Angular,它是许多不同客户端 MVC 框架中强大的代表,另一方面,我们有 Meteor,它是在某些领域缩短开发时间的反应性框架。

第十一章,“数据存储”,探讨了 HTML5 中可用的新客户端存储 API,以及用于处理文件的新 API。这些新功能使我们能够在页面刷新后持久保存数据,并保存不会在每个请求中来回传输的客户端信息。

第十二章,“多媒体”,介绍了在浏览器中播放视频和音频文件的一些方法,这在过去是由外部插件完成的。

附录 A,“安装 Node.js 和使用 npm”,简要介绍了安装 Node.js 及其包管理器 npm。

附录 B,“社区和资源”,包含了 HTML5 开发的主要组织的简短历史和参考资料。

本书所需的内容

开始所需的一切只是一个现代浏览器,如 Firefox,Chrome,Safari,Opera 或 Internet Explorer 9,一个简单的文本编辑器,如 Notepad ++,Emacs 或 Vim,以及互联网连接。

在第七章,“数据序列化”和后续章节中,您还需要安装 Node.js 来尝试一些配方。安装过程在附录 A中有介绍,安装 Node.js 和使用 npm

这本书是为谁写的

这本书是为那些以某种方式已经使用过 JavaScript 的程序员而写的。它适用于那些与大量后端代码一起工作,并希望快速了解 HTML5 和 JavaScript 世界的人。它适用于那些使用复制/粘贴来修补页面的一部分并想了解背后工作原理的人。它适用于希望通过 HTML5 实现的新技术和功能更新他们的 JavaScript 开发人员。

本书既适用于初学者又适用于经验丰富的开发人员,假设您已经具有一些 HTML,JavaScript 和 jQuery 的经验,但不一定需要深入的知识。

约定

在本书中,您将找到一些文本样式,用于区分不同类型的信息。以下是一些这些样式的示例,以及它们的含义解释。

文本中的代码单词显示如下:“d3.behavior.zoom()方法使我们能够向我们的projection类型添加自动缩放功能,并使用scaleExtent中给定的比例和缩放范围。”

代码块设置如下:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Chart example</title>
  </head>
  <body>
    <div id="chart" style="height:200px;width:800px;"></div>
    <script src="img/jquery.min.js"></script>
    <script src="img/jquery.flot.js"></script>
    <script src="img/jquery.flot.navigate.js"></script>
    <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体显示:

#carousel {
 perspective: 500px;
  -webkit-perspective: 500px;
  position:relative; display:inline-block;
  overflow:hidden;
}

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

Object:
 color: "#00cc00"
 data: Array[50]
 name: "one"

新术语重要单词以粗体显示。您在屏幕上看到的单词,例如菜单或对话框中的单词,会以这样的方式出现在文本中:“此外,我们可以添加一个包含默认文本的属性 data-placeholder,例如我们的示例中的职业。如果未指定,它将默认为选择某些选项进行单选。”

注意

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

提示

提示和技巧看起来像这样。

第一章:文本数据的显示

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

  • 舍入数字以进行显示

  • 填充数字

  • 显示公制和英制测量单位

  • 在用户的时区中显示格式化日期

  • 显示已经过去的动态时间

  • 显示数学

  • 创建无限滚动列表

  • 创建可排序的分页表

  • 创建多选过滤器

  • 创建范围过滤器

  • 创建组合复杂过滤器

  • 在 HTML 中显示代码

  • 渲染 Markdown

  • 自动更新字段

介绍

与 Web 应用程序开发相关的最常见任务是显示文本。本章将涵盖程序员在浏览器中显示数据时面临的一些问题,并将解释如何以简单而有效的方式解决这些问题,为程序员提供几种不同的选择。这些示例将包含标记的渲染或其他数据类型转换为纯文本。

舍入数字以进行显示

在文本之后,应用程序中使用的第二种最常见的数据类型是数字。有许多不同的处理数字的方式,当需要给定精度时,我们将看一些这些方式。第一个明显的选择是使用 JavaScript 的Number对象包装器来处理数值。

准备工作

Number对象包含toFixed([digits])方法,可用于显示数字;这里的digits参数可以在 0 和 20 之间取值。如果需要,数字将自动舍入,或者如果需要,数字将填充额外的零。好的,让我们看看它的效果。

如何做...

执行以下步骤来演示使用Number对象:

  1. 首先,我们将创建一个数字列表;请注意,这些数字是有意挑选的,以说明一些函数的特性:
    var listOfNumbers=
        [1.551, 1.556, 1.5444, 1.5454, 1.5464, 1.615, 1.616, 1.4, 1.446,1.45];
  1. 迭代列表,并使用.toFixed()方法显示数字,分别使用digits参数的值 0、1 和 2:
for (var i = 0; i < listOfNumbers.length; i++) {
      var number = listOfNumbers[i];
         // iterate over all of the numbers and write to output all the value
      document.write(number + "---"
                   + number.toFixed(2) + "---"
                   + number.toFixed(1) + "---"
                   + number.toFixed() + "<br />");
    };

它是如何工作的...

执行代码后得到的结果将打印出带有它们各自toFixed表示的数字,这应该很简单。

让我们来看一些特征值:

  • 1.616.toFixed(2)将返回1.62

  • 1.4.toFixed(2)将返回1.40,如预期的那样,添加一个尾随零

  • 1.5454.toFixed()将返回2,因为toFixed()的默认值是0;这意味着没有小数点,另外0.5部分被舍入为1,所以这里使用了天花板值

  • 1.615.toFixed(2)将返回1.61,忽略0.005部分,或者将使用地板值

toFixed()方法在大多数情况下都能按预期工作,只要我们不需要更高的精度或仅用它来显示数字,其中舍入的类型并不是关键。

此外,当我们需要在有类似 1.446 的数字的情况下进行舍入时,我们不能依赖于toFixed(); 调用1.446.toFixed(1)将导致不一致和不可预测的结果。

还有更多...

有各种方法可以解决这个问题。快速而肮脏的解决方案是重新定义Number.prototype.toFixed()函数,但我们鼓励您不要这样做,因为这样做可能会产生不明显的副作用。如果不是绝对必要,对内置对象的函数进行重新定义被认为是一种反模式。问题在于如果另一个库或一段代码使用相同的函数。另一个库可能期望我们重新定义的函数以某种方式工作。这些类型的重新定义很难跟踪;即使我们添加一个函数而不是重新定义它,其他人可能会做同样的事情。例如,假设我们决定向Number对象添加一些函数:

Number.prototype.theFunction = function(arg1,arg2){}

没有保证其他人没有将theFunction添加到Number对象中。我们可以进行额外的检查来验证函数是否已经存在,但我们不能确定它是否会按我们希望的方式工作。

相反,使用一个实用函数来实现一致的数据将是一个更好的选择。

解决问题的一种方法是首先将数字乘以10 ^ digits,然后在结果上调用Math.round(number)方法,或者您可以调用Math.ceil(number)。例如,如果您需要将值向上舍入到最接近的整数,使用以下方法:

    function round(number, digits) {
        if(typeof digits === "undefined" || digits < 0){
          digits = 0;
        }
        var power = Math.pow(10, digits),
         fixed = (Math.round(number * power) / power).toString();
        return fixed;
    };

现在,由于数字乘以10 ^ digits,然后四舍五入,我们不会观察到toFixed()的问题。请注意,这种方法与toFixed()的行为不同,不仅在处理舍入的方式上有所不同,而且还会添加尾随零。

另一个选择是使用一个类似 Big.js 这样的任意精度库,如果精度很重要的话(github.com/MikeMcl/big.js)。

填充数字

有时我们需要将数字填充到一定的范围。例如,假设我们想要以五位数字的形式显示一个数字,比如00042。一个明显的解决方案是使用迭代方法并在前面添加字符,但还有一些更简洁的解决方案。

准备工作

首先,我们需要看一下我们将要使用的一些函数。让我们看一下Array.join(separator)方法,它可以用来从元素列表创建连接的文本:

new Array('life','is','life').join('*')

这将导致"life*is*life",显示了用给定分隔符连接的相当简单的元素。另一个有趣的方法是Array.slice(begin[, end]),它返回数组的一部分的副本。对于我们的用途,我们只对begin参数感兴趣,它可以具有正值和负值。如果我们使用正值,这意味着这将是使用基于零的索引的切片的起始索引;例如,考虑以下代码行:

new Array('a','b','c','d','e','f','g').slice(4);

上面的代码将返回一个包含元素'e''f''g'的数组。

另一方面,如果对begin元素使用负值,则表示从数组末尾的偏移量,考虑以下使用负值的相同示例:

new Array('a','b','c','d','e','f','g').slice(-3);

结果将是'e','f','g',因为我们是从末尾切片。

如何做...

让我们回到我们的问题:如何为数字添加前导零创建一个干净的解决方案?对于迭代解决方案,我们创建一个接受数字、格式化结果的大小和用于填充的字符的方法;例如,让我们以'0'为例:

function iterativeSolution(number,size,character) {
   var strNumber = number.toString(),
    len = strNumber.length,

    prefix = '';
   for (var i=size-len;i>0;i--) {
      prefix += character;
   }
 return prefix + strNumber;
}

在这里,我们将数字转换为字符串,以便获得其表示的长度;之后,我们简单地创建一个prefix,它将有size-len个字符的character变量,并返回结果prefix + strNumber,这是该数字的字符串表示。

您可能会注意到,如果size小于len,则会返回原始数字,这可能需要更改,以使该函数适用于这种特殊情况。

另一种方法是使用Array.slice()方法来实现类似的结果:

function sliceExample(number,prefix){
   return (prefix+number).slice(-prefix.length);
}
sliceExample(42,"00000");

这将只是在数字前面添加一个前缀,并从末尾切掉多余的'0',使解决方案更加简洁,并且还能够更灵活地确定前缀的内容。这样做的缺点是我们手动构造了将成为方法调用sliceExample(42,"00000")一部分的前缀。为了使这个过程自动化,我们可以使用Array.join

function padNumber(number,size,character){
  var prefix = new Array(1 + size).join(character);

我们创建一个预期的size + 1的数组,因为在连接时,我们将得到总数组size-1 个连接的元素。这将构造预期大小的前缀,而其他部分将保持不变:

  return (prefix + number).slice(-prefix.length);
 }

一个示例方法调用将是padNumber(42,5,'0'); 这将不具有以前方法的灵活性,但在处理更大的数字时会更简单。

它是如何工作的...

这个配方相当简单,但需要注意的一点是功能性方法。如果有一件事可以从这个配方中带走的话,那就是迭代解决方案并不总是最好的。当涉及到 JavaScript 时,通常有几种其他完成任务的方法;它们并不总是那么直接,有时甚至不是更快,但它们可能更加干净。

还有更多...

如果由于某种原因我们经常填充数字,将函数添加到Number对象中并使用this关键字删除input参数数字可能是有意义的:

Number.prototype.pad=function(size,character){
     //same functionality here
}

由于该函数现在是每个Number对象的一部分,我们可以直接从任何数字中使用它;让我们来看下面的例子:

  3.4.pad(5,'#');

此外,如果不应包括“。”字符在填充的计算中,我们可以添加一个额外的检查,以减少前缀的大小。

注意

请注意,在舍入数字以进行显示配方中,我们解释了为什么向标准对象添加函数是一种可能会对我们产生反作用的黑客行为。

显示公制和英制测量

处理计算和测量的网站通常需要解决同时使用公制和英制计量单位的问题。本教程将演示一种数据驱动的方法来处理单位转换。由于这是一本 HTML5 书籍,解决方案将在客户端而不是服务器端实现。

我们将实现一个客户端,“理想体重”计算器,支持公制和英制测量。这一次,我们将创建一个更通用和优雅的数据驱动解决方案,利用现代 HTML5 功能,如数据属性。目标是尽可能抽象出混乱和容易出错的转换。

准备工作

计算体重指数(BMI)的公式如下:

BMI =(千克中的体重/(米中的身高 x 米中的身高))

我们将使用 BMI = 22 来计算“理想体重”。

如何做...

  1. 创建以下 HTML 页面:
<!DOCTYPE HTML>
<html>
    <head>
        <title>BMI Units</title>
    </head>
    <body>
        <label>Unit system</label>
        <select id="unit">
            <option selected value="height=m,cm 0;weight=kg 1;distance=km 1">Metric</option>
            <option value="height=ft,inch 0;weight=lbs 0;distance=mi 1">Imperial</option>
        </select><br>

        <label>Height</label>
        <span data-measurement="height" id="height">
            <input data-value-display type="text" id="height" class="calc">
            <span data-unit-display></span>
            <input data-value-display type="text" id="height" class="calc">
            <span data-unit-display></span>
        </span>
        <br>
        <label>Ideal Weight</label>
        <span data-measurement="weight" id="weight">
            <span data-value-display type="text">0</span>
            <span data-unit-display></span>
        </span> <br>

        <script src="img/jquery.min.js"></script>
        <script type="text/javascript" src="img/unitval.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
        </script>
    </body>
</html>

这个页面看起来非常像我们为基于 BMI 的理想体重计算器制作的常规页面。主要区别如下:

  • 我们有一个英制/公制选择输入

  • 我们还有额外的自定义数据属性,为 HTML 字段赋予特殊含义

  • 我们使用data-measurement来表示元素将显示的测量类型(例如,体重或身高)

  • 我们使用data-display-unitdata-display-value来表示显示单位字符串和测量值的字段

  1. 创建一个名为example.js的文件,其中包含以下代码:
(function() {
    // Setup unitval
    $.unitval({
        weight: {
            "lbs": 0.453592, // kg
            "kg" : 1 // kg
        },
        height: {
            "ft"  : 0.3048, // m
            "inch": 0.0254, // m
            "m"   : 1, // m
            "cm"  : 0.01, // m
        }
    });
    $("#unit").change(function() {
        var measurementUnits = $(this).val().split(';').map(function(u) {
            var type_unitround = u.split('='),
                unitround = type_unitround[1].split(' ');
            return {
                type: type_unitround[0],
                units: unitround[0].split(','),
                round: unitround[1]
            };
        });
        // Setup units for measurements.
        $('body').unitval(measurementUnits);
    });

    $("#unit").trigger("change");

    $('#height').on('keyup change',function() {
        var height = $('#height').unitval(), bmi = 22;
        var idealWeight = bmi * height * height;
        $("#weight").unitval(idealWeight);
    });

}

代码的第一部分配置了一个名为unitval的 jQuery 插件,其中包含我们将使用的测量和单位的转换因子(体重和身高)。

第二部分通过从select字段中读取规范来设置文档的测量单位。它指定了一个测量数组,每个测量都有以下内容:

  • 一个类型字符串,例如“身高”

  • 单位列表,例如["ft", "inch"]

  • 用于最后一个单位的小数位数

第三部分是一个常规计算器,几乎与没有单位转换时写的一样。唯一的例外是,值是使用名为$.unitval的 jQuery 插件从具有data-measurement属性的元素中获取的。

  1. 我们将编写一个通用的单位转换器。它将需要两个函数:一个将用户显示(输入)数据转换为标准国际(SI)测量单位的函数,另一个将其从 SI 单位转换回用户友好的显示单位。我们的转换器将支持同时使用多个单位。在从输入转换时,第一个参数是测量类型(例如,距离),第二个是值-单位对的数组(例如,[[5,'km'],[300,'m']]),单个对(例如[5,'km']),或者只是值(例如5)。

  2. 如果第二个参数是一个简单的值,我们将接受一个包含单位的第三个参数(例如'km')。输出始终是一个简单的 SI 值。

在将值转换为所需的输出单位时,我们将单位指定为数组,例如,作为['km', 'm']或作为单个单位。我们还指定最后一个单位的小数位数。我们的输出是转换后的值数组。

转换是使用Factors对象中的值完成的。该对象包含我们将要使用的每个测量名称的属性。每个这样的属性都是一个对象,其中包含该测量的可用单位作为属性,其 SI 因子作为值。在example.js中查看示例。

  1. jQuery 插件unitval.js的源代码如下:
(function() {
    var Factors = {};
    var Convert = window.Convert = {
        fromInput: function(measurement, valunits, unit) {
            valunits = unit ? [[valunits, unit]] // 3 arguments
                : valunits instanceof Array && valunits[0] instanceof Array ? valunits  
                : [valunits]; // [val, unit] array

            var sivalues = valunits.map(function(valunit) { // convert each to SI
                return valunit[0] * Factors[measurement][valunit[1]];
            });
            // sivalues.sum():
            return sivalues.reduce(function(a, e) { return a + e; });
        },
        toOutput: function(measurement, val, units, round) {
            units = units instanceof Array ? units : [units];
            var reduced = val;
            return units.map(function(unit, index) {
                var isLast = index == units.length - 1,
                    factor = Factors[measurement][unit];
                var showValue = reduced / factor;
                if (isLast && (typeof(round) != 'undefined'))
                    showValue = showValue.toFixed(round) - 0;
                else if (!isLast) showValue = Math.floor(showValue);
                reduced -= showValue * factor;
                return showValue;
            });
        }
    };
    $.unitval = function(fac) {
        Factors = fac;
    }
    // Uses .val() in input/textarea and .text() in other fields.
    var uval = function() {
        return ['input','textarea'].indexOf(this[0].tagName.toLowerCase()) < 0 ?
                this.text.apply(this, arguments) : this.val.apply(this, arguments);
    }
  1. 我们的通用转换器很有用,但不太方便或用户友好;我们仍然必须手动进行所有转换。为了避免这种情况,我们将在元素上放置数据属性,表示它们显示的测量。在其中,我们将放置用于显示值和单位的单独元素。当我们设置测量单位时,函数setMeasurementUnits将在具有此数据属性的每个元素上设置它们。此外,它还将相应地调整内部值和单位元素:
// Sets the measurement units within a specific element.
// @param measurements An array in the format [{type:"measurement", units: ["unit", ...], round:N}]
// for example [{type:"height", units:["ft","inch"], round:0}]
    var setMeasurementUnits = function(measurements) {
        var $this = this;
        measurements.forEach(function(measurement) {
            var holders = $this.find('[data-measurement="'+measurement.type+'"]');
            var unconverted = holders.map(function() { return $(this).unitval(); })
            holders.attr('data-round', measurement.round);
            holders.find('[data-value-display]').each(function(index) {
                if (index < measurement.units.length)    
                    $(this).show().attr('data-unit', measurement.units[index]);
                else $(this).hide();
            });
            holders.find('[data-unit-display]').each(function(index) {
                if (index < measurement.units.length)    
                    $(this).show().html(measurement.units[index]);
                else $(this).hide();
            });

            holders.each(function(index) { $(this).unitval(unconverted[index]); });
        });
    };
  1. 由于每个元素都知道其测量和单位,因此我们现在可以简单地在其中放入 SI 值,并让它们显示转换后的值。为此,我们将编写unitval。它允许我们设置和获取“联合”值,或在具有data-measurement属性的元素上设置单位选项:
    $.fn.unitval = function(value) {
        if (value instanceof Array) {
            setMeasurementUnits.apply(this, arguments);
        }
        else if (typeof(value) == 'undefined') {
            // Read value from element
            var first       = this.eq(0),
                measurement = first.attr('data-measurement'),
                displays    = first.find('[data-value-display]:visible'),
                // Get units of visible holders.
                valunits = displays.toArray().map(function(el) {
                    return [uval.call($(el)), $(el).attr('data-unit')] });
            // Convert them from input
            return Convert.fromInput(measurement, valunits);
        }
        else if (!isNaN(value)) {
            // Write value to elements
            this.each(function() {
                var measurement   = $(this).attr('data-measurement'),
                    round         = $(this).attr('data-round'),
                    displays      = $(this).find('[data-value-display]:visible'),
                    units         = displays.map(function() {
                        return $(this).attr('data-unit'); }).toArray();
  var values = Convert.toOutput(measurement, value, units, round);
                displays.each(function(index) { uval.call($(this), values[index]); });
            });
        }
    }
}());

此插件将在下一节中解释。

它是如何工作的...

HTML 元素没有测量单位的概念。为了支持单位转换,我们添加了自己的数据属性。这些属性允许我们赋予某些元素特殊的含义,其具体内容由我们自己的代码决定。

我们的约定是,具有data-measurement属性的元素将用于显示指定测量的值和单位。例如,具有data-measurement="weight"属性的字段将用于显示重量。

此元素包含两种类型的子元素。第一种类型具有data-display-value属性,并显示测量的值(始终是一个数字)。第二种类型具有data-display-unit属性,并显示测量的单位(例如,"kg")。对于用多个单位表示的测量(例如,高度可以以“5 英尺 3 英寸”的形式表示),我们可以使用两种类型的多个字段。

当我们改变我们的单位制度时,setMeasurementUnits会向以下元素添加附加的数据属性:

  • data-round属性附加到data-measurement元素

  • 向包含适当单位的data-display-value元素添加了data-unit 属性

  • data-display-unit元素填充了适当的单位

因此,$.unitval()知道我们页面上每个测量元素上显示的值和单位。该函数在返回之前读取并将测量转换为 SI。我们所有的计算都使用 SI 单位。最后,当调用$.unitval(si_value)时,我们的值会在显示之前自动转换为适当的单位。

该系统通过识别只有在读取用户输入和显示输出时才真正需要转换时,最小化了容易出错的单位转换代码的数量。此外,数据驱动的方法允许我们完全从我们的代码中省略转换,并专注于我们的应用逻辑。

在用户的时区中显示格式化的日期

在这个示例中,我们将学习如何在用户的本地时区中格式化并显示日期;此外,我们还将看到 JavaScript 中如何使用和表示日期。最好的方法是让用户选择他们希望日期显示的时区,但不幸的是,这很少是一个选项。

准备工作

就像大多数编程语言一样,JavaScript 使用 Unix 时间。这实际上是一种表示给定时间实例的系统,即自 1970 年 1 月 1 日午夜以来经过了多少秒或在 JavaScript 的情况下是毫秒,通常称为协调世界时的时间。

注意

关于 UTC 的一些有趣的小知识:缩写是法语版本 Temps Universel Coordonné和英语版本协调世界时之间的妥协,法语版本将是 TUC,英语版本将是 CUT。

这个数字实际上并不完全符合 UTC,也没有考虑到闰秒等各种非典型情况,但在大多数情况下这是可以接受的。

在 JavaScript 中,我们有Date对象,可以以不同的方式构造:

new Date() // uses local time
new Date(someNumber) //create date with milliseconds since epoch
new Date(dateString) // create date from input string representation
new Date(year, month, day [, hour, minute, second, millisecond])

注意

请注意,在各种浏览器中,从字符串表示创建日期可能会有不同的行为,Date.parse方法解析字符串为日期也是如此。

在构造过程中,如果您提供了一些参数并省略了可选参数,它们将默认为零。还有一件事要注意的是,JavaScript 中的月份是基于零的,而日期不是。

注意

在 JavaScript 中,将Date对象作为函数而不是构造函数使用,使用new Date(...),将导致您获得该日期的字符串表示,而不是获得Date对象,这与大多数其他 JavaScript 对象的预期不同。

如何做...

  1. 您需要做的第一件事是创建Date对象:
  var endOfTheWorld= new Date(1355270400000);
  1. 然后,只需使用本地化的日期和时间表示:
    document.writeln(endOfTheWorld.toLocaleDateString());
    document.writeln(endOfTheWorld.toLocaleTimeString());
  1. 如果您需要知道用户时区与 UTC 之间的小时偏移量,可以使用以下代码:
var offset = - new Date().getTimezoneOffset()/60;
  1. 此偏移变量表示本地用户时区到 UTC 的小时数。这里的减号将逻辑反转为日期;这意味着差异将从日期到 UTC 而不是从 UTC 到日期。

它是如何工作的...

我们通常可以从服务器端返回毫秒表示,并在本地时区中格式化数字。因此,假设我们的 API 返回了毫秒1355270400000,实际上是 2012 年 12 月 12 日,也被称为世界末日日期。

日期的创建如下:

var endOfTheWorld= new Date(1355270400000);

在本地字符串中打印时,有一些可用的选项;其中之一是toLocaleDateString

   endOfTheWorld.toLocaleDateString()

此方法使用底层操作系统来获取格式约定。例如,在美国,格式为月/日/年,而在其他国家,格式为日/月/年。对于我们的情况,世界末日是在“2012 年 12 月 12 日星期三”。您还可以使用适当的getX方法手动构造打印日期。

还有一种打印本地时间的方法叫做toLocaleTimeString,可以用在我们的世界末日日期上。因为这种方法也为我们使用操作系统的本地时间,所以它是 01:00:00,因为我们处于 UTC+1 时区。对我们来说,这意味着我们有一个额外的小时可以活着;或者也许不是?

为了获取本地用户的偏移量,Date对象中有一个名为getTimezoneOffset()的方法,它返回日期到 UTC 的时区偏移量(以分钟为单位)。问题在于没有小时的方法,此外,它是反直觉的,因为我们通常想要知道从 UTC 到给定日期的差异。

还有更多...

如果处理日期是您的应用程序中常见的事情,那么使用一个库是有意义的,比如Moment.jsmomentjs.com/)。

Moment.js 提供了对国际化和更高级的日期操作的支持。例如,从当前日期减去 10 天只需使用以下代码即可完成:

moment().subtract('days', 10).calendar();

要从今天的开始时间获取时间,请使用以下代码:

moment().startOf('day').fromNow();

显示经过的动态时间

在每个主要网站上,通常都会有这些很棒的计数器,显示页面上各种元素的时间戳。例如,这可能是“您在 3 小时前打开了此页面”或“2 分钟前发表了评论”。这就是为什么,除了名称“动态经过的时间”,这个功能也被称为“时间过去”。

准备工作

我们将使用一个名为timeago的 jQuery 插件,专门为此目的设计,可以从timeago.yarp.com/获取。

如何做…

我们将创建一个简单的页面,其中我们将通过执行以下步骤显示经过的时间:

  1. 因为timeago是一个 jQuery 插件,我们首先需要包含 jQuery,然后添加timeago插件:
 <script src="img/jquery.min.js">
 </script>
 <script src="img/jquery.timeago.js" type="text/javascript"></script>
  1. 举个例子,添加以下 HTML:
        <p> Debian was first announced <abbr class='timeago' title="1993-08-16T00:00:00Z">16 August 1993</abbr>
          </p>
          <p> You opened this page <span class='page-opened' /> </p>
           <p> This is done use the time element
              <time datetime="2012-12-12 20:09-0700">8:09pm on December 12th, 2012</time>
          </p>
  1. 这将使我们能够对timeago插件提供的基本功能有一个概述。之后,让我们添加以下 JavaScript:
 $(document).ready(function() {
          jQuery.timeago.settings.allowFuture = true;
          var now= new Date();
          $(".timeago").timeago();
          $(".page-opened").text( $.timeago(now));
          $("time").timeago();
          //$("some-future-date") $.timeago(new Date(999999999999));
      });

就是这样;现在您有一个完全工作的时间示例,它将计算自给定日期以来的时间并更新它,另外,与page-opened选择的第二部分将随着用户在页面上花费更多时间而自动更新。

它是如何工作的…

您可能想知道的第一件事是关于abbrtime标签。实际上,第一个是“缩写”的表示,并且可以选择性地为其提供完整的描述。如果存在完整的描述,title属性必须包含此完整描述,而不包含其他内容。完整的描述通常在浏览器中显示为工具提示,但这是一个标准。为什么我们选择abbr标签来显示时间?嗯,有一个名为time的新的 HTML5 时间元素,围绕它有一些争议,因为它被从规范中删除,但后来又被重新添加。这个元素在语义上更正确,而且以机器可读的格式表示日期,可以被浏览器用来启用类似“添加到日历”的功能。使用abbr元素的理由只支持旧的浏览器,但随着时间的推移,这变得越来越不相关。目前,大多数现代桌面和移动浏览器都支持语义上正确的time元素,即使 IE 9+也支持它。

其余的 HTML 由标准的、众所周知的标签和一些标记组成,例如为了以后选择这些元素而添加的不同 CSS 类。

让我们来看看 JavaScript;首先我们使用标准的 jQuery 文档准备好函数:

$(document).ready(function() {

之后,我们将allowFuture的设置设置为true,以启用timeago插件与未来日期一起工作,因为这不是默认设置的:

jQuery.timeago.settings.allowFuture = true;

如果timeago直接应用于选定的abbrtime元素,则我们无需做任何其他操作,因为计算是自动完成的:

 $(".timeago").timeago();
 $("time").timeago();

您还可以注意到,我们可以直接从 JavaScript 中获取给定日期的文本,并以任何我们认为合适的方式处理它:

$(".page-opened").text( $.timeago(now));

还有更多...

在处理国际化和本地化应用程序时,会有一些问题。其中之一是timeago自动处理的时区支持。我们唯一需要确保的是我们的时间戳遵循ISO 8601en.wikipedia.org/wiki/ISO_8601)时间格式,并具有完整的时区标识符(en.wikipedia.org/wiki/ISO_8601#Time_zone_designators)。另一个经常出现的问题是语言支持,但在这方面我们大多数都有覆盖,因为有许多语言的本地化版本的插件,甚至您可以创建自己的版本并贡献给社区。要做到这一点,您可以使用github.com/rmm5t/jquery-timeago/tree/master/locales上托管的代码。

还有一些其他执行类似工作的实现,例如John Resigpretty date,可以在他的博客ejohn.org/blog/javascript-pretty-date/上找到。

显示数学

在技术写作方面,我们经常希望在页面内显示数学公式。过去,这是通过在服务器上从某种标记创建图像来完成的,甚至是手动使用外部程序创建图像。自 MathML 引入以来,这就不再需要了;这样可以节省我们在解决布局问题上的时间,并使浏览器原生支持显示方程式。在撰写本书时,尽管大多数功能的规范已经可用了几年,但并非所有主要浏览器都支持 MathML。

显示数学

准备工作

数学标记语言MathML)是一种应用程序描述公式的标准化方式,不仅旨在实现 Web 集成,还可用于其他应用程序。

W3C 维护了一个使用 MathML 的软件列表;可以在www.w3.org/Math/Software/找到。规范的几个修订是由工作组完成的(www.w3.org/Math/),最新的是第 3 版(www.w3.org/TR/MathML3/)。

HTML5 增加了在 HTML 内嵌入 MathML 文档的支持。

在这个配方中,我们要描述一个公式,如前面π的连分数,使用 MathML,其中我们有一个不同表示π的示例。

如何做...

  1. 我们将使用一个名为MathJax的库,可以从作者的 CDN 检索,也可以单独下载并包含在项目中。
<script type="text/javascript"
      src="img/MathJax.js?config=TeX-AMS-MML_HTMLorMML">
 </script>
  1. 我们可以通过添加以下 MathML 示例来继续:
<math >
       <mrow>
           <mi>π</mi>
         <mo>=</mo>
         <mfrac>
            <mstyle scriptlevel="0">
              <mn>3</mn>
            </mstyle>
            <mstyle scriptlevel="0">
               <mrow>
                 <mn>7</mn>
                 <mo>+</mo>
                 <mfrac numalign="left">
                   <mstyle scriptlevel="0">
                     <msup><mn>1</mn></msup>
                   </mstyle>
                 </mfrac>
               </mrow>
            </mstyle>
         </mfrac>
      </mrow>
    </math>

元素的基本含义将在后面解释,但您可以注意到,示例在很少的嵌套级别后变得非常庞大,很难阅读。这是因为 MathML 从未打算手动创建,而是作为某些应用程序的格式来使用。

  1. 那么,如果我们想启用可编辑的标记,对我们来说真正简单的选项是什么?嗯,最简单的选择是一种称为ASCIIMath的东西;为了启用它,我们需要改变请求中的config参数:
<script type="text/javascript" src="img/MathJax.js?config=AM_HTMLorMML-full"> </script>

通常我们使用所有可能的输入格式和呈现选项的版本,但这样我们会遇到 JavaScript 文件大小的问题。

那么,使用ASCIIMath有多简单呢?嗯,我们之前解释的表达式可以用一行显示:

 <p>
        `π = 3+1/(7+1/(15+1/(1+1/...)))`
 </p>

注意

请注意,表达式包含在[P37]"中,否则重音字符将被呈现为 HTML 和 CSS 或任何其他已配置的呈现方法。

还有更多...

ASCIIMath方法非常简单,并且在 Khan Academy(www.khanacademy.org/)和 Math StackExchange(math.stackexchange.com/)等主要网站上非常受欢迎。如果您有兴趣了解如何使用ASCIIMath,可以在其官方网页www1.chapman.edu/~jipsen/mathml/asciimath.html上获取更多信息。使用MathJax,您还可以呈现其他标记格式语言,如 Tex 和 Latex。

注意

Tex 是由Donald Knuth制作的排版格式,目的是帮助他撰写他的著名书籍。另一方面,Latex 是一种使用 Tex 作为排版格式的文档标记。有关它们的更多信息可以在en.wikipedia.org/wiki/TeXwww.latex-project.org/上找到。

创建一个无限滚动列表

无限滚动列表是由社交网络网站(如 Facebook 和 Twitter)推广的。它们的目标是营造整个可用内容已经加载的假象。此外,通过这种技术,用户试图找到下一页按钮而导致的正常滚动中断可以避免。

同时,我们也希望避免不必要的带宽浪费;这意味着一次加载整套数据不是一个选择。

解决方案是监视用户的滚动并检测页面底部的接近。当用户足够接近底部时,我们可以通过将其附加到当前显示内容的末尾来自动加载下一页的内容。

准备工作

您必须已经有一个按页面提供内容的服务。此示例默认情况下可以工作,但要使其完全功能,需要一个实际的 HTTP 服务器,以便 Ajax 请求下一页的工作。

如何做...

让我们编写 HTML 页面、CSS 样式和 JavaScript 代码。

  1. 创建一个名为index.html的文件,其中包含我们示例的完整 HTML、CSS 和 JavaScript 代码。我们需要在 HTML 文档中插入一个 DOCTYPE;否则,浏览器将以“怪癖模式”运行,高度测量函数$(window).height()将无法工作。
<!DOCTYPE HTML>

我们将在页面中添加一个内容占位符元素:

<div id="content"></div>
  1. 为了演示目的,我们将添加以下 CSS 代码以使页面可见。可以跳过这个 CSS:
div.page {
   min-height: 1200px;
   width: 800px;
   background-color:#f1f1f1;
   margin:0.3em;
   font-size: 3em;
}
div.error {
   color:#f00;
}
  1. 最后,我们添加 JavaScript 代码。首先加载 jQuery:
<script src="img/jquery.min.js">
</script>

然后我们可以添加我们的脚本:

<script type="text/javascript">
(function() {

我们的页面获取器使用 null 错误参数和一个简单的包含页面编号的字符串(例如Page 1)来调用回调函数,但它也可以执行 Ajax 请求。有关如何修改它以进行 Ajax 请求的更多信息,请参见以下代码。

这个函数人为地限制了 10 页的内容。第十页后,回调函数将带有错误调用,表示没有更多可用页面:

var page = 1;
function getPage(callback) {
   if (page <= 10)
       callback(null, 'Page ' + page);
   else
       callback("No more pages");
   page += 1;
};
  1. 我们使用triggerPxFromBottom来指定何时开始加载下一页。当只剩下triggerPxFromBottom像素要滚动时,将开始加载下一页。它的值设置为0;这意味着用户必须到达当前可见页面的末尾才能触发加载过程:
var currentlyLoading = false;
var triggerPxFromBottom = 0;
  1. loadNext将下一页附加到#content div 中。但是,如果回调函数带有错误调用,它将在页面的最后部分下方显示没有更多内容。错误事件发生后,将不再加载更多页面。这意味着当getPage返回错误时,我们的代码将停止加载新页面。这是期望的行为:
function loadNext() {
   currentlyLoading = true;
   getPage(function(err, html) {
        if (err) {
            $("<div />")
                .addClass('error')
                .html("No more content")
                .appendTo("#content");
        } else {
            $("<div />")
                .addClass('page')
                .html(html).appendTo("#content");
            currentlyLoading = false;
        }
      });
}
  1. 当页面以任何方式滚动时,将调用此事件处理程序。它计算剩余的滚动像素数。如果像素数足够小且代码当前未加载页面,则调用页面加载函数:
$(window).on('scroll', function() {
    var remainingPx = $(document).height()
        - $(window).scrollTop()
        - $(window).height();
    if (remainingPx <= triggerPxFromBottom
        && !currentlyLoading)
        loadNext();
});
  1. 最后,我们第一次调用loadNext()来加载第一页:
loadNext();
}());
</script>

它是如何工作的...

浏览器的可见区域(也称为视口)具有自己的尺寸,可以通过调用 jQuery 的$.fn.height()函数来获取$(window)对象的高度。另一方面,$(document).height()为我们提供页面整个内容的高度。最后,$(window).scrollTop()给出滚动偏移量。

使用这些函数,我们可以计算剩余需要滚动的像素。然后,我们在用户滚动页面时重新计算和检查这个值。如果值足够小,我们调用我们的加载函数。同时,我们确保在当前加载过程完成之前停止加载新页面。(否则,用户的滚动操作可能会在等待内容加载时加载更多页面。)

还有更多...

这是getPage函数的一个可能的 Ajax 实现。该函数向在相同域上托管的请求处理程序发送 Ajax 请求,路径为/pages/<number>,以检索下一页的 HTML 内容:

function getPage(cb) {
    $.get('/pages/' + page)
        .success(function(html) { cb(null, html); })
        .error(function() { cb("Error"); }
    page += 1;
}

要使此版本工作,您需要在服务器端代码中实现请求处理程序。

您的服务器端代码可以返回错误,比如 404,表示没有更多的内容可用。因此,jQuery 永远不会调用我们的成功回调,我们的代码将停止加载新页面。

无限滚动列表配方提供了很好的用户体验,但它有一个重大缺点。我们必须确保contents元素下面没有重要的页面内容。这意味着放在底部的页面元素(通常是页脚链接和版权信息)可能无法到达。

创建一个可排序的分页表

在创建网站时,我们遇到的最常见任务之一是显示列表和表格。大多数技术都侧重于服务器端的排序、分页和数据呈现。我们的解决方案完全在客户端,适用于小到中等数量的数据。客户端解决方案的主要好处是速度;排序和切换页面将几乎是瞬间完成的。

在这个配方中,我们将创建一个客户端可排序的分页表。

准备工作

我们假设一个服务以 JSON 对象的形式提供数据,其中包含一个data属性,该属性是一个数组的数组:

{data:[["object1col1", "object1col2"], ["object2col1", "object2col2"],  …]}

在我们的示例中,我们将显示附近的人员列表。表中的每个人都有自己的 ID 号码、姓名、年龄、与我们的距离和交通方式。

我们将以公里为单位显示距离,并希望能够按姓氏对人员列表进行排序。

随着表格显示问题迅速超出最初的简单问题,我们不打算构建自己的解决方案。相反,我们将使用可在datatables.net/上获得的出色的 jQuery DataTables 插件。

提示

下载示例代码

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

如何做...

让我们编写 HTML 页面、CSS 样式和 JavaScript 代码。

  1. 首先,我们将创建一个包含空表的 HTML 页面。我们还将添加一些 CSS 来导入表的基本 DataTables 样式。样式表通常随 DataTables 分发。我们的index.html文件如下:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Sortable paged table</title>
        <style type="text/css">
            @import "http://live.datatables.net/media/css/demo_page.css";
            @import "http://live.datatables.net/media/css/demo_table.css";
            #demo, #container {
                width:700px;
            }
            #demo td {
                padding: 0.2em 2em;
            }
            #demo_info {
                width:690px;
                height:auto;
            }
        </style>
    </head>
    <body>
        <div id="container">
            <table id="demo">
                <thead>
                    <tr>
                        <th>Id</th><th>Name</th><th>Age</th><th>Distance</th><th>Transportation</th>
                    </tr>
                </thead>
                <tbody>
                </tbody>
            </table>
        </div>
        <script src="img/jquery.min.js"></script>
        <script type="text/javascript" src="img/jquery.dataTables.min.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>

示例包括一个链接到官方网站上托管的 DataTables 的缩小版本。

DataTables 插件在表格下方附加了pagerinfo元素。因此,我们需要将表格包装在一个container元素内。

  1. example.js文件如下:
(function() {
    $.extend($.fn.dataTableExt.oSort, {
        "lastname-sort-pre": function (a) {
            return a.split(' ').reverse().join(' ');
        },
        "lastname-sort-asc": function(a, b) { return a < b ? -1 : a > b ? 1 : 0; },
        "lastname-sort-desc": function(a, b) { return a > b ? -1 : a < b ? 1 : 0; },
        "unitnumber-pre": function(a) { return new Number(a.split(' ')[0]); },
        "unitnumber-asc": function(a, b) { return a - b; },
        "unitnumber-desc": function(a, b) { return b - a; }
    } )
    var fetchData = function(callback) {
        var data = [
            [1,'Louis Garland', 12, 32, 'Walking'],
            [2,'Misty Lamar',32, 42, 'Bus'],
            [3,'Steve Ernest',32, 12, 'Cycling'],
            [4,'Marcia Reinhart',42, 180, 'Bus'],
            [5,'Lydia Rouse',35, 31, 'Driving'],
            [6,'Sean Kasten',80,42, 'Driving'],
            [7,'Patrick Sharkey',65,43, 'Cycling'],
            [8,'Becky Rashid',63, 51, 'Bus'],
            [9,'Michael Fort',34, 23, 'Walking'],
            [10,'Genevieve Blaine',55, 11, 'Walking'],
            [11,'Victoria Fry',58, 14, 'Walking'],
            [12,'Donald Mcgary',34, 15, 'Cycling'],
            [13,'Daniel Dreher',16, 23, 'Walking'],
            [14,'Valerie Santacruz',43, 35, 'Driving'],
            [15,'Jodi Bee',23, 13, 'Walking'],
            [16,'Jo Montana',14, 31, 'Cycling'],
            [17,'Stephanie Keegan',53, 24, 'Driving'],
            [18,'Philip Dewey',12, 29, 'Cycling'],
            [19,'Jack Clemons',11, 44, 'Walking'],
            [20,'Steve Serna',14, 60, 'Cycling']
        ];
        callback({data:data});
    };
    window.myTable = {};
    var table = window.myTable.table = $("#demo").dataTable({
        'bLengthChange': false, 'bFilter': false,
        'iDisplayLength': 10,
        'aoColumnDefs':[{
            aTargets: [3], // distance
            mRender: function(data) { return data + ' km'; },
            sType: 'unitnumber'
        }, {
            aTargets: [1],
            sType: 'lastname-sort'
        }]
    });
    var setData = window.myTable.setData = function(data) {
        table.fnClearTable();
        table.fnAddData(data);
        table.fnDraw();
    };

    fetchData(function(result) {
        window.myTable.data = result.data;
        setData(result.data);
    });

}());

示例中fetchData的实现提供了硬编码的示例数据。您可以轻松地将其替换为对您的服务的请求。setData函数是一个方便的函数,用于更改表数据——我们将使用相同的脚本,该脚本将调用此函数来设置其自己的数据,用于多个示例。最后,其余的代码是特定于 DataTables,并将在下一节中进行解释。

它是如何工作的...

以下图片显示了生成的表格:

它是如何工作的...

要初始化表格,我们使用dataTable初始化函数。我们可以将多个选项传递给函数。例如,我们可以通过将iDisplayLength属性的值设置为10来指定每页 10 个项目。

因为我们要对Distance列(第 3 列)进行稍微不同于仅显示的渲染,所以我们在aoColumnDefs选项中为目标列 3 添加了一个项目,为该列设置了一个自定义渲染函数。这是一个简单地将km字符串附加到我们的数字的函数;但我们也可以使用更复杂的函数(涉及自定义日期格式化、单位转换等)。

分页在 DataTables 中自动工作——插件附加了一个分页控件,提供对上一页/下一页的访问。排序也大部分自动工作。然而,在我们的特定示例中,尽管Name列以"firstname lastname"的格式显示,但我们需要对其进行特殊排序(按姓氏)。为此,我们为该列指定了一个名为lastname-sort的自定义排序类型。我们还为Distance列指定了一个特殊的排序类型,称为unitnumber

DataTables 允许我们定义自定义排序类型作为插件。自定义排序器具有以下属性:

  • 在将其传递给排序器之前,对列值进行预处理的预处理函数

  • 一个升序排序函数,根据传递的两个参数的值返回一个值:如果第一个值较小,则返回-1,如果它们相等,则返回 0,如果第一个值较大,则返回 1

  • 一个与升序排序函数类似的降序排序函数

这些属性使我们能够按照Name列的姓氏进行排序,以及按照Distance列的数字进行排序。

还有更多...

这里是fetchData函数的一个简单 Ajax 替代,向托管在同一域上路径为/people的请求处理程序发送一个 Ajax 请求以检索数组数据:

function fetchData(cb) {
    $.get('/people/').success(cb);
}

请注意,这种解决方案对于大型数据集效果不佳。虽然现代客户端具有处理大量数据的性能,但带宽也是一个考虑因素。在使用此解决方案之前,应仔细考虑带宽要求和目标客户端(桌面或移动)。

创建多选过滤器

在显示表格时的一个常见任务是将表格中的数据过滤为满足某些条件的子集。多选表格过滤器适用于具有有限数量值的列。例如,如果我们有一个包含某些人的数据的表,其中一列是人员使用的交通方式,则在该列上使用的过滤器将是多选过滤器。用户应该能够选择一个或多个交通方式,表视图将显示所有使用所选方式的人员。

准备就绪

我们将假设我们正在使用上一个示例中的代码和数据。我们有一个人员列表,他们的交通方式显示在一个可排序、分页的表中,使用 DataTables jQuery 插件。我们将复制上一个示例中的文件,然后对其进行补充。

我们需要过滤的数据已经在tableData全局变量中可用;我们可以过滤这些数据,然后使用全局的tableSetData函数来显示过滤后的表格。

过滤将在交通字段上进行。

如何做...

让我们修改上一个代码,向我们的表格添加多选过滤器:

  1. 在上一个配方的index.html文件中,在开头的<body>标签后添加一个多选选择列表:
<select id="list" style="width:100px;"  multiple>
</select>
  1. 在关闭</body>标签之前为filter.js添加一个脚本元素:
<script type="text/javascript" src="img/filter.js"></script>
  1. 我们还将修改example.js末尾的fetchData调用,以触发自定义事件,通知任何观察者数据已经被获取并设置:
$(function() {
    fetchData(function(result) {
        window.myTable.data = result.data;
        setData(result.data);
        $("#demo").trigger("table:data");
    });
});

代码被包装以在页面加载后执行,以便事件触发工作。在页面加载之前,无法触发任何事件。

  1. 创建一个名为filter.js的文件,并添加以下代码:
(function() {
    function getUnique(data, column) {
        var unique = [];
        data.forEach(function(row) {
            if (unique.indexOf(row[column]) < 0) unique.push(row[column]); });
        return unique;
    }

    function choiceFilter(valueList, col) {
        return function filter(el) {
            return valueList.indexOf(el[col]) >= 0;
        }
    }
    $("#demo").on('table:data', function() {
        getUnique(window.myTable.data, 4).forEach(function(item) {
            $("<option />").attr('value', item).html(item).appendTo("#list");
        });
    })
    $("#list").change(function() {
        var filtered = window.myTable.data.filter(
                choiceFilter($("#list").val(), 4));
        window.myTable.setData(filtered);
    });
}());

工作原理...

实现多选过滤器的最简单方法是使用多选选择元素。

当数据可用时,我们还需要填充元素。为此,我们在获取数据后触发我们的新自定义事件table:data。监听器从数据的交通列中提取唯一值,并用这些值为选择列表添加选项。

当选择发生变化时,我们提取所选值(作为数组),并使用choiceFilter创建一个新的过滤函数,这是一个高阶函数。高阶函数返回一个新的过滤函数。这个过滤函数接受一个表行参数,并在该行的第四列的值包含在指定列表中时返回true

过滤函数被传递给Array.filter;它将此函数应用于每一行,并返回一个仅包含过滤函数返回true的行的数组。然后显示过滤后的数据,而不是原始数据。

创建范围过滤器

表格也可以通过其数字列进行过滤。例如,给定一个表格,其中每一行都是一个人,其中一列包含有关该人年龄的数据,我们可能需要通过指定年龄范围来过滤该表格。为此,我们使用范围过滤器。

准备工作

我们将假设我们正在使用创建可排序的分页表配方中的代码和数据。我们有一个人员名单,他们的年龄显示在一个可排序的分页表中,使用 DataTables jQuery 插件。我们将从配方中复制文件,然后添加一些额外的过滤代码。

我们需要过滤的数据已经在tableData全局变量中可用;我们可以过滤这些数据,然后使用tableSetData全局函数来显示过滤后的表格。

过滤将在年龄字段上进行。

如何做...

让我们修改上一个代码,向我们的表格添加范围过滤器:

  1. 在上一个配方的index.html文件中,在开头的<body>标签后添加两个输入元素:
 Age: <input id="range1" type="text">
 to <input id="range2" type="text"> <br>       
  1. 在关闭</body>标签之前为filter.js添加一个脚本元素:
<script type="text/javascript" src="img/filter.js"></script>
  1. 最后,我们创建我们的filter.js脚本:
(function() {
    function number(n, def) {
        if (n == '') return def;
        n = new Number(n);
        if (isNaN(n)) return def;
        return n;
    }
    function rangeFilter(start, end, col) {
        var start = number(start, -Infinity),
            end = number(end, Infinity);
        return function filter(el) {
            return start < el[col] && el[col] < end;
        }
    }
    $("#range1,#range2").on('change keyup', function() {
        var filtered = window.myTable.data.filter(
            rangeFilter($("#range1").val(), $("#range2").val(), 2));
        window.myTable.setData(filtered);
    });
}());

工作原理...

过滤数组数据的最简单方法是使用 JavaScript 内置的Array.filter函数。这是一个高阶函数;它的第一个参数是一个函数,它接受一个行参数,并在行应该添加到过滤后的数组时返回true,或者在行应该被排除时返回false

为了提供这样的功能,我们创建自己的高阶函数。它接受开始和结束范围以及指定的列。返回结果是一个过滤每一行的函数。

为了忽略输入中的空值或无效值,我们使用number函数。如果输入字段为空或包含非数字数据,则提供默认值(范围的开始为-Infinity,结束为+Infinity)。这也使我们能够进行单侧范围过滤。

Array.filter函数返回通过过滤器的所有元素的数组。我们在表格中显示这个数组。

创建组合复杂过滤器

在显示表格时,我们有时希望使用涉及多个列的多个条件来过滤表格元素。例如,给定一个包含人员信息的人员表,例如他们的姓名、年龄和交通方式,我们可能只想查看年龄大于 30 岁且使用公交车交通的人。我们可能还想按姓名过滤人员。为此,我们必须同时对数据应用多个过滤器,例如年龄范围过滤器、多选过滤器和文本过滤器。这样做的最简单方法是创建一个过滤器组合函数。

准备工作

我们假设我们正在使用创建可排序的分页表配方中的代码,并且我们将根据前两个配方中描述的方式添加我们的过滤器。这次我们将允许组合过滤器。

如何做...

让我们修改前面的代码,向我们的表格添加多个过滤器:

  1. 在开头的<body>标签后,我们将在页面中添加与过滤相关的输入:
<select id="list" style="width:100px;"  multiple>
</select>
Age: <input id="range1" type="text">
to <input id="range2" type="text">,
Name: <input type="text" id="name"> <br>
  1. 在关闭</body>标签之前添加filter.js脚本:
<script type="text/javascript" src="img/filter.js"></script>
  1. 我们将修改example.js,在页面加载后获取数据并在显示数据后触发table:data事件:
    $(function() {
        fetchData(function(data) {
            window.myTable.data = data;
            setData(data);
            $("#demo").trigger("table:data");
        });
    });
  1. 然后我们可以通过组合前两个配方中的代码来创建filter.js
(function() {
    function getUnique(data, column) {
        var unique = [];
        data.forEach(function(row) {
            if (unique.indexOf(row[column]) < 0)
                unique.push(row[column]);
        });
        return unique;
    }
    function choiceFilter(valueList, col) {
        return function filter(el) {
            return valueList.indexOf(el[col]) >= 0;
        }
    }
    function number(n, def) {
        if (n == '') return def;
        n = new Number(n);
        if (isNaN(n)) return def;
        return n;
    }
    function rangeFilter(start, end, col) {
        var start = number(start, -Infinity),
            end = number(end, Infinity);
        return function filter(el) {
            return start < el[col] && el[col] < end;
        };
    }
    function textFilter(txt, col) {
        return function filter(el) {
            return el[col].indexOf(txt) >= 0;
        };
    }
    $("#demo").on('table:data', function() {
        getUnique(window.myTable.data, 4)
        .forEach(function(item) {
            $("<option />").attr('value', item)
                .html(item).appendTo("#list");
        });
    });
    var filters = [null, null, null];
    $("#list").change(function() {
        filters[0] = choiceFilter($("#list").val(), 4);
        filterAndShow();
    });
    $("#range1,#range2").on('change keyup', function() {
        filters[1] = rangeFilter($("#range1").val(),
            $("#range2").val(), 2);
        filterAndShow();
    });
    $("#name").on('change keyup', function() {
        filters[2] = textFilter($("#name").val(), 1); filterAndShow();
    });
    function filterAndShow() {
        var filtered = window.myTable.data;
        filters.forEach(function(filter) {
            if (filter) filtered = filtered.filter(filter);
        });
        window.myTable.setData(filtered);
    };
}());

它是如何工作的...

与之前的配方一样,我们使用Array.filter函数来过滤表格。这次我们连续应用多个过滤器。我们将所有过滤函数存储在一个数组中。

每当输入发生变化时,我们更新适当的过滤函数,并重新运行filterAndShow()来显示过滤后的数据。

还有更多...

DataTables 是一个高度灵活的表格库,具有许多选项和丰富的 API。更多信息和示例可以在官方网站www.datatables.net/上找到。

在 HTML 中显示代码

在 HTML 中显示代码或甚至在 HTML 中显示 HTML 代码是一种常见需求,特别是在技术文档或博客中。这已经做过太多次,通过从格式化代码中获取图像并将其作为页面的一部分。图像中的代码可能不会被搜索引擎捕捉到。此外,它可能限制我们到特定的页面布局或屏幕尺寸,而在今天的移动革命中,这不是一个选择。

准备工作

这个配方的唯一要求是要显示的数据需要被正确转义;这意味着<p>awesome </p>需要被转换为&lt;p&gt;awesome &lt;/p&gt;。这可以在服务器端完成,也可以在保存之前进行转义。

如何做...

  1. 我们将使用Google 代码美化,因为在发言时,这个库在任何 CDN 上都不完全可用;你可以从code.google.com/p/google-code-prettify/获取它。

  2. 之后,我们可以在<pre /> <code />块中添加转义代码:

<body onload="prettyPrint()">
     <div>
          <pre class="prettyprint">
            <code>
              SELECT *
              FROM Book
              WHERE price &lt; 100.00
              ORDER BY name;
            </code>
          </pre>
        </div>
</body>
  1. 这两个标签中的任何一个都必须包含prettyprint CSS 类。除此之外,我们还需要包含onload="prettyPrint()"属性。

  2. 还有一个选项,可以从 JavaScript 中添加的其他事件监听器中调用prettyPrint函数:

<script>
       window.addEventListener('load', function (e){
          prettyPrint();
       }, false);
       </script>

它是如何工作的...

prettyprint类会自动选择所有标记有适当 CSS 类的块,并自动检测所使用的编程语言,然后进行高亮显示。

词法分析器应该适用于大多数语言;在常见语言中,有特定语言的自定义脚本,例如基于 lisp 的语言。

还有更多...

因为prettyprint自动检测源语言,如果我们想要获得更好的结果,我们可以自行指定。例如,如果我们想要显示 XML,代码将如下所示:

<pre class="prettyprint"><code class="language-xml">...</code></pre>

大多数常见语言都有 CSS 类。

prettyprint是其中一个较旧的可用脚本,还有一些替代方案可以提供更多的自定义选项和更好的 JavaScript API。

其中一些,如SyntaxHighliger (alexgorbatchev.com/SyntaxHighlighter/),Rainbow (craig.is/making/rainbows),和Highlight.js (softwaremaniacs.org/soft/highlight/en/),通常可以在大多数网站上找到。

渲染 Markdown

Markdown 是一种流行的轻量级标记语言。这种语言类似于维基标记(在维基百科上使用),强调简单性。它的主要目的是让用户编写纯文本并获得格式化的 HTML 输出。因此,它被流行的网站使用,如 Reddit、Stack Overflow、GitHub,以及各种论坛,作为不太直观的 BBCode 格式的替代品。

Markdown 是为我们的用户启用格式化文本输入的最快方式,而无需将完整的 HTML 编辑器嵌入页面。有多个库可以渲染 markdown;在这个示例中,我们将使用简单的markdown-js脚本来实时渲染 markdown。

如何做...

渲染 markdown 非常简单。一个最简单的例子如下:

<!DOCTYPE HTML>
<html>
    <head>
        <title>Render markdown</title>
        <style type="text/css">
            #markdown, #render { width: 48%; min-height:320px; }
            #markdown { float: left; }
            #render { float: right; }
        </style>
    </head>
    <body>
        <textarea id="markdown">
# Markdown example.
This is an example of markdown text. We can link to [Google](http://www.google.com)
or insert Google's logo:
![Google Logo](https://www.google.com/images/srpr/logo3w.png)

## Text formatting
We can use *emphasis* or **strong** text,
> insert a quote
etc.</textarea>
        <div id="render"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/markdown.js"></script>
        <script type="text/javascript">
            function rendermd(val) { $("#render").html(markdown.toHTML($("#markdown").val())); }
            $("#markdown").on('keyup', rendermd); $(rendermd);
        </script>
    </body>
</html>

它是如何工作的...

当页面加载时,textarea元素中的 markdown 文本将被渲染到右侧的#render元素中。每次按键都会导致脚本更新渲染的元素。

还有更多...

从官方网站daringfireball.net/projects/markdown/了解更多关于 markdown 格式的信息。

自动更新字段

这些天,在字段上自动更新是很常见的,其中一个部分是给定选择的结果,或者显示给定的图像或文本块。其中一个例子是密码强度计算;例如,在谷歌上搜索“货币转换器”会在结果中显示一个框,你可以在其中进行美元和欧元之间的货币转换。以这种方式链接字段是有意义的,当我们有两个或更多逻辑上相关的字段,或者一个是另一个的结果形式时。

为了演示这一点,我们将创建一个温度转换器,其中更新一个字段将导致另一个字段的更改,因为这些值是相关的。

准备工作

对于这个示例,我们只需要对 jQuery 有基本的了解,并且有一个简单的公式来在摄氏度和华氏度之间进行转换:

Celsius = (Fahrenheit -32) x (5/9)

或者:

Fahrenheit = Celsius  x(9/5) +32

如何做...

  1. 首先,我们将创建 HTML 部分,并创建两个将自动更新并添加适当标签的输入字段:
<div>
<label for='celsius'>C&deg;</label>
<input id='celsius' type='text' /> =
<label for='fahrenheit'>F&deg;</label>
<input id='fahrenheit' type='text' />
</div>
  1. 之后,我们必须确保已经包含了 jQuery:
<script src="img/jquery.min.js"> </script>
  1. 接下来,我们可以添加处理字段之间绑定的脚本:
$(document).ready(function() {
  $('#celsius').keyup(function(data) {
  var celsius = new Number(data.currentTarget.value);
  var farenheit =celsius *(9/5) + 32;
    $('#farenheit').val(farenheit);
    });
   $('#farenheit').keyup(function(data) {
       var farenheit = new Number(data.currentTarget.value);
    var celsius =(farenheit-32)*(5/9);
     $('#celsius').val(celsius);
     });
        });

这将连接并自动计算温度的前后。

它是如何工作的...

首先让我们看一下显示部分,这里没有什么特别的;我们使用一个简单的文本输入类型,并为每个字段添加适当的标签。此外,我们可以使用转义字符&deg;来显示度字符。

如果我们看一下 jQuery keyup事件,我们会发现它在用户释放键盘上的键时执行。这个事件可以附加在任何 HTML 元素上,但只有在元素处于焦点时才会起作用;因此,它在输入元素上使用起来更有意义。由于keyup事件有一个选项来执行一个将接受事件对象的函数,所以对于我们的情况,它如下所示:

$('#celsius').keyup(function(event) {

event对象中,我们可以访问触发事件的元素并访问其值:

event.currentTarget.value

之后,我们可以进行计算(摄氏度(9/5) + 32*)并将结果设置为另一个元素的值,以便在华氏度中显示:

$('#fahrenheit').val(fahrenheit);

由于我们希望绑定可以双向工作,我们也可以在华氏度的输入字段上做同样的事情:

$('#farenheit').keyup(function(event) {

当然,你需要使用适当的公式(华氏度-32)(5/9)*)来返回到摄氏度。

还有更多...

这个食谱展示了如何简单地使用 jQuery event来实时更新输入文本,它也可以用于创建自动完成框或功能,比如谷歌的即时搜索。这里的想法是,我们可以并且应该为各种 HTML 元素使用单向或双向绑定,特别是当我们谈论派生数据或数据是同一来源的表示时。

第二章:图形数据的显示

在本章中,我们将涵盖许多常见的图形任务,例如:

  • 创建折线图

  • 创建柱状图

  • 创建饼图

  • 创建面积图

  • 显示组合图表

  • 创建气泡图

  • 显示带有标记位置的地图

  • 显示带有路径的地图

  • 显示仪表

  • 显示树

  • 使用 Web 字体的 LED 记分牌

介绍

在本章中,我们将介绍使用基于现代 HTML5 标准的各种 JavaScript 库显示图形数据。主要目的是让您对从 2D 图形到 SVG 数据驱动文档的各种视觉部分感兴趣,并通过解决问题的示例来帮助您。

创建折线图

线图是最基本的图表类型。它们通过线连接在一起显示一系列数据点。线图通常用于可视化时间序列数据。

有各种库实现这种图表功能,有付费的也有免费的。我们将使用Flot图表库。它是免费的,简单易用,过去 4 年来一直在积极开发。它还旨在产生美观的图表。

在这个示例中,我们将制作一个时间序列图表,显示过去 24 小时的室外温度历史。

准备工作

我们需要从官方网站www.flotcharts.org/下载 Flot,并将内容提取到一个名为flot的单独文件夹中。

操作步骤...

让我们编写 HTML 和 JavaScript 代码。

  1. 创建一个包含图表占位符的基本 HTML 页面。我们还将包括 jQuery(Flot 所需)和 Flot 本身。Flot 需要在占位符 div 中绘制图表画布,因此我们将提供一个。图表占位符需要指定其宽度和高度,否则 Flot 将无法正确绘制:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
    </head>
    <body>
        <div id="chart" style="height:200px; width:800px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.flot.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. example.js中添加绘制图表的代码。getData函数生成一些看起来很真实的随机数据,您可以轻松地用一个从服务器获取数据的函数替换它。数据需要以两个元素数组的形式返回。在这对中,第一个(x 轴)值是标准的 UNIX 时间戳(以毫秒为单位),通常在 JavaScript 中使用,而第二个(y 轴)值是温度。

  2. 绘制图表非常简单。$.plot函数在指定的占位符中绘制包含指定图表选项的指定系列的图表:

$(function() {    
    function getData(cb) {
        var now  = Date.now();
        var hour = 60 * 60 * 1000;
        var temperatures = [];
        for (var k = 24; k > 0; --k)
            temperatures.push([now - k*hour,
                Math.random()*2 + 10*Math.pow((k-12)/12,2)]);
        cb({data:temperatures});
    }
    getData(function(data) {
        $.plot("#chart", [data], {xaxis: {mode: 'time'}});
    });
});

就是这样!以下是最终的结果:

操作步骤...

它是如何工作的...

$.plot函数接受三个参数:

  • 占位符选择器。这是 Flot 将绘制图表的地方。

  • 要绘制的系列数组。Flot 可以同时在同一图表上绘制多个系列。每个系列都是一个对象,至少必须包含data属性。该属性是一组两个元素数组,它们是系列的 x 和 y 值。其他属性允许我们控制特定系列的绘制方式-这些将在下一个示例中更详细地探讨。默认情况下,Flot 使用预设颜色绘制常规线图。

  • 一个包含广泛的图表绘制选项的options对象,用于图表标签、轴、图例和网格。这些选项也将在下一个示例中探讨。

在这个示例中,我们为 x 轴指定了“时间”模式。这会导致 Flot 在我们的轴上适当地标记小时、天、月或年(取决于数据的时间跨度)。

还有更多...

以下是getData函数的简单 Ajax 替代,发送一个 Ajax 请求到同一域上的路径/chart上托管的请求处理程序以检索图表数据:

function getData(cb) {
    $.get('/chart').success(cb);
}

创建柱状图

与通常用于显示平均值或瞬时值的折线图不同,条形图用于可视化属于离散组的数据。例如每日、每月和每周的销售量(组是天、月和周),每个用户的页面访问量,每辆车的燃料消耗等。

Flot 图表库还可以绘制条形图。在这个示例中,我们将可视化过去七天的每日销售量。我们还将分别显示来自不同产品的销售量,堆叠在彼此之上。

准备工作

我们需要从官方网站www.flotcharts.org/下载 Flot,并将内容提取到名为flot的单独文件夹中。

如何做...

让我们修改折线图代码,以绘制我们的柱状图。

  1. 首先,我们将复制上一个折线图示例中的相同 HTML 页面,但是我们会做一些更改。为了绘制堆叠条形图,我们需要堆叠插件,它位于jquery.flot.stack.js文件中。图表占位符的高度增加以获得对各个堆叠条形图的更好概览:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
    </head>
    <body>
        <div id="chart" style="height:300px; width:800px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.flot.js"></script>
        <script src="img/jquery.flot.stack.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 然后我们将创建example.js脚本:
$(function() {    
    var day = 24 * 60 * 60 * 1000;
    function getData(cb) {
        var now  = new Date();
        now = new Date(now.getYear(), now.getMonth(), now.getDate()).getTime();
        var products = [];
        for (var product = 1; product < 4; ++product) {
            var sales = { label: "Product " + product, data: [] };
            for (var k = 7; k > 0; --k)
                sales.data.push([now - k*day, Math.round(Math.random()*10)]);
            products.push(sales);
        }
        cb({series:products});
    }

    getData(function(data) {
        $.plot("#chart", data.series, {
            series: {
                stack: true, lines: { show: false },
                bars: { show: true, barWidth: 0.8 * day, align:'center' }
            }, xaxis: {mode: 'time'}
        });
    });
});

代码在下一节中进行了解释。以下是生成的图表的外观:

如何做...

它是如何工作的...

与以前的示例一样,$.plot函数接受三个参数。第一个参数是图表占位符,第二个是数据,第三个是包含图表选项的对象。

以下是我们输入数据的方案:

[
  {label: "Product 1", data:[
    [timestamp, value],
    [timestamp, value], …]},
  {label: "Product 2", data: […]},
  {label: "Product 3", data: […]}
]

输入数据是一个系列的数组。每个系列代表一个产品的销售情况。系列对象有一个label属性表示产品,以及一个data属性,它是一个数据点的数组。每个数据点都是一个二维数组。此数组的第一个元素是日期,表示为以毫秒为单位的 UNIX 时间戳——即当天的确切开始。第二个元素是当天的销售数量。

为了更轻松地操作日期,我们定义一个表示一天中毫秒数的变量。稍后,我们将使用此变量来定义图表中条形的宽度。

Flot 会自动从预定义列表中为我们选择系列颜色(但是,我们也可以指定我们需要的颜色,我们将在下面的示例中看到)。

代码中指定了几个系列选项。我们通过将stack属性的值设置为true来告诉 Flot 堆叠我们的系列。我们还确保隐藏了默认情况下会显示的线条。

为了使柱形图的中心与日期的 x 轴刻度对齐,我们将bar对象中的align属性的值设置为center

我们输入数据中的每个系列都有一个标签。因此,Flot 会自动生成一个放置在右上角的图例。

Flot 会自动选择轴的边界,但可以使用options对象来控制它们。

创建饼图

当可视化比例或百分比作为一个整体时,通常使用饼图。饼图足够简单,可以自己绘制;但是,为了获得更灵活和美观的结果,我们将使用 Flot 图表库及其饼图插件。

Flot 的饼图插件可以显示带有图例或不带图例的饼图,并具有广泛的选项来控制标签的位置。它还能够渲染倾斜的饼图和甜甜圈图。还包括交互式饼图的支持。

在这个示例中,我们将制作一个关于访问者浏览器的饼图。

准备工作

我们需要从官方网站www.flotcharts.org/下载 Flot,并将内容提取到名为flot的单独文件夹中。

如何做...

让我们编写 HTML 和 JavaScript 代码。

  1. index.html中创建以下 HTML 页面:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
    </head>
    <body>
        <div id="chart" style="height:600px; width:600px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.flot.js"></script>
        <script src="img/jquery.flot.pie.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>

页面中有一个图表的占位符元素。

Flot 依赖于包含的 jQuery 库。要绘制饼图,我们需要添加 Flot 的饼图插件。

  1. 创建example.js脚本:
$(function() {    
    var day = 24 * 60 * 60 * 1000;
    function getData(cb) {
        var browsers = [
            {label: 'IE', data: 35.5, color:"#369"},
            {label: 'Firefox', data: 24.5, color: "#639"},
            {label: 'Chrome', data: 32.1, color: "#963"},
            {label: 'Other', data: 7.9, color: "#396"}
        ];
        cb(browsers);
    }

    getData(function(data) {
        $.plot("#chart", data, {
        series: {
            pie: {
                show: true,
                radius: 0.9,
                label: {
                    show: true,
                    radius: 0.6,
                },
                tilt: 0.5
            }
        },
        legend: { show: false }
        });
    });
});

它生成以下饼图:

如何操作...

工作原理...

Flot 要求饼图切片数据以对象数组的形式提供。每个对象包含以下两个属性:

  • label:这是切片的标签

  • data:这是切片的编号——一个可以是任何值的数字(不需要是百分比)

在调用$.plot时,第一个参数是饼图的占位符元素,第二个是饼图切片的数组,第三个包含饼图选项。

为了显示饼图,最小的options对象如下:

{pie: {show: true}}

自定义默认饼图,我们使用以下内容添加到pie属性中:

  • radius:指定饼图的大小,以画布的百分比表示。

  • labelshow(布尔值)属性设置为true以显示饼图标签,radius属性控制标签与饼图中心的距离。

  • tilt:这会对饼图进行 3D 倾斜。如果省略,Flot 将渲染一个无标题的圆形饼图。

还有更多...

还有更多可用的选项,例如以下内容:

  • innerRadius:将其设置为值,例如0.5,以创建一个圆环图。

  • combine:此属性用于将较小的切片合并为单个切片。它是一个包含以下属性的对象:

  • threshold:设置为整体的百分比,例如,0.1

  • color:这是用于渲染“其他”部分的颜色,例如,#888

有关更多详细信息,请参阅people.iola.dk/olau/flot/examples/pie.html上的饼图示例。

创建面积图

在需要在线图的位置上堆叠多个结果时,通常使用面积图。它们也可以在某些情况下用于增强图表的视觉吸引力。

这个示例将展示一个使用面积图来增强视觉吸引力的例子:显示海拔数据。

假设我们需要可视化一个 8 公里的下坡徒步旅行,然后是 12 公里的平地行走的海拔。我们还想标记图表的“山脉”部分。最后,我们希望海拔线下的区域以一种让人联想到颜色浮雕地图的方式填充,低海拔使用绿色,中等海拔使用黄色,高海拔使用白色。

准备工作

在这个示例中,我们还将使用 Flot 图表库,因此我们需要从官方网站www.flotcharts.org/下载 Flot 并将内容提取到名为flot的单独文件夹中。

如何操作...

  1. 我们的 HTML 文件需要一个图表占位符元素和必要的脚本。以下是内容:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
        <style type="text/css">
            #chart { font-family: Verdana; }
        </style>
    </head>
    <body>
        <div id="chart" style="height:200px; width:800px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.flot.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 我们将在包含以下代码的example.js脚本中绘制图表:
$(function() {    
    function getData(cb) {
        var altitudes = [];
        // Generate random but convincing-looking data.
        for (var k = 0; k < 20; k += 0.5)
            altitudes.push([k, Math.random()*50 + 1000*Math.pow((k-15)/15,2)]);
        cb(altitudes);
    }

    getData(function(data) {
        $.plot("#chart", [{data: data}], {
            xaxis: {
                tickFormatter: function(km) { return km + ' km'; }
            },
            lines: {
                fill: true,
                fillColor: {colors: ["#393", "#990", "#cc7", "#eee"] }
            },
            grid: {
                markings: [{ xaxis: { from: 0, to: 8 }, color: "#eef" }]
            }
        });
    });
});

以下是我们的结果:

如何操作...

海拔线下的区域以一种让人联想到颜色浮雕的方式填充。山区部分由markings对象创建的蓝色区域标记。

工作原理...

与我们所有的示例一样,example.js中的getData函数生成随机数据,然后调用提供的回调函数以使用数据。我们可以很容易地编写一个替代函数,而不是从服务器获取数据,而是使用 jQuery。

单次调用$.plot将绘制面积图。第一个参数是目标容器。第二个参数是要绘制的系列数组——在这种情况下只有一个。

第三个参数更复杂。它包括以下部分:

  • xaxis属性指定我们的 x 轴的行为。我们通过提供自己的刻度格式化程序来覆盖默认的刻度标签。此格式化程序在刻度值后添加"km"字符串。

  • lines属性指定我们将使用填充线图。我们希望有类似山的渐变填充效果,因此我们指定了一个包含 CSS 颜色字符串数组的渐变对象,即{color: [颜色数组]}

  • grid属性用于在我们的图表上标记山脉段。我们指定它应该包含一个 x 轴段的标记,跨越 0 到 8 公里的范围,并具有浅蓝色。

还有更多...

Flot 有更多的面积图选项——它们可以在随分发的 API 文档中找到。

要使用这个配方,我们需要从服务器提供我们自己的数据数组。以下是getData函数的一个简单 Ajax 替代,向托管在同一域上的请求处理程序发送 Ajax 请求,以检索图表数据的路径/areachart。这很简单:

function getData(cb) {
    $.get('/areachart').success(cb);
}

显示组合图表

组合图表是具有多个 x 或 y 轴的图表,并且可能具有多种类型的系列(线条、条形和面积)。有时,我们可能希望在单个图表上呈现多种异构类型的数据,通常是为了可视化其相关性。

在这个配方中,我们将尝试通过在单个图表上呈现温度和海拔来可视化一次登山。高度系列将是一个具有渐变颜色的面积图,让人联想到地形图,但温度系列将是一条线状图,如果高于摄氏 19 度则为红色,如果低于摄氏 19 度则为蓝色。

为了做到这一点,我们需要一个能够处理两个 y 轴的图表库。我们将使用 Flot 图表库,因为它能够显示具有两个或多个 x 或 y 轴的图表。

准备工作

就像在以前的配方中一样,我们需要从官方网站www.flotcharts.org/下载 Flot 并将内容提取到名为flot的单独文件夹中。

如何做...

让我们编写 HTML 和 JavaScript 代码。

  1. 我们的 HTML 文件需要一个图表占位符、jQuery、Flot 和我们的示例脚本。这次我们还需要threshold插件,以便有两种温度颜色。以下是内容:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
        <style type="text/css">
            #chart { font-family: Verdana; }
        </style>
    </head>
    <body>
        <div id="chart" style="height:200px; width:800px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.flot.js"></script>
        <script src="img/jquery.flot.threshold.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 我们的图表是在example.js中使用以下代码绘制的:
$(function() {    
    function getData(cb) {
        var altitudes = [], temperatures = [];
        // Generate random but convincing-looking data.
        for (var k = 0; k < 20; k += 0.5) {
            altitudes.push([k, Math.random()*50 + 1000*Math.pow((k-15)/15,2)]);
            temperatures.push([k, Math.random()*0.5 + k/4 + 15]);
        }
        cb({alt:altitudes, temp:temperatures});
    }

    getData(function(data) {
        $.plot("#chart", [
           {
             data: data.alt, yaxis:1,
             lines: {fill:true, fillColor: {
             colors: ["#393", "#990", "#cc7", "#eee"] } }
                },
           {
             data: data.temp, yaxis:2, color: "rgb(200, 20, 30)",
             threshold: { below: 19, color: "rgb(20, 100, 200)" }
                }
            ], {
            yaxes: [ { }, { position: "right"}],
            xaxis: {
                tickFormatter: function(km) { return km + ' km'; }
            },
            grid: {
                markings: [{ xaxis: { from: 0, to: 8 }, color: "#eef" }]
            }
        });
    });
});

以下屏幕截图显示了最终结果:

如何做...

它是如何工作的...

使用getData函数,我们为绘图生成了两个系列,一个包含温度,另一个包含海拔。

在绘制图表时,我们首先调用getData函数。在提供的回调中,我们将数据传递给$.plot函数,该函数接受目标容器元素、系列数组和绘图选项。

数组中的第一个系列包含高度数据。我们有两个 y 轴,所以我们需要声明我们将用于该系列的 y 轴——第一个 y 轴。其余的参数声明了填充渐变;有关更多信息,请参阅创建面积图配方。

第二个系列使用第二个 y 轴。新的是threshold属性。它指定对于低于 19 度的值,线的颜色应该不同(蓝色而不是红色)。

我们将在options对象中通过指定yaxes属性(注意名称中的复数形式)来配置第二个 y 轴。该属性是一个包含 y 轴选项的数组。我们将使用第一个轴的默认值,因此为空对象。我们将把第二个轴放在右侧。

x 轴的单位是公里,因此我们的tickformatter函数在数字后添加字符串" km"

最后,我们用网格标记选项将“山脉部分”(从 0 到 8 公里)标记为蓝色。

还有更多...

这里是getData函数的一个简单 Ajax 替代,向托管在同一域上的请求处理程序发送 Ajax 请求,以检索图表数据的/charts路径。此处理程序应返回以下格式的对象:

{alt: data1, temp: data2}

其中data1data2是包含数据的二维数组。

function getData(cb) {
    $.get('/charts').success(cb);
}

创建气泡图

气泡图可以将值集显示为圆圈。它们适用于大小在 10 到 100 之间的数据集。它们特别适用于可视化数量级差异的值,并且可以在这些情况下取代饼图。

由于气泡图更复杂且稍微不太常见,我们需要一个灵活的库来绘制它们。优秀的 D3 库(d3js.org/)非常适合;它提供了一组工具(核心数据驱动 DOM API 加上“pack”数据布局),可以实现气泡图的创建。

我们将绘制一个气泡图,显示来自引荐网站的访问者数量。

操作步骤如下...

让我们编写 HTML 和 JavaScript 代码。

  1. 我们将创建一个包含图表占位符的 HTML 页面。我们将包括图表库 D3,以及将从我们的example.js文件绘制气泡图的代码:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Chart example</title>
        <style type="text/css">
            #chart text { font-family: Verdana; font-size:10px; }
        </style>
    </head>
    <body>
        <div id="chart"></div>
        <script src="img/d3.v2.js?2.9.5"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 然后我们将在example.js中添加以下代码:
(function() {
var getData = function(cb) {
    cb({children:[
        {domain: 'google.com', value: 6413},
        {domain: 'yahoo.com', value: 831},
        {domain: 'bing.com', value: 1855},
        {domain: 'news.ycombinator.com', value: 5341},
        {domain: 'reddit.com', value: 511},
        {domain: 'blog.someone.com', value: 131},
        {domain: 'blog.another.com', value: 23},
        {domain: 'slashdot.org', value: 288},
        {domain: 'twitter.com', value: 327},
        {domain: 'review-website.com', value: 231}
    ]});
}

// r is the dimension of the bubble chart
var r = 640,
    fill = d3.scale.category20c();

// create the visualization placeholder
var vis = d3.select("#chart").append("svg")
    .attr("width", r)
    .attr("height", r)
    .attr("class", "bubble");

// create a pack layout for the bubbles
var bubble = window.bubble = d3.layout.pack()
    .sort(null)
    .size([r, r])
    .padding(1.5);

    getData(function(json) {
        // Process the data with the pack layout
        var data = bubble.nodes(json);
        // Create a node for every leaf data element
        var selection = vis.selectAll("g.node")
            .data(data.filter(function(d) { return !d.children; }));
        var node = selection.enter().append("g");

        node.attr("class", "node");
        node.append("title")
            .text(function(d) { return d.domain });
        node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
        node.append("circle")
            .attr("r", function(d) { return d.r; })
            .style("fill", function(d) { return fill(d.domain); });
        node.append("text")
            .attr("text-anchor", "middle")
            .attr("dy", ".3em")
            .text(function(d) { return d.domain.substring(0, d.r / 3); });
    });
}());

在接下来的部分中,我们将解释 D3 的工作原理以及我们如何使用它来创建气泡图:

工作原理...

与大多数其他图表库不同,D3 没有任何预定义的图表类型,可以绘制。相反,它提供了一组模块化工具,您可以自由混合和匹配,以创建任何类型的数据驱动文档。

然而,D3 包含一些非常特定于可视化的工具。

例如,d3.scale.category20c创建一个序数比例尺。序数比例尺将输入值映射到一组离散的输出值。在这种情况下,离散的值集是一组预定义的 20 种输出颜色。比例尺是一个函数——它将输入值映射到输出值。我们可以明确指定哪些输入值映射到哪些输出值,但如果我们不这样做,它会根据使用情况进行推断。在我们的情况下,这意味着第一个域名将映射到第一个颜色,第二个将映射到第二个,依此类推。

其他工具包括类似于 jQuery 的 DOM 选择工具,在我们的示例中,我们使用它们将 SVG 元素添加到我们的图表占位符中。

另一个例子是 D3 布局。要绘制气泡图,我们需要一个包布局。布局根据某些规则和约束将一组具有值的对象映射到一组输出坐标。一个常见的例子是力布局,它是一种通过在图形节点之间迭代应用虚拟力来排列对象的图形布局。

我们使用的是将对象层次化地打包成圆圈的包布局。我们的数据是平面的,因此包布局仅用于自动排列我们的圆圈。创建一个包布局并将其分配给bubble变量。

包布局通过将bubble.nodes函数应用于输入数据来工作。此函数查找输入数据中每个对象中的value属性。基于这个属性(它将其视为相对半径)和布局的大小,它将以下属性添加到我们的数据中:x、y 和 r,并返回结果数组。

此时,我们已经有了绘制气泡图所需的大部分数据:我们有气泡的位置和尺寸。现在我们需要做的就是将它们转换为适当的 SVG 元素。我们用来做这个的工具是 D3 的selectAll函数。

与 jQuery 选择器不同,D3 的selectAll可以用于在文档和数据对象之间维护双向映射。我们使用选择的.data函数指定映射到我们选择的数据数组。

声明了这个映射之后,我们可以决定当一个元素被添加到我们的数据数组时会发生什么,使用.enter函数。在我们的示例中,我们声明一个新的 SVG 图形元素被添加到 SVG 画布中,并将该声明分配给node变量。

需要注意的是,我们的节点变量并不持有 SVG 元素;相反,它是未来将创建的节点集合中每个图形 SVG 元素的选择,每当新的数据元素“进入”选择时,节点上的操作指定将在每个添加的 SVG 元素上执行的操作。

我们指定每个节点都将有一个title属性(将在鼠标悬停时显示)。此标题的内部文本取决于数据数组中的特定元素。为了描述这一点,我们将一个函数作为.text()调用的参数传递。传递函数的第一个参数将是特定节点的数据元素,返回的值应该是将设置为标题的文本。

类似地,我们将我们的气泡移动到由包布局计算的位置。之后,我们添加一个由包布局计算的半径的圆和颜色比例尺来生成圆的颜色。

最后,以相同的方式附加文本节点。

以下是结果的样子:

它是如何工作的...

还有更多...

此示例使用 SVG(可缩放矢量图形)标记来呈现可视化。大多数现代浏览器都支持 SVG,但 IE9 之前的 Internet Explorer 版本不支持。但是,D3 不仅限于 SVG,它还能够生成 HTML 元素,这些元素可以用作 IE 旧版本的替代品。

展示带有标记位置的地图

谷歌地图的崛起和他们出色的 API 使地图嵌入网站变得流行起来。嵌入式地图有各种用途:显示用户去过的地方,显示事件的位置,显示商店的位置等等。地图可以与我们网站上显示的每个文本地址一起显示。

在这个教程中,我们将制作一个简单的地图,并在上面标记一个位置。为此,我们将使用Leaflet库(leafletjs.com/),这是一个广泛被 Flickr、FourSquare、Craigslist、Wikimedia 和其他流行网站使用的知名库。

我们将显示一个OpenStreetMap地图图层。OpenStreetMap(www.openstreetmap.org/)是一个类似维基百科的免费协作创建的街道地图,覆盖范围广泛。

我们还将添加一个描述气球,当点击标记时会显示。

如何做...

让我们编写 HTML 和 JavaScript 代码。

  1. 在我们的 HTML 文件中添加 Leaflet 的样式表,以及 IE8 和更旧版本所需的条件额外 CSS:
<link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.css" />
 <!--[if lte IE 8]>
     <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.ie.css" />
<![endif]-->
  1. 在我们的脚本中包含 Leaflet 库 JS 文件:
<script src="img/leaflet.js"></script>
  1. 在我们的页面上放置地图的占位符。我们还必须指定它的高度,否则 Leaflet 将无法正常工作:
<div id="map" style="height:200px;"></div>
  1. 通过添加example.js来添加我们的 JS 代码:
<script src="img/example.js"></script>
  1. 最后,在example.js中添加创建地图的代码:
var map = L.map('map').setView([51.505, -0.09], 13);

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution:'Copyright (C) OpenStreetMap.org',
        maxZoom:18
        }).addTo(map);

var marker = L.marker([51.5, -0.09]).addTo(map);
marker.bindPopup("<b>Hello world!</b><br>I am a popup.").openPopup();

它是如何工作的...

大多数地图库通过使用瓦片图像图层来绘制它们的地图。瓦片图像图层是具有预定义固定大小的图像网格。这些图像是地图的切片部分,已经预先渲染并托管在瓦片服务器上。

地图使用称为缩放级别的离散缩放点。不同的缩放级别使用不同的瓦片图像。

在某些情况下,特别是在高缩放级别下,服务器会根据需要在空间超出合理存储空间大小的情况下动态渲染瓦片。例如,OpenStreetMap 使用 19 个缩放级别。第一级使用单个瓦片,第二级将此瓦片分成四个瓦片,第三级使用 16 个瓦片,依此类推。在第 19 个缩放级别,有 480 亿个瓦片,假设平均瓦片大小为 10KB,那将需要 480TB 的存储空间。

当用户滚动地图时,以前未加载的区域的瓦片会动态加载并显示在容器中。当用户更改缩放级别时,旧缩放级别的瓦片将被移除,新的瓦片将被添加。

在我们的example.js文件中,我们使用 Leaflet 的函数(在L命名空间对象中找到)来创建地图。地图初始化为位于伦敦的中心,使用代表[纬度,经度]对的数组。另一个参数是缩放级别,设置为13

之后添加了一个瓦片图层。我们指定 OpenStreetMap 使用的瓦片服务器模式如下:

http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png

其中s是服务器字母(abc),z是缩放级别,xy是瓦片的离散坐标。例如,在缩放级别 1 时,xy中的每一个可以是12,而在缩放级别 2 时,它们可以在 1 到 4 的范围内。我们还指定了可用的最大缩放级别。

我们向地图添加自己的标记。初始化参数是一个[纬度,经度]对。之后,我们可以在标记内部添加一个弹出窗口,显示文本和/或任意 HTML。我们立即打开弹出窗口。

它是如何工作的...

使用 Leaflet 绘制的地图

显示带有路径的地图

在显示地图时,有时我们可能希望显示的不仅仅是位置。除了标记,另一个最常见的地图叠加层是路径和区域。

在这个食谱中,我们将创建一个显示路径和区域的地图。

如何做...

让我们编写 HTML 和 JavaScript 代码。

  1. 就像在显示带有标记位置的地图食谱中一样,我们需要包含适当的 CSS 和脚本。以下是一个示例 HTML 文件:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Map example</title>
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.css" />
        <!--[if lte IE 8]>
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.ie.css" />
        <![endif]-->
    </head>
    <body>
        <div id="map" style="height:480px; width:640px;"></div>
        <script src="img/jquery.min.js"></script>
        <script src="img/leaflet.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 然后我们可以将我们的代码添加到example.js中:
var map = L.map('map').setView([52.513, -0.06], 14)

L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
    attribution:'Copyright (C) OpenStreetMap.org',
    maxZoom:18
}).addTo(map);

var polyline = L.polyline([
    [52.519, -0.08],
    [52.513, -0.06],
    [52.52, -0.047]
]).addTo(map);

var polygon = L.polygon([
    [52.509, -0.08],
    [52.503, -0.06],
    [52.51, -0.047]
], {
    color:"#f5f",
    stroke: false,
    fillOpacity:0.5
}).addTo(map);

它是如何工作的...

我们使用L.map函数创建地图,并使用setView在指定的[纬度,经度]数组和缩放级别上设置地图的位置。我们还添加了标准的 OpenStreetMap 瓦片图层。

首先,我们创建并添加一个标准折线。由于我们没有指定任何选项,Leaflet 对颜色、不透明度、边框等都使用了合理的默认值。折线构造函数采用[纬度,经度]对的数组,并绘制通过它们的顶点的线。

它是如何工作的...

之后,我们创建一个稍微定制的多边形。与折线构造函数一样,多边形也采用[纬度,经度]对的数组。此外,我们自定义了背景颜色,删除了多边形的边框,并指定了多边形的不透明度为 50%。

显示表盘

模拟表盘对于可视化数值在预定义最小值和最大值之间并随时间变化的数据非常有用。示例包括燃料量,当前速度,磁盘空间,进程和内存使用等。

在这个食谱中,我们将为 jQuery 制作一个非常灵活的、数据驱动的表盘插件。然后我们将使用这个插件来显示模拟汽车速度表。以下是速度表的外观:

显示表盘

该食谱广泛使用了 HTML5 的画布。

如何做...

让我们为我们的示例编写 HTML 代码,表盘插件和将它们联系在一起的代码。

  1. 制作一个简单的 HTML 文件,其中包含我们的表盘的画布:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Gauge example</title>
    </head>
    <body>
        <canvas id="gauge" width="400" height="400"></canvas>
        <script src="img/jquery.min.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 然后在example.js中编写我们的表盘插件代码:
(function($) {
  1. 这是一个支持函数,它替换了Array.forEach,可以在单个项目和数组上工作。我们的表盘将支持多个条纹、指针和刻度,但当提供单个条纹时,它也应该能够工作:
    function eachOrOne(items, cb) {
        return (items instanceof Array ? items : [items]).map(cb);
    }
  1. 以下是一个通用函数,它围绕中心c(角度量为a)旋转点pt。方向是顺时针的:
    function rotate(pt, a, c) {
        a = - a;
        return { x: c.x + (pt.x - c.x) * Math.cos(a) - (pt.y-c.y) * Math.sin(a),
                 y: c.y + (pt.x - c.x) * Math.sin(a) + (pt.y-c.y) * Math.cos(a) };
    }
  1. 以下是我们的表盘插件
    $.gauge = function(target, options) {
        var defaults = {
            yoffset: 0.2,
            scale: {
                type: 'linear',
                values: [1, 200],
                angles: [0, Math.PI]
            },
            strip: {
                scale: 0, radius: 0.8, width: 0.05,
                color: "#aaa", from: 0, to: 200
            },           
            ticks: {
                scale: 0, radius: 0.77, length: 0.1, width: 1, color: "#555",
                values: {from: 0, to:200, step: 10},
            },           
            labels: {
                scale: 0, radius: 0.65,
                font: '12px Verdana', color: "#444",
                values: {from: 0, to:200, step: 20}
            },
            needle: {
                scale: 0, length: 0.8, thickness: 0.1,
                color: "#555", value: 67
            }
        };

默认情况下,我们的表盘具有以下特点:

  • 从顶部偏移 20%

  • 具有值范围 1 到 200 的线性刻度,角度范围 0 到 180 度,

  • 具有 80%或总半径宽度为总半径 5%的单条带,颜色为灰色,范围从 0 到 200。

  • 具有一个从 0 到 200 的单个ticks数组,步长为 10

  • 具有从 0 到 200 的标签,步长为 20

  • 具有单个指针设置为值 67

  1. 我们允许用户覆盖选项,并指定之前提到的任何组件的多个:
        var options = $.extend(true, {}, defaults, options);
        for (var key in defaults) if (key != 'yoffset')
            options[key] = eachOrOne(options[key], function(item) {
                return $.extend(true, {}, defaults[key], item);
            });        
        var $target = $(target);
        var ctx = $target[0].getContext('2d');
  1. 我们构建我们的scale函数,并用实际数组替换指定值范围的对象。请注意,您可以指定实际数组,而不是range对象:
        options.scale = eachOrOne(options.scale, function(s) {
            return $.gauge.scale(s);
        });
        eachOrOne(options.ticks, function(t) {
            return t.values = $.gauge.range(t.values);
        });
        eachOrOne(options.labels, function(l) {
            return l.values = $.gauge.range(l.values);
        });
  1. 以下是绘图代码:
        function draw(options) {
  1. 我们将使用仪表中心作为参考点,并清除画布:
            var w = $target.width(), h = $target.height(),
                c = {x: w * 0.5, y: h * (0.5 + options.yoffset)},
                r = w * 0.5,
                pi = Math.PI;
            ctx.clearRect(0, 0, w, h);
  1. 然后我们将绘制所有条带(一个或多个)作为弧线:
            // strips
            eachOrOne(options.strip, function(s) {
                var scale = options.scale[s.scale || 0];
                ctx.beginPath();
                ctx.strokeStyle = s.color;
                ctx.lineWidth = r * s.width;
                ctx.arc(c.x, c.y, s.radius * r, scale(s.to), scale(s.from), true);
                ctx.stroke();
            });
  1. 然后绘制所有刻度(我们使用非常短、非常粗的弧线作为刻度)。我们的scale函数将range中的值转换为角度:
            // ticks
            eachOrOne(options.ticks, function(s) {
                var scale = options.scale[s.scale || 0];
                ctx.strokeStyle = s.color;
                ctx.lineWidth = r * s.length;
                var delta = scale(s.width) - scale(0);
                s.values.forEach(function(v) {
                    ctx.beginPath();
                    ctx.arc(c.x, c.y, s.radius * r,
                        scale(v) + delta, scale(v) - delta, true);
                    ctx.stroke();
                });
            });
  1. 然后我们绘制标签。我们通过将其放在最右边的垂直居中位置来确定位置,然后按照与值缩放的量逆时针旋转它:
            // labels
            ctx.textAlign    = 'center';
            ctx.textBaseline = 'middle';
            eachOrOne(options.labels, function(s) {
                var scale = options.scale[s.scale || 0];
                ctx.font = s.font;
                ctx.fillStyle = s.color;
                s.values.forEach(function(v) {
                    var pos = rotate({x: c.x + r * s.radius, y:c.y},
                        0 - scale(v), c);
                    ctx.beginPath();
                    ctx.fillText(v, pos.x, pos.y);
                    ctx.fill();
                });
            });
  1. 最后,我们绘制指针。指针由一个圆和一个三角形组成,圆心位于仪表的中心旋转点,三角形从那里延伸。我们旋转所有三角形点的方式与旋转标签中心的方式相同:
            // needle
            eachOrOne(options.needle, function(s) {
                var scale = options.scale[s.scale || 0];
                var rotrad = 0 - scale(s.value);
                var p1 = rotate({x: c.x + r * s.length, y: c.y},    rotrad, c),
                    p2 = rotate({x: c.x, y: c.y + r*s.thickness/2}, rotrad, c),
                    p3 = rotate({x: c.x, y: c.y - r*s.thickness/2}, rotrad, c);
                ctx.fillStyle = s.color;
                ctx.beginPath();
                ctx.arc(c.x, c.y, r * s.thickness / 2, 0, 2*Math.PI);
                ctx.fill();
                ctx.beginPath();
                ctx.moveTo(p1.x, p1.y);
                ctx.lineTo(p2.x, p2.y);
                ctx.lineTo(p3.x, p3.y);
                ctx.fill();                
            });            
        }        
        draw(options);
  1. 在绘制整个仪表之后,gauge函数返回一个函数,该函数可用于更改仪表指针值并重新绘制它:
        return function(val, i) {
            i = i || 0;
            options.needle[i].value = val;
            draw(options);
        }
    };
  1. 这些是常见的辅助函数。range函数创建一个值数组,而scale创建一个将值从一个范围缩放到另一个范围的函数。两者都支持对数刻度:
    $.gauge.range = function(opt) {
        if (opt instanceof Array) return opt;
        var arr = [], step = opt.step;
        var last = opt.from;
        for (var k = opt.from; k <= opt.to; k+= step)
            arr.push(opt.log ? Math.pow(opt.log, k) : k);
        return arr;
    };
    $.gauge.scale = function(opt, f) {
        if (opt.type == 'linear') opt.type = function(x) { return x; };
        else if (opt.type == 'log') opt.type = Math.log;
        var f = opt.type,
            v0 = f(opt.values[0]),
            v1 = f(opt.values[1]);
        return function(v) {
            return (f(v) - v0) / (v1 - v0)
                    * (opt.angles[1] - opt.angles[0]) + Math.PI + opt.angles[0];
        };
    }
}(jQuery));

使用 jQuery 对象作为参数调用匿名函数,在函数的范围内变为$。这是构建具有自己私有范围的 jQuery 插件的典型方式,并在该范围内使 jQuery 作为$可用,而不管全局命名空间中的$是否与 jQuery 相同。

  1. 我们将在example.js中绘制我们的仪表。以下是内容:
$(function() {
    var g = $.gauge("#gauge", {
        scale: {
            angles: [-0.3, Math.PI+0.3],
            values: [0, 220]
        },
        strip: [
            { from: 0,   to: 140, color:"#ada" },
            { from: 140, to: 180, color:"#dda" },
            { from: 180, to: 220, color:"#d88" }
        ],
        ticks: [{
            color: "rgba(0,0,0,0.33)",
            values: { from: 0, to: 220, step:10 },
            length:0.05, radius:0.8, width:0.3
        }, {
            color: "rgba(0,0,0,0.33)",
            values: { from: 0, to: 220, step:20 },
            length:0.11, radius: 0.77, width:0.3
        }],
        labels: {
            color: "#777",
            values: { from: 0, to: 220, step:20 },
            radius: 0.62
        },
        needle: { color:"#678" }
    });
    g(25);
});

它是如何工作的...

我们为仪表指定了一个线性刻度,角度略低于中间,并且速度值在 0 到 220 的范围内。我们创建了三个条带,绿色的范围是 0 到 140 公里/小时,黄色的范围是 140 到 180 公里/小时,红色的范围是 180 到 220 公里/小时。我们将使用两组条带:每 20 公里/小时一个较大的,每 10 公里/小时一个较小的,都是半透明的。最后,我们添加了一个带有蓝色色调的指针。

最后,我们可以使用返回的函数设置仪表值,我们将其设置为 25 公里/小时。

显示树

在这个配方中,我们将看看如何以树状布局显示数据。我们将通过 JSON 文件来可视化 Linux 的一个小家族树。此外,我们将使用D3.js文件来操作 DOM 以显示数据。

显示树

准备工作

首先,我们需要有将用于可视化的数据。我们需要获取这个配方示例中的tree.json文件。

如何做...

我们将编写 HTML 和支持 JavaScript 代码,应该从 JSON 文件生成数据:

  1. 让我们首先看一下 JSON 数据的结构:
{
  "name": "GNU/Linux",
  "url": "http://en.wikipedia.org/wiki/Linux",
  "children": [
    {
      "name": "Red Hat",
      "url": "http://www.redhat.com",
      "children": [ .. ]
   } ]
...
}

每个对象都有一个name属性,表示分布名称,一个url属性,其中包含指向官方网页的链接,以及可选的children属性,其中包含其他对象的列表。

  1. 下一步将是使用 HTML5 文档类型创建页面,并添加对D3.js的依赖项和名为tree.css的 CSS 文件:
<!DOCTYPE html>
<html>
  <head>
    <title>Linux Tree History</title>
    <script src="img/d3.v2.js"></script>
    <link type="text/css" rel="stylesheet" href="tree.css"/>
  </head>
  1. body部分,我们将添加一个具有名为locationid<div>标签,我们将用它作为占位符,并另外包含一个名为tree.js的 JavaScript 文件,该文件将用于包含映射数据的逻辑:
  <body>
    <div id="location"></div>
    <script type="text/javascript" src="img/tree.js"></script>
  </body>
</html>
  1. 让我们从在tree.js文件中创建显示区域开始。首先,我们创建提供内部私有状态的匿名函数:
(function() {
  1. 然后,我们设置生成图像的大小,给定widthheight。为简单起见,我们将它们设置为固定值:
var width = 1000,
          height = 600;
  1. 然后,我们设置了一个标准的 D3 布局树:
  var tree = d3.layout.tree()
          .size([height, width - 200]);
  var diagonal = d3.svg.diagonal()
          .projection(function(d) {
            return [d.y, d.x];
          });
  1. 由于我们需要指定和创建实际的 SVG,我们使用之前在 HTML 中选择的id来选择位置,然后附加 SVG 元素:
  var vis = d3.select("#location").append("svg")
          .attr("width", width)
          .attr("height", height)
          .append("g")
          .attr("transform", "translate(60, 0)");
  1. 我们还需要从tree.json中读取数据,并以某种方式使用给定的层次结构创建节点和链接:
d3.json("tree.json", function(json) {
    var nodes = tree.nodes(json);
    vis.selectAll("path.link")
          .data(tree.links(nodes))
          .enter().append("path")
          .attr("class", "link")
          .attr("d", diagonal);
    var node = vis.selectAll("g.node")
            .data(nodes)
            .enter().append("g")
            .append("a")
            .attr("xlink:href", function(d) {
                 return d.url;
              })
            .attr("class", "node")
            .attr("transform", function(d) {
                return "translate(" + d.y + "," + d.x + ")";
              });

    node.append("circle")
            .attr("r", 20);

    node.append("text")
            .attr("dx", -19)
            .attr("fill", "white")
            .attr("dy", -19)
            .style("font-size", "20")
            .text(function(d) {
              return d.name;
            });
  1. 我们可以使用 CSS 样式页面,选择页面链接背景和圆圈的颜色:
 .node circle {
     fill: #fc0;
     stroke: steelblue;
     stroke-width: 1px;
}
.link {
  fill: none;
  stroke: #fff;
  stroke-width: 5.0px;
}
body{  
    background-color: #000;
 }

它是如何工作的...

d3.layout.tree()创建一个具有默认设置的新树布局,其中假定数据元素中的每个输入都有一个子数组。

使用d3.svg.diagonal(),我们创建了一个具有默认访问器函数的生成器。 返回的函数可以生成连接节点的立方贝塞尔路径数据,其中我们有用于平滑线条的切线。

注意

有关贝塞尔曲线的更多信息,请访问en.wikipedia.org/wiki/Bézier_curve。 它背后有一些数学知识,但最简单的解释是,它是一条受到某些点影响的线,使其成为定义曲线的不错选择。

由于我们希望树从左到右而不是默认的从上到下,我们需要通过进行投影来改变默认行为:

var diagonal = d3.svg.diagonal()
          .projection(function(d) {
              return [d.y, d.x];
          });

该函数将使用[d.y, d.x]而不是默认的[d.x,d.y]。 你可能已经注意到了.append("g")函数,它添加了 SVG g元素,这是一个用于将各种相关元素分组在一起的容器元素。 我们可以在其中有多个嵌套元素,一个在另一个内部,到任意深度,允许我们在各个级别创建组:

<g>
      <g>
      <g>
       </g>
     </g>
   </g>

要读取 JSON 数据,我们使用了以下内容:

d3.json("tree.json", function(json) { … }

这将对tree.json资源进行 AJAX 调用。

注意

请注意,默认情况下,您的浏览器不会允许跨域请求。 这包括对本地文件系统的请求。 要克服这一点,请使用附录 A 中解释的本地 Web 服务器,安装 Node.js 和使用 npm。 另一个选择是使用 JSONP 作为一个很好的解决方法,因为在这种安全限制下有一些缺点。 在第八章中,与服务器通信,我们将介绍这些限制背后的问题和原因。

有关更多信息,请查看 W3C 页面www.w3.org/TR/cors/

然后,我们使用tree.nodes(json)自动映射来自 JSON 文件的数据,其中对我们在数据中有什么进行了一些假设; 例如,我们可以有一个父节点或子节点。

之后,我们使用类似于 jQuery 的 W3C 选择器选择了所有的path.link

vis.selectAll("path.link")

使用.data,我们将它们与tree.links返回的链接信息绑定:

.data(tree.links(nodes))

D3 的树布局有一个links函数,它接受一个节点数组,并返回表示这些节点的父节点到子节点的链接的对象数组。 不会创建叶子节点的链接。 返回对象中存储的信息有一个source或父节点和target或子节点。 现在,在接下来的部分中,有一个非常 D3 魔术的.enter()函数。 每个数组中的元素都是.data([theArray])的一部分,并且在选择中找不到相应的 DOM 元素时,它就会“进入数据”,从而允许我们使用.append.insert.select.empty操作符。 在我们的情况下,我们想要创建具有 CSS 类link和使用我们之前定义的对角线函数计算的d属性的 SVG 路径元素:

           .enter()
           .append("path")
           .attr("class", "link")
           .attr("d", diagonal)

因此,对于每个数据元素,它将创建<path class='link' d='dataCalucatedByDiagonal' />

SVG 路径元素是一个用于表示线条绘制的概念,例如,具有各种类型的几何和表示。d属性包含了用moveto(M)lineto(L)curve( cubic and quadratic besiers)arc(A)closepath(Z)vertical lineto (V)等指定的路径数据。

了解 D3 为我们生成了什么,以便更全面地了解它是如何工作的。比如说我们想要显示一个简单的线:

它是如何工作的...

SVG 代码如下:

<svg  version="1.1">
  <g style="stroke: red; fill: none;">
    <path d="M 10 30 L 200 10"/>
 </g>
</svg>

检查路径数据值,我们可以看到它的意思是将pen(M)移动到(10,30),并画line(L)(200,10)

在我们的树的例子中,我们使用路径绘制线条,所以下一步是绘制节点。我们应用相同的过程,选择所有旧的g.node元素并输入节点数据,但是我们不是创建<path/>元素,而是只是附加"g",并额外添加一个带有<xlink:href>属性的<a>元素:

…
            .append("a")
            .attr("xlink:href", function(d) {
                 return d.url;
              })

由于我们已经自动迭代了所有数据节点,我们可以访问d.url,检索每个节点的 URL,并将其设置为我们稍后要添加的所有内部元素的链接。

不要忘记我们需要旋转坐标,因为我们希望树从左到右显示:

            .attr("transform", function(d) {
                return "translate(" + d.y + "," + d.x + ")";
              });

在此之后,我们可以向每个元素附加其他元素,为了创建圆,我们添加以下内容:

    node.append("circle")
            .attr("r", 20);

这样就创建了一个半径为 20px 的 SVG 圆,另外,我们附加了将显示分布名称的<text/>元素:

   node.append("text")
            .attr("dx", -19)
            .attr("dy", -19)
             ...

注意,我们将文本元素移动了(-19,-19),以避免与圆和线重叠,就是这样。

还有更多...

你首先要做的事情是玩弄一下那些是常数的值,比如图像大小或文本偏移量。这将帮助你更好地理解变化如何影响布局。有各种不同的函数来生成布局,你可以以径向方式创建它,或者使它看起来像树突一样。

有各种方法可以添加交互,你可以在代码的某些部分进行更新,使某些部分动画化,甚至在 SVG 内部包含 HTML。

使用网络字体的 LED 记分牌

在这个食谱中,我们将创建一个 LED 记分牌,类似于篮球比赛中使用的记分牌,通过巧妙地使用 HTML 网络字体。该食谱的主要目标是介绍网络字体及其提供的功能。

使用网络字体的 LED 记分牌

提示

有关网络字体的完整规范可以在 W3C 上找到www.w3.org/TR/css3-webfonts/

准备工作完成

在开始之前,你需要获取我们在这个例子中要使用的字体。这些文件可以从示例代码中检索,它们都有一个RADIOLAND前缀。

如何做...

为了创建记分牌,我们将创建一个 HTML 页面,一个支持 JavaScript 代码,用于更新计时器和相关数据,以及一个使用网络字体的 CSS 文件:

  1. 首先,我们将从创建 HTML 页面开始;在head部分,包括stylesheet.css和对 jQuery 的依赖。
  <link rel="stylesheet" href="stylesheet.css" type="text/css" charset="utf-8">
  <script src="img/jquery.min.js"></script>
  1. body部分,添加我们将用作分数占位符的div元素,并另外包括scoreboard.js
    <div class="counter"></div>
    <div class="score">
              <span class="home"></span>
               <span class="period"></span>
               <span class="guests"></span>
    </div>
  </div>
  <script type="text/javascript" src="img/scoreboard.js"></script>
  1. 我们现在可以创建stylesheet.css文件,首先定义具有 LED 外观的网络字体:
@font-face {
  font-family: 'RadiolandRegular';
  src: url('RADIOLAND-webfont.eot');
  src: url('RADIOLAND-webfont.eot?#iefix') format('embedded-opentype'),
    url('RADIOLAND-webfont.woff') format('woff'),
    url('RADIOLAND-webfont.ttf') format('truetype'),
    url('RADIOLAND-webfont.svg#RadiolandRegular') format('svg');
  font-weight: normal;
  font-style: normal;
}
  1. 由于字体现在被定义为RadiolandRegular,我们可以直接引用它:
div.counter{
  font: 118px/127px 'RadiolandRegular', Arial, sans-serif;
  color: green;
}
    .score {
      font: 55px/60px 'RadiolandRegular', Arial, sans-serif;
      letter-spacing: 0;
      color: red;
      width: 450px;
    }

  .period {
      font: 35px/45px 'RadiolandRegular', Arial, sans-serif;
      color: white;
    }

    div.display {
      padding: 50px;
    }
  1. 我们可以继续创建将要使用的 JavaScript,并且我们将使用一个名为game的模拟对象,该对象具有游戏信息。一般来说,这个对象应该通过 AJAX 调用从服务器检索,但为了简单起见,我们使用了一些预定义的值:
  var game = {
    periodStart: 1354650343000,
    currentPeriod: 1,
    score: {
      home: 15,
      guests: 10
    }
  };
  1. 为了使我们的显示对象的创建逻辑和数据获取逻辑分离,我们可以将其放在一个函数中:
  function fetchNewData() {
    // server data
    var game = {
      periodStart: new Date().getTime(),
      //the server will return data like: periodStart: 1354838410000,
      currentPeriod: 1,
      score: {
        home: 15,
        guests: 10
      }
    };
    //return display data
    return {
      periodStart: game.periodStart,
      counter: '00:00',
      period: game.currentPeriod + ' Period',
      score: {
        home: game.score.home,
        guests: game.score.guests
      }
    };
  }
  1. 我们还创建了一个 config 对象,可以在其中定义游戏参数,例如周期数和每周期的分钟数:
  var config = {
    refreshSec: 1,
    periods: 4,
    minPerPeriod: 12
  };
  1. 然后我们定义 updateCounter()updateScore() 函数,它们将更新显示并执行计时器的计算。我们将检查当前时间是否小于游戏开始时间,并将计时器设置为 00:00。如果当前时间大于最大可能时间,则将计时器设置为最大可能时间:
  function updateCounter() {
          var now = new Date(),
          millsPassed = now.getTime() - displayData.periodStart;

         if (millsPassed < 0) {
           displayData.counter = '00:00';
         } else if (millsPassed > config.minPerPeriod * 60 * 1000) {
           displayData.counter = config.minPerPeriod + ':00';
         } else {
           //counting normal time
           var min = Math.floor(millsPassed/60000);
           if (min<10) {
             min = '0' + min;
           }
           var sec = Math.floor((millsPassed % 60000)/1000);
           if (sec<10) {
             sec = '0'+sec;
           }
           displayData.counter = min+':'+sec;
         }
         $('.counter').text(displayData.counter);
         $('.period').text(displayData.period);
  1. 随后,我们添加一个将更新得分的函数:
  function updateScore(){
    $('.home').text(displayData.score.home);
    $('.guests').text(displayData.score.guests);
  }
  1. 最后,我们可以调用 setInterval 函数,该函数将每 500 毫秒调用更新:
    setInterval(updateCounter, 500);
    setInterval(updateScore, 500);

工作原理…

这个配方中的 HTML 和 JavaScript 代码非常简单直接,但另一方面,我们正在深入研究 CSS 和字体文件。

通过添加 @font-face at-rule,我们可以指定在其他元素中使用在线字体。通过这样做,我们允许使用客户端机器上不可用的不同字体。

@font-face 的定义中,我们添加了 font-family ——一个我们随后可以应用在任何元素上的名称定义。例如,考虑以下示例,我们将我们的字体称为 someName

@font-face {
  font-family: someName;
  src: url(awesome.woff) format("woff"),
       url(awesome.ttf) format("opentype");
}

您可以在此示例中以及我们的 stylesheet.css 中的 url 旁边注意到名为 format("woff") 的格式定义。可以应用以下格式:

  • .woff:这代表Web 开放字体格式WOFF),这是由 Mozilla 开发的一种较新的标准之一。完整规范可在 www.w3.org/TR/WOFF/ 上找到。该格式的目标是为其他格式提供替代解决方案,这些解决方案在需要一定级别的许可证时会更加优化。该格式允许将元数据附加到文件本身,其中可以包含许可证。

  • .ttf.otfTrueType 字体TTF)和扩展版本OpenType 字体OTF)是一些最广泛使用的类型。TrueType 的标准是由苹果电脑在 80 年代末开发的,作为一些 PostScript 标准的替代品。它为字体开发人员提供了灵活性和对用户以多种不同大小显示字体的控制。由于其流行和功能,它迅速传播到其他平台,如 Windows。OpenType 是基于 TrueType 的后继版本。该规范由微软开发,并得到 Adobe Systems 的补充。OpenType 是微软公司的注册商标。详细规范可以在 www.microsoft.com/typography/otspec/default.htm 上找到。

  • .eot:嵌入式 OpenType 字体是设计用于网页的 OpenType 字体的一种形式。对嵌入版本的扩展与制作版权保护密切相关。由于其他字体很容易被复制,EOT 只向用户提供可用字符的子集,使得复制整个字体更加困难。有关 EOT 的更多信息,请参阅 W3C 规范 www.w3.org/Submission/EOT/

  • .svg.svgz:SVG 和带有扩展名 .svgz 的经过解压缩的版本可以用来表示字体。字体定义存储为 SVG 字形,可以轻松支持。有关 SVG 字体的更多信息可以在规范 www.w3.org/TR/SVG/fonts.html 中找到。不幸的是,目前写作时,这种格式在 IE 和 Firefox 中不受支持。

@font-face 上还可以使用一些其他属性,例如 font-stylefont-weightfont-stretch。此外,我们可以通过为 unicode-range 设置值来指定 Unicode 中使用的字符范围。规范中的一些示例如下:

  • unicode-range: U+0-7F;:这是基本 ASCII 字符的代码范围

  • unicode-range: U+590-5ff;:这是希伯来字符的代码范围

Web 字体的一个问题是 CSS2 的规范没有要求特定的格式。这通常意味着我们需要提供几种不同的格式,以在各种浏览器中获得相同的体验。

注意

有许多font-face定义生成器可以简化所有这些可能选项的创建。其中一个是FontSquirrelwww.fontsquirrel.com/tools/webfont-generator)。

Web 字体正在成为 Web 的最常见构建块之一,因此,当我们需要一个出色的排版时,它们应该始终被考虑。图像、SVG、Coufons 和类似类型与文本不太兼容。我们可能会使用这些来获得出色的文本外观,但搜索引擎无法访问文本,大多数辅助功能软件将忽略它,甚至可能使页面大小变大。另一方面,使用文本允许我们对数据进行各种 CSS 调整,我们可以使用选择器,比如:first-letter:first-line:lang

还有更多...

Google 有许多我们可以使用的字体,这些字体可以在www.google.com/fonts/上找到。除了标准的字体包含,他们还有一个基于 JavaScript 的字体加载器。这个加载器解决了在“真正”的字体加载时看到回退文本渲染的问题,通常被称为未样式化文本的闪烁FOUT)。例如,我们可以这样做来包含一个名为'Noto Sans'的字体:

<script type="text/javascript">
  WebFontConfig = {
    google: { families: [ 'Noto+Sans::latin' ] }
  };
  (function() {
    var wf = document.createElement('script');
    wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
      '://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
    wf.type = 'text/javascript';
    wf.async = 'true';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(wf, s);
  })(); </script>

之后,我们可以简单地在 CSS 中使用font-family: 'Noto Sans', sans-serif;来包含它。

注意

有关 Google 字体选项的更多信息,请访问developers.google.com/fonts/。至于所谓的 FOUT 以及一些对抗它的方法,Paul Irishpaulirish.com/2009/fighting-the-font-face-fout/的文章中有更多内容。

第三章:动画数据显示

在本章中,我们将涵盖以下食谱:

  • 制作运动图表

  • 显示力导向图

  • 制作实时范围图表过滤器

  • 制作图像轮播

  • 缩放和平移图表

  • 使用 Web 通知 API

  • 从数据集创建交互式地理图表

介绍

我们都生活在一个信息时代,每天都会产生大量数据。这种过量的数据急需以用户可访问的格式呈现。

本章将涵盖一些常见的动画数据可视化方法,带有轻微的交互。大多数示例将是数据驱动文档,绑定到页面上的 D3,以及一些其他动画数据显示方法。

注意

在整本书中,我们都使用 D3,所以了解一些它的起源是很好的。Mike Bostock,这个杰出的核心作者,创建了这个库,作为他在博士研究期间创建的一个库 Protovis 的继任者,考虑到了 Web 标准并进行了性能改进。他还在他的网站bost.ocks.org/mike/上提供了一个令人惊叹的可视化列表,大部分是为纽约时报做的。

制作运动图表

在处理基于时间的数据时,通常希望有一个视图,其中时间变化将被可视化。一种方法是使用随时间更新的运动图表,这就是我们将用这个食谱创建的内容。

制作运动图表

准备工作

我们将使用一个用于创建交互式图表的工具包Rickshaw,可以从code.shutterstock.com/rickshaw/获取,并且也是示例代码的一部分。除此之外,我们还需要包括D3.js,因为 Rickshaw 是建立在其之上的。

如何做...

为了创建这个食谱,我们将添加 JavaScript 代码,随机生成数据,并使用 Rickshaw 创建一个交互式图表。

  1. 首先,我们在头部添加外部 JavaScript 和 CSS。按照惯例,我们可以将供应商库放在一个单独的文件夹js/vendor/和 css/vendor/中。
<!doctype html>
<head>
  <link type="text/css" rel="stylesheet"href="css/vendor/graph.css">
  <title>Motion chart</title>
  <script src="img/d3.v2.js"></script>
  <script src="img/rickshaw.js"></script>
</head>
  1. 我们在 body 部分添加图表的占位符。
<div id="content">
  <div id="chart"></div>
</div>
  1. 我们继续主要部分,即js/example.js文件,我们首先创建一个调色板,然后是刷新率。
(function () {
  //create a color palette
    var palette = new Rickshaw.Color.Palette({scheme: 'munin' });
  // we set the refresh rate in milliseconds
    var refreshRate = 500;
  1. 下一步是使用大小为900px x 600px的 SVG 创建Rickshaw.Graph,类型为line。我们使用之前选择的刷新率和指定的调色板。
// create graph
var graph = new Rickshaw.Graph({
  element: document.getElementById("chart"),
  width: 900,

height: 600,
  renderer: 'line',
  series: new Rickshaw.Series.FixedDuration(
    [
      { name : 'one' },
      { name : 'two' },
      { name : 'three' }
    ], palette, {
      timeInterval: refreshRate,
      maxDataPoints: 50
      }
    )
  });
  1. 接下来,我们可以在创建的图表中添加 Y 轴。
var yAxis = new Rickshaw.Graph.Axis.Y({
  graph: graph
});

因为我们创建了所需的对象,它们可以通过调用.render在屏幕上呈现。

graph.render();
yAxis.render();
  1. 我们需要数据来显示,所以我们将生成一些随机数据,并将其添加到图表中。为了延迟添加数据,可以使用 setInterval 在 refreshRate 周期上。
//random util
function getRandomInRange(n){
  return Math.floor(Math.random() * n);
}
// generate random data and add it to the graph
setInterval( function() {
  var data = {
    one: getRandomInRange(50) + 100,
    two: Math.abs(Math.sin(getRandomInRange(30)+1) ) *(getRandomInRange(100) + 100),
    three: 400 + getRandomInRange(110)*2
  };
  graph.series.addData(data);
  //update
  graph.render();  yAxis.render();
}, refreshRate );

在这一点上,我们应该看到类似于食谱开头所示的图形。

它是如何工作的...

我们选择的Rickshaw.Color.Palettemunin方案。还有其他调色板可供选择,例如spectrum14cool。调色板用于简化和自动选择图表的颜色。例如,如果我们手动多次调用.color()方法。

palette.color()
"#00cc00"
palette.color()
"#0066b3"
palette.color()
"#ff8000"

它将始终返回下一个颜色。调色板是一组预定义的颜色,可以在给定的规则集之间选择。例如,原始的任天堂 Game Boy 有四种绿色的阴影,可以用来显示所有的游戏。如果我们看一下 Rickshaw 中调色板的实现,我们会注意到它们只是一系列颜色。以下是 Rickshaw 源代码中调色板cool的定义片段:

this.schemes.cool = [
  '#5e9d2f',
  '#73c03a',
  '#4682b4',
  '#7bc3b8',
  '#a9884e',
  '#c1b266',
  '#a47493',
  '#c09fb5'
  ];

如果我们看一下Rickshaw.Graph的创建,除了 SVG 大小,我们选择了 ID 为chart的元素,图表将在其中呈现。

element: document.getElementById("chart")

此外,我们将renderer类型设置为line,但也可以设置为areastackbarscatterplot,具体取决于结果。

对于series属性,我们使用以下代码片段:

series: new Rickshaw.Series.FixedDuration([
  {name: 'one'},
  {name: 'two'},
  {name: 'three'}
  ], palette, {
  timeInterval: refreshRate,
  maxDataPoints: 50
  })

第一个参数是带有数据名称的数组,然后是调色板,最后是选项对象,我们在其中设置了更新timeInterval。另外,maxDataPoints设置为50,这表示当前显示的数据样本数,也就是我们将显示最后 50 个对象。

之后,我们第一次在graphyAxis对象上调用了.render()方法,然后在setInterval()方法中,我们在每次数据变化时调用了它们的重新渲染。我们构建的渲染数据格式如下:

var data = {
  one: someNumber,
  two: someNumber,
  three:  someNumber
  };

前面的格式表示了特定时间点的三行值。

这个数据对象被传递到系列中,使用了Rickshaw.Series.FixedDuration定义的addData()方法,设置了series属性的最新更新。

graph.series.addData(data);

如果我们需要获取所有显示帧的当前数据,我们可以调用graph.series.dump()方法。

例如,这将返回以下结果:

Object:
 color: "#00cc00"
 data: Array[50]
 name: "one"

还有更多...

有各种方法可以自定义chart ID:过滤信息,添加控件,或者从远程服务器提供数据。如果我们想要附加一个图例,我们可以在图形渲染之前简单地创建这样一个对象,并将其附加到我们的图形对象上。

var legend = new Rickshaw.Graph.Legend({
  element: document.getElementById('legend'),
  graph: myGraph
  });

显示力导向图

在这个示例中,我们将创建一个图表,其中包含威廉·莎士比亚的戏剧哈姆雷特中的一些角色。想法是以一种有趣和互动的方式可视化角色之间的联系。将被可视化的图表类型被称为力导向图。

显示力导向图

准备就绪

为了可视化角色之间的联系,它们需要以某种方式存储。有一个名为data.json的示例文件,是代码示例的一部分,您可以使用它。尽管我们鼓励您创建自己的示例数据,或者至少玩弄现有的数据,但出于简单起见,我们将使用代码示例中提供的数据。

如何做...

我们将创建一个 JSON 文件,包含关系和图像信息,HTML 和相关的 JavaScript。

  1. 首先,我们可以开始创建食谱的数据。我们可以定义nodes列表,其中对象将被放置,具有name属性,指定节点的名称,icon将是图像的 URL,group 将是
{
  "nodes": [
    {
      "name": "Hamlet",
        icon":"http://upload.wikimedia.org/wikipedia/commons/thumb/4/4e/Bernhardt_Hamlet2.jpg/165px-Bernhardt_Hamlet2.jpg"
    },
    {
      "name": "King Claudius",
      "icon": "http://upload.wikimedia.org/wikipedia/commons/thumb/b/b4/Massalitinov_and_Knipper_in_Hamlet_1911.jpg/167px-Massalitinov_and_Knipper_in_Hamlet_1911.jpg"
    },
  1. 在添加数据中的节点之后,我们还需要关于它们如何连接的信息。为此,我们将向模型添加一个links列表。
"links": [
  {
    "source": 1,
    "target": 0
  }
  {
    "source": 3,
    "target": 0
  }
]
  1. 现在我们可以继续创建 HTML 文件。对于这个实现,我们将使用D3.js,所以我们需要包含它,并设置两个 CSS 类,一个用于链接,另一个用于节点文本。
<script src="img/d3.v2.min.js"></script>
<style>
.link {
  stroke: #aaa;
}
.node text {
  pointer-events: all;
  font: 14px sans-serif;
  cursor: pointer;
  user-select: none;
}
</style>
  1. 之后,我们可以开始在主脚本中添加部分。与之前的示例一样,我们首先将 SVG 添加到body元素中,并设置一些预定义的大小。
(function (){
  var width = 960,    height = 600;
  var svg = d3.select("body").append("svg")
  .attr("width", width)
  .attr("height", height);
}
  1. 现在我们可以为图形创建布局。
var force = d3.layout.force()
.gravity(.04)
.distance(350)
.charge(-200)
.size([width, height]);
  1. 下一步是将 JSON 文档中的数据映射到力布局,并创建所有的linksnodes
d3.json("data.json", function(json) {
  force.nodes(json.nodes)
  .links(json.links)
  .start();
  var link = svg.selectAll(".link")
  .data(json.links)
  .enter().append("line")
  .attr("class", "link");
  var node = svg.selectAll(".node")
  .data(json.nodes)
  .enter().append("g")
  .attr("class", "node")
  .call(force.drag);
}
  1. 然后我们从模型中附加image,定义为icon和节点名称的text
node.append("image")
.attr("xlink:href", function(d){return d.icon;})
.attr("x", -32)
.attr("y", -32)
.attr("width", 100)
.attr("height", 100);

node.append("text")
.attr("dx", -32)
.attr("dy", -32)
.text(function(d) { return d.name });
  1. 此外,对于力的变化和更新,我们将设置一个监听器,用于更新链接和节点位置。
force.on("tick", function() {
  link.attr("x1", function(d) { return d.source.x; })
  .attr("y1", function(d) { return d.source.y; })
  .attr("x2", function(d) { return d.target.x; })
  .attr("y2", function(d) { return d.target.y; });

  node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
  });
}());

它是如何工作的...

首先,我们将看一下 CSS,更具体地说是我们设置为allpointer-events。这个设置使元素成为鼠标事件的目标,当指针在内部或边缘上时,只能用在 SVG 元素上。为了禁用文本的选择,我们使用 CSS 属性user-select,并将其设置为none的值。

提示

user-select在各个浏览器中不一致,为了使用它,我们可以添加特定于浏览器的 CSS hack,例如以下内容:

-webkit-touch-callout: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;

此处使用的布局是d3.layout.force(),它不会创建固定的视觉表示,而是我们定义参数,如frictiondistancegravity strength。根据数据和鼠标交互,我们会得到不同的视图。

var force = d3.layout.force()
.gravity(.04)
.distance(350)
.charge(-200)
.size([width, height]);

在设置参数和关于linksnodes的数据信息后构建布局时,我们需要调用start()方法。

force.nodes(json.nodes)
.links(json.links)
.start();

我们想要为数据中的所有节点创建g元素,并设置适当的 CSS 类node

var node = svg.selectAll(".node")
.data(json.nodes)
.enter().append("g")
.attr("class", "node")
.call(force.drag);

还要添加一个行为,以允许使用.call(force.drag)进行交互拖动。

g元素表示一个容器,可用于对其他元素进行分组。应用于g元素的转换也会应用于其所有子元素。这个特性使得该元素成为组织不同视图块的好选择。

注意

有关g元素的更多信息可以在 SVG 规范中找到www.w3.org/TR/SVG/struct.html#Groups

force.drag()方法在d3.layout.force()方法中预定义。拖动事件固定在mouseover上,以允许捕捉移动节点。当接收到mousedown事件时,节点被拖动到鼠标位置。有趣的是,这种行为支持来自移动设备(如 iOS 或 Android)的触摸事件。为了在拖动时禁用节点的点击事件,我们捕获并阻止了mouseup事件的传播。

要为节点创建图像,我们使用 SVG image标签,并使用xlink:href将 URL 添加到存储在d.icon中的数据。

node.append("image")
.attr("xlink:href", function(d){return d.icon;})

为了从布局中获得更新,每次可视化的每个刻度都会分派tick事件。为了保持元素更新,我们为该事件添加了一个监听器。

force.on("tick", function() {
  link.attr("x1", function(d) { return d.source.x; })
  .attr("y1", function(d) { return d.source.y; })
  .attr("x2", function(d) { return d.target.x; })
  .attr("y2", function(d) { return d.target.y; });
  node.attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });
  });

监听器为linknode的移动设置了正确的位置。

还有更多...

这里更明显的选项之一是为可视化添加更多交互。节点可以被制作成可折叠的,还可以为节点添加链接。节点之间的关系可以设置为更细粒度的级别。有多种方法可以使数据随时间刷新并重新加载图表的某些部分。如果需要,可以预设期望的布局,以便节点将尝试确认某个特定的位置。

注意

github.com/mbostock/d3/wiki/Force-Layout了解更多关于 D3 力导向布局和相关功能的信息。

制作实时范围图表过滤器

在处理大量数据时,通常希望添加一些过滤或选择要显示的数据的方法。本教程将涵盖图形的简单范围过滤器和显示时间变化数据系列的图表。

制作实时范围图表过滤器

准备工作

我们将使用与制作动态图表教程相同的工具包来创建交互式图表。必要的库 Rickshaw 可以从code.shutterstock.com/rickshaw/获取,并且也是示例代码的一部分。除此之外,我们还需要 D3,因为 Rickshaw 是基于 D3 的。

操作步骤

我们将创建一个包含 JavaScript 文件的 HTML 页面,同时为图形生成随机数据,并添加额外的过滤元素。

  1. 首先,我们将制作一个 HTML 页面,并包含所需的库的 CSS。
<!DOCTYPE html>
  <html>
    <head>
      <link type="text/css" rel="stylesheet"href="css/vendor/graph.css">
      <link type="text/css" rel="stylesheet"href="css/vendor/legend.css">
      <link rel="stylesheet" type="text/css"href="http://code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css">
      <link type="text/css" rel="stylesheet"
        href="css/main.css">
    </head>
  1. 请注意,我们添加了一个额外的文件legend.css,其中包含有关图例的布局信息。然后我们可以添加我们自定义的 CSS 文件。
<link type="text/css" rel="stylesheet" href="css/main.css">
  1. 用于图形、图例和幻灯片的 HTML 占位符将是常规的div元素。
<div id="content">
<div id="chart"></div>
<div id="legend"></div>
</div>
<div style="clear:both"></div>
<div id="slider"></div>
  1. 我们添加了库的依赖项。除了 Rickshaw 及其依赖项 D3 之外,我们还将添加 jQuery 和 jQuery UI,因为我们将使用其中的控件。现在,我们可以继续进行主要的 JavaScript,并开始定义颜色调色板和刷新率。
var refreshRate = 300;
var palette = new Rickshaw.Color.Palette( { scheme: 'munin' } );
  1. 下一步是在大小为900px x 500px的图表元素中创建图表。
var graph = new Rickshaw.Graph( {
  element: document.getElementById("chart"),
  width: 900,
  height: 500,
  renderer: 'area',
  series: new Rickshaw.Series.FixedDuration([{
    color: palette.color(),
    name: 'NASDAQ'
  },
  {
    color: palette.color(),
    name: 'NIKKEI'
  }], palette, {
    timeInterval: refreshRate,
    maxDataPoints: 200,
    timeBase: new Date().getTime() / 1000
  })
});
  1. 至于slider属性,Rickshaw 为我们提供了一个现成的控件,我们可以将其连接到我们创建的图表上。
var slider = new Rickshaw.Graph.RangeSlider({
  graph: graph,
  element: $('#slider')
});
  1. 要绘制 Y 轴,我们可以创建它,并将其连接到我们的图表上。
var yAxis = new Rickshaw.Graph.Axis.Y({
  graph: graph
});
  1. 为了在显示的数据样本上创建颜色和名称的图例,我们可以使用一个控件,并将其连接到我们的图表上,同时还要指定它将被渲染的元素。
var legend = new Rickshaw.Graph.Legend({
  graph: graph,
  element: $('#legend').get(0)
});
  1. 因为这个示例有一个时间序列组件,我们将生成随机数据。在生成数据之后,我们调用graph.series.addData(data)并重新渲染graphyAxis属性。这个生成、数据更新和渲染发生在每个refreshRate毫秒。
function getRandomInRange(n){
  return Math.floor(Math.random() * n);
}
setInterval( function() {
  var data = {
    one: getRandomInRange(50) + 100,
    two: 400 + getRandomInRange(110)*2
  };
  graph.series.addData(data);
  graph.render();
  yAxis.render();
}, refreshRate );

操作方法...

让我们来看看图表的系列输入参数。

series: new Rickshaw.Series.FixedDuration([{
  color: palette.color(),
  name: 'NASDAQ'
  }, {
  color: palette.color(),
  name: 'NIKKEI'
  }], palette,

除了图表数据,我们还有namecolor属性。现在,你可能会问自己,为什么有一个color属性并输入一个调色板呢?嗯,我们这样做是为了使其他插件能够读取这些信息。

其中一个插件是Rickshaw.Graph.Legend,它构建一个显示每个数据流信息的图例框。

我们还在 X 轴上添加了一个范围过滤器Rickshaw.Graph.RangeSlider

var slider = new Rickshaw.Graph.RangeSlider({
  graph: graph,
  element: $('#slider')
});

在后台,slider属性使用了 jQuery UI 控件,设置为range:true。最小值和最大值来自当前图表数据。slider属性有一个slide事件,用于限制图表上显示的样本大小。

因为数据不断添加到图表中,slider属性的最小值和最大值根据图表的事件相应地设置。这些都是在开发自定义控件时需要牢记的一些考虑因素。

它是如何工作的...

将滑块设置为仅显示给定时间的一部分。因为时间改变了,滑块会随着数据一起移动。

制作图像轮播

图像轮播是网站上使用的最受欢迎的营销和展示工具之一。它们也可以用于显示图像库或演示文稿。

在这个示例中,我们将构建一个图像轮播。它将支持自动定时转换,如果用户移动到轮播区域,转换将停止。它将有一个导航区域,包括表示当前活动图像和剩余图像数量的控制矩形。

这将是一个利用 HTLM5 功能的 3D 轮播,如 CSS3 3D 转换。

准备工作

我们需要在目录中与我们的代码一起放置三张图片。它们应分别命名为1.jpg2.jpg3.jpg

操作方法...

我们将使用 jQuery、HTML5 和 CSS 转换来创建图像轮播。

  1. 首先,我们将创建一个带有轮播和灰色图像控件的 HTML 页面。我们将把控件放在轮播的中下部分。
<!DOCTYPE html>
<html>
  <head>
    <title>Image carousel</title>
    <style type="text/css">
  1. 为了获得具有深度的 3D 视图,主容器必须具有perspective属性。它表示观察者与屏幕的距离。它会使附近的东西看起来更大,而远处的东西看起来更小。
#carousel {
 perspective: 500px;
  -webkit-perspective: 500px;
  position:relative; display:inline-block;
  overflow:hidden;
}
  1. 我们将把所有的图像放在旋转器内,然后旋转旋转器本身。为了做到这一点,旋转器上的旋转必须保持子元素的 3D 转换。

  2. 此外,旋转器和图像都将具有过渡动画。我们通过添加transition属性来指定这一点。在我们的示例中,过渡将在变换上起作用,并且持续一秒钟。

#rotator {
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  position:relative;
  margin:30px 100px;
  width:200px; height:200px;
  transition: transform 1s;
  -webkit-transition: -webkit-transform 1s;
}
#rotator img {
  position:absolute;
  width: 200px; height:200px;
  transition: transform 1s;
  -webkit-transition: -webkit-transform 1s;
}
#controls {
  text-align: center;
  position:absolute;
  left:0; bottom:0.5em;
  width:100%;
}
#controls span {
  height: 1em; width: 1em;
  background-color:#ccc;
  margin: 0 0.5em;
  display: inline-block;
}
  </style>
</head>
<body>
  <div id="carousel">
    <div id="rotator">
      <img class="image" src="img/1.jpg">
      <img class="image" src="img/2.jpg">
      <img class="image" src="img/3.jpg">
      </div>
    <div id="controls"></div>
  </div>
  <script src="img/jquery.min.js"></script>
  <script type="text/javascript" src="img/example.js">
</script>
</body>
</html>
  1. 轮播动画和可点击控件的代码将在example.js中。
(function() {
  $("#carousel").on('mouseover', pause);
  $("#carousel").on('mouseout', start);
  var position = 0;
  var all = $("#carousel").find('.image');
  var total = all.length;
  1. 我们将把所有图像放在它们在 3D 空间中的适当位置,每个图像都旋转了一个角度的倍数,并且移动了一个计算出的量。有关更多信息,请参阅本食谱的它是如何工作...部分。
  var angle = (360 / total);
  var deg2radfac = 2 * Math.PI / 360;
  var zMovement = $("#rotator").width() / 2 *Math.tan(deg2radfac * angle / 2);
  all.each(function(k) {
    var trans = 'rotateY(' + (angle * k).toFixed(0) + 'deg)'
    + 'translateZ('+ zMovement.toFixed(0) + 'px)';
    $(this).css('transform', trans);
  });
  $("#rotator").css('transform', 'translateZ('+ (0 - zMovement).toFixed(0) + 'px)');
  1. 对于每个图像,我们添加一个控制标记,可以激活该图像。
for (var k = 0; k < all.length; ++k) {
  $('<span />').attr('data-id', k).appendTo("#controls");
}
$("#controls").on('click', 'span', function() {
  changeTo(position = $(this).attr('data-id'));
});
ctrls = $("#controls span");
start();
  1. 最后,让我们编写改变旋转木马位置的函数。change函数通过dir元素改变位置,changeTo直接将位置更改为指定的元素。然后我们可以启动我们的旋转木马计时器。
function change(dir) {
  dir = dir || 1;
  position += dir;
    if (position >= all.length) position = 0;
    else if (position < 0) position = 0;
    changeTo(position);
  }
function changeTo(position, cb) {
  ctrls.css({'opacity': 0.33});
  ctrls.eq(position).css({'opacity': 1});
  $("#rotator").css('transform',
  'translateZ('+ (0 - zMovement).toFixed(0) + 'px) ' +
  'rotateY(' + (angle * position).toFixed() + 'deg) ');
  }
function start() { timer = setInterval(change, 5000); }
function pause() {
  if (timer) { clearInterval(timer); timer = null; }
  }
}());

它是如何工作的...

它是如何工作...

我们的旋转木马的构建取决于我们将要使用的图像数量。为了更好地了解我们应用变换时到底发生了什么,让我们看一下旋转木马的俯视图。前面的图显示了一个有五个面的旋转木马。每个面都被一个距离z从中心点移开,然后以角度a旋转多次。角度可以计算如下:a = 360 / 边数

然而,翻译z稍微难以计算。为了做到这一点,我们需要看一下由z和一半边宽组成的三角形。通过应用三角函数方程tan(a/2) = (w/2) / z,我们可以计算z = w/2 / tan(a/2)

为了旋转旋转木马,我们每 5 秒将rotator父元素旋转一个角度a。用户可以点击控件来改变旋转。

我们还通过zrotator向相反方向移动,以使旋转木马中前端元素的距离保持不变,就好像它没有被翻译过一样。

我们希望这个食谱通过使用一些新的 HTML5 功能为制作旋转木马这个略显沉闷的主题增添一些乐趣和新鲜感,这肯定会让用户们大吃一惊。

注意

截至目前,一些 CSS3 功能并不是普遍可用的。尽管 Internet Explorer 9 在其他方面支持大量 HTML5,但它并没有这些功能,尽管它们在 Internet Explorer 10 中是可用的。在使用这些技术之前,请查看目标浏览器的要求。

缩放和平移图表

我们在本书前一章讨论的图表是静态的。因此,它们非常适合可视化有限数量的数据。但是,当数据集变得太大时,用户可能需要交互地选择图表中显示的数据范围。

为了实现这一点,我们将制作一个能够进行交互控制的图表,例如缩放和平移。Flot 图表库通过其导航插件轻松支持此功能。

在这个食谱中,我们将以 30 分钟的间隔显示一周的温度历史。我们将允许用户缩放和平移历史。

准备工作

我们需要从官方网站www.flotcharts.org/下载 Flot,并将内容提取到一个名为flot的单独文件夹中。

如何做...

为了创建这个食谱,我们将添加 Flot、jQuery,并创建一个 HTML 文件。

  1. 首先,我们创建一个基本的 HTML 页面,其中包含一个图表的占位符。我们还将包括 jQuery(Flot 所需的),Flot 本身和 Flot 导航插件。Flot 需要在一个占位符div中绘制图表画布,因此我们将提供一个。占位符需要通过 CSS 指定widthheight;否则 Flot 可能无法正确绘制图表。
<!DOCTYPE HTML>
<html>
  <head>
    <title>Chart example</title>
  </head>
  <body>
    <div id="chart" style="height:200px;width:800px;"></div>
    <script src="img/jquery.min.js"></script>
    <script src="img/jquery.flot.js"></script>
    <script src="img/jquery.flot.navigate.js"></script>
    <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>
  1. 我们将在example.js中添加我们的代码。
$(function() {
  var now  = Date.now();
  var hour = 60 * 60 * 1000, day = 24*hour;
  var weekAgo = now - 7*day;
  var zoomOut = null;

  function getData(cb) {
    var temperatures = [];
    // Generate random but convincing-looking data.
    for (var k = 24 * 7; k >= 0; --k)
    temperatures.push([now - k*hour,Math.random()*2 + 10*Math.sin(k/4 + 2)]);
    cb(temperatures);
  }

  getData(function(data) {
    var p = $.plot("#chart", [{data: data}], {
      xaxis: {
        mode: 'time',
        zoomRange: [day / 2, 7 * day],
        panRange: [weekAgo, now]
      },
    yaxis: { zoomRange: false,   panRange: false },
    zoom: { interactive: true },pan:  { interactive: true }
    });
  zoomOut = p.zoomOut.bind(p);
  });
  $('<input type="button" value="zoom out">')
  .appendTo("#chart")
  .click(function (e) {
    e.preventDefault();
    zoomOut && zoomOut();
  });
});

它是如何工作的...

为了绘制图表,首先我们编写了函数getData来生成一些看起来令人信服的随机温度数据,白天上升,夜晚下降。由于它是基于回调的,我们可以用一个从服务器获取数据的函数来替换这个函数。

绘图函数$.plot需要三个参数。第一个是绘图占位符,第二个是我们需要绘制的系列数组,第三个是绘图选项。我们将只传递一个系列。

我们图表的新添加是绘图选项和缩小按钮。我们在轴选项中指定了缩放和平移范围。我们的 Y 轴不支持缩放和平移,因此已被禁用。

zoomRange选项指定了在缩放时完整绘图的最小和最大范围。例如,我们的选项指定绘图将缩放至少显示半天,最多显示一周的完整范围。

panRange选项指定了 X 轴上的最小最小值和最大最大值。在我们的示例中,我们指定用户不能将图表平移以使其最小值低于weekAgo,也不能将其最大值高于now

最后,我们指定了缩放和平移将是交互式的。这意味着用户可以双击放大,也可以通过鼠标拖动进行平移。

为了允许用户重置缩放,我们添加了一个zoomOut按钮,它调用zoomOut函数。每当我们重新绘制绘图时,我们需要更新此函数,因为从$.plot调用返回的对象会发生变化。这样就允许多次调用getData

通过这样做,我们为我们的图表添加了交互性,允许用户自定义他们想要查看的数据范围。Flot 导航适用于各种图表;务必查看前一章,以了解支持的一些图表类型的概述。

使用 Web 通知 API

Web 通知是现代浏览器中添加的较新功能之一。它们旨在作为用户在网页上下文之外的警报。想法是它们是为浏览器而设计的,例如,在使用移动浏览器时,通知可以进入设备的主屏幕。在桌面上,它们通常显示在屏幕的右上角,至少在大多数桌面环境中是这样。

使用 Web 通知 API

准备工作

为了本例的目的,我们将使用从古腾堡计划www.gutenberg.org/获取的数据。数据是来自《孙子兵法》中间谍的使用章节的提示,可以在此处的data.json代码示例下找到。

如何做...

为了创建这个示例,我们将创建一个 HTML 文件,并使用 jQuery 来简化。

  1. 首先,我们可以从 HTML 部分开始,我们只需创建一个简单的button和一个带有 IDfallbackdiv元素,如果浏览器不支持通知,我们将使用它。
<body>
 <button id="show">Show quote</button>
 <div id="fallback" ></div>
  <script src="img/jquery.min.js"></script>
  <script src="img/notification.js"></script>
  <script src="img/display.js"></script>
</body>
  1. 让我们首先创建notification.js文件,我们将用它作为创建simpleNotifations.show(data)的实用程序。我们首先要做的检查是验证对webkitNotifications的支持,在撰写本文时,这是唯一完整的实现。
var simpleNotification = (function () {
  var my = {};
   my.show = function (data) {
    if (window.webkitNotifications) {
      //check if there is a support for webkitNotifications
      if (window.webkitNotifications.checkPermission()== 0) {
        var notification = webkitNotifications.createNotification(data.icon, data.title, data.body);
        notification.show();
        //set timeout to hide it
        setTimeout(function(){
        notification.cancel();
      }, data.timeout);
    } else {
      webkitNotifications.requestPermission(function () {
        //call the same function again
        my.show(data);
      });
    }
  }
  1. 接下来是对基于真实标准的 Web 通知对象的检查,在未来,随着浏览器对其实现的不断增加,它应该是第一个。
else if (window.Notification) {
  if ("granted" === Notification.permissionLevel()) {
    var notification = new Notification(data.title, data);
      notification.show();
    } else if ("default" === Notification.permissionLevel() ) {
      Notification.requestPermission(function () {
        //call the same function again
        my.show(data);
      });
    }
  }
  1. 最后一种情况;如果系统不支持任何类型的通知,我们只需使用回调来处理这种情况,同时关闭实用程序。
  }else{
    //Notifications not supported, going with fallback
    data.errorCallback();
    }
  };
  return my;
}());
  1. 接下来,我们可以继续创建display.js文件,该文件将从数据中获取一个随机引用,并调用先前定义的simpleNotification.show()方法。首先我们将进行获取。
  function fetchRandomQuote(location,data){
    $.ajax(
      {
        url:location,
        dataType:'json',
        success: function(result){
          var quoteNumber = Math.floor(Math.random()*26)+1;
          var obj = result.quotes[quoteNumber];
          for(var key in obj){
            data.title += key;
            data.body = obj[key];
        }
       simpleNotification.show(data);
    }}
    );
  };
  1. 因为我们希望所有通知都有一些默认行为,例如图标、默认消息或回退函数,所以我们使用默认的data对象进行调用。
$(document).ready(function() {
  $("#show").click(function (){
    var data = {
      icon: "images/war.png",
      title: "The Art of War - The Use of Spies ",
      body: "text",
      timeout : 7000,
      errorCallback: function(){
        $("#fallback").text(this.body);
        }
      };
    fetchRandomQuote('js/data.json',data);
    });});

它是如何工作的...

我们将更深入地研究notification.js文件,其中大部分通知逻辑都在其中。我们对通知的检查测试if (window.webkitNotifications)if (window.Notification)尝试查看浏览器中是否有这样的对象。如果没有这样的对象,这意味着不支持该类型的通知。另一方面,如果满足了if条件,这意味着我们有支持,并且可以请求权限。

  if (window.webkitNotifications.checkPermission() == 0)

之后,我们可以自由创建通知,并使用给定的icontitlebody参数显示它。

var notification = webkitNotifications.createNotification(data.icon, data.title, data.body);
notification.show();

如果我们希望通知在给定的超时后隐藏,我们添加以下函数:

setTimeout(function(){
  notification.cancel()
}, data.timeout);

另一方面,如果我们没有权限显示通知,我们需要从用户那里请求,然后再次调用我们的函数。

webkitNotifications.requestPermission(function () {
  my.show(data);
}

提示

对权限的请求必须来自用户触发的 HTML 元素上的事件。在我们的情况下,这是按钮上的onClick函数。更具体地说是 jQuery 点击$("#show").click(function (){ ...}

我们不需要过多关注数据的获取,但在我们的默认对象中,我们有icon参数的值为images/war.png,我们将用于通知,以及fallback函数和timeout配置。

var data = {
  icon: "images/war.png",
  title: "The Art of War - The Use of Spies ",
  body: "text",
  timeout : 7000,
  errorCallback: function(){
    $("#fallback").text(this.body);
    } };

注意

在撰写本文时,Chrome 是唯一长期支持通知的浏览器,但 Safari 6.0 和 Firefox 22 Aurora 也有初始实现。

有关 Web 通知的完整规范可以在www.w3.org/TR/notifications/中找到。

从数据集创建交互式地理图表

在这个示例中,我们将看到如何创建看起来很酷的交互式地理图表,以及如何用它们来显示数据。这在显示更大地理区域的统计数据方面变得非常普遍,通常是选举结果或全球变暖影响。为了拥有覆盖多个不同国家的地图,我们将可视化英联邦成员和成员申请者的统计数据。

注意

英联邦是由 54 个独立主权国家自愿组成的联合体(其中一个成员目前被暂停)。大多数是前英国殖民地或这些殖民地的附属地。英联邦中没有一个政府对其他国家行使权力,就像政治联盟一样。相反,这种关系是一个国际组织,通过这个组织,具有不同社会、政治和经济背景的国家被视为地位平等,并在共同价值观和目标的框架内合作,这些价值观和目标在新加坡宣言中有所阐述,可以从中阅读

en.wikipedia.org/wiki/Member_states_of_the_Commonwealth_of_Nations

从数据集创建交互式地理图表

准备工作

有 JSON 对象定义了各个地区的边界和比例尺级别,大部分是从公共领域数据集中获取的,可在www.naturalearthdata.com/downloads/上找到。

在我们的案例中,我们使用了一个world-data.json文件,可以在代码示例中找到。

如何做...

获取world-data.json文件后,我们可以开始创建 HTML 和 JavaScript 文件。

  1. 让我们首先看一下world-data.json文件中的这些国家边界数据,例如巴哈马。
{
  "type":"Feature",
  "properties":{
    "name":"The Bahamas"
  },
  "geometry":{
    "type":"MultiPolygon",
    "coordinates":[":[
        [
          [
            [
              -77.53466,
              23.75975
            ],
              [
                -77.78,
                23.71
              ], …
            ]}}

在它们的属性中,我们有国家的名称,以及以多个点表示的国家的几何形状。

注意

有许多不同的方法来创建和表示边界数据。为了创建自己的边界或获取已有数据,Open Street Map(www.openstreetmap.org/)是一个提供这些选项的伟大项目。例如,名为 Osmosis 的工具可以用于从wiki.openstreetmap.org/wiki/Osmosis获取许多不同缩放级别的有关某些对象的矢量数据。

  1. 我们可以继续在头部添加 CSS 和D3.js的依赖项。
<style>
  .frame {
    stroke: #333;
    fill: none;
    pointer-events: all;
  }
  .feature {
    stroke: #ccc;
  }
</style>
<script src="img/d3.v2.js"></script>
  1. 在 body 部分,我们直接从example.js文件开始,并定义有关欧元区国家名称部分和生成随机数字和颜色的实用程序。
<script>
var commonwealth = [
"Australia", "Algeria",
"The Bahamas", "Bangladesh",
"Belize", "Botswana",
"Brunei", "Cameroon",
"Canada", "Cyprus",
"Gambia", "Ghana",
"Guyana", "India",
"Jamaica", "Kenya",
"Lesotho", "Malawi",
"Malaysia", "Mozambique",
"Madagascar", "Namibia",
"New Zealand", "Nigeria",
"Pakistan", "Papua New Guinea",
"Rwanda", "Sierra Leone",
"Solomon Islands", "Somaliland",
"South Africa", "South Sudan",
"Sudan", "Sri Lanka",
"Swaziland", "United Republic of Tanzania",
"Trinidad and Tobago", "Yemen",
"Uganda", "United Kingdom",
"Vanuatu", "Zambia"
];

function random(number) {
  return Math.floor(Math.random()*number).toString(16)
}
function randomColor() {
  return "#"+random(255)+random(255)+random(255);
}
  1. 在那里,我们添加了一个用于获取随机颜色的实用函数,如果国家是该区域的一部分,则为#bbb
function getColorForCountry(name){
  if(commonwealth.indexOf(name)<0){
    return "#bbb";
  }else {
    return randomColor();
  }
}
  1. 然后,为了获得类似框架的效果,我们设置了周围的边距。
var margin = {
  top: 10,  right: 10,
  bottom: 10,  left: 10
},
  width = 960 - margin.left - margin.right,
  height = 500 - margin.top – margin.bottom;
  1. 接下来,我们定义投影、缩放行为和路径的类型,其中缩放行为在zoom事件上添加了对move()方法的回调。
var projection = d3.geo.mercator()
  .scale(width)
  .translate([width / 2, height / 2]);

var path = d3.geo.path()
  .projection(projection);
var zoom = d3.behavior.zoom()
  .translate(projection.translate())
  .scale(projection.scale())
  .scaleExtent([height, 10 * height])
  .on("zoom", move);
  1. 我们使用先前设置的宽度和高度创建 SVG 图像,并调用缩放行为以进入所选的缩放级别。
var svg = d3.select("body").append("svg")
  .attr("width", width + margin.left + margin.right)
  .attr("height", height + margin.top + margin.bottom)
.append("g")
  .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
  .call(zoom);
  1. 首先,我们为地图的选定特征创建了g元素。
var feature = svg.append("g")
  .selectAll(".feature");
  1. 然后,我们通过创建 SVG 矩形为地图添加了一个框架。
svg.append("rect")
  .attr("class", "frame")
  .attr("width", width)
  .attr("height", height);
  1. 现在我们需要从world-data.json文件中获取数据,为各个国家创建路径,并根据d.properties.name是否属于所需组来填充它们的适当颜色。
d3.json("js/world-data.json", function(data) {
  feature = feature
    .data(data.features)
    .enter().append("path")
    .attr("class", "feature")
    .attr("d", path)
    .style("fill", function(d){return getColorForCountry(d.properties.name)});
  });
  1. 最后,在缩放时调用move()函数。
function move() {
  projection.translate(d3.event.translate).scale(d3.event.scale);
  feature.attr("d", path);
}

它是如何工作的...

首先,d3.geo.mercator()从球形数据表示构造了一个墨卡托投影

注意

墨卡托投影是由杰拉尔德斯·墨卡托在 1569 年创建的柱状地图投影。它被广泛用于表示地图,但它的问题是随着我们从赤道向极地移动,对象的大小和形状会发生扭曲。有关墨卡托投影的更多信息,请参见en.wikipedia.org/wiki/Mercator_projectionen.wikipedia.org/wiki/File:Cylindrical_Projection_basics2.svg

它是如何工作的...

d3.geo.path()方法使用一些预定义的设置创建一个新的地理生成。我们将此路径生成器设置为使用我们的projection类型。

var path = d3.geo.path()
  .projection(projection);

d3.behavior.zoom()方法使我们能够为我们的projection类型添加自动缩放功能,并在scaleExtent中给定缩放的比例和范围。此外,这会在zoom事件上创建一个监听器,调用move()函数。

d3.behavior.zoom()
  .scale(projection.scale())
  .scaleExtent([height, 10 * height])
  .on("zoom", move);

创建县的主要特点是,我们从world-data获取要素数据,并创建实际表示各个国家的 SVG 路径,然后可以对其进行样式和填充颜色。

d3.json("js/world-data.json", function(data) {
  feature = feature
    .data(data.features)
    .enter().append("path")

这种类型的地图也被称为分级地图,意思是显示某些统计变量的专题地图。

文件js/world-data.json包含每个国家的边界和一些元数据。元数据与我们的英联邦国家列表进行匹配。如果它们匹配,国家就会被着色。请注意,我们的地图数据中还有一些国家是不可用的。

它是如何工作的...

还有更多...

在处理 JavaScript 和地图时,经常出现两种格式。一种是 GeoJSON(www.geojson.org/),一种用于各种地理数据结构的格式。另一种是名为 TopoJSON(github.com/mbostock/topojson)的格式,它是 GeoJSON 的扩展,用于编码拓扑。TopoJSON 使用称为弧的线段来获得比 GeoJSON 更好的特性。

有一家名为 CartoDB 的公司,cartodb.com/,专门从事地图制作,并在后台广泛使用 D3。他们提供一个免费计划,有很多值得一试的选项,尽管它是商业产品。

第四章:使用 HTML5 输入组件

在本章中,我们将看一下 HTML5 中添加的一些出色的新元素类型。涵盖的主题有:

  • 使用text输入字段

  • 使用textarea

  • 输入日期

  • 输入时间

  • 电话输入

  • 范围输入字段

  • 颜色选择器输入

  • 使用单选下拉菜单

  • 使用多选选择列表

  • 获取地理位置输入

  • 在客户端使用文件输入

  • 使用拖放文件区域

介绍

表单是日常 Web 应用程序开发的一部分。我们进行了大量的重新发明来启用各种输入功能。HTML5 添加了一些新的输入类型以及对现有结构的许多不同属性和扩展。这些新的东西大多已经在现代浏览器中可用,并且使我们所有人的生活更加轻松。对于尚未存在的东西,我们使用在旧系统上运行的备用方案。没有理由您今天不应该开始使用至少其中一些功能。

注意

有各种方法来确定对 HTML5 功能的支持。许多网站提供了支持的功能列表,但其中一些值得一提的是caniuse.com/html5please.com/。如果您不想添加备用方案,您通常可以参考它们以获取最新信息。

使用文本输入字段

我们将看一下使用 HTML <input type="text">输入数据的基本示例之一。这种输入类型会自动从输入值中删除换行符,因此适用于单行文本使用,如下面的屏幕截图所示:

使用文本输入字段

如何做...

在 HTML 文档的 body 部分,我们将创建一个表单,其中类型为text的输入将被放置:

  1. 首先,我们添加最基本的输入类型text
<form>
  <p>
    First name  <input name="firstname" type="text">
  </p>
  1. 接下来,我们添加一个启用音频输入的表单:
  <p>
    Speak up <input name="quote" type="text" x-webkit-speech speech>
  </p>
  1. 还添加一个带有placeholder属性和一个带有autofocus属性的输入:
  <p>
    Last name: <input name="lastname" type="text" placeholder="John Doe">
  </p>
  <label>
    Comment <input name="comment" type="text" title="This is area to insert your opinion" autofocus >  </label>
  1. 最后,我们添加submit并关闭表单:
  <input type="submit" >
</form>

它是如何工作的...

<input name="firstname" type="text" >元素是最基本的 HTML 输入元素,在提交表单时,查询参数将是:

?firstname=someText&...

下一个输入元素具有一个属性x-webkit-speech speech,即 Chrome 特定属性,允许语音输入,这意味着您可以使用麦克风插入文本。

注意

请注意,这不太可能成为标准,因为它依赖于谷歌服务器端处理语音,因此远非开放网络。为了获得广泛的接受,应该提供开放的语音提供者。

对于第三个输入元素,我们使用了placeholder属性,在输入字段内添加了一个漂亮的提示。

HTML5 中添加的一个新属性是autofocus。它是一个布尔值属性,允许我们指定页面加载后应该具有初始焦点的表单控件。在我们的情况下,我们使用了单词语法,但autofocus="true"也会起到同样的作用。这里需要额外注意的一点是,这只能应用于一个表单元素,因为这是将获得初始焦点的元素,而且它不能应用于input type="hidden",因为这样做没有太多意义。

还有更多...

如果我们正在使用自己的备用方法来插入语音数据,我们可以简单地检查当前是否支持它,以支持其他浏览器:

  var hasSupportForSpeach = 
    document.createElement("input").webkitSpeech != undefined;

还有一个触发的事件,我们可以用于语音输入:

onwebkitspeechchange="myOnChangeFunction()"

注意

开发的语音输入的开放替代方案是Web Speech API。其主要目标是为开发人员提供一种手段,以便将语音输入和输出作为文本到语音。API 定义不包括识别将在何处进行的实现,这意味着服务器端或客户端实现取决于供应商。有关 API 的更多信息,请访问dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html

负责 HTML5 中关于语音集成的初始要求和规范的孵化工作组可以在以下网址找到:www.w3.org/2005/Incubator/htmlspeech/

使用文本区域

在这个示例中,我们将看看textarea元素,并创建一个简单的表单来展示一些可以使用的属性。textarea用于作为多行纯文本编辑控件。

如何做...

我们将创建一个示例form来演示textarea元素的一些用法:

  1. 首先,我们添加一个带有设置placeholder属性的texarea元素:
<form>
  <label>Short info: <textarea placeholder="some default text"></textarea>
  </label>
  1. 然后我们添加一个带有设置rowscols属性的文本区域:
<label>
  Info with default size set: <textarea  rows="4" cols="15" placeholder="some default text"></textarea>
</label>
  1. 然后我们添加一个设置了maxlength的文本区域:
  <label>
    Max area limited to 5 characters <textarea maxlength="5" placeholder="Inset text here limit 5 char"></textarea>
  </label>
  1. 然后我们添加一个设置了title属性的元素:
  <label>
    Tip on hover <textarea maxlength="5" title="add an super awesome comment"></textarea>
  </label>
  1. 最后我们添加submit并关闭form
  <input type="submit"/>
</form>

它是如何工作的...

第一个示例是一个常规的<textarea />元素,允许多行文本和换行。我们还可以使用rowscols等属性来启用一些初始大小。这也可以通过 CSS 设置初始大小来实现:

textarea{
  width:300px;
  height:100px;
}

现在大多数新的浏览器都有一个可以拖动的文本区域的小右下角,使用户可以调整大小。可以通过在 CSS 中设置max-widthmax-height来禁用文本区域的可调整大小。

我们还可以使用maxlength属性限制可以插入的字符数,例如在我们的示例中将其设置为最多 5 个字符maxlength="5"

还有一个title属性,可以用来向用户添加有关输入字段的提示。

它是如何工作的...

注意

诸如titlemaxlengthplaceholder之类的属性不仅适用于textarea,还可以用于其他元素。例如,title属性是 HTML5 中的全局属性之一,可以添加到任何元素上。我们可以有以下片段:

<input type="text" autofocus maxlength="10" placeholder="Awesome">

有关输入属性和全局元素属性的更多信息,请访问网站www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#global-attributes

输入日期

在 HTML5 之前,我们被迫创建自定义控件,这些控件总是缺少一些功能,或者与某些浏览器不兼容。现在,日期有单独的输入类型,在这个示例中我们将看到如何使用它们。不幸的是,它们在各种用户代理中仍然没有完全实现,但是每个人都在慢慢赶上。

如何做...

我们将简单地创建一个基本的 HTML 文档,并在 body 元素中创建一个表单:

  1. 首先在 body 部分添加form,并在其中添加date input元素:
<form>
  <label>
    Select date  <input name="theDate" type="date">
  </label>
  1. 类似地,我们为monthweek添加一个输入元素:
<label>
  Select month <input name="theMonth" type="month">
</label>
<label>
  Select week <input name="theWeek" type="week">
</label>
  1. 最后,我们添加一个简单的submit并关闭form
  <input type="submit" />
</form>

它是如何工作的...

根据您的浏览器支持情况,您将获得一个空的输入字段或一个完整的日期输入控件:

它是如何工作的...

<input type="week" /> rendered on Opera v12.11

在表单提交时,表单发送的参数是有效的字符串:

?theDate=2012-12-21&theMonth=2012-12&theWeek=2012-W5

日期、周和月的创建和编号符合 ISO 8601 标准,在大多数编程语言中被广泛接受,或者至少有一种标准化的表示和访问数据的方式。还有一个选项可以指定minmax属性,这些属性应该是有效的日期、月份和周字符串,以及step,它将定义选择控件的步骤,并默认为1

输入时间

在这个示例中,我们将看看如何使用时间输入控件以及它们如何与日期选择结合。总体思路是让用户代理呈现一个可以用作输入的时钟。有选项可以包括时区,并且有纯时间表示,并将通过创建一个简单的表单来尝试它们,如下面的屏幕截图所示:

输入时间

如何做...

与其他示例类似,我们创建一个包含少量输入元素的表单:

  1. 首先我们开始表单并添加time输入元素:
<form>
  <label>
    Select time <input name="time" type="time" >
  </label>
  1. 我们添加一个datetime-local输入:
  <label>
    Date and time local <input name="datetime-local" type="datetime-local" >
  </label>
  1. 我们还添加了一个datetime输入:
  <label>
    Select date and time <input name="datetime" type="datetime" >
  </label>
  1. 最后我们添加提交并关闭表单
  <input type="submit">
</form>

它是如何工作的...

在表单提交时,所选的值将作为查询参数添加到 URL 中,例如:

/?time=00%3A00%3A00.00&datetime-local=2012-11-02T12%3A00&datetime=2012-12-21T12%3A00Z/

这里的time参数的值为00:00:00,其中%3A是 URL 编码的:字符。

类似地,datetime-local2012-11-02T12%3A00值实际上是2012-11-02T12:00,遵循YYYY-MM-DDThh:mm:ss模式的日期和时间参数。

至于datetime变量,字符串的格式为YYYY-MM-DDThh:mm:ssTZD,其中我们有关于时区的附加信息。

由于我们对输入元素有了正确的上下文,当在具有良好支持的浏览器上打开时,控件将被优化。

它是如何工作的...

电话输入

在这个示例中,我们将看一下电话号码的输入类型。由于不同国家之间的电话号码格式非常不同,因此电话输入不需要任何特定的模式,除非明确指定。如果我们需要一些特定的模式,我们可以进行各种类型的验证,如在第六章数据验证中详细讨论的那样。

使用文本输入类型的主要优势在于更具语义,因此可以在移动设备上进行更多优化。

如何做...

与前面相关的示例一样,我们只需在 HTML 文档的主体中添加input元素:

  <form>
    <label>
      Insert phone <input type="tel" >
    </label>
  <input type="submit" >
  </form>

它是如何工作的...

当您首次尝试时,它看起来像是常规的input type="text"元素。但这个现在更具语义正确性。现在为什么这很重要,或者为什么我们应该关心它呢?

移动设备将把这个识别为电话号码,并自动打开数字键盘,就像 Android 设备在下面的屏幕截图中显示的那样:

它是如何工作的...

还有更多...

还有input type="url"input type="search"input type="email",它们为元素提供语义,允许移动设备选择正确的键盘。它们还可以具有关于数据如何插入的附加验证和逻辑,从而允许更多类型特定的功能。其中一些将在与验证相关的示例中介绍。

在 HTML5 中,对于所有输入类型,都添加了一个称为inputmode的属性,该属性源自术语输入模式。这些属性为浏览器提供了关于应使用何种类型的键盘的提示。这些属性可以具有以下值:

  • verbatim:此值指定可以常用于非散文文本的字母数字字符,例如用户名、关键字或密码。

  • latin:此值指定用户首选语言中的拉丁输入,具有在移动设备上的文本预测等打字助手。

  • latin-name:此值指定与latin相同的规则,但用于名称。

  • latin-prose:此值指定与latin相同的规则,但具有完整的打字助手,用于在电子邮件、聊天或评论等实现中使用。

  • full-width-latin:此值指定与latin-prose相同,但用于用户的辅助语言。

  • 假名片假名:此值指定假名罗马字输入,通常使用全宽字符的平假名输入,并支持转换为汉字。至于片假名,它是与此相关的另一种形式。所有这些都是用于日语输入文本的。有关日语书写系统的更多信息,请访问:en.wikipedia.org/wiki/Japanese_writing_system

  • numeric:此值指定数字字符输入,包括用户选择的千位分隔符和表示负数的字符。其目的是输入数字代码,例如一些街道号码或信用卡。如果我们确定使用数字,应优先使用type="number"输入,因为这更语义正确。

  • telemailurl:此值可用于提供与我们为相应输入类型使用的相同提示。我们应该优先使用这些值中的输入类型。

浏览器不支持所有状态并具有回退机制。同样,在这里,状态大多对移动设备或特殊用途设备有意义。

范围输入字段

有时我们希望输入一个用户从给定值范围中选择的值,使用“滑块”。为了在 HTML5 中实现这一点,添加了<input type="range" >,允许对元素的值进行不精确的控制。

如何做...

通过简单的步骤,我们将创建几个使用 HTML5 不同功能的范围控件:

  1. 我们首先通过添加以下部分的body文本来添加一个 HTML 页面:
<form>
  <label>
    Select the range <input name="simpleRange" type="range" />
  </label>
  <br/>
  <label>
    Select within range <input id="vertical" name="simpleRangeLimited" min="20" max="100" type="range" />
  </label>
  <br/>
  <label>
    Custom step <input id="full" name="simpleRangeSteped"       type="range" value="35" min="0" max="220" step="5" />
  </label>
  <span id="out"> </span>
  <br/>
  <label>
    Temperature scale
    <input min="0" max="70" type="range" name="themp"       list="theList">
  </label>
    <datalist id="theList">
      <option value="-30" />
      <option value="10" label="Low" />
      <option value="20" label="Normal" />
      <option value="45" label="High" />
      <option value="some invalid Value" />
    </datalist>
  <br/>
  <input type="submit" />
</form>
  1. 为了垂直显示其中一个滑块,我们可以在 HTML 的head标签中添加 CSS:
  #vertical {
     height: 80px;
     width: 30px;
  };
  1. 我们还可以使用 JavaScript 显示所选的值:
<script src="img/jquery.min.js">
</script>
<script type="text/javascript">
  (function($){
    var val = $('#full').val();
    var out  = $('#out');
    out.html(val);
    $('#full').change(function(){
      out.html(this.value);
    });
  }($));
</script>

它是如何工作的...

浏览器会捕获type="range",并创建一个值为0min和值为100max的滑块,步长为1

它是如何工作的...

为了垂直显示它,使用 CSS 设置宽度和高度。为了使其在 Chrome 上工作,由于尚未实现通过大小更改渲染,您可以在 CSS 中添加以下代码,强制其垂直显示:

-webkit-appearance: slider-vertical;

如果我们希望通过更改滑块直接更新小型显示,我们可以通过 JavaScript 实现这一点,通过向输入范围元素添加事件侦听器:

    $('#full').change(function(){
  out.html(this.value);
});

还有一个选项,可以将input type="range"元素与datalist连接起来,从而创建具有预定义选项的刻度:

  <datalist id="theList">
  <option value="-30" />
  <option value="10" label="Low" />
  <option value="20" label="Normal" />
  <option value="45" label="High" />
  <option value="some invalid Value" />
</datalist>

它是如何工作的...

datalist元素中的选项可以具有无效值或超出使用属性 min 和 max 指定的范围的值,因此将被忽略。另一方面,有效的值将在可选择的滑块上添加一个标记。

它是如何工作的...

datalist还有一个可选的label属性,可以添加,并应该在显示的标记旁边呈现文本。浏览器不支持此功能与标签的显示,但它是规范的一部分。

还有更多...

在撰写本文时,Firefox 和 IE 都没有完全支持type="range"元素,因此我们可以使用 JavaScript 添加基本支持。已经有一个实用程序脚本可用于解决此问题,可在frankyan.com/labs/html5slider/上找到,并且源代码也可在[github.com/html5-ds-book/html5slider](http:// https://github.com/html5-ds-book/html5slider)上找到。为了启用它,只需包含html5slider.js,然后魔术就会发生。

颜色选择器输入

作为新的输入类型之一,我们有input type="color"元素,它允许您选择颜色,并且所选颜色将具有我们习惯的简单颜色表示。颜色表示具有更常用的十六进制颜色表示名称,在本教程中,我们将通过创建一个带有颜色选择器的表单来看一个简单的示例:

颜色选择器输入

如何做...

我们将创建一个简单的表单,其中将添加一个颜色选择器,该表单是 HTML 主体的一部分:

<form>
  <label>
    Select your favorite color <input type="color" value="#0000ff">
  </label>
  <input type="submit" >
</form>

它是如何工作的...

颜色输入类型被选中,并显示当前选定的颜色。单击颜色后,我们可以直接从系统颜色选择控件中选择菜单。

所选值表示为一个简单的颜色字符串,其中包含一个#字符和一个不区分大小写的十六进制字符串。

如果浏览器不支持此功能,我们可以有一种自定义的处理方式。检查支持的一种方法是使用modenrizer.js方法:

  <script src="img/modernizr.min.js"></script>
  <script type="text/javascript">
    if(!Modernizr.inputtypes.color){
      //do a different method of color picking
      console.log("Browsers has no support for color going with fallback")
    }
  </script>

它允许我们在其他浏览器跟上实现时实施一个回退。

它是如何工作的...

使用单选下拉菜单

单选下拉菜单是标准的 HTML 组件。它们的使用虽然简单,但有时可能令开发人员和用户感到沮丧。浏览器要求向所选项目添加一个“selected”属性。要以编程方式设置select元素的值,代码必须首先找到当前选定的项目并删除其“selected”属性,然后找到具有指定值的项目并向其添加“selected”属性。

然而,开发人员可能希望以更简单的方式指定下拉字段的值。只需添加一个包含值的属性即可。在本示例中,我们将通过向下拉菜单添加一个新属性来解决这个问题。

如何做...

让我们开始吧。

  1. 我们将创建一个带有下拉菜单的 HTML 页面。在 HTML 中,下拉菜单是使用select元素制作的。要添加选择选项,我们在select元素内添加一个或多个 option 元素。通常,我们会通过为其添加 selected 属性来指定预选选项:
<select name="dropdown">
  <option value="1">First</option>
  <option value="2" selected="selected">Second</option>
  <option value="3">Third</option>
</select>
  1. 然而,这可能不方便在服务器端生成或在客户端使用模板生成。往往情况下,我们的列表元素是静态的,只是值会改变。为了简化模板,我们可以在我们的index.html中以不同的方式进行:
<!DOCTYPE HTML>
<html>
  <head>
  <title>Dropdown</title>
  </head>
  <body>
    <select name="dropdown" data-value="2">
      <option value="1">First</option>
      <option value="2">Second</option>
      <option value="3">Third</option>
    </select>
    <script src="img/jquery.min.js">
    </script>
    <script type="text/javascript" src="img/example.js">
    </script>
  </body>
</html>
  1. 然后我们可以在example.js中设置值:
$(function() {
  $('body').on('change', 'select[data-value]', function() { $(this).attr('data-value', $(this).val()); });
  window.updateDropdowns = function() {
    $('select[data-value]').each(function() {
      $(this).val($(this).attr('data-value'));
    });
  }
  updateDropdowns();
});

它是如何工作的...

example.js中的代码在页面加载时运行。此时,它找到所有具有 data-value 属性的 select 元素,并使用 jQuery 的多功能函数$.fn.val设置所选选项。此外,它为所有当前和未来具有 data-value 属性的 select 项目绑定了一个全局事件,该事件将该值同步到实际值。

这是单选下拉菜单的更自然模型。

还有更多...

重要的是要注意,此代码将无法正确处理在页面加载后生成的客户端生成的 HTML。要处理这种情况,必须在向页面添加新的select元素后调用updateDropdowns方法。

使用多选选择列表

选择列表可以被制作成允许用户选择多个元素。

多选选择列表具有特殊的序列化模型。在本示例中,我们将看看该模型是如何工作的以及如何使用它。

我们将创建一个包含多选选择列表的表单页面。该表单将向另一个页面发送GET请求,我们将在那里通过 JavaScript 提取所选项目。

使用多选选择列表

如何做...

按照以下步骤:

  1. 创建一个基本页面,其中包含一个多选选择列表的表单,如下所示的代码片段:
<!DOCTYPE HTML>
<html>
  <head>
    <title>Dropdown</title>
  </head>
  <body>
    <form method="get" action="result.html">
      <select name="multi" multiple>
        <option value="1">First</option>
        <option value="2">Second</option>
        <option value="3">Third</option>
        <option value="4">Fourth</option>
        <option value="5">Fifth</option>
      </select>
      <input type="submit" value="Submit">
  </form>
  </body>
</html>
  1. 然后我们将创建接收列表并显示所选值的页面如下:
<!DOCTYPE HTML>
<html>
  <head>
    <title>Dropdown</title>
  </head>
  <body>
    <div id="result">
    </div>
    <script src="img/jquery.min.js"></script>
    <script type="text/javascript" src="img/example.js">
    </script>
  </body>
</html>
  1. 以下是显示结果的example.js代码片段:
$(function() {
  var params = window.location.search.substring(1).split('&').
    map(function(param) {
      var nameval = param.split('=');
      return { name: nameval[0], value: nameval[1] };
    });
    console.log(params);
    var selectValues = params.
    filter(function(p) { return p.name == 'multi'; }).
      map(function(p) { return p.value; })
    $("#result").text("Selected: " + selectValues.join(','));
});

它是如何工作的...

表单提交生成的地址如下所示:

result.html?multi=2&multi=3

这种格式打破了许多流行框架对表单数据性质的假设。通常,它们将表单数据视为字典,其中一个名称对应一个值。然而,在这种情况下,数据无法放入这样的字典中,因为多选列表会生成具有相同名称和不同值的多个参数。

相反,我们将参数视为列表,这使我们能够提取和过滤两个值。

获取地理位置输入

HTML5 中一个令人兴奋的新功能是地理位置 API(www.w3.org/TR/geolocation-API/)。它允许开发人员请求用户的位置。此 API 允许开发人员获取地理坐标,如纬度和经度。

在使用此 API 之前,开发人员必须依赖更原始的方法,例如 GeoIP 数据库。这些方法产生的结果精度较低。根据用户的浏览器、设备以及其上的 GPS 的可用性,地理位置 API 可以提供几米的精度。

在这个示例中,我们将在地图上显示用户的位置。为此,我们将使用Leaflet库。使用此库显示地图的方法在显示地图一章中有介绍,第二章,图形数据的显示

如何做到这一点...

让我们开始吧。

  1. 我们将创建一个包含地图占位符的 HTML 页面,其中将包括 leaflet 库(CSS 和 JS 文件)以及我们用于获取和显示用户位置的代码,位于example.js中,如下面的代码片段所示:
<!DOCTYPE HTML>
<html>
  <head>
    <title>Geolocation example</title>
     <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.css" />
     <!--[if lte IE 8]>
       <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.ie.css" />
     <![endif]-->
  </head>
  <body>
    <div id="map" style="height:480px; width:640px;"></div>
      <script src="img/jquery.min.js"></script>
      <script src="img/leaflet.js"></script>
      <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>
  1. 然后我们将在example.js中添加以下代码:
$(function() {
  var map = L.map('map').setView([51.505, -0.09], 13)

  L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
    attribution:'Copyright (C) OpenStreetMap.org',
    maxZoom:18
    }).addTo(map);

  if ("geolocation" in navigator) {
    var marker = L.marker([51.5, -0.09]).addTo(map);
    var watchId = navigator.geolocation.watchPosition(function(position) {
    var userLatLng = new L.LatLng(position.coords.latitude, position.coords.longitude);
    marker.setLatLng(userLatLng);
    map.panTo(userLatLng);
    });
  }
  else alert("Sorry, geolocation is not supported in your browser");
});

它是如何工作的...

地理位置 API 可通过navigator对象中的geolocation对象找到。有多种可用方法,如下所示:

  • getCurrentPosition:此方法在获得位置后调用其回调函数参数一次

  • watchCurrentPosition:此方法在每次更新位置信息时调用其第一个回调函数参数,并返回一个观察者 ID

  • clearWatch:此方法通过使用我们返回的观察者 ID 清除观察回调

在我们的示例中,我们使用watchCurrentPosition,并为其提供一个回调,该回调设置标记的位置。用户将首先被要求允许网站访问他或她的位置。在获得许可并找到位置后,我们的回调将使用位置对象调用。

位置对象包含timestampcoords属性。coords属性是一个包含纬度经度信息的对象。timestamp属性是一个 UNIX UTC 时间戳,表示位置信息更新的时间。

还有更多...

此示例在直接打开文件时将无法工作。要查看示例,必须在相同目录中启动本地服务器。有关如何启动本地服务器的更多信息,请参见附录,安装和使用 http-server

在客户端使用文件输入

HTML 一直缺乏一种方便的方法来读取用户的文件。在 HTML5 之前,客户端访问用户文件的唯一方法是使用文件类型的输入元素,将该文件上传到服务器,然后将其发送回浏览器。

HTML5 通过 JavaScript 代码在用户的浏览器内本地读取用户文件的能力。该实现是文件输入元素功能的扩展,具有额外的 API。

在这个示例中,我们将使用用户选择的新 HTML5 文件 API(www.w3.org/TR/FileAPI/)来显示文本文件。

如何做到这一点...

让我们编写代码。

  1. 创建一个包含文件input字段和内容div的 HTML 页面,以显示所选文件的内容:
<!DOCTYPE HTML>
<html>
  <head>
    <title>File API example</title>
  </head>
  <body>
    <input type="file" id="file" value="Choose text file">
      <div id="content"></div>
        <script src="img/jquery.min.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>
  1. 然后我们将添加代码来读取example.js中选择的文件:
$(function() {
  $("#file").on('change', function(e) {

我们可以从输入元素的文件属性中读取所选文件。

  for (var k = 0; k < this.files.length; ++k) {
    var f = this.files[k];
  1. 要读取内容,我们使用FileReader对象。我们需要实例化它,告诉它要读取哪个文件(以及根据其类型应该如何读取它),然后在读取完成时附加一个事件侦听器,该事件侦听器将访问文件内容。操作如下:
  var fr = new FileReader();
    if (f.type && f.type.match('image/.+'))
      fr.readAsDataURL(f);
    else
      fr.readAsText(f);
  1. 在调用onload函数时,变量f将在每个onload调用的最后一个文件的值上发生变化。为了避免这种情况,我们使用匿名函数模式捕获变量。
  (function(f) {
    fr.onload = function(e) {
  1. 监听器被调用时会传入一个事件,其中目标属性包含我们的结果或整个文件的文本。
  if (f.type && f.type.match('image/.+'))
    $("<img />").attr('src', e.target.result)
    .appendTo("#content");
  else
    $("<pre />").text(e.target.result)
    .appendTo("#content");
  }
  }(f));
  }
  });
});

它是如何工作的...

HTML5 文件 API 包括两个新添加:

  • 文件输入元素具有一个包含所选文件列表的文件属性。

  • 存在一种称为FileReader的新类型对象,它允许我们使用其方法以不同的方式读取所选文件。其中包括readAsBinaryStringreadAsTextreadAsDataURLreadAsArrayBuffer等。它还为我们提供了事件监听器,我们可以设置以在加载文件内容或发生错误时获取文件内容。

要显示文本文件,我们使用读取器的readAsText属性。结果,文件数据将提供给读取器的onload监听器。文件的内容是一个简单的字符串,我们将其附加到显示预格式文本的元素内的div内容中。

为了检索图像,我们调用readAsDataURL,然后轻松地创建一个新的图像元素,其src属性设置为该数据 URL。然后我们将此元素添加到内容div中。

如果选择了一个文件夹,我们的示例将显示文件夹的全部内容,包括文本和图像。

还有更多...

可以为文件选择对话框指定过滤器,限制文件的类别。例如,添加accept="image/*"将告诉浏览器输入期望任何类型的图像,而添加accept="image/jpeg"将告诉浏览器输入期望仅 JPEG 图像。此过滤器基于媒体类型。有关可用媒体类型的更多信息,请访问www.iana.org/assignments/media-types

注意

尽管 IE9 支持许多 HTLM5 功能,但不支持 HTML5 文件 API。支持是在 IE 版本 10 中添加的。

使用拖放文件区域

使用 HTML5,我们有另一种读取用户文件的选择:我们可以使用拖放区域。通常用户发现拖放直观,并且更喜欢它而不是其他编辑和操作方法。

拖放还使用户能够将元素从不同的窗口或选项卡拖放到我们的窗口中,这意味着它们比常规文件上传按钮具有更多用途。

在这个示例中,我们将为图像创建一个拖放区域。它将同时适用于拖动的文件和从不同窗口或选项卡中拖动的图像。

注意

有关 HTML5 拖放规范的更多信息,请访问www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html

如何做...

让我们写代码。

  1. 我们将创建一个带有拖放区域的 HTML 页面。为了使区域更容易放置,我们将为其添加一些填充、边距和边框。
<!DOCTYPE HTML>
<html>
  <head>
    <title>File API example</title>
      <style type="text/css">
        #content {
          padding:0.5em;
          margin:0.5em;
          border: solid 1px; #aaa;
        }
      </style>
  </head>
  <body>
    <div id="content"><p>Drop images here</p></div>
    <script src="img/jquery.min.js"></script>
    <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>
  1. 然后我们将在example.js中添加代码来读取放置的文件或从另一个网站中拖放的图像。
$(function() {
  $("#content").on('drop', function(e) {

拖放的默认浏览器操作是导航到被放置的项目。我们希望阻止这种情况发生。

  e.preventDefault();
  e.stopPropagation();
  var files = e.originalEvent.dataTransfer.files;
  1. 我们将使用文件读取器将图像读取为DataURL,将文本文件读取为文本。
for (var k = 0; k < files.length; ++k) {
  var f = files[k];
  var fr = new FileReader();
    if (f.type && f.type.match('image/.+'))
      fr.readAsDataURL(f);
    else
      fr.readAsText(f);
  1. 在闭包中捕获每个文件使我们能够从异步onload回调中引用它。在那里,我们将其附加到内容元素中,如下面的代码片段所示:
(function(f) {
  fr.onload = function(e) {
    if (f.type && f.type.match('image/.+'))
      $("<img />").attr('src', e.target.result)
      .appendTo("#content");
    else
      $("<pre />").text(e.target.result)
     .appendTo("#content");
    }
  }(f));
}
  1. 或者,如果项目是从不同的窗口或选项卡拖动的,我们需要从 items 属性中读取它。我们正在寻找类型为text/html的项目,如下所示:
var items = e.originalEvent.dataTransfer.items;
for (var k = 0; k < items.length; ++k) {
  if (items[k].type == 'text/html') {
    items[k].getAsString(function (html) {
      $(html).appendTo("#content");
     });
    }
  }
  });
});

它是如何工作的...

example.js的第一部分中,我们使用了标准的 HTML5 API。您可以在前一个示例在客户端使用文件输入中了解更多信息——简而言之,它允许我们将文件读取为文本或DataURL并将其放入文档中。

代码的这一部分支持图像文件和文本文件。

第二部分略有不同,仅在从不同网站拖动元素或图像时才会调用。它适用于任何draggable HTML 元素 - 此元素也将作为 HTML 添加到我们的内容页面。图像数据将无法访问。

结合起来,这里描述的 API 在在线富文本、UI 或图形编辑器中非常强大。我们可以将其与图像上传服务或包含各种预制元素的面板结合使用,然后将其拖放到拖放区域内。

还有更多...

正如在本教程中所看到的,HTML5 拖放 API 不仅限于文件。通过设置draggable="true"属性,任何页面上的任何元素都可以被设置为可拖动。

dragstart事件将在拖动开始时立即在draggable元素上触发。当我们将元素移动到潜在的放置目标上时,将触发dragenterdragoverdragleave事件。最后,当元素被放置时,我们在本教程中使用的drop事件将被触发,以及dragend事件。

最后,为了对拖动对象的内容进行精细的编程控制,可以使用DataTransfer对象。例如,可以在draggable元素上放置以下dragStart处理程序:

function onDragStart(e) {
  e.dataTransfer.setData('text/html', '<p>Hello world</p>');
}

将导致浏览器将指定的 HTML 内容放置在拖动对象内部。

自定义 HTML5 draggable元素的最佳功能是与系统中其他应用程序的兼容性。可拖动的对象可以“移动”到浏览器之外的其他应用程序中,例如邮件客户端、图像编辑器等。因此,HTML5 应用程序距离成为操作系统内的一流公民又近了一步。

第五章:自定义输入组件

在本章中,我们将涵盖以下内容:

  • 使用 contentEditable 进行基本的富文本输入

  • 高级富文本输入

  • 创建下拉菜单

  • 创建自定义对话框

  • 为输入创建自动完成

  • 创建自定义单选列表

  • 创建多选列表

  • 使用地图进行地理位置输入

介绍

到目前为止,我们介绍了几种不同的从用户那里获取输入的方法。HTML5 提供了许多新功能,使得以前用 JavaScript 完成的输入组件的功能成为可能。

通常,我们需要扩展这个标准功能。在本章中,我们将看到创建一些自定义输入组件的方法,并使用已有的输入组件,其中我们添加额外的功能以扩展或简化最终用户体验。

使用 contentEditable 进行基本的富文本输入

在 HTML5 中,新的 contentEditable 属性使我们可以将每个元素转换为可编辑的富文本字段。理论上,这可以使我们编写在浏览器内部直接工作的复杂富文本编辑器。

除了其他功能,新的 API 还可以用于发送编辑命令。这是使用 document.execCommand 函数完成的,该函数将命令字符串作为第一个参数,选项作为第三个参数。

实际上,每个浏览器供应商都以稍微不同的方式实现接口。然而,大多数现代浏览器都是完全兼容的。

您可以在 tifftiff.de/contenteditable/compliance_test.html 上测试您的浏览器的兼容性;然而,该测试并未考虑到一些浏览器可能在相同的命令上有不同的行为。

在这个示例中,我们将创建一个非常简单的 contentEditable 字段,支持一些命令(段落样式、撤销/重做、粗体/斜体/下划线、项目符号和编号列表)。

如何做到...

我们将创建一个包含 contentEditable div 的 HTML 页面。

  1. 我们将在 div 中添加一些填充,以便更容易点击。在 div 上方,我们将放置我们的格式化按钮和一个下拉菜单:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Simple rich text editor</title>
        <style type="text/css">
            #edit { margin: 0.5em 0.1em; padding:0.5em;
           border: solid 1px #bbb; }
        </style>
   </head>
   <body>
   <div>
       <select class="btn style">
           <option value="P">Normal</option>
       </select>
       <button class="btn undo">Undo</button>
       <button class="btn redo">Redo</button>
       <button class="btn bold">B</button>
       <button class="btn italic">I</button>
       <button class="btn under">U</button>
       <button class="btn bullet">Bullet</button>
       <button class="btn number">Number</button>
   </div>
   <div id="edit" contentEditable="true">
   </div>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/example.js"></script>
   </body>
</html>
  1. 然后我们可以让编辑控件在我们的 example.js 文件中工作:
$(function() {
    var editCommand = function(cmd, arg) { return document.execCommand(cmd, true, arg); };
  1. 我们将把所有的 editCommand 绑定放在一个对象中:
    var bindings = {
        '.undo': editCommand.bind(this, 'undo'),
        '.redo': editCommand.bind(this, 'redo'),
        '.bold': editCommand.bind(this, 'bold'),
        '.italic': editCommand.bind(this, 'italic'),
        '.under': editCommand.bind(this, 'underline'),
        '.bullet': editCommand.bind(this, 'insertUnorderedList'),
        '.number': editCommand.bind(this, 'insertOrderedList')
    };
  1. 然后我们将它们应用到适当的编辑控件上:
    for (var key in bindings) $(key).on('click', bindings[key]);
  1. 最后,我们将定义并添加额外的段落样式:
    var styles = {
        'Heading 1': 'H1',
        'Heading 2': 'H2',
        'Heading 3': 'H3',
    };
    for (var key in styles)
        $('<option>').html(key).attr('value', styles[key]).appendTo('.style');

    $('.style').on('change', function() {
        editCommand('formatBlock', $(this).val());
    });
});

它是如何工作的...

document.execCommand 函数允许我们向当前活动的 contentEditable 字段发送命令。这些命令的工作方式就像工具栏按钮在常规富文本编辑器中的工作方式一样。例如,命令 "bold" 切换文本的粗体; 第二次应用时,它将文本恢复到其原始状态。该函数接受以下三个参数:

  • commandName:这是要执行的命令的名称。

  • showDefaultUIboolean):这告诉浏览器是否应该向用户显示与命令相关的默认用户界面,如果需要的话。

  • Value:这为取决于命令类型的命令提供参数。例如,粗体、斜体和下划线需要一个 boolean 值。

在这个示例中,我们不会跟踪光标当前选择的状态。我们将把这种跟踪留给一个更高级版本的编辑器的示例。然而,值得在这里提到的是,我们可以使用 document.queryCommandState 函数来检索与当前光标位置的命令相关的状态(或者如果当前选择有活动选择)。

高级富文本输入

虽然基本的基于 contentEditable 的富文本输入字段在大多数情况下足够了,但有时还不够。我们可能希望允许用户插入更复杂的对象,如图片和表格。

在这个示例中,我们将创建一个支持插入图片和基本表格的高级富文本编辑器。

我们将基于使用 contentEditable 进行基本富文本输入配方中演示的简单富文本编辑器构建此编辑器。

准备就绪

我们将从使用 contentEditable 进行基本富文本输入配方中的代码开始,并对其进行改进。

如何做…

让我们编写代码。

  1. 我们将使用原始的index.htmlexample.js文件,但我们将修改 HTML 文件。我们将添加两个控件:一个表按钮和一个文件选择器来插入图像:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Simple rich text editor</title>
        <style type="text/css">
            #edit {margin: 0.5em 0.1em;padding:0.5em;border:solid 1px #bbb;}
            #edit table td { border: solid 1px #ccc; }
        </style>
   </head>
   <body>
   <div>
       <select class="btn style">
              <option value="P">Normal</option>
       </select>
       <button class="btn undo">Undo</button>
       <button class="btn redo">Redo</button>
       <button class="btn bold">B</button>
       <button class="btn italic">I</button>
       <button class="btn under">U</button>
       <button class="btn bullet">Bullet</button>
       <button class="btn number">Number</button>
       <button class="btn table">Table</button>
       <input type="file" class="btn image">Image</input>
   </div>
   <div id="edit" contentEditable="true">
   </div>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/example.js"></script>
   <script type="text/javascript" src="img/example-table.js"></script>
   <script type="text/javascript" src="img/example-image.js"></script>
   </body>
</html>
  1. 要向我们的富文本添加表格,我们将创建一个名为example-table.js的新脚本。表按钮将具有双重功能。一个功能将是更改当前活动表中的行/列数。如果没有活动表,它将插入一个具有指定行数和列数的新表。以下是example-table.js中的代码:
$(function() {
    var editCommand = function(cmd, arg) {
        return document.execCommand(cmd, true, arg);
    };
    $('.table').on('click', function() {
        var rows = prompt("How many rows?"),
            cols = prompt("How many columns?");
        var loc = document.getSelection().getRangeAt(0)
                .startContainer.parentElement;
        while (loc.id != 'edit'
            && loc.nodeName.toLowerCase() != 'table')
                loc = loc.parentElement;
        var isInTable = loc.nodeName.toLowerCase() == 'table';
        var contents;
        if (isInTable)
            contents = $(loc).find('tr').toArray().map(function(tr) {
                return $(tr).find('td').toArray().map(function(td) {
                    return td.innerHTML;
                });
            });
        var table = $('<table />');
        for (var k = 0; k < rows; ++k) {
            var row = $('<tr />').appendTo(table);
            for (var i = 0; i < cols; ++i) {
                var cell = $('<td />').appendTo(row);
                if (contents && contents[k] && contents[k][i])
                    cell.html(contents[k][i]);                
                else cell.html('&nbsp;');
            }            
        }
        if (isInTable) $(loc).remove();
        editCommand('insertHTML', table[0].outerHTML);
    });    

});
  1. 要向我们的富文本添加图像,我们将创建一个名为example-image.js的新脚本。图像选择器将在指定位置插入用户选择的图像。以下是example-image.js的内容:
$(function() {
    var editCommand = function(cmd, arg) {
        return document.execCommand(cmd, true, arg);
    };
    $(".image").on('change', function(e) {
        for (var k = 0; k < this.files.length; ++k) {
            var f = this.files[k];
            var fr = new FileReader();
            if (f.type && f.type.match('image/.+'))
                fr.readAsDataURL(f);
            else
                fr.readAsText(f);
            (function(f) {
              fr.onload = function(e) {
                if (f.type && f.type.match('image/.+'))
                    editCommand('insertHTML',
                        $("<img />").attr('src',     e.target.result)[0].outerHTML);
                }
            }(f));
        }
    });
});

它是如何工作的…

我们向编辑器添加了两个新控件:表控件和插入图像控件。

表控件要求用户首先指定行数和列数。它通过检查当前光标位置的父元素来确定用户当前是否在表内。如果找到表,则记住其内容。

随后,将根据指定的列数和行数构建新表。如果旧表在该行/列位置包含一些内容,则将该内容复制到新构建的单元格中。最后,如果存在旧表,则将删除旧表,并使用insertHTML命令添加新表。

图像插入控件使用 HTML5 文件 API 用于文件输入,以将用户选择的图像文件读取为数据 URL。读取后,它使用相同的insertHTML命令将它们添加到内容中。

还有更多…

使用这种方法,很容易构建新的控件,将任何类型的内容添加到contentEditable字段中。这使我们能够创建具有专业功能的自定义富文本或页面编辑器。

然而,如果目标是向我们的页面添加一个功能齐全的通用富文本编辑器,我们建议使用已经可用的许多优秀的编辑器组件之一,例如 TinyMCE(www.tinymce.com/)。

创建下拉菜单

下拉菜单经常用于 Web 应用程序中以显示扩展功能。不经常使用或对少数用户有用的操作可以添加到菜单中,从而使界面更清晰。

HTML5 和 CSS3 允许我们完全使用 CSS 编写下拉菜单。我们将在本节中创建这样的菜单。

准备就绪

让我们分析下拉菜单的结构。下拉菜单有一个激活按钮,显示它和一个或多个项目如下:

  • 常规(操作)项

  • 分隔符项

  • 子菜单项(激活子菜单)

我们的 HTML 元素结构应反映下拉菜单的结构。我们的 CSS 代码将控制菜单的定位和显示。

我们将有三个按钮显示略有不同但结构相同的菜单。

第一个将具有默认行为-下拉,左对齐,并在右侧显示子菜单。

第二个将具有修改后的行为-右对齐,并在左侧显示子菜单。

最后,第三个将具有非常不同的行为;它将出现在按钮上方,子菜单出现在右侧,但向上移动。

准备就绪

如何做…

要创建菜单,我们将使用 HTML 和 CSS。

  1. 让我们首先在 HTML 文件中创建菜单结构。基本上,它是之前讨论过的相同结构,复制了三次,但在样式上略有变化,特别是包含菜单和子菜单的无序列表元素:
<!DOCTYPE HTML>
<html>
<head>
<title>Dropdown menu</title>
<link rel="stylesheet" type="text/css" href="example.css">
<style type="text/css">
.screen-bottom {
    position:fixed;
    bottom:3em;
}
</style>
</head>
<body>

<div class="dropdown-menu">
    <a class="btn">Normal</a>
    <ul class="menu">
        <li><a href="item1">Item 1</a>
        <li><a href="item2">Item 2</a>
        <li class="separator"></li>
        <li class="dropdown-menu">
        <a href="#" class="submenu">More...</a>
        <ul class="menu">
            <li><a href="item3">Item 3</a>
            <li><a href="item4">Item 4</a>
        </ul>
        </li>
    </ul>
</div>

<div class="dropdown-menu">
    <a class="btn">Right aligned</a>
    <ul class="menu right-align">
        <li><a href="item1">Item 1</a>
        <li><a href="item2">Item 2</a>
        <li class="separator"></li>
        <li class="dropdown-menu">
        <a href="#" class="submenu">More to the left...</a>
        <ul class="menu left-side">
            <li><a href="item3">Item 3</a>
            <li><a href="item4">Item 4</a>
        </ul>
        </li>
    </ul>
</div>

<div class="screen-bottom">
    <div class="dropdown-menu">
        <a class="btn">Up above</a>
        <ul class="menu up">
            <li><a href="item1">Item 1</a>
            <li><a href="item2">Item 2</a>
            <li class="separator"></li>
            <li class="dropdown-menu">
            <a href="#" class="submenu">More bottom-align</a>
            <ul class="menu bottom-align">
                <li><a href="item3">Item 3</a>
                <li><a href="item4">Item 4</a>
            </ul>
            </li>
        </ul>
    </div>
</div>
</body>
</html>
  1. 然后让我们在example.css中为这个菜单添加适当的 CSS。我们将使用border-box的尺寸模型。与常规模型不同,在常规模型中,边框和填充在元素的指定尺寸(宽度或高度)之外,而在border-box模型中,填充和边框包括在指定的尺寸中:
.dropdown-menu * {
    -webkit-box-sizing: border-box; /* Safari/Chrome, WebKit */
        -moz-box-sizing: border-box; /* Firefox, other Gecko */
            box-sizing: border-box;
}
div.dropdown-menu {
    display:inline-block;
    position:relative;
    margin:0 1em;
}
  1. 我们将为显示下拉菜单的菜单项目以及菜单本身添加样式。默认情况下,内部菜单绝对定位在内容的下方:
a.btn {
    padding: 0.5em 2em;
    background-color:#f1f1f1;
}
.dropdown-menu ul.menu {
    width:auto;
    background-color:#f9f9f9;
    border: solid 1px #ddd;
    display:none;
    position:absolute;
    top:50%;
    left:0;
    list-style:none;
    padding:0;
    min-width:170px;
}
  1. 当按钮处于活动状态时,我们需要让菜单在悬停时显示:
.dropdown-menu:hover > ul.menu,
.dropdown-menu:active > ul.menu {
    display:block;
}
  1. 我们需要子菜单相对于其父项目定位:
.dropdown-menu > ul.menu > li {
    position:relative;
}
  1. 我们将设置常规项目和分隔符项目的样式:
.dropdown-menu > ul.menu > li:hover {
    background-color:#eee;
}
.dropdown-menu > ul.menu > li > a {
    padding:0.3em 1.5em;
    display:block;
}
.dropdown-menu > ul.menu > li.separator {
    height:0.01em;
    margin:0.3em 0;
    border-bottom: solid 1px #ddd;
}
  1. 常规子菜单的定位略有不同:与父项目内联,但距左侧 90%:
li.dropdown-menu ul.menu {
    left:90%;
    right:auto;
    top:0em;
}
  1. 最后,我们应用专门的样式,用于右对齐和卷起菜单,以及与父菜单底部对齐的子菜单:
.dropdown-menu ul.menu.right-align {
    left:auto;
    right:0;
}
.dropdown-menu ul.menu.up {
    top: auto;
    bottom:50%;
}
li.dropdown-menu ul.menu.left-side {
    right: 90%;
    left: auto;
}
li.dropdown-menu ul.menu.bottom-align {
    top:auto;
    bottom:0;
}

工作原理

为了动态显示菜单,我们使用hoveractive CSS 伪选择器。它们使我们能够在光标悬停在元素上或元素被标记为active时以不同的样式显示元素。将整个菜单放在菜单项内允许我们通过在菜单项上使用这些选择器来显示它。

为了定位菜单和子菜单,我们使用position:relative的父菜单项目和position:absolute的子菜单项目的组合。当我们使用这种组合时,我们的子定位属性是相对于第一个相对父级,即项目。

这使我们能够将菜单放置在任何位置:默认情况下在父项目下方,或作为选项在上方(对于子菜单,默认情况下在右侧,作为选项在左侧)。它还允许我们以任何我们喜欢的方式对齐子菜单:默认情况下左对齐,作为选项右对齐(对于子菜单,默认情况下顶部对齐,作为选项底部对齐)。

这些组合应该使我们能够在任意位置构建菜单,而不必担心菜单可能超出屏幕。

创建自定义对话框

自定义对话框可用于各种用户输入。我们可以要求用户填写表单(例如,登录表单可以显示为对话框)。我们还可以用它们来要求用户接受或拒绝需要立即注意的某些操作(例如,对话框询问用户“您确定要删除所选项目吗?”)。

由于理论上我们可以在对话框中显示任何其他页面部分,如果我们有一个灵活的方法来做到这一点,那将是很好的。实现灵活性的最简单方法是将对话框分为三个部分:视图、模型和控制器。

在这个教程中,我们将创建一个通用对话框。它将包括一个视图(支持 JavaScript 模板的 HTML)、一个模型(从模板中获取)、以及一个使控制器的事件绑定列表。

这是一个高级教程。如果您不熟悉嵌入式 JavaScript 模板EJS),我们建议在阅读本教程之前先阅读第九章中的 EJS 教程,客户端模板

创建自定义对话框

准备工作

我们将使用 John Resig 的类似 EJS 模板的简单实现。这是一个将 EJS 模板转换为 JavaScript 函数的编译器。我们不打算解释编译器——知道它接受包含模板内容的元素的 ID 并返回模板函数就足够了。应用于对象时,此函数会产生 HTML 输出。

以下是模板编译器函数:

// Simple JavaScript Templating
// John Resig - http://ejohn.org/ - MIT Licensed
(function(){
  var cache = {};

  this.tmpl = function tmpl(str, data){
    // Figure out if we're getting a template, or if we need to
    // load the template - and be sure to cache the result.
    var fn = !/\W/.test(str) ?
      cache[str] = cache[str] ||
        tmpl(document.getElementById(str).innerHTML) :

      // Generate a reusable function that will serve as a template
      // generator (and which will be cached).
      new Function("obj",
        "var p=[],print=function(){p.push.apply(p,arguments);};" +

        // Introduce the data as local variables using with(){}
        "with(obj){p.push('" +

        // Convert the template into pure JavaScript
        str
          .replace(/[\r\t\n]/g, " ")
          .split("<%").join("\t")
          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
          .replace(/\t=(.*?)%>/g, "',$1,'")
          .split("\t").join("');")
          .split("%>").join("p.push('")
          .split("\r").join("\\'")
      + "');}return p.join('');");

    // Provide some basic currying to the user
    return data ? fn( data ) : fn;
  };
})();

注意

解释 JavaScript 微模板的原始文章可以在ejohn.org/blog/javascript-micro-templating/找到。

如何做...

我们将编写页面代码和对话框库。

  1. 让我们创建一个名为index.html的文件。它将包含一个秘密的div区域和一个登录对话框的模板,可以预先填充用户名并显示错误消息的能力:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Simple rich text editor</title>
        <link rel="stylesheet" type="text/css" href="dialog.css">
        <style type="text/css">
        .dialog.tmplExample .button-area { margin-top: 20px; text-align:right; }
        .dialog.tmplExample p.error.hidden { display:none; }
        .dialog.tmplExample p.error { color:#c00; }
        div.secret { display:none; }
        </style>
   </head>
   <body>
   <div>
       <div class="secret">
           Welcome to the secret place, where only authenticated users may roam.
       </div>
   </div>
   <script id="tmplExample" type="text/html">
        <p class="error hidden"></p>
        <p><label for="user">User:</label>
          <input name="user" type="text" value="<%= user %>" ></p>
        <p><label for="pass">Pass:</label>
          <input name="pass" type="password" value="<%= pass %>" ></p>
        <p class="button-area">
            <button class="login" type="button">Login</button>
        </p>
   </script>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/tmpl.js"></script>
   <!--<script type="text/javascript" src="img/dialog.js"></script>-->
   <script type="text/javascript" src="img/example.js"></script>
   </body>
</html>
  1. 为了看看我们希望对话框 API 如何工作,接下来我们将创建example.js。它将立即显示一个登录对话框,一旦输入正确的密码,对话框将关闭,然后显示秘密内容。否则,对话框内将显示错误消息:
$(function() {
    dialog("tmplExample", {title: 'Login to continue', user: 'jack.r', pass: ''}, {
        'button.login => click': function(dialog, ev) {
            var data = dialog.data();
            if (data.pass == 'secret') { dialog.close(); $('.secret').show(); }            
            else { dialog.find('p.error').text('Invalid password').show(); }
        }
    });
});
  1. 接下来让我们创建dialog.js。它应该导出一个名为dialog的函数,该函数接受三个参数:对话框模板的 ID,要填充到模板中的数据,以及包含事件绑定的对象:
(function() {
    window.dialog = function(template, data, bindings) {
  1. 首先,构建dialog chrome:
        var holder = $("<div />").addClass('dialog')
                .addClass(template);
        var titlebar = $("<div />").addClass('title')
                .appendTo(holder);
        var titletext = $("<span />").addClass('titletext')
                .appendTo(titlebar);
        var close = $("<span />").addClass('close')
                .html('x').appendTo(titlebar);
        var form = $("<form />").addClass('dialog')
                .appendTo(holder);
  1. 用模板化的 HTML 填充它,设置标题,并显示它:
        form.html(tmpl(template, data));
        $(titletext).text(data.title || "Dialog");
        holder.appendTo('body');
  1. 应用selector => event格式的绑定:
        for (var key in bindings) if (bindings.hasOwnProperty(key))
          (function(key) {
            var selectorEvent = key.split(/\s+=>\s+/);
            form.find(selectorEvent[0]).on(selectorEvent[1],
                function() {
                    var args = [].slice.call(arguments);
                    args.unshift(self);
                    bindings[key].apply(this, args);
                });
        }(key));
  1. 构建要返回的dialog对象。提供find()函数用于字段,data()函数用于提取所有表单数据为 JSON 对象,以及事件绑定和关闭函数:
        var self = {};
        self.find = form.find.bind(form);
        self.data = function() {
            var obj = {};
            form.serializeArray().forEach(function(item) {
                if (obj[item.name]) {
                    if (!(obj[item.name] instanceof 'array'))
                        obj[item.name] = [ obj[item.name] ];
                    obj[item.name].push(item.value);
                }
                else obj[item.name] = item.value;
            });
            return obj;
        }
        self.close = function() {
            holder.trigger('dialog:close');
            holder.remove();
        };
        self.on = holder.on.bind(holder);
        close.on('click', self.close);
        return self;
    };
}());
  1. 最后,我们将在dialog.css中自定义对话框的定位和样式。
div.dialog {
    position:fixed;
    top:10%;
    left: 50%;
    margin-left: -320px;
    width:640px;
    height:auto;
    border: solid 1px #ccc;
    background-color:#fff;
    box-shadow: 2px 2px 5px #ccc;
}
div.dialog div.title { border-bottom: solid 1px #eee; }
div.dialog div.title span { padding: 0.5em 1em; display:inline-block; }
div.dialog div.title span.close { float: right; cursor: pointer; }
div.dialog form.dialog { padding: 1em; }

它是如何工作的...

为了创建一个灵活的对话框库,我们将显示对话框所需的数据分为视图、模型和控制器。

tmpl.js库提供了一个使用提供的模型对象处理 EJS 模板的函数。此函数的内部超出了本示例的范围。

我们的dialog函数构建了一个包含标题栏、关闭按钮和表单的 chrome。然后,它用模板和数据填充表单。最后,它将我们的绑定应用于此表单的内容。绑定采用 jQuery 的selector => event格式,可用于响应任何类型的用户输入。

该函数返回一个dialog对象(在变量 self 中构造)。该对象提供以下便利函数:

  • find:这允许用户使用选择器在表单中查找元素

  • data:这将提取表单中的所有数据输入为易于使用的 JSON 对象

  • close:这将关闭对话框

  • on:这允许用户添加额外的绑定(例如,dialog:close事件)

该对象还提供了一个方便的事件,名为dialog:close,当对话框关闭时触发。

我们使用dialog.css样式化这个对话框。对话框的定位采用纯 CSS 定位方法:使用固定宽度和负边距,我们避免了需要读取窗口宽度的需求,因此完全避免了使用 JS。

灵活的模板语言允许我们创建任何复杂度的对话框内容,而bindings语法允许完全控制所有用户输入。我们可以通过提供模板和一些或全部绑定来简化创建一些常见对话框。

通过这种通用方法,创建简单的消息框、通用文本提示或具有可变数量字段的复杂对话框表单都同样容易。

创建输入自动完成

通常与搜索字段或输入相关的一个常见功能是,我们可以在输入一些数据后猜测文本。这可以是我们数据库中的任何字段,比如员工姓名。在这个示例中,我们将看一下创建输入自动完成的一些可能方式;你可以决定什么最适合你的用例。

创建输入自动完成

准备工作

在这个例子中,我们将使用一个样本 JSON 文件,模拟一个 REST API 返回的结果。该文件可以从示例中检索,文件名为countries.json,其中包含一个对象列表——国家与其对应语言的映射。在这个例子中,我们将同时使用jQueryUIjqueryui.com/)和一个名为Chosen的库(github.com/harvesthq/chosen)。为什么要同时使用两者?嗯,我们可以使用它们中的任何一个,或者都不用,但这里的想法是展示使用列表选择创建良好用户体验的不同方式。另外,由于我们将模拟 REST 服务,我们需要运行一个服务器;有关更多信息,请参阅附录 A,安装 Node.js 和使用 npm

如何做...

对于这个例子,我们将使用 HTML 和相应的 JavaScript 和 CSS:

  1. 我们将首先从head部分开始,添加 jQueryUI 和 Chosen 的 CSS 依赖项。此外,我们还将添加一个小的 CSS 部分,其中我们将定义 Chosen 的单个选择器的大小:
 <head>
    <meta charset="utf-8">
    <title>Autocomplete</title>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/css/lightness/jquery-ui-1.10.2.custom.css" type="text/css" media="all">
    <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/chosen/0.9.11/chosen.css">
    <style type="text/css">
      .marker{
        width:350px;
      }
    </style>
 </head>
  1. 接下来,我们可以添加 HTML 的主体部分,我们将在其中创建表单。首先,我们将创建一个块,其中将有一个输入文本字段,该字段将连接到建议水果的列表:
<div>
        <label>
          Pick your favorite fruit <input name="favFruit" type="text" list="fruit" placeholder="Example:'Apple'">
        </label>
        <datalist id="fruit">
          <option value="apple" label="Apple"></option><option value="apricot"></option><option value="banana"></option><option value="berries"></option>
        </datalist>
      </div>
  1. 下一个输入字段是用于选择国家,我们将使用 Chosen 类型的选择器,该选择器将作为控件的一部分具有自动完成功能:
     <div>
        <label for="country">Your country </label>
        <select id="country" name="country" data-placeholder="Choose a Country..." class="marker">
            <option value=""></option>
            <option value="United States">United States</option>
            <option value="United Kingdom">United Kingdom</option>
            <option value="Afghanistan">Afghanistan</option>
            <option value="Aland Islands">Aland Islands</option>
            <option value="Andorra">Andorra</option>
            <option value="Angola">Angola</option>
            <option value="Anguilla">Anguilla</option>
            <option value="Antarctica">Antarctica</option>
            <option value="Antigua and Barbuda">Antigua and Barbuda</option>
           </select>
     </div>
  1. 另一个输入元素用于语言选择。我们将使用从服务器检索的 JSON 数据,或者在我们的情况下是一个 JSON 文件。此外,我们将添加一个职业输入和一个提交按钮:
   <div>
        <label for="language">Language</label>
        <input type="text" id="language" name="language" placeholder="Example: English"/>
     </div>
     <div>
        <label for="occupation">Occupation</label>
        <input type="text" id="occupation" name="occupation" placeholder="Example: prog">
     </div>
     <div>
      <input type="submit">
     </div>
  1. 不要忘记这个块需要作为form的一部分才能提交。另一个选项是让元素指定一个form属性:
<input type="text" id="occupation" name="occupation" placeholder="Example: prog" form ="someFormId" >
  1. 这将设置给定元素的所有者,允许我们将元素放在文档的任何位置。这里的限制是对于给定元素只能有一个表单。

  2. 下一步是包含 jQuery、jQueryUI 和 Chosen 的外部供应商 JavaScript:

<script src="img/jquery.min.js"></script>
 <script src="img/chosen.jquery.min.js"></script>
 <script src="img/jquery-ui.min.js" type="text/javascript"></script>
  1. 之后,我们可以开始 jQuery 选择和逻辑:
$(function() { ...}
  1. 要为元素启用 Chosen,我们选择它们并直接在它们上调用插件;就是这么简单:
$(".marker").chosen();
  1. 另一个选择是使用 jQueryUI 的autocomplete插件。一种方法是在本地拥有数据,然后将其应用于某些选择:
 var occupation = ["programmer","manager","doctor","designer"];
      $("#occupation").autocomplete({
          source:occupation,
          minLength:2,
          delay:200
      });
  1. 在组件的配置中,source属性可以接受可能的字符串选项列表,minLength属性指定应在触发自动完成之前插入的最小字符数。delay可以设置在按键和对源数据进行搜索之间的毫秒数。

注意

请注意,将延迟设置为“低”可能会导致向数据源发送大量请求的副作用。

  1. 数据也可以位于远程服务器上,并作为插件的源进行检索,可以应用一些额外的过滤:
   $("#language").autocomplete({
     source: function (request, response) {
     //matcher for terms filtering on client side
     var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( request.term ), "i" );
              //simulate a server side JSON api
     $.getJSON("countries.json?term=" + request.term,
        function (data) {
     response($.map(data, function (value, key) {
       for(var name in value) {
         var result = {};
           if(matcher.test( value[name])){
             result.label=value[name]+" "+name;
             result.value=value[name];
             return result;
           }
        }
      })
     );
   });
  },
  minLength: 2,
  delay: 200
    });

它是如何工作的...

最简单的情况是使用标准的 HMTL5 标签来实现自动完成。代码如下:

<input name="favFruit" type="text" list="fruit" placeholder="Example:'Apple'" />

list="fruit"属性将输入字段连接到datalist。此属性用于标识将建议给用户的预定义选项列表。

另一个选择是使用 Chosen,这是一个使选择更加用户友好的 JavaScript 插件。可以通过以下简单的 HTML 实现:

        <select id="country" name="country" data-placeholder="Choose a Country..." class="marker">
            <option value=""></option>
            <option value="United States">United States</option>
            <!-- … Other options -->
        </select>

这将通过使用 jQuery 选择器来激活插件元素:

     $(".marker").chosen();

Chosen 将自动样式化选择并添加自动完成功能,如果设置了data-placeholder属性,它将模仿 HTML5 的placeholder属性的标准行为。

注意

请注意,为简单起见,其他国家已被移除,在实际使用情况下,您可以使用 ISO 3166-1(www.iso.org/iso/country_codes.htm)中定义的国家列表以及相应的维基百科文章en.wikipedia.org/wiki/ISO_3166-1

另一个选择是使用 jQueryUI 自动完成组件。这就是为什么我们将更详细地分析一个带有服务器端数据源的示例。有三种选择:

  • 客户端过滤:我们获取整个 JSON 文档,或者其他任何文档,然后在客户端对数据进行过滤。如果可能的话,这通常是一个很好的方法。但并非所有情况都适用。包含列表的数据可能非常庞大。考虑搜索引擎上的自动完成功能,可能会有大量的可能结果列表。

  • 服务器端过滤:我们只获取通过某个查询参数过滤的数据的一部分。过滤是在服务器端完成的,在大多数情况下会更慢。即使数据立即返回而不需要太多处理,额外的请求也会增加一些额外的延迟,这可能并非总是如此。

  • 服务器端和客户端过滤:在处理大型数据集时,这两种方法的结合可能是我们最好的方法。只有在满足一定的阈值时,我们才能向服务器请求更多的数据。

注意

值得一提的是,如果我们创建一个搜索输入字段,使用 HTML5 的input type="search"是语义上正确的。此控件可以启用单行输入字段,并可以添加autosave属性以启用预先搜索过的术语的下拉列表。代码如下:

<input id="mySearchField" type="search" autosave>

为了在用户输入时使用从服务器检索到的数据,我们可以在配置的source属性中使用一个函数:

source: function (request, response) {

request对象中,我们可以通过request.term属性获取插入input元素的当前数据。然后,如果我们想要在客户端过滤数据,我们可以创建一个正则表达式匹配器,就像在我们的情况下一样,因为我们总是访问相同的 JSON 文件:

  var matcher = new RegExp( "^" + $.ui.autocomplete.escapeRegex( request.term ), "i" );

之后,我们使用 Ajax 调用服务来读取数据:

   $.getJSON("countries.json?term=" + request.term, function (data) {

请注意,大多数 REST API 通常都有自己的过滤功能,通常通过request参数进行过滤,在我们的情况下,我们没有这样的功能,但是为了说明问题,我们可以很容易地做如下操作:

'countries.json?term=' + request.term

因此,回调函数接收 JSON 数据,但由于我们获取了所有未经过滤的数据,我们使用jQuery.map( arrayOrObject, callback( value, indexOrKey ) )来将原始对象数组中的所有项目转换为新数组,遵循callback函数中定义的规则。

在我们的情况下,JSON 的格式如下:

[
  {
    "Afghanistan": "Pashto"
  },
  {
    "Albania": "Albanian"
  }
…
]

为了使用language名称过滤数据,我们应该返回与我们在matcher中定义的条件匹配的对象子列表:

function (data) {
  response($.map(data, function (value, key) {
    for(var name in value) {
     var result = {};
     if(matcher.test( value[name])){
        result.label=value[name]+" "+name;
        result.value=value[name];
        return result;
     }
   }
  }));
  }

您可能会注意到返回的结果具有labelvalue属性;这是source的可能格式之一,其中我们有一个这样的对象数组。

如果我们要比较 jQueryUI 处理数据的方法与 Chosen 库,我们可以得出结论,jQueryUI 在处理不同数据源时更灵活。另一方面,Chosen 在某种程度上只是为标准的 HTML 元素添加样式,比使用巧妙的技巧更加符合规范。此外,Chosen 非常专注于做一些非常好的事情,并且在不具有 jQueryUI 这样体积庞大的库所带来的负担的情况下提供了出色的用户体验。

还有更多...

list属性可以用于除hiddencheckboxradiofilebutton类型之外的所有输入类型,这些类型会被忽略。仔细想想,不在这些类型上工作是有道理的,因为在它们上面使用自动完成并没有太多实际用途。

如果我们需要在旧版浏览器上具有相同的行为,或者需要回退模式,我们可以将该内容放在datalist元素中:

 <datalist id="fruits">
  <label>
   or select on from this list of element:
   <select name="Fallback">
    <option value="">
    <option>Apple</option>
    <option>Orange</option>
    <!-- ... -->
   </select>
  </label>
 </datalist>

如果支持datalist元素,这些数据将不会显示,从而使我们能够为旧版浏览器添加支持。

还有其他各种数据源可以与 jQueryUI 自动完成一起使用,例如 JSONP 或 XML。

创建自定义单选列表

在上一个示例中,我们使用了 Chosen。在这个示例中,我们将更深入地了解创建简单选择框的过程,使其成为最用户友好的选择器之一。

准备工作

在这个示例中,我们将使用 Chosen (github.com/harvesthq/chosen)及其依赖项 jQuery,通过从 CDN 添加它们。

如何做...

我们创建一个 HTML 文件和相应的 JavaScript 代码:

  1. 首先,我们将从 HTML 的 head 部分开始,包括 Chosen 的 CSS 样式:
 <head>
    <meta charset="utf-8">
    <title>Single select list</title>
    <link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/chosen/0.9.11/chosen.css">
    <style type="text/css">
       .drop-down{
        width: 250px;
      }
    </style>
 </head>
  1. 我们将创建一个简单的表单,用户可以在其中选择他们最喜欢的编程语言和职位。为此,我们添加了带有多个可用选项的select元素:
<form>
    <div>
      <label>
        Favorite programming language:
        <select id="programming" data-placeholder="Your favorite programming language" class="drop-down">
          <option value=""></option>
          <option>Java</option>
          <option>Python</option>
          <option>Clojure</option>
          <option>C</option>
          <option selected>Java Script </option>
          <option>Lisp</option>
          <option>Pascal</option>
          <option>VB</option>
        </select>
    </label>
    </div>
  1. 可以使用optgroup元素对可能的选项进行分组:
<div>
     <label>
       You consider your self to be a:
        <select id="occupation" data-placeholder="Occupation" class="drop-down">
          <optgroup label="Software">
              <option>Java developer</option>
              <option>Node developer</option>
              <option>Software Achitect</option>
              <option selected>Engineer</option>
              <option>Manager</option>
          <optgroup>
          <optgroup label="Hardware">
            <option>Semiconductor</option>
            <option>Manager</option>
            <option>Computer Hardware Engineer</option>
          </optgroup>
        </select>
      </label>
    </div>
  1. 最后,我们只需为表单添加一个简单的提交:
    <input type="submit" />
    </form>
  1. 为了包含 Chosen,我们从 CDN 添加它们的实现:
 <script src="img/jquery.min.js"></script>
   <script src="img/chosen.jquery.min.js"></script>
  1. 为了指定应该应用 Chosen 的元素,我们使用 jQuery 选择:
         $(function() {
          $("#programming").chosen({
             allow_single_deselect:true
          });
          $("#occupation").chosen();
         });

工作原理...

Chosen 最好的地方在于它的简单性;我们只需使用 jQuery 选择元素并应用插件。有一个允许取消选择的选项,我们可以在创建这种元素时启用它:

    $("#programming").chosen({allow_single_deselect:true});

注意

请注意,Chosen 可以与Prototype JS一起使用,而不是 jQuery;在那里,元素的选择将是new Chosen(someElement);

此外,我们可以添加一个名为data-placeholder的属性,其中包含默认文本,例如我们的示例中的Occupation。如果未指定,单选默认为Select Some Option

提示

对于select元素,如果未指定selectedIndex或没有带有selected属性的选项,浏览器会假定第一个元素被选中。为了允许未选择任何选项,我们可以将第一个选项设置为空,从而启用data-placeholder文本支持。

还有更多...

如果您需要使用在 Chosen 初始创建后会更改的选项数据,您可以动态更新组件,然后在所选字段上触发liszt:updated事件。liszt:updated事件是 Chosen 特定的内部事件。调用事件后,Chosen 将根据更新的内容重新构建列表。例如,在 ID 为countries的元素上,触发将如下所示:

   $("#form_field").trigger("liszt:updated");

创建多选列表

Chosen 可以用于创建漂亮的多选项。在这个示例中,我们将创建一个菜单订购表单,其中使用了这种类型的选择。

创建多选列表

准备工作

这个示例将包含与创建自定义单选列表相同的部分,并在此基础上构建。

如何做...

我们首先要有与创建自定义单选列表相同的基础,并添加以下部分:

  1. 首先,我们添加将具有我们在 head 部分创建的drop-downCSS 类的选择:
                <div>
      <label for="cocktails">Place the order for cocktails</label>
     <select id="cocktails" data-placeholder="Add cocktails"  multiple class="drop-down" name="cocktails">
          <option value=""></option>
          <option>Black Velvet</option>
          <option>Moonwalk</option>
          <option>Irish coffee</option>
          <option>Giant Panda</option>
          <option selected>Jungle Juice</option>
          <option selected>Mojito</option>
          <option selected disabled>Joker</option>
          <option disabled>Long Island Iced Tea</option>
          <option disabled>Kamikaze</option>
     </select>
    </div>
  1. 我们还可以对select元素的选项进行分组,如StartersPizza
<div>
      <label for="food">Select the food order</label>
     <select id="food" data-placeholder="Select some off the menu element" multiple class="drop-down" name="food">
      <optgroup label="Starters">
            <option>White Pizza</option>
            <option>Calzone</option>
        <optgroup>
       <optgroup label="Pizza">
            <option>Chees and Tomato</option>
            <option>Garden Veggie</option>
            <option>Pepperoni</option>
        <optgroup>
        <optgroup label="Salads">
          <option>House Salad</option>
          <option>Cezar Salad</option>
          <option>Sopska</option>
        </optgroup>
     </select>
    </div>
  1. 只需选择所有具有drop-downCSS 类的元素,并为它们启用 Chosen:
  <script type="text/javascript">
   $(function() {
    $('.drop-down').chosen();
    }
   </script>

工作原理...

Chosen 的一个主要特点是轻松设置,因此 JavaScript 部分非常简单,因为我们只有基本的元素选择。可以通过在选项上使用selected属性在页面呈现给用户之前选择选项,例如Mojito。它们也可以通过使用disabled属性禁用选择,因此在我们的情况下,选项Long Island Iced Tea将不会出现在选择中。

Optgroups、selected states、multiple attributes 以及其他属性都像标准的 HTML5 行为一样受到尊重。这意味着我们不需要期望特殊的东西或在服务器端处理表单时进行一些定制。

使用地图的地理位置输入

自从引入 HTML5 地理位置 API 以来,读取用户的位置变得更加简单。然而,有时我们可能希望允许用户更正或验证他们的位置,或者指定一个与他们自己不同的位置。

在本章中,我们将制作一个位置输入,允许用户通过在地图上标记来指定位置。

我们的位置选择器将表示为一个链接。单击链接后,用户将有选择使用输入字段搜索其位置的选项,然后通过单击地图选择位置。

与我们所有的地图示例一样,我们将使用流行的 Leaflet(leafletjs.com/)地图库。

准备工作

我们希望我们的地图输入行为类似于大多数输入字段。我们将使用一个下拉样式的机制,类似于大多数日期选择器组件。用户将点击一个链接来修改位置,然后地图下拉菜单将出现。用户做出选择后,下拉菜单将消失。

我们还将在地图上添加一个搜索框,以帮助用户找到所需的位置。为此,我们将使用Nominatimnominatim.openstreetmap.org/),这是 OpenStreetMap 提供的免费地理编码服务。以下是 Nominatim JSON 响应的示例:

[{
    [snip]
    "lat": "52.5487969264788",
    "lon": "-1.81642935385411",
    "display_name": "135, Pilkington Avenue, Castle Vale, Birmingham, West Midlands, England, B72 1LH, United Kingdom",
    [snip]
}]

这是一个包含各种数据的搜索结果数组,其中包括我们需要的数据,如纬度、经度和显示名称。

如何做...

让我们写代码。

  1. 与我们的 HTML 页面一样,我们始终从 HTML 页面开始。我们的输入字段由三个组件组成:要单击的链接、包含纬度和经度数据的隐藏输入字段以及基于地图的位置选择器。地图和搜索框不包括在 HTML 中,它们是按需创建的。

为了使地图下拉菜单出现在链接下方,它相对于其容器进行定位。为了使自动完成链接出现在单独的行上,它们的显示样式设置为block

<!DOCTYPE HTML>
<html>
    <head>
        <title>Location input using a map</title>
        <style type="text/css">
            div[data-input-map] {
                position:relative;
            }
            div[data-map] {
                width: 480px;
                height: 320px;
                position:absolute;
                top:100%;
                left:0;
            }
            div[data-results] a {
                display:block;
            }
        </style>
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.css" />
        <!--[if lte IE 8]>
        <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet-0.4/leaflet.ie.css" />
        <![endif]-->
    </head>
    <body>
        <div data-input-map>
            <a href="#">Set location</a>
            <input data-location type="hidden" name="location" value="51.5,-0.09" />
        </div>
        <script src="img/jquery.min.js"></script>
        <script src="img/leaflet.js"></script>
        <script type="text/javascript" src="img/example.js"></script>
    </body>
</html>
  1. 为了使此选择器工作,将以下代码添加到example.js中:
$('body').on('click', '[data-input-map] > a', function(e) {
    e.preventDefault();
    var par = $(this).parent();

    // Read the current location of the input
    var location = par.find('[data-location]');
    var latlng = location.val().split(',').map(parseFloat);

    // Create the map element and center the map at the current
    // location. Add a marker to that location.
    var mape = $('<div data-map />')
        .appendTo(par)[0];
    var map = L.map(mape).setView(latlng, 13)
    L.tileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',{
        attribution:'Copyright (C) OpenStreetMap.org',
        maxZoom:18
    }).addTo(map);
    var marker = L.marker(latlng).addTo(map);

    // Update the location when a new place is clicked.
    map.on('click', function(e) {
        marker.setLatLng(e.latlng);
        location.val([e.latlng.lat, e.latlng.lng].join(','));
        setTimeout(function() {
            $(mape).remove();
            inpe.remove();
        }, 500);
    });

    // Given a street address return a list of locations with
    // names and latlngs using the nominatim service.
    function findLocation(query, callback) {
        $.ajax('http://nominatim.openstreetmap.org/search', {
            data: { format: 'json', q: query },
            dataType: 'json'
        }).success(function(data) {
            callback(data.map(function(item) {
                return {
                    latlng: [item.lat, item.lon],
                    name: item.display_name
                };
            }));
        });
    }

    // Add a search box
    var inpe = $('<input type="text" data-search />')
        .appendTo(par);
    delaySearch = null;

    // Fire a search 1 second after the input stops changing,
    // displaying the results in a list  
    inpe.on('keydown keyup keypress', function() {
        if (delaySearch) clearTimeout(delaySearch);
        delaySearch = setTimeout(function() {
            par.find('div[data-results]').remove();
            var autocomplete = $('<div data-results />')
                .appendTo(par);
            findLocation(inpe.val(), function(results) {
                results.forEach(function(r) {
                    $('<a href="#" />')
                        .attr('data-latlng', r.latlng.join(','))
                        .text(r.name).appendTo(autocomplete);
                });
                // When a result is picked, center the map there and
                // allow the user to pick the exact spot.
                autocomplete.on('click', 'a', function(e) {
                    e.preventDefault();
                    var latlng = $(this).attr('data-latlng')
                        .split(',');
                    map.setView(latlng, 13);
                    autocomplete.remove()
                });
            });
        }, 1000);
    });

});

如何做...

它是如何工作的...

example.js中的代码使用户可以单击设置位置链接,并使用地图选择位置。click事件绑定添加到文档主体,以便更容易地向页面添加新的位置输入。

我们从隐藏输入字段中解析纬度和经度,然后创建一个以这些坐标为中心的地图,并在同一位置放置一个标记。

当用户点击地图时,位置将被更新,并且地图将在 500 毫秒后被移除。这应该足够长,让用户注意到他的更改已成功应用。

此外,我们在点击的链接旁边添加了一个搜索框。当用户在其中输入搜索查询时,将通过向 Nominatim 发出 Ajax 请求来执行搜索。为了避免过载服务,搜索会延迟 1 秒;如果用户在那一秒内输入了内容,查询将被取消,并且将安排在 1 秒后发送新的查询。

在获取结果后,代码将它们显示为链接列表。单击后,它们将重新定位地图,使其位于单击搜索结果的确切位置。搜索结果列表将被移除,并允许用户选择确切的位置。

第六章:数据验证

在本章中,我们将涵盖以下配方:

  • 通过长度验证文本

  • 通过范围验证数字

  • 使用内置模式验证

  • 内置约束和自定义验证的高级用法

  • 计算密码强度

  • 验证美国邮政编码

  • 使用异步服务器端验证

  • 结合客户端和服务器端验证

介绍

表单通常希望用户以某种方式行为,并按要求插入数据。这就是数据验证的作用。服务器端验证始终是必须要做的,应该考虑在客户端进行表单验证。

验证使应用程序用户友好,节省时间和带宽。客户端和服务器端验证相辅相成,应始终使用。在本章中,我们将介绍一些主要用于 HTML5 客户端检查的新机制,以及如何解决一些常见问题。

通过长度验证文本

在客户端进行的最基本检查之一是插入或提交表单的文本长度。这经常被忽略,但这是必须要做的检查之一,不仅仅是在客户端。想象一下,如果我们的输入没有任何限制,一些大文本可能会轻松地使服务器超载。

如何做...

让我们创建一个简单的 HTML 表单,其中包含一些我们将应用一些约束的不同输入:

  1. 页面头部是标准的,所以我们将直接进入创建表单,首先添加name输入,限制为20个字符,如下所示:
<form>
    <div>
      <label>
        Name <input id="name" type="text" name="name" maxlength="20" title="Text is limited to 20 chars"placeholder="Firstname Lastname" >
      </label>
    </div>
  1. 在那之后,我们将添加另一个input字段,最初具有无效值,长于指定的测试目的,如下所示:
    <div>
      <label>
        Initially invalid <input value="Some way to long value" maxlength="4" name="testValue" title="You should not have more than 4 characters">
      </label>
    </div>
  1. 此外,我们将添加textarea标签,其中将添加spellcheck属性,如下所示:
    <div>
      <label>
        Comment <textarea spellcheck="true" name="comment" placeholder="Your comment here"> </textarea>
      </label>
    </div>
  1. 在那之后,我们将添加两个按钮,一个用于提交表单,另一个用于启用 JavaScript 备用验证,如下所示:
    <button type="submit">Save</button>
    <button id="enable" type="button">Enable JS validation</button>
  1. 由于我们将使用 jQuery Validate 插件测试备用版本,因此我们将添加这两个库的依赖项,并包括我们自己的formValidate.js文件,稍后将对其进行定义:
    <script src="img/jquery.min.js"></script>
    <script src="img/jquery.validate.min.js"></script>
    <script src="img/formValidate.js" ></script>
  1. 我们还需要选择要提交的表单,并在单击启用备用按钮时使用插件添加基于 JavaScript 的验证:
    $("#enable").click(function(){
      $("#userForm").validate({
        rules: {
          name : "required",
          comment: {
            required: true,
            minlength: 50
          }
        },
        messages: {
          name: "Please enter your name",
          comment: {
            required: "Please enter a comment",
            minlength: "Your comment must be at least 50 chars long"
          }
        }
      });
    });

注意

请注意,我们还添加了将显示在验证错误上的消息。

启用 JavaScript 的按钮仅用于演示目的,在实际应用中,您可能会将其作为备用或作为唯一方法。由于我们只检查最大长度,除非我们先前使用不正确的值呈现了 HTML,否则验证不应该是一个问题。至于验证消息,在撰写本文时,所有现代浏览器和 IE 10 都支持,但尚未有移动浏览器添加支持。我们可以首先检查浏览器是否支持拼写检查属性,然后相应地采取行动:

if ('spellcheck' in document.createElement('textarea')){
    // spellcheck is supported
} else {
   //spellchek is not supported
}

它是如何工作的...

最初,我们将查看maxlength属性。正如您所期望的那样,浏览器不允许用户输入违反此类型的约束,它们通常在插入最大值后停止输入。

因此,问题是如何违反此约束?

嗯,如果渲染的 HTML 一开始就是无效的,或者如果数据是以编程方式更改的,那么表单将在没有验证的情况下提交。这实际上是指定的行为;有一个脏标志,指示输入是否来自用户。在我们的情况下,只要我们不在标记为最初无效的输入中更改任何内容,表单就会成功提交。另一方面,当用户更改该元素中的一些数据时,表单将因验证错误而失败,如下面的屏幕截图所示:

它是如何工作的...

在 Chrome Version 28 开发版本上显示的验证弹出窗口

在验证错误弹出窗口中显示的旁边的错误消息将具有title属性的内容,这意味着除了标准提示之外,此属性还有另一个用途。这个消息框在不同的浏览器上看起来不同。

尽管在浏览器上启用语法和拼写检查的主要控制权在用户手中,但有一个名为spellcheck的属性,可以添加以提示浏览器进行拼写检查。在我们的例子中,注释将如下屏幕截图所示:

它是如何工作的...

这个属性是可继承的,并且可以与lang属性结合使用。例如,如果我们有以下片段:

<html lang="en">
<body spellcheck="true">
  <textarea></textarea>
  <div lang="fr">
    <textarea></textarea>
     <input type="text">
  </div>
</body>
</html>

在我们的例子中使用了多种不同的语言。由于页面设置了lang="en"属性,因此嵌套在其中的所有元素都将使用英语词典。而因为div元素具有lang="fr"属性,所有嵌套的元素(textareainput type = text)将根据法语词典进行检查。

注意

有关拼写检查的更多信息可以在 WHATWG 页面上找到www.whatwg.org/specs/web-apps/current-work/multipage/editing.html#spelling-and-grammar-checking。还有一件事要注意的是,过去spellcheck属性必须设置为truefalse,但是最新更改后可以留空www.w3.org/TR/html-markup/global-attributes.html#common.attrs.spellcheck

为什么我们说用户有完全控制权呢?嗯,如果用户一直在浏览器中选择拼写检查或从未检查过,那么该选项将覆盖此标签的行为。此属性可以应用于文本输入相关元素,以及其内容已被编辑的元素。

备用或以不同的方式是使用 JavaScript 来验证文本长度。因为 HTML5 中没有minlength属性,所以没有标准的最小长度验证方式。因此,我们将使用 jQuery 验证插件。还有一种方法可以使用pattern属性和正则表达式来做到这一点,但我们将在本章的使用内置模式验证中详细讨论。

要启用验证,我们选择表单并通过指定验证规则来设置规则,其中键是表单参数名称,值是应用的验证检查:

 $("#userForm").validate({
        rules: {
          name : "required",
          comment: {
            required: true,
            minlength: 50
          }
        },

之后,我们为每个单独的检查添加消息,其中键再次是表单参数名称,如下所示:

        messages: {
          name: "Please enter your name",
          comment: {
            required: "Please enter a comment",
            minlength: "Your comment must be at least 50 chars long"
          }

验证规则还将包括添加到表单元素的原始属性。在我们的例子中,标记为最初无效的输入具有maxlength属性,这将作为 JavaScript 配置的其他规则的一部分添加。此外,JavaScript 中定义的规则可以移动为适当的表单元素的一部分。最后,JavaScript 版本的结果应该看起来像以下的屏幕截图:

它是如何工作的...

还有更多...

在我们的例子中,jQuery 验证插件显示的侧边文本的样式与标签相同。当存在验证错误时,会添加一个名为.error的简单 CSS 类,还有一个选项在发生验证问题时或被移除时执行函数。这可以在配置验证元素时完成,如下所示:

       highlight: function(currentElement) {
          console.log("error on " +currentElement );
        }, unhighlight: function(currentElement) {
          console.log("no more error on " +currentElement );
        }

就样式验证消息和元素而言,它们将在本章后面讨论。

按范围验证数字

当涉及到表单中的数字时,基本验证是检查数字是否在给定范围内。为了实现这一点,应该将minmax属性应用于数字、范围和日期/时间相关的输入。

操作步骤...

我们将创建一个包含几个输入元素的表单,这些元素需要进行范围限制,如下所示:

  1. 首先,我们将通过创建年龄为18的数字input字段来开始,如下面的代码片段所示:
<form>
  <div>
    <label>
      Age <input id="age" type="number" name="name" min="18" max="140" />
    </label>
  </div>
  1. 我们将添加range输入,用于用户将下注的Bet值,如下所示:
  <div>
    <label>
      Bet <input id="deposit" value="1000" type="range" name="deposit" min="0" max="2000" />
       <output id="depositDisplay">1000</output>
    </label>
  </div>
  1. 我们还包括以下限制为minmaxstep的输入:
  <div>
    <label>
      Doubles <input value="4" type="number" name="doubles" min="0" step="5" max="10" title="The value should be multiple of 5"/>
    </label>
  </div>
<div>
    <label>
      Awesomeness <input id="awesomeness" value="11" type="range" name="awesomeness" min="0" step="3" max="50" />
      <output id="awesomenessDisplay">10</output>
    </label>
  </div>
  1. 之后,我们将添加 jQuery 的依赖项,我们的example.js,以及一个输入submit,如下所示:
<input type="submit" />
</form>
  <script src="img/jquery.min.js"></script>
  <script src="img/example.js"> </script>
  1. 我们还将在example.js脚本中简单地将范围输入与输出字段链接起来,以便进行简单的显示,如下所示:
$(function() {
  $('#deposit').change(function() {
    $('#depositDisplay').html(this.value);
  });

  $('#awesomeness').change(function() {
    $('#awesomenessDisplay').html(this.value);
  });
 });

工作原理...

正如您所期望的那样,用户期望年龄在18140之间,如果输入不在该范围内,我们将得到一个下溢约束违规,显示适当的(值必须大于或等于{min})消息。同样,我们得到一个溢出约束违规,显示消息,值必须小于或等于{max}

对于输入类型range,用户无法超出范围,甚至无法触发步骤不匹配的验证错误。步骤不匹配错误应该只在初始值不在min值内以及step属性的倍数时触发:

<input value="11" type="range" name="awesomeness" min="0" step="3" max="50" />

这里11不应该是有效的,因为step属性的值是3,并且没有办法使用滑块到达11,但是初始值就是这样,所以我们应该得到一个验证错误,但这取决于浏览器。大多数当前版本的浏览器在渲染时只是纠正了最初选择的值。

如果我们尝试提交双打输入的表单,我们应该会收到一个验证消息,如下面的截图所示:

工作原理...

在这里,我们收到消息,因为值是4,但约束是min="0" step="5" max="10",这意味着输入的值必须是5的倍数。

用户无法使用输入类型range获得验证消息,但是使用输入类型number可以,因为用户可以在此手动插入数据。

使用内置的模式验证

为了创建更复杂的验证,我们需要使用 JavaScript。为了简化开发,引入了input字段的pattern属性。这使我们能够使用正则表达式进行验证检查,在本教程中,我们将看一些可以在其中使用的元素。

操作步骤...

在这个例子中,我们将使用简单的 HTML 创建一个表单,如下所示:

  1. 首先,我们将直接在body部分添加表单,从用户名字段开始:
  <div>
    <label>
      Username: <input type="text" title="only letters allowed" name="username" pattern="^[a-zA-Z]+$" />
    </label>
  </div>
  1. 然后,我们将添加电话,如下所示:
  <div>
    <label>
      Phone <input type="tel" name="phone" pattern="[\+]?[1-9]+" />
    </label>
  </div>
  1. 我们将包括网页url,如下所示:
  <div>
    <label>
      Webpage <input type="url" name="webpage" />
    </label>
  </div>
  1. 我们将添加电子邮件Gmail输入,如下所示:
  <div>
    <label>
      Emails <input type="email" name="emails" multiple required />
    </label>
  </div>
  <div>
    <label>
    Gmail <input type="email" name="emails" pattern="[a-z]+@gmail.com" maxlength="14"/>
    </label>
  </div>

工作原理...

如果指定了pattern属性,则使用早期版本的 JavaScript 正则表达式。整个文本必须与给定的表达式匹配。对于我们的例子,我们使用了宽松的验证,例如对于输入类型tel,我们允许数字和可选的前导+,由模式[\+]?[1-9]+指定。

一些其他输入类型,如URLemail,使用它们内置的验证。所有邮件必顶匹配以下正则表达式:

/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/

现在这是非常宽容的,所以我们可以添加额外的验证,就像我们在标记为Gmail的输入中添加的那样。约束可以组合,或者如果某个属性接受多个条目,所有这些条目都将根据约束进行验证,就像我们在以下电子邮件示例中所做的那样:

工作原理...

还要记住,我们需要使用 title 或 placeholder 或其他方式添加提示,因为用户将默认收到请匹配请求的格式消息,并且不知道自己做错了什么。

还有更多...

有一个名为html5pattern.com/的网站,旨在作为常用输入模式的来源。这绝对是一个很好的资源,我们鼓励您访问它。

内置约束和自定义验证的高级用法

到目前为止,我们已经使用了一些内置的验证机制。现在我们将更深入地研究其中一些,并了解如何添加自定义内容。当我们创建一个具有大多数这些功能的表单时,我们还将更改样式并应用一些更高级的检查,以及看到如何在某些元素上禁用验证。

注意

表单验证的当前工作草案版本可以在www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#client-side-form-validation找到。

如何做...

我们将创建一个表单,其中将使用 CSS 样式的错误消息,并使用 HTML 和 JavaScript 进行自定义验证,如下所示:

  1. 我们将首先创建头部部分,在其中包括example.css,其中 CSS 文件将包含具有有效、无效、可选和必需状态的input元素的选择器:
  <head>
    <title>Built In Validation</title>
    <link rel="stylesheet" href="example.css">
  </head>
  1. 下一步是创建example.css文件。valid.png图像可以在源示例中找到。在现实生活中,您可能不会使用所有这些状态来设计表单的外观,但我们在这里添加它是为了展示可以做什么:
input:invalid {
    background-color: red;
    outline: 0;
}
input:valid {
    background-color: green;
    background: url(valid.png) no-repeat right;
    background-size: 20px 15px;
    outline: 0;
}
input:required{
  box-shadow: inset 0 0 0.6em black;
}
input:optional{
  box-shadow: inset 0 0 0.6em green;
}

注意

CSS box-shadow在旧版浏览器中并不完全支持,例如 IE 8。box-shadow的规范可以在www.w3.org/TR/css3-background/#box-shadow找到。

  1. head部分之后,我们将开始在body部分中添加表单元素。首先,我们将添加namenickname字段,使它们成为required,稍后我们将确保它们的值不相同:
  <div>
    <label>
      Name <input required name="name" x-moz-errormessage="We need this."/>
    </label>
  </div>
  <div>
    <label>
    Nickname <input required name="nickname"/>
    </label>
  </div>
  1. 我们还可以包括两个与日期/时间相关的输入,一个用于week,另一个用于month,我们将限制周数从 2013 年第二周到 2014 年第二周,并允许选择其他每个月:
       <div>
      <label>
        Start week <input type="week" name="week" min="2013-W02" max="2014-W02" required />
      </label>
    </div>
    <div>
      <label>
        Best months <input value="2013-01" type="month" step="2" name="month" />
      </label>
    </div>
  1. 此外,我们将添加三个按钮:一个用于提交表单,另一个用于使用 JavaScript 检查有效性,另一个用于在没有验证和约束检查的情况下提交:
    <button type="submit">Save</button>
    <button type="submit" formnovalidate>Save but don't validate</button>
    <button type="button">Check Validity</button>
  1. 在表单之外,我们将添加一个div元素来显示一些日志信息:
  <div id="validLog"></div>
  1. 至于 JavaScript,我们添加了 jQuery 的依赖项并包括example.js
    <script src="img/jquery.min.js"></script>
    <script src="img/example.js"></script>
  1. example.js文件中,我们将为检查有效性按钮添加一个事件,在其中我们将打印每个表单元素的验证错误的ValidityState值到validLog
    $(function() {
      var attemptNumber = 1;
      $("button[type=button]").click(function(){
        var message = (attemptNumber++)+"#<br/>";
        var isValid = $('form')[0].checkValidity();
        if(isValid){
          message += "Form is valid";
        }else{
          $("input").each(function( index ) {
            var validityState = $(this)[0].validity;
            var errors = "";
            If(!validityState.valid){
              message += "Invalid field <b> " + $(this).attr("name")+"</b>: ";
              for(key in validityState){
                if(validityState[key]){
                errors += key+" ";
            }
          }
          message += "  " + errors + " <br />";
        }
        });
      }
      message += "<hr />";
      $("#validLog").prepend(message);
    });
  1. 要添加自定义验证,我们将使用.setCustomValidity()方法,因此它将检查namenickname的值是否相同,如果是,我们将添加验证错误,如果不是,我们将删除自定义检查:
    $("input[name='nickname']").change(function(){
      if($(this).val() === $("input[name='name']").val()){
        $(this)[0].setCustomValidity("You must have an awesome nickname so nickname and name should not match");
      }else{
      $(this)[0].setCustomValidity("");

    });
    $("input[name='name']").change(function(){
      if($(this).val() === $("input[name='nickname']").val()){
      $(this)[0].setCustomValidity("Nickname and name should not match");
      }else{
      $(this)[0].setCustomValidity("");
      }
    });

它是如何工作的...

required属性标记了表单内的 HTML 元素,要求在提交表单之前必须有一个值。第一个没有值的字段将在提交时获得焦点,并向用户显示带有消息的提示:

它是如何工作的...

在 Firefox 上,有几种自定义显示给用户的消息的方法;我们可以使用x-moz-errormessage属性。在我们的情况下,这是x-moz-errormessage="We need this.",但这只在那里起作用。在 Chrome 上,title属性还会在标准消息旁边额外显示,但原始消息仍然存在。更改消息的另一种方法是使用 JavaScript 设置值:

 <input type="text" required="required" oninvalid="this.setCustomValidity('Please put some data here')">

至于样式化表单元素,有 CSS 伪类选择器:required:optional

注意

在 WebKit 中,有特定于浏览器的 CSS 选择器,可以用来设置提示框的样式,如下所示:

::-webkit-validation-bubble {…}

::-webkit-validation-bubble-message {…}

但是因为它们是特定于浏览器的,它们在实际使用中并不是非常有用。

minmaxstep属性可以用于与日期相关的输入类型,而不仅仅是数字。日期类型的默认步长是一天,周类型是一周,依此类推。如果我们设置一个与默认步长不同的步长,例如在月份输入上,如果我们将步长设置为 2,用户将无法从日期选择器控件中选择每隔一个月,但仍然可以在文本中输入错误的日期,触发stepMismatch

因为验证是在提交表单之前触发的,如果输入无效,submit事件将不会被调用。如果我们需要在不进行验证的情况下提交数据,可以使用formnovalidate属性,如下所示:

<button type="submit" formnovalidate>Save but don't validate</button>

有时我们可能需要从 JavaScript 中访问元素的validityState值;为此,可以在表单和表单中的输入元素上执行checkValidity()方法。顾名思义,它检查元素的状态,当它在表单上调用时,所有子元素都会被检查验证,此外,我们还可以在每个单独的元素上调用该方法,例如inputselecttextarea。在我们的情况下,对于表单,它是这样的:

$('form')[0].checkValidity();

$('form')[0]元素给我们提供了所选 jQuery 对象的包装 DOM 元素,也可以通过在所选元素上调用.get()来完成。每个元素都有一个我们可以读取的validitystate值,如下所示:

       $("input").each(function(index) {
          var validityState = $(this)[0].validity;
       …

在这一点上,我们可以访问validityState对象的几个内置检查,例如:valueMissingtypeMismatchpatternMismatchtooLongrangeUnderflowrangeOverflowstepMismatchbadInputcustomError。如果存在这样的约束违规,每个都将返回true。在本示例代码中,我们只是将约束违规的名称打印到日志中。

如果我们有相互依赖的字段,或者需要实现一些自定义验证逻辑,会发生什么?没问题,我们可以在每个依赖字段上使用setCustomValidity()方法。在我们的情况下,我们希望namenickname变量的输入不同。因此,我们添加了更改监听器,如果它们相同,我们只需使用customValidity("your message here")设置消息,当我们需要移除违规时,我们将消息设置为空字符串:

    $("input[name='nickname']").change(function(){
      if($(this).val() === $("input[name='name']").val()){
      $(this)[0].setCustomValidity("You must have an awesome nickname so nickname and name should not be the same");
      }else{
      $(this)[0].setCustomValidity("");
      }
    });

此外,还有两个 CSS 伪选择器:valid:invalid,我们将用它们来根据它们的validityState值来设置元素的样式。

注意

客户端表单验证的规范可以在以下网址找到:www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#client-side-form-validation。至于约束 API,更多信息可以在www.whatwg.org/specs/web-apps/current-work/#the-constraint-validation-api找到。

需要注意的一点是,并非所有浏览器都完全支持所有功能。例如,IE 9 没有对任何约束或新输入类型的支持。有关当前浏览器支持的更多信息,请访问caniuse.com/#search=form%20valiwww.quirksmode.org/compatibility.html

还有更多...

如果我们想使用一些属性来禁用整个表单的验证,我们可以设置表单属性称为novalidate。例如,这将禁用检查,但允许使用输入类型范围的minmax

还有另一种方法可以禁用标准浏览器提示框并创建自定义提示框:

    $('form').each(function(){
      $(this)[0].addEventListener('invalid', function(e) {
        e.preventDefault();
        console.log("custom popup");
      },true);
    });

在使用内置约束之前,应该考虑几个问题:

  • 我们需要知道用户何时点击了提交按钮吗?

  • 我们是否需要为不支持表单验证 API 的浏览器提供客户端验证?

如果我们需要知道用户何时尝试提交表单,我们可以附加点击事件监听器,而不是提交。至于旧版浏览器,我们可以选择依赖必须存在的服务器端验证,但如果我们不想在客户端失去功能,可以通过添加 webshim 的方式来做到这一点,afarkas.github.com/webshim/demos/index.html

更多内容...

计算密码强度

许多网站在其注册表单上显示用户选择的密码强度。这种做法的目标是帮助用户选择一个更好、更强的密码,这样就不容易被猜测或暴力破解。

在这个示例中,我们将制作一个密码强度计算器。它将通过计算潜在攻击者必须在猜测密码之前进行的暴力破解尝试的数量来确定密码强度。它还会警告用户,如果他的密码在 500 个常用密码列表中。

做好准备

在开始之前,重要的是要看一下我们将如何计算攻击者必须进行的暴力破解尝试的数量。我们将看两个因素:密码的长度和用户使用的字符集的大小。

字符集的大小可以通过以下方式确定:

  • 如果用户在密码中添加小写字母表字母,则字符集的大小将增加 26(字母表中的字母数)

  • 如果用户在密码中的任何位置使用大写字母,将添加额外的 26 个字符

  • 如果用户添加一个数字,则添加 10 个字符

  • 如果用户添加特殊字符,如句号、逗号、括号、和等,则添加 24 个字符

  • 如果用户使用其他表中找不到的 Unicode 字符,则添加 20 个字符

如何做...

让我们编写 HTML 和 JavaScript 代码:

  1. 创建一个简单的 HTML 页面,其中包含一个password输入,然后添加一个div元素,我们将使用密码强度结果进行更新。常见密码将通过名为common-passwords.js的脚本包含:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Password strength calculator</title>
   </head>
   <body>
   <input type="password" id="pass" value="" />
   <div id="strength">0 (very poor)</div>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/common-passwords.js"></script>
   <script type="text/javascript" src="img/example.js"></script>
   </body>
</html>

common-passwords.js脚本没有包含在这里,但可以在附加代码中找到。

  1. 检查逻辑的代码在example.js中:
$(function() {
    function isCommon(pass) {
      return ~window.commonPasswords.indexOf(pass);
    }

    function bruteMagnitude(pass) {
      var sets = [
      { regex: /\d/g, size: 10 },
      { regex: /[a-z]/g, size: 26 },
      { regex: /[A-Z]/g, size: 26 },
      { regex: /[!-/:-?\[-`{-}]/g, size: 24 },
      ];
      var passlen = pass.length,
      szSet = 0;

      sets.forEach(function(set) {
        if (set.regex.test(pass)) {
          szSet += set.size;
          pass = pass.replace(set.regex, '');
        }
        });
        // other (unicode) characters
        if (pass.length) szSet += 20;
        return passlen * Math.log(szSet) / Math.LN10;
    }

    var strengths = ['very poor', 'poor', 'passing', 'fair',
        'good', 'very good', 'excellent'];

    function strength(pass) {
        if (isCommon(pass) || !pass.length) return 0;
        var str = bruteMagnitude(pass);
        return str < 7  ? 0 // very poor
             : str < 9  ? 1 // poor      - 10 million - 1 billion
             : str < 11 ? 2 // passing   - 1 billion - 100 billion
             : str < 13 ? 3 // fair      - 100 billion - 10 trillion
             : str < 15 ? 4 // good      - 10 trillion - 1 quadrillion
             : str < 17 ? 5 // very good - 1-100 quadrillion
             : 6;           // excellent - over 100 quadrillion
    }
  1. password字段中按下键时,更新指示密码强度的div元素:
    $('#pass').on('keyup keypress', function() {
      var pstrength = strength($(this).val());
      $("#strength").text(pstrength + ' (' + strengths[pstrength] + ')');
    });
});

它是如何工作的...

计算分为两部分:检查密码的普遍性和密码的复杂性。

我们通过检查密码是否可以在commonPasswords数组中找到来检查密码是否常见,该数组由common-password.js提供。如果未找到条目,Array#indexOf返回 1。按位非运算符~将该值转换为零,这等于 false。所有大于或等于 0 的其他数字将具有负值,这是 true 值。因此,如果在数组中找到密码,则整个表达式返回 true。

bruteMagnitude函数中,我们使用passwordLength和字符setsize方法计算密码的暴力破解数量级:

magnitude = log10(setSize passwordLength) = passwordLength * log10(setSize)

这是暴力破解密码攻击者必须尝试猜测密码的密码数量级的近似值。

基于这些信息,我们现在可以给出实际的密码强度。如果密码属于前 500 个常见密码之一,它将被分类为弱密码。否则,它将根据其暴力破解强度按照以下表进行分类:

数量级 密码数量 评级
少于 7 少于 1000 万 非常差
7 到 8 1000 万到 10 亿
9 到 10 10 亿到 1000 亿 通过
11 到 12 1000 亿到 10 万亿 公平
13 到 14 10 万亿到 1 千万亿 良好
15 到 17 1 到 100 万亿 非常好
大于 17 大于 100 万亿 优秀

分类,以及描述性文本将在每次按键时更新,并显示给用户在密码字段下方。

验证美国邮政编码

在网页上具有地址表单的情况下,在客户端验证邮政编码可能会很有用。

输入数字是一个容易出错的过程。如果我们能够提供一些基本的即时验证来告知用户可能在其数据输入中出现的错误,那对用户来说将会很好。

另一方面,一个令人满意的完整邮政编码数据库具有非平凡的大小。在客户端加载完整数据库可能会很困难且不太理想。

在这个示例中,我们将编写一个客户端邮政编码验证函数。在这个过程中,我们将学习如何将一个非平凡的邮政编码数据库转换为可以在客户端加载的较小表示。

准备工作

首先让我们下载邮政编码数据库文件。unitedstateszipcode.org网站提供了一个 CSV 格式的免费邮政编码数据库(www.unitedstateszipcodes.org/zip-code-database/)。

我们将从该文件中提取一个较小的数据库,该数据库可以在客户端加载。为此,我们将编写一个 Node.js 脚本,请确保已安装 Node.js。从nodejs.org/下载 Node.js,在附录 A 中有解释,安装 Node.js 和使用 npm

注意

Node.js是建立在 Chrome 的 V8 JavaScript 引擎之上的平台,用于编写快速的异步网络应用程序。它配备了一个名为npm的出色模块管理器,以及一个包含数以万计模块库的注册表。

如何做...

在与zip_code_database.csv相同的目录中,我们将创建一个新的 Node.js 脚本。为了处理 CSV 文件,我们将使用一个 CSV 解析库。

  1. 从相同目录的命令提示符中,让我们通过运行以下命令来安装 node 模块 CSV:
npm install csv

  1. 然后我们将创建createdb.js,它将解析 CSV 文件并从中提取最少量的数据,即美国州和邮政编码:
var csv = require('csv');
var zips = {};
csv().from.path('zip_code_database.csv').on('record', function(zc) {
    // column 0 is zipcode; column 5 is state
    // column 12 is country, 13 is decomissioned (0/1)
    // filter non-decmissioned US zipcodes
    if (zc[12] == 'US' && !parseInt(zc[13])) {
      zips[zc[5]] = zips[zc[5]] || [];
      zips[zc[5]].push(parseInt(zc[0].trim(), 10));
    }
}).on('end', function() {
  1. 此时,我们有一个可用的邮政编码数组。但是,如果我们直接写出所有这些邮政编码,将会得到一个相当大的 400 KB 的 JSON 数组,在使用 GZIP 压缩后为 150 KB。许多有效的邮政编码数字是连续的。我们可以利用这一点,将它们表示为范围。通过应用这种技术,我们得到一个 115 KB 的文件,在压缩后为 45 KB。这个大小看起来更加可接受:
    var zipCodeDB = [];
    function rangify(arr) {
      var ranges = [], first = 0, last = 0;
      for (var k = 0; k < arr.length; ++k) {
        var first = arr[k];
        while (arr[k] + 1 >= arr[k + 1] && k < arr.length - 1) ++k;
        var last = arr[k];
        ranges.push(first != last? [first, last]:[first]);
        first = last = 0;
      }
      return ranges;
    }
  1. 最终表示将是一个按state名称排序的 JSON 数组。数组中的每个元素表示一个州,包含两个属性:州名和表示为数字的有效邮政编码列表,或表示为二维数组的邮政编码范围:
    var list = [];
    for (var state in zips) if (state != 'undefined') {
        list.push({state: state, codes: rangify(zips[state])});
    }
    list = list.sort(function(s1, s2) {
        return s1.state < s2.state ? -1
             : s1.state > s2.state ?  1
             :0;
    });
    console.log('window.zipCodeDB =', JSON.stringify(list));
    }
  1. 在相同目录中从命令行中运行此脚本node createdb.js > zipcodedb.js将生成zipcodedb.js文件,其中包含数据库。以下是数据库 JSON 的示例:
window.zipCodeDB = [{
    "state": "AA",
    "codes": [34004, 34006, 34008, 34011, [34020, 34025],
        [34030, 34039], 3404, ...]
},
...
]
  1. 现在我们可以使用这个数据库来创建基本的验证器,将其包含在我们的index.html页面中。页面将包含一个简单的州选择下拉菜单和一个邮政编码字段。在邮政编码字段下方将是验证消息:
<!DOCTYPE HTML>
<html>
    <head>
        <title>Zip code validation</title>
   </head>
   <body>
   <p>State: <select id="state"></select></p>
   <p>Zipcode: <input type="text" id="zipcode" value="" /></p>
   <div id="validate">Invalid zipcode</div>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/zipcodedb.js"></script>
   <script type="text/javascript" src="img/example.js"></script>
   </body>
</html>
  1. 最后,我们将编写一个lookup函数来检查给定的邮政编码是否在我们的数据库中,我们将在用户输入时用它来验证用户输入。我们将使用相同的数据库填充州下拉菜单:
$(function() {

    function lookup(zipcode) {
      function within(zipcode, ranges) {
        for (var k = 0; k < ranges.length; ++k)
        if (zipcode == ranges[k]
        || (ranges[k].length > 1
        && ranges[k][0] <= zipcode
        && zipcode <= ranges[k][1])) return k;
        return -1;
        }
        for (var k = 0; k < window.zipCodeDB.length; ++k) {
          var state = window.zipCodeDB[k],
          check = within(zipcode, state.codes);
          if (~check) return state.state;
        }
        return null;
    }

    window.zipCodeDB.forEach(function(state) {
      $('<option />').attr('value', state.state)
      .text(state.state).appendTo('#state');
    });

    $("#zipcode").on('keypress keyup change', function() {
      var state = lookup($(this).val());
      if (state == $("#state").val())
      $('#validate').text('Valid zipcode for ' + state);
      else $('#validate').text('Invalid zipcode');
    });
});

工作原理...

为了在客户端验证邮政编码,我们首先需要将数据库转换为更小的大小。

下载的数据库包含了许多额外的数据,例如城市到邮政编码的映射,邮政编码类型,时区和地理坐标,以及已废弃的邮政编码。我们删除了额外的数据,只留下仍在使用中的美国邮政编码及其所在州。

为了进一步减少数据库,我们将更长的有效邮政编码范围表示为包含范围内第一个和最后一个数字的数组。这有助于将数据库大小进一步减小到一个合理的大小,与中等网站图像的大小相比。

为了使用数据库,我们编写了一个简单的lookup函数,检查邮政编码是否在任何州的zipcoderanges的值列表中,并在找到时返回州名。

在用户输入邮政编码时,验证信息会自动更新。

使用异步服务器端验证

许多验证检查只能在服务器端执行。以下是示例:

  • 在验证用户注册表单时,我们需要检查输入的用户名是否可用

  • 当用户输入邮政地址时,我们可能需要向外部服务询问地址是否正确

服务器端验证检查的问题在于它们需要是异步的。因此,它们不能被写成 JavaScript 函数,返回验证结果。

为了解决这个问题,在这个示例中,我们将制作一个使用续传风格的验证器。示例中有一个用户名输入字段,用于与服务器进行验证。服务器会检查用户名是否可用于注册或已被其他用户占用。

准备工作

我们将简要介绍续传风格。这是大多数 JavaScript 库用于异步操作的风格,例如服务器通信。例如,在 jQuery 中,我们不是写以下代码:

data = $.getJSON('/api/call');
doThingsWith(data);

我们写如下:

$.getJSON('/api/call', function(data) {
    doThingsWith(data);
});

我们可以将相同的转换应用于验证函数,如下所示:

var errors = validate(input)
if (errors.length) display(errors);

这将变成:

validate(input, function(errors) {
    if (errors.length) display(errors);
});

这意味着我们还需要更改validate函数。例如,如果我们有如下所示:

function validate(input) {
    if (condition(input))
      return [{message: "Input does not satisfy condition"}];
    else return [];
}

将其转换为续传风格后,我们将会有:

function validate(input, callback) {
    condition(input, function(result) {
      if (result) callback([{message: "Input does not satisfy condition"}]);
      else callback([]);
    });
}

这使我们能够在验证函数中使用服务器端调用,例如$.getJSON,如下所示:

function validate(input, callback) {
    $.getJSON('/api/validate/condition', function(result)
    if (result) callback([{message: "Input does not satisfy condition"}]);
    else callback([]);
    });
}

现在我们可以从浏览器中使用我们的服务器端验证器。

如何做...

我们将编写包含要验证的表单和实现验证的 JavaScript 代码的 HTML 页面。

  1. 让我们从 HTML 页面开始。它必须包含一个带有用户名输入和默认情况下隐藏的红色文本验证结果的表单:
<!DOCTYPE HTML>
<html>
<head>
<title>Async validation</title>
<style type="text/css">
p[data-validation-error] {
    display:none;
    color:red;
}
</style>
</head>
<body>
<form>
    <p>Username:</p>
    <p><input name="user" id="user" value="" /></p>
    <p data-validation-error="user"></p>
</form>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 验证代码将在example.js中 - 它包含一个模拟async服务器调用的函数,一个用于延迟执行async服务器调用以防止多次调用的函数,以及一个显示验证结果的函数:
$(function() {
    function validate(name, callback) {
      // Simulate an async server call
      setTimeout(function() {
        callback(~['user', 'example'].indexOf(name) ?
        'Username is already in use' : null);
        },500);
    }
    function createDelayed(ms) {
      var t = null;
      return function(fn) {
        if (t) clearTimeout(t);
        t = setTimeout(fn, ms);
        };
    };
    var delayed = createDelayed(1500);

    var user = $('input[name="user"]'),
    form = user.parents('form');
    user.on('keyup keypress', function() {
      delayed(validate.bind(null, $(this).val(), function callback(err) {
        var validationError = form.find('p[data-validation-error="user"]');
        console.log(validationError);
        if (err) validationError.text(err).show();
        else validationError.hide();
      }));
    });
});

工作原理...

example.js中的validate函数中的代码通过使用setTimeout函数模拟了服务器调用。可以轻松地用真实的服务器验证 API 调用替换这段代码,类似于jQuery.getJSON来获取验证结果。

createDelayed函数创建一个delayer对象。delayer对象包装要延迟的函数。它不同于setInterval,因为如果在延迟到期之前再次调用delayer对象,先前的超时将被取消并重新启动。这有助于我们避免在每次按键时向服务器发送请求,而是在用户停止输入后1500ms发送请求。

我们在每次用户按键时调用delayer对象,将"this"绑定到null,将第一个参数绑定到输入字段的当前值,将callback函数绑定到一个函数,如果存在返回的验证错误,则显示它。

结合客户端和服务器端验证

在处理真实的 Web 表单时,我们通常需要对多个字段进行各种验证。一些字段可能只需要在客户端执行的检查,而有些可能还需要服务器端验证。

在这个示例中,我们将设计和实现自己的验证插件,支持异步验证。它将类似于 jQuery Validate 的工作。我们还将实现一些基本的验证方法,如requiredminLengthremote

我们将在一个简单的用户注册表单上使用这些方法,直到用户在所有字段中输入有效数据为止,该表单将被阻止提交。

准备工作

我们设计过程的第一步是设计将在验证器中使用的数据结构。我们将创建一个类似于 jQuery Validate 的 API,它以配置对象作为参数。但是,我们将选择更现代的 HTML5 方法,其中验证规则嵌入到 HTML 中,如下所示:

  <form data-avalidate>
    <input name="field"
        data-v-ruleName="ruleParam" name="user" value="" />
    <span data-v-error="ruleName">{parameterized} rule error</span>
  </form>

为了支持这种规则和消息结构,Validate 将利用验证插件。

每个插件都将有一个唯一的名称。

该插件将是一个接受三个参数的函数:正在进行验证的元素、规则参数对象和在验证完成时调用的callback函数。callback函数将有两个参数:第一个参数将指示字段是否有效,第二个参数将包含消息参数。

该插件将阻止表单提交,除非验证所有字段的有效性。

如何做...

让我们编写 HTML 和 JavaScript 代码。

  1. index.html页面将包含嵌入其验证规则的表单。请注意,我们还可以混合使用标准的 HTML 表单验证,例如通过required属性,如下所示:
<!DOCTYPE HTML>
<html>
<head>
<title>Async validation</title>
<style type="text/css">
[data-v-error] { display:none; color:red; }
label { width: 10em; display:inline-block; text-align: right; }
</style>
</head>
<body>
  <form data-avalidate>
    <p>
    <label for="user">Username:</label>
    <input name="user"
      required
      data-v-minlen="6"
      data-v-server="/api/validate/unique"
      value="" />
    <span data-v-error="minlen">Must be at least {minlen} characters long</span>
    <span data-v-error="server">{username} is already in use</span>
    </p>

    <p>
    <label for="email">Email:</label>
    <input name="email" type="email"
      required
      data-v-minlen="6"
      data-v-server="/api/validate/email"
      value="" />
    <span data-v-error="server">{email} is already in use</span>
    </p>

    <p><label for="pass">Password:</label>
    <input name="pass" type="password"
      required
      data-v-minlen="8"
      data-v-strength="3"
      value="" />
    <span data-v-error="minlen">Must be at least {minlen} characters long</span>
    <span data-v-error="strength">Strength is {strength}</span>
    </p>

    <p><label for="pass2">Password (again):</label>
    <input name="pass2" type="password"
      required
      data-v-equals="pass"
      value="" />
    <span data-v-error="equals">Must be equal to the other password</span>
    </p>

    <input type="submit">

</form>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/avalidate.js"></script>
<script type="text/javascript" src="img/avalidate-plugins.js"></script>
</body>
</html>

注意

这个 HTML 文件的有趣之处在于除了avalidate.jsavalidate-plugins.js之外,没有包含其他脚本,但它们为这个表单提供了完整的验证。

  1. 让我们看看需要添加到avalidate.js的代码:
;(function($) {

为了正确执行异步验证,我们需要能够延迟请求,直到用户停止输入。为此,我们使用createDelayed - 它创建超时,在每次调用时重置自身:

    function createDelayed(ms) {
      var t = null;
      return function(fn) {
        if (t) clearTimeout(t);
        t = setTimeout(fn, ms);
      };
    }

showError在表单旁边显示适当的错误,并用模板文本填充它。第一次运行时,它将模板移出error元素的内部文本,并添加到一个新的属性中:

    function showError(error, strings) {
      var tmpl;
      if (!error.attr('data-v-template')) {
      tmpl = error.text().toString();
      error.attr('data-v-template', tmpl);
    } else tmpl = error.attr('data-v-template');
      for (var key in strings)
      tmpl = tmpl.replace('{'+key+'}', strings[key]);
      error.text(tmpl).show();
    }

elementVerifier在一个元素上执行。它查找由data-v-pluginName属性指定的所有验证器插件,从属性中读取插件选项,然后运行异步插件。

  1. 当所有插件完成验证时,如果没有找到错误,它将标记元素为有效。否则,它会显示错误,就像它们出现的那样:
    function elementVerifier() {
      var isValid = true, waiting = 0, field = this;
      $.each(this.attributes, function(i, attr) {
        if (!attr.name.match(/data-v-/)) return;
        var plugin = attr.name.toString().replace('data-v-',''),
        options = attr.value;

        ++waiting;
        $.avalidate[plugin].call(field, options, function (valid, strings) {
          var error = $(field).parent().find('[data-v-error="'+plugin+'"]');
          if (!valid) {
            showError(error, strings);
            isValid = false;
          }
          else error.hide();
          if (!--waiting && isValid)
          $(field).attr('data-valid', 1);
        });
      });
    }
  1. setupFormVerifier通过绑定到其字段中发生的所有更改、键盘和鼠标事件,启用了对特定表单的验证过程。它为每个元素创建一个单独的delayer变量,并使用该delayer运行elementVerifier对象。最后,它禁止表单提交,除非elementVerifier对象标记所有字段为有效:
    function setupFormVerifier(form) {
      form.on('change keyup mouseup', 'input,textarea,select', function() {
        var $this = $(this)
        var delayer = $this.data('avalidate');
        if (!delayer) {
          delayer = createDelayed(800);
          $this.data('avalidate', delayer);
        }
        $this.attr('data-valid', 0);
        delayer(elementVerifier.bind(this));
        }).on('submit', function(e) {
            var all = $(this).find('input,textarea,select').filter('[type!="submit"]'),
          valid = all.filter('[data-valid="1"]');
          if (all.length != valid.length)
          e.preventDefault();
        });
    }
  1. 以下是使一切无需手动干预的部分。我们监听文档body对象上到达的所有事件,如果一个事件到达一个应该启用验证但没有启用的表单,我们就会在其上运行setupFormVerifier(一次):
    $(function() {
      $('body').on('submit change keyup mouseup', 'form[data-avalidate]', function() {
        if (!$(this).attr('data-avalidate-enabled')) {
          setupFormVerifier($(this));
          $(this).attr('data-avalidate-enabled', 1)
        }
      });
    });
}(jQuery));
  1. 插件更容易编写。这是avalidate-plugins.js。请注意,服务器插件是用setTimeout模拟的。在进行 AJAX 调用时,同样的原则也适用:
;(function($) {

    $.avalidate = {};
    $.avalidate.equals = function(name, callback) {
      var other = $(this).parents('form').find('[name="'+name+'"]').val();
      callback($(this).val() === other, {});
    };
    $.avalidate.minlen = function(len, callback) {
       callback($(this).val().length >= len || $(this).text().length >= len, {minlen: len});
    };
    $.avalidate.server = function(param, cb) {
      setTimeout(function() {
        var val = $(this).val();
        if (~param.indexOf('mail'))
        cb('test@test.com' != val, {email: val });
        else
        cb('username' != val, { username: val });
      }.bind(this), 500);
    };
    $.avalidate.strength = function(minimum, cb) {
        cb($(this).val().length > minimum, {strength: 'Low'});
    };

}(jQuery));

它是如何工作的...

这个验证器利用了新的 HTML5 数据属性功能。HTML5 通过添加输入元素属性和类型确实包含了一些很棒的新验证选项,但这还不够。为了解决这个问题,我们遵循 HTML5 模型,并为验证方法和验证错误消息添加了自己的数据属性。

为了使这些新的数据属性起作用,我们需要加载 JavaScript 代码。JavaScript 初始化元素的一个缺点是,每当我们在页面上添加新元素时,都需要调用初始化函数。这个插件成功地避免了这个缺点,它使用了新的 jQuery 绑定 API。与直接绑定到表单不同,监听器附加到了文档body对象上。因此,它可以与所有表单元素一起工作,包括新添加的元素。

灵活的插件使验证器能够轻松扩展,而无需修改核心。添加新的验证规则就像添加一个新函数一样简单。

最后,我们的错误消息可以包含由验证器提供的可选消息字符串填充的用户友好模板。

注意

您可能已经注意到,JavaScript 文件以分号字符(;)开头。这使它们更安全,可以进行连接和缩小。如果我们在另一个用括号括起来的脚本之前添加一个以值结尾的脚本(在没有分号的情况下将整个脚本内容视为函数调用),那么该值将被视为该函数调用的参数。为了避免这种情况,我们在括号之前添加一个分号,终止可能缺少分号的任何先前语句。

第七章:数据序列化

在本章中,我们将介绍以下内容:

  • 将 JSON 反序列化为 JavaScript 对象

  • 将对象序列化为 JSON 字符串

  • 解码 base64 编码的二进制数据

  • 将二进制数据或文本编码为 base64

  • 将二进制数据序列化为 JSON

  • 序列化和反序列化 cookie

  • 将表单序列化为请求字符串

  • 使用 DOMParser 读取 XML 文档

  • 在客户端对 XML 文档进行序列化

介绍

数据存储和传输的基本概念之一是序列化。我们将介绍一些准备数据以便发送到另一个环境或永久保存的方法。除此之外,我们还将看到一些读取另一个计算机环境序列化的数据的方法。

将 JSON 反序列化为 JavaScript 对象

最简单的情况是将 JSON 数据读入 JavaScript 对象。以这种方式格式化的数据是轻量级的,此外它是 JavaScript 的一个子集。有几种方法可以读取这些数据,我们将看看如何通过创建一个简单的 JSON 片段,然后将其转换为 JavaScript 对象来实现这一点。

如何做...

这个例子足够简单,可以作为 HTML 文件中的脚本,甚至可以在 firebug 或开发者工具控制台上执行:

  1. 我们首先需要以下序列化的 JSON 字符串:
var someJSONString = '{"comment":"JSON data usually is retrieved from server","who":"you"}';
  1. 有几种不需要添加外部 JavaScript 依赖项的方法来实现这一点,一种是通过使用eval,另一种是通过使用json
     var evalData =  eval('(' + someJSONString + ')');
     var jsonData =  JSON.parse(someJSONString);
  1. 之后,我们将尝试访问反序列化对象的一些属性:
     document.writeln(someJSONString.who + " access without conversion <br/>" );
     document.writeln(jsonData.who + " with parse <br/>" );
     document.writeln(evalData.who + " with eval <br/>");

在执行时,第一个document.writeln方法应该返回undefined,因为我们正在尝试访问尚未反序列化的 JSON 字符串上的属性,而在另外两个方法中,我们应该得到值you

它是如何工作的...

JSON 是一种与语言无关的格式,但与此同时 JSON 是 JavaScript,这意味着我们可以使用eval函数。现在这非常简单,因为这是一个顶级函数,它接受字符串作为输入进行评估。如果作为参数传递的字符串具有 JavaScript 语句,eval将执行这些语句。这可能是一个危险的事情,因为它执行传递给它的代码。如果它用于您不信任的代码,那么您可能会从潜在恶意的第三方那里获得利用。对于eval的大多数用例,已经有很好的替代方案。当我们使用eval时,调试也可能非常困难,因此在大多数情况下我们应该避免使用它。

在进行 JSON 解析时,在大多数现代浏览器上,我们可以使用已添加到 JavaScript 1.7 的JSON.parse(text[, reviver])语句。该函数解析一个字符串作为 JSON,并具有可选参数reviver,它是一个可以转换解析产生的值的函数。例如,如果我们想要在每个值上附加"a?",我们可以定义如下内容:

    var transformed = JSON.parse(someJSONString, function(key, val) {
      if (key === "") return val;
      return val +' a?';
     });

现在,如果我们尝试访问transformed.who,我们将得到"you a?"。最终对象将包含以下信息:

{comment: "JSON data usually is retrieved from server a?", who: "you a?"}

这意味着解析的原始字符串的每个值都附加了值'a?',并且在给定迭代中的键取了值commentwho

如果reviver函数对于给定值返回undefinednull,那么该属性将被删除,因此它可以用作过滤机制。

还有更多...

在不支持本地 JSON 的旧浏览器中会发生什么。有两个简单的选择,我们可以只包含 JSON 2 或 JSON 3:

   <script src="img/json3.min.js"></script>
   <script src="img/json2.js"></script>

JSON 3 是一个兼容几乎所有 JavaScript 平台的 polyfill,从某种意义上说,它是 JSON 2 的更新实现,这是我们应该使用的。 JSON 2 没有正确处理的几个不一致性和特殊情况,尽管在撰写本文时,旧版本更为普遍。此外,JSON 3 解析器不使用evalregex,这使其在移动设备上更安全,并带来性能优势,这在移动设备上可能非常重要。

如果您的项目中已经有 jQuery,您可以使用jQuery.parseJSON(json),类似地,Prototype JS 有自己的实现,String#evalJSON()

提示

一个常见的错误是使用单引号而不是双引号。大多数 JSON 实现不允许使用单引号,这可能是为了简单起见。引用 Douglas Crockford 的话:JSON 的设计目标是最小化、可移植、文本化,并且是 JavaScript 的子集。我们在互操作方面需要达成的共识越少,我们就越容易进行互操作。

将对象序列化为 JSON 字符串

前一个配方的反向操作是将 JavaScript 对象序列化为 JSON 字符串。同样,对于浏览器是否支持 JSON 的规则也适用,但在大多数浏览器中这不是问题。一种方法是手动创建字符串,但这只是一种容易出错和混乱的浏览器方式,因此我们将尝试一些现有的方法。

如何做到...

在以下示例中,我们仅使用 JavaScript,因此可以将其放在 HTML 文件中的简单脚本标记中:

  1. 首先需要数据以便将其序列化为字符串,因此我们将创建一个简单的 JavaScript 对象:
    var someJSON = {
      "firstname":"John",
      "lastname":"Doe",
      "email":"john.doe@example.com"
     };
  1. 我们创建另一个对象,其中我们将拥有toJSON()函数:
    var customToJSON = {
      "firstname":"John",
      "lastname":"Doe",
      "email":"john.doe@example.com",
      toJSON: function () {
      return {"custom":"rendering"};
      }
    };
  1. 为了将 JavaScript 对象转换为字符串,我们将使用JSON.stringify(value [, replacer [, space]])函数:
     var jsonString = JSON.stringify(someJSON);
     var jsonStringCustomToJSON = JSON.stringify(customToJSON);
  1. 之后,我们将尝试该函数的其他参数,对于replacer,我们将创建一个允许属性的列表,对于第三个参数,我们将尝试两种不同的选项:
    var allowedProperties=["firstname","lastname"];
     var jsonCensured = JSON.stringify(someJSON , allowedProperties);
     var jsonCensured3Spaces = JSON.stringify(someJSON,allowedProperties,30);
     var jsonCensuredTab = JSON.stringify(someJSON,allowedProperties,"\t");
  1. 然后我们可以简单地将输出写入文档对象:
     document.writeln(jsonString + "  <br/>" );
     document.writeln(jsonStringCustomToJSON + "  <br/>" );
     document.writeln(jsonCensured + "  <br/>" );
     document.writeln(jsonCensured3Spaces + "  <br/>" );
     document.writeln(jsonCensuredTab + "  <br/>" );

它是如何工作的...

JSON stringify方法接受三个参数,最后两个是可选的。当只使用一个参数时,它将返回 JavaScript 对象的 JSON 字符串形式,如果对象中的某些属性未定义,则在数组中找到时将被省略或替换为 null。如果对象内定义了toJSON()函数,则将使用该函数来选择要转换的对象。这允许对象定义其自己的 JSON 表示。在我们的情况下,JSON.stringify(customToJSON)的评估版本将如下所示:

{"custom":"rendering"} 

stringify的完整函数定义如下:

JSON.stringify(value[, replacer ] [, space])

我们可以使用replacer来过滤将被列入白名单的属性。replacer可以是一个StringNumber对象的数组,它们将作为允许的参数列表。

space参数可以添加并具有StringNumber类型的值。如果是Number,它表示要用作空格的空格字符数。在我们的示例中,如果您在浏览器中打开生成的 HTML,可以看到这一点。如果space参数是String,则使用传递的值的前 10 个字符作为 JSON 的空格。

需要注意的一点是,对于非数组对象,序列化属性的顺序不能保证。在对象序列化后,您不应依赖属性的顺序。现在,由于这一点,序列化可能不是该过程的最准确定义,因此通常被称为字符串化。

还有更多...

对于不支持 JSON 的旧用户代理,我们再次遇到类似的问题。对于这种情况,我们建议使用 JSON 3:

   <script src="img/json3.min.js"></script>

此外,出于调试目的,您不应使用stringify函数,因为正如我们之前提到的,它以特定方式转换未定义的对象,因此通过这样做可能会得出错误的结论。这种错误的结论只与顺序和 JSON 兼容性有关,但对于对象的一般调试来说,它是完全有效的。

解码 base64 编码的二进制数据

直到最近,JavaScript 没有任何原生支持存储二进制数据类型的功能。大多数二进制数据都被处理为字符串。无法使用字符串处理的二进制数据(例如图像)被处理为 base64 编码的字符串。

注意

Base64 是一种将二进制数据编码的方法,它将字节组转换为 base64 数字组。其目标是通过安全地使用仅以可打印字符表示的二进制数据来避免数据丢失,这些字符不会以特殊方式解释。

HTML5 对二进制数据有更好的支持,可以使用ArrayBuffer类和类型化数组类进行存储和操作。然而,传统的库和 API 可能仍然使用 base64 数据。为了在现代浏览器中进行更有效的二进制处理,我们可能希望将这些数据转换为数组缓冲区。

在这个示例中,我们将编写一个转换函数,将 base64 编码的字符串转换为数组缓冲区。

准备工作

要编写此函数,首先我们需要了解 base64 编码的工作原理。

访问二进制数据的通常方法是一次一个字节。一个字节有 8 位。如果我们尝试为一个字节分配数字解释,它将能够表示 2 ^ 8 = 256 个不同的数字。换句话说,一个字节可以表示一个单个的基数 256 位数。

我们需要将二进制数据表示为 base64 数字。它们由字母A-Za-z0-9+/表示-共 64 个字符,足以每个字符存储 6 位数据。为此,我们将从二进制数据中取出 6 位的组。6 位和 8 位的最小公倍数是 24 位,这意味着每组 3 个字节由一组 4 个 base64 数字表示。

我们可以得出结论,解码过程将取出所有四组 base64 编码的数字,并从每组中产生 3 个字节。

但是,如果总字节数不能被 3 整除会发生什么?Base64 使用附加字符"="(等号)来表示最后一组缺少的字节数。在字符串末尾添加一个字符来指示最后一组少 1 个字节(最后一组只有两个字节);当最后一组少 2 个字节时,添加两个字符(最后一组只有一个字节)。

现在我们已经了解了 base64 的工作原理,我们准备编写一个 base64 解码器。

如何做...

让我们开始吧。

  1. 创建一个包含文本字段以输入text和两个div元素的index.html页面。其中一个元素将用于显示 base64 字符串,另一个将用于显示转换后字节的十进制值:
<!DOCTYPE HTML>
<html>
    <head>
      <title>Text to base64 and binary</title>
   </head>
   <body>
   <input type="text" id="text" value="Enter text here">
   <div id="base64"></div>
   <div id="bytes"></div>
   <script src="img/jquery.min.js"></script>
   <script type="text/javascript" src="img/atobuf.js"></script>
   <script type="text/javascript" src="img/example.js"></script>
   </body>
</html>
  1. 创建example.js并放置代码,以便在用户输入文本时应用更改:
$(function() {
    $("#text").on('keyup keypress', function() {
      var base64 = btoa($(this).val()),
      buf = atobuf(base64),
      bytes = new Uint8Array(buf),
      byteString = [].join.call(bytes, ' ');
      $("#base64").text(base64);
      $("#bytes").text(byteString);
    });
}
  1. 创建atobuf.js,导出一个函数,该函数接受一个 base64 字符串并返回一个包含解码字节的 ArrayBuffer 对象:
(function(exports) {
    var key = {};
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz 0123456789+/='
        .split('').forEach(function(c, i) {
            key[c] = i;
        });

    exports.atobuf = function atobuf(b64str) {
        var b64l = b64str.length,
            bytes = b64l / 4 * 3;
        if (b64str[b64str.length - 1] == '=') bytes -= 1;
        if (b64str[b64str.length - 2] == '=') bytes -= 1;

        var buf = new ArrayBuffer(bytes),
            arr = new Uint8Array(buf),
            at = 0;

        for (var k = 0; k < bytes; k+=3) {
            var e1 = key[b64str[at++]],
                e2 = key[b64str[at++]],
                e3 = key[b64str[at++]],
                e4 = key[b64str[at++]];

            var b1 = (e1 << 2) | (e2 >> 4),
                b2 = ((e2 & 0xF) << 4) | (e3 >> 2),
                b3 = ((e3 & 0x3) << 6) | e4;

            arr[k] = b1;
            if (k+1<bytes) arr[k+1] = b2;
            if (k+2<bytes) arr[k+2] = b3;
        }

        return buf;
    };

}(typeof(exports) !== 'undefined' ? exports : this));

它是如何工作的...

index.htmlexample.js中的代码非常简单,我们设置了一个页面,以便轻松预览和测试我们的转换函数的结果。为了存储字节,我们在传递的缓冲区上创建了一个Uint8Array对象。这是 HTML5 中引入的一种新类型的数组,它使我们能够将ArrayBuffer对象中的单个字节读取为无符号 8 位整数。

值得注意的是,Uint8Array对象没有join方法。这就是为什么我们通过编写[].join.call(bytes, ' ')从空数组中“借用”该方法,该方法调用join方法(通常属于空数组)就像它是对象 bytes 的方法一样。

atobuf.js内部,我们将atobuf函数导出为 CommonJS 模块(通过附加到exports对象)或作为附加到全局对象的函数。

为了加快转换速度,我们预先定义一个转换字典,将字符映射到其适当的数值。

它是如何工作的...

让我们看看位操作代码是如何工作的。编码值有 6 位,而解码值有 8 位。在阅读解释时,请注意位是从右到左枚举的,其中位 0 是最右边的位,位 1 是第二右边的位,依此类推。

对于第一个解码字节,我们需要将存储在第一个编码值中的 6 位定位为解码值中的位 2 到 7。这就是为什么我们将它们向左移动两个位置。我们还需要从第二个编码值中获取位 4 和 5 作为第一个解码值的位 0 和 1。这意味着我们需要将它们向右移动四个位置。

对于第二个字节,我们需要从第二个编码值中获取位 0 到 3 的位,将其定位为解码值中的位 4 到 7。为了做到这一点,我们使用二进制 AND 操作将位 4 和 5 清零,并将其余四位左移。我们还需要从第三个编码值中获取位 2 到 5 作为位 0 到 3,因此我们需要将它们向右移动两个位置。

对于第三个字节,我们需要从第三个编码值中获取位 0 到 1,位于 6 到 7 的位置,这意味着使用 AND 将其余部分清零,并将其左移六个位置。最后一个编码值的位都在第三个字节的正确位置上,因此我们将它们原样取出。

将二进制数据或文本编码为 base64

HTML5 对二进制数据的支持是通过ArrayBuffer对象,相关类型数组。当涉及到传输数据时,通常的做法是通过 base64。这主要用于处理文本数据,但随着 Data URI 的使用增加,base64 变得越来越相关。在这个示例中,我们将看到如何使用这种方案来编码数据。

如何做...

我们将创建一个 HTML 文件,其中我们将使用canvas元素,该元素将生成一些数据,然后将其编码为 base64:

  1. 为了拥有二进制数据,我们将使用 canvas 创建一个图像,因此我们添加一个canvas元素:
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Binary data to Base64</title>
  </head>
  <body>
    <canvas id="myCanvas" width="100" height="100"></canvas>
  1. 我们可以有一些输入字段,用于显示文本编码:
    <input type="text" id="text" placeholder="Insert some text">
  1. 在该元素之后,我们可以放置两个输出元素,一个用于来自图像的编码二进制数据,另一个用于编码文本:
    <div>
      <b> Text Base64:</b>
      <output id="content"></output>
    </div>
    <hr />
    <div>
      <b> Image Base64:</b>
      <output id="imgBase"></output>
    </div>
  1. 然后我们包括对 jQuery 和example.js的依赖:
    <script src="img/jquery.min.js"></script>
    <script type="text/javascript" src="img/example.js"></script>
  1. 由于我们已经包含了example.js文件,因此我们可以继续创建编码数据的逻辑。为了拥有一些二进制数据,我们将使用 canvas 创建一个正方形的图像:
  var canvas = $('#myCanvas')[0],
  context = canvas.getContext('2d');
  context.beginPath();
  context.rect(0, 0, 100, 100);
  context.fillStyle = 'green';
  context.fill();
  var imgdata = context.getImageData(0,0, 200, 200);

注意

CanvasRenderingContext2D的当前方法定义可以在 WHATWG 找到:www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#canvasrenderingcontext2d

  1. 为了创建 base64 编码的数据,我们将把数组转换为字符串,以便我们可以定义一个函数如下:
function arrayToString(inputArray){
    var stringData = '';
    var bytes = new Uint8ClampedArray(inputArray);
    var length = bytes.byteLength;
    for (var i = 0; i < length; i++) {
      stringData += String.fromCharCode(bytes[i]);
    }
    return stringData;
  }
  1. 现在我们可以调用该函数并使用内部的btoa()方法来接受将要编码的字符串:
  var stringData = arrayToString(imgdata.data);
  var b64encoded = btoa(stringData);
  1. 为了证明我们现在可以返回,我们将使用atob来解码 base64 编码的字符串数据:
  var originalStringData = atob(b64encoded);
  1. 现在,为了从解码后的字符串数据返回到原始二进制数组,我们需要定义一个函数如下:
  function stringToArray(raw){
   var rawLength = raw.length;
   var array = new Uint8ClampedArray(new ArrayBuffer(rawLength));
    for(i = 0; i < rawLength; i++) {
      array[i] = raw.charCodeAt(i);
    }
   return array;
  }
  1. 之后我们可以在我们的解码数据上调用该函数:
var originalArray = stringToArray(originalStringData);
  1. 我们将在页面上打印 base64 编码的字符串:
  $("#imgBase").text(b64encoded);
  1. 由于 base64 算法做出了某些假设,因此最初不支持 UTF。Johan Sundström 创建了一个解决方法,利用标准函数并使 UTF 成为可能:
  function utf8ToB64(str) {
    return window.btoa(unescape(encodeURIComponent(str)));
  }

  function b64ToUtf8(str) {
    return decodeURIComponent(escape(window.atob(str)));
  }
  1. 这只对我们的文本数据感兴趣,因此我们可以通过将输入字段与输出标签连接来尝试它,我们将得到一个 base64 编码的文本:
  $("#text").keyup(function(e) {
    var currentValue = $(this).val();
    $("#content").val(utf8ToB64(currentValue));
  });

它是如何工作的...

现代浏览器支持atob("base64encoded")btoa("stringToBeEncoded")。这些方法允许对 base64 字符串进行编码和解码。我们使用btoa()对字符串数据进行编码,得到的结果具有 ASCII 字符A-Z,a-z,0-9和符号(/,+,=),使数据便于传输。数据范围限制是有代价的,编码后的数据现在比原始二进制流大约 33%。另一方面,编码后的数据通常更易压缩,因此 gzip 将使大小更或多或少相等。

注意

JavaScript 类型化数组提供了一种比使用标准类型更高效地访问原始二进制数据的方式。它们受到所有现代浏览器和 IE 10 的支持。有关类型化数组的更多信息可以在 MDN 上找到:developer.mozilla.org/en-US/docs/JavaScript/Typed_arrays

为了测试二进制数据编码,我们使用了从 HTML 画布生成的数组。为了在那里检索二进制数组表示,我们使用了以下语句:

context.getImageData(0,0, 200, 200);

这返回一个包含属性宽度、高度和数据的ImageData对象。数据属性表示为一个Uint8ClampedArray对象。这种类型的数组类似于标准的Array对象,其中每个项目都是一个 8 位(1 字节)无符号整数。存储在此数组中的所有值都在 0 到 255 的范围内,非常适合颜色。在我们的情况下,我们可以通过使用console.log()记录数组中存储的值,并获得以下值:

 [0,128,0,255,0,128,0,255 …]

0 代表红色,128 代表绿色,第三个 0 代表蓝色,另一方面 255 代表不透明度级别。由于我们想要将数组的数据编码为 base64,我们不能简单地调用btoa(theArray),因为我们只存储toString值而不是整个数组:

[Object Uint8ClampedArray]

注意

如果我们比较Uint8arrayUint8ClampedArray,主要区别在于前者在插入超出范围的值时使用模数缩短,而后者则将值夹紧。例如,如果我们将值 300 设置为 255 限制,对于Uint8ClampedArray将变为 255,而对于另一个将变为 45。同样,值-1 将被夹紧为 0。有关Uint8ClampedArray的更多信息可以在以下找到:

www.khronos.org/registry/typedarray/specs/latest/#7.1

我们添加arrayToString的主要原因是为我们创建一个字符串,以便我们以后可以在btoa中使用。同样,我们需要stringToArray来恢复转换。

当涉及到文本时,核心函数btoa()/atob()不支持 Unicode。如果我们尝试转换值大于"\u0100"的字符,我们将得到:

Error: InvalidCharacterError: DOM Exception 5

作为对此的修复,我们添加了utf8ToB64()b64ToUtf8()方法。

注意

这两个都是 Johan Sundström 的聪明技巧,是 MDN 推荐的修复方法。更多信息可以从ecmanaut.blogspot.com/2006/07/encoding-decoding-utf8-in-javascript.html获取。

这个技巧利用了标准函数对encodeURIComponent()/decodeURIComponent()escape()/unescape()

encodeURICompoenentunescape的组合是如何工作的?

这是这种方法的一个示例:

 > encodeURICompoenent(" ");
 "%20"

我们得到的结果是一个百分比编码的字符串,其中 UTF-8 字符被替换为适当的百分比表示。现在我们可以只使用encodeURIComponent,因为百分比编码只使用 ASCII 字符:

> "\u2197"
"↗"
> encodeURIComponent("\u2197")
"%E2%86%97"
> btoa(encodeURIComponent('\u2197'));
"JUUyJTg2JTk3"

但是这种方法的一个缺点是,结果的百分比编码字符串比初始字符串大得多,而由于 base64 增加了额外的开销,它很容易变得非常庞大。

escapeunescape函数已经被弃用,因为它们对非 ASCII 字符不起作用,但在我们的情况下,输入是有效的,因此它们可以被使用。至于未来版本,它们不是标准的一部分,但它们可能会保留下来。unescape函数返回指定十六进制编码值的 ASCII 字符串。使用这个的好处是现在我们有一个更小的字符串表示。这个黑客的另一个好处是它使用了浏览器可用的多个编码函数来扩展标准功能。

还有更多...

在用户代理支持方面,IE 是目前唯一尚未包含btoa()atob()的浏览器,但这仅适用于早于 IE 10 的版本。为了在不受支持的用户代理上启用它,我们可以使用一个 polyfill。有几种不同的 polyfill,但我们可以使用一个叫做base64.js的。

有一个有趣的异步资源加载器叫做yenope.js,它非常快速,并允许自定义检查。如果我们想要包含base64.js,我们可以测试所需函数的存在,如果不存在,它将自动包含它。

yepnope({
    test: window.btoa && window.atob,
    nope: 'base64.js',
    callback: function () {
       //safe to use window.btoa and window.atob
    }
});

注意

Yepnope 是许多条件资源加载器之一,但它是其中一个简单的。yepnope函数是整个加载器的核心。因此它非常小并且集成在 Modernizer 中;更多信息可以在yepnopejs.com/找到。

将二进制数据序列化为 JSON

在使用 REST API 时,如果需要将二进制数据包含在 JSON 中,最简单的方法之一是使用 base64。图像和类似的资源很可能应该存在为单独的资源,但它们也可以作为 JSON 文档的一部分。在这个配方中,我们将介绍在 JSON 文档中包含图像的一个简单例子。

如何做到...

我们将从canvas元素生成一些二进制数据,并将其序列化为 JSON:

  1. 我们首先创建一个 HTML 文件,在其中放置一个简单的canvas,一个用于输出的div元素,并包括 jQuery,然后创建脚本:
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Binary data to json</title>
    <style type="text/css">
      div {
        word-wrap: break-word;
      }
    </style>
  </head>
  <body>
    <canvas id="myCanvas" width="75" height="75"></canvas>
    <hr />
    <div>
      <output id="generatedJson"> </output>
    </div>
    <script src="img/jquery.min.js"></script>
    <script type="text/javascript" src="img/example.js"></script>
  </body>
</html>
  1. example.js脚本中,我们可以在canvas元素上创建一个简单的圆:
  var canvas = $('#myCanvas')[0],
      context = canvas.getContext('2d');
  context.beginPath();
  context.arc(50, 50, 20, 0, Math.PI*2, true);
  context.closePath();
  context.fillStyle = 'green';
  context.fill();
 var imgdata = context.getImageData(0,0, 50, 50);
  1. 然后我们定义了与将二进制数据或文本编码为 base64部分中使用的相同的arrayToString函数:
  function arrayToString(inputArray){
    var stringData = '',
    len = inputArray.byteLength;
    for (var i = 0; i < len; i++) {
      stringData += String.fromCharCode(inputArray[i]);
    }
    return stringData;
  }
  1. 然后我们对数据进行编码,并创建一个 JavaScript 对象,同时创建两个数据 URI 形式的canvas元素,一个是jpeg,另一个是png
  var imageEncoded = btoa(arrayToString(imgdata.data));
 var jsObject = {
    "name":"pie chart or not a pie...chart",
    "dataURL" : {
      "jpeg": canvas.toDataURL('image/jpeg'),
      "png": canvas.toDataURL('image/png')
    },
    "image" : imageEncoded
  };
  1. 为了创建 JSON 对象,我们可以使用JSON.stringify,然后将结果打印到generatedJson div 中:
 var jsonString = JSON.stringify(jsObject, null , 2);
  $("#generatedJson").text(jsonString);

它是如何工作的...

代码与上一个配方非常相似,我们使用 2D 上下文为 canvas 创建了一个简单的圆:

  context.beginPath();
  context.arc(50, 50, 20, 0, Math.PI*2, true);
  context.closePath();

然后我们从图像中获取了二进制数据,并应用了与将二进制数据或文本编码为 base64配方中相同的逻辑。一个特定的特性是使用 Data URI,它简单地在指定的格式中创建图像的 base64 编码渲染。在我们的例子中,我们创建了jpegpng的渲染。如果你复制出数据包含在

  "dataURL" : {
      "jpeg": "copy data rendered here",
      "png":"or copy data from here"
    }

并将其粘贴到浏览器的 URL 选择中,它将呈现图像。数据 URI 将在标题为数据存储的章节中详细讨论。

还有更多...

base64 编码可以与 XML 一起使用来存储更复杂或二进制数据。由于编码的字符基不会干扰解析,因此不需要 CDATA 部分。

与服务器交换二进制数据的其他格式有很多,例如 BSON、Base32 或 Hessian。Base64 是最常用的,因为它非常简单且易于集成。

base64 的一个很好的用途是将文本存储到 URL 参数中,使文本易于表示和重建,你可以在hashify.me上看到。

序列化和反序列化 cookies

尽管 HTML5 取得了很多进展,但浏览器仍然具有非常奇怪的 Cookie API。它的工作方式容易出错,并且与 JavaScript 的正常语义不一致。

全局的document对象有一个cookie属性,如果给它赋一个字符串,它会神奇地将指定的 Cookie 添加到 Cookie 列表中。当尝试读取 Cookie 时,会返回一个包含所有 Cookie 的不同值。

这个 API 如果没有包装器就不太有用。在这个教程中,我们将用一个实际有意义的包装器来包装这个 API。我们将通过制作一个表单页面来测试这个包装器,该页面在每次修改后保存自身(在页面重新加载后保留数据)两分钟。

准备工作

让我们找出document.cookie的工作原理。我们可以设置 Cookie 如下:

document.cookie = "name=test; expires=Fri, 18 Jan 2023 00:00:00 GMT; path=/";

这为当前网站的整个域设置了一个名为 test 的 Cookie,到 2023 年 1 月 18 日过期。现在,如果我们尝试从document.cookie读取,我们将得到"name=test",这意味着所有额外的数据已被剥离。如果我们继续添加另一个 Cookie:

document.cookie = "otherName=test; expires=Fri, 18 Jan 2023 00:00:00 GMT; path=/"

然后尝试访问document.cookie,我们会得到两个 Cookie:

"name=test; otherName=test"

要实际清除 Cookie,我们需要按以下方式设置expires日期和路径:

document.cookie = "otherName=test; expires=Fri, 18 Jan 2000 00:00:00 GMT; path=/"

然后我们回到包含"name=test"document.cookie

最后,如果我们省略expires日期,我们将得到一个持续到用户关闭浏览器或我们通过将其过期日期设置为过去来清除它的 Cookie:

document.cookie = "otherName=test; path=/"

但是,如果值包含字符;会发生什么?Cookie 值将在此字符处被截断,并且下一个参数(过期日期或路径)将被忽略。幸运的是,我们可以通过使用encodeURIComponent来编码值来解决这个问题。

现在我们有足够的信息来编写我们的 Cookie 处理库。

如何做...

让我们写代码:

  1. index.html中创建表单页面,其中包含三个文本字段,并包括我们的 Cookie 包装器脚本和表单保存脚本:
<!DOCTYPE HTML>
<html>
<head>
<title>Cookie serialization</title>
</head>
<body>
<form method="post">
    <input type="text" name="text1" value="Form data will be saved"><br>
    <input type="text" name="text2" value="in the cookie formdata"><br>
    <input type="text" name="text3" value="and restored after reload">
</form>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/cookie.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建cookie.js,实现并导出 Cookie API。它将具有以下功能:
  • cookie.set(name, value, options):此函数设置 Cookie 的值。该值可以是任意对象,只要它可以被JSON.stringify序列化。可用的选项包括expiresdurationpath
(function(exports) {

    var cookie = {};

    cookie.set = function set(name, val, opt) {
      opt = opt || {};
      var encodedVal = encodeURIComponent(JSON.stringify(val)),
      expires = opt.expires  ? opt.expires.toUTCString()
      : opt.duration ? new Date(Date.now()
                                     + opt.duration * 1000).toUTCString()
      : null;

      var cook = name +'=' + encodedVal + ';';
      if (expires) cook += 'expires=' + expires;
      if (opt.path) cook += 'path=' + opt.path;
      document.cookie = cook;
    };

    cookie.del = function(name) {
      document.cookie = name + '=deleted; expires='
      + new Date(Date.now() - 1).toUTCString();
    }
    cookie.get = function get(name) {
      var cookies = {};
      var all = document.cookie.split(';').forEach(function(cs) {
      var c = cs.split('=');
      if (c[1])
      cookies[c[0]] =
      JSON.parse(decodeURIComponent(c[1]));
      });
    if (name)
      return cookies[name]            
      else
      return cookies
    };

    exports.cookie = cookie;
}(typeof(exports) !== 'undefined' ? exports : this));
  1. 创建example.js来测试新的 Cookie API。它在文档加载时加载表单数据,并在更改时保存它:
$(function() {
    var savedform = cookie.get('formdata');
    savedform && savedform.forEach(function(nv) {
      $('form')
      .find('[name="'+nv.name+'"]')
      .val(nv.value);
    });
    $('form input').on('change keyup', function() {
      cookie.set('formdata', $('form').serializeArray(),
      {duration: 120});
    });
});

工作原理...

我们的 API 实现了几个方便处理 Cookie 的函数。

cookie.set函数允许我们设置 Cookie。它接受三个参数:名称、值和选项。

该值使用JSON.stringify进行序列化,然后使用encodeURIComponent进行编码。因此,我们可以存储任何可以使用JSON.stringify进行序列化的对象(但是不同浏览器之间存在大小限制)。

选项参数是一个对象,可以包含三个属性:expires、duration 和 path。expires属性是 Cookie 应该过期的日期。或者,可以提供duration,它是 Cookie 应该持续的秒数。如果这两者都被省略,Cookie 将持续到当前浏览器会话结束。最后,path属性是一个指定 Cookie 可用路径的字符串。默认是当前路径。

还有更多...

不应该使用 Cookie 来存储大量数据。大多数浏览器将每个 Cookie 的大小限制在 4KB。有些浏览器将所有 Cookie 的总大小限制在 4KB。存储在 Cookie 中的数据会随着每次向服务器发出的请求而传输,增加带宽的总使用量。

对于更大的数据,我们可以使用本地存储。更多信息可以在第十章中找到,数据绑定框架

请注意,此示例在本地文件系统上打开时不起作用。要使其起作用,必须运行本地 HTTP 服务器。有关如何运行简单 HTTP 服务器的更多信息,请参见附录。

将表单序列化为请求字符串

在处理表单时的一个常见任务是创建实际的请求字符串。有几种不同的方法可以做到这一点,首先想到的是只选择每个单独的表单元素并获取它的值,然后通过附加名称属性和值来创建字符串。这种方法非常容易出错,因此我们将看一下使用jQuery.serialize()的更好的解决方案。

如何做...

像往常一样,我们可以从 HTML 开始:

  1. 首先我们添加基本的head部分和一个输出元素,显示生成的请求字符串:
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>JavaScript objects to form data</title>
  </head>
  <body>
    <label><b>Generated string:</b></label>
    <output id="generated">none</output>
    <hr/>
    <output id="generatedJson">none</output>
    <hr/>
  1. 然后我们可以继续创建一个简单的表单,其中包括全名、电子邮件和令人惊叹的百分比的输入:
  <form id="theForm">
    <label>Full name</label>
    <input type="text" id="fullName" name="fullName" placeholder="Some Name">
    <label>Email address </label>
    <input type="email" id="email" name="email" placeholder="example@example.com">
    <label>Percent of awesomeness </label>
    <input type="number" id="awesomeness" name="awesomeness" value="50" min="1" max="100">
    <br/>
    <input type="submit">
  </form>
  1. 接下来,我们可以包含所需的 JavaScript 依赖项到 jQuery 和我们的example.js脚本中:
 <script src="img/jquery.min.js"></script>
    <script src="img/example.js"></script>
  1. 然后我们可以继续创建example.js文件,在表单元素的每次更新时对表单进行序列化:
$(function() {
$("#theForm").keyup(function(){
    var theForm = $("#theForm"),
    parameterArray = theForm.serializeArray();
    $("#generated").text(theForm.serialize());
    $("#generatedJson").text(JSON.stringify(parameterArray));
   });
});

它是如何工作的...

.serialize()函数将表单元素及其值转换为百分比编码的字符串。百分比编码通常称为 URL 编码,是一种以 URI 友好的方式表示信息的方法。因此,它是大多数使用的表单的核心部分,它具有application/x-www-form-urlencoded的 MIME 类型。

如果表单中有一个按钮,它不会被视为生成的字符串的一部分,因为该按钮未被点击以提交表单。此外,仅当复选框和单选按钮被选中时,它们才是生成的字符串的一部分。

另一方面,如果我们需要一些 JSON 表示,那么我们可以使用.serializeArray(),这个函数将创建一个 JavaScript 数组。在获得这个元素数组之后,我们可以使用JSON.stringify()创建一个 JSON。默认的 JSON 表示在大多数情况下可能不是很有用,但我们可以很容易地重组和过滤这些元素。

.serializeArray().serialize()函数只保存 W3C 定义的“成功控件”(www.w3.org/TR/html401/interact/forms.html#h-17.13.2),在这里,您将获得与通过按钮点击正常提交表单相同的行为。

还有更多...

首先要注意的是,使用这两种方法不会序列化文件选择元素中的数据。为此和其他类似用例,FormData对象在XMLHttpRequest Level 2中被引入。该对象允许创建一组键/值对,以便使用XMLHttpRequest发送。使用此方法创建的数据以与标准提交相同的方式发送,其中编码设置为"multipart/form-data"

我们在示例中看到,即使使用.serializeArray(),从表单元素创建 JSON 对象也可能变得混乱。为了简化事情并使更复杂的 JSON 直接从元素创建,创建了form2jsgithub.com/maxatwork/form2js。一个简单的示例是创建一个简单的人对象:

{
    "person" :
    {
        "contact" :
        {
            "email" : "test@example.com",
            "phone" : "0123456789"
        }
    }
}

为了实现这一点,我们只需在name属性中创建定义,脚本会处理其他所有事情:

<input type="email" name="person.contact.email" value="test@example.com" />
<input type="text" name="person.contact.phone" value="0123456789" />

此库有标准的 JavaScript 版本和 jQuery 插件。它还具有其他功能,例如具有对象数组或自定义字段处理程序。

使用 DOMParser 读取 XML 文档

虽然XMLHttpRequest允许我们下载和解析 XML 文档,但有时我们可能希望手动解析 XML 数据文档。例如,手动解析可以使我们在script标签中包含任意 XML 数据(例如,基于 XML 的模板)在页面中。这可以帮助减少发送到浏览器的请求数量。

在这个示例中,我们将从textarea输入中读取一个简单的 XML 文档,并使用DOMParser解析它,然后将结果显示为树。

如何做...

让我们编写测试 HTML 页面和解析器:

  1. 创建index.html,它应包含一个textarea元素用于输入 XML(包含一个示例 XML 文档),一个文档body对象的占位符,以及一些文档树的 CSS 样式:
<!DOCTYPE HTML>
<html>
<head>
<title>Deserializing XML with DOMParser</title>
<style type="text/css">
div.children { padding-left: 3em; }
h3 { padding:0; margin:0; }
.children .text { padding-top: 0.5em; }
.attribute .name { padding-left: 1.5em; width:5em;
    display:inline-block; font-weight:bold; }
.attribute .value { padding-left: 1em; font-style:oblique; }
</style>
</head>
<body>
<textarea rows="11" cols="60">
&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; ?&gt;
&lt;root&gt;
    Text in document
    &lt;element attribute=&quot;value&quot; foo=&quot;bar&quot; /&gt;
    &lt;bold weight=&quot;strong&quot;&gt;Text in element&lt;/bold&gt;
    &lt;list&gt;
      &lt;item&gt;item text 1&lt;/item&gt;
      &lt;item&gt;item text 2&lt;/item&gt;
    &lt;/list&gt;        
&lt;/root&gt;
</textarea>
<div id="tree">
</div>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js,并添加解析文档并将其转换为 HTML 树的代码:
$(function() {
  function parseDocument(text) {
    function displayElement(e) {
      var holder = $("<div />").addClass('element');
      $("<h3 />").text(e.nodeName).appendTo(holder);
      if (e.attributes && e.attributes.length) {
        var attrs = $("<div />").addClass('attributes')
        .appendTo(holder);
        for (var a = 0; a < e.attributes.length; ++a) {
          var nameval = e.attributes[a];
          var attr = $("<div />").addClass('attribute')
          .appendTo(attrs);
          $('<span />').addClass('name')
          .text(nameval.name).appendTo(attr);
          $('<span />').addClass('value')
                        .text(nameval.value).appendTo(attr);
          }
        }
        if (e.childNodes.length) {
          var children = $("<div />").appendTo(holder)
          .addClass('children');
          for (var c = 0; c < e.childNodes.length; ++c) {
            var child = e.childNodes[c];
            if (child.nodeType == Node.ELEMENT_NODE)
                        displayElement(child).appendTo(children);
                    else if (child.nodeType == Node.TEXT_NODE
                          || chilc.nodeType == Node.CDATA_SECTION_NODE)
                        $("<div />").addClass('text')
                            .text(child.textContent)
                            .appendTo(children);
                }
            }
            return holder;
        }
        var parser = new DOMParser();
        var doc = parser.parseFromString(text, 'application/xml');
        window.doc = doc;
        return displayElement(doc.childNodes[0]);
    }
    function update() {
      $('#tree').html('')
        parseDocument($('textarea').val()).appendTo('#tree');
    }
    update();
    $('textarea').on('keyup change', update);
});

它是如何工作的...

要解析 XML 文档,我们创建一个新的DOMParser对象并调用parseFromString方法。我们将文档类型指定为 application/xml - 解析器还可以解析text/html并返回HTMLDocument元素,或解析image/svg+xml并返回SVGDocument元素。

生成的文档具有与window.document中找到的非常相似的 API(相同的 DOM API 可用)。我们创建一个递归函数,迭代根元素的所有子元素并生成 HTML。它为元素名称构造标题,为属性名称和值创建 span 元素,为文本节点创建 div 元素,并调用自身生成 HTML 以显示元素节点。结果是一个 DOM 树:

它是如何工作的...

在客户端对 XML 文档进行序列化

JSON 比 JavaScript 更简单易用;已经有很多 REST 服务使用 XML。在这个示例中,我们将创建一个简单的表单,使用 DOM API for XML 构建 XML 文档。

如何做...

让我们开始:

  1. 首先我们创建一个简单的 HTML 文档:
<!doctype html>
<html>
  <head>
    <meta charset="utf-8">
    <title>Create XML from JavaScript objects</title>
  </head>
  <body>
    <output id="log"> </output>
  1. 完成后,我们将在文本中包含一个 KML 文档,在实际应用中,这可能会通过 AJAX 加载,但为简单起见,我们将直接添加数据:
  <kml id="test" >
    <Document>
      <name>Red Pyramid</name>
      <description><![CDATA[]]></description>
      <Style id="style1">
        <IconStyle>
          <Icon>
            <href></href>
          </Icon>
        </IconStyle>
      </Style>
      <Placemark>
        <name>Red Pyramid</name>
        <styleUrl>#style1</styleUrl>
        <Point>
          <coordinates>31.206320,29.808853,0.000000</coordinates>
        </Point>
      </Placemark>
    </Document>
  </kml>

注意

您可能已经注意到在这个示例中使用了KMLKeyhole Markup Language)。最初这是由 Google 收购的一家公司开发的格式,但现在它是一个国际开放标准。这种格式广泛用于描述地标和位置。更多信息可以在以下网址找到:

developers.google.com/kml/documentation/

  1. 在这个块之后,我们只需包含 JavaScript example.js
    <script src="img/example.js"></script>
  </body>
</html>
  1. 我们将从头开始创建一个简单的 XML 文档,并将其序列化为字符串。该代码将简单地检索 HTML 文档的一个部分,其中包含 KML 数据,将其序列化为字符串,然后在文本区域中显示数据:
;(function() {
 var doc = document.implementation.createDocument("","root", null),
       node = doc.createElement("someNode");
  doc.documentElement.appendChild(node);

  document.getElementById('first')
  .appendChild(
    document.createTextNode(
      new XMLSerializer()
    .serializeToString(doc))
  );

  var kml = document.getElementById('test');
  document.getElementById('second')
  .appendChild(
    document.createTextNode(
      new XMLSerializer()
      .serializeToString(kml))
  );
}());

它是如何工作的...

这个示例的核心是XMLSerializer()方法,它可以用于将 DOM 子树或整个文档转换为文本。这个对象受大多数现代浏览器和 IE 9+的支持,但对于旧版本,您需要使用类似以下的一些回退:

function xmlStringify(someXML) {
  if (typeof XMLSerializer !== 'undefined') {
    return (new XMLSerializer()).serializeToString(someXML);
  }
  // fallback for IE
  if (someXML.xml) {
    return someXML.xml;
  }
  //Not supported
}

标准的 DOM 操作可以用于创建 XML 文档。对于 jQuery 也是一样,我们可以直接使用其功能来创建文档。只有当我们需要处理更大的文档或对 XML 进行大量工作时,情况才会变得更加复杂。如今大多数 REST 服务都有某种内容协商,所以通常 JSON 是一种可用且更好的选择。

还有更多...

还有一个名为 JXON(developer.mozilla.org/en-US/docs/JXON)的功能,它代表 JavaScript XML 对象表示法,是与 JavaScript 中 XML 文档的创建和使用相关的 API 的通用名称。这基本上定义了 JSON 和 XML 之间的双向转换的约定。

在与 XML 密切相关的工作中,XPath 可能是您最好的朋友。它可以非常灵活地访问与特定模式匹配的文档的特定部分。

注意

XPath(XML 路径语言)是一种用于选择 XML 文档中节点的查询语言。与 SQL 类似,它提供了某些计算函数。在 MDN 上有详尽的文档,网址为developer.mozilla.org/en-US/docs/XPath,同时也有规范文档,网址为www.w3.org/TR/xpath20/

第八章:与服务器通信

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

  • 创建一个 HTTP GET 请求来获取 JSON

  • 创建带有自定义头部的请求

  • 为你的 API 进行版本控制

  • 使用 JSONP 获取 JSON 数据

  • 从服务器读取 XML 数据

  • 使用 FormData 接口

  • 将二进制文件发布到服务器

  • 使用 Node.js 创建 SSL 连接

  • 使用 Ajax Push 进行实时更新

  • 使用 WebSockets 交换实时消息

创建一个 HTTP GET 请求来获取 JSON

从服务器检索信息的基本方法之一是使用 HTTP GET。在 RESTful 方式中,这种方法应该仅用于读取数据。因此,GET 调用不应该改变服务器状态。现在,这对于每种可能的情况可能并不正确,例如,如果我们在某个资源上有一个视图计数器,那么这是一个真正的改变吗?如果我们严格遵循定义,那么是的,这是一个改变,但这远非重要到足以被考虑。

在浏览器中打开一个网页会发出一个 GET 请求,但通常我们希望以一种脚本化的方式来检索数据。这通常是为了实现异步 JavaScript 和 XMLAJAX),允许重新加载数据而不进行完整的页面重新加载。尽管名称中包含 XML,但并不是必需的,如今,JSON 是首选的格式。

JavaScript 和XMLHttpRequest对象的组合提供了一种异步交换数据的方法,在这个示例中,我们将看到如何使用纯 JavaScript 和 jQuery 从服务器读取 JSON。为什么使用纯 JavaScript 而不直接使用 jQuery?我们坚信 jQuery 简化了 DOM API,但它并不总是可用,此外,我们需要了解异步数据传输背后的基础代码,以充分理解应用程序的工作原理。

准备工作

服务器将使用 Node.js 实现。请参考附录 A 中关于如何在您的计算机上安装 Node.js 以及如何使用 npm 的内容。在这个示例中,为了简单起见,我们将使用restifymcavage.github.io/node-restify/),这是一个用于创建正确的 REST web 服务的 Node.js 模块。

如何做到...

让我们执行以下步骤。

  1. 为了在服务器端脚本的根目录中包含restify到我们的项目中,使用以下命令:
npm install restify

  1. 添加依赖项后,我们可以继续创建服务器代码。我们创建一个server.js文件,它将由 Node.js 运行,在其开头我们添加restify
var restify = require('restify');
  1. 有了这个restify对象,我们现在可以创建一个服务器对象,并为get方法添加处理程序:
var server = restify.createServer();
server.get('hi', respond);
server.get('hi/:index', respond);
  1. get处理程序回调到一个名为respond的函数,因此我们现在可以定义这个函数,它将返回 JSON 数据。我们将创建一个名为hello的示例 JavaScript 对象,并且如果函数被调用时具有请求的参数索引部分,则在"hi/:index"处理程序中调用它:
 function respond(req, res, next) {
  console.log("Got HTTP " + req.method + " on " + req.url + " responding");
  var hello = [{
    'id':'0',
    'hello': 'world'
  },{
    'id':'1',
    'say':'what'
  }];
  if(req.params.index){
    var found = hello[req.params.index];
    if(found){
      res.send(found);
    } else {
      res.status(404);
      res.send();
    }
  };
  res.send(hello);
  addHeaders(req,res);
  return next();
}
  1. 我们在开始时调用的addHeaders函数是为了添加头部,以便访问来自不同域或不同服务器端口的资源:
function addHeaders(req, res) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
 };
  1. 头部的定义和它们的含义将在本章后面讨论。现在,让我们说它们使得浏览器可以使用 AJAX 访问资源。最后,我们添加一段代码,将服务器设置为监听 8080 端口:
server.listen(8080, function() {
  console.log('%s listening at %s', server.name, server.url);
});
  1. 要使用命令行启动服务器,我们输入以下命令:
node server.js

  1. 如果一切顺利,我们将在日志中收到一条消息:
restify listening at http://0.0.0.0:8080
  1. 然后我们可以通过直接从浏览器访问我们定义的 URL http://localhost:8080/hi来测试它,或者使用附录 A 中讨论的一些工具来查看通信,安装 Node.js 和使用 npm

现在我们可以继续进行客户端 HTML 和 JavaScript。我们将实现两种从服务器读取数据的方式,一种使用标准的XMLHttpRequest,另一种使用jQuery.get()。请注意,并非所有功能都与所有浏览器完全兼容。

  1. 我们创建了一个简单的页面,其中有两个div元素,一个带有 IDdata,另一个带有 IDsay。这些元素将用作从服务器加载数据的占位符:
    Hello <div id="data">loading</div>
    <hr/>
    Say <div id="say">No</div>s
    <script src="img/jquery.min.js"></script>
    <script src="img/example.js"></script>
    <script src="img/exampleJQuery.js"></script>
  1. example.js文件中,我们定义了一个名为getData的函数,它将创建一个 AJAX 调用到给定的url,并在请求成功时进行回调:
  function getData(url, onSuccess) {
    var request = new XMLHttpRequest();
    request.open("GET", url);
    request.onload = function() {
      if (request.status === 200) {
        console.log(request);
        onSuccess(request.response);
      }
    };
    request.send(null);
  }
  1. 之后,我们可以直接调用该函数,但为了演示调用发生在页面加载后,我们将在三秒后调用它:
 setTimeout(
    function() {
      getData(
        'http://localhost:8080/hi',
        function(response){
          console.log('finished getting data');
          var div = document.getElementById('data');
          var data = JSON.parse(response);
          div.innerHTML = data[0].hello;
        })
    },
    3000);
  1. jQuery 版本更加简洁,因为标准 DOM API 和事件处理带来的复杂性大大减少:
    (function(){
    $.getJSON('http://localhost:8080/hi/1', function(data) {
      $('#say').text(data.say);
 });
}())

工作原理...

一开始,我们使用npm install restify安装了依赖项;这足以使其工作,但为了更加明确地定义依赖关系,npm 有一种指定的方法。我们可以添加一个名为package.json的文件,这是一个主要用于发布 Node.js 应用程序的打包格式。在我们的情况下,我们可以使用以下代码定义package.json

{
  "name" : "ch8-tip1-http-get-example",
  "description" : "example on http get",
  "dependencies" : ["restify"],
  "author" : "Mite Mitreski",
  "main" : "html5dasc",
  "version" : "0.0.1"
}

如果我们有一个像这样的文件,npm 将在调用npm install时自动处理依赖项的安装,该命令是在放置package.json文件的目录中从命令行中调用的。

Restify有一个简单的路由,其中函数被映射到给定 URL 的适当方法。'/hi'的 HTTP GET 请求与server.get('hi', theCallback)映射,其中执行theCallback,并应返回一个响应。

当我们有一个参数化的资源时,例如在'hi/:index'中,与:index相关联的值将在req.params下可用。例如,在对'/hi/john'的请求中,要访问john值,我们只需使用req.params.index。此外,index 的值在传递给我们的处理程序之前将自动进行 URL 解码。在restify中请求处理程序的另一个值得注意的部分是我们在最后调用的next()函数。在我们的情况下,这大多数情况下并没有太多意义,但一般来说,如果我们希望调用链中的下一个处理程序函数被调用,我们负责调用它。在特殊情况下,还有一种使用error对象触发自定义响应的方法来调用next()

在客户端代码方面,XMLHttpRequest是异步调用背后的机制,当调用request.open("GET", url, true)并将最后一个参数值设置为true时,我们获得了真正的异步执行。现在你可能会想为什么这个参数在这里,难道不是在加载页面后已经完成了调用吗?这是真的,调用是在加载页面后完成的,但是,例如,如果参数设置为false,请求的执行将是一个阻塞方法,或者用通俗的话来说,脚本将暂停,直到我们得到一个响应。这可能看起来是一个小细节,但它对性能有很大的影响。

jQuery 部分非常简单;有一个函数接受资源的 URL 值,数据处理函数,以及一个success函数,在成功获取响应后调用:

jQuery.getJSON( url [, data ] [, success(data, textStatus, jqXHR) ] )

当我们打开index.htm时,服务器应该记录类似以下的内容:

Got HTTP GET on /hi/1 responding
Got HTTP GET on /hi responding

这里一个来自 jQuery 请求,另一个来自纯 JavaScript。

还有更多...

XMLHttpRequest Level 2是添加到浏览器中的新改进之一,尽管它不是 HTML5 的一部分,但仍然是一个重大变化。Level 2 的变化中有几个功能,主要是为了使其能够处理文件和数据流,但也有一个我们已经使用的简化。以前,我们必须使用onreadystatechange并遍历所有状态,如果readyState4,即等于DONE,我们才能读取数据:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'someurl', true);
xhr.onreadystatechange = function(e) {
  if (this.readyState == 4 && this.status == 200) {
   // response is loaded
  }
}

然而,在 Level 2 请求中,我们可以直接使用request.onload = function() {}而不必检查状态。表中可以看到可能的状态:

状态名称 数值 描述
UNSENT 0 对象已创建
OPENED 1 调用了open方法
HEADERS_RECEIVED 2 已经跟踪了所有重定向,并且最终对象的所有标头现在都可用
LOADING 3 响应正在被恢复
DONE 4 已接收数据或在传输过程中出现问题,例如无限重定向

还有一件事需要注意的是,XMLHttpRequest Level 2 在所有主要浏览器和 IE 10 中都受支持;旧版XMLHttpRequest在较旧版本的 IE(早于 IE 7)中实例化的方式不同,我们可以通过新的ActiveXObject("Msxml2.XMLHTTP.6.0")通过 ActiveX 对象访问它。

使用自定义标头创建请求

HTTP 头是发送到服务器的request对象的一部分。其中许多提供有关客户端用户代理设置和配置的信息,因为这有时是制作从服务器获取的资源的描述的基础。其中一些,如EtagExpiresIf-Modified-Since与缓存密切相关,而其他一些,如DNT代表“不要跟踪”(www.w3.org/2011/tracking-protection/drafts/tracking-dnt.html)可能是相当有争议的。在这个示例中,我们将看一种在服务器和客户端代码中使用自定义X-Myapp头的方法。

准备工作

服务器将使用 Node.js 实现,因此您可以参考附录 A,安装 Node.js 和使用 npm,了解如何在您的计算机上安装 Node.js 以及如何使用 npm。在这个例子中,为了简单起见,我们将使用 restify (mcavage.github.io/node-restify/)。此外,在浏览器和服务器中监视控制台对于理解后台发生的事情至关重要。

如何做...

  1. 我们可以从package.json文件中定义服务器端的依赖项开始:
{
  "name" : "ch8-tip2-custom-headers",
  "dependencies" : ["restify"],
  "main" : "html5dasc",
  "version" : "0.0.1"
}
  1. 之后,我们可以从命令行调用npm install,这将自动检索restify并将其放置在项目根目录中创建的node_modules文件夹中。在这部分之后,我们可以继续在server.js文件中创建服务器端代码,在那里我们将服务器设置为侦听端口 8080,并为'hi'和其他路径添加一个路由处理程序,当请求方法为HTTP OPTIONS时:
var restify = require('restify');
var server = restify.createServer();
server.get('hi', addHeaders, respond);
server.opts(/\.*/, addHeaders, function (req, res, next) {
  console.log("Got HTTP " + req.method + " on " + req.url + " with headers\n");
 res.send(200);
  return next();
});
server.listen(8080, function() {
  console.log('%s listening at %s', server.name, server.url);
});

注意

在大多数情况下,当我们将应用程序的构建写入 Restify 时,文档应该足够了,但有时,查看源代码也是一个好主意。它可以在github.com/mcavage/node-restify/上找到。

  1. 值得注意的一件事是,我们可以有多个链接的处理程序;在这种情况下,我们在其他处理程序之前有addHeaders。为了使每个处理程序都能传播,应该调用next()
function addHeaders(req, res, next) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, X-Myapp');
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  res.setHeader('Access-Control-Expose-Headers', 'X-Myapp, X-Requested-With');
  return next();
};

addHeaders添加了访问控制选项,以启用跨源资源共享。跨源资源共享CORS)定义了浏览器和服务器可以相互交互以确定是否应该允许请求的方式。它比允许所有跨源请求更安全,但比简单地允许所有跨源请求更强大。

  1. 之后,我们可以创建处理程序函数,该函数将返回服务器接收到的标题和一个 hello world 类型的 JSON 响应:
   function respond(req, res, next) {
  console.log("Got HTTP " + req.method + " on " + req.url + " with headers\n");
  console.log("Request: ", req.headers);
  var hello = [{
    'id':'0',
    'hello': 'world',
    'headers': req.headers
  }];
  res.send(hello);
  console.log('Response:\n ', res.headers());
  return next();
}

此外,我们还将请求和响应头记录到服务器控制台日志中,以便查看后台发生了什么。

  1. 对于客户端代码,我们需要一个简单的"纯净"JavaScript 方法和 jQuery 方法,因此为了做到这一点,包括example.jsexampleJquery.js以及一些div元素,我们将用它们来显示从服务器检索到的数据:
     Hi <div id="data">loading</div>
    <hr/>
    Headers list from the request: <div id="headers"></div>
    <hr/>
    Data from jQuery: <div id="dataRecieved">loading</div>
    <script src="img/jquery.min.js"></script>
    <script src="img/example.js"></script>
    <script src="img/exampleJQuery.js"></script>
  1. 添加标题的一个简单方法是在open()调用之后在XMLHttpRequest对象上调用setRequestHeader
  function getData(url, onSucess) {
    var request = new XMLHttpRequest();
    request.open("GET", url, true);
    request.setRequestHeader("X-Myapp","super");
    request.setRequestHeader("X-Myapp","awesome");
    request.onload = function() {
      if (request.status === 200) {
        onSuccess(request.response);
      }
    };
    request.send(null);
  }
  1. XMLHttpRequest 会自动设置标题,比如"Content-Length","Referer"和"User-Agent",并且不允许你使用 JavaScript 更改它们。

注意

关于这一点的更完整的标题列表和背后的原因可以在 W3C 文档中找到,网址是www.w3.org/TR/XMLHttpRequest/#the-setrequestheader%28%29-method

  1. 为了打印结果,我们添加一个函数,该函数将把每个标题键和值添加到无序列表中:
  getData(
    'http://localhost:8080/hi',
    function(response){
      console.log('finished getting data');
      var data = JSON.parse(response);
      document.getElementById('data').innerHTML = data[0].hello;
      var headers = data[0].headers,
          headersList = "<ul>";
      for(var key in headers){
        headersList += '<li><b>' + key + '</b>: ' + headers[key] +'</li>';
      };
      headersList += "</ul>";
      document.getElementById('headers').innerHTML = headersList;
    });
  1. 当这个被执行时,所有请求头的列表应该显示在页面上,我们自定义的x-myapp应该显示出来:
 host: localhost:8080
 connection: keep-alive
 origin: http://localhost:8000
 x-myapp: super, awesome
 user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.27 (KHTML, like Gecko) Chrome/26.0.1386.0 Safari/537.27
  1. jQuery 方法更简单,我们可以使用beforeSend钩子调用一个函数来设置'x-myapp'标题。当我们收到响应时,将其写入到 ID 为dataRecived的元素中:
$.ajax({
    beforeSend: function (xhr) {
      xhr.setRequestHeader('x-myapp', 'this was easy');
    },
    success: function (data) {
     $('#dataRecieved').text(data[0].headers['x-myapp']);
    }
  1. jQuery 示例的输出将是包含在x-myapp标题中的数据:
Data from jQuery: this was easy

工作原理...

您可能已经注意到,在服务器端,我们添加了一个处理HTTP OPTIONS方法的路由,但我们从未明确地在那里调用过。如果我们查看服务器日志,应该会有类似以下输出的内容:

      Got HTTP OPTIONS on /hi with headers
      Got HTTP GET on /hi with headers

这是因为浏览器首先发出一个预检请求,这在某种程度上是浏览器询问是否有权限发出"真正"请求。一旦获得了许可,原始的 GET 请求就会发生。如果OPTIONS响应被缓存,浏览器将不会为后续请求发出任何额外的预检请求。

XMLHttpRequestsetRequestHeader函数实际上将每个值附加为逗号分隔的值列表。由于我们调用了两次该函数,标题的值如下:

'x-myapp': 'super, awesome'

还有更多...

对于大多数用例,我们不需要自定义标题成为我们逻辑的一部分,但有很多 API 会充分利用它们。例如,许多服务器端技术会添加包含一些元信息的X-Powered-By标题,比如JBoss 6PHP/5.3.0。另一个例子是 Google Cloud Storage,其中除了其他标题之外,还有以x-goog-meta为前缀的标题,比如x-goog-meta-project-namex-goog-meta-project-manager

对 API 进行版本控制

在进行第一次实现时,我们并不总是有最佳解决方案。API 可以扩展到一定程度,但之后需要进行一些结构性的更改。但我们可能已经有依赖于当前版本的用户,因此我们需要一种方式来拥有同一资源的不同表示版本。一旦一个模块有了用户,API 就不能随我们的意愿改变。

解决此问题的一种方法是使用所谓的 URL 版本控制,我们只需添加一个前缀。例如,如果旧的 URL 是http://example.com/rest/employees,新的 URL 可以是http://example.com/rest/v1/employees,或者在子域下可以是v1.example.com/rest/employee。只有在您对所有服务器和客户端都有直接控制权时,此方法才有效。否则,您需要有一种处理回退到旧版本的方法。

在这个示例中,我们将实现所谓的"语义版本",semver.org/,使用 HTTP 头来指定接受的版本。

准备工作

服务器将使用 Node.js 实现,因此您可以参考附录 A,安装 Node.js 和使用 npm,了解如何在您的计算机上安装 Node.js 以及如何使用 npm。在本例中,我们将使用 restify (mcavage.github.io/node-restify/) 作为服务器端逻辑来监视请求以了解发送了什么。

如何做...

让我们执行以下步骤。

  1. 我们需要首先定义依赖关系,然后安装restify,然后我们可以继续创建服务器代码。与之前的示例的主要区别是定义"Accept-version"头。restify 具有内置处理此头的功能,使用版本化路由。创建服务器对象后,我们可以设置哪些方法将在哪个版本上调用:
server.get({ path: "hi", version: '2.1.1'}, addHeaders, helloV2, logReqRes);
server.get({ path: "hi", version: '1.1.1'}, addHeaders, helloV1, logReqRes);
  1. 我们还需要处理HTTP OPTIONS,因为我们使用跨域资源共享,浏览器需要进行额外的请求以获取权限:
server.opts(/\.*/, addHeaders, logReqRes, function (req, res, next) {
  res.send(200);
  return next();
});
  1. 版本 1 和版本 2 的处理程序将返回不同的对象,以便我们可以轻松地注意到 API 调用之间的差异。在一般情况下,资源应该是相同的,但可能有不同的结构变化。对于版本 1,我们可以有以下内容:
function helloV1(req, res, next) {
  var hello = [{
    'id':'0',
    'hello': 'grumpy old data',
    'headers': req.headers
  }];
  res.send(hello);
  return next()
}
  1. 至于版本 2,我们有以下内容:
function helloV2(req, res, next) {
  var hello = [{
    'id':'0',
    'awesome-new-feature':{
      'hello': 'awesomeness'
    },
    'headers': req.headers
  }];
  res.send(hello);
  return next();
}
  1. 我们必须做的另一件事是添加 CORS 头,以启用accept-version头,因此在路由中包含了addHeaders,应该类似于以下内容:
function addHeaders(req, res, next) {
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, accept-version');
  res.setHeader('Access-Control-Allow-Methods', 'GET, OPTIONS');
  res.setHeader('Access-Control-Expose-Headers', 'X-Requested-With, accept-version');
  return next();
};

注意

请注意,您不应忘记调用next(),以便在路由链中调用下一个函数。

  1. 为了简单起见,我们只会在 jQuery 中实现客户端,因此我们创建一个简单的 HTML 文档,其中包括必要的 JavaScript 依赖项:
     Old api: <div id="data">loading</div>
    <hr/>
     New one: <div id="dataNew"> </div>
    <hr/>
    <script src="img/jquery.min.js"></script>
    <script src="img/exampleJQuery.js"></script>
  1. example.js文件中,我们对我们的 REST API 进行了两次 AJAX 调用,一次设置为使用版本 1,另一次设置为使用版本 2:
  $.ajax({
      url: 'http://localhost:8080/hi',
      type: 'GET',
      dataType: 'json',
      success: function (data) {
      $('#data').text(data[0].hello);
    },
    beforeSend: function (xhr) {
      xhr.setRequestHeader('accept-version', '~1');
    }
  });
  $.ajax({
      url: 'http://localhost:8080/hi',
      type: 'GET',
      dataType: 'json',
      success: function (data) {
      $('#dataNew').text(data[0]['awesome-new-feature'].hello);
    },
    beforeSend: function (xhr) {
      xhr.setRequestHeader('accept-version', '~2');
    }
  });

注意accept-version头包含值~1~2。这表示所有语义版本,例如 1.1.0 和 1.1.1 1.2.1,都将被~1匹配,类似地对于~2。最后,我们应该得到如下文本的输出:

Old api:grumpy old data
New one:awesomeness

它是如何工作的...

版本化路由是 restify 的内置功能,通过使用accept-version来工作。在我们的示例中,我们使用了版本~1~2,但是如果我们不指定版本会发生什么呢?restify 将为我们做出选择,因为请求将被视为客户端发送了*版本的方式进行处理。我们的代码中定义的第一个匹配路由将被使用。还有一个选项,可以设置路由以匹配多个版本,通过为某个处理程序添加版本列表:

  server.get({path: 'hi', version: ['1.1.0',  '1.1.1',  '1.2.1']}, sendOld);

这种类型的版本控制非常适合在不断增长的应用程序中使用,因为随着 API 的变化,客户端可以保持其 API 版本而无需在客户端开发中进行任何额外的努力或更改。这意味着我们不必对应用程序进行更新。另一方面,如果客户端确信他们的应用程序将在更新的 API 版本上运行,他们可以简单地更改请求头。

还有更多...

版本控制可以通过使用自定义内容类型来实现,前缀为vnd,例如,application/vnd.mycompany.user-v1。一个例子是谷歌地球的内容类型 KML,其中定义为application/vnd.google-earth.kml+xml。注意内容类型可以有两部分;我们可以有application/vnd.mycompany-v1+json,其中第二部分将是响应的格式。

使用 JSONP 获取 JSON 数据

JSONP 或带填充的 JSON 是一种利用<script>标签进行跨域请求的机制。通过简单地在script元素上设置src属性或添加元素本身来执行 AJAX 传输。浏览器将执行 HTTP 请求以下载指定的 URL,这不受同源策略的限制,这意味着我们可以使用它从不受我们控制的服务器获取数据。在本示例中,我们将创建一个简单的 JSONP 请求和一个简单的服务器来支持它。

准备工作

我们将创建一个简化的服务器实现,该服务器在之前的示例中使用过,因此我们需要安装 Node.js 和 restify(mcavage.github.io/node-restify/),可以通过定义package.json或简单安装来安装。有关使用 Node.js,请参阅附录 A,安装 Node.js 和使用 npm

如何做...

  1. 首先,我们将创建一个简单的路由处理程序,它将返回一个 JSON 对象:
function respond(req, res, next) {
  console.log("Got HTTP " + req.method + " on " + req.url + " responding");
  var hello = [{
    'id':'0',
    'what': 'hi there stranger'
  }];
  res.send(hello);
  return next();
}
  1. 我们可以自己编写一个版本,将响应包装成具有给定名称的 JavaScript 函数,但是为了在使用 restify 时启用 JSONP,我们可以简单地启用捆绑的插件。这是通过指定要使用的插件来完成的:
var server = restify.createServer();
server.use(restify.jsonp());
server.get('hi', respond);
  1. 之后,我们只需将服务器设置为监听端口 8080:
server.listen(8080, function() {
  console.log('%s listening at %s', server.name, server.url);
});
  1. 内置插件会检查请求字符串中是否有名为callbackjsonp的参数,如果找到,结果将是带有作为这些参数之一的值传递的函数名的 JSONP。例如,在我们的情况下,如果我们在http://localhost:8080/hi上打开浏览器,我们会得到以下结果:
[{"id":"0","what":"hi there stranger"}]
  1. 如果我们使用callback参数或设置了 JSONP 的相同 URL,例如http://localhost:8080/hi?callback=great,我们应该收到用该函数名包装的相同数据:
great([{"id":"0","what":"hi there stranger"}]);

这就是 JSONP 中的 P,表示填充的地方。

  1. 因此,我们接下来需要创建一个 HTML 文件,其中我们将显示来自服务器的数据,并包括两个脚本,一个用于纯 JavaScript 方法,另一个用于 jQuery 方法:
    <b>Hello far away server: </b>
    <div id="data">loading</div>
    <hr/>
    <div id="oneMoreTime">...</div>
    <script src="img/jquery.min.js"></script>
    <script src="img/example.js"></script>
    <script src="img/exampleJQuery.js"></script>
  1. 我们可以继续创建example.js,在其中创建两个函数;一个将创建一个script元素,并将src的值设置为http://localhost:8080/?callback=cool.run,另一个将在接收到数据时作为回调服务:
var cool = (function(){
  var module = {};

  module.run = function(data){
    document.getElementById('data').innerHTML = data[0].what;
  }

  module.addElement = function (){
    var script = document.createElement('script');
    script.src = 'http://localhost:8080/hi?callback=cool.run'
    document.getElementById('data').appendChild(script);
    return true;
  }
  return module;
}());
  1. 之后,我们只需要添加元素的函数:
cool.addElement();

这应该从服务器读取数据并显示类似以下的结果:

Hello far away server:
hi there stranger

cool对象中,我们可以直接运行addElement函数,因为我们将其定义为自执行。

  1. jQuery 示例要简单得多;我们可以将数据类型设置为 JSONP,其他一切都与任何其他 AJAX 调用一样,至少从 API 的角度来看:
$.ajax({
    type : "GET",
    dataType : "jsonp",
    url : 'http://localhost:8080/hi',
    success: function(obj){
      $('#oneMoreTime').text(obj[0].what);
    }
});

我们现在可以使用标准的success回调来处理从服务器接收到的数据,而且我们不必在请求中指定参数。jQuery 将自动将callback参数附加到 URL,并将调用委托给success回调。

它是如何工作的...

我们在这里所做的第一个重大飞跃是信任数据的来源。从服务器返回的结果在从服务器下载数据后进行评估。已经有一些努力在json-p.org/上定义更安全的 JSONP,但它远未普及。

下载本身是通过添加另一个主要限制到可用性的HTTP GET方法。超媒体作为应用程序状态的引擎HATEOAS),等等,定义了使用 HTTP 方法进行创建、更新和删除操作,使得 JSONP 对于这些用例非常不稳定。

另一个有趣的地方是 jQuery 如何将调用委托给success回调。为了实现这一点,会创建一个唯一的函数名,并将其发送到callback参数,例如:

/hi?callback=jQuery182031846177391707897_1359599143721&_=1359599143727

此函数稍后会回调到jQuey.ajax的适当处理程序。

还有更多...

使用 jQuery,如果应该处理jsonp的服务器参数不叫callback,我们也可以使用自定义函数。这是通过以下配置完成的:

jsonp: false, jsonpCallback: "my callback"

与 JSONP 一样,我们不使用XMLHttpRequest,也不期望任何与 AJAX 调用一起使用的函数被执行或者参数被填充。期望这样做是一个非常常见的错误。关于这一点可以在 jQuery 文档中找到更多信息:api.jquery.com/category/ajax/

从服务器读取 XML 数据

REST 服务的另一种常见数据格式是 XML。如果我们有选择格式的选项,那么几乎没有情况下 JSON 不是更好的选择。如果我们需要使用多个命名空间和模式进行严格消息验证,或者出于某种原因,我们使用可扩展样式表语言转换XSTL),那么 XML 是更好的选择。最重要的原因是需要处理和支持不使用 JSON 的传统环境。大多数现代服务器端框架都内置了内容协商支持,这意味着根据客户端的请求,它们可以以不同的格式提供相同的资源。在这个示例中,我们将创建一个简单的 XML 服务器,并从客户端使用它。

准备工作

对于服务器端,我们将使用 Node.js 和 restify(mcavage.github.io/node-restify/)进行 REST 服务,并使用 xmlbuilder(github.com/oozcitak/xmlbuilder-js)创建简单的 XML 文档。为此,我们可以使用 npm 安装依赖项,或者定义一个简单的package.json文件,例如示例文件中提供的文件。

如何做...

让我们按照以下步骤演示 XML 的使用。

  1. 服务器端代码与我们之前创建的其他基于 restify 的示例类似。由于我们只是想演示 XML 的使用,我们可以使用 xmlbuilder 创建一个简单的结构:
var restify = require('restify');
var builder = require('xmlbuilder');
var doc = builder.create();
doc.begin('root')
  .ele('human')
    .att('type', 'female')
      .txt('some gal')
      .up()
  .ele('human')
    .att('type', 'male')
      .txt('some guy')
  .up()
  .ele('alien')
    .txt('complete');
  1. 使用起来非常简单;doc.begin('root')语句创建了文档的根,ele()att()语句分别创建了元素和属性。由于我们总是在上次添加的嵌套级别上添加新的部分,为了将光标移动到上一个级别,我们只需调用up()函数。

在我们的情况下,将生成的文档如下:

<root>
  <human type="female">some gal</human>
  <human type="male">some guy</human>
  <alien>complete</alien>
</root>
  1. 为了为资源创建路由,我们可以创建server.get('hi', addHeaders, respond),其中add headers 是 CORS 的头部,响应将返回我们创建的 XML 文档作为字符串:
function respond(req, res, next) {
  res.setHeader('content-type', 'application/xml');
  res.send(doc.toString({ pretty: true }));
  return next();
}
  1. restify 不直接支持application/xml;如果我们保持这样,服务器的响应将是application/octet-stream类型。为了添加支持,我们将创建restify对象,并添加一个接受 XML 的格式化程序:
var server = restify.createServer({
  formatters: {
   'application/xml': function formatXML(req, res, body) {
      if (body instanceof Error)
        return body.stack;

      if (Buffer.isBuffer(body))
        return body.toString('base64');

      return body;
    }
  }
});

服务器应该返回正确的content-type和 CORS 头部,以及响应数据:

< HTTP/1.1 200 OK
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Headers: X-Requested-With
< content-type: application/xml
< Date: Sat, 02 Feb 2013 13:08:20 GMT
< Connection: keep-alive
< Transfer-Encoding: chunked

  1. 由于服务器已经准备好,我们可以继续在客户端创建一个基本的 HTML 文件,其中我们将包含 jQuery 和一个简单的脚本:
   Hello <div id="humans"></div>
   <hr/>
  <script src="img/jquery.min.js">
  </script>
  <script src="img/exampleJQuery.js"></script>
  1. 为简单起见,我们使用jQuery.ajax(),其中dataType的值将是xml
        (function(){
  $.ajax({
    type: "GET",
    url: "http://localhost:8080/hi",
    dataType: "xml",
    success: function(xml) {
      $("root > human", xml).each(function(){
        var p = $("<p></p>");
        $(p).text($(this).text()).appendTo("#humans");
      });
    }
  });
}())

它是如何工作的...

虽然大多数示例代码应该很简单,但你可能会想知道application/octet-stream是什么?嗯,它是一种通用二进制数据流的互联网媒体类型。如果我们用浏览器打开资源,它会要求我们保存它或者用什么应用程序打开它。

我们在restify实现中添加的formatter接受一个带有请求、响应和主体的函数。对我们来说,最感兴趣的是body对象;我们检查它是否是Error的实例,以便以某种方式处理它。需要进行的另一个检查是body是否是Buffer的实例。JavaScript 与二进制数据的处理不太好,因此创建了一个Buffer对象来存储原始数据。在我们的情况下,我们只返回主体,因为我们已经构建了 XML。如果我们经常进行这样的处理,直接为 JavaScript 对象添加格式化可能是有意义的,而不是手动创建包含 XML 数据的字符串。

在客户端,我们使用jQuery.ajax()来获取 XML,当这种情况发生时,success回调不仅接收文本,还接受一个 DOM 元素,我们可以使用标准的 jQuery 选择器来遍历。在我们的情况下,使用"root> human",我们选择所有的human元素,并且对其中的文本,每个元素都向"#humans"添加一个段落,就像处理 HTML 一样:

.   $("root > human", xml).each(function(){
        var p = $("<p></p>");
        $(p).text($(this).text()).appendTo("#humans");
      });  

还有更多...

JXON (developer.mozilla.org/en-US/docs/JXON)是在我们必须支持 XML 时的一个很好的选择。没有标准化,它遵循一个简单的约定,将 XML 转换为 JSON。在处理 XML 时,另一个很好的选择是使用 XPath——XML Path Language (www.w3.org/TR/xpath/),这是一种查询语言,可用于从某些节点中检索值或选择它们进行其他操作。XPath 在大多数情况下是最简单的选择,因此通常应该是我们的首选。

旧版本的 jQuery(1.1.2 版本之前)默认支持 XPath,但后来被移除,因为标准选择器在进行 HTML 转换时更加强大。

ECMAScript for XML 或通常称为 E4X 是一种编程语言扩展,用于原生支持 XML。虽然最新版本的 Firefox 中有几种实现可用,但它正在被移除。

使用 FormData 接口

添加到XMLHttpRequest Level 2 (www.w3.org/TR/XMLHttpRequest2/)中的新功能之一是FormData对象。这使我们能够使用一组可以使用 AJAX 发送的键值对。最常见的用途是发送二进制文件或任何其他大量的数据。在这个示例中,我们将创建两个脚本,一个将发送FormData,一个使用纯 JavaScript,另一个使用 jQuery,以及支持它的服务器端代码。

准备工作

服务器将在 Nodejs 中使用 restify (mcavage.github.io/node-restify/)完成。为了安装依赖项,可以创建一个package.json文件,其中将添加 restify。

如何做...

  1. 服务器应该能够接受HTTP POST,类型为multipart/form-data;这就是为什么restify有一个内置的名为BodyParser的插件。这将阻止解析 HTTP 请求体:
var server = restify.createServer();
server.use(restify.bodyParser({ mapParams: false }));
server.post('hi', addHeaders, doPost);
  1. 这将切换内容类型,并根据内容类型执行适当的逻辑,如application/jsonapplication/x-ww-form-urlencodedmutipart/form-dataaddHeaders参数将与我们在其他示例中添加的相同,以启用 CORS。为了简化我们的doPost处理程序,我们只记录请求体并返回 HTTP 200:
function doPost(req, res, next) {
  console.log("Got HTTP " + req.method + " on " + req.url + " responding");
  console.log(req.body);
  res.send(200);
  return next();
}
  1. 对于客户端,我们创建一个简单的脚本的 HTML 文件:
(function (){
var myForm = new FormData();
myForm.append("username", "johndoe");
myForm.append("books", 7);
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:8080/hi");
xhr.send(myForm);
  }());
  1. jQuery的方式要简单得多;我们可以将FormData设置为jQuery.ajax()中的data属性的一部分,此外,在发送之前我们需要禁用数据处理并保留原始内容类型:
(function(){
  var formData = new FormData();
  formData.append("text", "some strange data");
  $.ajax({
    url: "http://localhost:8080/hi",
    type: "POST",
    data: formData,
    processData: false,  // don't process data
    contentType: false   // don't set contentType
  });
}());

它是如何工作的...

传输的数据将具有与提交具有multipart/form-data编码类型的表单相同的格式。这种编码的需求来自于将混合数据与文件一起发送。大多数 Web 浏览器和 Web 服务器都支持这种编码。这种编码可以用于不是 HTML 甚至不是浏览器的表单。

如果我们查看发送的请求,我们可以看到它包含以下数据:

Content-Length:239
Content-Type:multipart/form-data; boundary=----WebKitFormBoundaryQXGzNXa82frwui6S

负载将如下所示:

------WebKitFormBoundaryQXGzNXa82frwui6S
Content-Disposition: form-data; name="username"
johndoe
------WebKitFormBoundaryQXGzNXa82frwui6S
Content-Disposition: form-data; name="books"
7
------WebKitFormBoundaryQXGzNXa82frwui6S--

您可能注意到,每个部分都包含一个Content-Disposition部分,其中包含数据的来源或者在我们的情况下,我们在每个附加到FormData对象的键中设置的名称。还有一个选项可以在每个单独的部分上设置内容类型,例如,如果我们有一个来自名为profileImage的控件的图像,那么该部分可以如下所示:

Content-Disposition: form-data; name="profileImage"; filename="me.png"
Content-Type: image/png

example.js中对xhr.sent()的最后一次调用在发送FormData类型的对象时会自动设置内容类型。

如果我们需要支持旧的不支持XMLHttpRequest级别 2 的传统浏览器,我们可以检查FormData是否存在,并相应地处理该情况:

if (typeof FormData === "undefined")

我们作为后备使用的方法不能是一个 AJAX 调用,但这不应该是一个问题,因为所有现代浏览器 IE<10 版本都不支持它。

向服务器发送二进制文件

向服务器发送文本、XML 或 JSON 相对容易,大多数 JavaScript 库都针对这种情况进行了优化。

发送二进制数据稍微棘手。现代应用程序可能需要能够上传生成的二进制文件;例如,在 HTML5 画布上绘制的图像、使用 JSZip 创建的 ZIP 文件等。

此外,能够上传使用 HTML5 文件 API 选择的文件是非常方便的。我们可以做一些有趣的事情,比如通过将文件分割成较小的部分,并将每个部分单独上传到服务器来实现可恢复的文件上传。

在这个配方中,我们将使用文件输入来上传用户选择的文件。

准备工作

服务器将使用 Node.js 实现-您可以从nodejs.org/下载并安装 Node.js。服务器将使用 Node.js 框架Connec t (www.senchalabs.org/connect/)实现。

操作步骤...

让我们编写客户端和服务器代码。

  1. 创建一个名为index.html的文件,包括文件输入、上传按钮、进度条和消息容器的文件上传页面:
<!DOCTYPE HTML>
<html>
<head>
<title>Upload binary file</title>
<style type="text/css">
.progress {
    position:relative;
    height:1em; width: 12em;
    border: solid 1px #aaa;
}
.progress div {
    position: absolute;
    top:0; bottom:0; left:0;
    background-color:#336699;
}
</style>
</head>
<body>
<input type="file"   id="file" value="Choose file">
<input type="button" id="upload" value="Upload"><br>
<p id="info"></p>
<div class="progress"><div id="progress"></div></div>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/uploader.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建一个名为uploader.js的文件,实现一个二进制文件上传器。它将文件发送到指定的 URL,并返回一个对象,用于绑定进度事件:
window.postBinary = function(url, data) {
    var self = {},
        xhr = new XMLHttpRequest();
    xhr.open('POST', url, true);
    xhr.responseType = 'text';    
    self.done = function(cb) {
        xhr.addEventListener('load', function() {
            if (this.status == 200)
                cb(null, this.response)
            else
                cb(this.status, this.response)
        });
        return self;
    }
    self.progress = function(cb) {
        xhr.upload.addEventListener('progress', function(e) {
            if (e.lengthComputable) 
                cb(null, e.loaded / e.total);
            else
                cb('Progress not available');
        });
        return progress;
    };
    xhr.send(data);    
    return self;
};
  1. 创建一个名为example.js的文件,使用uploader.js提供的 API 为上传表单添加上传功能:
$(function() {
    var file;
    $("#file").on('change', function(e) {
        file = this.files[0]
    });
    $("#upload").on('click', function() {
        $("#info").text("Uploading...");
        $("#progress").css({width:0});
        if (!file) {
            $("#info").text('No file selected')
            return;
        }
        var upload =  postBinary('/upload/' + file.name, file);
        upload.progress(function(err, percent) {
            if (err) {
                $("#info").text(err);
                return;
            }
            $("#progress").css({width: percent + '%'});
        });
        upload.done(function(err, res) {
            if (err) {
                $("#info").text(err + ' ' + res);
                return;
            }
            $("#progress").css({width: '100%'});
            $("#info").text("Upload complete");
        });

    });
});
  1. 创建一个名为server.js的文件,这是一个基于 Node.js Connect 框架的 Node.js 服务器,用于处理文件上传和提供静态文件:
var path = require('path'),
    connect = require('connect'),
    fs = require('fs');

connect()
    .use('/upload', function(req, res) {        
        var file = fs.createWriteStream(
            path.join(__dirname, 'uploads', req.url))
        req.pipe(file);
        req.on('end', function() {
            res.end("ok");
        });
    })
    .use(connect.static(__dirname))
    .listen(8080);
  1. 从存放server.js的目录打开命令提示符,并输入以下命令来创建一个用于上传的目录,安装 connect 库,并启动服务器:
mkdir uploads
npm install connect
node server.js

  1. 将浏览器导航到http://localhost:8080以测试示例。所有创建的文件(包括server.js)应该在同一个目录中。

工作原理...

HTML5 中的新XMLHttpRequest对象具有支持更多类型数据的send方法。它可以接受FileBlobArrayBuffer对象。我们将这种新功能与 HTML5 文件 API 一起使用,以上传用户选择的文件。您可以在第四章的在客户端使用文件输入配方中找到有关此 API 的更多信息,使用 HTML5 输入组件

新 API 还提供了一个upload对象,类型为XMLHttpRequestUpload。它允许我们附加事件监听器来监视上传进度。我们使用这个功能来显示上传的进度条。

服务器接受'/upload'处的上传,并将文件保存到uploads目录。此外,它还提供示例目录中的静态文件。

还有更多...

新的 XHR API 仅适用于 Internet Explorer 10 及以上版本。

有些浏览器可能无法触发上传进度事件。

使用 Node.js 创建 SSL 连接

常见的安全问题是所谓的中间人攻击,这是一种窃听的形式,攻击者会独立连接到受害者并将消息转发到所需的位置。攻击者必须能够拦截消息并自行更改。只有在攻击者能够成功冒充两个涉及方时,这才有可能。安全套接字层SSL)及其后继者传输层安全TSL)通过加密数据来防止这种类型的攻击。在这个示例中,我们创建一个使用 restify 的 Node.js 服务器,该服务器支持 HTTPS。

准备就绪

为了启用 HTTPS,我们将使用证书和服务器私钥。为了生成这个,我们需要 OpenSSL(www.openssl.org/),这是一个完整的开源工具包,实现 SSL 和 TLS,以及一个通用的密码库。

首先,在命令行上,生成一个 RSA([en.wikipedia.org/wiki/RSA_(algorithm)](http://en.wikipedia.org/wiki/RSA_(algorithm))私钥:

 openssl genrsa -out privatekey.pem 1024

准备就绪

将生成的实际密钥应该类似于以下内容:

准备就绪

你生成的应该要长得多。

注意

请注意,私钥之所以称为私钥是有原因的,你不应该将其放在任何版本控制系统中,也不应该让所有人都可以访问。这应该保持安全,因为这是你的真实身份证明。

接下来,我们将使用刚刚创建的私钥创建一个证书签名请求CSR)文件,并输入一些额外的信息:

openssl req -new -key privatekey.pem -out csr.pem

填写表格后,我们生成了一个 CSR 文件,用于向证书颁发机构请求签署您的证书。这个文件可以发送给他们进行处理,他们会给我们一个证书。由于我们只是创建一个简单的示例,我们将使用我们的私钥自签名文件:

openssl x509 -req -in csr.pem -signkey privatekey.pem -out publiccert.pem

publiccert.pem文件是我们将在服务器上用作证书的文件。

如何做...

  1. 首先,我们添加依赖项,然后创建一个options对象,从中读取我们生成的密钥和证书:
var restify = require('restify');
var fs = require('fs');
// create option for the https server instance
var httpsOptions = {
  key: fs.readFileSync('privatekey.pem'),//private key
  certificate: fs.readFileSync('publiccert.pem')//certificate
};

注意

Node.js 中的文件 IO 是使用fs模块提供的。这是标准 POSIX 功能的包装器。可以在nodejs.org/api/fs.html上找到有关它的文档。

  1. 我们继续创建路由和处理程序,并为了不重复两个服务器实例的逻辑,我们创建一个通用的serverCreate函数:
var serverCreate = function(app) {
  function doHi(req, res, next) {
    var name = 'nobody';
    if(req.params.name){
      name = req.params.name;
    }
    res.send('Hi ' + name);
    return next();
  }
  app.get('/hi/', doHi);
  app.get('/hi/:name', doHi);
}
  1. 然后我们可以使用这个函数来创建两个服务器的实例:
serverCreate(server);
serverCreate(httpsServer);
  1. 我们可以将标准服务器设置为监听端口80,HTTPS 版本设置为端口443
server.listen(80, function() {
  console.log('started at %s', server.url);
});

httpsServer.listen(443, function() {
  console.log('started at %s', httpsServer.url);
});
  1. 现在我们可以调用node server.js来启动服务器,并尝试从浏览器访问以下页面:
  • http://localhost:80/hi/John

  • http://localhost:443/hi/UncleSam

工作原理...

当运行服务器时,你可能会遇到以下类似的错误:

Error: listen EACCES
 at errnoException (net.js:770:11)
 at Server._listen2 (net.js:893:19)

问题在于服务器本身无法绑定到小于 1024 的端口,除非具有 root 或管理员权限(众所周知)。

我们刚刚创建的 HTTPS 服务器使用了公钥密码学。每个对等方都有两个密钥:一个公钥和一个私钥。

注意

在密码学中,通常涉及的各方被称为 Alice 和 Bob,因此我们将使用相同的名称。关于这个主题的更多信息可以在维基百科上找到en.wikipedia.org/wiki/Alice_and_Bob

Alice 和 Bob 的公钥与所有人共享,而他们的私钥则保密。为了让 Alice 加密她需要发送给 Bob 的消息,她需要 Bob 的公钥和她自己的私钥。另一方面,如果 Bob 需要解密他从 Alice 那里收到的相同消息,他需要她的公钥和他自己的私钥。

在 TLS 连接中,公钥是证书。这是因为它是签名的,以证明真正的所有者是他们声称的人;例如 Bob。TSL 证书可以由一个实际确认 Bob 是他所声称的人的证书颁发机构签名。Firefox、Chrome 和其他浏览器有一个受信任的用于签发证书的根 CA 列表。这个根 CA 可能会向其他签名机构签发证书,然后将它们出售给普通公众;这是一个非常有趣的业务,你不觉得吗?

在我们的情况下,我们自签了我们的证书,所以它不被浏览器信任,当我们打开它时,我们会得到以下可爱的小页面:

它是如何工作的...

当我们使用 CA 签名的证书时,这条消息将不会出现,因为我们将拥有一个被我们的浏览器认可为受信任的权威。

还有更多内容...

开放 Web 应用安全项目,或 OWASP (www.owasp.org/),在创建 Web 应用程序时,有一个关于常见安全问题和陷阱的全面数据库。在那里,您可以找到有关 HTML5 应用程序安全的很棒的安全速查表(www.owasp.org/index.php/HTML5_Security_Cheat_Sheet)。在涉及 HTTPS 时,一个常见的问题是存在不总是来自相同协议的混合内容。增加安全性的一个简单方法是将每个请求都发送到 TLS/SSL。

使用 Ajax Push 进行实时更新

Comet 是一种 Web 模型,其中长时间保持的 HTTP 请求允许服务器向浏览器“推送”数据,而无需浏览器明确发出请求。Comet 以许多不同的名称而闻名,如 Ajax Push,Server Push,Reverse Ajax 双向 Web 等。在这个示例中,我们将创建一个简单的服务器,向客户端发送或“推送”当前时间。

准备工作

在这个示例中,我们将使用 Node.js 和一个名为Socket.IO的库(socket.io/)。这个依赖项可以包含在package.json文件中,也可以直接从 npm 安装。

操作方法...

让我们开始吧。

  1. 首先,我们将从服务器端开始,我们将添加所需的 Socket.IO、HTTP 和文件系统的require语句:
var app = require('http').createServer(requestHandler),
    io = require('socket.io').listen(app),
    fs = require('fs')
  1. 服务器初始化为requestHandler,我们将在其中提供一个位于同一目录中的index.html文件,稍后我们将创建:
function requestHandler (req, res) {
  fs.readFile('index.html',
    function (err, data) {
      if (err) {
        res.writeHead(500);
        return res.end('Error loading index.html');
      }
    res.writeHead(200);
    res.end(data);
    });
}
  1. 如果无法读取文件,它将返回 HTTP 500,如果一切正常,它将返回数据,这是一个非常简化的处理程序。我们将服务器设置为侦听端口 80,然后我们可以继续进行与 Socket.IO 相关的配置:
io.configure(function () {
  io.set("transports", ["xhr-polling"]);
  io.set("polling duration", 10);
});

在这里,我们将唯一允许的传输设置为xhr-polling,以便进行示例。Socket.IO 支持多种不同的向客户端发送服务器端事件的方式,因此我们禁用了其他所有内容。

注意

请注意,在实际应用中,您可能会希望保留其他传输方法,因为它们可能是给定客户端的更好选择,或者作为备用机制。

  1. 之后,我们可以继续进行事件。在每次连接时,我们向客户端发出一个带有一些 JSON 数据的ping事件,第一次,每当收到pong事件时,我们等待 15 秒,然后再次发送一些带有当前服务器时间的 JSON 数据:
io.sockets.on('connection', function (socket) {
  socket.emit('ping', {
    timeIs: new Date()
  });
  socket.on('pong', function (data) {
    setTimeout(function(){
    socket.emit('ping', {
      timeIs: new Date()
    });
    console.log(data);
    }, 15000);
  });
});
  1. 现在在客户端,我们将包含socket.io.js文件,由于我们正在从 node 提供我们的index.html文件,它将被添加到以下默认路径:
  <script src="img/socket.io.js"></script>
  1. 之后,我们连接到localhost并等待ping事件,每次收到这样的事件时,我们都会附加一个带有服务器时间的p元素。然后我们向服务器发出pong事件:
    <script>
      var socket = io.connect('http://localhost');
      socket.on('ping', function (data) {
        var p = document.createElement("p");
        p.textContent = 'Server time is ' + data.timeIs;
        document.body.appendChild(p);
        socket.emit('pong', {
          my: 'clientData'
        });
      });
    </script>

现在当我们启动服务器并通过打开http://localhost访问index.html时,我们应该可以在没有明确请求的情况下获得服务器更新:

Server time is 2013-02-05T06:14:33.052Z

它是如何工作的...

如果我们不将唯一的传输方法设置为 Ajax 轮询或 xhr-polling,Socket.IO 将尝试使用最佳可用方法。目前,支持几种传输:WebSocket、Adobe Flash Socket、AJAX 长轮询、AJAX 多部分流式传输、Forever IFrame 和 JSONP 轮询。

根据使用的浏览器,不同的方法可能更好、更差或不可用,但可以肯定的是 WebSockets 是未来。长轮询在浏览器端更容易实现,并且适用于支持XMLHttpRequest的每个浏览器。

顾名思义,长轮询是指客户端请求服务器事件。这个请求保持打开状态,直到服务器向浏览器发送了一些新数据或关闭了连接。

如果我们在我们的示例中打开控制台,我们可以看到向服务器发送了一个请求,但由于响应尚未完成,它没有关闭:

hOC6eXNTrdIhwO9aHcqX?t=1360049439710/socket.io/1/xhr-polling GET (pending)

由于我们将服务器轮询持续时间设置为 10 秒,使用io.set("polling duration", 10),这个连接将被关闭,然后重新打开。您可能会想知道为什么我们需要关闭连接?如果不关闭,服务器上的资源将很容易被耗尽。

您可能会注意到服务器控制台中的关闭和数据发送:

   debug - xhr-polling received data packet 5:::{"name":"pong","args":[{"my":"clientData"}]}
   debug - setting request GET /socket.io/1/xhr-polling/5jBJdDQ6Uc2ZYXzZHcqd?t=1360050667340
   debug - setting poll timeout
   debug - discarding transport

还有一件事需要注意的是,一旦连接关闭,无论是因为收到响应还是因为服务器端超时,都会创建一个新连接。新创建的请求通常会有一个等待它的服务器连接,从而显著减少延迟。

还有更多...

Socket.IO 还有许多其他功能,我们没有涉及。其中之一是向所有连接的客户端广播消息。例如,为了让每个人都知道有新用户连接,我们可以这样做:

  io.sockets.on('connection', function (soc) {
  soc.broadcast.emit('user connected');
});

即使我们不使用 Node.js,大多数编程语言都可以使用彗星技术或“黑客技术”,这是改善用户体验的好方法。

使用 WebSockets 交换实时消息

在 HTML5 Web Sockets 之前,需要实现实时更新的 Web 应用程序,如聊天消息和游戏移动,必须采用低效的方法。

最流行的方法是使用长轮询,即保持与服务器的连接直到事件到达。另一种流行的方法是将 JavaScript 的分块流式传输到iframe元素,也被称为彗星流

HTML5 WebSockets 使得可以与 Web 服务器交换实时消息。该 API 更清洁、更易于使用,错误更少,并且提供更低的消息延迟。

在这个示例中,我们将实现一个基于 WebSockets 的简单聊天系统。为了使系统更易于扩展,我们将在底层 WebSockets 上使用 dnode。dnode 库为多种语言和平台提供了完整的基于回调的 RPC:Node.js、Ruby、Java 和 Perl。基本上,它使我们能够调用服务器端代码,就好像它在客户端执行一样。

准备工作

服务器将使用 Node.js 实现——您可以从nodejs.org/下载并安装 Node.js。

为了做好准备,您还需要安装一些 node 模块。为该示例创建一个新目录,并输入以下命令以安装 node 模块:

npm install -g browserify
npm install express shoe dnode

如何做到这一点...

让我们来编写客户端和服务器。

  1. index.html中创建包含消息列表、用户列表和文本输入框的主要聊天页面。聊天页面的样式填满整个浏览器视口。
<!DOCTYPE HTML>
<html>
<head>
<title>Using websockets</title>
<style type="text/css">
#chat { position: absolute; overflow: auto;
    top:0; left:0; bottom:2em; right:12em; }
#users { position: absolute; overflow: auto;
    top:0; right: 0; width:12em; bottom: 0; }
#input { position: absolute; overflow: auto;
    bottom:0; height:2em; left: 0; right: 12em; }

#chat .name { padding-right:1em; font-weight:bold; }
#chat .msg { padding: 0.33em; }
</style>
</head>
<body>
<div id="chat">
</div>
<div id="users">
</div>
<input type="text" id="input">
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/example.min.js"></script>
</body>
</html>
  1. 创建一个名为chat.js的文件——JavaScript 中的聊天室实现。chat()函数创建一个聊天室,并返回chatroom的公共 API,包括joinleavemsgpinglisten函数。
function keysOf(obj) {
    var k = [];
    for (var key in obj)
        if (obj.hasOwnProperty(key))
            k.push(key);
    return k;
}
function chat() {
    var self = {},
        users = {},
        messages = [];

    // Identify the user by comparing the data provided
    // for identification with the data stored server-side
    function identify(user) {
        return users[user.name] && user.token
            == users[user.name].token;
    }
    // Send an event to all connected chat users that
    // are listening for events
    function emit(event) {
        console.log(event);
        for (var key in users) if (users.hasOwnProperty(key))
            if (users[key].send) users[key].send(event);
    }
    // This function resets the timeout countdown for a
    // specified user. The countdown is reset on every user
    // action and every time the browser sends a ping
    // If the countdown expires, the user is considered
    // to have closed the browser window and no longer present
    function resetTimeout(user) {
        if (user.timeout) {
            clearTimeout(user.timeout);
            user.timeout = null;
        }
        user.timeout = setTimeout(function() {
            self.leave(user, function() {});
        }, 60000);
    }

    // When a user attempts to join, he must reserve a
    // unique name. If this succeeds, he is given an auth
    // token along with the name. Only actions performed
    // using this token will be accepted as coming from
    // the user. After the user joins a list of users and
    // past messages are sent to him along with the
    // authentication information.
    self.join = function(name, cb) {
        if (users[name]) return cb(name + " is in use");
        users[name] = {
            name: name,
            token: Math.round(Math.random() * Math.pow(2, 30))
        }
        resetTimeout(users[name]);
        emit({type: 'join', name: name});
        cb(null, { you: users[name], messages: messages,
           users: keysOf(users) });
    }
    // The leave function is called when the user leaves
    // after closing the browser window.
    self.leave = function(user, cb) {
        if (!identify(user)) return
        clearTimeout(users[user.name].timeout);
        delete users[user.name];
        emit({type: 'leave', name: user.name});
        cb(null);
    }
    // The message function allows the user to send a
    // message. The message is saved with a timestamp
    // then sent to all users as an event.
    self.msg = function(user, text) {
        if (!identify(user)) return;
        resetTimeout(users[user.name]);
        var msg = {
            type: 'msg',
            name: user.name,
            text: text,
            time: Date.now()
        }
        messages.push(msg);
        emit(msg);
    }
    // The ping function allows the browser to reset
    // the timeout. It lets the server know that the
    // user hasn't closed the chat yet.
    self.ping = function(user) {
        if (identify(user))
            resetTimeout(users[user.name]);
    }
    // The listen function allows the user to provide
    // a callback function to be called for every event.
    // This way the server can call client-side code.
    self.listen = function(user, send, cb) {
        if (!identify(user)) return
        users[user.name].send = send;
    }
    return self;
};
module.exports = chat;
  1. 让我们创建名为server.js的 Node.js 脚本,实现 web 服务器:
var express = require('express'),
    http    = require('http'),
    chat    = require('./chat.js'),
    shoe    = require('shoe'),
    dnode   = require('dnode')
// Create an express app
var app = express();
// that serves the static files in this directory
app.use('/', express.static(__dirname));
// then create a web server with this app
var server = http.createServer(app);
// Create a chat room instance,
var room = chat();
// then create a websocket stream that
// provides the chat room API via dnode
// and install that stream on the http server
// at the address /chat
shoe(function (stream) {
    var d = dnode(room);
    d.pipe(stream).pipe(d);
}).install(server, '/chat');
// start the server
server.listen(8080);
  1. 创建一个名为example.js的文件来实现聊天客户端:
var shoe = require('shoe'),
    dnode = require('dnode');

$(function() {

    // Add a message to the message div
    function addMsg(msg) {
        var dMsg = $("<div />").addClass('msg'),
            dName = $("<span />").addClass('name')
                .text(msg.name).appendTo(dMsg),
            dText = $("<span />").addClass('text')
                .text(msg.text).appendTo(dMsg);
        dMsg.appendTo("#chat");
        $("#chat").scrollTop($("#chat")[0].scrollHeight);
    }

    // Re-display a list of the present users.
    function showUsers(users) {
        $("#users").html('');
        users.forEach(function(name) {
            $("<div />").addClass('user')
                .text(name).appendTo('#users');
        });
    }

    // Create a client-side web sockets stream
    // piped to a dnode instance
    var stream = shoe('/chat');
    var d = dnode();
    // When the remote chat API becomes available
    d.on('remote', function (chat) {
        // Attempt to join the room until a suitable
        // nickname that is not already in use is found
        function join(cb, msg) {
            var name = prompt(msg || "Enter a name");
            chat.join(name, function(err, data) {
                if (err) join(cb, err);
                else cb(data);
            });
        }
        join(function(data) {
            var me = data.you,
                users = data.users;
            // Show the users and messages after joining
            showUsers(users);
            data.messages.forEach(addMsg);
            // Allow the user to send messages
            $("#input").on('keydown', function(e) {
                if (e.keyCode == 13) {
                    // sending works by calling the
                    // remote's msg function.
                    chat.msg(me, $(this).val());
                    $(this).val('');
                }

            });
            // Tell the remote we're listening for
            // events
            chat.listen(me, function(e) {
                if (e.type == 'msg')
                    return addMsg(e);
                if (e.type == 'leave')
                    delete users[users.indexOf(e.name)];
                else if (e.type == 'join')
                    users.push(e.name);
                showUsers(users);
            });
            // Tell the remote every 30 seconds that
            // we're still active
            setInterval(function() {
                chat.ping(me);
            }, 30000);

        });
    });
    // pipe dnode messages to the websocket stream
    // and messages from the stream to dnode
    d.pipe(stream).pipe(d);
});
  1. 使用browserify创建example.min.js
browserify example.js –-debug -o example.min.js

  1. 启动 node 服务器:
node server.js

  1. 将浏览器导航到localhost:8080来测试示例。

它是如何工作的...

我们在这里没有直接使用 WebSockets API。原因是,使用原始的 WebSockets 发送响应消息并不是很容易——它们不支持请求-响应循环。因此,要实现一些 RPC 调用,比如询问服务器名称是否可用,将会更加困难。

另一方面,dnode 协议支持将本地回调传递给远程函数,远程函数反过来可以将自己的回调传递给接收到的回调,依此类推——从而实现一个非常强大的完整 RPC。这使我们能够扩展我们的应用程序,以满足新的需求。作为一个奖励,结果 API 更加清晰和表达力更强。

这是我们使用 dnode 实现聊天室的步骤:

  1. 我们创建了一个简单的对象,使用延续传递风格来返回所有函数的错误和值。这是我们的聊天室对象,定义了应用程序的 RPC API。

  2. 我们定义了一个基于shoe库的 WebSockets 服务器,为每个连接的客户端创建一个新的 Node.js 流。然后将其安装到/chat路由的常规 HTTP 服务器上。

  3. 我们通过将每个连接的客户端流传输到基于聊天室对象的新创建的 dnode 流来连接这两者。

就是这样!然后,在客户端使用 API,做如下操作:

  1. 我们定义了一个基于shoe库的 WebSockets 客户端,它连接到/chat路由的 HTTP 服务器,并在建立连接时创建一个新的 Node.js 流。

  2. 我们将该流传输到一个新创建的 dnode 客户端。

  3. 建立连接后,dnode 客户端接收到一个包含第 1 步中定义的 API 的对象——所有函数都可用。

注意

github.com/substack/dnode了解更多关于 dnode 的信息。

截至 2013 年 2 月,IE 9 及以下版本不支持 WebSockets API。最新版本的 Android(v 4.2)内置浏览器也不支持 WebSockets API。

第九章:客户端模板

在本章中,我们将涵盖以下内容:

  • 使用 Handlebars 渲染对象

  • 使用 EJS 渲染对象

  • 使用 Jade 渲染对象

  • 使用 Handlebars 渲染数组

  • 使用 EJS 渲染数组

  • 使用 Jade 渲染数组

  • 在 Handlebars 中使用助手简化模板

  • 在 Handlebars 中使用部分模板重用模板

  • 在 EJS 中使用部分模板重用模板

  • 在 Jade 中使用过滤器

  • 在 Jade 中使用混合

  • 使用 Jade 中的布局和块

介绍

现代服务应用程序通常为多个平台构建,其中只有一个平台是 Web。其他平台可能包括 iOS、Android 和其他需要通过 API 使用服务的网站。其中一些平台可能不支持 HTML。它们可能还需要为相同的数据显示不同的 HTML 或在显示数据之前进行预处理。

结果是,Web 应用程序已经从使用服务器端 HTML 渲染转变为使用客户端 HTML 渲染。服务提供序列化的原始数据(通常为 JSON,有时为 XML),客户端决定如何显示数据。

在本章中,我们将介绍几种流行的客户端模板语言,每种语言都有不同的模板方法。

  • EJS 将 HTML 与 JavaScript 的全部功能结合在一起

  • Handlebars 将 HTML 与简洁但更受限制的块结构相结合

  • Jade 将 HTML 语法替换为更清晰的版本,并支持动态功能

我们将学习如何在每个模板语言中执行一些常见任务,比如显示基本对象,显示列表(或循环),以及使用部分模板。

使用 Handlebars 渲染对象

Handlebars 是一种模板语言,它在 HTML 中添加了最小的语法。它的目标是最小化模板中存在的逻辑量,并强制传递的模型对象与视图中应该呈现的内容相对应。

在这个示例中,我们将演示使用一个简单的例子来展示 Handlebars 的一些优点和缺点。我们将根据一天中的时间来呈现用户的问候语。

准备工作

我们需要从github.com/wycats/handlebars.js下载 Handlebars。浏览器版本位于dist目录中。创建一个示例目录并将handlebars.js复制到该目录中,或直接下载(在 Linux 上):

wget https://raw.github.com/wycats/handlebars.js/master/dist/handlebars.js

如何做...

让我们写下代码:

  1. 创建包含name输入、greeting占位符和 Handlebars 模板的index.html
<!DOCTYPE HTML>
<html>
<head>
<title>Displaying objects with Handlebars</title>
</head>
<body>
<form method="post">
    <p>Name: <input id="name" type="text" name="name" value="John"></p>
</form>
<div id="greeting">
</div>
<script id="template" type="text/x-handlebars-template">

{{#if evening}}
    Good evening,
{{/if}}
{{#if morning}}
    Good morning,
{{/if}}
{{#if day}}
    Hello,
{{/if}}
<b>{{name}}</b>

</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/handlebars.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js来将模板绑定到数据和视图:
$(function() {
    var template = Handlebars.compile($('#template').html());
    function changeName() {
        var hour = new Date().getHours();
        $('#greeting').html(template({
            name: $("#name").val(),
            evening: hour > 18,
            morning: hour < 10,
            day: hour >= 10 && hour <= 18
        }));
    }
    $('#name').on('keypress keyup', changeName);
    changeName();
});

它是如何工作的...

我们通常通过将 Handlebars 模板添加到带有type属性设置为text/x-handlebars-templatescript元素中,将它们嵌入到 HTML 页面中。浏览器会忽略未知类型的脚本,因此我们可以确保内容保持不变。

使用模板分为两个阶段。在第一阶段,我们需要编译模板文本。这个过程会返回一个以 JavaScript 函数形式的编译模板。在第二阶段,我们将模型对象作为参数传递给该函数(编译模板),并获得 HTML 输出。

Handlebars 是一种非常有主见的和极简的模板语言。在模板中使用比较运算符等程序逻辑是严格禁止的。这是有意设计的,这是一个好主意,如果业务逻辑发生变化,我们不需要更新模板。例如,如果我们开始将午夜到凌晨 2 点视为晚上,我们不需要更改模板-我们只需要在创建模型时添加条件,然后将其传递给 Handlebars。

另一方面,我们可以看到 Handlebars 有时会对其限制有些过分。例如,它不支持 case 结构,枚举或诸如'else if'之类的结构。因此,我们必须要么为每种可能的状态使用布尔表达式,要么将实际文本或值保留在模型中。在某些情况下,模型可能会干扰属于视图的信息。

使用 EJS 渲染对象

EJS 是一种模板语言,允许用户在模板中混合 HTML 和 JavaScript。类似于 PHP 和 ERB,它通过在 HTML 中添加额外的标记来使用户能够从 HTML 中“逃离”到编程语言,并使用该语言的全部功能。

在这个教程中,我们将使用一个简单的示例来演示 EJS。我们将根据一天中的时间来渲染用户问候。

准备工作

我们需要从embeddedjs.com/下载 EJS,并在我们的recipe文件夹中提取ejs_production.js

如何做...

让我们开始吧。

  1. 创建包含name输入,greeting占位符和 EJStemplateindex.html
<!DOCTYPE HTML>
<html>
<head>
<title>Displaying an EJS object</title>
</head>
<body>
<form method="post">
    <p>Name: <input id="name" type="text" name="name" value="John"></p>
</form>
<div id="greeting">
</div>
<script id="template" type="text/ejs">
    <% if (hour > 18) { %>
        Good evening,
    <% } else if (hour < 10) { %>
        Good morning,
    <% } else { %>
        Hello,
    <% } %>
    <b><%= name %></b>
</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/ejs_production.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js以将模板绑定到数据和视图:
$(function() {
    var template = new EJS({
        text: $('#template').html()
    });
    function changeName() {
        $('#greeting').html(template.render({
            name: $("#name").val(),
            hour: new Date().getHours()
        }));
    }
    $('#name').on('keypress keyup', changeName);
    changeName();
});

它是如何工作的...

将 EJS 模板嵌入页面的常见方法是将它们添加到具有type属性设置为text/ejsscript元素中。浏览器会忽略未知类型的脚本,因此我们可以确保内容保持不变。

在 EJS 的开放和关闭标签<% %>之间,我们可以编写任意的 JavaScript,这将在渲染模板时执行。模板的其余部分是纯 HTML。这使得 EJS 非常容易使用。

当我们想要打印 JavaScript 表达式的值时,我们使用不同的开标签<%=,它将表达式值打印为纯文本,转义任何包含的 HTML。

要使用模板,我们创建一个新的 EJS 对象。这将调用 EJS 编译器,将模板编译成更高效的形式。然后我们可以调用这个对象的render方法,传递变量(数据模型)以在渲染模板时使用。

还有更多...

要打印 HTML 表达式的值而不转义,我们可以使用<%-标签而不是<%=标签。这使我们能够将 HTML 代码插入为 DOM 节点(而不是将它们视为纯文本)。

使用 Jade 渲染对象

Jade 是一种简洁的模板语言。它使用显著的空格来表示块和元素的层次结构。它支持许多高级功能,例如混合,这是子模板,以及块,这是可以通过继承替换的模板部分。

在这个教程中,我们将使用 Jade 渲染一个简单的问候语。在本章的后面,我们将看一些更高级的功能。

准备工作

我们需要在我们的recipe文件夹中下载jade.min.js,可以在github.com/visionmedia/jade上找到。

如何做...

让我们开始吧。

  1. 创建index.html,它将包含一个要求用户输入姓名的小表单,一个用于渲染问候语的占位符,以及问候语模板:
<!DOCTYPE HTML>
<html>
<head>
<title>Displaying an object with Jade </title>
</head>
<body>
<form method="post">
    <p>Name: <input id="name" type="text" name="name" value="John"></p>
</form>
<div id="greeting">
</div>
<script id="template" type="text/jade">

if hour > 18
    span Good evening,
else if hour < 10
    span
        | Good
        | morning,
else
    span Hello,
b= name

</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/jade.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js以编译模板并将其绑定到数据和视图:
$(function() {
    var template = jade.compile(
        $('#template').html()
    );
    function changeName() {
        $('#greeting').html(template({
            name: $("#name").val(),
            hour: new Date().getHours()
        }));
    }
    $('#name').on('keypress keyup', changeName);
    changeName();
});

它是如何工作的...

Jade 模板与生成的 HTML 结构非常相似。我们的模板生成一个包含问候文本的单个span元素,以及另一个包含用户姓名的b(粗体)元素。

Jade 支持条件语句。它们的语法看起来与元素语法完全相似,只是它们不会被渲染。条件不需要用括号括起来,但布尔表达式会被作为 JavaScript 代码进行评估。

正如“早上好”问候所示,我们可以使用竖线字符将文本分成多行

显示变量的内容(转义 HTML 标记),我们使用"="(等于)字符。如果我们不希望内容被过滤,可以使用"-"(减号)字符。

要使用 Jade 模板,我们使用jade.compile进行编译。这将得到一个template函数。如果我们将一个对象传递给这个函数,我们将得到一个渲染后的 HTML 作为结果。我们在#greeting元素内显示 HTML。

使用 Handlebars 渲染数组

显示对象列表是我们需要单独的模板语言的最常见原因,否则我们可以轻松地通过直接操作 DOM 来完成。Handlebars 对于数组迭代有一个简单、清晰和直接的语法——each结构,它的工作方式与其他语言中的for each循环非常相似。

在这个示例中,我们将渲染一个消息对象列表。每个消息对象都有一个作者、到达时间、正文和阅读状态。我们将使用不同的样式来区分已读和未读的消息。

像本章中的其他示例一样,模板将包含在 HTML 文件中的script标签内。然而,编译可以在我们选择的任何字符串上调用;因此可以通过向服务器发送请求来下载模板数据。

准备工作

我们需要从github.com/wycats/handlebars.js下载 Handlebars。浏览器版本在dist目录中。创建一个示例目录并将handlebars.js复制到该目录,或直接下载(在 Linux 上):

wget https://raw.github.com/wycats/handlebars.js/master/dist/handlebars.js

如何做...

按照以下步骤进行:

  1. 创建index.html,其中包含一个标题,Handlebars 模板,用于渲染消息列表的占位符,以及一些列表样式:
<!DOCTYPE HTML>
<html>
<head>
<title>Rendering an array with EJS</title>
<style type="text/css">
.message {
    border-bottom:solid 1px #ccc;
    width: 250px;
    padding: 5px; }
.message p { margin: 0.5em 0; }
.message.unread { font-weight:bold; }    
.message .date {
    float: right;
    font-style: italic;
    color: #999; }
</style>
</head>
<body>
<h2>Messages</h2>
<div id="list">
</div>
<script id="template" type="text/x-handlebars-template">

{{#each list}}
    <div class="message {{status}}">
        <p><span class="name">{{name}}</span>
        <span class="date">{{date}}</span></p>
        <p class="text">{{text}}</p>
    </div>
{{/each}}

</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/handlebars.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js以在占位符元素中显示一个示例数组,使用template变量:
$(function() {
    var template = Handlebars.compile($('#template').html());
    $('#list').html(template({list:[
        { status: 'read',   name: 'John', date: 'Today',
            text: 'just got back, how are you doing?' },
        { status: 'unread', name: 'Jenny', date: 'Today',
            text: 'please call me asap' },
        { status: 'read',   name: 'Jack', date: 'Yesterday',
            text: 'where do you want to go today?' },
    ]}));
});

它是如何工作的...

Handlebars 有{{#each}}助手,它遍历作为第一个参数传递的数组。

在块内,数组元素的每个成员变量都进入当前作用域,并且可以直接通过名称访问。这个特性极大地简化了这个模板,因为它避免了在循环内重复变量名。

从这个示例中我们可以看到,我们不仅限于在元素内部使用变量,还可以在属性中间或 HTML 的任何其他地方使用它们。

使用 EJS 渲染数组

在使用模板语言时,最常见的任务之一是渲染项目列表。由于 EJS 是基于转义到 JavaScript 的,可以使用语言中的循环结构来渲染列表。

在这个示例中,我们将渲染一个消息对象列表。每个消息对象都有一个作者、到达时间、正文和阅读状态。我们将使用不同的样式来区分已读和未读的消息。

准备工作

我们需要从embeddedjs.com/下载 EJS,并在我们的recipe文件夹中提取ejs_production.js

如何做...

让我们开始吧。

  1. 创建index.html,其中包含一个标题,EJS 模板,用于渲染消息列表的占位符,以及一些列表样式:
<!DOCTYPE HTML>
<html>
<head>
<title>Rendering an array with EJS</title>
<style type="text/css">
.message {
    border-bottom:solid 1px #ccc;
    width: 250px;
    padding: 5px; }
.message p { margin: 0.5em 0; }
.message.unread { font-weight:bold; }    
.message .date {
    float: right;
    font-style: italic;
    color: #999; }
</style>
</head>
<body>
<h2>Messages</h2>
<div id="list">
</div>
<script id="template" type="text/ejs">
<% for (var k = 0; k < list.length; ++k) {
    var message = list[k];  %>
    <div class="message <%= message.status %>">
        <p><span class="name"><%= message.name %></span>
        <span class="date"><%= message.date %></span></p>
        <p class="text"><%= message.text %></p>
    </div>
<% } %>
</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/ejs_production.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 使用example.js中的render函数并传递一些文本数据:
$(function() {
    var template = new EJS({
        text: $('#template').html()
    });
    $('#list').html(template.render({list:[
        { status: 'read',   name: 'John', date: 'Today',
            text: 'just got back, how are you doing?' },
        { status: 'unread', name: 'Jenny', date: 'Today',
            text: 'please call me asap' },
        { status: 'read',   name: 'Jack', date: 'Yesterday',
            text: 'where do you want to go today?' },
    ]}));
});

它是如何工作的...

render函数中,我们传递一个包含消息数组的模型对象给渲染器。

要渲染数组,我们使用标准的 JavaScript for 循环。我们可以在开放和闭合标签之间添加任何有效的 JavaScript 代码。在我们的示例中,我们在循环体内赋值一个变量,然后在整个模板中使用它。

从示例中可以清楚地看出,EJS 允许你在模板文本的任何地方转义到 JavaScript。甚至在 HTML 属性中转义也是允许的(我们正在为消息添加一个与消息状态相对应的类,已读或未读),通过在class属性内转义。

还有更多...

这个示例表明 EJS 几乎和 JavaScript 本身一样强大。然而,不建议在模板内编写任何业务逻辑代码。相反,准备好你的模型对象,使模板代码编写起来更加直观。

使用 Jade 渲染数组

Jade 还支持将项目列表呈现为其他模板语言。我们可以使用each结构来迭代数组中的元素,并为每个元素输出一些 HTML 元素。

在这个示例中,我们将呈现一系列消息对象。每个消息对象都将有一个作者、到达时间、正文和阅读状态。我们将使用不同的样式来区分已读和未读的消息。

我们还将为奇数和偶数行使用不同的背景。

准备工作

我们需要在recipe文件夹中下载jade.min.js,可在github.com/visionmedia/jade中找到。

如何做...

按照以下步骤:

  1. 创建包含 CSS 样式、占位符和模板script元素的index.html
<!DOCTYPE HTML>
<html>
<head>
<title>Rendering an array with EJS</title>
<style type="text/css">
.message {
    border-bottom:solid 1px #ccc;
    width: 250px;
    padding: 5px; }
.message p { margin: 0.5em 0; }
.message.unread { font-weight:bold; }    
.message.odd { background-color:#f5f5f5; }
.message .date {
    float: right;
    font-style: italic;
    color: #999; }
</style>
</head>
<body>
<h2>Messages</h2>
<div id="list">
</div>
<script id="template" type="text/jade">

each msg,i in list
  .message(class=msg.status + (i % 2?' odd':' even'))
    p
      span.name=msg.name
      span.date=msg.date
    p.text=msg.text

</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/jade.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js来包装元素和模板与一些模型数据:
$(function() {
    var template = jade.compile($('#template').html());
    $('#list').html(template({list:[
        { status: 'read',   name: 'John', date: 'Today',
            text: 'just got back, how are you doing?' },
        { status: 'unread', name: 'Jenny', date: 'Today',
            text: 'please call me asap' },
        { status: 'read',   name: 'Jack', date: 'Yesterday',
            text: 'where do you want to go today?' },
    ]}));
});

它是如何工作的...

除了允许我们访问数组元素之外,Jade 中的each结构还可以提供元素的索引。

使用这个索引,我们演示了 Jade 可以支持任意表达式。我们为奇数编号的消息添加了一个奇数类,并为偶数编号的消息添加了一个偶数类。当然,最好使用 CSS 伪选择器来做到这一点,例如:

.message:nth-child(odd) { ... }
.message:nth-child(even) { ... }

Jade 允许我们省略元素的名称,只使用类和/或 ID 属性。在这些情况下,假定元素是div

我们可以在元素标签后附加 CSS 样式类和 ID。Jade 将为元素添加相应的属性。

还有更多...

我们可以传递一个包含要添加到元素的类数组的变量,而不是连接样式类。

使用 Handlebars 简化模板

在编写模板时,我们经常需要显示常见的视觉元素,例如警报、对话框和列表。这些元素可能具有复杂的内部结构,每次都编写模板将模型映射到这个结构可能是一个容易出错和重复的过程。

Handlebars 允许我们通过将常见元素的模板替换为调用助手来简化包含常见元素的模板的编写。

在这个示例中,我们将编写 Handlebars 助手来呈现链接、图像和无序列表。我们将显示一个包含姓名、照片和链接到其个人资料的人员列表。

准备工作

我们需要从github.com/wycats/handlebars.js下载 Handlebars。浏览器版本位于dist目录中。创建一个示例目录并将handlebars.js复制到该目录中,或者直接下载(在 Linux 上):

wget https://raw.github.com/wycats/handlebars.js/master/dist/handlebars.js

如何做...

按照以下步骤:

  1. 创建包含列表样式、列表占位符和列表模板的index.html。模板将利用我们的新助手:
<!DOCTYPE HTML>
<html>
<head>
<title>Helpers in Handlebars</title>
<style type="text/css">
li { padding:1em; }
li img { vertical-align:middle; }
</style>
</head>
<body>
<div id="list">
</div>
<script id="template" type="text/x-handlebars-template">

{{#ul list}}
    {{img image alt=name}} {{name}}
{{else}}
    No items found
{{/ul}}

</script>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/handlebars.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 实现助手,并在example.js中呈现模板:
$(function() {    
    Handlebars.registerHelper('ul', function(items, options) {
        if (items .length) return '<ul>' + items.map(function(item) {
            return '<li>' + options.fn(item) + '</li>';  
        }).join('') + '</ul>'
        else
            return options.inverse(this);
    });

    Handlebars.registerHelper('img', function(src, options) {
        return new Handlebars.SafeString('<img src="' + src
            + '" alt="'+ (options.hash['alt'] || '')
            + '" title="'+ (options.hash['title'] || '')
            + '">');
    });

    var template = Handlebars.compile($('#template').html());

    $('#list').html(template({list:[
        { name: 'John',  image: '1.png'},
        { name: 'Jack',  image: '2.jpg'},
        { name: 'Jenny', image: '3.jpg'},
    ]}));
});

它是如何工作的...

在我们的模板中,我们使用了两个新的助手,ul用于显示列表和img标签用于显示图像。

Handlebars 有两种不同类型的助手:常规和块。块助手以以下格式调用:

{{#helperName argument param=value otherparam=value}}
    body
{{else}}
    alternative
{{/name}}

当 Handlebars 遇到一个块时,它会调用它的块函数,该函数接受一个或两个参数:

function helper(argument, options) {
…
}

如果指定,第一个参数将传递给helper函数。如果第一个参数不可用,则options参数将成为第一个。

命名参数也是可选的,并在hash属性中作为options参数可用。

接下来是必需的块参数,在helper函数内部可用,称为options.fn。块参数是一个函数,它接受一个上下文并返回使用该上下文渲染块的结果

else块也是一个块函数(options.inverse)。它是可选的,可以省略。如果省略,将一个默认的空块函数作为options.inverse传递。

在我们的示例中,我们将列表内容传递给我们的ul助手。如果列表中有项目,这个助手在每个项目上使用常规块;否则,它使用替代块来显示空列表消息。

另一种类型的助手是常规助手,可以按照以下方式调用:

{{helperName argument param=value otherparam=value}}

普通助手的工作方式与块助手类似,只是它们不接收块参数。在我们的示例中,我们将alt文本作为命名参数传递给呈现的图像。

两种类型的助手都应返回呈现的 HTML。

在我们的example.js文件中,我们通过调用Handlebars.registerHelper注册了我们的两个新助手。这使它们可以用于需要呈现的所有后续模板。之后,我们可以对模板调用render,并使用我们的数据,这将调用助手来生成结果的 HTML:

<ul>
    <li> <img src="img/1.png" alt="John" title=""> John </li>
    <li> <img src="img/2.jpg" alt="Jack" title=""> Jack </li>
    <li> <img src="img/3.jpg" alt="Jenny" title=""> Jenny </li>
</ul>

在 Handlebars 中使用部分模板重用模板

Handlebars 部分模板是可以从其他模板中调用的模板,并带有特定的上下文。

部分模板的一个示例用途是用户登录框。这样的框将显示用户名、未读通知的数量,以及如果用户已登录则显示注销链接;否则将显示可在使用 Facebook 和 Twitter 时使用的常规登录选项。

当没有参数需要传递给助手或不需要复杂逻辑时,部分模板可以用来代替助手。当动态生成的内容量较小,HTML 量较大时,它们特别有用。这是因为在部分模板内部,可以直接编写 HTML,而无需将其转换为字符串。

在这个示例中,我们将使用部分模板来呈现一个分级对话模型。这个例子还表明,部分模板可以在自身内部递归地重复使用。

准备工作

我们需要从github.com/wycats/handlebars.js下载 Handlebars。浏览器版本在dist目录中。创建一个示例目录,并将handlebars.js复制到该目录,或直接下载(在 Linux 上):

wget https://raw.github.com/wycats/handlebars.js/master/dist/handlebars.js

如何操作...

让我们开始吧。

  1. 创建index.html,其中将包含对话占位符、主对话模板和递归部分线程模板:
<!DOCTYPE HTML>
<html>
<head>
<title>Partials in Handlebars</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="list" class="conversation">
</div>

<script id="thread-template" type="text/x-handlebars-template">
    <div class="message">
        <img src="img/{{image}}">
        <span class="name">{{from}}</span>
        <span class="date">{{date}}</span>
        <p class="text">{{text}}</p>
    </div>
    <div class="replies">
        {{#each replies}}
            {{> thread}}
        {{/each}}
    </div>
</script>

<script id="template" type="text/x-handlebars-template">
<h2>{{topic}}</h2>
{{> thread}}
<p><input type="button" value="Reply"></p>
</script>

<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/handlebars.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 为了给显示的消息添加样式,创建style.css并添加以下 CSS 代码:
* { box-sizing: border-box; }
.conversation { width: 70ex; }
.message {
    background-color:#f5f5f5;
    padding: 5px;
    margin:5px 0;
    float:left;
    clear: both;
    width:100%; }
.message p {
    margin: 0 0 0.5em 0; }
.message .name {
    font-weight: bold; }
.message img {
    float: left;
    margin-right: 1em}
.message.unread {
    font-weight:bold; }    
.message .date {
    margin-left:1em;
    float: right;
    font-style: italic;
    color: #999; }
.replies {
    margin-left:3em;
    clear:both; }
  1. 渲染将从example.js中进行:
$(function() {    

    Handlebars.registerPartial('thread', $("#thread-template").html());

    var template = Handlebars.compile($('#template').html());

    $('#list').html(template({
        topic: "The topic of this conversation",
        from: 'John',  
        image: '1.png',
        text: "I wrote some text",
        date: 'Yesterday',
        replies:[
            {from: 'Jack',
                image: '2.jpg',
                text: "My response to your text is favorable",
                date: 'Today' ,
                replies: [
                    {from: 'John',
                        image: '1.png',
                        text: "Thank you kindly",
                        date: 'Today'}
                ]},
            {from: 'Jenny',
                image: '3.jpg',
                text: "I'm also going to chime in",
                date: 'Today' }
        ]}));
});

工作原理...

这个示例中消息的数据结构是递归的。它包含消息的详细信息:用户名和用户照片、消息文本和消息日期。但它还包含对该消息的回复,这些回复本身也是消息。

为了呈现这个结构,我们编写了一个单个对话线程的部分模板,指定如何显示消息的详细信息,但也遍历所有的回复,并为每个回复调用自身。

然后从主模板中调用这个部分模板,得到完整的对话树。

Handlebars 部分模板是使用当前上下文中的变量调用的。部分与我们直接替换部分模板的调用一样工作,用部分模板的内容替换部分的调用:

{{> partial}}

还有更多...

部分模板可以用于头部、尾部、菜单,甚至是递归。将网站的大部分可重用部分拆分为部分模板是一种推荐的做法,以避免复制并使这些部分更容易更改和重用。

在 EJS 中使用部分模板重用模板

部分模板是需要从多个页面多次包含的较大的 HTML 部分。部分模板的常见用途包括头部、尾部、站点菜单、登录框、警报等。

最新版本的 EJS 不支持部分模板;它们已被移除。然而,还有另一种方法可以在模板中使用其他模板,即通过在数据模型中包含编译后的模板本身。

在这个食谱中,我们将使用 EJS 中的递归部分模板来呈现对话线程。

准备工作

我们需要从embeddedjs.com/下载 EJS,并在我们的recipe文件夹中提取ejs_production.js

如何做...

让我们开始吧。

  1. 创建index.html,其中包含对话占位符、主对话模板和递归部分线程模板:
<!DOCTYPE HTML>
<html>
<head>
<title>Partials in EJS</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="list" class="conversation">
</div>

<script id="thread-template" type="text/ejs">
    <div class="message">
        <img src="img/<%= thread.image %>">
        <span class="name"><%= thread.from %></span>
        <span class="date"><%= thread.date %></span>
        <p class="text"><%= thread.text %></p>
    </div>
    <div class="replies">
        <% thread.replies && thread.replies.forEach(function(reply) { %>
            <%= partial.render({thread:reply, partial:partial}) %>
        <% }); %>
    </div>
</script>

<script id="template" type="text/ejs">
<h2><%= thread.topic %></h2>
<%= partial.render({thread: thread, partial: partial}) %>
<p><input type="button" value="Reply"></p>
</script>

<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/ejs_production.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. style.css中添加必要的样式来呈现模板:
* { box-sizing: border-box; }
.conversation { width: 70ex; }
.message {
    background-color:#f5f5f5;
    padding: 5px;
    margin:5px 0;
    float:left;
    clear: both;
    width:100%; }
.message p {
    margin: 0 0 0.5em 0; }
.message .name {
    font-weight: bold; }
.message img {
    float: left;
    margin-right: 1em}
.message.unread { font-weight:bold; }    
.message .date {
    margin-left:1em;
    float: right;
    font-style: italic;
    color: #999; }
.replies {
    margin-left:3em;
    clear:both; }
  1. example.js中添加渲染代码:
$(function() {    
    var template = new EJS({
        text: $('#template').html()
    });

    var threadTemplate = new EJS({
        text:$("#thread-template").html()
    });
    $('#list').html(template.render({
        partial: threadTemplate,
        thread:{
            topic: "The topic of this conversation",
            from: 'John',  
            image: '1.png', text: "I wrote some text",
            date: 'Yesterday',
            replies:[
                {from: 'Jack',
                    image: '2.jpg',
                    text: "My response to your text is favorable",
                    date: 'Today' ,
                    replies: [
                        {from: 'John',
                            image: '1.png',
                            text: "Thank you kindly",
                            date: 'Today'}
                    ]},
                    {from: 'Jenny',
                        image: '3.jpg',
                        text: "I'm also going to chime in",
                        date: 'Today' }
        ]}}));
});

它是如何工作的...

消息线程是一个递归数据结构。它包含消息细节(如日期、用户和文本)以及回复,这些回复本身也是消息线程。

为了使部分模板对象在模板内可用,我们将其包含在传递的模型中。然后我们可以从模板中调用它,并将其进一步传递到递归部分线程模板的模型中。

这个部分模板显示消息细节,然后继续调用自身以呈现每个回复(如果有的话)。在每次调用中,我们传递部分模板对象,以便在下一次调用中可用。

当没有更多的线程需要呈现时,过程结束,得到一个完整的消息树:

它是如何工作的...

还有更多...

尽管 EJS 不再原生支持部分模板,但这个食谱展示了我们如何仍然可以在 EJS 模板之间重用。我们可以通过传递所有注册的部分模板的表格以及每个模型来轻松扩展到完整的部分支持。

在 Jade 中使用过滤器

Jade 过滤器是强大的功能,使用户能够在 Jade 模板中使用不同的标记。它们的主要用途是通过使用户能够为模板的特定部分使用适当的工具,使模板更加简洁。

在这个食谱中,我们将使用 Jade 过滤器将 markdown 嵌入到我们的模板中,并解释过滤器的工作原理。

准备工作

Jade 的客户端版本github.com/visionmedia/jade可以在jade.js文件中找到,但默认情况下不支持 markdown 过滤器。要添加对 markdown 的支持,我们需要编辑此文件并找到开始定义markdown过滤器的行:

  markdown: function(str){
    var md;

    // support markdown / discount
    try {
      ….
    }
    str = str.replace(/\\n/g, '\n');
    return md.parse(str).replace(/\n/g, '\\n').replace(/'/g,''');
  },

然后用以下函数替换它:

  markdown: function(str){
    str = str.replace(/\\n/g, '\n');
    return markdown.toHTML(str).replace(/\n/g, '\\n').replace(/'/g,''');
  },

这将通知 Jade 使用全局定义的 markdown 对象,我们将通过包含外部 markdown 脚本来提供。

如何做...

让我们开始吧。

  1. 创建index.html,其中包含我们模板的占位符和模板本身:
<!DOCTYPE HTML>
<html>
<head>
<title>Using the markdown filter in Jade</title>
</head>
<body>
<h2>Rendered markdown</h2>
<div id="list">
</div>
<script id="template" type="text/jade">

#header
  | Hello
#content
  :markdown
    # Jade-markdown hybrid
    **Jade** simplifies the writing of HTML markup and dynamic
    templates. However, its not very good at simplifying the
    writing textual content that combines headings, paragraphs
    and images.

    This is where the **markdown** filter steps in. The filter
    allows you  to write text documents and easily embed
    [links](http://google.com) or images such as:

    ![Google](https://www.google.com/images/srpr/logo3w.png)

    Because filters are post-processed by Jade, we can easily
    add dynamic content such as the current date:
    #{new Date().toString()} or model #{prop} passed to the
    template function and have it processed by Jade.

</script>
<script src="img/jquery.min.js"></script>
<script src="img/markdown.js"></script>
<script type="text/javascript" src="img/jade.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建简单的example.js文件将模板绑定到元素:
$(function() {
    var template = jade.compile($('#template').html());
    $('#list').html(template({prop: 'properties' }));
});

它是如何工作的...

当 Jade 遇到:markdown块时,它将块内找到的文本传递给我们之前创建的 markdownfilter函数。这个filter函数调用 markdown-js 的 HTML 方法来处理 markdown 并生成 HTML。

它是如何工作的...

在 Jade 中使用 mixin

与其他模板语言中的部分模板类似,Jade 的 mixin 是可以接受参数的较小的模板片段。当生成常见的 HTML 块时,如警报框、对话框和菜单时,mixin 非常有用。

在这个食谱中,我们将通过重新实现线程对话模板来比较 Jade 的 mixin 和其他模板语言中的部分模板。这是一个递归模板,用于呈现线程对话树。

准备工作

我们需要在我们的recipe文件夹中下载jade.min.js,可在github.com/visionmedia/jade找到。

如何做...

让我们开始吧。

  1. 创建index.html,其中包含对话占位符、主对话模板和递归部分线程模板:
<!DOCTYPE HTML>
<html>
<head>
<title>Mixins in Jade</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="list" class="conversation">
</div>

<script id="thread-template" type="text/jade">
</script>

<script id="template" type="text/jade">

mixin thread(thread)
  .message
    img(src=thread.image)
    span.name=thread.from
    span.date=thread.date
    p.text=thread.text
  .replies
    if thread.replies
      each reply in thread.replies
        +thread(reply)

h2=thread.topic
+thread(thread)
p
  input(type="button",value="Reply")

</script>

<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/jade.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
</body>
</html>
  1. 创建example.js将数据传递给模板:
$(function() {    
    var template = jade.compile($('#template').html());
    $('#list').html(template({
        thread:{
            topic: "The topic of this conversation",
            from: 'John',  
            image: '1.png',
            text: "I wrote some text",
            date: 'Yesterday',
            replies:[
                {from: 'Jack',
                    image: '2.jpg',
                    text: "My response to your text is favorable",
                    date: 'Today' ,
                    replies: [
                        {from: 'John',
                            image: '1.png',
                            text: "Thank you kindly",
                            date: 'Today'}
                    ]},
                    {from: 'Jenny',
                        image: '3.jpg',
                        text: "I'm also going to chime in",
                        date: 'Today' }
            ]}}));
});
  1. 创建style.css来为对话线程设置样式:
* { box-sizing: border-box; }
.conversation { width: 70ex; }
.message {
    background-color:#f5f5f5;
    padding: 5px;
    margin:5px 0;
    float:left;
    clear: both;
    width:100%;
}
.message p {
    margin: 0 0 0.5em 0; }
.message .name {
    font-weight: bold; }
.message img {
    float: left;
    margin-right: 1em}
.message.unread { font-weight:bold; }    
.message .date {
    margin-left:1em;
    float: right;
    font-style: italic;
    color: #999; }
.replies {
    margin-left:3em;
    clear:both; }

它是如何工作的...

消息线程是一个递归数据结构。它包含消息细节(如日期、用户和文本),还包括回复,这些回复本身就是消息线程。

为了渲染这个结构,我们写了一个 Jade 混合。混合以线程作为参数,并显示其属性以及线程顶部节点中的文本。

最后,如果线程对象中有回复,它会遍历所有这些回复,并递归地将每个回复作为参数调用自身。通过在混合名称前加上字符“+”来调用混合。

主模板显示顶级消息的主题。然后它调用混合与顶级线程,这导致完整线程树的渲染。

在 Jade 中使用布局和块

为了让我们轻松创建不同的模板,Jade 支持模板继承。Jade 的模板继承允许我们定义一个主布局模板,然后通过扩展主布局替换该模板的部分。

在这个示例中,我们将使用模板继承来模拟一个完整的网站,包含标题、菜单、一些内容和页脚。内容将分为两个页面。

准备就绪

Jade 的客户端版本不支持布局和块。指定要扩展的模板需要访问文件系统,这在浏览器中不可用。但是,我们可以使用 browserify 预编译模板。为此,我们将编写一个 browserify 插件,该插件注册了 Jade 文件的处理程序。

首先让我们安装先决条件。我们需要nodejs,可以从nodejs.org/下载。在命令提示符中安装 node 后,我们将安装 browserify Version 1(截至目前,Version 2 不支持插件):

npm install -g browserify@1

接下来,我们将为我们的示例创建一个新目录:

mkdir example && cd example

在该目录中,我们将安装 Jade(和 markdown 以添加 markdown 支持到 Jade):

npm install jade markdown

如何做...

按照以下步骤:

  1. 让我们编写 browserify 插件,browserify.jade.js
var jade = require('jade');
module.exports = function(browserify) {
    browserify.register('jade', function(tmpl, file) {
        var fn =  jade.compile(tmpl, {
            client: true,
            filename:true,
            path: __dirname
        });
        return ["var jade = require('jade/lib/runtime.js');",
                'module.exports=',fn.toString()].join('');
    });
};
  1. 然后创建index.html,在这种情况下,它是一个简单的占位符,用于模板填充的内容:
<!DOCTYPE HTML>
<html>
<head>
<title>Blocks and layouts in Jade</title>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div id="content">
</div>
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/example.min.js"></script>
</body>
</html>
  1. 然后我们可以创建example.js,它将加载两个模板,并使菜单点击呈现不同的模板:
$(function() {    
    var templates = {
        'layout':require('./layout.jade'),
        'example':require('./example.jade')
    };
    console.log(templates.layout.toString())
    $('body').on('click', 'a', function() {
        var template = templates[$(this).text().trim()];
        $("#content").html(template({}));
    });
    $("#content").html(templates.layout({}));
});
  1. 这是layout.jade的内容(也渲染菜单):
#header
 | Welcome to example.jade
ul.menu
  block menu
    li
      a layout
    li
      a example
.content
  block content
    div
      p This is the default page
#footer
  | Copyright notice and extra links here
  1. 将其添加到example.jade中,通过向menu块追加项目并替换content块来扩展layout.jade
extends layout
block append menu
  li
    a new item
block content
  :markdown
    Different content with *markdown* support.
    This means [linking](http://google.com) is easy.
  1. 让我们添加style.css,使其看起来漂亮:
* { box-sizing: border-box; }
#content {
    max-width: 800px;
    margin: 0 auto; }
ul.menu {
    background-color: #ccc;
    margin: 0; padding:0; }
ul.menu li {
    display:inline-block;
    border-top: solid 1px #ddd;
    border-left: solid 1px #ddd;
    border-right: solid 1px #bbb; }
ul.menu li a {
    display: inline-block;
    cursor:pointer;
    padding: 0.5em 1em; }
.content {
    padding: 1em;
    background-color:#f5f5f5; }
#header {
    background-color:#333;
    color: #ccc;
    padding:0.5em;
    font-size: 1.5em; }
#footer {
    margin-top: 0.5em;}
  1. 最后,通过在命令提示符中输入以下命令,将所有内容包装到example.min.js中:
browserify -p ./browserify-jade.js example.js -o example.min.js

它是如何工作的...

让我们从browserify-jade.js开始。

这个插件通过使用 browserify 注册新的文件扩展名'jade',并告诉它使用我们的转换函数处理它遇到的每个 jade 文件的内容来工作。这意味着它将拦截require('layout.jade')require('example.jade')

我们的插件函数使用 Jade 编译接收到的模板内容,然后编写生成的 JavaScript 函数的代码。但是,为了确保生成的模板可以使用 Jade 辅助函数,它还通过 require 包含runtime.js。该文件包含所有编译模板需要的基本 Jade 库(以这种方式添加将导致 browserify 将其包含在最终捆绑包中)。

块和继承的工作原理:

要使模板成为可继承的布局,我们只需要在其中放置命名块。在我们的示例中,layout.jade中有两个命名块 - 一个用于菜单,一个用于内容。

命名块允许我们扩展模板,然后用自己的内容替换部分内容。我们在example.jade文件中这样做。该文件继承自布局,使用“block append”在菜单块中追加一个新的菜单项,并完全用自己的 markdown 内容替换内容块。

提示

也可以通过简单地使用“block prepend”来在块前添加内容。

当我们运行browserify命令时,它会将模板和example.js合并成一个名为example.min.js的单个文件,我们在页面中引入它:

它是如何工作的...

结果是一个简单的页面,有一个标题、菜单、内容和一个页脚块。当菜单中的链接被点击时,适当的模板会被加载并在页面上呈现。

第十章:数据绑定框架

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

  • 创建具有数据绑定的基本 Angular 视图

  • 渲染列表和使用 Angular 控制器

  • Angular 中的路由、过滤器和后端服务

  • 使用 Angular 的客户端验证

  • 使用 Angular 指令制作图表组件

  • 为 Meteor.js 构建应用程序结构

  • Meteor.js 中的响应式编程和数据

  • Meteor.js 中的实时 HTML 和用户特定数据

  • Meteor.js 中的安全机制

介绍

在现代 Web 应用程序中,很多代码逐渐从服务器移动到浏览器。因此,出现了新的可能性和挑战。

其中一种可能性是即时自动数据绑定。客户端代码使我们能够将模型对象绑定到网页的部分。这意味着模型中的更改会自动且立即反映在显示该模型的视图元素中。

此外,代码组织方面也出现了挑战。JavaScript 没有提供足够的模块设施或代码组织模型,这在较大的浏览器应用程序中是必需的。

在本章中,我们将涵盖两个试图提供解决方案的完整框架。这些框架支持声明性的、数据绑定的方法来编写 Web 应用程序。同时,它们提供了模块化和组织设施,使我们能够为我们的代码提供清晰的结构,将其分离为模型、视图、控制器或视图模型和组件。

本章的前半部分将涵盖 Angular——这是来自 Google 的一个框架,提供客户端绑定,并可以与任何服务器端堆栈(Rails、Node.js、Django 等)一起使用。它提供了数据绑定和组织设施。在本章中,我们将做以下工作:

  • 使用数据绑定创建基本的 Angular 视图

  • 使用 Angular 控制器编写一个小型列表编辑应用程序

  • 为我们的应用程序添加验证

  • 使用 Angular 路由和过滤器创建一个简单的 Markdown 维基,并定义一个本地存储服务

  • 使用指令创建显示图表的组件

第二部分将涵盖 Meteor——一个完整的框架和平台,涵盖了客户端和服务器端,还提供了数据绑定和组织设施。Meteor 更多地是一种真正不同的构建 Web 应用程序的方式,我们将为其基础知识进行一些介绍:

  • 按照 Meteor 的风格构建应用程序

  • 响应式编程的基础知识,以及如何在 Meteor 中处理数据

  • 实时 HTML 和用户数据

  • 安全和认证

创建具有数据绑定的基本 Angular 视图

Angular.js 允许我们创建具有自动数据绑定的视图。这意味着我们可以指定模型对象,其属性将绑定到元素属性或内容。

自动数据绑定简化了编程。我们不需要添加事件监听器来监视元素内部的更改,然后通过添加类、更改属性或修改它们的内容来手动更新元素,我们只需简单地更改模型对象,元素将自动更新。

在这个示例中,我们将创建一个简单的货币转换器,使用固定的汇率将美元转换为英镑。

准备工作

Angular 可以通过 CDN 获得,因此无需下载。我们只需将其包含到我们的页面中。

如何做...

让我们编写 Angular 模板。

创建一个名为index.html的文件,其中包含以下代码:

<!doctype html>
<html>
  <head>
    <script src="img/angular.min.js"></script>
  </head>
  <body>
    <div ng-app>
      <label>Amount in USD:</label>
      $<input type="text" ng-model="usdValue" placeholder="Enter USD amount">
      <hr>
      <label>Amount in GBP:</label><span ng-show="usdValue"> £ {{usdValue * 0.65}}</span>
    </div>
  </body>
</html>

它是如何工作的...

我们的 HTML 页面不是用纯粹的标准 HTML 编写的。有很多新的属性添加进来,我们将在接下来的段落中解释它们。

ng-app属性告诉 Angular 我们页面的哪一部分应该由它管理。在我们的情况下,这是页面上唯一的div元素。我们也可以将这个标签放在 HTML 元素上,这样整个页面将由 Angular 控制。

div内部,我们有一个带有ng-model属性的输入元素,其值为usdValue。这个属性会导致 Angular 在视图模型中添加一个名为usdValue的新属性。当内容发生变化时,该属性的值将自动更新为输入字段的值。这个属性现在在视图中变为全局可用。

我们的span元素包含一个值为usdValueng-show属性。这个属性将导致span元素只在usdValue有一个非假值时显示。空值、空字符串、未定义和零是“假值”的例子,对于这些值,span元素将被隐藏。

最后,在span内部,我们既有货币,又有放置在双大括号内的表达式。由于这个表达式取决于usdValue变量的值,所以span元素的内容将在该值发生变化时自动更新。

结果绑定将span元素与输入字段连接起来。每当输入字段发生变化时,模型usdValue变量会自动更新。这反过来会导致span元素的可见性和内容自动更新。

还有更多...

ng属性在 Angular 中被称为属性指令。Angular 还允许你编写自己的属性指令。

注意

ng属性是非标准的,HTML 验证器在遇到它们时会抱怨。为了解决这个问题,你可以在它们前面加上data前缀。例如,data-ng-model将会验证通过,因为带有data前缀的自定义属性是符合标准的。

渲染列表和使用 Angular 控制器

Angular 允许我们通过代码与视图进行交互,让我们为视图设置一个控制器。控制器可以修改视图作用域(模型)并调用其他操作,比如后台服务。

在这个示例中,我们将使用一个控制器来编写一个简单的待办事项列表。

如何做...

让我们开始吧。

  1. 创建一个名为index.html的文件,用于显示任务列表、添加新任务的表单和隐藏所有任务的按钮:
<!doctype html>
<html ng-app>
<head>
<script src="img/angular.min.js"></script>
<script src="img/example.js"></script>
</head>
<body>
<div ng-controller="TodoListController">
    <ul>
        <li ng-repeat="task in tasks" ng-show="task.shown">
        <input type="checkbox" ng-model="task.complete">  
        {{task.text}}
        </li>
    </ul>
    <form ng-submit="addTask()">
        <input type="text" placeholder="Write a task here..." ng-model="taskToAdd">
        <input type="submit" value="Add">
    </form>
    <button ng-click="hideComplete()">Hide complete</button>
</div>
</body>
</html>
  1. 创建一个名为example.js的文件来定义我们待办事项列表的控制器:
function TodoListController($scope) {
    $scope.tasks = [
        {text: "Write a todo list",
            complete: false, shown: true },
        {text: "Save it to the backend",
            complete: false, shown: true },
    ];
    $scope.addTask = function() {
        $scope.tasks.push({
            text: $scope.taskToAdd,
            complete: false,
            shown:true
        });
        $scope.taskToAdd = "";
    };
    $scope.hideComplete = function() {
        $scope.tasks.forEach(function(t) {
            if (t.complete)
                t.shown = false;
        });
    };
}

它是如何工作的...

在这个例子中,我们声明 Angular 应用将成为我们整个页面,通过将ng-app属性设置为页面的根元素。

div元素代表我们的视图。这个元素有一个ng-controller属性,指定了视图的控制器。控制器是在窗口的全局作用域中定义的一个函数。

在这个视图中,我们使用ng-repeat指令来显示我们的任务列表。在这个列表中,有一个复选框表示任务的完成状态和任务的文本。

tasks变量属于视图的作用域。我们期望这个变量包含一个任务数组,每个任务都有一个text描述,在complete字段中有完成状态(与任务的复选框绑定),以及一个hidden标志。

页面底部是用于向列表添加任务的表单。使用ng-submit属性,我们声明该表单在提交时应执行addTask()函数。为了做到这一点,我们期望作用域包含addTask()函数。该表单中的text字段绑定到taskToAdd变量。

最后,为了隐藏已完成的任务,我们在页面上添加了一个按钮。通过将其ng-click属性的值设置为hideCompleted(),我们告诉按钮在点击时执行hideCompleted()函数。为此,我们期望视图的作用域包含一个hideCompleted()函数。

我们如何将必要的函数和一些数据附加到视图上?

为了做到这一点,我们可以使用在example.js中定义的控制器函数。当视图加载时,控制器函数被调用。

在这个控制器中,没有 DOM 操作代码或 DOM 事件绑定。相反,我们有一个由 Angular 传递给控制器的$scope参数。这个参数表示视图的变量作用域。控制器只是将变量(如tasks数组)附加到该作用域,变量立即对视图可用。

我们还将addTask()hideCompleted()函数附加到作用域上。内部的代码非常简单:

  • addTasktaskToAdd作用域变量的内容中推送一个新任务到列表中,然后将其值重置为空字符串。这将导致 Angular 更新任务列表并重置与taskToAdd绑定的input字段的内容。

  • hideCompleted遍历任务列表并为每个已完成的任务设置hidden标志。结果,ng-show指令会自动导致标记为hidden的任务在视图中被隐藏。

还有更多...

这个例子突出了使用自动数据绑定框架时的主要区别。

没有自动数据绑定,我们需要手动将数据传递给模板渲染函数,然后为操作设置事件绑定。操作将手动从视图中提取数据,进行实际的模型操作,然后再次调用渲染函数。这是一种命令式的模板方法。

使用自动数据绑定框架,在模板中指定其部分与模型对象的连接方式。然后,要更新视图,我们只需简单地操纵或更改模型,视图会自动更新以反映这些变化。这是一种声明式的模板方法。

这个配方中唯一的缺点是我们的控制器必须在全局变量作用域中声明。我们将在下一个配方中展示如何避免这种情况。

Angular 中的路由、过滤器和后端服务

为了更好地利用浏览器的后退按钮功能,以及允许用户复制和粘贴链接,Angular 提供了一个路由器模块。路由器的功能类似于服务器端的路由器,在 URL 的哈希后附加路径以及查询字符串参数。它将重定向用户到适当的控制器和视图对。

此外,为了在视图和服务器之间共享数据,我们需要为后端存储定义一个模块。我们将使用 HTML5 的localStorage——结果代码将非常相似。

在这个配方中,我们将构建一个简单的基于 markdown 的维基,它将页面存储在本地存储中。Angular 还支持过滤器,因此我们将编写一个 markdown 过滤器模块。

如何做...

让我们编写我们的 Angular 应用程序:

  1. 创建一个名为index.html的文件,用于托管 Angular 应用程序。它包括所有必要的脚本,并设置将托管视图的div元素:
<!doctype html>
<html ng-app="wiki">
<head>
<link rel="stylesheet" type="text/css" href="style.css">
<script src="img/markdown.js"></script>
<script src="img/angular.min.js"></script>
<script src="img/angular-sanitize.min.js"></script>
<script src="img/edit-controller.js"></script>
<script src="img/view-controller.js"></script>
<script src="img/storage.js"></script>
<script src="img/markdown-filter.js"></script>
<script src="img/app.js"></script>
</head>
<body>
<div id="main" ng-view>
</div>
</body>
</html>
  1. 为了显示 markdown,我们需要一个markdown过滤器。在名为markdown-filter.js的 Angular 模块markdown中定义 Angular 过滤器:
angular.module('markdown', []).filter('markdown', function() {
    return function(input) {
        return input ? markdown.toHTML(input) : ''
    };
});
  1. 为了存储维基页面,我们需要一个存储模块。在storage.js中定义一个工厂,用于在名为storage的模块内创建Storage对象:
angular.module('storage', []).factory('Storage', function() {
    var self = {};
    self.get = function get(id) {
        var page = localStorage["page-"+id];
        if (page) return JSON.parse(page);
        else return {id: id, text: null};
    };
    self.save = function save(page) {
        var stringified = JSON.stringify(page);
        localStorage["page-"+page.id] = stringified;
    };
    return self;
});
  1. 现在让我们在app.js中定义我们的维基应用。除了storagemarkdown模块,我们还将使用ngSanitize来显示不安全的 HTML。我们将定义两个路由,一个用于编辑,另一个用于查看页面:
var wwwApp = angular.module('wiki',
    ['storage', 'markdown', 'ngSanitize'])
    .config(['$routeProvider', '$locationProvider',
        function($routeProvider, $locationProvider) {
            $locationProvider
                .html5Mode(true).hashPrefix('!');
            $routeProvider.when('/edit/:page', {
                templateUrl: '../edit.html',
                controller: EditController
            })
            .when('/:page', {
                templateUrl: 'view.html',
                controller: ViewController
            })
        }]);
  1. 让我们在view.html中定义我们的查看模板。除了显示文章,它还应该提供一个编辑链接以及返回主页的链接:
<div ng-show="page.text"
    ng-bind-html-unsafe="page.text | markdown">
</div>
<br>
<a href="edit/{{page.id}}">Edit this page</a> -
<a href="./">Go to the start page</a>
  1. 现在让我们在view-controller.js中定义查看控制器。它应该从存储中加载显示的文章。
function ViewController($scope, $routeParams, Storage) {
    $scope.page = Storage.get($routeParams.page || 'index');
}
  1. edit.html中添加编辑模板:
<div class="edit">
    <div class="left">        
        <textarea ng-model="page.text"></textarea>
    </div>
    <div class="right"
        ng-bind-html-unsafe="page.text | markdown">
    </div>
</div>
<a ng-click="savePage()"
    href="../{{page.id}}">Save</a>
  1. 然后在edit-controller.js中定义编辑控制器;它应该从storage中加载页面,并定义savePage()方法来保存页面:
function EditController($scope, Storage, $routeParams) {
    $scope.page = Storage.get($routeParams.page);
    $scope.savePage = function() {
        Storage.save({id: $scope.page.id, text: $scope.page.text});
    };
}
  1. 最后,让我们通过在style.css中添加一些 CSS 来为事物增添一些样式:
* { box-sizing: border-box; }
#main { padding: 0em 1em; }
.edit .left {
    float:left;
    width: 50%;
    padding-right: 1em; }
.edit .right {
    float: right;
    width: 50%;
    padding-left: 1em; }
.edit textarea {
    width: 100%;
    min-height: 24em;}
.edit input {
    width: 70%; }
.edit {
    float:left;
    width: 100%;
    clear:both; }
  1. 要运行应用程序,为该目录运行一个 HTTP 服务器。假设您已安装了 Node.js(参见附录 A,“安装 Node.js 和使用 npm”),安装http-server,然后在app目录中运行它:
npm install -g http-server
http-server

  1. 将浏览器指向http://localhost:8080/以查看结果。

它是如何工作的...

前面的示例定义了一个简单的单控制器应用程序,因此它没有真正需要路由和模块化。另一方面,这个示例实现了一个更复杂的应用程序,具有多个视图和控制器,以及存储和过滤器模块。

我们的 Angular 应用程序始于app.js—定义为一个名为wiki的模块,与我们index.html文件中的html标签的ng-app属性相同。它包含了加载我们自定义的markdownstorage模块并设置控制器和视图的主要粘合代码。

要配置我们的应用程序,我们加载两个对象:$locationProvider$routeProvider

['$routeProvider', '$locationProvider',  function($routeProvider, $locationProvider) { … }]

前面的加载语法是数组语法,我们在数组的元素中定义要加载的模块名称;然后我们在数组的末尾定义接受这些模块作为参数的函数,并执行使用它们的代码。

我们使用locationProvider模块来启用html5mode,在这种模式下,每个 URL 看起来都像是作为单独的页面加载的,不包含任何哈希。这种模式需要 HTML5 浏览器历史 API。作为备用,我们定义了一个前缀!,在哈希之后和 URL 之前使用。

要定义我们的路由,我们使用routeProvider。我们声明任何/edit/:page URL 将由EditController处理,并使用edit.html模板显示。URL 的:page部分是一个URL参数,匹配任何文本—它将在控制器中可访问。我们还定义了一个/:page路由用于查看页面,由ViewController处理,并使用view.html模板。

view模板包含一个div元素,仅当页面文本被定义时才显示。我们使用ng-bind-html-unsafe指令来实现这一点。该指令允许我们将一个表达式绑定到元素,该表达式评估为任意 HTML,这正是我们需要markdown过滤器的地方。

要使用过滤器,我们使用管道字符:

ng-bind-html-unsafe="page.text | markdown"

页面的编辑链接位于页面底部,将我们带到该页面的编辑视图。同样,在编辑页面上,我们将 markdown 文本和生成的 HTML 绑定到不同的元素。结果是,更改文本区域会立即更新显示的 HTML,为我们提供生成页面的实时预览。

查看和编辑控制器都非常简单:第一个控制器从存储加载文章,而第二个控制器定义了一个save()函数,将文章保存回存储。

我们控制器中的新参数是它们接收的额外参数:

function EditController($scope, Storage, $routeParams) ...
function ViewController($scope, $routeParams, Storage) ...

这些参数导致 Angular 通过将它们作为控制器的参数传递来注入所请求的对象。在这种情况下,请求了Storage对象(在storage模块中定义),以及请求了内置的$routeParams对象。参数的顺序并不重要,重要的是它们的名称。我们可以通过使用数组语法来避免这种行为:

var EditController = ['$scope', 'Storage', '$routeParams',  function($scope, Storage, $routeParams) { … }]

使用上述语法,Angular 将按照数组中指定的顺序注入对象。

定义过滤器很简单。在markdown-filter.js中,我们定义了一个名为markdown的新模块。然后我们声明该模块将提供一个名为markdown的过滤器。要定义过滤器,我们定义一个构造并返回过滤器的函数。返回的过滤器应该是一个接受单个输入参数并返回过滤输出的函数。我们的markdown过滤器只是在输入参数上调用markdown.toHTML

storage对象在storage.js中以类似的方式定义。在这里,我们定义了一个名为storage的新模块。在这个模块中,我们为Storage对象定义了一个构造函数,提供了get()save()函数。然后我们可以通过添加一个名为Storage的参数在任何控制器中注入我们的存储。在 Angular 中,这些由工厂创建的可注入对象通常被称为服务

还有更多...

使用ng-bind-html-unsafe是不安全的,可能允许攻击者编写一个页面,注入窃取个人信息或代表用户执行其他操作的任意脚本。为了避免这种情况,应尽可能使用ngSanitize模块中的$sanitize服务来处理 HTML。

使用 Angular 的客户端验证

Angular 自己扩展了新的 HTML5 验证属性,并允许用户向模板添加错误条件。借助 Angular 的这些功能,我们可以向表单添加自定义错误消息和样式。

在这个配方中,我们将在 Angular 中创建一个简单的用户注册表单,然后我们将向表单添加一些验证规则。

如何做...

让我们执行以下步骤:

创建一个名为index.html的文件,其中包含注册表单和验证规则:

<!doctype html>
<html ng-app>
<head>
<script src="img/angular.min.js"></script>
<style type="text/css">
    form { display: block; width: 550px; margin: auto; }
    input[type="submit"] { margin-left: 215px; }
    span.err { color: #f00; }
    label { width: 120px; display:inline-block; text-align: right; }
</style>
</head>
<body>
<div>
    <form name="register">
        <p>
        <label for="user">User:</label>
        <input type="text" name="name" ng-model="user.name"
            required  ng-minlength="5" ng-maxlength="32">
        <span ng-show="register.name.$error.required" class="err">
            Required</span>
        <span ng-show="register.name.$error.minlength" class="err">
            Minimum 5 characters</span>
        <span ng-show="register.name.$error.maxlength" class="err">
            Maximum 32 characters</span>
        </p>

        <p>
        <label for="pass">Pass:</label>
        <input type="password" name="pass" ng-model="user.pass"
            required  ng-minlength="6" ng-maxlength="32"
            ng-pattern="/^(?=.*[a-zA-Z])(?=.*[0-9])/">
        <span ng-show="register.pass.$error.required" class="err">
            Required</span>
        <span ng-show="register.pass.$error.minlength" class="err">
            Minimum 6 characters</span>
        <span ng-show="register.pass.$error.maxlength" class="err">
            Maximum 32 characters</span>
        <span ng-show="register.pass.$error.pattern" class="err">
            Must have both letters and numbers</span>
        </p>

        <p>
        <label for="age">Age:</label>
        <input type="number" name="age" ng-model="user.age"
            required min="13">
        <span ng-show="register.age.$error.required" class="err">
            Required</span>
        <span ng-show="register.age.$error.min" class="err">
            Must be 13 or older</span>
        </p>

        <p>
        <label for="email">Email:</label>
        <input type="email" name="email" ng-model="user.email"
            required>
        <span ng-show="register.email.$error.required" class="err">
            Required</span>
        <span ng-show="register.email.$error.email" class="err">
            Not a valid email address</span>
        </p>

        <p>
        <label for="url">Website:</label>
        <input type="url" name="website" ng-model="user.website"
            required>
        <span ng-show="register.website.$error.required" class="err">
            Required</span>  
        <span ng-show="register.website.$error.url" class="err">
            Not a valid website URL</span>
        </p>

        <input type="submit" value="Register" ng-disabled="register.$invalid">
    </form>
</div>
</body>
</html>

它是如何工作的...

Angular 通过扩展内置的 HTML5 验证规则和新添加的规则和属性来添加验证支持。让我们看看我们在表单中使用的这些规则:

我们的第一个字段是用户的用户名。除了 HTML5 的required属性外,我们还使用了两个验证指令:ng-minlengthng-maxlength来指定用户名的最小和最大长度。

Angular 的另一个增强是能够从其他独立元素中访问模板中的当前验证状态。错误跨度显示验证错误。但是,只有在发生相应的验证错误时才会显示它们。

要访问验证状态,我们可以使用以下格式:

<formName>.<fieldName>.$error.<checkName>

例如,要检查register表单中的user字段是否有minlength错误,我们可以使用以下属性:

register.user.$error.minlength

同样,我们可以使用number输入字段,并使用minmax属性检查数字是否在指定范围内。相应的$error字段分别为$error.min$error.max

对于电子邮件和 URL 输入,我们可以分别使用$error.email$error.url字段。

最后,在表单的末尾,在我们的提交按钮中,如果一个字段中有错误,我们使用ng-disable来禁用表单的提交。要检查错误,我们可以简单地使用以下语法:

<formName>.$invalid

在我们的情况下,如下所示:

register.$invalid

如果任何字段中的任何验证规则生成错误,则上述代码将返回true

使用 Angular 指令制作图表组件

Angular 指令允许我们以非常强大的方式扩展 HTML 语法,通过添加新的属性和元素。这使我们能够创建感觉本地的组件:从日期和时间选择器到数据网格、图表和可视化。

这样的组件可以在不向我们的控制器添加初始化代码的情况下重复使用。我们只需告诉组件它应该绑定到哪个模型,它将自动更新其外观以反映模型中的任何更改。

在这个配方中,我们将使用 Flot 制作一个图表指令来绘制我们的图表。在这个过程中,我们将了解一些 Angular 指令的强大功能。

准备工作

我们需要从www.flotcharts.org/下载 Flot 并将 ZIP 存档解压缩到我们的配方目录中,创建一个名为flot的子目录。

如何做...

让我们写代码。

  1. 创建一个名为index.html的文件。它将包括所有必要的脚本和一个使用我们的chart指令显示图表的视图。
<!doctype html>
<html ng-app="chart">
<head>
<script src="img/angular.min.js"></script>
<script src="img/jquery.min.js"> </script>
<script src="img/jquery.flot.js"></script>
<script src="img/random.js"></script>
<script src="img/chart.js"></script>
<script src="img/controller.js"></script>
<script src="img/app.js"></script>
</head>
<body>
<div id="main" ng-controller="Controller">
    <chart style="display:block; width:800px; height:200px;"
        data="chart.data" options="chart.options">
</div>
</body>
</html>
  1. 要实现控制器,创建一个名为controller.js的文件,它将设置图表数据和选项。此外,它将每 50 毫秒使用随机生成的点更新图表数据:
function Controller($scope, $timeout) {
    $scope.chart = {
        data: [getRandomData()],
        options: {lines: {fill:true}}
    };
    setInterval(function updateData(delay) {
        $scope.$apply(function() {
            $scope.chart.data[0] = getRandomData();
        });
    }, 50);
}
  1. 要创建一个随机数据生成函数,创建一个名为random.js的文件,并添加以下代码:
(function() {
    var data = [], maximum = 200;
    window.getRandomData = function getRandomData() {
        if (data.length)
            data = data.slice(1);
        while (data.length < maximum) {
            var previous = data.length ? data[data.length - 1] : 50;
            var y = previous + Math.random() * 10 - 5;
            data.push(y < 0 ? 0 : y > 100 ? 100 : y);
        }
        var res = [];
        for (var i = 0; i < data.length; ++i)
        res.push([i, data[i]])
        return res;
    }
}());
  1. 最后,在名为chart.js的文件中编写chart指令:
angular.module('chart', []).directive('chart', function() {
    var dir = {};
    dir.restrict = 'E';
    dir.scope = {
        data: '&',
        options: '&'
    }
    dir.link = function(scope, el, attrs) {
        console.log(scope)
        var data = scope.data(),
            opts = scope.options(),
            flot = $.plot(el, data, opts);
        function updateOnData(newdata) {
            data = newdata;
            flot.setData(data);
            flot.setupGrid();
            flot.draw();
        };
        function updateOnOptions(options) {
            opts = options;
            flot = $.plot(el, data, opts);
        }

        scope.$watch('data()', updateOnData, {objectEquality: true});
        scope.$watch('options()', updateOnOptions, {objectEquality: true});
    }
    return dir;
});

工作原理...

这是一个相当常规的 Angular 应用,其中有一个带有控制器的div元素。控制器在作用域中设置了一个新对象。

控制器中的setInterval调用值得特别一提。我们试图在 Angular 的浏览器事件循环之外修改作用域对象。

提示

浏览器事件循环是一种等待和分发事件的编程构造。此类事件包括鼠标和键盘事件,由setTimeoutsetInterval设置的超时和间隔,脚本加载,图像加载或xmlhttprequest完成等。

Angular 注册在事件循环中的所有函数都被包装在一个作用域应用包装器中,通知作用域在执行后应检查自身更新。然而,Angular 之外的函数,如setTimeoutsetInterval,不会进行此包装,我们必须使用 Angular $scope对象上的$apply函数手动进行包装($scope.$apply)。

chart指令工厂定义在chart模块内。该工厂创建指令,这是一个对象。让我们解释一下这个对象的属性:

  • directive.restrict:这将指令限制为特定类型。E表示指令限制为元素。此外,还有三种可能性可用:A表示属性,C表示 CSS 类,M表示特殊注释形式。

  • directive.scope:这允许我们配置定义指令本地(隔离)范围的属性。我们可以使用不同的特殊字符来导入不同类型的内容:

  • &字符表示将属性解释为表达式。它允许我们设置任意单向绑定,并监视表达式进行更新。要获取表达式的值,我们需要将导入的作用域变量作为函数调用。

  • =字符表示将属性解释为另一个作用域的变量。这允许我们设置双向数据绑定。

  • @字符表示将属性解释为字符串值。返回属性的字符串值。

  • directive.link:这用于将指令与新元素链接。对于指令的每个实例(在我们的例子中是每个元素),这只会执行一次。它允许我们定义执行渲染新图表的代码,以及设置范围观察以更新图表。它使用scopeelementattribute参数进行调用。

在我们的例子中,chart指令被限制为元素。因为我们不需要双向数据绑定,使用&,我们将dataoptions属性都解释为表达式。这允许使用过滤器和其他操作,这非常有用,而且=解释不提供这个功能。

link函数内,我们渲染初始图表。因为我们将两个属性都解释为表达式,所以需要将它们作为函数调用以获取值。

提示

与 Angular 控制器不同,我们的link函数的参数顺序很重要,总是:scopeelementattributes。这是因为它们不是由 Angular 依赖注入系统处理的。

为表达式设置观察也略有不同——观察字符串是一个函数调用。

dataoptions都是复杂对象,其内容可以在不改变对象本身的情况下进行修改。因此,我们需要向watch函数传递第三个参数,该参数指定在比较监视表达式的值时应使用对象相等性。默认值是检查对象引用,这对我们的图表不起作用。

dataoptions被修改时,我们重新渲染我们的图表。我们的chart元素现在是完全动态的,可以从每 50 毫秒更新一次数据点的示例中看出。这些更新立即反映在图表上。

还有更多...

除了directive.link属性之外,还有directive.compile。即使有多个实例,它也只被调用一次。它允许我们在元素内部转换模板并在指令内部包含内容。还有更多属性可用-详细文档可以在官方网站的 Angular 指南中找到angularjs.org/

为 Meteor.js 构建应用程序的结构

Meteor.js 的第一个承诺是它是构建 Web 应用程序的更快方式。今天使用的大多数 Web 框架都在同一机架上拥有 Web 服务器和数据库,并将渲染的 HTML 发送到浏览器。它们都使用标准的请求和响应式开发。

如今,我们还有很多智能客户端:在浏览器中运行的 JavaScript 应用程序或 Android 或 iOS 中的本机客户端。所有这些都与云连接;它们都以某种方式与 Google、Facebook、Twitter 或 Amazon 对齐。

Meteor 提供了一种围绕智能包构建代码的新方法,这些代码模块可以在客户端或服务器端执行,甚至两者兼而有之。开发人员可以选择他们将在应用程序中使用的智能包。Meteor 将创建一个准备好成为云一部分的捆绑包。在这个示例中,我们将构建一个非常基本的 Meteor 应用程序,以了解 Meteor 的几乎所有功能。关于 Meteor 的一个重要事项是,它仍在不断发展中,应该作为这样对待。

准备工作

在撰写本文时,官方支持的平台是 Mac OS 和 GNU/Linux。有一个 Windows 的预览安装程序可用作 MSI 安装程序包,网址为win.meteor.com/,它应该具有相同的功能,但有一些更多的错误和一个不舒服的 shell。官方对 Windows 的支持计划在未来实现,所以这不应该是一个大问题。

对于 Linux 和 Mac,安装是通过命令行完成的:

curl https://install.meteor.com | /bin/sh

这个命令将运行并将 Meteor 安装到您的计算机上,但它只适用于 Debian 和 RedHat 类别的发行版。如果您的操作系统不属于这些类别,就没有必要担心,可能已经有一个 Meteor 的软件包已经包含在您的发行版存储库中,但它可能会落后几个版本。

Meteor 是建立在 Node.js 之上的,并使用它自己的系统来管理包。默认情况下,它还使用 MongoDB 作为数据库。

如何做...

  1. 安装了 Meteor 后,我们可以开始创建一个名为simple的应用程序:
meteor create simple

这将创建一个名为simple的文件夹,在其中我们应该有名为simple.htmlsimple.jssimple.css的文件,以及一个名为.meteor的子文件夹。

  1. 要启动应用程序,只需在文件夹中键入meteor
meteor
[[[[[ /the-example-location/simple ]]]]]
Running on: http://localhost:3000/

它是如何工作的...

在深入代码之前,我们将看一些 Meteor 背后的想法。创建者用他们的七大原则来推广这个框架,这些原则大多符合您的期望:

Meteor 的七大原则

数据传输。不要通过网络发送 HTML。发送数据,让客户端决定如何呈现它。

一种语言。在 JavaScript 中编写接口的客户端和服务器部分。

数据库无处不在。使用相同的透明 API 从客户端或服务器访问数据库。

延迟补偿。在客户端,使用预取和模型模拟,使其看起来就像您与数据库有零延迟的连接。

全栈响应性。使实时成为默认。从数据库到模板的所有层都应该提供事件驱动的接口。

拥抱生态系统。Meteor 是开源的,集成而不是替代现有的开源工具和框架。

简单等于高效。使某物看起来简单的最好方法是使其实际上变得简单。通过干净、经典美观的 API 来实现这一点。

其中一些原则被夸大了,但尽管如此,Meteor 肯定是构建 Web 应用程序的一种新方式。

让我们回到生成的代码,并从simple.js开始:

if (Meteor.isClient) {
  Template.hello.greeting = function () {
    return "Welcome to simple.";
  };

  Template.hello.events({
    'click input' : function () {
      // template data, if any, is available in 'this'
      if (typeof console !== 'undefined'){
        console.log("You pressed the button");
      }
    }
  });
}
if (Meteor.isServer) {
  Meteor.startup(function () {
    // code to run on server at startup
    });
}

提供Meteor.isServerMeteor.isClient变量,以便根据代码是在客户端还是服务器上运行来更改行为。

如果我们在simple.jsserver部分添加console.log("I'm running"),我们可以注意到服务器控制台重新加载服务器:

 I'm running

这基本上是我们在 Meteor 中创建服务器代码的方式,我们可以选择是要一个单个文件还是一堆其他文件。Meteor 收集我们项目树中的所有文件,除了serverpublic子目录。它们被最小化,并且被提供给每个客户端。

与 Node.js 创建异步回调的方式不同,Meteor 使用单个线程处理每个请求,这意味着它应该会导致更易维护的代码。

如果我们看一下simple.html,我们有一个简单的模板,使用了simple.js中的客户端代码,其中使用了适当的Template.hello.events事件和Template.hello.greeting中的数据:

<head>
  <title>simple</title>
</head>
<body>
  {{> hello}}
</body>
<template name="hello">
  <h1>Hello World!</h1>
  {{greeting}}
  <input type="button" value="Click" />
</template>

我们暂时不会深入讨论模板背后的细节,但这个基本示例应该很简单。如果我们在http://localhost:3000上已启动的应用程序中打开浏览器,我们可以看到数据加载到模板中。当我们点击按钮时,将调用console.log("You pressed the button")函数,并且消息应该显示在控制台中。请注意,这应该是浏览器的控制台,而不是服务器控制台,因为该部分设置为在客户端运行。

处理敏感数据的代码部分,例如令牌或密码,应该只是服务器的一部分,这可以通过将该代码放在名为server的文件夹中轻松实现。在生产模式下,CSS 文件和 JavaScript 被打包并捆绑后提供给客户端。在开发过程中,它们被单独发送以简化调试。

您可能已经注意到,向客户端提供的 HTML 文件与我们在应用程序文件夹中的文件有些不同且更大。这是因为 Meteor 扫描 HTML 文件以查找顶级元素<head><body><template>template部分被转换为可以从Template.*命名空间调用的 JavaScript 函数。至于<head><body>元素,它们分别被连接在一起,并且自动包括了 DOCTYPE 和 CSS 等其他部分。

还有更多...

如果我们需要 Meteor 提供一些静态文件,例如图标图片pdf文件,或者例如robots.txt,我们可以使用public目录。应用程序的根目录是public文件夹的根目录;例如,如果我们有一个名为meme.png的文件,它将可以通过http://localhost:3000/meme.png访问。

以下是一个简单的目录结构:

`-- simple
    |-- public
    |   `-- meme.png
    |-- simple.css
    |-- simple.html
    `-- simple.js

Meteor.js 中的响应式编程和数据

Meteor 使用 NoSQL 文档导向存储,默认使用 Mongo DB。名称来自单词"humongous",意思是非常大。该数据库是 NoSQL 数据库家族的一部分,这意味着它不像传统关系数据库那样存储数据。Mongo DB 以类似 JSON 的文档格式持久化数据,使得与基于 JavaScript 的框架集成变得更加容易。在这个示例中,我们将看到如何从 Meteor 中使用数据库,以及数据访问是如何被编排的。

准备工作

示例文件中有一个icon.png图像;除此之外,只需要在您的计算机上安装 Meteor 并打开命令行。

如何做到这一点...

  1. 首先,我们可以从命令行开始创建名为movies的应用程序:
>meteor create movies

为了简化生成的结构,我们将创建两个文件夹:一个名为server,另一个名为clientmovies.cssmovies.jsmovies.html文件可以放在client目录中,因为我们将在那里放置与客户端相关的代码。

  1. server目录中,我们创建一个名为bootstrap.js的文件,它将使用我们定义的少量对象来初始化数据库:
Meteor.startup(function () {
  if (Movies.find().count() === 0) {
    var data = [
      {
        name: "North by northwest",
        score: "9.9"
      },
      {
        name: "Gone with the wind",
        score:"8.3"
      },
      {
        name: "1984",
        score: "9.9"
      }
    ];

    var timestamp = (new Date()).getTime();
    for (var i = 0; i < data.length; i++) {
      var itemId = Movies.insert({
        name: data[i].name,
        score: data[i].score,
        time: timestamp
      });
    }
  }
});
  1. 你可能会想知道的第一件事是,这个Movies对象是什么?这是一个我们将在不同文件中定义的集合,可以称为publish.js,因为我们将从服务器上发布该集合。该文件将包括以下内容:
Movies = new Meteor.Collection("movies");
Meteor.publish('movies', function () {
  return Movies.find();
});
  1. 至于客户端,我们已经生成了文件,所以我们开始创建一个简单的 HTML 和一个 handlebar 模板。在模板内部,我们将遍历电影并打印出一个包含电影名称和评分的元素列表。此外,在模板中,我们放置一个包含对图像的引用的按钮:
<body>
  <div id="main">
      {{> movies}}
  </div>
</body>

<template name="movies">
  <h3>List of favorite movies</h3>
  <div id="lists">
    <div>
      <ul>
        {{#each movies}}
          <li><b>{{name}}</b>  {{score}}<li/>
        {{/each}}
      </ul>
      <button>
        <img src="img/icon.png" width="30px" height="30px" />
      </button>
    </div>
  </div>
</template>

为了使icon.png图像作为静态文件可用,我们需要创建一个名为public的文件夹,并将图像放在其中。这遵循约定优于配置的原则,大多数情况下,你没有真正需要不遵循它。

  1. 至于客户端,在之前生成的movies.js文件中,我们应该自动订阅电影的servers集合。此外,我们将添加一个功能来填充movies变量,并为按钮添加一个事件,该事件将触发保存一个随机的新电影:
// Define mongo style collections to match server/publish.js.
Movies = new Meteor.Collection("movies");

// Always be subscribed to the movies list.
Meteor.autorun(function () {
    Meteor.subscribe('movies');
});

// fill the movies variable with data from the collection sorted by name
Template.movies.movies = function () {
  return Movies.find({}, {sort: {name: 1}});
};

// on click we insert a random movie
Template.movies.events({
  'click button': function(){
    Movies.insert({
      name: "random awesome movie",
      score: Math.random() * 10
    });
  }
});
  1. 现在一切应该都正常工作了。在使用meteor启动应用程序后,我们可以在默认端口http://localhost:3000/上在浏览器中访问它。如果我们想要更改应用程序运行的端口,例如在端口3333上,我们可以使用以下命令:
meteor --port 3333

它是如何工作的...

首先,我们可以从数据开始,如果服务器正在运行,我们可以打开另一个控制台,在那里我们可以访问相同的目录。然后,在控制台中打开相同的文件夹后,我们运行以下命令:

meteor mongo
MongoDB shell version: 2.2.3
connecting to: 127.0.0.1:3002/meteor

这打开了一个简单的控制台,我们可以在其中查询我们的数据库。Mongo 将数据存储为集合,为了获取所有可用电影的名称,我们可以使用以下命令:

> db.getCollectionNames()
[ "movies", "system.indexes" ]

movies集合是我们在bootstrap.js初始化中定义的集合;至于system.indexes,它是一个包含数据库所有索引的集合。要使用该集合操作数据,我们可以使用ensureIndex()dropIndex()

在控制台中,我们可以分配以下变量:

> var x = db.getCollection("movies");
> x
meteor.movies

可以使用find()查询集合;如果我们尝试在没有参数的情况下调用它,它将返回所有元素:

> x.find();
{ "name" : "North by northwest", "score" : "9.9", "time" : 1360630048083, "_id" : "bc8f1a7a-71bd-49a9-b6d9-ed0d782db89d" }
{ "name" : "Gone with the wind", "score" : "8.3", "time" : 1360630048083, "_id" : "1d7f1c43-3108-4cc5-8fbf-fc8fa10ef6e2" }
{ "name" : "1984", "score" : "9.9", "time" : 1360630048083, "_id" : "08633d22-aa0b-454f-a6d8-aa2aaad2fbb1" }
...

数据是基本的 JSON,易于使用 JavaScript 进行操作。如果你看一下对象,你会注意到"_id" : "08633d22-aa0b-454f-a6d8-aa2aaad2fbb1"键值对。这是由 Mongo 生成的唯一键,我们用它来引用和操作该对象,通常称为文档

如果我们想删除 ID 为beef20a3-c66d-474b-af32-aa3e6503f0de的记录,我们可以使用以下命令:

> db.movies.remove({"_id":"beef20a3-c66d-474b-af32-aa3e6503f0de"});

之后,我们可以调用db.movies.find()来查看一个现在缺失了。还有很多其他用于数据操作的命令,但大多数都很直观,你可以根据它们的名称轻松猜到。作为一个快速提醒和学习工具,有一个可以调用的help函数:

>help
>db.help()

这两个命令会列出命令列表,并简要解释每个命令的作用。你不应该被命令的数量所压倒,因为我们不会使用大部分命令,但它仍然是一个很好的参考。

注意

有关 MongoDB 命令的更详细的教程,请访问mongodb.org并单击TRY IT OUT以尝试在线 shell。网络上有大量关于 NoSQL 的资源,但Martin Flower做的一个很好的介绍可以在www.youtube.com/watch?v=qI_g07C_Q5I上找到。

如果我们打开浏览器,我们可能会注意到每次点击“随机”按钮时,都会立即添加一条新记录。这看起来非常快,不仅仅是因为服务器在本地运行。每当客户端向服务器发出写入请求时,它会立即更新本地缓存,而无需等待服务器的响应。当服务器接收到请求并接受更新时,客户端在屏幕上不需要做任何事情。这应该是大多数情况下发生的,它节省了往返等待时间,使屏幕更具响应性。另一方面,如果服务器拒绝更新,客户端的缓存将被更新为正确的结果。

在 Meteor 中,为了访问数据库,客户端和服务器使用相同的 API。在框架的每个设计决策中,都强调减少往返服务器的时间。请求和响应以及消息失效都被编排为这样做。

我们在movies.js中使用autorun自动从服务器获取更新:

Meteor.autorun(function () {
    Meteor.subscribe('movies');
});

autorun函数中的代码块是所谓的响应上下文,使我们能够以命令式风格编写代码,但获得响应式行为。

响应式编程是围绕变化传播的编程范式之一。在命令式编程中,如果我们有一个表达式,比如z = x + y,这意味着x + y的计算结果将被分配给z,如预期的那样。例如,如果我们有x = 42y = 13,那么z = 42 + 13z = 55xy的值以后可以更改,例如,它们可以更改为x=4y=4,但这不会以任何方式影响z,它仍然是55

这方面的最简单的例子是现代的电子表格程序,比如 Microsoft Excel 或 Google 文档电子表格。电子表格单元格通常包含文字值,例如数字,或者包含从其他单元格派生值的公式。在我们的单元格C3中,我们可以有公式"=A1+B1",这意味着当我们更改A1B1中的一些值时,C3将自动更新。

在 MVC 架构中,可以使用响应式编程进行简化,从视图自动传播变化到模型,然后返回,这在实时系统中非常有益。

使用响应上下文可以避免我们编写一整套调用。在我们的例子中,我们首先需要取消订阅当有变化发生,然后再次订阅以从服务器获取数据。这减少了大量可能会出现错误的代码,并增加了维护阶段的复杂性。

注意

除了Meteor.autorun,响应上下文还应用于Templates以及Meteor.renderMeteor.renderList函数。

至于可以触发更改的数据源,我们可以使用数据库 collectionssession 变量,以及与身份验证和授权相关的一些其他函数。您可以在 Meteor 关于响应性的文档中找到更多详细信息 docs.meteor.com/#reactivity

如果您同时打开两个不同的浏览器,您可能会注意到即使会话不同,也会显示相同的数据。为了拥有特定于用户的数据,我们将在下一个示例中创建一个示例。

您可能希望将整个集合发送到客户端,但首先要仔细考虑客户端实际需要的是什么。通常,只发送特定字段而不是整个文档可能更明智。为了降低网络流量,客户端的某些部分可以关闭订阅,对于这些部分的文档将从本地缓存中删除,除非在其他活动订阅中使用。

还有更多...

因为我们使用的数据存储在数据库中,如果我们使用某些外部应用程序更改了那里的数据,它也会触发对客户端的更改。在下一个示例中,我们将看到如何允许多个用户为每个用户拥有自己的收藏列表,而不是一个单一的全局列表。

Meteor.js 中的实时 HTML 和特定于用户的数据

您可能已经注意到在上一个示例中,我们使用的数据是全局的,而不是特定于用户的。在这个示例中,我们将看到如何创建会话数据,并深入研究模板以及与其关联的数据。为了演示这一点,我们将创建一个小型的图像投票应用程序,用户将被提示输入名称,然后他们将获得 50 点,可以用于对图像进行投票。

准备工作

为了使示例更简单,我们将从我们的 public 目录静态提供图像,这样您就可以下载示例代码中的样本图像,或者使用您自己的图像。

如何做...

  1. 我们像任何其他普通的 Meteor 应用程序一样开始:
>meteor create gallery

  1. 因为在这个示例中我们将使用更多的代码,所以创建一个带有 public 文件夹用于静态文件,以及 serverclient 文件夹用于服务器和客户端代码是有意义的。之后,我们可以将生成的画廊文件移动到 client 文件夹,并将图像添加到 public 文件夹中。为了简单起见,图像将被命名为 1.jpg2.jpg3.jpg,以及猜猜看,4.jpg。然后我们继续在 server 文件夹中创建一个 bootstrap.js 文件:
// if the database is empty fill it with data
Meteor.startup(function () {
  //has some images
  if (Images.find().count() < 4) {
    var images =[
      {
        name: "Awesome Cat",
        url: "img/1.jpg",
        votes: "0"
      },{
        name:"Cool Cat",
        url: "img/2.jpg",
        votes: "0"
      },{
        name:"Mjauuu",
        url: "img/3.jpg",
        votes: "0"
      },{
        name:"The Cat",
        url: "img/4.jpg",
        votes: "0"
      }
    ];

    for (var i = 0; i < images.length; i++) {
      Images.insert(images[i]);
    }

    Users.insert({
      name: "awesome user",
      pointsLeft: "30"
    });
  }
});
  1. 这将使用一个简单的用户初始化数据库,并添加一些关于图像的数据,还添加一个条件,只有在数据库中少于四张图像时才会发生这种情况。

注意

您可能会注意到我们使用 for 循环来插入数据,但自 MongoDB 2.2 版本以来,db.collection.insert() 函数可以接受一个元素数组,并将它们批量插入到集合中,但我们没有使用这种方法,因为它会导致稍微复杂的结构,我们希望选择最简单的情况。您可以在 docs.mongodb.org/manual/reference/method/db.collection.insert/ 上阅读更多关于 db.collecton.insert() 的信息。

  1. 之后,我们可以继续定义和发布集合,使集合在客户端可用:
// DB collection of movies
Images = new Meteor.Collection("images");

// DB collection of users
Users = new Meteor.Collection("users");

// Publish complete set of lists to all clients.
Meteor.publish('images', function () {
  return Images.find();
});

// Publish for users
Meteor.publish('users', function () {
  return Users.find();
});
  1. 现在我们可以继续在 gallery.html 中编写模板代码:
<body>
  <div class="box">
    {{> main}}
  </div>
  {{> footer}}
</body>

<template name="footer">
  <footer>
    {{footerText}}
  </footer>
</template>
  1. main 模板将检查当前是否有用户。如果有,它将显示投票,否则,它将显示一个简单的表单以输入名称:
<template name="main">
  {{#if hasUserEnteredName}}
    {{> votes}}
    {{> gallery}}
  {{else}}
  <label>Please insert your name
    <input name="name">
    </input>
    <button class="name">start</button>
    </label>
  {{/if}}
</template>
  1. votes 模板将显示用户剩余的投票数,画廊将显示图像以及当前投票数的信息,还会添加一个用于投票的按钮:
<template name="votes">
  <h3>You have <i>{{numberOfVotes}}</i> votes left</h3>
</template>

<template name="gallery">
  <div>
    {{#each images}}
    <div class="item">
      <p>
        <b>Cat named:</b>{{name}}
        </p>
      <img src="img/{{url}}" />
      <p>
        Votes:
        <progress value="{{votes}}" max="500" />
        <output>{{votes}}</output>
      </p>
      <button class="vote">Vote for me</button>
    </div>
    {{/each}}
  </div>
</template>
  1. 我们可以启动应用程序,看看是否一切都如预期那样。如果您在两个浏览器会话中同时打开应用程序,并输入两个不同的名称,您会注意到当我们对图像进行投票时,另一个浏览器会话上的投票数会立即更新。如何做...

它是如何工作的...

你可能想要看的第一件事是数据库中的状态。在服务器启动的同时,您可以启动meteor mongo控制台,并使用db.getCollectionNames()列出集合,您应该会得到以下结果:

[ "images", "system.indexes", "users" ]

集合名称是我们在publish.js文件中定义的。至于数据库中包含的数据,我们决定在图像集合中使用public文件夹中的图像的 URL,因为这对于这种情况更简单。

注意

如果您需要存储或操作诸如图像之类的二进制数据,您可以在 MongoDB 中进行操作,并且它与 Meteor 非常兼容。在那里,我们可以使用 EJSON,其中 E 代表扩展。基本上,它支持所有 JSON 类型,同时通过 JavaScript 的Date()对象和Uint8Array添加额外的数据。您还可以定义自己的自定义数据类型,并类似于常规 JSON 使用 EJSON 对象。还有一些其他方法,如EJSON.newBinary(size)EJSON.addType(name,factory),您可以在docs.meteor.com/#ejson上阅读更多关于它们的信息。还有配置自己已经存在的 MongoDB 实例的选项。这是在启动 meteor 之前定义一个环境变量来完成的:

MONGO_URL=mongodb://localhost:3317 meteor

这样可以方便地拥有一个非 Meteor 应用程序使用的相同的 MongoDB 服务器。

gallery.js文件中,为了在客户端有一些数据,我们使用了Session变量。这实际上是一个全局对象,可以在客户端上用来存储任何键值对。就像你在其他框架和语言中习惯的那样,我们有Session.set("theKey", someValue)来存储theKeysomeValue,并且使用Session.get("theKey")来检索数据。至于初始化,有Session.setDefault("theKey", initalValue),这样可以方便地避免在加载应用程序的新版本时重新初始化变量。

注意

您可以在 Meteor 规范的docs.meteor.com/#session中阅读更多关于 Session 对象的信息。

正如你可能已经注意到的那样,我们可以嵌套模板。这是标准的 handlebar 行为,在我们的情况下,我们使用它来简化视图逻辑。在现实生活中,不仅将只能在应用程序的其他部分中重用的部分分开是更有意义的,而且同时,你也不希望有使你的代码难以阅读的庞大模板。为了有事件,我们可以使用标准的 CSS 选择器将它们添加到我们的模板中,因此,如果我们想要在main模板中使用.name CSS 类的元素的click事件上附加一个回调,我们可以使用以下代码:

Template.main.events({
    'click .name' : function () { ... }
});

在事件回调中,我们可以访问一些对我们有用的对象。我们在Template.gallery.events中使用了其中一些,在onclick回调中接受两个参数;我们可以在这个对象中看到这一点,并且我们可以访问相关的文档。具有触发元素的数据上下文允许轻松地操纵该部分:

Template.gallery.events({
  'click .vote' : function(e,t) {
    //this object can be used to access elements
      }
});

传递给回调的两个参数允许访问事件类型以及当前目标的DOMElement

注意

有关事件映射和选择器的更多信息,以及事件回调中可访问的其他内容,可以在docs.meteor.com/#eventmaps找到。您还可以附加在模板呈现后调用的回调,docs.meteor.com/#template_rendered。还有一个选项可以使用其他模板引擎,而不是 handlebars,例如 Jade,docs.meteor.com/#templates。这可以做到,因为模板的响应上下文不依赖于引擎;甚至可以通过附加字符串手动构建 HTML,它仍然可以工作。

main模板中,除了Users.insert,我们还使用了db.collection.findOne函数,该函数返回找到的第一个对象。这是通过将我们指定的查询与数据库匹配,并在自然顺序中检索找到的第一个结果来完成的。

注意

有关findOne的更详细解释,请参阅 MongoDB 文档docs.mongodb.org/manual/reference/method/db.collection.findOne/

至于元素的更新,集合接受两个参数,第一个是选择查询,例如在voteForImage中,我们使用了 MongoDB 生成的_id来匹配元素,第二个是使用$set修饰符更新所选文档的pointsLeft属性:

    Users.update(
        {_id:currentUser._id},
        {$set:{pointsLeft:currentUser.pointsLeft}}
      );

注意

有几种不同的更新修饰符可以使用,您可以在文档中详细了解它们,但为了让您快速掌握基础知识,您可以使用 10gen 提供的一些参考卡。更多细节可以在www.10gen.com/reference找到。

在我们的例子中,我们可以使用$inc修饰符来增加给定数量的值,但为了简单起见,我们选择了更通用的$set修饰符。另外,可以进行的另一个额外改进是将集合声明(例如Images = new Meteor.Collection('images'))移动到一个通用文件中,用于服务器和客户端,而不是在各自的文件夹中,以减少代码重复。

还有更多...

在某个时间点,您可能希望基于标准 JavaScript 的setTimeoutsetInterval函数进行一些定时器。如果您尝试这样做,将会收到错误提示,但Meteor.setTimeoutMeteor.setInterval函数提供了相同的功能(docs.meteor.com/#timers)。

当涉及到视图时,到目前为止,您可能已经看到它完全由 JavaScript 处理。这是 Meteor 的一个问题,因为像这样生成的内容很难被搜索引擎(如 Google)索引。为了帮助您解决这个问题,有一个名为spiderable的包(docs.meteor.com/#spiderable),可以用作临时修复。

Meteor.js 中的安全机制

Meteor 的安全性一直存在很多争议。到处都是数据库并不代表安全。我们在客户端和服务器端代码中使用相同的 API,毫无疑问,我们也可以删除集合。在 JavaScript 控制台玩耍一段时间后,我们很容易删除我们之前示例中的所有Users。您可以随时为安全性编写自己的实现;例如,您可以覆盖默认的服务器方法处理程序,使UsersImages集合可以从客户端访问:

Meteor.startup(function () {
  var collection = ['Users', 'Images'];
  var redefine = ['insert', 'update', 'remove'];
  for (var i = 0; i < collection.length; i++) {
    for (var j = 0; j < redefine.length; i++){
      Meteor.default_server.method_handlers['/' + collection[i] + '/' + redefine[j]] = function() {
        console.log('someone is hacking you, oh no !!! Too bad for him...');
      };
    }
  }
});

在这个示例中,我们将看一下 Meteor 保护应用程序的方式,以及身份验证和授权的一些机制。为此,我们将创建一个简单的列表输入应用程序。

准备就绪

对于这个配方,不需要特殊的准备工作;你只需要命令行和安装了 Meteor 的版本。

如何做...

让我们开始吧。

  1. 创建一个名为secure的应用程序,使用meteor create secure。在生成的secure.html文件中,我们将定义一个小模板,其中包含一个输入元素、一个按钮和一个已有列表条目的列表:
<body>
  {{> list}}
</body>

<template name="list">
  <h1>This is my awesome list</h1>
  <input placeholder="enter awesomeness"> </input>
  <button>Add</button>
  <ul>
    {{#each items}}
      <li>{{text}} </li>
    {{/each}}
  </ul>
</template>
  1. secure.js中的附带代码将初始化列表的一个元素,并添加模板的渲染日期:
Notes = new Meteor.Collection("Notes");

if (Meteor.isClient) {
  Template.list.items = function () {
    return Notes.find();
  };

  Template.list.events({
    'click button' : function () {
      Notes.insert({
        text: $('input').val()
      });
    }
  });
}

if (Meteor.isServer) {
  Meteor.startup(function () {
    //initialize
    if(Notes.find().count() < 1){
      Notes.insert({
        text: "awesomeness"
      });
    }
  });
}
  1. 启动应用程序并尝试一下,看看它是否工作。如果此时打开浏览器的控制台,我们可以直接访问Notes.remove(),这通常是我们想要禁止的事情之一。我们可以使用以下命令删除生成的应用程序中的一个默认智能软件包:
meteor remove insecure

  1. 通过手动编辑.meteor/packages也可以实现同样的效果。这将使我们的应用程序变得“安全”,甚至过于安全,如果这样的话。

  2. 现在,如果我们尝试在浏览器的控制台中玩耍,我们会得到以下消息:

insert failed: Access denied

如果我们只是点击之前有效的添加按钮,将会出现相同的消息。这是因为现在所有对数据库的请求都被视为匿名的,我们只能得到服务器发布的内容。

  1. 我们总是可以自己编写身份验证,但内置的身份验证非常好;要将其添加到我们的项目中,我们可以使用以下代码:
> meteor add accounts-base
accounts-base: A user account system
> meteor add accounts-password
accounts-password: Password support for accounts.
> meteor add email
email: Send email messages
>  meteor add accounts-ui
accounts-ui: Simple templates to add login widgets to an app.

  1. 如前所述,我们可以直接在packages文件中添加这些软件包;最好经常检查一下你在尝试的示例应用程序中有什么,这样你就不会感到惊讶。

注意

各种软件包和第三方库扩展了核心 Meteor 功能。有用于 D3、underscore、backbone 等的软件包,每天都会添加更多。这些以及一些基本的支持逻辑可以在docs.meteor.com/#packages找到。还有一种方法可以为您的应用程序创建自己的扩展和通用逻辑。

  1. 我们包含的软件包是一组辅助程序,使身份验证用户管理逻辑自动化。Accounts-UI 甚至使我们能够轻松拥有出色的用户界面进行登录。那么我们需要做些什么来启用它呢?首先,我们将在希望登录 UI 出现的地方添加一小段代码:
 <div id="login">
    {{loginButtons align="right"}}
  </div>
  1. 此外,我们需要配置我们想要的登录类型,所以在我们的情况下,我们使用了一个简单的用户名和密码类型,可以选择输入电子邮件。我们将这个配置添加到secure.js文件中:
Accounts.ui.config({
    passwordSignupFields: 'USERNAME_AND_OPTIONAL_EMAIL'
});
  1. 您可能还想添加的另一件事是一个简单的 CSS 样式来定位登录框:如何做...

简单注册并登录到帐户后,我们应该注意到一个带有我们用户名的用户链接。我们可以使用这个来注销。正如你所注意到的,这是尽可能轻松的。

Accounts-UI 还有许多其他配置选项,以及用于连接 Twitter、Facebook、Google 和 GitHub 帐户的扩展。更多信息可以在docs.meteor.com/#accounts_ui_config找到,软件包文档可以在docs.meteor.com/#accountsui找到。此外,您还可以使用诸如发送验证电子邮件或确认之类的功能。

工作原理...

登录后,如果我们尝试使用按钮添加文本,我们会注意到我们仍然没有访问权限,因为我们的用户没有被授权进行插入。要允许特定用户进行插入,使用以下代码:

  Notes.allow({
      insert: function (userId, doc) {
        console.log(userId);
        console.log(doc);
        //do the check for the permission and return true if allowed
        return true;
      }
    });

在这个插入回调中,我们可以允许或拒绝对给定文档的userId用户的访问。对于我们的情况,文档是我们正在尝试插入的Notes对象,而userId是当前登录用户的 ID。除了collection.allow,还有它的对应物collection.deny,我们可以使用它来禁止对某些方法的访问,即使有allow规则。

现在,很容易创建一个高级授权系统,我们可以以编程方式指定访问权限。有程序员认为这会导致很多开销,对于某些应用程序可能是这样,但对于大多数应用程序来说,设置访问权限应该非常简单。

总的来说,我们绝对不应该信任来自客户端的数据。只有他们必须使用的部分才能被访问,输入应该经过过滤。将信用卡数据发送到客户端是摧毁你的业务的一种简单方式。仅仅因为 Meteor 为我们做了这么多事情,这并不意味着我们应该忘记其他常见的做法,比如数据验证。

你可能想知道我们用于注册的用户数据存储在哪里。如果我们使用meteor mongo访问 Mongo 控制台,应该会包含类似以下内容的users集合:

{
  "createdAt": 1362434550460,
  "services": {
    "password": {
      "srp": {
        "identity": "bE9uYyziWxM2soGem",
        "salt": "FDEduAsvpf5ZJCWea",
        "verifier": "11a2fa4139c8283db1ce61e5f5fa7bf875da27a9b8ec195 baae49cd69c7f3ea48e1c1db471e1bc6aa1a9894a0633f44098717e0c6af367dcd39f 964d63f4fd5346f3b314bd897b76d3f31aa8aeb37030e5fef099b77efb594ad07103 6ec31fb6a3016f0c6cc43605469f798e20fc5b005e982e579014aef7742aac3 bc5792271"
      }
    },
    "resume": {
      "loginTokens": [
        {
          "token": "PDbpT6jtKcdvZMurr",
          "when": 1362434550460
        }
      ]
    }
  },
  "username": "mite",
  "emails": [
    {
      "address": "mitemitereski@gmail.com",
      "verified": false
    }
  ],
  "_id": "QuZEe4uSPK6MfM5PQ"
}

你可能注意到,这更多或多少是你可能期望存储在数据库中的标准数据。密码经过哈希处理并加盐,以防止一些常见的攻击。

就是这样;我们有一个非常简单但安全的应用程序。列表的数据当然不是用户特定的,但可以通过为每个创建的文档添加一个所有者字段来轻松扩展。

还有更多...

有一件事你应该接受的是,Meteor 仍然没有完全完成。每个版本都在进行大量的更改,直到它变得完全稳定。大部分用户请求的功能都在被添加,同时还有其他重要的架构改进正在实施,所以你需要随着每个版本更新部分代码。

一个很好的信息资源是示例应用程序;你可以通过调用meteor create –list命令列出它们;至于再次获取代码,你可以使用meteor create -example nameofexample

在部署方面,我们可以自由地使用提供的服务器,但也有一个选项可以将其部署在www.meteor.com上。这是由这个有趣框架背后的初创公司提供的服务。在那里部署只需要一个命令:

>meteor deploy myapp.meteor.com

有关云解决方案的更多信息,请访问docs.meteor.com/#meteordeploy

还有一个选项,可以从我们的 Meteor 应用程序生成一个完全独立的 Node.js 应用程序,并使用其他云服务。可以使用以下命令完成:

>meteor bundle packed.tgz

至于运行解压文件,请使用以下命令:

> PORT=3000 MONGO_URL=mongodb://localhost:2222/myapp node main.js

这是可能的,因为 Meteor 在幕后是一个具有不同类型打包的 Node.js 框架。

第十一章:数据存储

本章涵盖以下配方:

  • Data URI

  • 会话和本地存储

  • 从文件中读取数据

  • 使用 IndexedDB

  • 存储的限制以及如何请求更多

  • 操作浏览器历史记录

介绍

当我们谈论存储时,大多数开发人员会考虑将数据存储在服务器上的某个数据库中。HTML5 确实在可以传递和保存到客户端方面取得了长足的进步。无论是用于临时使用、缓存,还是完全离线使用整个应用程序,客户端存储正在变得越来越普遍。

所有这些伟大的功能使我们能够在客户端存储数据,从而使应用程序变得更快、更易用和更可达。即使在基于云的解决方案中,我们仍然需要一些本地数据,这将使用户体验更好。

本章涵盖了一些与 HTML5 相关的特性,涉及数据存储。

Data URI

我们已经在本书中的多个场合使用了 Data URI统一资源标识符),但从未详细介绍过我们可以用它做什么,以及有什么限制。Data URI 通常被称为 Data URL统一资源定位符),尽管从技术上讲,它们实际上并没有从远程站点定位任何内容。

在这个例子中,我们将使用不同的媒体类型并检查大小约束。

准备工作

对于这个例子,我们只需要浏览器和一些样本文本文件。这些文件可以作为示例文件中的files文件夹的一部分下载。

如何做...

为了查看一些可用的选项,我们将创建一个简单的 HTML 文件,其中包含几种不同的使用场景:

  1. head部分将包括example.css文件:
<head>
<title>Data URI example</title>
<link rel="stylesheet" type="text/css" href="example.css">
</head>
  1. body部分,我们添加一个div元素,用作 CSS 图像 Data URI 的容器:
<div id="someImage">
CSS image
</div>
  1. 通过使用 Data URI,我们可以创建一个简单的编辑器,通过点击链接打开:
<a href="data:text/html,<body contenteditable>write here">open editor</a>
  1. base64 是可选的,可以使用字符集:
<a href="data:text/plain;charset=utf-8,програмерите%20ќе%20го%20населат%20светот">this is some UTF-8 text </a>
  1. Data URI 可以是原始 SVG:
<p>Image tag </p>
<imgsrc='data:image/svg+xml,<svg version="1.1"><circle cx="100" cy="50" r="40" stroke="black" stroke-width="1" fill="red" /></svg>' />
  1. 使用 Data URI 的伴随 CSS 代码用于表示background-image
img {
  width: 300px;
  height: 110px;
}

#someImage {
  background-image : url('data:image/svg+xml,<svg version="1.1"><path d="M 100,100 l150,0a150,150 0 0,0 -37,-97 z" fill="green" stroke="black" stroke-width="2" stroke-linejoin="round" /></svg>');
}

这将显示两个图像和链接到简单编辑器和一个小文本文件:

如何做...

它是如何工作的...

一个常见的误解是 Data URI 只能用来表示图像。正如我们在例子中看到的,这并不是这样。严格来说,Data URI 不是 HTML5 的特性,而是在 RFC-2397(tools.ietf.org/html/rfc2397)中包含的,1998 年指定,最初在 1995 年提出了这个想法。其背后的想法是直接内联嵌入数据。URI 形式被指定为:

data:[<mediatype>][;base64],<data>

mediatype属性是 Internet 媒体类型,或者它的旧名称是 MIME。如果我们不指定它,它默认为text/plain;charset=US-ASCII

除了酷和不同之外,我们为什么要使用 Data URI?

一个很好的理由是从当前显示的文档中派生数据。例如,我们可以从canvas元素创建图像,或者从当前表格生成 CSV 文件。

另一个原因是网页加载速度。这是矛盾的,因为 Data URI 通常是 base64 编码的,这会增加文档的大小到 1/3。加快速度的原则是减少请求的数量。这对于传输应该小于几千字节的小文件是有意义的,否则,不再发出另一个请求的收益很小,如果有的话。这种方法的另一个问题是,我们正在失去单独资源的缓存。否则将被单独缓存的文件,现在具有与嵌入它的文档相同的属性。如果该文档经常更改,则嵌入的数据将每次重新加载。

其他用例是对各种资源有限制的环境。电子邮件就是这种情况的一个例子,在这种情况下,为了实现单一文档体验而不必将图像作为附件,可以使用 Data URI。

还有更多...

在一些数据 URI 的应用中,安全性可能是一个问题,但如果大多数浏览器中的客户端应用程序遵循规范,那么只有允许的mediatype数据将被处理。

HTML5 中的属性有大小限制。HTML 4 有ATTSPLEN限制,其中指定属性的最大长度为65536个字符。HTML5 不是这种情况,目前每个浏览器版本都有不同的状态。对于 Firefox 3.x,它是 600 KB,对于 Chrome 19,它是 2 MB,IE 8 的限制是 32 KB。可以肯定地说,这只对较小的资源有意义。

会话和本地存储

Cookie 是保存应用程序状态的常用方式,可能是一些选中的复选框或某种临时数据,例如,向导应用程序中的当前流程,甚至是会话标识符。

这是一个经过验证的方法已经有一段时间了,但有一些使用情况是不舒服创建 Cookie 并且它们会施加一定的限制和开销,这是可以避免的。

会话和本地存储解决了一些 Cookie 的问题,并且使数据在客户端上的简单存储成为可能。在这个示例中,我们将创建一个简单的表单,利用 HTML5 存储 API。

准备工作

在这个示例中,我们将使用可以从images文件夹中检索的几个图像,或者您可以使用自己的选择。此外,由于我们将使用来自 JSON 对象的 REST API 的模拟响应,我们需要启动一个本地 HTTP 服务器来提供我们的静态文件。

如何做到这一点...

我们可以先创建一个表单,其中包含狗的选择和留下评论的区域。当我们在表单中点击一个按钮时,将显示所选狗的图像。除此之外,我们还将有一个输出字段,用于显示当前用户的访问次数:

  1. 我们在head部分链接一个简单的 CSS 类:
<meta charset="utf-8">
<title>Session and storage</title>
<link rel="stylesheet" type="text/css" href="example.css" />
  1. 表单将包含以下单选按钮和文本区域:
<form id="dogPicker">
<fieldset>
<legend>Pick a dog</legend>
<div id="imageList"></div>
<p>The best is:</p>
<p>
<input id="dog1" type="radio" name="dog" value="dog1" />
<label for="dog1">small dog</label>

<input id="dog2" type="radio" name="dog" value="dog2" />
<label for="dog2">doggy</label>

<input id="dog3" type="radio" name="dog" value="dog3" />
<label for="dog3">other dog</label>
</p>
</fieldset>

<label for="comment">Leave a comment</label>
<textarea id="comment" name="comment" ></textarea>
<button id="send" type="button">Pick</button>
</form>
  1. 我们添加一个访问次数的计数器如下:
<p>
      You have opened this page <output id="counter">0</output> times
</p>
  1. 还有一个简单的div元素作为所选狗图片的占位符和对 jQuery 的依赖,以及包括我们稍后将编写的example.js文件:
<div id="selectedImage"></div>
<script src="img/jquery.min.js"></script>
<script src="img/example.js" ></script>
  1. 对于example.js文件,我们创建一个函数,将在点击按钮时将评论存储在会话中。如果数据不可用,将对"dogs.json"变量进行请求:
$(function() {
  $('#send').click(function() {
vardogId = $("#dogPicker :radio:checked").val();
var comment = $('#comment').val();
    //different ways to set data
sessionStorage.comment = comment;
    // if no data available do AJAX call
    if (localStorage.dogData) {
showSelectedImage(dogId);
    } else {
      $.ajax({
url: "dogs.json",
      }).done(function(data){
localStorage.dogData = JSON.stringify(data);
showSelectedImage(dogId);
      });
    }
  });

提示

使用#dogPicker :radio:checked,我们选择dogPickerID 的元素的所有选中输入radio子元素。

  1. 由于评论的数据存储在会话中,点击后我们可以有一种加载它的方式,以备下次使用:
  if (sessionStorage.comment) {
    $('#comment').val(sessionStorage.comment);
  }
  1. 但是使用localStorage,我们可以递增viewCount变量,或者首次初始化它:
  if (localStorage.viewCount) {
localStorage.viewCount++;
    $('#counter').val(localStorage.viewCount);
  } else {
localStorage.viewCount = 1;
  }
  1. showSelectedImages方法遍历每个狗对象,在我们的localStorage列表中创建一个带有所选文件的图像元素:
 function showSelectedImage(dogId){
vardogList = JSON.parse(localStorage.dogData);
vardogFile;
    $.each(dogList.dogs, function(i,e){
      if(e.id === dogId){
dogFile = e.file;
      };
    });
      $('#selectedImage').html("<imgsrc='images/" + dogFile + "'></img>");
  }

如果我们选择一个单选按钮并单击它,狗的图像应该显示出来,如果我们尝试重新加载缓存,那么(Ctrl + F5)在大多数浏览器中,评论数据仍然会保留。如果我们在另一个标签中打开相同的 URL,那么评论就不应该存在,这意味着会话与单个浏览器窗口或标签相关联。另一方面,计数器应该每次递增,而且不会为dogs.json文件执行额外的请求。

它是如何工作的...

sessionStoragelocalStorage共享通用的Storage接口,并且它们被定义为www.w3.org/TR/webstorage/的一部分。我们可以使用点表示法来读取或写入存储,例如storage.key = someValuesomeValue = storage.key。更长的形式是使用方法调用访问数据,storage.setItem(key, value)storage.getItem(key)

这里对键和值的限制是它们必须是“字符串”。在我们的例子中,我们需要存储 JSON,所以为了使其与值兼容,我们使用了JSON.stringifyJSON.parse。还有一个方法storage.removeItem(key)来删除一个项目,或者用storage.clear()来清除整个存储。

sessionStorage是一个用于存储在浏览器会话期间持续存在的信息的对象,这就是名称的由来。即使重新加载后信息仍然保留,使其成为会话 cookie 的强大替代品。存储的项目的有效上下文是当前网站域,在当前打开的选项卡的会话期间。例如,如果我们在域example.com/1.html上存储一个项目,它将在example.com/2.html或同一域的任何其他页面上都可以访问。

LocalStorage是一种持久存储,与sessionStorage不同,它在会话结束后仍然有效。这类似于标准 cookie 的行为,但与 cookie 不同的是,cookie 只能保存非常有限的数据。localStorage在大多数浏览器上默认为 5MB,在 IE 上为 10MB。需要记住的是,我们将数据存储为字符串而不是它们的原始形式,例如整数或浮点数,因此最终存储的表示将更大。如果我们超出存储限制,那么将抛出一个带有QUOTA_EXCEEDED_ERR错误消息的异常。

在我们的代码中,我们使用localStorage来缓存 JSON 资源,从而完全控制了失效。此外,我们为给定用户的访问次数创建了一个简单的计数器。

显而易见的隔离是hostnameport的组合,需要单独存储。较少人知道的是,Web 存储还取决于scheme/host/port的元组。Scheme 包含子域和协议。因此,如果页面加载了混合类型的资源,有些是用https,有些是用http,你可能得不到那么明显的结果。虽然混合资源不是一个好的安全实践,但它经常发生。无论哪种情况,敏感数据都不应存储在本地或会话存储中。

另一种情况是大多数现代浏览器都有的私人/无痕模式。在该模式下打开页面时,将使用一个新的临时数据库来存储这些值。在此模式下存储的所有内容只会成为该会话的一部分。

还有更多...

本地存储使用一个在浏览器的主 UI 线程上运行的同步 API。因此,如果我们在多个不同的窗口上打开相同的网站,就有很小的可能发生竞争条件。对于大多数用例来说,这并不是一个真正的问题。要从客户端清除数据,我们可以随时调用storage.clear(),但大多数浏览器现在都有开发者工具来简化操作:

还有更多...

在填充方面有很多可用的,例如code.google.com/p/sessionstorage/gist.github.com/remy/350433。您可能想知道它们是如何工作的,因为存储是添加到浏览器的新功能。它们大多使用 cookie 来存储数据,因此通常受到 2 KB 的限制,即 cookie 的最大大小。其他使用 IE userData(msdn.microsoft.com/en-us/library/ms531424%28VS.85%29.aspx)对象在旧版本的 IE 上启用其使用。还有一些库,例如www.jstorage.info/,为多个浏览器版本提供相同的接口。此外,还有Persists.js,它可以启用多种不同的回退解决方案:flash - Flash 8 持久存储,gears - 基于 Google gears 的持久存储,localstorage - HTML5 草案存储,whatwg_db - HTML5 草案数据库存储,globalstorage - HTML5 草案存储(现已过时),IE - Internet Explorer 用户数据行为,和 cookie - 基于 cookie 的持久存储。

注意

还有一个基于自定义对象的回退,可以创建以在旧浏览器上启用localStorage。有关更多信息,请访问 MDN 的developer.mozilla.org/en-US/docs/DOM/Storage#Compatibility

globalStorage在几个版本的 Firefox 中实现了,但由于与实现相关的混乱很多,它已经从 Firefox 13 中移除,以及 Web 存储的规范。

在安全性方面,将敏感数据存储在客户端存储中从来都不是一个好主意。如果您的站点存在 XSS 漏洞,那么存储可以被读取。使用服务器端密钥加密数据并没有太多意义,因为这将使我们依赖于服务器数据。在非 TLS 站点上还可能发生 DNS 欺骗攻击。如果域名被欺骗,浏览器将无法判断数据是否是从“错误”的站点访问的。

对 Web 存储提出了很多批评,主要是由于用户跟踪。如果我们在几个不同的站点中有相同的广告商,那么他可以轻松地跟踪用户在这些站点上的访问。这使得用户的匿名性大大降低,成为易受攻击的目标。有几种提出的解决方案来解决这个问题,例如,对第三方iframes进行限制和创建此类数据的域名黑名单,但目前没有一种完全解决问题。

从文件中读取数据

我们已经使用文件输入来读取一些数据,但从未详细介绍过文件读取和可用于我们的对象。在这个示例中,我们将使用输入文件创建一个简单的文件阅读器,它将作为文件 API 提供的一些选项的迷你演示:目录和系统,www.w3.org/TR/file-system-api/

如何做到...

我们将创建一个包含文件输入控件和上传状态的进度输出的 HTML 文件:

  1. 我们创建控件和一些输出占位符:
<body>
<p>
<progress id="progress" value="0" max="100"></progress>
<output id="percent" for="progress">0</output>
</p>
<p>
<div id="fileInfo"></div>
</p>
<input type="file" id="file" value="Choose text file">
<button type="button" id="abort">Abort</button>
<button type="button" id="slice">Read 5 bytes</button>
<div id="state"></div>
<br />
<label>
        Contents:
<div id="content"></div>
</label>
  1. 添加依赖项到 jQuery 和我们的example.js
<script src="img/jquery.min.js"></script>
<script type="text/javascript" src="img/example.js"></script>
  1. 我们可以继续创建example.js文件;在这里,我们在abort按钮上附加一个事件处理程序,并使用FileReader对象:
$(function() {

varfr = new FileReader();

  $('#abort').click(function(){
fr.abort();
console.log('aborting file change');
  });
  1. 从所选的文件输入中,我们将使用当前配置项迭代上传的文件,并为一些常见事件添加事件处理程序:
$('#file').on('change', function(e) {
    for (var i = 0; i <this.files.length; i++) {
var f = this.files[i];
fr = new FileReader();

fr.onerror = function (e) {
        $('#state').append('error happened<br />').append(e).append('\n');
      }

fr.onprogress = function (e) {
var percent = (e.loaded * 100 / e.total).toFixed(1);
        $('#progress').attr('max', e.total).attr('value', e.loaded);
        $('#percent').val(percent + ' %');
      }
fr.onabort = function() {
        $('#state').append('aborted<br />');
      }

fr.onloadstart = function (e) {
        $('#state').append('started loading<br />');
      }

      if (f.type&& (f.type.match('image/.+')) || (f.type.match('video/.+'))) {
fr.readAsDataURL(f);
      } else if (f.type.match('text/.+')) {
fr.readAsText(f);
      } else {
        $('#state').append('unknown type of file loaded, reading first 30 bytes <br />');
      }

fr.onload = function(e) {
        $('#state').append('finished reading <br />');
appendContents(f,e);
      }
      $('#fileInfo').html(getMetaData(f));
    }
  });
  1. getMetaData函数将从file对象中读取可用的元数据,并创建一个简单的 HTML 表示:
function getMetaData(file){
var text = "<b>file: </b>" + file.name + " <br />";
    text += "<b>size: </b>" + file.size + " <br />";
    text += "<b>type: </b>" + file.type + " <br />";
    text += "<b>last modified: </b>" + file.lastModifiedDate.toString() + " <br />";
    return text;
  }

注意

您可以在 W3C 文件 API 规范的www.w3.org/TR/FileAPI/#dfn-file中阅读有关文件接口的更多信息。

  1. 通过读取文件类型,我们还可以确定输出内容。在我们的情况下,如果我们有文件,即图像,我们将数据附加为img标签上的src,另一方面,对于其他文件类型,我们只是打印文本表示:
function appendContents(f,e) {
    if (f.type&&f.type.match('image/.+')){
      $("<img />").attr('src', e.target.result).appendTo("#content");
    } else {
      $("<pre />").text(e.target.result).appendTo("#content");
    }
  }
  1. 还有另一种通过访问属性文件来读取文件输入中的文件列表的方法。slice按钮将仅从文件中读取前 15 个字节:
$('#slice').click(function(){
varfileList = $('#file').prop('files');
    $.each(fileList, function(i,file) {
fr = new FileReader();
var blob = file.slice(0, 15);
fr.readAsBinaryString(blob);
fr.onload = function(e) {
        $("<pre />").text(e.target.result).appendTo("#content");
      }
    });
   });
  });

到目前为止,我们应该有一个正在运行的网站,一旦上传文件,文件将被读取和显示。为了查看进度事件,您可以尝试使用大文件,否则它可能会立即运行。至于slice按钮,最好尝试使用一个简单的.txt文件,以便您可以查看内容。

工作原理...

这些规范背后的主要思想是在客户端实现完整功能的文件系统 API。关于当前状态的不幸之处在于,只有 Chrome 实现了大多数来自文件系统和 FileWriter API 的功能,而其他浏览器支持 FileReader 和 File API。这就是为什么我们决定使用在所有主要浏览器中都受支持并使用最常见功能的工作示例。

对于读取和简单操作,我们使用包含可以使用FileReader读取的File对象的FileList。HTML5 在<input type="file">控件上定义了一个文件属性,可以使用 jQuery($('#file').prop('files')))或直接从所选的 HTML 元素中访问,就像我们在this.files.length的情况下所做的那样。此属性实际上是一个称为FileList的类似数组的对象,其中包含File对象。FileList实例具有一个方法item(index)和一个属性length。每个项目都是一个File对象,一个扩展了Blob的接口,不可变的原始二进制数据。文件是一个表示,并具有以下属性:

  • name:此属性表示文件的名称。

  • lastModifiedDate:此属性表示文件的最后修改日期。如果浏览器无法获取此信息,则将当前日期和时间设置为Date对象。

但除此之外,还有来自Blob接口的方法,如下所示:

  • size:此属性表示文件的大小(以字节为单位)

  • type:MIME 类型。此元数据可以直接读取,就像我们在getMetaData函数中所做的那样。元数据可以以各种不同的方式使用,例如在我们的情况下,根据文件类型匹配图像f.type&&f.type.match('image/.+'),然后显示img标签或其他文本。

Blob类型还包含slice方法的定义,由于File扩展了Blob,因此也可以在那里使用。slice(start, end, contentType)方法返回一个新对象,其中新的contentType属性被切片,新文件将从原始文件中切片。

提示

在较旧的浏览器版本中,例如,Firefox 版本小于 12 和 Chrome 版本小于 21,您需要使用slice方法的前缀版本。对于 Chrome,它是File.webkitSlice(),对于 Firefox,它是File.mozSlice()Blob对象也可以从字节数组创建。

FileReader对象实际上是执行文件中包含的数据读取的对象,因为File对象本身只是对真实数据的引用。在FileReader中有用于从Blob中读取的方法,如下所示:

  • void readAsArrayBuffer(blob): 此方法将文件读取为二进制数组

  • void readAsText(blog, optionalEncoding): 此方法将文件读取为文本,其中可以添加可选的编码字符串名称以指定应使用的编码。如果省略编码,则将使用编码确定算法自动选择编码,如规范中所定义的,在大多数情况下应该足够。

  • void readAsDataUrl(blob): 该方法从给定的文件创建一个数据 URL

您可能会注意到,这些方法实际上并不返回读取的数据。这是因为FileReader对象是异步读取数据的,所以一旦数据被读取,就会运行回调函数。还有一个abort方法,可以在调用后停止文件的读取,这是我们在示例中点击abort按钮时调用的方法。

可以附加到文件读取器的事件处理程序可能会在某些情况下触发。在我们的示例中,我们只打印文件读取器的状态。以下事件可以被处理:

  • onabort: 一旦读取操作被中止,就会触发此事件。

  • onerror: 当发生错误时调用此事件。这是我们经常想要处理或至少知道何时发生的事件,尽管处理程序是可选的。错误可能发生在各种不同的原因,我们的处理程序可以接受一个参数来检查FileError错误代码。例如,处理程序可以执行以下操作:

fr.onerror = function (err){
  switch(err.code){
    case FileError.ENCODING_ERR:
      // handle encoding error
      break;
    case FileError.SYNTAX_ERR:
      // handle invalid line ending
      break;
    case FileError.ABORT_ERR:
    // handle abort error
    break;
    default :
    //handle all other errors , or unknown one
    break;
  }
}

FileError对象包含已发生的相应错误,但我们只处理给定情况下的一些情况。

  • onload – 一旦读取操作成功完成,就会调用此事件。处理程序接受并处理事件,从中我们可以读取数据:
fr.onload = function (e){
    // e.target.result contains the data from the file.
}
  • onloadstart: 此方法在读取过程的最开始调用。

  • onloadend: 当我们成功读取时调用此方法,但即使发生错误,它也是一个很好的清理资源的候选者。

  • onprogress: 在读取数据时定期调用此方法。在进度处理程序中,我们可以读取几个对我们有用的属性,以便在progress元素上进行更新。我们可以读取已读取该文件的总字节数,这意味着我们可以简单地计算数据的百分比:

fr.onprogress = function (e) {
var percent = (e.loaded * 100 / e.total).toFixed(1);
        $('#progress').attr('max', e.total).attr('value', e.loaded);
        $('#percent').val(percent + ' %');
      }

在大多数情况下,onloadonerror就足够了,但我们可能需要向用户显示视觉显示或通知他们读取状态。

要检查浏览器是否支持我们使用的功能,我们可以使用:

if (window.File&&window.FileReader&&window.FileList&&window.Blob) {
   // has support for File API
}

还有更多...

对于更高级的逻辑和文件写入,有FileWriterDirectoryReaderFileEntryDirectoryEntryLocalFileSystem等等。问题在于,目前只有 Chrome 支持它们。

要请求受限文件系统,我们调用window.requestFileSystem(type, size, successCallback, errorCallback),这是 FileSystem API 的一部分。受限环境意味着这个文件系统与用户的文件系统是分开的,所以你不能随意写入任何地方。

自 Chrome 12 以来,文件系统已经被添加前缀,当前版本的 Chrome 25 仍在使用该版本。一个简单的文件系统请求可能是:

window.webkitRequestFileSystem(
window.TEMPORARY,
  2*1024*1024,
  function (fs){
console.log("Successfully opened file system " + fs.name);
  });

受限环境中的文件用FileEntry表示,目录用DirectoryEntry表示。

一旦我们成功打开了文件系统,我们就可以读取FileEntries

function (fs){
fs.root.getFile(
    "awesome.txt",
     { create : true },
     function (fileEntry) {
console.log(fileEntry.isDirectory); // false
console.log(fileEntry.fullPath); // '/awesome.txt'
    }
   );
}

这个fs.root调用是对文件系统根目录的引用,至于fileEntry参数,有很多方法可以用于移动文件、删除文件、将其转换为 URL、复制以及您可能期望从文件系统中获得的所有其他功能。这些 URL 是相对于给定的受限文件系统的,因此我们可以期望在特定受限文件系统的root目录中有类似/docs/books/dragon/的内容。

Erick Bidelman 是 FileSystem API 背后的程序员之一,他实现了一个使用众所周知的 UNIX 命令(如cpmvls)的功能的包装器。该库称为filer.jsgithub.com/ebidel/filer.js。他还有一个名为ibd.filesystem.js的 FileSystem API polyfill,(github.com/ebidel/idb.filesystem.js),它使用 IndexedDB 在其他浏览器中模拟功能。

还有一个 API 的同步版本,我们使用webkitRequestFileSystemSync调用它。我们希望使用同步读取的原因是 Web workers,因为这样做是有意义的,因为我们不会像那样阻塞主应用程序。

规范中提到了几种用例,因此这些用例的概述版本如下:

  • 持久上传器是一种一次上传一个文件块到服务器的方式,因此当服务器或浏览器发生故障时,它可以继续使用服务器接收到的最后一个文件块,而不是重新上传整个文件。

  • 游戏或富媒体应用程序中,资源作为 tarballs 下载并在本地展开,相同的资源可以预取,只需一个请求而不是许多小请求,这可以减少查找时间。

  • 应用程序创建的文件,如离线视频、音频或任何其他类型的二进制文件查看器和编辑器,可以保存在本地系统中以供进一步处理。

使用 IndexedDB

除了本地和会话存储外,IndexedDB 还为我们提供了一种在浏览器中存储用户数据的方式。IndexedDB 比本地存储更先进:它允许我们在对象存储中存储数据,并支持对数据进行索引。

在这个示例中,我们将创建一个简单的待办事项列表应用程序,它将其数据存储在 IndexedDB 中。我们将使用第十章中介绍的 Angular 框架,数据绑定框架来简化我们的代码。我们将找出 IndexedDB 是否是更适合更大、更复杂的数据模型和更复杂的搜索和检索需求的选择。

待办事项列表应用程序将支持当前和已归档的项目,并允许按日期筛选项目。

如何做...

让我们写代码:

  1. 创建index.html。为了简化我们的应用程序代码,我们将使用angular.js模板。我们的模板将包含以下元素:
  • 选择以在当前和已归档的待办事项之间进行选择

  • 使用 HTML5 日期组件的日期范围过滤器

  • 带有复选框和每个项目的年龄的待办事项列表

  • 添加新项目的表单

  • 对已完成的当前项目进行归档的归档按钮

<!doctype html>
<html ng-app="todo">
<head>
<script src="img/angular.min.js"></script>
<script src="img/example.js"></script>
<script src="img/service.js"></script>
<meta charset="utf8">
<style type="text/css">
        .todo-text {
            display: inline-block;
            width: 340px;
vertical-align:top;
        }
</style>
</head>
<body>
<div ng-controller="TodoController">
<select ng-model="archive">
<option value="0">Current</option>
<option value="1">Archived</option>
</select>
        From: <input type="date" ng-model="from">
        To: <input type="date" ng-model="to">
<ul>
<li ng-repeat="todo in todos | filter:{archived:archive}">
<input type="checkbox" ng-model="todo.done"
ng-disabled="todo.archived"
ng-click="updateItem(todo)">
<span class="todo-text">{{todo.text}}</span>
<span class="todo-age">{{todo.date | age}}</span>
</li>
</ul>
<form ng-submit="addItem()">
<input ng-model="text">
<input type="submit" value="Add">
</form>
<input type="button" ng-click="archiveDone()"
            value="Archive done">
<div ng-show="svc.error">{{svc.error}}</div>
</div>
</body>
</html>
  1. 创建example.js,它将定义设置和操作index.html模板范围的控制器,并为日期定义年龄过滤器:
var app = angular.module('todo', []);

app.filter('age', function() {
    return function(timestamp) {
var s = (Date.now() - timestamp) / 1000 / 3600;
        if (s < 1) return "now";
        if (s < 24) return s.toFixed(0) + 'h';
        if (s < 24*7) return (s / 24).toFixed(0) + 'd';
        return (s /24/7).toFixed(0) + 'w';
    };
});
var DAY = 1000*3600*24;

function TodoController($scope, DBTodo) {
    $scope.svc = DBTodo.data;
    $scope.archive = 0;
    $scope.from = new Date(Date.now() - 3*DAY)
        .toISOString().substr(0, 10);
    $scope.to = new Date(Date.now() + 1*DAY)
        .toISOString().substr(0, 10);
    $scope.todos = [];

    function updateItems() {
DBTodo.getItems(
            new Date($scope.from).getTime(),
            new Date($scope.to).getTime(),
            function(err, items) {
                $scope.todos = items;
            });
    };
    $scope.addItem = function() {
DBTodo.addItem({
            date: Date.now(),
            text: $scope.text,
            archived: 0,
            done: false
        }, function() {
            $scope.text = "";
updateItems();
        });
    };
    $scope.updateItem = function(item) {
DBTodo.updateItem(item);
    };
    $scope.archiveDone = function(item) {
DBTodo.archive(updateItems);
    };
    $scope.$watch('from',updateItems);
    $scope.$watch('to', updateItems);
}
  1. service.js中定义控制器所需的DBTodo服务:
angular.module('todo').factory('DBTodo', function($rootScope) {

首先,我们需要从全局定义中删除前缀:

window.indexedDB = window.indexedDB || window.mozIndexedDB ||
window.webkitIndexedDB || window.msIndexedDB;
window.IDBTransaction = window.IDBTransaction ||
window.webkitIDBTransaction || window.msIDBTransaction;
window.IDBKeyRange = window.IDBKeyRange ||
window.webkitIDBKeyRange || window.msIDBKeyRange;

var self = {}, db = null;
self.data = {error: null};

我们的初始化函数打开数据库并指定请求的版本。当数据库不存在时,将调用onupgradeneeded函数,我们可以使用它来创建我们的对象存储和索引。我们还使用一些随机生成的项目填充数据库:

    function initialize(done) {

varreq = window.indexedDB.open("todos", "1");
varneedsPopulate = false;
req.onupgradeneeded = function(e) {
db = e.currentTarget.result;
varos = db.createObjectStore(
                "todos", {autoIncrement: true});
os.createIndex(
                "date", "date", {unique: false});
os.createIndex(
                "archived", "archived", {unique: false});
needsPopulate = true;
        }
req.onsuccess = function(e) {
db = this.result;
            if (needsPopulate) populate(done);
            else done();
        };
req.onerror = function(e) {
self.data.error = e.target.error;
        };
    }

Random item generator
    function pickRandomText(k) {
var texts = ["Buy groceries",
            "Clean the car",
            "Mow the lawn",
            "Wash the dishes",
            "Clean the room",
            "Do some repairs"],
            selected = texts[(Math.random() * texts.length)
                .toFixed(0)];
            return selected + " " + k;
    }

该函数用25天内分布的50个随机项目填充数据库:

    function populate(done) {
var now = Date.now();
var t = db.transaction('todos', 'readwrite');
t.oncomplete = done;

vartbl = t.objectStore('todos');
var N = 50;
        for (var k = N; k > 0; --k) {
tbl.add({
                text: pickRandomText(k),
                date: Date.now() - (k / 2) * DAY,
                archived: k > 5 ? 1 : 0,
                done: (k > 5 || Math.random() < 0.5)
            });
        }
    }

withDB是一个辅助函数,确保在执行指定函数之前初始化数据库:

    function withDB(fn) {
        return function() {
varargs = arguments, self = this;
            if (!db) initialize(function() {
fn.apply(self, args);
            });
            else fn.apply(self, args);            
        };
    }

withScope是一个辅助函数,它创建一个函数,在其中调用$rootScope.$apply来指示 angular 范围对象的更新:

    function withScope(fn) {
        return function() {
varargs = arguments, self = this;
            $rootScope.$apply(function() {
fn.apply(self, args);
            });
        };
    }

最后,getItemsupdateItemarchiveaddItemDBTodo服务的公共 API:

self.getItems = withDB(function(from, to, cb) {
var list = [];
var index = db.transaction('todos')
            .objectStore('todos').index('date');
varreq = index.openCursor(IDBKeyRange.bound(from, to, true, true));
req.onsuccess = function(e) {
var cursor = e.target.result;
            if (!cursor)
                return withScope(function() {
cb(null, list);
                })();
list.push(cursor.value);
cursor.continue();
        };
    });

self.updateItem = withDB(function(item, done) {
var t = db.transaction('todos', 'readwrite'),
            ix = t.objectStore('todos').index('date'),
req = ix.openCursor(IDBKeyRange.only(item.date));
t.oncomplete = done &&withScope(done);
req.onsuccess = function(e) {
var cursor = e.target.result;
            if (cursor) cursor.update(item);
        };            
    });

self.archive = withDB(function(done) {
var current = IDBKeyRange.only(0);
var t = db.transaction('todos', 'readwrite'),
req = t.objectStore('todos')
            .index("archived")
            .openCursor(current);

t.oncomplete = withScope(done);

req.onsuccess = function(e) {
var cursor = e.target.result;
            if (!cursor) return;
            if (cursor.value.done) {
cursor.value.archived = 1;
cursor.update(cursor.value);
            }
cursor.continue();
        };

    });

self.addItem = withDB(function(item, done) {         
var t = db.transaction('todos', 'readwrite'),
os = t.objectStore('todos');
t.oncomplete = withScope(done);
os.add(item);
    });

    return self;
});
  1. 在支持 IndexedDB 和日期输入(例如 Google Chrome)的浏览器中打开index.html

它是如何工作的...

与普通的 JavaScript API 相比,IndexedDB API 相当冗长。IndexedDB 使用 DOM 事件来表示异步任务的完成。大多数 API 调用都会返回一个请求对象。要获取结果,我们需要将事件监听器附加到这个对象上。

例如,打开数据库的结果是一个请求对象。我们可以将三个事件监听器附加到这个对象上:

  • onsuccess: 当数据库成功打开时调用

  • onerror: 当发生错误时调用

  • onupgradeneeded: 当数据库不是指定版本或尚不存在时调用

IndexedDB 数据库是一个包含一个或多个对象存储的面向对象数据库。

对象存储具有主键索引。在我们的例子中,主键是自动生成的,但我们也可以指定一个现有属性作为主键。

每个对象存储可能有一个或多个索引。索引可以通过指定应该被索引的属性路径来添加 - 在我们的例子中,我们为todos存储在日期和归档字段上定义了两个索引。

所有对数据库的查询都在事务中执行。创建事务时,我们定义将在事务中使用的对象存储。与请求一样,事务也有事件监听器:

  • oncomplete: 当事务完成时调用

  • onerror: 如果发生错误,则调用此方法

  • onabort: 如果事务被中止,则调用此方法

在事务中,我们可以通过调用transaction.objectStore('name')来访问对象存储。对该对象存储的所有操作都将在事务内完成。

对象存储支持多种方法来添加、获取和删除项目,以及访问索引的方法。要添加项目,我们使用add方法。要访问需要显示或更新的项目,我们使用索引,通过调用objectStore.index('name')

索引提供了对象存储 API 的子集,用于检索数据,如getcountopenCursor

要更新项目或获取多个项目,我们使用openCursor方法。它返回一个request,我们可以将onsuccess监听器附加到该请求上。该监听器将对游标访问的每个项目调用。可以通过request.result访问游标。

当我们处理完访问的项目后,可以调用cursor.continue来前进到下一个项目。onsuccess监听器将再次被调用,这次游标指向下一个项目。

我们可以通过指定键范围和方向(升序或降序)来限制游标的访问。键范围可以使用IDBKeyRange方法生成:

  • upperBound: 该方法用于指定上限范围

  • lowerBound: 该方法用于指定下限范围

  • bound: 该方法用于指定上限和下限范围

  • only: 该方法用于指定仅包含一个键的范围。

除了指定边界upperBoundlowerBoundbound之外,它们还支持额外的布尔参数,允许我们指定边界是否包含。

总而言之,当我们实现getItems方法来获取指定日期之间的所有项目时,我们需要:

  • 打开一个到 todos 对象存储的事务

  • 从事务中打开 todos 对象存储

  • 从对象存储中打开date索引

  • 创建一个IDBKeyRange边界,指定第一个日期作为下限,第二个日期作为上限(并指示边界包含两个 true 参数)

  • 使用创建的键范围从date索引中打开游标

  • 使用游标请求来迭代所有项目并将它们添加到数组中

  • 使用事务的oncomplete处理程序在添加所有项目时调用回调函数

还有更多...

IndexedDB API 非常冗长和低级。它不是用于直接被 Web 应用程序使用的;相反,它旨在提供手段在其上编写更高级的数据库实现。

但更重要的是,IndexedDB 不支持一些我们已经接受为标准的真实数据库中的一些基本功能:

  • 没有复合索引,这意味着我们无法编写有效的查询来绑定对象的多个属性。

  • 如果我们希望按照与索引键提供的顺序不同的顺序对项目进行排序,我们将不得不填充一个数组并手动对结果进行排序。

  • 没有连接,这意味着我们需要手动编写代码来连接两个对象存储,并选择最合适的索引来最小化工作量。

因此,我们不建议在 IndexedDB API 成熟之前使用它,或者在其上编写更完整和不那么冗长的数据库实现。

注意

查看 PouchDB (pouchdb.com/)以获取更完整的解决方案,或者查看db.js (aaronpowell.github.com/db.js/)以获取更简洁的 API。

存储的限制以及如何请求更多

到目前为止,我们已经看到了在客户端有多种不同的方式来存储和访问数据。所有这些方式都给了我们在客户端存储大量数据的选择。问题是为什么没有一种方法可以填满所有设备的存储空间?

我们将看到为什么这并不是无处不在的,至少不是没有一些浏览器漏洞。为了做到这一点,我们将创建一个简单的案例,我们将使用localStorage将数据存储到浏览器中,只要用户代理允许。

如何做...

  1. 我们可以开始创建一个名为example.js的文件,在那里我们将生成大小为1k和大小为100k的数据。1k 的数据可以通过创建一个包含1025个元素的数组来生成,然后我们将其与字母"a"连接,得到一个包含1024个字符的字符串"a"
var testing = (function (me) {
me.data1k =  new Array(1025).join("a"); // about 1k
me.data100k = new Array((1024*100)+1).join("b");// about 100k
  1. 接下来,我们将创建一个简单的函数,该函数将接受条目数量和每个条目的数据:
me.run = function (max, data) {
var el = document.getElementById('status');
el.setAttribute('max', max);
    try {
      for (i = 0; i < max; i++) {
console.log(i);
el.setAttribute('value', 1+i);
localStorage.setItem(i, data);
    }
    } catch (err) {
maxReached(i, err);
    }
}
The maxReached function will display the last entry that was successfully stored:
  function maxReached(i, err) {
console.log("max reached");
console.log(err);
var div = document.getElementById('max');
div.innerHTML = "Reached max " + i + " entry";
  }
  1. 我们还将添加一个函数,用于清除整个localStorage对象:
me.clear = function() {
var progress = document.getElementById('status');
progress.setAttribute('value','0');
localStorage.clear();
console.log("removed all data from localStorage");
  }
  1. 在这之后,我们可以创建一个 HTML 文件,在那里我们将有几个按钮,一个用于清除所有数据,其他用于填充生成的数据到存储中:
<body>
<progress id="status" value="0" max="100"></progress>
<div id="max">have not reached max</div>
<button type="button" onclick="testing.clear()" >clear</button>
<button type="button" onclick="testing.run(100,testing.data1k)" >100 entries 1K</button>
<button type="button" onclick="testing.run(500,testing.data100k)" >500 entries 100K</button>
<script src="img/example.js"></script>
</body>

它是如何工作的...

存储限制以及行为取决于浏览器。规范本身说用户代理应该限制存储区域的总空间量。此外,他们应该为每个子域(例如a.example.comb.example.com等)提供相同数量的存储空间。还有一个选项可以提示用户请求更多的存储空间;不幸的是,目前只有 Opera 才这样做。

它是如何工作的...

在 Firefox 中有一个名为dom.storage.default_quota的可配置属性,可以在about:config中找到,但你不能真的指望用户在那里手动设置一个增加的值。对于 IndexDB,存储大小没有限制,但初始配额设置为 50MB。

还有更多...

如果我们谈论 HTML5 文件系统 API 中的限制,我们有几种存储类型定义。

临时存储是基本的,所以我们不需要特殊权限来获取它;这使得它成为缓存的一个不错的选择。Chrome 目前有一个 1GB 的临时池,并且计划将 IndexedDB 和 WebSQL 纳入相同的池中。对于临时存储,没有持久性的保证,因此它可以随时被移除。

注意

有关 WebSQL 的更多信息可以在 W3C 上找到,尽管该规范已不再开发或维护www.w3.org/TR/webdatabase/

另一方面,持久存储是持久的。数据在重新启动后仍然存在,并且直到用户或我们的应用手动删除为止。当我们进行请求文件系统调用时,浏览器将提示我们是否同意,如果我们同意,我们将收到QUOTA_EXCEEDE_ERR

还有一种类型为无限的存储,但这是 Chrome 特有的,并且旨在从扩展和 Chrome 应用中使用。

已经采取了一些努力来标准化存储请求的方式,因此为此目的创建了 Quota API 规范,www.w3.org/TR/quota-api/。规范本身定义了一个 API,用于管理各种持久 API 的本地存储资源的使用和可用性。

有一个StorageQuota接口,描述了获取更多PERSISTENT数据的过程。Chrome 中提供了实现的带前缀版本:

window.webkitStorageInfo.requestQuota(PERSISTENT, 10*1024*1024, function(bytes){
console.log(bytes);
}, function (error){
console.log(error);
});

通过调用该方法,将出现提示要求用户请求权限。

操纵浏览器历史

历史 API 允许您使用 JavaScript 操纵浏览器历史。一些操作在用户代理中很长时间以来就已经可用了。一个新功能是可以在历史中添加新条目,更改在位置栏中显示的 URL 等。

这意味着我们可以创建一个遵守 REST 方式的单页面应用。现在页面可以具有唯一的标识符,将直接导航到具有特定状态的特定视图,而无需进行页面重新加载或进行一些客户端端的黑客攻击。

准备就绪

在这个示例中,我们将使用一些图片,因此您可以选择自己的选择,或者使用位于img/文件夹下的示例文件中提供的图片。这些图片也将在我们的网页中的img/中提供,因此您应该运行 HTTP 服务器。

如何做到...

让我们开始吧:

  1. 我们为猫查看器创建 HTML 代码:
<div>
<nav>
<ul>
<li><div data-id="0" data-url="/mycat.html">A cat</div></li>
<li><div data-id="1" data-url="/awesome.html">Some cat</div></li>
<li><div data-id="2" data-url="/somecat.html">The cat</div></li>
</ul>
</nav>
<div id="image">
</div>
</div>
  1. 我们包含了对 jQuery 和我们的脚本example.js的依赖:
<script src="img/jquery.min.js"></script>
<script src="img/example.js"></script>
  1. 可选地,我们可以添加一些非常基本的样式,使 div 元素的行为更像链接,尽管在一般情况下我们也可以使用<a>元素,但覆盖锚点的点击行为并不总是最佳主意。样式可能类似于以下内容:
<style>
nav div {
text-decoration:underline;
      cursor: pointer;
    }
</style>
  1. 至于example.js文件,我们有一个称为catson的小型类似 JSON 的结构,描述了我们的数据:
varcatson = [
  {
  "name":"Awesome cat",
  "url":"1.jpg"
  },
  {
  "name":"Crazy cat",
  "url":"2.jpg"
  },
  {
  "name":"Great cat",
  "url":"3.jpg"
  }
];
  1. 文档加载时,我们检查当前用户代理中是否支持历史 API:
$(document).ready( function() {
  function hasSupportForHistory() {
    return window.history&&history.pushState;
  }

  if ( !hasSupportForHistory() ) {
    $('body').text('Browser does not have support for History fall backing');
    return;
  }
  1. 接下来,我们为我们的导航元素添加一个点击处理程序:
  $("nav div").click( function(e) {
console.log('clicking');

var title = $(this).text(),
url = document.URL.substring(0, document.URL.lastIndexOf('/')) + $(this).data('url'),
        id = $(this).data('id'),
img = '<imgsrc="img/'+ catson[id].url +'" />',
        text = '<h1>'+catson[id].name+'</h1>';

    // change the displayed url
history.pushState(null, title, url);
    $('#image').html(text + img);
    // stop default propagation of event
e.preventDefault();
  })

此时,您应该有一个运行中的示例,如果您点击周围,您会注意到浏览器 URL 已更改,但我们依赖于只有一个页面。

如果您刷新一些其他生成的 URL,您应该会收到类似的消息:

Error code 404.
Message: File not found.
Error code explanation: 404 = Nothing matches the given URI.

这是因为我们只是模拟网页,而页面本身并不存在。

它是如何工作的...

历史 API 背后的思想很简单。它是一个允许我们通过window.history对象操纵浏览器历史的对象。

如果我们想回到上一页,我们只需调用:

window.history.back();

或者前往下一页:

window.history.forward();

还有一个更一般的方法,允许我们在历史中向前或向后移动n页,例如,要后退三页,我们调用:

window.history.go(-3);

这个 API 提供的所有方法中最有趣的可能是pushState(statetitleurl)replaceState(statetitleurl)。我们在示例中使用的第一个方法将具有给定状态对象的 URL 添加到历史堆栈中。为了完全符合规则,我们应该使用方法的第一个参数,即代表当前文档状态的状态对象。在我们的例子中,这将是catison列表的一个 cat 对象。

pushState类似,replaceState方法是更新而不是使用相同参数在历史堆栈上添加新状态。

状态对象本身可以通过history.state变量访问,类似于history.state变量,当前堆栈的大小也有一个history.length变量。history.state变量可用于存储给定段的数据,这使得它成为浏览器中存储数据的另一个选项。

注意

您可以在 WHATWG 的实时规范中阅读有关 History API 的更多信息:www.whatwg.org/specs/web-apps/current-work/multipage/history.html

你需要考虑的第一件事是制定一个聪明的路由,这样你就不会有损坏和不存在的 URL。这意味着我们可能需要在服务器端做一些工作,以便 URL 的状态可用于呈现。主要目标是提高可用性,而不是过度使用新功能,所以要小心在哪里真正需要这个功能。

对于旧版浏览器,有一个名为history.js的出色 polyfill,(github.com/browserstate/history.js),它还为开发添加了一些其他不错的功能。

还有一个名为Path.js的库,它使用 History API 进行高级路由,但也滥用hashbangs#)来实现良好的功能。

当我们谈论完全滥用时,有一个整个游戏是使用history.replaceState来使 URL 栏成为一个屏幕。这个游戏叫做 Abaroids,可以在www.thegillowfamily.co.uk/找到。

第十二章:多媒体

在本章中,我们将涵盖以下配方:

  • 播放音频文件

  • 播放视频文件

  • 自定义媒体元素的控件

  • 向您的视频添加文本

  • 多媒体嵌入

  • 使用 HTML5 音频将文本转换为语音

介绍

HTML5 添加了两个元素音频和视频,它们提供了以前使用浏览器插件完成的功能。在大多数情况下,我们发现的播放器都是基于 Flash 的,但最近情况正在改变。大多数浏览器现在对基本的 HTML5 媒体元素相关功能有很好的支持。

播放器的自定义选项非常有限,并且是特定于供应商的。大多数网站都有一些仍然使用 Flash 制作的自定义播放器,因为这是完成工作的最佳方式。

Flash 本身不会突然消失,但是使用开放标准的替代方案总是有说服力的理由。同样的情况也发生在网络游戏行业,HTML5 正在逐渐取代基于 Flash 的游戏市场。

播放音频文件

音频元素使得在浏览器中播放音频文件变得简单。这个元素的采用引起了很多争议,主要是因为缺乏格式的共同基础。最初,W3C 规范建议使用 Ogg Vorbis (www.vorbis.com/)格式。

注意

有关不同格式的浏览器支持的最新信息可以在www.caniuse.com上找到。

在这个配方中,我们将看一下元素和一些可以应用在它上面的基本属性。

准备工作

为了播放音频,我们需要一个实际的音频文件。您可以自己选择一个,或者使用示例中附带的文件。该文件将从音乐文件夹中提供。我们将使用 Jason Weinberger & the WCFSO 在免费音乐档案馆freemusicarchive.org/music/Jason_Weinberger__the_WCFSO/Jason_Weinberger__the_Waterloo-Cedar_Falls_Symphony_Orchestra/提供的 Mozart—Clarinet Concerto in A K. 622, II. Adagio。

该文件的类型是.mp3,但是为了这个例子,我们还需要一个.ogg文件。有很多在线和离线的转换器可用,所以我们可以使用media.io。例如。如果您不想麻烦,示例文件中还有一个转换后的song.ogg文件可用。

如何做...

我们将创建一个包含音频播放器的 HTML 文件:

  1. body 部分将包含以下内容:
    <p>
      <audio id="mulipleSrc" controls preload loop>
          Audio not supported
        <source src="img/Jason_Weinberger__the_WCFSO_-_04_-_Mozart_-_Clarinet_Concerto_in_A_K_622_II_Adagio.mp3"type="audio/mpeg" />
        <source src="img/song.ogg" type="audio/ogg" />
    <a href="music/song.ogg">download file </a>
      </audio>
    <p>
  1. 归因的一小段文字:
    Mozart - Clarinet Concerto in A K. 622, II. Adagio by <a href="http://freemusicarchive.org/music/Jason_Weinberger__the_WCFSO/Jason_Weinberger__the_Waterloo-Cedar_Falls_Symphony_Orchestra/">Jason Weinberger</a> & the WCFSO is licensed under a Creative Commons Attribution License.
    </p>

就是这样,您应该在浏览器中有一个可访问的音频播放器。

它是如何工作的...

旧的方法是使用<object><embed>,并传递了许多特定于播放器的参数给嵌入的.swf文件,看起来像下面的代码:

<object data="somePlayer.swf">
  <param name="quality" value="medium">
</object>

新的方法相当简单,我们可以添加一个带有指定src属性的音频元素:

<audio src="img/myFile.ogg" autoplay>
  Some fallback HTML code
</audio>

这将自动在页面上播放文件,而不给用户关于停止音乐的选项。为了让用户代理呈现播放器,我们添加了属性控件。我们通过设置src属性施加的另一个限制是只播放该文件。你可能会想为什么我们需要多个来源,但原因很简单。在当前状态下,一些浏览器支持某些格式,而其他浏览器不支持。如果我们想要在所有现代浏览器中获得支持,那么我们就提供了多个来源的选项。

注意

在撰写本文时,这是使用 Windows 操作系统的浏览器格式支持的大致情况。

浏览器/功能 WAV Opus Ogg MP3 ACC
Firefox 20
Chrome 26
IE 9
Opera

注意

除了提供浏览器支持统计数据的标准网站外,您还可以使用 SoundCloud 完成的测试套件来检查areweplayingyet.org/上的各个功能,或者在github.com/soundcloud/areweplayingyet上查看源代码。

源元素允许我们为任何媒体元素指定多个备用资源。它本身没有意义,因此应该是某些媒体标签的一部分。我们可以有多个具有不同src、类型和媒体属性的源元素。例如,我们可以有以下元素:

<source src='audio.oga' type='audio/ogg; codecs=flac'>

如果您不确定您提供的任何源是否可以在用户的浏览器中使用,您可以在source元素上附加onerror事件侦听器。这个处理程序可以用来执行一个回退。

还有一些其他属性可以用于媒体元素。除了全局属性外,媒体指定的属性包括:

  • autoplay属性:它是一个布尔值属性,定义了浏览器是否应该在具有足够大的媒体文件部分时立即开始播放。该元素的默认状态是缺失,这意味着我们默认情况下没有自动播放。

  • preload属性:它向浏览器提供提示,即使用户尚未点击播放,源文件也应该被下载。这里的想法是我们期望将来某个时候会播放文件,相当于将值设置为auto。该值也可以设置为none,这表明浏览器应该暂停预加载,因为我们不希望用户按下播放按钮。还有一个选项是将值设置为 metadata,这意味着只加载媒体文件的元数据,比如长度。

  • muted属性:它也是一个基于布尔值的属性,默认值为 false,表示将没有声音。

  • loop属性:它在完成后将音频设置为重新开始。

  • controls属性:它简单地添加了播放器控件。

  • mediagroup属性:它用于对多个媒体元素进行分组,例如,如果我们希望两个元素使用相同的控件,我们可以设置它们使用相同的媒体组。

  • crossorigin属性:它可以指定限制src属性符合跨域资源共享CORS)。

大多数其他的自定义和 JavaScript 访问将在以下教程中介绍。如果我们使用没有设置控件属性的元素,最好将音频元素的 CSS 设置为display:none,以确保它不会占用页面空间。

播放视频文件

为了在浏览器中添加对视频的本地支持,HTML5 引入了视频元素。这与音频元素非常相似,因为它们共享共同的接口,所以相同的属性适用。还有一些其他属性仅适用于视频元素。此外,源的编解码器大多不同,对于视频,我们有 H.264/MPEG-4、VP8、VP9 和 Theora。

在这个教程中,我们将看到如何通过创建一个简单的页面来使用内置播放器。

注意

HTML5 媒体元素的规范可以在www.whatwg.org/specs/web-apps/current-work/multipage/the-video-element.html找到。

准备工作

我们需要一个视频文件来使用我们的播放器,所以您可以自己选择一个。我们选择使用archive.org/details/animationandcartoons上提供的视频之一。

这个视频叫做《Boogie Woogie Bugle Boy》,由《Walter Lantz Productions》制作,在 1941 年被提名奥斯卡奖。

注意

Archive.org,也称为互联网档案馆,是一个非营利数字图书馆,其使命是“普遍获取所有知识”。除了是一个图书馆之外,它还托管了各种多媒体。更著名的子项目之一是 wayback machine,archive.org/web/web.php,这是一个网站过去状态的快照存档。还有一个名为nasaimages.org的子项目,旨在使 NASA 的图像和视频更接近公众。互联网档案馆提供的数据量非常庞大,使其成为一个很好的信息来源。

此外,我们将使用一个海报图像,在视频开始之前显示该图像;图像名为poster.png,是示例源的一部分,但您可以使用任何您喜欢的图像。

如何做...

我们创建一个简单的 HTML,其中包含视频元素,并为我们的视频提供一个源:

  1. body 部分将包含以下代码:
    <p>
      <video width="640" height="360" poster="poster.png" controls preload loop>
          Video not supported <a href="http://archive.org/download/WalterLantz-BoogieWoogieBugleBoy1941/WalterLantz-BoogieWoogieBugleBoy1941.ogv"> download </a> instead
        <source src="img/WalterLantz-BoogieWoogieBugleBoy1941.ogv" type="video/ogg" />
      </video>
  1. 并且归因将包含以下代码:
    <p>
    Video is part of animation shorts on <a href="http://archive.org/details/more_animation"> archive.org</a>. The video
    is titled : Walter Lantz - Boogie Woogie Bugle Boy
    </p>

打开后,我们应该有一个运行中的视频播放器,就像以下截图一样:

如何做...

它是如何工作的...

视频元素与音频元素非常相似,所有音频元素的属性都适用于视频元素。视频特定的属性包括:

  • Widthheight:它们表示元素的宽度和高度。控制将调整视频大小以适应指定的大小。视频的实际大小取决于正在播放的文件。

  • poster:这是一个属性,使我们能够在用户决定播放视频之前在视频元素上显示静态图像。

通过向视频添加各种属性的组合,我们可以使用户体验更好;在我们的代码示例中,视频将居中显示,因为宽度和高度属性与视频的实际宽度和高度不匹配。

如果我们想要播放视频的特定范围,也有内置的支持。例如,我们可能希望从第 30 秒播放到第 40 秒。要在src属性的 URL 中执行此操作,我们在哈希(#)后附加一个片段定义,如以下代码所示:

<source src="img/myvideo.ogv#t=30,40" />

通用定义如下:

#t=[starttime],[endtime]

变量starttimeendtime是可选的,可以是指定从开始的秒数,也可以是小时:分钟:秒的格式。

如果我们想要从第 80 秒播放到视频结束,源将如下所示:

<source src="img/myvideo.ogv#t=80" />

视频通常以一些有损压缩格式编码,因为它们作为原始格式传输时非常大。

注意

您可以在以下链接中了解有关有损压缩的更多信息en.wikipedia.org/wiki/Lossy_compression。其主要思想是通过牺牲一定程度的信息和质量来显著减小原始视频的大小。

微软和苹果拥有使用 H.264 的许可证,或者更常见的是通过扩展名.mp4.m4v。该编解码器有许多不同的版本和组合,此外,它受 YouTube 和 iTunes 的支持,使其成为一个非常受欢迎的选择。Firefox 和 Chrome 原本计划放弃对其的支持,因为该格式是专有的,并且必须支付一定的特许费,这使得它成为一个非常有争议的选择。Firefox 计划在将来支持该编解码器,但前提是有第三方解码器可用。

注意

有关 H.264 的更多信息,请访问en.wikipedia.org/wiki/H.264/MPEG-4_AVC

Ogg Theora 来自Xiph.org,这个组织提供了我们在音频元素配方中使用的.ogg容器和 Vorbis 音频编解码器,以及其他贡献。这受到 Firefox、Opera 和 Chrome 的支持,但至少默认情况下不受 IE 和 Safari 的支持。

注意

有关 Ogg Theora 的更多信息,请访问www.theora.org/

WebM 支持 Vorbis 作为音频编解码器,支持 VP8 作为视频编解码器。VP8 是由一家名为 On2 的公司开发的编解码器,后来被 Google 收购。此外,WebM 原生支持 Chrome、Opera 和 Firefox,至于 IE 和 Safari,用户需要下载额外的插件。

注意

有关 WebM 的更多信息,包括格式、工具和相关文档,请访问www.webmproject.org/

还有更多...

拥有多个来源是好的,但并不总是一个选择。我们还希望为旧浏览器提供备用方案,为此我们必须依赖插件。

如果您引用来自 YouTube 或 Vimeo 等第三方付费网站的视频,您可以简单地放置嵌入播放器的iframe

<iframe width="420" height="345"src="img/WEbzZP-_Ssc">
</iframe>

还有一些服务器 JavaScript 库可以使备用过程变得简单。其中之一是mediaelementjs.com/

安装很简单,因为我们只需要包含.js.css文件作为依赖项,如下所示:

<code><script src="img/jquery.js"></script>
  <script src="img/mediaelement-and-player.min.js"></script>
  <link rel="stylesheet" href="mediaelementplayer.css" />
</code>

至于备用播放器:

<video src="img/myvideo.ogv" />
  <!-- other sources -->
  <object width="320" height="240" type="application/x-shockwave-flash" data="flashmediaelement.swf">
    <param name="movie" value="flashmediaelement.swf" />
    <param name="flashvars" value="controls=true&file=myvideo.mp4" />
    <img src="img/myvideo.jpg" width="320" height="240" title="No video playback capabilities" />
  </object>
</video>

备用播放器只是mediaelement.js的众多功能之一;移动浏览器有很多选项,API 也有很多简化。

注意

如果您对可能的转换工具或编解码器背后的政策以及对它们的详细解释感兴趣,请查看 Mark Pilgram 的书Dive into HTML5,可在fortuito.us/diveintohtml5/video.html上找到。

还有一篇有趣的文章,标题为“面向所有人的视频”,讨论了在不同浏览器上启用视频支持的主题,camendesign.com/code/video_for_everybody

自定义媒体元素的控件

媒体元素,目前是视频和音频,可以使用 JavaScript 进行控制,因为这些元素本身包含有用的方法和属性。在这个配方中,我们将介绍一些最基本的功能和方法,这些功能和方法可以应用在具有HTMLMediaElement接口的元素上。

注意

HTML5 媒体元素的规范可以在www.w3.org/TR/html5/embedded-content-0.html#htmlmediaelement找到。

准备工作

在这个配方中,我们还需要一个视频文件,所以我们可以使用上一个配方中的相同视频。

如何做...

我们首先创建一个 JavaScript 控制器,它将具有媒体播放器的非常基本的功能。

  1. 我们的控制器方法将接受一个命令的选择器并执行该命令,我们需要以下内容:
var videoController = (function () {
  var my = {};
  function findElement(selector){
   var result = document.querySelector(selector);
   if (!result) {
    throw "element " + selector + " not found ";
   }
   return result;
  }

  function updatePlaybackRate(el, speed) {
   el.playbackRate += speed;
  }

  function updateVolume(el, amount) {
   el.volume += amount;
  }

  my.play = function(video) {
   var el = findElement(video);
   el.play();
  }

  my.pause = function(video) {
   var el = findElement(video);
   el.pause();
  }

  my.toggleMute = function(video) {
   var el = findElement(video);
    el.muted = !el.muted;
  }

  my.increasePlaybackRate = function(video, speed) {
   var el = findElement(video);
   updatePlaybackRate(el, speed);
  }

  my.decreasePlaybackRate = function(video, speed) {
   var el = findElement(video);
   updatePlaybackRate(el, -speed);
  }

  my.increaseVolume = function(video, amount) {
   var el = findElement(video);
   updateVolume(el, amount)
  }
  return my;
}());

现在在一个简单的场景中,我们可能只需使用标准方法而不添加另一层,但这里的想法是,我们可以根据需要扩展功能,因为我们可以从 JavaScript 中访问元素。

  1. 对于 HTML,我们将拥有与播放视频配方中相似的版本。我们将有一些按钮,这些按钮将使用我们的视频控制器,并额外添加一个简单的样式。让我们在头部添加以下内容:
  <head>
    <title>Video custom controls</title>
    <style>
      video {
        box-shadow: 0 0 10px #11b;
      }
    </style>
  </head>
  1. 身体部分将包含控制按钮:
    <p>
      <video id="theVideo" width="640" height="480" poster="poster.png" preload loop>
          Video playback not supported <a href="http://archive.org/download/WalterLantz-BoogieWoogieBugleBoy1941/WalterLantz-BoogieWoogieBugleBoy1941.ogv"> download </a>
        <source src="img/WalterLantz-BoogieWoogieBugleBoy1941.ogv" type="video/ogg" />
      </video>
    </body>
    <p>
    The Dashboard: <br/>
      <button onclick="videoController.play('#theVideo')">Play</button>
      <button onclick="videoController.pause('#theVideo')">Pause</button>
      <button onclick="videoController.increasePlaybackRate('#theVideo',0.1)">Speed++</button>
      <button onclick="videoController.decreasePlaybackRate('#theVideo',0.1)">Speed-- </button>
      <button onclick="videoController.decreaseVolume('#theVideo', 0.2) ">Vol-</button>
      <button onclick="videoController.increaseVolume('#theVideo', 0.2) ">Vol+</button>
      <button onclick="videoController.toggleMute('#theVideo')">Toggle Mute</button>
    <p>
    Video is part of animation shorts on <a href="http://archive.org/details/more_animation"> archive.org</a>. The video
    is titled : Walter Lantz - Boogie Woogie Bugle Boy
    </p>
  1. 然后我们将依赖项添加到我们的example.js文件中。
<script src="img/example.js"> </script>

之后我们应该有一个完全运行的视频播放器。

它是如何工作的...

使用 JavaScript,我们可以访问和操作任何媒体元素的属性。这个选项使我们能够对标准元素进行许多不同类型的定制。这些属性大多数在HTMLMediaElement中定义;在那里我们可以读取和写入currentTimeplaybackRatevolumemuteddefaultMuted等等。

注意

有关更全面的HTMLMediaElement属性以及只读属性,请参考www.w3.org/TR/html5/embedded-content-0.html#media-elements上可用的规范。

通过更改属性,我们可以制作自定义播放器,以及各种不同的视觉更新。媒体元素会触发大量不同的事件。在这些事件上,我们可以附加事件侦听器,并根据状态更改进行更新。以下事件会被触发:loadstartabortcanplaycanplaythroughdurationchangeemptiedendederrorloadeddataloadedmetadatapauseplayplayingprogressratechangeseekedseekingstalledsuspendtimeupdatevolumechangewaiting

注意

事件的名称是不言自明的,如果您对特定事件感兴趣,可以阅读文档了解它们的用途,文档位于www.w3.org/TR/html5/embedded-content-0.html#mediaevents

在我们的示例中,我们可以添加一个监听器来显示当前速率的速率:

  my.displayRate = function (video, output) {
   var vid = findElement(video),
       out = findElement(output);

   vid.addEventListener('ratechange', function(e) {
     console.log(e);
     out.innerHTML = 'Speed x' + this.playbackRate;
   }, false);
  }

然后在 HTML 中添加一个输出元素,并调用我们新添加的方法:

    <output id="speed"></output>
    <script>
      videoController.displayRate("#theVideo","#speed");
    </script>

现在,第一次播放视频时,速率更改事件会被触发,并且速率设置为1。每次连续的速率更改都会触发相同的事件。

注意

W3C 在www.w3.org/2010/05/video/mediaevents.html上有一个关于媒体元素触发的事件的很好的演示。

这里还有一件有趣的事情要注意,<audio>元素也可以用于视频文件,但只会播放文件中的音频流。

向您的视频添加文本

在显示多语言视频时,我们经常希望为讲其他语言的人提供文本。这是许多会议演讲以及许多电影和电视节目的常见做法。为了在视频中启用外部文本轨道资源,创建了 WebVTT(dev.w3.org/html5/webvtt/)标准。

准备工作

为简单起见,我们将使用与其他示例中相同的视频以及海报图像。至于其他文件,我们将自己创建它们。您也可以自己选择其他视频,因为视频本身并不那么重要。

如何做...

我们从 HTML 开始,其中包括视频元素,另外还添加了轨道元素以及简单的example.js。执行以下步骤:

  1. 在 body 元素中包括:
    <p>
      <video width="640" height="360" poster="poster.png" controls preload loop>
     Video playback not supported <a href="http://archive.org/download/WalterLantz-BoogieWoogieBugleBoy1941/WalterLantz-BoogieWoogieBugleBoy1941.ogv"> download</a> instead
        <source
        src="img/WalterLantz-BoogieWoogieBugleBoy1941.ogv" type="video/ogg" />
        <track src="img/video.vtt" kind="subtitles" srclang="en" label="English" default />
        <track src="img/karaoke.vtt" kind="captions" srclang="gb" label="Other" />
      </video>
    <p>
    Video is part of animation shorts on <a href="http://archive.org/details/more_animation"> archive.org</a>. The video
    is titled : Walter Lantz - Boogie Woogie Bugle Boy
    </p>
    <script src="img/example.js"></script>
  1. JavaScript 只会记录我们的视频元素可用的对象。这里的想法是展示可以通过代码访问和操作轨道。脚本将包含以下内容:
(function(){
  var video = document.getElementById('theVideo'),
      textTracks = video.textTracks;

   for(var i=0; i < textTracks.length; i++){
    console.log(textTracks[i]);
   }
}())
  1. 至于我们为轨道创建的.vtt文件,我们将手动创建它们。文件video.vtt将包含以下内容:
WEBVTT

1
00:00:01.000 --> 00:00:13.000
this is the video introduction

2
00:00:15.000 --> 00:00:40.000
There is also some awesome info in
multiple lines.
Why you ask?
Why not ...

3
00:00:42.000 --> 00:01:40.000
We can use <b>HTML</b> as well
<i> Why not?</i>

4
00:01:42.000 --> 00:02:40.000
{
"name": "Some JSON data",
"other": "it should be good for meta data"
}

5
00:02:41.000 --> 00:03:40.000 vertical:lr
text can be vertical

6
00:03:42.000 --> 00:04:40.000 align:start size:50%
text can have different size relative to frame
  1. 至于karaoke.vtt,它将包含以下代码:
WEBVTT

1
00:00:01.000 --> 00:00:10.000
This is some karaoke style  <00:00:01.000>And more <00:00:03.000> even more  <00:00:07.000>  

运行示例后,我们应该在给定范围内有字幕。

提示

如果您手动构建 WebVTT 文件,您会注意到很容易出错。有一个很好的验证器可用于quuz.org/webvtt/,源代码在github.com/annevk/webvtt上。

它是如何工作的...

视频已经有一段时间了,但添加字幕并不是一个选择。轨道元素以标准方式使我们能够向视频添加信息。轨道不仅用于字幕,还可以用于其他类型的定时提示。

注意

cue这个词的一般定义是,它代表了一个说或做的事情,作为一个信号,让演员或其他表演者进入或开始他们的讲话或表演。

Cues 可以包含其他数据格式,如 JSON、XML 或 CSV。在我们的示例中,我们包含了一个小的 JSON 数据片段。这些数据可以以许多不同的方式使用,因为它与特定时间段相关联,但字幕并不是它的真正用途。

轨道元素的kind属性可以包含以下值:

  • 字幕:这是给定语言的转录或翻译。

  • 字幕:它与字幕非常相似,但也可以包括音效或其他音频。这种类型的主要意图是用于音频不可用的情况。

  • 描述:这是视频的文本描述,用于在视觉部分不可用的情况下使用。例如,它可以为盲人或无法跟随屏幕的用户提供描述。

  • 章节:此轨道可以包含给定时期的章节标题。

  • 元数据:这是一个非常有用的轨道,用于存储以后可以由脚本使用的元数据。

除了kind属性之外,还有src属性是必需的,并显示轨道源的 URL。轨道元素还可以包含srclang,其中包含定时轨道的语言标签。

注意

语言标签通常具有两个字母的唯一键,用于表示特定语言。有关更多详细信息,您可以查看tools.ietf.org/html/bcp47

还有一个default属性,如果在轨道上存在,则该轨道将成为默认显示的轨道。

此外,我们还可以使用label属性,该属性可以具有自由文本值,用于指定元素的唯一标签。

注意

轨道元素的一个巧妙用法可以在以下网址找到:www.samdutton.net/mapTrack/

WebVTT 标准定义了文件需要以字符串"WEBVTT"开头。在此之后,我们有提示定义,零个或多个此类元素。

每个提示元素具有以下形式:

[idstring]
[hh:]mm:ss.ttt --> [hh:]mm:ss.ttt [cue settings]
Text string

idstring是一个可选元素,但如果我们需要使用脚本访问提示,则最好指定它。至于timestamp,我们有一个标准格式,其中小时是可选的。第二个timestamp必须大于第一个。

文本字符串允许包含简单的 HTML 格式,如<b><i><u>元素。还有一个选项可以添加<c>元素,用于为文本的部分添加 CSS 类,例如<c.className>styled text </c>。还有一个选项可以添加所谓的语音标签<v someLabel> the awesome text </v>

提示设置也是可选的,并且在时间范围之后附加。在此设置中,我们可以选择文本是水平显示还是垂直显示。设置是区分大小写的,因此它们必须像示例中显示的那样小写。可以应用以下设置:

  • 垂直:它与值vertical:rl一起使用,其中rl代表从右到左的书写,vertical:lr代表从左到右。

  • :此设置指定文本将在垂直方向显示的位置,或者在我们已经使用垂直时,它指定水平位置。该值用百分比或数字指定,其中正值表示顶部,负值表示底部。例如,line:0line:0%表示顶部,line:-1%line:100%表示底部。

  • 位置:这是一个设置,用于指定文本在水平方向上显示的位置,或者如果我们已经设置了垂直属性,则指定文本在垂直方向上显示的位置。它的值应该在 0 到 100 之间。例如,可以是position:100%表示右侧。

  • 大小:它指定文本区域的宽度/高度,以百分比表示,具体取决于附加的垂直设置。例如,size:100%表示文本区域将显示。

  • 对齐:这是一个属性,用于设置文本在由大小设置定义的区域内的对齐方式。它可以具有以下值align:startalign:middlealign:end

在文本字符串中,我们还可以按照给定单词的更详细的出现顺序,以一种卡拉 OK 的风格。例如,参见以下内容:

This is some karaoke style  <00:00:02.000>And more <00:00:03.000>

它说明在 2 秒之前我们有一些文本,活动提示And more在 2 到 3 秒之间。

关于文本字符串的另一点是,它不能包含字符串-->,和字符<,因为它们是保留字符。但不用担心,我们总是可以使用转义版本,例如&amp;代替&

如果我们使用文件进行元数据跟踪,则不适用这些限制。

还有更多...

我们还可以使用 CSS 样式文本。如前所述,VTT 文件可以包含带有<c.someClass>的轨道,以进行更精细的样式设置,但在一般情况下,我们希望对整个轨道应用样式。可以对所有提示应用样式:

::cue  {
        color: black;
        text-transform: lowercase;
        font-family: "Comic Sans";
}

但是,通过将他们的字幕设置为 Comic Sans,您可能会使用户感到疏远。

过去的提示::cue:past{}::cue:future{}也有选择器,对于制作卡拉 OK 式的渲染很有用。我们还可以使用::cue(selector)伪选择器来定位匹配某些条件的节点。

并非所有功能在现代浏览器中都完全可用,目前写作时最兼容的是 Chrome,因此对于其他浏览器来说,使用 polyfill 是一个好主意。一个这样的库是captionatorjs.com/,它为所有现代浏览器添加了支持。除了为 WebVTT 添加支持外,它还支持格式如.sub.srt和 YouTube 的.sbv

还有另一种为视频轨道开发的格式。它的名字是定时文本标记语言TTML)1.0 www.w3.org/TR/ttaf1-dfxp/,目前只有 IE 支持,没有计划在其他浏览器中获得支持。这个标准更复杂,基于 XML,因此更加冗长。

嵌入多媒体

媒体元素可以与其他元素合作并组合在一起。各种 CSS 属性可以应用于元素,并且有选项将视频与 SVG 组合。我们可以在画布元素中嵌入视频,并对渲染的图像应用处理。

在这个示例中,我们将创建一个简单的情况,其中我们在画布中嵌入一个视频。

准备工作

在这个示例中,我们将需要一个视频用于我们的视频元素,另一个要求是视频具有跨域资源共享支持,或者位于我们的本地服务器上。确保这一点的最简单方法是使用我们本地运行的服务器上的视频。

注意

www.spacetelescope.org/videos/astro_bw/的 NASA 和 ESA 提供了许多不同格式的视频。

如何做到...

我们将通过以下步骤在画布元素上渲染视频:

  1. 首先从 HTML 文件开始,我们添加一个视频元素和一个画布:
      <video id="myVideo" width="640" height="360" poster="poster.png" controls preload>
          Video not supported
        <source src="img/video.mp4" type="video/mp4" />
      </video>
        <canvas id="myCanvas" width="640" height="360"> </canvas>
        <button id="start">start showing canvas </button>
    <script src="img/example.js"> </script>
  1. 我们的 JavaScript 代码示例将附加事件处理程序,以在画布元素上开始渲染视频的灰度版本:
(function (){
  var button = document.getElementById('start'),
      video = document.getElementById('myVideo'),
      canvas = document.getElementById('myCanvas');

  button.addEventListener("click", function() {
    console.log('started drawing video');
    drawVideo();
  },false);

  function drawVideo(){
   var context = canvas.getContext('2d');
   // 0,0 means to right corner
  context.drawImage(video, 0, 0);
   var pixels = context.getImageData(0,0,640,480);
   pixels = toGrayScale(pixels);
   context.putImageData(pixels,0,0);
   // re-draw
   setTimeout(drawVideo,10);
  }

  function toGrayScale(pixels) {
    var d = pixels.data;
    for (var i=0; i<d.length; i+=4) {
      var r = d[i],
          g = d[i+1],
          b = d[i+2],
          v = 0.2126*r + 0.7152*g + 0.0722*b;
      d[i] = d[i+1] = d[i+2] = v
    }
    return pixels;
  };
}())

我们应该有一个运行的示例。这里的另一个附加说明是,我们的原始视频应该是彩色的,以便注意到差异。

它是如何工作的...

视频元素应该在这一点上是清晰的,至于画布,我们将从限制开始。在画布上绘制图像有 CORS 限制。这种安全约束实际上是有道理的,因为我们正在从图像中读取数据并根据此执行代码。这可能会被一些恶意来源利用,因此添加了这些约束。

使用canvas.getContext('2d'),我们可以获得一个绘图上下文,可以在其中绘制来自视频元素的当前图像。在绘制图像时,我们可以修改单个像素。这使我们有可能在视频上创建滤镜。

对于我们的示例,我们创建了一个简单的灰度滤镜。滤镜函数toGrayScale遍历像素数据,因为每三个值代表 RGB 中像素的颜色,我们读取它们的数据并创建一个调整后的值:

  v = 0.2126*r + 0.7152*g + 0.0722*b;

接下来,我们将调整后的值应用于所有三个值。这些魔术数字被选择为了补偿红色和蓝色值,因为人眼对它们的平均值不太敏感。我们可以在这里使用三个值的平均值,结果会类似。

注意

如果您对其他滤镜感兴趣,可以在www.html5rocks.com/en/tutorials/canvas/imagefilters/上找到一篇关于这个主题的好文章,这些滤镜适用于图像,但同样适用于视频。

还有更多...

另一个值得一看的有趣演示是类似立方体的视频播放器,html5playbook.appspot.com/#Cube,它使用各种不同的方式来创建酷炫的效果。

如果您对在 HTML5 应用程序中处理和合成音频感兴趣,可以在www.w3.org/TR/webaudio/上找到一个新的高级 API,可以实现这一点。

使用 HTML5 音频将文本转换为语音

如果我们今天要构建基于网络的导航应用程序,大部分组件已经可以使用。有 Google 地图或开放街道地图组件来显示地图,以及提供驾驶路线的 API 服务。

但是关于基于语音的导航指引呢?那不是需要另一个将文本转换为语音的 API 服务吗?

由于 HTML5 音频和 Emscripten(一个 C 到 JavaScript 编译器),我们现在可以在浏览器中完全使用名为 espeak 的免费文本到语音引擎。

在这个例子中,我们将使用 espeak 来生成用户在简单页面上输入的文本。大部分工作将包括准备工作-我们需要设置espeak.js

准备好了

我们需要从(github.com/html5-ds-book/speak-js)下载 speak.js。单击下载 zip 按钮并将存档下载到新创建的文件夹中。在该文件夹中提取存档-它应该创建一个名为speak-js-master的子文件夹。

如何做...

执行以下步骤:

  1. 创建包含文本输入字段和“说话”按钮的页面index.html
<!doctype html>
<html>
  <head>
    <script src="img/jquery.min.js"></script>
    <script src="img/speakClient.js"></script>
    <script src="img/example.js"></script>
    <meta charset="utf8">    
  </head>
  <body>
    <div id="audio"></div>
    <input type="text" id="text" value="" placeholder="Enter text here">
    <button id="speak">Speak</button>
  </body>
</html>
  1. 创建example.js并为按钮添加点击操作:
$(function() {
    $("#speak").on('click', function(){
        speak($("#text").val());
    });
});
  1. 从命令行安装http-server(如果尚未安装),然后启动服务器:
npm install -g http-server
http-server
  1. 在浏览器中打开localhost:8080并测试演示。

它是如何工作的...

将文本转换为语音的引擎是 eSpeak (espeak.sourceforge.net/)。这个引擎是用 C 编写的,然而,浏览器原生支持的唯一语言是 JavaScript。我们如何在浏览器中使用这个引擎?

Emscripten 是一个旨在解决这一限制的编译器。它接受由 LLVM 编译器从 C 或 C++源代码生成的 LLVM 字节码,并将其转换为 JavaScript。Emscripen 利用了许多现代 JavaScript 特性,如类型化数组,并依赖于现代优化 JavaScript JIT 编译器的出色性能。

为了避免阻塞浏览器,语音生成器是从在speakClient.js中创建的 Web Worker 中调用的。生成的 WAV 数据由工作线程传回,转换为 base64 编码,并作为数据 URL 传递给新创建的音频元素。然后,该元素被附加到页面上的#audio 元素上,并通过调用play方法来激活播放。

还有更多...

Espeak 根据 GNU GPL v3 许可证授权。因此,它可能不适用于专有项目。

有关 Emscripten 的更多信息可以在 Emscripten 维基上找到:github.com/kripken/emscripten/wiki

附录 A. 安装 Node.js 和使用 npm

介绍

Node.js 是建立在 Google Chrome 的 V8 JavaScript 引擎之上的事件驱动平台。该平台为 V8 实现了完全非阻塞的 I/O,并主要用于构建实时 I/O 密集型的 Web 应用程序。

Node.js 安装程序提供以下两个主要组件:

  • node 二进制文件,可用于运行为该平台编写的 JavaScript 文件

  • node 包管理器npm,可用于安装由 node 社区编写的 node 库和工具

安装 Node.js

Node.js 的安装程序和分发程序可以在其官方网站nodejs.org/上找到。安装过程因操作系统而异。

在 Windows 上,提供了两个基于 MSI 的安装程序,一个用于 32 位操作系统,另一个用于 64 位操作系统。要在 Windows 上安装 Node.js,只需下载并执行安装程序。

对于 Mac OS X,同一位置提供了一个pkg安装程序;下载并运行 PKG 文件将允许您使用 Apple 安装程序安装 Node.js。

在 Linux 上,安装过程取决于发行版。许多流行发行版的说明可在 node 维基上找到github.com/joyent/node/wiki/Installing-Node.js-via-package-manager

使用 npm

Node.js 安装程序附带了 node 包管理器 npm。npm 用于命令行;要使用它,我们需要运行一个终端程序(命令提示符)。

在 Windows 上,我们可以使用基本的cmd.exe,或者我们可以从sourceforge.net/projects/console/下载并安装 Console。

在 Mac OS X 上,Terminal.app可用于运行命令。

在 Linux 上,使用您喜欢的终端。Ubuntu Linux 上的默认终端是 gnome 终端。

打开终端并输入:npm。此命令运行 npm 而不带任何参数。结果,npm 将打印一个列出可用子命令的一般使用概述。

安装本地包

让我们为名为test的项目创建一个空目录,转到该目录,并在那里使用 npm 安装underscore库。运行以下命令:

mkdir test
cd test
npm install underscore

最后一个命令将告诉 npm 运行带有参数underscoreinstall子命令,这将在本地安装 underscore 包。npm 将在下载和安装包时输出一些进度信息。

在安装包时,npm 会在当前目录中创建一个名为node_modules的子目录。在该目录中,它会为安装的包创建另一个目录。在这种情况下,underscore 包将放置在underscore目录中。

安装全局包

一些 npm 包设计为全局安装。全局包为操作系统添加新功能。例如,可以全局安装 coffee-script 包,这将使coffee命令在我们的系统上可用。

要安装全局包,我们使用-g 开关。看下面的例子:

npm install -g coffee-script

在某些系统上,需要请求管理员权限来运行此程序。您可以使用sudo命令来做到这一点:

sudo npm install -g coffee-script

npm 将下载并安装 coffee-script 以及其所有依赖项。完成后,我们可以开始使用coffee命令,在系统上现在可用。我们现在可以运行 coffee-script 代码。假设我们想要运行一个简单的内联 hello-world 脚本;我们可以使用-e开关。看下面的例子:

coffee -e "echo 'Hello world'"

要了解有关 npm 子命令的全局包的更多信息,我们可以使用 npm 的 help 子命令。例如,要了解有关install子命令的更多信息,请运行以下命令:

npm help install

有关最新版本的 npm 的更多信息可以在官方 npm 文档npmjs.org/doc/中找到。

附录 B.社区和资源

WHATWG

2004 年,WHAT 工作组由 Mozilla、Apple 和 Opera 的成员组成。更常用的名称 WHATWG 代表 Web 超文本应用技术工作组。该组的主要目标是促进 HTML 的演进和发展。W3C 对 HTML 的缓慢发展也是该组成立的原因。

该组的重点是 HTML 标准,还包括 Web 存储、Web 套接字、Web 工作者和服务器端事件。过去还开发和讨论了其他标准。该规范的编辑是 Ian "Hixie" Hickson (www.hixie.ch/)。

我们在整本书中经常提到 WHATWG 规范,它是最新的文档,因为它是一个活动标准。这意味着标准会随着社区提出的变更请求而不断更新。另一方面,规范并不会以不兼容的方式简单地中断。

有关该组织的更多信息可从www.whatwg.org/找到。

还有一个面向开发者的规范可在developers.whatwg.org/找到。

万维网联盟

这个组织也被称为 W3C,是主要的国际 Web 标准制定者。它成立于 1994 年,由 Tim Berners-Lee 领导,拥有全职员工负责协调各种规范。

该联盟由其成员(www.w3.org/Consortium/Member/List)管理,包括非营利组织、公司以及个人。您必须成为会员才能访问该网站。只要 W3C 批准申请,任何人都可以成为会员。此外,每个成员支付根据所在国家收入调整的费用。

有人批评了成员基础,因为它主要由能够支付并在会议和旅行上花费大量资金的公司组成。

2006 年,W3C 宣布将与 WHATWG 合作,停止开发从未完全起飞的 XHTML 标准。目前,这两个组织有不同的目标。WHATWG 更关注浏览器应该追求的目标,而 W3C 则具有活动标准的特定快照,使它们分道扬镳。WHATWG 的编辑 Hixie 向两个组织发送反馈。有关这两个规范的差异的更多信息,请参见以下网址:

www.whatwg.org/specs/web-apps/current-work/multipage/introduction.html#how-do-the-whatwg-and-w3c-specifications-differ?.

W3C 的官方网站是www.w3.org/

其他资源

Mozilla 开发者网络(MDN),developer.mozilla.org)提供了大量关于 Web 开发的数据。有 HTML、JavaScript、CSS、DOM、AJAX、SVG、WebGL 等主题。一些信息可能是针对 Firefox 的,但大多数不是。驱动 MDN 的平台 Kuma 可在 GitHub 上找到,github.com/mozilla/kuma。此外,还有许多其他方式可以为文档的改进做出贡献,更多信息请参见developer.mozilla.org/en-US/docs/Project:About#About_MDN。目前,MDN 是最全面和最简单的日常 Web 开发文档。

HTML5 rocks(www.html5rocks.com)是一个很好的教程和文章资源。该项目由 Google 和大多数 Chrome 团队运行,但也有许多其他非 Google 员工加入。一些教程使用了很多 Chrome 特定的内容,但它们仍然是目前最好的文章之一。该项目可作为 GitHub 存储库在github.com/html5rocks上找到。

深入了解 HTML5diveintohtml5.info/)是 Mark Pilgrim 的一本文化书籍,既有趣又提供了很好的起点。

HTML5 测试(html5test.com/)可以为您的浏览器打分,并提供不同主要浏览器之间的比较。

更详细的跨浏览器支持信息可在 quirks mode www.quirksmode.org/compatibility.html上找到。

posted @ 2024-05-24 11:09  绝不原创的飞龙  阅读(11)  评论(0编辑  收藏  举报