Django 博客開發教程 8 - 博客文章詳情頁

首頁展現的是全部文章的列表,當用戶看到感興趣的文章時,他點擊文章的標題或者繼續閱讀的按鈕,應該跳轉到文章的詳情頁面來閱讀文章的詳細內容。如今讓咱們來開發博客的詳情頁面,有了前面的基礎,開發流程都是同樣的了:首先配置 URL,即把相關的 URL 和視圖函數綁定在一塊兒,而後實現視圖函數,編寫模板並讓視圖函數渲染模板。html

設計文章詳情頁的 URL

回顧一下咱們首頁視圖的 URL,在 blogurls.py 文件裏,咱們寫了:python

blog/urls.py

from django.conf.urls import url

from . import views

urlpatterns = [
    url(r'^$', views.index, name='index'),
]

首頁視圖匹配的 URL 去掉域名後其實就是一個空的字符串。對文章詳情視圖而言,每篇文章對應着不一樣的 URL。好比咱們能夠把文章詳情頁面對應的視圖設計成這個樣子:當用戶訪問 <網站域名>/post/1/ 時,顯示的是第一篇文章的內容,而當用戶訪問 <網站域名>/post/2/ 時,顯示的是第二篇文章的內容,這裏數字表明瞭第幾篇文章,也就是數據庫中 Post 記錄的 id 值。下面依照這個規則來綁定 URL 和視圖:git

blog/urls.py

from django.conf.urls import url

from . import views

app_name = 'blog'
urlpatterns = [
    url(r'^$', views.index, name='index'),
    url(r'^post/(?P<pk>[0-9]+)/$', views.detail, name='detail'),
]

Django 使用正則表達式來匹配用戶訪問的網址。這裏 r'^post/(?P<pk>[0-9]+)/$' 整個正則表達式恰好匹配咱們上面定義的 URL 規則。這條正則表達式的含義是,以 post/ 開頭,後跟一個至少一位數的數字,而且以 / 符號結尾,如 post/1/、 post/255/ 等都是符合規則的,[0-9]+ 表示一位或者多位數。此外這裏 (?P<pk>[0-9]+) 表示命名捕獲組,其做用是從用戶訪問的 URL 裏把括號內匹配的字符串捕獲並做爲關鍵字參數傳給其對應的視圖函數 detail。好比當用戶訪問 post/255/ 時(注意 Django 並不關心域名,而只關心去掉域名後的相對 URL),被括起來的部分 (?P<pk>[0-9]+) 匹配 255,那麼這個 255 會在調用視圖函數 detail 時被傳遞進去,實際上視圖函數的調用就是這個樣子:detail(request, pk=255)。咱們這裏必須從 URL 裏捕獲文章的 id,由於只有這樣咱們才能知道用戶訪問的到底是哪篇文章。github

可能上述的正則表達式你有點難以理解,關於正則表達式的部分並不是 Django 相關的內容,而是 Python 的內容。Django 只是在這裏使用了 Python 處理正則表達式的 re 模塊。所以若是想更好地理解 Python 中正則表達式的相關知識,請自行查看 Python 官方文檔中 re 模塊的文檔。正則表達式

此外咱們經過 app_name='blog' 告訴 Django 這個 urls.py 模塊是屬於 blog 應用的,這種技術叫作視圖函數命名空間。咱們看到 blogurls.py 目前有兩個視圖函數,而且經過 name 屬性給這些視圖函數取了個別名,分別是 index、detail。可是一個複雜的 Django 項目可能不止這些視圖函數,例如一些第三方應用中也可能有叫 index、detail 的視圖函數,那麼怎麼把它們區分開來,防止衝突呢?方法就是經過 app_name 來指定命名空間,命名空間具體如何使用將在下面介紹。若是你忘了在 blogurls.py 中添加這一句,接下來你可能會獲得一個 NoMatchReversed 異常。數據庫

爲了方便地生成上述的 URL,咱們在 Post 類裏定義一個 get_absolute_url 方法,注意 Post 自己是一個 Python 類,在類中咱們是能夠定義任何方法的。django

blog/models.py

from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.six import python_2_unicode_compatible

@python_2_unicode_compatible
class Post(models.Model):
    ...

    def __str__(self):
        return self.title
    
    # 自定義 get_absolute_url 方法
    # 記得從 django.urls 中導入 reverse 函數
    def get_absolute_url(self):
        return reverse('blog:detail', kwargs={'pk': self.pk})

注意到 URL 配置中的 url(r'^post/(?P<pk>[0-9]+)/$', views.detail, name='detail') ,咱們設定的 name='detail' 在這裏派上了用場。看到這個 reverse 函數,它的第一個參數的值是 'blog:detail',意思是 blog 應用下的 name=detail 的函數,因爲咱們在上面經過 app_name = 'blog' 告訴了 Django 這個 URL 模塊是屬於 blog 應用的,所以 Django 可以順利地找到 blog 應用下 name 爲 detail 的視圖函數,因而 reverse 函數會去解析這個視圖函數對應的 URL,咱們這裏 detail 對應的規則就是 post/(?P<pk>[0-9]+)/ 這個正則表達式,而正則表達式部分會被後面傳入的參數 pk 替換,因此,若是 Post 的 id(或者 pk,這裏 pk 和 id 是等價的) 是 255 的話,那麼 get_absolute_url 函數返回的就是 /post/255/ ,這樣 Post 本身就生成了本身的 URL。app

編寫 detail 視圖函數

接下來就是實現咱們的 detail 視圖函數了:ide

blog/views.py

from django.shortcuts import render, get_object_or_404
from .models import Post

def index(request):
    # ...

def detail(request, pk):
    post = get_object_or_404(Post, pk=pk)
    return render(request, 'blog/detail.html', context={'post': post})

視圖函數很簡單,它根據咱們從 URL 捕獲的文章 id(也就是 pk,這裏 pk 和 id 是等價的)獲取數據庫中文章 id 爲該值的記錄,而後傳遞給模板。注意這裏咱們用到了從 django.shortcuts 模塊導入的 get_object_or_404 方法,其做用就是當傳入的 pk 對應的 Post 在數據庫存在時,就返回對應的 post,若是不存在,就給用戶返回一個 404 錯誤,代表用戶請求的文章不存在。函數

編寫詳情頁模板

接下來就是書寫模板文件,從下載的博客模板(若是你尚未下載,請 點擊這裏 下載)中把 single.html 拷貝到 templatesblog 目錄下(和 index.html 在同一級目錄),而後更名爲 detail.html。此時你的目錄結構應該像這個樣子:

blogproject\
    manage.py
    blogproject\
        __init__.py
        settings.py
        ...
    blog/
        __init__.py
        models.py
        ,,,
    templates\
        blog\
            index.html
            detail.html

在 index 頁面博客文章列表的標題繼續閱讀按鈕寫上超連接跳轉的連接,即文章 post 對應的詳情頁的 URL,讓用戶點擊後能夠跳轉到 detail 頁面:

templates/blog/index.html

<article class="post post-1">
  <header class="entry-header">
    <h1 class="entry-title">
      <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
    </h1>
    ...
  </header>
  <div class="entry-content clearfix">
    ...
    <div class="read-more cl-effect-14">
      <a href="{{ post.get_absolute_url }}" class="more-link">繼續閱讀 <span class="meta-nav">→</span></a>
    </div>
  </div>
</article>
{% empty %}
  <div class="no-post">暫時尚未發佈的文章!</div>
{% endfor %}

這裏咱們修改兩個地方,第一個是文章標題處:

<h1 class="entry-title">
  <a href="{{ post.get_absolute_url }}">{{ post.title }}</a>
</h1>

咱們把 a 標籤的 href 屬性的值改爲了 {{ post.get_absolute_url }}。回顧一下模板變量的用法,因爲 get_absolute_url 這個方法(咱們定義在 Post 類中的)返回的是 post 對應的 URL,所以這裏 {{ post.get_absolute_url }} 最終會被替換成該 post 自身的 URL。

一樣,第二處修改的是繼續閱讀按鈕的連接:

<a href="{{ post.get_absolute_url }}" class="more-link">繼續閱讀 <span class="meta-nav">→</span>
</a>

這樣當咱們點擊首頁文章的標題或者繼續閱讀按鈕後就會跳轉到該篇文章對應的詳情頁面了。然而若是你嘗試跳轉到詳情頁後,你會發現樣式是亂的。這在 真正的 Django 博客首頁 時講過,因爲咱們是直接複製的模板,尚未正確地處理靜態文件。咱們能夠按照介紹過的方法修改靜態文件的引入路徑,但很快你會發如今任何頁面都是須要引入這些靜態文件,若是每一個頁面都要修改會很麻煩,並且代碼都是重複的。下面就介紹 Django 模板繼承的方法來幫咱們消除這些重複操做。

模板繼承

咱們看到 index.html 文件和 detail.html 文件除了 main 標籤包裹的部分不一樣外,其它地方都是相同的,咱們能夠把相同的部分抽取出來,放到 base.html 裏。首先在 templates 目錄下新建一個 base.html 文件,這時候你的項目目錄應該變成了這個樣子:

blogproject\
    manage.py
    blogproject\
        __init__.py
        settings.py
        ...
    blog\
        __init__.py
        models.py
        ,,,
    templates\
        base.html
        blog\
            index.html
            detail.html

把 index.html 的內容所有拷貝到 base.html 文件裏,而後刪掉 main 標籤包裹的內容,替換成以下的內容。

templates/base.html

...
<main class="col-md-8">
    {% block main %}
    {% endblock main %}
</main>
<aside class="col-md-4">
  {% block toc %}
  {% endblock toc %}
  ...
</aside>
...

這裏 block 也是一個模板標籤,其做用是佔位。好比這裏的 {% block main %}{% endblock main %} 是一個佔位框,main 是咱們給這個 block 取的名字。下面咱們會看到 block 標籤的做用。同時咱們也在 aside 標籤下加了一個 {% block toc %}{% endblock toc %} 佔位框,由於 detail.html 中 aside 標籤下會多一個目錄欄。當 {% block toc %}{% endblock toc %} 中沒有任何內容時,{% block toc %}{% endblock toc %} 在模板中不會顯示。但當其中有內容是,模板就會顯示 block 中的內容。

在 index.html 裏,咱們在文件最頂部使用 {% extends 'base.html' %} 繼承 base.html,這樣就把 base.html 裏的代碼繼承了過來,另外在 {% block main %}{% endblock main %} 包裹的地方填上 index 頁面應該顯示的內容:

templates/blog/index.html

{% extends 'base.html' %}

{% block main %}
    {% for post in post_list %}
        <article class="post post-1">
          ...
        </article>
    {% empty %}
        <div class="no-post">暫時尚未發佈的文章!</div>
    {% endfor %}
    <!-- 簡單分頁效果
    <div class="pagination-simple">
        <a href="#">上一頁</a>
        <span class="current">第 6 頁 / 共 11 頁</span>
        <a href="#">下一頁</a>
    </div>
    -->
    <div class="pagination">
      ...
    </div>
{% endblock main %}

這樣 base.html 裏的代碼加上 {% block main %}{% endblock main %} 裏的代碼就和最開始 index.html 裏的代碼同樣了。這就是模板繼承的做用,公共部分的代碼放在 base.html 裏,而其它頁面不一樣的部分經過替換 {% block main %}{% endblock main %} 佔位標籤裏的內容便可。

若是你對這種模板繼承仍是有點糊塗,能夠把這種繼承和 Python 中類的繼承類比。base.html 就是父類,index.html 就是子類。index.html 繼承了 base.html 中的所有內容,同時它自身還有一些內容,這些內容就經過 「覆寫」 {% block main %}{% endblock main %}(把 block 看作是父類的屬性)的內容添加便可。

detail 頁面處理起來就簡單了,一樣繼承 base.html ,在 {% block main %}{% endblock main %} 裏填充 detail.html 頁面應該顯示的內容,以及在 {% block toc %}{% endblock toc %} 中填寫 base.html 中沒有的目錄部分的內容。不過目前的目錄只是佔位數據,咱們在之後會實現如何從文章中自動摘取目錄。

templates/blog/detail.html

{% extends 'base.html' %}

{% block main %}
    <article class="post post-1">
      ...
    </article>
    <section class="comment-area">
      ...
    </section>
{% endblock main %}
{% block toc %}
    <div class="widget widget-content">
        <h3 class="widget-title">文章目錄</h3>
        <ul>
            <li>
                <a href="#">教程特色</a>
            </li>
            <li>
                <a href="#">誰適合這個教程</a>
            </li>
            <li>
                <a href="#">在線預覽</a>
            </li>
            <li>
                <a href="#">資源列表</a>
            </li>
            <li>
                <a href="#">獲取幫助</a>
            </li>
        </ul>
    </div>
{% endblock toc %}

修改 article 標籤下的一些內容,讓其顯示文章的實際數據:

<article class="post post-{{ post.pk }}">
  <header class="entry-header">
    <h1 class="entry-title">{{ post.title }}</h1>
    <div class="entry-meta">
      <span class="post-category"><a href="#">{{ post.category.name }}</a></span>
      <span class="post-date"><a href="#"><time class="entry-date"
                                                datetime="{{ post.created_time }}">{{ post.created_time }}</time></a></span>
      <span class="post-author"><a href="#">{{ post.author }}</a></span>
      <span class="comments-link"><a href="#">4 評論</a></span>
      <span class="views-count"><a href="#">588 閱讀</a></span>
    </div>
  </header>
  <div class="entry-content clearfix">
    {{ post.body }}
  </div>
</article>

再次從首頁點擊一篇文章的標題或者繼續閱讀按鈕跳轉到詳情頁面,能夠看到預期效果了!

博客文章詳情頁

總結

本章節的代碼位於:Step8: blog detail view

若是遇到問題,請經過下面的方式尋求幫助。

更多Django 教程,請訪問 追夢人物的博客

相關文章
相關標籤/搜索