vue-quill-editor自定义 blot 实现:扩展文本格式类型

【免费下载链接】vue-quill-editor @quilljs editor component for @vuejs(2) 【免费下载链接】vue-quill-editor 项目地址: https://gitcode.com/gh_mirrors/vu/vue-quill-editor

引言

在富文本编辑器(Rich Text Editor)开发中,开发者经常需要扩展基础文本格式以满足特定业务需求。尽管 vue-quill-editor 基于 Quill 提供了丰富的默认格式,但面对如"添加自定义高亮样式"、"实现带编号的引用块"等场景时,原生功能往往无法满足需求。本文将系统讲解如何通过自定义 Blot( blot 是 Quill 中用于表示文档结构的基本单元)扩展 vue-quill-editor 的文本格式类型,包含从基础概念到高级应用的完整实现方案。

核心概念解析

Quill 文档模型基础

Quill 采用基于 Delta 和 Blot 的分层架构:

  • Delta:用于描述文档变更的 JSON 格式,类似操作日志
  • Blot:文档的基本构建块,映射 DOM 元素并处理格式逻辑

mermaid

Blot 类型与继承关系

Quill 提供三种基础 Blot 类型:

  1. InlineBlot:内联元素(如 <strong><em>
  2. BlockBlot:块级元素(如 <p><blockquote>
  3. EmbedBlot:嵌入式内容(如图片、视频)

自定义 Blot 需继承这些基础类型并实现必要方法。

实现步骤

1. 环境准备

确保项目中已正确安装 vue-quill-editor:

npm install vue-quill-editor --save
# 或使用国内镜像
cnpm install vue-quill-editor --save

全局引入(main.js):

import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'
import 'quill/dist/quill.snow.css' // 引入样式

Vue.use(VueQuillEditor)

2. 自定义 Inline Blot 示例:高亮文本

2.1 创建 HighlightBlot 类
import Quill from 'quill'

const Inline = Quill.import('blots/inline')

class HighlightBlot extends Inline {
  static create(value) {
    const node = super.create(value)
    // 设置自定义样式
    node.style.backgroundColor = value.color || '#ffff00' // 默认黄色
    node.style.padding = '0 2px'
    node.style.borderRadius = '2px'
    return node
  }

  static formats(node) {
    // 返回当前格式值,用于 Quill 的 format API
    return {
      color: node.style.backgroundColor
    }
  }

  format(name, value) {
    if (name === 'highlight' && value) {
      this.domNode.style.backgroundColor = value.color || '#ffff00'
    } else {
      super.format(name, value)
    }
  }
}

// 注册自定义 blot,'highlight' 为格式名称,对应 HTML 标签 <span>
HighlightBlot.blotName = 'highlight'
HighlightBlot.tagName = 'span'
HighlightBlot.className = 'ql-highlight'

Quill.register(HighlightBlot)
2.2 集成到 Vue 组件
<template>
  <div>
    <div class="toolbar">
      <button @click="applyHighlight">
        应用高亮
      </button>
    </div>
    <quill-editor 
      v-model="content"
      :options="editorOptions"
      @ready="onEditorReady"
    />
  </div>
</template>

<script>
import Quill from 'quill'
// 导入上面定义的 HighlightBlot
import './blots/highlight-blot.js'

export default {
  data() {
    return {
      content: '',
      editorOptions: {
        theme: 'snow',
        modules: {
          toolbar: [
            ['bold', 'italic', 'underline'],
            [{ 'header': [1, 2, 3, false] }],
            [{ 'color': [] }, { 'background': [] }],
            ['link', 'image']
          ]
        }
      },
      quill: null
    }
  },
  methods: {
    onEditorReady(quill) {
      this.quill = quill
    },
    applyHighlight() {
      if (this.quill) {
        const range = this.quill.getSelection()
        if (range) {
          // 应用自定义格式
          this.quill.formatText(
            range.index, 
            range.length, 
            'highlight', 
            { color: '#ffeb3b' } // 黄色高亮
          )
        }
      }
    }
  }
}
</script>

<style>
/* 自定义高亮样式 */
.ql-highlight {
  background-color: #ffeb3b;
  padding: 0 2px;
  border-radius: 2px;
}
</style>

3. 自定义 Block Blot 示例:带编号的引用块

3.1 创建 NumberedQuoteBlot 类
import Quill from 'quill'

const Block = Quill.import('blots/block')
const Container = Quill.import('blots/container')

// 创建容器 blot
class QuoteContainerBlot extends Container {
  static blotName = 'quote-container'
  static tagName = 'div'
  static className = 'ql-quote-container'
  
  constructor(domNode) {
    super(domNode)
    // 初始化计数器
    this.counter = 1
  }
  
  // 新增子元素时更新编号
  attach() {
    super.attach()
    this.updateNumbers()
  }
  
  // 移除子元素时更新编号
  detach() {
    super.detach()
    this.updateNumbers()
  }
  
  updateNumbers() {
    const children = this.children.filter(child => 
      child.statics.blotName === 'numbered-quote'
    )
    
    children.forEach((child, index) => {
      child.updateNumber(index + 1)
    })
  }
}

// 创建引用项 blot
class NumberedQuoteBlot extends Block {
  static blotName = 'numbered-quote'
  static tagName = 'div'
  static className = 'ql-numbered-quote'
  
  static create() {
    const node = super.create()
    // 创建编号元素
    const numberNode = document.createElement('span')
    numberNode.className = 'quote-number'
    node.appendChild(numberNode)
    
    // 创建内容容器
    const contentNode = document.createElement('span')
    contentNode.className = 'quote-content'
    node.appendChild(contentNode)
    
    return node
  }
  
  constructor(domNode) {
    super(domNode)
    this.numberNode = domNode.querySelector('.quote-number')
    this.contentNode = domNode.querySelector('.quote-content')
  }
  
  // 更新编号显示
  updateNumber(num) {
    this.numberNode.textContent = `${num}.`
  }
  
  // 重写格式方法
  format(name, value) {
    if (name === 'numbered-quote' && value) {
      // 处理自定义格式逻辑
    } else {
      super.format(name, value)
    }
  }
  
  // 重写内容获取方法
  getContent() {
    return this.contentNode.innerHTML
  }
  
  // 重写内容设置方法
  setContent(value) {
    this.contentNode.innerHTML = value
  }
}

Quill.register(QuoteContainerBlot)
Quill.register(NumberedQuoteBlot)
3.2 使用带编号的引用块
// 在编辑器就绪时添加自定义按钮处理
onEditorReady(quill) {
  this.quill = quill
  
  // 添加自定义格式按钮事件
  document.querySelector('.toolbar button').addEventListener('click', () => {
    const range = quill.getSelection()
    if (range) {
      // 检查是否已存在容器
      let container = quill.scroll.descendants(QuoteContainerBlot)[0]
      
      if (!container) {
        // 创建新容器
        const index = range.index
        const length = range.length
        const text = quill.getText(index, length)
        
        quill.deleteText(index, length)
        container = quill.insertEmbed(index, 'quote-container', true)
        quill.insertEmbed(index + 1, 'numbered-quote', true)
        quill.insertText(index + 2, text)
      } else {
        // 向现有容器添加引用项
        const index = quill.getSelection().index
        quill.insertEmbed(index, 'numbered-quote', true)
      }
    }
  })
}

高级应用:实现自定义格式工具栏

带配置面板的自定义格式

<template>
  <div>
    <div class="custom-toolbar">
      <div class="highlight-controls">
        <select v-model="highlightColor">
          <option value="#ffff00">黄色</option>
          <option value="#ff9800">橙色</option>
          <option value="#ff5722">红色</option>
        </select>
        <button @click="applyHighlight">应用</button>
      </div>
    </div>
    <quill-editor 
      v-model="content"
      :options="editorOptions"
    />
  </div>
</template>

<script>
export default {
  data() {
    return {
      content: '',
      highlightColor: '#ffff00',
      editorOptions: {
        // ... 配置与前面相同
      }
    }
  },
  methods: {
    applyHighlight() {
      if (this.quill) {
        const range = this.quill.getSelection()
        if (range && range.length > 0) {
          this.quill.formatText(
            range.index, 
            range.length, 
            'highlight', 
            { color: this.highlightColor }
          )
        }
      }
    }
  }
}
</script>

<style scoped>
.highlight-controls {
  display: flex;
  gap: 8px;
  margin-bottom: 16px;
  align-items: center;
}

.highlight-controls select,
.highlight-controls button {
  padding: 6px 12px;
  border-radius: 4px;
  border: 1px solid #ccc;
}
</style>

常见问题与解决方案

1. Blot 注册冲突

问题:注册自定义 Blot 时提示 "Blot already registered"
解决方案

// 检查是否已注册
if (!Quill.imports['highlight']) {
  Quill.register(HighlightBlot)
}

2. 格式序列化问题

问题:自定义格式在获取 HTML 内容时丢失
解决方案:重写 Blot 的 formats() 方法确保格式被正确序列化

static formats(node) {
  return {
    highlight: {
      color: node.style.backgroundColor,
      // 其他需要保存的格式属性
    }
  }
}

3. 工具栏集成问题

问题:自定义格式无法添加到内置工具栏
解决方案:扩展工具栏配置

editorOptions: {
  modules: {
    toolbar: {
      container: [
        // 现有工具栏配置
        [{ 'highlight': ['yellow', 'orange', 'red'] }]
      ],
      handlers: {
        'highlight': function(value) {
          if (value) {
            this.quill.format('highlight', { color: value })
          } else {
            this.quill.removeFormat(this.quill.getSelection())
          }
        }
      }
    }
  }
}

性能优化策略

1. 避免频繁 DOM 操作

// 不佳
for (let i = 0; i < 100; i++) {
  blot.updateNumber(i) // 每次调用都触发 DOM 更新
}

// 优化
blot.batchUpdate(() => {
  for (let i = 0; i < 100; i++) {
    blot.updateNumber(i) // 批量处理,只触发一次 DOM 更新
  }
})

2. 使用事件委托处理动态内容

// 在父容器上统一监听事件
this.quill.container.addEventListener('click', (e) => {
  if (e.target.closest('.ql-highlight')) {
    // 处理高亮元素点击
  } else if (e.target.closest('.ql-numbered-quote')) {
    // 处理引用块点击
  }
})

3. 实现 Blot 缓存机制

class CachedBlot extends Inline {
  static cache = new WeakMap()
  
  constructor(domNode) {
    super(domNode)
    if (!CachedBlot.cache.has(domNode)) {
      // 缓存DOM节点计算结果
      CachedBlot.cache.set(domNode, this.computeStyles(domNode))
    }
  }
  
  computeStyles(node) {
    // 复杂样式计算逻辑,只执行一次
    return {
      // 计算结果
    }
  }
}

总结与扩展

通过自定义 Blot,我们可以突破 vue-quill-editor 的原生功能限制,实现几乎任何文本格式需求。本文介绍的实现模式可扩展到更复杂的场景:

  1. 实现代码块语法高亮:继承 BlockBlot 并集成 Prism 等语法高亮库
  2. 创建交互式内容块:添加按钮、输入框等交互元素到自定义 Blot
  3. 实现数学公式编辑:结合 MathJax 或 KaTeX 创建公式编辑 Blot

自定义 Blot 的核心在于理解 Quill 的文档模型和 Blot 生命周期,通过合理的继承与方法重写,可以构建出既强大又高效的富文本编辑体验。建议开发者在实现过程中参考 Quill 官方文档并利用浏览器 DevTools 调试 Blot 的 DOM 结构与行为。

参考资料

  • Quill 官方文档 - https://quilljs.com/docs/
  • Quill GitHub 仓库 - https://github.com/quilljs/quill
  • vue-quill-editor GitHub 仓库 - https://github.com/surmon-china/vue-quill-editor

【免费下载链接】vue-quill-editor @quilljs editor component for @vuejs(2) 【免费下载链接】vue-quill-editor 项目地址: https://gitcode.com/gh_mirrors/vu/vue-quill-editor

更多推荐