Flask Mega-Tutorial V2.0 第20章:一些JavaScript Magic

最近在Flask Web Development作者博客看到第二版Flask Mega-Tutorial已在2017年底更新,现翻译给大家参考,希望帮助大家学习flask。

这是Flask Mega-Tutorial系列的第二十章,我将添加一个功能,当您将鼠标悬停在用户的昵称上时,会弹出一个漂亮的窗口。

供您参考,以下是本系列文章的列表。

注意1:如果您正在寻找本教程的旧版本,请在此处

注意2:如果您想在此博客上支持我的工作,或者只是没有耐心等待每周的文章,我将提供完整的本教程版本,打包成电子书或视频集。欲了解更多信息,请访问courses.miguelgrinberg.com

如今,构建一个Web应用而不使用JavaScript是不可能的。 您一定知道,JavaScript是Web浏览器中可本地运行的唯一语言。 第14章中,您看到了我在Flask模板中添加一个简单的JavaScript的启用链接,以提供博客文章的实时语言翻译。在本章中,我将深入探讨该主题,并向您展示另一个有用的JavaScript技巧,以使应用更有趣并吸引用户。

社交网站的常见用户交互模式是,当您将鼠标悬停在用户名上的任意位置(在页面上出现的任何位置)时,会在弹出面板中显示该用户的主要信息。如果您从未注意到这一点,请访问Twitter,Facebook,LinkedIn或任何其他主要的社交网络,当您看到用户名时,只需将鼠标指针停留在其上方几秒钟,便会看到弹出窗口。本章将专门为Microblog构建该功能,您可以在下面查看其预览:

用户弹出

本章的GitHub链接为:BrowseZipDiff


服务器端支持

在深入研究客户端之前,让我们先进行一些服务器工作,这些工作是支持这些用户弹出窗口所必需的。用户弹出窗口的内容将由新路由返回,这将是现有个人主页路由的简化版本。这是视图函数:

app/main/routes.py用户弹出视图函数。

@bp.route('/user/<username>/popup')
@login_required
def user_popup(username):
    user = User.query.filter_by(username=username).first_or_404()
    form = EmptyForm()
    return render_template('user_popup.html', user=user, form=form)

该路由将被附加到URL /user/<username>/popup,并将仅加载所请求的用户,然后使用其渲染到模板。该模板是个人主页的模板的简化版本:

app/templates/user_popup.html:用户弹出模板。

<table class="table">
    <tr>
        <td width="64" style="border: 0px;"><img src="{{ user.avatar(64) }}"></td>
        <td style="border: 0px;">
            <p><a href="{{ url_for('main.user', username=user.username) }}">{{ user.username }}</a></p>
            <small>
                {% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
                {% if user.last_seen %}
                <p>{{ _('Last seen on') }}: {{ moment(user.last_seen).format('lll') }}</p>
                {% endif %}
                <p>
                    {{ _('%(count)d followers', count=user.followers.count()) }}, 
                    {{ _('%(count)d following', count=user.followed.count()) }
                }</p>
                {% if user != current_user %}
                    {% if not current_user.is_following(user) %}
                    <p>
                        <form action="{{ url_for('main.follow', username=user.username) }}" method="post">
                            {{ form.hidden_tag() }}
                            {{ form.submit(value=_('Follow'), class_='btn btn-default btn-sm') }}
                        </form>
                    </p>
                    {% else %}
                    <p>
                        <form action="{{ url_for('main.unfollow', username=user.username) }}" method="post">
                            {{ form.hidden_tag() }}
                            {{ form.submit(value=_('Unfollow'), class_='btn btn-default btm-sm') }}
                        </form>
                    </p>
                    {% endif %}
                {% endif %}
            </small>
        </td>
    </tr>
</table>

当用户将鼠标指针悬停在用户名上方时,我将在以下各节中编写的JavaScript代码将调用此路由。客户端将服务器端返回的响应中的html内容显示在弹出窗口中。当用户将鼠标移开时,弹出窗口将被删除。听起来很简单,对吧?

如果您想了解弹出窗口是什么样的,现在可以运行应用,转到任何用户的个人主页,然后在地址栏的URL中追加 /popup 以查看全屏版本的弹出窗口内容。

Bootstrap Popover组件简介

第11章中,我向您介绍了Bootstrap框架,它是创建美观网页的便捷方法。到目前为止,我仅使用了该框架的一小部分。Bootstrap捆绑了许多常见的UI元素,所有元素均在 https://getbootstrap.com 的Bootstrap文档中提供了演示和示例。这些组成部分之一是Popover(弹窗),在文档中将其描述为“用于容纳辅助信息的小的覆盖窗口”。正是我需要的!

大多数bootstrap组件都是通过HTML标记定义的,该标记引用Bootstrap CSS的定义内容来添加漂亮的样式。 一些高级的组件还需要JavaScript。 应用程序在网页中包含这些组件的标准方式是在适当的位置添加HTML,然后为需要脚本支持的组件调用JavaScript函数,以便初始化或激活它。 popover组件确实需要JavaScript的支持。

执行弹出框的HTML部分非常简单,您只需定义将触发弹出框出现的元素。就我而言,这将处理每个博客帖子中的可点击用户名。 app/templates/_post.html子模板具有已定义的用户名:

            <a href="{{ url_for('main.user', username=post.author.username) }}">
                {{ post.author.username }}
            </a>

现在根据popover文档,我需要在页面上出现的每个链接上调用JavaScript函数popover(),这将初始化弹出窗口。初始化调用接受许多配置弹出窗口的选项,包括传递想要在弹出窗口中显示的内容,以及使用什么方法触发弹出窗口出现或消失(单击,悬停在元素上等),如果内容是纯文本或HTML,那么在文档中可以找到更多的选项。不幸的是,在阅读完这些信息之后,我的疑惑更多了,因为这个组件看起来并没有按照我需要的方式工作。 以下是我实现此功能需要解决的问题列表:

  • 面中将有许多用户名链接,每个显示的博客帖子都对应一个。渲染页面后,我需要一种方法可以从JavaScript中找到所有这些链接,以便可以将它们初始化为弹出窗口。
  • Bootstrap文档中的popover示例都将目标HTML元素的data-content属性设置为popover的内容,因此当触发悬停事件时,Bootstrap需要做的只是显示弹出窗口。这对我来说要做的就不止这些了,因为我想对服务器进行Ajax调用以获取内容,并且只有当收到服务器的响应时,我才希望弹出窗口出现。
  • 使用“悬停”模式时,只要将鼠标指针停留在目标元素内,弹出窗口便会保持可见状态。当您将鼠标移开时,弹出窗口将消失。这具有糟糕的副作用,即如果用户要将鼠标指针移到弹出窗口本身中,则弹出窗口将消失。我需要找出一种方法来将悬停行为扩展为包含弹出窗口,以便用户可以移动到弹出窗口中,例如,单击那里的链接。

实际上,在使用基于浏览器的应用程序时,事情变得非常复杂的情况并不少见。您必须非常特别地考虑DOM元素如何相互交互,并使它们以给用户带来良好体验的方式运行。

在页面加载完成后执行函数

显然,一旦每个页面加载,我将需要运行一些JavaScript代码。我将要运行的函数将搜索页面中用户名的所有链接,并使用Bootstrap中的popover组件进行配置。

jQuery JavaScript库作为Bootstrap的依赖项加载,因此我将利用它。使用jQuery时,您可以用$(...)封装来注册一个函数,函数将会在页面加载完毕后运行。 我可以将它添加到app/templates/base.html模板中,以便它可以在应用程序的每个页面上运行:

app/templates/base.html:页面加载后运行函数。

...
<script>
    // ...

    $(function() {
        // write start up code here
    });
</script>

如您所见,我已经在<script>元素中添加了我的启动函数,而第14章中,我已在该元素中定义了translate()函数。

使用选择器查找DOM元素

我的第一个问题是创建一个JavaScript函数,该函数可以找到页面中的所有用户链接。该函数将在页面完成加载后运行,并在完成后为所有页面配置悬停和弹出行为。现在我将集中精力寻找链接。

如果您从第14章回忆起,实时翻译中涉及的HTML元素具有唯一的ID。例如,ID = 123的帖子添加了属性id="post123"。然后使用jQuery,该表达式$('#post123')在JavaScript中用于在DOM中定位此元素。$()函数非常强大,并且具有相当复杂的查询语句来搜索基于CSS选择器的DOM元素。

我用于翻译功能的选择器旨在使用id属性查找一个具有唯一标识符的特定元素。 识别元素的另一种方法是使用class属性,它可以分配给页面中的多个元素。 例如,我可以用class="user_popup"标记所有的用户链接,然后我可以通过$('.user_popup')获取这些元素的列表(CSS选择器中,#前缀代表查询id属性,.前缀代表查询class属性)。 在本处,返回值将是具有该类的所有元素的集合。

弹窗和DOM

通过使用Bootstrap文档中的popover示例并在浏览器的调试器中检查DOM,我确定Bootstrap将popover组件创建为DOM中目标元素的同级组件。正如我上面提到的,这会影响悬停事件的行为,一旦用户将鼠标从<a>链接移到弹出窗口本身,它将触发“鼠标移出” 事件。

我可以扩展悬停事件以包含弹出窗口,就是将弹出窗口作为目标元素的子元素,这样悬停事件就会继承。 通过查看文档中的弹出选项,可以通过在container选项中传递父元素来完成此操作。

将popover作为悬停元素的子元素可以很好地用于按钮或一般的<div><span>元素,但在我的情况下,popover的target将是显示用户名的可点击链接的<a> 元素。 使popover成为<a>元素的子元素的问题是,弹出窗口将获得<a>父元素的链接行为。 最终的结果是这样的:

        <a href="..." class="user_popup">
            username
            <div> ... popover elements here ... </div>
        </a>

为避免弹出窗口位于<a>元素内,我将使用另一个技巧。我将把<a>元素包装在一个<span>元素内,然后将hover事件和popover与<span>关联。产生的结构为:

        <span class="user_popup">
            <a href="...">
                username
            </a>
            <div> ... popover elements here ... </div>
        </span>

<div><span>元素是不可见的,因此它们是用于帮助组织和构建DOM的重要元素。 div元素是块元素,有点像HTML文档中的段落,而<span>元素是行内元素,它可以用于字词级别。 本处,我决定使用<span>元素,因为我要包装的<a>元素也是行内元素。

因此,我将继续进行重构,将我的app/templates/_post.html子模板重构为包含<span>元素:

...
                {% set user_link %}
                    <span class="user_popup">
                        <a href="{{ url_for('main.user', username=post.author.username) }}">
                            {{ post.author.username }}
                        </a>
                    </span>
                {% endset %}
...

如果您想知道HTML的popover元素在哪里,好消息是我不必为此担心。 当我在刚刚创建的<span>元素上调用popover()初始化函数时,Bootstrap框架会为我动态地插入弹出组件。

悬停事件

如上所述,Bootstrap中popover组件使用的悬停行为不够灵活,无法满足我的需求,但是如果您查看trigger选项的文档,则“ hover”只是可能的值之一。一个引起我注意的是manual模式,在这种模式下,可以通过JavaScript调用手动显示或删除弹出窗口,这种模式可以让我自由地实现悬停逻辑,所以我将使用该选项并实现我自己的悬停事件处理程序,并以我需要的方式工作。

所以我的下一步是将一个“hover”事件附加到页面中的所有链接。 使用jQuery,可以通过调用element.hover(handlerIn, handlerOut)将悬停事件附加到任何HTML元素。 如果在元素集合上调用这个函数,jQuery方便地将事件附加到所有元素上。 这两个参数是两个函数,分别在用户将鼠标指针移入和移出目标元素时调用对应的函数。

app/templates/base.html:悬停事件。

    $(function() {
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = $(event.currentTarget);
            },
            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
            }
        )
    });

事件参数是一个事件对象,它包含了一些有用的信息。 在本处,我使用event.currentTarget来提取事件的目标元素。

鼠标进入受影响的元素后,浏览器立即调度悬停事件。对于弹出式窗口,您只想在鼠标停留在元素上的一小段时间后激活,以便当鼠标指针短暂经过元素但不停留在该元素上时,弹出窗口不会闪烁。由于该事件不支持延迟,因此这是我自己需要实现的另一件事。因此,我将在“ mouse in”事件处理程序中添加一秒钟的计时器:

app/templates/base.html:悬停延迟。

    $(function() {
        var timer = null;
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = $(event.currentTarget);
                timer = setTimeout(function() {
                    timer = null;
                    // popup logic goes here
                }, 1000);
            },
            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
            }
        )
    });

setTimeout()函数在浏览器环境中可用。它需要两个参数,一个函数和一个时间(以毫秒为单位)。setTimeout()的作用是在给定的延迟后被调用。因此我添加了一个暂时为空的函数,该函数将在调度悬停事件后一秒钟调用。由于JavaScript语言中的闭包机制,该函数可以访问在外部作用域中定义的变量,例如elem

我将timer对象存储在hover()调用之外定义的timer变量中,以使timer对象也可以被“mouse out”处理程序访问。 我需要这么做的原因是为了获得良好的用户体验。 如果用户将鼠标指针移动到其中一个用户链接中,并在移动它之前停留了半秒钟,我不希望该timer继续运行并调用显示弹出窗口的函数。 所以我的鼠标移出事件处理程序检查是否有一个活动的timer对象,如果有,就取消它。

Ajax请求

Ajax请求不是一个新话题,因为我在第14章中已将其作为实时语言翻译功能的一部分进行了介绍。使用jQuery时,$.ajax()函数将发送异步请求到服务器。

我要发送到服务器的请求将具有/ user / <用户名> / popup URL,我已在本章开始时将其添加到应用程序中。该请求的响应将包含我需要在弹出窗口中插入的HTML。

关于此请求,我的直接问题是知道username需要包含在URL中的值是什么。事件处理函数中的鼠标是通用的,它将针对页面中找到的所有用户链接运行,因此该函数需要根据其上下文确定用户名。

elem变量包含悬停事件中的目标元素,它是包裹<a>元素的<span>元素。 为了提取用户名,我可以从<span>开始浏览DOM,移至第一个子元素,即<a>元素,然后从中提取文本,这就是在网址中要使用的用户名 。 使用jQuery的DOM遍历函数,可以很简单地做到:

elem.first().text().trim()

应用于DOM节点的first()函数返回其第一个子节点。 text()函数返回节点的文本内容。 该函数不会对文本进行任何修剪,例如,如果在一行中有<a>,在下一行中有文本,在另一行中有</a>text()将返回文本周围的所有空白。 为了消除所有空白并只留下文本,我使用了名为trim()的JavaScript函数。

这就是我能够向服务器发出请求所需的全部信息:

app/templates/base.html:XHR请求。

    $(function() {
        var timer = null;
        var xhr = null;
        $('.user_popup').hover(
            function(event) {
                // mouse in event handler
                var elem = $(event.currentTarget);
                timer = setTimeout(function() {
                    timer = null;
                    xhr = $.ajax(
                        '/user/' + elem.first().text().trim() + '/popup').done(
                            function(data) {
                                xhr = null
                                // create and display popup here
                            }
                        );
                }, 1000);
            },
            function(event) {
                // mouse out event handler
                var elem = $(event.currentTarget);
                if (timer) {
                    clearTimeout(timer);
                    timer = null;
                }
                else if (xhr) {
                    xhr.abort();
                    xhr = null;
                }
                else {
                    // destroy popup here
                }
            }
        )
    });

代码中,我在外部范围中定义了一个新变量xhr。 这个变量将保存我通过调用$.ajax()来初始化的异步请求对象。 不幸的是,当直接在JavaScript端构建URL时,我无法使用Flask中的url_for(),所以在这种情况下,我必须显式连接URL的各个部分。

$.ajax()调用返回一个promise,这是一个代表异步操作的特殊JavaScript对象。 我可以通过添加.done(function)来附加一个完成回调函数,所以一旦请求完成,我的回调函数就会被调用。 回调函数将接收到的响应作为参数,您可以在上面的代码中看到,我将其命名为data。 这将是我要放入popover的HTML内容。

但在我们获得弹窗之前,还有一个细节需要处理,以便给予用户一个良好的体验。 回想一下之前添加的逻辑,如果用户在触发鼠标进入事件之后的一秒内将鼠标指针移出<span>,将触发取消弹窗的逻辑。 同样的逻辑也需要应用于异步请求,所以我添加了第二个子句来放弃我的xhr请求对象(如果存在)。

弹出框的创建和销毁

所以最后,我可以使用在Ajax回调函数中传递给我的data参数来创建我的popover组件:

app/templates/base.html:显示弹出窗口。

                                function(data) {
                                    xhr = null;
                                    elem.popover({
                                        trigger: 'manual',
                                        html: true,
                                        animation: false,
                                        container: elem,
                                        content: data
                                    }).popover('show');
                                    flask_moment_render_all();
                                }

弹出窗口的实际创建非常简单,Bootstrap的popover()函数完成了设置弹出窗口的所有工作。弹出窗口的选项作为参数给出。我已使用“手动”触发模式,HTML内容,没有淡入淡出的动画(以便它更快地显示和消失)配置了此弹出框,并且将父级设置为<span>元素本身,以便将悬停行为扩展到通过继承弹出。最后,我将data参数传递给Ajax回调作为content参数。

popover()调用创建了一个弹窗组件,该组件也具有一个名为popover()的方法来显示弹窗。因此我不得不添加第二个popover('show')调用来将弹窗显示到页面中。

弹出窗口的内容包括第十二章中通过Flask-Moment插件生成的“最后访问”日期。 文档中提到,当通过Ajax添加新的Flask-Moment元素时,需要调用flask_moment_render_all()函数来适当地渲染这些元素。

现在剩下的就是完善鼠标移出事件处理程序上的删除弹出窗口逻辑。 如果用户将鼠标移出目标元素,该处理程序已经具有中止弹出操作的逻辑。 如果这些条件都不适用,那么这意味着弹出窗口当前显示并且用户正在离开target区域,所以在这种情况下,对目标元素的popover('destroy')调用将正确地执行移除和清理。

app/templates/base.html:销毁弹窗。

                function(event) {
                    // mouse out event handler
                    var elem = $(event.currentTarget);
                    if (timer) {
                        clearTimeout(timer);
                        timer = null;
                    }
                    else if (xhr) {
                        xhr.abort();
                        xhr = null;
                    }
                    else {
                        elem.popover('destroy');
                    }
                }

原文链接:https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-xx-some-javascript-magic

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值