作为一个后端 Java 开发,为何、如何自己实现一个 Markdown 编辑器
Quiet 项目简介:juejin.cn/post/717122…
Q:读完这篇文章能收获什么?
A:可以自己实现一个简易且跟自己的项目风格融合的 Markdown 编辑器。
上一篇:
Dubbo + Spring Security Oauth2 如何优雅解决服务提供者无法获取当前用户(下)
背景
在做 Quiet 项目中的接口文档管理部分时,接口编辑、预览、运行这三部分的布局均参考了 Yapi,里面有一个接口备注的区域用的是 Markdown 编辑器,看了下 Yapi 用的 Markdown 编辑器已年久失修了,那就换其他的 Markdown 编辑器,找了一圈,找到了很多优秀的 Markdown 编辑器,然而还是没找到自己想要的 Markdown 编辑器。
那就自己写一个吧!
组件拆分
一个 Markdown 编辑器可以拆分为三个小组件:Toolbar、Editor、Viewer
Toolbar:工具栏,主要提供鼠标点击的快捷操作
Editor:用户输入 Markdown 格式内容的区域
Viewer:渲染用户输入的内容
Toolbar 的按钮相当于在 Editor 区域按照 Markdown 的格式修改或添加文本内容,除此之外,就是控制 Markdown 的样式、展现内容,当然可以包含其他的功能,但是基本都差不多。
三个小组件的实现
在这三个小组件的实现过程中,我尽量去复用 Arco Design 的组件,毕竟操刀 css 那是前端大佬吃饭的手艺活,我可干不来。。。而且如果自己写(估计也写不好)或者换了一种样式,会显得这个 Markdown 编辑器与项目的样式格格不入,那简直太难受了(有蚂蚁在爬~)。
Toolbar 的实现
Toolbar 可以分为两块内容,左边是提供用户快捷输入或修改内容的,比如粗体、斜体、引用等,右边是控制编辑器的展示及其他功能,如目录、仅编辑区、仅预览区、全屏等,在 Arco Design 里,卡片组件的上部分可分为 Title
和 Extra
与此布局类似:

而且卡片的下半部分刚好可以用来放 Editor 和 Viewer,那就用卡片来作为整个编辑器的主体,上部分的样式改一下 headerStyle
就可以了,这个我做得来~
其他的样式,比如 Hover 出现提示信息,下拉菜单等,Arco Design 都有合适的组件可以使用,但是 Icon 的选择也是一个麻烦事,因为 Arco Design 的 Icon 不够我用,有的菜单操作找不到合适的组件使用,比如上标、下标、表格、行内公式、块级公式、展示左边区域、展示右边区域这些就没找到合适的 Icon 使用,没有的 Icon 那就得自己搞定了。
寻找合适的 Icon
React Icon 的开源项目还是很多的,但是,还是老问题,适配亮色和暗黑模式,又要跟 Arco Design 风格相近的 Icon 属实难找,甚至尝试了自己编辑一个 SVG(之前也没玩过这玩意)。
我先是找了 iconfont,确实找到了自己想要的 Icon,但是没办法像 Arco Design 的 Icon 自动适配两种模式,在亮色模式还好好的 Icon,一切换到暗黑模式,Icon 就变得乌漆麻黑的,而且从 iconfont 下载的 SVG 代码也是乱七八糟的(可以自行下一个 SVG 看看),看不懂啊,想改也没地方下手。
几经周转找到了 iconpark,一开始用的时候也是跟 iconfont 一样的效果,放弃了,后面知晓了可以通过修改 SVG 的属性,SVG 的颜色会自动使用字体的颜色,才重新尝试了 iconpark。从 iconpark 下载的 SVG 代码很符合我的口味,优雅啊!
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="6" y="6" width="28" height="36" rx="2" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="butt"
stroke-linejoin="miter"/>
<path d="M42 6V42" stroke="currentColor" stroke-width="4" stroke-linecap="butt" stroke-linejoin="miter"/>
</svg>
这是我修改后的 SVG 代码,从 iconpark 下载的原始 SVG 代码中,stroke
是 #333
,这是用来配置 SVG 的颜色,知道了 currentColor
可以使用当前该元素所处环境的文字颜色,就尝试了下,嘿!还真行!
编辑区域
看过其他 Markdown 编辑器的编辑区,很多用的是 textarea
,属实难看,在找 Markdown 编辑器的时候看中过一个比较可以的 Markdown 编辑器,提了个 issue 看能否支持编辑区高亮,被作者拒绝了,我也无奈~
然后看了 bytemd 的编辑区,诶!codemirror?等等,这个我好像在哪见到过。。。
是的,我在写 Json Schema 可视化编辑器 的时候有了解过,当时因为属实有点丑,就放弃了,用了 Monaco Editor,而且 Semi Design 的代码示例也是用的这个,很漂亮呀!同时也有很多开源的主题可以选择,那就没理由不用它了。
选一个 Monaco Editor 的 React 组件使用:react-monaco-editor、monaco-react,两者差别不大。

Markdown 渲染区域
渲染 Markdown 的 React 开源组件也很多,也有很多基础轮子可以用,比如 remark、marked 等等很多优秀的开源项目,但是!这些我都放弃了,因为!我用不来,或者说用不好会贴切一点。
最后选择了 react-markdown ,它的 示例 渲染得挺合我口味的(忽略左边的编辑区域),而且之前玩过这个开源项目,它的样式是导入一些主题的 css 文件,那我修改 css 让渲染的结果可以适配暗黑模式和亮色模式即可,这个我也做得来~
实现过程
这部分主要是说明各个组件我是如何实现的,怎么实现的,实现过程中遇到的部分问题是怎么解决的,至于所有源码,在文末 结语 附有说明,感兴趣可以自己去看哈,这里就不贴大量代码了。
Toolbar
在 Toolbar 中,每个图标都是一个可操作的区域,这个区域很小,鼠标移动过去后有样式变化,给用户反馈:这是个可操作的区域

这个地方用 Button 的话有点大材小用了,而且改样式也很麻烦,那就自己定义一个:
export const Option = styled.div`
background-color: rgb(var(--gray-2));
border-radius: var(--border-radius-small);
color: var(--color-text-1);
padding: 0 8px;
line-height: 26px;
height: 26px;
font-size: 14px;
cursor: pointer;
:hover {
background-color: rgb(var(--gray-3));
}
`;
在所有的操作中可以分为三类:
- 光标选中的字符前后添加内容,并移动光标位置
- 在下一行添加内容,并移动光标位置
- 在光标当前行的开头添加内容,并移动光标位置
但是有一个菜单比较特别,那就是标题菜单,标题需要去掉/添加合适的 #
数量来设置/修改当前行的标题级别。
定义四个方法:
changeHeading
修改当前行的标题级别setStartAndEndCharacters
在选中的字符前后添加内容addCharactersAtLineStart
在当前行的开头添加内容addBlockValue
在下一行添加内容块,并移动光标位置
至于如何获取光标位置,如何在 Monaco Editor 中添加内容,在 Monaco Editor 的官网有非常详细的文档,这里就不过多提及了,或者可以自己看下文末的源码。
在实现 Editor 和 Viewer 之前,提供了可拖拽的分割线,让用户可以自行调整 Editor 和 Viewer 两者的占用比例,使用了 ResizeBox 组件实现。
Editor
Editor 的实现主要是调整 Monaco Editor 的属性,属性的设置是结合了上面选的 Monaco Editor React 组件和 Monaco Editor 的文档进行调整。
// https://github.com/lin-mt/quiet-web/tree/master/src/components/QuietEditor
<QuietEditor
value={value}
paddingTop={16}
paddingBottom={16}
lineNumbers={'off'}
language={'markdown'}
minimapEnabled={false}
lineDecorationsWidth={0}
onMount={handleEditorDidMount}
onChange={(val) => {
setValue(val);
if (props.onChange) {
props.onChange(val);
}
}}
style={{
width: '100%',
height: '100%',
borderRadius: 0,
borderWidth: 0,
}}
/>
Viewer
Viewer 使用的是 react-markdown,样式部分需要引入具体的 css 文件,为了能适应亮色模式和暗黑模式,我把 css 文件直接复制出来,而不是使用 import
的方式引入,这样更方便修改它的样式。
为了让 Markdown 支持更多的功能,比如数学公式、代码高亮、emoji、Mermaid等等,需要引入相应的插件支持,可使用的插件有 remark plugins 和 rehype plugins。
实现一个 rehype 插件
为了支持 Mermaid 图表,找了很多插件,古老的插件倒是有,但是用不了,找到了一个插件 remark-mermaidjs,目前这个插件还有些 BUG。
然后就想看看有没有 rehype 插件支持 Mermaid,也确实找到了 rehype-mermaid,可以用,但是也存在跟 remark-mermaidjs 类似的 BUG,好在我能看懂部分代码,在组件 MermaidBlock.tsx 中用到了 useEffect
,deps
参数是空的,也就是只会渲染一次,内容出现变化的时候,图就消失了,那就在 deps
参数中加上 props.code
试试,诶!还真行~!
到此,Markdown 编辑器的基础功能基本实现了。虽然上面的实现过程描述得云淡风轻,但是实际我在实现的时候还是耗费了大量时间。
效果图
编辑器的亮色模式和暗色模式是如何实现的?这个需要结合 Arco Design Pro 的主题切换,同时在 Markdown 的样式中使用 Arco Design 的颜色变量代替 css 的颜色常量即可,当然用到的开源轮子也要配置好它的亮色模式和暗色模式。
亮色模式
暗色模式
结语
前端代码可能写得有点粗糙(javaer 瑟瑟发抖)😄
目前编辑器还实现了仅编辑区、仅预览区、网页全屏等功能,所有的代码均在 quiet-web 下的 src/components/QuietMarkdown
,目前图片上传的功能还未实现,后端的文件服务还在写,感兴趣的可以关注下~
当然,这个编辑器还有很多需要改进的点,如果要实现通用的编辑器,需要提供插件、国际化、样式适配等扩展性功能。还有一些细节不是很完善,比如在使用无序列表、有序列表的时候,按回车键,下一行会自动添加前缀,无论当前行是否有内容,这个体验跟 bytemd 是不一样的。
好了!这篇文章到这里就结束了!现在多多少少也应该知道了该如何自己实现一个简易的 Markdown 编辑器了吧。

后面的两篇会写得晚点,因为还在写实现代码。
下一篇
如何结合 Minio 实现一个简单的可嵌入的 Spring Boot Starter 文件服务
下下篇
如何让 Markdown 编辑器实现两种方式的文件上传
转载自:https://juejin.cn/post/7174105780335935524