开发博客文章详情页

2019-08-1623269 阅读30 评论

首页展示的是所有文章的列表,当用户看到感兴趣的文章时,他点击文章的标题或者继续阅读的按钮,应该跳转到文章的详情页面来阅读文章的详细内容。现在让我们来开发博客的详情页面,有了前面的基础,开发流程都是一样的了:首先配置 URL,即把相关的 URL 和视图函数绑定在一起,然后实现视图函数,编写模板并让视图函数渲染模板。

设计文章详情页的 URL

回顾一下我们首页视图的 URL,在 blog\urls.py 文件里,我们写了:

blog/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
]

首页视图匹配的 URL 去掉域名后其实就是一个空的字符串。对文章详情视图而言,每篇文章对应着不同的 URL。比如我们可以把文章详情页面对应的视图设计成这个样子:当用户访问 <网站域名>/posts/1/ 时,显示的是第一篇文章的内容,而当用户访问 <网站域名>/posts/2/ 时,显示的是第二篇文章的内容,这里数字代表了第几篇文章,也就是数据库中 Post 记录的 id 值。下面依照这个规则来绑定 URL 和视图:

blog/urls.py

from django.urls import path

from . import views

app_name = 'blog'
urlpatterns = [
    path('', views.index, name='index'),
    path('posts/<int:pk>/', views.detail, name='detail'),
]

这里 'posts/<int:pk>/' 刚好匹配我们上面定义的 URL 规则。这条规则的含义是,以 posts/ 开头,后跟一个整数,并且以 / 符号结尾,如 posts/1/、 posts/255/ 等都是符合规则的,此外这里 <int:pk> 是 django 路由匹配规则的特殊写法,其作用是从用户访问的 URL 里把匹配到的数字捕获并作为关键字参数传给其对应的视图函数 detail。比如当用户访问 posts/255/ 时(注意 django 并不关心域名,而只关心去掉域名后的相对 URL),<int:pk> 匹配 255,那么这个 255 会在调用视图函数 detail 时被传递进去,其参数名就是冒号后面指定的名字 pk,实际上视图函数的调用就是这个样子:detail(request, pk=255)。我们这里必须从 URL 里捕获文章的 id,因为只有这样我们才能知道用户访问的究竟是哪篇文章。

Tip:

django 的路由匹配规则有很多类型,除了这里的 int 整数类型,还有 str 字符类型、uuid 等,可以通过官方文档了解:Path converters

此外我们通过 app_name='blog' 告诉 django 这个 urls.py 模块是属于 blog 应用的,这种技术叫做视图函数命名空间。我们看到 blog\urls.py 目前有两个视图函数,并且通过 name 属性给这些视图函数取了个别名,分别是 index、detail。但是一个复杂的 django 项目可能不止这些视图函数,例如一些第三方应用中也可能有叫 index、detail 的视图函数,那么怎么把它们区分开来,防止冲突呢?方法就是通过 app_name 来指定命名空间,命名空间具体如何使用将在下面介绍。如果你忘了在 blog\urls.py 中添加这一句,接下来你可能会得到一个 NoMatchReversed 异常。

为了方便地生成上述的 URL,我们在 Post 类里定义一个 get_absolute_url 方法,注意 Post 本身是一个 Python 类,在类中我们是可以定义任何方法的。

blog/models.py

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

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 配置中的 path('posts/<int:pk>/', 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 对应的规则就是 posts/<int:pk>/ int 部分会被后面传入的参数 pk 替换,所以,如果 Post 的 id(或者 pk,这里 pk 和 id 是等价的) 是 255 的话,那么 get_absolute_url 函数返回的就是 /posts/255/ ,这样 Post 自己就生成了自己的 URL。

编写 detail 视图函数

接下来就是实现我们的 detail 视图函数了:

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 拷贝到 templates\blog 目录下(和 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-{{ post.pk }}">
  <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 模板继承的方法来帮我们消除这些重复操作。

模板继承

我们看到 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>

再次从首页点击一篇文章的标题或者继续阅读按钮跳转到详情页面,可以看到预期效果了!

博客文章详情页

-- EOF --

30 评论
登录后回复
TestNovice
2022-11-15 16:56:48

每个最后一张图片的效果,在继承的block main中没有

标签。这个标签应该怎么获取

回复
kevin-aron
2022-08-06 01:26:29

Reverse for 'detail' with keyword arguments '{'pk': 1}' not found. 1 pattern(s) tried: ['blogs/posts//$']
请问出现这个错误该怎么解决呢?
我的url.py里面没有用path,用的是url

回复
Feko
2022-07-27 15:57:02

通过+1

回复
yutianxiong
2021-05-06 16:28:14

博主你好,为什么我用这个函数get_absolute_url的时候页面没有返回url呢,h1标签里面也没有返回数据

回复
番薯和甜瓜的故事 yutianxiong
2021-05-06 20:58:11

index.html
修改成{% url 'detail' pk=post.pk %}
这样试下能不能实现

回复
yutianxiong 番薯和甜瓜的故事
2021-05-07 09:16:08

他这个模板不是要用两个花括号吗

回复
yutianxiong 番薯和甜瓜的故事
2021-05-07 09:16:30

1947571022我扣扣大佬加我

回复
伐里个伐
2020-11-21 16:13:12

emmm,锚点更改后变成一串乱码是为什么呢?

回复
Yasin-Yan
2020-11-04 15:39:44

为什么之前index.html里用的是{% load static %}到了base.html里要用{% load staticfiles %}了呢 😄

回复
追梦人物 Yasin-Yan
2020-11-11 18:03:53

差不多吧,新版 django 推荐使用 static 标签

回复
Rdeeeeeee
2020-10-19 00:05:13

错误信息:

NoReverseMatch at /
'blog' is not a registered namespace
Request Method: GET
Request URL:    http://127.0.0.1:8000/
Django Version: 2.2.3
Exception Type: NoReverseMatch
Exception Value:    
'blog' is not a registered namespace

detail 函数

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

get_absolute_url函数

def get_absolute_url(self):
        return reverse('blog:detail', kwargs={'pk': self.pk})

博主能否帮忙看一下哪里出了问题呢?感谢!!

回复
追梦人物 Rdeeeeeee
2020-10-19 11:51:02

blog app 下的 url.py 设置了 app_name = 'blog' 吗?

回复
Extrader
2020-05-27 17:11:17

最顶部我使用的这个
{% extends 'blog/base.html' %}
不然会报错找不到base.html文件

回复
Maoning Guan Extrader
2020-05-30 10:44:05

你的base.html放错地方了吧。base.html是放在templates目录下,不是templates/blog目录下

回复
ALingFeng233
2020-05-19 11:12:44

我的在首页有显示测试文章,但是点击标题或者继续阅读后跳转到对应文章内容详情页的时候却显示{% empty %}的内容,这是怎么一回事?

回复
Maoning Guan ALingFeng233
2020-05-30 10:47:42

文章详情页的article标签里面填的是single.html模板里面的article标签内容,不是index.html里面的内容。

回复
YAO ZELIANG
2020-04-02 02:36:54

今天学到这里,打卡,学习体验极好!!

回复
_大西洋暖流_
2020-03-21 21:01:17

博主的教程写得很好,这一章我感觉这里好像写错了一点。
"把 index.html 的内容全部拷贝到 base.html 文件里"应该是"把 single.html 的内容全部拷贝到 base.html 文件里"。
single.html里面有"

"这一段内容,是给单篇文章添加评论用的。用index.html来制作base.html就会出现后面的detail.html页面里缺失评论区。

回复
giveup-llw
2019-11-23 19:46:56

请教博主,报错django.db.utils.IntegrityError: NOT NULL constraint failed: blog1_post.author_id是哪里出现问题了

回复
giveup-llw giveup-llw
2019-11-23 20:00:36

可以了 可能是之前数据给我删了的原因,我重新迁移数据库就好了。作者设为可空

回复
clearlies
2019-11-08 18:23:31

请教一下,报错:django.urls.exceptions.NoReverseMatch: Reverse for 'detail' with keyword arguments '{'pk': 1}' not found. 1 pattern(s) tried: ['posts//$']
查看代码也是和博主一样的,百度也没有找到能解决的方法。

回复
Henrybsbhp clearlies
2019-11-09 22:05:29

注意一下 path('posts/<int:pk>/', views.detail, name='detail')有没有写错。应该是你的 posts 部分写错了,正确的是 posts/<int:pk>/,请注意检查看看。

回复
Ming-1c
2019-10-14 20:43:51

博主,写的太好了,看的爽,emmm

回复
微微挪车
2019-09-20 11:06:27

我跟着教程做,点击文章标题,挑战到 <网站域名>/posts/2/,但是返回的都是404~,不应该是detail.html吗?

回复
微微挪车 微微挪车
2019-09-20 11:18:41

已解决,是忘记导入404

回复
ZhangHanAA
2019-09-19 10:30:18

我在使用 get_object_or_404这个函数的时候,出现了TypeError: cannot unpack non-iterable int object 这个错误,不知道怎么解决。

回复
追梦人物 ZhangHanAA
2019-09-19 10:37:57

很有可能你的关键字参数传的不对,函数调用发来看一下呢。

回复
719923505
2019-09-04 14:48:43

1、模板标签跟模板变量有什么区别,{%%} ,{{}}怎么确认什么情况下用哪个?
2、什么时候需要get_absolute_url?

回复
方世玉17927
2019-08-21 20:02:09

博主你好,,我用django2的环境看你的第一版教程出现了很多Bugs,改的也非常辛苦,,希望你的第二版能早点出来。

回复
追梦人物 方世玉17927
2019-09-09 16:17:40

第一版最多支持到 1.11。第二版已经更新一半了,请关注。

回复