實現一個掘金Style的文章編輯器

前言

我是一個掘金重度用戶,不僅經常在掘金上挖掘含金量高的文章,偶爾還在掘金上創作技術文章。相信讀者們也對掘金非常滿意,尤其是它的文章編輯器,不僅支持Markdown編輯,而且還支持代碼高亮、分屏預覽、自動保存等等。本文將用React+CodeMirror+Showdown實現一個類似於掘金編輯器的單頁應用。

動圖效果

先不說那麼多,先上動圖效果吧。

佈局

下面是掘金文章編輯器的佈局。

可以看到,編輯器主要由5個部分組成:

  1. 頂部欄
  2. 左側Markdown編輯器
  3. 左側底部
  4. 右側預覽
  5. 右側底部

我們首先需要做的是將各個位置擺放出來。

創建一個文件叫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.FCFunctionComponent的簡寫,表示一個函數型組件。在組件中返回的是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)就能讓中間部分填滿屏幕高度了。

效果如下。

頂部標題輸入框

我們需要在頂部加入標題輸入框。將classNametopBar的div標籤替換爲下面內容。其中Inputantd中的組件。

<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代碼,並加入到#contentinnerHTML中。這樣,就可以實時預覽編輯的內容了。

另外,我們還需要自定義一下預覽模塊的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加入交流羣。

<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自動生成
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章