React框架快速實(shí)現(xiàn)簡(jiǎn)易的Markdown編輯器
前言
最近我在項(xiàng)目中需要實(shí)現(xiàn)一個(gè) markdown編輯器 的需求,并且是以React
框架為開(kāi)發(fā)基礎(chǔ)的,類(lèi)似掘金這樣的:
我的第一想法肯定是能用優(yōu)秀的開(kāi)源就一定用開(kāi)源的,畢竟不能老是重復(fù)造輪子。于是我在我的前端群里問(wèn)了很多群友,他們都給了甩過(guò)來(lái)一堆開(kāi)源的markdown編輯器項(xiàng)目,但我一看全是基于Vue
使用的,不符合我的預(yù)期,逛了一下github
,也沒(méi)看到我滿(mǎn)意的項(xiàng)目,所以就想自己實(shí)現(xiàn)一個(gè)啦
需要實(shí)現(xiàn)的功能
我們自己實(shí)現(xiàn)的話(huà),看看需要支持哪些功能,因?yàn)樽鲆粋€(gè)初版的簡(jiǎn)易編輯器,所以功能實(shí)現(xiàn)得不會(huì)太多,但絕對(duì)夠用:
- markdown語(yǔ)法解析,并實(shí)時(shí)渲染
- markdown主題css樣式
- 代碼塊高亮展示
- 「編輯區(qū)」和「展示區(qū)」的頁(yè)面同步滾動(dòng)
- 編輯器工具欄中工具的實(shí)現(xiàn)
這里先放上我最終實(shí)現(xiàn)好了的效果圖:
同時(shí),我也給大家提供了一個(gè)在線(xiàn)體驗(yàn)的地址 (opens new window),因?yàn)樽龅谋容^倉(cāng)促,歡迎大家給我提意見(jiàn)和pr
具體實(shí)現(xiàn)
具體的實(shí)現(xiàn)也是按照我們上述列出來(lái)的功能的順序來(lái)一一實(shí)現(xiàn)的
說(shuō)明:本文通過(guò)循序漸進(jìn)的方式講解,所以重復(fù)代碼可能有點(diǎn)多。并且每一部分的注釋是專(zhuān)門(mén)用于講解該部分的代碼的,所以在看每一部分功能代碼時(shí),只需要看注釋部分就好~
一、布局
import React, { } from 'react' export default function MarkdownEdit() { return ( <div className="markdownEditConainer"> <textarea className="edit" /> <div className="show" /> </div> ) }
css樣式我就不一一列舉了,整體就是左邊是編輯區(qū),右邊是展示區(qū),具體樣式如下:
二、markdown語(yǔ)法解析
接下來(lái)就需要思考如何將 「編輯區(qū)」 輸入的markdown
語(yǔ)法解析成html
標(biāo)簽并最終渲染在 「展示區(qū)」
查找了一下目前比較優(yōu)秀的markdown
解析的開(kāi)源庫(kù),常用的有三個(gè),分別是Marked
、Showdown
、markdown-it
,并借鑒了一下其它大佬的想法,了解了一下這三個(gè)庫(kù)的優(yōu)缺點(diǎn),對(duì)比如下:
庫(kù)名 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
Marked | 性能好,正則解析(中文支持比較好) | 擴(kuò)展性較差 |
Showdown | 擴(kuò)展性好、正則解析(中文支持好) | 性能較差 |
markdown-it | 擴(kuò)展性好、性能較好 | 逐字符解析(中文支持不好) |
剛開(kāi)始我選擇了showdown
這個(gè)庫(kù),因?yàn)檫@個(gè)庫(kù)使用起來(lái)特別方便,而且官方已經(jīng)在庫(kù)中提供了很多擴(kuò)展功能,只需要配置一些字段即可。
但是后來(lái)我又分析了一波,還是選用了markdown-it
,因?yàn)橹罂赡苄枰龈嗟恼Z(yǔ)法擴(kuò)展,showdown
的官方文檔寫(xiě)的比較生硬,而且markdown-it
使用的人也多,生態(tài)比較好,雖然其官方?jīng)]有支持很多擴(kuò)展的語(yǔ)法,但是已經(jīng)有很多基于makrdown-it
的功能擴(kuò)展插件了,最重要的是markdown-it
的官方文檔寫(xiě)得好?。ǘ矣兄形奈臋n)!
接下來(lái)寫(xiě)一下markdown
語(yǔ)法解析的代碼吧(其中步驟1、2、3表示的是markdown-it庫(kù)的用法)
import React, { useState } from 'react' // 1. 引入markdown-it庫(kù) import markdownIt from 'markdown-it' // 2. 生成實(shí)例對(duì)象 const md = new markdownIt() export default function MarkdownEdit() { const [htmlString, setHtmlString] = useState('') // 存儲(chǔ)解析后的html字符串 // 3. 解析markdown語(yǔ)法 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> ) }
對(duì)于將 html字符串 轉(zhuǎn)化為 真正的html標(biāo)簽 的操作,我們借助了React提供的dangerouslySetInnerHTML
屬性,詳細(xì)的使用可以看React 官方文檔(opens new window)
此時(shí)一個(gè)簡(jiǎn)單的markdown
語(yǔ)法解析功能就實(shí)現(xiàn)了,來(lái)看看效果
兩邊確實(shí)正在同步更新,但是…看起來(lái)好像哪里不太對(duì)!其實(shí)是沒(méi)問(wèn)題的,被解析好的 html字符串 每個(gè)標(biāo)簽都被附帶上了特定的類(lèi)名,只是現(xiàn)在我們引入任何的樣式文件,例如下圖
我們可以打印解析出來(lái)的html字符串看看是什么樣的
<h1 id="">大標(biāo)題</h1> <blockquote> <p>本文來(lái)自公眾號(hào):前端印象</p> </blockquote> <pre><code class="js language-js">let name = '零一' </code></pre>
三、markdown主題樣式
接下來(lái)我們可以去網(wǎng)上找一些markdown的主題樣式css文件,例如我用一個(gè)最簡(jiǎn)單Github
主題的markdown樣式。另外我還是很推薦Typora Theme (opens new window),上面有很多很多的markdown主題
因?yàn)槲疫@個(gè)樣式主題是有一個(gè)前綴id write
(Typora上的大部分主題前綴也是#write
),所以我們給展示區(qū)的標(biāo)簽加上該類(lèi)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" // 新增write的ID名 dangerouslySetInnerHTML={{ __html: htmlString }} /> </div> ) }
再來(lái)看看加入樣式后的渲染結(jié)果圖
四、代碼塊高亮
markdown語(yǔ)法的解析已經(jīng)完成了,并且也有對(duì)應(yīng)的樣式了,但是代碼塊好像還沒(méi)有高亮樣式
這塊兒我們自己來(lái)從0到1的實(shí)現(xiàn)是不可能的,可以用現(xiàn)成的開(kāi)源庫(kù) highlight.js
,highlight.js 官方文檔 (opens new window),這個(gè)庫(kù)能幫你做的就是檢測(cè)代碼塊標(biāo)簽元素,并為其加上特定的類(lèi)名。這里放上這個(gè)庫(kù)的API文檔(opens new window)
highlight.js
默認(rèn)是檢測(cè)它所支持的所有語(yǔ)言的語(yǔ)法的,我們就不需要關(guān)心了,并且其提供了很多的代碼高亮主題,我們可以在官網(wǎng)進(jìn)行預(yù)覽,如下圖所示:
更大的好消息來(lái)了!markdown-it
已經(jīng)將highlight.js
集成進(jìn)去了,直接設(shè)定一些配置即可,并且我們需要先將該庫(kù)下載下來(lái)。具體的可以看markdown-it中文官網(wǎng) - 高亮語(yǔ)法配置(opens new window)
同時(shí)在目錄highlight.js/styles/
下有很多很多的主題,可以自行導(dǎo)入
接下來(lái)就來(lái)實(shí)現(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庫(kù) 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> ) }
來(lái)看一下代碼高亮的效果圖:
五、同步滾動(dòng)
markdown編輯器還有一個(gè)重要的功能就是在我們滾動(dòng)一個(gè)區(qū)域的內(nèi)容時(shí),另一塊區(qū)域也跟著同步的滾動(dòng),這樣才方便查看
接下來(lái)我們來(lái)實(shí)現(xiàn)一下,我會(huì)將我實(shí)現(xiàn)時(shí)踩的坑也一并列出來(lái),讓大家也印象深刻點(diǎn),免得以后也犯同樣的錯(cuò)誤
剛開(kāi)始主要實(shí)現(xiàn)思路就是當(dāng)滾動(dòng)其中一塊區(qū)域時(shí),計(jì)算滾動(dòng)比例(scrollTop / scrollHeight
),然后使另一塊區(qū)域當(dāng)前的滾動(dòng)距離占總滾動(dòng)高度的比例等于該滾動(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ū)域的滾動(dòng)事件 const handleScroll = (block: number, event) => { let { scrollHeight, scrollTop } = event.target let scale = scrollTop / scrollHeight // 滾動(dòng)比例 // 當(dāng)前滾動(dòng)的是編輯區(qū) if(block === 1) { // 改變展示區(qū)的滾動(dòng)距離 let { scrollHeight } = show.current show.current.scrollTop = scrollHeight * scale } else if(block === 2) { // 當(dāng)前滾動(dòng)的是展示區(qū) // 改變編輯區(qū)的滾動(dòng)距離 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> ) }
這是我做的時(shí)候的第一版,確實(shí)是實(shí)現(xiàn)了兩塊區(qū)域的同步滾動(dòng),但是存在兩個(gè)bug,來(lái)看看是哪兩個(gè)
bug1:
這是一個(gè)很致命的bug,先埋個(gè)伏筆,先來(lái)看效果:
同步滾動(dòng)的效果實(shí)現(xiàn)了,但能很明顯得看到,當(dāng)我手動(dòng)滾動(dòng)完以后停止了任何操作,但是兩個(gè)區(qū)域仍然在不停的滾動(dòng),這是為什么呢?
排查了一下代碼,發(fā)現(xiàn) handleScroll
這個(gè)方法會(huì)無(wú)限觸發(fā),假設(shè)當(dāng)我們手動(dòng)滾動(dòng)一次編輯區(qū)后會(huì)觸發(fā)其 scroll
方法,即會(huì)調(diào)用 handleScroll
方法,然后會(huì)去改變「展示區(qū)」的滾動(dòng)距離,此時(shí)又會(huì)觸發(fā)展示區(qū)的 scroll
方法,即調(diào)用 handleScroll
方法,然后會(huì)去改變「編輯區(qū)」的滾動(dòng)距離 … 就這樣一直循環(huán)往復(fù),才會(huì)出現(xiàn)圖中的bug
后來(lái)我想了個(gè)比較簡(jiǎn)單的解決辦法,就是用一個(gè)變量記住你當(dāng)前手動(dòng)觸發(fā)的是哪個(gè)區(qū)域的滾動(dòng),這樣就可以在 handleScroll
方法里區(qū)分此次滾動(dòng)是被動(dòng)觸發(fā)的還是主動(dòng)觸發(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ū)主動(dòng)觸發(fā)滾動(dòng); 2: 展示區(qū)主動(dòng)觸發(fā)滾動(dòng) let scrollTimer; // 結(jié)束滾動(dòng)的定時(shí)器 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; // 記錄主動(dòng)觸發(fā)滾動(dòng)的區(qū)域 if(scrolling === 2) return; // 當(dāng)前是「展示區(qū)」主動(dòng)觸發(fā)的滾動(dòng),因此不需要再驅(qū)動(dòng)展示區(qū)去滾動(dòng) driveScroll(scale, showRef.current) // 驅(qū)動(dòng)「展示區(qū)」的滾動(dòng) } else if(block === 2) { if(scrolling === 0) scrolling = 2; if(scrolling === 1) return; // 當(dāng)前是「編輯區(qū)」主動(dòng)觸發(fā)的滾動(dòng),因此不需要再驅(qū)動(dòng)編輯區(qū)去滾動(dòng) driveScroll(scale, editRef.current) } } // 驅(qū)動(dòng)一個(gè)元素進(jìn)行滾動(dòng) const driveScroll = (scale: number, el: HTMLElement) => { let { scrollHeight } = el el.scrollTop = scrollHeight * scale if(scrollTimer) clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { scrolling = 0 // 在滾動(dòng)結(jié)束后,將scrolling設(shè)為0,表示滾動(dòng)結(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了,同步滾動(dòng)也算很不錯(cuò)得實(shí)現(xiàn)了,現(xiàn)在的效果就跟文章開(kāi)頭展示的圖片里效果一樣了
bug2:
這里還存在一個(gè)很小的問(wèn)題,也不算是bug,應(yīng)該算是設(shè)計(jì)上的思路問(wèn)題,那就是兩個(gè)區(qū)域其實(shí)還沒(méi)完完全全實(shí)現(xiàn)同步滾動(dòng)。先來(lái)看看原先的設(shè)計(jì)思想
編輯區(qū)和展示區(qū)的可視高度是一樣的,但一般編輯區(qū)的內(nèi)容經(jīng)過(guò)markdown渲染后,總的滾動(dòng)高度是會(huì)高于編輯區(qū)總的滾動(dòng)高度的,所以我們無(wú)法僅憑scrollTop
和scrollHeight
使得兩個(gè)區(qū)域同步滾動(dòng),比較晦澀,用具體的數(shù)據(jù)來(lái)看一下
屬性 | 編輯區(qū) | 展示區(qū) |
---|---|---|
clientHeight | 300 | 300 |
scrollHeight | 500 | 600 |
假設(shè)我們現(xiàn)在滾動(dòng)編輯區(qū)到最底部,那么此時(shí)
「編輯區(qū)」的 scrollTop
應(yīng)為 scrollHeight - clientHeight = 500 - 300 = 200,按照我們?cè)居?jì)算滾動(dòng)比例的方式得出 scale = scrollTop / scrollHeight = 200 / 500 = 0.4
「展示區(qū)」同步滾動(dòng)后,scrollTop = scale * scrollHeight = 0.4 * 600 = 240 < 600 - 300 = 300。但事實(shí)就是編輯區(qū)滾動(dòng)到最底部了,而展示區(qū)還沒(méi)有,顯然不是我們要的效果
換一種思路,我們?cè)谟?jì)算滾動(dòng)比例時(shí),應(yīng)計(jì)算的是當(dāng)前的 scrollTop
占 scrollTop
最大值的比例,這樣就能實(shí)現(xiàn)同步滾動(dòng)了,仍然用剛才那個(gè)例子來(lái)看: 此時(shí)編輯區(qū)滾動(dòng)到最底部,那么scale
應(yīng)為 scrollTop / (scrollHeight - clientHeight) = 200 / (500 - 300) = 100%,表示編輯區(qū)滾動(dòng)到最底部了,那么在展示區(qū)同步滾動(dòng)時(shí),他的 scrollTop
就變成了 scale * (scrollHeight - clientHeight) = 100% * (600 - 300) = 300,此時(shí)的展示區(qū)也同步滾動(dòng)到了最底部,這樣就實(shí)現(xiàn)了真正的同步滾動(dòng)了
來(lái)看一下改進(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)后的計(jì)算滾動(dòng)比例的方法 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ū)動(dòng)一個(gè)元素進(jìn)行滾動(dòng) const driveScroll = (scale: number, el: HTMLElement) => { let { scrollHeight, clientHeight } = el el.scrollTop = (scrollHeight - clientHeight) * scale // scrollTop的同比例滾動(dòng) 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> ) }
兩個(gè)bug都已經(jīng)解決了,同步滾動(dòng)的功能也算完美實(shí)現(xiàn)啦。但對(duì)于同步滾動(dòng)這個(gè)功能,其實(shí)有兩種概念,一種是兩個(gè)區(qū)域在滾動(dòng)高度上保持同步滾動(dòng);另一種就是右側(cè)的展示區(qū)域?qū)?yīng)左側(cè)的編輯區(qū)的內(nèi)容進(jìn)行滾動(dòng)。我們現(xiàn)在實(shí)現(xiàn)的是前者,后者可以后續(xù)作為新功能實(shí)現(xiàn)一下~
六、工具欄
最后我們就再實(shí)現(xiàn)一下編輯器的工具欄部分的工具(加粗、斜體、有序列表等等),因?yàn)檫@幾個(gè)工具的實(shí)現(xiàn)思路都一致,我們就拿 「加粗」 這個(gè)工具舉例子,其余的就可以模仿著寫(xiě)出來(lái)了
加粗工具的實(shí)現(xiàn)思路:
光標(biāo)是否選中文字?
- 是。將選中文字的兩側(cè)加上**
- 否。在光標(biāo)所在處添加文字**加粗文字**
動(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>'; } }) 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ū)動(dòng)一個(gè)元素進(jìn)行滾動(dòng) 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)的位置。未選中文字時(shí):selectionStart === selectionEnd ;選中文字時(shí):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è)一個(gè)加粗的按鈕 */} <textarea className="edit" ref={edit} onScroll={(e) => handleScroll(1, e)} onChange={(e) => setValue(e.target.value)} // 直接修改value的值,useEffect會(huì)同步渲染展示區(qū)的內(nèi)容 value={value} /> <div className="show" id="write" ref={show} onScroll={(e) => handleScroll(2, e)} dangerouslySetInnerHTML={{ __html: htmlString }} /> </div> ) }
借助這樣的思路,就可以完成其它各種工具的實(shí)現(xiàn)了。
七、補(bǔ)充
為了保證包的體積足夠小,我將第三方依賴(lài)庫(kù)、markdown主題、代碼高亮主題都通過(guò)外鏈的形式導(dǎo)入了
八、最后
一個(gè)簡(jiǎn)易版的markdown編輯器就實(shí)現(xiàn)了,大家可以手動(dòng)嘗試實(shí)現(xiàn)一下。后續(xù)我也會(huì)繼續(xù)發(fā)一些教程,對(duì)這個(gè)編輯器的功能進(jìn)行擴(kuò)展
點(diǎn)擊下載源碼,后續(xù)擴(kuò)展一下功能,并作為一個(gè)完整的組件發(fā)布到npm給大家使用,希望大家多多支持~(其實(shí)我已經(jīng)悄悄發(fā)布,但因功能還不是太完善,就不先拿出來(lái)給大家使用了,這里簡(jiǎn)單放個(gè)npm包的地址 (opens new window))
相關(guān)文章
詳解webpack2+node+react+babel實(shí)現(xiàn)熱加載(hmr)
這篇文章主要介紹了詳解webpack2+node+react+babel實(shí)現(xiàn)熱加載(hmr) ,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-08-08使用react render props實(shí)現(xiàn)倒計(jì)時(shí)的示例代碼
這篇文章主要介紹了使用react render props實(shí)現(xiàn)倒計(jì)時(shí)的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12React?中使用?react-i18next?國(guó)際化的過(guò)程(react-i18next?的基本用法)
i18next?是一款強(qiáng)大的國(guó)際化框架,react-i18next?是基于?i18next?適用于?React?的框架,本文介紹了?react-i18next?的基本用法,如果更特殊的需求,文章開(kāi)頭的官方地址可以找到答案2023-01-01React中memo useCallback useMemo方法作用及使用場(chǎng)景
這篇文章主要為大家介紹了React中三個(gè)hooks方法memo useCallback useMemo的作用及使用場(chǎng)景示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助2023-03-03React中Provider組件詳解(使用場(chǎng)景)
這篇文章主要介紹了React中Provider組件使用場(chǎng)景,使用Provider可以解決數(shù)據(jù)層層傳遞和每個(gè)組件都要傳props的問(wèn)題,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2024-02-02