Skip to content

富文本编辑器

作者:Yuan Tang
更新于:5 个月前
字数统计:2.7k 字
阅读时长:10 分钟

tinymce

注意事项

  1. 可以安装对应的 vue/react 组件,直接作为组件使用。或者直接下载做原生操作
  2. 报找不到文件的错误,需要把 tinymce 复制到 public 文件夹内

安装

  • 安装 vue 模块的组件

    yarn add @tinymce/tinymce-vue

    如果是 react 项目则改为 @tinymce/tinymce-react

  • 安装 tinymce

    yarn add tinymce

使用

  • 首先引入富文本编辑器

    js
    import tinymce from 'tinymce'
  • 挂载到元素上

    通过 tinymce.init() 方法的 selector 属性传入一个 id 选择器或类选择器,表示把富文本编辑器挂载到该元素上。

    注意:

    由于要获取到元素,因此需要等待元素加载完毕才能获取,vue2 项目中要把方法写在 mounted 生命周期上,vue3 项目要把方法写在 onMounted 钩子上

    vue
    <script setup>
    import { onMounted } from 'vue'
    
    onMounted(() => {
      tinymce.init({
        selector: '#mytinymce'
      })
    })
    </script>
    
    <template>
      <div id="mytinymce" />
    </template>
  • 复制文件到 public 文件

    此时它加载完后会试图加载它的 js 文件和 css 文件,因此要把 node_modules 内它的整个内容复制到 public 文件内。否则会报错。

    后续如果项目打包上线了,也需要把这些资源放到服务器的静态资源目录中。

常见界面需求

隐藏不需要的部分

  • 组件的方式

    引入组件

    js
    import { Editor } from '@tinymce/tinymce-vue'

    页面中使用

    vue
    <script setup>
    const initObj = {
      selector: '#mytinymce',
      Menubar: false,
      toolbar: true,
      statusbar: true,
    }
    </script>
    
    <Editor :init="initObj"></Editor>
  • 直接使用tinymce 的方式

    Menubar 控制菜单,toolbar 控制工具栏,statusbar 控制状态栏。想要隐藏只需要在配置中把其设为 false 即可。

    js
    onMounted(() => {
      tinymce.init({
        selector: '#mytinymce',
        Menubar: false,
        toolbar: true,
        statusbar: true,
      })
    })

自定义样式

Skin 控制皮肤,或者通过 skin_url 导入自定义皮肤。也可以通过 content_css 定义内容区域样式。Icons_url 导入图标。

皮肤在复制到 public 文件中的 skin 文件夹下 ui 文件夹内寻找,复制其文件夹名称即可。其本质是寻找其对应皮肤文件夹下的 skin.css 文件。

自定义皮肤则是在 public 下找到自定义的文件夹名称(如下方示例代码的 myskin )下寻找 content.min.cssskin.min.css 文件。

js
onMounted(() => {
  tinymce.init({
    selector: '#mytinymce',
    Menubar: false,
    toolbar: true,
    statusbar: true,
    skin: 'oxide',
    skin_url: 'myskin',
  })
})

中文化

把文件转为二进制形式获取为 blob 形象。

下载中文包,解压 langs 文件夹到 public 文件夹内,在配置项中通过 language 属性赋值刚解压的js文件名。

js
onMounted(() => {
  tinymce.init({
    selector: '#mytinymce',
    Menubar: false,
    toolbar: true,
    statusbar: true,
    skin: 'oxide',
    skin_url: 'myskin',
    language: 'zh-Hans',
  })
})

自己选择工具栏内容和排序

把文件转为二进制形式获取为 blob 形象。

通过 toolbar 设置排序,用 | 作为组分隔开每组,每个单词对应不同的功能,从左到右对应排序。

js
onMounted(() => {
  tinymce.init({
    selector: '#mytinymce',
    Menubar: false,
    toolbar: 'undo redo | styles | blob italic | fontfamily fontsize fontcolor',
    statusbar: true,
    skin: 'oxide',
    skin_url: 'myskin',
    language: 'zh-Hans',
  })
})

进阶需求

获取内容

通过 tinymce.activeEditor.getContent() 获取内容。这么获取的内容是 html 富文本内容,如果想要获取纯文本内容,则需要在括号内设置 {format: 'text'}

设置内容

通过 tinymce.activeEditor.setContent() 设置内容。括号内输入内容,如 '<p>tydumpling</p>' 。也可以选中内容设置内容,方法为选中内容后通过 tinymce.activeEditor.selection.setContent('hello') 替换为 hello

二次开发

自带插件

在安装的时候有一个 plugins 文件夹,里面有它自带的插件,如image、link、code、table等。

使用方法为在 plugins 属性中配置插件,在 toolbar 中设置即可。

js
onMounted(() => {
  tinymce.init({
    selector: '#mytinymce',
    Menubar: false,
    plugins: 'code image',
    toolbar: 'undo redo | styles | blob italic | fontfamily fontsize fontcolor | code image',
    statusbar: true,
    skin: 'oxide',
    skin_url: 'myskin',
    language: 'zh-Hans',
  })
})

如果引用他人的插件,把他复制到 plugins 文件夹下,再把文件名配置到 plugins 属性和 toolbar 属性中,就算完成。

开发自己的工具栏按钮

首先通过 setup 函数中注册,该函数的形参可获取 Editor 对象,通过 ui.registry.addButon 方法创建按钮和功能。

该方法接收两个参数,参数1是字符串,为该按钮的名称;参数2是一个对象,icon为该按钮的图标,tooltip为提示功能,onAction是一个函数,用于设置功能。

js
onMounted(() => {
    tinymce.init({
        selector: '#mytinymce',
        Menubar: false,
        plugins: 'code image',
        toolbar: 'undo redo | styles | blob italic | fontfamily fontsize fontcolor | code image',
        statusbar: true,
        skin: 'oxide',
        skin_url: 'myskin',
        language: 'zh-Hans',
        setup(editor) {
            // 对选中的内容做某些操作
            editor.ui.registry.addButton('red', {
                icon: 'help',
                toolTip: '字体标红',
                onAction: () => {
                    let text = editor.selection.getContent()
                    editor.selection.sgetContent(`<span class="red-text">${text}</span>`)
                }
            });
            // 把内容展示在页面中
            editor.ui.registry.addButton('show', {
            	icon: 'emjio',
            	toolTip: '展示选中内容',
            	onAction: () => {
                	let text = editor.selection.getContent()
                	popShow.value = true
            		popValue.value = text
            });
        },
        content_css: '/mycontent.css' // 大段的css样式可在 public 文件夹中创建一个css文件设置类名样式
    })
})

vue-quill-editor

详细可见官网:

.

安装

bash
# use npm
npm install vue-quill-editor

# use yarn
yarn add vue-quill-editor

注:该插件是基于 Quill 的,无需下载 Quill,因为在安装 vue-quill-editor 时,会把所需的依赖进行统一安装。

引入

全局引用

main.js 中引入插件

javascript
// 全局挂载 VueQuillEditor
import VueQuillEditor from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

Vue.use(VueQuillEditor)

局部(组件)引用

javascript
// 引入样式和quillEditor
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import { quillEditor } from 'vue-quill-editor'

// 注册 quillEditor
components: {
  quillEditor
},

由以上代码可以看到全局应用和在组件中引入的方式是不一样的,全局引用采用的是 import 直接引入,局部引用时导出一个对象在对象中得到 quillEditor ,这里我们可以看一下源码,发现源码中不仅默认导出了 VueQuillEditor,还导出了一个包含 quillEditor 的对象。

全局引用`: `import VueQuillEditor from 'vue-quill-editor'
局部引用`: `import { quillEditor } from 'vue-quill-editor'

使用

  • options :富文本上方的功能
vue
<script>
// 工具栏配置项
const toolbarOptions = [
  // 加粗 斜体 下划线 删除线 -----['bold', 'italic', 'underline', 'strike']
  ['bold', 'italic', 'underline', 'strike'],
  // 引用  代码块-----['blockquote', 'code-block']
  ['blockquote', 'code-block'],
  // 1、2 级标题-----[{ header: 1 }, { header: 2 }]
  [{ header: 1 }, { header: 2 }],
  // 有序、无序列表-----[{ list: 'ordered' }, { list: 'bullet' }]
  [{ list: 'ordered' }, { list: 'bullet' }],
  // 上标/下标-----[{ script: 'sub' }, { script: 'super' }]
  [{ script: 'sub' }, { script: 'super' }],
  // 缩进-----[{ indent: '-1' }, { indent: '+1' }]
  [{ indent: '-1' }, { indent: '+1' }],
  // 文本方向-----[{'direction': 'rtl'}]
  [{ direction: 'rtl' }],
  // 字体大小-----[{ size: ['small', false, 'large', 'huge'] }]
  [{ size: ['small', false, 'large', 'huge'] }],
  // 标题-----[{ header: [1, 2, 3, 4, 5, 6, false] }]
  [{ header: [1, 2, 3, 4, 5, 6, false] }],
  // 字体颜色、字体背景颜色-----[{ color: [] }, { background: [] }]
  [{ color: [] }, { background: [] }],
  // 字体种类-----[{ font: [] }]
  [{ font: [] }],
  // 对齐方式-----[{ align: [] }]
  [{ align: [] }],
  // 清除文本格式-----['clean']
  ['clean'],
  // 链接、图片、视频-----['link', 'image', 'video']
  ['image', 'video']
]

export default {
  name: 'LocalQuillEditor',
  data() {
    return {
      content: '',
      editorOption: {
        modules: {
          toolbar: toolbarOptions
        },
        theme: 'snow',
        placeholder: '请输入正文'
      }
    }
  },
  methods: {
    // 失去焦点事件
    onEditorBlur(e) {
      console.log('onEditorBlur: ', e)
    },
    // 获得焦点事件
    onEditorFocus(e) {
      console.log('onEditorFocus: ', e)
    },
    // 内容改变事件
    onEditorChange(e) {
      console.log('onEditorChange: ', e)
    }
  }
}
</script>

<template>
  <div class="local-quill-editor">
    <quill-editor
      ref="myLQuillEditor"
      v-model="content"
      :options="editorOption"
      class="editor"
      @blur="onEditorBlur"
      @focus="onEditorFocus"
      @change="onEditorChange"
    >
      >
    </quill-editor>
  </div>
</template>

<style scoped lang="scss">
.editor {
  height: 500px;
}
</style>

图片上传到服务器

一般情况下,富文本的图片上传是把图片转为 base64 的形式,而这么转换会导致这条数据太大,单纯一张图片转成base64 后已经 234kb 左右了,用户在输入几个字就要超出数据库该字段存储的空间。因此需要把图片上传改为上传到服务器中,用返回的路径作为渲染的路径。

第一步永远是阅读文档,官方文档指路:

。官方文档的描述中这个插件的功能包含提供图片上传到服务器的功能,刚好符合我们的需求,可以继续往下看。

安装

bash
npm install quill-image-extend-module --save-dev

导入

js
import { Quill, quillEditor } from 'vue-quill-editor'
import { ImageExtend, QuillWatch, container } from 'quill-image-extend-module'

Quill.register('modules/ImageExtend', ImageExtend)

使用

vue
<script>
import { Quill, quillEditor } from 'vue-quill-editor'
import { ImageExtend, QuillWatch, container } from 'quill-image-extend-module'

Quill.register('modules/ImageExtend', ImageExtend)
export default {
  components: { QuillEditor: quillEditor },
  data() {
    return {
      content: '',
      // 富文本框参数设置
      editorOption: {
        modules: {
          ImageExtend: {
            loading: true,
            name: 'img',
            action: updateUrl,
            response: (res) => {
              return res.info
            }
          },
          toolbar: {
            container,
            handlers: {
              image() {
                QuillWatch.emit(this.quill.id)
              }
            }
          }
        }
      }
    }
  }
}
</script>

<template>
  <div class="quill-wrap">
    <QuillEditor
      ref="myQuillEditor"
      v-model="content"
      :options="editorOption"
    />
  </div>
</template>

所有可配置项

js
 editorOption: {
    modules: {
        ImageExtend: {  // 如果不作设置,即{}  则依然开启复制粘贴功能且以base64插入 
            name: 'img',  // 图片参数名
            size: 3,  // 可选参数 图片大小,单位为M,1M = 1024kb
            action: updateUrl,  // 服务器地址, 如果action为空,则采用base64插入图片
            /* response 为一个函数用来获取服务器返回的具体图片地址
            * 例如服务器返回{code: 200; data:{ url: 'baidu.com'}},则 return res.data.url */
            response: (res) => {
                return res.info
            },
            headers: (xhr) => {
            // xhr.setRequestHeader('myHeader','myValue')
            },  // 可选参数 设置请求头部
            sizeError: () => {},  // 图片超过大小的回调
            start: () => {},  // 可选参数 自定义开始上传触发事件
            end: () => {},  // 可选参数 自定义上传结束触发的事件,无论成功或者失败
            error: () => {},  // 可选参数 上传失败触发的事件
            success: () => {},  // 可选参数  上传成功触发的事件
            change: (xhr, formData) => {
            // xhr.setRequestHeader('myHeader','myValue')
            // formData.append('token', 'myToken')
            } // 可选参数 每次选择图片触发,也可用来设置头部,但比headers多了一个参数,可设置formData
        },
        toolbar: {  // 如果不上传图片到服务器,此处不必配置
            container: container,  // container为工具栏,此次引入了全部工具栏,也可自行配置
            handlers: {
                'image': function () {  // 劫持原来的图片点击按钮事件
                    QuillWatch.emit(this.quill.id)
                }
            }
        }
    }
}

注意事项

由于不同的用户的服务器返回的数据格式不尽相同,因此在配置中,必须做如下操作

js
// 你必须把返回的数据中所包含的图片地址 return 回去
respnse: (res) => {
  return res.info // 这里切记要return回你的图片地址
}

比如服务器返回的成功数据为

js
{
	code: 200,
	starus: true,
	result: {
    	img: 'http://placehold.it/xx.jpg' // 服务器返回的数据中的图片的地址
 	}
}

那么应该在参数中写为:

js
// 你必须把返回的数据中所包含的图片地址 return 回去
respnse: (res) => {
  return res.result.img // 这里切记要return回你的图片地址
}

与其他模块一起使用(以resize-module为例子)

vue
<script>
import { Quill, quillEditor } from 'vue-quill-editor'
import { ImageExtend, QuillWatch, container } from 'quill-image-extend-module'
import ImageResize from 'quill-image-resize-module'

Quill.register('modules/ImageExtend', ImageExtend)
// use resize module
Quill.register('modules/ImageResize', ImageResize)
export default {
  components: { QuillEditor: quillEditor },
  data() {
    return {
      content: '',
      // 富文本框参数设置
      editorOption: {
        modules: {
          ImageResize: {},
          ImageExtend: {
            name: 'img',
            size: 2, // 单位为M, 1M = 1024KB
            action: updateUrl,
            headers: (xhr) => {
            },
            response: (res) => {
              return res.info
            }
          },
          toolbar: {
            container,
            handlers: {
              image() {
                QuillWatch.emit(this.quill.id)
              }
            }
          }
        }
      }
    }
  }
}
</script>

<template>
  <div class="quill-wrap">
    <QuillEditor
      ref="myQuillEditor"
      v-model="content"
      :options="editorOption"
    />
  </div>
</template>

Contributors

Yuan Tang