Django博客开发基础教程
一、前言
1.1.环境
python版本:3.6
Django版本:1.11.6
1.2.预览效果
最终搭建的blog的样子,基本上满足需求了。框架搭好了,至于CSS,可以根据自己喜好随意搭配。
二、建立博客应用
2.1.建立项目和应用
创建工程blogproject
python manage.py startproject blogproject
创建blog应用
python manage.py startpapp blog
打开 blogproject\ 目录下的 settings.py 文件,找到 INSTALLED_APPS
设置项,将 blog 应用添加进去。
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', ]
2.2.目录结构
三、创建blog的数据库模型
3.1.设计博客的数据库表结构
博客最主要的功能就是展示我们写的文章,它需要从某个地方获取博客文章数据才能把文章展示出来,通常来说这个地方就是数据库。我们把写好的文章永久地保存在数据库里,当用户访问我们的博客时,Django 就去数据库里把这些数据取出来展现给用户。
博客的文章应该含有标题、正文、作者、发表时间等数据。一个更加现代化的博客文章还希望它有分类、标签、评论等。为了更好地存储这些数据,我们需要合理地组织数据库的表结构。
我们的博客初级版本主要包含博客文章,文章会有分类以及标签。一篇文章只能有一个分类,但可以打上很多标签。我们把分类和标签做成单独的数据库表,再把文章和分类、标签关联起来。下面分别是分类和标签的数据库表:
分类id 分类名
1 python
2 Django
标签id 标签名
1 python学习
2 Django学习
3.2.编写博客模型代码
分类数据库表:
# blog/models.py from django.db import models class Category(models.Model): name = models.CharField(max_length=100)
Category
就是一个标准的 Python 类,它继承了 models.Model
类,类名为 Category
。Category
类有一个属性 name
,它是 models.CharField
的一个实例。
我们需要 3 个表格:文章(Post)、分类(Category)以及标签(Tag),下面就来分别编写它们对应的 Python 类。模型的代码通常写在相关应用的 models.py 文件里
# blog/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): ''' Django 要求模型必须继承 models.Model 类。 Category 只需要一个简单的分类名 name 就可以了。 CharField 指定了分类名 name 的数据类型,CharField 是字符型, CharField 的 max_length 参数指定其最大长度,超过这个长度的分类名就不能被存入数据库。 ''' name = models.CharField(max_length=100) class Tag(models.Model): '''标签''' name = models.CharField(max_length=100) class Post(models.Model): '''文章''' # 文章标题 title = models.CharField(max_length=70) # 文章正文,我们使用了 TextField。 # 存储比较短的字符串可以使用 CharField,但对于文章的正文来说可能会是一大段文本,因此使用 TextField 来存储大段文本。 body = models.TextField() # 这两个列分别表示文章的创建时间和最后一次修改时间,存储时间的字段用 DateTimeField 类型。 created_time = models.DateTimeField() modified_time = models.DateTimeField() # 文章摘要,可以没有文章摘要,但默认情况下 CharField 要求我们必须存入数据,否则就会报错。 # 指定 CharField 的 blank=True 参数值后就可以允许空值了。 excerpt = models.CharField(max_length=200,blank=True) # 我们在这里把文章对应的数据库表和分类、标签对应的数据库表关联了起来,但是关联形式稍微有点不同。 # 我们规定一篇文章只能对应一个分类,但是一个分类下可以有多篇文章,所以我们使用的是 ForeignKey,即一对多的关联关系。 # 而对于标签来说,一篇文章可以有多个标签,同一个标签下也可能有多篇文章,所以我们使用 ManyToManyField,表明这是多对多的关联关系。 # 同时我们规定文章可以没有标签,因此为标签 tags 指定了 blank=True。 category = models.ForeignKey(Category,on_delete=models.CASCADE) tags = models.ManyToManyField(Tag,blank=True) # 文章作者,这里 User 是从 django.contrib.auth.models 导入的。 # django.contrib.auth 是 Django 内置的应用,专门用于处理网站用户的注册、登录等流程,User 是 Django 为我们已经写好的用户模型。 # 这里我们通过 ForeignKey 把文章和 User 关联了起来。 # 因为我们规定一篇文章只能有一个作者,而一个作者可能会写多篇文章,因此这是一对多的关联关系,和 Category 类似。 author = models.ForeignKey(User,on_delete=models.CASCADE)
四、迁移数据库
4.1.设置数据库为Mysql
更改setting.py默认配置
# DATABASES = { # 'default': { # 'ENGINE': 'django.db.backends.sqlite3', # 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), # } # } DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'django', #数据库名字 'USER': 'root', #账号 'PASSWORD': '123456', #密码 'HOST': '127.0.0.1', #IP 'PORT': '3306', #端口 } }
导入Pymysql
# blog/__init__.py import pymysql pymysql.install_as_MySQLdb()
4.2.迁移数据库
分别运行下面两条命令
python manage.py makemigrations
python manage.py migrate
当我们执行了 python manage.py makemigrations
后,Django 在 blog 应用的 migrations\ 目录下生成了一个 0001_initial.py 文件,这个文件是 Django 用来记录我们对模型做了哪些修改的文件。目前来说,我们在 models.py 文件里创建了 3 个模型类,Django 把这些变化记录在了 0001_initial.py 里。
不过此时还只是告诉了 Django 我们做了哪些改变,为了让 Django 真正地为我们创建数据库表,接下来又执行了 python manage.py migrate
命令。Django 通过检测应用中 migrations\ 目录下的文件,得知我们对数据库做了哪些操作,然后它把这些操作翻译成数据库操作语言,从而把这些操作作用于真正的数据库。
你可以看到命令的输出除了 Applying blog.0001_initial... OK 外,Django 还对其它文件做了操作。这是因为除了我们自己建立的 blog 应用外,Django 自身还内置了很多应用,这些应用本身也是需要存储数据的。可以在 settings.py 的 INSTALLED_APP
设置里看到这些应用,当然我们目前不必关心这些。
#blogproject/settings.py INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'blog', ]
运行下面的命令将看到输出了经 Django 翻译后的数据库表创建语句,这有助于你理解 Django ORM 的工作机制。
python manage.py sqlmigrate blog 0001
4.3.存数据
打开一个交互式命令行
python manage.py shell
首先我们来创建一个分类和一个标签:
我们首先导入 3 个之前写好的模型类,然后实例化了一个 Category
类和一个 Tag
类,为他们的属性 name
赋了值。为了让 Django 把这些数据保存进数据库,调用实例的 save
方法即可。
创建文章之前,我们需要先创建一个 User,用于指定文章的作者。创建 User 的命令 Django 已经帮我们写好了,依然是通过 manage.py 来运行。首先exit()退出命令交互栏,运行 python manage.py createsuperuser
命令并根据提示创建用户:用户名,邮箱,密码
再次运行 python manage.py shell
进入 Python 命令交互栏,开始创建文章:
由于我们重启了 shell,因此需要重新导入了 Category
、Tag
、Post
以及 User
。我们还导入了一个 Django 提供的辅助模块 timezone,这是因为我们需要调用它的 now()
方法为 created_time
和 modified_time
指定时间,容易理解 now
方法返回当前时间。然后我们根据用户名和分类名,通过 get
方法取出了存在数据库中的 User
和 Category
(取数据的方法将在下面介绍)。接着我们为文章指定了 title
、body
、created_time
、modified_time
值,并把它和前面创建的 Category 以及 User 关联了起来。允许为空 excerpt
、tags
我们就没有为它们指定值了。
4.4.取数据
数据已经存入数据库了,现在要把它们取出来看看:
objects
是我们的模型管理器,它为我们提供一系列从数据库中取数据方法,这里我们使用了 all
方法,表示我们要把对应的数据全部取出来。可以看到 all
方法都返回了数据,这些数据应该是我们之前存进去的,但是显示的字符串有点奇怪,无法看出究竟是不是我们之前存入的数据。为了让显示出来的数据更加人性化一点,我们为 3 个模型分别增加一个 __str__
方法:
# blog/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): ''' Django 要求模型必须继承 models.Model 类。 Category 只需要一个简单的分类名 name 就可以了。 CharField 指定了分类名 name 的数据类型,CharField 是字符型, CharField 的 max_length 参数指定其最大长度,超过这个长度的分类名就不能被存入数据库。 ''' name = models.CharField(max_length=100) def __str__(self): return self.name class Tag(models.Model): '''标签''' name = models.CharField(max_length=100) def __str__(self): return self.name class Post(models.Model): '''文章''' # 文章标题 title = models.CharField(max_length=70) # 文章正文,我们使用了 TextField。 # 存储比较短的字符串可以使用 CharField,但对于文章的正文来说可能会是一大段文本,因此使用 TextField 来存储大段文本。 body = models.TextField() # 这两个列分别表示文章的创建时间和最后一次修改时间,存储时间的字段用 DateTimeField 类型。 created_time = models.DateTimeField() modified_time = models.DateTimeField() # 文章摘要,可以没有文章摘要,但默认情况下 CharField 要求我们必须存入数据,否则就会报错。 # 指定 CharField 的 blank=True 参数值后就可以允许空值了。 excerpt = models.CharField(max_length=200,blank=True) # 我们在这里把文章对应的数据库表和分类、标签对应的数据库表关联了起来,但是关联形式稍微有点不同。 # 我们规定一篇文章只能对应一个分类,但是一个分类下可以有多篇文章,所以我们使用的是 ForeignKey,即一对多的关联关系。 # 而对于标签来说,一篇文章可以有多个标签,同一个标签下也可能有多篇文章,所以我们使用 ManyToManyField,表明这是多对多的关联关系。 # 同时我们规定文章可以没有标签,因此为标签 tags 指定了 blank=True。 category = models.ForeignKey(Category,on_delete=models.CASCADE) tags = models.ManyToManyField(Tag,blank=True) # 文章作者,这里 User 是从 django.contrib.auth.models 导入的。 # django.contrib.auth 是 Django 内置的应用,专门用于处理网站用户的注册、登录等流程,User 是 Django 为我们已经写好的用户模型。 # 这里我们通过 ForeignKey 把文章和 User 关联了起来。 # 因为我们规定一篇文章只能有一个作者,而一个作者可能会写多篇文章,因此这是一对多的关联关系,和 Category 类似。 author = models.ForeignKey(User,on_delete=models.CASCADE) def __str__(self): return self.title
定义好 __str__
方法后,解释器显示的内容将会是 __str__
方法返回的内容。这里 Category
返回分类名 name
,Tag
返回标签名,而 Post
返回它的 title
。
exit() 退出 Shell,再重新运行 python manage.py shell
进入 Shell。
可以看到返回的是我们之前存入的数据。
此外我们在创建文章时提到了通过 get
方法来获取数据,这里 all
方法和 get
方法的区别是:all
方法返回全部数据,是一个类似于列表的数据结构(QuerySet);而 get
返回一条记录数据,如有多条记录或者没有记录,get
方法均会抛出相应异常。
五、博客首页视图
5.1.Django处理HTTP请求
Web 应用的交互过程其实就是 HTTP 请求与响应的过程。无论是在 PC 端还是移动端,我们通常使用浏览器来上网,上网流程大致来说是这样的:
- 我们打开浏览器,在地址栏输入想访问的网址,比如 http://www.cnblogs.com/。
- 浏览器知道我们想要访问哪个网址后,它在后台帮我们做了很多事情。主要就是把我们的访问意图包装成一个 HTTP 请求,发给我们想要访问的网址所对应的服务器。通俗点说就是浏览器帮我们通知网站的服务器,说有人来访问你啦,访问的请求都写在 HTTP 里了,你按照要求处理后告诉我,我再帮你回应他!
- 服务器处理了HTTP 请求,然后生成一段 HTTP 响应给浏览器。浏览器解读这个响应,把相关的内容在浏览器里显示出来,于是我们就看到了网站的内容。比如你访问了我的博客主页http://www.cnblogs.com/derek1184405959/,服务器接收到这个请求后就知道用户访问的是首页,首页显示的是全部文章列表,于是它从数据库里把文章数据取出来,生成一个写着这些数据的 HTML 文档,包装到 HTTP 响应里发给浏览器,浏览器解读这个响应,把 HTML 文档显示出来,我们就看到了文章列表的内容。
因此,Django 作为一个 Web 框架,它的使命就是处理流程中的第二步。即接收浏览器发来的 HTTP 请求,返回相应的 HTTP 响应。于是引出这么几个问题:
- Django 如何接收 HTTP 请求?
- Django 如何处理这个 HTTP 请求?
- Django 如何生成 HTTP 响应?
对于如何处理这些问题,Django 有其一套规定的机制。我们按照 Django 的规定,就能开发出所需的功能
Hello视图函数
我们先以一个最简单的 Hello World 为例来看看 Django 处理上述问题的机制是怎么样的。
绑定url和视图函数
首先 Django 需要知道当用户访问不同的网址时,应该如何处理这些不同的网址(即所说的路由)。Django 的做法是把不同的网址对应的处理函数写在一个 urls.py 文件里,当用户访问某个网址时,Django 就去会这个文件里找,如果找到这个网址,就会调用和它绑定在一起的处理函数(叫做视图函数)。
下面是具体的做法,首先在 blog 应用的目录下创建一个 urls.py 文件,在 blog\urls.py 中写入这些代码:
# blog/urls.py from django.conf.urls import url from . import views urlpatterns = [ url(r'^$',views.index,name='index'), ]
我们首先从 django.conf.urls 导入了 url
函数,又从当前目录下导入了 views 模块。然后我们把网址和处理函数的关系写在了 urlpatterns
列表里。
绑定关系的写法是把网址和对应的处理函数作为参数传给 url
函数(第一个参数是网址,第二个参数是处理函数),另外我们还传递了另外一个参数 name
,这个参数的值将作为处理函数 index
的别名,这在以后会用到。
注意这里我们的网址是用正则表达式写的,Django 会用这个正则表达式去匹配用户实际输入的网址,如果匹配成功,就会调用其后面的视图函数做相应的处理。
比如说我们本地开发服务器的域名是 http://127.0.0.1:8000,那么当用户输入网址 http://127.0.0.1:8000 后,Django 首先会把协议 http、域名 127.0.0.1 和端口号 8000 去掉,此时只剩下一个空字符串,而 r'^$'
的模式正是匹配一个空字符串(这个正则表达式的意思是以空字符串开头且以空字符串结尾),于是二者匹配,Django 便会调用其对应的 views.index
函数。
注意:在项目根目录的 blogproject\ 目录下(即 settings.py 所在的目录),原本就有一个 urls.py 文件,这是整个工程项目的 URL 配置文件。而我们这里新建了一个 urls.py 文件,且位于 blog 应用下。这个文件将用于 blog 应用相关的 URL 配置。不要把两个文件搞混了。
编写视图函数
第二步就是要实际编写我们的 views.index
视图函数了,按照惯例视图函数定义在 views.py 文件里:
from django.shortcuts import HttpResponse def index(request): return HttpResponse('欢迎访问我的博客')
我们前面说过,Web 服务器的作用就是接收来自用户的 HTTP 请求,根据请求内容作出相应的处理,并把处理结果包装成 HTTP 响应返回给用户。
这个两行的函数体现了这个过程。它首先接受了一个名为 request
的参数,这个 request
就是 Django 为我们封装好的 HTTP 请求,它是类 HttpRequest
的一个实例。然后我们便直接返回了一个 HTTP 响应给用户,这个 HTTP 响应也是 Django 帮我们封装好的,它是类 HttpResponse
的一个实例,只是我们给它传了一个自定义的字符串参数。
浏览器接收到这个响应后就会在页面上显示出我们传递的内容:欢迎访问我的博客
配置项目URL
还差最后一步了,我们前面建立了一个 urls.py 文件,并且绑定了 URL 和视图函数 index
,但是 Django 并不知道。Django 匹配 URL 模式是在 blogproject\ 目录(即 settings.py 文件所在的目录)的 urls.py 下的,所以我们要把 blog 应用下的 urls.py 文件包含到 blogproject\urls.py 里去:
# blogproject/urls.py from django.contrib import admin from django.conf.urls import url,include urlpatterns = [ url('admin/', admin.site.urls), url('', include('blog.urls')), ]
我们这里导入了一个 include
函数,然后利用这个函数把 blog 应用下的 urls.py 文件包含了进来。此外 include 前还有一个 r''
,这是一个空字符串。这里也可以写其它字符串,Django 会把这个字符串和后面 include 的 urls.py 文件中的 URL 拼接。比如说如果我们这里把 r''
改成 r'blog/'
,而我们在 blog.urls 中写的 URL 是 r'^$'
,即一个空字符串。那么 Django 最终匹配的就是 blog/ 加上一个空字符串,即 blog/。
运行结果
运行 python manage.py runserver
打开开发服务器,在浏览器输入开发服务器的地址 http://127.0.0.1:8000/,可以看到 Django 返回的内容了。
5.2.使用Django模板系统
这基本上就上 Django 的开发流程了,写好处理 HTTP 请求和返回 HTTP 响应的视图函数,然后把视图函数绑定到相应的 URL 上。
但是等一等!我们看到在视图函数里返回的是一个 HttpResponse
类的实例,我们给它传入了一个希望显示在用户浏览器上的字符串。但是我们的博客不可能只显示这么一句话,它有可能会显示很长很长的内容。比如我们发布的博客文章列表,或者一大段的博客文章。我们不能每次都把这些大段大段的内容传给 HttpResponse
。
Django 对这个问题给我们提供了一个很好的解决方案,叫做模板系统。Django 要我们把大段的文本写到一个文件里,然后 Django 自己会去读取这个文件,再把读取到的内容传给 HttpResponse
。让我们用模板系统来改造一下上面的例子。
首先在我们的项目根目录(即 manage.py 文件所在目录)下建立一个名为 templates 的文件夹,用来存放我们的模板。然后在 templates\ 目录下建立一个名为 blog 的文件夹,用来存放和 blog 应用相关的模板。
当然模板存放在哪里是无关紧要的,只要 Django 能够找到的就好。但是我们建立这样的文件夹结构的目的是把不同应用用到的模板隔离开来,这样方便以后维护。我们在 templates\blog 目录下建立一个名为 index.html 的文件
在 templates\blog\index.html 文件里写入下面的代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>{{ title }}</title> </head> <body> <h1>{{ welcome }}</h1> </body> </html>
这是一个标准的 HTML 文档,只是里面有两个比较奇怪的地方:{{ title }}
,{{ welcome }}
。这是 Django 规定的语法。用 {{ }} 包起来的变量叫做模板变量。Django 在渲染这个模板的时候会根据我们传递给模板的变量替换掉这些变量。最终在模板中显示的将会是我们传递的值。
模板写好了,还得告诉 Django 去哪里找模板,在 settings.py 文件里设置一下模板文件所在的路径。在 settings.py 找到 TEMPLATES
选项,其中 DIRS
就是设置模板的路径,在 [] 中写入 os.path.join(BASE_DIR, 'templates')
,即像下面这样:
TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [os.path.join(BASE_DIR, 'templates')] , 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ], }, }, ]
这里 BASE_DIR
是 settings.py 在配置开头前面定义的变量,记录的是工程根目录 blogproject\ 的值(注意是最外层的 blogproject\ 目录)。在这个目录下有模板文件所在的目录 templates\,于是利用os.path.join
把这两个路径连起来,构成完整的模板路径,Django 就知道去这个路径下面找我们的模板了。
视图函数可以改一下了:
from django.http import HttpResponse from django.shortcuts import render def index(request): return render(request, 'blog/index.html', context={ 'title': '我的博客首页', 'welcome': '欢迎访问我的博客首页' })
这里我们不再是直接把字符串传给 HttpResponse
了,而是调用 Django 提供的 render
函数。这个函数根据我们传入的参数来构造 HttpResponse
。
我们首先把 HTTP 请求传了进去,然后 render
根据第二个参数的值 blog/index.html 找到这个模板文件并读取模板中的内容。之后 render
根据我们传入的 context
参数的值把模板中的变量替换为我们传递的变量的值,{{ title }}
被替换成了 context
字典中 title
对应的值,同理 {{ welcome }}
也被替换成相应的值。
最终,我们的 HTML 模板中的内容字符串被传递给 HttpResponse
对象并返回给浏览器(Django 在 render
函数里隐式地帮我们完成了这个过程),这样用户的浏览器上便显示出了我们写的 HTML 模板的内容
六、真正的Django博客首页视图
在此之前我们已经编写了 Blog 的首页视图,并且配置了 URL 和模板,让 Django 能够正确地处理 HTTP 请求并返回合适的 HTTP 响应。不过我们仅仅在首页返回了一句话:欢迎访问我的博客。这是个 Hello World 级别的视图函数,我们需要编写真正的首页视图函数,当用户访问我们的博客首页时,他将看到我们发表的博客文章列表。像前面演示的那样
6.1.首页视图函数
上一节我们阐明了 Django 的开发流程。即首先配置 URL,把 URL 和相应的视图函数绑定,一般写在 urls.py 文件里,然后在工程的 urls.py 文件引入。其次是编写视图函数,视图中需要渲染模板,我们也在 settings.py 中进行了模板相关的配置,让 Django 能够找到需要渲染的模板。最后把渲染完成的 HTTP 响应返回就可以了。相关的配置和准备工作都在之前完成了,这里我们只需专心编写视图函数,让它实现我们想要的功能即可。
首页的视图函数其实很简单,代码像这样:
# blog/views.py from django.shortcuts import render from . models import Post def index(request): post_list = Post.objects.all().order_by('-created_time') return render(request,'blog/index.html',{'post_list':post_list})
我们曾经在前面的章节讲解过模型管理器 objects
的使用。这里我们使用 all()
方法从数据库里获取了全部的文章,存在了 post_list
变量里。all
方法返回的是一个 QuerySet
(可以理解成一个类似于列表的数据结构),由于通常来说博客文章列表是按文章发表时间倒序排列的,即最新的文章排在最前面,所以我们紧接着调用了 order_by
方法对这个返回的 queryset 进行排序。排序依据的字段是 created_time
,即文章的创建时间。-
号表示逆序,如果不加 -
则是正序。 接着如之前所做,我们渲染了 blog\index.html 模板文件,并且把包含文章列表数据的 post_list
变量传给了模板。
6.2.处理静态文件
我们的项目使用了从网上下载的一套博客模板 点击这里下载全套模板。这里面除了HTML 文档外,还包含了一些 CSS 文件和 JavaScript 文件以让网页呈现出我们现在看到的样式。同样我们需要对 Django 做一些必要的配置,才能让 Django 知道如何在开发服务器中引入这些 CSS 和 JavaScript 文件,这样才能让博客页面的 CSS 样式生效。
按照惯例,我们把 CSS 和 JavaScript 文件放在 blog 应用的 static\ 目录下。因此,先在 blog 应用下建立一个 static 文件夹。同时,为了避免和其它应用中的 CSS 和 JavaScript 文件命名冲突(别的应用下也可能有和 blog 应用下同名的 CSS 、JavaScript 文件),我们再在 static\ 目录下建立一个 blog 文件夹,把下载的博客模板中的 css 和 js 文件夹连同里面的全部文件一同拷贝进这个目录。目录结构
用下载的博客模板中的 index.html 文件替换掉之前我们自己写的 index.html 文件。如果你好奇,现在就可以运行开发服务器,看看首页是什么样子。
如图所示,你会看到首页显示的样式非常混乱,原因是浏览器无法正确加载 CSS 等样式文件。需要以 Django 的方式来正确地处理 CSS 和 JavaScript 等静态文件的加载路径。CSS 样式文件通常在 HTML 文档的 head 标签里引入,打开 index.html 文件,在文件的开始处找到 head 标签包裹的内容,大概像这样:
<head> <title>Black & White</title> <!-- meta --> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- css --> <link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> <link rel="stylesheet" href="css/pace.css"> <link rel="stylesheet" href="css/custom.css"> <!-- js --> <script src="js/jquery-2.1.3.min.js"></script> <script src="js/bootstrap.min.js"></script> <script src="js/pace.min.js"></script> <script src="js/modernizr.custom.js"></script> </head>
CSS 样式文件的路径在 link 标签的 href 属性里,而 JavaScript 文件的路径在 script 标签的 src 属性里。可以看到诸如 `href="css/bootstrap.min.css" 或者 src="js/jquery-2.1.3.min.js" 这样的引用,由于引用文件的路径不对,所以浏览器引入这些文件失败。我们需要把它们改成正确的路径。把代码改成下面样子,正确地引入 static 文件下的 CSS 和 JavaScript 文件:
{% load staticfiles %} <!DOCTYPE html> <html> <head> <title>Black & White</title> <!-- meta --> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <!-- css --> <link rel="stylesheet" href="{% static 'blog/css/bootstrap.min.css' %}"> <link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"> <link rel="stylesheet" href="{% static 'blog/css/pace.css' %}"> <link rel="stylesheet" href="{% static 'blog/css/custom.css' %}"> <!-- js --> <script src="{% static 'blog/js/jquery-2.1.3.min.js' %}"></script> <script src="{% static 'blog/js/bootstrap.min.js' %}"></script> <script src="{% static 'blog/js/pace.min.js' %}"></script> <script src="{% static 'blog/js/modernizr.custom.js' %}"></script> </head> <body> <!-- 其它内容 --> <script src="{% static 'blog/js/script.js' %}"></script> </body> </html>
我们把引用路径放在了一个奇怪的符号里,例如:href="{% static 'blog/css/bootstrap.min.css' %}"。用 {% %} 包裹起来的叫做模板标签。我们前面说过用 {{ }} 包裹起来的叫做模板变量,其作用是在最终渲染的模板里显示由视图函数传过来的变量值。而这里我们使用的模板标签的功能则类似于函数,例如这里的 static
模板标签,它把跟在后面的字符串 'css/bootstrap.min.css'
转换成正确的文件引入路径。这样 css 和 js 文件才能被正确加载,样式才能正常显示。
为了能在模板中使用 {% static %} 模板标签,别忘了在最顶部添加 {% load staticfiles %} 。static 模板标签位于 staticfiles 模块中,只有通过 load 模板标签将该模块引入后,才能在模板中使用 {% static %} 标签。
替换完成后你可以刷新页面并看看网页的源代码,看一看 {% static %} 模板标签在页面渲染后究竟被替换成了什么样的值。例如我们可以看到
<link rel="stylesheet" href="{% static 'blog/css/pace.css' %}">
这一部分最终在浏览器中显示的是:
<link rel="stylesheet" href="/static/blog/css/pace.css">
这正是 pace.css 文件所在的路径,其它的文件路径也被类似替换。可以看到 {% static %} 标签的作用实际就是把后面的字符串加了一个 /static/ 前缀,比如 {% static 'blog/css/pace.css' %}
最终渲染的值是 /static/blog/css/pace.css
。而 /static/ 前缀是我们在 settings.py 文件中通过 STATIC_URL = '/static/'
指定的。事实上,如果我们直接把引用路径写成 /static/blog/css/pace.css
也是可以的,那么为什么要使用 {% static %} 标签呢?想一下,目前 URL 的前缀是 /static/,如果哪一天因为某些原因,我们需要把 /static/ 改成 /resource/,如果你是直接写的引用路劲而没有使用 static 模板标签,那么你可能需要改 N 个地方。如果你使用了 static 模板标签,那么只要在 settings.py 处改一个地方就可以了,即把 STATIC_URL = '/static/'
改成 STATIC_URL = '/resource/'
。
注意这里有一个 CSS 文件的引入
<link rel="stylesheet" href="http://code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css">
我们没有使用模板标签,因为这里的引用的文件是一个外部文件,不是我们项目里 static\blog\css\ 目录下的文件,因此无需使用模板标签。
正确引入了静态文件后样式显示正常了。
6.3修改模板
目前我们看到的只是模板中预先填充的一些数据,我们得让它显示从数据库中获取的文章数据。下面来稍微改造一下模板:
在模板 index.html 中你会找到一系列 article 标签:
templates/blog/index.html ... <article class="post post-1"> ... </article> <article class="post post-2"> ... </article> <article class="post post-3"> ... </article> ...
这里面包裹的内容显示的就是文章数据了。我们前面在视图函数 index 里给模板传了一个 post_list
变量,它里面包含着从数据库中取出的文章列表数据。就像 Python 一样,我们可以在模板中循环这个列表,把文章一篇篇循环出来,然后一篇篇显示文章的数据。要在模板中使用循环,需要使用到前面提到的模板标签,这次使用 {% for %} 模板标签。将 index.html 中多余的 article 标签删掉,只留下一个 article 标签,然后写上下列代码:
templates/blog/index.html ... {% for post in post_list %} <article class="post post-{{ post.pk }}"> ... </article> {% empty %} <div class="no-post">暂时还没有发布的文章!</div> {% endfor %} ...
可以看到语法和 Python 的 for 循环类似,只是被 {% %} 这样一个模板标签符号包裹着。{% empty %} 的作用是当 post_list
为空,即数据库里没有文章时显示 {% empty %} 下面的内容,最后我们用 {% endfor %} 告诉 Django 循环在这里结束了。
你可能不太理解模板中的 post
和 post_list
是什么。post_list
是一个 QuerySet
(类似于一个列表的数据结构),其中每一项都是之前定义在 blog\models.py 中的 Post 类的实例,且每个实例分别对应着数据库中每篇文章的记录。因此我们循环遍历 post_list
,每一次遍历的结果都保存在 post
变量里。所以我们使用模板变量来显示 post
的属性值。例如这里的 {{ post.pk }}
(pk 是 primary key 的缩写,即 post 对应于数据库中记录的 id 值,该属性尽管我们没有显示定义,但是 Django 会自动为我们添加)。
我们把标题替换成 post
的 title
属性值。注意要把它包裹在模板变量里,因为它最终要被替换成实际的 title 值。
<h1 class="entry-title"> <a href="single.html">{{ post.title }}</a> </h1>
下面这 5 个 span 标签里分别显示了分类(category)、文章发布时间、文章作者、评论数、阅读量。
<div class="entry-meta"> <span class="post-category"><a href="#">Django 博客教程</a></span> <span class="post-date"><a href="#"><time class="entry-date" datetime="2012-11-09T23:15:57+00:00">2017年5月11日</time></a></span> <span class="post-author"><a href="#">追梦人物</a></span> <span class="comments-link"><a href="#">4 评论</a></span> <span class="views-count"><a href="#">588 阅读</a></span> </div>
再次替换掉一些数据,由于评论数和阅读量暂时没法替换,因此先留着,我们在之后实现了这些功能后再来修改它,目前只替换分类、文章发布时间、文章作者:
<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>
这里 p 标签里显示的是摘要
<div class="entry-content clearfix"> <p>免费、中文、零基础,完整的项目,基于最新版 Django 1.10 和 Python 3.5。带你从零开始一步步开发属于自己的博客网站,帮助你以最快的速度掌握 Django 开发的技巧...</p> <div class="read-more cl-effect-14"> <a href="#" class="more-link">继续阅读 <span class="meta-nav">→</span></a> </div> </div>
替换成 post
的摘要:
<div class="entry-content clearfix"> <p>{{ post.excerpt }}</p> <div class="read-more cl-effect-14"> <a href="#" class="more-link">继续阅读 <span class="meta-nav">→</span></a> </div> </div>
再次访问首页,它显示:暂时还没有发布的文章!好吧,做了这么多工作,但是数据库中其实还没有任何数据呀!接下来我们就实际写几篇文章保存到数据库里,看看显示的效果究竟如何。
七、在Django Admin后台发布文章
在此之前我们完成了 Django 博客首页视图的编写,我们希望首页展示发布的博客文章列表,但是它却抱怨:暂时还没有发布的文章!如它所言,我们确实还没有发布任何文章,本节我们将使用 Django 自带的 Admin 后台来发布我们的博客文章。
7.1.在Django后台注册模型
前面已经用python manage.py createsuperuser,创建了超级用户。
要在后台注册我们自己创建的几个模型,这样 Django Admin 才能知道它们的存在,注册非常简单,只需要在 blog\admin.py 中加入下面的代码:
# blog/admin.py from django.contrib import admin from .models import Post,Category,Tag admin.site.register(Post) admin.site.register(Category) admin.site.register(Tag)
运行开发服务器,访问 http://127.0.0.1:8000/admin/ ,就进入了到了Django Admin 后台登录页面,输入刚才创建的管理员账户密码就可以登录到后台了。
可以看到我们刚才注册的三个模型了,点击 Posts 后面的增加按钮,将进入添加 Post 的页面,也就是新增博客文章。然后在相关的地方输入一些测试用的内容,增加完后点击保存,这样文章就添加完毕了,你也可以多添加几篇看看效果。注意每篇文章必须有一个分类,在添加文章时你可以选择已有分类。如果数据库中还没有分类,在选择分类时点击 Category 后面的 + 按钮新增一个分类即可。
访问 http://127.0.0.1:8000/ 首页,你就可以看到你添加的文章列表了,下面是我所在环境的效果图:
7.2.定制Admin后台
在 admin post 列表页面,我们只看到了文章的标题,但是我们希望它显示更加详细的信息,这需要我们来定制 Admin 了,在 admin.py 添加如下代码:
# blog/admin.py from django.contrib import admin from .models import Post,Category,Tag class PostAdmin(admin.ModelAdmin): list_display = ['title', 'created_time', 'modified_time', 'category', 'author'] admin.site.register(Post,PostAdmin) admin.site.register(Category) admin.site.register(Tag)
这里只是为了简单地到达期望效果
刷新 Admin Post 列表页面,可以看到显示的效果好多了。
八、博客文章详情页
首页展示的是所有文章的列表,当用户看到感兴趣的文章时,他点击文章的标题或者继续阅读的按钮,应该跳转到文章的详情页面来阅读文章的详细内容。现在让我们来开发博客的详情页面,有了前面的基础,开发流程都是一样的了:首先配置 URL,即把相关的 URL 和视图函数绑定在一起,然后实现视图函数,编写模板并让视图函数渲染模板。
8.1.设置文章详情页的url
回顾一下我们首页视图的 URL,在 blog\urls.py 文件里,我们写了:
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 和视图:
# 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,因为只有这样我们才能知道用户访问的究竟是哪篇文章。
此外我们通过 app_name='blog'
告诉 Django 这个 urls.py 模块是属于 blog 应用的,这种技术叫做视图函数命名空间。我们看到 blog\urls.py 目前有两个视图函数,并且通过 name 属性给这些视图函数取了个别名,分别是 index、detail。但是一个复杂的 Django 项目可能不止这些视图函数,例如一些第三方应用中也可能有叫 index、detail 的视图函数,那么怎么把它们区分开来,防止冲突呢?方法就是通过 app_name 来指定命名空间,命名空间具体如何使用将在下面介绍。
为了方便地生成上述的 URL,我们在 Post
类里定义一个 get_absolute_url
方法,注意 Post
本身是一个 Python 类,在类中我们是可以定义任何方法的。
blog/models.py from django.db import models from django.contrib.auth.models import User from django.urls import reverse 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。
8.2.编写detail视图函数
接下来就是实现我们的 detail
视图函数了:
# blog/views.py from django.shortcuts import render,get_object_or_404 from . models import Post def index(request): post_list = Post.objects.all().order_by('-created_time') return render(request,'blog/index.html',{'post_list':post_list}) def detail(request,pk): post = get_object_or_404(Post,pk=pk) return render(request,'blog/detail.html',{'post':post})
视图函数很简单,它根据我们从 URL 捕获的文章 id(也就是 pk,这里 pk 和 id 是等价的)获取数据库中文章 id 为该值的记录,然后传递给模板。注意这里我们用到了从 django.shortcuts 模块导入的 get_object_or_404
方法,其作用就是当传入的 pk 对应的 Post 在数据库存在时,就返回对应的 post
,如果不存在,就给用户返回一个 404 错误,表明用户请求的文章不存在。
8.3.编写详情页模板
接下来就是书写模板文件,从下载的博客模板中把 single.html 拷贝到 templates\blog 目录下(和 index.html 在同一级目录),然后改名为 detail.html。
在 index 页面博客文章列表的标题和继续阅读按钮写上超链接跳转的链接,即文章 post
对应的详情页的 URL,让用户点击后可以跳转到 detail 页面:
<article class="post post-{{ post.pk }}"> <header class="entry-header"> <h1 class="entry-title"> <a href="{{ post.get_absolute_url }}">{{ post.title }}</a> </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"> <p>{{ post.excerpt }}</p> <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>
这里我们修改两个地方,第一个是文章标题处:
<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 模板继承的方法来帮我们消除这些重复操作。
8.4.模板继承
我们看到 index.html 文件和 detail.html 文件除了 main 标签包裹的部分不同外,其它地方都是相同的,我们可以把相同的部分抽取出来,放到 base.html 里。首先在 templates\ 目录下新建一个 base.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 'blog/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 'blog/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>
再次从首页点击一篇文章的标题或者继续阅读按钮跳转到详情页面,可以看到预期效果了!
上一篇: Python的进程和线程
下一篇: 合并-查找接口实现
推荐阅读