Django CKEditor 5 实战:接入、图片上传、按日期存储与生产环境避坑

在 Django 项目里接入 django_ckeditor_5 并不难,但如果只是“装上就用”,很快就会遇到工具栏太简陋、图片目录混乱、生产环境图片打不开、正文里混杂外链图片等问题。本文结合实战,讲清楚如何把 CKEditor 5 配置成真正适合技术博客和内容后台使用的版本,包括自定义上传视图、按年月日归档图片、生产环境 /media/ 配置,以及一套可复用到其它 Django 项目的完整方案。

如果你正在用 Django 做博客、CMS 或内容后台,django_ckeditor_5 基本是一个绕不开的选择。

它能比较快地把 CKEditor 5 接进项目里,让后台拥有一个现代化的富文本编辑器。但真正在项目里用一段时间后,你通常会发现:“能用”和“好用”之间,其实还差很多事情。

最常见的问题通常有这些:

  • 工具栏太保守,不适合长期写文章
  • 图片虽然能上传,但文件名和目录很乱
  • 粘贴截图后,不确定图片是否真的上传成功
  • 开发环境没问题,生产环境图片却打不开
  • 正文里既有本站图片,也有外链图片,后期越来越难维护

所以这篇文章不讲“最小安装”,而是直接讲一套更适合长期使用的方案:编辑器增强、上传视图接管、图片按日期归档、生产环境 /media/ 正确服务。


一、为什么默认接入通常不够用

django_ckeditor_5 默认上传逻辑的核心思路其实很简单:接收文件、保存文件、返回 URL。它能工作,但不负责目录治理,也不负责文件命名策略。

这会导致一个很现实的问题:项目刚开始的时候一切都还好,几个月后,media/ 目录里可能已经是这样的状态:

  • image.png
  • image_1.png
  • 截图 2026-03-19 12.33.01.png
  • xxx.webp

目录越来越乱,文件名不可控,后期清理、迁移、接 CDN 都会变麻烦。更糟糕的是,生产环境里就算编辑器已经返回了 /media/...,如果 Nginx 没有明确服务 /media/,浏览器照样打不开。

所以默认接法的问题不在于“不能上传”,而在于:它不适合长期维护。


二、更适合博客后台的整体方案

如果你的后台是拿来持续写文章的,我更建议把这件事分成四层去做:

1. 编辑器层

使用 CKEditor5Field,并把工具栏配置成适合长期写内容的版本,比如保留:

  • 标题、加粗、斜体、链接
  • 列表、待办、引用
  • 代码、代码块、表格、分隔线
  • 图片、媒体嵌入
  • 查找替换、显示块、源码模式

2. 上传层

不要直接依赖包默认上传视图,而是自己接管上传入口。这样你可以复用包已有的权限校验和表单校验,同时自己控制上传路径和命名规则。

3. 存储层

把正文图片统一保存到:

media/uploads/YYYY/MM/DD/

比如:

media/uploads/2026/03/19/editor-shot_a1b2c3d4.png

这样目录清晰、便于清理、便于备份,也更适合后续接 CDN。

4. 访问层

开发环境里 Django 可以在 DEBUG=True 时兜底服务 /media/,但生产环境一定要交给 Nginx 显式处理。这个步骤很多人一开始都会忽略。


三、在 Django 项目里接入 django_ckeditor_5

先安装包:

pip install django-ckeditor-5

然后在 INSTALLED_APPS 里加入:

INSTALLED_APPS = [
    ...
    "django_ckeditor_5",
]

模型字段改成:

from django_ckeditor_5.fields import CKEditor5Field

class Article(models.Model):
    title = models.CharField(max_length=100)
    body = CKEditor5Field("正文", config_name="default")

URL 先接入包默认路由:

path("ckeditor5/", include("django_ckeditor_5.urls")),

默认上传接口是:

/ckeditor5/image_upload/

但后面我们会把它切到自己的上传视图。


四、把编辑器配置成真正适合写文章的样子

默认 toolbar 通常太克制。对技术博客或者文档后台来说,至少应该补上这些:

  • code
  • codeBlock
  • insertTable
  • todoList
  • removeFormat
  • findAndReplace
  • showBlocks
  • sourceEditing
  • 图片样式、标题、缩放

一个比较实用的配置可以写成这样:

CKEDITOR_5_MAX_FILE_SIZE = 10 * 1024 * 1024
CKEDITOR_5_UPLOAD_FILE_TYPES = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "tiff"]

CKEDITOR_5_CONFIGS = {
    "default": {
        "toolbar": [
            "heading",
            "|",
            "bold",
            "italic",
            "underline",
            "strikethrough",
            "link",
            "|",
            "bulletedList",
            "numberedList",
            "todoList",
            "outdent",
            "indent",
            "|",
            "blockQuote",
            "code",
            "codeBlock",
            "insertTable",
            "horizontalLine",
            "|",
            "insertImage",
            "mediaEmbed",
            "removeFormat",
            "showBlocks",
            "findAndReplace",
            "|",
            "sourceEditing",
            "undo",
            "redo",
        ],
        "toolbarShouldNotGroupWhenFull": True,
        "image": {
            "toolbar": [
                "imageTextAlternative",
                "toggleImageCaption",
                "|",
                "imageStyle:inline",
                "imageStyle:block",
                "imageStyle:side",
                "|",
                "resizeImage",
            ],
        },
        "table": {
            "contentToolbar": [
                "tableColumn",
                "tableRow",
                "mergeTableCells",
                "tableCellProperties",
                "tableProperties",
            ],
        },
        "link": {
            "addTargetToExternalLinks": True,
            "defaultProtocol": "https://",
        },
        "placeholder": "支持直接粘贴截图、插入图片、表格、代码块和媒体链接。",
    }
}

这套配置的重点不是“按钮变多了”,而是它更适合高频写内容。尤其是 sourceEditingcodeBlockinsertTable 和图片 toolbar,几乎都是技术文章后台的高频需求。


五、图片上传路径为什么一定要自定义

图片上传如果继续使用默认逻辑,问题会越来越明显:

  • 不按日期归档
  • 文件名不可控
  • 目录难管理
  • 后续接对象存储或 CDN 不方便

所以更推荐把图片统一保存到:

media/uploads/YYYY/MM/DD/

例如:

media/uploads/2026/03/19/editor-shot_a1b2c3d4.png

这种结构最大的好处就是:一开始多做一步,后面省很多事。


六、怎么把 django_ckeditor_5 切到你自己的上传视图

这个包本身已经给了扩展点,它的 widget 渲染时会读取:

CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME

所以你不需要去改第三方包,只要做三件事:

  1. 写自己的上传视图
  2. 给它一个 URL name
  3. 在 settings 里指定这个 name

比如 settings 里这样配:

CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME = "blog_ck_editor_5_upload_file"

URL 里这样接:

path("ckeditor5/custom-image-upload/", upload_ckeditor_image, name="blog_ck_editor_5_upload_file"),
path("ckeditor5/", include("django_ckeditor_5.urls")),

这样编辑器的上传入口就会切到你的视图,但包默认路由仍然可以保留,不冲突。


七、自定义上传视图怎么写

这里的思路不是“全部自己重写”,而是:

  • 复用包已有的权限检查
  • 复用上传表单校验
  • 复用图片合法性校验
  • 你自己负责生成日期目录和文件名

一份可直接复用的代码如下:

from datetime import date
from pathlib import Path
from uuid import uuid4

from django.core.files.storage import default_storage
from django.http import JsonResponse
from django.utils.text import get_valid_filename
from django.views.decorators.http import require_POST
from django_ckeditor_5.exceptions import NoImageException
from django_ckeditor_5.forms import UploadFileForm
from django_ckeditor_5.permissions import check_upload_permission
from django_ckeditor_5.storage_utils import image_verify


def _dated_upload_name(filename: str) -> str:
    today = date.today()
    original = Path(filename or "image").name
    stem = get_valid_filename(Path(original).stem) or "image"
    suffix = Path(original).suffix.lower() or ".png"
    return str(
        Path("uploads")
        / today.strftime("%Y")
        / today.strftime("%m")
        / today.strftime("%d")
        / f"{stem}_{uuid4().hex[:8]}{suffix}"
    )


@require_POST
@check_upload_permission
def upload_ckeditor_image(request):
    form = UploadFileForm(request.POST, request.FILES)
    if not form.is_valid():
        error_message = form.errors.get("upload", ["Invalid form data"])[0]
        return JsonResponse({"error": {"message": error_message}}, status=400)

    uploaded = request.FILES["upload"]
    try:
        image_verify(uploaded)
    except NoImageException as ex:
        return JsonResponse({"error": {"message": str(ex)}}, status=400)

    uploaded.seek(0)
    saved_name = default_storage.save(_dated_upload_name(uploaded.name), uploaded)
    return JsonResponse({"url": default_storage.url(saved_name)})

这段实现解决了几个核心问题:

  • 图片按年月日归档
  • 文件名不会直接裸存
  • 原始文件名语义还能保留
  • 不容易重名覆盖

这一步其实就是从“能上传”进化到“能管理”。


八、怎么判断“粘贴图片”是不是真的上传成功了

很多人会误判。

看到图片出现在编辑器里,不代表它已经真的上传到了服务器。

更稳的判断方式有三个:

方法 1:看 HTML

如果正文源码里已经出现:

<img src="/media/uploads/2026/03/19/xxx.png">

说明上传链路大概率已经跑通了。

方法 2:看浏览器网络请求

开发者工具里确认是否发出了:

POST /ckeditor5/custom-image-upload/

返回 JSON 应该类似:

{
  "url": "/media/uploads/2026/03/19/editor-shot_a1b2c3d4.png"
}

方法 3:看服务器磁盘

直接检查目录里是否真的有文件:

ls -l media/uploads/2026/03/19/

这三步里,任何一步缺失,都说明你还没真正打通上传链路。


九、为什么生产环境总是“上传成功但图片不显示”

这是最常见的坑。

开发环境下你可能写了:

if settings.DEBUG:
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

所以本地一直看起来没问题。

但上线之后:

  • DEBUG=False
  • Django 不再兜底服务 /media/
  • /media/ 必须由 Nginx 负责

正确的 Nginx 配置应该至少有:

location /static/ {
    alias /srv/your_project/staticfiles/;
}

location /media/ {
    alias /srv/your_project/media/;
}

这也是为什么很多项目“本地正常、线上挂掉”。不是编辑器问题,而是资源访问链路没有补完整。


十、以后在别的项目里怎么复用

如果你后面在别的 Django 项目里还要继续用这套方案,按这个顺序做基本就够了:

  1. 安装 django-ckeditor-5
  2. 注册 INSTALLED_APPS
  3. 模型字段改成 CKEditor5Field
  4. 配好 MEDIA_URLMEDIA_ROOT
  5. 指定 CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME
  6. 写自己的上传视图
  7. URL 里把自定义上传接口接进去
  8. 生产环境补 /media/ 的 Nginx 配置
  9. 至少写三个测试:
    • 上传接口返回 200
    • 路径包含 uploads/YYYY/MM/DD/
    • 文件真实保存到磁盘

这比“复制一份最小安装教程”靠谱得多。


十一、排错时我建议按这个顺序查

如果以后又遇到 CKEditor 图片相关问题,我建议按下面顺序排:

1. 粘贴没反应

先查:

  • 浏览器有没有发上传请求
  • 上传接口是不是返回 403
  • 当前用户权限是否符合 CKEDITOR_5_FILE_UPLOAD_PERMISSION

2. 上传成功但图片不显示

先查:

  • HTML 里的 img src 是什么
  • 浏览器直接打开这个 URL 是否 200
  • Nginx 有没有正确配置 /media/

3. 图片路径太乱

先查:

  • 你是不是还在用默认上传视图
  • CK_EDITOR_5_UPLOAD_FILE_VIEW_NAME 有没有生效
  • 自定义视图是否真的生成了日期目录

4. 正文里还是外链图片

这通常不是 CKEditor 上传问题,而是历史正文本来就混有外链图片。这个要单独做本地化处理。


十二、这套方案后面还能继续优化什么

如果你已经把上面这套跑通了,后面还可以继续往前走:

  • 上传后自动压缩
  • 自动转 WebP
  • 历史外链图片本地化
  • 把底层存储切到 OSS / COS / S3
  • 接 CDN

但在我看来,第一阶段最重要的不是做高级优化,而是先把这四件事一次性做对:

  1. 配好更完整的 toolbar
  2. 接管上传视图
  3. 按年月日存图片
  4. 配好生产环境 /media/

这四步,就是从“凑合能用”到“长期可维护”的分界线。

上一篇 Django的一些最佳实践
下一篇:暂无

相关推荐