jQuery-热点-二-

48 阅读47分钟

jQuery 热点(二)

原文:zh.annas-archive.org/md5/80D5F95AD538B43FFB0AA93A33E9B04F

译者:飞龙

协议:CC BY-NC-SA 4.0

第五章:jQuery 文件上传器

现在可以仅使用一些最新的 HTML5 API 和 jQuery 创建一个功能齐全的文件上传小部件。我们可以轻松添加对高级功能的支持,例如多个上传和拖放界面,而且只需稍微借助 jQuery UI,我们还可以添加引人入胜的 UI 功能,例如详细的文件信息和进度反馈。

任务简报

在本项目中,我们将使用 HTML5 文件 API 提供核心行为构建一个高级多文件上传小部件,并使用 jQuery 和 jQuery UI 构建一个引人入胜的界面,访问者将乐于使用。

我们将构建小部件作为 jQuery 插件,因为这是我们可能想要封装的东西,这样我们就可以将其放入许多页面中,并且只需进行一些配置即可使其工作,而不是每次都需要构建自定义解决方案。

为什么很棒?

jQuery 提供了一些出色的功能,使编写可重复使用的插件变得轻而易举。在本项目中,我们将看到打包特定功能和生成所有必要标记以及添加所有所需类型行为的机制是多么容易。

在客户端处理文件上传为我们提供了许多增强体验功能的机会,包括有关每个选择的上传文件的信息,以及一个丰富的进度指示器,使访问者了解上传可能需要多长时间。

我们还可以允许访问者在上传过程中取消上传,或在上传开始之前删除先前选择的文件。这些功能纯粹使用服务器端技术处理文件上传是不可用的。

在此项目结束时,我们将制作以下小部件:

为什么很棒?

你的热门目标

要完成项目,我们需要完成以下任务:

  • 创建页面和插件包装器

  • 生成基础标记

  • 添加接收要上传文件的事件处理程序

  • 显示所选文件列表

  • 从上传列表中删除文件

  • 添加 jQuery UI 进度指示器

  • 上传所选文件

  • 报告成功并整理工作

任务清单

与我们以前的一些项目一样,除了使用 jQuery,我们还将在本项目中使用 jQuery UI。我们在书的开头下载的 jQuery UI 副本应该已经包含我们需要的所有小部件。

像以前的项目一样,我们还需要在此项目中使用 Web 服务器,这意味着使用正确的 http:// URL 运行页面,而不是 file:/// URL。有关兼容的 Web 服务器信息,请参阅以前的项目。

创建页面和插件包装器

在此任务中,我们将创建链接到所需资源的页面,并添加我们的插件将驻留在其中的包装器。

为起飞做准备

在这一点上,我们应该创建这个项目所需的不同文件。首先,在主项目文件夹中保存一个模板文件的新副本,并将其命名为 uploader.html。我们还需要一个新的样式表,应该保存在 css 文件夹中,命名为 uploader.css,以及一个新的 JavaScript 文件,应该保存在 js 文件夹中,命名为 uploader.js

新页面应链接到 jQuery UI 样式表,以便获取进度条小部件所需的样式,并且在页面的 <head> 中,直接在现有的对 common.css 的链接之后,添加该项目的样式表:

<link rel="stylesheet" href="css/ui-lightness/jquery-ui-1.10.0.custom.min.css" />

<link rel="stylesheet" href="css/uploader.css" />

我们还需要链接到 jQuery UI 和此示例的 JavaScript 文件。我们应该在现有的用于 jQuery 的 <script> 元素之后直接添加这两个脚本文件:

<script src="img/jquery-ui-1.10.0.custom.min.js"></script>
<script src="img/uploader.js"></script>

启动推进器

我们的插件只需要一个容器,小部件就可以将所需的标记渲染到其中。在页面的 <body> 中,在链接到不同 JavaScript 资源的 <script> 元素之前,添加以下代码:

<div id="uploader"></div>

除了链接到包含我们的插件代码的脚本文件之外,我们还需要调用插件以初始化它。在现有的 <script> 元素之后,直接添加以下代码:

<script>
    $("#uploader").up();
</script>

插件的包装器是一个简单的结构,我们将用它来初始化小部件。在 uploader.js 中,添加以下代码:

;(function ($) {

    var defaults = {
        strings: {
            title: "Up - A jQuery uploader",
            dropText: "Drag files here",
            altText: "Or select using the button",
            buttons: {
                choose: "Choose files", 
                upload: "Upload files" 
            },
            tableHeadings: [
                "Type", "Name", "Size", "Remove all x"
            ]
        }
    }

    function Up(el, opts) {

        this.config = $.extend(true, {}, defaults, opts);
        this.el = el;
        this.fileList = [];
        this.allXHR = [];
    }

    $.fn.up = function(options) {
        new Up(this, options);
        return this;
    };

}(jQuery));

目标完成 - 迷你简报

构建 jQuery 插件时,我们能做的最好的事情就是使我们的插件易于使用。根据插件的用途,最好尽可能少地有先决条件,因此,如果插件需要复杂的标记结构,通常最好让插件渲染它需要的标记,而不是让插件的用户尝试添加所有必需的元素。

鉴于此,我们将编写我们的插件,使得页面上只需要一个简单的容器,插件就可以将标记渲染到其中。我们在页面上添加了这个容器,并为其添加了一个 id 属性以便于选择。

使用我们的插件的开发人员将需要一种调用它的方法。jQuery 插件通过向 jQuery 对象添加附加方法来扩展 jQuery 对象,我们的插件将向 jQuery 添加一个名为 up() 的新方法,该方法像任何其他 jQuery 方法名称一样被调用 - 在被 jQuery 选择的一组元素上。

我们在 <body> 元素底部添加的额外 <script> 元素调用了我们的插件方法,以调用插件,这就是使用我们的插件的人会调用它的方式。

在我们的脚本文件中,我们以一个分号和一个立即调用的匿名函数开始。分号支持 jQuery 插件的模块化特性,并保护我们的插件免受其他不正确停止执行的插件的影响。

如果页面上另一个插件的最后一条语句或表达式没有以分号结束,而我们的插件又没有以分号开始,就可能导致脚本错误,从而阻止我们的插件正常工作。

我们使用一个匿名函数作为我们插件的包装器,并立即在函数体之后用一组额外的括号调用它。我们还可以通过在我们的插件中局部范围限定$字符并将jQuery对象传递给匿名函数作为参数,确保我们的插件与 jQuery 的noConflict()方法一起工作。

在匿名函数内部,我们首先定义一个称为defaults的对象字面量,该对象将用作我们插件的配置对象。该对象包含另一个称为strings的对象,其中我们存储了在各种元素中显示的所有不同文本部分。

为了使我们的插件易于本地化,我们使用配置对象来处理文本字符串,这样非英语母语的开发者就可以更容易地使用。尽可能使插件灵活是增加插件吸引力的一个好方法。

defaults对象之后,我们定义了一个构造函数,该函数将生成我们的小部件的实例。插件称为 Up,我们将其名称的第一个字母大写,因为这是应该使用new关键字调用的函数的一般约定。

构造函数可以接受两个参数;第一个是一个 jQuery 元素或元素集合,第二个是由使用我们的插件的开发者定义的配置对象。

在构造函数内部,我们首先向实例附加一些成员。第一个成员叫做config,它将包含由 jQuery 的extend()方法返回的对象,该方法用于合并两个对象,与大多数 jQuery 方法不同,它是在jQuery对象本身上而不是 HTML 元素集合上调用的。

它接受四个参数;第一个参数指示extend()方法深复制要合并到 jQuery 对象中的对象,这是我们需要做的,因为defaults对象包含其他对象。

第二个参数是一个空对象;任何其他对象都将被合并在一起,并将它们自己的属性添加到此对象中。这是方法将返回的对象。如果我们没有传递一个空对象,那么方法中传递的第一个对象将被返回。

下面的两个参数是我们要合并的对象。这些是我们刚刚定义的defaults对象和在调用构造函数时可能传递的opts对象。

这意味着如果开发者希望传递一个配置对象,他们可以覆盖我们在defaults对象中定义的值。未使用此配置对象覆盖的属性将被设置为默认值。

我们还将对元素或元素集合的引用作为实例的成员存储,以便我们可以在代码的其他部分轻松操作这些元素。

最后,我们添加了一对空数组,用于存储要上传的文件列表和进行中的 XHR 请求。我们将在项目的后期看到这些属性如何使用,所以现在不用太担心它们。

jQuery 提供了fn对象作为其原型的快捷方式,这是我们如何用我们的插件方法扩展 jQuery 的。在这种情况下,该方法被称为up(),并且是我们在uploader.html底部使用<script>元素调用的方法。我们指定该方法可能接受一个参数,该参数是包含插件使用者可能想要提供的配置选项的对象。

在方法内部,我们首先使用new关键字与我们的构造函数结合创建了一个上传器的新实例。我们将构造函数传递给方法所调用的元素(或元素集合)和options对象。

最后我们从方法中返回了this。 在添加到 jQuery 原型的方法中,this对象指的是 jQuery 集合。非常重要的是,为了保持链接,返回调用方法的元素集合。

机密情报

链接是 jQuery 的一个固有特性,使用它的开发人员来期望。重要的是满足开发人员对他们使用的编程样式的期望。使用我们的插件的人们希望在调用我们的插件方法后能够添加额外的 jQuery 方法。

现在我们通过返回this对象返回元素集合,开发人员可以做这样的事情:

$("#an-element").up().addClass("test");

所以这是一个简单的示例,但它应该说明为什么从插件中始终返回this是重要的。

生成底层标记

在这个任务中,我们将向我们的插件添加一个初始化方法,该方法将生成小部件所需的标记。

启动推进器

首先,我们应该直接在uploader.jsUp()构造函数之后添加以下代码:

Up.prototype.init = function() {
    var widget = this,
          strings = widget.config.strings,
          container = $("<article/>", {
            "class": "up"
          }),
    heading = $("<header/>").appendTo(container),
    title = $("<h1/>", {
        text: strings.title
    }).appendTo(heading),
    drop = $("<div/>", {
        "class": "up-drop-target",
        html: $("<h2/>", {
            text: strings.dropText
        })
    }).appendTo(container),
    alt = $("<h3/>", {
        text: strings.altText
    }).appendTo(container),
    upload = $("<input/>", {
        type: "file"
    }).prop("multiple", true).appendTo(container),
    select = $("<a/>", {
        href: "#",
        "class": "button up-choose",
        text: strings.buttons.choose
    }).appendTo(container),
    selected = $("<div/>", {
        "class": "up-selected"
    }).appendTo(container),
    upload = $("<a/>", {
        href: "#",
        "class": "button up-upload",
        text: strings.buttons.upload
    }).appendTo(container);

    widget.el.append(container);

}

我们还需要调用这个新的init()方法。修改添加到 jQuery 的fn对象的方法,使其如下所示:

$.fn.up = function(options) {
 new Up(this, options).init();
    return this;
};

我们还可以在插件生成的标记中添加 CSS。在uploader.css中,添加以下样式:

article.up { width:90%; padding:5%; }
article.up input { display:none; }
.up-drop-target { 
    height:10em; border:5px dashed #ccc; border-radius:5px; 
    margin-bottom:1em; text-align:center; 
}
.up-drop-target h2 { 
    margin-top:-.5em; position:relative; top:50%; 
}
.up-selected { margin:1em 0; border-bottom:1px solid #ccc; }

完成目标 - 迷你总结

我们可以通过将其添加到构造函数的prototype中来添加一个init()方法,该方法负责创建和注入小部件所构建的标记。构造函数创建的所有对象都将继承该方法。

我们首先存储了this对象,该对象在我们的init()方法中仍然指的是元素的 jQuery 集合,以便我们可以在下一个任务中轻松地在事件处理程序中引用它。

我们还将strings属性本地化作用域,以使解析稍微更快,因为我们经常引用此属性以将可见的文本字符串添加到小部件的可见 UI 中。

接下来,我们创建新的 HTML 元素并将它们存储在变量中。这意味着我们可以创建容器并将所有所需元素附加到其中,而它仍然在内存中,并且然后将整个小部件一次性注入到页面的 DOM 中,而不是重复地修改 DOM 并逐个添加元素。

小部件的外部容器是一个 <article> 元素,它具有一个易于样式化的类名。HTML5 规范描述了 <article> 作为一个独立的交互式小部件,所以我觉得这是我们小部件的完美容器。虽然同样相关,但 <article> 并不局限于我们传统上描述的“文章” - 例如,博客/新闻文章或编辑样式的文章。

我们有一个 <header> 元素来包含小部件的主标题,在其中我们使用一个标准的 <h1>。我们还在小部件内部使用两个 <h2> 元素来显示不同的部分(拖放区域和更传统的文件 <input>)。

<input> 元素具有 type 属性为 file,并且还给定了 multiple 属性,使用 jQuery 的 prop() 方法,以便在支持的浏览器中上传多个文件。目前的 IE 版本(9 及以下)不支持此属性。

我们还在 <input> 之后直接添加了一个 <a> 元素,我们将用它来打开用于选择要上传的文件的打开对话框。标准的 file 类型 <input> 的问题在于没有标准!

几乎每个浏览器都以不同的方式实现 file 类型的 <input>,一些浏览器显示一个 <input> 以及一个 <button>,而一些浏览器只显示一个 <button> 和一些文本。还不可能对由控件生成的 <input><button> 进行样式设置,因为它们是 shadow DOM 的一部分。

注意

有关影子 DOM 的更多信息,请参见 glazkov.com/2011/01/14/what-the-heck-is-shadow-dom/

为了解决这些跨浏览器的差异,我们将用 CSS 隐藏 <input>,并使用 <a> 元素,样式化为一个吸引人的按钮,来打开对话框。

我们还添加了一个空的 <div> 元素,我们将用它来列出所选文件并显示每个文件的一些信息,然后是另一个 <a> 元素,它将被样式化为按钮。这个按钮将用于启动上传。

我们使用了标准的 jQuery 1.4+ 语法来创建新的 HTML 元素,并为大多数我们创建的元素提供了配置对象。大多数元素都给定了一个类名,有些还会获得文本或 HTML 内容。我们使用的类名都受到合理前缀的限制,以避免与页面上已使用的现有样式潜在冲突。

我们添加的 CSS 主要是用于呈现。重要的方面是我们隐藏了标准的文件 <input>,并且给了拖放目标一个固定大小,以便文件可以轻松地放置在上面。

此时,我们应该能够在浏览器中运行页面(通过 web 服务器),并查看插件的基本元素和布局。页面应该与该项目的第一个截图中的样子一样。

添加接收要上传文件的事件处理程序

我们可以使用我们在上一个任务中添加的 init() 方法来附加小部件将需要处理的文件被选择上传的事件处理程序。这可能发生在文件被拖放到拖放目标上,或者使用按钮选择它们时。

启动推进器

uploader.js中的init()方法中向容器附加新的 HTML 元素之后(但仍在init()方法内部),添加以下代码:

widget.el.on("click", "a.up-choose", function(e) {
    e.preventDefault();

    widget.el.find("input[type='file']").click();
});

widget.el.on("drop change dragover", "article.up", function(e) {

    if (e.type === "dragover") {
        e.preventDefault();
        e.stopPropagation();
        return false;
    } else if (e.type === "drop") {
        e.preventDefault();
        e.stopPropagation();
        widget.files = e.originalEvent.dataTransfer.files;
    } else {
        widget.files = widget.el
        .find("input[type='file']")[0]
        .files;
    }

    widget.handleFiles();
});

目标完成 - 迷你总结

我们首先使用 jQuery 的 on() 方法,在事件委托模式下,将事件处理程序附加到小部件的外部容器上。我们将 click 事件指定为第一个参数,并将匹配我们带有类名 up-choose 的按钮的选择器指定为第二个参数。

在传递给 on() 的处理程序函数内部,我们首先使用 JavaScript 的 preventDefault() 阻止浏览器的默认行为,然后触发一个用于选择要上传的文件的隐藏<input>元素的click事件。这将导致文件对话框在浏览器中打开,允许选择文件。

然后,我们附加了另一个事件处理程序。这次我们正在寻找dropdragoverchange事件。当文件被拖放到拖放区域时,将触发drop事件;当文件被悬停在拖放区域上时,将触发dragover事件;如果文件被移除,将触发change事件。

所有这些事件将从拖放区域(带有类名up<article>)或隐藏的<input>中冒泡,并通过绑定事件处理程序的小部件的外部容器传递。

在这个处理程序函数内部,我们首先检查它是否是dragover事件;如果是,我们再次使用preventDefault()stopPropagation()阻止浏览器的默认行为。我们还需要从条件的这个分支返回false

if的下一个分支检查触发处理程序的事件是否是drop事件。如果是,我们仍然需要使用preventDefault()stopPropagation(),但这次我们还可以使用 jQuery 创建和传递给处理程序函数的事件对象获取所选文件的列表,并将它们存储在小部件实例的属性中。

如果这两个条件都不为true,我们就从<input>元素中获取文件列表。

我们需要的属性是 jQuery 封装到自己的事件对象中的originalEvent对象的一部分。然后,我们可以从dataTransfer对象中获取files属性。如果事件是change事件,我们只需获取隐藏的<input>files属性。

无论使用哪种方法,用于上传的文件集合都存储在小部件实例的 files 属性下。这只是一个临时属性,每次选择新文件时都会被覆盖,不像小部件的 filelist 数组,它将存储所有文件以进行上传。

最后我们调用 handleFiles() 方法。在下一个任务中,我们将把这个方法添加到小部件的 prototype 中,所以一旦完成了这个任务,我们就能在这里调用这个方法而不会遇到问题。

将两个事件组合起来,并以这种方式检测发生的事件要比附加到单独的事件处理程序要好得多。这意味着我们不需要两个分开的处理程序函数,它们都本质上做同样的事情,并且无论是用按钮和标准对话框选择文件,还是通过将文件拖放到拖放目标中选择文件,我们仍然可以获取文件列表。

此时,我们应该能够将文件拖放到拖放区域,或者点击按钮并使用对话框选择文件。然而,会抛出一个脚本错误,因为我们还没有添加我们插件的 handleFiles() 方法。

显示已选文件列表

在这个任务中,我们可以填充我们创建的 <div>,以显示已选择用于上传的文件列表。我们将构建一个表格,在表格中,每一行列出一个文件,包括文件名和类型等信息。

启动推进器

uploader.js 中的 init() 方法之后,添加以下代码:

Up.prototype.handleFiles = function() {

    var widget = this,
          container = widget.el.find("div.up-selected"),
          row = $("<tr/>"),
          cell = $("<td/>"),
          remove = $("<a/>", {
             href: "#"
          }),
    table;

    if (!container.find("table").length) {
        table = $("<table/>");

        var header = row.clone().appendTo(table),
              strings = widget.config.strings.tableHeadings;

        $.each(strings, function(i, string) {
                var cs = string.toLowerCase().replace(/\s/g, "_"),
                      newCell = cell.clone()
                                            .addClass("up-table-head " + cs)
                                            .appendTo(header);

                if (i === strings.length - 1) {
                    var clear = remove.clone()
                                                 .text(string)
                                                .addClass("up-remove-all");

                    newCell.html(clear).attr("colspan", 2);
                } else {
                    newCell.text(string);
                }
            });
        } else {
            table = container.find("table");
        }

        $.each(widget.files, function(i, file) {
        var fileRow = row.clone(),
              filename = file.name.split("."),
              ext = filename[filename.length - 1],
              del = remove.clone()
                                   .text("x")
                                   .addClass("up-remove");

        cell.clone()
              .addClass("icon " + ext)
              .appendTo(fileRow);

        cell.clone()
              .text(file.name).appendTo(fileRow);
        cell.clone()
             .text((Math.round(file.size / 1024)) + " kb")
             .appendTo(fileRow);

        cell.clone()
              .html(del).appendTo(fileRow);
        cell.clone()
              .html("<div class='up-progress'/>")
              .appendTo(fileRow);

        fileRow.appendTo(table);

        widget.fileList.push(file);
    });

    if (!container.find("table").length) {
        table.appendTo(container);
    }
}

我们还可以为我们创建的新标记添加一些额外的 CSS。将以下代码添加到 upload.css 的底部:

.up-selected table {
    width:100%; border-spacing:0; margin-bottom:1em;
}
.up-selected td {
    padding:1em 1% 1em 0; border-bottom:1px dashed #ccc;
    font-size:1.2em;
}
.up-selected td.type { width:60px; }
.up-selected td.name { width:45%; }
.up-selected td.size { width:25%; }
.up-selected td.remove_all_x { width:20%; }

.up-selected tr:last-child td { border-bottom:none; }
.up-selected a {
    font-weight:bold; text-decoration:none;
}
.up-table-head { font-weight:bold; }
.up-remove-all { color:#ff0000; }
.up-remove {
    display:block; width:17px; height:17px;
    border-radius:500px; text-align:center;
    color:#fff; background-color:#ff0000;
}
.icon { 
    background:url(../img/page_white.png) no-repeat 0 50%; 
}
.doc, .docx { 
    background:url(../img/doc.png) no-repeat 0 50%; 
}
.exe { background:url(../img/exe.png) no-repeat 0 50%; }
.html { background:url(../img/html.png) no-repeat 0 50%; }
.pdf { background:url(../img/pdf.png) no-repeat 0 50%; }
.png { background:url(../img/png.png) no-repeat 0 50%; }
.ppt, .pptx { 
    background:url(../img/pps.png) no-repeat 0 50%; 
}
.txt { background:url(../img/txt.png) no-repeat 0 50%; }
.zip { background:url(../img/zip.png) no-repeat 0 50%; }

目标完成 - 迷你总结

我们开始时将 handleFiles() 方法添加到小部件的 prototype 中,使得我们在上一个任务的最后添加的方法调用 widget.handleFiles() 起作用。它的添加方式与之前的 init() 方法完全相同,并且就像在 init() 内部一样,this 对象指向了小部件实例内部。这使得在页面上的元素、配置选项和选定文件列表都易于访问。

在方法内部,我们首先创建了一系列变量。就像在 init() 方法中一样,我们创建了一个名为 widget 的局部变量,用于存储 this 对象。虽然我们不会向这个方法添加任何事件处理程序,所以我们并不一定非要这样做,但我们确实多次访问对象,所以把它缓存在一个变量中是有道理的。

我们还使用 widget.el 缓存了选定的文件容器 - 不要忘记 el 已经引用了外部小部件容器的 jQuery 封装实例,所以我们可以直接在其上调用 jQuery 方法,如 find(),而无需重新封装它。

接下来,我们创建了一系列新的 DOM 元素,准备在循环内克隆它们。这是一种更好的创建元素的方法,特别是在循环内部,避免了不断创建新的 jQuery 对象。

我们还定义了一个名为table的变量,但我们并没有立即初始化它。相反,我们使用if条件来检查容器是否已经包含了一个<table>元素,通过检查 jQuery 的find("table")是否返回一个具有length的集合。

如果length等于false,我们知道没有选择任何<table>元素,因此我们使用 jQuery 创建了一个新的<table>元素,并将其赋给table变量。然后,我们为<table>创建了一个标题行,用于为新表的每一列添加标题。

此时,<table>元素只存在于内存中,因此我们可以将新行添加到其中,而不会修改页面的 DOM。我们还缓存了我们配置对象中使用的strings对象的tableHeadings属性的引用。

然后,我们使用 jQuery 的each()实用工具来创建用作表标题的所有<td>元素。除了能够在从页面选中的元素集合上调用each()之外,我们还可以调用each()在 jQuery 对象上,以便迭代一个纯 JavaScript 数组或对象。

each()方法接受要迭代的数组或对象。在这种情况下,它是一个数组,因此对数组中的每个项目调用的迭代函数接收到当前项目的索引和当前项目的值作为参数。

在迭代器内部,我们首先创建一个可以用作类名的新字符串。class这个词在 JavaScript 中是一个保留字,因此我们改用cs作为变量名。为了创建类名,我们只需使用 JavaScript 的toLowerCase()函数将当前字符串转换为小写,然后使用 JavaScript 的replace()函数删除任何空格。

注意

有关 JavaScript 中保留字的完整列表,请参阅 MDN 文档developer.mozilla.org/en-US/docs/JavaScript/Reference/Reserved_Words

replace()函数将正则表达式作为第一个参数匹配,将替换字符串作为第二个参数。我们可以使用字符串" "作为第一个参数,但那样只会删除第一个空格,而使用带有g标志的正则表达式允许我们移除所有空格。

然后,我们通过克隆在任务开始时创建并存储在变量中的元素之一来创建一个新的<td>元素。我们为了样式的目的给它一个通用的类名,以及我们刚刚创建的唯一类名,这样每一列都可以在需要时独立样式化,然后将它直接添加到我们刚刚创建的标题行中。

然后,我们通过检查当前索引是否等于数组长度减 1 来检查我们是否迭代了数组中的最后一项。如果是最后一项,我们通过克隆我们在任务开始时创建和缓存的<a>元素来添加一个清除所有链接。

我们将新<td>元素的文本设置为当前数组项的值,并添加up-remove-all类以进行样式设置,以便我们可以过滤由它分发的事件。我们还可以使用 jQuery 的attr()方法将colspan属性设置为2到这个<td>。然后,新的<a>元素被添加为新的<td>元素的 HTML 内容。

如果它不是数组中的最后一个项目,我们只需将新<td>元素的文本内容设置为当前数组项的值。

所有这些都是在外部if语句的第一个分支中完成的,当表不存在时发生。如果容器已经包含<table>元素,我们仍然通过选择页面上的<table>来初始化表变量。

不要忘记,我们所在的handleFiles()方法将在选择文件后被调用,所以现在我们需要为每个选择的文件在表中构建一行新行。

再次使用 jQuery 的each()方法,这次是为了迭代小部件的files属性中存储的文件集合。对于每个选择的文件(通过拖放到拖放区域或使用按钮),我们首先通过克隆我们的row变量创建一个新的<tr>

然后,我们在当前文件的name属性上使用.字符进行分割。通过获取split()函数创建的数组中的最后一个项目,我们存储文件的扩展名。

在这一点上,我们还创建一个删除链接,可以用来从要上传的文件列表中删除单个文件,方法是克隆我们在任务开始时创建的<a>元素。它被赋予文本x和类名up-remove

接下来,我们通过再次克隆缓存的cell变量中的<td>来创建一系列新的<td>元素。第一个<td>被赋予一个通用的类名icon,以及当前文件的扩展名,这样我们就可以为可以上传的不同文件类型添加图标,并将其附加到新行上。

第二个<td>元素显示文件的名称。第三个<td>元素显示文件的大小(以千字节为单位)。如果我们知道可能上传大文件,我们可以转换为兆字节,但对于这个项目的目的,千字节就足够了。

第四个<td>元素使用 jQuery 的html()方法添加了新的删除链接,最后一个<td>元素添加了一个空的<div>元素,我们将使用它来放置 jQuery UI 进度条小部件。

一旦新单元格被创建并附加到新行上,新行本身就被附加到表中。我们还可以将当前文件添加到我们的fileList数组中,准备上传。

最后,我们需要再次检查所选文件容器是否已经包含一个<table>元素。如果没有,我们将新建的<table>追加到容器中。如果它已经包含<table>,新行将已经添加到其中。

我们在这一部分添加的 CSS 纯粹是为了呈现。我做的一件事是添加一些类,以便显示可能选择上传的不同文件类型的图标。我只是添加了一些作为示例;您实际需要的会取决于您期望用户上传的文件类型。还为与我们添加的选择器不匹配的类型创建了通用图标。

注意

此示例中使用的图标属于 Farm Fresh 图标包。我已经为了简洁性而重命名了这些文件,并且可以在本书附带的代码下载中找到。这些图标可以在 Fat Cow 网络主机上获得 (www.fatcow.com/free-icons)。

在这一点上,我们应该能够在浏览器中运行页面,选择一些文件进行上传,并看到我们刚刚创建的新<table>

完成目标 - 小型总结

机密情报

在这个例子中,我们手动创建了显示所选文件列表所需的元素。另一种方法是使用模板引擎,比如 jsRender 或 Dust.js。这样做的好处是比我们手动创建更快更高效,能够使我们的插件代码更简单更简洁,文件也更小。

当然,这将给我们的插件增加另一个依赖,因为我们需要包含模板引擎本身,以及一个存储在 JavaScript 文件中的预编译模板。在这个例子中,我们并没有创建太多元素,所以可能不值得再添加另一个依赖。当需要创建许多元素时,添加依赖的成本被它增加的效率所抵消。

写 jQuery 插件时,这种事情需要根据具体情况逐案考虑。

从上传列表中移除文件

在这个任务中,我们将添加事件处理程序,使新文件列表中的删除全部删除链接起作用。我们可以将事件处理程序附加到我们之前添加其他事件处理程序的地方,以保持事情的井然有序。

启动推进器

upload.js中,在小部件的init()方法中,并且直接在现有的 jQuery on()方法调用之后,添加以下新代码:

widget.el.on("click", "td a", function(e) {

    var removeAll = function() {
        widget.el.find("table").remove();
        widget.el.find("input[type='file']").val("");
        widget.fileList = [];
    }

    if (e.originalEvent.target.className == "up-remove-all") {
        removeAll();
    } else {
        var link = $(this),
              removed,
              filename = link.closest("tr")
                                     .children()
                                     .eq(1)
                                     .text();

        link.closest("tr").remove();

        $.each(widget.fileList, function(i, item) {
        if (item.name === filename) {
            removed = i;
        }
    });
    widget.fileList.splice(removed, 1);

    if (widget.el.find("tr").length === 1) {
        removeAll();
    } 
  }
}); 

完成目标 - 小型总结

我们使用 jQuery 的on()方法再次添加了一个click事件。我们将它附加到小部件的外部容器,就像我们添加其他事件一样,这次我们根据选择器td a过滤事件,因为事件只会源自<td>元素内的<a>元素。

在事件处理程序内,我们首先阻止浏览器的默认行为,因为我们不希望跟随链接。然后,我们定义了一个简单的帮助函数,从小部件中移除<table>元素,清除文件<input>的值,并清除fileList数组。

我们需要清除<input>,否则如果我们选择了一些文件,然后将它们从文件列表中移除,我们将无法重新选择相同的一组文件。这是一个边缘情况,但这个简单的小技巧可以让它起作用,所以我们也可以包含它。

接下来,我们检查触发事件的元素的className属性是什么。我们可以使用传递给处理程序函数的 jQuery 事件对象中包含的originalEvent对象的target属性来查看此属性。我们还可以使用 jQuery 事件对象的srcElement属性,但这在当前版本的 Firefox 中不起作用。

className属性匹配up-remove-all时,我们简单地调用我们的removeAll()辅助函数来移除<table>元素并清除<input>fileList数组。

如果className属性与全部移除链接不匹配,我们必须仅移除包含被点击的<a><table>元素的行。我们首先缓存触发事件的<a>的引用,这在处理程序函数内部被设置为this

我们还定义了一个名为removed的变量,我们将很快初始化一个值。最后,我们存储了我们将要移除的行所代表的文件的filename

一旦我们设置了变量,我们首先要做的是移除我们可以使用 jQuery 的closest()方法找到的行,该方法找到与传递给该方法的选择器匹配的第一个父元素。

然后我们使用 jQuery 的each()方法来迭代fileList数组。对于数组中的每个项目,我们将项目的name属性与我们刚初始化的filename变量进行比较。如果两者匹配,我们将index号(由 jQuery 自动传递给迭代器函数)设置为我们的removed变量。

一旦each()方法完成,我们就可以使用 JavaScript 的splice()函数来移除当前<tr>所代表的文件。splice()函数接受两个参数(它可以接受更多,但我们这里不需要),第一个参数是要开始移除的项目的索引,第二个参数是要移除的项目数。

最后,我们检查<table>元素是否还有多于一行的行。如果只剩下一行,这将是标题行,所以我们知道所有文件都已删除。因此,我们可以调用我们的removeAll()辅助函数来整理并重置一切。

现在当我们已经将文件添加到上传列表中时,我们应该能够使用内联x按钮逐个删除文件,或者使用全部移除链接清除列表。

添加一个 jQuery UI 进度指示器

在这个任务中,我们将添加 jQuery UI 进度条小部件所需的元素和初始化代码。小部件实际上还不会执行任何操作,因为在下一个任务中我们不会上传任何东西,但我们需要连接好一切准备就绪。

启动推进器

我们将向小部件的原型添加一个initProgress()方法,用于选择我们添加到<table>元素中的<div>元素,并将它们转换为进度条小部件。我们还可以添加用于更新进度条的方法。

handleFiles()方法之后,直接添加以下代码:

Up.prototype.initProgress = function() {

    this.el.find("div.up-progress").each(function() {
        var el = $(this);

        if (!el.hasClass("ui-progressbar")) {
            el.progressbar();
        }
    });
}

接下来,我们需要在向<table>添加新行后调用此方法。在handleFiles()方法的末尾直接添加以下调用:

widget.initProgress();

现在我们可以添加更新进度条的代码了。在我们刚刚添加的initProgress()方法后面直接添加以下代码:

Up.prototype.handleProgress = function(e, progress) {

    var complete = Math.round((e.loaded / e.total) * 100);

    progress.progressbar("value", complete);
}

我们还需要为新的进度条添加一点 CSS。将以下代码添加到uploader.css的末尾:

.up-progress { 
    height:1em; width:100px; position:relative; top:4px; 
}

目标完成 - 迷你总结

这个任务比我们到目前为止在项目中涵盖的一些任务更短,但同样重要。我们添加了新方法的方式与为插件添加大部分功能的方式相同。

在这个方法中,我们首先选择所有类名为up-progress<div>元素。不要忘记我们可以使用this.el访问小部件的容器元素,并且作为 jQuery 对象,我们可以在其上调用 jQuery 方法,比如find()

然后,我们使用 jQuery 的each()方法遍历选择中的每个元素。在此任务中,我们使用标准的each()方法,其中集合中的当前元素在迭代函数中设置为this

在迭代函数中,我们首先缓存当前元素。然后我们检查它是否具有 jQuery UI 类名ui-progressbar,如果没有,我们将使用 jQuery UI 方法progressbar()将元素转换为进度条。

这样做意味着无论是选择要上传的初始文件集,还是将其他文件添加到现有的<table>中,进度条都将始终被创建。

handleFiles()方法末尾,我们还添加了对新的initProgress()方法的调用,每当选择新文件上传时都会调用该方法。

接下来,我们添加了handleProgress()方法,我们将在下一个任务中将其绑定到一个事件。该方法将传递两个参数,第一个是事件对象,第二个是一个已包装的 jQuery 对象,表示一个单独的进度条。

在方法中,我们首先计算已上传文件的比例。我们可以通过将事件对象的loaded属性除以total属性得出,然后除以 100 得出迄今为止已上传文件的百分比。

loadedtotal属性是特殊属性,当浏览器触发进度事件时会将它们添加到事件对象中。

一旦我们有了百分比,我们就可以调用进度条小部件的value方法,以便将值设置为百分比。这是一个 jQuery UI 方法,因此以特殊的方式调用。我们不直接调用value(),而是调用progressbar()方法,并将要调用的方法的名称value作为第一个参数传递。所有 jQuery UI 方法都是以这种方式调用的。

最后,我们添加了一些漂亮的 CSS 样式,以微调默认的 jQuery UI 主题提供的默认样式。现在,当我们添加要上传的文件时,我们应该在<table>中的每个文件后看到一个空的进度条。

正在上传所选文件

现在,我们有了附加到我们插件实例的文件列表,准备好上传。在这个任务中,我们将做到这一点,并使用 jQuery 异步上传文件。此行为将与我们添加到插件生成的标记中的上传文件按钮相关联。

我们还可以使用此任务来更新我们的进度条,显示每个正在上传的文件的当前进度。

启动推进器

由于这是另一个事件处理程序,我们将在init()方法中添加它,以及所有其他事件处理程序,以便它们都保持在一个地方。在现有的事件处理程序之后,在init()方法的末尾添加以下代码:

widget.el.on("click", "a.up-upload", function(e) {
    e.preventDefault();

  widget.uploadFiles();
}); 

接下来,添加新的uploadFiles()方法。这可以在我们在上一个任务中添加的与进度相关的方法之后进行:

Up.prototype.uploadFiles = function() {
    var widget = this,
    a = widget.el.find("a.up-upload");

    if (!a.hasClass("disabled")) {

        a.addClass("disabled");

        $.each(widget.fileList, function(i, file) {
            var fd = new FormData(),
                  prog = widget.el
                                        .find("div.up-progress")
                                        .eq(i);

            fd.append("file-" + i, file);

            widget.allXHR.push($.ajax({
                type: "POST",
                url: "/upload.asmx/uploadFile",
                data: fd,
                contentType: false,
                processData: false,
                xhr: function() {

                    var xhr = jQuery.ajaxSettings.xhr();

                    if (xhr.upload) {
                        xhr.upload.onprogress = function(e) {
                            widget.handleProgress(e, prog);
                        }
                    }

                    return xhr;
                }
            }));
        });     
    }
}

完成目标 - 迷你总结

在我们的uploadFiles()方法中,我们首先存储对小部件的引用,就像我们在添加的其他一些方法中所做的那样。我们还存储对上传文件按钮的引用。

接下来要做的是检查按钮是否没有disabled类名。如果它确实具有此类名,这意味着已为所选文件启动了上传,因此我们希望避免重复请求。如果按钮没有disabled类,则意味着这是第一次单击按钮。因此,为了防止重复请求,我们随后添加disabled类。

接下来,我们遍历我们收集到的文件列表,该列表存储在小部件实例的fileList属性中。对于数组中的每个文件,我们首先创建一个新的FormData对象。

FormData是新的 XMLHttpRequest (XHR) level 2 规范的一部分,它允许我们动态创建一个<form>元素,并使用 XHR 异步提交该表单。

一旦我们创建了一个新的FormData对象,我们还会存储与当前文件关联的进度条小部件的引用。然后,我们使用FormDataappend()方法将当前文件附加到新的FormData对象中,以便将文件编码并发送到服务器。

接下来,我们使用 jQuery 的ajax()方法将当前的FormData对象发布到服务器。ajax()方法将返回请求的jqXHR对象。这是 jQuery 增强了额外方法和属性的 XHR 对象的特殊版本。我们需要存储这个jqXHR对象,以便稍后使用。

我们将在下一个任务中详细介绍它的使用方式,但现在只需了解ajax()方法返回的jqXHR对象被推送到我们在项目开始时存储为小部件实例成员的allXHR数组中即可。

ajax()方法接受一个配置对象作为参数,允许我们控制请求的方式。我们使用type选项将请求设置为POST,并使用url选项指定要发布到的 URL。我们使用 data 选项将FormData对象添加为请求的有效载荷,并将contentTypeprocessData选项设置为false

如果我们不将contentType选项设置为false,jQuery 将尝试猜测应该使用哪种内容类型进行请求,这可能正确也可能不正确,这意味着一些上传将正常工作,而另一些上传将失败,看起来毫无明显原因。请求的content-type将默认设置为multipart/form-data,因为我们使用的是附加有文件的FormData

processData选项设置为false将确保 jQuery 不会尝试将文件转换为 URL 编码的查询字符串。

我们需要修改用于发出请求的基础 XHR 对象,以便我们可以将处理程序函数附加到进度事件上。在请求发出之前,必须将处理程序绑定到事件上,目前唯一的方法是使用xhr选项。

该选项接受一个回调函数,我们可以使用它来修改原始的 XHR 对象,然后返回给请求。在回调函数中,我们首先存储原始的 XHR 对象,可以从 jQuery 的ajaxSettings对象中获取它。

然后,我们检查对象是否具有upload属性,如果有,我们将匿名函数设置为onprogress的值。在此函数中,我们只需调用我们在上一个任务中添加的小部件的handleProgress()方法,将进度事件对象和我们在本任务开始处存储的 Progressbar 小部件传递给它。

报告成功并整理

在此任务中,我们需要显示每个文件何时完成上传。我们还需要清除小部件中的<table>,并在所有上传完成后重新启用上传按钮。

启动推进器

我们可以使用 jQuery 的done()方法显示每个单独文件上传完成的时间,我们可以在上一个任务中添加的ajax()方法之后链接此方法:

.done(function() {

    var parent = prog.parent(),
    prev = parent.prev();

    prev.add(parent).empty();
    prev.text("File uploaded!");
});

为了在上传后进行整理,我们可以利用 jQuery 的when()方法。我们应该在uploadFiles()方法中的each()方法之后直接添加以下代码:

$.when.apply($, widget.allXHR).done(function() {
    widget.el.find("table").remove();
    widget.el.find("a.up-upload").removeClass("disabled");
});

目标完成 - 迷你总结

因为 jQuery 的 ajax() 方法返回一个 jqXHR 对象,而且因为这个对象是一个称为promise 对象的特殊对象,我们可以在其上调用某些 jQuery 方法。done() 方法用于在请求成功完成时执行代码。

注意

你可能更习惯于使用 jQuery 的 success() 方法来处理成功的 AJAX 请求,或者 error()complete() 方法。这些方法在版本 1.9 中已从库中移除,因此我们应该使用它们的替代品 done()fail()always()

在这个函数中,我们只需要移除清除按钮和刚刚完成上传的文件的进度条小部件。我们可以通过从当前进度条小部件导航到它们来轻松找到需要移除的元素。

我们在上一个任务中存储了每个单独的进度条的引用,并且因为 done() 方法链接到了 ajax() 方法,所以在请求完成后仍然可以使用这个变量访问这个元素。

注意,在 done() 方法的末尾似乎有一个额外的闭合括号。这是因为它仍然位于我们在先前任务中添加的 push() 方法内部。关键是 done() 方法被添加到正确的位置——它必须链接到 push() 方法内部的 ajax() 方法。

一旦这些元素被移除,我们添加一个简单的消息,表示文件已完成上传。

一旦所有请求都完成,我们还需要从页面中移除 <table> 元素。这就是我们在上一个任务中上传文件时存储了所有生成的 jqXHR 对象的原因。我们可以使用 jQuery 的 when() 方法来做到这一点。

when() 方法可以接受一系列 promise 对象,并在它们全部解决时返回。然而,这个方法不接受数组,这就是为什么我们使用 JavaScript 的 apply() 方法调用它,而不是正常调用它。

我们可以再次使用 done() 方法来添加一个回调函数,一旦 when() 方法返回,就会调用该回调函数。在这个回调中,我们所做的就是移除显示已上传文件的 <table> 元素,并通过移除 disabled 类重新启用上传按钮。

这就是我们实际上需要做的,上传所选文件并分别接收每个文件的进度反馈,如下面的截图所示:

目标完成 - 迷你简报

提示

查看示例文件

要查看此项目的运行情况,您需要使用 Web 服务器查看我们创建的页面(在您自己的计算机上使用 http://localhost)。如果您在资源管理器或查找器中双击打开文件,它将无法正常工作。

任务完成

我们已经完成了项目。在这一点上,我们应该有一个易于使用并在支持的浏览器中提供丰富功能的上传插件,例如多个文件、文件信息、可编辑的上传列表和上传进度报告。

提示

并非所有浏览器都能使用此小部件旨在利用的功能。例如,Opera 浏览器认为通过程序触发文件对话框存在安全风险,因此不允许它。

此外,Internet Explorer 的旧版本(任何版本 10 之前的版本)根本无法处理此代码。

支持不兼容或遗留浏览器超出了此示例的范围,但添加一个备用方案是相对直接的,可以利用其他技术,比如 Flash,以支持我们的插件所展示的部分行为。

或者有一系列旧的 jQuery 插件,利用 <iframe> 元素来模拟通过 AJAX 上传文件。我选择关注支持的浏览器可以做什么,而不是专注于不支持的功能。

你准备好大干一场了吗?挑战高手

通过逐个上传文件,我们能够添加一个事件处理程序来监视正在上传的文件的进度。这也打开了取消上传单个文件的可能性。

对于这个挑战,为什么不试试看能否添加一个取消上传文件的机制。我们已经有了用于在上传之前删除文件的移除按钮。这些按钮可以很容易地更新,以便在上传进行中取消上传。

可以像附加进度事件处理程序一样向 XHR 对象添加取消事件的处理程序,因此这应该很容易实现。

第六章:使用 jQuery 扩展 Chrome

为 Chrome(或任何可以通过插件和扩展进行扩展的其他浏览器)构建一个扩展是创建自定义行为或附加工具以增强我们的浏览体验的简单方法。

Chrome 允许我们利用我们的 Web 开发技能扩展其浏览器界面,使用我们已经熟悉的技术,如 HTML、CSS 和 JavaScript,以及您可以使用 JavaScript 的地方通常也可以使用 jQuery。

任务简报

在这个项目中,我们将构建一个 Chrome 扩展,突出显示页面上用Schema.org 微数据标记的元素。微数据是一种用于指定有关各种不同实体(如企业、位置或人员)的描述性信息的方式,使用标准 HTML 属性,并据传言将成为 Google 排名算法中的重要因素。

每当我们访问包含联系方式描述的页面时,我们可以从页面中获取它们并将其存储在我们的扩展中,这样我们就可以逐渐建立起一个人们使用或制作我们喜爱的东西的联系信息目录。

在这个项目中,我们还可以使用模板化使创建重复的元素组更加高效,以及更易于维护。我们在上一个项目中使用了 JsRender,所以我们可以再次使用它,但这次我们需要以稍微不同的方式使用它。完成后,我们的扩展将类似于以下截图所示:

任务简报

为什么很棒?

微数据用于描述网页中包含的信息,以促进搜索引擎蜘蛛和 HTML 文档之间的更好互操作性。

当页面上的不同元素被描述为公司、人员、产品或电影时,它允许诸如搜索引擎之类的东西更好地理解页面上包含的信息。

微数据在 Web 上迅速变得更加普遍,并且在 Google 为搜索结果生成的结果中扮演着越来越重要的角色,因此现在是利用它的绝佳时机。

你的热门目标

这个项目分解成的任务如下:

  • 设置基本扩展结构

  • 添加一个清单并安装扩展

  • 添加一个沙箱 JsRender 模板

  • 将消息发布到沙盒

  • 添加内容脚本

  • 为微数据抓取页面

  • 添加保存微数据的机制

设置基本扩展结构

在这个任务中,我们将创建扩展所需的基础文件。扩展使用的所有文件都需要位于同一个目录中,因此我们将设置它并确保它包含我们需要的所有文件。

为起飞做准备

有一件事我应该指出,尽管希望你已经意识到 - 在该项目期间,我们将需要 Chrome 浏览器。如果你尚未安装它,作为一个网页开发人员,你真的应该安装它,至少是为了测试目的,立即下载并安装。

注意

Chrome 的最新版本可以从www.google.com/intl/en/chrome/browser/下载。

我们将把这个项目的所有文件保存在一个单独的目录中,所以现在在项目文件夹中建立一个目录,命名为chrome-extension。扩展将从与大多数其他项目使用的基本代码文件构建; 唯一的区别是所有文件都需要是扩展本地的。

我们需要一个 JsRender 的副本,所以我们也应该下载一个副本,并将其放在chrome-extension目录中。上次我们使用 JsRender 时我们链接到了在线托管的版本。这次我们将下载它。

注意

JsRender 的最新版本可以从github.com/BorisMoore/jsrender/下载。

我们可以使用用于启动其他项目的模板文件,但是我们应该确保指向 jQuery、JavaScript 文件和样式表的路径都指向同一个目录中的文件。Chrome 扩展使用的所有文件都必须在同一个文件夹中,这就是为什么我们下载脚本而不是链接到在线版本。

我们应该将 jQuery、JsRender 和common.css样式表的副本放入新目录中。我们还需要创建一个名为popup.js的新 JavaScript 文件和一个名为popup.css的新样式表,并将这些文件也保存到新目录中。

最后,我们可以创建一个名为popup.html的新 HTML 页面。这个文件也应该保存在chrome-extension目录中,并且应该包含以下代码:

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>jQuery-Powered Chrome Extension</title>
        <link rel="stylesheet" href="common.css" />
        <link rel="stylesheet" href="popup.css" />
    </head>
    <body>
        <script src="img/jquery-1.8.0.min.js"></script>
        <script src="img/jsrender.js"></script>
        <script src="img/popup.js"></script>
    </body>
</html>

启动推进器

我们刚刚创建的 HTML 文件将被用作扩展的弹出窗口。这是当单击工具栏中扩展图标时显示为弹出窗口的页面。在这个项目中,我们将创建一种称为浏览器操作的扩展类型,它会自动向 Chrome 的工具栏添加一个按钮,用于打开弹出窗口。

弹出窗口将显示一个按钮,用于触发对当前页面的微数据进行扫描,并显示任何先前保存的联系人。任何先前存储的联系人都将使用 localStorage API 检索,并且我们可以使用模板来渲染它们。

首先,我们可以向页面添加一般的标记; 在popup.html中,将以下代码添加到页面的<body>中:

<section role="main">
    <header>
        <h1>Web Contacts</h1>
    </header>
    <ul id="contacts"></ul>
</section>
<iframe id="poster" src="img/template.html"></iframe>

我们还可以为这些元素添加一些基本样式。在 popup.css 中,添加以下代码:

body { width:32em; padding:0 2em; }
header { padding-top:2em; }
ul { padding:0 0 1em; font-size:1.5em; }
iframe { display:none; }

目标完成 - 小结

Chrome 扩展使用与我们习惯使用的相同文件构建 - HTML、CSS 和 JavaScript。该扩展将在工具栏中添加一个按钮,当单击此按钮时,将显示一个弹出窗口。我们在此任务中添加的 HTML 页面是此弹出窗口的基础。

我们创建页面的方式与创建任何其他标准 HTML5 页面的方式相同。我们像往常一样链接到 CSS 和 JavaScript 文件,然后添加一个小的<section>容器,它将用作任何先前保存的联系人的容器。最初不会有任何联系人,当有联系人时,我们将使用模板来呈现它们。

我们已经添加了一个包含<h1><header>,为保存的联系人添加了一个标题,并添加了一个空的<ul>元素,我们将很快用脚本填充它。

最后,我们在页面中添加了一个<iframe>,它将被隐藏。稍后我们将使用这个来与扩展的另一部分通信。元素的src属性设置为我们想要发送消息的页面。

我们添加的 CSS 纯粹是为了演示,并仅以简单的布局放置了初始元素。我们还链接到每个其他项目都使用的公共 CSS 文件,但不要忘记,扩展使用的所有文件都必须在扩展的目录中。

机密情报

因为我们正在创建浏览器操作,所以我们将在 Chrome 的工具栏中添加一个新按钮,只要加载了未打包的扩展,它就可见。默认情况下,它将具有标准扩展图标 - 一个拼图块,但我们可以用我们自己创建的图标替换它。

我们还可以创建其他类型的扩展,这些扩展不会将按钮添加到工具栏。我们可以创建页面操作而不是浏览器操作,该操作将在地址栏中添加一个图标而不是工具栏。

该图标是否在所有页面上可见取决于扩展的行为方式。例如,如果我们想要在每次页面在浏览器中加载时运行我们的扩展,但只在页面上找到Schema.org微数据时显示图标,我们可以使用页面操作。

浏览器操作,例如我们将在此创建的操作,在查看的页面不受影响时始终可访问。我们使用浏览器操作而不是页面操作,因为我们扩展的用户可能希望能够查看他们以前发现并保存的联系人,因此浏览器操作非常适合通过扩展存储的任何数据。

添加清单并安装扩展

为了实际安装我们的扩展并看到我们迄今为止的劳动成果,我们需要创建一个清单文件。这个特殊的文件以 JSON 格式保存,控制扩展的某些方面,例如它使用的页面以及它可以运行的内容脚本。

准备起飞

在新文件中添加以下代码:

{
    "name": "Web Contacts",
    "version": "1.0",
    "manifest_version": 2,
    "description": "Scrape web pages for Schema.org micro-data",
    "browser_action": {
        "default_popup": "popup.html"
    }
}

将此文件保存在我们在任务开始时在主项目目录中创建的chrome-extension目录中,文件名为manifest.json

注意

如果您使用的文本编辑器在另存为类型:(或相似)下没有显示**.json**,请选择**所有类型 (*)选项,并在文件名:**输入字段中键入完整的文件名manifest.json

启动推进器

要查看当前的扩展程序,需要将其加载到 Chrome 中作为扩展程序。为此,您应该转到设置 | 工具 | 扩展程序

注意

在最近的 Chrome 版本中,通过点击具有三条杠图标的按钮(位于浏览器窗口右上角)来访问设置菜单。

当扩展程序页面加载时,应该会有一个按钮来加载未打包的扩展程序…。如果没有,请选中开发者模式复选框,然后该按钮将出现。

点击按钮,然后选择chrome-extension文件夹作为扩展目录。这样应该会安装扩展程序,并为我们添加浏览器操作按钮到工具栏。

目标完成 - 迷你总结

在扩展程序加载到浏览器之前,需要一个简单的清单文件。当前版本的 Chrome 仅允许至少为 Version 2 的清单。扩展程序必须具有清单,否则将无法运行。这是一个简单的文本文件,以 JSON 格式编写,用于向浏览器提供有关扩展程序的一些基本信息,例如名称、作者和当前版本。

我们可以指定我们的扩展程序是一个浏览器操作,它将一个按钮添加到 Chrome 的工具栏上。我们还可以使用清单指定在弹出窗口中显示的页面。

单击我们扩展的新按钮时,将会在扩展程序弹出窗口中显示我们在上一个任务中添加的 HTML 页面(popup.html),如下面的屏幕截图所示:

目标完成 - 迷你总结

添加一个沙盒化的 JsRender 模板

在这个任务中,我们可以添加 JsRender 将用于显示已保存联系人的模板。此时,我们还没有保存任何联系人,但我们仍然可以准备好它,并且当我们有了一些联系人时,它们将被渲染到弹出窗口中,而无需任何麻烦。

准备起飞

Chrome 使用内容安全策略CSP)来防止大量常见的跨站脚本XSS)攻击,因此我们不允许执行使用eval()new Function()的任何脚本。

像许多其他流行库和框架一样,JsRender 模板库在编译模板时使用new Function(),因此不允许直接在扩展程序内部运行。我们可以通过两种方式解决这个问题:

  • 我们可以转换到一个提供模板预编译的模板库,比如流行的 Dust.js。然后我们可以在浏览器外部编译我们的模板,并在扩展内部链接到包含模板编译成的函数的 JavaScript 文件。使用 new Function() 创建的函数甚至在扩展安装之前就已经被创建了,然后模板可以在扩展内部呈现,并与扩展内部提供的任何数据插值。

  • 或者,Chrome 的扩展系统允许我们在指定的沙盒内部使用某些文件。由于代码与浏览器中的扩展数据和 API 访问隔离,因此允许在沙盒中运行不安全的字符串到函数特性,例如 eval()new Function()

在这个示例中,我们将使用沙盒功能,以便我们可以继续使用 JsRender。

启动推进器

首先,我们必须设置沙盒,这是通过使用我们之前创建的清单文件指定要沙盒化的页面来完成的。将以下代码直接添加到 manifest.json 中,直接在最终闭合大括号之前:

"sandbox": {
    "pages": ["template.html"]
}

提示

不要忘记在 browser_action 属性的最终闭合大括号之后直接添加逗号。

我们已将 template.html 指定为沙盒页面。创建一个名为 template.html 的新文件,并将其保存在 chrome-extension 目录中。它应包含以下代码:

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <script id="contactTemplate" type="text/x-jsrender">
            {{for contacts}}
                <li>
                    <article>
                        <div class="details">
                            <h1>{{:name}}</h1>
                            {{if url}}
                                <span>website: {{url}}</span>
                            {{/if}}
                            {{if jobTitle}}
                                <h2>{{:jobTitle}}</h2>
                            {{/if}}
                            {{if companyName}}
                                <span class="company">
                                    {{:companyName}}
                                </span>
                            {{/if}}
                            {{if address}}
                                <p>{{:address}}</p>
                            {{/if}}
                            {{if contactMethods}}
                                <dl>
                                    {{for ~getMembers(contactMethods)}}
                                        <dd>{{:key}}</dd>
                                        <dt>{{:val}}</dt>
                                    {{/for}}
                                </dl>
                           {{/if}}
                        </div>
                    </article>
                </li>
            {{/for}}
        </script>
        <script src="img/jquery-1.9.0.min.js"></script>
        <script src="img/jsrender.js"></script>
        <script src="img/template.js"></script>
    </head>
</html>

模板页面还引用了 template.js 脚本文件。我们应该在 chrome-extension 目录中创建此文件,并将以下代码添加到其中:

(function () {
    $.views.helpers({
        getMembers: function (obj) {
            var prop,
                 arr = [];

            for (prop in obj) {
                if (obj.hasOwnProperty(prop)) {
                    var newObj = {
                        key: prop,
                        val: obj[prop]
                     }

                    arr.push(newObj);
                }
            }

            return arr;
        }
    });
} ());

完成目标 - 迷你总结

我们首先向扩展添加了一个新的 HTML 页面。名为 template.html 的页面类似于常规网页,只是没有 <body>,只有一个 <head>,它链接到一些 JavaScript 资源,并包含我们将使用的模板的 <script> 元素。

提示

通常在 Chrome 扩展中,CSP 阻止我们运行任何内联脚本 - 所有脚本都应驻留在外部文件中。在 <script> 元素上使用非标准的 type 属性允许我们规避这一点,以便我们可以将我们的模板存储在页面内,而不是使用另一个外部文件。

新页面的主体是模板本身。Schema.org 微数据允许人们添加大量附加信息以描述页面上的元素,因此扩展中可能存储各种不同的信息。

因此,我们的模板利用了很多条件来显示如果它们存在的东西。扩展程序应始终显示名称,但除此之外,它可能显示图像、工作标题和公司、地址或各种联系方式,或者它们的任何组合。

模板中最复杂的部分是getMembers()辅助函数。我们将使用 JsRender 的{{for}}标记为contactMethods对象中的每个对象调用此辅助函数,该标记使用波浪号(~)字符调用辅助函数。在循环内,我们将能够访问辅助函数返回的值,并将这些值插入到相关元素中。

接下来,我们添加了template.js脚本文件。此时,我们需要添加到此脚本文件的所有内容只是模板用于呈现任何联系方式的辅助方法。这些将采用{ email: me@me.com }的格式。

使用 JsRender 的helpers()方法注册辅助程序。此方法接受一个对象,其中指定辅助程序的名称为键,应调用的函数为值。

函数接收一个对象。我们首先创建一个空数组,然后使用标准的for in循环迭代对象。我们首先使用 JavaScript 的hasOwnProperty()函数检查正在迭代的属性是否属于对象,且不是从原型继承的。

然后,我们只需创建一个新对象,并将键设置为名为key的属性,将值设置为名为val的属性。这些是我们在模板中使用的模板变量,用于在我们的模板中的<dl>中插入。

然后,将此新对象推送到我们创建的数组中,并且一旦对传递给辅助函数的对象进行了迭代,我们将该数组返回给模板,以便{{for}}循环进行迭代。

在沙盒中发布消息

在此任务中,我们将建立我们的弹出窗口与沙盒模板页面之间的通信,以查看如何在打开弹出窗口时让模板进行呈现。

启动推进器

首先,我们可以添加将消息发送到沙盒页面以请求模板进行呈现的代码。在popup.js中,添加以下代码:

var iframe = $("#poster"),
    message = {
        command: "issueTemplate",
        context: JSON.parse(localStorage.getItem("webContacts"))
    };
    iframe.on("load", function () {
        if (message.context) {
            iframe[0].contentWindow.postMessage(message, "*");
        } else {
            $("<li>", {
                text: "No contacts added yet"
            }).appendTo($("#contacts"));
        }
    });

window.addEventListener("message", function (e) {
    $("#contacts").append((e.data.markup));
});

接下来,我们需要添加响应初始消息的代码。将以下代码直接添加到template.js中,放在我们上一个任务中添加的辅助方法之后:

var template = $.templates($("#contactTemplate").html());

window.addEventListener("message", function (e) {
    if (e.data.command === "issueTemplate") {

        var message = {
            markup: template.render(e.data.context)
        };

        e.source.postMessage(message, event.origin);
    }
});

目标完成 - 小型总结

首先,我们在popup.js中设置了初始消息传递。我们在变量中缓存了来自弹出窗口的<iframe>元素,然后编写了一条消息。消息是以对象文字的形式,具有command属性和context属性。

command属性告诉在<iframe>中运行的代码要执行什么操作,而context包含要渲染到模板中的数据。我们将要渲染的数据存储在 localStorage 的webContacts键下,并且数据将以 JSON 格式存储,因此我们需要使用JSON.parse()将其转换回 JavaScript 对象。

然后,我们使用 jQuery 的on()方法为<iframe>元素添加加载处理程序。传递给on()的匿名函数中包含的代码将在<iframe>的内容加载完成后执行。

一旦发生这种情况,我们检查 message 对象的 context 属性是否具有真值。如果是,我们使用 <iframe>contentWindow 属性的 postMessage() 函数将 message 对象发布到 <iframe>

postMessage() 函数接受两个参数 - 第一个是要发布的内容,在这种情况下是我们的 message 对象,第二个参数指定哪些文件可以接收此消息。我们将其设置为通配符 *,这样任何文件都可以订阅我们的消息。

如果没有存储的联系人,则我们 message 对象的 context 属性将具有假值 null。在这种情况下,我们只需创建一个新的 <li> 元素,其中包含一条文本消息,说明没有保存的联系人,并将其直接附加到 popup.html 中硬编码的空 <ul> 中。

我们的脚本文件 popup.js 也需要接收消息。我们使用标准的 JavaScript addEventListener() 函数将一个监听器附加到 window 上的 message 事件上。默认情况下,jQuery 不处理 message 事件。

popup.js 收到的消息将是包含要渲染的 HTML 标记的沙盒页面的响应。标记将包含在事件对象的 data 属性中的名为 markup 的属性中。我们简单地选择 popup.html 中的 <ul> 元素,并附加我们收到的标记。

我们还在 template.js 中添加了一些代码,该脚本文件被我们 <iframe> 内的页面引用。我们在这里再次使用 addEventListener() 函数来订阅消息事件。

这次我们首先检查发送消息的对象的 command 属性是否等于 issueTemplate。如果是,然后我们创建并渲染数据到我们的 JsRender 模板中,并构建一个包含渲染模板标记的新 message 对象。

创建了消息对象后,我们将其发布回 popup.js。我们可以使用事件对象的 source 属性获取 window 对象发送消息,并且可以使用事件对象的 origin 属性指定哪些文件可以接收消息。

这两个属性非常相似,除了 source 包含一个 window 对象,而 origin 包含一个文件名。文件名将是一个特殊的 Chrome 扩展名。在这一点上,我们应该能够启动弹出窗口,并看到没有联系人消息,因为我们还没有保存任何联系人。

添加一个内容脚本

现在,一切都已准备就绪以显示存储的联系人,因此我们可以专注于实际获取一些联系人。为了与用户在浏览器中导航的页面交互,我们需要添加一个内容脚本。

内容脚本就像一个常规脚本一样,只是它与浏览器中显示的页面进行交互,而不是与组成扩展的文件进行交互。我们会发现,我们可以在这些不同区域之间(浏览器中的页面和扩展)发送消息,方法与我们发送消息到我们的沙盒类似。

启动推进器

首先,我们需要向 chrome-extension 目录中添加一些新文件。我们需要一个名为 content.js 的 JavaScript 文件和一个名为 content.css 的样式表。我们需要告诉我们的扩展使用这些文件,因此我们还应该在此项目之前创建的清单文件(manifest.json)中添加一个新部分:

"content_scripts": [{
    "matches": ["*://*/*"],
    "css": ["content.css"],
    "js": ["jquery-1.9.0.min.js", "content.js"]
}]

这个新的部分应该直接添加到我们之前添加的沙盒部分之后(像以前一样,在sandbox属性后别忘了添加逗号)。

接下来,我们可以向 content.js 添加所需的行为:

(function () {

    var people = $("[itemtype*='schema.org/Person']"),
        peopleData = [];

    if (people.length) {

        people.each(function (i) {

            var person = microdata.eq(i),
                data = {},
                contactMethods = {};

            person.addClass("app-person");

        });
    }
} ());

我们还可以添加一些基本样式,用 content.css 样式表突出显示包含微数据属性的任何元素。现在更新此文件,使其包含以下代码:

.app-person { 
    position:relative; box-shadow:0 0 3px rgba(0,0,0, .5); 
    background-color:#fff;
}

目标完成 - 迷你总结

首先,我们更新了清单文件以包括内容脚本。正如我之前提到的,内容脚本用于与浏览器中显示的可见页面进行交互,而不是与扩展使用的任何文件进行交互。

我们可以使用清单中的 content_script 规则来启用内容脚本。我们需要指定内容脚本应加载到哪些页面中。我们在 URL 的 protocolhostpath 部分使用通配符(*)以便在访问任何页面时加载脚本。

使用 Schema.org 微数据来描述人物时,存在的不同信息被放置在一个容器内(通常是一个 <div> 元素,尽管任何元素都可以被使用),该容器具有特殊属性 itemtype

此属性的值是一个 URL,指定了它包含的元素描述的数据。所以,要描述一个人,这个容器将具有 URL schema.org/Person。这意味着容器中的元素可能有描述特定数据的附加属性,比如姓名或职务。容器内的元素上的这些附加属性将是 itemprop

在这种情况下,我们使用了一个 jQuery 属性包含选择器(*=)来尝试从页面中选择包含此属性的元素。如果属性选择器返回的数组长度(因此不为空),我们就知道页面上至少存在一个这样的元素,因此可以进一步处理该元素。

具有此属性的元素集合存储在名为 people 的变量中。我们还在变量 peopleData 中创建了一个空数组,准备存储页面上找到的所有人的所有信息。

然后,我们使用 jQuery 的each()方法来迭代从页面选择的元素。在我们的each()循环中,不使用$(this),我们可以使用我们已经从页面中选择的元素集合,与当前循环的索引一起使用 jQuery 的eq()方法来引用每个元素,我们将其存储在名为person的变量中。

我们还创建一个空对象并将其存储在名为data的变量中,准备存储每个人的微数据,以及一个名为contactMethods的空对象,因为任何电话号码或电子邮件地址的微数据都需要添加到我们的模板可消耗的子对象中。

此时我们所做的就是向容器元素添加一个新的类名。然后,我们可以使用content.css样式表向元素添加一些非常基本的样式,以引起用户的注意。

抓取页面的微数据

现在,我们已经安装好了我们的内容脚本,我们可以与扩展程序的用户访问的任何网页进行交互,并检查它是否具有任何微数据属性。

此时,任何包含微数据的元素都会被用户突出显示,因此我们需要添加功能,允许用户查看微数据并在愿意的情况下保存,这就是我们将在此任务中介绍的内容。

启动推进器

content.js中为每个具有itemtype属性的元素容器添加类名之后,添加以下代码:

person.children().each(function (j) {

    var child = person.children().eq(j),
        iProp = child.attr("itemprop");

    if (iProp) {

        if (child.attr("itemscope") !== "") {

            if (iProp === "email" || iProp === "telephone") {
                contactMethods[iProp] = child.text();
            } else {
                data[iProp] = child.text();
            }
        } else {

            var content = [];

            child.children().each(function (x) {
                content.push(child.children().eq(x).text());
            });

            data[iProp] = content.join(", ");
        }
    }
});

var hasProps = function (obj) {
    var prop,
    hasData = false;

    for (prop in obj) {
        if (obj.hasOwnProperty(prop)) {
            hasData = true;
            break;

        }
    }

    return hasData;
};

if (hasProps(contactMethods)) {
    data.contactMethods = contactMethods;
}

peopleData.push(data);

目标完成 - 小结

在上一个任务中,我们为每个标记了微数据的元素容器添加了一个类名。在此任务中,我们仍处于处理每个容器的each()循环的上下文中。

因此,在这个任务中添加的代码中,我们首先再次调用each(),这次是在容器元素的直接子元素上;我们可以使用 jQuery 的children()方法轻松获取这些子元素。

在这个each()循环中,我们首先使用传递给我们迭代函数的循环计数器(j)作为 jQuery 的eq()方法的参数来获取现有缓存的person变量中的当前项目。这样可以避免在我们的循环中创建一个全新的 jQuery 对象。

我们还将当前元素的itemprop属性的值存储在一个名为iProp的变量中,因为我们需要多次引用它,并且使用一个漂亮的短变量意味着我们需要输入更少的内容。

此时我们不知道我们是否正在处理常规元素还是包含微数据的元素,因此我们使用一个if语句来检查我们刚刚设置的iProp变量是否具有真值。如果元素没有itemprop属性,则此变量将保存一个空字符串,该空字符串为假值,如果元素只是常规元素,则停止代码进一步进行。

在此条件语句内部,我们知道我们正在处理包含微数据的元素,但数据可能采用不同的格式。例如,如果元素包含地址,它将不直接包含任何内容,而是将包含数据的自己的子元素。在这种情况下,元素将具有一个itemscope属性。首先,我们希望处理不包含itemscope属性的元素,因此我们嵌套条件的第一个分支检查通过选择itemscope属性返回的值是否不是空字符串。

如果记得我们的模板,我们设置了一个帮助函数,使用对象显示联系信息。为了创建这个新对象而不是创建data对象的新属性,我们使用另一个嵌套的if语句来检查iProp变量是否包含电子邮件或电话号码。

如果是这样,我们将iProp变量的值作为contactMethods对象的键,元素的文本作为值添加。如果iProp变量不包含电子邮件地址或电话号码,我们将iProp变量设置为data对象的键,并将其值设置为元素的内容。

第二个嵌套if语句的下一个分支是对具有itemscope属性的元素的。在这种情况下,我们首先定义一个空数组,并将其存储在名为content的变量中。然后,我们使用 jQuery 的each()方法迭代子元素,并将每个元素的文本内容推入content数组。

一旦我们遍历了子元素并填充了数组,我们就可以将当前的iProp变量和content数组中的数据添加到我们的data对象中。任何具有itemscope属性的元素仍应该具有itemprop属性,因此这应该仍然有效。

因此,在这一点上,我们的数据对象应该是对我们主容器内部元素设置的微数据的准确表示。但在对它们进行任何操作之前,我们需要检查contentMethods对象是否已填充,并且如果已填充,则将其添加到我们的data对象中。

我们可以使用hasProps()函数来检查对象是否具有自己的属性。该函数将接收要测试的对象作为参数。在函数内部,我们首先定义hasData变量,将其设置为false

然后,我们使用for in循环来迭代对象的每个属性。对于每个属性,我们检查该属性是否实际存在于对象上,并且未使用 JavaScript 的hasOwnProperty()函数继承。如果属性确实属于对象,我们将hasData设置为true,然后使用break退出循环。

然后,我们通过将其传递给我们的hasProps()函数来检查contactMethods对象是否有任何属性,如果有,我们将其添加到data对象中。最后,一旦所有这些处理都完成,我们将data对象添加到我们在代码开头定义的peopleData数组中。

添加一个保存微数据的机制

在这一点上,如果 Chrome 中显示的页面包含任何个人微数据,我们将有一个包含一个或多个包含微数据和描述其文本的对象的数组。在此任务中,我们将允许用户存储该数据(如果他/她愿意)。

因为我们的内容脚本在网页的上下文中运行而不是在我们的扩展中,所以我们需要再次使用消息传递来将任何收集到的数据传递回扩展以进行存储。

准备升空

为了在我们的内容脚本和扩展之间设置消息传递,我们需要添加一个背景页。背景页在扩展被安装和启用时持续运行,这将允许我们设置处理程序来监听并响应从内容脚本发送的消息。

背景页面可以是 HTML 或 JavaScript。在本项目中,我们将使用 JavaScript 版本。现在创建一个新文件,并将其保存在 chrome-extension 目录中为 background.js。我们还需要通过向 manifest.json 文件中添加一个新的 background 部分来将此文件注册为背景脚本:

"background": {
    "scripts": ["jquery-1.9.0.min.js", "background.js"]
}

这段代码应该直接放在列出 content_scripts 的数组之后。再次提醒,不要忘记数组后面的逗号。

启动推进器

首先,我们将向我们的背景页面添加所需的行为。在 background.js 中,添加以下代码:

chrome.extension.onConnect.addListener(function (port) {

    port.onMessage.addListener(function (msg) {

        if (msg.command === "getData") {

            var contacts = localStorage.getItem("webContacts")
|| '{ "message": "no contacts" }',
                  jsonContacts = JSON.parse(contacts);

            port.postMessage(jsonContacts);

        } else if (msg.command === "setData") {

          localStorage.setItem("webContacts", 
JSON.stringify({ 
              contacts: msg.contacts 
        }));

            port.postMessage({ message: "success" });
        }
    });
});

接下来,在 content.js 中,在我们将 data 对象推入 peopleData 数组之后,直接添加以下代码:

$("<a/>", {
    href: "#",
    "class": "app-save",
    text: "Save"
}).on("click", function (e) {
    e.preventDefault();

    var el = $(this),
          port = chrome.extension.connect(),
          contacts;

    if (!el.hasClass("app-saved")) {

        port.postMessage({ command: "getData" });
        port.onMessage.addListener(function (msg) {

            if (msg.message === "no contacts") {

                contacts = [peopleData[i]];

                port.postMessage({ 
                    command:"setData", 
                    contacts:contacts 
                });
            } else if (msg.contacts) {

                contacts = msg.contacts;
                contacts.push(peopleData[i]);

                port.postMessage({ 
                    command: "setData", 
                    contacts: contacts 
            });

        } else if (msg.message === "success") {

            el.addClass("app-saved")
               .text("Contact information saved");

        port.disconnect();

            }
        });
    }
}).appendTo(person);

最后,我们可以为我们刚刚添加的新保存链接添加一些样式。在 content.css 中,在文件底部添加以下代码:

.app-save { position:absolute; top:5px; right:5px; }
.app-saved { opacity:.5; cursor:default; }

目标完成 - 小型简报

在这个任务中,我们添加了相当多的代码,因为我们更新了几个不同的文件,以使扩展的不同部分进行通信。

添加通信模块

首先,我们更新了我们在任务开始时添加的行为页面。我们将使用 localStorage 来存储扩展收集的保存的联系人,但是只有运行在用户查看的网页上下文中的内容脚本才能访问给定页面的 localStorage 区域,但我们需要访问扩展本身的 localStorage 区域。

为了实现这一点,我们的 background.js 文件将充当一个中介,它将访问扩展的 localStorage 区域,并在内容脚本和扩展之间传递数据。

首先,我们添加了一个监听器到 onConnect 事件,我们可以通过 Chrome 的 extension 实用模块访问。当内容脚本与扩展建立连接时,浏览器将自动打开一个端口。表示此端口的对象将自动传递给我们的处理程序函数。

我们可以使用端口来添加一个消息事件的处理程序。与项目早期的简单 <iframe> 通信一样,此处理程序函数将自动传递触发事件的消息。

在消息处理程序内部,我们检查消息的command属性是否等于getData。如果是,我们首先创建一个contacts对象,该对象将由 localStorage getItem()方法获取的联系人或者仅包含消息no contacts的非常简单的 JSON 对象组成,我们可以手动创建。

一旦我们有了这两个 JSON 对象之一,我们就可以使用 Chrome 的原生 JSON parse()方法将其解析为一个真正的 JavaScript 对象。然后,我们可以使用postMessage()方法将此对象传回端口。每当建立一个新的连接时,一个新的端口将被打开,所以消息将自动传回到正确的端口,无需我们进行额外的配置。

如果msg对象的command属性不等于getData,它可能会等于setData。如果是,我们想要将一个或多个新的联系人存储到 localStorage。在这种情况下,我们将要存储的联系人作为msg对象的contacts属性中的对象传递,所以我们可以简单地在这个属性的对象上使用stringify()方法作为setItem()方法的第二个参数。

然后,我们再次使用port对象的postMessage()方法传回一条简短的消息,确认数据保存成功。

更新内容脚本

其次,我们更新了content.js文件,以便收集和存储访问者在网页上找到的任何联系信息。

我们首先添加一个新的<a>元素,该元素将用作保存联系信息的按钮,并且将添加到包含微数据的任何元素中。我们为新元素添加了一个简单的# href属性,一个用于样式目的的类名,以及文本保存

大多数新功能都包含在使用 jQuery 的on()方法创建新的<a>元素时直接附加到每个元素上的单击事件处理程序中。

在这个事件处理程序中,我们首先使用preventDefault()停止浏览器的默认行为,就像我们通常在将事件处理程序附加到<a>元素时一样。然后,我们通过将$(this)存储在一个名为el的变量中来缓存对当前<a>元素的引用。还使用extension模块的connect()方法打开一个新的端口来处理我们的通信需求。声明了一个名为contacts的变量,但没有立即定义。

代码的其余部分位于一个条件语句内,该条件语句检查元素是否已经具有类名app-saved,这将有助于防止同一页面上同一人的重复条目被保存到本地存储中。

在条件语句中,我们首先需要获取先前存储的联系人,因此我们通过向我们刚刚打开的端口发送消息来请求行为页面上的保存联系人。我们将一个具有command属性设置为getData的对象作为消息发送。

然后,我们使用addListener()方法对此消息的响应添加了一个处理程序,该方法在onMessage事件上。我们的其余代码位于此处理程序中,其中包含根据响应消息不同而有不同反应的另一个条件语句。

条件语句的第一个分支处理响应msgmessage属性包含字符串no contacts的情况。在这种情况下,我们创建一个新数组,其中包含从点击的保存链接中收集的联系人信息。我们已经在peopleData数组中有这些信息,并且由于我们仍处于更新每个人的循环中,因此我们可以使用i变量来存储正确的人员。

然后,我们可以将此数组发送到行为页面,以永久存储在扩展程序的本地存储区域中。

如果msg对象没有message属性,可能有contacts属性。此属性将包含先前存储的联系人数组,因此我们可以将数组保存到变量中,并在将更新后的数组发送回行为页面进行永久存储之前将新联系人添加到此数组中。

条件语句的最后一个分支处理了联系人成功保存的情况。在这种情况下,msg对象的message属性将包含success字符串。在这种情况下,我们将类名app-saved添加到<a>元素,并将文本更改为联系信息已保存。由于不再需要端口,我们可以使用port对象的disconnect()方法关闭它。

添加简单的样式

最后,我们为保存链接添加了一些非常简单的样式。一旦用户发起的操作完成,显示反馈非常重要。

在这个例子中,我们通过改变链接的文本简单地使用 CSS 使其更加不透明,使其看起来好像不再可点击,这是因为我们在脚本中使用的if语句的情况。

现在,我们应该能够浏览到包含微数据并保存联系信息的页面。当单击浏览器操作按钮时,我们将看到弹出窗口,其中应显示保存的联系人,如项目开始时的屏幕截图所示。

机密情报

在测试内容脚本时,重要的是要意识到每当内容文件更改时,这在本例中意味着 JavaScript 文件或样式表,都必须重新加载扩展程序。

要重新加载扩展程序,在 Chrome 的扩展程序页面中列出的扩展程序下方有一个重新加载Ctrl + R)链接。我们需要点击此链接以应用对任何内容文件所做的更改。扩展程序的其他部分,例如弹出窗口文件,不需要重新加载扩展程序。

扩展程序员的另一个有用工具是开发者工具,它可以专门打开以监视后台页面中的代码。在使用后台页面时,进行故障排除和脚本调试时,这可能非常有用。

任务完成

在这个项目中,我们涵盖了构建 Chrome 扩展的大部分基础知识。我们介绍了创建一个浏览器操作,当点击它时触发弹出窗口,以显示保存的联系人。

我们还了解了如何安全地对需要运行危险代码(如eval()new Function)的页面进行沙盒化,以保护我们的扩展不受 XSS 攻击的影响,并且我们如何使用简单的消息传递 API 向包含沙盒化页面的<iframe>元素发送消息并接收响应。

我们看到,除了定义在扩展上下文中运行的脚本之外,还可以添加在浏览器中显示的网页上下文中运行的内容脚本。我们还学会了如何使用manifest.json文件来指定扩展的这些不同区域。

我们还看到可以使用更高级的消息传递系统,允许我们打开允许进行更复杂双向消息传递的端口。通过端口通信,我们可以从扩展的不同区域发送并接收尽可能多的消息,以完成保存数据到扩展 localStorage 区域等特定任务。

我们还了解了可以使用Schema.org微数据描述的数据类型,以及可以添加到元素中进行描述的 HTML 属性。除了能描述人以外,还有用于描述地点、公司、电影等等的Schema.org格式。

我们学到了很多关于在 Chrome 中创建扩展,但是我们还使用了大量 jQuery 方法,以简化我们编写的脚本,以驱动扩展程序。

你准备好全力以赴了吗?一个热门挑战

当我们的扩展保存新联系人时,包含微数据的突出显示元素将被赋予新的 CSS 类名,并且会对它们进行一些非常简约的额外样式修改。

这样做是可以的,但确认成功的更好方法是利用 Chrome 的桌面通知系统,生成类似 Growl 风格的弹出式通知来确认成功。

访问developer.chrome.com/extensions/notifications.html查看通知文档,并查看是否可以更新扩展以包括此功能。

第七章:制作自己的 jQuery

在 jQuery 1.8 发布中,引入了一项全体设计希望已久的新功能-能够构建只包含特定任务所需功能的自定义版本的 jQuery。

任务简报

在这个项目中,我们将设置我们需要使用 jQuery 构建工具的环境。我们将看到我们需要使用的其他软件,如何运行构建工具本身,以及我们可以期望构建工具的输出。

为什么它很棒?

尽管有人通常会说他们在构建的每个网站中都使用 jQuery(对我来说通常是这样),但我期望很少有人会说他们在每个项目中都使用完全相同的 jQuery 方法,或者他们使用了大量可用方法和功能。

减少文件大小以满足移动空间的需求,以及诸如 Zepto 等微框架的兴起,它以更小的尺寸提供了大量 jQuery 功能,这促使 jQuery 提供了一种精简大小的方法。

从 jQuery 1.8 开始,我们现在可以使用官方 jQuery 构建工具来构建我们自己的定制版本的库,从而只选择我们所需的功能来最小化库的大小。

注意

有关 Zepto 的更多信息,请查看 zeptojs.com/.

你的顶尖目标

要成功完成这个项目,我们需要完成以下任务:

  • 安装 Git 和 Make

  • 安装 Node.js

  • 安装 Grunt.js

  • 配置环境

  • 构建自定义 jQuery

  • 运行 QUnit 单元测试

任务清单

我们将使用 Node.js 来运行构建工具,所以你现在应该下载一个副本。Node 网站(nodejs.org/download/)提供了 64 位和 32 位 Windows 的安装程序,以及 Mac OS X 的安装程序。它还为 Mac OS X、Linux 和 SunOS 提供了二进制文件。下载并安装适合你的操作系统的版本。

jQuery 的官方构建工具(尽管它除了构建 jQuery 之外还可以做很多其他事情)是 Grunt.js,由 Ben Alman 编写。我们不需要下载它,因为它是通过 Node Package ManagerNPM)安装的。我们将在项目后面详细看这个过程。

注意

要了解更多关于 Grunt.js 的信息,请访问官方网站 gruntjs.com.

首先,我们需要设置一个本地工作区。我们可以在根项目文件夹中创建一个名为 jquery-source 的文件夹。当我们克隆 jQuery Github 仓库时,我们会将 jQuery 源代码存储在这里,并且 Grunt 也会在这里构建最终版本的 jQuery。

安装 Git 和 Make

我们需要安装的第一件事是 Git,我们需要它来从 Github 存储库克隆 jQuery 源代码到我们自己的计算机,这样我们就可以处理源文件。我们还需要一个叫做 Make 的东西,但我们只需要在 Mac 平台上真正安装它,因为在 Windows 上安装 Git 时它会自动安装。

提示

因为我们将创建的文件仅供我们自己使用,并且我们不想通过将代码推送回存储库来为 jQuery 做出贡献,所以我们不需要担心在 Github 上创建账户。

准备起飞

首先,我们需要下载 Git 和 Make 的相关安装程序。根据你是在 Mac 还是 Windows 平台上开发,需要不同的应用程序。

Mac 开发者

Mac 用户可以访问git-scm.com/download/mac获取 Git。

接下来我们可以安装 Make。Mac 开发者可以通过安装 XCode 来获取。可以从developer.apple.com/xcode/下载。

Windows 开发者

Windows 用户可以安装msysgit,可以通过访问code.google.com/p/msysgit/downloads/detail?name=msysGit-fullinstall-1.8.0-preview20121022.exe获取。

启动推进器

下载完成安装程序后,运行它们来安装应用程序。安装程序默认选择的设置对这个任务来说应该是合适的。首先我们应该安装 Git(或者在 Windows 上安装 msysgit)。

Mac 开发者

Mac 开发者只需要运行 Git 的安装程序将其安装到系统中。安装完成后,我们可以安装 XCode。我们只需要运行安装程序,Make 以及一些其他工具将被安装并准备好。

Windows 开发者

msysgit 的完整安装程序完成后,你应该可以看到一个命令行界面(标题为 MINGW32),表明一切准备就绪,你可以开始进行编码。但是,在我们开始编码之前,我们需要编译 Git。

为了做到这一点,我们需要运行一个叫做initialize.sh的文件。在 MINGW32 窗口中,cdmsysgit目录。如果你允许它安装到默认位置,你可以使用以下命令:

cd C:\\msysgit\\msysgit\\share\\msysGit

一旦我们在正确的目录中,就可以在 CLI 中运行initialize.sh。和安装一样,这个过程可能需要一些时间,所以请耐心等待 CLI 返回$字符的闪烁光标。

注意

以这种方式编译 Git 需要互联网连接。

Windows 开发者需要确保Git.exe和 MINGW 资源可以通过系统的PATH变量访问。这可以通过转到控制面板 | 系统 | 高级系统设置 | 环境变量来更新。

在对话框的底部部分,双击路径,并将以下两个路径添加到位于您选择安装位置内的msysgit文件夹中的bin文件夹中的git.exe文件中:

  • ;C:\msysgit\msysgit\bin;

  • C:\msysgit\msysgit\mingw\bin;

提示

谨慎更新路径!

您必须确保Git.exe的路径与其余路径变量之间用分号分隔。如果在添加Git.exe路径之前路径不以分号结尾,请确保添加一个。错误地更新路径变量可能导致系统不稳定和/或数据丢失。我在上一个代码示例的开头显示了一个分号,以说明这一点。

路径更新后,我们应该能够使用常规命令提示符来运行 Git 命令。

安装后的任务

在终端或 Windows 命令提示符(我将两者简称为 CLI 以便简洁起见)窗口中,我们应该首先cd进入我们在项目开始时创建的jquery-source文件夹。根据您本地开发文件夹的位置不同,此命令看起来会像下面这样:

cd c:\jquery-hotshots\jquery-source

要克隆 jQuery 仓库,请在 CLI 中输入以下命令:

git clone git://github.com/jquery/jquery.git

同样,在 CLI 返回到闪烁的光标以指示进程完成之前,我们应该看到一些活动。

根据您所开发的平台不同,您应该会看到类似以下截图的内容:

安装后的任务

完成目标 - 迷你总结

我们安装了 Git,然后使用它克隆了 jQuery 的 Github 仓库到这个目录,以获取 jQuery 源代码的最新版本。如果您习惯于 SVN,克隆仓库的概念上与检出仓库是相同的。

再次说明,这些命令的语法在 Mac 和 Windows 系统上非常相似,但请注意,在 Windows 中使用路径时需要转义反斜杠。完成此操作后,我们应该会在jquery-source目录内看到一个名为jquery的新目录。

如果我们进入此目录,会看到一些更多的目录,包括:

  • build:此目录由构建工具用于构建 jQuery

  • speed:此目录包含基准测试

  • src:此目录包含编译为 jQuery 的所有单个源文件

  • 测试:此目录包含 jQuery 的所有单元测试

它还包含一系列各种文件,包括:

  • 授权和文档,包括 jQuery 的作者和项目贡献指南

  • Git 特定文件,如.gitignore.gitmodules

  • Grunt 特定文件,如 Gruntfile.js

  • JSHint 用于测试和代码质量目的

我们不需要直接使用 Make,但是当我们构建 jQuery 源代码时,Grunt 会使用它,因此它需要存在于我们的系统中。

安装 Node.js

Node.js 是一个用 JavaScript 构建的运行服务器端应用程序的平台。例如,可以轻松创建一个接收和响应 HTTP 请求的网络服务器实例,使用回调函数。

服务器端 JS 与更熟悉的客户端对应物并不完全相同,但在您所熟悉和喜爱的舒适语法中,您会发现许多相似之处。在这个项目中,我们实际上不会编写任何服务器端 JavaScript — 我们只需要 Node 来运行 Grunt.js 构建工具。

为起飞做准备

要获取适用于您平台的适当安装程序,请访问 Node.js 网站 nodejs.org 并点击下载按钮。如果支持的话,应该会自动检测到适合您平台的正确安装程序。

启动推进器

在 Windows 或 Mac 平台上,安装 Node 非常简单,因为两者都有安装程序。此任务将包括运行安装程序,这显然是简单的,并使用 CLI 测试安装。

在 Windows 或 Mac 平台上,运行安装程序,它将指导您完成安装过程。我发现在大多数情况下默认选项都很好。与之前一样,我们还需要更新Path变量以包括 Node 和 Node 的包管理器 NPM。这些目录的路径在不同平台上会有所不同。

Mac

Mac 开发者应检查 $PATH 变量是否包含对 usr/local/bin 的引用。我发现这已经在我的 $PATH 中了,但是如果您发现它不存在,您应该添加它。

注意

有关更新 $PATH 变量的更多信息,请参阅 www.tech-recipes.com/rx/2621/os_x_change_path_environment_variable/

Windows

Windows 开发者需要像以前一样更新Path变量,其中包括以下路径:

  • C:\Program Files\nodejs\;

  • C:\Users\Desktop\AppData\Roaming\npm;

注意

Windows 开发者可能会发现 Path 变量已经包含了一个 Node 条目,因此可能只需要添加 NPM 的路径。

目标完成 - 迷你总结

一旦安装了 Node,我们就需要使用 CLI 与其进行交互。要验证 Node 是否已正确安装,请在 CLI 中键入以下命令:

node -v

CLI 应该报告使用的版本,如下所示:

目标完成 - 迷你总结

我们可以通过运行以下命令来测试 NPM:

npm -v

安装 Grunt.js

在这个任务中,我们需要安装 Grunt.js,这个过程非常快速且简单,就像安装 Node 一样。我们甚至不需要手动下载任何东西,就像以前一样,相同的命令应该在 Mac 或 Windows 系统上都能工作,只需要非常小的调整。

启动推进器

我们需要使用Node 包管理器 NPM来安装它,可以通过运行以下命令来执行(注意,不能运行 Node 本身):

npm install -g grunt-cli

注意

Mac 用户可能需要在命令开头使用 superuser do

sudo –s npm install –g grunt

准备等待几分钟。同样,当 Grunt 需要的资源被下载和安装时,我们应该会看到大量活动。一旦安装完成,提示符将返回到闪烁的光标。CLI 应该会像以下截图一样显示,具体取决于您正在开发的平台:

启动推进器

完成目标 - 迷你总结

如果一切顺利(通常情况下应该如此,除非您的系统出现问题),那么在 Grunt 及其依赖项通过 NPM 全局下载和安装完成时,CLI 中将会看到大量活动,一旦完成,Grunt 将被安装并准备就绪。

提示

需要互联网连接才能使用 NPM 自动下载和安装软件包。

为了验证 Grunt 是否已正确安装,我们可以在 CLI 中输入以下命令:

grunt -version

这将输出当前 Grunt 的版本,并且应该可以从任何目录中运行,因为 Grunt 已经全局安装了。

机密情报

除了构建自定义版本的 jQuery 外,Grunt 还可以用于创建几种不同的常见项目。我们首先选择以下项目类型之一:

  • gruntfile

  • commonjs

  • jquery

  • node

我们可以运行内置的 init 任务,并指定其中一个项目,Grunt 将继续设置包含该项目常用资源的骨架项目。

例如,运行 jquery init 任务将设置一个工作目录,用于创建一个 jQuery 插件。在该目录中,Grunt 将创建源脚本文件和单元测试的文件夹,以及创建一系列文件,包括一个 package.json 文件。

很可能在某个时候,所有新的 jQuery 插件都需要按照 Grunt 创建此项目类型时的方式来构建结构,因此,对于任何 jQuery 插件开发者来说,Grunt 将成为一款不可或缺的、节省时间的工具。

配置环境

在我们准备构建自己的 jQuery 版本之前,还有一些事情需要做。我们还可以通过构建 jQuery 的完整版本来测试我们的安装和配置,以确保一切都按预期工作。

准备起飞

我们需要安装一些额外的 Grunt 依赖项,以便我们可以使用从 Github 克隆的源文件来创建 jQuery 脚本文件。项目还使用了一系列 NPM 模块,这些模块也需要安装。幸运的是,NPM 可以自动为我们安装所有内容。

启动推进器

在构建 jQuery 源码之前,我们需要在 jquery 源码文件夹中安装一些额外的 Grunt 依赖项。我们可以使用 NPM 来做到这一点,因此可以在 CLI 中输入以下命令:

npm install 

注意

在运行 install 命令之前,请确保您已经使用 cd 命令导航到 jquery 目录。

在运行 install 命令后,CLI 应该会有很多活动,而在进程结束时,CLI 应该会显示类似以下截图的内容:

启动推进器

为了测试一切是否按预期进行,我们可以构建 jQuery 的完整版本。只需在 CLI 中运行 grunt 命令:

grunt

注意

如果此时出现任何错误或警告,说明某些内容未安装或配置正确。失败的原因可能有很多,所以最好的做法是卸载我们安装的所有内容,然后重新开始整个过程,确保所有步骤都严格按照要求进行。

同样,我们应该会在 CLI 上看到很多活动,以表明事情正在发生:

启动推进器

目标完成 - 迷你总结

安装过程完成后,我们应该会发现 Node 依赖项已经安装到 jquery 目录中的一个名为 node_modules 的目录中。在这个文件夹中是 Grunt 针对这个特定项目所需要的任何其他文件。

为了测试一切,我们然后使用 grunt 命令运行 jQuery 的默认构建任务。此任务将执行以下操作:

  • 阅读所有 jQuery 源文件

  • 为任务的输出创建一个 /dist 目录

  • 构建 jquery.js 分发文件

  • 使用 jshint 对分发文件进行代码检查

  • 运行单元测试

  • 构建分发文件的源映射

  • 构建 jquery.min.js 分发文件

脚本文件应该是完整文件 230 KB,.min 文件为 81 KB,尽管随着 jQuery 版本号的增加,这些数字可能会有所不同。

构建自定义 jQuery

在这个任务中,我们将构建一个自定义版本的 jQuery,它不会包含构成 "完整" jQuery 的所有不同模块,这些模块会合并成一个文件,通常我们从 jQuery 站点下载,就像上一个任务结束时我们构建的文件一样,而是仅包含核心模块。

启动推进器

现在我们可以构建一个自定义版本的 jQuery。要构建一个精简版的 jQuery,省略所有非核心组件,我们可以在 CLI 中输入以下命令:

grunt custom:-ajax,-css,-deprecated,-dimensions,-effects,-offset

目标完成 - 迷你总结

一旦我们拥有源代码并配置好本地环境,我们就能够构建一个自定义版本的 jQuery,只包含核心组件,而省略了所有可选组件。

在这种情况下,我们排除了所有可选组件,但我们可以排除其中任何一个,或任意组合它们,以生成一个仅仅尽可能大的脚本文件。

如果此时检查 /dist 目录,我们应该会发现完整的脚本文件现在是 159 KB,而 .min 版本只有 57 KB,大约节省了文件大小的 30%;对于几分钟的工作来说,这还不错!

注意

项目功能或范围的变化可能需要重新构建源文件并包括以前排除的模块。一旦排除,就无法将可选模块添加到构建的文件中而不重新构建。

机密情报

随着 jQuery 的发展,特别是在 2.0 里程碑之后,越来越多的 jQuery 组件将被公开到构建工具作为可选组件,因此将有可能排除更广泛的库部分。

虽然在撰写时我们节省的文件大小可能会被我们的大多数访问者不会在其缓存中拥有我们的自定义版本的 jQuery 而需要下载的事实所抵消,但可能会有一天我们能够将文件大小缩小到这样的程度,以至于下载我们的超轻量级脚本文件仍然比从缓存中加载完整源文件更有效率。

使用 QUnit 运行单元测试

QUnit 是 jQuery 的官方测试套件,并包含在我们在项目早期从 Git 克隆的源代码中。如果我们在jquery文件夹内的测试文件夹中查找,我们应该会发现有很多单元测试,用于测试构成 jQuery 的不同组件。

我们可以针对 jQuery 的各个组件运行这些测试,以查看 QUnit 需要的环境,并查看使用它测试 JavaScript 文件有多容易。为此任务,我们需要安装一个 web 服务器和 PHP。

注意

有关 QUnit 的更多信息,请参阅qunitjs.com上的文档。

为起飞做好准备

Mac 开发者应该已经拥有运行 QUnit 所需的一切,因为 Mac 计算机已经预装了 Apache 和 PHP。然而,Windows 开发者可能需要做一些设置。

在这种情况下,web 服务器有两个选择,Apache 或者 IIS。两者都支持 PHP。那些希望使用 Apache 的开发者可以安装像WAMPWindows Apache Mysql PHP)这样的东西,以便安装和配置 Apache,并将 MySQL 和 PHP 安装为模块。

要下载并安装 WAMP,请访问 Wamp Server 网站的下载部分(www.wampserver.com/en/)。

选择适合您平台的安装程序并运行它。这应该会安装和配置一切必要的内容。

希望使用 IIS 的人可以通过控制面板中的程序和功能页面的添加/删除 Windows 组件区域安装它(在这种情况下需要 Windows 安装光盘),或者使用Web 平台安装程序WPI),可以从www.microsoft.com/web/downloads/platform.aspx下载。

下载并运行安装程序。一旦启动,搜索 IIS 并让应用程序安装它。安装完成后,也通过 WPI 搜索 PHP 并进行安装。

要使用 web 服务器和 PHP 运行 QUnit,你需要将项目文件夹内的jquery目录中的源文件复制到 web 服务器用于提供文件的目录中,或者配置 web 服务器以提供jquery目录中的文件。

在 Apache 上,我们可以通过编辑httpd.conf文件(在开始菜单中应该有一个条目)来配置默认目录(当浏览器请求时用于提供页面的目录)。向下阅读配置文件,直到找到默认目录的行,并更改它,使其指向项目文件夹中的jquery目录。

在 IIS 上,我们可以使用 IIS 管理器添加一个新网站。在左侧的连接窗格中右键单击站点,然后选择添加网站…。填写打开的对话框中的详细信息,我们就可以开始了。

启动推进器

要运行测试,我们只需要在浏览器中使用localhost:8080(或配置的任何主机名/端口号)访问/test目录:

localhost:8080/test

测试应该显示如下屏幕截图所示:

启动推进器

完成目标 - 小结

当在浏览器中访问测试套件的 URL 时,QUnit 将运行为 jQuery 编写的所有单元测试。目前对完整版本的 jQuery 有超过 6000 个测试,对所有可选模块都排除的自定义版本有约 4000 个测试。

你可能会发现一些测试失败。别担心,这是正常的,原因是我们从 Git 获取的默认 jQuery 版本将是最新的开发版本。就我写作时而言,当前版本的 jQuery 是 1.8.3,但从 Git 克隆的版本是 2.0.0pre。

要解决这个问题,我们可以切换到当前稳定分支,然后从那里进行构建。所以如果我想获取版本 1.8.3,我可以在 CLI 中使用以下命令:

git checkout 1.8.3

然后我们可以再次构建源码,运行 QUnit,所有测试应该都会通过。

注意

在检出 jQuery 源码的另一个版本后,我们需要在jquery目录中运行npm install来重新安装节点依赖项。

机密情报

单元测试并不总是被前端开发者严格遵循,但是一旦你的应用程序跨越了一定的规模和复杂度阈值,或者在团队环境中工作时,单元测试就变得对于维护至关重要,所以至少学习基础知识是最好的。

QUnit 使得编写 JavaScript 单元测试变得容易。它采用了围绕着用简单函数证明的断言概念的简单 API。QUnit 的 API 包括我们可以使用的方法来进行这些断言,包括:

  • equal()

  • notEqual()

  • ok()

这样可以轻松检查变量是否等于特定值,或者函数的返回值是否不等于特定值,等等。

在 QUnit 中,使用全局的 test() 方法构建测试,该方法接受两个参数:描述测试的字符串和执行测试的函数:

test("Test the return value of myCustomMethod()", function() {
    //test code here
});

在函数内部,我们可以使用一个或多个断言来检查我们正在测试的方法或函数执行的操作的结果:

var value = myCustomMethod();
equal(value, true, "This method should return true");

equal() 方法检查第一个和第二个参数是否相等,最后一个参数是描述我们期望发生的情况的字符串。

提示

如果打开 jquery/test/unit 目录中的一些脚本文件,可以很容易地看出如何构造测试。

QUnit 网站上的文档非常出色。它不仅清晰简洁地描述了 API,还提供了大量关于单元测试概念的信息,因此对于初学者来说是一个很好的起点。

在该网站上,您还可以找到在 Grunt 之外运行 QUnit 所需的源文件以及一个 HTML 模板页面,您可以在浏览器中运行测试套件。

任务完成

在这个任务中,我们不仅学会了如何通过排除不需要的组件来构建自定义版本的 jQuery,以及如何运行 jQuery 的单元测试套件,而且,也许更重要的是,我们学会了如何设置一个体面的构建环境,用于编写干净、无错的应用级 JavaScript。

你准备好了吗?挑战来了!

我们已经学会了如何构建我们自己的 jQuery,并排除了最大数量的组件,所以在撰写本文时,我们已经没有太多可以做的了。

如果您在 jQuery 1.9 版本发布后阅读本文,则可能会有更多的组件可以排除,或者其他构建 jQuery 的技术,因此,为了真正巩固您对构建过程的理解,请构建一个新的自定义构建,也排除任何新的可选组件。

如果没有任何新的可选组件,我建议您花些时间为您编写的任何自定义脚本编写 QUnit 测试。其思想是编写一个复制错误的测试。然后您可以修复错误并观察测试通过。

第八章:使用 jQuery 进行无限滚动

无限滚动是许多热门网站采用的一种技术,它最小化了页面最初加载的数据量,然后在用户滚动到页面底部时逐步加载更多数据。你可以在 Facebook 或 Twitter 的时间线上看到这种效果,等等。

任务简报

在本项目中,我们将使用 jQuery 构建一个无限滚动系统,模仿前述网站上看到的效果。我们将请求一些数据并在页面上显示它。一旦用户滚动到页面底部,我们将请求下一页的数据,依此类推,直到用户继续滚动。

一旦我们建立了无限滚动系统,我们应该得到类似以下截图的结果:

任务简报

为什么很棒?

如果您有大量数据要显示,并且它可以轻松按照时间顺序排列,那么使用无限滚动技术是最大程度地提高页面用户体验的简单方法,通过渐进式披露向用户逐渐展示更多内容。

首先可以显示一小部分数据,这样可以加快页面加载速度,同时防止您的访问者被大量数据所压倒,随着用户交互逐渐增加。

本项目将要消费的数据是 YouTube 上 TEDTalks 频道上传的视频列表,以 JSON 格式提供。

注意

请记住,JSON 是一种轻量级的基于文本的数据格式,非常适合在网络上进行传输。有关 JSON 的更多信息,请参阅 www.json.org/

在该频道上可以找到数千个视频,因此它是我们项目的一个很好的测试基础。按时间顺序排序的数据是一个无限滚动的绝佳基础。

注意

TEDTalks 频道可以直接在 YouTube 网站上查看,网址是 www.youtube.com/user/tedtalksdirector

您的热门目标

该项目将分解为以下任务:

  • 准备基础页面

  • 获取初始供稿

  • 显示初始结果集

  • 处理滚动到页面底部

任务清单

我们可以像在之前的一些示例中那样链接到 JsRender 的托管版本,但在这个项目中,我们将使用一个称为 imagesLoaded 的便捷小型 jQuery 插件,它允许我们在所选容器中的所有图像加载完成时触发回调函数。

imagesLoaded 插件可以从 github.com/desandro/imagesloaded 下载,并应保存在我们项目的 js 目录中。

准备基础页面

在此任务中,我们将设置我们在整个项目中要使用的文件,并准备我们的无限滚动页面的基础。

准备起飞

和往常一样,我们将为此项目使用自定义样式表和自定义脚本文件,所以让我们首先添加它们。创建一个名为infinite-scroller.js的新 JavaScript 文件,并将其保存在js目录中。然后创建一个名为infinite-scoller.css的新样式表,并将其保存在css目录中。最后,将template.html文件的副本保存在根项目文件夹中,并将其命名为infinite-scroller.html

启动推进器

示例页面使用的底层标记将是最小的 - 我们将使用的许多元素将由我们的模板动态生成,我们也可以在此任务中添加它们。

首先,我们应该将对新文件的引用添加到 HTML 页面中。首先,在infinite-scroller.html<head>中,直接在对common.css的链接之后添加一个<link>元素:

<link rel="stylesheet" href="css/infinite-scroller.css" />

接下来,我们可以链接到两个新的 JavaScript 文件。在 jQuery 之后直接添加以下<script>元素:

<script src="img/jsrender.js">
</script>
<scriptsrc="img/jquery.imagesloaded.min.js"></script>
<scriptsrc="img/infinite-scroller.js"></script>

我们还需要添加一个简单的容器来渲染我们的数据。将以下代码添加到页面的<body>中:

<div id="videoList"></div>

现在我们可以添加我们将要使用的模板了。在这个项目中,我们将使用两个模板 - 一个用于呈现外部容器和用户数据,它将被呈现一次,另一个用于呈现视频列表,我们可以根据需要重复使用。

与以前一样,它们将位于页面<body>中的<script>元素内。在现有的<script>元素之前,添加以下新模板:

<script id="containerTemplate" type="text/x-jsrender">
    <section>
        <header class="clearfix">
            <imgsrc="img/{{>avatar}}" alt="{{>name}}" />
            <hgroup>
                <h1>{{>name}}</h1>
                <h2>{{>summary.substring(19, 220)}}</h2>
            </hgroup>
        </header>
        <ul id="videos"></ul>
    </section>
</script>

现在轮到视频模板了:

<script id="videoTemplate" type="text/x-jsrender">
    <li>
        <article class="clearfix">
            <header>
                <a href="{{>content[5]}}" title="Watch video">
                    <imgsrc="img/{{>thumbnail.hqDefault}}" alt="{{>title}}" />
                </a>
                <cite>
                    <a href="{{>content[5]}}" 
                    title="Watch video">{{>title}}</a>
                </cite>
            </header>
            <p>
                {{>~Truncate(12, description)}}
                    <a class="button" href="{{>content[5]}}" 
                    title="Watch video">Watch video</a>
            </p>
            <div class="meta">
                <dl>
                    <dt>Duration:</dt>
                    <dd>{{>~FormatTime(duration)}}</dd>
                    <dt>Category:</dt>
                    <dd>{{>category}}</dd>
                    <dt>Comments:</dt>
                    <dd>{{>commentCount}}</dd>
                    <dt>Views:</dt>
                    <dd>{{>viewCount}}</dd>
                    <dt>Likes:</dt>
                    <dd>{{>likeCount}}</dd>
                </dl>
            </div>
        </article>
    </li>
</script>

现在我们也可以为这些元素添加样式了。在infinite-scroller.css中,添加以下选择器和规则:

section { width:960px; padding-top:20px; margin:auto; }
section { 
    width:960px; padding:2em 2.5em 0; 
    border-left:1px solid #ccc; border-right:1px solid #ccc; 
    margin:auto; background-color:#eee; 
}
section> header { 
    padding-bottom:2em; border-bottom:1px solid #ccc; 
}
img, hgroup, hgroup h1, hgroup h2 { float:left; }
hgroup { width:80%; }
headerimg { margin-right:2em; }
hgroup h1 { font-size:1.5em; }
hgroup h1, hgroup h2 { width:80%; }
hgroup h2 { 
    font-weight:normal; margin-bottom:0; font-size:1.25em;
    line-height:1.5em; 
}
ul { padding:0; }
li { 
    padding:2em 0; border-top:1px solid #fff; 
    border-bottom:1px solid #ccc; margin-bottom:0; 
    list-style-type:none; 
}
article header a { 
    display:block; width:27.5%; margin-right:2.5%; float:left; }
aimg { max-width:100%; }
article cite { 
    width:70%; margin-bottom:10px; float:left; 
    font-size:1.75em; 
}
article cite a { width:auto; margin-bottom:.5em; }
article p { 
    width:45%; padding-right:2.5%; 
    border-right:1px solid #ccc; margin:0 2.5% 2em 0;
    float:left; line-height:1.75em; 
}
article .button { display:block; width:90px; margin-top:1em; }
article dl { width:19%; float:left; }
article dt, article dd { 
    width:50%; float:left; font-size:1.15em; text-align:right; 
} 
article dt { margin:0 0 .5em; clear:both; font-weight:bold; }

li.loading{ height:100px; position:relative; }
li.loading span { 
    display:block; padding-top:3em; margin:-3em 0 0 -1em; 
    position:absolute; top:50%; left:50%; text-align:center;
    background:url(../img/ajax-loader.gif) no-repeat 50% 0; 
}

注意

此项目中使用的ajax-loader.gif图像可以在本书的附带代码下载中找到。

目标完成 - 小结

因此,实际上整个页面都是由我们添加到页面<body>中的模板构建的,除了一个空的<div>,它将为我们提供一个容器来渲染数据。该模板包含了用于视频列表的标记,以及用于显示视频作者信息的标记。

在第一个模板中,数据的外部容器是一个<section>元素。在其中是一个<header>,显示有关用户的信息,包括他/她的个人资料图片、姓名和简介。

YouTube 返回的实际简介可能相当长,因此我们将使用 JavaScript 的substring()函数返回此摘要的缩短版本。该函数传递两个参数;第一个是从哪个字符开始复制,第二个是结束字符。

在第二个模板中,实际的视频列表将显示在第一个模板中添加的<ul>元素中,每个视频占据一个<li>。在每个<li>内,我们有一个<article>元素,这是一个适当的独立内容单元的容器。

<article>中,我们有一个包含视频的一些关键信息的<header>,如标题和缩略图。在<header>之后,我们显示视频的简短摘要在<p>元素中。我们还使用我们的缩短帮助函数Truncate(),从第 12 个字符开始。

最后,我们使用<dl>显示关于视频的一些元信息,例如播放次数、点赞次数和视频的持续时间。

我们使用另一个辅助函数来显示视频中的持续时间,FormatTime()。YouTube 返回视频的长度(以秒为单位),所以我们可以将其转换为一个格式良好的时间字符串。

我们使用>字符来 HTML 编码我们插入到页面中的任何数据。这样做是为了安全考虑,始终是最佳选择。

添加的 CSS 纯粹是用于表现的;仅用于以列表格式布局页面,并使其看起来略有趣味和可呈现。请随意更改布局样式的任何方面,或者元素的主题。

机密情报

你们中注重 SEO 的人会意识到,一个几乎完全由 AJAX 传递的内容构建的页面不太可能在搜索结果中得到很好的位置。传统上,这几乎肯定是正确的,但现在我们可以使用 HTML History API 中令人惊叹的pushState()方法来提供一个完全可由搜索引擎索引的动态网站。

pushState()的完整描述超出了本书的范围,但有很多很好的示例和教程。被许多人认为是 History API 的权威指南的是 Mozilla 开发者网络上关于pushState()的文档,其中包括关于pushState()的部分。你可以在 developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history 上查看文档。

获取初始饲料

在这个任务中,我们将专注于获取初始数据集,以便在页面首次加载时创建页面。我们需要编写我们的代码,使得获取第一页数据的函数对于任何数据页都是可重用的,以便我们稍后在项目中可以使用它。

准备起飞

我们可以使用 jQuery 提供的标准document ready快捷方式,就像我们在许多之前的项目中所做的那样。我们可以通过将以下代码添加到我们之前创建的infinite-scroller.js文件中来做好准备:

$(function () {

    //rest of our code will go here...  

});

启动推进器

首先,我们可以添加从 YouTube 检索数据的代码。用以下内容替换前面代码段中的注释:

var data = {},
    startIndex = 1;

var getUser = function () {
    return $.getJSON("http://gdata.youtube.com/feeds/api/users/tedtalksdirector?callback=?", {
        v: 2,
        alt: "json"
    }, function (user) {
        data.userdata = user.entry;
    });
};

var getData = function () {
    return $.getJSON("https://gdata.youtube.com/feeds/api/videos?callback=?", {
        author: "tedtalksdirector",
        v: 2,
        alt: "jsonc",
        "start-index": startIndex
    }, function (videos) {
        data.videodata = videos.data.items;
    });
};

接下来,我们需要稍微处理一下响应。我们可以使用以下代码,在我们之前添加的代码之后直接添加,以执行回调函数,一旦两个 AJAX 请求都完成,就会执行该回调函数:

$.when(getUser(), getData()).done(function () {
    startIndex+=25;

    var ud = data.userdata,
        clean = {};

    clean.name = ud.yt$username.display;
    clean.avatar = ud.media$thumbnail.url;
    clean.summary = ud.summary.$t;
    data.userdata = clean;
});

目标完成 - 迷你总结

我们首先定义了几个变量。第一个是一个空对象,我们将用我们的 AJAX 请求的结果填充它。第二个是一个整数,表示我们希望获取的第一个视频的索引号。YouTube 视频不像常规的 JavaScript 数组那样从零开始,所以我们最初将变量定义为1

接下来,我们添加了我们将用于获取数据的两个函数。第一个是请求获取我们将要显示其 Feed 的用户的个人资料数据。我们只会在页面最初加载时使用此函数一次,但您将会看到为什么重要的是我们以这种方式将函数定义为变量。

第二个函数将被重用,因此将其存储在一个变量中是一个很好的方法,可以随时调用它以获取新的视频数据页面。重要的是这两个函数都返回getJSON()方法返回的jqXHR对象。

这两个请求都使用 jQuery 的getJSON()方法进行请求。在用户请求中,我们只需要设置valt查询参数,这些参数设置在传递给getJSON()的第二个参数中的对象中。我们想要获取其个人资料数据的用户实际上是我们正在进行请求的 URL 的一部分。

此请求的回调函数简单地将从请求接收到的user.entry对象的内容添加到我们的data对象的userdata属性中。

第二个请求需要稍微更多的配置。我们仍然使用v参数设置我们要使用的 API 版本,但这次我们将响应格式设置为jsonc而不是json。在此请求的回调函数中,我们将视频数组存储在我们的data对象的videodata属性中。

JSON-C 代表 json-in-script,是 Google 可以针对某些请求进行响应的格式。以 JSON-C 格式返回的数据通常比以 JSON 格式返回的相同响应更轻量级,更高效,这是由于 Google 的 API 已经进行了工程化。

当使用这种格式时,我们需要使用的属性只有在返回时才会返回。我们在请求用户数据时不使用它的唯一原因是因为该特定查询没有 JSON-C 响应。

有关从 Google 的 API 返回的 JSON-C 响应的更多信息,请参阅 developers.google.com/youtube/2.0/developers_guide_jsonc 上的文档。

接下来我们使用 jQuery 的when()方法来启动我们的两个请求,然后使用done()方法在两个jqXHR对象都已解析后执行回调函数。这就是为什么单独使用的getUser()函数以与可重用的getData()函数相同的方式结构化很重要的原因。

done()的回调函数内部,我们首先将startIndex变量增加 25,这样当我们发出另一个请求时,我们就会获得下一个包含 25 个视频的“页面”。现在我们已经有了第一页的数据,当我们稍后使用getData()函数时,我们将自动获得“下一页”的结果。

注意

when()done()方法是自 jQuery 1.5 以来处理异步操作的首选方法。

此时,我们只需要对我们的userdata对象进行一点处理。有一大堆我们不需要使用的数据,而我们需要使用的一些数据被埋在嵌套对象中,所以我们简单地创建一个名为clean的新对象,并直接在这个对象上设置我们需要的数据。

一旦完成了这个操作,我们就可以将我们的干净对象保存回我们的data对象,覆盖原始的userdata对象。这样做可以使对象在我们的模板中更容易处理。

显示初始结果集

现在我们已经从 YouTube 的 API 返回数据,我们可以渲染我们的模板了。然而,为了渲染我们的模板,我们需要添加用于格式化部分数据的辅助函数。在此任务中,我们可以添加这些辅助函数,然后渲染模板。

启动推进器

模板辅助函数不需要驻留在$.done()回调函数内部。我们可以直接在infinite-scroller.js中的此代码之前添加它们:

var truncate = function (start, summary) {
        return summary.substring(start,200) + "...";
    },
    formatTime = function (time) {
        var timeArr = [],
            hours = Math.floor(time / 3600),
            mins = Math.floor((time % 3600) / 60),
            secs= Math.floor(time % 60);

        if (hours> 0) {
            timeArr.push(hours);
        }

        if (mins< 10) {
            timeArr.push("0" + mins);
        } else {
            timeArr.push(mins);
        }

        if (secs< 10) {
            timeArr.push("0" + secs);
        } else {
            timeArr.push(secs);
        } 

        return timeArr.join(":");
    };

接下来,我们只需要注册这些辅助函数。在上一段代码后面直接添加以下内容:

$.views.helpers({
    Truncate: truncate, 
    FormatTime: formatTime
});

最后,我们可以渲染我们的模板。我们希望一个可以从代码的任何位置调用的函数,以备将来进行进一步的请求。在注册辅助函数后添加以下代码:

var renderer = function (renderOuter) {

    var vidList = $("#videoList");

    if (renderOuter) {
        vidList.append(
$("#containerTemplate").render(data.userdata));
    }
    vidList.find("#videos")
           .append($("#videoTemplate").render(data.videodata));
}

现在我们只需要在我们的$.done()回调函数的末尾调用这个函数:

renderer(true);

目标完成 - 小结

我们的第一个辅助函数,truncate()非常简单。我们只是返回该函数作为参数接收的字符串的缩短版本。substring()函数接受两个参数;第一个是在字符串中开始复制的位置,第二个参数是要复制的字符数,我们固定在200

为了显示字符串已经被缩短,我们还在返回的字符串末尾附加了一个省略号,这就是我们在这里使用辅助函数的原因,而不是像之前直接在模板中使用子字符串一样。

formatTime()辅助函数稍微复杂一些,但仍然相对简单。这个函数将接收以秒为单位的时间,我们希望将其格式化为稍微漂亮一些的字符串,显示小时(如果有的话)、分钟和秒。

我们首先创建一个空数组来存储字符串的不同组成部分。然后,我们创建一些变量来保存我们将要创建的时间字符串的小时、分钟和秒部分。

小时数通过将总秒数除以 3600(一小时的秒数)来计算。我们对其使用Math.floor(),以便只得到一个整数结果。我们需要稍微不同地计算分钟,因为我们需要考虑小时数。

在这里我们使用模数运算符(%)首先去除任何小时,然后将余数除以60,这将告诉我们总分钟数或在考虑小时后剩余的分钟数。要计算秒数,我们只需要再次使用模数运算符和值60

然后,我们使用一系列条件语句来确定要添加到数组中的变量。如果有任何小时数(这在视频的性质上是不太可能的),我们将它们推入数组中。

如果分钟数少于10,我们在分钟数前添加0,然后将其推入数组中。如果分钟数超过10,我们只需将mins变量推入数组中。在将其推入数组之前,对secs变量应用相同的逻辑。

这个函数通过将数组中的项目连接起来并使用冒号作为分隔符来返回一个格式良好的时间。字符串将以H:MM:SSMM:SS的格式呈现,具体取决于视频的长度。然后,我们使用 JsRender 的helpers对象向模板注册辅助函数,该对象本身嵌套在由模板库添加到 jQuery 的views对象中。我们希望添加的辅助函数被设置为对象文字中的值,其中键与模板中的函数调用匹配。

接下来,我们添加了一个函数,我们可以调用该函数来呈现我们的模板。renderer()函数接受一个布尔值参数,指定是否同时呈现容器模板和视频模板,或只呈现视频模板。在函数内部,我们首先缓存对视频列表的外部容器的引用。

如果renderOuter参数具有真值(也就是说,如果它具体保留了值true),我们就呈现containerTemplate并将其附加到页面的空<div>中。然后,我们呈现videoTemplate,将呈现的 HTML 附加到由containerTemplate添加的<ul>中。

最后,我们第一次调用我们的renderer()函数,将true作为参数传递,以同时呈现容器和初始视频列表。

处理滚动到页面底部

现在我们已经得到了第一页的视频,我们想添加一个处理程序,监视窗口的滚动事件,并检测页面是否已经滚动到底部。

启动推进器

首先,我们需要添加一些新的变量。修改文件顶部附近的第一组变量,使其显示如下:

var data = {},
    startIndex = 1,
    listHeight = 0,
    win = $(window),
    winHeight = win.height();

现在我们需要更新我们的renderer()函数,以便在模板被渲染后更新新的listHeight变量。在我们渲染videoTemplate后添加以下代码:

vidList.imagesLoaded(function () {
    listHeight = $("#videoList").height();
});

接下来,我们可以为滚动事件添加一个处理程序。在infinite-scroller.js中的when()方法后面,添加以下代码:

win.on("scroll", function () {

    if (win.scrollTop() + winHeight >= listHeight) {
        $("<li/>", {
            "class": "loading",
            html: "<span>Loading older videos...</span>"
        }).appendTo("#videos");

        $.when(getData()).done(function () {
            startIndex += 25;

            renderer();

            $("li.loading").remove();

        });
    }
}).on("resize", function() {
    winHeight = win.height();
});

我们正在使用一个旋转器来向用户显示正在检索更多数据的信息。我们需要一些额外的样式来处理旋转器的位置,所以我们也可以将以下代码添加到我们的infinite-scroller.css样式表的底部:

li.loading{ height:100px; position:relative; }
li.loading span { 
    display:block; padding-top:38px; margin:-25px 0 0 -16px; 
    position:absolute; top:50%; left:50%; text-align:center; 
    background:url(../img/ajax-loader.gif) no-repeat 50% 0;
}

目标完成 - 迷你总结

我们使用我们缓存的win对象和on()方法将处理程序附加到窗口。事件类型被指定为scroll。在回调函数内部,我们首先检查当前窗口的scrollTop属性加上视口的height是否大于或等于我们的videolist容器的height。我们需要这样做来知道页面何时滚动到底部。

如果两个高度相等,我们创建一个临时加载器,向用户提供视觉反馈,表明正在发生某些事情。我们将一个新的<li>元素附加到包含视频的<ul>中,并给它一个类名为loading,以便我们可以轻松地用一些 CSS 来定位它。我们将一个<span>元素设置为新列表项的内容。

我们可以使用 jQuery 的scrollTop()方法获取scrollTop属性的当前值。我们正在使用窗口height的缓存值。我们的滚动处理程序将相当密集,因为它将在用户滚动时被调用,因此使用窗口height的缓存值会使这个过程稍微更有效率一些。

但这意味着如果窗口被调整大小,这个值将不再准确。我们通过为窗口添加一个调整大小处理程序来解决这个问题,每当窗口调整大小时重新计算这个值。这是通过在滚动处理程序之后链接另一个对on()方法的调用来完成的,该方法查找window对象的调整大小事件,并相应地更新winHeight变量。

然后我们再次使用 jQuery 的when()方法,调用我们的getData()函数来检索下一个 25 个视频。我们还再次使用done()方法来在请求完成后执行回调函数。

在这个回调函数中,我们再次将我们的startIndex变量增加25,准备请求下一组视频。getData()函数将填充我们的data对象,新的视频数据,所以我们只需调用我们的renderer()函数来显示新的视频,然后移除临时加载器。

在这一点上,我们应该有一个完全功能的无限加载器,当用户滚动到页面底部时加载更多视频。当我们滚动到底部时,我们应该能够运行页面并看到类似以下的内容:

目标完成 - 迷你总结

任务完成

在这个项目中,我们编写的大部分代码都是关于获取我们想要显示的数据。实际上,添加无限滚动功能本身只需要一小部分代码 - 一个监视滚动事件并在文档滚动到底部时触发新数据请求的单个处理程序。

如你所见,这是一个非常容易作为附加层来修改现有功能的功能。这种技术最适合能够轻松按时间顺序排列的数据,新项目出现在顶部,旧项目出现在底部。

这并不一定是分页数据的完全替代,但在处理诸如新闻故事、博客文章、推文或状态更新等内容时,肯定是有意义的。它与社交数据配合得非常好。

你准备好大干一场了吗?一个高手挑战。

在这个项目中,我们只是为每个 YouTube 视频提供了回到全屏视频播放器的链接。所以,当访问者点击视频缩略图或标题时,他们将被送到 YouTube 实际观看视频。

虽然这样做并没有什么本质上的错,但更酷的做法是打开一个包含在<iframe>中嵌入的视频播放器的灯箱。这样访问者就可以在不离开您的网站的情况下观看视频。来自 YouTube 视频的响应包含一个可以用作<iframe>src属性的链接,那为什么不试试自己连接一下呢?

你会注意到,如果你滚动到页面底部,然后立即继续向下滚动,同一组视频将被多次请求。作为另一个任务,看看你是否可以通过仅在当前没有请求正在进行时才请求更多数据来防止这种情况发生。

这应该非常容易设置,只需在请求开始时设置一个标志,结束时删除标志。然后,只有在标志未被设置时才能发出请求。