代码语言
知识点思维导图
13 个知识节点
参考资料
富文本编辑器(02) - 样式隔离
读完后,你应能完成以下任务:
- 绘制“富文本编辑器(02) - 样式隔离 / 前言”的关键对象与数据流,解释“在开发富文本编辑器时,样式隔离是一个很关键的问题。”,并用源码位置、日志或 Trace 标注证据。
- 为“富文本编辑器(02) - 样式隔离 / 样式隔离面临的挑战”设计正常与异常输入,验证“页面的 h1、h2 等标题样式可能影响编辑器内的标题显示”,输出首个偏差位置与回归测试结果。
- 实现“富文本编辑器(02) - 样式隔离 / 编辑器样式泄露”的最小代码或配置,检验“编辑器的字体、颜色设置可能影响页面布局”,输出命令、结果与 Diff,并说明不适用边界。
区别于官网的样式方案:https://tiptap.dev/docs/editor/getting-started/style-editor(sass + tailwind),也就是 @apply,这个方式就是需要切换css文件,然后嵌套样式,也可以下面这种方案,纯 JS 的方式写 TailwindCSS
一、前言
在开发富文本编辑器时,样式隔离是一个很关键的问题。编辑器的样式需要与页面其他部分的样式完全隔离,避免相互影响。同时,编辑器内部的各种组件(如标题、段落、列表等)也需要有统一的样式管理方案。
这篇我们来讲讲如何基于 Tailwind CSS 实现 TipTap 编辑器的样式隔离,提供一个可扩展、易维护的样式架构方案。(普通使用还是基于 simple template 去修改比较好优点是开箱即用,缺点是开发效率和性能要差点)
二、样式隔离面临的挑战
2.1 全局样式污染
编辑器运行在页面环境中,容易受到全局 CSS 样式的影响:
- 页面的
h1、h2等标题样式可能影响编辑器内的标题显示 - 全局的
p、ul、ol等元素样式会干扰编辑器内容 - 第三方组件库的样式可能意外影响编辑器
2.2 编辑器样式泄露
编辑器内部的样式可能影响页面其他部分:
- 编辑器的字体、颜色设置可能影响页面布局
- 编辑器的边距、内边距设置可能破坏页面设计
- 编辑器的主题切换可能影响页面其他组件
2.3 主题兼容性问题
编辑器需要支持多种主题,同时与页面主题保持一致:
- 亮色/暗色主题切换时,编辑器样式需要同步更新
- 不同主题下的颜色对比度需要符合可访问性标准
- 主题切换时的过渡动画需要平滑自然
2.4 组件嵌套样式冲突
编辑器内部组件之间的样式可能相互冲突:
- 多级列表的嵌套样式问题
- 自定义组件(如高亮块、分栏)与基础组件的样式冲突
- 组件间距的上下边距重叠问题
三、基于 Tailwind 的样式隔离方案
3.1 核心设计理念
我们的样式隔离方案基于以下核心理念:
- 完全无 CSS 方案 - 所有样式都通过 Tailwind 类名实现,避免自定义 CSS 文件
- 作用域隔离 - 使用 Tailwind 的作用域前缀确保样式不会泄露
- 主题一致性 - 编辑器主题与页面主题保持同步
- 可扩展性 - 支持多种主题和自定义样式
3.2 样式抽离
首先,我们需要把样式抽离出去,方便维护,也就是当前目录下的 style.ts:
"use client"
import { useEditor, EditorContent } from "@tiptap/react"
import StarterKit from "@tiptap/starter-kit"
import Placeholder from "@tiptap/extension-placeholder"
import "./tiptap.css"
// 样式组件化
import { EditorAllClassNames } from "./style"
const Tiptap = () => {
const editor = useEditor({
extensions: [
StarterKit,
Placeholder.configure({
placeholder: `Write something, ' / ' for commands…`
})
],
content: "",
immediatelyRender: false,
editorProps: {
attributes: {
class: "focus:outline-none min-h-80"
}
}
})
return <EditorContent editor={editor} className={EditorAllClassNames} />
}
export default Tiptap
3.3 prosemirror-devtools
然后按谷歌插件,prosemirror-devtools,来依次写样式,这样可以知道编辑器已经有哪些扩展,也避免某些扩展没有写到样式,对于不需要样式的我们也写个空串,cn 函数会帮我们优化类名问题

import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
3.4 组件样式定义
style.ts 定义样式
import { cn } from "@/lib/utils"
// ============================ NODES ============================
// #region
// 编辑器容器样式
export const EditorContainerClassNames = "mx-auto max-w-3xl px-4 pt-24 pb-8 2xl:max-w-4xl"
// 正文:16 1.6 2%,组件间距 16px
export const paragraphClassNames =
"[&_p]:text-base [&_p]:leading-[1.6] [&_p]:tracking-[2%] [&_p]:my-4"
// 引用 - 支持多种颜色
export const blockquoteClassNames = cn(
"[&_blockquote]:border-l-4 [&_blockquote]:pl-4 [&_blockquote]:my-4 [&_blockquote]:border-gray-500"
)
// 无序列表
export const bulletListClassNames = "[&_ul]:!list-disc [&_ul]:pl-4 [&_ul]:my-4"
// 代码块 pre->code,快捷键
export const codeBlockClassNames =
"[&_pre]:bg-white [&_pre]:dark:bg-black [&_pre]:border [&_pre]:border-gray-200 [&_pre]:dark:border-gray-800 [&_pre]:p-4 [&_pre]:my-4 [&_pre]:block [&_pre]:rounded-md [&_pre_code]:bg-transparent [&_pre_code]:border-0 [&_pre_code]:p-0"
// 标题组件
export const headingClassNames = cn(
"[&_h1]:text-[28px] [&_h1]:leading-[1.5] [&_h1]:tracking-[2%] [&_h1]:my-4 [&_h1]:font-semibold",
"[&_h2]:text-[22px] [&_h2]:leading-[1.5] [&_h2]:tracking-[2%] [&_h2]:my-4 [&_h2]:font-semibold",
"[&_h3]:text-[18px] [&_h3]:leading-[1.6] [&_h3]:tracking-[2%] [&_h3]:my-4 [&_h3]:font-semibold",
"[&_h4]:text-[16px] [&_h4]:leading-[1.6] [&_h4]:tracking-[2%] [&_h4]:my-4 [&_h4]:font-semibold",
"[&_h5]:text-[14px] [&_h5]:leading-[1.6] [&_h5]:tracking-[2%] [&_h5]:my-4 [&_h5]:font-semibold",
"[&_h6]:text-[12px] [&_h6]:leading-[1.6] [&_h6]:tracking-[2%] [&_h6]:my-4 [&_h6]:font-semibold"
)
// 分割线
export const horizontalRuleClassNames =
"[&_hr]:my-4 [&_hr]:h-px [&_hr]:bg-gray-400 [&_hr]:border-0"
// 列表项,li,这样可以控制 li 节点,暂时没用上
export const listItemClassNames = "[&_li]:my-1"
// 有序列表
export const orderedListClassNames = "[&_ol]:!list-decimal [&_ol]:pl-4 [&_ol]:my-4"
// #endregion
// ============================ MARKS ============================
// #region
// 超链接
export const linkClassNames = "[&_a]:text-blue-500 [&_a]:underline [&_a]:cursor-pointer"
// 粗体
export const boldClassNames = "[&_strong]:font-bold"
// 行内 code,快捷键 `` 或者 command + e
export const codeClassNames =
"[&_code:not(pre_code)]:bg-white [&_code:not(pre_code)]:dark:bg-black [&_code:not(pre_code)]:border [&_code:not(pre_code)]:border-gray-200 [&_code:not(pre_code)]:dark:border-gray-800 [&_code:not(pre_code)]:inline [&_code:not(pre_code)]:px-1 [&_code:not(pre_code)]:text-sm [&_code:not(pre_code)]:py-0.5 [&_code:not(pre_code)]:rounded-md"
// 斜体 em command + i
export const italicClassNames = ""
// 删除线 s command + shift + s
export const strikeClassNames = ""
// 下滑线 u command + u
export const underlineClassNames = ""
// #endregion
// 统一导出
export const EditorAllClassNames = cn(
EditorContainerClassNames,
paragraphClassNames,
blockquoteClassNames,
bulletListClassNames,
codeBlockClassNames,
headingClassNames,
horizontalRuleClassNames,
listItemClassNames,
orderedListClassNames,
linkClassNames,
boldClassNames,
codeClassNames,
italicClassNames,
strikeClassNames,
underlineClassNames
)
3.5 其它复杂样式
这里我选择新建一个 tiptap.css 来写复杂的样式,比如下面是 placeholder 需要的样式
/* CSS 变量定义 */
:root {
--placeholder: #adb5bd;
}
.dark {
--placeholder: #6b7280;
}
/* editor placeholder */
.tiptap p.is-empty::before {
color: var(--placeholder);
content: attr(data-placeholder);
float: left;
height: 0;
pointer-events: none;
}
3.6 主题切换
四、总结
- 前言:在开发富文本编辑器时,样式隔离是一个很关键的问题。
- 基于 Tailwind 的样式隔离方案:完全无 CSS 方案 - 所有样式都通过 Tailwind 类名实现,避免自定义 CSS 文件 -> 作用域隔离 - 使用 Tailwind 的作用域前缀确保样式不会泄露 -> 主题一致性 - 编辑器主题与页面主题保持同步 -> 可扩展性 - 支持多种主题和自定义样式
- prosemirror-devtools:然后按谷歌插件,prosemirror-devtools,来依次写样式,这样可以知道编辑器已经有哪些扩展,也避免某些扩展没有写到样式,对于不需要样式的我们也写个空串,cn 函数会帮我们优化类名问题
- 工程边界:编辑器的样式需要与页面其他部分的样式完全隔离,避免相互影响。
- 实现机制:完全无 CSS 方案 - 所有样式都通过 Tailwind 类名实现,避免自定义 CSS 文件
- 选型依据:区别于官网的样式方案:https://tiptap.dev/docs/editor/getting-started/style-editor(sass +
学完自测
选择所有正确答案;提交后逐项核对判断依据。