手把手教你用100行程式碼實現基於 react的 markdown 輸入 + 即時預覽線上編輯器

清夜發表於2017-12-25

DOM結構

首先,先上效果圖:

12

首先說明一下,本文的一些細節或者技巧是建立在我的另外一篇文章上的,如果你在讀的過程中,有什麼地方不太清楚的,可以先去看看那篇文章,或許可以找到答案。

左側是 markdown輸入框,右側是對應的 markdown輸出即時預覽框,兩個元素框可以相互跟隨滾動。

由效果圖可以基本確定,整個頁面大概分為三個大塊,頂部的 header標題輸入框、主體左側 markdown輸入框、主體右側 markdown即時預覽框。

於是,可以很快速地寫下 DOM

render() {
  return [
    <header key='header'>
      <input type="text" placeholder="輸入文章標題..." spellCheck="false"/>
    </header>,
    <div key='main'>
      <div>
        <div contentEditable="plaintext-only"></div>
      </div>
      <div>
        <div></div>
      </div>
    </div>
  ]
}
複製程式碼

結構很簡單,沒什麼好說的,除了那個帶有 contentEditable屬性的div元素

能夠作為輸入框的元素大概有三種:inputtextarea以及contentEditable不為 false的元素

因為要輸入大段文字內容,所以 input是指望不上了,又需要比較方便地獲取到輸入內容的總高度,斟酌再三 textarea也可以劃掉了,只剩下第三個選項了。

對於一個元素,只要指定其 contentEditable屬性,並且其值不為 false,那麼此元素就是可以編輯的,不過大部分人只知道 contentEditable=true是什麼意思,可能還不知道此屬性值還可以為 plaintext-only

並且此屬性還不止可以取這兩個值,此屬性一共支援 6個值,至於為什麼我這裡使用 plaintext-only而不是 true,以及那 6個值都是什麼意思,具體可以參考 張鑫旭大神的這篇文章, 當取值為 plaintext-only時,表示當前可編輯元素只能輸入純文字,富文字是無法輸入的。

另外,contentEditable屬性本身雖然早就已經被包括 IE6在內的絕大部分瀏覽器所支援,相容性不是問題,但是當其取值為plaintext-only時,則只有 chrome等現代瀏覽器可正確識別,並且識別率還比較低,不過我覺得這不是問題,既然連markdown線上編輯器都用上了,那麼這個使用者的電腦上不至於還有 IE6的存在。

將樣式補齊後,基本上編輯器的雛形就有了,左側輸入框可以任意輸入內容了,下一步,就要把所輸入的內容即時轉為對應的預覽頁面。


marked

markdown轉為 HTML的外掛有很多,我這裡用的是其中較受歡迎的一個:marked,此外掛的優勢在於編譯速度,正好符合我們即時預覽的需求。

首先安裝此外掛,安裝完成後引入元件內:

import marked from 'marked'
複製程式碼

此外掛使用很簡單,只需要傳入你所需要編譯的 markdown文字,然後再根據需求設定相應的配置就行。

我們這裡這裡需要傳入的 markdown文字自然就是在左側輸入框內輸入的內容了,由於需要即時編譯,所以就需要監聽此輸入框元素的輸入事件(input),每次輸入都將輸入的文字重新編譯一次:

<div contentEditable="plaintext-only" onInput={this.onContentChange}></div>
複製程式碼

監聽到文字內容發生變化,則對文字進行編譯,並將編譯出來的 HTML傳入到右側即時預覽容器元素中:

onContentChange(e) {
  this.setState({
    previewContent: marked(e.target.innerText, {breaks: true})
  })
}
複製程式碼

marked就是暴露出來的編譯方法,使用 previewContent這個 state來為右側預覽容器傳入內容。

需要注意的是,我使用 innerText而不是 innerHTML來獲取 contentEditable元素的內容,這是因為如果你使用 innerHTML的話,當你輸入一些特殊字元,例如 ><等,innerHTML的最終值都會自動幫你把這些特殊字元轉為對應的 字元實體,例如 <轉為 &lt;>轉為 &gt;

這本來是沒什麼問題的,只要能正確顯示就行了,但我們還需要將這些字元通過 marked轉譯為對應的 HTML,這樣就有問題了,而使用 innerText就可以避免這個問題。

即時編譯預覽的問題GET


程式碼高亮

我們有時候可能會輸出一些程式碼,如果能讓程式碼高亮那就完美了,於是我引入了 highlight.js這個外掛。

此外掛可配合 marked一起使用,只需要對 marked進行配置,將 highlight.js作為一個配置項即可:

marked.setOptions({
  highlight (code) {
    return highlight.highlightAuto(code).value
  }
})
複製程式碼

這樣,只要是通過 marked方法編譯出來的HTML,就自動會應用上 highlight.js了,如果你還有其他的需求,也可以自己對 marked進行配置。

程式碼高亮GET


跟隨滾動

這個問題的詳細分析我已經在另外一篇文章中說得差不多了,不清楚得可以去看下。

解決此問題的關鍵點只有兩個:

  1. 正確判斷當前主動滾動的容器元素
  2. 確定輸入框容器元素與預覽框容器元素之間 scrollTop的比例值
  • 正確判斷當前主動滾動的容器元素

不論是滑鼠滾輪滾動還是拖動滾動條滾動,此時滑鼠都肯定是在那個被滾動的容器範圍內的,滑鼠進入某個元素範圍內會觸發 mouseover事件,所以可以使用此事件來記錄當前滑鼠將要滾動的容器元素。

<div className="common-container editor-container" onMouseOver={this.setCurrentIndex.bind(this, 1)}>
複製程式碼
setCurrentIndex(index) {
  this.currentTabIndex = index
}
複製程式碼

如上,記錄 this.currentTabIndex這個值,不同的值表示當前滑鼠位於不同的元素上,接下來的滾動事件肯定就是這個元素觸發的,確定了主動滾動元素,則其他的滾動就都是被動的跟隨滾動了,便可以進行區分處理。

  • 確定輸入框容器元素與預覽框容器元素之間 scrollTop的比例值

這個比例值 scale是可以根據已知條件確定的,即:

scale = (ch1 - ph1) / (ch2 - ph2)
複製程式碼

至於上面的公式是什麼意思,請移步我的另外一篇文章,裡面有詳細說明。

this.scale = (this.previewWrap.offsetHeight - this.previewContainer.offsetHeight) / (this.editWrap.offsetHeight - this.editContainer.offsetHeight)
複製程式碼

previewWrap為右側預覽容器的內容元素,previewContainer為右側預覽容器元素;editWrap為左側 markdown編輯容器的內容元素,editContainer為左側 markdown編輯容器元素。

顯而易見,由於你在左側編輯框中輸入內容的時候,輸入框的內容高度(this.editWrap.offsetHeight)以及預覽框內容的高度(this.previewWrap.offsetHeight)肯定是會發生相應變化的,所以 scale值也就不固定。

簡單點話,每次監聽到輸入框的 input事件(輸入、刪除等操作都會觸發此事件),就重新計算一遍 scale值,這點效能損耗微乎其微,完全可用,不過本著一個技術人崇高的敬業精神也,稍微分析一下其實這點效能損耗還可以降到更低。

scale這個值只有當滾動容器的時候才會用到,所以沒必要每次改變輸入框內的文字就重新計算一次,只要保證在滾動的時候這個值是正確的就行了,並且也沒必要每次滾動的時候都要重新計算一次 scale,只要輸入框的內容沒變,使用上次計算出來的值即可,因而可以使用一個變數 hasContentChanged來記錄標識輸入框內容是否發生了變化。

onContentChange(e) {
  this.setState({
    previewContent: marked(e.target.innerText)
  })
  !this.hasContentChanged && (this.hasContentChanged = true)
}
複製程式碼

簡單的 markdown即時預覽編輯器基本上就是這樣了,如果你想要更加複雜的功能,只需要在此基礎上進行增改即可。

例如,你想在其中插入一張圖片,用markdown語法連結一個圖片的格式大概是這種 ![圖片](https://avatars2.githubusercontent.com/u/21095835?s=460&v=4),其實這與編輯器本身已經無關了,你只需要將上傳的圖片儲存到伺服器,或者用 Blob暫存在瀏覽器,然後將地址按照正確的語法賦給編輯器就行了。

本文的可執行示例程式碼已經放在了 Github上,有興趣的可以去看下。


更多

根據以上思路,基本上可以完成一個 markdown線上+預覽編輯器了,雖然功能較為簡單,但是確實是可用的,想要更加複雜的功能,可能還需要你自己在此基礎上進行增改,比如自定義搜尋、搜尋結果高亮、markdown輸入文字高亮等,這些功能雖說難度不高,但是也不是幾行程式碼就能完成的事情,如果真跟這些較勁的話,那麼 996怕是跑不了了。

不過別擔心,很明顯,線上編輯器是一個歷史悠久的剛需,在輪子造的飛起的前端領域,一個預置了所有你需要功能的開箱即用的編輯器外掛肯定早就存在了,而你要做的,只是隨便寫幾行配置就行。

類似的外掛,大名鼎鼎的有 AceCodeMirror等。

聽說一篇文章如果寫得太長,耐心看到後面的人就會出現斷崖式下跌,所以我決定將剩下的內容放到下一篇文章中,下篇文章,我將介紹如何使用 Aceh和CodeMirror來打造一個與本文類似的線上編輯器。

相關文章