Vue3使用CKEditor 5 自定义上传

一、功能亮点

✅ 深度集成 CKEditor5 最新版本 (v45.0.0)
✅ 支持中文字体、字号、颜色等排版功能
✅ 实现图片本地上传并自动插入编辑器
✅ 完整 Markdown 兼容支持
✅ 实时字数统计功能
✅ 完美适配 Vue3 组合式 API

二、环境准备

1.安装核心依赖

npm install ckeditor5 @ckeditor/ckeditor5-vue

三、核心实现

1. 编辑器初始化配置

<script setup>
// 按需引入 45+ 个功能插件
import { ClassicEditor, FontFamily, ImageUpload /* ... */ } from 'ckeditor5'

const config = ref({
  plugins: [FontFamily, ImageUpload, /* ... */ ],
  toolbar: {
    items: [
      'fontFamily', 'fontSize', 'uploadImage', 
      '|', 'bold', 'italic', 'codeBlock'
    ]
  },
  image: {
    toolbar: ['toggleImageCaption', 'imageTextAlternative'],
    upload: {
      types: ['jpeg', 'png', 'webp'] // 限制图片类型
    }
  }
})
</script>

2. 自定义上传适配器

// UploadAdapter.js
import axios from "axios"

export default class UploadAdapter {
  constructor(loader) {
    this.loader = loader;
  }

  async upload() {
    const data = new FormData();
    data.append('typeOption', 'upload_image');
    data.append('file', await this.loader.file);
    
    return new Promise((resolve,reject) => {
      axios({
        url: 'http://localhost:3000/upload',
        method: 'post',
        data,
        headers: {
          Authorization: sessionStorage.getItem("Authorization")
        },
        // withCredentials: true
      })
      .then(res => {
        // 自定义接口返回参数
        console.log(res)
        resolve({
          'uploaded': 0,
          'default': res.data.url // 根据后端返回格式
        });
      })
      .catch(err => {
        console.log('上传失败');
        
        reject(err)
      })
    })
  }
  abort() {
    // The upload process can be aborted by calling abort()
  }
}

3. 注册上传适配器

<script setup>
const onEditorReady = (editor) => {
  editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
    return new UploadAdapter(loader)
  }
}
</script>

四、后端接口实现(Node.js)

1.安装依赖

npm init -y
npm install express multer cors
// server.js
const express = require('express');
const multer = require('multer');
const cors = require('cors');
const path = require('path');
const fs = require('fs');

const app = express();

// 允许跨域
app.use(cors({
  origin: '*',
  methods: ['POST'],
  allowedHeaders: ['Content-Type']
}));

// 创建上传目录
const uploadDir = path.join(__dirname, 'uploads');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

// 配置 multer 存储
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

const upload = multer({ storage });

// 上传接口
app.post('/upload', upload.single('file'), (req, res) => {
  try {
    if (!req.file) {
      return res.status(400).json({
        uploaded: 0,
        error: { message: '未收到文件' }
      });
    }

    
    const fileUrl = `http://localhost:3000/uploads/${req.file.filename}`;

    // 返回 CKEditor 要求的格式
    res.json({
      uploaded: 1,
      url: fileUrl
    });

  } catch (err) {
    console.error('上传错误:', err);
    res.status(500).json({
      uploaded: 0,
      error: { message: '服务器处理失败' }
    });
  }
});

// 启动服务
const PORT = 3000;
app.listen(PORT, () => {
  console.log(`服务已启动:http://localhost:${PORT}`);
});

五、完整代码

1.ckeditor.vue

<script setup>
import { ref, computed, useTemplateRef } from 'vue'
import { Ckeditor } from '@ckeditor/ckeditor5-vue'
import {
  ClassicEditor,
  Alignment,
  Autoformat,
  AutoImage,
  AutoLink,
  Autosave,
  Base64UploadAdapter,
  BlockQuote,
  Bold,
  Bookmark,
  Code,
  CodeBlock,
  Essentials,
  FindAndReplace,
  FontBackgroundColor,
  FontColor,
  FontFamily,
  FontSize,
  Fullscreen,
  GeneralHtmlSupport,
  Heading,
  Highlight,
  HorizontalLine,
  HtmlComment,
  HtmlEmbed,
  ImageBlock,
  ImageCaption,
  ImageInline,
  ImageInsert,
  ImageInsertViaUrl,
  ImageResize,
  ImageStyle,
  ImageTextAlternative,
  ImageToolbar,
  ImageUpload,
  Indent,
  IndentBlock,
  Italic,
  Link,
  List,
  ListProperties,
  Markdown,
  MediaEmbed,
  PageBreak,
  Paragraph,
  PasteFromMarkdownExperimental,
  PasteFromOffice,
  PlainTableOutput,
  RemoveFormat,
  ShowBlocks,
  SourceEditing,
  SpecialCharacters,
  SpecialCharactersArrows,
  SpecialCharactersCurrency,
  SpecialCharactersEssentials,
  SpecialCharactersLatin,
  SpecialCharactersMathematical,
  SpecialCharactersText,
  Strikethrough,
  Subscript,
  Superscript,
  Table,
  TableCaption,
  TableCellProperties,
  TableColumnResize,
  TableLayout,
  TableProperties,
  TableToolbar,
  TextTransformation,
  TodoList,
  Underline,
  WordCount
} from 'ckeditor5'
import MyUploadAdapter from "./UploadAdapter";

import translations from 'ckeditor5/translations/zh-cn.js'

import 'ckeditor5/ckeditor5.css'


const data = ref('<p>Hello world!</p>')

const config = ref(computed(() => {
  return {
    licenseKey: 'GPL',
    plugins: [
      Alignment,
      Autoformat,
      AutoImage,
      AutoLink,
      Autosave,
      Base64UploadAdapter,
      BlockQuote,
      Bold,
      Bookmark,
      Code,
      CodeBlock,
      Essentials,
      FindAndReplace,
      FontBackgroundColor,
      FontColor,
      FontFamily,
      FontSize,
      Fullscreen,
      GeneralHtmlSupport,
      Heading,
      Highlight,
      HorizontalLine,
      HtmlComment,
      HtmlEmbed,
      ImageBlock,
      ImageCaption,
      ImageInline,
      ImageInsert,
      ImageInsertViaUrl,
      ImageResize,
      ImageStyle,
      ImageTextAlternative,
      ImageToolbar,
      ImageUpload,
      Indent,
      IndentBlock,
      Italic,
      Link,
      List,
      ListProperties,
      Markdown,
      MediaEmbed,
      PageBreak,
      Paragraph,
      PasteFromMarkdownExperimental,
      PasteFromOffice,
      PlainTableOutput,
      RemoveFormat,
      ShowBlocks,
      SourceEditing,
      SpecialCharacters,
      SpecialCharactersArrows,
      SpecialCharactersCurrency,
      SpecialCharactersEssentials,
      SpecialCharactersLatin,
      SpecialCharactersMathematical,
      SpecialCharactersText,
      Strikethrough,
      Subscript,
      Superscript,
      Table,
      TableCaption,
      TableCellProperties,
      TableColumnResize,
      TableLayout,
      TableProperties,
      TableToolbar,
      TextTransformation,
      TodoList,
      Underline,
      WordCount
    ],
    toolbar: {
      items: [
        'sourceEditing',
        'showBlocks',
        'findAndReplace',
        'fullscreen',
        '|',
        'heading',
        '|',
        'fontSize',
        'fontFamily',
        'fontColor',
        'fontBackgroundColor',
        '|',
        'bold',
        'italic',
        'underline',
        'strikethrough',
        'subscript',
        'superscript',
        'code',
        'removeFormat',
        '|',
        'specialCharacters',
        'horizontalLine',
        'pageBreak',
        'link',
        'bookmark',
        // 'insertImage',
        'uploadImage',
        'mediaEmbed',
        'insertTable',
        'insertTableLayout',
        'highlight',
        'blockQuote',
        'codeBlock',
        'htmlEmbed',
        '|',
        'alignment',
        '|',
        'bulletedList',
        'numberedList',
        'todoList',
        'outdent',
        'indent'
      ],
      shouldNotGroupWhenFull: false
    },
    fontFamily: {
            supportAllValues: true
        },
    fontSize: {
      options: [10, 12, 14, 'default', 18, 20, 22],
      supportAllValues: true
    },
    fullscreen: {
            onEnterCallback: container =>
                container.classList.add(
                    'editor-container',
                    'editor-container_classic-editor',
                    'editor-container_include-word-count',
                    'editor-container_include-fullscreen',
                    'main-container'
                )
        },
    heading: {
      options: [
        {
          model: 'paragraph',
          title: 'Paragraph',
          class: 'ck-heading_paragraph'
        },
        {
          model: 'heading1',
          view: 'h1',
          title: 'Heading 1',
          class: 'ck-heading_heading1'
        },
        {
          model: 'heading2',
          view: 'h2',
          title: 'Heading 2',
          class: 'ck-heading_heading2'
        },
        {
          model: 'heading3',
          view: 'h3',
          title: 'Heading 3',
          class: 'ck-heading_heading3'
        },
        {
          model: 'heading4',
          view: 'h4',
          title: 'Heading 4',
          class: 'ck-heading_heading4'
        },
        {
          model: 'heading5',
          view: 'h5',
          title: 'Heading 5',
          class: 'ck-heading_heading5'
        },
        {
          model: 'heading6',
          view: 'h6',
          title: 'Heading 6',
          class: 'ck-heading_heading6'
        }
      ]
    },
    htmlSupport: {
            allow: [
                {
                    name: /^.*$/,
                    styles: true,
                    attributes: true,
                    classes: true
                }
            ]
        },
    image: {
            toolbar: [
                'toggleImageCaption',
                'imageTextAlternative',
                '|',
                'imageStyle:inline',
                'imageStyle:wrapText',
                'imageStyle:breakText',
                '|',
                'resizeImage'
            ]
        },
    initialData: '<h2>Congratulations on setting up CKEditor 5! 🎉</h2>',
    link: {
            addTargetToExternalLinks: true,
            defaultProtocol: 'https://',
            decorators: {
                toggleDownloadable: {
                    mode: 'manual',
                    label: 'Downloadable',
                    attributes: {
                        download: 'file'
                    }
                }
            }
        },
    list: {
            properties: {
                styles: true,
                startIndex: true,
                reversed: true
            }
        },
    placeholder: '在这里输入或粘贴您的内容!',
    table: {
            contentToolbar: ['tableColumn', 'tableRow', 'mergeTableCells', 'tableProperties', 'tableCellProperties']
        },
    translations: [translations]
  }
}))

const editorWordCount = useTemplateRef('editorWordCountElement')


const onReady = (editor) => {
  editor.plugins.get("FileRepository").createUploadAdapter = (loader) => {
    return new MyUploadAdapter(loader)
  }

  [...editorWordCount.value.children].forEach(child => child.remove());
  
  const wordCount = editor.plugins.get('WordCount');
    editorWordCount.value.appendChild(wordCount.wordCountContainer);
}


</script>

<template>
  <div class="container">
    <ckeditor v-model="data" :editor="ClassicEditor" :config="config" @ready="onReady" />
    <div class="editor_container__word-count" ref="editorWordCountElement"></div>
  </div>
</template>

<style>
.container {
  width: 100%;
}
.ck-content {
  height: 500px;
}
</style>

2.UploadAdapter.js

import axios from "axios"

export default class UploadAdapter {
  constructor(loader) {
    this.loader = loader;
  }

  async upload() {
    const data = new FormData();
    data.append('typeOption', 'upload_image');
    data.append('file', await this.loader.file);
    
    return new Promise((resolve,reject) => {
      axios({
        url: 'http://localhost:3000/upload',
        method: 'post',
        data,
        headers: {
          Authorization: sessionStorage.getItem("Authorization")
        },
        // withCredentials: true
      })
      .then(res => {
        // 自定义接口返回参数
        console.log(res)
        resolve({
          'uploaded': 0,
          'default': res.data.url // 根据后端返回格式
        });
      })
      .catch(err => {
        console.log('上传失败');
        
        reject(err)
      })
    })
  }
  abort() {
    // The upload process can be aborted by calling abort()
  }
}

本方案已通过以下环境验证:

Vue 3.5.13

CKEditor 45.0.0

ckeditor5-vue 7.3.0

Node.js v20.17.0

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容