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


DOM結構

首先,先上效果圖:html

12

首先說明一下,本文的一些細節或者技巧是創建在個人另一篇文章上的,若是你在讀的過程當中,有什麼地方不太清楚的,能夠先去看看那篇文章,或許能夠找到答案。前端

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

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

因而,能夠很快速地寫下 DOMajax

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元素chrome

可以做爲輸入框的元素大概有三種:inputtextarea以及contentEditable不爲 false的元素瀏覽器

由於要輸入大段文本內容,因此 input是期望不上了,又須要比較方便地獲取到輸入內容的總高度,斟酌再三 textarea也能夠劃掉了,只剩下第三個選項了。bash

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

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

另外,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來打造一個與本文相似的在線編輯器。

相關文章
相關標籤/搜索