我是一個掘金重度用戶,不只常常在掘金上挖掘含金量高的文章,偶爾還在掘金上創做技術文章。相信讀者們也對掘金很是滿意,尤爲是它的文章編輯器,不只支持Markdown編輯,並且還支持代碼高亮、分屏預覽、自動保存等等。本文將用React+CodeMirror+Showdown實現一個相似於掘金編輯器的單頁應用。css
先不說那麼多,先上動圖效果吧。html
下面是掘金文章編輯器的佈局。node
能夠看到,編輯器主要由5個部分組成:react
咱們首先須要作的是將各個位置擺放出來。git
建立一個文件叫Demo.tsx
,輸入如下內容。(咱們先無論怎麼構建一個React+Typescript應用,這裏只看邏輯)github
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
來引用,就能夠將其涵蓋的樣式(包括僞類)「繼承」過來。npm
而後,咱們在樣式文件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)
就能讓中間部分填滿屏幕高度了。微信
效果以下。markdown
咱們須要在頂部加入標題輸入框。將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
的默認樣式。
效果以下。
咱們用很受歡迎的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編輯器和右邊的預覽同步。
在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加入交流羣。
本篇文章由一文多發平臺ArtiPub自動發佈