共计 6452 个字符,预计需要花费 17 分钟才能阅读完成。
前言
我是一个掘金重度用户,不仅经常在掘金上挖掘含金量高的文章,偶尔还在掘金上创作技术文章。相信读者们也对掘金非常满意,尤其是它的文章编辑器,不仅支持 Markdown 编辑,而且还支持代码高亮、分屏预览、自动保存等等。本文将用 React+CodeMirror+Showdown 实现一个类似于掘金编辑器的单页应用。
动图效果
先不说那么多,先上动图效果吧。
布局
下面是掘金文章编辑器的布局。
可以看到,编辑器主要由 5 个部分组成:
- 顶部栏
- 左侧 Markdown 编辑器
- 左侧底部
- 右侧预览
- 右侧底部
我们首先需要做的是将各个位置摆放出来。
创建一个文件叫Demo.tsx
,输入以下内容。(我们先不管怎么构建一个 React+Typescript 应用,这里只看逻辑)
import React from 'react';
// 引入样式
import style from './Demo.scss';
const Demo: React.FC = () => {
return (<div className={style.articleEdit}>
<div className={style.topBar}>
顶部栏
</div>
<div className={style.main}>
<div className={style.editor}>
<div className={style.markdown}>
左侧 Markdown 编辑器
</div>
<div className={style.footer}>
左侧底部
</div>
</div>
<div id="preview" className={style.preview}>
<div
id="content"
className={style.content}
>
右侧预览
</div>
<div className={style.footer}>
右侧底部
</div>
</div>
</div>
</div>
);
};
export default Demo;
这里的 React.FC
是FunctionComponent
的简写,表示一个函数型组件。在组件中返回的是 jsx
中的模版内容。style.xxx
是 React 独有的引用样式的一种方式,即样式封装在 className
中,在 React 组件中直接通过 className
来引用,就可以将其涵盖的样式(包括伪类)“继承”过来。
然后,我们在样式文件 Demo.scss
中输入以下样式内容。
.articleEdit {
height: 100vh;
color: red;
font-size: 24px;
}
.topBar {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
border-bottom: 1px solid #eee;
}
.main {display: flex;}
.editor {flex: 1 1 50%;}
.markdown {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 100px);
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
}
.preview {flex: 1 1 50%;}
.content {
display: flex;
align-items: center;
justify-content: center;
height: calc(100vh - 100px);
border-bottom: 1px solid #eee;
}
.footer {
display: flex;
align-items: center;
justify-content: center;
height: 50px;
border-right: 1px solid #eee;
}
在样式中,我采用了弹性布局 display: flex
来做分屏。对于如何自动填充高度,稍稍有些麻烦,不过最后通过 100vh
解决了。vh
这个单位其实是浏览器视野中高度的百分比单位。假设浏览器屏幕高度为 640px,1vh
就代表 6.4px。因此,顶部高度 50px
,底部高度50px
,中间的高度设置为height: calc(100% - 100px)
就能让中间部分填满屏幕高度了。
效果如下。
顶部标题输入框
我们需要在顶部加入标题输入框。将 className
为topBar
的 div 标签替换为下面内容。其中 Input
是antd
中的组件。
<div className={style.topBar}>
<Input className={style.title} placeholder="请输入文章标题"/>
</div>
在 Demo.scss
中加入以下内容。
.title {
margin-left: 10px !important;
font-size: 24px !important;
border: none !important;
}
.title:focus {box-shadow: none !important;}
这里 important
是为了覆盖 antd
的默认样式。
效果如下。
左侧 Markdown 编辑器
我们用很受欢迎的 CodeMirror 来做 Markdown 编辑器支持。在 React 中我们引用 react-codemirror2
封装好的第三方封库。
我们更改一下 Demo.tsx
为以下内容。
import React from 'react';
import {Input} from "antd";
import {UnControlled as CodeMirror} from 'react-codemirror2'
// 引入样式
import style from './Demo.scss';
// 引入 CodeMirror 样式
import 'codemirror/mode/markdown/markdown';
const Demo: React.FC = () => {
// 调整 CodeMirror 高度
setTimeout(() => {const $el = document.querySelector('.CodeMirror');
if ($el) {$el.setAttribute('style', 'min-height:calc(100vh - 100px);box-shadow:none');
}
}, 100);
return (<div className={style.articleEdit}>
<div className={style.topBar}>
<Input className={style.title} placeholder="请输入文章标题"/>
</div>
<div className={style.main}>
<div className={style.editor}>
<div className={style.markdown}>
<CodeMirror
className={style.codeMirror}
options={{
mode: 'markdown',
theme: 'eclipse',
lineNumbers: true,
smartIndent: true,
lineWrapping: true,
}}
/>
</div>
<div className={style.footer}>
左侧底部
</div>
</div>
<div id="preview" className={style.preview}>
<div
id="content"
className={style.content}
>
右侧预览
</div>
<div className={style.footer}>
右侧底部
</div>
</div>
</div>
</div>
);
};
export default Demo;
在这里,我们引用了 CodeMirror 中 Markdown 的样式,然后在代码中引用了 UnControlled
为 CodeMirror 组件,并加入相应的配置。另外,由于第三方组件是将 .CodeMirro
写死为 height: 300px
,我们需要手动将该高度调整为我们需要的高度,用了document.querySelector
以及 $el.setAttribute
这两个方法(见以上代码)。
在 Demo.scss
引入 CodeMirror 的 CSS 样式,内容如下。
@import '../../../node_modules/codemirror/lib/codemirror.css';
@import '../../../node_modules/codemirror/theme/eclipse.css';
...
.codeMirror {width: 100%;}
右侧预览
这次我们将用 showdown 来做预览模块。
这次我们还是首先改造一下Demo.tsx
。加入一部分引入逻辑和监听函数。
import showdown from 'showdown';
showdown.setOption('tables', true);
showdown.setOption('tasklists', true);
showdown.setFlavor('github');
...
const Demo: React.FC = () => {
...
// markdown to html 转换器
const converter = new showdown.Converter();
// 内容变化回调
const onContentChange = (editor: Editor, data: EditorChange, value: string) => {const $el = document.getElementById('content');
if (!$el) return;
$el.innerHTML = converter.makeHtml(value);
};
return (
...
<CodeMirror
className={style.codeMirror}
options={{
mode: 'markdown',
theme: 'eclipse',
lineNumbers: true,
smartIndent: true,
lineWrapping: true,
}}
onChange={onContentChange}
/>
...
<div
id="content"
className={style.content}
>
<article
id="content"
className={style.content}
/>
</div>
...
)
};
其中,我们在 CodeMirror
中加入了 onContentChange
回调,每一次 Markdown 中内容更新时,会利用 showdown
来生成 HTML 代码,并加入到 #content
的innerHTML
中。这样,就可以实时预览编辑的内容了。
另外,我们还需要自定义一下预览模块的 CSS 内容,我们在 Demo.scss
中加入以下内容。
...
article {
height: 100%;
padding: 20px;
overflow-y: auto;
line-height: 1.7;
}
h1 {
font-weight: bolder;
font-size: 32px;
}
h2 {
font-weight: bold;
font-size: 24px;
}
h3 {
font-weight: bold;
font-size: 20px;
}
h4 {
font-weight: bold;
font-size: 16px;
}
h5 {
font-weight: bold;
font-size: 14px;
}
h6 {
font-weight: bold;
font-size: 12px;
}
ul {list-style: inherit;}
ol {list-style: inherit;}
pre {
overflow-x: auto;
color: #333;
font-family: Monaco, Consolas, Courier New, monospace;
background: #f8f8f8;
}
img {
max-width: 100%;
margin: 10px 0;
}
table {
max-width: 100%;
overflow: auto;
font-size: 14px;
border: 1px solid #f6f6f6;
border-collapse: collapse;
border-spacing: 0;
thead {
color: #000;
text-align: left;
background: #f6f6f6;
}
}
td,
th {
min-width: 80px;
padding: 10px;
}
tbody tr:nth-of-type(odd) {background: #fcfcfc;}
tbody tr:nth-of-type(even) {background: #f6f6f6;}
效果如下。
这样,我们就可以在左边编辑 Markdown 的时候右边预览跟着一起实时渲染了。
底部
底部相对来说比较简单,就是往里填充内容就可以了。
在 Demo.tsx
的 footer 部分分别填入如下内容。
...
<label style={{marginLeft: 20}}>Markdown 编辑器 </label>
...
<label style={{marginLeft: 20}}> 预览 </label>
...
在 Demo.scss
中的 .footer
中去掉justify-content: center
,让其按照默认的左对齐。
效果如下。
Markdown 和预览滑动联动
编辑功能做好了,但是我们想让 Markdown 编辑器和右边的预览同步。
在 Demo.tsx
中加入一个函数,挂在 CodeMirror 组件上。
...
// 监听左右侧上下滑动
const onEditorScroll = (editor: Editor, scrollInfo: ScrollInfo) => {const $el = document.querySelector('#content') as HTMLDivElement;
if (!$el) return;
$el.scrollTo(0, Math.round(scrollInfo.top / scrollInfo.height * ($el.scrollHeight + $el.clientHeight)));
};
...
<CodeMirror
className={style.codeMirror}
options={{
mode: 'markdown',
theme: 'eclipse',
lineNumbers: true,
smartIndent: true,
lineWrapping: true,
}}
onChange={onContentChange}
onScroll={onEditorScroll}
/>
...
这里,我们利用了 scrollTo
的方法。这个方法接收 x 和 y 参数。由于我们是垂直滚动,因此只用了 y 参数。
总结
这样,我们就实现了一个简易的掘金风格的文章编辑器。当然,掘金编辑器还有很多功能(例如自动保存、展开收缩、字数统计等等),这里只实现了一部分主要功能。
本文里实现的文章编辑器是我的新开源项目 ArtiPub(意为 Article Publisher)其中一部分。该项目旨在解决文章发布管理困难的问题,希望实现多平台文章发布,现正在不断开发中。感兴趣的可以关注一下,加我微信 tikazyq1 或扫下方二维码注明 ArtiPub 加入交流群。
- 本文编辑器源码: https://github.com/crawlab-te…
- ArtiPub Github: https://github.com/crawlab-te…
<p align=”center”>
<img src="https://user-gold-cdn.xitu.io/2019/7/31/16c48234c8f5b366?w=674&h=896&f=jpeg&s=132795" height="360">
</p>
本文由文章发布工具 ArtiPub 自动生成