Django 网页开发快速上手——实现一个博客应用

news/2025/1/14 13:05:46/文章来源:https://www.cnblogs.com/zhangbihan999/p/18670571

目录
  • 0 前言
    • 博客定位
    • 成果展示
    • 环境配置
  • 1 创建 project
  • 2 创建 app
  • 3 Django 三大元素——MVT
  • 4 创建你的第一个 view
  • 5 创建你的第一个 template
  • 6 migration 与 admin 端
  • 7 创建你的第一个 model
  • 8 连通 view, template 和 model
  • 9 实现注册/登录/登出功能
  • 10 模版继承
  • 11 用 css 文件美化页面
  • 12 实现新建/更新/删除博客功能
  • 13 结语

0 前言

博客定位

读者的注意力很宝贵,所以在阅读这篇万字长文之前,有必要先向您汇报能够学到什么。

这篇博客使用 python 的 django 网页开发框架在本地实现一个博客应用,主要参考 django 官方教程 和 flask 官方教程 (实现的这个博客应用是 flask 的快速入门项目,我给出了它的 django 版本)。

在一步一步的实现过程中,我会向您介绍:

如何使用 django 新建 project / app,MVT (Model, View, Template)分别是什么、如何书写,django 中的 migration 和 migrate 如何工作,html 模版继承,如何操作数据库,如何使用 django 自带的 admin 端等基础知识。

不会涵盖到:

如何进行软件测试、如何打包部署等进阶内容。

总之,这篇博客适合想要快速上手 django 网页开发但又苦于寻找优质学习资料抑或阅读英文材料的同学。如果是这样,那么这篇博客应当可以成为你踏入 django 大门的一个不错的开始,但要想真正掌握 django,这篇博客并不足够。

内容很干,不过笔者也在努力尝试让文章不那么枯燥,因为有趣真的很重要。花几天时间跟着实现一遍,相信你一定可以学到不少东西。

笔者本人跟着教程实践过了,安全无毒,放心食用。

实现过程中遇到任何问题或有任何建议欢迎在评论区指出!笔者看到会及时修正。

成果展示

项目代码仓库:https://github.com/zhangbihan999/learndjango

image-20250110154906979

image-20250110154946288

image-20250110155009148

image-20250110155021619

环境配置

本文的所有命令都在安装了 django 包的 python 环境中执行。请首先确保您已经配置好了 python 环境,且使用pip install django命令在当前虚拟环境中安装了 django 包。

环境配置参考教程:

miniconda 环境配置

可以跟着上面文章的前三步走,到了第四步,文章作者使用的是 pycharm,如果你和我一样使用的是 vscode,那么请按照下面方法选择配置的虚拟环境:

image-20250110161306959

image-20250110161334343

选择想要使用的环境即可:

image-20250110161400626

1 创建 project

打开终端,找一个你喜欢的位置,执行mkdir learndjango命令创建一个叫做 learndjango 的文件夹。

执行cd learndjango进入该文件夹。

创建 django 项目的命令为:

django-admin startproject <项目名称> <指定文件夹>

这里我们执行:

django-admin startproject mysite .

这将会在 learndjango 文件夹(.代表当前文件夹)下创建一个名为 mysite 的 django 项目。

于是你现在可以看到如下文件结构:

learndjango/manage.pymysite/__init__.pysettings.pyurls.pyasgi.pywsgi.py

现在让我们检验这个 django 项目是否能正常工作。

执行下面的命令启动项目:

python manage.py runserver

可以看到终端有如下输出:

Watching for file changes with StatReloader
Performing system checks...System check identified no issues (0 silenced).You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.
January 07, 2025 - 16:49:42
Django version 5.1.4, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.

服务器运行起来了,进入 http://127.0.0.1:8000/ 网址,你将看到一个 "Congratulations!" 页面,说明项目能够正常运行!

这里本机就是服务器,程序在本地运行,方便我们在开发的时候能够及时预览修改效果。

2 创建 app

现在我们已经有了一个 project,接下来就可以在 project 里创建 app。

project 和 app 的区别是什么?

一个 app 就是一个能够执行某项功能的软件,比如,一个博客系统,一个投票软件等。

一个 project 则是构成某个网站的所有 app 及配置的集合。

一个 project 能够包含多个 app;一个 app 可以被多个 project 包含。

创建 app 的命令是:

python manage.py startapp <app名称>

保持服务器依然开启,我们重新打开一个终端来到learndjango文件夹下,执行:

python manage.py startapp blog

确保上述命令是在与 manage.py 平级的文件夹下执行。如果报错,请检查当前是否处于正确的、配有 django 的虚拟环境。

于是现在的文件结构就成了这样:

learndjango/manage.pymysite/__init__.pysettings.pyurls.pyasgi.pywsgi.pyblog/__init__.pyadmin.pyapps.pymigrations/__init__.pymodel.pytests.pyviews.py

blog/文件夹下将会是我们搭建的博客应用。

3 Django 三大元素——MVT

在正式步入开发之前,我们首先来学习 django 中的三个主要部件:model, view, template,也称作 MVT。

它们分别承担什么功能呢?

想象一下,客户端发送一个请求给服务器,服务器自然要给客户端一个响应。view 做的就是这个事情。view 的本质是函数,它接收用户请求,然后给出响应。

在处理用户请求时,有些请求难免涉及到数据库。比如说,用户现在要查询某个商品的价格,那么 view 在拿到这个请求时,就需要去访问数据库,从数据库里找这个商品的对应价格,然后再返回给用户。在这个过程中,商品就是一个 model,可以理解为数据库中的一样东西。我们在写代码的时候需要定义这个 model 的结构,它与数据库中的某个数据表相对应,是后端与数据库沟通的桥梁。

ok,假如我们拿到的返回数据很多、信息很庞杂,就需要做一些排版美化的工作,这就用到 template 了。template 的作用就是,规划以何种方式将后端返回的数据展示在前端中。

4 创建你的第一个 view

我们已经知道,view 就是一个接收用户请求然后返回响应的函数,那么接下来就创建最简单的 view。

打开blog/views.py并写入以下内容:

from django.http import HttpResponse   # 引入 http 响应对象def index(request):return HttpResponse("Hello, world. You're at the blog index.")

当有请求触发这个 view 时,它就会返回上述字符串信息。

如何触发这个 view 呢?我们需要为它分配一个 url,每当访问这个 url 时,该 view 就会被触发。

就像四大天王是掌管南天门的神一样,django 中,掌管每个 app 的 url 分配的神是urls.py文件!

由于 django 中默认是没有为每个初始化的 app 给出urls.py文件的,所以我们需要手动创建blog/urls.py文件,然后填入以下内容:

from django.urls import pathfrom . import views   # 引入当前文件夹下的 views.pyurlpatterns = [path("", views.index, name="index"),
]

path()方法至少需要传入两个参数,分别是:分配的 url、该 url 对应的 view(访问该 url 时会触发的方法)。这里我们再为它起一个名字name="index",后面会用到。

ok,现在我们就已经在 blog 这个 app 内定义了 url 的分配关系,接下来我们还得让管理这个 app 的 project (mysite) 知道 blog 的 url 分配规则。就好像是分级管理一样,blog/urls.py只管理 blog 内部的 url 分配,而mysite/urls.py则管理所有 app 的 url 分配。

mysite/urls.py是初始化 project 时会自动创建的,于是我们可以找到该文件直接打开,添加新的内容:

from django.contrib import admin
from django.urls import include, pathurlpatterns = [path('admin/', admin.site.urls),path('blog/', include("blog.urls"))
]

include()方法的作用是将一个 url 配置文件引入到当前的 url 配置中,从而组成一个层层嵌套的大的路由系统。

我们添加了path('blog/', include("blog.urls")),意味着当用户访问/blog/时,django 会自动去blog/urls.py文件里找相关的 url 配置。

整体逻辑就是,为每个 app 建立独立的urls.py文件,然后在 project 的urls.py里面引入,从而避免将所有 url 路由分配都写在 project 的urls.py中,造成臃肿和管理上的不便。

尝试访问 http://127.0.0.1:8000/blog/,可以看到,页面中展示了Hello, world. You're at the blog index.这行字符串,说明我们在blog/views.py中定义的index()方法被成功触发啦!(注:请确保已经在manage.py所在文件夹下执行python manage.py runserver拉起了本地服务器)

5 创建你的第一个 template

现在我们只是返回了一个字符串,所以直接展示在页面上似乎也看得过去,但是当我们有大量数据时,就必须要考虑数据的排版和布局。

数据的排版和布局通常由 html 文件来管理,这些 html 文件就是我们上面所说的 template。定义好 template 之后,还需要告诉index(),你在返回数据的时候不能只是返回数据,还需要用 template 渲染。

我们先完成后半部分工作,让index()渲染指定 template:

修改blog/views.py内容如下:

from django.shortcuts import renderdef index(request):return render(request, 'blog/index.html', {'name': 'z', 'content': "大太阳又出来啦"})

render()方法用来渲染模版,它接收三个参数,分别是 request、模版文件路径和传递给模版的内容。

于是当index()被触发时,它就会加载数据并渲染指定模版。

现在我们来定义模版。

在 django 中,render()方法默认会去当前 app 的templates文件夹下加载模版文件,换句话说,上面我们告诉render()blog/index.html找模版文件,实际上render()会去blog/templates/blog/index.html找文件。

所以首先需要手动创建一个templates文件夹在外层 blog 文件夹下,然后在templates下创建blog/index.html。一通操作下来,局部文件结构如下:

learndjango/blog/templates/blog/index.html

这里聪明的你可能会问,为什么在templates文件夹下还要建立一层blog文件夹呢?

这其实是 django 推荐的一种文件组织形式。如果多个应用都有相同名称的模版文件,可能会发生冲突,而使用子文件夹则可以清楚地表明模版属于哪个应用,避免混乱。

接下来往模版文件index.html里填东西:

<!DOCTYPE html>
<html>
<head><title>博客页面</title>
</head>
<body><h1>欢迎,{{ name }}!</h1><p>{{ content }}</p>
</body>
</html>

这里不会展开介绍 html 语法,你只需要知道,{{ }}是访问变量。上面我们在render()方法中指出,传给模版的数据是字典{'name': 'z', 'content': "大太阳又出来了"},现在在模版里面,使用{{name}}就能够访问到它对应的值,content也是如此。

一切到现在为止似乎都完成得很好,但还需要最后一步,我们需要把 blog 这个应用注册到 project 中,只有注册之后,project 才能访问到我们在 blog 这个 app 下定义的静态资源,包括上面定义的 templates,以及后面会涉及到的 models 等。

具体操作方式就是,来到mysite/setting.py文件下,找到INSTALLED_APPS这个列表并添加一个新的元素'blog'

现在再返回 http://127.0.0.1:8000/blog/,刷新一下,可以看到,“大太阳又出来啦”!

6 migration 与 admin 端

上面我们使用的数据都是自己定义的比较简单的数据,实际生活中,数据都是从数据库中提取出来的,下面我们介绍的 model 就是与数据库沟通的桥梁。

不过在介绍 model 之前,首先需要介绍 django 中与 model 紧密相关的一个概念:migration

不知道你是否有注意到,自你启动服务器以来,终端中已经很多次地弹出了下面这行红字:

You have 18 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

就是说有 18 个还没有 apply 的 migration,然后 django 提示你说使用python manage.py migrate命令来使 migrations 生效。

不着急执行命令,我们先来看看什么是migration

总的来讲,它就是对数据库表结构修改的描述

怎么理解,就像把大象装进冰箱要分三步,在 django 中,修改数据库表的结构则需要分成两步:

  1. 生成对数据库表结构修改的描述,也就是 migration
  2. 使用python manage.py migrations命令将 migration 中定义的修改应用在数据库上

这里我们的措辞很讲究,注意只有涉及到修改数据库表结构的修改才会生成 migration,比如,创建新的模型(表)、删除模型(表)、添加字段(列)、删除字段(列)等,而不涉及到修改表结构的操作,如在表中增加新记录、更新某些字段的值,则不会生成 migration。

这就是 migration。

那么为什么终端会提示我们有尚未应用的 migration 呢?

这是因为 django 在初始化 project 的时候,已经预先内置了一些应用,这些应用自身就携带了一些 migration,如果不应用这些 migration,那么应用就无法正常启动。

举个例子,细心的你一定发现了,在刚才和我们有过短暂谋面的mysite/urls.py文件中,它的urlpatterns的内容是这样的:

urlpatterns = [path('admin/', admin.site.urls),path('blog/', include("blog.urls"))
]

除了我们自己添加的blog/,还有一个 django 自带的admin/,那也就是说这个 url 已经被自动分配给了某个应用,那我们就试着访问一下 http://127.0.0.1:8000/admin/。

结果报错了。这就是因为admin/本身携带的 migration 还没有被采用,修改没有作用到数据库上,导致数据库中现在缺乏运行该应用的表,所以运行不起来。

重新打开一个终端,在learndjango文件夹下执行python manage.py migrate,然后再刷新 http://127.0.0.1:8000/admin/,可以看到,出现了一个管理员登录界面,这是 django 自带的后端管理服务。

要想登录,首先需要创建一个超级账户。

来到终端,执行python manage.py createsuperuser,输入邮箱密码创建账号即可。然后在登录页面输入信息便可进入管理员端。

django 自带的管理员端是我们操作数据库的图形界面。点击Users,就可以看到你刚刚创建的用户。后面当我们创建了新的表时,也可以通过管理员端直接操作自定义的表。

image-20250108172932764

7 创建你的第一个 model

ok,前菜介绍完了,接下来正式创建第一个 model。

上面说过,model 是对一类物品的结构描述,如果你有数据库知识,那么应该不难理解,如果没有,你可以理解为,model 描述了一类物体的结构。

以我们将要开发的博客应用为例,每篇博客就可以定义为一个 model,它应该包含作者、内容、发表日期等信息,那么这就是它的结构。

来到blog/models.py并填充如下内容:

class User(models.Model):username = models.CharField(max_length=150, unique=True)password = models.CharField(max_length=128)class Post(models.Model):author = models.ForeignKey(User, on_delete=models.CASCADE)created = models.DateTimeField(auto_now_add=True)title = models.CharField(max_length=200)body = models.TextField()

这个模型对应的数据库表如下所示:

  • User
    • id: django 默认为每个 model 自动添加一个主键字段id
    • username: CharField 表示填充内容为字符串
    • password: CharField 表示填充内容为字符串
  • Post
    • id: 同样由 django 自动生成
    • author: 外键,将每个 Post 对象与一个 User 对象相关联;on_delete=models.CASCADE表示当关联的 User 对象被删除后,所有绑定的 Post 对象也会被删除
    • created: 用 DateTimeField 并设置 auto_now_add=True,这样在创建记录时会自动填充为当前时间
    • title: 文章标题,CharField 表示填充内容为字符串
    • body: 文章内容,TextField 表示填充内容为文本域

还记得我们对数据库表的结构做出修改的两步吗?首先是创建 migration,然后应用 migration 到数据库。

所以首先我们需要为当前对models.py的修改生成对应的 migration。

生成 migration 的命令格式为:

python manage.py makemigrations <app 名称>

这里我们执行:

python manage.py makemigrations blog

于是可以看到终端输出如下内容:

Migrations for 'blog':blog/migrations/0001_initial.py+ Create model User+ Create model Post

说明 migration 成功生成。

然后执行:

python manage.py migrate

终端输出:

Operations to perform:Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:Applying blog.0001_initial... OK

至此我们便成功地在数据库中创建了两个新的表 User 和 post。

但是吧,这个表现在我们并能够直观地看到,有没有办法能够看到这个表呢?

有。

还记得我们的管理员端吗?如果能够把新建的表在管理员端展示就很方便观看了。

为了达到这个目的,我们需要在blog/admin.py文件中把新建的表注册到管理员端:

from django.contrib import adminfrom .models import User, Postadmin.site.register(User)
admin.site.register(Post)

然后来到 http://127.0.0.1:8000/admin/,刷新一下,即可看到 blog 应用下的两个新建的表:

image-20250108173029986

我们可以直接在这里向两个表中写入内容。

先来到Users中,注册一个账户:

image-20250108173413881

这样就添加成功了,但是它对这个用户的描述我不喜欢。我不想要它展示User object (1),而是展示用户名,怎么修改呢?

这就要回到blog/models.py中,重写User类自带的__str__()方法:

class User(models.Model):username = models.CharField(max_length=150, unique=True)password = models.CharField(max_length=128)def __str__(self):return self.username

回到网页,刷新一下,可以看到,现在展示的就是用户名啦:

image-20250108173757871

这里考考你:为什么这次我们修改了models.py文件后却没有生成 migration 呢?

因为我们只是重写了一个方法,并没有修改数据库表的结构。

然后我们再新建一个 Post 对象:

image-20250108174122021

同样的,我想让它用博客的标题命名而不是Post object (1),是不是也要回去修改blog/models.py呀:

class Post(models.Model):author = models.ForeignKey(User, on_delete=models.CASCADE)created = models.DateTimeField(auto_now_add=True)title = models.CharField(max_length=200)body = models.TextField()def __str__(self):return self.title

刷新一下,目标达成:

image-20250108174304889

8 连通 view, template 和 model

至此,我们已经在数据库中创建好了表并填充了一些数据,接下来需要做的事情是,从数据库中提取数据,然后填充至 template,展示到页面上。

首先修改blog/views.py文件的内容,让它能够从数据库中提取数据:

from django.shortcuts import render
from .models import Postdef index(request):posts = Post.objects.all()return render(request, 'blog/index.html', {'posts': posts})

posts = Post.objects.all()存储 Post 表中的所有对象,然后在render()方法里以字典的格式返回给index.html,对应的键为posts

然后来修改blog/index.html定义的模板:

<!DOCTYPE html>
<html>
<head><title>博客页面</title>
</head>
<body>{% for post in posts %}<article><header><div><h1>{{ post.title }}</h1><div>by {{ post.author.username }} on {{ post.created|date:"Y-m-d" }}</div></div></header></article>{% endfor %}
</body>
</html>

{% %}在 html 文件中表示的是控制流。{% for post in posts %}表达的含义是遍历传入 html 的字典posts中的所有对象,并把当前访问的对象记作post,于是在{% endfor %}之前的部分我们就可以通过{{ post.<属性> }}的形式访问字典中的对应值。

回到 http://127.0.0.1:8000/blog/ 界面,可以看到我们刚才在管理员端添加的 Post 对象成功展示出来啦!

9 实现注册/登录/登出功能

我们想实现一个博客应用,应用就离不开注册/登录/登出功能,所以接下来就实现它们。

首先实现注册功能。

先创建templates/blog/register.html文件,并填充如下内容,作为注册时用户需要填充的表单:

<!DOCTYPE html>
<html>
<head><title>博客页面</title>
</head>
<body><form method="post"><label for="username">Username</label><input name="username" id="username" required><label for="password">Password</label><input type="password" name="password" id="password" required><input type="submit" value="Register"></form>
</body>

然后我们需要回到blog/views.py中,定义一个register()方法,当它被触发时,就会渲染上面的register.html界面:

def register(request):return render(request, 'blog/register.html')

怎么触发这个方法呢?

我们需要在blog/urls.py中为这个 view 绑定一个 url,当 url 被触发时,register()方法也随之触发,于是现在的urlpatterns内容应该如下:

urlpatterns = [path("", views.index, name="index"), path("register/", views.register, name="register")
]

现在尝试访问 http://127.0.0.1:8000/blog/register/,可以看到表单成功展示:

image-20250109001415080

任务还没有完成。

当我们填完表单并点击Register按钮后,register()方法还需要接收表单内容,将其创建成一个User对象存入User数据库中,才算创建完成。那我们就在register()中添加这段处理逻辑:

from django.shortcuts import redirect   # 记得引入用到的方法
from .models import Userdef register(request):if request.method == "POST":username = request.POST.get("username")password = request.POST.get("password")user = User.objects.create(username=username,password=password)return redirect('index')return render(request, 'blog/register.html')

上述内容中有一个判断条件if request.method == "POST":,就是说只有 request 的类型为POST时才会触发if中的内容。

这里介绍一下GETPOST这两种请求类型。

通常而言,当我们访问一个网页时,请求类型都是GET,就比如我们访问 http://127.0.0.1:8000/blog/register/ 和 http://127.0.0.1:8000/blog/ 时,发出的访问请求都是GET类型。

因此当我们直接访问 http://127.0.0.1:8000/blog/register/ 时,if不会被触发,register()会直接执行return render(request, 'blog/register.html')这行代码,只是把表单加载出来。

而当我们填完表格内容点击Register按钮提交数据时,浏览器会把表单填写的数据发送给register()方法,且发送的请求类型是POST,因为我们在register.html中定义了<form method="post">

于是if条件被满足,我们从request.POST这个字典中分别取出键usernamepassword对应的值,然后用它们创建一个新的User对象,并通过return redirect('index')返回主页。

看起来一切都很顺利,让我们尝试注册一个 user 吧(完成表单内容,然后点击Register按钮)。

很好,不出意外的话应该是出意外了,出现了如下报错:

image-20250109003250181

这里提到了 CSRF 验证,它是 django 启用的一种保护机制,这里不深入探讨该机制,只需要知道,当我们在 html 表单中使用POST方法时,必须要在表单中包含{% csrf_token %},以便生成并发送 CSRF 令牌,从而通过 django 设定的保护机制,所以现在的register.html表单应该是这样:

<form method="post">{% csrf_token %}<label for="username">Username</label><input name="username" id="username" required><label for="password">Password</label><input type="password" name="password" id="password" required><input type="submit" value="Register">
</form>

回到 http://127.0.0.1:8000/blog/register/,刷新页面,重新创建一个用户再提交,可以看到我们成功返回 index 页面,说明用户创建成功。

接下来实现登录功能。

同样的,首先创建templates/blog/login.html并填充内容:

<!DOCTYPE html>
<html>
<head><title>博客页面</title>
</head>
<body><form method="post">{% csrf_token %}<label for="username">Username</label><input name="username" id="username" required><label for="password">Password</label><input type="password" name="password" id="password" required><input type="submit" value="Log In"></form>
</body>

然后和上面的注册一样,需要定义login()方法,分配 url,这里不再赘述,直接给出相关内容:

blog/views.py中新增方法:

from django.contrib import messagesdef login(request):if request.method == "POST":username = request.POST.get("username")password = request.POST.get("password")user = User.objects.get(username=username)if user is not None:if password == user.password:request.session['user_id'] = user.id  # 记录当前登录的用户request.session['username'] = user.usernamemessages.success(request, "登录成功!")return redirect('index')else:messages.error(request, "密码不正确!")else:messages.error(request, "用户不存在!")return render(request, 'blog/login.html')

blog/urls.pyurlpatterns修改如下:

urlpatterns = [path("", views.index, name="index"), path("register/", views.register, name="register"),path("login/", views.login, name="login")
]

在上面的login()方法中有一行代码叫做request.session['user_id'] = user.id,怎么理解呢?

你可以把session想象成一个临时储物柜,每个用户登录后,django 会为他们分配一个独一无二的储物柜,用来保存当前用户的信息,之后当用户访问网站的其他页面的时候,django 都能够根据储物柜的内容知道这个用户是谁。

ok,现在尝试访问 http://127.0.0.1:8000/blog/login/ 并用刚才注册的用户登录,可以跳转到主页面则说明成功!

one more step,记得把register()方法中的return redirect('index')修改成return redirect('login'),刚才因为没有创建login路由所以用index临时顶替了,正常来讲注册成功后应该是跳转到登录界面而不是主界面。

最后再来实现登出。

blog/views.py中新增方法:

def logout(request):request.session.flush()   # 清除所有 session 数据messages.success(request, "您已成功退出登录!")return redirect('login')

blog/urls.pyurlpatterns添加logout/

urlpatterns = [path("", views.index, name="index"), path("register/", views.register, name="register"),path("login/", views.login, name="login"),path("logout/", views.logout, name="logout"),
]

在上面的logout()方法中有一行request.session.flush(),这是因为上面在登录的时候,我们为用户分配了一个临时储物柜,那么当用户登出时,我们就要清空储物柜中关于用户的所有内容。

为了检验logout()能否正常工作,我们在index.html里添加一个跳转至名称为logout的 url (path("logout/", views.logout, name="logout")声明了这个 url 的名称为logout)的超链接:

<!DOCTYPE html>
<html>
<head><title>博客页面</title>
</head>
<body><a href="{% url 'logout' %}">Log Out</a>   <!-- 就是这个超链接 -->{% for post in posts %}......{% endfor %}
</body>
</html>

试着在index界面点击一下带有Log Out标签的超链接,如果能跳回登录界面就说明logout()正常工作!

10 模版继承

上面我们已经实现了注册/登录/登出功能,接下来则需要把它们添加到页面当中去,变成用户可以点击的按钮,从而实现页面间的跳跃。

在此之前,大家回忆一下,生活中很多网站都有一个顶部导航栏,不管你访问网站下的哪个网页,这个顶部导航栏的内容都是不会改变的,或者说是不会刷新的。

这个顶部导航里通常放的,就是注册、登录、登出这种常用的基本功能,无论用户在哪个页面都能够点击它。

那么如何实现这种呢?

我们知道,控制页面展示格式的是 templates,也就是之前写的那些 html 文件。可是如果在每个页面的 html 文件里都手写一个顶部导航栏出来,显然不够高效,不够优雅,能不能够只在一个 html 文件里写一遍,然后让其他 html 对该顶部导航栏元素进行复用呢?

可以。这就需要用到 html 文件的模版继承功能。

如果你学过面向对象编程,那么就很好理解,就像类可以继承一样,在 html 文件中,模板也可以继承。一个模版的子类可以继承父类的内容,并在此基础上进行修改。

为了实现这个可继承的、可复用的顶部导航栏,我们首先创建templates/blog/base.html文件并写入:

<!doctype html>
<title>{% block title %}{% endblock %} - Blog</title>
<nav><h1>Blog</h1><ul>{% if request.session.user_id %}<li><span>{{ request.session.username }}</span></li><li><a href="{% url 'logout' %}">Log Out</a></li>{% else %}<li><a href="{% url 'register' %}">Register</a></li><li><a href="{% url 'login' %}">Log In</a></li>{% endif %}</ul>
</nav>
<section><header>{% block header %}{% endblock %}</header>{% block content %}{% endblock %}
</section>

模版继承的核心概念是blockblock包含的部分,就是子模板可以修改的部分。

上面的base.html中有 3 个block,分别是:

{% block title %}{% endblock %}
{% block header %}{% endblock %}
{% block content %}{% endblock %}

现在这 3 个block的内容都为空,我们可以在子模板中修改,让它们不为空。

login.html为例:

{% extends 'blog/base.html' %}{% block title %}
Log In
{% endblock %}{% block header %}<h1>Log In</h1>
{% endblock %}{% block content %}<form method="post">{% csrf_token %}<label for="username">Username</label><input name="username" id="username" required><label for="password">Password</label><input type="password" name="password" id="password" required><input type="submit" value="Log In"></form>
{% endblock %}

{% extends 'blog/base.html' %}指定了要继承的模板文件。

{% block title %}{% endblock %}由空变成了{% block title %}Log In{% endblock %},因此当你查看当前所在网页blog/login/的标签时,会发现它的名字为Log In - Blog

{% block header %}{% endblock %}由空变成了{% block header %}Log In{% endblock %}{% block content %}{% endblock %}由空变成了一个form表格,因此你能够看到图中结构:

image-20250109225035747

还有一点值得注意,那就是在base.html中,还有这样一块无序列表内容,也就是导航栏(被子类重用的部分):

	<ul>{% if request.session.user_id %}<li><span>{{ request.session.username }}</span></li><li><a href="{% url 'logout' %}">Log Out</a></li>{% else %}<li><a href="{% url 'register' %}">Register</a></li><li><a href="{% url 'login' %}">Log In</a></li>{% endif %}</ul>

{% if request.session.user_id %}会尝试从当前session中获取user_id,就是我们刚才讲的login()方法会将用户信息存入session这个保险柜。

如果保险柜里有user_id,说明已经是登录状态,此时我们展示的是{{ request.session.username }}用户名称和Log Out登出链接。

如果保险柜里没有user_id,说明当前没有用户登录,此时展示的就是Register注册链接和Log In登录链接。

同样的逻辑,我们让register.htmlindex.html也去继承base.html模板,实现导航栏的复用。

register.html:

{% extends 'blog/base.html' %}{% block title %}
Register
{% endblock %}{% block header %}<h1>Register</h1>
{% endblock %}{% block content %}<form method="post">{% csrf_token %}<label for="username">Username</label><input name="username" id="username" required><label for="password">Password</label><input type="password" name="password" id="password" required><input type="submit" value="Register"></form>
{% endblock %}

index.html:

{% extends 'blog/base.html' %}{% block title %}
Posts
{% endblock %}{% block header %}<h1>Posts</h1>
{% endblock %}{% block content %}{% for post in posts %}<article><header><div><h1>{{ post.title }}</h1><div>by {{ post.author.username }} on {{ post.created|date:"Y-m-d" }}</div></div></header></article>{% endfor %}
{% endblock %}

现在就可以随意在页面之间跳跃啦!

11 用 css 文件美化页面

现在这个博客软件已经初具雏形了,但看上去还是过于朴素,接下来就给它来点小小的美化。

我们知道,html 文件控制的是页面内容的基本排版,如果要控制页面样式,则需要用到 css 文件。

在 django 中,css 文件属于静态文件 (static file),且 django 一般会去static文件夹下找静态文件,就像render()一般会去templates文件夹下找 html 文件一样,因此首先创建一个和templates文件夹平级的文件夹static

templates文件夹的使用规则一样,为了避免 django 在找文件时发生冲突,static文件夹下也需要创建一个以当前 app 命名的子文件夹,然后在里面放该 app 的静态文件,这里我们创建一个style.css文件。一通操作下来局部文件结构应该是这样:

learndjango/blog/templates/static/blog/style.css

这篇博客不关注如何书写 css 文件,所以你直接 copy 以下内容到style.css即可:

html { font-family: sans-serif; background: #eee; padding: 1rem; }
body { max-width: 960px; margin: 0 auto; background: white; }
h1 { font-family: serif; color: #377ba8; margin: 1rem 0; }
a { color: #377ba8; }
hr { border: none; border-top: 1px solid lightgray; }
nav { background: lightgray; display: flex; align-items: center; padding: 0 0.5rem; }
nav h1 { flex: auto; margin: 0; }
nav h1 a { text-decoration: none; padding: 0.25rem 0.5rem; }
nav ul  { display: flex; list-style: none; margin: 0; padding: 0; }
nav ul li a, nav ul li span, header .action { display: block; padding: 0.5rem; }
.content { padding: 0 1rem 1rem; }
.content > header { border-bottom: 1px solid lightgray; display: flex; align-items: flex-end; }
.content > header h1 { flex: auto; margin: 1rem 0 0.25rem 0; }
.flash { margin: 1em 0; padding: 1em; background: #cae6f6; border: 1px solid #377ba8; }
.post > header { display: flex; align-items: flex-end; font-size: 0.85em; }
.post > header > div:first-of-type { flex: auto; }
.post > header h1 { font-size: 1.5em; margin-bottom: 0; }
.post .about { color: slategray; font-style: italic; }
.post .body { white-space: pre-line; }
.content:last-child { margin-bottom: 0; }
.content form { margin: 1em 0; display: flex; flex-direction: column; }
.content label { font-weight: bold; margin-bottom: 0.5em; }
.content input, .content textarea { margin-bottom: 1em; }
.content textarea { min-height: 12em; resize: vertical; }
input.danger { color: #cc2f2e; }
input[type=submit] { align-self: start; min-width: 10em; }

同时我们还要对base.html做必要修改:

添加对blog/style.css文件的引用并为section组件附上class="content"从而使它能够按照style.css中定义的方式渲染。

<!doctype html>
<title>{% block title %}{% endblock %} - Blog</title>{% load static %}
<link rel="stylesheet" href="{% static 'blog/style.css' %}"><nav>
......
<nav>
<section class="content">......
</section>

由于上面这些修改涉及到了文件夹层面,所以刷新并不能使改变生效,需要在终端终止服务器后使用python manage.py runserver命令重新启动才行。

重启后访问 http://127.0.0.1:8000/blog/login/,会发现页面美化了许多:

image-20250109234753715

登录进去看看主页面:

image-20250109234829500

现在只展示了每个博客的标题,我们把内容也展示出来。

修改index.html如下:

{% extends 'blog/base.html' %}{% block title %}
Posts
{% endblock %}{% block header %}<h1>Posts</h1>
{% endblock %}{% block content %}{% for post in posts %}<article class="post"><header><div><h1>{{ post.title }}</h1><div class="about">by {{ post.author.username }} on {{ post.created|date:"Y-m-d" }}</div></div></header><p class="body">{{ post.body }}</p></article>{% endfor %}
{% endblock %}

这次除了增加<p class="body">{{ post.body }}</p>这行代码来展示博客内容,我们还为<article class="post"><div class="about">都增加了class属性,这是因为style.css文件中定义的样式是通过class属性来找到它要渲染的对象的。

后面书写 html 内容时会直接把class写进去,和style.css做个对应,不再过多介绍。

可以看到,样式被成功应用啦:

image-20250110135529382

12 实现新建/更新/删除博客功能

基本框架已经搭建完成,接下来实现一些 app 内部的功能,比如新建、更新、删除博客。

首先是新建博客。

首先在blog/views.py中定义create()方法:

def create(request):if request.method == "POST":title = request.POST.get("title")body = request.POST.get("body")error = Noneif title is None:error = "标题不能为空"elif body is None:error = "正文不能为空"if error is None:user = User.objects.get(id=request.session['user_id'])  # 匹配用户 id 来获取 User 实例post = Post(title=title, body=body, author=user)post.save()  # 保存到数据库return redirect('index')return render(request, 'blog/create.html')

然后创建templates/blog/create.html文件并定义表单组件:

{% extends 'blog/base.html' %}{% block title %}
New Post
{% endblock %}{% block header %}<h1>New Post</h1>
{% endblock %}{% block content %}<form method="post">{% csrf_token %}<label for="title">Title</label><input name="title" id="title" placeholder="输入标题"  required><label for="body">Body</label><textarea name="body" id="body" placeholder="输入正文"></textarea><input type="submit" value="Save"></form>
{% endblock %}

接着在blog/urls.py中为该 view 绑定一个 url:

urlpatterns = [......path("create/", views.create, name="create")
]

同时我们还要修改index.html,把创建新博客的选项展示给用户:

......
{% block header %}<h1>Posts</h1>{% if request.session.user_id %}<a class="action" href="{% url 'create' %}">New</a>{% endif %}
{% endblock %}
......

这里使用了条件渲染,只有当前已有用户登录时才展示新建选项。

然后试着新建一个博客吧!

image-20250110142235991

表单提交后自动跳转至主页面,成功创建博客。

然后是修改博客。

依然先在blog/views.py中定义update()方法:

from django.shortcuts import get_object_or_404def update(request, post_id):# 使用 django 的 get_object_or_404() 方法尝试获取对象,如果不存在则返回 404 页面post = get_object_or_404(Post, id=post_id)if request.method == "POST":title = request.POST.get("title")body = request.POST.get("body")error = Noneif title is None:error = "标题不能为空"elif body is None:error =  "正文不能为空"if error is None:# 更新博客内容post.title = titlepost.body = bodypost.save()  # 保存到数据库return redirect('index')return render(request, 'blog/update.html', {'post': post})

然后书写文件templates/blog/update.html:

{% extends 'blog/base.html' %}{% block title %}
Edit "{{ post.title }}"
{% endblock %}{% block header %}<h1>Edit "{{ post.title }}"</h1>
{% endblock %}{% block content %}<form method="post">{% csrf_token %}<label for="title">Title</label><input name="title" id="title" value="{{ post.title }}" required><label for="body">Body</label><textarea name="body" id="body">{{ post.body }}</textarea><input type="submit" value="Save"></form>
{% endblock %}

再为该 view 在blog/urls.py中分配一个 url:

urlpatterns = [......path("<int:post_id>/update/", views.update, name="update"),
]

仔细观察会发现,我们定义的update()方法和之前的所有 view 不一样,除了request,它还需要接受一个参数post_id,因为我们必须知道当前修改的是哪一个 post。

那这个参数的值从哪里传过去呢?

这里你会发现为update()分配的 url 也和前面的 url 不一样,它的path()里的第一个参数是"<int:post_id>/update/"而不是update/post_id这个参数的值就当你在 url 中键入要修改的博客的 id 时传入的。

然而 id 是记不住的,终究不能够让用户每次都在 url 中输入自己要修改的博客的 id,所以我们在index.html中为每个博客添加一个跳跃至修改界面的超链接:

{% block content %}{% for post in posts %}<article class="post"><header><div>......</div>{% if request.session.user_id == post.author.id %}<a class="action" href="{% url 'update' post_id=post.id %}">Edit</a>{% endif %}</header>......</article>{% endfor %}
{% endblock %}

同样使用到了条件渲染{% if request.session.user_id == post.author.id %},这是因为用户只有权限修改自己创建的博客。

当点击超链接<a class="action" href="{% url 'update' post_id=post.id %}">Edit</a>时,会自动将post.id赋值给post_id,也就是path("<int:post_id>/update/", views.update, name="update")中定义的变量,从而完成变量的传递。因此你只需要点击链接,django 就自动为你完成了背后的一系列逻辑,然后将修改页面呈现在你面前!

最后来实现删除博客。

这次反着来,我们首先在blog/urls.py中为删除分配 url:

urlpatterns = [......path("<int:post_id>/delete", views.delete, name="delete")
]

然后在templates/blog/update.html中添加一个触发delete()的按钮:

{% block content %}<form method="post">......</form><form action="{% url 'delete' post_id=post.id %}" method="post">{% csrf_token %}<input class="danger" type="submit" value="Delete" onclick="return confirm('确定删除博客吗?');"></form>
{% endblock %}

action="{% url 'delete' post_id=post.id %}"指定了表单提交后将由哪个 url 来处理请求。

最后在blog/views.py中实现delete():

def delete(request, post_id):post = get_object_or_404(Post, id=post_id)post.delete()return redirect('index')

现在就实现了博客的创建、更新和删除自由!开始尽情在博客应用里畅玩吧!

13 结语

恭喜你完成了本博客的学习,相信你对 django 项目开发已经有了一定程度的了解。

接下来可以在网络上搜集更多的进阶知识,进一步探索 django 的世界!

有任何建议欢迎在评论区留言哦~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/869034.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

DataGrip2024.3.3最新2099年激活教程附带激活工具

前言 看到新版本发布了,准备把DataGrip 2024.3.3来个大变身,激活依旧可破解至 2099 年,安装过DataGrip的小伙伴需要卸载后再重装。接下来,我将为大家详细介绍这一版本的更新内容及具体的激活方法 安装环境 [名称]:DataGrip [大小]:500MB [版本]:2024.3.3 [语言]:简体中…

QD_0001:浅谈前端框架原理

最近在看卡颂大佬的《React 设计原理》,看了第一章,就有一种醍醐灌顶的感觉**,于是决定记录分享一下这一章的内容。这里也极力推荐各位小伙伴读一下。 本人其实是 Vue 开发者,没有太多地使用过 React,只是多多少少听过一些概念,能看懂一些 React 代码 因此我的文章,会更…

CF div2 992(A~E)

VP赛时三题。被AB题卡炸了,C题反倒发挥正常,D题可惜只想到了一半 A 没发现数据范围很小可以暴力 + 题干减号看成了加号,导致创造了二十多分钟才过A题的新纪录( code B 贪心 or 找规律,也是牢了一会儿。 显然要贪心地创造出能用上第二个操作的情景。所以从\(1\)位置出发,每…

深度学习入门之手写数字识别

模型定义 我们使用 CNN 和 MLP 来定义模型: import torch.nn as nnclass Model(nn.Module):def __init__(self):"""定义模型结构输入维度为 1 * 28 * 28 (C, H, W)"""super(Model, self).__init__()# 卷积层 1self.conv1 = nn.Sequential(# 二维…

省选构造专题

省选构造专题 The same permutation 首先打个表,发现在 \(1\le n\le 5\) 之内的是否有合法方案的情况为 √√√ 大了打不出来了。 考虑一下 \(4,5\) 连续有解,注意到一个偶数有解,则这个偶数 \(+1\) 也必定有解。 考虑以下构造方法即对于某一个交换,可以在它前后各添加一个…

自动化进程如何优化敏捷开发中的工作流

一、敏捷开发管理工具的现状 1.1 敏捷开发管理工具的基本功能 目前,敏捷开发管理工具的主要功能包括任务管理、进度跟踪、团队协作、资源分配、需求变更管理等。这些工具通常采用看板、任务板、甘特图、Burndown图等形式,帮助团队成员可视化地管理任务、跟踪项目进度、协调跨…

不知道前端代码哪里报错了?我有七种方式去监控它!

大家好,我是桃子,前端小菜鸟一枚,在日常工作中,有时候是不是不知道前端代码哪里报错了今天我给大家分享七中方法去监控它 我们先来说说前端中的错误类型有哪一些 错误类型 1、SyntaxError SyntaxError 是解析时发生语法错误,这个错误是捕获不到的,因为它是发生在构建阶段,…

web.config站内301永久重定向代码示例

注:此代码只适用于IIS服务器,如需要将123.asp重定向到123.html,请使用以下代码。 修改说明: 在web.config文件中添加301重定向规则,将123.asp重定向到123.html。<?xml version="1.0" encoding="UTF-8"?> <configuration><system.web…

请问云服务器需要开放哪些常用端口?

云服务器需要开放的端口与具体使用环境是有关系的,开放的端口越多,存在的安全隐患也就越大,所以开放端口越少越好。服务类型 端口 说明Web服务 80(HTTP), 443(HTTPS) 提供网站访问服务。FTP 21(文件管理) 提供文件传输服务。注:21端口可以关闭或修改。远程连接服务 3…

Audacity 3.7 (Linux, macOS, Windows) - 开源音频编辑器和录音工具

Audacity 3.7 (Linux, macOS, Windows) - 开源音频编辑器和录音工具Audacity 3.7 (Linux, macOS, Windows) - 开源音频编辑器和录音工具 Audacity is the worlds most popular audio editing and recording app 请访问原文链接:https://sysin.org/blog/audacity/ 查看最新版。…

CAP:Serverless + AI 让应用开发更简单

AI 已被广泛视为推动行业进步的关键力量,其在各行业的落地步伐加快。企业在构建 AI 应用开发过程中经常会面临 AI 技术门槛过高、试错周期过长、GPU 资源昂贵且弹性能力不足、缺乏配套工具、业务与模型的开发运维过于割裂、缺乏定制化能力等挑战,成为企业构建 AI 应用的『绊脚…

【运维自动化-作业平台】如何使用全局变量之密文类型?

密文类型的全局变量使用场景相对较少,使用方式也是直接引用即可,目前仅支持shell。一起来看看如何使用实操演示 1、新建作业时创建一个密文类型的全局变量app_secret2、添加一个执行脚本的步骤,脚本里打印下这个全局变量3、调试执行更多应用场景 上面这个示例是用最简单的ec…