富文本编辑器
Vue 富文本编辑器工具。
TipTap
基本信息
- 简介: 无头、框架无关的富文本编辑器
- 链接: https://tiptap.dev/
- GitHub: https://github.com/ueberdosis/tiptap
- npm:
@tiptap/vue-3
特点
- ✅ 基于 ProseMirror
- ✅ 完全无头(Headless),UI 完全可定制
- ✅ 模块化设计,按需引入扩展
- ✅ TypeScript 支持
- ✅ 协同编辑支持
- ✅ Markdown 快捷键
- ✅ 现代化 API
安装
bash
npm install @tiptap/vue-3 @tiptap/starter-kit基础用法
vue
<script setup lang="ts">
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
const editor = useEditor({
content: '<p>Hello World! 🌍️</p>',
extensions: [
StarterKit,
],
})
</script>
<template>
<EditorContent :editor="editor" />
</template>
<style>
/* 基础样式 */
.ProseMirror {
padding: 1rem;
border: 1px solid #ccc;
border-radius: 4px;
min-height: 200px;
}
.ProseMirror:focus {
outline: none;
border-color: #0abab5;
}
</style>带工具栏的完整示例
vue
<script setup lang="ts">
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { ref } from 'vue'
const editor = useEditor({
content: '<p>开始编辑...</p>',
extensions: [StarterKit],
})
</script>
<template>
<div class="editor" v-if="editor">
<!-- 工具栏 -->
<div class="toolbar">
<button
@click="editor.chain().focus().toggleBold().run()"
:class="{ 'is-active': editor.isActive('bold') }"
>
粗体
</button>
<button
@click="editor.chain().focus().toggleItalic().run()"
:class="{ 'is-active': editor.isActive('italic') }"
>
斜体
</button>
<button
@click="editor.chain().focus().toggleStrike().run()"
:class="{ 'is-active': editor.isActive('strike') }"
>
删除线
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 1 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 1 }) }"
>
H1
</button>
<button
@click="editor.chain().focus().toggleHeading({ level: 2 }).run()"
:class="{ 'is-active': editor.isActive('heading', { level: 2 }) }"
>
H2
</button>
<button
@click="editor.chain().focus().toggleBulletList().run()"
:class="{ 'is-active': editor.isActive('bulletList') }"
>
无序列表
</button>
<button
@click="editor.chain().focus().toggleOrderedList().run()"
:class="{ 'is-active': editor.isActive('orderedList') }"
>
有序列表
</button>
<button
@click="editor.chain().focus().toggleCodeBlock().run()"
:class="{ 'is-active': editor.isActive('codeBlock') }"
>
代码块
</button>
<button @click="editor.chain().focus().undo().run()">
撤销
</button>
<button @click="editor.chain().focus().redo().run()">
重做
</button>
</div>
<!-- 编辑器内容 -->
<EditorContent :editor="editor" />
</div>
</template>
<style scoped>
.editor {
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
}
.toolbar {
display: flex;
gap: 4px;
padding: 8px;
background: #f5f5f5;
border-bottom: 1px solid #ddd;
flex-wrap: wrap;
}
.toolbar button {
padding: 6px 12px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.toolbar button:hover {
background: #e9e9e9;
}
.toolbar button.is-active {
background: #0abab5;
color: white;
border-color: #0abab5;
}
.ProseMirror {
padding: 16px;
min-height: 300px;
outline: none;
}
.ProseMirror h1 {
font-size: 2em;
margin: 0.5em 0;
}
.ProseMirror h2 {
font-size: 1.5em;
margin: 0.5em 0;
}
.ProseMirror code {
background: #f4f4f4;
padding: 2px 4px;
border-radius: 3px;
font-family: monospace;
}
.ProseMirror pre {
background: #1e1e1e;
color: #d4d4d4;
padding: 12px;
border-radius: 4px;
overflow-x: auto;
}
</style>常用扩展
bash
# 图片支持
npm install @tiptap/extension-image
# 链接支持
npm install @tiptap/extension-link
# 表格支持
npm install @tiptap/extension-table @tiptap/extension-table-row @tiptap/extension-table-cell @tiptap/extension-table-header
# 高亮支持
npm install @tiptap/extension-highlight
# 文本对齐
npm install @tiptap/extension-text-align
# 占位符
npm install @tiptap/extension-placeholder使用扩展示例
vue
<script setup lang="ts">
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import Image from '@tiptap/extension-image'
import Link from '@tiptap/extension-link'
import Highlight from '@tiptap/extension-highlight'
import Placeholder from '@tiptap/extension-placeholder'
const editor = useEditor({
extensions: [
StarterKit,
Image,
Link.configure({
openOnClick: false,
}),
Highlight.configure({
multicolor: true,
}),
Placeholder.configure({
placeholder: '开始输入内容...',
}),
],
content: '',
})
// 插入图片
const addImage = () => {
const url = window.prompt('图片 URL')
if (url) {
editor.value.chain().focus().setImage({ src: url }).run()
}
}
// 添加链接
const setLink = () => {
const url = window.prompt('链接 URL')
if (url) {
editor.value.chain().focus().setLink({ href: url }).run()
}
}
</script>获取和设置内容
vue
<script setup lang="ts">
import { useEditor, EditorContent } from '@tiptap/vue-3'
import StarterKit from '@tiptap/starter-kit'
import { ref } from 'vue'
const editor = useEditor({
extensions: [StarterKit],
content: '<p>初始内容</p>',
})
// 获取 HTML
const getHTML = () => {
console.log(editor.value.getHTML())
}
// 获取 JSON
const getJSON = () => {
console.log(editor.value.getJSON())
}
// 获取纯文本
const getText = () => {
console.log(editor.value.getText())
}
// 设置内容
const setContent = () => {
editor.value.commands.setContent('<p>新内容</p>')
}
// 清空内容
const clearContent = () => {
editor.value.commands.clearContent()
}
</script>优势
- 🎨 完全可定制: UI 完全由你控制
- 📦 模块化: 只引入需要的功能
- 🚀 性能优异: 基于 ProseMirror
- 🤝 协同编辑: 内置协同编辑支持
- 📱 移动端友好: 响应式设计
适用场景
- 需要高度定制 UI 的项目
- 需要协同编辑功能
- 现代化的 Web 应用
- 需要精确控制编辑器行为
wangEditor
基本信息
- 简介: 开源 Web 富文本编辑器,开箱即用
- 链接: https://www.wangeditor.com/
- GitHub: https://github.com/wangeditor-team/wangEditor
- npm:
@wangeditor/editor,@wangeditor/editor-for-vue
特点
- ✅ 开箱即用,配置简单
- ✅ 中文文档完善
- ✅ 功能丰富(表格、代码高亮、上传图片等)
- ✅ 轻量级(gzip 后约 40kb)
- ✅ TypeScript 支持
- ✅ 支持 Vue 2/3
- ✅ 国内团队维护
安装
bash
# Vue 3
npm install @wangeditor/editor @wangeditor/editor-for-vue@next
# Vue 2
npm install @wangeditor/editor @wangeditor/editor-for-vue基础用法(Vue 3)
vue
<script setup lang="ts">
import { onBeforeUnmount, ref, shallowRef } from 'vue'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
// 编辑器实例,必须用 shallowRef
const editorRef = shallowRef()
// 内容 HTML
const valueHtml = ref('<p>hello</p>')
const toolbarConfig = {}
const editorConfig = {
placeholder: '请输入内容...',
}
// 组件销毁时,也及时销毁编辑器
onBeforeUnmount(() => {
const editor = editorRef.value
if (editor == null) return
editor.destroy()
})
const handleCreated = (editor) => {
editorRef.value = editor // 记录 editor 实例,重要!
}
const handleChange = (editor) => {
console.log('content changed', editor.getHtml())
}
</script>
<template>
<div style="border: 1px solid #ccc">
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
:defaultConfig="toolbarConfig"
mode="default"
/>
<Editor
style="height: 500px; overflow-y: hidden;"
v-model="valueHtml"
:defaultConfig="editorConfig"
mode="default"
@onCreated="handleCreated"
@onChange="handleChange"
/>
</div>
</template>工具栏配置
vue
<script setup lang="ts">
const toolbarConfig = {
// 显示哪些菜单
toolbarKeys: [
'headerSelect',
'bold',
'italic',
'underline',
'through',
'|',
'color',
'bgColor',
'|',
'fontSize',
'fontFamily',
'lineHeight',
'|',
'bulletedList',
'numberedList',
'todo',
'|',
'justifyLeft',
'justifyCenter',
'justifyRight',
'justifyJustify',
'|',
'indent',
'delIndent',
'|',
'insertLink',
'insertImage',
'insertVideo',
'insertTable',
'codeBlock',
'divider',
'|',
'undo',
'redo',
'|',
'fullScreen',
],
// 排除哪些菜单
excludeKeys: [
'group-video', // 排除视频
],
}
</script>编辑器配置
vue
<script setup lang="ts">
const editorConfig = {
placeholder: '请输入内容...',
// 自动 focus
autoFocus: true,
// 只读模式
readOnly: false,
// 最大长度限制
maxLength: 10000,
// 配置上传图片
MENU_CONF: {
uploadImage: {
// 服务端地址
server: '/api/upload',
// 单个文件的最大体积限制,默认为 2M
maxFileSize: 2 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['image/*']
allowedFileTypes: ['image/*'],
// 自定义上传参数
meta: {
token: 'xxx',
},
// 自定义增加 http header
headers: {
Authorization: 'Bearer xxx',
},
// 跨域是否传递 cookie
withCredentials: true,
// 超时时间,默认为 10 秒
timeout: 10 * 1000,
// 自定义插入图片
customInsert(res, insertFn) {
// res 即服务端的返回结果
const url = res.data.url
const alt = res.data.alt
const href = res.data.href
// 从 res 中找到 url alt href ,然后插入图片
insertFn(url, alt, href)
},
},
},
}
</script>自定义上传图片
vue
<script setup lang="ts">
const editorConfig = {
MENU_CONF: {
uploadImage: {
// 自定义上传
async customUpload(file, insertFn) {
// file 即选中的文件
// 自己实现上传,并得到图片 url alt href
const formData = new FormData()
formData.append('file', file)
const res = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await res.json()
// 最后插入图片
insertFn(data.url, data.alt, data.href)
},
},
},
}
</script>获取和设置内容
vue
<script setup lang="ts">
import { shallowRef } from 'vue'
const editorRef = shallowRef()
// 获取 HTML
const getHtml = () => {
const html = editorRef.value.getHtml()
console.log(html)
}
// 获取纯文本
const getText = () => {
const text = editorRef.value.getText()
console.log(text)
}
// 设置 HTML
const setHtml = () => {
editorRef.value.setHtml('<p>新内容</p>')
}
// 清空内容
const clear = () => {
editorRef.value.clear()
}
// 禁用/启用
const disable = () => {
editorRef.value.disable()
}
const enable = () => {
editorRef.value.enable()
}
</script>优势
- 📦 开箱即用: 默认配置即可使用
- 🇨🇳 中文友好: 中文文档完善
- 🎯 功能全面: 常用功能都内置
- 🪶 轻量级: 体积小,性能好
- 🛠️ 易于配置: 配置简单直观
适用场景
- 快速开发,不需要过度定制
- 中文项目
- 需要完整功能的编辑器
- 对体积有一定要求
对比选择
| 特性 | TipTap | wangEditor |
|---|---|---|
| UI 定制 | ✅✅✅ 完全自定义 | ⚠️ 有限定制 |
| 上手难度 | ⚠️ 较高 | ✅ 简单 |
| 开箱即用 | ❌ 需要自己构建 UI | ✅ 开箱即用 |
| 功能丰富度 | ✅ 通过扩展实现 | ✅ 内置丰富功能 |
| 体积 | ⚠️ 按需引入 | ✅ 约 40kb (gzip) |
| TypeScript | ✅ 完整支持 | ✅ 支持 |
| 协同编辑 | ✅ 内置支持 | ❌ 不支持 |
| 中文文档 | ⚠️ 英文为主 | ✅ 完善 |
| 移动端 | ✅ 友好 | ✅ 支持 |
| 学习曲线 | 陡峭 | 平缓 |
| 社区活跃度 | ✅✅ 非常活跃 | ✅ 活跃 |
推荐选择
选择 TipTap 如果你:
- 需要高度定制 UI
- 需要协同编辑功能
- 追求现代化的开发体验
- 有时间学习和定制
选择 wangEditor 如果你:
- 需要快速上手
- 不需要过度定制
- 中文项目
- 需要开箱即用的完整功能
其他选择
Quill
- 链接: https://quilljs.com/
- 特点: 老牌编辑器,功能强大,但更新较慢
Slate
- 链接: https://www.slatejs.org/
- 特点: React 专用,完全可定制,学习曲线陡峭
CKEditor 5
- 链接: https://ckeditor.com/ckeditor-5/
- 特点: 功能最全面,但体积较大,商业项目需付费