Django搭建個人博客:錨點定位

**老讀者注意:**上一章消息通知有個bug,即發給管理員的notify必須移動到new_comment.save()的後面,否則會導致action_object存儲爲NULL,並且導致本章的html拼接錨點失效。

原文已更正,爲博主的疏忽表示歉意。

上一章已經實現了消息通知功能,可以很人性化的把用戶引導到被他人回覆的頁面中去。

但是仔細想想,似乎還有不方便的地方:如果頁面中評論較多,想找到感興趣的那一條評論還是要費點功夫的。所以這個消息通知,最好是能夠不僅前往正確的頁面,還要前往正確的位置(需求是無窮無盡的…)。

爲了實現這個功能,本章就要介紹一個非常古老的功能:錨點定位。以及如何在Django中實現它。

錨點是什麼

我們在寫html文件的容器時,經常會用到id屬性:

<div id="fruit">apple</div>

這個id屬性不僅可以作爲Javascript或者css代碼查詢某個容器的標記,還可以作爲錨點,定位頁面應該前往的位置。輸入下面的地址:

http://www.myblog.com/home#fruit

瀏覽器就會打開home頁面,並且視窗前往id="fruit"的容器。

明白了錨點是什麼,下面就通過三種不同的實現方法,看看錨點在Django博客項目中是如何應用的。

三種實現

html拼接

錨點首先要實現的功能,就是當管理員點擊消息通知時,瀏覽器視窗前往此通知的評論位置

因此首先修改文章詳情頁面,給渲染評論的div容器添加id屬性:

templates/article/detail.html

...
<!-- 已有代碼,遍歷樹形結構 -->
{% recursetree comments %}
{% with comment=node %}

<!-- 唯一新增代碼:id屬性 -->
<div class="..." id="comment_elem_{{ comment.id }}" >

    ...

    <!-- 下面都是已有代碼 -->
    <div class="children">
        {{ children }}
    </div>
    {% endif %}
</div>

{% endwith %}
{% endrecursetree %}
...

我們還是用comment.id來給每條評論賦予唯一的id值。注意id屬性保持唯一性。前面在二級回覆的Modal中用了comment_{{ comment.id }},這裏千萬不要重複了。

然後修改通知列表模板,添加錨點:

templates/notice/list.html

...
{% for notice in notices %}
<li ...>
    <!-- 新增 comment_elem_{{ notice.action_object.id }} 錨點 -->
    <a href="{% url "notice:update" %}?article_id={{ notice.target.id }}&notice_id={{ notice.id }}#comment_elem_{{ notice.action_object.id }}"
       target="_blank"
       >
        ...
    </a>
    ...
</li>
{% endfor %}
...

注意這裏url中拼接了兩種玩意兒:

  • 跟在?後面的是查詢參數,用於給視圖傳遞參數,是之前寫的舊代碼
  • 跟在#後面的是錨點,也就是本章正在學的東東

?#一個重要的差別,就是?不能夠傳遞到下個頁面的url中去,而#可以。

測試一下,用普通用戶賬號發幾條一級評論,登錄管理員賬號並點擊消息通知:

瀏覽器視窗沒有在頁面頂部,而是直接前往到該條評論處。

通過html拼接是實現錨點最簡單直接的方法。

視圖拼接

html拼接雖好,但它不是萬能的。如果要前往一個當前頁面還沒有創建的容器,該怎麼辦?

舉個栗子。按照目前我們的博客設計,當用戶發表評論時,頁面會刷新、視窗將停留在文章詳情的頂部。但實際上這時候視窗應該停留在新發表的評論處才比較合理,因爲用戶可能想檢查一下自己發表的評論是否正確。而在原頁面時由於新評論都還沒發表,所以comment.id是不存在的,沒辦法用html拼接錨點。讀者好好思考一下是不是這樣。

這種情況下就需要在視圖中拼接錨點了。修改文章評論視圖,將錨點拼接到redirect函數中:

comment/views.py

...
# 文章評論視圖
def post_comment(request, article_id, parent_comment_id=None):
    ...
    # 已有代碼
    if request.method == 'POST':
        ...
        if comment_form.is_valid():
            ...
            if parent_comment_id:
                ...
            new_comment.save()
            if not request.user.is_superuser:
                notify.send(...)

            # 新增代碼,添加錨點
            redirect_url = article.get_absolute_url() + '#comment_elem_' + str(new_comment.id)
            # 修改redirect參數
            return redirect(redirect_url)

get_absolute_url()是之前章節寫的方法,用於查詢某篇文章的地址。

說白了就是把拼接的位置從模板挪到了視圖中,因爲新評論必須在視圖中保存之後纔會被分配一個id值。

流動的數據

最後我們來看稍微複雜點的情況。

當用戶發表一級評論時,我們在視圖中拼接錨點解決了刷新當前頁面並定位的問題。但是二級評論是通過iframe + ajax實現的,這又該怎麼辦?

理一理思路。

首先,新評論的id值是在視圖中創建的,但是由於視圖是從iframe中請求的,在視圖中沒辦法刷新iframe的父頁面。所以我們唯一能做的就是把數據傳遞出去,到前端去處理。

修改文章評論視圖:

comment/views.py

# 引入JsonResponse
from django.http import JsonResponse

...
# 文章評論視圖
def post_comment(request, article_id, parent_comment_id=None):
    article = get_object_or_404(ArticlePost, id=article_id)

    # 已有代碼
    if request.method == 'POST':
        ...
        if comment_form.is_valid():
            ...
            if parent_comment_id:
                ...

                # 修改此處代碼
                # return HttpResponse("200 OK")
                return JsonResponse({"code": "200 OK", "new_comment_id": new_comment.id})

            ...

新引入的JsonResponse返回的是json格式的數據,由它將新評論的id傳遞出去。

json是web開發中很常用的輕量級數據格式,非常像python的字典,讀者請自行了解。

特別提醒json格式必須用雙引號。

現在數據在iframe中了。但是我們需要刷新的是iframe的父頁面啊,所以還要繼續把數據往父頁面“扔"

修改二級評論的模板:

templates/comment/reply.html

...
<script>
...

function confirm_submit(article_id, comment_id){
    ...
    $.ajax({
        ...
        // 成功回調函數
        success: function(e){
            
            // 舊代碼
            // if(e === '200 OK'){
            //     parent.location.reload();
            // };
            
            // 新代碼
            if(e.code === '200 OK'){
                // 調用父頁面的函數
                parent.post_reply_and_show_it(e.new_comment_id);
            };
        }
    });
}
</script>

由於現在ajax獲取的是json數據,因此用e.code獲取視圖返回的狀態。

舊代碼用parent.location.reload()刷新了父頁面。同樣的,用parent.abc()可以調用父頁面的abc()函數。這樣就把數據傳遞到父頁面裏去了。

這下就好說了。在父頁面中(文章詳情模板)添加需要執行錨點拼接的函數:

templates/article/detail.html

...

{% block script %}
...
<script>
    ...

    // 新增函數,處理二級回覆
    function post_reply_and_show_it(new_comment_id) {
        let next_url = "{% url 'article:article_detail' article.id %}";
        // 去除 url 尾部 '/' 符號
        next_url = next_url.charAt(next_url.length - 1) == '/' ? next_url.slice(0, -1) : next_url;
        // 刷新並定位到錨點
        window.location.replace(next_url + "#comment_elem_" + new_comment_id);
    };
</script>
{% endblock script %}

函數中運用了JavaScript三元運算符a ? b : c,翻譯成人話就是:如果a成立則返回b,如果a不成立就返回c。作用是去掉url尾部的/,否則錨點不會生效。你可能會問,三元運算符多麻煩,爲什麼不直接把url末尾一個字符剔除掉呢?答案是這樣寫代碼更加健壯。萬一哪天Django解析的url尾部沒有斜槓了呢。

window.location.replace()作用是重定向頁面,在這裏面終於可以愉快的拼接錨點了。

一切都OK啦。測試發表二級評論,運氣好的同學應該可以順利將視窗定位到剛評論的位置了。

感受到數據的流動沒有?

總結

本章學習了錨點的html拼接、視圖拼接、ajax+iframe綜合運用,理解後就能應付絕大部分的狀況了。

錨點雖然古老,但並不陳舊。

合理的運用錨點,可以讓你的博客相當的人性化,這也是好網站的一個標誌。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章