从零配置Quill编辑器:在Vue和React项目中实现图片上传与自定义工具栏

菜鸟学习笔记之React-Quill富文本编辑器 React-Quill 是Antd推荐使用的富文本编辑器,此文记录一下最近在项目中使用它遇到的一些问题。 首先,大家在百度相关资料时,应当搜索React-Quill,而不是Quill。 github点这里:https://github.com/zenoamaro/react-quill 安装依赖这些就都不说了,只记录干货.... 调用示例: <ReactQuill them... 阅读详情

从零构建企业级富文本编辑器:在Vue与React生态中的深度集成与功能定制

如果你正在为博客平台、内容管理系统或者任何需要用户输入格式化文本的应用寻找一个强大且可控的富文本编辑器,那么Quill.js很可能已经进入了你的视野。它不像某些庞然大物那样自带所有功能,却因其模块化架构和API驱动的设计哲学,为开发者提供了前所未有的灵活性和控制力。这意味着,你可以从一个轻量、干净的核心开始,然后像搭积木一样,只为你需要的功能——比如图片上传、自定义工具栏、表格、@提及——添加相应的模块。

本文将带你超越简单的“安装-引入”步骤,深入探讨如何在现代前端框架(Vue 3和React)中,从零开始深度集成Quill,并实现那些让产品经理眼前一亮、让用户体验更丝滑的高级功能。我们将聚焦于工程化配置、自定义功能扩展以及与框架状态管理的优雅结合,目标是打造一个既强大又易于维护的企业级编辑器解决方案。

1. 项目初始化与Quill核心集成策略

在开始写代码之前,我们需要明确一个核心思想:Quill是一个框架无关的库。无论是Vue还是React,我们集成的本质都是在框架的组件生命周期中,创建并管理一个Quill实例。因此,理解其原生API是第一步。

1.1 环境准备与包管理

首先,通过npm或yarn安装Quill核心库及其样式。这里有一个关键点:Quill 2.0已经发布,它带来了性能提升和更好的API,但社区生态(如一些第三方模块)可能还未完全跟进。对于生产环境,使用稳定的1.3.7版本是更稳妥的选择。我们将同时安装框架封装库以提高开发效率。

Vue 3项目:

npm install quill@1.3.7 vue-quill-editor@next --save
# vue-quill-editor@next 是针对Vue 3的版本

React项目:

npm install quill@1.3.7 react-quill --save

接下来,我们需要引入Quill的主题样式。Quill提供了“snow”和“bubble”两种主题,snow是带有固定工具栏的经典主题,bubble是工具栏在选中文本时浮现的气泡式主题。

/* 在你的全局样式文件(如App.vue或index.css)中引入 */
/* 使用Snow主题 */
@import '~quill/dist/quill.snow.css';
/* 或者使用Bubble主题 */
/* @import '~quill/dist/quill.bubble.css'; */

/* 可选:引入核心样式,如果你需要高度自定义 */
/* @import '~quill/dist/quill.core.css'; */

注意:直接引入CSS文件可能会与你的构建工具(如Vite、Webpack)的样式处理流程产生冲突。如果遇到样式丢失,可以尝试在项目的入口JS文件中使用import 'quill/dist/quill.snow.css';。

1.2 理解Quill的配置项:超越默认值

创建一个Quill实例时,配置对象是你的控制中心。让我们深入看看几个关键配置:

// 这是一个详细的配置示例,我们将在后续拆解
const editorOptions = {
  theme: 'snow', // 必选,决定编辑器UI框架
  placeholder: '开始创作吧...',
  readOnly: false, // 可用于实现“预览模式”
  modules: {
    // 模块配置是功能扩展的核心,下文详述
    toolbar: { /* ... */ },
    keyboard: { /* ... */ },
    history: { /* ... */ },
    clipboard: { /* ... */ },
  },
  formats: [ // 白名单机制,控制允许的格式,提升安全性
    'bold', 'italic', 'underline', 'strike',
    'blockquote', 'code-block', 'header',
    'list', 'bullet', 'indent',
    'link', 'image', 'video',
    'color', 'background', 'align'
  ],
  bounds: document.body, // UI元素(如工具提示)的边界容器
};

modules配置详解: 这是Quill模块化架构的体现。默认情况下,Quill会自动加载keyboard, history, clipboard, toolbar等核心模块。你可以通过配置禁用它们或调整其行为。

模块名作用常用配置项
toolbar工具栏控件container (工具栏DOM/配置数组), handlers (自定义处理器)
keyboard键盘快捷键绑定bindings (自定义快捷键映射)
history撤销/重做堆栈delay (操作合并延迟), maxStack (堆栈大小), userOnly (是否只记录用户操作)
clipboard粘贴板处理matchers (自定义粘贴内容转换器)
syntax代码块语法高亮true或配置对象,依赖highlight.js

在下一节,我们将重点攻克第一个实战难点:自定义工具栏与图片上传。

2. 深度自定义:工具栏、图片上传与内容安全

默认工具栏可能不符合你的产品设计,而图片上传功能几乎是所有内容型应用的标配。Quill没有内置上传逻辑,但这正是其灵活性的体现——我们可以完全掌控这个过程。

2.1 构建符合产品调性的自定义工具栏

工具栏配置有两种主要形式:简单数组和详细对象。简单数组用于快速定义按钮和下拉菜单的顺序。

// 方式一:简单数组配置(最常用)
const toolbarOptions = [
  ['bold', 'italic', 'underline', 'strike'],        // 文本样式组
  ['blockquote', 'code-block'],                     // 块级元素组
  [{ 'header': [1, 2, 3, 4, 5, 6, false] }],       // 标题下拉菜单
  [{ 'list': 'ordered'}, { 'list': 'bullet' }],     // 列表
  [{ 'color': [] }, { 'background': [] }],          // 颜色选择器
  ['link', 'image', 'video'],                       // 媒体与链接
  ['clean']                                         // 清除格式
];

但如果你需要更精细的控制,比如为某个按钮绑定自定义的点击行为(如图片上传),就需要使用对象配置。

// 方式二:对象配置(支持自定义处理器)
const toolbarOptions = {
  container: [ /* 同上方的数组配置 */ ],
  handlers: {
    // 关键:覆盖默认的图片处理器
    'image': function() {
      // 这里实现自定义图片上传逻辑,下文展开
      this.customImageHandler();
    },
    // 也可以为其他按钮添加自定义行为
    'link': function(value) {
      if (value) {
        let href = prompt('请输入链接地址');
        this.quill.format('link', href);
      } else {
        this.quill.format('link', false);
      }
    }
  }
};

在Vue或React的封装组件中,你需要将toolbarOptions传递给编辑器的配置。

2.2 实现健壮的图片上传功能

图片上传不仅仅是弹出一个文件选择框。我们需要考虑:上传进度提示、错误处理、服务器响应适配、图片插入编辑器。下面是一个结合了现代异步编程和用户体验的完整示例。

我们首先创建一个独立的图片上传工具函数:

// utils/imageUploader.js
/**
 * 处理图片上传
 * @param {File} file - 图片文件对象
 * @param {Function} onProgress - 上传进度回调 (0-100)
 * @returns {Promise<string>} - 解析为图片URL的Promise
 */
export const uploadImage = async (file, onProgress = null) => {
  // 1. 前端校验
  const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
  if (!validTypes.includes(file.type)) {
    throw new Error('仅支持JPG, PNG, GIF或WebP格式的图片');
  }
  if (file.size > 5 * 1024 * 1024) { // 5MB限制
    throw new Error('图片大小不能超过5MB');
  }

  // 2. 创建FormData
  const formData = new FormData();
  formData.append('image', file); // 字段名需与后端约定
  // 可以附加其他参数,如上传目录
  formData.append('folder', 'article');

  try {
    // 3. 使用Fetch API发送请求,支持进度监控(需注意浏览器兼容性)
    const xhr = new XMLHttpRequest();
    const promise = new Promise((resolve, reject) => {
      xhr.upload.addEventListener('progress', (event) => {
        if (event.lengthComputable && onProgress) {
          const percent = Math.round((event.loaded / event.total) * 100);
          onProgress(percent);
        }
      });

      xhr.addEventListener('load', () => {
        if (xhr.status >= 200 && xhr.status < 300) {
          const response = JSON.parse(xhr.responseText);
          // 假设后端返回 { code: 0, data: { url: '...' } }
          if (response.code === 0) {
            resolve(response.data.url);
          } else {
            reject(new Error(response.message || '上传失败'));
          }
        } else {
          reject(new Error(`上传失败,状态码: ${xhr.status}`));
        }
      });

      xhr.addEventListener('error', () => reject(new Error('网络错误')));
      xhr.addEventListener('abort', () => reject(new Error('上传已取消')));
    });

    xhr.open('POST', '/api/upload/image'); // 你的上传接口
    // 记得设置认证头,如果需要的话
    // xhr.setRequestHeader('Authorization', `Bearer ${token}`);
    xhr.send(formData);

    return await promise;
  } catch (error) {
    console.error('图片上传失败:', error);
    throw error; // 将错误抛给调用者处理
  }
};

接下来,在Quill的工具栏处理器中集成这个上传函数:

// 在Vue组件或React组件的初始化方法中
function initializeQuill() {
  const quill = new Quill(editorContainer, {
    theme: 'snow',
    modules: {
      toolbar: {
        container: toolbarOptions,
        handlers: {
          image: function () {
            // 保存当前Quill实例和选区
            const quillInstance = this.quill;
            const range = quillInstance.getSelection();
            if (!range) {
              // 如果没有焦点,先聚焦到编辑器末尾
              quillInstance.focus();
              quillInstance.setSelection(quillInstance.getLength());
            }

            // 创建隐藏的文件输入框
            const input = document.createElement('input');
            input.setAttribute('type', 'file');
            input.setAttribute('accept', 'image/*');
            input.style.display = 'none';

            input.onchange = async () => {
              if (!input.files || input.files.length === 0) return;
              const file = input.files[0];

              try {
                // 可选:在插入点先插入一个占位符或loading图
                const placeholderId = `img-loading-${Date.now()}`;
                quillInstance.insertEmbed(range.index, 'image', '/loading-placeholder.gif');
                quillInstance.insertText(range.index + 1, '\n'); // 保持格式
                quillInstance.setSelection(range.index + 2); // 移动光标

                // 执行上传
                const imageUrl = await uploadImage(file, (percent) => {
                  console.log(`上传进度: ${percent}%`);
                  // 这里可以更新占位符,显示进度条(需要更复杂的DOM操作)
                });

                // 上传成功,删除占位符,插入真实图片
                quillInstance.deleteText(range.index, 2); // 删除占位符和换行
                quillInstance.insertEmbed(range.index, 'image', imageUrl);
                quillInstance.insertText(range.index + 1, '\n');
                quillInstance.setSelection(range.index + 2);

              } catch (error) {
                console.error('上传处理失败:', error);
                // 删除占位符
                quillInstance.deleteText(range.index, 2);
                // 可以插入错误提示文本
                quillInstance.insertText(range.index, `[图片上传失败: ${error.message}]`, 'code');
                alert(`图片上传失败: ${error.message}`);
              } finally {
                // 清理DOM
                document.body.removeChild(input);
              }
            };

            // 触发文件选择
            document.body.appendChild(input);
            input.click();
          }
        }
      }
    }
  });
}

这个方案提供了良好的用户体验和错误恢复能力。在实际项目中,你可能会使用UI组件库的Modal和Progress组件来替代原生的alert和console.log,使其更美观。

2.3 内容安全与XSS防御

富文本编辑器是XSS攻击的潜在入口。Quill的Delta格式本身是安全的,但当你允许HTML粘贴或直接设置HTML内容时,风险就出现了。

策略一:使用Quill的格式化白名单(formats配置) 如上文所示,严格定义formats数组,只允许必要的格式。这能阻止未被允许的HTML标签和属性。

策略二:粘贴时净化HTML Quill的clipboard模块可以添加matchers来净化粘贴的内容。推荐使用成熟的库如DOMPurify。

import DOMPurify from 'dompurify';

// 在Quill配置中
modules: {
  clipboard: {
    matchers: [
      ['*', customMatcher]
    ]
  }
}

// 自定义匹配器,净化所有HTML节点
function customMatcher(node, delta) {
  if (node.nodeType === Node.ELEMENT_NODE) {
    // 使用DOMPurify进行净化
    const cleanHTML = DOMPurify.sanitize(node.innerHTML, {
      ALLOWED_TAGS: ['b', 'i', 'u', 's', 'a', 'p', 'br', 'ul', 'ol', 'li', 'h1', 'h2', 'h3'], // 自定义允许的标签
      ALLOWED_ATTR: ['href', 'target'] // 自定义允许的属性
    });
    // 将净化后的HTML转换回Delta(这里需要实现,或使用Quill的clipboard转换)
    // 这是一个简化示例,实际更复杂
    const tempQuill = new Quill(document.createElement('div'));
    tempQuill.clipboard.dangerouslyPasteHTML(cleanHTML);
    return tempQuill.getContents();
  }
  return delta;
}

策略三:输出前的最终净化 在将编辑器内容发送到服务器或渲染到页面其他部分前,始终进行最后一次净化。

// 获取HTML内容
const rawHTML = quill.root.innerHTML;
// 净化
const safeHTML = DOMPurify.sanitize(rawHTML, sanitizeConfig);
// 现在可以安全地使用safeHTML了

通过组合以上策略,可以极大提升编辑器的内容安全性。

3. 与Vue 3的深度集成:响应式、组件化与状态管理

在Vue 3中,我们追求的是响应式、组合式API和良好的TypeScript支持。虽然vue-quill-editor提供了开箱即用的组件,但理解其原理并实现自定义集成能带来更大的灵活性。

3.1 创建可复用的Quill编辑器组件

我们将创建一个使用Composition API的RichTextEditor.vue组件,它支持v-model双向绑定,并暴露编辑器实例以供父组件调用。

<!-- RichTextEditor.vue -->
<template>
  <div class="rich-text-editor">
    <!-- 工具栏可以分离,实现更灵活的布局 -->
    <div v-if="showToolbar" ref="toolbarRef" class="ql-toolbar-container">
      <!-- 工具栏内容将通过Quill动态生成,或你也可以在这里定义静态工具栏HTML -->
    </div>
    <!-- 编辑器容器 -->
    <div ref="editorRef" :class="editorClass"></div>
  </div>
</template>

<script setup>
import { ref, watch, onMounted, onBeforeUnmount, nextTick, computed } from 'vue';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { uploadImage } from '@/utils/imageUploader'; // 导入之前写的上传工具

const props = defineProps({
  modelValue: { type: String, default: '' },
  placeholder: { type: String, default: '请输入内容...' },
  readOnly: { type: Boolean, default: false },
  showToolbar: { type: Boolean, default: true },
  // 允许传入自定义的Quill配置,覆盖默认值
  editorOptions: { type: Object, default: () => ({}) }
});

const emit = defineEmits(['update:modelValue', 'change', 'focus', 'blur', 'ready']);

const editorRef = ref(null);
const toolbarRef = ref(null);
let quillInstance = null;

// 合并默认配置和用户配置
const defaultOptions = {
  theme: 'snow',
  placeholder: props.placeholder,
  readOnly: props.readOnly,
  modules: {
    toolbar: {
      container: toolbarRef, // 将工具栏指向我们的DOM元素
      handlers: {
        image: customImageHandler
      }
    },
    history: {
      delay: 1000,
      maxStack: 50,
      userOnly: true
    }
  },
  formats: ['bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block',
            'header', 'list', 'bullet', 'indent', 'link', 'image', 'color', 'background', 'align']
};

const finalOptions = computed(() => ({
  ...defaultOptions,
  ...props.editorOptions,
  modules: {
    ...defaultOptions.modules,
    ...(props.editorOptions.modules || {})
  }
}));

// 自定义图片处理器
async function customImageHandler() {
  if (!quillInstance) return;
  const range = quillInstance.getSelection();
  const input = document.createElement('input');
  input.type = 'file';
  input.accept = 'image/*';
  input.onchange = async () => {
    if (input.files?.[0]) {
      try {
        const url = await uploadImage(input.files[0]);
        quillInstance.insertEmbed(range.index, 'image', url);
        quillInstance.setSelection(range.index + 1);
      } catch (err) {
        console.error('图片插入失败:', err);
        // 触发一个自定义事件,让父组件可以处理错误
        emit('image-upload-error', err);
      }
    }
  };
  input.click();
}

// 初始化Quill
onMounted(async () => {
  await nextTick(); // 等待DOM渲染
  if (!editorRef.value) return;

  quillInstance = new Quill(editorRef.value, finalOptions.value);

  // 设置初始内容
  if (props.modelValue) {
    // 使用dangerouslyPasteHTML需要确保内容安全,或者使用setContents处理Delta
    quillInstance.clipboard.dangerouslyPasteHTML(props.modelValue);
  }

  // 监听内容变化,更新v-model
  quillInstance.on('text-change', () => {
    const html = editorRef.value.querySelector('.ql-editor').innerHTML;
    // 避免触发无限循环,判断内容是否真的改变了
    if (html !== props.modelValue) {
      emit('update:modelValue', html);
      emit('change', html);
    }
  });

  // 监听焦点事件
  quillInstance.on('selection-change', (range) => {
    if (range) {
      emit('focus', quillInstance);
    } else {
      emit('blur', quillInstance);
    }
  });

  emit('ready', quillInstance);
});

// 监听只读状态变化
watch(() => props.readOnly, (newVal) => {
  if (quillInstance) {
    quillInstance.enable(!newVal);
  }
});

// 监听外部modelValue变化(例如表单重置)
watch(() => props.modelValue, (newVal) => {
  if (quillInstance && newVal !== quillInstance.root.innerHTML) {
    quillInstance.clipboard.dangerouslyPasteHTML(newVal);
  }
}, { deep: true });

// 组件销毁时清理
onBeforeUnmount(() => {
  if (quillInstance) {
    quillInstance.off('text-change');
    quillInstance.off('selection-change');
    // 可以调用quillInstance = null; 但Quill实例会由GC回收
  }
});

// 暴露编辑器实例给父组件(通过模板ref)
defineExpose({
  quill: quillInstance,
  getHTML: () => quillInstance ? quillInstance.root.innerHTML : '',
  getDelta: () => quillInstance ? quillInstance.getContents() : null,
  clear: () => {
    if (quillInstance) {
      quillInstance.setText('');
    }
  }
});
</script>

<style scoped>
.rich-text-editor {
  border: 1px solid #ccc;
  border-radius: 4px;
}
.ql-toolbar-container {
  border-bottom: 1px solid #ccc;
}
/* 覆盖Quill默认样式,使其适应组件 */
:deep(.ql-container) {
  font-family: inherit;
  font-size: 16px;
  min-height: 200px;
  max-height: 500px;
  overflow-y: auto;
}
</style>

现在,你可以在父组件中像使用原生表单组件一样使用它:

<template>
  <form @submit.prevent="handleSubmit">
    <RichTextEditor
      v-model="articleContent"
      placeholder="撰写你的文章..."
      :read-only="isPreviewMode"
      @ready="onEditorReady"
      @image-upload-error="handleImageError"
    />
    <button type="submit">发布</button>
  </form>
</template>

<script setup>
import { ref } from 'vue';
import RichTextEditor from './RichTextEditor.vue';

const articleContent = ref('<p>初始内容</p>');
const isPreviewMode = ref(false);
const editorInstance = ref(null);

function onEditorReady(quill) {
  editorInstance.value = quill;
  console.log('编辑器已就绪,实例:', quill);
}

function handleImageError(error) {
  // 使用UI库通知用户
  console.error('图片上传错误:', error);
}

function handleSubmit() {
  console.log('提交的内容:', articleContent.value);
  // 发送到API...
}
</script>

3.2 与Pinia/Vuex状态管理结合

在大型应用中,编辑器状态(如草稿自动保存)可能需要纳入全局状态管理。这里的关键是防抖和序列化。

// stores/editorStore.js (使用Pinia示例)
import { defineStore } from 'pinia';
import { debounce } from 'lodash-es';

export const useEditorStore = defineStore('editor', {
  state: () => ({
    currentDraft: null,
    autoSaveStatus: 'idle', // 'idle', 'saving', 'success', 'error'
  }),
  actions: {
    // 防抖的自动保存
    autoSave: debounce(async function(content) {
      this.autoSaveStatus = 'saving';
      try {
        await api.saveDraft({ content });
        this.autoSaveStatus = 'success';
        setTimeout(() => { this.autoSaveStatus = 'idle'; }, 2000);
      } catch (error) {
        this.autoSaveStatus = 'error';
        console.error('自动保存失败:', error);
      }
    }, 3000), // 3秒防抖

    // 从Delta格式保存,便于后续差异比较或协同编辑
    saveDelta(delta) {
      // Delta是JSON,可以直接存储
      localStorage.setItem('article-delta', JSON.stringify(delta));
    }
  }
});

在编辑器组件中监听变化并触发自动保存:

// 在RichTextEditor.vue的text-change事件处理中
import { useEditorStore } from '@/stores/editorStore';
const editorStore = useEditorStore();

quillInstance.on('text-change', (delta, oldDelta, source) => {
  // ... 更新v-model
  if (source === 'user') { // 只保存用户输入,避免程序设置内容触发保存
    const html = getHTML();
    editorStore.autoSave(html);
    // 也可以保存Delta
    // editorStore.saveDelta(quillInstance.getContents());
  }
});

这种模式实现了编辑器内容与全局状态的松耦合,保持了组件的可复用性。

4. 在React生态中的实践:Hooks、受控组件与性能优化

React的函数式组件和Hooks为集成Quill带来了不同的模式。核心思想是将Quill实例封装成一个受控组件,并利用Hooks管理其生命周期和状态。

4.1 使用Hooks封装Quill编辑器

我们将创建一个useQuill自定义Hook,它负责Quill实例的创建、销毁和事件绑定,返回编辑器实例和状态。

// hooks/useQuill.js
import { useEffect, useRef, useState, useCallback } from 'react';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';

export default function useQuill({ placeholder, readOnly, toolbarConfig, onTextChange }) {
  const editorRef = useRef(null);
  const quillInstanceRef = useRef(null);
  const [isReady, setIsReady] = useState(false);

  // 初始化Quill
  useEffect(() => {
    if (!editorRef.current || quillInstanceRef.current) return;

    const options = {
      theme: 'snow',
      placeholder,
      readOnly,
      modules: {
        toolbar: toolbarConfig || [
          ['bold', 'italic', 'underline'],
          ['link', 'image', 'code-block']
        ]
      }
    };

    const quill = new Quill(editorRef.current, options);
    quillInstanceRef.current = quill;
    setIsReady(true);

    // 清理函数
    return () => {
      if (quillInstanceRef.current) {
        // 移除所有事件监听器
        quillInstanceRef.current.off('text-change');
        quillInstanceRef.current = null;
      }
    };
  }, [placeholder, readOnly, toolbarConfig]); // 依赖项变化会重建实例

  // 绑定文本变化事件
  useEffect(() => {
    if (!quillInstanceRef.current || !onTextChange) return;

    const handler = () => {
      const html = editorRef.current?.querySelector('.ql-editor').innerHTML;
      onTextChange(html);
    };
    quillInstanceRef.current.on('text-change', handler);

    return () => {
      quillInstanceRef.current?.off('text-change', handler);
    };
  }, [onTextChange, isReady]); // 注意:当onTextChange变化时重新绑定

  // 暴露给组件的API
  const setContent = useCallback((html) => {
    if (quillInstanceRef.current && html !== undefined) {
      quillInstanceRef.current.clipboard.dangerouslyPasteHTML(html);
    }
  }, []);

  const getDelta = useCallback(() => {
    return quillInstanceRef.current?.getContents() || null;
  }, []);

  const insertImage = useCallback(async (file) => {
    if (!quillInstanceRef.current) return;
    const range = quillInstanceRef.current.getSelection();
    // ... 调用上传逻辑,同Vue示例
  }, []);

  return {
    editorRef,
    quillInstance: quillInstanceRef.current,
    isReady,
    setContent,
    getDelta,
    insertImage
  };
}

现在,我们可以用这个Hook构建一个React编辑器组件:

// components/ReactQuillEditor.jsx
import React, { useState, useEffect } from 'react';
import useQuill from '../hooks/useQuill';
import './ReactQuillEditor.css';

const ReactQuillEditor = ({ value, onChange, placeholder, readOnly }) => {
  const [internalValue, setInternalValue] = useState(value || '');

  const { editorRef, setContent, isReady } = useQuill({
    placeholder,
    readOnly,
    onTextChange: (html) => {
      setInternalValue(html);
      if (onChange) {
        onChange(html);
      }
    }
  });

  // 当外部value变化时(如表单重置),更新编辑器内容
  useEffect(() => {
    if (isReady && value !== internalValue) {
      setContent(value);
      setInternalValue(value);
    }
  }, [value, isReady, setContent, internalValue]);

  return (
    <div className="react-quill-editor">
      <div ref={editorRef} className="quill-editor-container" />
      {/* 可以在这里添加自定义的状态提示,如字数统计 */}
      <div className="editor-status">
        字数: {internalValue.replace(/<[^>]*>/g, '').length}
      </div>
    </div>
  );
};

export default ReactQuillEditor;

4.2 性能优化与避免不必要的重渲染

在React中,富文本编辑器是一个重组件,需要小心处理其渲染性能。

优化点1:避免Quill实例重建 useQuill Hook的初始化useEffect依赖项应尽可能稳定。避免将内联对象或函数作为配置传入。

// 不好:每次渲染都会创建新的toolbarConfig对象
<ReactQuillEditor toolbarConfig={['bold', 'italic']} />

// 好:使用useMemo或常量
const toolbarConfig = useMemo(() => ['bold', 'italic'], []);
<ReactQuillEditor toolbarConfig={toolbarConfig} />

优化点2:防抖处理onChange 如果父组件在onChange中执行重操作(如自动保存到服务器),必须防抖。

// 在父组件中
import { debounce } from 'lodash';

const handleChange = useCallback(
  debounce((html) => {
    saveToBackend(html);
  }, 1000),
  []
);

<ReactQuillEditor onChange={handleChange} />

优化点3:使用React.memo包裹组件 如果编辑器组件的props不经常变化,使用React.memo防止不必要的重渲染。

export default React.memo(ReactQuillEditor, (prevProps, nextProps) => {
  // 只有当value和readOnly变化时才重渲染
  return prevProps.value === nextProps.value && 
         prevProps.readOnly === nextProps.readOnly;
});

4.3 与React状态管理库集成

在Redux或MobX中管理编辑器状态,模式与Vue类似。关键在于将编辑器视为一个“黑盒”,只通过其API获取和设置内容,而将业务逻辑(如保存、验证)放在状态管理中。

// 使用Redux Toolkit示例
import { useDispatch, useSelector } from 'react-redux';
import { autoSaveArticle, setContent } from './editorSlice';

const EditorContainer = () => {
  const dispatch = useDispatch();
  const content = useSelector(state => state.editor.content);
  const isSaving = useSelector(state => state.editor.isSaving);

  const handleChange = useCallback(
    debounce((html) => {
      dispatch(setContent(html));
      dispatch(autoSaveArticle());
    }, 1500),
    [dispatch]
  );

  return (
    <div>
      <ReactQuillEditor value={content} onChange={handleChange} />
      {isSaving && <div>正在保存草稿...</div>}
    </div>
  );
};

5. 高级功能拓展:表格、协同编辑与生态模块

当基础功能满足后,你可能会需要更高级的特性。Quill的模块化架构让这些成为可能。

5.1 集成表格支持

Quill本身不支持表格,但社区有优秀的模块如quill-better-table。集成步骤如下:

npm install quill-better-table --save
// 在初始化Quill之前注册模块
import Quill from 'quill';
import { QuillBetterTable } from 'quill-better-table';

Quill.register('modules/better-table', QuillBetterTable);

// 在配置中启用
const quill = new Quill('#editor', {
  modules: {
    toolbar: {
      container: [
        // ... 其他按钮
        ['insertTable'] // 添加表格按钮
      ],
      handlers: {
        'insertTable': function() {
          this.quill.getModule('better-table').insertTable(3, 3); // 插入3x3表格
        }
      }
    },
    'better-table': true // 启用表格模块
  }
});

5.2 探索Quill生态模块

Quill有一个丰富的生态系统,你可以在Awesome Quill找到大量第三方模块。

模块名称功能安装命令
quill-image-resize图片拖拽调整大小npm i quill-image-resize
quill-emoji表情符号选择器npm i quill-emoji
quill-mention@提及自动完成npm i quill-mention
quill-markdown-shortcutsMarkdown快捷键支持npm i quill-markdown-shortcuts

集成示例(以quill-mention为例):

import QuillMention from 'quill-mention';

Quill.register('modules/mention', QuillMention);

const quill = new Quill('#editor', {
  modules: {
    mention: {
      allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/,
      mentionDenotationChars: ["@", "#"],
      source: async (searchTerm, renderList) => {
        // 从API获取匹配的用户列表
        const users = await fetchUsers(searchTerm);
        renderList(users, searchTerm);
      },
      renderItem: (item) => {
        return `<div>${item.name} (${item.email})</div>`;
      }
    }
  }
});

5.3 向协同编辑迈进

如果你需要实现类似Google Docs的实时协同编辑,Quill的Delta格式是绝佳的基础。你可以使用Operational Transformation (OT) 或 Conflict-free Replicated Data Types (CRDT) 算法。虽然实现完整的协同编辑非常复杂,但思路是:

  1. 每个操作都转换为Delta。
  2. 通过WebSocket将Delta发送到服务器。
  3. 服务器使用OT算法转换并广播给其他客户端。
  4. 客户端应用转换后的Delta。

已有一些库在此基础之上构建,如ShareDB和Yjs。Yjs甚至提供了Quill的绑定器y-quill。

npm install yjs y-quill
import * as Y from 'yjs';
import { QuillBinding } from 'y-quill';
import Quill from 'quill';

const ydoc = new Y.Doc();
const ytext = ydoc.getText('quill');
const provider = new WebrtcProvider('your-room-name', ydoc); // 使用WebRTC通信

const quill = new Quill('#editor');
const binding = new QuillBinding(ytext, quill);

这会在所有连接的客户端之间建立实时同步,无需中心服务器转发(通过WebRTC直连)。

从简单的文本格式化到复杂的实时协作,Quill提供了一个强大而灵活的基础。关键在于理解其模块化哲学:从核心开始,按需添加。无论是与Vue的响应式系统深度结合,还是在React的Hooks范式下封装,抑或是探索表格、协同编辑等高级功能,Quill都能提供相应的扩展点。

在实际项目中,我建议从最小功能集开始,逐步添加模块。同时,务必重视内容安全,对用户输入进行严格的净化处理。编辑器是一个复杂组件,其稳定性和性能需要持续关注和优化,但通过合理的架构设计,它完全可以成为你应用中最强大、最受用户喜爱的功能之一。

react-quill的使用 立即下载

相关推荐

Cursor集成Claude 4.6免费版:本地IDE合规调用指南

大语言模型编程助手(LLM Copilot)已成为现代开发标配,其核心能力依赖于上下文理解、代码推理与结构化输出等关键技术原理。Anthropic官方提供的Claude系列模型,尤其Claude 4.6,凭借200K超长上下文、tool use函数调用和严格可控的API设计,在工程实践中展现出显著技术价值。该能力可无缝嵌入本地IDE环境,支撑代码解释、跨文件分析、单元测试生成等高频开发场景。本文聚焦Cursor这一开源可审计IDE,详解如何合规复用Anthropic免费tier接口(/v1/messages

weixin_34095889的博客 537

react项目封装富文本编辑器

一、富文本组件封装 1、安装依赖 yarn add react-quill 富文本依赖 yarn add quill-emoji 表情依赖 2、引入依赖 // react-quill 富文本依赖样式 import ReactQuill, { Quill } from 'react-quill'; import 'react-quill/dist/quill.snow.css'; // quill-emoji 表情依赖样式 import quillEmoji from 'quill-emoji';

情非得已小猿猿 2539

REALTEK PKE8710ECF开发板入手试玩(一)

REALTEK PKE8710ECF开发板入手试玩

lbqhope的专栏 691

富文本初体验之react-quill——2022-07-19

react-quill的基本使用步骤。

m0_51400566的博客 4233

百度飞桨+文心大模型实战:生成式AI工程师认证备考全攻略(含真题解析)

本文详细解析百度飞桨与文心大模型结合的生成式AI工程师认证备考策略,涵盖认证体系、学习路径、核心技能突破及真题分析。帮助开发者掌握大模型原理、飞桨平台操作和文心大模型应用,高效通过工信部与百度联合认证考试。

loss4bartender的博客 682

ST-LINK调试STM32H743必看:CubeMX工程中缺失的这行代码导致程序跑飞

本文深入剖析了使用ST-LINK调试STM32H743时,因CubeMX工程中缺失电源电压档位配置代码而导致程序跑飞或出现“Can not access memory”错误的问题。文章解释了H7系列电源与时钟耦合的核心原理,提供了具体的代码修复方案和系统化的调试排查思路,帮助开发者从根本上解决这一常见调试困境。

weixin_29191669的博客 281

富文本编辑器 之 react-quill 踩过的坑~

react-quill 功能之 图片粘贴内存过大处理、图片缩放、自定义字体样式、自定义行高、做了很久的react-quill,表示不想再碰了,但是后续还要添加自定义插入表格的功能。。

qq_44416706的博客 5120

工业缺陷标注要人命?我从零搭了一套自动标注系统,标注效率直接提升10倍

你是不是也深陷工业缺陷标注的无尽内耗?这不是个例,工业视觉项目落地,80%的时间和成本,都耗在了数据标注环节。普通的图像标注(猫狗分类、人脸识别),标注门槛低、规则简单、一致性要求宽松;而工业缺陷标注,是CV领域公认的地狱级难度:缺陷尺寸小到几像素、种类繁杂且非标、行业专业性极强、标注精度要求到像素级、正负样本极度不均衡,人工标注不仅效率低、成本高,更直接决定了模型的最终上限。我在工业视觉行业摸爬滚打了6年,踩遍了人工标注的所有坑,最终从零搭建了一套工业缺陷专用的闭环自动标注系统,把原来一张图30分钟的标注

专注于Python爬虫开发,分享爬虫技巧、项目实战与反爬经验,使用Scrapy、BeautifulSoup等工具,解决数据抓取难题。 202

推荐一款Markdown富文本编辑器 React-quill,以及修改React-quill默认配置,支持React !

支持react的富文本编辑器,react-quill 。

萧萧的博客 1万+

Unity必学:Protobuf-net预编译实战

本文深入解析Unity项目使用Protobuf-net Precompile的必要性。作为高效跨平台序列化方案,Protobuf-net解决了Unity工程在网络同步、存档系统中的性能瓶颈,特别是针对iOS等AOT平台的兼容问题。通过预编译模式,将运行时反射转为静态代码生成,显著提升移动端性能表现,支持协议平滑升级,已成为大厂项目的标配方案。文章从原理、实践到多平台适配,全面展示了这一"序列化发动机"如何支撑现代Unity项目的数据传输需求。

本博客聚焦游戏开发技巧、创业实战心得及大学热门课程干货。这里既有技术深度,也有思维广度,无论你是开发者、创业者还是高校学子,都能在这里找到适合自己的成长之路! 495

【OpenClaw从入门到精通】第41篇:2026年4月最新版——从零开始搭建你的第一个安全AI助理(保姆级实战教程)

2026年3月CNCERT联合发布《OpenClaw安全使用实践指南》后,安全部署成为OpenClaw使用的核心前提。本文针对新手及进阶用户,基于官方安全指引,提供三套实战部署方案:阿里云一键部署(新手首选)、Docker容器隔离部署(进阶推荐)、本地安全安装(测试专用),并详解阿里云百炼Coding Plan API接入流程。全文涵盖环境准备、分步实操、安全加固、问题排查等全流程,所有命令可直接复制执行,无需依赖外部代码库。通过本文,读者可零基础搭建安全隔离的OpenClaw AI助理,兼顾实用性与安全性

专注于人工智能、软件开发、工控自动化、工厂数字化及智能化等领域,希望和大家共同进步! 2199

ReactQuill安装与配置完全指南

ReactQuill 是一个基于 React 的富文本编辑器组件,它封装了 Quill 编辑器以更好地融入 React 生态。此项目广泛应用于需要在 React 应用中集成强大而灵活的文本编辑功能的场景。主要编程语言是 JavaScript,并且全面支持 ES6 语法以及 TypeScript。 ## 项目使用的关键技术和框架 - **React**: 作为基础框架,负责构建用户界面。 - *...

gitblog_07215的博客 987

在STM32F103RBT6微控制器上利用USB虚拟Com端口技术,高效实现ADC数据传输:详细操作步骤与示例代码解析

以上就是在STM32F103RBT6上使用USB虚拟Com端口实现ADC数据传输的全部内容。虽然过程中有很多步骤和细节需要注意,但只要按照步骤来,理解每一段代码的作用,就完全可以掌握这个过程。在实际应用中,我们可能需要根据实际需要修改一些代码,如调整ADC的参数,改变数据的处理方式等。但是,只要理解了这个过程,我们就可以很灵活地进行修改。希望这篇文章对你有所帮助。如果你有任何疑问,欢迎在评论区留言,我会尽我所能进行解答。

qq_38334677的博客 1168

react中quill插件的使用

##react中quill的使用 近期使用由于项目需要一款富文本编辑框,本来打算试着使用drift.js写一款富文本编辑器。但由于时间比较紧急最后选择引用第三方插件,在众多富文本插件中初步选择了Quill和CKEditor5,最后选择了简单便捷的Quill。下面是目录: 引入插件 配置toolbar toolbar进一步自定义字体 toolbar自定义上传图片 引入插件: 通过npm inst...

qq_31290307的博客 6763

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

ReactQuill编辑器定制完全指南:从基础配置到高级优化

你是否在使用ReactQuill时遇到过这些问题:编辑器字体单一、行高不协调、工具栏功能不够用?别担心,这篇指南将带你从零开始,打造一个专业级的定制化编辑器。 ## 编辑器配置的痛点与解决方案 ### 常见问题分析 - **字体单调**:默认字体库有限,难以满足品牌需求 - **排版混乱**:行高不一致,影响阅读体验 - **功能缺失**:缺少行高控制、自定义格式等高级功能 - **样式冲

gitblog_00047的博客 1114

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

react-quill插件使用

三.代码封装一个插件(里面有问题的记录)二.本人安装的插件版本。

qq_39634880的博客 5816

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

从 0 到 1:ReactQuill 自定义工具栏完全指南

你是否曾因富文本编辑器工具栏功能冗余而烦恼?是否需要为企业级应用定制符合品牌调性的编辑体验?ReactQuill(基于 Quill 编辑器的 React 组件)提供了灵活的工具栏定制能力,但官方文档对高级配置的阐述较为零散。本文将系统讲解从基础配置到高级功能的全流程,帮助你构建既美观又实用的编辑器工具栏。 读完本文你将掌握: - 3 种工具栏配置方案的优缺点对比 - 自定义按钮与下拉菜单的实现模...

gitblog_00790的博客 1441

React--》从零开始搭建一个文章后台管理系统

本项目讲解的是一个简单的文章后台管理系统,系统的功能很简单,看完本篇文章你将了解学习到的知识有很多,而且博主也将源码开源出来,希望可以帮助大家。

ztK63LrD的博客 6877

react18中使用react-quill富文本编辑器

react18中使用react-quill富文本编辑器

jsmeng626的博客 2483

React中:富文本编辑器(react-quill),自定义上传图片到七牛云

React中:富文本编辑器(react-quill),自定义上传图片到七牛云

前端报刊 2148

富文本编辑器Quill的简单React封装

最近要在React项目中实现富文本编辑器,我在基于不同插件实现多种版本之后,体会到了Quill的强大,决定在项目中使用Quill实现富文本编辑器功能。 大多数富文本编辑器都有所见即所得的(指不能进行扩展)的问题,这意味着我们很难或者不可能对其进行自定义满足一些特定的需求。Quill的强大之处在于通过将自身组织成模块,并提供强大的API来构建额外的模块解决扩展的问题,即所有能看到的,不能看到的功能...

smk108的博客 1万+

React-Quill中的图片上传及显示

React-Quill中的图片处理

song279811799的博客 1万+

React中使用富文本编辑器Quill,支持粘贴图片

富文本编辑器Quill,粘贴图片组件

song279811799的博客 1万+

React Quill Demo 预览md 预览html

React-Quill 什么都好,就是有两点我觉得自定义性不好,一是样式难调,图片上传内置式转化成base64的,如果要上传文件形式上传图片就不行。但是学习了就要记录万一以后就用得上呢。 环境搭建:这里有环境搭建文章 Demo地址:github地址 Demo预览: ...

qq_33683097的博客 2204

vue中使用vue-quill-editor富文本编辑器,自定义toolbar修改工具栏options

基于webpack和vue 一、npm 安装 vue-quill-editor 二、在main.js中引入 import VueQuillEditor from 'vue-quill-editor' // require styles 引入样式 import 'quill/dist/quill.core.css' import 'quill/dist/quill.snow.css' i...

div_ma的博客 12万+

vue实战021:Vue-Quill-Editor富文本编辑器使用

博客站对多的就是文章了,要想写出好看的文章格式离不开功能强大的富文本编辑器,今天写一个富文本编辑页来完善博客中的文章编写功能,富文本有很多插件可以使用,这里我选择了vue-quill-editor,这是一款基于Quill、适用于Vue2的富文本编辑器。 下载安装Vue-Quill-Editor vue-quill-editor插件安装,这个很简单直接在项目中打开cmd,运行命令:npm i...

kevinfan的博客 4341
上一篇: QT5.14.1开发避坑实录:如何正确配置VS2017的调试符号路径(解决Qt5cored.pdb报错)
下一篇: uniapp安卓APP热更新避坑指南:从接口设计到版本控制全流程
糖果HTML
博客等级 码龄3年 76粉丝 1357原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值