Flask Mega Tutorial 第20章:一些JavaScript魔法

本系列删改自Miguel Grinberg的Flask Mega-Tutorial系列。点此查看作者原文


服务端支持

在我们探究客户端之前,让我们在服务端做点工作,对于支持弹出框来说,这是必要的方式。弹出框的内容将会在新路由中返回,这是现有的用户资料路由的简化版本。这是视图函数:

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()
    return render_template('user_popup.html', user=user)

这个路由将与 /user/<username>/popup URL相关联,然后会加载被请求用户然后渲染模板。其模板是用户资料页面的简短版本:

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) %}
                    <a href="{{ url_for('main.follow', username=user.username) }}">
                        {{ _('Follow') }}
                    </a>
                    {% else %}
                    <a href="{{ url_for('main.unfollow', username=user.username) }}">
                        {{ _('Unfollow') }}
                    </a>
                    {% endif %}
                {% endif %}
            </small>
        </td>
    </tr>
</table>

当用户将鼠标指针悬停于一个用户名上时,下面我写的JavaScript代码将会调用此路由。服务器将会将HTML内容作为响应返回到弹出框,随后客户端将显示它。当用户将鼠标移开,弹出框将会消失。

如果你想知道弹出框看起来什么样,你现在就可以运行应用,进入任何一个用户的资料页面,然后在地址栏的URL后面追加 /popup,就能看到全屏版本的弹出框内容了。


Bootstrap Popover组件

在第11章中我介绍了Bootstrap框架来方便的创建好看的web页面。到目前为止,我只用到这个框架中很小的一部分。Bootstrap捆绑了很多常用的UI元素,Bootstrap文档 https://getbootstrap.com 中有所有这些组件的演示和示例。其中就有 Popover,文档中称其为“覆盖小部分内容,用于放置次要信息”,正是我需要的!

大多Bootstrap组件都是HTML标记定义的,引用Bootstrap CSS来添加好看的样式。其中有一些高级的依赖JavaScript。一般使在web页面中使用这些组件的方法是在适当的位置添加HTML,然后为需要脚本支持的组件调用初始化或激活它的JavaScript函数。而这个popover组件就是依赖JavaScript支持的。

实现popover的HTML部分是很简单的,你只需要定义用于触发popover显示的元素就行。在我的例子中,将会是每个博客帖子的可点击的用户名。 app/templates/_post.html 子模版中有已经定义好的用户名:

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

根据popover的文档,和上面那个一样,我得在每个链接上调用 popover() JavaScript函数,这将会初始化popup。该初始化调用接受许多选项来配置该popup,包括传递你想要显示在popup的内容的选项,用什么方法触发popup显示或不显示(一个点击,在元素上悬停,等等),内容是不是空白文本或HTML,在文档中你还能看到几个其他的选项。不幸的是,看完这些信息我的疑问更多了,因为这个组件似乎并没有被设计成我需要的方式工作。要实现这个功能,我有以下几个问题需要解决:

  • 页面中会有很多的用户名链接,每个都对应一个博客帖子。我得找到一个方法来在页面被渲染后找到所有这些链接,这样我才能将它们初始化为popover。
  • Bootstrap文档中的popover例子都将popover的内容作为一个data-content属性来添加到目标HTML元素,因此当悬停事件被触发后,Bootstrap要做的就算显示popover。这对我来说真的很麻烦,因为我想像服务器发起Ajax调用来获取内容,并且只有在接收到服务器响应后才显示popup。
  • 当使用“hover”模式时,只要你吧指针指向目标的元素,popup会一直保持显示。当你移开鼠标,popup就会消失。如果用户想把鼠标指针移向popup本身,会带来很丑的副作用,popup会消失。我得想个办法拓展悬停行为,使之包括popup本身,这样用户可以把指针移向popup,然后,比如说,点击其中的链接。

事实上,在使用基于浏览器的应用时,事情很快变得很复杂,这并不少见。你必须仔细的思考DOM元素是如何相互作用的,并使它们的行为方式能给用户良好的用户体验。


在页面加载时执行函数

很明显在每个页面加载时我需要运行一些JavaScript代码。我要运行的函数会搜索页面中所有username链接,然后将它们配置到Bootstrap的popover组件。

JavaScript的jQuery库是Bootstrap的依赖项,所以我会利用它。使用jQuery时,你可以通过在$(...)中包装页面来加载该函数以运行,我可以将它添加到 app/templates/base.html 模板中,这样使之在应用程序中的每个页面都运行:

app/template/base.html

...
<script>
    // ...

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

如你所见,我已经将我的起始函数添加到了<script>元素中。


用选择器查找DOM元素

我的第一个问题是创建一个JavaScript函数来查找页面中所有的user链接。这个函数会在页面载入结束后运行,完成后会为它们配置悬停和popup行为。现在我要专注于找到链接。

如果你还记得在第14章中,涉及到在线翻译的HTML元素有唯一ID。比如,ID为123的帖子有一个 id="post123"属性。然后使用jQuery,$('#post123')用于在DOM中找到这个元素。$()函数非常强大,并且基于CSS选择器可以搜索到DOM元素。

我用于翻译功能的选择器是设计来查找具有唯一标识符比如id属性的特定元素。标识元素的另一个选择是使用class属性,同一个class可以被分配给页面的多个元素。比如,我可以将所有的用户链接用 class="user_popup"标记,然后我可以用JavaScript的 $('.user_popup')获取链接列表(在CSS选择器中,#前缀用于搜索ID,而.前缀用于搜索class)。在本例中的返回值会是具有该class的所有属性的集合。


Popover和DOM

通过在Bootstrap文档中播放Popover示例并在浏览器调试器中检查DOM,我确定了Bootstrap将popover组件创建为DOM中目标元素的兄弟元素。正如我先前提到的,这影响了悬停事件的行为,用户一把鼠标从<a>链接移开到popup本身,,就会触发“mouse out”。

让popover成为目标元素的子元素,就可以拓展悬停事件来包含popover本身,这样就可以继承悬停事件。查看文档中popover的选项,可以通过在container选项传递父元素来实现。

对于按钮,一般的<div>,<span>元素来说,让popover成为其子元素会很好使,但在本例中,popover的目标是个显示可点击用户名的链接的<a>元素。问题是,让popover成为<a>元素的子元素会使其获得其父元素<a>的链接行为。最后的结果会是像这样的:

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

为了避免popover早<a>元素里面,我会使用另一个技巧。我会将<a>元素包含在一个<span>元素中,然后将悬停事件和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 %}
...

如果你好奇popover HTML元素在哪,好消息是不用担心。当我在刚创建的<span>元素上调用popover()初始化函数时, Bootstrap框架将会动态的插入popup组件。


悬停事件

正如前面提到的,Bootstrap中使用的popover的悬停行为不够灵活,无法满足我的需要,但如果你看一下文档中的trigger选项,“悬停”只是其中一个可能的值。吸引我的是“manual”模式,通过JavaScript调用可以手动地显示或者移除popover。这个模式使我可以自由实现自己的悬停逻辑,所以我将使用这个选项,实现我自己的悬停逻辑来按照我需要的方式工作。

下一步我要做的是将“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参数是一个事件对象,包含了有用的信息。在本例中,我用 event.currentTarget 提取了事件的目标元素。

浏览器在鼠标进入受影响的元素后立即发起悬停事件。在popup的情况中,我只希望在鼠标停留在元素上一段事件后才激活此事件,这样当鼠标指针快速滑过元素但没有停留时不会有popup闪现。由于此事件不支持延时,这是另一个我需要自己实现的功能。所以我将会向“mouse in”事件处理器加入1秒计时器。

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变量中,为了让“mouse out”处理器也能访问timer对象。我这么做的原因是为了良好的用户体验。如果用户将鼠标指针移入其中一个用户链接并且停留在那,比如说,半秒后移开,我不想计时器依然调用那个显示popup的函数。所以我的鼠标移开事件处理器检查是否还有有效的timer对象,如果有,则取消。


Ajax请求

Ajax请求并不是什么新的话题,在14章,作为在线语言翻译功能的一部分,我介绍了这个话题。使用jQuery时,$.ajax()函数像服务端发送一个匿名请求。

我发送给服务器的请求有 /user/<username>/popup URL,也就是本章开头我添加的URL。这个请求的响应将包含我要插入到popup的HTML。

目前我对这个请求的问题是,如何知道在URL中所需的username的值。事件处理函数的鼠标是通用的,它将查找页面中找到的所有用户链接,所以函数需要搞清楚上下文环境中的用户名。

elem变量包含了悬停事件的目标元素, 也就是<span>元素包围的<a>元素。要获取用户名,我可以从DOM的<span>元素开始,移动到其第一个子元素,也就是<a>元素,然后获取其文本,也就是我的URL中所需的用户名。用jQuery的DOM遍历函数很简单:

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

应用到DOM节点的first()函数返回其第一个子元素。text()函数返回节点的文本内容。这个函数不会对文本做其他的事情,比如说,如果一个行中有个<a>,文本在下一行,</a>则在另一伙,text()会返回文本周围的所有空白符。我使用JavaScript的trim()函数消除所有的空白符,只留下文本。

这是我所需的能够向服务器端发起请求的所有信息:

app/templates/base.html

    $(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()的。不幸的是,我不能用Flask的url_for()在JavaScript端直接创建URL,所以我将显式地将URL拼接起来。

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

在实现popover之前,还有一个可以提升用户体验的细节要解决。回想一下,我给“mouse out”事件处理器函数添加了逻辑,当用户将鼠标指针移出时,取消一秒延迟。我需要向异步请求应用同样的想法,所以我添加了第二个子句,当xhr请求对象已存在时中止它。


创建与取消Popover

最后,我终于可以创建我自己的popover组件了,使用Ajax回调函数中传递给我的data参数:

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();
                                }

popup的实际创建很简单,Bootstrap的popover函数做了所有的主要工作。popover的选项以参数的形式给定。我已经将此popover配置了“manual”触发模式,HTML内容,没有渐变动画(使它快速出现与消失),以及我把<span>元素的父元素设置为其本身,使得popover继承其悬停行为。最后,我将data参数作为content参数传递到Ajax回调。

popover()调用的返回是新创建的popover组件,由于奇怪的原因,有另一个也叫popover()的方法用于显示它。因此我得添加第二个 popover('show')调用来时popover可以显示到页面上。

popup的内容包含了“last seen”日期,由Flask-Moment插件生成,当新的Flask-Moment元素通过Ajax添加,需要调用flask_moment_render_all()函数来呈现这些元素。

现在剩下的就是处理鼠标移除时间处理器的删除popup问题。这个处理器已经有了在用户将鼠标移出目标元素时中止popover操作的逻辑。如果这些条件都不适用,则表示popover当前已经显示,用户正在移开目标区域,因此在这种情况下,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');
                    }
                }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值