django-Vue搭建博客:文章标题图

车辰龙
2023-12-01

本章是对文章的完善与复习Django知识,即文件的上传与下载。所以本章我们就为文章创建标题图片来巩固Django文件上传与下载即DRF中的文件上传与下载。

教程来源 杜塞-django-vue系列
博客链接 传送门

JSON格式的载体是字符串,不能直接处理文件流。

怎么办?很多开发者使用DR处理文件上传还是沿用Django的老路子,即用multipart/form-data表单夹带元数据的文件。这种方法是可行的,但是前端却有点别扭。

出上面方法外,还有三种方法:

  • 用Base64对文件进行编码(将文件变成字符串)。这样简单粗暴,但是会增加数据的传世,和前后端编码/解码的开销。
  • 首先使用multipart/form-data中单独发送文件,然后后端保存好文件id返回给客户端。客户端拿到文件id,发送带有文件id的Json数据,在服务器端将他们关联起来。
  • 首先单独发送Json数据,然后后端保存好这些元数据后将其id返回给客户端。接着客户端发送带有元数据的id文件,在服务器关联起来。

三种方法各有优势,又有点抽象。

本文使用第二种方法实现博客文章标题图片的功能。

模型和视图

图片字段为ImageField依赖Pillow库,先把库安装好:
pip install Pillow

旧版本pip安装会失败,如果失败求升级以下pip

按上述两步走思路:先上传图片、在上传文章数据的流程,将标题图设计成一个单独的模型:

# article/models.py
# 文章标题图片
class Avatar(models.Model):
    content = models.ImageField(upload_to='avatar/%Y%m%d')
# 博客文章
class Article(models.Model):
	···
	    # 标题图
    avarat = models.ForeignKey(
        Avatar,
        null=True,
        blank=True,
        on_delete=models.SET_NULL,
        related_name='article'
    )
	···

Avatar模型仅包含一个图片字段。接受的图片将保存在media/avatar/年月日/目录下。

当然avatar字段也可以直接卸载article中,本文为方便图片能多次利用而已。(数据迁移!!!)

接着就是按部就班写视图集。

from .serializers import AvatarViewSet
class AvatarViewSet(viewsets.ModelViewSet):
    queryset = Avatar.objects.all()
    serializer_class = AvatarSerializer
    permission_classes = [IsAdminUserOrReadOnly]

视图集还是老样子,序列化器放在后面写。

因为图片属于媒体文件,他也需要路由,因此配置相对较多:

# drf_vue_blog/settings.py 主项目的setting文件
MEDIA_URL='/media/'
MEDIA_ROOT = os.path.join(BASE_DIR,'media')

注册路由:

from django.conf.urls import static,url
urlpatterns = [
    path('admin/', admin.site.urls),
    path('api/', include(router.urls)),
    # Django 2.x版本
	url(r'^media/(?P<path>.*)$',static.serve,{"document_root":settings.MEDIA_ROOT},name='media'),
]
# Django3.x版本
# if settings.DEBUG:
#   urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

准备换工作做好了,我们就开始考虑着手序列化器。

序列化器

图片是在文章上传之前单独上传的,因此需要有一个单独的序列化器:

# article/serializers.py
# from .model import Avatar
class AvatarSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='avatar-detail')

    class Meta:
        model = Avatar
        fields = '__all__'

DRF 对图片的处理进行了封装,通常不需要你关心细节,只需要想其他Json接口i一样写序列化机器就可以。

图片上传完后,会将其id、url、等信息返回给前端,前端图片的信息一千套的结构表示到文章接口中,并适当的时候将其连接到文章数据中:

class ArticleBaseSerializer(serializers.HyperlinkedModelSerializer):
    """文章序列化器"""
	···
    # avatar字段
    avatar = AvatarSerializer(read_only=True)
    avatar_id = serializers.IntegerField(
        write_only=True,
        allow_null=True,
        required=False
    )

    # avatar_id字段验证器
    def vaildate_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改为’lultipart/form-data’。并再body进行上传图片,具体操作百度。

创建新图片:

>http -a xianwei:admin123456 -f POST http://127.0.0.1:8000/api/avatar/ content@"E:\Lake.jpg"
···
{
    "content": "http://127.0.0.1:8000/media/avatar/20210619/Lake.jpg",
    "id": 6,
    "url": "http://127.0.0.1:8000/api/avatar/6/"
}

看到创建图片后返回的 id 了吗?其实就是图片是先于 Json 数据单独上传的,上传完毕后客户端将其 id 记住,以便真正提交 Json 时能与之对应。

更新已有图片:

>http -a xianwei:admin123456 -f PATCH http://127.0.0.1:8000/api/avatar/6/ content@"E:\cc.jpg"
···

{
    "content": "http://127.0.0.1:8000/media/avatar/20210619/cc.jpg",
    "id": 6,
    "url": "http://127.0.0.1:8000/api/avatar/6/"
}

删除:

>http -a xianwei:admin123456 -f DELETE http://127.0.0.1:8000/api/avatar/6/
HTTP/1.1 204 No Content
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 0
Date: Sat, 19 Jun 2021 10:51:46 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.9.4
Vary: Accept, Cookie
X-Content-Type-Options: nosniff

列表:

>http -a xianwei:admin123456 http://127.0.0.1:8000/api/avatar/
HTTP/1.1 200 OK
···
{
    "count": 1,
    "next": null,
    "previous": null,
    "results": [
        {
            "content": "http://127.0.0.1:8000/media/avatar/20210619/Bahamas_Aerial.jpg",
            "id": 5,
            "url": "http://127.0.0.1:8000/api/avatar/5/"
        }
    ]
}

将已存在文章添加标题图:

>http -a xianwei:admin123456 PATCH http://127.0.0.1:8000/api/article/16/ avatar_id=5
HTTP/1.1 200 OK
··
{
    "author": {
        "date_joined": "2021-06-13T14:58:00",
        "id": 3,
        "last_login": null,
        "username": "xianwei"
    },
    "avatar": {
        "content": "http://127.0.0.1:8000/media/avatar/20210619/Bahamas_Aerial.jpg",
        "id": 5,
        "url": "http://127.0.0.1:8000/api/avatar/5/"
    },
    "body": "bbb",
    "body_html": "<p>bbb</p>",
    "category": null,
    "created": "2021-06-17T20:52:34.325383",
    "tags": [],
    "title": "category_11",
    "toc_html": "<div class=\"toc\">\n<ul></ul>\n</div>\n",
    "updated": "2021-06-19T19:26:13.775564",
    "url": "http://127.0.0.1:8000/api/article/16/"
}

都是可以正常上传的。

需要注意,分类分类和标题图字段都是使用{field}_id进行赋值的。

重构

接下来就是对代码优化,仔细看一下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):
    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)


    # avatar_id字段验证器
    def validate_avatar_id(self, value):
        self.check_obj_exists_or_fail(
            model=Avatar,
            value=value,
            message='incorrect_avatar_id'
        )
        return value
    # category_id 字段的验证器
    def vaildate_category_id(self, value):
        self.check_obj_exists_or_fail(
            model=Category,
            value=value,
            message='incorrect_category_id'
        )

  • 把两个字段验证器的雷同代码抽象到 check_obj_exists_or_fail() 方法里。
  • check_obj_exists_or_fail() 方法检查了数据对象是否存在,若不存在则调用钩子方法 fail() 引发错误。
    fail() 又会调取 default_error_messages 属性中提供的错误类型,并将其返回给接口。

看起来似乎代码行数更多了,但更整洁了。起码你的报错信息不再零散分布在整个序列化器中,并且合并了两个验证器的重复代码,维护起来会更省事。

下章预示

接下来的两章会学习添加评论模块,有兴趣的小伙伴可以继续跟随学习(当然评论模块后续前端开发会用到),当然我们以可以使用第三方评论模块,
后续也会出使用教程。

 类似资料: