【VUE】Toast-UI/Editor工具实现图片自定义上传到服务器

乜承嗣
2023-12-01

Toast-UI/Editor工具实现图片自定义上传到服务器

author: jwensh
date: 2021.06.17

所遇问题

在项目开发用到富文本框,期间使用过 Tinymcetoast-ui/editor(一款支持Markdown、WYSIWYG模式的编辑器),最后选择了 toast-ui,这里不介绍两者的区别,主要感觉它好用。遇到的问题是:富文本编辑内容时,相关资源(img、video等)如何上传到指定服务器上?

  • Toast UI Editor 版本 "@toast-ui/editor": "2.5.2"

  • VUE2 工程里使用组件和插件

    npm install @toast-ui/editor
    

需要解决问题

  1. 找到编辑器中所有涉及到资源文件的操作:图片上传截图粘贴图片拖入
  2. 将上诉操作都使用自定义操作

1. 原 toast-ui 怎么处理上面三个操作的?

默认使用 addImageBlobHook 监听事件将图片转换成 Base64 追加到文本中

Toast UI Editor 支持三种图片上传方式:弹窗选择拖拽截屏粘贴;三种上传方式最终都会被 addImageBlobHook 监听,并处理为 Base64 格式的图片;

  /**
   * Initialize default image importer
   * @private
   */
  _initDefaultImageImporter() {
    this.eventManager.listen('addImageBlobHook', (blob, callback) => {
      const reader = new FileReader();

      reader.onload = event => {
        callback(event.target.result);
      };

      reader.readAsDataURL(blob);
    });
  }

2. 重写其中监听事件

  • 先看下是否有事件相关的设置方法
  • eventManager.js 中提供了删除监听事件的函数 removeEventHandler(typeStr, handler) 第 210行
/**
  * Remove event handler from given event type
  * @param {string} typeStr Event type name
  * @param {function} [handler] - registered event handler
  */
 removeEventHandler(typeStr, handler) {
   const { type, namespace } = this._getTypeInfo(typeStr);

   if (type && handler) {
     this._removeEventHandlerWithHandler(type, handler);
   } else if (type && !namespace) {
     // dont use dot notation cuz eslint
     this.events.delete(type);
   } else if (!type && namespace) {
     this.events.forEach((eventHandlers, eventType) => {
       this._removeEventHandlerWithTypeInfo(eventType, namespace);
     });
   } else if (type && namespace) {
     this._removeEventHandlerWithTypeInfo(type, namespace);
   }
 }
  • 所以,第一步:去掉默认的 addImageBlobHook 监听事件
// 删除默认监听事件
this.editor.eventManager.removeEventHandler('addImageBlobHook')
  • 第二步:添加自定义的 addImageBlobHook 监听事件
// 添加自定义监听事件
this.editor.eventManager.listen('addImageBlobHook', (blob, callback) => {
  // 此处填写自己的上传逻辑,url为上传后的图片地址
  this.uploadFile(blob, url => {
    callback(url)
  })
})

3. 服务端实现上传逻辑

后端服务实现一个可以上传文件的接口,单文件上传即可;语言自行选择,静态文件可以使用 nginx 挂载

4. 前端具体的实现

  • vue的源码
<template>
  <div :id="id" />
</template>

<script>
/**
 * @author jwensh
 * 这里使用 @toast-ui/editor@v2.5.2 版本的markdown编辑器
*/
import 'codemirror/lib/codemirror.css' // Editor's Dependency Style
import '@toast-ui/editor/dist/toastui-editor.css' // Editor's Style

import Editor from '@toast-ui/editor'
import defaultOptions from './default-options'

export default {
  name: 'MarddownEditor',
  props: {
    value: {
      type: String,
      default: ''
    },
    id: {
      type: String,
      required: false,
      default() {
        return 'markdown-editor-' + +new Date() + ((Math.random() * 1000).toFixed(0) + '')
      }
    },
    options: {
      type: Object,
      default() {
        return defaultOptions
      }
    },
    mode: {
      type: String,
      default: 'wysiwyg'
    },
    height: {
      type: String,
      required: false,
      default: '300px'
    },
    language: {
      type: String,
      required: false,
      default: 'en_US'
    }
  },
  data() {
    return {
      editor: null
    }
  },
  computed: {
    editorOptions() {
      const options = Object.assign({}, defaultOptions, this.options)
      options.initialEditType = this.mode
      options.height = this.height
      options.language = this.language
      return options
    }
  },
  watch: {
    value(newValue, preValue) {
      if (newValue !== preValue && newValue !== this.editor.getHtml()) {
        this.editor.setHtml(newValue)
      }
    },
    language(val) {
      this.destroyEditor()
      this.initEditor()
    },
    height(newValue) {
      this.editor.height(newValue)
    },
    mode(newValue) {
      this.editor.changeMode(newValue)
    }
  },
  mounted() {
    this.initEditor()
  },
  methods: {
    initEditor() {
      this.editor = new Editor({
        el: document.getElementById(this.id),
        ...this.editorOptions
      })
      if (this.value) {
        this.editor.setHtml(this.value)
      }
      this.editor.on('blur', () => {
        this.$emit('updateContent', this.editor.getHtml())
      })
      // 删除默认监听事件后,添加自定义监听事件
      this.editor.eventManager.removeEventHandler('addImageBlobHook')
      this.editor.eventManager.listen('addImageBlobHook', (blob, callback) => {
        // 此处填写自己的上传逻辑,url为上传后的图片地址
          const formData = new FormData()
	      formData.append('files', blob)
	      const ajax = new XMLHttpRequest()
	      ajax.open('POST', 'http://***/v1/uploadfiles?user_id=jwensh', true)
	      ajax.send(formData)
	      ajax.onreadystatechange = function() {
	        if (ajax.readyState === 4) {
	          if ((ajax.status >= 200 && ajax.status < 300) || ajax.status === 304) {
	            const obj = JSON.parse(ajax.responseText)
	            if (obj.code && obj.code === 'true') {
	              callback(obj.result.root_path + obj.result.url)
	            }
	          }
	        }
	      }
	   })
    }
  }
}
</script>

到此算是解决三种方式(Popup、Drag、Screenshot)按照自己的上传逻辑进行图片上传

参考

  1. https://github.com/nhn/tui.editor
  2. https://nhn.github.io/tui.editor/latest/tutorial-example01-editor-basic
 类似资料: