jQuery2-开发秘籍-四-

70 阅读46分钟

jQuery2 开发秘籍(四)

原文:zh.annas-archive.org/md5/44BEA83CD04274AA076F60D831F59B04

译者:飞龙

协议:CC BY-NC-SA 4.0

第八章:理解插件开发

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

  • 创建一个插件模板

  • 创建一个工具提示插件

  • 构建内容和图像滑块插件

  • 创建一个 RSS 订阅阅读器插件

  • 从头开始编写一个图像裁剪插件

介绍

jQuery 插件允许开发人员编写可在任何 jQuery 项目中快速重用的可移植代码。作为本书的一部分,我们已经创建了许多功能,您可能希望在多个项目中使用。通过创建具有所需功能的 jQuery 插件,您可以抽象出这些功能的复杂性,并使其简单地包含在您需要的任何地方。

在开始本章之前,请创建一个名为chapter8的易于访问的目录。在此文件夹中,添加最新版本的 jQuery 库,该库将在本章中使用。

创建一个插件模板

多年来,创建 jQuery 插件已经变得非常流行,有许多关于插件创建最佳实践的文章和在线讨论。这些文章中的许多都深入讨论了如何创建一个插件模板,该模板可用作任何 jQuery 插件的起点。本配方将向您展示如何创建自己的 jQuery 插件模板,该模板将在本章中使用。

准备就绪

在前面创建的chapter8文件夹内,创建一个名为jquery.plugin-template.js的 JavaScript 文件。

如何做…

要创建一个基本的插件模板,该模板将成为本章中使用的所有插件的基础,请将以下代码添加到jquery.plugin-template.js中:

;(function ($) {

    var name = 'pluginName';
    Plugin.prototype = {
        defaults: {

        }
    };

    // The actual plugin constructor
    function Plugin(element, options) {
        var $scope = this;
        $scope.$element = $(element);
        $scope.element = element;
        $scope.options = $.extend({}, this.defaults, options);
        $scope.init = function () {

        }
    }

    $.fn[name] = function (options) {
        return this.each(function () {
            new Plugin(this, options).init();
        });
    }
})(jQuery);

它是如何工作的…

在 jQuery 网站上阅读插件文档(learn.jquery.com/plugins/basic-plugin-creation/) ,以查看一组指南和最佳实践。

在本配方中创建的插件使用简单的概念和最佳实践来创建一个轻量级的插件模板。 Addy Osmani 撰写了一篇很受欢迎的文章(coding.smashingmagazine.com/2011/10/11/essential-jquery-plugin-patterns/) ,其中提供了关于插件编写的深入见解,同时遵循这些推荐的最佳实践。

看看我们的插件模板,首先要注意的是文档开头的分号。这是为了确保任何之前包含的插件或脚本都已正确关闭。

为了符合 jQuery 的作者建议,整个插件被包裹在一个立即调用的函数表达式IIFE)中,以为插件提供范围。jQuery 作为本地变量$提供给 IIFE,以允许开发人员以通常的方式引用 jQuery 库而不会发生冲突。

在插件构造函数中,声明了一个$scope变量,以便清楚地表示插件的范围。然后将插件正在初始化的元素分配给插件的范围,以及任何提供的插件选项。使用 jQuery 的extend()函数将defaults对象与options对象合并,覆盖可能在options中提供的任何默认值。最后,将init()函数添加到插件的范围,这是您将放置插件初始化代码的地方,如下所示:

$.fn[name] = function (options) {
   return this.each(function () {
      new Plugin(this, options).init();
   });
}

上述代码使得插件可用,就像任何其他使用指定插件名称(($('.element').pluginName();)的 jQuery 对象方法一样。使用this.each(),它将为插件初始化的每个元素创建一个新的插件实例,并调用插件的init()函数。

创建一个提示框插件

提示框是向用户展示关于他们正在使用的 UI 的其他信息的一种流行方式。本步骤将向您展示如何创建自己的基本提示框插件,您可以在所有项目中轻松使用。

准备工作

复制jquery.plugin-template.js文件,并创建jquery.tooltip.js,它将成为此步骤的插件文件。在与插件文件和 jQuery 库相同的目录中创建recipe-2.htmlrecipe-2.js

如何做…

要创建一个简单的提示框插件和示例网页,请执行以下步骤:

  1. recipe-2.html中添加以下 HTML 代码,创建一个非常简单的网页,网页中的元素可以有一个提示框。

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 8 :: Recipe 2</title>
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.tooltip.js"></script>
        <script src="img/recipe-2.js"></script>
    </head>
    <body>
    <p><input type="text" class="hasTooltip" data-title="This is a tooltip on an input box" /></p>
    <p><a href="http://www.google.com/" target="_blank" class="hasTooltip" title="External link to http://www.google.com/">Google.com</a></p>
    <button class="hasTooltip" data-title="A button with a tooltip">Button</button>
    </body>
    </html>
    
  2. jquery.tooltip.js的顶部,更新name变量,并将插件默认设置更改如下:

    var name = 'tooltip';
    Plugin.prototype = {
    defaults: {
                'height': 30,
                'fadeInDelay': 200
    }
    };
    
  3. 使用以下代码更新$scope.init()函数:

    $scope.init = function() {
    $scope._text = (typeof $scope.$element.data('title') != "undefined") ? $scope.$element.data('title') : $scope.$element.prop("title");
                //Only display the tooltip if a title has been specified
                if (typeof $scope._text != "undefined") {
                    var $html = $("<div class='tooltip-frame'>"
                        +   "<div class='tooltip-arrow'></div>"
                        +   "<div class='tooltip-text'>" + $scope._text + "</div>"
                        + "</div>");
    
                    $html.css({
                        'position': 'absolute',
                        'text-align': 'center',
                        'height': $scope.options.height,
                        'line-height': $scope.options.height + "px",
                        'left': $scope.$element.offset().left + $scope.$element.outerWidth() + 15,
                        'top': $scope.$element.offset().top + ($scope.$element.outerHeight() / 2) - ($scope.options.height / 2),
                        'background-color': 'rgba(0, 0, 0, 0.81)',
                        'color': '#FFF',
                        'padding': '0 10px 0 10px',
                        'border-radius': '5px',
                        'opact': 'none'
                    }).find('.tooltip-arrow').css({
                            'width': 0,
                            'height': 0,
                            'border-top': '10px solid transparent',
                            'border-bottom': '10px solid transparent',
                            'border-right': '10px solid rgba(0, 0, 0, 0.81)',
                            'position': 'absolute',
                            'left': '-10px',
                            'top': (($scope.options.height / 2) - 10)
                        });
    
                    $scope.$element.on("mouseover", function(){
                        $html.fadeIn($scope.options.fadeInDelay);
                        $scope.$element.after($html);
                    }).on("mouseout", function(){
                        $html.remove();
                    });
                }
            }
    
  4. 将以下 jQuery 代码添加到recipe-2.js中,为所有具有hasTooltip类的 HTML 元素初始化提示框插件:

    $(function(){
        $('.hasTooltip').tooltip();
    });
    
  5. 在 Web 浏览器中打开recipe-2.html,将鼠标悬停在屏幕上的一个元素上,以查看提示框出现。

它是如何工作的…

作为此步骤的一部分创建的 HTML 页面仅用于提供可以附加提示框的一些元素。

对插件模板的第一个更改是设置默认设置。在这种情况下,我们设置了提示框的高度和淡入动画持续时间。您可以通过将这些功能添加到此处的默认设置中,引入自己的其他功能。

当为每个选定的元素初始化插件时,将调用init()函数,该函数包含此插件的大部分逻辑。

插件模板使得元素的“jQueryfied”版本通过$scope.$element可用。我们可以使用prop()data()函数来检查元素上是否指定了标题,并将其存储在$scope._text中,这将被用作提示框的文本。

然后将检查此变量,以确保有可用的文本来显示。如果没有文本,我们将不显示提示框。

如果 $scope._text 被定义,我们使用以下代码创建工具提示 HTML:

var $html = $("<div class='tooltip-frame'>"
       +   "<div class='tooltip-arrow'></div>"
       +   "<div class='tooltip-text'>" + $scope._text + "</div>"
       + "</div>");

var 语句很重要,以确保为每个选定的元素创建一个新的工具提示元素。通过将 HTML 代码包装在 $() 内,我们可以在将其插入到 DOM 中之前在此元素上使用 jQuery 函数。工具提示的 HTML 代码添加了标题文本并创建了一个将显示左箭头的元素。

使用 jQuery 的 css() 函数,一系列 CSS 样式被应用于新创建的 HTML 代码,以定位和样式化工具提示。工具提示的左侧和顶部位置是使用将显示工具提示的选定元素的偏移量、宽度和高度来计算的。请注意,使用 outerWidth()outerHeight() 函数而不是 width()/height() 函数,以包含填充和边框并返回尺寸。

jQuery 的 find() 函数也与 css() 函数一起使用,用于向左箭头添加样式。

最后,两个事件侦听器被附加到选定的元素上,以便当用户的鼠标移动到元素上时显示工具提示,并在用户的鼠标移出时移除工具提示。fadeIn() 函数从 defaults 对象中取得 duration 参数,当初始化工具提示插件时可以被覆盖。

要为所有具有 hasTooltip 类的元素初始化工具提示插件,将以下 jQuery 代码添加到 recipe-2.js

$(function(){
    $('.hasTooltip').tooltip();
});

在这里,你可以覆盖默认设置,例如,使用以下代码:

$(function(){
    $('.hasTooltip').tooltip({
       'height': 50,
          'fadeInDelay': 500              
    });
});

这还不是全部...

这个配方提供了一个非常基本的工具提示插件。你可以在此基础上扩展很多额外的功能,比如定位,并允许插件用户指定工具提示在哪个事件上打开。

构建内容和图片滑块插件

在第七章中,用户界面动画,你看到了如何使用 jQuery 创建一个简单的内容滑块。本配方将向你展示如何将该配方转换为一个可重用的 jQuery 插件,还可以向滑块添加图片。你不需要阅读前一个配方来完成这个,但建议你这样做,以便更好地理解代码的工作原理。

准备工作

复制 jquery.plugin-template.js 文件并将其重命名为 jquery.slider.js,它将成为此配方的插件。你还需要找到一张宽度为 600 像素、高度为 250 像素的图片,将其用于滑块。最后,在 jquery.slider.js 文件和 jQuery 库相同目录下创建 recipe-3.htmlslider.cssrecipe-3.js

如何做...

执行以下步骤来创建您的图片和内容滑块插件:

  1. 将以下 HTML 添加到 recipe-3.html

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 8 :: Recipe 3</title>
        <link href="slider.css" rel="stylesheet" type="text/css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.slider.js"></script>
        <script src="img/recipe-3.js"></script>
    </head>
    <body>
    <div class="mySlider">
        <div>Slider Content 1</div>
        <img src="img/british-countryside.jpg" />
        <div>Slider Content 3</div>
        <div>Slider Content 4</div>
    </div>
    </body>
    </html>
    
  2. jquery.slider.js 的顶部,将插件名称更新为 slider,并将默认设置如下:

    var name = 'slider';
    Plugin.prototype = {
       defaults: {
          width: 600,
          height: 250
    }
    };
    
  3. 更新插件的 $scope.init() 函数如下所示:

    $scope.init = function () {
    $scope.$element.addClass("slider-frame").css({
       width: $scope.options.width,
       height: $scope.options.height
    });
    $scope.$element.append('<ul class="slider-nav"></ul>');
    var _sliderItems = $scope.$element.find('div, img');
    _sliderItems.wrapAll("<div class='slider-content'></div>");
    $scope.$element.find('.slider-content').css({
       width: $scope.options.width * _sliderItems.length,
       position: 'relative'
    });
    _sliderItems.css({
       float: 'left',
       width: $scope.options.width,
       height: $scope.options.height
    });
    var _sliderNav = $scope.$element.find('.slider-nav');
    for (var i = 0; i < _sliderItems.length; i++) {
       _sliderNav.append("<li><a href='#" + i + "' " + ((i == 0) ? "class='active'" : "") + ">" + (i + 1) + "</a></li>");
    }
    _sliderNav.on("click", "li a", function(){
       var index = this.hash.replace("#", "");
       _sliderNav.find('li a').removeClass("active");
       $(this).addClass("active");
       $scope.$element.find('.slider-content').animate({
          left: -(index * $scope.options.width) + "px"
       });
    });
    }
    
  4. 将以下 jQuery 代码添加到 recipe-3.js 中以初始化滑块插件:

    $(function(){
        $('.mySlider').slider();
    });
    
  5. 将以下 CSS 代码添加到 slider.css 中:

    .slider-frame {
        overflow: hidden;
        position: relative;
        margin: auto;
        border: solid 1px #CCC;
    }
    .slider-nav {
        list-style: none;
        padding: 0;
        margin: 0;
        height: 35px;
        position: absolute;
        bottom: 0;
        left: 0;
        right: 0;
        text-align: center;
    }
    .slider-nav li {
        display: inline-block;
        margin-right: 5px;
    }
    .slider-nav li a {
        display: block;
        color: #FFF;
        text-decoration: none;
        border-radius: 30px;
        background-color: #333;
        width: 25px;
        height: 25px;
        text-align: center;
        line-height: 25px;
    }
    .slider-nav li a:hover {
        background-color: #000;
    }
    .slider-nav li a.active {
        background-color: #FFF;
        color: #333;
    }
    
  6. 在 Web 浏览器中打开 recipe-3.html,您将看到一个动态创建的图像和内容滑块。

工作原理…

HTML 页面设置了滑块插件所需的 HTML。有一个包含子项的容器分区,滑块插件将使用这些子项作为幻灯片。子项可以是分区元素或图像。

recipe-3.js 中的 jQuery 代码选择 mySlider 分区元素并初始化滑块插件。

我们之前创建的插件模板负责 jQuery 插件的设置。我们的滑块插件的功能放在 init() 函数中。在此函数的开头,将 slider-frame 类添加到选定的元素(.mySlider)中,以便它从 slider.css 样式表中继承一些基本样式。使用来自 options 对象的值,使用 jQuery css() 函数设置元素的宽度和高度,如下所示:

$scope.$element.addClass("slider-frame").css({
width: this.options.width,
height: this.options.height
});

之后,使用 $scope.$element.append('<ul class="slider-nav"></ul>'); 将空的无序列表插入到滑块中,该列表已准备好创建幻灯片导航。

代码的下一部分设置了动画的滑块。如在 第七章 用户界面动画创建动画内容滑块 配方中所解释的,滑块需要其容器的宽度为其幻灯片的组合宽度,以便幻灯片可以浮动在一起,并使用动画移动到视图中,如下面的代码所示:

var _sliderItems = $scope.$element.find('div, img');
_sliderItems.wrapAll("<div class='slider-content'></div>");
$scope.$element.find('.slider-content').css({
width: $scope.options.width * _sliderItems.length,
position: 'relative'
});

为此,选择滑块的子项(幻灯片),然后使用 jQuery wrapAll() 函数将其包装在一个分区元素中。该元素的宽度设置为幻灯片的个数乘以单个幻灯片的宽度。为了浮动每个幻灯片,使用 css() 函数设置 float 属性,如下面的代码所示:

_sliderItems.css({
   float: 'left',
   width: $scope.options.width,
   height: $scope.options.height
});

配置了每个幻灯片后,代码的下一步是为 slider-nav 无序列表元素添加每个幻灯片的列表项,以形成导航:

var _sliderNav = $scope.$element.find('.slider-nav');
for (var i = 0; i < _sliderItems.length; i++) {
   _sliderNav.append("<li><a href='#" + i + "' " + ((i == 0) ? "class='active'" : "") + ">" + (i + 1) + "</a></li>");
 }

插件的最后阶段是监听导航列表中锚元素的点击,代码如下,以允许用户使用此导航更改可见幻灯片:

_sliderNav.on("click", "li a", function(){
   var index = this.hash.replace("#", "");
   _sliderNav.find('li a').removeClass("active");
   $(this).addClass("active");
   $scope.$element.find('.slider-content').animate({
      left: -(index * $scope.options.width) + "px"
});
});

当用户点击链接时,使用 animate() 函数根据所选链接更改 slider-content 分区元素的左侧位置。在 第七章 用户界面动画创建动画内容滑块 配方中可以阅读更多相关信息。

还有更多…

要将流行的自动滑块效果添加到此插件,回顾一下 第七章 中的 创建一个带动画内容滑块 配方,用户界面动画

另请参阅

  • 创建一个带动画内容滑块的 配方在 第七章,用户界面动画

创建一个 RSS 阅读器插件

RSS 阅读器是许多网站非常受欢迎的附加组件。此配方将向您展示如何使用 Google Feed API 创建可配置的 feed 阅读器插件,从而使您可以轻松地在任何网站上重用该插件。

准备工作

再次复制 jquery.plugin-template.js 文件并将其重命名为 jquery.rssreader.js,以提供此配方插件的基础。在同一目录中,创建 recipe-4.jsrssreader.cssrecipe-4.html

如何实现…

要创建 RSS 阅读器插件,请执行以下步骤:

  1. 将以下 HTML 代码添加到 recipe-4.html 中,以创建一个基本的网页,并使 Google Feed API 可供页面内使用:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 8 :: Recipe 4</title>
        <link href="rssreader.css" rel="stylesheet" type="text/css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jsapi"></script>
        <script type="text/javascript">
            google.load("feeds", "1");
        </script>
        <script src="img/jquery.rssreader.js"></script>
        <script src="img/recipe-4.js"></script>
    </head>
    <body>
    <div class="myRSSContent"></div>
    </body>
    </html>
    
  2. 将以下 CSS 代码添加到 rssreader.css 中,以创建 RSS 阅读器的样式:

    @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400);
    .rssreader-frame {
        background-color: #333;
        border-radius: 5px;
        border: solid 1px #1f1f1f;
        padding: 0 10px 10px 10px;
        font-family: 'Source Sans Pro', sans-serif !important;
    }
    .rssreader-frame h1 {
        margin: 5px 0 5px 0;
        padding: 0;
        font-size: 22px;
        color: #FFF;
        line-height: 30px;
        font-weight: 200;
    }
    .rssreader-frame ul {
        margin: 0;
        padding: 0;
        list-style: none;
    }
    .rssreader-frame ul h4 {
        margin: 0;
        position: relative;
        font-weight: 200;
        color: #E1E1E1;
    }
    .rssreader-frame p.description {
        margin: 0 -10px 10px -10px;
        padding: 0 10px 10px 10px;
        color: #CCC;
        font-size: 12px;
        border-bottom: solid 1px #494949;
    }
    .rssreader-frame ul h4 a {
        line-height: 25px;
        margin-right: 110px;
        display: block;
        text-decoration: none;
        color: #8bd;
    }
    .rssreader-frame ul h4 .entry-date {
        width: 100px;
        position: absolute;
        right: 0;
        top: 0;
        height: 25px;
        line-height: 25px;
        text-align: right;
    }
    .rssreader-frame ul li p {
        color: #666;
        margin: 0 0 10px 0;
        padding: 0 0 10px 0;
        border-bottom: dotted 1px #494949;
    }
    
  3. jquery.rssreader.js 的顶部,更新 defaults 对象和 name 变量如下所示:

    var name = 'rssreader';
    Plugin.prototype = {
    defaults: {
        url: 'http://feeds.bbci.co.uk/news/technology/rss.xml',
        amount: 5,
        width: null,
        height: null
       }
    };
    
  4. 更新插件 init() 函数以包含以下代码:

            $scope.init = function () {
                $scope.$element.addClass("rssreader-frame");
                if ($scope.options.width != null) {
                   $scope.$element.width($scope.options.width);
                }
                var feed = new google.feeds.Feed($scope.options.url);
                feed.setNumEntries($scope.options.amount);
                feed.load(function(result) {
                    if (!result.error) {
                        var _title = $("<h1>" + result.feed.title + "</h1>");
                        var _description = $("<p class='description'>" + result.feed.description + "</p>");
                        var _feedList = $("<ul class='feed-list'></ul>");
                        for (var i = 0; i < result.feed.entries.length; i++) {
                            var entry = result.feed.entries[i];
                            var date = new Date(entry.publishedDate);
                            var dateString = date.getDate() + "/" + (date.getMonth() + 1) + "/" + date.getFullYear();
                            var _listElement = $("<li></li>");
                            _listElement.append("<h4><a href='" + entry.link + "'>" + entry.title + "</a><div class='entry-date'>" + dateString + "</div></h4>");
                            _listElement.append("<p>" + entry.content + "</p>");
                            _feedList.append(_listElement);
                        }
                        $scope.$element.append(_title);
                        $scope.$element.append(_description);
                        $scope.$element.append(_feedList);
                        if ($scope.options.height != null && (_feedList.outerHeight() + _title.outerHeight()) > $scope.options.height) {
                            _feedList.css({
                                'height': ($scope.options.height - _title.outerHeight()),
                                'overflow-y': 'scroll',
                                'padding-right': 10
                            });
                        }
                    }
                });
            }
    
  5. 将以下几行 jQuery 添加到 recipe-4.js 中,以为 myRSSContent 元素初始化插件:

    $(function(){
        $('.myRSSContent').rssreader({
            width: 400,
            height: 300
        });
    });
    
  6. 在 Web 浏览器中打开 recipe-4.html,您将看到以下 RSS 阅读器:如何实现…

它是如何工作的…

创建此配方的网页的 HTML 代码有一个用于初始化 RSS 阅读器插件的单个 division 元素,并作为 RSS 内容的容器。此外,Google Feed API 也被用于此页面,位于 jquery.rssreader.js 文件之前。使用 Google Feed API 意味着我们可以轻松创建一个插件,而不需要任何服务器端工作。这也使得插件很容易移植到任何网站上。在 developers.google.com/feed/v1/devguide#hiworld 上阅读更多关于此 API 的信息。

CSS 代码为插件内部创建的 RSS 阅读器元素设置样式。不需要进一步解释此代码。

与本章中的其他插件一样,模板负责插件设置,我们的插件功能位于 init() 函数内,该函数在插件初始化后执行一次。

此函数的第一部分将 rssreader-frame 类添加到所选元素中,CSS 代码使用该类应用各种样式。然后,查看 options 变量,如果已提供,则在所选元素上设置宽度。

使用 Google Feed API,使用options对象的URLamount值配置了反馈请求,如下所示。这将告诉 API 在哪里收集 RSS 内容以及要返回多少个项目。

var feed = new google.feeds.Feed($scope.options.url);
feed.setNumEntries($scope.options.amount);

之后,使用load()函数进行请求,并指定回调函数,如下所示:

feed.load(function(result) {
if (!result.error) {
// -- HIDDEN CODE
}
}

如果没有发生错误,则创建标题、描述和无序列表元素,并将它们存储在本地变量中,如以下代码所示:

var _title = $("<h1>" + result.feed.title + "</h1>");
var _description = $("<p class='description'>" + result.feed.description + "</p>");
var _feedList = $("<ul class='feed-list'></ul>");

使用result.feed对象,可以提取用于放置在这些元素中的反馈标题和描述。这些元素被创建并包裹在 jQuery 选择器($())内,以便 jQuery 的函数可以在稍后对这些元素进行操作。

然后我们循环遍历每个条目,并为每个条目创建一个列表项。在每个列表项内,我们添加了反馈内容、日期、标题和链接。使用 JavaScript 的Date()函数,创建一个更易读的日期以插入到 DOM 中。要将每个元素添加到先前创建的无序列表元素中,使用了_feedList.append(_listElement);

标题、描述和现在已完全填充了 RSS 内容的列表可以使用以下代码插入到 DOM 中:

$scope.$element.append(_title);
$scope.$element.append(_description);
$scope.$element.append(_feedList);

最后,使用以下代码来为 RSS 订阅阅读器应用任何指定的高度,并在内容过大无法适应指定高度时添加滚动条:

if ($scope.options.height != null && (_feedList.outerHeight() + _title.outerHeight()) > $scope.options.height) {
   _feedList.css({
   'height': ($scope.options.height - _title.outerHeight()),
   'overflow-y': 'scroll',
   'padding-right': 10
});
}

另请参阅

  • 第六章中的创建新闻滚动条示例,用户界面

从头开始编写图像裁剪插件

当允许用户上传自己的图像时,无论是用于个人资料图片还是其他用途,让他们能够在浏览器中裁剪图像为用户提供了巨大的好处。这是因为大多数用户不会知道如何使用诸如 Photoshop 之类的第三方应用程序来更改图像。Internet 上有许多免费的图像裁剪插件和许多教程可以帮助您使用它们,但几乎没有提供完整解决方案的示例。本篇将向您展示如何从零开始创建自己的图像裁剪插件,如何将图像上传到 Web 服务器,并如何从图像裁剪器获取数据以按照用户的规范调整并保存图像。

准备就绪

由于此示例包含客户端和服务器端代码,因此请确保您仔细遵循每个步骤。在开始此示例之前,请在 Web 服务器的 Web 根目录中设置以下目录结构:

准备就绪

根据上述结构,您需要在您的 Web 根目录(前图中的www)下创建includesuploads文件夹。在includes文件夹中,保存 jQuery 库并创建以下四个文件:

  • imagecrop.css

  • jquery.imagecrop.js(像以前一样复制jquery.plugin-template.js文件以创建此插件的基础)

  • recipe-5.css

  • recipe-5.js

在 Web 根目录中,您需要创建 index.htmlupload.php 文件。

注意

本示例将 不会 在 IE9 或更低版本中工作,因为较旧的浏览器不支持 XMLHttpRequestFormDataFileReader API。

如何做…

仔细按照以下每个步骤,然后阅读 工作原理… 部分,以充分理解插件及其相关代码:

  1. 将以下 HTML 代码添加到 index.html 中,以创建一个带有图像上传表单的 Web 页面:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 8 :: Recipe 5 - Image Crop Plugin</title>
        <link href="includes/imagecrop.css" rel="stylesheet" type="text/css" />
        <link href="includes/recipe-5.css" rel="stylesheet" type="text/css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.imagecrop.js"></script>
        <script src="img/recipe-5.js"></script>
    </head>
    <body>
        <div class="container">
            <h3>#1: Select Image</h3>
            <input type="file" id="selectedImage" />
            <h3>#2: Crop Image</h3>
            <div class="image-preview">
                <div class="preview-msg">Select and image to upload</div>
                <img id="croppable-image" style="display: none;" />
            </div>
            <h3>#3: Upload</h3>
            <div class="progress-bar"><div class="inner"></div></div>
            <div class="actions">
                <button class="upload-button">Upload</button>
            </div>
        </div>
    </body>
    </html>
    
  2. 将以下 CSS 代码放入 recipe-5.css 中,为您刚刚创建的 HTML 页面和表单添加样式:

    @import url(http://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400);
    body {
        background-color: #F1F1F1;
        font-family: 'Source Sans Pro', sans-serif !important;
    }
    h1, h2, h3 {
        font-weight: 300;
        margin: 0;
    }
    .container {
        width: 800px;
        margin: 50px auto auto auto;
        background-color: #FFFFFF;
        padding: 20px;
        border: solid 1px #E1E1E1;
    }
    .container h3 {
        line-height: 40px;
    }
    .container .image-preview {
        border: solid 1px #E1E1E1;
        width: 800px;
        height: 600px;
        overflow: hidden;
        margin: auto;
        position: relative;
    }
    .container .image-preview .preview-msg {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background-color: #F1F1F1;
        text-align: center;
        font-size: 22px;
        line-height: 600px;
        font-weight: 300;
        z-index: 1;
    }
    #croppable-image {
        position: relative;
        z-index: 2;
    }
    .container .progress-bar {
        height: 30px;
        border: solid 1px #E1E1E1;
    }
    .container .progress-bar .inner {
        height: 30px;
        width: 0;
        background-color: #54ee86;
    }
    .container .actions {
        text-align: right;
        margin-top: 10px;
    }
    .container .actions .upload-button {
        height: 30px;
        width: 60px;
    }
    
  3. 将以下 jQuery 代码添加到 recipe-5.js 中,该代码将允许用户从其本地文件系统中选择并预览图像,然后启动图像裁剪插件:

    $(function(){
        var _selectedFile;
        $(document).on("change", "#selectedImage", function(){
            var reader = new FileReader();
            var files = $(this).prop("files");
            if (files.length > 0) {
                _selectedFile = files[0];
                reader.onload = function() {
                    var image = new Image;
                    image.src = this.result;
                    if (image.width > 800 || image.height > 600) {
                        alert("Image cannot be larger that 800x600");
                    } else {
                        $('.preview-msg').hide();
                        $('#croppable-image').prop("src", this.result).fadeIn().imagecrop();
                    }
                };
                reader.readAsDataURL(_selectedFile);
            }
        });
        $(document).on("click", ".upload-button", function(){
            var _selectedImage = $('#croppable-image');
            if (_selectedImage.data("selection-width") > 0 && _selectedImage.data("selection-height") > 0) {
                var data = new FormData();
                data.append("image", _selectedFile);
                data.append("selection-width", _selectedImage.data("selection-width"));
                data.append("selection-height", _selectedImage.data("selection-height"));
                data.append("selection-left", _selectedImage.data("selection-x"));
                data.append("selection-top", _selectedImage.data("selection-y"));
                var xhr = new XMLHttpRequest();
                xhr.open("POST", "/upload.php");
                xhr.onprogress = function(event) {
                    var percent = (event.loaded / event.total * 100);
                    $('.progress-bar .inner').width(percent + "%");
                }
                xhr.onload = function() {
                    var response = JSON.parse(this.response);
                    if (response.success == false) {
                        alert(response.error);
                    }
                }
                xhr.send(data);
            } else {
                alert("Please crop the image before upload");
            }
        });
    });
    
  4. jquery.imagecrop.js 中,按照以下代码片段的示例,更新插件名称和默认值:

    var name = 'imagecrop';
        Plugin.prototype = {
            defaults: {
                minWidth: 100,
                minHeight: 100
       }
    };
    
  5. 在由插件模板文件创建的插件构造函数中,在声明 $scope.options 之后直接添加以下声明,如下面的代码片段所示:

    $scope.options = $.extend({}, this.defaults, options);
    $scope.imageSelection = {
       start: {
          x: 0,
          y: 0
       },
       end: {
          x: 0,
          y: 0
       },
       top: 0,
       left: 0
    };
    var _frame;
    var _overlayLayer;
    var _selectionLayer;
    var _selectionOutline;
    
  6. 更新插件 $scope.init() 函数,包括以下代码:

    //Has this element already been initialised?
    if (typeof $scope.$element.data("selection-x") != "undefined") {
       //Yes, so reuse the DOM elements...
       _frame = $(document).find('.crop-frame').css({
          width: $scope.$element.width(),
          height: $scope.$element.height()
       });
          _overlayLayer = $(document).find('.overlay-layer');
          _selectionLayer = $(document).find('.selection-layer');
          _selectionOutline = $(document).find('.selection-outline');
    } else {
       //No, let's initialise then...
       _frame = $("<div class='crop-frame'></div>").css({
          width: $scope.$element.width(),
          height: $scope.$element.height()
       });
       _overlayLayer = $("<div class='overlay-layer'></div>");
       _selectionLayer = $("<div class='selection-layer'></div>");
       _selectionOutline = $("<div class='selection-outline'></div>");
       //Wrap the image with the frame
       $scope.$element.wrap(_frame);
       _overlayLayer.insertAfter($scope.$element);
       _selectionLayer.insertAfter($scope.$element);
       _selectionOutline.insertAfter($scope.$element);
       /** EVENTS **/
       _selectionLayer.on("mousedown", $scope.onSelectionStart);
       _selectionLayer.on("mouseup", $scope.onSelectionEnd);
       _selectionOutline.on("mouseup", $scope.onSelectionEnd); 
       _selectionOutline.on("mousedown", $scope.onSelectionMove);
    }
    $scope.updateElementData();
    /** UPDATE THE OUTLINE BACKGROUND **/
    _selectionOutline.css({
       'background': 'url(' + $scope.$element.prop("src") + ')',
       'display': 'none'
    });
    
  7. $scope.init() 函数之后,添加以下额外的函数:

    /**
    * MAKING THE SELECTION
    */
    $scope.onSelectionStart = function(event) {
       $scope.imageSelection.start = $scope.getMousePosition(event);
       _selectionLayer.bind({
         mousemove: function(event) {
       $scope.imageSelection.end = $scope.getMousePosition(event);
       $scope.drawSelection();
        }
      });
    };
    $scope.onSelectionEnd = function() {
       _selectionLayer.unbind("mousemove");
       //Hide the element if it doesn't not meet the minimum specified dimensions
       if (
          $scope.getSelectionDimentions().width < $scope.options.minWidth || $scope.getSelectionDimentions().height < $scope.options.minHeight
    ) {
          _selectionOutline.hide();
       }
       _selectionOutline.css({
          'z-index': 1001
       });
    };
    $scope.drawSelection = function() {
       _selectionOutline.show();
       //The smallest top value and the smallest left value are used to set the position of the element
       $scope.imageSelection.top = ($scope.imageSelection.end.y < $scope.imageSelection.start.y) ? $scope.imageSelection.end.y : $scope.imageSelection.start.y;
    $scope.imageSelection.left = ($scope.imageSelection.end.x < $scope.imageSelection.start.x) ? $scope.imageSelection.end.x : $scope.imageSelection.start.x;
    _selectionOutline.css({
       position: 'absolute',
       top: $scope.imageSelection.top,
       left: $scope.imageSelection.left,
       width: $scope.getSelectionDimentions().width,
       height: $scope.getSelectionDimentions().height,
       'background-position': '-' + $scope.imageSelection.left + 'px -' + $scope.imageSelection.top + 'px'
    });
    $scope.updateElementData();
    };
       /**
    * MOVING THE SELECTION
    */
    $scope.onSelectionMove = function() {
       //Prevent trigger the selection events
       _selectionOutline.addClass('dragging');
       _selectionOutline.on("mousemove mouseout", function(event){
          if ($(this).hasClass("dragging")) {
             var left = ($scope.getMousePosition(event).x - ($(this).width() / 2));
            //Don't allow the draggable element to over the parent's left and right
            if (left < 0) left = 0;
            if ((left + $(this).width()) > _selectionLayer.width()) left = (_selectionLayer.width() - $(this).outerWidth());
            var top = ($scope.getMousePosition(event).y - ($(this).height() / 2));
            //Don't allow the draggable element to go over the parent's top and bottom
            if (top < 0) top = 0;
            if ((top + $(this).height()) > _selectionLayer.height()) top = (_selectionLayer.height() - $(this).outerHeight());
            $scope.imageSelection.left = left;
            $scope.imageSelection.top = top;
            //Set new position
            $(this).css({
               top: $scope.imageSelection.top,
               left: $scope.imageSelection.left,
               'background-position': '-' + $scope.imageSelection.left + 'px -' + $scope.imageSelection.top + 'px'
            });
       }
       }).on("mouseup", function(){
       $(this).removeClass('dragging');                $scope.updateElementData();
       });
    }
    
  8. 在您添加的函数下方插入以下辅助函数:

    /**
    * HELPER FUNCTIONS
    */
    $scope.getMousePosition = function(event) {
       return {
          y: (event.pageY - _selectionLayer.offset().top),
          x: (event.pageX - _selectionLayer.offset().left)
       };
    };
    $scope.getSelectionDimentions = function() {
       //Work out the width and height based on the start and end positions
       var width = ($scope.imageSelection.end.x - $scope.imageSelection.start.x);
       var height = ($scope.imageSelection.end.y - $scope.imageSelection.start.y);
       //If any negatives turn them into positives
       if (height < 0) height = (height * -1);
       if (width < 0) width = (width * -1);
       return {
          width: width,
          height: height,
          x: $scope.imageSelection.start.x,
          y: $scope.imageSelection.start.y
       };
    }
    $scope.updateElementData = function() {
        $scope.$element.data({
          "selection-x": $scope.imageSelection.left,
          "selection-y": $scope.imageSelection.top,
          "selection-width": $scope.getSelectionDimentions().width,
          "selection-height": $scope.getSelectionDimentions().height
       });
    }
    
  9. 将以下 CSS 代码添加到 imagecrop.css 中,为图像裁剪插件创建的元素添加样式:

    .crop-frame {
        position: relative;
        margin: auto;
    }
    .selection-layer {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 1000;
    }
    .selection-outline {
        border: dotted 1px #000000;
        z-index: 999;
    }
    .selection-outline:hover, .selection-outline:active {
        cursor: move;
    }
    .overlay-layer {
        background-color: rgba(255, 255, 255, 0.60);
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        z-index: 998;
    }
    
  10. 最后,将以下 PHP 代码添加到 upload.php 中,该代码将从您刚刚创建的 Web 表单中获取数据,然后裁剪图像并将其保存到 uploads 目录中:

    <?php
    if (isset($_FILES['image'])) {
        $response = array(
            "success" => false,
            "error" => ""
        );
        //GET SELECTION DATA
        $selectionWidth = (isset($_POST['selection-width'])) ? $_POST['selection-width'] : 0;
        $selectionHeight = (isset($_POST['selection-height'])) ? $_POST['selection-height'] : 0;
        $selectionTop = (isset($_POST['selection-top'])) ? $_POST['selection-top'] : 0;
        $selectionLeft = (isset($_POST['selection-left'])) ? $_POST['selection-left'] : 0;
        //GET IMAGE DATA
        $fileName = $_FILES['image']['name'];
        $ext = pathinfo($fileName, PATHINFO_EXTENSION);
        if ($selectionWidth > 800 || $selectionHeight > 600) {
            $response["error"] = "Image cannot be larger than 800 x 600";
        } else if (!in_array($ext, array("png", "jpg"))) {
            $response["error"] = "Invalid file type";
        } else {
    if ($ext == "png") {
    $source = imagecreatefrompng($_FILES['image']['tmp_name']);
            } else {
    $source = imagecreatefromjpeg($_FILES['image']['tmp_name']);
            }        $dest = imagecreatetruecolor($selectionWidth, $selectionHeight);
    imagecopyresampled($dest, $source, 0, 0, $selectionLeft, $selectionTop, $selectionWidth, $selectionHeight, $selectionWidth, $selectionHeight);
            $path = "/uploads/";
            if (!imagejpeg($dest, getcwd() . $path . $fileName, 100)) {
                $response["error"] = "Could not save uploaded file";
            } else {
                $response["success"] = true;
            }
        }
        header("Content-Type: application/json; charset=UTF-8");
        echo json_encode($response);
    }
    
  11. 在您的 Web 浏览器中导航到 index.html 文件,您将看到一个包含三个步骤的简单 Web 表单。通过选择 选择文件 按钮并从计算机中选择图像,您将看到图像显示在预览框内。在预览框中,您可以点击并拖动一个选择区域到图像上。完成后,点击 上传 将图像上传到 Web 服务器(通过进度条指示),并且图像将被裁剪并保存到您之前创建的 uploads 文件夹中。

工作原理…

了解本示例的不同部分非常重要。本示例的第一个元素是上传表单本身,在上传之前,它提供了在浏览器中查看用户选择的图像的功能。本示例的第二个元素是图像裁剪插件本身,这是我们将重点关注的内容。最后,为了提供完整的解决方案,本示例的上传元素接收图像裁剪插件提供的数据,并将其发布到 PHP 脚本。然后,该 PHP 脚本将获取这些数据进行裁剪,并将图像保存到用户指定的位置。

图像选择和预览

index.html 中的 HTML 代码创建了一个带有文件输入元素的基本界面。当用户点击 选择文件 按钮时,将会打开浏览窗口,允许他们从计算机中选择文件。使用 JavaScript 的 FileReader 类,我们可以读取此文件并在浏览器中显示它。查看 recipe-5.js,您将看到一个包含执行此操作的代码的 change 事件处理程序。

在代码中的这一点上,有一个基本的验证检查,以确保所选图片不大于 800 x 600 像素。如果是,则向用户显示警报,并且图片不会加载。

图片加载完成后,#cropableImage 元素的 source 属性被更新为所选图片,将其显示在屏幕上。最后,在图片元素上初始化了图片裁剪插件,如下所示:

$('#croppable-image').prop("src", this.result).fadeIn().imagecrop();

图片裁剪插件

图片裁剪插件动态创建了一系列元素,充当图层和容器,允许我们让用户进行选择。为了更容易理解每个图层的作用,它们在下图中进行了说明:

图片裁剪插件

遮罩 层用白色背景和 0.6 的不透明度淡化了大部分图片。选择 层是监听鼠标事件的层,指示用户正在进行选择。这样做的主要原因是,如果将鼠标事件附加到图片本身,我们将在某些允许您将图片拖动到一个带有图片的可视化表示的浏览器中遇到困难,这会妨碍我们的功能。选择轮廓 层是插件在用户进行选择时绘制的内容。其背景是所选图片,除了位置被调整以仅显示已选择的图片部分,提供对遮罩遮挡的原始图片的聚焦。

插件初始化时,有一组局部变量和默认值声明,插件将在其运行过程中使用;这些显示在以下代码片段中:

$scope.imageSelection = {
start: {
   x: 0,
   y: 0
},
end: {
   x: 0,
   y: 0
},
top: 0,
left: 0
};
var _frame;
var _overlayLayer;
var _selectionLayer;
var _selectionOutline;

var 开头的变量将存储代表图层的不同 DOM 元素。imageSelection 对象存储用户的初始点击坐标,然后是用户完成选择时的坐标。然后,我们可以使用这些坐标来计算选择的宽度和位置。topleft 参数存储了选择的最终坐标,一旦宽度和高度已经计算出来。

在插件的 init() 函数内部,有一个初始检查以确定图片是否已初始化。如果是,则图层 DOM 元素已经被创建并插入,如下所示:

if (typeof $scope.$element.data("selection-x") != "undefined") {
   // -- HIDDEN CODE
} else {
   // -- HIDDEN CODE
}

如果 DOM 元素可用,则使用 jQuery 的find()函数选择元素并将它们存储在关联变量中。如果没有,则创建并存储。可能已为图像初始化插件的一种场景是用户决定更改所选图像。图像源发生变化,但 DOM 元素可以保持原位并以不同的尺寸重用。

当图层元素首次创建时,会创建一个容器分隔元素,其类名为crop-frame,尺寸与所选图像相同,如下面的代码片段所示:

_frame = $("<div class='crop-frame'></div>").css({
    width: $scope.$element.width(),
    height: $scope.$element.height()
});

用户选择必须精确匹配实际图像像素尺寸,否则裁剪计算将不正确。然后,选定的图像元素将使用 jQuery 的wrap()函数包装在此框架内,如下所示:

$scope.$element.wrap(_frame);
_overlayLayer.insertAfter($scope.$element);
_selectionLayer.insertAfter($scope.$element);
_selectionOutline.insertAfter($scope.$element); 

其他创建的图层插入到所选图像元素之后,位于crop-frame分隔元素内,如上面的代码所示。

图层创建的最后一部分附加了各种处理选择过程不同部分的事件处理程序函数:

_selectionLayer.on("mousedown", $scope.onSelectionStart);
_selectionLayer.on("mouseup", $scope.onSelectionEnd);
_selectionOutline.on("mouseup", $scope.onSelectionEnd);
_selectionOutline.on("mousedown", $scope.onSelectionMove);

这里指定的每个函数稍后在plugin类中声明。在init()函数的末尾,调用updateElementData()函数,该函数设置所选图像元素上的初始选择尺寸(例如,selection-x)并在选择轮廓图层上设置背景图像。

当用户首次单击选择图层时,鼠标位置将被存储为起始坐标。然后,当用户拖动鼠标进行选择时,新的鼠标坐标被存储为结束坐标,并调用drawSelection()函数。drawSelection()函数使用起始和结束坐标来计算选择的宽度和高度,并更新选择轮廓图层的 CSS 以显示此内容,如下所示:

$scope.drawSelection = function() {
   _selectionOutline.show();
   //The smallest top value and the smallest left value are used to set the position of the element
$scope.imageSelection.top = ($scope.imageSelection.end.y < $scope.imageSelection.start.y) ? $scope.imageSelection.end.y : $scope.imageSelection.start.y;
$scope.imageSelection.left = ($scope.imageSelection.end.x < $scope.imageSelection.start.x) ? $scope.imageSelection.end.x : $scope.imageSelection.start.x;
_selectionOutline.css({
   position: 'absolute',
   top: $scope.imageSelection.top,
   left: $scope.imageSelection.left,
   width: $scope.getSelectionDimentions().width,
   height: $scope.getSelectionDimentions().height,
   'background-position': '-' + $scope.imageSelection.left + 'px -' + $scope.imageSelection.top + 'px'
});
$scope.updateElementData();
};

作为此函数的一部分,选择轮廓图层的背景位置将被更新以显示实际选择,并调用updateElementData()函数以将新的选择数据应用于所选图像。

当用户完成选择并释放鼠标按钮时,将调用onSelectionEnd()函数。此函数确定选择是否小于允许的最小值;如果是,则隐藏选择。将鼠标移动事件从选择图层解绑,以避免与后续功能发生冲突,并更新选择轮廓图层的z-index属性,以便选择轮廓图层移动到选择图层上方,从而实现拖动功能。拖动功能在第六章用户界面中的创建基本拖放功能配方中进行了详细介绍。有关详细说明,请参阅该配方。

图像上传

recipe-5.js 中,为 上传 按钮的点击事件附加了事件处理程序。在此事件的回调函数内,首先确定用户是否已经进行了选择。如果没有,则显示警告,要求用户进行裁剪选择。

如果已经进行了有效的选择,将创建一个新的 FormData 对象来存储要上传到 PHP 脚本的数据,如下所示:

var data = new FormData();
data.append("image", _selectedFile);
data.append("selection-width", _selectedImage.data("selection-width"));
data.append("selection-height", _selectedImage.data("selection-height"));
data.append("selection-left", _selectedImage.data("selection-x"));
data.append("selection-top", _selectedImage.data("selection-y"));

_selectedFile 变量包含对所选文件的引用,在文件输入的更改事件中可用。

将所需数据存储在 FormData 对象中后,创建一个新的 XMLHttpRequest 对象来将数据发送到 PHP 上传脚本,如下代码片段所示:

var xhr = new XMLHttpRequest();
xhr.open("POST", "/upload.php");
xhr.onprogress = function(event) {
   var percent = (event.loaded / event.total * 100);
   $('.progress-bar .inner').width(percent + "%");
}
xhr.onload = function() {
   var response = JSON.parse(this.response);
   if (response.success == false) {
      alert(response.error);
}
}
xhr.send(data);

此代码不言自明,简单地允许我们直接从 JavaScript 中进行 POST,无需 HTML 表单。 onprogress() 函数由 XHR 请求调用,当图像正在上传时允许我们更新 HTML 页面上的进度条以反映上传进度。 onload() 函数在操作完成时调用,允许我们显示任何发生的错误。

使用 PHP 进行裁剪和保存图像

PHP 脚本相对简单。它接受并存储通过 JavaScript 提供的 POST 请求中的信息,并对图像宽度和扩展名进行基本验证,仅允许 JPG 和 PNG 图像。

如果图像通过了验证,则根据提供的图像使用 imagecreatefrompng()imagecreatefromjpeg() 在 PHP 中创建图像资源。然后,如下所示的代码行创建了一个具有指定裁剪尺寸的空白图像:

$dest = imagecreatetruecolor($selectionWidth, $selectionHeight);

你可以将这个空白图像看作是 PHP 将用来在上面绘制修改后图像的画布。然后,提供的图像被裁剪,并且使用 imagecopyresampled() 将新图像存储在空白画布上,如下所示:

imagecopyresampled($dest, $source, 0, 0, $selectionLeft, $selectionTop, $selectionWidth, $selectionHeight, $selectionWidth, $selectionHeight);

最后,新图像将保存到在此配方开始时创建的 uploads 目录中,如下所示:

imagejpeg($dest, getcwd() . $path . $fileName, 100)

当你打开 uploads 目录时,你应该能看到新图像。

还有更多...

本配方提供了一个基本的完整解决方案,用于预览、裁剪、上传和保存图像,但还有许多可以改进的地方。客户端和服务器端的验证都可以进行大幅改进,以允许其他图像类型,并检查文件大小以及尺寸。

FileReader 正在将本地文件读入浏览器时,可以像为上传部分实现进度条一样添加加载器或进度条。

最后,可以改进拖放功能,使选择区域的中心不会“捕捉”到鼠标指针,因为这可能会对用户造成困惑。

另请参阅

  • 在 第六章 用户界面创建基本的拖放功能 配方中

第九章:jQuery UI

在本章中,我们将涵盖:

  • 创建时尚且功能性的按钮

  • 创建用户信息和输入对话框

  • 在应用程序中实现进度条

  • 快速向输入框添加日期选择器界面

  • 创建自动完成搜索功能

介绍

jQuery UI 是建立在 jQuery JavaScript 库之上的用户界面库。jQuery UI 提供了许多交互式插件、效果和界面元素,开发人员可以在其界面中使用。本章将演示 jQuery UI 的最常见元素,如按钮和日期选择器,并向您展示如何快速将它们添加到您的网站或 Web 应用程序中。

在开始本章之前,请确保您已访问过jqueryui.com/并下载了 jQuery UI 库。通过他们网站上的Download Builder下载库;确保所有默认选项保持选中状态。本章中使用的 jQuery UI 版本为 v1.10.3,但大多数示例也适用于更新版本。他们的网站还提供了丰富的文档和示例,帮助您快速入门 jQuery UI。

要开始本章的示例,请创建一个名为 chapter9 的易于访问的文件夹,并将 jQuery 库放入其中。创建一个名为 jquery-ui 的子文件夹,并将 jQuery UI 库的 cssjs 文件夹放入此子文件夹。

创建时尚且功能性的按钮

使用 CSS3 快速创建时尚按钮相对容易,但要添加额外的功能通常需要更多的时间投入。jQuery UI 提供了一个按钮 API,可用于创建各种按钮控件,这些控件可以轻松添加到 UI 中,并在 JavaScript 代码中进行交互。本示例演示了如何创建常见的按钮控件,以便您可以在需要时重新使用代码。

准备工作

在之前创建的 chapter9 文件夹中,创建 recipe-1.htmlrecipe-1.js

如何做…

使用 jQuery UI 库创建一系列不同的按钮控件,执行以下步骤:

  1. 为了添加各种按钮元素,请将以下 HTML 代码添加到 recipe-1.html 中,确保在必要时更新对 jQuery 和 jQuery UI 库的引用:

    <!DOCTYPE html>
    <html>
    <head>
       <title>Chapter 9 :: Recipe 1</title>
       <script src="img/jquery.min.js"></script>
       <script src="img/jquery-ui-1.10.3.custom.min.js"></script>
       <link type="text/css" rel="stylesheet" href="jquery-ui/css/ui-lightness/jquery-ui-1.10.3.custom.min.css" />
       <script src="img/recipe-1.js"></script>
    </head>
    <body>
       <h3>Default buttons: a, button and input</h3>
       <a href="#">Button 1</a>
       <button>Button 2</button>
       <input type="submit" name="button3" value="Button 3" />
       <h3>Button options: Disabled, Custom Label and icons</h3>
       <button class="button4">Button 4: Disabled</button>
       <button class="button5">Button 5</button>
       <button class="button6">Button 6 with icons</button>
       <h3>Button Sets: Radio and Checkbox's</h3>
       <div class="buttonSet1">
          <button>One</button>
          <button>Two</button>
          <button>Three</button>
       </div>
       <div class="buttonSet2">
          <input type="checkbox" id="check1" /><label for="check1">Check 1</label>
          <input type="checkbox" id="check2" /><label for="check2">Check 2</label>
          <input type="checkbox" id="check3" /><label for="check3">Check 3</label>
       </div>
       <h3>Buttons with events</h3>
       <button class="enableDisable">Enable/Disable</button>
    </body>
    </html>
    
  2. 将以下 jQuery 代码添加到 recipe-1.js 中,以将 UI 样式和功能应用于按钮元素:

    $(function(){
       //Default buttons
       $('a, button, input[type=submit]').button();
       //Button options
       $('#button4').button('option', 'disabled', true);
       $('#button5').button({label: 'Button 5 with custom label'});
       $('#button6').button('option', 'icons', {primary: 'ui-icon-arrowthick-1-e', secondary: 'ui-icon-circle-arrow-e'});
       //Button sets
       $('.buttonSet1').buttonset();
       $('.buttonSet2').buttonset();
       //Button events
       $('.enableDisable').button().click(function(){
          var _button4 = $('.button4');
          if (_button4.button('option', 'disabled')) {
             _button4.button('option', 'disabled', false);
          } else {
             _button4.button('option', 'disabled', true);
          }
       });
    }); 
    
  3. 在 Web 浏览器中打开 recipe-1.html,您将看到用默认 jQuery UI 主题样式化的各种按钮元素。如何做…

工作原理…

HTML 提供了一系列不同的按钮元素,可以通过 jQuery UI 按钮 API 使用。通过查看此网页,您将能够了解以下元素的工作原理,并在需要时重新使用代码:

  • 包括 ainputbutton 元素的默认按钮

  • 默认按钮及其选项,如自定义标签、图标和禁用

  • 允许复选框和单选按钮功能的按钮集

  • 按钮上的事件

要初始化 jQuery UI 按钮 API,请以典型的 jQuery 方式选择按钮或一组按钮元素,并使用 button() 函数,如下所示:

$('a, button, input[type=submit]').button();

这将为所选按钮应用 jQuery UI CSS 和附加功能。button() 函数还接受一系列选项,以便您可以单独操作按钮元素。这在 recipe-1.js 中的 Button options 部分中显示。

通过在 HTML 代码中分组按钮,并使用 buttonset() 函数,您可以创建一组按钮,这些按钮共同形成复选框或单选按钮功能,如下所示:

<div class="buttonSet1">
   <button>One</button>
   <button>Two</button>
   <button>Three</button>
</div>

您仍然可以使用正常的 jQuery 与 HTML 按钮元素进行交互,以附加事件并执行操作。使用此配方,标记为启用/禁用的按钮具有附加的点击事件处理程序,如下所示:

$('.enableDisable').button().click(function(){
   var _button4 = $('.button4');
   if (_button4.button('option', 'disabled')) {
      _button4.button('option', 'disabled', false);
   } else {
      _button4.button('option', 'disabled', true);
   }
});

这使用了 jQuery UI 提供的 button('option') 功能来检查按钮的禁用状态,然后根据其当前状态将其设置为 truefalse。在打开的 Web 浏览器中的 recipe-1.html 中,单击此按钮将视觉上启用和禁用标记为按钮 4的按钮。请注意,在上面的示例中,可以在 button() 函数后方便地链接 click() 函数。

还有更多内容…

jQuery UI 库提供了更多类型的按钮。转到其网站提供的文档(jqueryui.com/button/)了解简单示例和更多详细信息。

另请参阅

  • 为用户信息和输入创建对话框

为用户信息和输入创建对话框

在第六章,用户界面中,您已经学会了如何创建自己的模态弹出窗口。jQuery UI 提供了一个易于使用的 API,帮助您快速地向应用程序添加模态或对话框。这个配方将研究 jQuery UI 对话框的默认行为,并向您展示如何使用它们。再次强调,这个配方旨在让您能够轻松找到所需的代码,并在方便时重用它。

准备工作

chapter9 文件夹中,创建 recipe-2.htmlrecipe-2.js 并打开它们以供编辑。

如何做…

要了解如何快速向应用程序添加对话框或模态,请执行以下步骤:

  1. 将以下 HTML 添加到 recipe-2.html 中,以便在 JavaScript 代码中使用按钮和对话框元素:

    <!DOCTYPE html>
    <html>
    <head>
       <title>Chapter 9 :: Recipe 2</title>
       <script src="img/jquery.min.js"></script>
       <script src="img/jquery-ui-1.10.3.custom.min.js"></script>
       <link type="text/css" rel="stylesheet" href="jquery-ui/css/ui-lightness/jquery-ui-1.10.3.custom.min.css" />
       <script src="img/recipe-2.js"></script>
    </head>
    <body>
    <div class="actions">
       <button id="openSecondDialog">Open Second Dialog with Animation</button>
       <button id="openModalDialog">Open Modal Dialog</button>
       <button id="openConfirmationDialog">Open Confirmation Dialog</button>
    </div>
    <div id="default-dialog" title="Default Dialog">
       <p>This is a dialog with default behaviour.</p>
    </div>
    <div id="second-dialog" title="Second Dialog">
       <p>This is a dialog with animation that is opened by a button.</p>
    </div>
    <div id="modal-dialog" title="Modal Dialog">
       <p>This is a modal dialog.</p>
    </div>
    <div id="confirmation-dialog" title="Confirmation Dialog">
       <p>Are you sure you want to close this dialog?</p>
    </div>
    </body>
    </html>
    
  2. 将以下 JavaScript 代码添加到 recipe-2.js 中,以初始化打开对话框的对话框元素和按钮:

     $(function(){
       //Set up the dialog elements
       $('#default-dialog').dialog();
       $('#second-dialog').dialog({
          autoOpen: false,
          show: {
             effect: "fade",
             duration: 500
          },
          hide: {
             effect: "explode",
             duration: 1000
          }
       });
       $('#modal-dialog').dialog({
          autoOpen: false,
          modal: true
       });
       $('#confirmation-dialog').dialog({
          autoOpen: false,
          resizable: false,
          buttons: {
             "Yes": function() {
                $(this).dialog("close");
             },
             "No": function() {
                alert("Your dialog will stay open.");
             }
          }
       });
       //Set up button elements
       $('.actions').buttonset();
       $('#openSecondDialog').click(function(){
          $('#second-dialog').dialog("open");
       });
       $('#openModalDialog').click(function(){
          $('#modal-dialog').dialog("open");
       });
       $('#openConfirmationDialog').click(function(){
          $('#confirmation-dialog').dialog("open");
       });
    });
    
  3. 在 Web 浏览器中打开 recipe-2.html,您将看到默认对话框已经打开。使用按钮集中的按钮打开各种其他对话框类型。

工作原理…

与前面的食谱一样,HTML 代码创建了 jQuery UI 库将用于应用所需功能和样式的元素。页面中有四个对话框元素和三个按钮,用于打开附加对话框。

JavaScript 代码依次初始化每个对话框元素,提供不同的选项和设置。第一个对话框元素#default-dialog通过简单地使用以下 jQuery UI 代码而不使用任何选项进行初始化:

$('#default-dialog').dialog();

这将把#default-dialog HTML 元素转换为 jQuery UI 对话框,并在屏幕上显示它。

第二个对话框初始化时,autoOpen选项设置为false,因此当用户首次访问页面时不会自动打开。要打开此对话框,用户必须单击标有Open Second Dialog with Animation的按钮。第二个对话框提供了一些动画选项,如下所示:

$('#second-dialog').dialog({
   autoOpen: false,
   show: {
   effect: "fade",
   duration: 500
},
hide: {
   effect: "explode",
    duration: 1000
   }
});

这将确保在打开对话框时使用淡入动画,并在关闭对话框时使用爆炸动画。阅读 jQuery UI 对话框文档(api.jqueryui.com/dialog/),以发现可用的动画效果。

第三个对话框是一个模态对话框。只需在打开模态时将modal: true选项添加到dialog()函数中,即可添加一个遮罩,遮挡页面的其余部分视图。

本食谱中的第四个对话框是一个确认对话框。使用按钮选项,您可以指定一些按钮和回调来保存按钮操作,如下所示:

$('#confirmation-dialog').dialog({
   autoOpen: false,
   resizable: false,
   buttons: {
   "Yes": function() {
      $(this).dialog("close");
   },
   "No": function() {
      alert("Your dialog will stay open.");
      }
   }
});

resize选项设置为false,以覆盖允许用户更改对话框大小的默认行为。

更多内容...

通过阅读文档(api.jqueryui.com/dialog/),您将发现更多可供选择的对话框类型。表单对话框特别有用,可以快速检索用户输入,并具有内置的验证功能。

另请参阅

  • 创建时尚和功能性按钮

在您的应用程序中实现进度条

进度条允许用户详细了解应用程序正在执行的过程。进度条是更新用户请求的任务进度的理想解决方案,该任务可能需要很长时间才能完成。此操作可以是文件上传或其他耗时的服务器端进程。本食谱将向您展示如何使用 jQuery UI 进度条 API 轻松地将进度条添加到您的应用程序中。

准备工作

在您之前创建的chapter9文件夹中创建recipe-3.htmlrecipe-3.jsrecipe-3.css

如何实现...

学习如何快速将进度条添加到您的应用程序中,执行以下步骤:

  1. 将以下 HTML 代码添加到recipe-3.html中,以创建具有所需进度条 HTML 元素的网页:

    <!DOCTYPE html>
    <html>
    <head>
       <title>Chapter 9 :: Recipe 3</title>
       <script src="img/jquery.min.js"></script>
       <script src="img/jquery-ui-1.10.3.custom.min.js"></script>
       <link type="text/css" rel="stylesheet" href="jquery-ui/css/ui-lightness/jquery-ui-1.10.3.custom.min.css" />
       <link type="text/css" rel="stylesheet" href="recipe-3.css" />
       <script src="img/recipe-3.js"></script>
    </head>
    <body>
    <div class="progress-bar"><div class="progress-label">Press "Start Progress" to begin load...</div></div>
    <button class="start-progress">Start Progress</button>
    </body>
    </html>
    
  2. 为了为进度条标签提供一些基本样式,将以下 CSS 代码添加到 recipe-3.css 中:

    .progress-bar {
       position: relative;
    }
    .progress-label {
       position: absolute;
       left: 0;
       top: 0;
       right: 0;
       bottom: 0;
       text-align: center;
       line-height: 35px;
    }
    
  3. 将以下 JavaScript 代码添加到 recipe-3.js 中,以初始化进度条并为开始进度按钮提供功能:

    $(function(){
       var progressBar = $('.progress-bar');
       var progressLabel = $('.progress-label');
       progressBar.progressbar({
          change: function() {
             progressLabel.text(progressBar.progressbar("value") + "% complete...");
          },
          complete: function() {
             progressLabel.text("Completed!");
          }
       });
       $('.start-progress').button().click(doStuff);
       function doStuff() {
          var progressValue = ((progressBar.progressbar("value") || 0) + 1);
          progressBar.progressbar("value", progressValue);
          if (progressValue < 100) {
             setTimeout(doStuff, 100);
          }
       }
    });
    
  4. 在网页浏览器中打开 recipe-3.html,然后单击开始进度按钮。进度条将开始显示进度,直到达到 100%。如何操作…

工作原理…

HTML 页面创建了两个元素,jQuery UI 将使用它们来创建进度条和标签:

<div class="progress-bar"><div class="progress-label">Press "Start Progress" to being load...</div></div>

默认的标签文本被添加到标签元素中,在用户首次访问网页时将显示。网页还添加了一个开始进度按钮,以便用户可以启动加载操作。

本示例中的加载操作只是一个虚假过程。您可以轻松地将此代码与 XmlHttpRequest 结合使用,用于图像上传,例如在 第八章 的从头开始编码图像裁剪插件配方中使用的代码,理解插件开发

要初始化添加到 HTML 页面中的 progress-bar 元素中的进度条,使用 progressbar() 函数:

progressBar.progressbar({
   change: function() {
   progressLabel.text(progressBar.progressbar("value") + "% complete...");
   },
   complete: function() {
      progressLabel.text("Completed!");
   }
});

提供一个具有两个属性的对象给此函数设置更改和完成事件回调函数。这使我们能够在进度值发生变化时执行操作,以及在进度完成时执行操作。在本示例中,我们只是更新进度标签,以通知用户完成的百分比值。确保您阅读文档 (jqueryui.com/progressbar/),以便您了解所有可用的选项。

使用 progressBar.progressbar("value"),可以从进度条元素中检索进度值。然后可以使用该值来更新进度标签文本。

当用户单击开始进度按钮时调用的 doStuff() 函数作为进度。它使用 setTimeout() 每 100 毫秒调用自身,然后按如下方式增加进度条值:

var progressValue = ((progressBar.progressbar("value") || 0) + 1);
progressBar.progressbar("value", progressValue);

另请参阅

  • 从头开始编码图像裁剪插件 配方在 第八章 中,理解插件开发

快速向输入框添加日期选择器界面

日期选择器为用户提供了一个易于使用的界面,以便他们快速选择所需的日期。jQuery UI 提供了一个可以快速添加到输入字段的日期选择器。日期选择器提供了许多配置选项,如日期格式化和限制,使开发人员更容易根据需要限制用户的输入。本示例将向您展示如何将日期选择器添加到两个输入字段中,更改日期选择器的日期格式,并为每个字段应用日期限制。

准备就绪

与前一个示例一样,在之前创建的 chapter9 文件夹中创建 recipe-4.htmlrecipe-4.jsrecipe-4.css

如何操作…

按照以下每个步骤创建一个简单的界面,其中包含两个日期选择器和配置选项:

  1. 将以下 HTML 代码插入到 recipe-4.html 中,以创建带有日期选择器元素的基本网页和用户界面:

    <!DOCTYPE html>
    <html>
    <head>
       <title>Chapter 9 :: Recipe 4</title>
       <script src="img/jquery.min.js"></script>
       <script src="img/jquery-ui-1.10.3.custom.min.js"></script>
       <link type="text/css" rel="stylesheet" href="jquery-ui/css/ui-lightness/jquery-ui-1.10.3.custom.min.css" />
       <link type="text/css" rel="stylesheet" href="recipe-4.css" />
       <script src="img/recipe-4.js"></script>
    </head>
    <body>
       <div class="frame">
          <div class="settings">
             <label>Restrict:</label>
             <select class="restrict">
                <option value="1">1 Year</option>
                <option value="2">2 Years</option>
                <option value="3">3 Years</option>
            </select>
            <label>Format:</label>
            <select class="formatDate">
               <option value="dd/mm/yy">English Format</option>
               <option value="mm/dd/yy">American Format</option>
             </select>
          </div>
          <div class="datepickers">
             Start: <input type="text" class="start" />
             End: <input type="text" class="end" />
          </div>
       </div>
    </body>
    </html>
    
  2. 添加以下 CSS 到 recipe-4.css,以为用户界面提供基本样式和定位:

    .frame {
       width: 500px;
       margin: 100px auto auto auto;
       background-color: #494949;
       border-radius: 5px;
       box-shadow: 5px 5px 5px #CCC;
    }
    .frame .settings {
       line-height: 40px;
       text-align: center;
       background-color: #333;
       color: #FFF;
       border-top-left-radius: 5px;
       border-top-right-radius: 5px;
    }
    .frame .datepickers {
       line-height: 100px;
       text-align: center;
       color: #CCC;
    }
    
  3. 添加以下 jQuery 代码到 recipe-4.js,以设置日期选择器元素并为 recipe-4.html 中的其他元素提供功能:

    $(function(){
       var _start = $('.start');
       var _end = $('.end');
       var _restrict = $('.restrict');
       var _formatDate = $('.formatDate');
       var _dateFormat = 'dd/mm/yy';
       _start.datepicker({
          dateFormat: _dateFormat,
          minDate: new Date(),
          onClose: function(selectedDate) {
             _end.datepicker("option", "minDate", selectedDate);
             restrictDates();
          }
       });
       _end.datepicker({
          dateFormat: _dateFormat,
          onClose: function(selectedDate) {
             _start.datepicker("option", "maxDate", selectedDate);
          }
       });
       _formatDate.change(function(){
          _dateFormat = _formatDate.val();
          _start.datepicker("option", "dateFormat", _dateFormat);
          _end.datepicker("option", "dateFormat", _dateFormat);
       });
       _restrict.change(function(){
          restrictDates();
       });
       function restrictDates() {
          var maxDate = _start.datepicker("getDate");
          if (maxDate != null) {
          maxDate.setFullYear(maxDate.getFullYear() + parseInt(_restrict.val()));
          _end.datepicker("option", "maxDate", maxDate);
    }
       }
    });
    
  4. 在 Web 浏览器中打开 recipe-4.html,您将看到一个简单的界面,其中包含两个输入和两个下拉菜单。标记为 startend 的两个输入在您点击输入字段时会为您提供一个日期选择器界面。然后,您可以使用日期选择器选择要插入到相关输入中的日期。通过使用两个下拉菜单,您可以更改日期和日期选择器的行为。格式选项将日期格式更改为英文或美式。限制下拉菜单将允许您选择结束日期选择器允许用户选择的最大年数,超过所选开始日期。

工作原理…

HTML 和 CSS 为我们提供了一个简单的界面,可与 jQuery UI 结合使用,以演示一些日期选择器的功能。在 recipe-4.js 的顶部,有一些变量保存了将由 jQuery 使用的不同 HTML 元素的引用,以及保存了英文日期格式的变量。

要为输入元素添加日期选择器,需要使用 jQuery UI 的 datepicker() 函数以及所需的选项:

_start.datepicker({
   dateFormat: _dateFormat,
   minDate: new Date(),
   onClose: function(selectedDate) {
      _end.datepicker("option", "minDate", selectedDate);
      restrictDates();
   }
});

dateFormat 选项设置所选日期选择器的格式。minDate 选项设置日期选择器允许用户选择的最小日期;使用 new Date() 来将此限制设置为当前日期。在关闭日期选择器后将执行指定的 onClose 函数。在此函数中,为结束输入设置了 minDate 选项。这将确保用户无法选择早于所选开始日期的结束日期。restrictDates() 函数也在此处被调用。restrictDates() 函数定义如下:

function restrictDates() {
   var maxDate = _start.datepicker("getDate");
   if (maxDate != null) {
      maxDate.setFullYear(maxDate.getFullYear() + parseInt(_restrict.val()));
      _end.datepicker("option", "maxDate", maxDate);
   }
}

此函数对结束日期选择器应用限制,使用户无法选择比所选开始日期大 n 年的结束日期。这里,n限制下拉菜单指定的值。就像设置 minDate 一样,设置 maxDate 使用的是所选开始日期加上指定年数的数量。当用户更改下拉菜单选择时,也会使用 change() 函数调用此函数。

当用户选择更改日期格式时,以下代码用于更新每个日期选择器元素的格式:

_formatDate.change(function(){
   _dateFormat = _formatDate.val();
   _start.datepicker("option", "dateFormat", _dateFormat);
   _end.datepicker("option", "dateFormat", _dateFormat);
});

日期选择器 API 的一部分提供了许多选项。阅读文档 (api.jqueryui.com/datepicker/) 以了解其他可用选项。

创建自动完成搜索功能

本配方将向用户建议搜索词,当他们在搜索输入框中输入时。这是一个非常受欢迎的功能,对用户非常有帮助,因为它在用户甚至还没有进行搜索之前就提供了一些关于搜索结果的见解。jQuery UI 提供了可以快速添加到任何输入元素中的自动完成功能。

准备工作

在你保存其他配方文件的 chapter9 文件夹中创建 recipe-5.htmlrecipe-5.jsrecipe-5.css

本配方利用了 Trakt.tv 提供的高质量 API(trakt.tv/api-docs/)。在开始本配方之前,你需要注册(免费)并获取一个 API 密钥。一旦注册成功,你可以在以下页面找到你的 API 密钥:trakt.tv/api-docs/authentication

注意

在编写本配方时,已知 Google Chrome 中存在一个已知的错误,在该错误中,如果你尝试从本地机器(即使用 file:// 而不是 http(s):// 访问 recipe-5.html)调用 AJAX 中的 jQuery 来调用外部源,你可能会收到一个 Access-Control-Allow-Origin 错误。如果你遇到了这个问题,要么通过 web 服务器提供你的配方文件,要么使用其他浏览器。

为了演示自动完成功能如何在真实情境中使用,本配方将使用上述 API 创建相关的电视节目搜索。它将允许用户搜索一个电视节目(并通过自动完成提供建议),一旦用户选择了一个,与所选节目相关的节目将被显示。

如何操作...

要添加自动搜索功能,请执行以下操作:

  1. 将以下 HTML 代码添加到 recipe-5.html 中,以创建基本的网页:

    <!DOCTYPE html>
    <html>
    <head>
       <title>Chapter 9 :: Recipe 5</title>
       <script src="img/jquery.min.js"></script>
       <script src="img/jquery-ui-1.10.3.custom.min.js"></script>
       <link type="text/css" rel="stylesheet" href="jquery-ui/css/ui-lightness/jquery-ui-1.10.3.custom.min.css" />
       <link type="text/css" rel="stylesheet" href="recipe-5.css" />
       <script src="img/recipe-5.js"></script>
    </head>
    <body>
    <div class="frame">
       <h1>RELATED <span>TV</span> SHOWS</h1>
       <div class="head">
          <p>Find TV shows related to your favorites.</p>
          <div class="search-input-frame">
             <input type="text" id="searchInput" placeholder="Search for a TV show..." />
       </div>
       </div>
       <div class="results">
          <div class="searching">Searching for related shows...</div>
          <ul id="results-list"></ul>
       </div>
    </div>
    </body>
    </html>
    
  2. 将以下 CSS 放置在 recipe-5.css 中,将 HTML 代码转换成一个具有吸引力的网页:

    @import url(http://fonts.googleapis.com/css?family=Roboto:400,300,100);
    body {
       background-color: #333;
       font-family: 'Roboto', sans-serif;
    }
    .frame {
       width: 800px;
       background-color: #FFF;
       margin: 100px auto auto auto;
       padding: 20px;
       border-radius: 5px;
    }
    .frame h1 {
       margin: -93px 0 0 0;
       color: #FFF;
       font-size: 70px;
       text-align: center;
    }
    .frame h1 span {
       color: #00B5B5;
    }
    .search-input-frame #searchInput {
       width: 780px;
       border: none;
       font-weight: bold;
       color: #999;
       background: #373737;
       font-size: 14px;
       height: 40px;
       padding: 0 0 0 10px;
       margin: 0;
       border-radius: 5px;
       line-height: 40px;
    }
    .frame .head p {
       font-style: italic;
       text-align: center;
    }
    .frame .results ul {
       list-style: none;
       margin: 10px 0 0 5px;
       padding: 0;
    }
    .frame .results ul li {
       line-height: 30px;
       font-size: 18px;
    }
    .frame .results .searching {
       display: none;
       text-align: center;
       font-style: italic;
       font-size: 18px;
       line-height: 100px;
    }
    .frame .results ul li.no-results {
       line-height: 100px;
       text-align: center;
       font-size: 16px;
       font-weight: bold;
    }
    
  3. 将以下 jQuery 添加到 recipe-5.js 中,以初始化搜索输入元素上的自动完成功能:

    $(function(){
       $('#searchInput').autocomplete({
          minLength: 2,
          source: function(input, response) {            
       },
       select: function (event, ui) { 
       }
       });
    });
    
  4. 在你刚刚添加的源函数中插入以下代码,根据用户的输入调用 Trakt.tv API,以提供自动完成功能所需的数据。确保你将 [API KEY HERE] 替换为你的 Trakt.tv API 密钥,如下所示:

    $.ajax({
    type: 'GET',
    url: 'http://api.trakt.tv/search/shows.json/[API KEY HERE]?query=' + input.term + "&limit=10",
    dataType: 'jsonp',
    success: function(data) {
       var results = [];
    for (var i = 0; i < data.length; i++) {
    results.push({
    id: data[i].tvdb_id,
    label: data[i].title,
    value: data[i].title
       });
       }
       response(results);
    }
    });
    
  5. 为了基于用户的自动完成部分填充主要结果列表,将以下 jQuery 代码添加到你刚刚添加的 select 函数中。再次记得用你的 API 密钥替换 [API KEY HERE]

    var showId = ui.item.id;
    var _searchingMsg = $('.searching');
    var _resultList = $('#results-list');
    _resultList.empty();
    _searchingMsg.fadeIn();
    $.ajax({
    type: 'GET',
       url: 'http://api.trakt.tv/show/related.json/[API KEY HERE]/' + showId,
       dataType: 'jsonp',
    success: function(data) {
    _searchingMsg.hide();
    for (var i = 0; i < data.length; i++) {
       resultList.append("<li><a target='_blank' href='" + data[i].url + "'>" + data[i].title + "</a></li>");
    }
    }
    });
    
  6. 在网页浏览器中打开 recipe-5.html,并搜索你最喜欢的电视节目:如何操作...

工作原理...

jQuery UI 自动完成功能将所有复杂性封装起来,以便开发者只需要考虑提供数据和选择后的操作。

这个配方中的 HTML 页面创建了一个用户可以在其中进行搜索的网页。然后在 jQuery 代码中选择此输入,并使用autocomplete()函数来初始化所选元素上的自动完成功能,如下所示:

$('#searchInput').autocomplete({
   minLength: 2,
   source: function(input, response) {            

},
select: function (event, ui) { 

}
});

在提供给autocomplete()函数的对象上的source属性是显示给用户的下拉菜单中使用的数据。source属性可以是数组、字符串或函数。当source是字符串时,它期望值是提供数据的资源 URL,以期望的格式提供数据。因为我们使用的是一个不会提供期望格式数据的外部 API,所以我们使用第三个选项,并在函数内进行一些额外的处理。minLength属性允许我们控制用户必须输入多少字符才能触发自动完成功能

首先,需要从 Trackt.tv 检索数据。为此,使用熟悉的 jQuery $.ajax()函数:

$.ajax({
   type: 'GET',
   url: 'http://api.trakt.tv/search/shows.json/[API KEY HERE]?query=' + input.term + "&limit=10",
   dataType: 'jsonp',
   success: function(data) {

}
});

source()函数接受两个参数:input(对象)和response(函数)。使用input.term,我们可以获取用户输入到搜索输入框中的值,并将其插入到 API URL 中以搜索电视节目。限制查询字符串变量被设置为10,以便只返回 10 个结果。

注意,这个配方中的两个 AJAX 请求的dataType属性都被设置为jsonp。这是为了在与 API 一起工作时防止任何跨域问题。在www.jquery4u.com/json/jsonp-examples/上阅读更多关于 jQuery 的 JSONP 的信息。

如果请求成功,我们可以遍历所有结果并创建一个以自动完成功能期望的格式的数组,如下所示:

var results = [];
for (var i = 0; i < data.length; i++) {
   results.push({
      id: data[i].tvdb_id,
      label: data[i].title,
      value: data[i].title
});
}
response(results);

调用response()函数,这是source()函数的第二个参数;这将把结果发送给自动完成功能以显示。

在这些配方中,下一段功能发生在用户从自动完成建议列表中选择选项时。在提供给autocomplete()函数的对象上的select属性接受一个回调函数,当用户做出选择时执行。使用ui参数,然后可以从表示用户选择的对象中检索数据。在这种情况下,我们需要 ID,以便我们可以将其传递回到 Trackt.tv API,并检索相关的电视节目列表:

var showId = ui.item.id;

这个变量被用作另一个$.ajax()请求的一部分。在这个请求成功后,结果将被循环遍历,并且为每个相关的电视节目插入一个列表项。还会添加一个链接到 Trakt.tv 网页,显示更多关于每个节目的信息,如下所示:

for (var i = 0; i < data.length; i++) {
   resultList.append("<li><a target='_blank' href='" + data[i].url + "'>" + data[i].title + "</a></li>");
}

另见

  • 在 [第三章 使用 AJAX 和 JSON 加载和操作动态内容中创建自动建议功能] 这个示例

第十章:使用 jQuery Mobile

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

  • 创建基本的移动网站模板

  • 构建完整的静态网站

  • 构建动态移动网站

  • 实现快速呼叫功能

  • 实现发送短信功能

  • 添加移动友好的列表

  • 使用面向触摸的事件

  • 创建移动兼容表单

  • 构建完整的注册和登录系统

  • 构建完整的移动 Web 应用程序

介绍

jQuery Mobile 是一个精心设计的框架,旨在使创建移动友好的网站和应用程序更容易。jQuery Mobile 结合了为移动体验量身定制的可主题化 UI 元素,并提供了针对触摸屏设备上特殊事件的自定义事件。

本章介绍了 jQuery Mobile 并深入了解其功能。在本章中,您将学习如何创建一个利用常见元素(如按钮和列表)的基本移动网站。然后,您将继续学习一些移动特定功能,如触摸呼叫

在开始本章之前,请确保您已从 jQuery 网站(jquerymobile.com/download)下载了最新版本的 jQuery Mobile 框架。创建一个名为chapter10的文件夹,用于保存本章的所有配方文件。在此文件夹中,创建一个名为jquery-mobile的文件夹,并将主要的 jQuery Mobile JavaScript 和 CSS 文件放入其中,包括images文件夹,其中包含 CSS 中引用的所有图标精灵。

本章使用的 jQuery Mobile 版本是 1.3.2,但大多数配方也适用于更新版本。

对于本章中的一些配方,您将需要运行 PHP 和 MySQL 的 Web 服务器。该 Web 服务器可以是本地开发服务器,也可以是托管在云中的服务器。您还需要访问 MySQL 管理界面,如 PHPMyAdmin,以便运行 SQL 脚本。

创建基本的移动网站模板

这个配方将展示一个简单的 jQuery Mobile 网页的基本布局是什么样的。您还将能够将此 HTML 页面用作未来 jQuery 移动项目的模板。

准备工作

在您之前创建的chapter10文件夹中,创建recipe-1.html

如何操作…

将以下 HTML 代码插入到recipe-1.html中,以创建一个非常基本的 jQuery Mobile 单页网站:

<!DOCTYPE html>
<html>
<head>
    <title>Chapter 10 :: jQuery Mobile Template</title>
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
    <script src="img/jquery.min.js"></script>
    <script src="img/jquery.mobile.min.js"></script>
</head>
<body>
<div data-role="page">
    <header data-role="header">
        <h1>Chapter 10 :: Recipe 1 :: jQuery Mobile Webpage Template</h1>
    </header>
    <div data-role="content">
        <p>This is where your page content will go.</p>
    </div>
    <footer data-role="footer">
        <h4>Look how easy it is to add a styled footer</h4>
    </footer>
</div>
</body>
</html>

确保更新对 jQuery 库和 CSS 的引用,以反映您下载的文件。在 Web 浏览器中打开recipe-1.html,您将看到如何使用 jQuery Mobile 快速创建一个基本的移动友好网页。

工作原理…

乍一看,模板页面与典型的 HTML 网页并没有太大区别。文档中声明了 HTML5 标准化文档类型<!DOCTYPE html>,所需的 CSS 和 JavaScript 包含在文档的头部。

与一般 HTML 页面不同的是,视口的meta标签,并不总是存在。它告诉浏览器如何设置页面的尺寸和缩放级别。如果这些没有被设置,大部分移动设备将使用虚拟宽度,使网页看上去缩小。

注意

data-属性是 HTML5 的新实现,允许自定义元素属性,并仍提供有效的标记。这些data-*属性允许你存储关于特定元素的任意信息,而 jQuery Mobile 利用了这个能力。

在 jQuery Mobile 中,使用data-role属性来指示元素的作用。在这个配方中创建的简单模板中,我们使用了 page、header、content 和 footer 角色来创建简单页面的结构。这些角色都不言而喻,但在本章的过程中也会变得更加清晰。

还有更多…

正如所有 jQuery 实现一样,提供了丰富的文档可供参考(jquerymobile.com),所有开发者都应该加以利用。要充分利用 jQuery Mobile,请确保阅读文档。

参见

  • 创建一个完整的静态网站

创建一个完整的静态网站

这个配方将向你展示如何快速使用 jQuery Mobile 创建一个简单的静态网站。在前一个配方中创建的模板中,只需要添加带有正确角色的附加元素,就能创建额外的页面和它们之间的导航。

准备工作

在之前创建的chapter10文件夹中创建recipe-2.html,并确保你已准备好你新创建的 jQuery Mobile 模板。

如何做…

要使用 jQuery Mobile 创建功能性移动网站,请执行以下步骤:

  1. 将之前创建的 jQuery Mobile 模板复制到recipe-2.html中,并删除<body>标签内的所有内容,如以下代码片段所示:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: jQuery Mobile Template</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
    </head>
    <body>
    
    </body>
    </html>
    
  2. <body>标签之间插入以下代码,可创建简单静态网站的主页:

    <div data-role="page" id="home">
        <header data-role="header">
            <h1>Home Page</h1>
        </header>
        <div data-role="content">
            <p>This is the content for the home page. You can choose to go to another page using the buttons below.</p>
            <a href="#about" data-role="button" data-theme="a">About</a>
            <a href="#contact" data-role="button" data-theme="a">Contact</a>
        </div>
        <footer data-role="footer">
            <h4>Chapter 10 :: Recipe 2</h4>
        </footer>
    </div>
    
  3. 要创建一个链接到主页的关于页面,请在主页声明后但仍在<body>标签内添加以下代码:

    <div data-role="page" id="about" data-title="About Page">
        <header data-role="header">
            <h1><a href="#home" data-role="button" data-theme="b" data-icon="arrow-l" data-inline="true">Back</a> About Page</h1>
        </header>
        <div data-role="content">
            <p>This is the content for the about page.</p>
        </div>
        <footer data-role="footer">
            <h4>Chapter 10 :: Recipe 2</h4>
        </footer>
    </div>
    
  4. 重复上一步,使用以下代码来添加最终的联系页面:

    <div data-role="page" id="about" data-title="About Page">
        <header data-role="header">
            <h1><a href="#home" data-role="button" data-theme="b" data-icon="arrow-l" data-inline="true">Back</a> About Page</h1>
        </header>
        <div data-role="content">
            <p>This is the content for the about page.</p>
        </div>
        <footer data-role="footer">
            <h4>Chapter 10 :: Recipe 2</h4>
        </footer>
    </div>
    
  5. 在网页浏览器中打开recipe-2.html,你将看到移动网站,并且你可以使用主页上的按钮浏览到关于和联系页面之间。

它是如何运行的…

在这个配方的代码中,你会看到data-role属性被多次使用来指示许多 HTML 元素的功能。要声明多个页面,只需重复使用模板中使用的基本页面结构,并根据需要更改内容。考虑下面的例子:

<div data-role="page" id="about" data-title="About Page">
    <header data-role="header">

    </header>
    <div data-role="content">

    </div>
    <footer data-role="footer">

    </footer>
</div>

这是用于本案例中的关于页面的基本结构。主div元素被指定为使用data-role="page"表示一个页面。为了允许导航到此页面,定义了一个唯一的 ID,就像你会对任何 HTML 元素一样(id="about")。页面分割线上还有data-title属性,使得可以覆盖文档头中的<title>标签的内容,这样就可以根据每页更改页面标题。

您可以使用锚元素创建到此方式创建的页面之一的内部链接,就像下面的代码行所示:

<a href="#about" data-role="button" data-theme="a">About</a>

当用户点击链接时,他们将看到带有唯一 ID about的页面,该 ID 在href属性中表示为#about。默认页面转换也将被用来提供平滑的导航效果。data-role="button"属性用于将元素样式设置为按钮,而data-theme="a"属性指定了要使用的主题样式。阅读上述文件以查看默认可用的主题以及如何创建自己的主题。

参见也

  • 创建基本移动网站模板

  • 构建动态移动网站

构建动态移动网站

在上一个案例中,我们创建了一个基本网站,允许您为用户提供内容并相对容易地手动更新它。在大多数情况下,这是不够的。今天的大多数网站依赖某种形式的数据库,以便定期为它们提供丰富的、新的内容,移动网站也应该一样。本案例将向您展示如何使用 PHP 与 jQuery Mobile 创建动态页面,该页面的内容来自 Web 服务器。

准备工作

您需要在 Web 服务器的 Web 根目录中创建以下目录结构。在下面的图中,www是 Web 根目录;这可能对您来说是不同的:

准备工作

在 Web 服务器的 Web 根目录(www)中,创建一个includes文件夹以及文件index.htmlcategories.php。在includes文件夹中,创建一个名为jquery-mobile的子文件夹,并确保所有 jQuery Mobile 库文件都已复制到其中。此外,在includes文件夹中,创建script.js并添加 jQuery 库(jquery.min.js)。

如何实现…

要使用 PHP 创建一个动态移动网站,请仔细执行以下步骤:

  1. 重新使用作为第一个案例模板的结构,将以下代码添加到index.html。这将创建一个主页和一个 ID 为categorypage的额外空白页面。

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: Recipe 3</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="includes/jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
        <script src="img/script.js"></script>
    </head>
    <body>
    <div id="home" data-role="page">
        <header data-role="header"><h1>Dynamic page creation demo</h1></header>
        <div data-role="content">
            <h2>Select a category:</h2>
            <a href="#categorypage?cat=colours" data-role="button">Colours</a>
            <a href="#categorypage?cat=shapes" data-role="button">Shapes</a>
            <a href="#categorypage?cat=sounds" data-role="button">Sounds</a>
        </div>
    </div>
    <div id="categorypage" data-role="page">
        <div data-role="header"><h1></h1></div>
        <div data-role="content"></div>
    </div>
    </body>
    </html>
    
  2. 添加以下 PHP 代码到categories.php中,以能够根据 jQuery Mobile 站点上的请求提供内容:

    <?php
    $categories = array(
        "colours" => array(
            "title" => "Colours",
            "description" => "Some of my favorite colours",
            "items" => array(
                "Black",
                "Green",
                "Red",
                "Blue",
                "Purple"
            )
        ),
        "shapes" => array(
            "title" => "Shapes",
            "description" => "Some shapes I really like",
            "items" => array(
                "Triangle",
                "Circle",
                "Square"
            )
        ),
        "sounds" => array(
            "title" => "Sounds",
            "description" => "Some crazy sounds",
            "items" => array(
                "Buzz",
                "Swish",
                "Boom!",
                "Tick"
            )
        )
    );
    if (isset($_POST['category'])) {
        $response = array(
            "success" => true,
            "data" => array()
        );
        $category = $_POST['category'];
        if (isset($categories[$category])) {
            $response["data"] = $categories[$category];
        } else {
            $response["success"] = false;
            $response["data"] = "Invalid category provided";
        }
        header("Content-Type: application/json; charset-UTF-8");
        echo json_encode($response);
    }
    
  3. 为了捕捉用户对动态页面之一的请求,将以下 jQuery 代码添加到script.js,它将监听pagebeforechange事件。这允许我们在用户发送到类别页面之前进行拦截。

    $(document).bind("pagebeforechange", function(e, data) {
        if (typeof data.toPage === "string") {
            var urlObject = $.mobile.path.parseUrl(data.toPage);
            if (urlObject.hash.search(/^#categorypage/) !== -1 && urlObject.hash.search(/cat=*/) !== -1) {
                displayCategory(urlObject, data.options);
                //We are handling the change page event ourselves so prevent the default behaviour
                e.preventDefault();
            }
        }
    });
    
  4. 为了能够生成动态页面,需要从 PHP 脚本中获取内容。在script.js的末尾添加以下 JavaScript 函数,以便发起 AJAX 请求收集这些数据并生成动态页面的标记:

    function displayCategory(urlObject, options) {
        var catName = urlObject.hash.replace(/.*cat=/, "");
        var pageId = urlObject.hash.replace( /\?.*$/, "");
        var _page = $(pageId);
        var _header = _page.children(":jqmData(role=header)");
        var _content = _page.children(":jqmData(role=content)");
        $.ajax({
            url: 'categories.php',
            type: 'POST',
            data: {
                category: catName
            },
            success: function(response) {
                if (response.success) {
                    var category = response.data;
                    //Add title to header
                    _header.find("h1").html(category.title);
                    //Create content HTML
                    var contentHtml = "<p>" + category.description + ":</p><ul>";
                    for (var i = 0; i < category.items.length; i++) {
                        var item = category.items[i];
                        contentHtml += "<li>" + item + "</li>";
                    }
                    contentHtml += "</ul>";
                    _content.html(contentHtml);
                    //Update the URL to reflect the page the user is actually on
                    _page.data("url", urlObject.href);
                    $.mobile.changePage(_page, options);
                } else {
                    alert(response.data);
                }
            }
        });
    }
    
  5. 通过网页浏览器(例如,http://localhost/)访问您新创建的移动网站,您将看到首页,其中提供了三个不同类别的按钮。单击其中一个按钮,将进入新页面,该页面显示的内容是从您刚刚创建的 PHP 脚本提供的。

工作原理...

在此配方中用于创建移动站点的 HTML 代码与之前的静态移动站点几乎没有区别。唯一的区别是只有一个附加页面,没有标题或内容。这是因为附加页面将被重复使用以动态创建多个页面,并且其标题和内容将根据用户请求设置。

index.html中的 HTML 代码创建了带有标签为颜色形状声音的三个按钮的首页。这些按钮中的每一个都是指向相同内部页面的链接,该页面带有一些额外信息,如以下代码行所示:

#categorypage?cat=colours

每个按钮为cat变量提供不同的值,表示不同的类别页面。当用户单击其中一个页面时,默认行为是使 jQuery Mobile 将用户导航到此内部页面。由于我们正在动态创建这些内部页面,因此我们需要拦截此行为,从 PHP 脚本中收集请求的类别内容,然后生成页面。为此,我们绑定到pagebeforechange事件,如下所示:

$(document).bind("pagebeforechange", function(e, data) {
    if (typeof data.toPage === "string") {
        var urlObject = $.mobile.path.parseUrl(data.toPage);
        if (urlObject.hash.search(/^#categorypage/) !== -1 && urlObject.hash.search(/cat=*/) !== -1) {
            displayCategory(urlObject, data.options);
            //We are handling the change page event ourselves to prevent the default behaviour
            e.preventDefault();
        }
    }
});

由于我们只想拦截特定的页面请求,在我们请求动态内容之前,我们执行了一些检查。我们可以从提供给事件处理程序函数的data对象中获取请求 URL。我们首先检查 URL 是否为字符串,如下所示:

if (typeof data.toPage === "string") {

如果是字符串,则按照以下方式解析为 URL 对象:

var urlObject = $.mobile.path.parseUrl(data.toPage);

创建了 URL 对象后,可以执行两个最终检查,以查看请求的页面是否属于类别页面,如下所示:

if (urlObject.hash.search(/^#categorypage/) !== -1 && urlObject.hash.search(/cat=*/) !== -1) {

使用search()函数,可以搜索字符串#categorypage以检查是否正在请求类别页面,然后再次检查是否还提供了一个cat变量。

如果这些检查通过,则调用displayCategory()函数来收集和呈现动态页面的内容。还使用了e.preventDefault()来阻止 jQuery Mobile 在生成具有动态内容之前将用户导航到请求的页面。

displayCategory()函数的顶部,声明了一系列变量,如下所示:

var catName = urlObject.hash.replace(/.*cat=/, "");
var pageId = urlObject.hash.replace( /\?.*$/, "");
var _page = $(pageId);
var _header = _page.children(":jqmData(role=header)");
var _content = _page.children(":jqmData(role=content)");

前两个从请求 URL 获取值,即请求的类别和页面 ID(即#categorypage)。

然后,使用页面 ID 通过典型的 jQuery 选择器从 index.html 中选择页面 DOM 元素。接着,使用 page 元素,可以找到并存储页面标题和内容的 DOM 元素,稍后可以使用标准的 jQuery 进行操作。

然后,使用 jQuery 的 $.ajax() 函数进行 AJAX 请求,向 categories.php 发起 POST 请求,指定了来自请求页面 URL 的 catName 的值。

categories.php PHP 脚本保存一个多维数组,其中存储了三种不同类别的数据。此 PHP 脚本获取了提交的类别并使用 isset() 检查 $categories 数组中是否存在匹配的类别。如果存在,则将 $response 数组的数据值更新为请求的类别的数据。如果没有请求的类别数据,则将 $response 数组的成功值设置为 false 并提供错误消息。

最后,PHP 脚本在将 $response 数组编码为 JSON 并输出之前设置内容类型和字符集。

displayCategory() 函数发起的 AJAX 请求将接收此 JSON 数据并相应处理。

通过检查 response.success 是否为真,可以确定是否有一些数据要显示给请求的类别。如果有,则可以添加页面的标题以及为内容创建的 HTML 代码,如下所示:

var category = response.data;
//Add title to header
_header.find("h1").html(category.title);
//Create content HTML
var contentHtml = "<p>" + category.description + ":</p><ul>";
for (var i = 0; i < category.items.length; i++) {
var item = category.items[i];
contentHtml += "<li>" + item + "</li>";
}
contentHtml += "</ul>";
_content.html(contentHtml);

为确保 URL 反映用户正在查看的页面,使用 jQuery 的 data() 函数设置 categorypage 元素的 url 属性如下:

_page.data("url", urlObject.href);

最后,调用 changePage() 函数将用户导航到新生成的页面,在那里,他们将看到从 PHP 脚本提供的请求内容。changePage() 函数还会插入一个条目到浏览器历史中,以提供典型的浏览器导航行为。

更多信息…

此配方中的 PHP 脚本提供了填充附加类别页面的内容,并将此内容存储在 PHP 数组中。这仅供演示目的,可以轻松地是存储在由 PHP 脚本访问的数据库中的内容。

参见

  • 构建完整的静态网站

实现快速呼叫功能

HTML5 允许开发人员告知浏览器启动应用程序以进行电话呼叫,方式与发邮件类似。本配方将向您展示如何使用 jQuery Mobile 按钮来实现此功能,以便用户点击此按钮时,他们的默认呼叫应用程序将打开,并预填充电话号码。

准备工作

chapter10 文件夹中,创建 recipe-4.html 以供本配方使用。

如何做…

为了让用户能够点击按钮进行电话呼叫,而不必将号码复制粘贴到其通话应用程序中,执行以下简单步骤:

  1. 将以下 HTML 代码添加到 recipe-4.html 中,以重复使用移动网站模板:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: jQuery Mobile Template</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
    </head>
    <body>
    <div data-role="page">
        <header data-role="header">
            <h1>Chapter 10 :: Recipe 4</h1>
        </header>
        <div data-role="content">
    
        </div>
        <footer data-role="footer">
            <h4>The HTML5 tel: attribute allows you to provide interaction to telephone numbers</h4>
        </footer>
    </div>
    </body>
    </html>
    
  2. 在主页的 content 部分中添加一个具有 tel: 属性的按钮,一旦按下即会启动通话应用程序,如下所示:

    <p>This is my simple mobile website, click the button below to call me!</p>
    <a href="tel: 01234 567891" data-role="button">Call Me!</a>
    
  3. 在 Google Chrome 中打开 recipe-4.html,并点击 Call Me! 按钮,将弹出一个警告,提示该网站正在请求打开外部应用程序。在移动浏览器上打开该网页将会打开您设备的默认通话应用程序,允许您使用按钮元素上指定的号码进行通话。

它是如何工作的…

多年来,mailto: 属性一直可用于允许网站打开用户的默认邮件客户端。以下代码行显示了一个示例:

<a href="mailto:name@domain.com">E-Mail Me</a>

HTML5 允许一些额外的属性以类似的方式工作,以允许额外的功能。tel: 属性是其中之一。支持此属性的浏览器将在用户点击链接时打开设备或计算机上安装的默认通话应用程序。请注意,要打开 Skype,一款流行的 VOIP 应用程序,您可能需要使用一个名为 callto: 的替代属性。

另请参阅

  • 实现发送短信功能

实现发送短信功能

上一个配方介绍了如何直接从您的移动网站拨打电话。让用户轻松发送短信也是一个有用的功能。本配方将向您展示如何添加一个按钮,当点击时将在用户设备上打开默认的短信客户端。

准备工作

在开始本章之前创建的 chapter10 文件夹中创建 recipe-5.html

如何做…

允许用户通过您的移动网站快速发送短信消息非常容易。执行以下简单步骤来了解如何:

  1. 再一次,使用本章第一个配方中创建的 jQuery Mobile 模板,在 recipe-5.html 中创建一个简单的移动网站,使用以下代码:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: Recipe 5</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
    </head>
    <body>
    <div data-role="page">
        <header data-role="header">
            <h1>Chapter 10 :: Recipe 5</h1>
        </header>
        <div data-role="content">
    
        </div>
        <footer data-role="footer">
            <h4>Use the HTML5 sms: attribute to open the default SMS client on click</h4>
        </footer>
    </div>
    </body>
    </html>
    
  2. recipe-5.html 的主页的 content 部分中添加以下文本和锚元素:

    <p>This is my simple mobile website, click the button below to send me an SMS message!</p>
    <a href="sms:01234 567891" data-role="button">SMS Me!</a>
    
  3. 在移动设备上打开 recipe-5.html,并点击 SMS Me! 按钮。你的默认短信客户端将打开,接收者字段已经填充了 HTML 中指定的号码。

它是如何工作的…

除了 HTML5 提供的新 tel: 属性外,还提供了 sms: 属性。这将告诉兼容的设备使用指定的电话号码打开默认的短信客户端。以下代码行显示了一个示例:

<a href="sms:01234 567891" data-role="button">SMS Me!</a>

此锚元素还具有 data-role 属性和一个按钮的值,以便 jQuery Mobile 为简单按钮添加适当的样式。

还有更多…

除了电话号码外,还可以指定一些文本自动添加到消息正文中;将锚元素更改如下以添加此功能:

<a href="sms:01234 567891?body=This is some text in the body" data-role="button">SMS Me!</a>

另请参阅

  • 实现快速通话功能

添加移动友好的列表

在此食谱书中,有各种利用 HTML 列表以简单有效的方式呈现数据的食谱。jQuery Mobile 允许开发人员快速将移动友好且易于触摸的列表添加到其 jQuery Mobile 网站中。此食谱为您提供了 jQuery Mobile 提供的更常见类型的列表的多个示例。您可以随时复制和重用这些列表的代码。

准备工作

在之前创建的chapter10文件夹中,创建一个名为recipe-6.html的 HTML 文件。

如何做…

要了解如何添加不同类型的移动友好列表,请执行以下步骤:

  1. 通过将以下 HTML 添加到recipe-6.html中,创建一个基本的 jQuery Mobile 网站:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: Recipe 6</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
    </head>
    <body>
    <div data-role="page">
        <header data-role="header">
            <h1>Chapter 10 :: Recipe 6 :: Lists</h1>
        </header>
        <div data-role="content">
    
        </div>
    </div>
    </body>
    </html>
    
  2. 要创建最常见的列表类型 - 基本链接列表 - 在recipe-6.htmlcontent部分元素中添加以下代码:

    <p>This page contains a selection of list examples for you to reuse at your convenience.</p>
    <h2>Basic linked list</h2>
    <ul data-role="listview">
    <li><a href='#'>Linked Item 1</a></li>
    <li><a href='#'>Linked Item 2</a></li>
    <li><a href='#'>Linked Item 3</a></li>
    </ul>
    
  3. 要创建嵌套列表,请在content部分元素中添加以下 HTML 结构。请注意 HTML 中的注释,为了使此列表正确工作,您需要从 Web 服务器提供recipe-6.html。这样做的原因在本食谱的*如何做...*部分中给出。

    <h2>Nested list</h2>
    <p>Please note that the sub-list will not work if you have opened recipe-6.html directly in a web browser. For the sub-list navigation to work you must serve this HTML file from a web server. i.e. http://localhost/recipe-6.html.</p>
    <ul data-role="listview">
    <li><a href='#'>Top Level Item 1</a></li>
    <li><a href='#'>Top Level Item 2</a></li>
    <li><a href='#'>Top Level Item 3 - With Sub-Level</a>
    <ul data-role="listview">
    <li><a href='#'>Second Level Item 1</a></li>
    <li><a href='#'>Second Level Item 2</a></li>
    </ul>
    </li>
    </ul>
    
  4. 当在列表中显示内容时,可能需要允许用户以多种方式与每个列表项进行交互。jQuery Mobile 允许开发人员轻松地在列表元素旁边添加带有图标的按钮,以增强其功能性。使用data-split-icon属性将此功能添加到列表中,如下面的代码所示:

    <h2>List items with buttons</h2>
    <ul data-role="listview" data-split-icon="delete">
    <li><a href='#'>Jane Doe</a><a href='#'></a></li>
    <li><a href='#'>John Doe</a><a href='#'></a></li>
    <li><a href='#'>James Mathews</a><a href='#'></a></li>
    </ul>
    
  5. 长列表可能会变得难以导航。jQuery Mobile 允许开发人员快速为任何列表添加过滤选项,使用户可以查找他们需要的列表项。要添加具有此功能的列表,请使用以下代码:

    <h2>List with filter</h2>
    <ul data-role="listview" data-filter="true">
    <li><a href='#'>Cat</a></li>
    <li><a href='#'>Dog</a></li>
    <li><a href='#'>Lizard</a></li>
    <li><a href='#'>Rabbit</a></li>
    </ul>
    
  6. 在 Web 浏览器中打开recipe-6.html将呈现一系列列表示例,如下面的屏幕截图所示,您可以在将来的项目中随时使用:如何做…

工作原理…

使用data-role="listview"属性和值,可以将基本的 HTML 列表转换为移动友好的实现。jQuery Mobile 会自动添加样式,就像按钮和其他元素一样。

如果您参考文档(jquerymobile.com/demos/1.2.1/docs/lists/docs-lists.html),您将获得所有可用列表类型的完整列表以及详细示例。

此食谱中使用的大多数示例都很简单且不言自明。食谱的嵌套列表部分具有一些额外的功能,可能不太明显。对于大多数移动设备,屏幕空间非常有限,特别是在纵向模式下。因此,不允许嵌套列表以传统方式展开(向右展开并具有不同的缩进),如下列表所示是没有意义的:

  • 顶层项目 1

  • 顶层项目 2

  • 顶层项目 3 – 带有子级

    • 第二级项目 1

    • 第二级项目 2

为了节省空间并提供更好的用户体验,当您添加嵌套列表时,jQuery Mobile 将创建一个附加页面,并在其中显示子列表项。当用户选择一个带有列表的列表项时,他们将被带到一个额外的页面,其中显示子级项目。

撰写本食谱时,除非从使用 HTTP 的 Web 服务器提供页面,否则为子级项目创建的附加页面不起作用。

在这个示例中更为强大的一项是能够快速为您的列表添加过滤器。只需在 HTML 列表上添加 data-filter="true" 属性和值,jQuery Mobile 就会自动在列表顶部添加过滤器输入框,允许用户过滤掉不需要的列表项。

使用面向触摸的事件

除了 jQuery 提供的典型事件,如 clickhover,jQuery Mobile 还为开发者提供了触摸中心的事件。使用这些事件,可以为您的移动应用程序添加额外的功能,以适应这些额外的用户交互。本食谱提供了许多有用事件的示例,您可以随时重复使用它们。

准备工作

在您的 Web 服务器的 Web 根目录中,创建 recipe-7.htmlrecipe-7.js

如何做…

要了解可用的触摸中心事件及其用法,请执行以下步骤:

  1. 通过将以下 HTML 添加到 recipe-7.html 来创建一个基本的移动网站,其中包含一个空列表。确保根据需要更新所包含库的引用。

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 11 :: Recipe 1</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="includes/jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
        <script src="img/recipe-7.js"></script>
    </head>
    <body>
    <div data-role="page">
        <header data-role="header">
            <h1>Chapter 10 :: Recipe 7 :: jQuery Mobile Touch Events</h1>
        </header>
        <div data-role="content">
            <p>Perform various touch events and watch the output below.</p>
            <ul id="touch-event-response" data-role="listview"></ul>
        </div>
    </div>
    </body>
    </html>
    
  2. recipe-7.js 的顶部,添加以下函数,该函数将在您刚刚在 recipe-7.html 中创建的列表中添加新的列表项:

    function addEvent(msg) {
        var _list = $('#touch-event-response');
        _list.append("<li>" + msg + "</li>");
        _list.listview('refresh');
    }
    
  3. 要在用户轻触时添加新的列表项,请将以下 JavaScript 代码添加到 recipe-7.js 中:

    $(function(){
        $(document).bind('tap', function(){
            addEvent("Tap");
        });
    });
    
  4. 若要监听 taphold 事件并添加新的列表项,请在 $(function(){}) 块中的前一个 .bind() 语句下直接添加以下代码:

    $(document).bind('taphold', function(){
    addEvent("Tap & Hold");
    });
    
  5. 可以使用相同的方法来监听 swipe 事件。追加以下 .bind() 定义:

    $(document).bind('swipe', function(){
    addEvent("Swipe");
    });
    
  6. 若要在用户向左滑动时清除列表,请追加以下 JavaScript 代码:

    $(document).bind('swipeleft', function(){
    $('#touch-event-response').empty();
    });
    
  7. 最后,要在用户更改设备方向时检测,请在 swipeleft 绑定定义之后添加以下代码:

    $(window).bind('orientationchange', function(event){
    addEvent("Orientation changed to: " + event.orientation);
    });
    
  8. 使用支持移动和触摸的设备,打开 recipe-7.html 并执行一系列触摸事件,以查看添加到列表的适当响应。当您向左滑动时,列表应该被清空,当您改变设备的方向时,将添加一个新的列表项,指示新的方向(纵向或横向)。

工作原理…

通过使用以下代码,可以监听 jQuery Mobile 提供的各种事件:

$(document).bind('[EVENT]', function() {
});

要查看可用的完整事件列表,请阅读 jQuery Mobile 网站上的文档(jquerymobile.com/demos/1.2.1/docs/api/events.html),其中提供了详细的带有示例的全面列表。

在这个食谱中,addEvent()函数接受一个字符串,它将附加到在recipe-7.html中创建的简单移动网站中的列表元素中。当您使用 JavaScript 操作 jQuery Mobile 列表时,您必须调用refresh方法,以确保样式重新应用到新添加的元素上。如以下代码所示:

var _list = $('#touch-event-response');
_list.append("<li>" + msg + "</li>");
_list.listview('refresh');

知道用户何时更改设备的方向可以帮助重新整理页面上的元素,以改善用户体验。通过 jQuery Mobile,这非常容易实现。只需绑定到orientationchange事件,并准备event对象的orientation属性来确定新的方向是什么,如下面的代码所示:

$(window).bind('orientationchange', function(event){
addEvent("Orientation changed to: " + event.orientation);
});

请注意,与本食谱中的其他事件不同,这个事件被绑定到window而不是document,因为document不知道浏览器或设备的方向。

另请参阅

  • 第二章,通过使用 jQuery 事件与用户进行交互

创建兼容移动设备的表单

jQuery Mobile 提供了一系列与 jQuery UI 类似但针对移动设备进行优化的表单组件。本食谱提供了更常用的表单元素的示例,以便您随时重复使用。

准备工作

在您之前创建的chapter10文件夹中,创建 recipe-8.html

如何操作…

要了解 jQuery Mobile 提供了哪些表单元素以及如何使用它们,请执行以下每一步:

  1. 创建一个简单的 jQuery Mobile 网站来存放所有的示例。将以下 HTML 代码添加到recipe-8.html

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: Recipe 8</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
    </head>
    <body>
    <div data-role="page" id="home">
        <header data-role="header" data-theme="b">
            <h1>Chapter 10 :: Recipe 8</h1>
        </header>
        <div data-role="content">
        </div>
    </div>
    </body>
    </html>
    
  2. 在您刚刚添加的content区域元素内添加以下代码,以创建一系列文本输入元素:

    <h1>Text Input</h1>
    <div data-role="fieldcontain">
    <label for="textInput">Text input:</label>
    <input type="text" name="textInput" id="textInput" />
    </div>
    <div data-role="fieldcontain">
    <label for="textArea">Text area:</label>
    <textarea id="textArea" name="textArea"></textarea>
    </div>
    <div data-role="fieldcontain">
    <label for="textSearch">Text search:</label>
    <input type="text" name="textSearch" id="textSearch" data-type="search">
    </div>
    
  3. 要创建两种不同类型的选择菜单,请在文本输入下添加以下代码:

    <h1>Select Menu</h1>
    <div data-role="fieldcontain">
    <label for="simpleSelect">Simple select:</label>
    <select id="simpleSelect">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
           <option value="3">Option 3</option>
    </select>
    </div>
    <div data-role="fieldcontain">
    <label for="customSelect">Custom select:</label>
    <select id="customSelect" data-native-menu="false">
    <option value="1">Option 1</option>
    <option value="2">Option 2</option>
          <option value="3">Option 3</option>
    </select>
    </div>
    
  4. 要创建复选框和单选按钮,请使用以下代码:

    <h1>Selection</h1>
    <h2>Checkboxes</h2>
    <fieldset data-role="controlgroup">
    <input type="checkbox" name="checkbox-1" id="checkbox-1">
    <label for="checkbox-1">Option 1</label>
    <input type="checkbox" name="checkbox-2" id="checkbox-2">
    <label for="checkbox-2">Option 2</label>
    <input type="checkbox" name="checkbox-3" id="checkbox-3">
    <label for="checkbox-3">Option 3</label>
    </fieldset>
    <h2>Radio buttons</h2>
    <fieldset data-role="controlgroup">
    <input type="radio" name="radio-1" id="radio-1">
    <label for="radio-1">Option 1</label>
    <input type="radio" name="radio-1" id="radio-2">
    <label for="radio-2">Option 2</label>
    <input type="radio" name="radio-1" id="radio-3">
    <label for="radio-3">Option 3</label>
    </fieldset>
    <h2>Inline selection</h2>
    <fieldset data-role="controlgroup" data-type="horizontal">
    <input type="checkbox" name="checkbox-1" id="checkbox-4">
    <label for="checkbox-4">Option 1</label>
    <input type="checkbox" name="checkbox-2" id="checkbox-5">
    <label for="checkbox-5">Option 2</label>
    <input type="checkbox" name="checkbox-3" id="checkbox-6">
    <label for="checkbox-6">Option 3</label>
    </fieldset>
    <fieldset data-role="controlgroup" data-type="horizontal">
    <input type="radio" name="radio-2" id="radio-4">
    <label for="radio-4">Option 1</label>
    <input type="radio" name="radio-2" id="radio-5">
    <label for="radio-5">Option 2</label>
    <input type="radio" name="radio-2" id="radio-6">
    <label for="radio-6">Option 3</label>
    </fieldset>
    
  5. 最后,要创建一些附加元素——开关和滑块——请添加以下代码:

    <h1>Additional</h1>
    <div data-role="fieldcontain">
    <label for="switch">Switch:</label>
    <select id="switch" data-role="slider">
    <option value="1">On</option>
          <option value="0">Off</option>
    </select>
    </div>
    <div data-role="fieldcontain">
    <label for="slider">Slider:</label>
    <input type="number" data-type="range" name="slider" id="slider" value="50" min="0" max="100" data-highlight="true">
    </div>
    
  6. 在网络浏览器中打开recipe-8.html将呈现出一系列不同的表单元素。然后你可以轻松地选择并重复使用任何元素类型的代码,如下面的屏幕截图所示:如何操作…

它是如何工作的…

本食谱中使用的每种类型的 jQuery Mobile 元素在以下各节中都有详细解释。

文本输入

jQuery Mobile 提供了不同的文本输入元素。典型的文本输入和文本区域元素可以通过在div元素内添加标签和inputtextarea元素,并将其放在具有fieldcontain类的div元素内来轻松创建,如以下代码所示:

<div data-role="fieldcontain">
<label for="textInput">Text input:</label>
<input type="text" name="textInput" id="textInput" />
</div>

要创建搜索输入,只需将data-type="search"添加到input元素中。这会向input元素添加一个搜索图标,并在用户输入一些文本后提供清除按钮。

选择菜单

此教程中提供的两个选择菜单在外观上看起来相同。当选择第一个简单示例时,您将获得一个下拉列表,看起来像在一个普通非移动优化网站上的典型选择菜单。

第二个示例添加了额外的data-native-menu="false"属性,一旦单击,将提供不同的选择菜单。这个额外的菜单使得使用触摸界面更容易进行选择。以下屏幕截图对两种类型的选择菜单进行了比较:

Select menu

复选框和单选按钮

使用具有data-role="controlgroup"属性的fieldset元素,非常容易创建复选框和单选按钮,如下面的代码片段所示:

<fieldset data-role="controlgroup">
<input type="checkbox" name="checkbox-1" id="checkbox-1">
<label for="checkbox-1">Option 1</label>
<input type="checkbox" name="checkbox-2" id="checkbox-2">
<label for="checkbox-2">Option 2</label>
<input type="checkbox" name="checkbox-3" id="checkbox-3">
<label for="checkbox-3">Option 3</label>
</fieldset>

要创建一组单选按钮,可以重复使用前面的代码,将type属性更改为radio并确保它们的name属性中都有相同的值。

除了这些界面元素外,jQuery Mobile 还可以提供嵌入式等效物。只需将data-type="horizontal"属性添加到fieldset元素上,就可以获得复选框或单选按钮的嵌入式版本。

额外信息

作为此教程的最后两个元素提供的是一个开关和一个滑块。switch元素实质上是一个只有两个选项的选择菜单,但以更适合触摸的方式呈现。通过将data-type="range"属性添加到数字输入中(如下所示的代码),可以创建slider元素,允许用户轻松地在表单上输入和更改数字值:

<div data-role="fieldcontain">
<label for="slider">Slider:</label>
<input type="number" data-type="range" name="slider" id="slider" value="50" min="0" max="100" data-highlight="true">
</div>

还有更多…

作为此教程的一部分提供的所有示例都显示为默认尺寸。当默认尺寸稍大时,jQuery Mobile 为所有表单元素提供额外的较小尺寸。

要使用小型等效物,将属性data-mini="true"添加到需要较小尺寸的元素中。

建立完整的注册和登录系统

本教程向您展示如何使用 jQuery Mobile 和 PHP 与 MySQL 数据库从头开始创建简单的注册和登录系统。这个教程将为本章的下一个教程中的完整网络应用程序奠定基础。

准备工作

您应该已经准备好一个可用于完成此教程的 PHP 和 MySQL 服务器。在 Web 服务器的 Web 根目录中,创建index2.htmlscript2.js,它们将保存应用程序的主要功能。

如何去做…

要创建完整的注册和登录系统,请确保您仔细遵循以下每个说明:

  1. 将以下 HTML 代码添加到index2.html中,以创建一个简单的 jQuery Mobile 网站和主页:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Chapter 10 :: Register & Login</title>
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="includes/jquery-mobile/jquery.mobile.min.css" />
        <script src="img/jquery.min.js"></script>
        <script src="img/jquery.mobile.min.js"></script>
        <script src="img/script2.js"></script>
        <link rel="stylesheet" href="styles.css" />
    </head>
    <body>
    <div data-role="page" id="home">
        <header data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Home Page</h1>
        </header>
        <div data-role="content">
            <p>Welcome to my community.</p>
            <a data-role="button" href="#login">Login</a>
            <a data-role="button" data-theme="a" href="#register">Register</a>
        </div>
    </div>
    </body>
    </html>
    
  2. 使用以下 HTML 将登录页面添加到index2.html

    <div data-role="page" id="login" data-title="Login">
        <header data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Login</h1>
        </header>
        <div data-role="content">
            <div data-role="fieldcontain">
                <label for="login-username">Username:</label>
                <input type="text" name="username" id="login-username" value="" />
            </div>
            <div data-role="fieldcontain">
                <label for="login-password">Password:</label>
                <input type="password" name="password" id="login-password" value="" />
            </div>
            <button data-role="button" id="login-account" data-theme="a">Login</button>
            <p>Don't have an account? <a href='#register'>Register</a>.</p>
        </div>
    </div>
    
  3. 还可以使用以下 HTML 代码创建注册页面,确保将页面代码添加到index2.html中 HTML 文档的body部分中:

    <div data-role="page" id="register" data-title="Register">
        <header data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Register</h1>
        </header>
        <div data-role="content">
            <div data-role="fieldcontain">
                <label for="register-username">Username:</label>
                <input type="text" name="username" id="register-username" value="" />
            </div>
            <div data-role="fieldcontain">
                <label for="register-password">Password:</label>
                <input type="password" name="password" id="register-password" value="" />
            </div>
            <div data-role="fieldcontain">
                <label for="register-passwordagain">Password Again:</label>
                <input type="password" name="register-passwordagain" id="register-passwordagain" value="" />
            </div>
            <button data-role="button" id="register-account" data-theme="a">Register</button>
            <p>Already have an account? <a href='#login'>Login</a>.</p>
        </div>
    </div>
    
  4. 最后一个要添加的页面是成员页面。使用以下 HTML 代码创建此页面:

    <div data-role="page" id="member">
        <header data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Member's Page</h1>
        </header>
        <div data-role="content">
            <p>You're logged in.</p>
            <button data-role="button" data-theme="a" id="logout">Logout</button>
        </div>
    </div>
    
  5. 使用以下 SQL 代码,在您的 MySQL 数据库中创建名为chapter10的数据库和名为user的表:

    SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO";
    SET time_zone = "+00:00";
    
    --
    -- Database: `chapter10`
    --
    CREATE DATABASE `chapter10` DEFAULT CHARACTER SET latin1 COLLATE latin1_swedish_ci;
    USE `chapter10`;
    
    -- --------------------------------------------------------
    
    --
    -- Table structure for table `user`
    --
    
    CREATE TABLE IF NOT EXISTS `user` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      `username` varchar(128) DEFAULT NULL,
      `password` varchar(512) DEFAULT NULL,
      UNIQUE KEY `id` (`id`),
      UNIQUE KEY `username` (`username`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=latin1 AUTO_INCREMENT=8;
    
  6. 在您的 Web 服务器的 Web 根目录中创建connect.db.php并添加以下 PHP 代码来连接到chapter10数据库。如有需要,请更新数据库用户名和密码。

    <?php
    $mysqli = new mysqli("localhost", "root", "", "chapter10");
    if ($mysqli->connect_errno) {
        die("Failed to connect to MySQL: (" . $mysqli->connect_errno . ") " . $mysqli->connect_error);
    }
    $pwsalt = "TH1SISF0RCHAPTER10";
    
  7. 为了能够向user表中添加新用户,创建register.php并在您的 Web 服务器的 Web 根目录中添加以下 PHP 代码:

    <?php
    require_once("connect.db.php");
    $username = isset($_POST['username']) ? strtolower($_POST['username']) : "";
    $password = isset($_POST['password']) ? $_POST['password'] : "";
    $passwordAgain = isset($_POST['passwordagain']) ? $_POST['passwordagain'] : "";
    
    $response = array(
        "success" => false,
        "errors" => array()
    );
    
    if (strlen($username) < 3 || strlen($username) > 32) {
        $response["errors"]["username"] = "Username must be between 3 and 64 characters in length";
    } else {
        $query = "SELECT `id` FROM `user` WHERE `username` = ? LIMIT 1";
        $stmt = $mysqli->stmt_init();
        if ($stmt->prepare($query)) {
            $stmt->bind_param("s", $username);
            if ($stmt->execute()) {
                $stmt->store_result();
                if ($stmt->num_rows > 0) {
                    $response["errors"]["username"] = "Username has already been taken";
                }
            } else {
                $response["errors"]["username"] = "Could not execute query";
            }
        } else {
            $response["errors"]["username"] = "Could query database for existing usernames";
        }
        $stmt->close();
    }
    
    if (strlen($password) < 6 || strlen($password) > 32) {
        $response["errors"]["password"] = "Password must be between 6 and 32 characters in length";
    }
    if ($password != $passwordAgain) {
        $response["errors"]["passwordagain"] = "Passwords must match";
    }
    
    if (empty($response["errors"])) {
        $query = "INSERT INTO `user` (`username`, `password`) VALUES (?, ?)";
        $stmt = $mysqli->stmt_init();
        if ($stmt->prepare($query)) {
            $password = crypt($password, $pwsalt);
            $stmt->bind_param("ss", $username, $password);
            if ($stmt->execute()) {
                $stmt->close();
                $response["success"] = true;
            } else {
                $response["errors"]["username"] = "Could not execute query";
            }
        } else {
            $response["errors"]["username"] = "Could not insert new user, please try again";
        }
    }
    $mysqli->close();
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  8. 为了允许用户使用他们新创建的帐户登录,创建login.php并在您的 Web 服务器的 Web 根目录中添加以下 PHP 代码:

    <?php
    session_start();
    require_once("connect.db.php");
    $username = isset($_POST['username']) ? strtolower($_POST['username']) : "";
    $password = isset($_POST['password']) ? $_POST['password'] : "";
    
    $response = array(
        "success" => false,
        "error" => "",
        "user" => array()
    );
    
    $query = "SELECT `id` FROM `user` WHERE `username` = ? AND `password` = ? LIMIT 1";
    $stmt = $mysqli->stmt_init();
    if ($stmt->prepare($query)) {
        $password = crypt($password, $pwsalt);
        $stmt->bind_param("ss", $username, $password);
        if ($stmt->execute()) {
            $res = $stmt->get_result();
            if ($res->num_rows > 0) {
                $row = $res->fetch_assoc();
                $response["success"] = true;
                $_SESSION['uid'] = $response["user"]["id"] = $row["id"];
                $_SESSION['username'] = $response["user"]["username"] = $username;
            } else {
                $response["error"] = "Incorrect username or password";
            }
        } else {
            $response["error"] = "Could not execute query";
        }
    } else {
        $response["error"] = "Could not query database";
    }
    $stmt->close();
    $mysqli->close();
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  9. 为了实现登出功能,在与login.php相同的目录中创建logout.php并添加以下代码:

    <?php
    session_start();
    $response = array(
        "success" => false,
        "error" => ""
    );
    if (isset($_SESSION["uid"]) && isset($_SESSION["username"])) {
        $_SESSION = array();
        session_destroy();
     $response["success"] = true;
    } else {
        $response["success"] = false;
        $response["error"] = "Not logged in";
    }
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  10. 为了允许用户提交他们的注册信息,将以下 JavaScript 代码添加到script2.js的 jQuery 加载块($(function(){});)中:

    $('#register-account').click(function(){
            $('.input-error').remove();
            var data = {
                username: $('#register-username').val(),
                password: $('#register-password').val(),
                passwordagain: $('#register-passwordagain').val()
            };
            $.ajax({
                type: 'POST',
                url: 'register.php',
                data: data,
                beforeSend: function() {
                    $.mobile.loading('show');
                },
                success: function(data) {
                    $.mobile.loading('hide');
                    if (data.success) {
                        $.mobile.showPageLoadingMsg("b", "Registration successful! You may now login.", true);
                    } else {
                        $.each(data.errors, function(key, value){
                            $('#register-' + key).parent().after("<div class='input-error'>" + value + "</div>");
                        });
                    }
                }
            });
        });
    
  11. 当用户尝试从登录页面登录时,将以下 JavaScript 代码添加到script2.js中刚刚为注册添加的代码下面,以对登录进行反应:

    $('#login-account').click(function(){
            var data = {
                username: $('#login-username').val(),
                password: $('#login-password').val()
            };
            $.ajax({
                type: 'POST',
                url: 'login.php',
                data: data,
                beforeSend: function() {
                    $.mobile.loading('show');
                },
                success: function(data) {
                    $.mobile.loading('hide');
                    if (data.success) {
                        $.mobile.showPageLoadingMsg("b", "Login Successful", true);
                        localStorage.setItem("user", JSON.stringify(data.user));
                        $.mobile.changePage("#member");
                    } else {
                        $.mobile.showPageLoadingMsg("b", data.error, true);
                    }
                }
            });
        });
    
  12. 为了让用户能够单击登出按钮并注销,将以下代码添加到script2.js中:

     $('#logout').click(function(){
            $.ajax({
                type: 'POST',
                url: 'logout.php',
                beforeSend: function() {
                    $.mobile.loading('show');
                },
                success: function(data) {
                    $.mobile.loading('hide');
                    if (data.success) {
                        localStorage.removeItem("user");
                        $.mobile.changePage("#home");
                    } else {
                        $.mobile.showPageLoadingMsg("b", data.error, true);
                    }
                }
            });
        });
    
  13. 为了在用户尝试导航到此页面时检查用户是否已登录,添加以下代码以防止访问成员页面:

    $(document).bind("pagebeforechange", function(e, data) {
            if (typeof data.toPage === "string") {
                var urlObject = $.mobile.path.parseUrl(data.toPage);
                if (urlObject.hash.search(/^#member/) !== -1) {
                    if (getUser() === false) {
                        e.preventDefault();
                        $.mobile.showPageLoadingMsg("b", "You must be logged in to access this page", true);
                        setTimeout(function(){
                            $.mobile.hidePageLoadingMsg();
                            $.mobile.changePage("#home");
                        }, 1500);
                    }
                }
            }
        });
    
  14. 前面的代码使用getUser()函数来确定用户是否已登录。将以下函数添加到script2.js的末尾,确保它添加在$(function(){});块之外:

    function getUser() {
        var user = localStorage.getItem("user");
        if (user == null) {
            return false;
        } else {
            return JSON.parse(user);
        }
    }
    
  15. 为了对 Web 服务器的错误消息添加一些基本样式,创建一个名为styles.css的文件,并添加以下 CSS 代码:

    .input-error {
        position: absolute;
        font-size: 10px;
        color: #ff0800;
    }
    
  16. 访问提供index2.html的 Web 服务器将允许您注册一个帐户。如果尝试访问成员页面而未登录,您将收到一条消息,告诉您必须登录,然后将被发送回主页。

工作原理...

本配方中创建的每个代码部分都在接下来的各节中有详细说明。

HTML

index2.html中的 HTML 创建了一个简单的 jQuery Mobile 网站,包括以下四个页面:

  • 主页

  • 注册页面

  • 登录页面

  • 成员页面

主页提供了指向登录和注册页面的链接,每个页面分别链接到对方。成员页面有一个登出按钮,允许用户一旦获得访问成员页面后退出登录。HTML 代码简单,每个元素都在本章的先前配方中有详细说明。

SQL

作为此配方的一部分提供的 SQL 代码可用于创建所需的chapter10数据库和存储用户帐户的user表。

PHP

此配方中创建了四个 PHP 文件。第一个是connect.db.php,它建立与 MySQL 数据库的连接,并包含在其他三个 PHP 文件中。PHP mysqli类用于在此配方中的 PHP 文件中连接和查询 MySQL 数据库。您可以在 PHP.net(www.php.net/mysqli)上找到有关此类的更多信息。

register.php文件通过 POST 请求接收一组值。这些值如下所示:

  • 用户名

  • 密码

  • 再次输入密码

PHP 脚本对所有三个输入执行基本验证,以确保指定的用户名长度在 3 到 32 个字符之间,并且已提供的密码至少为 6 个字符长。它还确保两个密码匹配,并查询数据库以确保请求的用户名尚未被占用。

如果通过了所有验证,将在数据库中插入新用户,然后允许此用户使用指定的详细信息登录。重要的是要注意,密码使用 PHP crypt()函数进行加密,使用默认设置。这是一种简单的加密方法,在生产环境中应使用更强大的加密技术。

login.php脚本通过 POST 请求接收用户名和密码,并查询用户表以查看是否有任何匹配的用户凭据;如果有,将为该用户创建一个 PHP 会话,并将用户对象返回给客户端。

logout.php脚本只是销毁 PHP 会话(如果存在),将用户注销。

这些 PHP 脚本中的每一个都以本配方中多次使用的标准格式返回数据。在每个脚本的顶部,创建一个数组,如以下代码所示:

$response = array(
    "success" => false,
    "errors" => array()
);

如果脚本成功执行且无需输出错误,则成功值更改为true,而errors数组保持为空。对于register.php脚本,当输入之一未通过验证时,将返回一个具有与输入匹配的键的关联数组。以下是一个示例:

$response["errors"]["username"] = "Username has already been taken";

这样,客户端上的 JavaScript 就知道在哪个输入框下放置错误消息,使用户更容易理解他们需要进行哪些更改。

输出响应数组时,它被转换为 JSON 对象,并使用 PHP header()函数适当地设置内容类型和字符集,如以下代码所示:

header("Content-Type: application/json; charset=UTF-8");
echo json_encode($response);

JavaScript

此示例中使用的 JavaScript 简单而且并不新奇。为注册、登录和注销按钮创建了三个 click 事件处理程序。提供给这些事件处理程序的回调函数从相关表单收集数据,并使用 jQuery $.ajax() 函数向 register.phplogin.phplogout.php 脚本分别发出 POST 请求。AJAX 与 jQuery 在 第三章 使用 AJAX 和 JSON 加载和操作动态内容 中已经广泛讨论过。

对于每个 AJAX 请求,beforeSend() 函数用于打开并显示一个旋转图像,向用户指示正在进行请求。在 AJAX 请求成功时,此旋转图像将被移除。以下代码展示了这一点:

$.ajax({
type: 'POST',
url: 'register.php',
data: data,
beforeSend: function() {
$.mobile.loading('show');
},
success: function(data) {
       $.mobile.loading('hide');
// -- HIDDEN CODE
}
});

此外,正如下面的代码所示,在每个 AJAX 请求的 success() 函数中,使用 $.mobile.showPageLoadingMsg() 函数向用户显示消息,无论是出错还是提供关于成功注册的信息:

$.mobile.showPageLoadingMsg("b", "Registration successful! You may now login.", true);

函数的第一个参数是主题,第二个参数是您希望显示的消息,将第三个参数设置为 true 将移除旋转图像,仅显示简单的文本消息。

如前所述,login.php 脚本返回一个表示新登录用户的对象。由于客户端 JavaScript 无法感知 PHP 会话,因此需要将此用户对象存储在本地,以便客户端知道已登录的用户。为此,使用本地存储,如下所示的代码行所示:

localStorage.setItem("user", JSON.stringify(data.user));

本地存储只允许存储字符串,但我们需要存储整个对象。为了解决这个问题,我们将对象转换为 JSON 字符串,然后当从本地存储中检索时再次转换为对象。上述示例使用 JSON.stringify() 函数将用户对象转换为字符串,并将其存储在名为 user 的本地存储中。

然后使用 getUser() 函数来检索并将字符串值转换为对象,如果当前没有登录用户,则返回 false

function getUser() {
    var user = localStorage.getItem("user");
    if (user == null) {
        return false;
    } else {
        return JSON.parse(user);
    }
}

当向销毁服务器会话的 logout.php 脚本发起的 AJAX 调用成功时,也会使用 localStorage.removeItem("user") 来移除客户端上的用户对象。

此示例中 JavaScript 的最后一个元素是限制访问成员页面。请注意,使用客户端代码强制执行的任何限制都可以被具有适当知识的任何用户绕过。这种类型的客户端限制仅用于增强用户体验,并且服务器端始终要求阻止用户无法执行的任何操作。

构建动态移动网站示例中,使用了 jQuery Mobile pagebeforechange 事件来检测用户尝试访问某个页面。在此示例中,使用相同的功能来捕获用户尝试访问成员页面时的情况。然后使用getUser()函数来确定用户是否已登录。如果他们没有登录,则阻止他们导航到成员页面,并在告知他们必须登录才能访问成员页面后将其发送回主页。

还有更多...

目前,要提交网站上的任何表单,用户需要点击或按下相关按钮。为了改进这一点,如果用户可以从表单中的任何输入框中按下 Enter 键或移动设备的等效按钮,则会更有益。

另请参阅

  • 构建动态移动网站

构建完整的移动 Web 应用程序

此示例向您展示了如何创建一个简单但完整的 Web 应用程序,允许注册用户编写可以在所有设备上访问的笔记。笔记应用程序在之前的登录和注册示例的基础上进行扩展,允许已登录用户创建和管理笔记或待办事项清单。

准备工作

在开始此示例之前,请确保您已完成之前的示例构建完整的注册和登录系统。您仍然需要一个运行 PHP 和 MySQL 的 Web 服务器才能完成此示例。

如何做...

要创建一个可以在所有移动设备和桌面设备上访问的完整移动 Web 应用程序,请执行以下步骤:

  1. 要存储用户创建的笔记,需要另一个数据库表。使用以下 SQL 代码在chapter10数据库中创建一个名为note的表:

    CREATE TABLE IF NOT EXISTS `note` (
      `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
      `user_id` bigint(20) unsigned NOT NULL,
      `text` varchar(2048) DEFAULT NULL,
      `added` datetime DEFAULT NULL,
      UNIQUE KEY `id` (`id`),
      KEY `user_id` (`user_id`)
    ) ENGINE=InnoDB  DEFAULT CHARSET=latin1;
    
    ALTER TABLE `note`
      ADD CONSTRAINT `note_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
    
  2. 虽然在index2.html中定义的大多数页面保持不变,但我们需要更新成员页面,以便有一个按钮将用户带到他们当前的笔记。使用以下 HTML 在index2.html中更新成员页面:

    <div data-role="page" id="member">
        <div data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Member's Page</h1>
        </div>
        <div data-role="content">
            <p>Welcome <strong><span class="username"></span></strong>, what would you like to do?</p>
            <a href="#notes" data-role="button" data-inline="true" data-icon="arrow-r">View Notes</a>
            <button data-role="button" data-theme="a" id="logout" data-inline="true" data-icon="delete">Logout</button>
        </div>
    </div>
    
  3. 现在,我们需要创建新按钮将用户带到的笔记页面。使用以下 HTML 代码在index2.html中的成员页面之后创建笔记页面:

    <div data-role="page" id="notes">
        <div data-role="header" data-theme="b">
            <h1><a href="#home" data-role="button" data-icon="home" data-iconpos="notext" data-inline="true"></a> Your Notes</h1>
        </div>
        <div data-role="content">
            <h1>Your Notes <a href="#add-note" data-icon="plus" data-role="button" data-inline="true">Add note</a><a href="#member" data-theme="e" data-icon="back" data-role="button" data-inline="true">Back</a></h1>
            <ul data-role="listview" data-filter="true" id="current-notes" data-icon="delete"></ul>
        </div>
    </div>
    
  4. 有了笔记页面,用户将能够查看他们当前的笔记,因此需要一种方法来创建新的笔记。使用以下 HTML 代码,将创建笔记页面添加到index2.html中:

    <div data-role="page" id="add-note" data-title="Add new note">
        <div data-role="header" data-theme="b">
            <h1>Add new note</h1>
        </div>
        <div data-role="content">
            <textarea id="note-text"></textarea>
            <div class='input-error' style="display: none;" id="note-error"></div>
            <div class="actions">
                <button data-role="button" id="save-new-note" data-theme="a" data-icon="check" data-inline="true">Save</button>
                <a href="#notes" data-role="button" data-theme="e" data-icon="delete" data-inline="true">Cancel</a>
            </div>
        </div>
    </div>
    
  5. 通过创建一个新的数据库表并更新用于附加功能的 HTML UI,我们现在需要创建提供与数据库交互的 PHP。为了让用户创建新的笔记,在您的 Web 服务器的 Web 根目录中创建一个名为addNote.php的文件,并插入以下代码:

    <?php
    session_start();
    
    require_once("connect.db.php");
    $text = isset($_POST['text']) ? $_POST['text'] : "";
    
    $response = array(
        "success" => false,
        "error" => "",
        "note" => array()
    );
    
    if (!isset($_SESSION['uid'])) {
        $response["error"] = "You must be logged in to add a new note";
    } else if (strlen($text) <= 0 || strlen($text) > 1024) {
        $response["error"] = "A note must be between 1 and 1024 characters in length";
    } else {
        $query = "INSERT INTO `note` (`user_id`, `text`, `added`) VALUES (?, ?, ?)";
        $stmt = $mysqli->stmt_init();
        if ($stmt->prepare($query)) {
            $now = date("Y-m-d H:i:s");
            $stmt->bind_param("sss", $_SESSION['uid'], $text, $now);
            if ($stmt->execute()) {
                $stmt->close();
                $response["success"] = true;
                $response["note"] = array(
                    "id" => $mysqli->insert_id,
                    "text" => $text,
                    "added" => $now
                );
            } else {
                $response["error"] = "Could not execute query";
            }
        } else {
            $response["error"] = "Could not insert new note, please try again";
        }
    }
    $mysqli->close();
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  6. 要使用用户当前的笔记填充笔记页面,我们需要能够从数据库中检索笔记。创建一个名为getNotes.php的文件,并添加以下 PHP 代码:

    <?php
    session_start();
    require_once("connect.db.php");
    
    $response = array(
        "success" => false,
        "error" => "",
        "notes" => array()
    );
    
    if (!isset($_SESSION['uid'])) {
        $response["error"] = "You must be logged in to add a new note";
    } else {
        $query = "SELECT * FROM `note` WHERE `user_id` = ? ORDER BY `added` DESC";
        $stmt = $mysqli->stmt_init();
        if ($stmt->prepare($query)) {
            $stmt->bind_param("s", $_SESSION['uid']);
            if ($stmt->execute()) {
                $res = $stmt->get_result();
                $response["success"] = true;
                if ($res->num_rows > 0) {
                    while ($row = $res->fetch_assoc()) {
                        $response["notes"][] = array(
                            "id" => $row["id"],
                            "text" => $row["text"],
                            "added" => $row["added"]
                        );
                    }
                }
            } else {
                $response["error"] = "Could not execute query";
            }
        } else {
            $response["error"] = "Could not query database";
        }
        $stmt->close();
    }
    $mysqli->close();
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  7. 用户还需要能够删除不需要的笔记。为此,在您的 Web 服务器的 Web 根目录中创建一个名为deleteNote.php的文件,并添加以下代码:

    <?php
    session_start();
    
    require_once("connect.db.php");
    $id = isset($_POST['id']) ? (int)$_POST['id'] : 0;
    
    $response = array(
        "success" => false,
        "error" => ""
    );
    
    if (!isset($_SESSION['uid'])) {
        $response["error"] = "You must be logged in to delete a note";
    } else if ($id <= 0) {
        $response["error"] = "Invalid note ID specified";
    } else {
        $query = "DELETE FROM `note` WHERE `id` = ?";
        $stmt = $mysqli->stmt_init();
        if ($stmt->prepare($query)) {
            $stmt->bind_param("i", $id);
            if ($stmt->execute()) {
                $stmt->close();
                $response["success"] = true;
            } else {
                $response["error"] = "Could not execute query";
            }
        } else {
            $response["error"] = "Could not insert new note, please try again";
        }
    }
    $mysqli->close();
    header("Content-Type: application/json; charset=UTF-8");
    echo json_encode($response);
    
  8. 当所有后端代码都就位后,我们现在可以添加 JavaScript 来将用户界面与这些后端代码联系起来。首先,我们需要对前一个示例中 script2.js 中的原始 JavaScript 代码进行一些更改。在 script2.js 的顶部,但仍然在 jQuery 的加载完成块 $(function(){}); 内部,添加以下一行代码:

    var _currentNotes = $('#current-notes');
    
  9. 在注销 AJAX 调用的 success() 函数中,在 $.mobile.changePage("#home"); 之前添加以下一行代码:

    _currentNotes.data("initialized", false);
    
  10. pagebeforechange 事件处理程序中,我们需要添加一些代码,以便我们可以在成员页面中显示当前登录用户的用户名。更新代码如下,添加 $('.username').html(user.username);

    if (urlObject.hash.search(/^#member/) !== -1) {
    var user = getUser();
    if (user === false) {
          e.preventDefault();
    $.mobile.showPageLoadingMsg("b", "You must be logged in to access this page", true);
    setTimeout(function(){
    $.mobile.hidePageLoadingMsg();
    $.mobile.changePage("#home");
    }, 1500);
    } else {
          $('.username').html(user.username);
    }
    }
    
  11. 在进行所需的 JavaScript 更新后,我们需要添加额外的功能。为了允许用户添加一个新的笔记,将以下代码插入到 script2.js 中,以捕获当用户点击保存笔记按钮时的情况:

    $('#save-new-note').click(function(){
            $('#note-error').hide();
            var _text = $('#note-text');
            $.ajax({
                type: 'POST',
                url: 'addNote.php',
                data: {
                    'text': _text.val()
                },
                beforeSend: function() {
                    $.mobile.loading('show');
                },
                success: function(data) {
                    $.mobile.loading('hide');
                    if (data.success) {
                        _text.val("");
                        _currentNotes.prepend(createNoteItem(data.note));
                        //If the list view has already been initialized then we need to refresh it
                        if (_currentNotes.hasClass('ui-listview')) {
                            _currentNotes.listview('refresh');
                        }
                        $.mobile.changePage("#notes");
                    } else {
                        $('#note-error').html(data.error).fadeIn();
                    }
                }
            });
        });
    
  12. 要填充笔记页面上当前可用的所有笔记,我们需要向 pagebeforechange 事件处理程序添加一些附加功能。将代码更新如下(一些代码已隐藏以示说明):

    $(document).bind("pagebeforechange", function(e, data) {
     if (typeof data.toPage === "string") {
      var urlObject = $.mobile.path.parseUrl(data.toPage);
      if (urlObject.hash.search(/^#member/) !== -1) {
       //HIDDEN CODE – DO NOT REMOVE
      } else if(urlObject.hash.search(/^#notes/) !== -1) {
       if (_currentNotes.data("initialized") != true) {
        e.preventDefault();
        _currentNotes.empty();
        _currentNotes.data("initialized", true);
        $.ajax({
         type: 'GET',
         url: 'getNotes.php',
         beforeSend: function() {
          $.mobile.loading('show');
         },
         success: function(data) {
          $.mobile.loading('hide');
          if (data.success) {
           for (var i = 0; i < data.notes.length; i++) {
            _currentNotes.append(createNoteItem(data.notes[i]));
           }
           //If the list view has already been initialized then we need to refresh it
           if (_currentNotes.hasClass('ui-listview')) {                                       _currentNotes.listview("refresh");
            }
           $.mobile.changePage("#notes");
           } else {
            alert(data.error);
           }
          }
        });
       }
      }
     }
    });
    
  13. 列出的当前可用笔记需要是可点击的,以允许用户删除它们。在 script2.js 中添加以下代码来监听当前笔记列表项的点击,然后发出 AJAX 调用到 deleteNote.php 脚本:

    $(document).on('click', '.delete-note', function(){
            var _listItem = $(this).closest('li');
            var id = _listItem.data("id");
            var response = confirm("Are you sure you want to delete this note?");
            if (response) {
                $.ajax({
                    type: 'POST',
                    url: 'deleteNote.php',
                    data: {
                        'id': id
                    },
                    beforeSend: function() {
                        $.mobile.loading('show');
                    },
                    success: function(data) {
                        $.mobile.loading('hide');
                        if (data.success) {
                            _listItem.remove();
                            _currentNotes.listview("refresh");
                        } else {
                            alert(data.error);
                        }
                    }
                });
            }
        });
    
  14. 最后,在 jQuery 的加载完成块 ($(function(){});) 外部添加以下函数,用于构建一个笔记的列表项:

    function createNoteItem(note) {
        return "<li data-id='" + note.id + "'><a href='javascript:void(0);' class='delete-note'>" + note.text + "</a></li>";
    }
    
  15. 通过访问从 Web 服务器提供的 index2.html,您将能够注册一个帐户,然后登录,就像之前的示例一样。一旦登录,点击查看笔记按钮将带您到一个空列表的页面。点击添加笔记按钮以添加新的笔记。一旦添加了新的笔记,您将会被带回到当前笔记列表,并显示您的新笔记。您可以通过点击它并确认要删除它来删除此笔记。您可以在已登录的会话跨多个设备上访问您的笔记。操作方法…

工作原理…

此示例代码的每个部分在接下来的几节中都有详细说明。

HTML

此示例中的 HTML 代码添加了一些额外的页面,以便已登录用户可以创建笔记并查看以前的笔记。笔记页面使用了一个带有过滤器的列表视图,该过滤器在本章的添加移动友好的列表示例中已经展示过。

SQL

在此示例中的简单 SQL 代码创建了一个名为 note 的附加表,用于存储所有用户的笔记。还定义了 note 表上的 user_id 字段和 user 表上的 id 字段之间的外键关系。

PHP

此示例中的所有 PHP 脚本都使用了与前一个示例相同的数据库连接文件和结构。为此示例创建了另外三个 PHP 脚本,如下所示:

  • addNote.php: 此脚本接受一个带有笔记文本的 POST 请求。然后,它使用 PHP $_SESSION超全局变量检查当前是否有已登录用户。如果有已登录用户,则验证提供的文本以确保其长度在 0 到 1024 个字符之间。如果是,则将其插入到数据库中,并附加用户 ID 和添加日期。为了获取新创建的笔记项的数据库 ID,使用了$mysqli->insert_id。然后,此 ID 在返回到请求脚本的note对象中。

  • deleteNote.php: 此脚本与addNote.php类似,接受一个带有笔记 ID 的 POST 请求。它还检查是否有已登录用户,如果有,则使用简单的 SQL 查询从数据库中删除指定的笔记。

  • getNotes.php: 通过使用已登录用户的 ID,从数据库中检索该用户的所有笔记,并将其转换为 JSON,以便可以使用 JavaScript 填充列表元素。

    注意

    如果 PHP 脚本需要访问会话数据,则必须在脚本顶部调用session_start()函数,即在任何其他代码之前。

JavaScript

script2.js的顶部,声明了_currentNotes变量,如下代码所示:

var _currentNotes = $('#current-notes');

这是因为在整个代码中需要当前笔记列表,并且通过重用相同的变量,jQuery 不会被强制多次重新选择元素。

为了动态填充#current-notes列表元素,使用了pagebeforechange事件。通过if...else语句的额外检查,可以确定用户何时尝试转到笔记页面,如下一行代码所示:

else if(urlObject.hash.search(/^#notes/) !== -1) {

当用户使用_currentNotes.data("initialized")访问此页面时,可以检查列表是否已填充。如果 initialized data属性已经设置,则已填充,并且无需再次从数据库中获取所有数据。如果 initialized 属性尚未设置为true,则会发出 AJAX 调用来收集当前的笔记并填充列表,如下代码所示:

_currentNotes.empty();
    _currentNotes.data("initialized", true);
    $.ajax({
     type: 'GET',
     url: 'getNotes.php',
     beforeSend: function() {
      $.mobile.loading('show');
     },
     success: function(data) {
      $.mobile.loading('hide');
      if (data.success) {
       for (var i = 0; i < data.notes.length; i++) {
        _currentNotes.append(createNoteItem(data.notes[i]));
       }
       //If the list view has already been initialized then we need to refresh it
if (_currentNotes.hasClass('ui-listview')) {                                       _currentNotes.listview("refresh");
        }
       $.mobile.changePage("#notes");
       } else {
        alert(data.error);
       }
      }
       });

_currentNotes.data("initialized", true);行用于将 initialized 属性设置为true,以便当用户返回页面时,脚本知道不要重新收集数据。然后,向getNotes.php脚本发出 AJAX 调用,并为每个返回的note对象使用createNoteItem()函数创建一个新的列表项。

如果 jQuery Mobile 已经初始化了#current-notes列表(意味着用户之前已经访问过该页面),则需要刷新列表视图。这是使用以下代码完成的,该代码取自 AJAX 调用的success()函数:

//If the list view has already been initialized then we need to refresh it
if (_currentNotes.hasClass('ui-listview')) {
   _currentNotes.listview("refresh");
      }

此食谱中的创建笔记和删除笔记功能非常简单,并且在本书中已经多次介绍过。简而言之,当单击保存笔记按钮或笔记列表项时,将分别向addNote.phpdeleteNote.php脚本发出 AJAX 调用。

在添加新笔记时,使用以下代码将新笔记项添加到当前笔记列表并将用户返回到笔记页面:

_currentNotes.prepend(createNoteItem(data.note));
//If the list view has already been initialized then we need to refresh it
if (_currentNotes.hasClass('ui-listview')) {
_currentNotes.listview('refresh');
}
$.mobile.changePage("#notes");

删除笔记时,使用以下代码来删除已删除的笔记项:

var _listItem = $(this).closest('li');
_listItem.remove();

由于 jQuery Mobile 为 DOM 添加了许多额外的元素来对列表进行样式设置,因此在单击锚点(在列表内)时,使用closest()函数来查找列表元素。另外,请注意使用$(document).on('click', '.delete-note'而不是$('.delete-note').click(),以便为动态添加的元素触发click事件处理程序。这在第二章中有介绍,通过使用 jQuery 事件与用户进行交互

还有更多...

本食谱提供了一个非常简单的完整移动 Web 应用程序的示例。有许多方面可以改进,但为了确保本食谱尽可能简洁,它们被省略了。

有一个可以改进的要素是deleteNote.php脚本的安全性方面。目前该脚本允许已登录的用户删除任何笔记,只要指定正确的 ID 即可。有一些知识的用户可以通过指定他们选择的笔记 ID 来劫持请求,潜在地删除另一个用户的笔记。通过检查指定的笔记 ID 是否属于已登录用户,可以很容易地避免这种情况。

请参阅

  • 第二章,通过使用 jQuery 事件与用户进行交互

  • 构建完整的注册和登录系统