前端富文本的选择和使用

377 阅读3分钟

导读:选择范围是 tiptap、quill、wangEditor。其中 quill 出现最早,star量最多,但对于vue3等新版本的前端框架兼容不友好;tiptap 是一个相对较新的富文本编辑器,其star量高于wangEditor,可以配置性高;但 wangEditor 官方文档提供的初始配置模板更加全面,用户可以直接使用文档中配置好的模板用于生产环境。

相关插件和使用配置:

1. wangEditor

  1. 定义 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
    
  2. 再main.js中导入并注册自定义菜单

    import { Boot } from "@wangeditor/editor";import menu1Conf from '@/components/wangEditor/customMenu.js'
    
    Boot.registerMenu(menu1Conf)
    
  3. 需要注意 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, &quot;Courier New&quot;, 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;
    

总结来说,这三个富文本编辑器在功能、定制性、集成性和用户体验方面都有不同的优势和特点。选择其中一个取决于你的项目需求、前端框架的使用情况以及个人偏好。建议在项目中进行实际测试,以确定哪个编辑器最适合你的特定用例。