导读:选择范围是 tiptap、quill、wangEditor。其中 quill 出现最早,star量最多,但对于vue3等新版本的前端框架兼容不友好;tiptap 是一个相对较新的富文本编辑器,其star量高于wangEditor,可以配置性高;但 wangEditor 官方文档提供的初始配置模板更加全面,用户可以直接使用文档中配置好的模板用于生产环境。
相关插件和使用配置:
1. wangEditor
- 关于vue3的使用可以参考:https://www.wangeditor.com/v5/for-frame.html#%E8%B0%83%E7%94%A8-api
- 关于工具栏的自定义扩展(vue3 + vite):
-
定义 customMenu.js 文件,例如:
class MyButtonMenu { constructor() { this.title = "My menu title"; // 自定义菜单标题 // this.iconSvg = '<svg>...</svg>' // 可选 this.tag = "button"; } // 获取菜单执行时的 value ,用不到则返回空 字符串或 false getValue(editor) { return " hello "; } // 菜单是否需要激活(如选中加粗文本,“加粗”菜单会激活),用不到则返回 false isActive(editor) { return false; } // 菜单是否需要禁用(如选中 H1 ,“引用”菜单被禁用),用不到则返回 false isDisabled(editor) { return false; } // 点击菜单时触发的函数 exec(editor, value) { if (this.isDisabled(editor)) return; editor.insertText(value); // value 即 this.value(editor) 的返回值 }}const menu1Conf = { key: 'menu1', // 定义 menu key :要保证唯一、不重复(重要) factory() { return new MyButtonMenu() // 把 `YourMenuClass` 替换为你菜单的 class },}export default menu1Conf
-
再main.js中导入并注册自定义菜单
import { Boot } from "@wangeditor/editor";import menu1Conf from '@/components/wangEditor/customMenu.js' Boot.registerMenu(menu1Conf)
-
需要注意 BasicEditor.vue 中的卸载自定义菜单
前面的dom结构: <Toolbar :editor="editorRef" :defaultConfig="toolbarConfig" :mode="mode" style="border-bottom: 1px solid #ccc" ref="editor_testDom" > </Toolbar> <Editor :defaultConfig="editorConfig" :mode="mode" v-model="valueHtml" style="height: 400px; overflow-y: hidden" @onCreated="handleCreated" @onChange="handleChange" @onDestroyed="handleDestroyed" @onFocus="handleFocus" @onBlur="handleBlur" @customAlert="customAlert" @customPaste="customPaste" /> // 编辑器实例,必须用 shallowRef,重要!const editorRef = shallowRef();const toolbarConfig = {} // 编辑器回调函数const handleCreated = (editor) => { editorRef.value = editor; // 记录 editor 实例,重要! // !!!选择'menu1'插入toolbar的位置 toolbarConfig.insertKeys = { index: 1, // 插入的位置,基于当前的 toolbarKeys keys: ['menu1'] };};
2. tiptap: 文档 - tiptap.dev/installatio…
**3. quill:**之前再vue2中使用没有问题,迁移到vue3中会有部分插件报错
-
相关配置插件以及版本匹配关系
"quill": "1.3.7", "vue-quill-editor": "^3.0.6", // x 暂时未使用 "quill-image-drop-module": "^1.0.3", // 图片拖拽 "quill-image-resize-module": "^3.0.0", // 图片缩放
-
相关报错的解决
// vue.config.js chainWebpack(config) { // 解决import模块quill-image-resize-module错误 config.plugin("provide").use(webpack.ProvidePlugin, [ { "window.Quill": "quill/dist/quill.js", "Quill": "quill/dist/quill.js", }, ]); }
-
quill editor 组件(其中图片上传使用的element组件,也可以使用别的方式处理图片的上传,参考: juejin.cn/post/727702…)
<template> <div> <el-upload :action="uploadUrl" :before-upload="handleBeforeUpload" :on-success="handleUploadSuccess" :on-error="handleUploadError" name="file" :show-file-list="false" :headers="headers" style="display: none" ref="upload" v-if="this.type == 'url'" > </el-upload> <div class="editor" ref="editor" :style="styles"></div> </div></template><script>import { quillEditor } from "vue-quill-editor";import Quill from "quill";import "quill/dist/quill.core.css";import "quill/dist/quill.snow.css";import "quill/dist/quill.bubble.css";import { getToken } from "@/utils/auth";// 组件导入import eeSourceBtn from './eeSourceBtn.js';Quill.register('modules/eeSourceBtn', eeSourceBtn);// modules:添加 eeSourceBtn: {}, 就可完成代码编辑// 设置调整图片大小import ImageResize from "quill-image-resize-module";Quill.register("modules/imageResize", ImageResize);// 设置拖拽import { ImageDrop } from "quill-image-drop-module";Quill.register("modules/imageDrop", ImageDrop);export default { name: "Editor", props: { /* 编辑器的内容 */ value: { type: String, default: "", }, /* 高度 */ height: { type: Number, default: 600, }, /* 最小高度 */ minHeight: { type: Number, default: null, }, /* 只读 */ readOnly: { type: Boolean, default: false, }, // 上传文件大小限制(MB) fileSize: { type: Number, default: 5, }, /* 类型(base64格式、url格式) */ type: { type: String, default: "url", }, }, data() { return { uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址 headers: { Authorization: "Bearer " + getToken(), }, Quill: null, currentValue: "", options: { theme: "snow", bounds: document.body, debug: "warn", modules: { // 显示源代码 eeSourceBtn: {}, // 设置拖拽 imageDrop: true, //设置图片大小, 也可以使用"imageResize:true",官网上采用的是下面的配置方式 imageResize: { displayStyles: { backgroundColor: "black", border: "none", color: "white", }, modules: ["Resize", "DisplaySize", "Toolbar"], }, // 工具栏配置 toolbar: [ ["bold", "italic", "underline", "strike"], // 加粗 斜体 下划线 删除线 ["blockquote", "code-block"], // 引用 代码块 [{ list: "ordered" }, { list: "bullet" }], // 有序、无序列表 [{ indent: "-1" }, { indent: "+1" }], // 缩进 [{ size: ["small", false, "large", "huge"] }], // 字体大小 [{ header: [1, 2, 3, 4, 5, 6, false] }], // 标题 [{ color: [] }, { background: [] }], // 字体颜色、字体背景颜色 [{ align: [] }], // 对齐方式 ["clean"], // 清除文本格式 ["link", "image", "video"], // 链接、图片、视频 ], }, placeholder: "请输入内容", readOnly: this.readOnly, }, }; }, computed: { styles() { let style = {'overflow-y': 'auto'}; if (this.minHeight) { style.minHeight = `${this.minHeight}px`; } if (this.height) { style.height = `${this.height}px`; } return style; }, }, watch: { value: { handler(val) { if (val !== this.currentValue) { this.currentValue = val === null ? "" : val; if (this.Quill) { this.Quill.pasteHTML(this.currentValue); } } }, immediate: true, }, }, mounted() { this.init(); }, beforeDestroy() { this.Quill = null; }, methods: { init() { const editor = this.$refs.editor; this.Quill = new Quill(editor, this.options); // 如果设置了上传地址则自定义图片上传事件 if (this.type == "url") { let toolbar = this.Quill.getModule("toolbar"); toolbar.addHandler("image", (value) => { this.uploadType = "image"; if (value) { this.$refs.upload.$children[0].$refs.input.click(); } else { this.quill.format("image", false); } }); } this.Quill.pasteHTML(this.currentValue); this.Quill.on("text-change", (delta, oldDelta, source) => { const html = this.$refs.editor.children[0].innerHTML; const text = this.Quill.getText(); const quill = this.Quill; this.currentValue = html; this.$emit("input", html); this.$emit("on-change", { html, text, quill }); }); this.Quill.on("text-change", (delta, oldDelta, source) => { this.$emit("on-text-change", delta, oldDelta, source); }); this.Quill.on("selection-change", (range, oldRange, source) => { this.$emit("on-selection-change", range, oldRange, source); }); this.Quill.on("editor-change", (eventName, ...args) => { this.$emit("on-editor-change", eventName, ...args); }); }, // 上传前校检格式和大小 handleBeforeUpload(file) { // 校检文件大小 if (this.fileSize) { const isLt = file.size / 1024 / 1024 < this.fileSize; if (!isLt) { this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`); return false; } } return true; }, handleUploadSuccess(res, file) { // 获取富文本组件实例 let quill = this.Quill; // 如果上传成功 if (res.code == 200) { // 获取光标所在位置 let length = quill.getSelection().index; // 插入图片 res.url为服务器返回的图片地址 quill.insertEmbed( length, "image", process.env.VUE_APP_BASE_API + res.fileName // res.fileName ); // 调整光标到最后 quill.setSelection(length + 1); } else { this.$message.error("图片插入失败"); } }, handleUploadError() { this.$message.error("图片插入失败"); }, },};</script><style>.ql-editor { min-height: 100px;}.editor,.ql-toolbar { white-space: pre-wrap !important; line-height: normal !important;}.quill-img { display: none;}.ql-snow .ql-tooltip[data-mode="link"]::before { content: "请输入链接地址:";}.ql-snow .ql-tooltip.ql-editing a.ql-action::after { border-right: 0px; content: "保存"; padding-right: 0px;}.ql-snow .ql-tooltip[data-mode="video"]::before { content: "请输入视频地址:";}.ql-snow .ql-picker.ql-size .ql-picker-label::before,.ql-snow .ql-picker.ql-size .ql-picker-item::before { content: "14px";}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="small"]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="small"]::before { content: "10px";}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="large"]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="large"]::before { content: "18px";}.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="huge"]::before,.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="huge"]::before { content: "32px";}.ql-snow .ql-picker.ql-header .ql-picker-label::before,.ql-snow .ql-picker.ql-header .ql-picker-item::before { content: "文本";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="1"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { content: "标题1";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="2"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { content: "标题2";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="3"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { content: "标题3";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="4"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { content: "标题4";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="5"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { content: "标题5";}.ql-snow .ql-picker.ql-header .ql-picker-label[data-value="6"]::before,.ql-snow .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { content: "标题6";}.ql-snow .ql-picker.ql-font .ql-picker-label::before,.ql-snow .ql-picker.ql-font .ql-picker-item::before { content: "标准字体";}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="serif"]::before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="serif"]::before { content: "衬线字体";}.ql-snow .ql-picker.ql-font .ql-picker-label[data-value="monospace"]::before,.ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before { content: "等宽字体";}</style>
-
上面的 eeSourceBtn 是自定的一个 ‘>_’ 按钮,用于查看富文本输入内容的源代码
// quill富文本编辑器新增的 >_ (将文本和文本源代码来回切换)class eeSourceBtn { constructor(quill, options) { let theClass = this; theClass.addDom(quill); quill.on("text-change", (delta, oldDelta, source) => { theClass.replaceSourceEditorContent(quill); }); //create btn let button = document.createElement("button"); //display button text button.innerHTML = ">_"; button.onclick = function (e) { e.preventDefault() e.stopPropagation() theClass.showSourceEditor(quill); }; //create btn container let buttonContainer = document.createElement("span"); buttonContainer.setAttribute("class", "ql-formats ee-flag-source"); buttonContainer.style.float = "right"; buttonContainer.style.marginRight = 0; buttonContainer.style.marginTop = "-25px"; buttonContainer.appendChild(button); //add to toolbar quill.container.previousSibling.appendChild(buttonContainer); } //add dom for source editor addDom(quill) { if (!quill.container.querySelector(".ql-ee-source")) { var txtArea = document.createElement("textarea"); txtArea.style.cssText = "width: 100%;margin: 0px;background: rgb(29, 29, 29);box-sizing: border-box;color: rgb(204, 204, 204);font-size: 13px;outline: none;padding: 12px 15px;line-height: 1.42;font-family: Consolas, Menlo, Monaco, "Courier New", monospace;position: absolute;top: 0;bottom: 0;border: none;"; var htmlEditor = quill.addContainer("ql-ee-source"); htmlEditor.style.cssText = "display:none"; htmlEditor.appendChild(txtArea); } } //for quill editor switch replaceSourceEditorContent(quill) { let quillEditor = quill.container.querySelector(".ql-editor"); let sourceContainer = quill.container.querySelector(".ql-ee-source"); let sourceEditor = sourceContainer.querySelector("textarea"); sourceEditor.value = quillEditor.innerHTML; } //add html DOM, show/hide, save event showSourceEditor(quill) { //1. find quill editor // let quillEditor = quill.container.querySelector(".ql-editor"); let sourceContainer = quill.container.querySelector(".ql-ee-source"); // let sourceEditor = sourceContainer.querySelector(".ql-editor"); let sourceEditor = quill.container.querySelector(".ql-editor"); // console.log(sourceEditor); //show/hide editor, value transfer if (sourceContainer.style.display === "none") { //show source editor sourceContainer.style.display = ""; // sourceEditor.value = quillEditor.innerHTML; sourceEditor.style.display = "none"; } else { //hidden source ditor sourceContainer.style.display = "none"; //set source content to quill editor // quillEditor.innerHTML = sourceEditor.value; sourceEditor.style.display = ""; } }} export default eeSourceBtn;
总结来说,这三个富文本编辑器在功能、定制性、集成性和用户体验方面都有不同的优势和特点。选择其中一个取决于你的项目需求、前端框架的使用情况以及个人偏好。建议在项目中进行实际测试,以确定哪个编辑器最适合你的特定用例。