泽恩小站-教程区 Help

文章标题图

现在虽然博客的功能大都实现了,但是界面还是比较朴素,特别是首页的文章列表几乎全是文字,看多了难免疲劳。因此,给每个文章标题配一张标题图 ,不仅美观,用户也能通过图片快速了解文章内容。实际上大部分社交网站也都是这么干的,毕竟人的天性就是懒,能看图就坚决不看字。

上传用户头像章节中,我们已经接触过上传、展示图片了。标题图的实现也差不多,不同的是本章会更近一步, 对图片进行缩放等处理,使页面整洁美观、并且高效。

准备工作

与用户头像类似,标题图是属于每篇博文自己的“资产”,因此需要修改model ,新建一个字段:

article/models.py class ArticlePost(models.Model): ... # 文章标题图 avatar = models.ImageField(upload_to='article/%Y%m%d/', blank=True) ...

注意上传地址中的%Y%m%d是日期格式化的写法。比如上传时间是2019年2月26日,则标题图会上传到media/article/20190226这个目录中。

记得数据迁移

标题图通常在创建新文章的时候就设置好了,而新文章是通过表单上传到数据库中的。因此接下来就是修改发表文章的表单类

article/forms.py ... class ArticlePostForm(forms.ModelForm): class Meta: ... fields = ('title', 'body', 'tags', 'avatar')

增加了avatar字段而已,没有新内容。

下一步就是修改视图 。因为POST的表单中包含了图片文件,所以要将request.FILES也一并绑定到表单类中,否则图片无法正确保存:

article/views.py ... def article_create(request): if request.method == "POST": # 增加 request.FILES article_post_form = ArticlePostForm(request.POST, request.FILES) ...

很好,功能差不多已经通了,接下来就是对图片进行处理。

处理图片

写代码之前先构思一下需要进行怎样的处理:

  • 标题图对画质没有太高的要求,因此需要缩小图片的体积 ,以便提高网页的加载速度。

  • 其次还需要对图片的长宽进行规范化。我比较喜欢将图片的宽度设置得相同 ,这样标题可以比较整齐。

下一个问题是,代码应该写到什么地方呢?似乎在modelform或者view里处理图片都可以。在这里我打算把代码写到model中去,这样不管你在任何地方上传图片(包括后台中!),图片都会得到处理。

想好之后,就要行动了。还记得Pillow这个库吗,我们很早就把它安装好了,现在是使用它的时候了:

article/models.py ... # 记得导入! from PIL import Image class ArticlePost(models.Model): ... # 前面写好的代码 avatar = models.ImageField(upload_to='article/%Y%m%d/', blank=True) # 保存时处理图片 def save(self, *args, **kwargs): # 调用原有的 save() 的功能 article = super(ArticlePost, self).save(*args, **kwargs) # 固定宽度缩放图片大小 if self.avatar and not kwargs.get('update_fields'): image = Image.open(self.avatar) (x, y) = image.size new_x = 400 new_y = int(new_x * (y / x)) resized_image = image.resize((new_x, new_y), Image.ANTIALIAS) resized_image.save(self.avatar.path) return article ...

**代码不多,但是有很多细节,值得仔细推敲。**不急,一行一行来:

  • save()是model内置的方法,它会在model实例每次保存时调用。这里改写它,将处理图片的逻辑“塞进去”。

  • super(ArticlePost, self).save(*args, **kwargs)的作用是调用父类中原有的save()方法,即将model中的字段数据保存到数据库中。因为图片处理是基于已经保存的图片的,所以这句一定要在处理图片之前执行 ,否则会得到找不到原始图片的错误。

  • 博文的标题图不是必须的, if中的self.avatar剔除掉没有标题图的文章,这些文章不需要处理图片。

  • 不太好理解的是if中的这个not kwargs.get('update_fields') 。还记得article_detail()视图中为了统计浏览量而调用了save(update_fields=['total_views'])吗?没错,就是为了排除掉统计浏览量调用的save() ,免得每次用户进入文章详情页面都要处理标题图,太影响性能了。

  • 接下来都是Pillow处理图片的流程了:打开原始图片,取得分辨率,将新图片的宽度设置为400并根据比例缩小高度,最后用新图片将原始图片覆盖掉。 Image.ANTIALIAS表示缩放采用平滑滤波。

  • 最后一步,将父类save()返回的结果原封不动的返回去。

完美!

模板与测试

剩下的工作就比较简单了。

修改发表文章的模板 ,让表单能够上传图片:

templates/article/create.html ... <!-- 记得增加 enctype ! --> <form ... enctype="multipart/form-data"> ... <!-- 文章标题图 --> <div class="form-group"> <label for="avatar">标题图</label> <input type="file" class="form-control-file" name="avatar" id="avatar"> </div> ... </form> ...

然后修改文章列表模板 ,让其能够展现标题图。

为了美观,这里稍微改动了列表循环的整体结构:

templates/article/list.html ... <!-- 列表循环 --> <div class="row mt-2"> {% for article in articles %} <!-- 标题图 --> {% if article.avatar %} <div class="col-3"> <img src="{{ article.avatar.url }}" alt="avatar" style="max-width:100%; border-radius: 20px" > </div> {% endif %} <div class="col"> <!-- 栏目 --> ... <!-- 标签 --> ... ... <hr style="width: 100%;"/> {% endfor %} </div> ...

接下来又是喜闻乐见的测试环节。

启动服务器,打开发表文章页面:

T29 1

选择几张分辨率各不相同的图片作为标题图,

发表几篇文章并回到文章列表页面:

T29 2

看起来似乎不错。

查看一下media目录下实际保存的图片:

T29 3

确实保存到想要的目录下,并且左下角显示图片的宽度全都为400了。

扫尾工作

功能已经实现了,但还有扫尾工作需要去做:

  • 需要对上传的图片做更多的验证工作,比如上传的文件是否为图片、分辨率是否满足要求。虽然在个人博客项目中这些验证并不是特别重要,但在其他项目中就说不好了:谁知道用户会上传些什么奇奇怪怪的东西?

  • 编辑文章、删除文章也同样需要处理上传的图片。你还可以将缩放分辨率的技术应用到用户头像上,比如裁剪成方形。

怎么实现这些功能就不赘述了,留给读者自己去折腾吧。

编辑文章

有不少读者在将标题图标签功能添加到编辑文章页面时遇到了困难,所以提示一下如何实现。

其实大家的思路是没错的,编辑和创建差不太多,唯一区别是编辑功能还需要把旧的数据给展现出来。关键是要注意几个问题:

  • 标题图是文件,你应该在request.FILES里获取它,而不是request.POST

  • tags不是普通的字段, article.tags是取不到值的,你应该用官方文档给的接口去获取、设置数据

所以视图相关代码应该这样写:

article/views.py # 更新文章 @login_required(login_url='/userprofile/login/') def article_update(request, id): ... if request.method == "POST": ... if article_post_form.is_valid(): ... if request.FILES.get('avatar'): article.avatar = request.FILES.get('avatar') article.tags.set(*request.POST.get('tags').split(','), clear=True) article.save() ... else: ... context = { ... 'tags': ','.join([x for x in article.tags.names()]), } ...
  • tags.set()tags.names()就是库提供的接口了,分别用于更新数据和获取标签名。注意tags.set()是如何将序列分隔并解包的。

  • 渲染空表单时用到了列表生成器将数据转换为字符串。

模板相关代码:

templates/article/update.html <form method="post" action="." enctype="multipart/form-data"> {% csrf_token %} ... <!-- 文章标题图 --> <div class="form-group"> <label for="avatar">标题图</label> <input type="file" class="form-control-file" name="avatar" id="avatar"> </div> <!-- 文章标签--> <div class="form-group"> <label for="tags">标签</label> <input type="text" class="form-control col-3" id="tags" name="tags" value="{{ tags }}"> </div> ... </form>

enctype="multipart/form-data"这个属性的意思是表单提交时不对字符编码。表单里带有文件时,一定要加上它。

核心代码大概就这样,我的GitHub仓库master分支更新了此部分的完整代码,提供给大家参考。

此外还有些细节问题可以优化,读者就自己去试试看吧!

轮子

虽然本文是自己动手写的代码(严格说来Pillow也是轮子),但想必你也猜到了,还有更加智能的轮子: django-imagekit ,这个库可以直接集成到model里面,比如这样:

article/models.py # 引入imagekit from imagekit.models import ProcessedImageField from imagekit.processors import ResizeToFit class ArticlePost(models.Model): ... avatar = ProcessedImageField( upload_to='article/%Y%m%d', processors=[ResizeToFit(width=400)], format='JPEG', options={'quality': 100}, )

字段中定义好了上传位置、处理规则、存储格式以及图片质量,你不需要写任何处理图片的代码了。

更多的用法见官方介绍

总结

本章学习了如何上传并处理文章的标题图,从此博客首页就有了漂亮的外观。

需要指出的是,个人博客所采用的服务器通常性能不佳,用来保存文章缩略图等小尺寸的图片倒还好,但是千万不要存储大尺寸的图片文件 ,否则用户等待几分钟都刷不开你的图片,那是很悲剧的。

因此建议你将大尺寸的图片、视频等放到专业的云对象存储服务商中,比如七牛云又拍云等,在你存储量很小时(10G以内)是花不了多少钱的。

Last modified: 06 January 2025