Tiptap 深度教程(三):核心扩展全面指南

前言回顾

第二章 中,我们成功使用 StarterKit 快速搭建了一个功能完备且经过性能优化的富文本编辑器。StarterKit 确实是一个极佳的起点,它为我们提供了开箱即用的基础功能。

然而,Tiptap 的真正威力在于其精细的模块化设计和"按需引入"的架构哲学。就像搭建乐高积木一样,你可以精确选择需要的功能模块,避免不必要的代码冗余。

为什么要学习独立扩展?

在实际项目开发中,你可能会遇到以下场景:

  • 性能优化需求:项目对包体积有严格要求,需要去除不必要的功能
  • 功能定制需求:需要更精细地控制每个功能的具体行为
  • 扩展功能需求 :需要添加 StarterKit 中没有包含的高级功能
  • 品牌定制需求:需要深度定制编辑器的外观和交互方式

当遇到这些情况时,深入了解 Tiptap 丰富的独立扩展生态系统就变得至关重要。

本章学习目标

通过本章的学习,你将掌握:

  • 扩展管理技能:独立查找、安装和配置 Tiptap 的核心扩展
  • 富内容集成:为编辑器添加链接、图片、语法高亮代码块等多媒体内容
  • 用户体验优化:通过占位符、@提及等交互功能,显著提升编辑器的易用性
  • 模块化思维:培养按需选择功能模块的架构思维

从 StarterKit 到独立扩展:模块化的艺术

理解 StarterKit 的本质

StarterKit 本质上是一个预配置的扩展包,它将多个常用的扩展打包在一起,为开发者提供便利。想象一下,它就像是一个"精装修"的房子,基础设施齐全,拎包入住。

但在实际开发中,我们往往需要"毛坯房",这样可以:

  • 精确控制每一个功能模块
  • 根据实际需求进行个性化配置
  • 优化最终的代码包体积

独立扩展配置示例

让我们看看如何用独立扩展替代 StarterKit

jsx 复制代码
import { useEditor } from '@tiptap/react';

// 核心功能扩展(必需)
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import History from '@tiptap/extension-history';

// 格式化扩展(按需引入)
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import Heading from '@tiptap/extension-heading';

const editor = useEditor({
  extensions: [
    // 基础扩展 - 这些是编辑器运行的基石
    Document,        // 文档根节点
    Paragraph,       // 段落节点
    Text,           // 文本节点
    History,        // 撤销/重做功能
    
    // 功能扩展 - 根据需求配置
    Bold,           // 粗体
    Italic,         // 斜体
    Heading.configure({
      levels: [1, 2, 3], // 只启用 H1、H2、H3 标题
    }),
  ],
  content: '<p>开始编写内容...</p>',
});

这种方式的核心优势

1. 精确控制

jsx 复制代码
// 示例:限制标题层级
Heading.configure({
  levels: [1, 2, 3], // 只保留主要标题层级
})

// 示例:自定义历史记录深度
History.configure({
  depth: 50, // 设置撤销历史记录数量
})

2. 性能优化 如果你的应用不需要引用块(Blockquote)或水平分割线(HorizontalRule),完全可以不引入这些扩展,从而:

  • 减少 JavaScript 包体积
  • 降低内存占用
  • 提升加载速度

3. 渐进式增强 你可以从最基础的功能开始,然后根据用户反馈和业务需求,逐步添加新功能:

jsx 复制代码
// 第一版:基础功能
const basicExtensions = [Document, Paragraph, Text, History, Bold, Italic];

// 第二版:增加列表功能
const enhancedExtensions = [...basicExtensions, BulletList, OrderedList, ListItem];

// 第三版:增加多媒体支持
const richExtensions = [...enhancedExtensions, Image, Link];

理解了这种模式后,添加任何新功能都变得非常简单:找到它、安装它、将它添加到 extensions 数组中

核心扩展分类详解

接下来,我们将从 Tiptap 官方的扩展库中精选几个最具代表性的例子,按功能分类进行深入讲解。

1. 交互式标记扩展(Interactive Marks)

这类扩展为内联文本添加交互能力,让用户可以对选中的文字进行特殊处理。

核心代表:链接扩展(@tiptap/extension-link

链接扩展不仅能将文本标记为可点击的链接,还具备自动识别 URL、链接预览等高级功能。

安装方式:

bash 复制代码
npm install @tiptap/extension-link

基础配置:

jsx 复制代码
import Link from '@tiptap/extension-link';

const editor = useEditor({
  extensions: [
    // ...其他扩展
    Link.configure({
      openOnClick: true,      // 点击时在新标签页中打开链接
      autolink: true,         // 自动将 URL 字符串转换为链接
      linkOnPaste: true,      // 粘贴时自动转换链接
      HTMLAttributes: {
        class: 'custom-link',  // 为链接添加自定义样式类
      },
    }),
  ],
});

UI 集成实现: 在实际项目中,你需要为用户提供添加和编辑链接的交互界面:

jsx 复制代码
import { useCallback } from 'react';

const MenuBar = ({ editor }) => {
  // 添加链接的处理函数
  const addLink = useCallback(() => {
    const previousUrl = editor.getAttributes('link').href;
    const url = window.prompt('请输入链接地址:', previousUrl);

    // 用户取消操作
    if (url === null) return;
    
    // 用户输入空字符串,移除链接
    if (url === '') {
      editor.chain().focus().extendMarkRange('link').unsetLink().run();
      return;
    }

    // 设置链接
    editor.chain().focus().extendMarkRange('link').setLink({ href: url }).run();
  }, [editor]);

  return (
    <div className="menu-bar">
      <button 
        onClick={addLink} 
        className={editor.isActive('link') ? 'is-active' : ''}
        title="添加链接"
      >
        🔗 链接
      </button>
    </div>
  );
};

高级功能扩展:

jsx 复制代码
// 支持更多链接属性
Link.configure({
  protocols: ['http', 'https', 'mailto', 'tel'], // 支持的协议
  validate: href => /^https?:\/\//.test(href),    // 链接验证
  HTMLAttributes: {
    target: '_blank',        // 新窗口打开
    rel: 'noopener noreferrer', // 安全属性
  },
})

2. 富内容节点扩展(Rich Content Nodes)

这类扩展允许你在编辑器中插入更复杂的块级内容,如图片、视频、代码块等。

核心代表一:图片扩展(@tiptap/extension-image

图片扩展让编辑器支持插入和展示图片,是构建现代富文本编辑器的必备功能。

安装方式:

bash 复制代码
npm install @tiptap/extension-image

基础使用:

jsx 复制代码
import Image from '@tiptap/extension-image';

const editor = useEditor({
  extensions: [
    // ...其他扩展
    Image.configure({
      inline: true,           // 是否内联显示
      allowBase64: true,      // 是否允许 base64 图片
      HTMLAttributes: {
        class: 'custom-image',
      },
    }),
  ],
});

完整的图片插入功能:

jsx 复制代码
const MenuBar = ({ editor }) => {
  // 通过 URL 添加图片
  const addImage = useCallback(() => {
    const url = window.prompt('请输入图片地址:');
    if (url) {
      editor.chain().focus().setImage({ src: url }).run();
    }
  }, [editor]);

  // 通过文件上传添加图片
  const uploadImage = useCallback((event) => {
    const file = event.target.files[0];
    if (file) {
      const reader = new FileReader();
      reader.onload = (e) => {
        editor.chain().focus().setImage({ src: e.target.result }).run();
      };
      reader.readAsDataURL(file);
    }
  }, [editor]);

  return (
    <div className="menu-bar">
      <button onClick={addImage}>
        🖼️ 插入图片(URL)
      </button>
      <input 
        type="file" 
        accept="image/*" 
        onChange={uploadImage}
        style={{ display: 'none' }}
        id="image-upload"
      />
      <label htmlFor="image-upload" className="upload-button">
        📁 上传图片
      </label>
    </div>
  );
};

核心代表二:语法高亮代码块(@tiptap/extension-code-block-lowlight

这是一个展示 Tiptap 与第三方库深度集成能力的绝佳例子,使用 lowlight 库实现代码语法高亮。

安装方式:

bash 复制代码
npm install @tiptap/extension-code-block-lowlight lowlight

完整配置:

jsx 复制代码
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
import { lowlight } from 'lowlight/lib/core';

// 按需导入语言支持
import javascript from 'highlight.js/lib/languages/javascript';
import typescript from 'highlight.js/lib/languages/typescript';
import css from 'highlight.js/lib/languages/css';
import python from 'highlight.js/lib/languages/python';

// 导入高亮主题样式
import 'highlight.js/styles/github-dark.css';

// 注册支持的编程语言
lowlight.registerLanguage('javascript', javascript);
lowlight.registerLanguage('typescript', typescript);
lowlight.registerLanguage('css', css);
lowlight.registerLanguage('python', python);

const editor = useEditor({
  extensions: [
    // ...其他扩展
    CodeBlockLowlight.configure({
      lowlight,
      defaultLanguage: 'javascript', // 默认语言
    }),
  ],
});

UI 集成:

jsx 复制代码
const MenuBar = ({ editor }) => {
  const addCodeBlock = useCallback(() => {
    editor.chain().focus().toggleCodeBlock().run();
  }, [editor]);

  const setLanguage = useCallback((language) => {
    editor.chain().focus().updateAttributes('codeBlock', { language }).run();
  }, [editor]);

  return (
    <div className="menu-bar">
      <button 
        onClick={addCodeBlock}
        className={editor.isActive('codeBlock') ? 'is-active' : ''}
      >
        💻 代码块
      </button>
      
      {editor.isActive('codeBlock') && (
        <select onChange={(e) => setLanguage(e.target.value)}>
          <option value="javascript">JavaScript</option>
          <option value="typescript">TypeScript</option>
          <option value="css">CSS</option>
          <option value="python">Python</option>
        </select>
      )}
    </div>
  );
};

3. 用户体验增强扩展(UX Enhancement Extensions)

这类扩展专注于改善用户的编辑体验,让编辑器更加易用和友好。

核心代表:占位符扩展(@tiptap/extension-placeholder

占位符扩展在编辑器内容为空时显示提示文字,为用户提供编辑指导。

安装方式:

bash 复制代码
npm install @tiptap/extension-placeholder

基础配置:

jsx 复制代码
import Placeholder from '@tiptap/extension-placeholder';

const editor = useEditor({
  extensions: [
    // ...其他扩展
    Placeholder.configure({
      placeholder: '开始编写你的内容...',
      showOnlyWhenEditable: true,  // 只在可编辑状态显示
      showOnlyCurrent: false,      // 在所有空行显示占位符
    }),
  ],
});

样式定制:

css 复制代码
/* 基础占位符样式 */
.tiptap p.is-editor-empty:first-child::before {
  content: attr(data-placeholder);
  float: left;
  color: #6b7280;
  pointer-events: none;
  height: 0;
  font-style: italic;
}

/* 高级占位符样式 */
.tiptap .is-empty::before {
  content: attr(data-placeholder);
  color: #adb5bd;
  font-style: italic;
  pointer-events: none;
}

/* 不同节点类型的占位符 */
.tiptap h1.is-empty::before {
  content: '输入标题...';
}

.tiptap p.is-empty::before {
  content: '输入段落内容...';
}

动态占位符:

jsx 复制代码
// 根据光标位置显示不同占位符
Placeholder.configure({
  placeholder: ({ node }) => {
    if (node.type.name === 'heading') {
      return '输入标题...';
    }
    if (node.type.name === 'paragraph') {
      return '在这里开始编写...';
    }
    return '开始输入内容...';
  },
});

4. 高级交互功能扩展(Advanced Interactive Features)

核心代表:@提及扩展(@tiptap/extension-mention

@提及功能是现代应用的标配,它基于 Tiptap 的 suggestion 工具实现,展示了如何构建复杂的交互功能。

安装方式:

bash 复制代码
npm install @tiptap/extension-mention

建议系统实现: 首先,我们需要创建一个建议系统:

jsx 复制代码
// suggestion.js
import { ReactRenderer } from '@tiptap/react';
import tippy from 'tippy.js';
import MentionList from './MentionList';

export default {
  items: ({ query }) => {
    // 模拟用户数据库查询
    const users = [
      { id: 1, name: '张三', avatar: '/avatars/zhang.jpg', role: '前端工程师' },
      { id: 2, name: '李四', avatar: '/avatars/li.jpg', role: '后端工程师' },
      { id: 3, name: '王五', avatar: '/avatars/wang.jpg', role: '产品经理' },
      { id: 4, name: '赵六', avatar: '/avatars/zhao.jpg', role: 'UI设计师' },
    ];

    return users
      .filter(user => user.name.toLowerCase().includes(query.toLowerCase()))
      .slice(0, 5);
  },

  render: () => {
    let component;
    let popup;

    return {
      onStart: (props) => {
        component = new ReactRenderer(MentionList, {
          props,
          editor: props.editor,
        });

        if (!props.clientRect) {
          return;
        }

        popup = tippy('body', {
          getReferenceClientRect: props.clientRect,
          appendTo: () => document.body,
          content: component.element,
          showOnCreate: true,
          interactive: true,
          trigger: 'manual',
          placement: 'bottom-start',
        });
      },

      onUpdate(props) {
        component.updateProps(props);

        if (!props.clientRect) {
          return;
        }

        popup[0].setProps({
          getReferenceClientRect: props.clientRect,
        });
      },

      onKeyDown(props) {
        if (props.event.key === 'Escape') {
          popup[0].hide();
          return true;
        }

        return component.ref?.onKeyDown(props);
      },

      onExit() {
        popup[0].destroy();
        component.destroy();
      },
    };
  },
};

提及列表组件:

jsx 复制代码
// MentionList.jsx
import React, { useState, useEffect, useImperativeHandle, forwardRef } from 'react';

const MentionList = forwardRef((props, ref) => {
  const [selectedIndex, setSelectedIndex] = useState(0);

  const selectItem = (index) => {
    const item = props.items[index];
    if (item) {
      props.command({ id: item.id, label: item.name });
    }
  };

  const upHandler = () => {
    setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
  };

  const downHandler = () => {
    setSelectedIndex((selectedIndex + 1) % props.items.length);
  };

  const enterHandler = () => {
    selectItem(selectedIndex);
  };

  useEffect(() => setSelectedIndex(0), [props.items]);

  useImperativeHandle(ref, () => ({
    onKeyDown: ({ event }) => {
      if (event.key === 'ArrowUp') {
        upHandler();
        return true;
      }

      if (event.key === 'ArrowDown') {
        downHandler();
        return true;
      }

      if (event.key === 'Enter') {
        enterHandler();
        return true;
      }

      return false;
    },
  }));

  return (
    <div className="mention-list">
      {props.items.length ? (
        props.items.map((item, index) => (
          <div
            className={`mention-item ${index === selectedIndex ? 'is-selected' : ''}`}
            key={item.id}
            onClick={() => selectItem(index)}
          >
            <img src={item.avatar} alt={item.name} className="avatar" />
            <div className="user-info">
              <div className="name">{item.name}</div>
              <div className="role">{item.role}</div>
            </div>
          </div>
        ))
      ) : (
        <div className="mention-item">未找到用户</div>
      )}
    </div>
  );
});

export default MentionList;

完整配置:

jsx 复制代码
import Mention from '@tiptap/extension-mention';
import suggestion from './suggestion';

const editor = useEditor({
  extensions: [
    // ...其他扩展
    Mention.configure({
      HTMLAttributes: {
        class: 'mention',
      },
      suggestion,
    }),
  ],
});

样式设计:

css 复制代码
.mention {
  background-color: #e3f2fd;
  border-radius: 4px;
  padding: 2px 6px;
  color: #1976d2;
  text-decoration: none;
  cursor: pointer;
}

.mention:hover {
  background-color: #bbdefb;
}

.mention-list {
  background: white;
  border-radius: 8px;
  box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  padding: 4px;
  max-height: 200px;
  overflow-y: auto;
}

.mention-item {
  display: flex;
  align-items: center;
  padding: 8px 12px;
  border-radius: 4px;
  cursor: pointer;
  transition: background-color 0.2s;
}

.mention-item:hover,
.mention-item.is-selected {
  background-color: #f3f4f6;
}

.mention-item .avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  margin-right: 12px;
}

.mention-item .user-info .name {
  font-weight: 500;
  color: #111827;
}

.mention-item .user-info .role {
  font-size: 12px;
  color: #6b7280;
}

扩展资源与社区

官方扩展库

Tiptap 官方提供了丰富的扩展库,涵盖各种功能需求:

文本格式化类:

  • @tiptap/extension-bold - 粗体
  • @tiptap/extension-italic - 斜体
  • @tiptap/extension-underline - 下划线
  • @tiptap/extension-strike - 删除线
  • @tiptap/extension-highlight - 高亮

布局结构类:

  • @tiptap/extension-bullet-list - 无序列表
  • @tiptap/extension-ordered-list - 有序列表
  • @tiptap/extension-blockquote - 引用块
  • @tiptap/extension-table - 表格

多媒体类:

  • @tiptap/extension-image - 图片
  • @tiptap/extension-youtube - YouTube 视频
  • @tiptap/extension-link - 链接

交互功能类:

  • @tiptap/extension-mention - @提及
  • @tiptap/extension-task-list - 任务列表
  • @tiptap/extension-collaboration - 协作编辑

寻找扩展的最佳途径

  1. 官方文档 :访问 Tiptap Extensions 页面
  2. GitHub 仓库 :查看 tiptap 官方仓库
  3. 社区贡献 :npm 上搜索 tiptap-extension- 前缀的第三方扩展
  4. 示例项目:研究官方提供的示例代码

选择扩展的决策指南

在选择扩展时,建议考虑以下因素:

功能匹配度:

  • 扩展是否完全满足你的需求?
  • 是否有多余的功能会增加包体积?

维护状态:

  • 扩展是否持续更新?
  • 是否与最新版本的 Tiptap 兼容?

社区支持:

  • 是否有活跃的社区讨论?
  • 遇到问题时是否容易找到解决方案?

性能影响:

  • 扩展会增加多少包体积?
  • 是否会影响编辑器的性能?

实战练习:构建一个完整的富文本编辑器

让我们将本章学到的知识整合起来,构建一个功能丰富的编辑器:

jsx 复制代码
import React from 'react';
import { useEditor, EditorContent } from '@tiptap/react';

// 基础扩展
import Document from '@tiptap/extension-document';
import Paragraph from '@tiptap/extension-paragraph';
import Text from '@tiptap/extension-text';
import History from '@tiptap/extension-history';

// 格式化扩展
import Bold from '@tiptap/extension-bold';
import Italic from '@tiptap/extension-italic';
import Underline from '@tiptap/extension-underline';
import Heading from '@tiptap/extension-heading';

// 富内容扩展
import Link from '@tiptap/extension-link';
import Image from '@tiptap/extension-image';
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';

// UX 增强扩展
import Placeholder from '@tiptap/extension-placeholder';
import Mention from '@tiptap/extension-mention';

// 第三方库
import { lowlight } from 'lowlight/lib/core';
import javascript from 'highlight.js/lib/languages/javascript';

// 注册语言
lowlight.registerLanguage('javascript', javascript);

const RichTextEditor = () => {
  const editor = useEditor({
    extensions: [
      // 基础扩展
      Document,
      Paragraph,
      Text,
      History,
      
      // 格式化扩展
      Bold,
      Italic,
      Underline,
      Heading.configure({
        levels: [1, 2, 3],
      }),
      
      // 富内容扩展
      Link.configure({
        openOnClick: true,
        autolink: true,
      }),
      Image,
      CodeBlockLowlight.configure({
        lowlight,
      }),
      
      // UX 增强扩展
      Placeholder.configure({
        placeholder: '开始编写你的内容...',
      }),
    ],
    content: '<p>欢迎使用富文本编辑器!</p>',
  });

  if (!editor) {
    return null;
  }

  return (
    <div className="rich-text-editor">
      <div className="menu-bar">
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive('bold') ? 'is-active' : ''}
        >
          粗体
        </button>
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'is-active' : ''}
        >
          斜体
        </button>
        {/* 更多按钮... */}
      </div>
      
      <EditorContent editor={editor} className="editor-content" />
    </div>
  );
};

export default RichTextEditor;

总结与展望

在本章中,我们深入探索了 Tiptap 模块化架构的强大威力:

核心收获

  1. 掌握了按需引入的哲学 :学会了如何从 StarterKit 过渡到独立扩展配置
  2. 学会了扩展分类管理:理解了不同类型扩展的特点和应用场景
  3. 实践了完整的集成流程:从安装、配置到 UI 集成的完整开发流程
  4. 培养了模块化思维:建立了可扩展、可维护的编辑器架构思维

技术深度

  • 交互式标记:掌握了链接等内联功能的实现
  • 富内容节点:学会了图片、代码块等复杂内容的集成
  • 用户体验增强:了解了占位符等 UX 功能的价值
  • 高级交互功能:初步了解了 @提及等复杂功能的实现原理

架构思维

现在你已经具备了根据任何产品需求,灵活组合 Tiptap 功能的能力。你可以:

  • 根据性能要求选择最小化的扩展集合
  • 根据功能需求添加特定的扩展模块
  • 根据用户体验目标配置最佳的交互方式

下章预告

但如果官方扩展仍然无法满足你的特殊需求呢?比如需要一个完全定制的内容类型,或者独特的编辑交互方式?

这正是我们下一章要探讨的主题。在 《Tiptap 深度教程(四):终极定制 - 从零创建你的专属扩展》 中,我们将:

  • 深入 Tiptap 扩展的底层原理
  • 学习如何从零开始创建 Node 和 Mark 扩展
  • 掌握扩展的高级配置和优化技巧
  • 构建一个完全自定义的扩展案例

准备好进入 Tiptap 的"深水区"了吗?让我们继续这段精彩的技术之旅!

相关推荐
AlienZHOU5 小时前
AI Coding 时代下,我的技术面试实践分享
前端·后端·面试
Captaincc8 小时前
AI用量v0.1.11更新发布 新增 jusage doctor 诊断指令 托盘展示token 和余额 新增 AutoClaw 支持
前端·后端·vibecoding
计算机魔术师9 小时前
德国Wiki被黑后两周,OpenAI终于把模型失控的账本摊开了
前端
kyriewen10 小时前
我让 AI 当面试官面了我一轮:第 3 个追问我就卡住了(附 10 道追问清单)
前端·面试·ai编程
IT_陈寒10 小时前
Python的GIL把我坑惨了,多线程跑得比单线程还慢
前端·人工智能·后端
前端snow11 小时前
ai agent --- 多agent框架之图编排引擎-langgraph
前端
竹林81811 小时前
OmniPic Studio v3.2.1 核心技术架构与全平台发版解析文档
前端·浏览器
JamesZhang8007811 小时前
页面内存只涨不跌? 一次泄漏排查, 牵出 WeakMap 的诞生
前端
Z小明11 小时前
第 6 章 组件进阶
前端·vue.js
江华森11 小时前
HTTP请求的完整过程详解:从DNS解析到TCP挥手的微秒级实战分析
前端