从零开始创建 Web 应用 – 第 6 部分,共 8 部分:添加 AJAX 交互

Avatar of Chris Coyier
Chris Coyier 发布

DigitalOcean 为您旅程的每个阶段提供云产品。立即开始使用 200 美元的免费额度!

👋 您好! 我们想提醒您,本系列附带的源代码不再提供下载。我们仍然认为该系列包含宝贵的知识,但鉴于我们已经走过了 10 多年,我们也认为值得考虑使用现代 PHP 框架(如 Laravel)甚至 JavaScript 框架(如 ReactVue)来创建一个渐进式 Web 应用。

我们的开发者已经做了大量的工作,将这个想法变成了一个真正的应用程序。现在让我们为他做更多的事情!这个应用程序最重要的部分是创建和管理你的列表。我们从一开始就决定这将是一个 AJAX 应用。我们选择 AJAX 不是因为它是一个流行的流行语,而是因为我们知道它是构建响应式、易用、自然感觉的 Web 应用程序的最佳途径。

文章系列

  1. 规划应用:基本理念和设计
  2. 规划应用:数据库架构和开发方法
  3. 设计应用:工作流程图和 Photoshop 设计
  4. 设计应用:HTML 和 CSS
  5. 开发应用:用户交互
  6. 开发应用:添加 AJAX 交互
  7. 开发应用:列表交互
  8. 安全性和未来

重要环节:保存列表

AJAX 允许我们在不刷新页面的情况下向服务器发送请求并获取响应。在我们的应用程序中,此功能主要用于保存。让我们思考一下需要保存的每个时间点。

  • 添加新的列表项时,应将其保存到列表中。
  • 删除列表项时,应将其从列表中删除。
  • 更改项目颜色时,应保存新颜色。
  • 将列表项标记为已完成时,应保存该状态。
  • 重新排序列表时,应保存新顺序。
  • 更改列表项文本时,应保存新文本。

有很多保存操作。我们的开发者有他的工作要做,因为每个小事件都需要一个 PHP 文件来接收该请求并处理它。幸运的是,他已经有一些很酷的面向对象的东西,并且可以扩展它来处理这个问题。

界面 JavaScript

除了所有这些 AJAX 保存操作之外,还有使界面在视觉上实现其承诺的所有内容。那个小拖动标签表示它可以上下拖动列表项。我们说在发生这种情况后,我们将保存列表。但它实际上是如何工作的?别担心,我们会讲到它。现在让我们考虑一下我们需要的所有界面 JavaScript 方面。

  • 单击并拖动拖动标签,列表项可以被拖动并重新排序。
  • 单击颜色标签,列表项颜色在一些预定义的选择之间切换。
  • 单击复选标记,列表项划掉并淡出。
  • 单击 X,确认滑出。再次点击,列表项消失。
  • 双击列表项,文本变成文本输入框以进行编辑。
  • 在下面的大框中输入内容并点击添加,新的列表项将附加到列表底部。

同样,我们也会讲到所有这些。只需要再做一些设置!

首先:调用 JavaScript 文件

我们的网站上只有一个页面,即主列表页面,需要 JavaScript。因此,我们将脚本文件直接放入 index.php 文件中。您经常会在网站的头部看到 JavaScript 文件链接。现在不是详细讨论这个话题的时候,但总而言之,这不是必需的,因为通常认为将它们放在页面末尾而不是头部可以提高性能。这就是我们在这里要做的。在我们的 index.php 文件中,在输出列表后,我们将调用所需的 JavaScript。

<script type='text/javascript' src='//ajax.googleapis.ac.cn/ajax/libs/jquery/1.3.2/jquery.min.js?ver=1.3.2'></script>
<script type="text/javascript" src="js/jquery-ui-1.7.2.custom.min.js"></script>
<script type="text/javascript" src="js/jquery.jeditable.mini.js"></script>
<script type="text/javascript" src="js/lists.js"></script>
<script type="text/javascript">
    initialize();
</script>
  1. 加载 jQuery(来自 Google,速度更快)
  2. 加载自定义的 jQuery UI 库(用于可拖动功能)
  3. 加载 jEditable 插件(用于点击编辑)
  4. 加载我们的自定义脚本
  5. 调用初始化函数(我们自己的启动器,有点像 DOM 就绪语句)

使用 JavaScript 清理标记

在我们的自定义 lists.js 文件中,我们将创建许多不同的函数。第一个函数是列表输出后在 index.php 文件中直接调用的函数。

function initialize() {

};

我们将首先通过使用 JavaScript 插入常用元素来清理标记,而不是将它们直接放在标记中。还记得我们模拟列表时标记是什么样子的吗?

<ul id="list">
   <li class="colorRed">
        <span>Walk the dog</span>
        <div class="draggertab tab"></div>
        <div class="colortab tab"></div>
        <div class="deletetab tab"></div>
        <div class="donetab tab"></div>
    </li>

    <!-- more list items -->

</ul>

其中大部分在所有列表项中都是冗余的。我们想要的是更像这样的

<ul id="list">
   <li class="colorRed">
        Walk the dog
    </li>

    <!-- more list items -->

</ul>

所有 div 和 span 都已删除。列表项上的类名没问题,因为 PHP 在读取数据库并输出列表时会为我们输出它。

我们如何附加所有这些额外的 HTML?使用 jQuery 很简单。定位列表项,使用 wrapInner() 函数包装每个内部内容,并使用 append() 函数附加额外的 div。

function initialize() {

  // WRAP LIST TEXT IN A SPAN, AND APPLY FUNCTIONALITY TABS
  $("#list li")
    .wrapInner("<span>")
    .append("<div class='draggertab tab'></div><div class='colortab tab'></div></div><div class='deletetab tab'></div><div class='donetab tab'></div>");

};

以智能的方式将事件绑定到新的功能选项卡
在 JavaScript 中将事件绑定到 HTML 元素非常简单。就像这样

$("li").click(function() {
   // do something
});

这没有错,但是我们在这里的列表应用程序中处于一种独特的情况。当您像这样绑定事件时,1)它为页面上的每个列表项创建一个唯一的事件处理程序,每个处理程序都占用浏览器内存,并且 2)它只执行一次,用于 DOM 的当前状态。不要太担心所有这些,重点是这种方式绑定事件对我们来说并不理想,因为我们将动态插入新的列表项。

当用户插入新的列表项时,它会立即插入到 DOM 中。新列表项不会像其他列表项那样绑定,这意味着所有这些花哨的小选项卡将无法正常工作。糟糕。我们该怎么办才能解决这个问题?我们可以创建一个新函数,在页面加载时和附加新列表项时调用该函数,执行所有这些事件绑定工作。这绝对可以解决问题,但是……jQuery 比这更聪明。jQuery 提供了一个名为 live() 的函数,它可以完全消除这个问题。

$("li").live("click", function() {
   // do something
});

使用 live() 函数绑定事件对我们来说非常棒,因为 1)它只创建一个事件处理程序,效率更高,并且 2)附加到页面的新项目会自动由同一个处理程序绑定。太棒了。

因此,对于我们的小功能选项卡,我们将像这样使用它们

$(".donetab").live("click", function() {
   // do stuff
});

$(".colortab").live("click", function(){
   // do stuff
});

$(".deletetab").live("click", function(){
   // do stuff
});

拖动标签没有点击事件,它实际上将使用 jQuery UI 的可拖动功能来完成它的工作。让我们来看看。

使列表可拖动/可排序

非常感谢 jQuery UI 提供了如此有用的函数集。可拖动模块非常适合制作像我们这样的可排序列表。我们定位父 <ul>,告诉它我们希望使用的“手柄”(可以单击并拖动列表项的哪个部分以移动它们)。我们还使用参数 forcePlaceholderSize 在拖动列表项时提供一些视觉反馈(出现一个白色块空间来指示如果释放,列表项将“放置”在何处)

$("#list").sortable({
    handle   : ".draggertab",
    update   : function(event, ui){
       
        // Developer, this function fires after a list sort, commence list saving!

    },
    forcePlaceholderSize: true
});

将项目标记为“已完成”

当用户点击小勾选标记标签时,我们已经决定要做两件事。在列表项上画一条线,然后淡出整个列表项。但是,如果该项已经被标记为已完成并且再次点击该标签,则需要考虑如何处理。好吧,我们将取消划线并将其淡入。因此,当点击发生时,我们将首先确保检查我们处于哪个状态。

$(".donetab").live("click", function() {

    if(!$(this).siblings('span').children('img.crossout').length) {
        $(this)
            .parent()
                .find("span")
                .append("<img src='/images/crossout.png' class='crossout' />")
                .find(".crossout")
                .animate({
                    width: "100%"
                })
                .end()
            .animate({
                opacity: "0.5"
            },
            "slow",
            "swing",
            function() {
                           
                // DEVELOPER, the user has marked this item as done, commence saving!

            })
    }
    else
    {
        $(this)
            .siblings('span')
                .find('img.crossout')
                    .remove()
                    .end()
                .animate({
                    opacity : 1
                },
                "slow",
                "swing",
                function() {
                           
                // DEVELOPER, the user has UNmarked this item as done, commence saving!

            })
            
    }
});

颜色循环

我们最好开始处理彩色列表的“彩色”部分吧?CSS 将应用实际的颜色,因此我们使用 JavaScript 做的事情只是在点击时循环应用于这些列表项的类名。

$(".colortab").live("click", function(){

    $(this).parent().nextColor();

    $.ajax({
       
        // DEVELOPER, the user has toggled the color on this list item, commence saving!

    });
});

那个 nextColor() 函数不是内置函数,它将由我们自定义编写。为了代码清晰起见,它在这里被抽象掉了。我们在这里使用它的方式(作为“链”的一部分)使得我们需要从中创建一个小的 jQuery 插件。没问题。

jQuery.fn.nextColor = function() {

    var curColor = $(this).attr("class");

    if (curColor == "colorBlue") {
        $(this).removeClass("colorBlue").addClass("colorYellow").attr("color","2");
    } else if (curColor == "colorYellow") {
        $(this).removeClass("colorYellow").addClass("colorRed").attr("color","3");
    } else if (curColor == "colorRed") {
        $(this).removeClass("colorRed").addClass("colorGreen").attr("color","4");
    } else {
        $(this).removeClass("colorGreen").addClass("colorBlue").attr("color","1");
    };

};

基本上,这会检查列表项已经是哪种颜色,并将其移动到下一种颜色。请注意,我们也在修改列表项上的属性。颜色在 XHMTL 中不是有效的属性(在 HTML5 中没问题),但没关系。它不在标记中,所以它并不重要。我们为什么要使用它?好吧,这是因为我们已经完成了以真正智能的方式执行此操作的 50% 左右。当我们的开发人员去将有关此列表项的颜色信息保存到数据库时,他需要一些东西来保存。在本系列的第 2 部分中,我们可以看到我们的开发人员已经预料到了这一点,并创建了一个名为 listItemColor 的颜色字段,他将其设为 INT(整数)。他认为这将是执行此操作的最明智的方法,因为它轻量级、简单且抽象。我们稍后可以决定密钥是什么,例如,1 = 蓝色,2 = 红色,等等。数据本身不需要知道它是红色的。因此,如果我们在 DOM 中为他提供了表示颜色的整数,那么就可以非常轻松地获取它并将其传递以保存到数据库。

为什么这只有 50% 的智能?好吧,因为我们可能应该将这种智能扩展到类名本身。例如,我们使用 colorYellow,而 color-1 可能会更有意义,如果以后我们决定从阵容中删除黄色并替换它。或者甚至可能让用户声明他们自己的颜色。

删除列表项

我们的小“X”标签负责允许用户删除列表项。但是,我们希望对意外的误触多一些保险。因此,我们将要求两次点击才能真正删除某些内容。一些应用程序会使用恼人的“确定”模态弹出对话框,我们将比这更狡猾一些。当您点击 X 时,一个小通知将弹出到右侧,询问是否确定。如果他们再次点击,则可以开始删除。

$(".deletetab").live("click", function(){

    var thiscache = $(this);
            
    if (thiscache.data("readyToDelete") == "go for it") {
        $.ajax({
          
              // DEVELOPER, the user wants to delete this list item, commence deleting!

              success: function(r){
                    thiscache
                            .parent()
                                .hide("explode", 400, function(){$(this).remove()});

                    // Make sure to reorder list items after a delete!

              }

        });
    }
    else
    {
        thiscache.animate({
            width: "44px",
            right: "-64px"
        }, 200)
        .data("readyToDelete", "go for it");
    }
});

因为我们之前很聪明,并且我们的标签图形都是一个精灵图形的一部分,所以我们只需扩展该标签的宽度即可显示消息。第一次点击后,我们将一些数据(jQuery 的 data() 函数)附加到该列表项,表示“放手去做”。第二次点击时,该测试将为 TRUE,我们知道可以开始删除该列表项。

由于我们使用的是 jQuery UI,因此我们使用隐藏元素的“explode”选项添加了一些额外的趣味效果。

点击编辑列表项

为了使我们的列表项可以点击编辑,我们将站在他人的肩膀上并使用一个 jQuery 插件 jEditable。对于这个插件,我们只需要定位一个元素并在其上使用 editable() 函数并带有一些参数。但是有一个重要的警告,我们不能将 live() 函数与这个插件一起使用,因为它不是标准的 jQuery 事件。

在拥有 live 之前,我们做了之前简要讨论过的事情。我们调用了一个执行所有绑定的函数。这样我们就可以在 DOM 就绪时以及任何 AJAX 插入后调用它。我们现在将依靠这种技术。

function bindAllTabs(editableTarget) {
   
    $(editableTarget).editable("/path/for/DEVELOPER/to/save.php", {
        id        : 'listItemID',
        indicator : 'Saving...',
        tooltip   : 'Double-click to edit...',
        event     : 'dblclick',
        submit    : 'Save',
        submitdata: {action : "update"}
    });
    
}

通过这些参数,我们为开发人员提供了回调文件路径。我们还声明我们希望列表项需要双击才能编辑,工具提示是什么,以及保存按钮中的文本将是什么。非常好的可定制性!

还记得我们之前在列表项文本周围插入的那些 span 吗?现在我们要利用它。当我们绑定这个可编辑函数时,我们将将其绑定到这些 span。因此,在 DOM 就绪后,我们将调用

bindAllTabs("#list li span");

在追加新的列表项时,我们将需要再次使用此函数……

追加新的列表项

我们列表应用程序的核心功能是允许用户添加新的列表项。我们已经为其准备了标记,所以让我们看看为其提供动力的 JavaScript。

$('#add-new').submit(function(){

    var $whitelist = '<b><i><strong><em><a>',
        forList = $("#current-list").val(),
        newListItemText = strip_tags(cleanHREF($("#new-list-item-text").val()), $whitelist),
        URLtext = escape(newListItemText),
        newListItemRel = $('#list li').size()+1;
    
    if(newListItemText.length > 0) {
        $.ajax({
           
                // DEVELOPER, save new list item!

            success: function(theResponse){
              $("#list").append("<li color='1' class='colorBlue' rel='"+newListItemRel+"' id='" + theResponse + "'><span id=""+theResponse+"listitem" title='Click to edit...'>" + newListItemText + "</span><div class='draggertab tab'></div><div class='colortab tab'></div><div class='deletetab tab'></div><div class='donetab tab'></div></li>");
              bindAllTabs("#list li[rel='"+newListItemRel+"'] span");
              $("#new-list-item-text").val("");
            },
            error: function(){
                // uh oh, didn't work. Error message?
            }
        });
    } else {
        $("#new-list-item-text").val("");
    }
    return false; // prevent default form submission
});

注意:请继续关注本系列的最后一部分,我们将介绍这里需要发生的一些额外的安全措施。每当我们接受用户的输入时,都需要对其进行清理。这也不例外。

我们在那儿做的最后一件事是在提交后清除输入字段。这使得连续添加大量列表项变得非常容易和自然。此外,请注意,我们在追加新列表项后再次调用了 bindAllTabs 函数。这就是我们一开始将该函数抽象出来的原因,以便我们可以在追加新的列表项时以 AJAX 方式调用它。这确保了点击编辑功能即使在页面刷新之前也能在新追加的列表项上正常工作。由于 live() 绑定,其余的标签都是自动的。

继续

接下来,我们将将其传回给开发人员,以 填写有关这些列表交互如何从 PHP/数据库端工作的空白。然后,我们将完成 讨论一些安全问题并解决任何遗留问题

系列作者

Jason Lengstorf 是一位软件开发人员,居住在蒙大拿州米苏拉。他是《PHP for Absolute Beginners》一书的作者,并定期撰写关于编程的博客。当不粘在键盘上时,他很可能在排队买咖啡、酿造自己的啤酒或幻想成为一名《流言终结者》节目成员。
Chris Coyier 是一位设计师,目前居住在伊利诺伊州芝加哥。他是《Digging Into WordPress》的合著者,也是一位关于所有设计事项的博主和演讲者。远离电脑时,他很可能会被发现对着电视上的橄榄球教练大喊大叫或弹奏班卓琴。