React框架快速實(shí)現(xiàn)簡(jiǎn)易的Markdown編輯器
前言
最近我在項(xiàng)目中需要實(shí)現(xiàn)一個(gè) markdown編輯器 的需求,并且是以React框架為開發(fā)基礎(chǔ)的,類似掘金這樣的:

我的第一想法肯定是能用優(yōu)秀的開源就一定用開源的,畢竟不能老是重復(fù)造輪子。于是我在我的前端群里問(wèn)了很多群友,他們都給了甩過(guò)來(lái)一堆開源的markdown編輯器項(xiàng)目,但我一看全是基于Vue使用的,不符合我的預(yù)期,逛了一下github,也沒看到我滿意的項(xiàng)目,所以就想自己實(shí)現(xiàn)一個(gè)啦
需要實(shí)現(xiàn)的功能
我們自己實(shí)現(xiàn)的話,看看需要支持哪些功能,因?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è)在線體驗(yàn)的地址 (opens new window),因?yàn)樽龅谋容^倉(cāng)促,歡迎大家給我提意見和pr
具體實(shí)現(xiàn)
具體的實(shí)現(xiàn)也是按照我們上述列出來(lái)的功能的順序來(lái)一一實(shí)現(xiàn)的
說(shuō)明:本文通過(guò)循序漸進(jìn)的方式講解,所以重復(fù)代碼可能有點(diǎ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ù),常用的有三個(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ò)展性好、性能較好 | 逐字符解析(中文支持不好) |
剛開始我選擇了showdown這個(gè)庫(kù),因?yàn)檫@個(gè)庫(kù)使用起來(lái)特別方便,而且官方已經(jīng)在庫(kù)中提供了很多擴(kuò)展功能,只需要配置一些字段即可。
但是后來(lái)我又分析了一波,還是選用了markdown-it,因?yàn)橹罂赡苄枰龈嗟恼Z(yǔ)法擴(kuò)展,showdown的官方文檔寫的比較生硬,而且markdown-it使用的人也多,生態(tài)比較好,雖然其官方?jīng)]有支持很多擴(kuò)展的語(yǔ)法,但是已經(jīng)有很多基于makrdown-it的功能擴(kuò)展插件了,最重要的是markdown-it的官方文檔寫得好?。ǘ矣兄形奈臋n)!
接下來(lái)寫一下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í)是沒問(wèn)題的,被解析好的 html字符串 每個(gè)標(biāo)簽都被附帶上了特定的類名,只是現(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)簽加上該類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)的樣式了,但是代碼塊好像還沒有高亮樣式
這塊兒我們自己來(lái)從0到1的實(shí)現(xiàn)是不可能的,可以用現(xiàn)成的開源庫(kù) highlight.js,highlight.js 官方文檔 (opens new window),這個(gè)庫(kù)能幫你做的就是檢測(cè)代碼塊標(biāo)簽元素,并為其加上特定的類名。這里放上這個(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ò)誤
剛開始主要實(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)在的效果就跟文章開頭展示的圖片里效果一樣了
bug2:
這里還存在一個(gè)很小的問(wèn)題,也不算是bug,應(yīng)該算是設(shè)計(jì)上的思路問(wèn)題,那就是兩個(gè)區(qū)域其實(shí)還沒完完全全實(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ū)還沒有,顯然不是我們要的效果
換一種思路,我們?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è)工具舉例子,其余的就可以模仿著寫出來(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ǔ)充
為了保證包的體積足夠小,我將第三方依賴庫(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)文章
React移動(dòng)端項(xiàng)目之pdf預(yù)覽問(wèn)題
這篇文章主要介紹了React移動(dòng)端項(xiàng)目之pdf預(yù)覽問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
ReactNative集成個(gè)推消息推送過(guò)程詳解
這篇文章主要為大家介紹了ReactNative集成個(gè)推消息推送過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-08-08
React-Hook中使用useEffect清除定時(shí)器的實(shí)現(xiàn)方法
這篇文章主要介紹了React-Hook中useEffect詳解(使用useEffect清除定時(shí)器),主要介紹了useEffect的功能以及使用方法,還有如何使用他清除定時(shí)器,需要的朋友可以參考下2022-11-11
React Native提供自動(dòng)完成的下拉菜單的方法示例
這篇文章主要為大家介紹了React Native提供自動(dòng)完成的下拉菜單的方法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-10-10

