Appearance
即使是一个最简单的博客项目,也绕不开文件的上传与下载,比如说博文的标题图片。很遗憾,Json 格式的载体是字符串,不能够直接处理文件流。
怎么办?很多开发者用 DRF 处理文件上传还是沿用了 Django 的老路子,即用 multipart/form-data
表单发送夹杂着元数据的文件。这种方法可行,但在主要接口中发送编码文件总感觉不太舒服。
除了上面这种老路子以外,你基本上还剩三种选择:
- 用 Base64 对文件进行编码(将文件变成字符串)。这种方法简单粗暴,并且只靠 Json 接口就可以实现。代价是数据传输大小增加了约 33%,并在服务器和客户端中增加了编码/解码的开销。
- 首先在
multipart/form-data
中单独发送文件,然后后端将保存好的文件 id 返回给客户端。客户端拿到文件 id 后,发送带有文件 id 的 Json 数据,在服务器端将它们关联起来。 - 首先单独发送 Json 数据,然后后端保存好这些元数据后将其 id 返回给客户端。接着客户端发送带有元数据 id 的文件,在服务器端将它们关联起来。
三种方法各有优劣,具体用哪种方法应当视实际情况确定。
本文将使用第二种方法来实现博文标题图的功能。
模型和视图
图片字段 ImageField
依赖 Pillow
库,先把它安装好:
python
python -m pip install Pillow
旧版本 pip 可能安装 Pillow 会失败,比如 pip==10.x 。如果安装过程中报错,请尝试升级 pip。
按照上述两步走的思路:先上传图片、再上传其他文章数据的流程,将标题图设计为一个独立的模型:
python
# 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/年月日/
的路径中。
接着按部就班的把视图集写了:
python
# 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]
图片属于媒体文件,它也需要路由,因此会多一点点配置工作:
python
# drf_vue_blog/settings.py
...
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
以及注册路由:
python
# 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)
这些准备工作都搞好了,就又到了喜闻乐见的写序列化器的环节。
序列化器
图片是在文章上传前先单独上传的,因此需要有一个单独的序列化器:
python
# 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 等信息返回到前端,前端将图片的信息以嵌套结构表示到文章接口中,并在适当的时候将其链接到文章数据中:
python
# 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
中上传图片文件。具体操作方式请百度。
创建新图片:
python
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 时能与之对应。
更新已有图片:
python
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/"
}
删除:
python
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
查找:
python
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
序列化器,发现分类和标题图的验证方法是比较类似的:
python
# 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
因此可以将它们整理整理,变成下面的样子:
python
# 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
属性中提供的错误类型,并将其返回给接口。
看起来似乎代码行数更多了,但更整洁了。起码你的报错信息不再零散分布在整个序列化器中,并且合并了两个验证器的重复代码,维护起来会更省事。