有一個markdown文檔,須要前端進行markdown解析,同時要能生成一個目錄。總體效果相似掘金。html
解析markdown的開源庫挺多的,可是生成的目錄並不知足個人預期。因而只好本身來作一個。前端
使用庫node
本文采用 marked
對markdown進行解析。 設置ref來獲取解析後的根節點react
<section className="doc-container">
<div
dangerouslySetInnerHTML={{__html: markdown}}
className="markdown-body"
ref={(ref) => {this.markdown = ref;}} // 用來獲取文檔解析後的根節點
></div>
</section>
複製代碼
當文檔解析成html內容後,下一步就須要獲取全部的標題(h1-h6)信息markdown
constructor(props) {
super(props);
this.state={
menuData: [], // 用來存儲目錄結構
menuState: '' // 用來存儲當前命中的標題
};
}
// 由於要計算高度,預留1秒等待文檔加載
componentDidMount() {
setTimeout(() => {
this.getAPs(['H1', 'H2', 'H3', 'H4']);
}, 1000);
}
// 獲取標題錨點
// 參數 nodeArr 表示須要解析到目錄的內容標題
getAPs = (nodeArr) => {
let nodeInfo = []; // 存儲目錄信息
// 對文檔根節點的每個子節點進行遍歷,選出全部須要解析的目錄標題
this.markdown.childNodes.forEach((item) => {
if (nodeArr.includes(item.nodeName)) {
nodeInfo.push({
type: item.nodeName, // 存儲該標題的類型
txt: item.getAttribute('id'), // 存儲該標題的文本 [ps:marked解析出來的h1-h6標題會在id裏填上對應的標題文本]
offsetTop: item.offsetTop // 存儲該標題離頁面頂部的距離
});
}
});
this.setState({
menuData: nodeInfo,
menuState: nodeInfo[0].txt
}, () => {
this.checkMenuScroll(); // 檢測滾動,稍後會講解
});
}
複製代碼
完成這一步後,能獲取到大概以下的數據ide
{
menuData: [
{
type: 'H1',
txt: '標題1',
offsetTop: 200
},
{
type: 'H2',
txt: '標題2',
offsetTop: 300
}
],
menuState: '標題1'
}
複製代碼
對menuData進行遍歷,生成對應的li標籤, class樣式單獨設置,並賦予點擊事件函數
<section className="doc-aside">
<ul>
{
this.state.menuData.map((item) => {
return (
<li
className={`${item.type}type`}
key={item.txt}
onClick={() => {this.scrollPage(item);}} // 當點擊時執行頁面移動,稍後會講解該函數
>
<a
className={menuState === item.txt ? 'on' : ''}
// 這裏用來處理該標題是否命中,a標籤不是必須
>{item.txt}</a>
</li>
);
})
}
</ul>
</section>
複製代碼
如今咱們已經根據文檔的內容生成了對應的標題。ui
接下來咱們須要對頁面滾動和標題點擊進行處理this
// 檢測頁面滾動函數
checkMenuScroll = () => {
// this.scroll 爲整個頁面的根節點,用來監聽滾動
this.scroll.addEventListener('scroll', () => {
let scrollTop = this.scroll.scrollTop; // 獲取當前頁面的滾動距離
let menuState = this.state.menuData[0].txt; // 設置menuState對象默認值爲第一個標題的文字
// 對menuData循環檢測,
// 若是當前頁面滾動距離 大於 一個標題離頁面頂部 的距離,則將該標題的文字賦值給menuState,循環繼續
// 若是當前頁面滾動距離 小於 一個標題離頁面頂部 的距離,說明頁面還沒滾動到該標題的位置,當前標題還沒有命中,以後的標題也不可能命中。 循環結束
for(let item of this.state.menuData) {
if (scrollTop >= item.offsetTop) {
menuState = item.txt;
} else {
break;
}
}
// 若是滑動到了頁面的底部,則命中最後一個標題
if (this.scroll.clientHeight + scrollTop === this.scroll.scrollHeight) {
menuState = this.state.menuData[this.state.menuData.length - 1].txt;
}
// 若是當前命中標題和前一個命中標題的文本不同,說明當前頁面處於其餘標題下的內容,切換menuState
if (menuState !== this.state.menuState) {
this.setState({menuState});
}
});
}
...
// 這裏須要設置一個ref,保存整個頁面的根節點,用來監聽滾動, 其餘內容保持不變
<div ref={(ref) => {this.scroll = ref;}}>
<section className="doc-aside">
<ul>
{
menuData.map((item) => {
return (
<li
className={`${item.type}type`}
key={item.txt}
onClick={() => {this.scrollPage(item);}}
>
<a className={menuState === item.txt ? 'on' : ''}>{item.txt}</a>
</li>
);
})
}
</ul>
</section>
<section className="doc-container">
<div
dangerouslySetInnerHTML={{__html: markdown}}
className="markdown-body"
ref={(ref) => {this.markdown = ref;}}
></div>
</section>
</div>
複製代碼
作完這一步,就已經實現了頁面滾動監聽,當咱們滾動頁面時,目錄能實時反饋用戶正在閱讀哪個標題下的內容。spa
最後,當點擊標題時,能自動切換到對應標題下的內容中
// 點擊目錄切換
scrollPage = (item) => {
// 建立一個setInterval,每16ms執行一次,接近60fps
let scrollToTop = window.setInterval(() => {
let currentScroll = this.scroll.scrollTop;
if (currentScroll > item.offsetTop) {
// 當頁面向上滾動時操做
this.scroll.scrollTo(0, currentScroll - Math.ceil((currentScroll - item.offsetTop) / 5));
} else if (currentScroll < item.offsetTop) {
// 頁面向下滾動時的操做
if (this.scroll.clientHeight + currentScroll === this.scroll.scrollHeight) {
// 若是已經滾動到了底部,則直接跳出
this.setState({menuState: item.txt});
window.clearInterval(scrollToTop);
} else {
this.scroll.scrollTo(0, currentScroll + Math.ceil((item.offsetTop - currentScroll) / 5));
}
} else {
window.clearInterval(scrollToTop);
}
}, 16);
}
複製代碼
到這裏,就已經實現了一個仿掘金的目錄,而且比掘金更好的是在點擊標題時能滾動到指定的內容部分。