AI协同写作应用-TipTap基础功能

46 阅读10分钟

前言

系列教程和源码在飞书文档编写。

本章概述

在本章中,我们将快速上手 Tiptap,从零开始创建一个功能完整的富文本编辑器。你将学会如何安装、配置和使用 Tiptap 的基础功能。

学习目标:

  • 创建一个新的前端项目
  • 安装 Tiptap 及其依赖
  • 创建第一个可用的编辑器
  • 使用 StarterKit 快速添加功能
  • 理解基本配置选项
  • 添加简单的工具栏

前置知识:

  • Node.js 和 npm/pnpm 基础
  • HTML、CSS、JavaScript 基础
  • 基础的命令行操作

预计学习时间: 30-45 分钟


1. 环境准备

1.1 检查 Node.js 版本

Tiptap 需要 Node.js 16+ 版本。

# 检查 Node.js 版本
node --version
# 应该显示 v16.0.0 或更高版本

# 检查 npm 版本
npm --version

如果版本过低,请访问 nodejs.org 下载最新的 LTS 版本。

1.2 选择包管理器

本教程推荐使用 pnpm,它比 npm 更快、更节省磁盘空间。

# 安装 pnpm(如果还没有)
npm install -g pnpm

# 验证安装
pnpm --version

当然,你也可以使用 npm 或 yarn:

# 使用 npm
npm install

# 使用 yarn
yarn add

💡 提示: 本教程的所有命令都使用 pnpm,如果你使用其他包管理器,请相应替换命令。


2. 创建项目

2.1 使用 Vite 创建项目

我们使用 Vite 创建一个 React + TypeScript 项目。

# 创建项目
pnpm create vite tiptap-demo --template react-ts

# 进入项目目录
cd tiptap-demo

# 安装依赖
pnpm install

为什么选择 Vite?

  • ⚡ 极快的启动速度
  • 🔥 热更新(HMR)快速
  • 📦 开箱即用的 TypeScript 支持
  • 🛠️ 现代化的构建工具

2.2 项目结构

创建完成后,项目结构如下:

tiptap-demo/
├── node_modules/
├── public/
├── src/
│   ├── App.css
│   ├── App.tsx
│   ├── main.tsx
│   └── vite-env.d.ts
├── index.html
├── package.json
├── tsconfig.json
└── vite.config.ts

2.3 启动开发服务器

pnpm dev

打开浏览器访问 http://localhost:5173,你应该能看到 Vite 的欢迎页面。


3. 安装 Tiptap

3.1 安装核心包

pnpm add @tiptap/react @tiptap/pm @tiptap/starter-kit

包的说明:

包名版本大小说明
@tiptap/react^2.x~15KBReact 集成包,提供 Hooks 和组件
@tiptap/pm^2.x~200KBProseMirror 核心依赖
@tiptap/starter-kit^2.x~30KB常用扩展集合(15+ 扩展)

总大小: ~245KB(未压缩),~80KB(gzip 压缩后)

3.2 验证安装

检查 package.json 文件,应该能看到:

{
  "dependencies": {
    "@tiptap/pm": "^2.x.x",
    "@tiptap/react": "^2.x.x",
    "@tiptap/starter-kit": "^2.x.x",
    "react": "^18.x.x",
    "react-dom": "^18.x.x"
  }
}

4. 创建第一个编辑器

4.1 清理默认代码

首先,清理 Vite 生成的默认代码。

修改 src/App.tsx

// src/App.tsx
import './App.css'

function App() {
  return (
    <div className="app">
      <h1>我的 Tiptap 编辑器</h1>
    </div>
  )
}

export default App

修改 src/App.css

/* src/App.css */
.app {
  max-width: 900px;
  margin: 0 auto;
  padding: 2rem;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}

h1 {
  margin-bottom: 2rem;
  color: #333;
}

4.2 创建编辑器组件

创建 src/Tiptap.tsx 文件:

// src/Tiptap.tsx
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'

function Tiptap() {
  const editor = useEditor({
    extensions: [
      StarterKit,
    ],
    content: '<p>Hello World! 🌍</p>',
  })

  return <EditorContent editor={editor} />
}

export default Tiptap

代码解析:

  1. 导入必要的模块

    import { useEditor, EditorContent } from '@tiptap/react'
    import StarterKit from '@tiptap/starter-kit'
    
    • useEditor: React Hook,用于创建编辑器实例
    • EditorContent: React 组件,用于渲染编辑器
    • StarterKit: 包含 15+ 个常用扩展
  2. 创建编辑器实例

    const editor = useEditor({
      extensions: [StarterKit],
      content: '<p>Hello World! 🌍</p>',
    })
    
    • extensions: 配置编辑器使用的扩展
    • content: 初始内容(HTML 格式)
  3. 渲染编辑器

    return <EditorContent editor={editor} />
    
    • EditorContent 组件接收编辑器实例并渲染

4.3 在 App 中使用

修改 src/App.tsx

// src/App.tsx
import Tiptap from './Tiptap'
import './App.css'

function App() {
  return (
    <div className="app">
      <h1>我的 Tiptap 编辑器</h1>
      <Tiptap />
    </div>
  )
}

export default App

4.4 添加基础样式

src/App.css 中添加编辑器样式:

/* src/App.css */

/* ... 之前的样式 ... */

/* 编辑器容器样式 */
.tiptap {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  padding: 1rem;
  min-height: 200px;
  outline: none;
  background-color: white;
}

/* 编辑器获得焦点时的样式 */
.tiptap:focus {
  border-color: #3b82f6;
  box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}

/* 段落样式 */
.tiptap p {
  margin: 0.75rem 0;
  line-height: 1.6;
}

/* 第一个段落不需要上边距 */
.tiptap p:first-child {
  margin-top: 0;
}

/* 最后一个段落不需要下边距 */
.tiptap p:last-child {
  margin-bottom: 0;
}

4.5 测试编辑器

保存所有文件,浏览器应该自动刷新。你应该能看到:

  • 一个带边框的编辑区域
  • 初始内容 "Hello World! 🌍"
  • 可以输入、删除文字
  • 可以使用快捷键(Ctrl+B 加粗、Ctrl+Z 撤销等)

测试清单:

  • ✅ 输入文字
  • ✅ 删除文字
  • ✅ 换行(按 Enter)
  • ✅ 撤销(Ctrl+Z)
  • ✅ 重做(Ctrl+Shift+Z)
  • ✅ 加粗(Ctrl+B)
  • ✅ 斜体(Ctrl+I)

5. 理解 StarterKit

5.1 StarterKit 包含的扩展

StarterKit 是一个扩展集合,包含了最常用的 15+ 个扩展:

Nodes(节点):

  • Document - 文档根节点
  • Paragraph - 段落
  • Text - 文本
  • Heading - 标题(H1-H6)
  • Blockquote - 引用块
  • CodeBlock - 代码块
  • BulletList - 无序列表
  • OrderedList - 有序列表
  • ListItem - 列表项
  • HardBreak - 硬换行
  • HorizontalRule - 水平分割线

Marks(标记):

  • Bold - 加粗
  • Italic - 斜体
  • Strike - 删除线
  • Code - 行内代码

Extensions(功能):

  • History - 撤销/重做
  • Dropcursor - 拖放光标
  • Gapcursor - 间隙光标

5.2 测试 StarterKit 功能

让我们测试一下这些功能。修改初始内容:

const editor = useEditor({
  extensions: [StarterKit],
  content: `
    <h1>欢迎使用 Tiptap</h1>
    <p>这是一个<strong>功能强大</strong>的<em>富文本编辑器</em>。</p>
    <h2>主要特性</h2>
    <ul>
      <li>支持多种文本格式</li>
      <li>可扩展的架构</li>
      <li>优秀的性能</li>
    </ul>
    <blockquote>
      <p>Tiptap 让编辑器开发变得简单而有趣。</p>
    </blockquote>
    <pre><code>const editor = useEditor({ ... })</code></pre>
  `,
})

现在你应该能看到:

  • 标题(H1、H2)
  • 加粗和斜体文字
  • 无序列表
  • 引用块
  • 代码块

5.3 自定义 StarterKit

你可以禁用某些扩展或自定义配置:

const editor = useEditor({
  extensions: [
    StarterKit.configure({
      // 禁用某些扩展
      heading: false,
      
      // 自定义扩展配置
      bulletList: {
        HTMLAttributes: {
          class: 'my-bullet-list',
        },
      },
      
      // 自定义标题级别
      heading: {
        levels: [1, 2, 3],  // 只允许 H1、H2、H3
      },
    }),
  ],
  content: '<p>Hello World!</p>',
})

6. 添加工具栏

现在让我们添加一个简单的工具栏,让用户可以点击按钮来格式化文字。

6.1 创建工具栏组件

修改 src/Tiptap.tsx

// src/Tiptap.tsx
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import './Tiptap.css'

function Tiptap() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World! 🌍</p>',
  })

  if (!editor) {
    return null
  }

  return (
    <div className="editor-container">
      {/* 工具栏 */}
      <div className="toolbar">
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive('bold') ? 'is-active' : ''}
        >
          <strong>B</strong>
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'is-active' : ''}
        >
          <em>I</em>
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleStrike().run()}
          className={editor.isActive('strike') ? 'is-active' : ''}
        >
          <s>S</s>
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
          className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
        >
          H1
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
          className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
        >
          H2
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={editor.isActive('bulletList') ? 'is-active' : ''}
        >
          • 列表
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={editor.isActive('orderedList') ? 'is-active' : ''}
        >
          1. 列表
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().undo().run()}
          disabled={!editor.can().undo()}
        >
          ↶ 撤销
        </button>
        
        <button
          onClick={() => editor.chain().focus().redo().run()}
          disabled={!editor.can().redo()}
        >
          ↷ 重做
        </button>
      </div>
      
      {/* 编辑器 */}
      <EditorContent editor={editor} />
    </div>
  )
}

export default Tiptap

代码解析:

  1. 空值检查

    if (!editor) return null
    

    首次渲染时编辑器可能为 null,需要检查。

  2. Commands 链式调用

    editor.chain().focus().toggleBold().run()
    
    • chain(): 开始链式调用
    • focus(): 让编辑器获得焦点
    • toggleBold(): 切换加粗状态
    • run(): 执行命令链
  3. 检查激活状态

    editor.isActive('bold')
    

    用于高亮当前激活的按钮。

  4. 检查命令可用性

    editor.can().undo()
    

    用于禁用不可用的按钮。

6.2 添加工具栏样式

创建 src/Tiptap.css 文件:

/* src/Tiptap.css */

.editor-container {
  border: 1px solid #e5e7eb;
  border-radius: 8px;
  overflow: hidden;
  background-color: white;
}

/* 工具栏样式 */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.25rem;
  padding: 0.75rem;
  background-color: #f9fafb;
  border-bottom: 1px solid #e5e7eb;
}

.toolbar button {
  padding: 0.5rem 0.75rem;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  background-color: white;
  cursor: pointer;
  font-size: 14px;
  font-weight: 500;
  color: #374151;
  transition: all 0.2s;
}

.toolbar button:hover:not(:disabled) {
  background-color: #f3f4f6;
  border-color: #9ca3af;
}

.toolbar button.is-active {
  background-color: #3b82f6;
  color: white;
  border-color: #3b82f6;
}

.toolbar button:disabled {
  opacity: 0.5;
  cursor: not-allowed;
}

.toolbar .divider {
  width: 1px;
  background-color: #e5e7eb;
  margin: 0 0.25rem;
}

/* 编辑器内容样式 */
.editor-container .tiptap {
  padding: 1rem;
  min-height: 300px;
  outline: none;
  border: none;
}

.editor-container .tiptap:focus {
  box-shadow: none;
}

/* 标题样式 */
.tiptap h1 {
  font-size: 2rem;
  font-weight: 700;
  margin: 1.5rem 0 1rem;
  line-height: 1.2;
}

.tiptap h2 {
  font-size: 1.5rem;
  font-weight: 600;
  margin: 1.25rem 0 0.75rem;
  line-height: 1.3;
}

.tiptap h3 {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 1rem 0 0.5rem;
  line-height: 1.4;
}

/* 列表样式 */
.tiptap ul,
.tiptap ol {
  padding-left: 1.5rem;
  margin: 0.75rem 0;
}

.tiptap li {
  margin: 0.25rem 0;
}

/* 引用块样式 */
.tiptap blockquote {
  border-left: 3px solid #3b82f6;
  padding-left: 1rem;
  margin: 1rem 0;
  color: #6b7280;
  font-style: italic;
}

/* 代码块样式 */
.tiptap pre {
  background-color: #1f2937;
  color: #f9fafb;
  padding: 1rem;
  border-radius: 6px;
  margin: 1rem 0;
  overflow-x: auto;
}

.tiptap code {
  background-color: #f3f4f6;
  color: #ef4444;
  padding: 0.2rem 0.4rem;
  border-radius: 3px;
  font-size: 0.9em;
  font-family: 'Courier New', monospace;
}

.tiptap pre code {
  background-color: transparent;
  color: inherit;
  padding: 0;
}

/* 水平分割线样式 */
.tiptap hr {
  border: none;
  border-top: 2px solid #e5e7eb;
  margin: 2rem 0;
}

6.3 测试工具栏

保存文件后,你应该能看到:

  • 一个漂亮的工具栏
  • 点击按钮可以格式化文字
  • 激活的按钮会高亮显示
  • 不可用的按钮会被禁用

测试步骤:

  1. 选中一些文字
  2. 点击 "B" 按钮,文字应该变粗
  3. 按钮应该高亮显示
  4. 再次点击,文字恢复正常

7. 基本配置选项

7.1 常用配置

const editor = useEditor({
  // 扩展配置
  extensions: [StarterKit],
  
  // 初始内容
  content: '<p>Hello World!</p>',
  
  // 是否可编辑
  editable: true,
  
  // 是否自动获取焦点
  autofocus: false,
  
  // 事件回调
  onUpdate: ({ editor }) => {
    console.log('内容已更新', editor.getHTML())
  },
  
  onCreate: ({ editor }) => {
    console.log('编辑器已创建')
  },
  
  onFocus: ({ editor }) => {
    console.log('编辑器获得焦点')
  },
  
  onBlur: ({ editor }) => {
    console.log('编辑器失去焦点')
  },
})

7.2 配置选项说明

选项类型默认值说明
extensionsExtension[]必需编辑器使用的扩展数组
contentstring | JSONContent''初始内容(HTML 或 JSON)
editablebooleantrue是否可编辑
autofocusboolean | 'start' | 'end'false自动获取焦点
onUpdatefunction-内容更新时触发
onCreatefunction-编辑器创建时触发
onFocusfunction-获得焦点时触发
onBlurfunction-失去焦点时触发

8. 完整源码

📄 src/Tiptap.tsx

import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import './Tiptap.css'

function Tiptap() {
  const editor = useEditor({
    extensions: [StarterKit],
    content: '<p>Hello World! 🌍</p>',
  })

  if (!editor) {
    return null
  }

  return (
    <div className="editor-container">
      <div className="toolbar">
        <button
          onClick={() => editor.chain().focus().toggleBold().run()}
          className={editor.isActive('bold') ? 'is-active' : ''}
        >
          <strong>B</strong>
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleItalic().run()}
          className={editor.isActive('italic') ? 'is-active' : ''}
        >
          <em>I</em>
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleStrike().run()}
          className={editor.isActive('strike') ? 'is-active' : ''}
        >
          <s>S</s>
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}
          className={editor.isActive('heading', { level: 1 }) ? 'is-active' : ''}
        >
          H1
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}
          className={editor.isActive('heading', { level: 2 }) ? 'is-active' : ''}
        >
          H2
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().toggleBulletList().run()}
          className={editor.isActive('bulletList') ? 'is-active' : ''}
        >
          • 列表
        </button>
        
        <button
          onClick={() => editor.chain().focus().toggleOrderedList().run()}
          className={editor.isActive('orderedList') ? 'is-active' : ''}
        >
          1. 列表
        </button>
        
        <div className="divider"></div>
        
        <button
          onClick={() => editor.chain().focus().undo().run()}
          disabled={!editor.can().undo()}
        >
          ↶ 撤销
        </button>
        
        <button
          onClick={() => editor.chain().focus().redo().run()}
          disabled={!editor.can().redo()}
        >
          ↷ 重做
        </button>
      </div>
      
      <EditorContent editor={editor} />
    </div>
  )
}

export default Tiptap

📄 src/App.tsx

import Tiptap from './Tiptap'
import './App.css'

function App() {
  return (
    <div className="app">
      <h1>我的 Tiptap 编辑器</h1>
      <Tiptap />
    </div>
  )
}

export default App

9. 本章总结

在本章中,我们学习了:

✅ 环境准备

  • 检查 Node.js 版本
  • 安装包管理器(pnpm)
  • 创建 Vite 项目

✅ 安装 Tiptap

  • 安装核心包(@tiptap/react、@tiptap/pm、@tiptap/starter-kit)
  • 理解包的作用和大小

✅ 创建编辑器

  • 使用 useEditor Hook
  • 渲染 EditorContent 组件
  • 添加基础样式

✅ StarterKit

  • 包含 15+ 个常用扩展
  • 自定义配置
  • 禁用特定扩展

✅ 添加工具栏

  • Commands 链式调用
  • 检查激活状态
  • 检查命令可用性
  • 添加工具栏样式

✅ 基本配置

  • 常用配置选项
  • 事件回调

🎯 关键知识点

1. useEditor Hook

const editor = useEditor({
  extensions: [StarterKit],
  content: '<p>Hello World!</p>',
})

2. Commands 链式调用

editor.chain().focus().toggleBold().run()

3. 检查状态

editor.isActive('bold')
editor.can().undo()

10. 下一步

现在你已经创建了第一个 Tiptap 编辑器!接下来我们将:

第 3 章:Tiptap 与 React 集成

  • 深入理解 useEditor Hook
  • 使用 EditorProvider
  • 实现自动保存
  • 处理 Next.js SSR

第 4 章:框架集成 - Vue/其他

  • Vue 集成
  • Angular 集成
  • Vanilla JavaScript

准备好继续学习了吗?🚀


11. 练习题

练习 1:添加更多按钮

在工具栏中添加以下按钮:

  • H3 标题
  • 引用块(Blockquote)
  • 代码块(CodeBlock)
  • 水平分割线(HorizontalRule)
💡 提示
<button
  onClick={() => editor.chain().focus().toggleBlockquote().run()}
  className={editor.isActive('blockquote') ? 'is-active' : ''}
>
  引用
</button>

练习 2:添加字符计数

在编辑器下方显示当前字符数。

💡 提示
const characterCount = editor.state.doc.textContent.length

<div className="character-count">
  {characterCount} 字符
</div>

练习 3:实现只读模式

添加一个切换按钮,可以切换编辑器的可编辑状态。

💡 提示
const [editable, setEditable] = useState(true)

useEffect(() => {
  if (editor) {
    editor.setEditable(editable)
  }
}, [editor, editable])

12. 常见问题

Q1: 为什么编辑器是 null?

A: 首次渲染时,编辑器还未初始化。解决方案:

if (!editor) return null

Q2: 如何获取编辑器内容?

A: 使用 getHTML()getJSON() 方法:

const html = editor.getHTML()
const json = editor.getJSON()

Q3: 如何设置编辑器内容?

A: 使用 setContent() 方法:

editor.commands.setContent('<p>新内容</p>')

Q4: 快捷键不工作?

A: 确保编辑器有焦点:

editor.chain().focus().toggleBold().run()

13. 扩展阅读