本文介紹如何使用 Ace和 CodeMirror來實現一個基於
react
的markdown
輸入+
即時預覽線上編輯器
Ace版本
Ace
算是一個久經考驗的老牌編輯器外掛了,現在很多大公司都在用這個東西,似乎 Github曾經就使用 Ace
用於構建它的線上編輯器(雖然現在不用了)。
Ace
在Github上只是存放了其專案,更多詳細的介紹,例如如何開始以及 API
等文件都放在它的官網上
溫馨提示:
- 如果你開啟其 官網發現載入失敗,或者頁面不全,那麼可能需要你翻牆重新請求一遍才行,因為雖然其官網的大部分資源牆內就能訪問,但一些指令碼檔案,例如
jQuery
是牆外的,所以可能出現資料載入失敗的情況。Ace
的文件讀起來可能有些困難,這裡的困難並不是指其文件都是英文的,如果只是英文閱讀障礙,線上翻譯一下也就ok
了,而是說你可能不知道該從哪裡閱讀,不知從何下手,這也是大部分開源專案的通病,這個問題可能就需要你多翻看幾遍,找到文件編寫規律後再閱讀應該就容易多了。
引入 Ace
本文所要實現的編輯器雖然是基於 Ace
,但是沒有直接使用 Ace
,而是使用了其一個封裝外掛 brace,至於為什麼不直接使用 Ace
,brace專案也有說明,可以自己去看看,另外,由於本文所要實現的編輯器還是基於 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文件裡找到的,這裡只簡單說明一下:
mode
:編輯器的整體模式或樣式,這裡取值為markdown
,表明需要用這個編輯器來輸入markdown
文字,這樣編輯器就會進行相應的初始設定。theme
:編輯器主題,這裡使用了github
這個主題。wrapEnabled
:當輸入的一句文字比一行的長度要長時,是否允許換行。tabSize
:使用幾個空格來表示表示一次Tab
按鍵。fontSize
:文字的字型大小height
:編輯器的高度,單位為px
。width
:編輯器的寬度,單位為px
。debounceChangePeriod
:多長時間對輸入響應一次,單位為ms
,類似於節流。onChange
:文字框內容發生變化時的回撥函式。onScroll
:文字框內容發生滾動時的回撥函式。name
:編輯器的id
。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/>
編輯器內輸入的文字高亮,是由編輯器元件的兩個屬性控制的:mode
和 theme
,當你指定了這兩個屬性時,你在編輯器內輸入的文字,無論是 markdown
標記還是程式碼段就都已經自動高亮的了,例如,在編輯器內輸入下述程式碼段,編輯器會自動對其進行高亮處理:
```css
#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版本
CodeMirror 和 Ace 都是開源線上編輯器中的佼佼者,在 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的官網上找到,這裡只稍微說明下。
ref
: 用於方便元件內部對CodeMirror
容器的引用onScroll
: 編輯器內容滾動時觸發的回撥onChange
: 編輯器內容發生變化時觸發的回撥options
: 一些配置引數,例如是否顯示行數、編輯器主題、縮排空格數、是否允許軟換行、是否只讀、文字內容的模式、是否自動閉合標籤、自定義快捷鍵等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
的編輯器是具備搜尋功能的,就像下面這樣:
Ace
和 CodeMirror
都是支援此功能的,不過 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
,根據實際需求新增即可。
小記
富文字編輯器一共都是前端領域的天坑,本文基於 Ace
和 CodeMirror
實現的編輯器只是用到了這兩個專案很少的一部分功能,不過也足以滿足大部分的需求了。
另外,說實話,Ace
的文件真是不太好看,而且這個編輯器也不太好使用,無法進行精確的自定義控制,別看上面我寫的內容不是太多,但是為了弄明白 Ace
的一些情況,從而做出一個 Demo
並寫出這篇文章,我最近幾天工作之餘的所有自由時間幾乎都貢獻在上面了,對開發者真的有點不太友好,相對而言,CodeMirror
做得就很好,不會有這樣那樣的問題,就算有問題,也容易除錯,最起碼在我看來是這樣,所以,我大概明白為何 Github
會選用 CodeMirror
而不是 Ace
來用於構建其線上編輯器了。
本文可執行的示例程式碼全都放到了 Github上,有興趣的可以看看,順手 Star哦~