TipTap 富文本编辑器在小说写作中的应用实践
🚀 TipTap 富文本编辑器在小说写作中的应用实践
💡 本文详细介绍如何在 Electron + Vue 3 项目中集成 TipTap 富文本编辑器,实现专业的写作体验,包括自定义扩展、实时统计、自动保存等核心功能。
📋 目录
- 项目背景
- TipTap 技术架构
- 核心功能实现
- 自定义扩展开发
- 性能优化策略
- 用户体验优化
- 总结与展望
🎯 项目背景
在开发 51mazi 小说写作软件时,我们需要一个功能强大、可扩展性强的富文本编辑器。经过技术选型对比,最终选择了基于 ProseMirror 的 TipTap 编辑器,它提供了:
- 🎨 丰富的编辑功能: 支持粗体、斜体、对齐等基础格式
- 🔧 高度可扩展: 支持自定义扩展和插件
- ⚡ 优秀的性能: 基于 ProseMirror 的高性能架构
- 🎯 Vue 3 集成: 原生支持 Vue 3 Composition API
专业的写作编辑器界面 - 基于 TipTap 的富文本编辑器
🏗️ TipTap 技术架构
技术栈选择
// package.json 依赖
{"@tiptap/vue-3": "^2.12.0","@tiptap/starter-kit": "^2.12.0","@tiptap/extension-bold": "^2.12.0","@tiptap/extension-italic": "^2.12.0","@tiptap/extension-text-align": "^2.12.0"
}
核心架构设计
// src/renderer/src/components/EditorPanel.vue
import { Editor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Bold from '@tiptap/extension-bold'
import Italic from '@tiptap/extension-italic'
import TextAlign from '@tiptap/extension-text-align'const editor = new Editor({extensions: [StarterKit,Bold,Italic,TextAlign.configure({ types: ['heading', 'paragraph'] }),TabInsert, // 自定义 Tab 键扩展Collapsible // 自定义折叠扩展],content: editorStore.content,editorProps: {attributes: {style: () =>`font-family: ${fontFamily.value}; font-size: ${fontSize.value}; line-height: ${lineHeight.value}; text-align: ${align.value}; white-space: pre-wrap;`}},onUpdate: ({ editor }) => {const content = editor.getText()editorStore.setContent(content)// 防抖自动保存if (saveTimer) clearTimeout(saveTimer)saveTimer = setTimeout(() => {autoSaveContent()}, 1000)}
})
🔧 核心功能实现
1. 编辑器初始化与配置
// 编辑器配置
const editorConfig = {extensions: [StarterKit,Bold,Italic,TextAlign.configure({ types: ['heading', 'paragraph'] }),TabInsert,Collapsible],content: editorStore.content,editorProps: {attributes: {style: () => getEditorStyle()}},onUpdate: handleContentUpdate,onFocus: handleEditorFocus,onBlur: handleEditorBlur
}// 获取编辑器样式
function getEditorStyle() {return `font-family: ${fontFamily.value}; font-size: ${fontSize.value}; line-height: ${lineHeight.value}; text-align: ${align.value}; white-space: pre-wrap;padding: 20px;min-height: 500px;outline: none;`
}
2. 工具栏功能实现
<template><div class="editor-toolbar"><!-- 字体选择 --><el-select v-model="fontFamily" class="toolbar-item" size="small"><el-option label="默认" value="inherit" /><el-option label="宋体" value="SimSun" /><el-option label="微软雅黑" value="Microsoft YaHei" /><el-option label="楷体" value="KaiTi" /><el-option label="黑体" value="SimHei" /></el-select><!-- 字号选择 --><el-select v-model="fontSize" class="toolbar-item" size="small"><el-option label="12px" value="12px" /><el-option label="14px" value="14px" /><el-option label="16px" value="16px" /><el-option label="18px" value="18px" /><el-option label="20px" value="20px" /></el-select><!-- 行高选择 --><el-select v-model="lineHeight" class="toolbar-item" size="small"><el-option label="1.2" value="1.2" /><el-option label="1.4" value="1.4" /><el-option label="1.6" value="1.6" /><el-option label="1.8" value="1.8" /><el-option label="2.0" value="2" /></el-select><!-- 格式按钮 --><el-buttonclass="toolbar-item"size="small":type="isBold ? 'primary' : 'default'"@click="toggleBold"><b>B</b></el-button><el-buttonclass="toolbar-item"size="small":type="isItalic ? 'primary' : 'default'"@click="toggleItalic"><i>I</i></el-button><!-- 操作按钮 --><el-button size="small" class="toolbar-item" @click="copyContent"><el-icon><DocumentCopy /></el-icon></el-button><el-button size="small" class="toolbar-item" type="primary" @click="saveContent">保存</el-button></div>
</template><script setup>
import { ref, computed } from 'vue'
import { DocumentCopy } from '@element-plus/icons-vue'const fontFamily = ref('inherit')
const fontSize = ref('16px')
const lineHeight = ref('1.6')
const align = ref('left')// 格式状态
const isBold = computed(() => editor.value?.isActive('bold'))
const isItalic = computed(() => editor.value?.isActive('italic'))// 切换格式
function toggleBold() {editor.value?.chain().focus().toggleBold().run()
}function toggleItalic() {editor.value?.chain().focus().toggleItalic().run()
}// 复制内容
function copyContent() {const content = editor.value?.getText()if (content) {navigator.clipboard.writeText(content)ElMessage.success('内容已复制到剪贴板')}
}
</script>
3. 实时统计功能
// src/renderer/src/stores/editor.js
export const useEditorStore = defineStore('editor', () => {const content = ref('')const typingStartTime = ref(null)const initialWordCount = ref(0)const typingSpeed = ref({perMinute: 0,perHour: 0})// 计算当前字数const chapterWords = computed(() => {return content.value.length})// 开始计时function startTypingTimer() {if (!typingStartTime.value) {typingStartTime.value = Date.now()initialWordCount.value = chapterWords.value}}// 更新码字速度function updateTypingSpeed() {if (!typingStartTime.value) returnconst now = Date.now()const timeElapsed = (now - typingStartTime.value) / 1000const wordsTyped = chapterWords.value - initialWordCount.valueif (timeElapsed > 0) {typingSpeed.value = {perMinute: Math.round((wordsTyped / timeElapsed) * 60),perHour: Math.round((wordsTyped / timeElapsed) * 3600)}}}// 设置内容时触发统计function setContent(newContent) {content.value = newContentstartTypingTimer()updateTypingSpeed()}return {content,chapterWords,typingSpeed,setContent,resetTypingTimer}
})
🔧 自定义扩展开发
1. Tab 键插入扩展
// src/renderer/src/extensions/TabInsert.js
import { Extension } from '@tiptap/core'export const TabInsert = Extension.create({name: 'tabInsert',addKeyboardShortcuts() {return {Tab: () => {this.editor.commands.insertContent('\t')return true}}}
})
2. 折叠功能扩展
// src/renderer/src/extensions/Collapsible.js
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'export const Collapsible = Extension.create({name: 'collapsible',addProseMirrorPlugins() {return [new Plugin({key: new PluginKey('collapsible'),props: {handleDOMEvents: {keydown: (view, event) => {// 处理折叠逻辑if (event.key === 'Tab' && event.shiftKey) {// 处理 Shift+Tab 折叠return true}return false}}}})]}
})
3. 自定义样式扩展
// src/renderer/src/extensions/CustomStyles.js
import { Extension } from '@tiptap/core'export const CustomStyles = Extension.create({name: 'customStyles',addGlobalAttributes() {return [{types: ['paragraph'],attributes: {customStyle: {default: null,parseHTML: element => element.getAttribute('data-custom-style'),renderHTML: attributes => {if (!attributes.customStyle) return {}return {'data-custom-style': attributes.customStyle,style: attributes.customStyle}}}}}]}
})
⚡ 性能优化策略
1. 防抖自动保存
// 防抖自动保存实现
let saveTimer = nullfunction handleContentUpdate({ editor }) {const content = editor.getText()editorStore.setContent(content)// 防抖保存if (saveTimer) clearTimeout(saveTimer)saveTimer = setTimeout(() => {autoSaveContent()}, 1000)
}async function autoSaveContent() {try {const file = editorStore.fileif (!file) returnconst content = editorStore.contentawait window.electron.writeFile(file.path, content)console.log('自动保存成功')} catch (error) {console.error('自动保存失败:', error)}
}
2. 内容渲染优化
// 纯文本转 HTML 优化
function plainTextToHtml(text) {if (!text) return ''const lines = text.split('\n')const htmlLines = lines.map((line) => {// 替换 Tab 为空格let html = line.replace(/\t/g, ' ')// 替换连续空格html = html.replace(/ {2,}/g, (match) => ' '.repeat(match.length))return html ? `<p>${html}</p>` : ''})return htmlLines.join('')
}
3. 内存管理
// 组件销毁时清理资源
onBeforeUnmount(async () => {if (saveTimer) clearTimeout(saveTimer)// 保存最后的内容await autoSaveContent()// 重置码字统计editorStore.resetTypingTimer()// 销毁编辑器editor.value && editor.value.destroy()
})
🎨 用户体验优化
1. 响应式工具栏
<template><div class="editor-stats"><span class="word-count">章节字数:{{ chapterWords }}</span><span v-if="typingSpeed.perMinute > 0" class="typing-speed">码字速度:{{ typingSpeed.perMinute }}字/分钟 ({{ typingSpeed.perHour }}字/小时)</span></div>
</template><style lang="scss" scoped>
.editor-stats {display: flex;justify-content: space-between;align-items: center;padding: 10px 20px;background-color: var(--bg-soft);border-top: 1px solid var(--border-color);font-size: 12px;color: var(--text-secondary);
}.word-count {font-weight: 500;
}.typing-speed {color: var(--text-muted);
}
</style>
2. 快捷键支持
// 快捷键配置
const keyboardShortcuts = {'Ctrl+S': () => saveContent(),'Ctrl+B': () => toggleBold(),'Ctrl+I': () => toggleItalic(),'Ctrl+Z': () => editor.value?.chain().focus().undo().run(),'Ctrl+Y': () => editor.value?.chain().focus().redo().run()
}// 注册快捷键
onMounted(() => {document.addEventListener('keydown', (event) => {const key = getKeyCombination(event)const handler = keyboardShortcuts[key]if (handler) {event.preventDefault()handler()}})
})function getKeyCombination(event) {const keys = []if (event.ctrlKey) keys.push('Ctrl')if (event.shiftKey) keys.push('Shift')if (event.altKey) keys.push('Alt')keys.push(event.key.toUpperCase())return keys.join('+')
}
3. 主题适配
// 编辑器主题样式
.editor-content {.ProseMirror {background-color: var(--bg-primary);color: var(--text-primary);border: 1px solid var(--border-color);border-radius: 8px;min-height: 500px;padding: 20px;&:focus {outline: none;border-color: var(--primary-color);box-shadow: 0 0 0 2px var(--primary-color-alpha);}p {margin: 0 0 1em 0;line-height: 1.6;}p:last-child {margin-bottom: 0;}}
}
📊 功能特性总结
✅ 已实现功能
- ✅ 基础编辑: 文本输入、删除、选择
- ✅ 格式控制: 粗体、斜体、对齐方式
- ✅ 字体设置: 字体族、字号、行高
- ✅ 实时统计: 字数统计、码字速度
- ✅ 自动保存: 防抖机制、本地存储
- ✅ 快捷键: 常用操作快捷键支持
- ✅ 主题适配: 多主题模式支持
🚀 技术亮点
- 高性能: 基于 ProseMirror 的高性能架构
- 可扩展: 支持自定义扩展和插件
- 用户友好: 直观的工具栏和统计信息
- 数据安全: 自动保存和错误处理机制
📝 总结与展望
TipTap 富文本编辑器在 51mazi 项目中的成功应用,展示了如何利用现代化的前端技术构建专业的写作工具。通过合理的架构设计、性能优化和用户体验优化,我们实现了一个功能完整、性能优秀的编辑器。
🎯 技术价值
- 架构设计: 模块化的扩展系统
- 性能优化: 防抖、懒加载等优化策略
- 用户体验: 实时统计、快捷键等交互优化
- 可维护性: 清晰的代码结构和状态管理
🔮 未来规划
- 更多格式: 支持更多文本格式和样式
- 协作功能: 多人协作编辑支持
- 版本控制: 更完善的版本管理功能
- 插件生态: 支持第三方插件扩展
📚 相关链接
- 项目地址: GitHub - 51mazi,给个 Star 哦~
- TipTap 官网: https://tiptap.dev
- 技术栈: TipTap + Vue 3 + Electron + Element Plus
🏷️ 标签
#TipTap
#富文本编辑
#Vue3
#Electron
#小说写作
#前端开发
#性能优化
💡 如果这篇文章对你有帮助,请给个 ⭐️ 支持一下!