1. 程式人生 > >基於Ace和CodeMirror打造markdown 輸入 + 即時預覽線上編輯器

基於Ace和CodeMirror打造markdown 輸入 + 即時預覽線上編輯器

本文介紹如何使用 AceCodeMirror來實現一個基於 reactmarkdown 輸入 + 即時預覽線上編輯器

Ace版本

Ace算是一個久經考驗的老牌編輯器外掛了,現在很多大公司都在用這個東西,似乎 Github曾經就使用 Ace用於構建它的線上編輯器(雖然現在不用了)。

AceGithub上只是存放了其專案,更多詳細的介紹,例如如何開始以及 API等文件都放在它的官網上

溫馨提示:

  1. 如果你開啟其 官網發現載入失敗,或者頁面不全,那麼可能需要你翻牆重新請求一遍才行,因為雖然其官網的大部分資源牆內就能訪問,但一些指令碼檔案,例如 jQuery
    是牆外的,所以可能出現數據載入失敗的情況。
  2. Ace的文件讀起來可能有些困難,這裡的困難並不是指其文件都是英文的,如果只是英文閱讀障礙,線上翻譯一下也就ok了,而是說你可能不知道該從哪裡閱讀,不知從何下手,這也是大部分開源專案的通病,這個問題可能就需要你多翻看幾遍,找到文件編寫規律後再閱讀應該就容易多了。

引入 Ace

本文所要實現的編輯器雖然是基於 Ace,但是沒有直接使用 Ace,而是使用了其一個封裝外掛 brace,至於為什麼不直接使用 Acebrace專案也有說明,可以自己去看看,另外,由於本文所要實現的編輯器還是基於 React的,所以為了使用方便,需要對 Ace

進行一層封裝,將其包裹成一個 React元件。

Github上也有人做過這種事情了,例如 react-ace,由於此專案規模較大,API和方法很多,此專案只是封裝了其部分功能,我看了下react-ace的封裝程式碼,可能它的封裝無法滿足我的需求,所以我就抽出了其中一部分程式碼,並進行了稍微的修改。

另外,本文所要實現的編輯器是間接基於 Ace,直接基於 brace的,所以所要安裝的包是 brace:

npm i brace -S

基本的 DOM結構和 100行程式碼實現基於react的markdown輸入+即時預覽線上編輯器是差不多的,只不過在左側輸入容器的子元素由原來具有 contentEditable="plaintext-only"

屬性的 div換成了 Ace元件:

<AceEditor
  mode="markdown"
  theme="github"
  wrapEnabled={true}
  tabSize={2}
  fontSize={14}
  showGutter={false}
  height={state.aceBoxH + 'px'}
  width={'100%'}
  debounceChangePeriod={60}
  onChange={this.onContentChange}
  onScroll={this.containerScroll.bind(this, 1)}
  name="aceEditorMain"
  editorProps={{$blockScrolling: true}}/>

上述 <AceEditor/>的元件屬性都是能在 Ace文件裡找到的,這裡只簡單說明一下:

  1. mode:編輯器的整體模式或樣式,這裡取值為 markdown,表明需要用這個編輯器來輸入 markdown文字,這樣編輯器就會進行相應的初始設定。
  2. theme:編輯器主題,這裡使用了 github這個主題。
  3. wrapEnabled:當輸入的一句文字比一行的長度要長時,是否允許換行。
  4. tabSize:使用幾個空格來表示表示一次 Tab按鍵。
  5. fontSize:文字的字型大小
  6. height:編輯器的高度,單位為 px
  7. width:編輯器的寬度,單位為 px
  8. debounceChangePeriod:多長時間對輸入響應一次,單位為 ms,類似於節流。
  9. onChange:文字框內容發生變化時的回撥函式。
  10. onScroll:文字框內容發生滾動時的回撥函式。
  11. name:編輯器的 id
  12. editorProps:當在文字框內輸入內容時,是否需要滾動條進行響應的滾動定位。

功能實現

大部分的功能點與100行程式碼實現基於react的markdown輸入+即時預覽線上編輯器這篇文件的類似,不過由於使用 Ace與 直接的 contentEditable="plaintext-only"屬性的 div還是存在很多不同的地方,需要對這些地方進行相應的調整。

  • onContentChange方法

當文字內容發生變化時,<AceEditor/>元件的回撥函式 onChange被觸發,其會返回一個值,此值就是當前編輯器的完整文字內容字串,所以直接接收即可,無需做其他的額外操作:

onContentChange(value) {
  this.previewWrap.innerHTML = marked(value)
}
  • 獲取 <AceEditor/>元件內容高度以及scrollTop值。

Ace使用了一種 VirtualRenderer的技術,你可能無法直接使用 DOM來獲取編輯器本身的某些屬性和方法,需要間接地呼叫 Ace暴露出來的方法才行。

例如,你需要這樣獲取編輯器文字內容的高度:

editorHandler.getSession().getScreenLength()*editorHandler.renderer.lineHeight

editorHandler是編輯器的一個 Handler,可以使用此 handler來完成一些對編輯器的操作,getScreenLength()方法獲取到編輯器內當前所有文字的總行數,這個行數是包括換行的,lineHeight是每行文字的高度,二者相乘即得到內容的總高度,我沒看到 Ace直接暴露出獲取內容總高度的方法,所以使用了這種操作。

如果你想獲取編輯器滾動的高度 scrollTop,那麼就需要使用下面這個方法:

editorHandler.renderer.getScrollTop()

或者直接呼叫屬性也可以:

editorHandler.renderer.scrollTop()

其中,editorHandler這個 Handler我再封裝 Ace的時候,已經暴露出來了,需要的時候匯出即可:

import AceEditor, {editorHandler} from '../../Component/AceEditor/index'

程式碼高亮

<AceEditor/>編輯器內輸入的文字高亮,是由編輯器元件的兩個屬性控制的:modetheme,當你指定了這兩個屬性時,你在編輯器內輸入的文字,無論是 markdown標記還是程式碼段就都已經自動高亮的了,例如,在編輯器內輸入下述程式碼段,編輯器會自動對其進行高亮處理:

#container {
  display: flex;
  border: 1px solid #bbb;
}
.left, .right {
  flex: 1;
  height: 100%;
  word-wrap: break-word;
  overflow-y: scroll;
}

輸入效果示例如下:

這裡寫圖片描述

至於預覽內容的高亮,依舊是藉助 highlight.js,不過這個東西感覺內建的樣式有點問題(也可能是我使用方法有問題),所以我只是使用了其 js指令碼,用於讓 marked輸出正確格式的 html,至於樣式,我沒有用 htghlight.js內建的,而是參照其樣式自己修改了一份 js-highlight.css

這樣做的好處是,既可以去除冗餘的程式碼減小程式碼體積,同時也能自定義自己喜歡的顏色主題。

CodeMirror版本

CodeMirrorAce 都是開源線上編輯器中的佼佼者,在 Github上的星數也都不相上下,不過據我至今的觀測來看,無論是除錯還是文件方面,CodeMirror都比 Ace更加友好得多,如果你對著 CodeMirror的文件無從下手的話,那麼建議你先去看看 Ace的文件,然後再回來看 CodeMirror的,你就會發現,二者的入手體驗真的不是在一個層次的。

引入 CodeMirror

CodeMirror的文件基本上也都是放在其官網上,Github上存放了其原始碼以及各種 Demo

下載完成後,同樣的,由於本文所要實現的編輯器是基於 React,所以最好將其封裝成一個 React元件,Github上也已經有人做過這個事了,不過和上述 react-ace的原因類似,react-codemirror這個專案也只是封裝了部分常用的 API和功能,直接拿來用也無法滿足我的要求,所以我就在其基礎上進行了稍微的修改。

封裝完成後的 CodeMirror元件的使用,可以類似於下面這種:

<CodemirrorEditor
  ref="editor"
  onScroll={this.containerScroll.bind(this, 1)}
  onChange={this.updateCode.bind(this)}
  options={
    lineNumbers: true,
    theme: 'solarized',
    tabSize: 2,
    lineWrapping: true,
    readOnly: false,
    mode: 'markdown',
    // 是否自動閉合標籤,基於 codemirror/addon/edit/closetag
    autoCloseTags: true,
    // 自定義快捷鍵
    extraKeys: this.setExtraKeys()
  }
  autoFocus={true}/>

這些屬性所代表的含義都可以在 CodeMirror的官網上找到,這裡只稍微說明下。

  1. ref: 用於方便元件內部對 CodeMirror容器的引用
  2. onScroll: 編輯器內容滾動時觸發的回撥
  3. onChange: 編輯器內容發生變化時觸發的回撥
  4. options: 一些配置引數,例如是否顯示行數、編輯器主題、縮排空格數、是否允許軟換行、是否只讀、文字內容的模式、是否自動閉合標籤、自定義快捷鍵等
  5. autoFocus: 是否自動聚焦

功能實現

大部分的功能點與上節Ace的類似,不過由於程式碼邏輯不同,所以需要細微調整。

  • containerScroll

編輯器內容滾動時觸發的回撥函式,呼叫 onScroll方法,此方法返回了當前編輯器的相關位置引數,可以直接獲取到滾動條的 scrollTop值,可以藉助 CodeMirror元件暴露出來的編輯器控制代碼 CodemirrorHandler,通過呼叫 scrollTo函式來控制滾動條的滾動。

CodemirrorHandler.scrollTo(null, this.previewContainer.scrollTop / state.scale)
  • updateCode

當編輯器內容發生變化出觸發的回撥函式,可以直接獲得編輯器輸入的文字內容,對此內容呼叫 marked方法將其編譯成對應的 HTML

程式碼高亮

CodeMirror也可以對輸入的內容進行高亮處理,CodeMirror元件的 mode屬性用於指定編輯器的模式,當指定此值為 markdown時,編輯器就會對輸入的內容按照 markdown的語法來進行高亮處理,例如新增 css類名等,除此之外,還需要配合樣式才能達到視覺上的效果。

CodeMirror內建了很多主題樣式,你可以根據自己的需求進行選擇:

這裡寫圖片描述

我這裡選擇了 solarized這個主題,所以需要將此主題對應的樣式檔案引入:

require('codemirror/theme/solarized.css')

除此之外,你還需要為 CodeMirror元件顯式配置這個主題才能生效:

theme: 'solarized'

輸入高亮的效果如下:

這裡寫圖片描述

至於預覽高亮樣式,操作與上節 Ace的相同,同樣是藉助 highlight.js,並且自定義了一份樣式表,用於預覽高亮的顯示效果,預覽效果如下:

這裡寫圖片描述

搜尋功能

在使用 Github線上編輯器的時候,會發現 Github的編輯器是具備搜尋功能的,就像下面這樣:

這裡寫圖片描述

AceCodeMirror都是支援此功能的,不過 Ace的文件實在是不太友好,也不好除錯,各種問題,所以我沒有深入研究,但是 CodeMirror就很好,我看了下 CodeMirror文件中關於編輯器內搜尋的部分,發現實現起來沒什麼難度,所以就花了點時間弄清楚其原理,然後給實現了一下。

CodeMirror沒有預定義搜尋功能,不過其程式碼包中有搜尋功能的 Addons包,只要將 search.js這個 addon包引入,就可以輕鬆實現搜尋功能了,除了搜尋 addon包,還有其他很多相關功能包,可根據實際需求進行增添:

這裡寫圖片描述

Addons這個東西我覺得很好,這樣一來對於一些可有可無的功能也就不必糾結了,如果不想用那個功能,就不引用相關 addon包就行,減小打包後的程式碼體積,如果想用了就加上,很方便。

想要實現編輯器內搜尋功能,首先你需要將搜尋的功能包引入:

require('codemirror/addon/search/search')

這樣,編輯器就具備搜尋功能了,不過還需要相應的樣式,才能實現視覺上的統一,此功能包基於另外一個功能包 dialog.js,搜尋框就是此功能包實現的,所以需要引入此功能的樣式:

require('codemirror/addon/dialog/dialog.css')

想要調出搜尋框,只需要使用快捷鍵 Ctrl+F(Win)或者 Cmd+F(Mac),然後在搜尋框內輸入要搜尋的字元,按下 Enter就行,和在 Github線上編輯器內搜尋功能的使用時一樣的,並且搜尋結果高亮顯示。

如果你想跳到下一個搜尋結果,只需要 Ctrl-G(Win)或者 Cmd-G(Mac),如果想跳到上一個搜尋結果,只需要 Shift-Ctrl-G(PC)或者Shift-Cmd-G(Mac)

自動閉合標籤

當你在寫 HTML結構的時候,有些編輯器會幫你自動閉合標籤,例如輸入 <div>,當輸入第 5個字元 >的時候,編輯器會自動補全 </div>CodeMirror也有個這樣的功能包:closetag:

require('codemirror/addon/edit/closetag')

當你引入此功能包,在編輯器內輸入 HTML程式碼段的時候,輸入 <div>,當鍵入最後一個字元 >的時候,你就會看到……編輯器沒反應,沒有幫你自動補全。

仔細看了下文件,發現原來還需要進行顯示配置才行:

autoCloseTags: true

配置好此屬性後,就可以自動補全了。

全屏顯示

CodeMirror也有全屏顯示的功能包:fullscreen.js

require('codemirror/addon/display/fullscreen')

使用此功能時,需要引入對應的樣式檔案:

require('codemirror/addon/display/fullscreen.css')

文件上說得很清楚,想調起此功能,只需要將游標定位在編輯器內,然後按下 F11鍵,你就會看到……確實是全屏了,But,你再仔細看看就會發現,你按的這個 F11調起的其實是瀏覽器的快捷鍵而非是編輯器的快捷鍵,因為 js-DOM再厲害,翻江倒海的能力也就在瀏覽器內部,怎麼可能會把瀏覽器包括標籤、選項卡、邊欄在內的 Native部件都給隱藏了?而且這種全屏,只是除去了瀏覽器無關部件,文件內容相應放大,佈局之類的沒有任何變化,並不是 fullscreen.js所要實現的功能。

fullscreen.js所實現的功能是隱藏掉瀏覽器頁面中除了編輯器之外所有的元素,讓編輯器佔滿整個頁面。

想要實現這種效果,你需要自定義快捷鍵,用於調起功能,並且攔截觸發瀏覽器自帶的全屏功能,自定義快捷鍵也是通過配置來實現的,例如如果你想要當按下 F11的時候,調起全屏功能,並且按 Esc的時候退出全屏:

extraKeys: {
  'F11'(cm) {
      // 全屏
      cm.setOption('fullScreen', !cm.getOption('fullScreen'))
    },
    'Esc'(cm) {
      // 退出全屏
      if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false)
    }
}

extraKeys就是用於配置快捷鍵的屬性,除了全屏快捷鍵,你還可以配置其他的快捷鍵,例如 掘金 的線上編輯器就提供了一些輸入 markdown程式碼段的快捷鍵:

這裡寫圖片描述

使用 CodeMirror來實現這種快捷鍵也沒什麼難度,主要是你要熟悉文件,知道呼叫哪些方法來達到目的。

[
  { name: 'Ctrl-H', value: '## ', offset: 0 },
  { name: 'Ctrl-B', value: '**', offset: 1 },
  { name: 'Ctrl-K', value: '[]()', offset: 3 },
  { name: 'Alt-K', value: '``', offset: 1 },
  { name: 'Alt-C', value: '```js\n\n```', offset: 0, offsetLine: 1 },
  { name: 'Alt-I', value: '![alt]()', offset: 1 },
  { name: 'Alt-L', value: '* ', offset: 0 }
]

CodeMirror還有其他的 Addons,並且在其 Github上也都有相應的 Demo,根據實際需求新增即可。

小記

富文字編輯器一共都是前端領域的天坑,本文基於 AceCodeMirror實現的編輯器只是用到了這兩個專案很少的一部分功能,不過也足以滿足大部分的需求了。

另外,說實話,Ace的文件真是不太好看,而且這個編輯器也不太好使用,無法進行精確的自定義控制,別看上面我寫的內容不是太多,但是為了弄明白 Ace的一些情況,從而做出一個 Demo並寫出這篇文章,我最近幾天工作之餘的所有自由時間幾乎都貢獻在上面了,對開發者真的有點不太友好,相對而言,CodeMirror做得就很好,不會有這樣那樣的問題,就算有問題,也容易除錯,最起碼在我看來是這樣,所以,我大概明白為何 Github會選用 CodeMirror而不是 Ace來用於構建其線上編輯器了。

本文可執行的示例程式碼全都放到了 Github上,有興趣的可以看看,順手 Star哦~

這裡寫圖片描述