基于ProseMirror富文本编辑器框架,分享Markdown改变图片尺寸如何保存的解决方案

2,623 阅读11分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路

有感而发

开头我先简单说道说道,富文本编辑器其实一直在前端业界都有一种声音,"富文本编辑器是前端天坑"。有这种反响的原因,我以个人的理解为,需求几乎没有明显的边界,并且因为编辑内容是十分自由的特性,而总会在某些场景下会有莫名其妙的bug出现。所以在设计组件和解决需求和bug问题的时候考虑的范围总是需要特别细致。当然了,感兴趣的掘友们可以参考一下这个问题doodlewind王福朋两位在富文本编辑器都有建树的前端大佬的回答,或许你更能体会到研发富文本编辑器需要的技术考量和技术深度。

ProseMirro富文本编辑器框架

什么是ProseMirror?

为了让可能第一次听的同学有个了解,我稍微介绍一下,ProseMirro是一个富文本编辑器框架,该框架致力为开发者能便捷打造一个能产生结构化,语义化文档的理想富文本编辑器。它提供了一系列的工具包来支撑这一概念,而且通过实现比普通html有更多结构化和兼具WYSIWYG风格(所见即所得)的接口来让开发者可以自定义的创造出符合需求的文档结构与内容。当然了优秀的框架自然也出自优秀的程序员,Marijn Haverbeke是一位来自德国柏林的大师级程序员,同时是知名代码编辑器codeMirror也是出自他手,并著有《Javascriot编程精解》。

基于ProseMirror开发的富文本编辑器

在如今的wiki(知识库文档)产品中,有一款开源的产品叫做Outline它的编辑器名为rich-markdown-editor这款编辑器正是基于ProseMirror生态开发的一款所见即所得的富文本编辑器,但是它的更新方向主要是为了Outline这款产品的需求方向而进行迭代,所以它对一些pr有着更严格的提交需求。

官网界面:

image.png

Outline编辑器界面:

image.png

这款编辑器相信有些同学看了可能会觉得眼熟。是的这款产品对标的正是大名鼎鼎的另一款知识库gitbook,但是这款产品却并不是基于ProseMirror而是由另一款知名的富文本框架slate研发而成,所以我们这里为了不跑题暂时不做拓展讲解了,感兴趣的同学可以延伸搜索相关资料进行了解。

gitbook编辑器界面:

image.png

解决Markdown保存图片修改尺寸

项目准备

为了方便掘友们的的理解,我就以我解决该问题时使用的项目也就是上文介绍的rich-markdown-editor编辑器来进行代码演示。

首先我们把代码给下载下来并跑起来,项目地址

image.png

# 安装依赖
  yarn install
   
# 启动项目
  yarn start

image.png

image.png

打开地址我们可以看到当前这个页面,并且选中images组件。这里补充一下Storybook,应该对于一些看过组件库开发之类的文章就有目染过,这个是一个UI开发环境,可以方便开发者独立调试ui组件。所以对于富文本编辑器来说一些节点其实也是一个个的组件。

当前页面这张图片是我随便找的一个图片链接替换了原来的图片例子的链接,因为原例子上的图片链接已经失效,所以掘友们可以自己替换一直图片上去即可。

所以大家可以打开 /src/stories/index.stories.tsx 找到当前这个路径下的Images常量替换括号里的图片链接 然后保存,大家刷新一下刚刚的页面即可看到效果。

image.png

通过点击图片聚焦之后我们可以看到菜单工具栏上是没有任何尺寸修改等输入框的这里的效果我们可以到语雀文档编辑器里移动到图片上是能看到弹出的工具栏里有支持填入图片尺寸的输入框,并且当前的图片也不支持拉伸修改图片尺寸。

语雀演示动图:

Large GIF (952x466).gif

rich-markdown-editor演示动图:

Large GIF (1600x532).gif

所以为了让makdown编辑器有这个能力,此时我们可以着手分析,应该如何把修改后的尺寸保存下来呢。我们来拆分一下此时遇到的问题。

  1. 当前编辑器暴露的内容为markdown而不是传统的Dom元素标签,意味着编辑器返回的只是一堆带有markdown语法的字符串,而并不是返回这种可以携带类似如下的内容:
"<img src="http://xxxxxxx.jpg" style="width: 300px; height: 300px;" />"
  1. 如何让后端的同学们可以知道我们修改过当前这篇文章的图片的尺寸,并且将当前这篇文章的最新尺寸的图片进行保存呢?

  2. 如何保证第一次把图片尺寸修改比原图要小,第二次再次修改比原图要大的时候保证图片不掉像素呢?

这里也为大家分享一下我第一次做的时候犯的错误🤣 ,也警示大家以后遇到该问题不要这样去实现。一开始针对这个需求,我相信很多同学的第一想法都是找到当前图片的Dom节点然后为style属性添加宽高,然后宽高可以在聚焦时候的工具栏里设立两个宽高的输入框,通过读取输入框的数据来同步修改style。但是这里这样处理却会引发上述一系列的问题点。因为后端只会通过一个字段来保存当前的文章内容,如下图大家可以在这里添加一个onChange方法通过一个回调函数实时看到编辑器内容改变后暴露的内容其实就是markdown语法字符串而已

image.png

image.png

错误的解决示范⛔️

所以通过更改样式的办法只能解决当前编辑时候的图片样子,而不能持久化。所以起初再三思考下,我想到了一个十分hack的办法,但是这里就需要借用到后端同学的能力,做法是也是通过增加两个宽高的输入框,填入宽高然后通过失去焦点之后读取到当前的宽高把数据通过设计一个当前富文本编辑器暴露的props属性,调用项目使用该编辑器传进来的Post请求把宽高以及当前文章的id一并传给后端由后端在服务器通过第一次上传的原图进行剪切把最新地址放在响应里返回回来,编辑器便把最新地址通过ProseMirror的新增节点api把原图片地址给整个替换掉,然后当前图片已达到更改的目的。效果大概如下:

错误示范动图:

Large GIF (406x226).gif

但是这带来了巨大的问题,第一、浪费了服务器的性能,以及存储空间,第二、存在异步问题会使使用者感觉卡顿。第三、在修改较大尺寸的时候服务器处理会变慢,导致修改体验会很差。所以这个错误的办法也只是当时为解决燃眉之急而想出来的办法。

正确解决的办法

而现在要讲的解决办法就是标题所要分享的解决方案。此时我们重新来审视一下上面遇到的三个问题。从前端与后端的角度我们可以发现其实大家沟通的桥梁其实就是markdown的字符串内容而已,那么有没有什么办法能让后端和以往一样只保存内容的字符串即可,而不需要有额外的负担,并且前端编辑器通过重新读取markdown字符串而能显示出正确的修改后的图片呢。是的,我们直接可以通过当前内容字符串来帮我们完成这个操作,我们可以把宽高直接存储在内容里,通过技术方法来读取内容里的宽高来改变我们当前的图片,并且修改的时候也能把改变后的宽高写入我们的内容里,这样后端也是只对内容做存储,而前端也是对内容做读取更改,但问题来了,宽高要放到内容的那个位置呢? 答案正是如下:

![测试图片](https://pic4.zhimg.com/80/v2-25b2aaa6bd002dae2cd3bfc917e7fc30_r.jpg?size=300x300)`

我巧妙的借助了url的优势把宽高参数存储在图片地址的后方,通过模拟url的query参数。这里给同学们补充一下,在编辑器里上方图片的markdown语法里的[测试图片]内容会成为img的alt属性参数,而(https://pic4.zhimg.com/80/v2-25b2aaa6bd002dae2cd3bfc917e7fc30_r.jpg?size=300x300)会成为img的src属性的参数所以这里依然能正确的读取到图片的地址然后显示出图片。然后为了优化用户的体验我直接把填入宽高的操作改为通过拉伸图片即可改变当前图片的宽高,而这个操作我们就要依赖于一个库re-resizable: 一个基于React可调整组件大小的组件。有了方案后我们就开始动手编写代码吧。

代码实操

首先我们先安装re-resizable

yarn add re-resizable

在代码文件夹中打开 /src/nodes/image.tsx 这里就是图片组件的核心代码,我们在这里引入一下re-resizable。然后我们可以在下方的119行代码下编写一个ImageBox组件:

image.png

在此之前我们可以在前面先写下如下ts类型用于后续使用

image.png

然后在162行代码下添加如下类型(这里主要为当前ProoseMirror图片节点的attrs属性,新增两个宽高默认属性)

image.png

toDom方法里对图片类型的返回配置增加以下配置该配置记录了当前image节点的attrs里的所有属性以及style样式,和内容是否可编辑的布尔属性

image.png

在305行代码我们可以添加一个用于把当前修改宽高后的尺寸等其他属性同步回节点的attrs属性里的方法

image.png

在323行代码拿到存储在节点属性上的宽高,进行解构

image.png

在332行代码我们使用当前ImageBox组件,并且把存储的节点中的属性传入到里面

image.png

在361行代码我们在toMarkdown方法里把最新的宽高拼接成刚刚方案里需要的格式由此让markdown的字符串内容里有我们最新的图片宽高。

image.png

在378行代码在getAttrs方法下我们增加如下方法,用于读取当前Img,src属性下的链接并且截取到?size=后的宽高数据,并且把图片地址分离开带有宽高的字符串把截取出来的宽高重新设定回attrs属性里。

image.png

此时我们保存代码重新刷新页面来看看效果:

动画演示:

Large GIF (1864x866).gif

这样我们就就很舒服的解决了保存尺寸的问题,并且对前后端的同学来说都没有带来负担,把处理尺寸的逻辑焦点一并交给了编辑器,这也是我们开发一个编辑器所要围绕的焦点,要尽量考虑不侵入使用者代码的逻辑为核心。

结尾

当然了,结尾的最后必定要放上项目地址:

https://gitee.com/coderq-sub/rich-markdown-editor-resize-image

因为我平时有许多练习的例子,和项目我都会在我当前这个小号仓库创建,我这么做的目的有两点,第一、因为我个人提交项目的时候喜欢手敲git的命令所以能让我持续保持使用git的命令,这对我现在使用git完成一些非常规的场景有很大的帮助。第二、就是主要是不想污染到自己大号的主仓库,因为我大号的主仓库项目都是在维护状态的,并不是Demo级别的练习项目。

ps: 可能对于ProseMirror相关的api内容并没有太仔细的拿出来说说,因为毕竟当前这篇文章的出发点是遇到同类型问题的时候我们应该怎么去解决,如果单独拿ProseMirror其中的api内容可能就会非常非常的多了。这个大家也可以期待一下,我下次会发一篇围绕ProseMirror相关的文章哦

😁 最后谢谢大家能滑到这里,毕竟第一次输出技术文,细节等考虑的方面可能并不是太周到,大家能给建议的我都十分虚心接受和参考的。