<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          手把手帶你 10 分鐘手?jǐn)]一個簡易的 MarkDown 編輯器

          共 18650字,需瀏覽 38分鐘

           ·

          2021-06-13 03:10

          點擊上方“技術(shù)漫談”,選擇“設(shè)為星標(biāo)
          第一時間關(guān)注技術(shù)干貨!


          前言

          最近我在項目中需要實現(xiàn)一個 「markdown編輯器」 的需求,并且是以React框架為開發(fā)基礎(chǔ)的,類似掘金這樣的:

          61eda9092c17cc2c74be8612e8ba340b.webpimg

          我的第一想法肯定是能用優(yōu)秀的開源就一定用開源的,畢竟不能老是重復(fù)造輪子。于是我在我的前端群里問了很多群友,他們都給了甩過來一堆開源的markdown編輯器項目,但我一看全是基于Vue使用的,不符合我的預(yù)期,逛了一下github,也沒看到我滿意的項目,所以就想自己實現(xiàn)一個啦

          需要實現(xiàn)的功能

          我們自己實現(xiàn)的話,看看需要支持哪些功能,因為做一個初版的簡易編輯器,所以功能實現(xiàn)得不會太多,但絕對夠用:

          • markdown語法解析,并實時渲染
          • markdown主題css樣式
          • 代碼塊高亮展示
          • 「編輯區(qū)」和「展示區(qū)」的頁面同步滾動
          • 編輯器工具欄中工具的實現(xiàn)

          這里先放上我最終實現(xiàn)好了的效果圖:

          0e52392c2c306e6f7fd4fe66dbd0ab18.webp最終效果圖

          我也將本文的代碼放在了 Github 倉庫 (opens new window)[1]上了,歡迎各位點個 ?? 「star」 支持一下

          同時,我也給大家提供了一個在線體驗的地址 (opens new window)[2],因為做的比較倉促,歡迎大家給我提意見和pr

          具體實現(xiàn)

          具體的實現(xiàn)也是按照我們上述列出來的功能的順序來一一實現(xiàn)的

          說明:本文通過循序漸進(jìn)的方式講解,所以重復(fù)代碼可能有點多。并且每一部分的注釋是專門用于講解該部分的代碼的,所以在看每一部分功能代碼時,只需要看注釋部分就好~

          一、布局

          import?React,?{??}?from?'react'


          export?default?function?MarkdownEdit()?{


          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?className="edit"?/>
          ????????????<div?className="show"?/>
          ????????</div>

          ????)
          }

          css樣式我就不一一列舉了,整體就是左邊是「編輯區(qū)」,右邊是「展示區(qū)」,具體樣式如下:

          51ddb5355cdd3b2476f36cfc501ed43d.webp布局圖

          二、markdown語法解析

          接下來就需要思考如何將 「「編輯區(qū)」」 輸入的markdown語法解析成html標(biāo)簽并最終渲染在 「「展示區(qū)」」

          查找了一下目前比較優(yōu)秀的markdown解析的開源庫,常用的有三個,分別是Marked、Showdown、markdown-it ,并借鑒了一下其它大佬的想法,了解了一下這三個庫的優(yōu)缺點,對比如下:

          庫名優(yōu)點缺點
          Marked性能好,正則解析(中文支持比較好)擴(kuò)展性較差
          Showdown擴(kuò)展性好、正則解析(中文支持好)性能較差
          markdown-it擴(kuò)展性好、性能較好逐字符解析(中文支持不好)

          剛開始我選擇了showdown這個庫,因為這個庫使用起來特別方便,而且官方已經(jīng)在庫中提供了很多擴(kuò)展功能,只需要配置一些字段即可。但是后來我又分析了一波,還是選用了markdown-it,因為之后可能需要做更多的語法擴(kuò)展,showdown的官方文檔寫的比較生硬,而且markdown-it使用的人也多,生態(tài)比較好,雖然其官方?jīng)]有支持很多擴(kuò)展的語法,但是已經(jīng)有很多基于makrdown-it的功能擴(kuò)展插件了,最重要的是markdown-it的官方文檔寫得好啊(而且有中文文檔)!

          接下來寫一下markdown語法解析的代碼吧(其中步驟1、2、3表示的是markdown-it庫的用法)

          import?React,?{?useState?}?from?'react'
          //?1.?引入markdown-it庫
          import?markdownIt?from?'markdown-it'

          //?2.?生成實例對象
          const?md?=?new?markdownIt()

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')??//?存儲解析后的html字符串

          ????//?3.?解析markdown語法
          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????onChange={(e)?=>
          ?parse(e.target.value)}?//?編輯區(qū)內(nèi)容每次修改就更新變量htmlString的值
          ????????????/>
          ????????????<div?
          ????????????????className="show"?
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}?//?將html字符串解析成真正的html標(biāo)簽
          ????????????/>

          ????????</div>

          ????)
          }

          對于將 「html字符串」 轉(zhuǎn)化為 「真正的html標(biāo)簽」 的操作,我們借助了React提供的dangerouslySetInnerHTML屬性,詳細(xì)的使用可以看React 官方文檔(opens new window)[3]

          此時一個簡單的markdown語法解析功能就實現(xiàn)了,來看看效果

          ec436edb9bbdad782dd06ebca9be1363.webpmarkdown語法解析效果展示圖

          兩邊確實正在同步更新,但是.....看起來好像哪里不太對!其實是沒問題的,被解析好的 html字符串 每個標(biāo)簽都被附帶上了特定的類名,只是現(xiàn)在我們引入任何的樣式文件,例如下圖

          3de61c1767d3f0e682dc112302ac356f.webpimg

          我們可以打印解析出來的html字符串看看是什么樣的

          <h1?id="">大標(biāo)題</h1>
          <blockquote>
          ??<p>本文來自公眾號:前端印象</p>
          </blockquote>
          <pre><code?class="js?language-js">let?name?=?'零一'
          </code></pre>

          三、markdown主題樣式

          接下來我們可以去網(wǎng)上找一些markdown的主題樣式css文件,例如我用一個最簡單Github主題的markdown樣式。另外我還是很推薦Typora Theme (opens new window)[4],上面有很多很多的markdown主題

          因為我這個樣式主題是有一個前綴id write(Typora上的大部分主題前綴也是#write),所以我們給展示區(qū)的標(biāo)簽加上該類id,并引入樣式文件

          import?React,?{?useState?}?from?'react'
          import?'./theme/github-theme.css'??//?引入github的markdown主題樣式
          import?markdownIt?from?'markdown-it'

          const?md?=?new?markdownIt()

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')

          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????onChange={(e)?=>
          ?parse(e.target.value)}?
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"??//?新增writeID名?
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>

          ????????</div>

          ????)
          }

          再來看看加入樣式后的渲染結(jié)果圖

          4c4966f782bc6d25592ef4f8b5819dab.webp帶樣式的markdown渲染效果圖

          四、代碼塊高亮

          markdown語法的解析已經(jīng)完成了,并且也有對應(yīng)的樣式了,但是代碼塊好像還沒有高亮樣式

          這塊兒我們自己來從0到1的實現(xiàn)是不可能的,可以用現(xiàn)成的開源庫 highlight.js,highlight.js 官方文檔 (opens new window)[5],這個庫能幫你做的就是檢測「代碼塊標(biāo)簽元素」,并為其加上特定的類名。這里放上這個庫的API文檔(opens new window)[6]

          highlight.js 默認(rèn)是檢測它所支持的所有語言的語法的,我們就不需要關(guān)心了,并且其提供了很多的代碼高亮主題,我們可以在官網(wǎng)進(jìn)行預(yù)覽,如下圖所示:

          8eab6a8287682067c773d54de04ed50d.webpimg

          更大的好消息來了!markdown-it已經(jīng)將highlight.js集成進(jìn)去了,直接設(shè)定一些配置即可,并且我們需要先將該庫下載下來。具體的可以看markdown-it中文官網(wǎng) - 高亮語法配置(opens new window)[7]

          同時在目錄highlight.js/styles/下有很多很多的主題,可以自行導(dǎo)入

          接下來就來實現(xiàn)一下代碼高亮的功能吧

          import?React,?{?useState,?useEffect?}?from?'react'
          import?markdownIt?from?'markdown-it'
          import?'./theme/github-theme.css'
          import?hljs?from?'highlight.js'??//?引入highlight.js庫
          import?'highlight.js/styles/github.css'??//?引入github風(fēng)格的代碼高亮樣式

          const?md?=?new?markdownIt({
          ????//?設(shè)置代碼高亮的配置
          ????highlight:?function?(code,?language)?{??????
          ????????if?(language?&&?hljs.getLanguage(language))?{
          ??????????try?{
          ????????????return?`<pre><code?class="hljs?language-${language}">`?+
          ???????????????????hljs.highlight(code,?{?language??}).value?+
          ???????????????????'</code></pre>';
          ??????????}?catch?(__)?{}
          ????????}
          ????
          ????????return?'<pre?class="hljs"><code>'?+?md.utils.escapeHtml(code)?+?'</code></pre>';
          ????}
          })

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')

          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????onChange={(e)?=>
          ?parse(e.target.value)}?
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>

          ????????</div>

          ????)
          }

          來看一下代碼高亮的效果圖:

          9597b71da13769da0d28250375faecfb.webp代碼高亮效果圖

          五、同步滾動

          markdown編輯器還有一個重要的功能就是在我們滾動一個區(qū)域的內(nèi)容時,另一塊區(qū)域也跟著同步的滾動,這樣才方便查看

          接下來我們來實現(xiàn)一下,我會將我實現(xiàn)時踩的坑也一并列出來,讓大家也印象深刻點,免得以后也犯同樣的錯誤

          剛開始主要實現(xiàn)思路就是當(dāng)滾動其中一塊區(qū)域時,計算滾動比例(scrollTop / scrollHeight),然后使另一塊區(qū)域當(dāng)前的滾動距離占總滾動高度的比例等于該滾動比例

          import?React,?{?useState,?useRef,?useEffect?}?from?'react'
          import?markdownIt?from?'markdown-it'
          import?'./theme/github-theme.css'??
          import?hljs?from?'highlight.js'
          import?'highlight.js/styles/github.css'?

          const?md?=?new?markdownIt({
          ????highlight:?function?(code,?language)?{??????
          ????????if?(language?&&?hljs.getLanguage(language))?{
          ??????????try?{
          ????????????return?`<pre><code?class="hljs?language-${language}">`?+
          ???????????????????hljs.highlight(code,?{?language??}).value?+
          ???????????????????'</code></pre>';
          ??????????}?catch?(__)?{}
          ????????}
          ????
          ????????return?'<pre?class="hljs"><code>'?+?md.utils.escapeHtml(code)?+?'</code></pre>';
          ????}
          })

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')
          ????const?edit?=?useRef(null)??//?編輯區(qū)元素
          ????const?show?=?useRef(null)??//?展示區(qū)元素

          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????//?處理區(qū)域的滾動事件
          ????const?handleScroll?=?(block:?number,?event)?=>?{
          ????????let?{?scrollHeight,?scrollTop?}?=?event.target
          ????????let?scale?=?scrollTop?/?scrollHeight??//?滾動比例

          ????????//?當(dāng)前滾動的是編輯區(qū)
          ????????if(block?===?1)?{
          ????????????//?改變展示區(qū)的滾動距離
          ????????????let?{?scrollHeight?}?=?show.current
          ????????????show.current.scrollTop?=?scrollHeight?*?scale
          ????????}?else?if(block?===?2)?{??//?當(dāng)前滾動的是展示區(qū)
          ????????????//?改變編輯區(qū)的滾動距離
          ????????????let?{?scrollHeight?}?=?edit.current
          ????????????edit.current.scrollTop?=?scrollHeight?*?scale
          ????????}
          ????}

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????ref={edit}
          ????????????????onScroll={(e)?=>
          ?handleScroll(1,?e)}
          ????????????????onChange={(e)?=>?parse(e.target.value)}?
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"
          ????????????????ref={show}
          ????????????????onScroll={(e)?=>
          ?handleScroll(2,?e)}
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>
          ????????</div>

          ????)
          }

          這是我做的時候的第一版,確實是實現(xiàn)了兩塊區(qū)域的同步滾動,但是存在兩個bug,來看看是哪兩個

          「bug1:」

          這是一個很致命的bug,先埋個伏筆,先來看效果:

          277e8a075851893b6dd4f63161a74523.webp初版同步滾動效果圖

          同步滾動的效果實現(xiàn)了,但能很明顯得看到,當(dāng)我手動滾動完以后停止了任何操作,但是兩個區(qū)域仍然在不停的滾動,這是為什么呢?

          排查了一下代碼,發(fā)現(xiàn) handleScroll 這個方法會無限觸發(fā),假設(shè)當(dāng)我們手動滾動一次編輯區(qū)后會觸發(fā)其 scroll方法,即會調(diào)用 handleScroll 方法,然后會去改變「展示區(qū)」的滾動距離,此時又會觸發(fā)展示區(qū)的 scroll方法,即調(diào)用 handleScroll 方法,然后會去改變「編輯區(qū)」的滾動距離 .... 就這樣一直循環(huán)往復(fù),才會出現(xiàn)圖中的bug

          后來我想了個比較簡單的解決辦法,就是用一個變量記住你當(dāng)前手動觸發(fā)的是哪個區(qū)域的滾動,這樣就可以在 handleScroll 方法里區(qū)分此次滾動是被動觸發(fā)的還是主動觸發(fā)的了

          import?React,?{?useState,?useRef,?useEffect?}?from?'react'
          import?markdownIt?from?'markdown-it'
          import?'./theme/github-theme.css'??
          import?hljs?from?'highlight.js'
          import?'highlight.js/styles/github.css'

          const?md?=?new?markdownIt({
          ????highlight:?function?(code,?language)?{??????
          ????????if?(language?&&?hljs.getLanguage(language))?{
          ??????????try?{
          ????????????return?`<pre><code?class="hljs?language-${language}">`?+
          ???????????????????hljs.highlight(code,?{?language??}).value?+
          ???????????????????'</code></pre>';
          ??????????}?catch?(__)?{}
          ????????}
          ????
          ????????return?'<pre?class="hljs"><code>'?+?md.utils.escapeHtml(code)?+?'</code></pre>';
          ????}
          })

          let?scrolling:?0?|?1?|?2?=?0??//?0:?none;?1:?編輯區(qū)主動觸發(fā)滾動;?2:?展示區(qū)主動觸發(fā)滾動
          let?scrollTimer;??//?結(jié)束滾動的定時器

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')
          ????const?edit?=?useRef(null)?
          ????const?show?=?useRef(null)??

          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????const?handleScroll?=?(block:?number,?event)?=>?{
          ????????let?{?scrollHeight,?scrollTop?}?=?event.target
          ????????let?scale?=?scrollTop?/?scrollHeight??

          ????????if(block?===?1)?{
          ????????????if(scrolling?===?0)?scrolling?=?1;??//?記錄主動觸發(fā)滾動的區(qū)域
          ????????????if(scrolling?===?2)?return;????//?當(dāng)前是「展示區(qū)」主動觸發(fā)的滾動,因此不需要再驅(qū)動展示區(qū)去滾動

          ????????????driveScroll(scale,?showRef.current)??//?驅(qū)動「展示區(qū)」的滾動
          ????????}?else?if(block?===?2)?{??
          ????????????if(scrolling?===?0)?scrolling?=?2;
          ????????????if(scrolling?===?1)?return;????//?當(dāng)前是「編輯區(qū)」主動觸發(fā)的滾動,因此不需要再驅(qū)動編輯區(qū)去滾動

          ????????????driveScroll(scale,?editRef.current)
          ????????}
          ????}

          ????//?驅(qū)動一個元素進(jìn)行滾動
          ????const?driveScroll?=?(scale:?number,?el:?HTMLElement)?=>?{
          ????????let?{?scrollHeight?}?=?el
          ????????el.scrollTop?=?scrollHeight?*?scale

          ????????if(scrollTimer)?clearTimeout(scrollTimer);
          ????????scrollTimer?=?setTimeout(()?=>?{
          ????????????scrolling?=?0????//?在滾動結(jié)束后,將scrolling設(shè)為0,表示滾動結(jié)束
          ????????????clearTimeout(scrollTimer)
          ????????},?200)
          ????}

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????ref={edit}
          ????????????????onScroll={(e)?=>
          ?handleScroll(1,?e)}
          ????????????????onChange={(e)?=>?parse(e.target.value)}?
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"
          ????????????????ref={show}
          ????????????????onScroll={(e)?=>
          ?handleScroll(2,?e)}
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>
          ????????</div>

          ????)
          }

          這樣就解決了上述的bug了,同步滾動也算很不錯得實現(xiàn)了,現(xiàn)在的效果就跟文章開頭展示的圖片里效果一樣了

          「bug2:」

          這里還存在一個很小的問題,也不算是bug,應(yīng)該算是設(shè)計上的思路問題,那就是兩個區(qū)域其實還沒完完全全實現(xiàn)同步滾動。先來看看原先的設(shè)計思想

          112368a4b69f308a75c9238aa219be4d.webpimg

          編輯區(qū)和展示區(qū)的可視高度是一樣的,但一般編輯區(qū)的內(nèi)容經(jīng)過markdown渲染后,總的滾動高度是會高于編輯區(qū)總的滾動高度的,所以我們無法僅憑scrollTopscrollHeight使得兩個區(qū)域同步滾動,比較晦澀,用具體的數(shù)據(jù)來看一下

          屬性編輯區(qū)展示區(qū)
          clientHeight300300
          scrollHeight500600

          假設(shè)我們現(xiàn)在滾動編輯區(qū)到最底部,那么此時「編輯區(qū)」的 scrollTop 應(yīng)為 scrollHeight - clientHeight = 500 - 300 = 200,按照我們原本計算滾動比例的方式得出 scale = scrollTop / scrollHeight = 200 / 500 = 0.4,那么「展示區(qū)」同步滾動后,scrollTop = scale * scrollHeight = 0.4 * 600 = 240 < 600 - 300 = 300。但事實就是編輯區(qū)滾動到最底部了,而展示區(qū)還沒有,顯然不是我們要的效果

          換一種思路,我們在計算滾動比例時,應(yīng)計算的是當(dāng)前的 scrollTopscrollTop最大值的比例,這樣就能實現(xiàn)同步滾動了,仍然用剛才那個例子來看:此時編輯區(qū)滾動到最底部,那么scale應(yīng)為 scrollTop / (scrollHeight - clientHeight) = 200 / (500 - 300) = 100%,表示編輯區(qū)滾動到最底部了,那么在展示區(qū)同步滾動時,他的 scrollTop 就變成了 scale * (scrollHeight - clientHeight) = 100% * (600 - 300) = 300,此時的展示區(qū)也同步滾動到了最底部,這樣就實現(xiàn)了真正的同步滾動了

          來看一下改進(jìn)后的代碼

          import?React,?{?useState,?useRef,?useEffect?}?from?'react'
          import?markdownIt?from?'markdown-it'
          import?'./theme/github-theme.css'??
          import?hljs?from?'highlight.js'
          import?'highlight.js/styles/github.css'

          const?md?=?new?markdownIt({
          ????highlight:?function?(code,?language)?{??????
          ????????if?(language?&&?hljs.getLanguage(language))?{
          ??????????try?{
          ????????????return?`<pre><code?class="hljs?language-${language}">`?+
          ???????????????????hljs.highlight(code,?{?language??}).value?+
          ???????????????????'</code></pre>';
          ??????????}?catch?(__)?{}
          ????????}
          ????
          ????????return?'<pre?class="hljs"><code>'?+?md.utils.escapeHtml(code)?+?'</code></pre>';
          ????}
          })

          let?scrolling:?0?|?1?|?2?=?0??
          let?scrollTimer;??

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')
          ????const?edit?=?useRef(null)?
          ????const?show?=?useRef(null)??

          ????const?parse?=?(text:?string)?=>?setHtmlString(md.render(text));

          ????const?handleScroll?=?(block:?number,?event)?=>?{
          ????????let?{?scrollHeight,?scrollTop,?clientHeight?}?=?event.target
          ????????let?scale?=?scrollTop?/?(scrollHeight?-?clientHeight)??//?改進(jìn)后的計算滾動比例的方法

          ????????if(block?===?1)?{
          ????????????if(scrolling?===?0)?scrolling?=?1;??
          ????????????if(scrolling?===?2)?return;????

          ????????????driveScroll(scale,?showRef.current)??
          ????????}?else?if(block?===?2)?{??
          ????????????if(scrolling?===?0)?scrolling?=?2;
          ????????????if(scrolling?===?1)?return;????

          ????????????driveScroll(scale,?editRef.current)
          ????????}
          ????}

          ????//?驅(qū)動一個元素進(jìn)行滾動
          ????const?driveScroll?=?(scale:?number,?el:?HTMLElement)?=>?{
          ????????let?{?scrollHeight,?clientHeight?}?=?el
          ????????el.scrollTop?=?(scrollHeight?-?clientHeight)?*?scale??//?scrollTop的同比例滾動

          ????????if(scrollTimer)?clearTimeout(scrollTimer);
          ????????scrollTimer?=?setTimeout(()?=>?{
          ????????????scrolling?=?0???
          ????????????clearTimeout(scrollTimer)
          ????????},?200)
          ????}

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????ref={edit}
          ????????????????onScroll={(e)?=>
          ?handleScroll(1,?e)}
          ????????????????onChange={(e)?=>?parse(e.target.value)}?
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"
          ????????????????ref={show}
          ????????????????onScroll={(e)?=>
          ?handleScroll(2,?e)}
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>
          ????????</div>

          ????)
          }

          兩個bug都已經(jīng)解決了,同步滾動的功能也算完美實現(xiàn)啦。但對于同步滾動這個功能,其實有兩種概念,一種是兩個區(qū)域在滾動高度上保持同步滾動;另一種就是右側(cè)的展示區(qū)域?qū)?yīng)左側(cè)的編輯區(qū)的內(nèi)容進(jìn)行滾動。我們現(xiàn)在實現(xiàn)的是前者,后者可以后續(xù)作為新功能實現(xiàn)一下~

          六、工具欄

          最后我們就再實現(xiàn)一下編輯器的工具欄部分的工具(加粗、斜體、有序列表等等),因為這幾個工具的實現(xiàn)思路都一致,我們就拿 「「加粗」」 這個工具舉例子,其余的就可以模仿著寫出來了

          加粗工具的實現(xiàn)思路:

          • 光標(biāo)是否選中文字?
            • 是。將選中文字的兩側(cè)加上**
            • 否。在光標(biāo)所在處添加文字**加粗文字**

          動圖效果演示:

          d670844e5461755b2ecd721e6d33881d.webp加粗工具動圖演示
          import?React,?{?useState,?useRef,?useEffect?}?from?'react'
          import?markdownIt?from?'markdown-it'
          import?'./theme/github-theme.css'??
          import?hljs?from?'highlight.js'
          import?'highlight.js/styles/github.css'

          const?md?=?new?markdownIt({
          ????highlight:?function?(code,?language)?{??????
          ????????if?(language?&&?hljs.getLanguage(language))?{
          ??????????try?{
          ????????????return?`<pre><code?class="hljs?language-${language}">`?+
          ???????????????????hljs.highlight(code,?{?language??}).value?+
          ???????????????????'</code></pre>';
          ??????????}?catch?(__)?{}
          ????????}
          ????
          ????????return?'<pre?class="hljs"><code>'?+?md.utils.escapeHtml(code)?+?'</code></pre>';
          ????}
          })

          let?scrolling:?0?|?1?|?2?=?0??
          let?scrollTimer;??

          export?default?function?MarkdownEdit()?{
          ????const?[htmlString,?setHtmlString]?=?useState('')
          ????const?[value,?setValue]?=?useState('')???//?編輯區(qū)的文字內(nèi)容
          ????const?edit?=?useRef(null)?
          ????const?show?=?useRef(null)??

          ????const?handleScroll?=?(block:?number,?event)?=>?{
          ????????let?{?scrollHeight,?scrollTop,?clientHeight?}?=?event.target
          ????????let?scale?=?scrollTop?/?(scrollHeight?-?clientHeight)??

          ????????if(block?===?1)?{
          ????????????if(scrolling?===?0)?scrolling?=?1;??
          ????????????if(scrolling?===?2)?return;????

          ????????????driveScroll(scale,?showRef.current)??
          ????????}?else?if(block?===?2)?{??
          ????????????if(scrolling?===?0)?scrolling?=?2;
          ????????????if(scrolling?===?1)?return;????

          ????????????driveScroll(scale,?editRef.current)
          ????????}
          ????}

          ????//?驅(qū)動一個元素進(jìn)行滾動
          ????const?driveScroll?=?(scale:?number,?el:?HTMLElement)?=>?{
          ????????let?{?scrollHeight,?clientHeight?}?=?el
          ????????el.scrollTop?=?(scrollHeight?-?clientHeight)?*?scale??

          ????????if(scrollTimer)?clearTimeout(scrollTimer);
          ????????scrollTimer?=?setTimeout(()?=>?{
          ????????????scrolling?=?0???
          ????????????clearTimeout(scrollTimer)
          ????????},?200)
          ????}

          ????//?加粗工具
          ????const?addBlod?=?()?=>?{
          ????????//?獲取編輯區(qū)光標(biāo)的位置。未選中文字時:selectionStart === selectionEnd ;選中文字時:selectionStart < selectionEnd
          ????????let?{?selectionStart,?selectionEnd?}?=?edit.current
          ????????let?newValue?=?selectionStart?===?selectionEnd
          ??????????????????????????value.slice(0,?start)?+?'**加粗文字**'?+?value.slice(end)
          ????????????????????????:?value.slice(0,?start)?+?'**'?+?value.slice(start,?end)?+?'**'?+?value.slice(end)
          ????????setValue(newValue)
          ????}

          ????useEffect(()?=>?{
          ????????//?編輯區(qū)內(nèi)容改變,更新value的值,并同步渲染
          ????????setHtmlString(md.render(value))
          ????},?[value])

          ????return?(
          ????????<div?className="markdownEditConainer">
          ????????????<button?onClick={addBlod}>加粗</button>???{/*?假設(shè)一個加粗的按鈕?*/}
          ????????????<textarea?
          ????????????????className="edit"?
          ????????????????ref={edit}
          ????????????????onScroll={(e)?=>
          ?handleScroll(1,?e)}
          ????????????????onChange={(e)?=>?setValue(e.target.value)}???//?直接修改value的值,useEffect會同步渲染展示區(qū)的內(nèi)容
          ????????????????value={value}
          ????????????/>
          ????????????<div?
          ????????????????className="show"
          ????????????????id="write"
          ????????????????ref={show}
          ????????????????onScroll={(e)?=>
          ?handleScroll(2,?e)}
          ????????????????dangerouslySetInnerHTML={{?__html:?htmlString?}}
          ????????????/>
          ????????</div>

          ????)
          }

          借助這樣的思路,就可以完成其它各種工具的實現(xiàn)了。

          在我已經(jīng)發(fā)布的markdown-editor-reactjs (opens new window)[8]中,已經(jīng)完成了其它工具的實現(xiàn),想要看代碼的可以去源碼里看

          七、補(bǔ)充

          為了保證包的體積足夠小,我將「第三方依賴庫」、「markdown主題」「代碼高亮主題」都通過外鏈的形式導(dǎo)入了

          八、最后

          一個簡易版的markdown編輯器就實現(xiàn)了,大家可以手動嘗試實現(xiàn)一下。后續(xù)我也會繼續(xù)發(fā)一些教程,對這個編輯器的功能進(jìn)行擴(kuò)展

          我將代碼都上傳到了 Github倉庫 (opens new window)[9](希望大家點個?? 「star」),后續(xù)擴(kuò)展一下功能,并作為一個完整的組件發(fā)布到npm給大家使用,希望大家多多支持~(其實我已經(jīng)悄悄發(fā)布,但因功能還不是太完善,就不先拿出來給大家使用了,這里簡單放個npm包的地址 (opens new window)[10])

          參考資料

          [1]

          Github 倉庫 (opens new window): https://github.com/zero2one3/markdown-editor-reactjs

          [2]

          在線體驗的地址 (opens new window): http://lpyexplore.gitee.io/taobao_staticweb/markdown-editor-reactjs/

          [3]

          React 官方文檔(opens new window): https://zh-hans.reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml

          [4]

          Typora Theme (opens new window): https://theme.typora.io/

          [5]

          highlight.js 官方文檔 (opens new window): https://highlightjs.org/

          [6]

          API文檔(opens new window): https://highlightjs.readthedocs.io/en/latest/api.html#highlightauto-code-languagesubset

          [7]

          markdown-it中文官網(wǎng) - 高亮語法配置(opens new window): https://markdown-it.docschina.org/#用法示例

          [8]

          markdown-editor-reactjs (opens new window): https://github.com/zero2one3/markdown-editor-reactjs

          [9]

          Github倉庫 (opens new window): https://github.com/zero2one3/markdown-editor-reactjs

          [10]

          npm包的地址 (opens new window): https://www.npmjs.com/package/markdown-editor-reactjs


          - End -

          創(chuàng)作不易,請各位給加個星標(biāo)點贊、在看?支持哦!

          瀏覽 54
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  国产无码一区二区 | 国产在线激情视频 | 国产六区色婷婷 | 超碰青青草在线 | 狼友视频入口 |