前端项目挑选一个合适的富文本编辑器

157 阅读3分钟
市面上有诸多富文本编辑器,工作中有用到wangeditor/editortinymce

对比两款富文本编辑器

先说结论 最后使用的是tinymce 原因: wangeditor不支持这种有序和无序列表嵌套情况,当这种情况出现的时候,会丢数据 !!!

Snipaste_2025-03-12_16-29-23.pngtinymce编辑器 支持这种情况

Snipaste_2025-03-12_16-34-15.png

wangeditor配置方法

文档地址快速开始 | wangEditor

github

import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { IDomEditor, IEditorConfig } from '@wangeditor/editor'

// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef<IDomEditor>()
const props = defineProps({
  editorId: propTypes.string.def('wangeEditor-1'),
  height: propTypes.oneOfType([Number, String]).def('500px'),
  editorConfig: {
    type: Object as PropType<IEditorConfig>,
    default: () => undefined
  },
  modelValue: propTypes.string.def('')
})

// 编辑器配置
const editorConfig = computed((): IEditorConfig => {
  return Object.assign(
    {
      readOnly: false,
      customAlert: (s: string, t: string) => {
        switch (t) {
          case 'success':
            ElMessage.success(s)
            break
          case 'info':
            ElMessage.info(s)
            break
          case 'warning':
            ElMessage.warning(s)
            break
          case 'error':
            ElMessage.error(s)
            break
          default:
            ElMessage.info(s)
            break
        }
      },
      autoFocus: false,
      scroll: true,
      uploadImgShowBase64: true
    },
    props.editorConfig || {}
  )
})

const handleCreated = (editor: IDomEditor) => {
  editorRef.value = editor
  valueHtml.value = props.modelValue
}

// 组件销毁时,及时销毁编辑器
onBeforeUnmount(() => {
  const editor = unref(editorRef.value)

  // 销毁,并移除 editor
  editor?.destroy()
})

const getEditorRef = async (): Promise<IDomEditor> => {
  await nextTick()
  return unref(editorRef.value) as IDomEditor
}

defineExpose({
  getEditorRef
})


<Toolbar
  :editor="editorRef"
  :editorId="editorId"
 />
<!-- 编辑器 -->
<Editor
   v-model="valueHtml"
   :editorId="editorId"
  :defaultConfig="editorConfig"
  :style="editorStyle"
  @on-created="handleCreated"
/>

tinymce配置方法

文档地址TinyMCE 7 Documentation | TinyMCE Documentation

github 从github上下载需要的版本然后解压放到自己的项目的public结构下,其中包含语言国际化

tinymce.png

//这些必须都导入到项目中
import tinymce from 'tinymce/tinymce'

import 'tinymce/themes/mobile/theme'
import 'tinymce/themes/silver/theme'
import 'tinymce/icons/default/icons'

import 'tinymce/plugins/advlist'
import 'tinymce/plugins/anchor'
import 'tinymce/plugins/autolink'
import 'tinymce/plugins/autoresize'
import 'tinymce/plugins/autosave'
import 'tinymce/plugins/charmap'
import 'tinymce/plugins/code'
import 'tinymce/plugins/codesample'
import 'tinymce/plugins/directionality'
import 'tinymce/plugins/emoticons'
import 'tinymce/plugins/fullpage'
import 'tinymce/plugins/fullscreen'
import 'tinymce/plugins/help'
import 'tinymce/plugins/hr'
import 'tinymce/plugins/image'
import 'tinymce/plugins/importcss'
import 'tinymce/plugins/insertdatetime'
import 'tinymce/plugins/link'
import 'tinymce/plugins/lists'
import 'tinymce/plugins/media'
import 'tinymce/plugins/nonbreaking'
import 'tinymce/plugins/pagebreak'
import 'tinymce/plugins/paste'
import 'tinymce/plugins/preview'
import 'tinymce/plugins/print'
import 'tinymce/plugins/quickbars'
import 'tinymce/plugins/save'
import 'tinymce/plugins/searchreplace'
import 'tinymce/plugins/tabfocus'
import 'tinymce/plugins/table'
import 'tinymce/plugins/template'
import 'tinymce/plugins/textcolor'
import 'tinymce/plugins/textpattern'
import 'tinymce/plugins/toc'
import 'tinymce/plugins/visualblocks'
import 'tinymce/plugins/visualchars'
import 'tinymce/plugins/wordcount'

//初始化方法
onMounted(async () => {
  loading.value = true
  await nextTick()
  tinymce.init({
    selector: '#rich-text-editor',//容器
    width: '100%',
    height: height,
    skin_url: '/tinymce/skins/ui/oxide', // Ensure the correct path
    content_css: '/tinymce/skins/content/default/content.css',
    emoticons_database_url: '/tinymce/emoticons/js/emojis.js',
    min_height: min_height,
    max_height: max_height,
    branding: false, // Hide technical support in the bottom right corner
    elementpath: true, // Disable the status bar at the bottom of the editor
    menubar: false,
    statusbar: false, // Hide the status bar at the bottom of the editor
    paste_data_images: true, // Allow pasting images
    resize: false, // resize editor tool
     /* enable automatic uploads of images represented by blob or data URIs*/
    automatic_uploads: true,
     // URL of our upload handler (for more details check: https://www.tiny.cloud/docs/configure/file-image-upload/#images_upload_url)
     images_upload_url: '/api/project/v1/uploadImage',
     // here we add custom filepicker only to Image dialog
     file_picker_types: 'image',
    /* and here's our custom image picker*/
     file_picker_callback: async (cb) => {
       const input = document.createElement('input')
       input.setAttribute('type', 'file')
       input.setAttribute('accept', 'image/*')

       input.addEventListener('change', async function (event: any) {
         const file = event.target!.files[0]

         const formData = new FormData()
         formData.append('file', file)

         try {
           const data = (await uploadImage(formData)) as ApiResponseData
           cb(data.message, { title: file.name })
         } catch (e) {
           console.error(e)
         }
      })

       input.click()
     },
     //根据自己需求添加 依赖 toolbar功能 选择需要加载的插件
    plugins:
      'print preview searchreplace autolink directionality visualblocks visualchars fullscreen link image media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern help emoticons autosave',

    toolbar: `formatselect bold italic underline numlist bullist`,
    contextmenu: false,
    // content_style: 'body { font-family:Helvetica,Arial,sans-serif; font-size:32rpx }',
    // extended_valid_elements: 'head,style,body,head[*],style[*],body[*],[*]',
    // cleanup: false,
    valid_children: '+div[style]'
  })
  const content = await processHtmlString(summaryData.value)
  const repairedContent = content + captureHeaderInner(content)
  loading.value = false
  tinymce.get('rich-text-editor').setContent(repairedContent)
})

function captureHeaderInner(htmlString: string) {
  const parser = new DOMParser()
  const doc = parser.parseFromString(htmlString, 'text/html')
  return doc.head.innerHTML
}

function getContent() {
  return {
    jobHtml: tinymce.get('rich-text-editor').getContent(),
    templateDescription: tinymce
      .get('rich-text-editor')
      .getContent({ format: 'text' })
      .replace(/[\s\n]/g, '')
      .substring(0, 50)
  }
}

function processHtmlString(htmlString: string) {
  if (!htmlString) return Promise.resolve('')
  const parser = new DOMParser()
  const doc = parser.parseFromString(htmlString, 'text/html')

  const uploadPromises: any[] = []

  return Promise.all(uploadPromises).then(() => {
    return doc.documentElement.innerHTML
  })
}


<div id="rich-text-editor" style="overflow-y: auto"></div>