AI协同写作应用-TipTap基础功能
前言
系列教程和源码在飞书文档编写。
本章概述
在本章中,我们将快速上手 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 | ~15KB | React 集成包,提供 Hooks 和组件 |
@tiptap/pm |
^2.x | ~200KB | ProseMirror 核心依赖 |
@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
代码解析:
-
导入必要的模块
import { useEditor, EditorContent } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit'-
useEditor: React Hook,用于创建编辑器实例 -
EditorContent: React 组件,用于渲染编辑器 -
StarterKit: 包含 15+ 个常用扩展
-
-
创建编辑器实例
const editor = useEditor({ extensions: [StarterKit], content: '<p>Hello World! 🌍</p>', })-
extensions: 配置编辑器使用的扩展 -
content: 初始内容(HTML 格式)
-
-
渲染编辑器
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
代码解析:
-
空值检查
if (!editor) return null首次渲染时编辑器可能为 null,需要检查。
-
Commands 链式调用
editor.chain().focus().toggleBold().run()-
chain(): 开始链式调用 -
focus(): 让编辑器获得焦点 -
toggleBold(): 切换加粗状态 -
run(): 执行命令链
-
-
检查激活状态
editor.isActive('bold')用于高亮当前激活的按钮。
-
检查命令可用性
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 测试工具栏
保存文件后,你应该能看到:
- 一个漂亮的工具栏
- 点击按钮可以格式化文字
- 激活的按钮会高亮显示
- 不可用的按钮会被禁用
测试步骤:
- 选中一些文字
- 点击 "B" 按钮,文字应该变粗
- 按钮应该高亮显示
- 再次点击,文字恢复正常
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 配置选项说明
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
extensions |
Extension[] |
必需 | 编辑器使用的扩展数组 |
content |
string | JSONContent |
'' |
初始内容(HTML 或 JSON) |
editable |
boolean |
true |
是否可编辑 |
autofocus |
boolean | 'start' | 'end' |
false |
自动获取焦点 |
onUpdate |
function |
- | 内容更新时触发 |
onCreate |
function |
- | 编辑器创建时触发 |
onFocus |
function |
- | 获得焦点时触发 |
onBlur |
function |
- | 失去焦点时触发 |
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()