基于draft.js封装一款酷炫的Markdown实时渲染编辑器(已开源) 您所在的位置:网站首页 js解析markdown的写法 基于draft.js封装一款酷炫的Markdown实时渲染编辑器(已开源)

基于draft.js封装一款酷炫的Markdown实时渲染编辑器(已开源)

2023-08-25 15:50| 来源: 网络整理| 查看: 265

前言 最终成果

可以在线耍一下:brilliant-js.com/ GitHub地址:github.com/brilliant-j… 一、项目背景

这又是一次夸语雀编辑器的日常,之所以如此钟爱语雀,因为它把typora搬到了线上,这种实时渲染Markdown真的太实用而且太高级了,从很早用语雀开始就一直有个这样的想法:如果哪一天我的项目需要线上编辑器,一定要搞个语雀这样的。

这一天终于来了,接到新的业务需求,需要引入一款React在线编辑器,记得上一次在公司项目中使用编辑器组件还是两年前,那是一款同样优秀的富文本编辑器,可惜不支持实时Markdown,那时的我没有选择,现在的我就要Markdown实时渲染的。

所谓的Markdown实时渲染就是类似这样的(示例来自语雀):

语雀富文本演示.gif

但正如那句名人名言:

美好的东西都不是轻易能得到的。

搜索了众多开源编辑器组件之后,发现讨论度最高也最常见的依然是纯富文本编辑器,其次就是左右两栏预览的纯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开源,至少保证了一定的稳健性

image.png

draft.js是一个基于原生Dom封装的编辑器基础设施 采用immutable类型的state,统一管理编辑器中所有内容 通过块样式和行内样式模式匹配,可以自行定制页面上展示的任何信息 四、开发实践

image.png

使用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;

这时候页面上什么都看不见,试试光标移动到最上一行并点击,就会发现一个跳跃的光标样式:

01.光标显示演示.gif

2. 顶层状态对象editorState

EditorState 是 Draft.js 最重要的一个对象,它是用来存储富文本编辑器所有内容和状态的。这个对象作为组件属性输入给 Editor 组件,一旦用户进行操作,比如敲一个回车,Editor 组件的 onChange 事件触发,onChange 函数返回一个全新的 EditorState 实例,Editor 接收这个新的输入,渲染新的内容,所以,最简单的写法就是前面代码所示那样。

image.png

所包含的内容:

(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 ( 打印信息 ); } ...

image.png 可以观察到 editorState 对象中存在以上属性,其中比较重要的三个属性有:

currentContent 是一个 ContentState 对象,存放的是当前编辑器中的内容,称为内容描述对象 selection 是一个 SelectionState 对象,它是当前选中状态的描述对象 redoStack 和 undoStack 就是撤销/重做栈,它是一个数组,存放的是 ContentState 类型的编辑器状态 3. 内容描述对象contentState

ContentState 也是一个 Immutable Record 对象,其中保存了编辑器里的全部内容和渲染前后的两个选中状态。可以通过 EditorState.getCurrentContent() 来获取当前的 ContentState,同样调用 .toJS() 后将它打印出来看下: image.png

4. 块描述对象blockMap 是一个非常重要的对象,其中以块的唯一key作为键,存放了编辑器中目前所有内容块。每一个块的结构类型为 ContentBlock ,表示一个编辑器内容中的每一个独立的 block,即视觉上独立的一块。

image.png

每一个内容块的具体结构为(请对照上图查看): key:块的唯一key type:块的类型 text:块的文本 块中的key对应当前块的DOM结构属性上的 data-offset-key ,之后操作数据是通过key来确定所要操作的块内容

image.png

entityMap:存放的是图片链接地址,链接文本和链接地址等信息 5.添加控件

在厘清核心API之后就能进一步封装了,由于我们要做的是一款Markdown实时渲染+富文本支持的编辑器,所以下一步要做的就是像其他富文本编辑器一样增加控件。

控件分为两种:行内样式控件和块级样式控件。

比如这样的加粗、高亮、斜体、行内代码就是用行内样式控件实现的。

image.png

而这样的标题、无序列表和引用就是块级样式控件实现的。

image.png

(1)设置行内样式控件

首先定义控件类型和标题

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中。 image.png 代码实现如下:

import { RichUtils } from 'draft-js' ... // 处理行内样式 const toggleInlineStyle = (inlineStyle: string) => { setEditorState(RichUtils.toggleInlineStyle(editorState, inlineStyle)); }; ... {controls.map((item) => { return ( { e.preventDefault(); toggleInlineStyle(item.type); }} > {item.label} ); }} 注意,想让加入inlineStyleRanges中的样式生效,还要设置好内联样式对应的映射规则editorStyleMap并加入到Editor编辑器组件的属性中 // 自定义行内样式 const editorStyleMap: DraftStyleMap = { //字体 bold: { fontWeight: "bold", }, italic: { fontStyle: "italic", }, red: { color: "red", }, }; ...

至此,内联样式控件功能就已经实现了: 02.行内样式演示.gif

draft.js开放的 customStyleMap api 自由度极高,你可以实现任何你想要的行内样式。

(2)设置块级控件样式

块级样式设置相对直观一点,直接更改当前行所在块的type类型即可,

image.png Draft.js 中 block 的 type 有 unstyled,paragraph,header-one,atomic …… 等值,在 Draft.js 的文档中 header-one 类型对应的是 元素, blockquote对应的是元素,依次类推...我们也选取了它来实现多级标题和代码块的功能。

引入块级代码控件:

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)); };

023.块级样式演示.gif

同样,你可以使用blockRenderFn定制任何你想要的块级样式。

6.Markdown实时渲染 接下来要实现的Markdown实时预览功能,才是给整个编辑器注入灵魂 (1)思路分析

目前已知我们输入到页面上的元素都会如实展现,比如输入一个#就是一个#,而不是标题,而Markdown语法中,#号后面接空格,空格后面的内容就是一级标题,我们要做两件事:1、把#空格后面跟着内容的块设置为type=h1;2、把#号空格删除;3、返回新的editorState

image.png

那么这个时候,如果Draft.js编辑器能暴露一个事件就好了,这个事件它可以监听用户输入,并且能把输入内容回传给我们,而且为了通俗易懂,最好这个事件名字就叫它handleBeforeInput,嗨!你猜怎么着,还真有一事件就叫这名,Draft.js编辑器上的属性之一handleBeforeInput,类型为函数,在用户输入过程中触发,参数是用户所输入内容。

image.png

拿到用户输入内容之后就可以对其进行相应Markdown语法的正则匹配

image.png

处理之后返回新的editorState

image.png

那么实时渲染就实现了!

Markdown实时渲染演示.gif

说明:原谅我没法截取完成代码进行演示,以上仅为简单实例,实际处理过程很复杂,要考虑到各种不同Markdown语法情况各自处理,还要在操作中准确修正光标位置,复杂场景下的光标管理应该是编辑器封装过程中最难的,可参考文末开源地址源码进一步研究

五、一些问题与解决过程 1.开发资源匮乏 场景:draft官方文档最大的有点是简洁,有条理,找API很方便,最大的缺点是过于简洁,有的核心API就一句话描述完毕,既没有演示案例,也没有一个系统结构的说明,让人云里雾里,学习起来非常吃力,在很多场景下,一堆API不知道该用哪个。 解决: 首先搭梯子观看关于draft.js的react开发者大会上的演讲,学习设计理念 然后找开源封装编辑器,学习里面的封装理念 也参考一些视频,开始用思维导图形式梳理draft.jsAPI,并以最小规模进行测试学习,整理成了很多篇介绍核心API的文章,供后面人参考。 2.代码输入过程中的括号匹配

场景:用户在代码块中光标所在位置换行,一个非闭合{或[或((大括号、中括号、小括号),需要有两个空格缩进,计算非闭合括号的数量,从而计算缩进量

解决:

当时用了一个很不优雅的方式处理的,写了很多冗余的代码 而且有bug,如果是之前已经闭合的,没法判断 后来刷算法题,偶然刷到匹配有效括号的问题,集合当前业务场景,重构算法使用栈的思路很优雅解决了 那时候是零零散散刷算法,经过这道题之后,深刻认识到了做算法题对日常业务编码的帮助还是很大的,开始根据自己的想法制作适合自己的路线图,系统刷题 3.图片粘贴方法功能测试 场景:开发过程中需要测试粘贴图片自动上传功能,普通base64方案占用体积过大造成卡顿 解决:使用koa2搭建了一个极简图片资源托管服务器,借助multer模块进行中间件处理,上传图片之后返回资源地址,并部署到了自己的云服务器,供项目组同学们共享,由于测试图片不需要持久化,这个项目同时采用了node的定时模块,在每天零点进行数据清空,不占用服务器内存。 4.光标管理

刚开始光标错位带来的bug真是让人头秃,做得多了才慢慢找到规律。

六、成果展示与总结

团队开源的Brilliant编辑器,完全按照本教程理念基于draft.js封装,并且开放全部源代码,地址在:github.com/brilliant-j…,已发布npm仓库支持零配置快速引用,所见即所得的编辑模式,给您极致的输入体验,欢迎围观和使用,也欢迎大家参与交流共建,项目会长期维护,逐步完善新功能,欢迎持续关注✨

在线体验地址: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 实验室设备网 版权所有