乐于分享
好东西不私藏

AI协同写作应用-编辑器实例

AI协同写作应用-编辑器实例

大家好!我是程序员库里。最近在做可以写到简历的《企业级前端AI和基建项目实战》 感兴趣的小伙伴可以私信我。有很多小伙伴顺利拿到Offer。

第 4 章:编辑器实例

深入理解 Tiptap 编辑器实例的创建、配置和管理

本章概述

在前面的章节中,我们已经学会了如何创建一个基本的 Tiptap 编辑器。本章将深入探讨编辑器实例(Editor Instance)的方方面面,包括:

  • • 编辑器实例的创建和初始化
  • • 编辑器的配置选项
  • • 编辑器的生命周期管理
  • • 编辑器的销毁和清理
  • • 多编辑器实例的管理

通过本章的学习,你将全面掌握编辑器实例的使用,为后续的高级功能打下坚实基础。


第一部分:编辑器实例基础

什么是编辑器实例

编辑器实例(Editor Instance)是 Tiptap 的核心对象,它包含了编辑器的所有状态、配置和方法。每个编辑器实例都是独立的,拥有自己的:

  • • 文档内容:编辑器中的所有内容
  • • 选区状态:光标位置和选中的文本
  • • 扩展配置:启用的扩展和它们的配置
  • • 命令方法:用于操作编辑器的命令
  • • 事件监听:监听编辑器的各种事件

编辑器实例的创建

在 React 中,我们使用 useEditor Hook 来创建编辑器实例:

import { useEditor } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'const editor = useEditor({  extensions: [StarterKit],  content: '<p>Hello World!</p>',})

编辑器实例的属性

编辑器实例提供了许多有用的属性:

// 检查编辑器是否可编辑console.log(editor.isEditable) // true 或 false// 检查编辑器是否为空console.log(editor.isEmpty) // true 或 false// 检查编辑器是否被销毁console.log(editor.isDestroyed) // true 或 false// 获取编辑器的 Schemaconsole.log(editor.schema)// 获取编辑器的 Stateconsole.log(editor.state)// 获取编辑器的 Viewconsole.log(editor.view)

编辑器实例的方法

编辑器实例提供了丰富的方法来操作编辑器:

// 设置内容editor.commands.setContent('<p>New content</p>')// 获取内容const html = editor.getHTML()const json = editor.getJSON()const text = editor.getText()// 聚焦编辑器editor.commands.focus()// 清空内容editor.commands.clearContent()// 销毁编辑器editor.destroy()

第二部分:编辑器配置选项详解

核心配置选项

useEditor 接受一个配置对象,包含以下核心选项:

1. extensions(扩展)

指定编辑器使用的扩展:

const editor = useEditor({  extensions: [    StarterKit,    Image,    Link,    // 更多扩展...  ],})

2. content(初始内容)

设置编辑器的初始内容,支持 HTML、JSON 或纯文本:

// HTML 格式const editor = useEditor({  extensions: [StarterKit],  content: '<p>Hello <strong>World</strong>!</p>',})// JSON 格式const editor = useEditor({  extensions: [StarterKit],  content: {    type: 'doc',    content: [      {        type: 'paragraph',        content: [          { type: 'text', text: 'Hello ' },          { type: 'text', marks: [{ type: 'bold' }], text: 'World' },          { type: 'text', text: '!' },        ],      },    ],  },})

3. editable(可编辑性)

控制编辑器是否可编辑:

const editor = useEditor({  extensions: [StarterKit],  content: '<p>Hello World!</p>',  editable: true, // 默认为 true})// 动态切换可编辑性editor.setEditable(false) // 设置为只读editor.setEditable(true)  // 设置为可编辑

4. autofocus(自动聚焦)

控制编辑器是否自动获得焦点:

const editor = useEditor({  extensions: [StarterKit],  content: '<p>Hello World!</p>',  autofocus: true, // 自动聚焦到编辑器开头  // autofocus: 'end', // 聚焦到编辑器末尾  // autofocus: 'all', // 选中所有内容  // autofocus: 10, // 聚焦到第 10 个字符位置})

5. editorProps(编辑器属性)

传递给 ProseMirror 的属性:

const editor = useEditor({  extensions: [StarterKit],  editorProps: {    attributes: {      class: 'prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none',      spellcheck: 'false',    },    handleDrop: (view, event, slice, moved) => {      // 自定义拖放处理      return false    },    handlePaste: (view, event, slice) => {      // 自定义粘贴处理      return false    },  },})

6. injectCSS(注入 CSS)

控制是否注入 Tiptap 的默认样式:

const editor = useEditor({  extensions: [StarterKit],  injectCSS: true, // 默认为 true})

7. parseOptions(解析选项)

控制内容解析的行为:

const editor = useEditor({  extensions: [StarterKit],  parseOptions: {    preserveWhitespace: 'full', // 保留所有空白字符  },})

事件回调配置

编辑器支持多种事件回调:

1. onCreate(创建时)

编辑器创建完成时触发:

const editor = useEditor({  extensions: [StarterKit],  onCreate: ({ editor }) => {    console.log('编辑器已创建', editor)  },})

2. onUpdate(更新时)

编辑器内容更新时触发:

const editor = useEditor({  extensions: [StarterKit],  onUpdate: ({ editor }) => {    console.log('内容已更新', editor.getHTML())  },})

3. onSelectionUpdate(选区更新时)

选区(光标位置)更新时触发:

const editor = useEditor({  extensions: [StarterKit],  onSelectionUpdate: ({ editor }) => {    console.log('选区已更新', editor.state.selection)  },})

4. onTransaction(事务时)

每次事务(Transaction)发生时触发:

const editor = useEditor({  extensions: [StarterKit],  onTransaction: ({ editor, transaction }) => {    console.log('事务发生', transaction)  },})

5. onFocus 和 onBlur(焦点事件)

编辑器获得或失去焦点时触发:

const editor = useEditor({  extensions: [StarterKit],  onFocus: ({ editor, event }) => {    console.log('编辑器获得焦点')  },  onBlur: ({ editor, event }) => {    console.log('编辑器失去焦点')  },})

6. onDestroy(销毁时)

编辑器销毁时触发:

const editor = useEditor({  extensions: [StarterKit],  onDestroy: () => {    console.log('编辑器已销毁')  },})

第三部分:进阶功能实战

案例一:配置完整的编辑器实例

让我们创建一个功能完整的编辑器,包含所有常用配置。

功能需求

  • • 支持多种文本格式
  • • 自动保存内容
  • • 显示字符计数
  • • 支持只读模式切换
  • • 完整的事件监听

实现步骤

步骤 1:安装依赖
npm install @tiptap/react @tiptap/starter-kit @tiptap/extension-character-count @tiptap/extension-placeholder
步骤 2:创建编辑器组件

src/components/FullFeaturedEditor.tsx

// 导入依赖import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import CharacterCount from '@tiptap/extension-character-count'import Placeholder from '@tiptap/extension-placeholder'import { useState, useEffect } from 'react'// 定义组件 Propsinterface FullFeaturedEditorProps {  initialContent?: string  onSave?: (content: string) => void}export default function FullFeaturedEditor(  initialContent = ''  onSave }: FullFeaturedEditorProps) {  // 状态管理  const [isEditable, setIsEditable] = useState(true)  const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')  // 创建编辑器实例  const editor = useEditor({    // 配置扩展    extensions: [      StarterKit.configure({        history: {          depth: 100, // 撤销/重做的历史深度        },      }),      CharacterCount.configure({        limit: 10000, // 字符数限制      }),      Placeholder.configure({        placeholder: '开始输入内容...',      }),    ],    // 设置初始内容    content: initialContent,    // 设置可编辑性    editable: isEditable,    // 自动聚焦    autofocus: 'end',    // 编辑器属性    editorProps: {      attributes: {        class: 'prose prose-sm sm:prose lg:prose-lg focus:outline-none min-h-[200px] p-4',      },    },    // 事件回调    onCreate: ({ editor }) => {      console.log('✅ 编辑器已创建')    },    onUpdate: ({ editor }) => {      setSaveStatus('unsaved')      // 自动保存(防抖)      const timer = setTimeout(() => {        handleSave(editor.getHTML())      }, 1000)      return () => clearTimeout(timer)    },    onFocus: () => {      console.log('编辑器获得焦点')    },    onBlur: () => {      console.log('编辑器失去焦点')    },    onDestroy: () => {      console.log('编辑器已销毁')    },  })  // 保存处理函数  const handleSave = async (content: string) => {    setSaveStatus('saving')    try {      await onSave?.(content)      setSaveStatus('saved')    } catch (error) {      console.error('保存失败:', error)      setSaveStatus('unsaved')    }  }  // 切换可编辑性  const toggleEditable = () => {    setIsEditable(!isEditable)    editor?.setEditable(!isEditable)  }  // 清空内容  const clearContent = () => {    if (confirm('确定要清空所有内容吗?')) {      editor?.commands.clearContent()    }  }  // 组件卸载时销毁编辑器  useEffect(() => {    return () => {      editor?.destroy()    }  }, [editor])  if (!editor) {    return<div>加载中...</div>  }  return (<div className="border rounded-lg shadow-sm">      {/* 工具栏 */}      <div className="border-b p-2 flex items-center justify-between bg-gray-50">        <div className="flex gap-2">          <button            onClick={toggleEditable}            className="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600"          >            {isEditable ? '可编辑' : '只读'}          </button>          <button            onClick={clearContent}            className="px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600"            disabled={!isEditable}          >            清空          </button>        </div>        {/* 状态信息 */}        <div className="flex items-center gap-4 text-sm text-gray-600">          {/* 保存状态 */}          <span>            {saveStatus === 'saved' && '已保存'}            {saveStatus === 'saving' && '保存中...'}            {saveStatus === 'unsaved' && '未保存'}          </span>          {/* 字符计数 */}          <span>            {editor.storage.characterCount.characters()} / 10000 字符          </span>          {/* 编辑器状态 */}          <span>            {editor.isEmpty ? '空文档' : '有内容'}          </span>        </div>      </div>      {/* 编辑器内容区 */}      <EditorContent editor={editor} />    </div>  )}
步骤 3:使用编辑器组件

src/App.tsx

import FullFeaturedEditor from './components/FullFeaturedEditor'export default function App() {  const handleSave = async (content: string) => {    // 模拟保存到服务器    await new Promise(resolve => setTimeout(resolve, 500))    console.log('内容已保存:', content)  }  return (<div className="container mx-auto p-8">      <h1 className="text-3xl font-bold mb-6">完整功能编辑器</h1>      <FullFeaturedEditor        initialContent="<p>这是一个功能完整的编辑器示例</p>"        onSave={handleSave}      />    </div>  )}

完整源码

src/components/FullFeaturedEditor.tsx(点击展开)
import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import CharacterCount from '@tiptap/extension-character-count'import Placeholder from '@tiptap/extension-placeholder'import { useState, useEffect } from 'react'interface FullFeaturedEditorProps {  initialContent?: string  onSave?: (content: string) => void}export default function FullFeaturedEditor(  initialContent = ''  onSave }: FullFeaturedEditorProps) {  const [isEditable, setIsEditable] = useState(true)  const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved')  const editor = useEditor({    extensions: [      StarterKit.configure({        history: {          depth: 100,        },      }),      CharacterCount.configure({        limit: 10000,      }),      Placeholder.configure({        placeholder: '开始输入内容...',      }),    ],    content: initialContent,    editable: isEditable,    autofocus: 'end',    editorProps: {      attributes: {        class: 'prose prose-sm sm:prose lg:prose-lg focus:outline-none min-h-[200px] p-4',      },    },    onCreate: ({ editor }) => {      console.log('编辑器已创建')    },    onUpdate: ({ editor }) => {      setSaveStatus('unsaved')      const timer = setTimeout(() => {        handleSave(editor.getHTML())      }, 1000)      return () => clearTimeout(timer)    },    onFocus: () => {      console.log('编辑器获得焦点')    },    onBlur: () => {      console.log('编辑器失去焦点')    },    onDestroy: () => {      console.log('编辑器已销毁')    },  })  const handleSave = async (content: string) => {    setSaveStatus('saving')    try {      await onSave?.(content)      setSaveStatus('saved')    } catch (error) {      console.error('保存失败:', error)      setSaveStatus('unsaved')    }  }  const toggleEditable = () => {    setIsEditable(!isEditable)    editor?.setEditable(!isEditable)  }  const clearContent = () => {    if (confirm('确定要清空所有内容吗?')) {      editor?.commands.clearContent()    }  }  useEffect(() => {    return () => {      editor?.destroy()    }  }, [editor])  if (!editor) {    return<div>加载中...</div>  }  return (<div className="border rounded-lg shadow-sm">      <div className="border-b p-2 flex items-center justify-between bg-gray-50">        <div className="flex gap-2">          <button            onClick={toggleEditable}            className="px-3 py-1 rounded bg-blue-500 text-white hover:bg-blue-600"          >            {isEditable ? '可编辑' : '只读'}          </button>          <button            onClick={clearContent}            className="px-3 py-1 rounded bg-red-500 text-white hover:bg-red-600"            disabled={!isEditable}          >            清空          </button>        </div>        <div className="flex items-center gap-4 text-sm text-gray-600">          <span>            {saveStatus === 'saved' && '已保存'}            {saveStatus === 'saving' && '保存中...'}            {saveStatus === 'unsaved' && '未保存'}          </span>          <span>            {editor.storage.characterCount.characters()} / 10000 字符          </span>          <span>            {editor.isEmpty ? '空文档' : '有内容'}          </span>        </div>      </div>      <EditorContent editor={editor} />    </div>  )}

测试功能

  1. 1. 打开浏览器,访问应用
  2. 2. 在编辑器中输入内容,观察字符计数和保存状态
  3. 3. 点击”只读”按钮,尝试编辑(应该无法编辑)
  4. 4. 点击”可编辑”按钮,恢复编辑功能
  5. 5. 点击”清空”按钮,清空所有内容
  6. 6. 打开浏览器控制台,观察事件日志

案例二:多编辑器实例管理

在某些场景下,我们需要在同一个页面中管理多个编辑器实例,例如:

  • • 评论系统(每条评论一个编辑器)
  • • 多文档编辑(同时编辑多个文档)
  • • 对比视图(并排显示两个编辑器)

功能需求

  • • 创建多个独立的编辑器实例
  • • 每个编辑器有独立的内容和状态
  • • 支持在编辑器之间复制内容
  • • 显示当前活动的编辑器

实现步骤

步骤 1:创建多编辑器组件

src/components/MultiEditorManager.tsx

// 导入必要的依赖import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import { useState } from 'react'// 定义编辑器数据类型interface EditorData {  id: string  title: string  content: string}// 单个编辑器组件function SingleEditor(  data,   isActive,   onFocus,   onUpdate }: {  data: EditorData  isActive: boolean  onFocus: () => void  onUpdate: (content: string) => void}) {  const editor = useEditor({    extensions: [StarterKit],    content: data.content,    onUpdate: ({ editor }) => {      onUpdate(editor.getHTML())    },    onFocus: () => {      onFocus()    },  })  if (!editor) return null  return (<div      className={`border rounded-lg p-4 ${        isActive ? 'ring-2 ring-blue-500' : ''      }`}    >      <h3 className="font-bold mb-2">{data.title}</h3>      <EditorContent editor={editor} className="prose prose-sm" />      <div className="mt-2 text-xs text-gray-500">        字符数: {editor.storage.characterCount?.characters() || 0}      </div>    </div>  )}// 多编辑器管理组件export default function MultiEditorManager() {  // 编辑器列表状态  const [editors, setEditors] = useState<EditorData[]>([    { id: '1', title: '文档 1', content: '<p>这是第一个编辑器</p>' },    { id: '2', title: '文档 2', content: '<p>这是第二个编辑器</p>' },    { id: '3', title: '文档 3', content: '<p>这是第三个编辑器</p>' },  ])  // 当前活动的编辑器 ID  const [activeEditorId, setActiveEditorId] = useState<string>('1')  // 更新编辑器内容  const handleUpdate = (id: string, content: string) => {    setEditors(prev =>      prev.map(editor =>        editor.id === id ? { ...editor, content } : editor      )    )  }  // 添加新编辑器  const addEditor = () => {    const newId = String(editors.length + 1)    setEditors(prev => [      ...prev,      {        id: newId,        title: `文档 ${newId}`,        content: '<p>新建文档</p>',      },    ])    setActiveEditorId(newId)  }  // 删除编辑器  const removeEditor = (id: string) => {    if (editors.length <= 1) {      alert('至少保留一个编辑器')      return    }    setEditors(prev => prev.filter(editor => editor.id !== id))    if (activeEditorId === id) {      setActiveEditorId(editors[0].id)    }  }  return (<div className="space-y-4">      {/* 工具栏 */}      <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">        <div>          <h2 className="text-xl font-bold">多编辑器管理</h2>          <p className="text-sm text-gray-600">            当前活动: {editors.find(e => e.id === activeEditorId)?.title}          </p>        </div>        <button          onClick={addEditor}          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"        >          ➕ 添加编辑器        </button>      </div>      {/* 编辑器列表 */}      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">        {editors.map(editor => (          <div key={editor.id} className="relative">            {/* 删除按钮 */}            <button              onClick={() => removeEditor(editor.id)}              className="absolute top-2 right-2 z-10 px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600"            >              ✕            </button>            {/* 编辑器 */}            <SingleEditor              data={editor}              isActive={activeEditorId === editor.id}              onFocus={() => setActiveEditorId(editor.id)}              onUpdate={(content) => handleUpdate(editor.id, content)}            />          </div>        ))}      </div>      {/* 统计信息 */}      <div className="p-4 bg-gray-50 rounded-lg">        <h3 className="font-bold mb-2">统计信息</h3>        <ul className="text-sm space-y-1">          <li>编辑器数量: {editors.length}</li>          <li>总字符数: {editors.reduce((sum, e) => sum + e.content.length, 0)}</li>        </ul>      </div>    </div>  )}
步骤 2:使用多编辑器组件

src/App.tsx

import MultiEditorManager from './components/MultiEditorManager'export default function App() {  return (<div className="container mx-auto p-8">      <MultiEditorManager />    </div>  )}

完整源码

src/components/MultiEditorManager.tsx(点击展开)
import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import { useState } from 'react'interface EditorData {  id: string  title: string  content: string}function SingleEditor(  data,   isActive,   onFocus,   onUpdate }: {  data: EditorData  isActive: boolean  onFocus: () => void  onUpdate: (content: string) => void}) {  const editor = useEditor({    extensions: [StarterKit],    content: data.content,    onUpdate: ({ editor }) => {      onUpdate(editor.getHTML())    },    onFocus: () => {      onFocus()    },  })  if (!editor) return null  return (<div      className={`border rounded-lg p-4 ${        isActive ? 'ring-2 ring-blue-500' : ''      }`}    >      <h3 className="font-bold mb-2">{data.title}</h3>      <EditorContent editor={editor} className="prose prose-sm" />      <div className="mt-2 text-xs text-gray-500">        字符数: {editor.storage.characterCount?.characters() || 0}      </div>    </div>  )}export default function MultiEditorManager() {  const [editors, setEditors] = useState<EditorData[]>([    { id: '1', title: '文档 1', content: '<p>这是第一个编辑器</p>' },    { id: '2', title: '文档 2', content: '<p>这是第二个编辑器</p>' },    { id: '3', title: '文档 3', content: '<p>这是第三个编辑器</p>' },  ])  const [activeEditorId, setActiveEditorId] = useState<string>('1')  const handleUpdate = (id: string, content: string) => {    setEditors(prev =>      prev.map(editor =>        editor.id === id ? { ...editor, content } : editor      )    )  }  const addEditor = () => {    const newId = String(editors.length + 1)    setEditors(prev => [      ...prev,      {        id: newId,        title: `文档 ${newId}`,        content: '<p>新建文档</p>',      },    ])    setActiveEditorId(newId)  }  const removeEditor = (id: string) => {    if (editors.length <= 1) {      alert('至少保留一个编辑器')      return    }    setEditors(prev => prev.filter(editor => editor.id !== id))    if (activeEditorId === id) {      setActiveEditorId(editors[0].id)    }  }  return (<div className="space-y-4">      <div className="flex items-center justify-between p-4 bg-gray-50 rounded-lg">        <div>          <h2 className="text-xl font-bold">多编辑器管理</h2>          <p className="text-sm text-gray-600">            当前活动: {editors.find(e => e.id === activeEditorId)?.title}          </p>        </div>        <button          onClick={addEditor}          className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"        >          ➕ 添加编辑器        </button>      </div>      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">        {editors.map(editor => (          <div key={editor.id} className="relative">            <button              onClick={() => removeEditor(editor.id)}              className="absolute top-2 right-2 z-10 px-2 py-1 bg-red-500 text-white text-xs rounded hover:bg-red-600"            >              ✕            </button>            <SingleEditor              data={editor}              isActive={activeEditorId === editor.id}              onFocus={() => setActiveEditorId(editor.id)}              onUpdate={(content) => handleUpdate(editor.id, content)}            />          </div>        ))}      </div>      <div className="p-4 bg-gray-50 rounded-lg">        <h3 className="font-bold mb-2">统计信息</h3>        <ul className="text-sm space-y-1">          <li>编辑器数量: {editors.length}</li>          <li>总字符数: {editors.reduce((sum, e) => sum + e.content.length, 0)}</li>        </ul>      </div>    </div>  )}

测试功能

  1. 1. 打开浏览器,查看三个并排的编辑器
  2. 2. 点击任意编辑器,观察蓝色边框(表示活动状态)
  3. 3. 在不同编辑器中输入内容,验证内容独立性
  4. 4. 点击”添加编辑器”按钮,添加新编辑器
  5. 5. 点击编辑器右上角的”✕”按钮,删除编辑器
  6. 6. 观察底部的统计信息更新

案例三:编辑器生命周期管理

理解编辑器的生命周期对于避免内存泄漏和性能问题至关重要。

功能需求

  • • 正确初始化编辑器
  • • 监听生命周期事件
  • • 正确清理和销毁编辑器
  • • 处理组件重新渲染

实现步骤

步骤 1:创建生命周期演示组件

src/components/EditorLifecycle.tsx

// 导入必要的依赖import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import { useState, useEffect, useRef } from 'react'export default function EditorLifecycle() {  // 状态管理  const [logs, setLogs] = useState<string[]>([])  const [showEditor, setShowEditor] = useState(true)  const [editorKey, setEditorKey] = useState(0)  const logRef = useRef<HTMLDivElement>(null)  // 添加日志  const addLog = (message: string) => {    const timestamp = new Date().toLocaleTimeString()    setLogs(prev => [...prev, `[${timestamp}] ${message}`])  }  // 创建编辑器实例  const editor = useEditor({    extensions: [StarterKit],    content: '<p>观察编辑器的生命周期</p>',    // 生命周期:创建    onCreate: ({ editor }) => {      addLog('onCreate: 编辑器已创建')      console.log('编辑器实例:', editor)    },    // 生命周期:更新    onUpdate: ({ editor }) => {      addLog('onUpdate: 内容已更新')    },    // 生命周期:选区更新    onSelectionUpdate: ({ editor }) => {      addLog('onSelectionUpdate: 选区已更新')    },    // 生命周期:事务    onTransaction: ({ editor, transaction }) => {      // 注意:这个事件触发非常频繁,建议谨慎使用      // addLog('onTransaction: 事务发生')    },    // 生命周期:获得焦点    onFocus: ({ editor, event }) => {      addLog('onFocus: 编辑器获得焦点')    },    // 生命周期:失去焦点    onBlur: ({ editor, event }) => {      addLog('onBlur: 编辑器失去焦点')    },    // 生命周期:销毁    onDestroy: () => {      addLog('onDestroy: 编辑器已销毁')    },  }, [editorKey]) // 依赖 editorKey,用于重新创建编辑器  // ✨ 组件卸载时清理  useEffect(() => {    addLog('组件已挂载')    return () => {      addLog('组件即将卸载')      // useEditor 会自动处理销毁,但我们可以手动调用      editor?.destroy()    }  }, [])  // 自动滚动日志到底部  useEffect(() => {    if (logRef.current) {      logRef.current.scrollTop = logRef.current.scrollHeight    }  }, [logs])  // 切换编辑器显示  const toggleEditor = () => {    setShowEditor(!showEditor)    if (!showEditor) {      addLog('👁️ 显示编辑器')    } else {      addLog('🙈 隐藏编辑器')    }  }  // 重新创建编辑器  const recreateEditor = () => {    addLog('🔄 重新创建编辑器')    setEditorKey(prev => prev + 1)  }  // 清空日志  const clearLogs = () => {    setLogs([])  }  return (<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">      {/* 左侧:编辑器 */}      <div className="space-y-4">        <div className="flex gap-2">          <button            onClick={toggleEditor}            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"          >            {showEditor ? '隐藏编辑器' : '显示编辑器'}          </button>          <button            onClick={recreateEditor}            className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"          >            重新创建          </button>        </div>        {showEditor && editor && (          <div className="border rounded-lg p-4">            <h3 className="font-bold mb-2">编辑器</h3>            <EditorContent editor={editor} className="prose prose-sm" />          </div>        )}        {!showEditor && (          <div className="border rounded-lg p-4 text-center text-gray-500">            编辑器已隐藏          </div>        )}      </div>      {/* 右侧:日志 */}      <div className="space-y-4">        <div className="flex items-center justify-between">          <h3 className="font-bold">生命周期日志</h3>          <button            onClick={clearLogs}            className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600"          >            清空日志          </button>        </div>        <div          ref={logRef}          className="border rounded-lg p-4 h-96 overflow-y-auto bg-gray-50 font-mono text-sm"        >          {logs.length === 0 ? (            <div className="text-gray-400">暂无日志</div>          ) : (            logs.map((log, index) => (              <div key={index} className="mb-1">                {log}              </div>            ))          )}        </div>        <div className="text-sm text-gray-600 space-y-1">          <p>💡 提示:</p>          <ul className="list-disc list-inside space-y-1">            <li>在编辑器中输入内容,观察 onUpdate 事件</li>            <li>点击编辑器内外,观察 onFocus 和 onBlur 事件</li>            <li>选择文本,观察 onSelectionUpdate 事件</li>            <li>点击"隐藏编辑器",观察销毁过程</li>            <li>点击"重新创建",观察创建过程</li>          </ul>        </div>      </div>    </div>  )}
步骤 2:使用生命周期组件

src/App.tsx

import EditorLifecycle from './components/EditorLifecycle'export default function App() {  return (<div className="container mx-auto p-8">      <h1 className="text-3xl font-bold mb-6">编辑器生命周期</h1>      <EditorLifecycle />    </div>  )}

完整源码

src/components/EditorLifecycle.tsx(点击展开)
import { useEditor, EditorContent } from '@tiptap/react'import StarterKit from '@tiptap/starter-kit'import { useState, useEffect, useRef } from 'react'export default function EditorLifecycle() {  const [logs, setLogs] = useState<string[]>([])  const [showEditor, setShowEditor] = useState(true)  const [editorKey, setEditorKey] = useState(0)  const logRef = useRef<HTMLDivElement>(null)  const addLog = (message: string) => {    const timestamp = new Date().toLocaleTimeString()    setLogs(prev => [...prev, `[${timestamp}] ${message}`])  }  const editor = useEditor({    extensions: [StarterKit],    content: '<p>观察编辑器的生命周期</p>',    onCreate: ({ editor }) => {      addLog('onCreate: 编辑器已创建')      console.log('编辑器实例:', editor)    },    onUpdate: ({ editor }) => {      addLog('onUpdate: 内容已更新')    },    onSelectionUpdate: ({ editor }) => {      addLog('onSelectionUpdate: 选区已更新')    },    onFocus: ({ editor, event }) => {      addLog('onFocus: 编辑器获得焦点')    },    onBlur: ({ editor, event }) => {      addLog('onBlur: 编辑器失去焦点')    },    onDestroy: () => {      addLog('onDestroy: 编辑器已销毁')    },  }, [editorKey])  useEffect(() => {    addLog('组件已挂载')    return () => {      addLog('组件即将卸载')      editor?.destroy()    }  }, [])  useEffect(() => {    if (logRef.current) {      logRef.current.scrollTop = logRef.current.scrollHeight    }  }, [logs])  const toggleEditor = () => {    setShowEditor(!showEditor)    if (!showEditor) {      addLog('显示编辑器')    } else {      addLog('隐藏编辑器')    }  }  const recreateEditor = () => {    addLog('重新创建编辑器')    setEditorKey(prev => prev + 1)  }  const clearLogs = () => {    setLogs([])  }  return (<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">      <div className="space-y-4">        <div className="flex gap-2">          <button            onClick={toggleEditor}            className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600"          >            {showEditor ? '隐藏编辑器' : '显示编辑器'}          </button>          <button            onClick={recreateEditor}            className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600"          >            重新创建          </button>        </div>        {showEditor && editor && (          <div className="border rounded-lg p-4">            <h3 className="font-bold mb-2">编辑器</h3>            <EditorContent editor={editor} className="prose prose-sm" />          </div>        )}        {!showEditor && (          <div className="border rounded-lg p-4 text-center text-gray-500">            编辑器已隐藏          </div>        )}      </div>      <div className="space-y-4">        <div className="flex items-center justify-between">          <h3 className="font-bold">生命周期日志</h3>          <button            onClick={clearLogs}            className="px-3 py-1 bg-red-500 text-white text-sm rounded hover:bg-red-600"          >            清空日志          </button>        </div>        <div          ref={logRef}          className="border rounded-lg p-4 h-96 overflow-y-auto bg-gray-50 font-mono text-sm"        >          {logs.length === 0 ? (            <div className="text-gray-400">暂无日志</div>          ) : (            logs.map((log, index) => (              <div key={index} className="mb-1">                {log}              </div>            ))          )}        </div>        <div className="text-sm text-gray-600 space-y-1">          <p>提示:</p>          <ul className="list-disc list-inside space-y-1">            <li>在编辑器中输入内容,观察 onUpdate 事件</li>            <li>点击编辑器内外,观察 onFocus 和 onBlur 事件</li>            <li>选择文本,观察 onSelectionUpdate 事件</li>            <li>点击"隐藏编辑器",观察销毁过程</li>            <li>点击"重新创建",观察创建过程</li>          </ul>        </div>      </div>    </div>  )}

测试功能

  1. 1. 打开浏览器,观察右侧日志面板
  2. 2. 在编辑器中输入内容,观察 onUpdate 事件
  3. 3. 点击编辑器内外,观察 onFocus 和 onBlur 事件
  4. 4. 选择文本,观察 onSelectionUpdate 事件
  5. 5. 点击”隐藏编辑器”,观察 onDestroy 事件
  6. 6. 点击”显示编辑器”,观察 onCreate 事件
  7. 7. 点击”重新创建”,观察完整的销毁和创建过程

第四部分:优化和最佳实践

1. 避免内存泄漏

问题

如果不正确销毁编辑器,会导致内存泄漏。

解决方案

import { useEditor } from '@tiptap/react'import { useEffect } from 'react'function MyEditor() {  const editor = useEditor({    extensions: [StarterKit],    content: '<p>Hello World!</p>',  })  // 正确:在组件卸载时销毁编辑器  useEffect(() => {    return () => {      editor?.destroy()    }  }, [editor])  return<EditorContent editor={editor} />}

2. 优化重新渲染

问题

编辑器配置对象在每次渲染时都会重新创建,导致不必要的重新渲染。

解决方案

import { useEditor } from '@tiptap/react'import { useMemo, useCallback } from 'react'function MyEditor() {  // ✅ 使用 useCallback 缓存事件处理函数  const handleUpdate = useCallback(({ editor }) => {    console.log('内容更新:', editor.getHTML())  }, [])  // ✅ 使用 useMemo 缓存扩展配置  const extensions = useMemo(() => [    StarterKit.configure({      history: {        depth: 100,      },    }),  ], [])  const editor = useEditor({    extensions,    content: '<p>Hello World!</p>',    onUpdate: handleUpdate,  })  return<EditorContent editor={editor} />}

3. 处理异步初始化

问题

编辑器实例可能在初始渲染时为 null

解决方案

function MyEditor() {  const editor = useEditor({    extensions: [StarterKit],    content: '<p>Hello World!</p>',  })  // ✅ 正确:检查编辑器是否存在  if (!editor) {    return<div>加载中...</div>  }  return (<div>      <button onClick={() => editor.commands.toggleBold()}>        加粗      </button>      <EditorContent editor={editor} />    </div>  )}

4. 动态更新配置

问题

如何在运行时动态更新编辑器配置?

解决方案

function MyEditor() {  const [isEditable, setIsEditable] = useState(true)  const editor = useEditor({    extensions: [StarterKit],    content: '<p>Hello World!</p>',    editable: isEditable,  })  // ✅ 使用 useEffect 同步状态  useEffect(() => {    if (editor) {      editor.setEditable(isEditable)    }  }, [editor, isEditable])  return (<div>      <button onClick={() => setIsEditable(!isEditable)}>        切换可编辑性      </button>      <EditorContent editor={editor} />    </div>  )}

5. 多编辑器实例的性能优化

问题

页面中有多个编辑器实例时,性能可能下降。

解决方案

// ✅ 使用 React.memo 避免不必要的重新渲染const SingleEditor = React.memo((  content,   onChange }: {   content: string  onChange: (content: string) => void}) => {  const editor = useEditor({    extensions: [StarterKit],    content,    onUpdate: ({ editor }) => {      onChange(editor.getHTML())    },  })  useEffect(() => {    return () => {      editor?.destroy()    }  }, [editor])  if (!editor) return null  return<EditorContent editor={editor} />})// ✅ 使用虚拟滚动(对于大量编辑器)import { FixedSizeList } from 'react-window'function ManyEditors({ editors }: { editors: EditorData[] }) {  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (<div style={style}>      <SingleEditor        content={editors[index].content}        onChange={(content) => handleUpdate(index, content)}      />    </div>  )  return (<FixedSizeList      height={600}      itemCount={editors.length}      itemSize={200}      width="100%"    >      {Row}    </FixedSizeList>  )}

6. 错误处理

问题

编辑器初始化或操作可能失败。

解决方案

function MyEditor() {  const [error, setError] = useState<string | null>(null)  const editor = useEditor({    extensions: [StarterKit],    content: '<p>Hello World!</p>',    onCreate: ({ editor }) => {      try {        // 初始化逻辑        console.log('编辑器已创建')      } catch (err) {        setError('编辑器初始化失败')        console.error(err)      }    },    onUpdate: ({ editor }) => {      try {        // 更新逻辑        const content = editor.getHTML()      } catch (err) {        setError('内容更新失败')        console.error(err)      }    },  })  if (error) {    return<div className="text-red-500">错误: {error}</div>  }  if (!editor) {    return<div>加载中...</div>  }  return<EditorContent editor={editor} />}

第五部分:总结和练习

本章总结

在本章中,我们深入学习了 Tiptap 编辑器实例的方方面面:

  1. 1. 编辑器实例基础
    • • 理解编辑器实例的概念和作用
    • • 掌握编辑器实例的属性和方法
    • • 学会创建和使用编辑器实例
  2. 2. 编辑器配置选项
    • • 核心配置:extensions、content、editable、autofocus 等
    • • 事件回调:onCreate、onUpdate、onFocus、onBlur 等
    • • 高级配置:editorProps、parseOptions 等
  3. 3. 进阶功能实战
    • • 案例一:配置完整的编辑器实例(自动保存、字符计数、只读模式)
    • • 案例二:多编辑器实例管理(动态添加/删除、独立状态)
    • • 案例三:编辑器生命周期管理(监听事件、正确清理)
  4. 4. 优化和最佳实践
    • • 避免内存泄漏
    • • 优化重新渲染
    • • 处理异步初始化
    • • 动态更新配置
    • • 多编辑器性能优化
    • • 错误处理

关键要点

  • • ✅ 编辑器实例是 Tiptap 的核心对象,包含所有状态和方法
  • • ✅ 使用 useEditor Hook 创建编辑器实例
  • • ✅ 编辑器支持丰富的配置选项和事件回调
  • • ✅ 必须在组件卸载时正确销毁编辑器,避免内存泄漏
  • • ✅ 使用 useMemo 和 useCallback 优化性能
  • • ✅ 多编辑器实例需要独立管理,避免状态混乱
  • • ✅ 理解编辑器生命周期,正确处理各个阶段

练习题

练习 1:创建带主题切换的编辑器

创建一个编辑器,支持亮色/暗色主题切换,要求:

  • • 使用 editorProps 动态设置 class
  • • 主题切换时编辑器样式随之改变
  • • 保持编辑器内容不变
💡 提示
const [theme, setTheme] = useState<'light' | 'dark'>('light')const editor = useEditor({  extensions: [StarterKit],  editorProps: {    attributes: {      class: theme === 'light'        ? 'prose prose-slate'        : 'prose prose-invert bg-gray-900 text-white',    },  },})useEffect(() => {  if (editor) {    editor.view.updateState(editor.state)  }}, [theme, editor])

练习 2:实现编辑器状态持久化

创建一个编辑器,自动保存内容到 localStorage,要求:

  • • 页面刷新后恢复之前的内容
  • • 使用 onUpdate 事件自动保存
  • • 使用防抖避免频繁保存
💡 提示
const STORAGE_KEY = 'editor-content'const [initialContent, setInitialContent] = useState(() => {  return localStorage.getItem(STORAGE_KEY) || '<p>开始输入...</p>'})const editor = useEditor({  extensions: [StarterKit],  content: initialContent,  onUpdate: ({ editor }) => {    const content = editor.getHTML()    // 使用防抖    const timer = setTimeout(() => {      localStorage.setItem(STORAGE_KEY, content)    }, 1000)    return () => clearTimeout(timer)  },})

练习 3:创建编辑器性能监控

创建一个编辑器,监控并显示性能指标,要求:

  • • 显示编辑器初始化时间
  • • 显示内容更新频率
  • • 显示当前文档大小
  • • 显示事务处理时间
💡 提示
const [metrics, setMetrics] = useState({  initTime: 0,  updateCount: 0,  docSize: 0,  avgTransactionTime: 0,})const startTime = useRef(Date.now())const transactionTimes = useRef<number[]>([])const editor = useEditor({  extensions: [StarterKit],  onCreate: () => {    const initTime = Date.now() - startTime.current    setMetrics(prev => ({ ...prev, initTime }))  },  onUpdate: ({ editor }) => {    setMetrics(prev => ({      ...prev,      updateCount: prev.updateCount + 1,      docSize: editor.getHTML().length,    }))  },  onTransaction: ({ transaction }) => {    const start = performance.now()    // 处理事务    const end = performance.now()    transactionTimes.current.push(end - start)    const avg = transactionTimes.current.reduce((a, b) => a + b, 0) /                 transactionTimes.current.length    setMetrics(prev => ({ ...prev, avgTransactionTime: avg }))  },})

常见问题

Q1: 为什么编辑器实例在初始渲染时是 null?

A: useEditor 是异步创建编辑器的,第一次渲染时返回 null。解决方案:

const editor = useEditor({ /* ... */ })if (!editor) {  return<div>加载中...</div>}return<EditorContent editor={editor} />

Q2: 如何在编辑器外部访问编辑器实例?

A: 使用 useRef 或状态提升:

// 方法 1:使用 refconst editorRef = useRef<Editor | null>(null)const editor = useEditor({  extensions: [StarterKit],  onCreate: ({ editor }) => {    editorRef.current = editor  },})// 方法 2:状态提升function Parent() {  const [editorInstance, setEditorInstance] = useState<Editor | null>(null)  return (<>      <Toolbar editor={editorInstance} />      <MyEditor onEditorCreate={setEditorInstance} />    </>  )}

Q3: 编辑器内容更新后,如何触发父组件重新渲染?

A: 使用 onUpdate 回调:

function Parent() {  const [content, setContent] = useState('')  return (<MyEditor      onUpdate={(newContent) => setContent(newContent)}    />  )}function MyEditor({ onUpdate }: { onUpdate: (content: string) => void }) {  const editor = useEditor({    extensions: [StarterKit],    onUpdate: ({ editor }) => {      onUpdate(editor.getHTML())    },  })  return<EditorContent editor={editor} />}

Q4: 如何在编辑器销毁前保存内容?

A: 使用 onDestroy 回调:

const editor = useEditor({  extensions: [StarterKit],  onDestroy: () => {    const content = editor?.getHTML()    if (content) {      localStorage.setItem('editor-content', content)      // 或发送到服务器      saveToServer(content)    }  },})

Q5: 多个编辑器实例之间如何共享配置?

A: 创建共享的配置对象:

// 共享配置const sharedConfig = {  extensions: [StarterKit],  editorProps: {    attributes: {      class: 'prose prose-sm',    },  },}// 编辑器 1const editor1 = useEditor({  ...sharedConfig,  content: '<p>编辑器 1</p>',})// 编辑器 2const editor2 = useEditor({  ...sharedConfig,  content: '<p>编辑器 2</p>',})

Q6: 如何禁用特定的编辑功能?

A: 使用 editable 和 editorProps

const editor = useEditor({  extensions: [StarterKit],  editable: true,  editorProps: {    // 禁用拖放    handleDrop: () => true,    // 禁用粘贴    handlePaste: () => true,    // 禁用特定按键    handleKeyDown: (view, event) => {      if (event.key === 'Enter') {        return true // 阻止回车      }      return false    },  },})

最后欢迎大家一起来学习 企业级前端AI和基建项目实战!已经有很多小伙伴拿到了Offer。包括字节、阿里等大厂。
本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » AI协同写作应用-编辑器实例

猜你喜欢

  • 暂无文章