基于draft.js封装一款酷炫的Markdown实时渲染编辑器(已开源) | 您所在的位置:网站首页 › js解析markdown的写法 › 基于draft.js封装一款酷炫的Markdown实时渲染编辑器(已开源) |
前言
最终成果
这又是一次夸语雀编辑器的日常,之所以如此钟爱语雀,因为它把typora搬到了线上,这种实时渲染Markdown真的太实用而且太高级了,从很早用语雀开始就一直有个这样的想法:如果哪一天我的项目需要线上编辑器,一定要搞个语雀这样的。 这一天终于来了,接到新的业务需求,需要引入一款React在线编辑器,记得上一次在公司项目中使用编辑器组件还是两年前,那是一款同样优秀的富文本编辑器,可惜不支持实时Markdown,那时的我没有选择,现在的我就要Markdown实时渲染的。 所谓的Markdown实时渲染就是类似这样的(示例来自语雀): 但正如那句名人名言: 美好的东西都不是轻易能得到的。 搜索了众多开源编辑器组件之后,发现讨论度最高也最常见的依然是纯富文本编辑器,其次就是左右两栏预览的纯Markdown编辑器,虽然这两种模式仍有大量适用场景,但我认为,在Markdown如此流行的今日,能把Markdown+富文本结合起来的实时渲染方案才是属于未来的编辑器。 怀着对语雀实时渲染的执念,也为之后同样喜欢实时渲染Markdown编辑器的同仁们增加一种选择方案,团队决定正式立项,开发一款自己定制的实时Markdown渲染React编辑器。 二、技术选型这里说的开发,并不是从零开始做一个编辑器,而是在流行的编辑器lib上,进行功能封装和定制,常见的lib定制方案有code-mirror,slate.js,quill.js等,基于react技术栈的考虑,最终选择slate.js了draft.js这两款,它们都是贴合react函数式编程理念,依靠state管理页面数据,熟悉react开发模式的话,在封装过程中学习成本低很多,但在入坑slate.js一段时间后发现它的致命弱点:对中文输入不友好,有时候输入法切换都能带来bug导致编辑器崩溃(现在过去很久了也不知道官方有没有修复好这个bug),所以最后胜出的是draft.js。 三、可行性分析那么用draft.js是否可以实现我想要的Markdown实时渲染模式呢? draft.js的设计理念和特点: React亲儿子,同样是facebook开源,至少保证了一定的稳健性使用Draft.js封装编辑器,和日常react开发模式基本一致,采用immutable数据结构管理顶层对象EditorState,为了便于undo/redo生成快照,原始editorState对象不可更改,你必须在每次操作之后返回一个新的editorState对象。 Editor对象接收的editorState属性是一个受控属性,你必须向编辑器组件传入一个能够更改editorState的onChange方法来实时更改editorState对象。 1.原生组件引入:一个基础Draft.js组件使用规则如以下代码所示: import { Editor, EditorState } from 'draft-js' import { useState } from 'react'; import 'draft-js/dist/Draft.css' function App() { const [editorState, setEditorState] = useState(() => EditorState.createEmpty()) const onChange = (editorState) => setEditorState(editorState) return ( ); } export default App;这时候页面上什么都看不见,试试光标移动到最上一行并点击,就会发现一个跳跃的光标样式: EditorState 是 Draft.js 最重要的一个对象,它是用来存储富文本编辑器所有内容和状态的。这个对象作为组件属性输入给 Editor 组件,一旦用户进行操作,比如敲一个回车,Editor 组件的 onChange 事件触发,onChange 函数返回一个全新的 EditorState 实例,Editor 接收这个新的输入,渲染新的内容,所以,最简单的写法就是前面代码所示那样。 所包含的内容: (1) 当前文本内容状态(ContentState) (2) 当前选中内容状态(SelectionState) (3) 所有的内容修饰器(Decorator) (4) 撤销和重做栈 (5) 最后一次变更操作的类型。editorState本质上是个immutable类型数据,可以调用toJS方法查看原始数据结构。 ... function App() { const [editorState, setEditorState] = useState(() => EditorState.createEmpty()) const onChange = (editorState) => setEditorState(editorState) const logData = () => { // editorState是immutable类型数据,可调用toJS方法打印原始数据结构 console.log(editorState.toJS()); } return ( 打印信息 ); } ...
ContentState 也是一个 Immutable Record 对象,其中保存了编辑器里的全部内容和渲染前后的两个选中状态。可以通过 EditorState.getCurrentContent() 来获取当前的 ContentState,同样调用 .toJS() 后将它打印出来看下:
在厘清核心API之后就能进一步封装了,由于我们要做的是一款Markdown实时渲染+富文本支持的编辑器,所以下一步要做的就是像其他富文本编辑器一样增加控件。 控件分为两种:行内样式控件和块级样式控件。 比如这样的加粗、高亮、斜体、行内代码就是用行内样式控件实现的。 而这样的标题、无序列表和引用就是块级样式控件实现的。 首先定义控件类型和标题 const controls = [ { label: "B", item: "BOLD", type: "inline" }, { label: "I", item: "ITALIC", type: "inline" }, { label: "RED", item: "RED", type: "inline" }, ];行内样式切换的处理逻辑,引用draft.js内部的 RichUtils.toggleInlineStyle ,原理就是获取当前选中内容并记录偏移量,并将选中内容所对应的偏移量及样式信息(style)加入到inlineStyleRanges中。
至此,内联样式控件功能就已经实现了:
draft.js开放的 customStyleMap api 自由度极高,你可以实现任何你想要的行内样式。 (2)设置块级控件样式块级样式设置相对直观一点,直接更改当前行所在块的type类型即可,
引入块级代码控件: const controls = [ { label: "H1", type: "header-one", category: "block" }, { label: "H2", type: "header-two", category: "block" }, { label: "H3", type: "header-three", category: "block" }, { label: "", type: "blockquote", category: "block" }, ]; {controls.map((item) => { return ( { e.preventDefault(); toggleBlockType(item.type); }} > {item.label} ); } }同样,块级样式设置也依赖于RichUtils的 toggleBlockType API,内部实现原理就是切换光标所在行的block的type值。 // 处理块级样式 const toggleBlockType = (blockType: string) => { onChange(RichUtils.toggleBlockType(editorState, blockType)); };同样,你可以使用blockRenderFn定制任何你想要的块级样式。 6.Markdown实时渲染 接下来要实现的Markdown实时预览功能,才是给整个编辑器注入灵魂 (1)思路分析目前已知我们输入到页面上的元素都会如实展现,比如输入一个#就是一个#,而不是标题,而Markdown语法中,#号后面接空格,空格后面的内容就是一级标题,我们要做两件事:1、把#空格后面跟着内容的块设置为type=h1;2、把#号空格删除;3、返回新的editorState 那么这个时候,如果Draft.js编辑器能暴露一个事件就好了,这个事件它可以监听用户输入,并且能把输入内容回传给我们,而且为了通俗易懂,最好这个事件名字就叫它handleBeforeInput,嗨!你猜怎么着,还真有一事件就叫这名,Draft.js编辑器上的属性之一handleBeforeInput,类型为函数,在用户输入过程中触发,参数是用户所输入内容。 拿到用户输入内容之后就可以对其进行相应Markdown语法的正则匹配 处理之后返回新的editorState 那么实时渲染就实现了! 说明:原谅我没法截取完成代码进行演示,以上仅为简单实例,实际处理过程很复杂,要考虑到各种不同Markdown语法情况各自处理,还要在操作中准确修正光标位置,复杂场景下的光标管理应该是编辑器封装过程中最难的,可参考文末开源地址源码进一步研究 五、一些问题与解决过程 1.开发资源匮乏 场景:draft官方文档最大的有点是简洁,有条理,找API很方便,最大的缺点是过于简洁,有的核心API就一句话描述完毕,既没有演示案例,也没有一个系统结构的说明,让人云里雾里,学习起来非常吃力,在很多场景下,一堆API不知道该用哪个。 解决: 首先搭梯子观看关于draft.js的react开发者大会上的演讲,学习设计理念 然后找开源封装编辑器,学习里面的封装理念 也参考一些视频,开始用思维导图形式梳理draft.jsAPI,并以最小规模进行测试学习,整理成了很多篇介绍核心API的文章,供后面人参考。 2.代码输入过程中的括号匹配场景:用户在代码块中光标所在位置换行,一个非闭合{或[或((大括号、中括号、小括号),需要有两个空格缩进,计算非闭合括号的数量,从而计算缩进量 解决: 当时用了一个很不优雅的方式处理的,写了很多冗余的代码 而且有bug,如果是之前已经闭合的,没法判断 后来刷算法题,偶然刷到匹配有效括号的问题,集合当前业务场景,重构算法使用栈的思路很优雅解决了 那时候是零零散散刷算法,经过这道题之后,深刻认识到了做算法题对日常业务编码的帮助还是很大的,开始根据自己的想法制作适合自己的路线图,系统刷题 3.图片粘贴方法功能测试 场景:开发过程中需要测试粘贴图片自动上传功能,普通base64方案占用体积过大造成卡顿 解决:使用koa2搭建了一个极简图片资源托管服务器,借助multer模块进行中间件处理,上传图片之后返回资源地址,并部署到了自己的云服务器,供项目组同学们共享,由于测试图片不需要持久化,这个项目同时采用了node的定时模块,在每天零点进行数据清空,不占用服务器内存。 4.光标管理刚开始光标错位带来的bug真是让人头秃,做得多了才慢慢找到规律。 六、成果展示与总结
在线体验地址:brilliant-js.com,如有问题可提issue交流。 七、参考资料 Draft.js官方文档 React.js Conf 2016 - Isaac Salier-Hellendag - Rich Text Editing with React - YouTube Rich text editing with Draft.js — Nikolaus Graf - YouTube 从插入图片功能的实现来介绍 Draft.js 富文本编辑器本文正在参与「掘金 2021 春招闯关活动」, 点击查看活动详情 |
CopyRight 2018-2019 实验室设备网 版权所有 |