Django-Vue搭建个人博客:文章标题图
6975 views, 2021/03/14 updated Go to Comments
即使是一个最简单的博客项目,也绕不开文件的上传与下载,比如说博文的标题图片。很遗憾,Json 格式的载体是字符串,不能够直接处理文件流。
怎么办?很多开发者用 DRF 处理文件上传还是沿用了 Django 的老路子,即用 multipart/form-data
表单发送夹杂着元数据的文件。这种方法可行,但在主要接口中发送编码文件总感觉不太舒服。
除了上面这种老路子以外,你基本上还剩三种选择:
- 用 Base64 对文件进行编码(将文件变成字符串)。这种方法简单粗暴,并且只靠 Json 接口就可以实现。代价是数据传输大小增加了约 33%,并在服务器和客户端中增加了编码/解码的开销。
- 首先在
multipart/form-data
中单独发送文件,然后后端将保存好的文件 id 返回给客户端。客户端拿到文件 id 后,发送带有文件 id 的 Json 数据,在服务器端将它们关联起来。 - 首先单独发送 Json 数据,然后后端保存好这些元数据后将其 id 返回给客户端。接着客户端发送带有元数据 id 的文件,在服务器端将它们关联起来。
三种方法各有优劣,具体用哪种方法应当视实际情况确定。
本文将使用第二种方法来实现博文标题图的功能。
模型和视图
图片字段 ImageField
依赖 Pillow
库,先把它安装好:
python -m pip install Pillow
旧版本 pip 可能安装 Pillow 会失败,比如 pip==10.x 。如果安装过程中报错,请尝试升级 pip。
按照上述两步走的思路:先上传图片、再上传其他文章数据的流程,将标题图设计为一个独立的模型:
# article/models.py ... class Avatar(models.Model): content = models.ImageField(upload_to='avatar/%Y%m%d') class Article(models.Model): ... # 标题图 avatar = models.ForeignKey( Avatar, null=True, blank=True, on_delete=models.SET_NULL, related_name='article' )
Avatar
模型仅包含一个图片字段。接收的图片将保存在 media/avatar/年月日/
的路径中。
接着按部就班的把视图集写了:
# article/views.py ... from article.models import Avatar # 这个 AvatarSerializer 最后来写 from article.serializers import AvatarSerializer class AvatarViewSet(viewsets.ModelViewSet): queryset = Avatar.objects.all() serializer_class = AvatarSerializer permission_classes = [IsAdminUserOrReadOnly]
图片属于媒体文件,它也需要路由,因此会多一点点配置工作:
# drf_vue_blog/settings.py ... MEDIA_URL = '/media/' MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
以及注册路由:
# drf_vuew_blog/urls.py ... from django.conf import settings from django.conf.urls.static import static ... router.register(r'avatar', views.AvatarViewSet) urlpatterns = [ ... ] # 把媒体文件的路由注册了 if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
这些准备工作都搞好了,就又到了喜闻乐见的写序列化器的环节。
序列化器
图片是在文章上传前先单独上传的,因此需要有一个单独的序列化器:
# article/serializers.py ... from article.models import Avatar class AvatarSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='avatar-detail') class Meta: model = Avatar fields = '__all__'
DRF 对图片的处理进行了封装,通常不需要你关心实现的细节,只需要像其他 Json 接口一样写序列化器就可以了。
图片上传完成后,会将其 id、url 等信息返回到前端,前端将图片的信息以嵌套结构表示到文章接口中,并在适当的时候将其链接到文章数据中:
# article/serializers.py ... class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): ... # 图片字段 avatar = AvatarSerializer(read_only=True) avatar_id = serializers.IntegerField( write_only=True, allow_null=True, required=False ) # 验证图片 id 是否存在 # 不存在则返回验证错误 def validate_avatar_id(self, value): if not Avatar.objects.filter(id=value).exists() and value is not None: raise serializers.ValidationError("Avatar with id {} not exists.".format(value)) return value ...
用户的操作流程如下:
- 发表新文章时,标题图需要先上传。
- 标题图上传完成会返回其数据(比如图片数据的 id)到前端并暂存,等待新文章完成后一起提交。
- 提交新文章时,序列化器对标题图进行检查,如果无效则返回错误信息。
这个流程在后面的前端章节会体现得更直观。
测试
接下来测试图片的增删改查。
Postman 操作文件接口需要将
Content-Type
改为multipart/form-data
,并在Body
中上传图片文件。具体操作方式请百度。
创建新图片:
PS C:\...> http -a dusai:admin123456 -f POST http://127.0.0.1:8000/api/avatar/ content@'D:\Image\Sea.jpg' ... { "content": "http://127.0.0.1:8000/media/avatar/20200908/Sea.jpg", "id": 1, "url": "http://127.0.0.1:8000/api/avatar/1/" }
看到创建图片后返回的 id 了吗?其实就是图片是先于 Json 数据单独上传的,上传完毕后客户端将其 id 记住,以便真正提交 Json 时能与之对应。
更新已有图片:
PS C:\...> http -a dusai:admin123456 -f PATCH http://127.0.0.1:8000/api/avatar/1/ content@'D:\Image\Sea.jpg' ... { "content": "http://127.0.0.1:8000/media/avatar/20200908/Sea_EdNw2EF.jpg", "id": 1, "url": "http://127.0.0.1:8000/api/avatar/1/" }
删除:
PS C:\Users\Dusai> http -a dusai:admin123456 -f DELETE http://127.0.0.1:8000/api/avatar/1/ HTTP/1.1 204 No Content Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS Content-Length: 0 Date: Tue, 08 Sep 2020 10:52:04 GMT Server: WSGIServer/0.2 CPython/3.8.2 Vary: Accept, Cookie X-Content-Type-Options: nosniff X-Frame-Options: DENY
查找:
PS C:\...> http -a dusai:admin123456 http://127.0.0.1:8000/api/avatar/ ... { "count": 4, "next": "http://127.0.0.1:8000/api/avatar/?page=2", "previous": null, "results": [ { "content": "http://127.0.0.1:8000/media/avatar/20200831/Playground.jpg", "id": 6, "url": "http://127.0.0.1:8000/api/avatar/6/" }, { "content": "http://127.0.0.1:8000/media/avatar/20200831/Shoes.jpg", "id": 7, "url": "http://127.0.0.1:8000/api/avatar/7/" }, { "content": "http://127.0.0.1:8000/media/avatar/20200831/Sea.jpg", "id": 9, "url": "http://127.0.0.1:8000/api/avatar/9/" } ] }
都正常运作了。
重构
仔细看下 ArticleBaseSerializer
序列化器,发现分类和标题图的验证方法是比较类似的:
# article/serializers.py ... class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): ... def validate_avatar_id(self, value): if not Avatar.objects.filter(id=value).exists() and value is not None: raise serializers.ValidationError("Avatar with id {} not exists.".format(value)) self.fail('incorrect_avatar_id', value=value) return value def validate_category_id(self, value): if not Category.objects.filter(id=value).exists() and value is not None: raise serializers.ValidationError("Category with id {} not exists.".format(value)) self.fail('incorrect_category_id', value=value) return value
因此可以将它们整理整理,变成下面的样子:
# article/serializers.py ... class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer): ... # 自定义错误信息 default_error_messages = { 'incorrect_avatar_id': 'Avatar with id {value} not exists.', 'incorrect_category_id': 'Category with id {value} not exists.', 'default': 'No more message here..' } def check_obj_exists_or_fail(self, model, value, message='default'): if not self.default_error_messages.get(message, None): message = 'default' if not model.objects.filter(id=value).exists() and value is not None: self.fail(message, value=value) def validate_avatar_id(self, value): self.check_obj_exists_or_fail( model=Avatar, value=value, message='incorrect_avatar_id' ) return value def validate_category_id(self, value): self.check_obj_exists_or_fail( model=Category, value=value, message='incorrect_category_id' ) return value
- 把两个字段验证器的雷同代码抽象到
check_obj_exists_or_fail()
方法里。 check_obj_exists_or_fail()
方法检查了数据对象是否存在,若不存在则调用钩子方法fail()
引发错误。fail()
又会调取default_error_messages
属性中提供的错误类型,并将其返回给接口。
看起来似乎代码行数更多了,但更整洁了。起码你的报错信息不再零散分布在整个序列化器中,并且合并了两个验证器的重复代码,维护起来会更省事。