更新
wangEditor V5 已正式发布,查看官网。
前言
新版本功能目前已经开发完,但尚未发布,代码还没有开源。待内部测试完成,即发布体验版、代码开源。
本文主要介绍一下新版本的功能升级、体验升级,以及我的一些思考总结。等后面有了阶段性的进展,我再来给大家分享。
产品的整体架构设计,在上一篇文章中已经介绍过了,没有太大变化。只做了一些属性 API 添加,再就是细节的代码重构。
当前状态
基本功能已经开发完了,现在刚开始组织内部测试。大家可以通过这个 demo 体验一下。但请注意:
- 可能还有很多 bug 和浏览器兼容性问题。如你发现了严重的 bug ,可给我留言。
- 不再支持 IE 浏览器
- 暂不支持移动端,先把 PC 浏览器(chrome safari firefox edge QQBrowser 等)支持好
- demo 是内部测试地址,随时可能调整和改变。如果看不了,可以给我留言。
内部测试将包含以下几个步骤:
- 用户功能测试。即终端用户可直接鼠标、键盘操作的功能,如:编辑器区域的输入、删除,样式操作,各个菜单等。我整理了一个表格,目前包含 68 个功能点,主要测试可用性和浏览器兼容性
- 配置和 API 测试。用户文档(尚未开放)中所有的安装、配置、API、扩展性,用于 Vue 和 React 。
- 目前已积累且关闭的近 4000 个 github issue ,全部在回归测试一遍。把该踩的坑都踩一遍。
未来工作
虽然基本功能已经开发完,但开发工作尚未结束,例如:
- 修复测试时的 bug ,code review 等,这会占用很大工作量
- i18n 多语言
- 单元测试和 e2e 测试
- CI/CD 流程
- Vue3 组件(目前只支持了 Vue2 ,Vue3 待发布体验版之后,再开发,会很快做出来)
另外,还有很多文档需要写
- 开发文档
- 用户文档(目前只写了一部分,还需要继续补充)
到这里,大家可能有一个疑问:为何到现在还没写单元测试呢?不是测试先行或者并行吗? 我来解释一下:
- 大家说的“测试先行或者并行”一般是在框架和基础确定、纯业务开发的情况,如我们日常工作的开发;还有就是模式和用法确定、只剩下功能实现的情况,如 Vue3 在开发时就测试并行。
- 而 wangEditor 新版本是一个探索性的项目,一边设计、一边试错,一边重构。今天写的 API 和 class ,明天可能就重构消失了,所以没法提前写单元测试。只能等待功能、配置和 API 都稳定之后,再来补充 —— 当然,写单元测试时,有可能还会去重构一部分代码,抽离逻辑,让它方便测试。
近期目标
当前是 7 月下旬,按照目前的工作量预估,还需要两个月的时间。其实主要看测试、修复 bug 的顺利程度。
所以,我目前的计划是:争取在 10.1 之前发布一个公测版本,保证没有严重、明显的 bug 和兼容性问题。
持久战
距离我上一篇文章 基于 slate.js(不依赖 React)设计富文本编辑器 已经过去快 2 个月了。当时我以为 2 周左右即可开发完基本功能,但现实很残酷。
我统计了一下,目前代码仓库已经超过了 2w 行代码,可见我这两个月的工作量是非常大的。
另外,大家也都清楚,编辑器是一个复杂度非常高的产品,所以这 2w 行代码可不是流水账一样写出来的,期间还有大量的设计、思考和重构。
它本质上就是一个持久战,其他国外的优秀富文本编辑器,开发周期也是按“年”来计算的,短期出不来东西。
所以,我在规划接下来的目标时,也没有那么激进,我会努力推进,但也要尊重事实,急不得。
这里我很庆幸自己当时选择了 slate.js (不依赖 React)作为内核,slate.js 是一个非常优秀的 core ,现在用起来非常爽,它可做了整整 4 年。
否则,我可能要花很长时间开发一个 core ,看不到任何 UI 的成果。而且做出来不一定就比 slate.js 好。
PS:并不是用 slate.js 作为 core 就很简单,依然很难,依然工作量很大。读这篇文章你应该就能体会到。
价值和优势
既然需要耗费那么大的经历,我为何要做它呢?也不挣钱。如果真把这些工作量换算成工资,那可真是不少钱。
很多人可能只知道点赞、加油,但不理解这个问题。
wangEditor 在很多年之前一开始的时候,是出于我个人的兴趣爱好和技术追求。但做到现在,特别是近期需要花费这么大精力去开发它,就不能单凭兴趣爱好和技术追求了 —— 否则成本也太高了。
我之所以会继续做它,是因为我看到了它的价值,或者看到它确实能解决很多用户问题。
而有价值的东西,迟早都会以某种方式变现的。
换句话说,如果现在真的有一个产品,跟我预想中的差不多,而且已经成熟且流行起来。那我就不会以现在这种方式来做了。
其他富文本编辑器的问题
大家能看到的其他富文本编辑器,如老牌的 ueditor kindeditor ,国外开源的 proseMirror slate quill draft ,以及非绝对开源的 tinyMCE CKEditor ,等等还有很多不是那么知名的。
你看着很多,感觉自己的选择范围很广。但如果你真的是富文本编辑器的用户,你会发现它们都存在各种个样的情况。这些情况不能叫做问题,但却会影响你的开发效率、开发成本、产品稳定性和扩展性。
- 技术老旧,依然使用
document.execCommand
- 中文不友好(英文很溜,或者需要英文场景的请忽略~)
- 只是一个 core ,功能都需要二次开发
- 有框架约束,如 slate draft
- 无官方 Vue React 等框架
- 踩的坑不够多,不够稳定(如新产品、小众产品)
以上问题,随便哪一个拿出来,都够你喝几壶。有这种体会的朋友,欢迎留言评论~
wangEditor 解决这些问题
创造价值就是解决用户的问题。wangEditor 新版本就是本着解决问题做的,而不仅仅是兴趣爱好、技术追求。这也是为何 wangEditor 要基于 slate.js 为内核,而不是非要自研内核。
对比上述问题,wangEditor 新版本的优势在于:
- 弃用
document.execCommand
,升级为 L1 能力(未来还会支持协同编辑) - 详细的中文文档、QQ 群,issue 处理流程
- 包含常见的所有功能,拿来就用,无需二次开发 (除非你有一些特殊需要)
- 无框架约束,可以用到任何地方(jQuery Vue React)
- 官方提供 Vue React 等组件,下文会有介绍
- 用户量大,踩过足够多的坑
使用
基本使用
wangEditor 新版本的使用将非常简单,通过以下代码可以体会一下:
<!-- 工具栏和编辑器强制分离,如何布局用户可自由发挥 -->
<div id="toolbar-container"></div>
<div id="editor-container"></div>
// 安装,或 CDN 引入(尚未发布,此处先忽略)
// 编辑器配置
const editorConfig = {}
editorConfig.placeholder = '请输入内容'
editorConfig.onChange = (editor) => {
// 当编辑器选区、内容变化时,即触发
console.log('content', editor.children)
console.log('html', editor.getHtml())
}
// 创建编辑器
const editor = wangEditor.createEditor({
textareaSelector: '#editor-container',
config: editorConfig,
content: [{ type: 'paragraph', children: [{ text: 'hello wangEditor' }] }], // 初始化内容
mode: 'default' // 或者 'simple'
})
// 创建工具栏
const toolbar = wangEditor.createToolbar({
editor, // 传入 editor ,以便让工具栏知道对应哪个编辑器
toolbarSelector: '#toolbar-container',
config: { /* toolbar config */ },
mode: 'default' // 或者 'simple'
})
工具栏可选
以上代码中 toolbar
是可选的,不需要可以不写。
即,没有工具栏,编辑器照样能正常使用。
自由控制 UI
相比于当前 V4 版本,新版本将不在帮用户生成 DOM ,用户可自定义 DOM 以实现自由的 UI 样式。例如:
- 工具栏放在下面
- 想要自定义 width height border 等
- 想要自定义 z-index
- 想要实现工具栏 fixed 到顶部
- 想要模仿腾讯文档、语雀文档的编辑器
配置和 API
wangEditor 支持丰富的配置、菜单和 API ,上述代码中没发一一演示,下文会有介绍。
你也可以去看看各个 demo 的源代码。
升级重点
功能增强
升级为 L1 能力
新版本弃用了 document.execCommand
API,升级为 L1 能力(有的叫 L2 ),彻底告别了 execCommand
那些莫名其妙的 bug 。
基于 slate 的数据模型,未来会探索协同编辑器。目前已经有了一些解决方案,如 slate-yjs 和 slate-collaborative
50+ 菜单
当前内置了 50+ 菜单,你可以自定义这些菜单的显示/隐藏,排序和分组。
可以在 demo 页执行 editor.getAllMenuKeys()
来查看上图。
55+ APIs
新版本定义了很多 API 进行各个方面的操作,如 config 、内容处理、DOM 操作、selection 、自定义事件等等。在 demo 页执行 editor
即可看到所有的 API 。
/**
* Editor 接口
*/
export interface IDomEditor extends Editor {
// event data 处理(粘贴、拖拽等),内部使用
insertData: (data: DataTransfer) => void
setFragmentData: (data: DataTransfer) => void
// config 相关 API
getConfig: () => IEditorConfig
getMenuConfig: (menuKey: string) => ISingleMenuConfig
getAllMenuKeys: () => string[]
alert: (info: string, type: AlertType) => void
// 内容处理 API
handleTab: () => void
getHtml: (withFormat?: boolean) => string
getText: () => string
getSelectionText: () => string
getHeaders: () => { id: string; type: string; text: string }[]
getParentNode: (node: Node) => Ancestor | null
// dom 相关 API
id: string
isDestroyed: boolean
isFullScreen: boolean
focus: () => void
isFocused: () => boolean
blur: () => void
updateView: () => void
destroy: () => void
scrollToElem: (id: string) => void
showProgressBar: (progress: number) => void
hidePanelOrModal: () => void
enable: () => void
disable: () => void
isDisabled: () => boolean
toDOMNode: (node: Node) => HTMLElement
fullScreen: () => void
unFullScreen: () => void
// selection 相关 API
select: (at: Location) => void
deselect: () => void
restoreSelection: () => void
getSelectionPosition: () => Partial<IPositionStyle>
getNodePosition: (node: Node) => Partial<IPositionStyle>
// 自定义事件 API
on: (type: string, listener: ee.EventListener) => void
off: (type: string, listener: ee.EventListener) => void
once: (type: string, listener: ee.EventListener) => void
emit: (type: string) => void
// undo redo
undo: () => void
redo: () => void
}
配置增强
相比于当前的 V4 版本,配置增加了
- 生命周期
onCreated
onDestroyed
onChange
- 悬浮菜单配置,如选中文字、选中链接、选中图片、选中表格、选中代码块
- 字数限制
maxLength
onMaxLength
/**
* editor config
*/
export interface IEditorConfig {
customAlert: (info: string, type: AlertType) => void
onCreated?: (editor: IDomEditor) => void
onChange?: (editor: IDomEditor) => void
onDestroyed?: (editor: IDomEditor) => void
onMaxLength?: (editor: IDomEditor) => void
onFocus?: (editor: IDomEditor) => void
onBlur?: (editor: IDomEditor) => void
// edit state
scroll: boolean
placeholder?: string
readOnly: boolean
autoFocus: boolean
maxLength?: number
// 悬浮菜单栏 menu
hoverbarKeys?: Array<IHoverbarConf>
}
可以在 demo 页执行 editor.getConfig()
看到所有配置
配置拆分
当前 V4 版本的配置都会混合在一起的,而新版本的配置是拆分为
- 编辑器配置(上文已说)
- 工具栏配置
- 各个菜单配置(下文再说,内容有点多)
工具栏配置,主要配置工具栏的菜单。接口定义如下:
/**
* toolbar config
*/
export interface IToolbarConfig {
toolbarKeys: Array<string | IMenuGroup>
excludeKeys: Array<string> // 排除哪些菜单
}
可以在 demo 页执行 toolbar.getConfig()
查看当前配置。
各个菜单的配置
当前 V4 版本,所有菜单的配置都在一起,如 editor.config.colors
配置颜色。当菜单过多时,这种方式就比较混乱了。所以新版本将菜单配置单独拆分出来。
// 编辑器配置,其中 MENU_CONF 存储各个菜单的配置
const editorConfig = { MENU_CONF: {} }
editorConfig.placeholder = '请输入内容'
// 各个菜单的配置
editorConfig.MENU_CONF['color'] = {
colors: ['#000', '#333', '#999', '#ccc']
}
editorConfig.MENU_CONF['fontSize'] = {
fontSizeList: ['12px', '16px', '24px', '40px']
}
// 最后创建编辑器
各个菜单的 key 可以通过 editor.getAllMenuKeys()
查看,上文已说。各个菜单的配置内容,可以通过 editor.getMenuConfig(menuKey)
来查看。如查看上传图片菜单的配置:
安全性增强
既然弃用了 document.execCommand
就不能直接操作 DOM 了。又基于 slate 的数据模型,所以使用 vdom 进行视图更新,我们是基于 snabbdom.js 来做 vdom patchView
使用 vdom 本身就可以过滤一部分 XSS 工具。另外代码中还对直接输出 html 的部分做了特殊字符的过滤,目前没有发现 XSS 的风险。
体验增强
要做“好用”的编辑器,要看功能、稳定性,还要看体验。
hoverbar 悬浮菜单
目前内置的 hoverbar 有几种:
- 选中文字
- 链接
- 图片
- 表格
- 代码块
hovarbar 也是 editor config 的一部分,可以支持配置、扩展。在 demo 可以查看配置
select 型菜单
像设置标题、字号、字体、行高等菜单,使用 select 类型,类似于 html 中的 <select>
menu group 菜单组
如果像把某些菜单折叠起来,可以使用菜单组。
配置也非常简单,可以自定义 icon title ,然后选择 menuKey 即可。
{
key: 'group-more-style', // 要以 group 开头
title: '更多样式',
iconSvg: '<svg>...</svg>',
menuKeys: ['through', 'code', 'clearStyle'], // 要折叠的菜单 key
}
菜单 disabled
例如,当光标在代码块中时,很多文本操作的菜单都是 disabed 的状态。因为代码块中的文本不可自行加样式。
这个规则可以在创建 menu 时自行定义,编辑器会在选区变化时,自动更新所有菜单的 disabled 。
// 文本样式(如 bold italic 等)的禁用规则
isDisabled(editor: IDomEditor): boolean {
if (editor.selection == null) return true
const [match] = Editor.nodes(editor, {
match: n => {
const type = DomEditor.getNodeType(n)
if (type === 'pre') return true // 代码块
if (Editor.isVoid(editor, n)) return true // void node
if (mark === 'bold') {
// header 中禁用 bold
if (type.startsWith('header')) return true
}
return false
},
universal: true,
})
// 命中,则禁用
if (match) return true
// 未命中,则不禁用
return false
}
代码高亮
新版本默认自带代码高亮功能,而且有 20 种编程语言可选择,不用再做任何加工。
分词是基于 prism.js 实现的,随着文字编辑,实时判断是否高亮。操作体验比 v4 要好很多。而且,输出的 html 引入 prism.js 即可显示代码高亮,实用非常方便。
有兴趣的可以参考 slate 代码高亮的 demo ,其中用到了 slate decorate 功能,非常赞。
规范化内容
富文本编辑器的内容,有时可能带来操作的问题。例如,用户从一开始就插入了一个代码块,此时想要在代码块上方再输入文字,怎么办?
我们的解决方案是:规范化内容 —— 如果代码块出现在第一个位置,就在前面强制加一个空 <p>
。问题就解决了。
类似于这样的问题还有很多,我们已经做了很多处理,可能还有不完善的,测试时再继续补充。
PS:有人可能会较真:往代码块前面加一个空 <p>
这不就不符合用户预期了吗?—— 这个我承认,不是 100% 符合用户预期。但如果我们从问题、解决方案、用户体验、成本、复杂度...这几个角度全面考虑,这个方案是不是最优的呢?
扩展性增强
slate 是基于插件机制的,wangEditor 新版本是基于 module 扩展机制的。即出了插件之外,module 还包括其他的功能,是一个功能模块的组合。
export interface IModuleConf {
// 注册菜单
menus: Array<IRegisterMenuConf>
// 渲染 modal -> view
renderTextStyle: RenderTextStyleFnType
renderElems: Array<IRenderElemConf>
// to html
textStyleToHtml: TextStyleToHtmlFnType
textToHtml: TextToHtmlFnType
elemsToHtml: Array<IElemToHtmlConf>
// slate 插件
editorPlugin: <T extends IDomEditor>(editor: T) => T
}
虽然产出的 wangEditor 包含很多内置的菜单和功能,但它内部就是将各个 module 拼接组合产出的。
// basic-modules 基本功能
import '@wangeditor/basic-modules/dist/css/style.css'
import basicModules from '@wangeditor/basic-modules'
// list-module
import '@wangeditor/list-module/dist/css/style.css'
import wangEditorListModule from '@wangeditor/list-module'
// table-module
import '@wangeditor/table-module/dist/css/style.css'
import wangEditorTableModule from '@wangeditor/table-module'
// video-module
import '@wangeditor/video-module/dist/css/style.css'
import wangEditorVideoModule from '@wangeditor/video-module'
// upload-image-module
import '@wangeditor/upload-image-module/dist/css/style.css'
import wangEditorUploadImageModule from '@wangeditor/upload-image-module'
// code-highlight
import '@wangeditor/code-highlight/dist/css/style.css'
import { wangEditorCodeHighlightModule } from '@wangeditor/code-highlight'
import registerModule from './register'
// 注册各个功能模块
basicModules.forEach(module => registerModule(module))
registerModule(wangEditorListModule)
registerModule(wangEditorTableModule)
registerModule(wangEditorVideoModule)
registerModule(wangEditorUploadImageModule)
registerModule(wangEditorCodeHighlightModule)
// 最终产出 wangEditor.js
所以,wangEditor 新版本天生就是具备扩展性的,可以继续扩展自己定义的 module 。
这些后面会在用户文档中写明。
可直接用于 Vue React
目前只有 Vue React 组件,其他框架像 svelte ng 等,发布之后再根据用户需求添加。
用于 Vue
目前只有 Vue2 组件,Vue3 组件会在发布之前开发完。
<div>
<div>
<button @click="insertText">insert text</button>
</div>
<div style="border: 1px solid #ccc;">
<Toolbar :editorId="editorId" :defaultConfig="toolbarConfig" :mode="mode"/>
</div>
<div style="border: 1px solid #ccc; margin-top: 10px;">
<Editor
:editorId="editorId"
:defaultConfig="editorConfig"
:defaultContent="defaultContent"
:mode="mode"
@onCreated="onCreated"
@onChange="onChange"
@onDestroyed="onDestroyed"
@onMaxLength="onMaxLength"
@onFocus="onFocus"
@onBlur="onBlur"
@customAlert="customAlert"
/>
</div>
</div>
import Vue from 'vue'
import { Editor, Toolbar, getEditor, removeEditor } from '@wangeditor/editor-for-vue' // 尚未发布
export default Vue.extend({
components: { Editor, Toolbar },
data() {
return {
//【特别注意】
// 1. editorId Toolbar 和 Editor 的关联,要保持一致
// 2. 多个编辑器时,每个的 editorId 要唯一
editorId: 'w-e-1001',
toolbarConfig: { /* 工具栏配置 */ },
defaultContent: [],
editorConfig: {
placeholder: '请输入内容...',
// 其他编辑器配置
// 菜单配置
}
mode: 'default' // or 'simple'
},
curContent: []
},
methods: {
onCreated(editor) {
console.log('onCreated', editor)
},
onChange(editor) {
console.log('onChange', editor.children)
this.curContent = editor.children
},
onDestroyed(editor) {
console.log('onDestroyed', editor)
},
onMaxLength(editor) {
console.log('onMaxLength', editor)
},
onFocus(editor) {
console.log('onFocus', editor)
},
onBlur(editor) {
console.log('onBlur', editor)
},
customAlert(info: string, type: string) {
window.alert(`customAlert in Vue demo\n${type}:\n${info}`)
},
insertText() {
// 获取 editor 实例,即可执行 editor API
const editor = getEditor(this.editorId)
if (editor == null) return
if (editor.selection == null) return
// 在选区插入一段文字
editor.insertText('一段文字')
},
},
// 及时销毁 editor
beforeDestroy() {
const editor = getEditor(this.editorId)
if (editor == null) return
// 销毁,并移除 editor
editor.destroy()
removeEditor(this.editorId)
}
})
用于 React
下面代码演示了 Hook 中的使用,class Component 的使用会在文档中说明,代码也会开源出来。
import React, { useState, useEffect } from 'react'
import { IDomEditor, IEditorConfig, IToolbarConfig, SlateDescendant } from 'wangEditor@next' // 尚未发布
import { Editor, Toolbar } from '@wangeditor/editor-for-react' // 尚未发布
function ReactEditor() {
// 存储 editor 实例
const [editor, setEditor] = useState<IDomEditor | null>(null)
// 存储 editor 的最新内容(json 格式)
const [curContent, setCurContent] = useState<SlateDescendant[]>([])
// 工具栏配置
const toolbarConfig: Partial<IToolbarConfig> = { /* 工具栏配置 */ }
// editor 配置
const editorConfig: Partial<IEditorConfig> = {}
editorConfig.placeholder = '请输入内容...'
editorConfig.onCreated = (editor: IDomEditor) => {
// 记录 editor 实例,重要 !
// 有了 editor 实例,就可以执行 editor API
setEditor(editor)
}
editorConfig.onChange = (editor: IDomEditor) => {
// editor 选区或者内容变化时,获取当前最新的的 content
setCurContent(editor.children)
}
// 其他编辑器配置
// 菜单配置
// 及时销毁 editor ,重要!!!
useEffect(() => {
return () => {
if (editor == null) return
editor.destroy()
setEditor(null)
}
}, [])
return (
<React.Fragment>
<div style={{ border: '1px solid #ccc'}}>
{/* 渲染 toolbar */}
<Toolbar editor={editor} defaultConfig={toolbarConfig} mode="default"/>
</div>
<div style={{ border: '1px solid #ccc', marginTop: '10px' }}>
{/* 渲染 editor */}
<Editor defaultConfig={editorConfig} defaultContent={[]} mode="default"/>
</div>
</React.Fragment>
)
}
export default ReactEditor
遇到的问题
世界上只有两种软件:1. 没人用的软件;2. 很多 bug 的软件;
开发过程中当然遇到了很多问题,下面是印象比较深的两个问题。(近期的,之前的问题都有点忘了)
拼音输入的问题
全选,输入拼音
slate 本身就有拼音输入的问题,可能是外国人不用拼音,所以它们不觉得是一个问题 —— 对,未发现的 bug 那就不能叫 bug 。
例如你现在访问 slate example ,focus 到编辑器,ctrl + a 全选,然后直接输入拼音。它就会出现问题。
但这个问题在 wangEditor 新版本中已经解决。
当然,有可能还有其他拼音输入相关的 bug ,如果你发现了,可给我留言。
maxLength
编辑器的输入使用 beforeinput
事件来劫持,而这无法阻止拼音输入法。但这个问题还是要解决的,就得使用其他的方法。
第一,在 compositionStart 时记录下 Range 节点的 text
function handleCompositionStart(e: Event, textarea: TextArea, editor: IDomEditor) {
// 记录下 dom text ,以便触发 maxLength 时使用
if (selection && Range.isCollapsed(selection)) {
const domRange = DomEditor.toDOMRange(editor, selection)
const curText = domRange.startContainer.textContent || ''
EDITOR_TO_TEXT.set(editor, curText)
}
}
第二,在 compositionEnd 时,判断是否触发 maxLength ,如果触发就直接 DOM 操作,还原 Range 节点的 text
export function handleCompositionEnd(e: Event, textarea: TextArea, editor: IDomEditor) {
const { data } = e
// 检查 maxLength
//【注意】这里只处理拼音输入的 maxLength 限制,英文、数组的限制,在 editor.insertText 中处理
if (DomEditor.checkMaxLength(editor, data)) {
const domRange = DomEditor.toDOMRange(editor, selection)
domRange.startContainer.textContent = EDITOR_TO_TEXT.get(editor) || '' // 如果触发 maxLength ,则还原 Range 节点的 text
textarea.changeViewState() // 重新定位光标
return
}
// 未触发 maxLength ,则插入文字
Editor.insertText(editor, data)
}
toHtml 的纠结
新建、编辑一篇文章,肯定需要发布展示它。toHtml 就是把编辑器的内容,转换为 html ,然后展示到页面上。
新版本编辑器的内容是基于 slate 数据模型的,是一个 json 格式的数据,当然你也可以通过编辑器获取 html 。
const editorConfig = {}
editorConfig.onChange = (editor) => {
console.log('content', editor.children) // 获取 content
console.log('html', editor.getHtml()) // 后去 html
}
// 创建编辑器
const editor = wangEditor.createEditor({
textareaSelector: '#editor-container',
config: editorConfig,
content: [], // 默认内容
})
既有 content 又有 html ,那该如何存储?如何发布展示呢?
- 只存储 html —— 那无法再次编辑了,因为创建编辑器时 content 不能传入 html —— 无法编辑,就直接 pass 了
- 只存储 content —— 那你在发布展示时如何转为 html 呢?
- 同时存储 content 和 html —— 数据冗余了,这本质就是一个数据,两种不同形式而已
对于这个问题,我纠结了很多天,最终也没能找到一个完美的方案。但还是提供了可行的方案。
只存储 content
可以再次编辑内容,数据也不会冗余。只剩下一个问题:如何将 content 转换为 html ?
我提供了一个解决方案:
- 前端 js 渲染
- 服务端 nodejs SSR 渲染(不支持其他语言和技术栈)
// 自己从服务端或这数据库获取 content
const content = await getContentFromDatabaseOrServer()
// 创建编辑器,只传入 content 即可
const editor = wangEditor.createEditor({ content })
const html = editor.getHtml()
const text = editor.getText()
// 将 html 或者 text 渲染到页面
适用于以下情况
- 使用 nodejs SSR 服务端渲染
- 前端渲染,但并不考虑至极的 js 体积优化(如 PC 单页面)
- 显示页面和编辑页面是同一个 SPA ,此时 wangEditor js 只加载一次
同时存储 content 和 html
如果你不适用于上述情况,那就只能同时存储 html 和 content 。出了数据冗余之外,其他问题都很简单。
再次编辑使用 content ,发布显示使用 html 。不限制技术栈,不限制前端渲染还是 SSR ,都行。
不过需要注意一点:这两者最好通过一个 http 请求来处理,否则可能出现数据不一致的问题。如只存储了 content 却没存储 html(bug,断网,断电等)
const editor = wangEditor.createEditor({ ... })
// 也可以通过 onChange 把 content html 实时同步到 textarea ,再提交表单
// 点击保存按钮
$('#button-save').on('click', () => {
// 1. 获取 content 和 html
const contentStr = JSON.stringify(editor.children)
const html = editor.getHtml()
// 2. 拼接 content 和 html ,中间插一个间隔字符串(不易重复的,自己定义即可),如 '<<split-symbol-a7qlu>>'
const contentAndHtml = contentStr + '<<split-symbol-a7qlu>>' + html
// 3. 提交 contentAndHtml 到服务端,需要你自行处理
// 服务端再根据 '<<split-symbol-a7qlu>>' 来拆分 content 和 html ,分别保存
//【注意】这里要拼接 content 和 html 一次性提交到服务端,是为了避免数据不同步的问题
// 例如,因为程序 bug ,突然断网、断电等情况,只提交成功了 content 却未提交成功 html
})
是否要等待新版本发布?
如果近期就使用,那就不用等新版本了。上文说了,这是一个持久战,等新版本正式发布,估计也得今年冬天了。
可以先使用 V4 版本,到时候升级 V5 成本不会很高,我会控制好这块。
总结
富文本编辑器是一个复杂度非常高的软件,这个我早有体会,而这次的新版本升级体会更深了。
同时,我参与其中也是收获很多,特别是对于软件架构和设计的思考。这是写业务代码永远都体会不到的。
也正是因为它复杂度高,成本大,工期长,才有足够的技术壁垒。
还是那句话:价值,最终都会变现。
最后,欢迎感兴趣的小伙伴一起加入开发,可以加入 qq 群 然后私聊群主。要求条件:
- 熟悉 ts
- 熟悉 slate.js 的使用和原理
- 熟悉 vdom 原理,熟悉 snabbdom.js 的用法,
- 熟悉 Vue 或者 React
- 熟悉 webpack 或者 rollup