1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        手把手帶你 10 分鐘手擼一個簡易的 MarkDown 編輯器

        共 18650字,需瀏覽 38分鐘

         ·

        2021-06-13 03:10

        點擊上方“技術(shù)漫談”,選擇“設(shè)為星標
        第一時間關(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)的

        說明:本文通過循序漸進的方式講解,所以重復(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標簽并最終渲染在 「「展示區(qū)」」

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

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

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

        接下來寫一下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標簽
        ????????????/>

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

        ????)
        }

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

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

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

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

        3de61c1767d3f0e682dc112302ac356f.webpimg

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

        <h1?id="">大標題</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ū)的標簽加上該類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.jshighlight.js 官方文檔 (opens new window)[5],這個庫能幫你做的就是檢測「代碼塊標簽元素」,并為其加上特定的類名。這里放上這個庫的API文檔(opens new window)[6]

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

        8eab6a8287682067c773d54de04ed50d.webpimg

        更大的好消息來了!markdown-it已經(jīng)將highlight.js集成進去了,直接設(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風格的代碼高亮樣式

        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)思路就是當滾動其中一塊區(qū)域時,計算滾動比例(scrollTop / scrollHeight),然后使另一塊區(qū)域當前的滾動距離占總滾動高度的比例等于該滾動比例

        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??//?滾動比例

        ????????//?當前滾動的是編輯區(qū)
        ????????if(block?===?1)?{
        ????????????//?改變展示區(qū)的滾動距離
        ????????????let?{?scrollHeight?}?=?show.current
        ????????????show.current.scrollTop?=?scrollHeight?*?scale
        ????????}?else?if(block?===?2)?{??//?當前滾動的是展示區(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)了,但能很明顯得看到,當我手動滾動完以后停止了任何操作,但是兩個區(qū)域仍然在不停的滾動,這是為什么呢?

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

        后來我想了個比較簡單的解決辦法,就是用一個變量記住你當前手動觸發(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;????//?當前是「展示區(qū)」主動觸發(fā)的滾動,因此不需要再驅(qū)動展示區(qū)去滾動

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

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

        ????//?驅(qū)動一個元素進行滾動
        ????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)計算的是當前的 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)了真正的同步滾動了

        來看一下改進后的代碼

        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)??//?改進后的計算滾動比例的方法

        ????????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ū)動一個元素進行滾動
        ????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)容進行滾動。我們現(xiàn)在實現(xiàn)的是前者,后者可以后續(xù)作為新功能實現(xiàn)一下~

        六、工具欄

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

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

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

        動圖效果演示:

        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ū)動一個元素進行滾動
        ????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ū)光標的位置。未選中文字時: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),想要看代碼的可以去源碼里看

        七、補充

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

        八、最后

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

        我將代碼都上傳到了 Github倉庫 (opens new window)[9](希望大家點個?? 「star」),后續(xù)擴展一下功能,并作為一個完整的組件發(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)作不易,請各位給加個星標點贊、在看?支持哦!

        瀏覽 54
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

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

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            色接久久| 俺也来最新色视频 | 黄色精品在线观看 | 午夜伦情电午夜伦情电影8090 | 国精产品综合视频 | 国产精品久久久久久久女厕留拍 | 国产精品高清在线观看 | 无尽女同3d扶她g乳动漫 无遮挡美女 | 别揉我胸啊嗯杨幂 | 欧美老妇性生话猛交大交在线视频 |