欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

React框架快速實(shí)現(xiàn)簡(jiǎn)易的Markdown編輯器

 更新時(shí)間:2022年04月26日 16:03:21   作者:「零一」  
這篇文章主要為大家介紹了使用React框架實(shí)現(xiàn)簡(jiǎn)易的Markdown編輯器,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

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

img

我的第一想法肯定是能用優(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)好了的效果圖:

最終效果圖

源碼下載點(diǎ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è),分別是MarkedShowdown、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)看看效果

markdown語(yǔ)法解析效果展示圖

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

img

我們可以打印解析出來(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渲染效果圖

四、代碼塊高亮

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ù)覽,如下圖所示:

img

更大的好消息來(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)效果圖

同步滾動(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ì)思想

img

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

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

假設(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)前的 scrollTopscrollTop最大值的比例,這樣就能實(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)圖效果演示:

加粗工具動(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)文章

  • React生命周期原理與用法踩坑筆記

    React生命周期原理與用法踩坑筆記

    這篇文章主要介紹了React生命周期原理與用法,結(jié)合實(shí)例形式總結(jié)分析了react生命周期原理、用法及相關(guān)注意事項(xiàng),需要的朋友可以參考下
    2020-04-04
  • 使用react完成點(diǎn)擊返回頂部操作

    使用react完成點(diǎn)擊返回頂部操作

    本文主要介紹了使用react完成點(diǎn)擊返回頂部操作,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-02-02
  • 詳解webpack2+node+react+babel實(shí)現(xiàn)熱加載(hmr)

    詳解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í)的示例代碼

    這篇文章主要介紹了使用react render props實(shí)現(xiàn)倒計(jì)時(shí)的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-12-12
  • JavaScript中React面向組件編程(上)

    JavaScript中React面向組件編程(上)

    本文主要介紹了React組件中默認(rèn)封裝了很多屬性,有的是提供給開(kāi)發(fā)者操作的,其中有三個(gè)屬性非常重要:state、props、refs。感興趣的小伙伴可以參考閱讀
    2023-03-03
  • React?中使用?react-i18next?國(guó)際化的過(guò)程(react-i18next?的基本用法)

    React?中使用?react-i18next?國(guó)際化的過(guò)程(react-i18next?的基本用法)

    i18next?是一款強(qiáng)大的國(guó)際化框架,react-i18next?是基于?i18next?適用于?React?的框架,本文介紹了?react-i18next?的基本用法,如果更特殊的需求,文章開(kāi)頭的官方地址可以找到答案
    2023-01-01
  • React中memo useCallback useMemo方法作用及使用場(chǎng)景

    React中memo useCallback useMemo方法作用及使用場(chǎng)景

    這篇文章主要為大家介紹了React中三個(gè)hooks方法memo useCallback useMemo的作用及使用場(chǎng)景示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2023-03-03
  • React?Hook?四種組件優(yōu)化總結(jié)

    React?Hook?四種組件優(yōu)化總結(jié)

    這篇文章主要介紹了React?Hook四種組件優(yōu)化總結(jié),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)孩子,需要的朋友可以參考一下
    2022-07-07
  • 淺談React中組件間抽象

    淺談React中組件間抽象

    這篇文章主要介紹了淺談React中組件間抽象,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2018-01-01
  • React中Provider組件詳解(使用場(chǎng)景)

    React中Provider組件詳解(使用場(chǎng)景)

    這篇文章主要介紹了React中Provider組件使用場(chǎng)景,使用Provider可以解決數(shù)據(jù)層層傳遞和每個(gè)組件都要傳props的問(wèn)題,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧
    2024-02-02

最新評(píng)論