先上成果圖
github?https://github.com/li328250157/markdown

網(wǎng)頁的布局
網(wǎng)頁布局分為三部分,分別是
頭部header,固定定位
側(cè)邊欄aside,固定定位,margin-top的值是header的高度
內(nèi)容contain,靜態(tài)定位, margin-top值為header的高度,margin-left的值為aside的寬度,是router-view的出口。分為兩部分:
主內(nèi)容,顯示md轉(zhuǎn)換后的html頁面,margin-right值為md目錄的寬度值
提取markdown的h1和h2目錄,用于標(biāo)題導(dǎo)航,固定定位
功能
為了md能在網(wǎng)頁上良好的展示,應(yīng)具備以下功能:
點(diǎn)擊左側(cè)的菜單,可以獲取到相應(yīng)的md內(nèi)容(字符串格式),將md內(nèi)容轉(zhuǎn)成html,為一級、二級標(biāo)題加錨點(diǎn)id
為html增加md的格式,引入一個(gè)css即可,參考網(wǎng)址
提取md中的一級二級標(biāo)題,在右側(cè)顯示文章目錄
點(diǎn)擊右側(cè)文章目錄,左側(cè)內(nèi)容可定位到相應(yīng)的位置,還要做平滑滾動處理,增強(qiáng)用戶體驗(yàn)
左側(cè)內(nèi)容滾動時(shí),右側(cè)目錄的激活項(xiàng)隨之動態(tài)變化
md轉(zhuǎn)成html
我使用了marked.js將md轉(zhuǎn)成html,并在這里為h1和h2加上了id值,作為錨點(diǎn)
//? 先安裝marked.js到本地
npm install marked --save
//? 在組件內(nèi)引入marked
import marked from 'marked';
//? marked的基本設(shè)置
marked.setOptions({
? ? renderer: rendererMD,
? ? gfm: true,
? ? tables: true,
? ? breaks: false,
? ? pedantic: false,
? ? sanitize: false,
? ? smartLists: true,
? ? smartypants: false
});
//? 實(shí)例化
let rendererMD = new marked.Renderer();
// 在計(jì)算屬性中,處理md的h1、h2,加上id值,并使用marked轉(zhuǎn)成html
computed: {
? ? compiledMarkdown: function() {
? ? ? ? let index = 0;
? ? ? ? rendererMD.heading = function(text, level) {
? ? ? ? ? ? if (level < 3) {
? ? ? ? ? ? ? ? return `<h${level} id="${index++}" class="jump" >${text}</h${level}>`;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? return `<h${level}>${text}</h${level}>`;
? ? ? ? ? ? }
? ? ? ? };
? ? ? ? return marked(this.content);
? ? }
}
在html中,用v-html綁定此計(jì)算屬性即可
<div class="markdown-body" ref="content" id="content" v-html="compiledMarkdown">
提取標(biāo)題
getTitle(content) {
? ? let nav = [];
? ? let navLevel = [1, 2];
? ? let tempArr = [];
? ? content
? ? ? ? .replace(/```/g, function(match) {
? ? ? ? ? ? return '\f';
? ? ? ? })
? ? ? ? .replace(/\f[^\f]*?\f/g, function(match) {
? ? ? ? ? ? return '';
? ? ? ? })
? ? ? ? .replace(/\r|\n+/g, function(match) {
? ? ? ? ? ? return '\n';
? ? ? ? })
? ? ? ? // 以至少一個(gè)#開始,緊接非換行符外任意個(gè)字符進(jìn)行惰性匹配,然后是一個(gè)換行符
? ? ? ? .replace(/(#+)[^#][^\n]*?(?:\n)/g, function(match, m1) {
? ? ? ? ? ? let title = match.replace('\n', '');
? ? ? ? ? ? let level = m1.length;
? ? ? ? ? ? tempArr.push({
? ? ? ? ? ? ? ? title: title.replace(/^#+/, '').replace(/\([^)]*?\)/, ''),
? ? ? ? ? ? ? ? level: level,
? ? ? ? ? ? ? ? children: []
? ? ? ? ? ? });
? ? ? ? });
? ? //? tempArr得到的是全部1-6級標(biāo)題,將一級和二級過濾出來
? ? nav = tempArr.filter(_ => _.level <= 2);
? ? let index = 0;
? ? //? 在此處加index值,這里和標(biāo)簽里綁定的id是對應(yīng)的
? ? nav = nav.map(_ => {
? ? ? ? _.index = index++;
? ? ? ? return _;
? ? });
? ? let retNavs = [];
? ? let toAppendNavList;
? ? navLevel.forEach(level => {
? ? ? ? // 遍歷一級和二級標(biāo)題,將同一級的元素組成一個(gè)新數(shù)組
? ? ? ? toAppendNavList = this.find(nav, {
? ? ? ? ? ? level: level
? ? ? ? });
? ? ? ? if (retNavs.length === 0) {
? ? ? ? ? ? // 處理一級標(biāo)題
? ? ? ? ? ? retNavs = retNavs.concat(toAppendNavList);
? ? ? ? } else {
? ? ? ? ? ? // 處理二級標(biāo)題,把二級標(biāo)題加到相應(yīng)的父節(jié)點(diǎn)的children中
? ? ? ? ? ? toAppendNavList.forEach(_ => {
? ? ? ? ? ? ? ? _ = Object.assign(_);
? ? ? ? ? ? ? ? let parentNavIndex = this.getParentIndex(nav, _.index);
? ? ? ? ? ? ? ? return this.appendToParentNav(retNavs, parentNavIndex, _);
? ? ? ? ? ? });
? ? ? ? }
? ? });
? ? //? 此處的retNavs就是處理后的樹
? ? return retNavs;
},
//? 處理屬于同一級的標(biāo)題,組成數(shù)組
find(arr, condition) {
? ? return arr.filter(_ => {
? ? ? ? for (let key in condition) {
? ? ? ? ? ? if (condition.hasOwnProperty(key) && condition[key] !== _[key]) {
? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? return true;
? ? });
},
//? 獲取此節(jié)點(diǎn)的父節(jié)點(diǎn)
getParentIndex(nav, endIndex) {
? ? //? 從當(dāng)前的index開始找 1.距離自己最近的(遞減體現(xiàn)) 2.level比本身小的(越小越高)
? ? for (var i = endIndex - 1; i >= 0; i--) {
? ? ? ? if (nav[endIndex].level > nav[i].level) {
? ? ? ? ? ? return nav[i].index;
? ? ? ? }
? ? }
},
//? 找到同一個(gè)父節(jié)點(diǎn)的所有子節(jié)點(diǎn)
appendToParentNav(nav, parentIndex, newNav) {
? ? //? 找到每一個(gè)二級標(biāo)題的傅標(biāo)題的index值
? ? let index = this.findIndex(nav, {
? ? ? ? index: parentIndex
? ? });
? ? if (index === -1) {
? ? ? ? // 這里處理的是三級及以下標(biāo)題
? ? ? ? // 如果在一級標(biāo)題里沒找到父節(jié)點(diǎn),就去每一個(gè)一級標(biāo)題里的children里找
? ? ? ? let subNav;
? ? ? ? for (var i = 0; i < nav.length; i++) {
? ? ? ? ? ? // 處理沒有父節(jié)點(diǎn)的情況
? ? ? ? ? ? subNav = nav[i];
? ? ? ? ? ? subNav.children.length && this.appendToParentNav(subNav.children, parentIndex, newNav);
? ? ? ? }
? ? } else {
? ? ? ? nav[index].children = nav[index].children.concat(newNav);
? ? }
},
//? 找符合條件的數(shù)組中的成員
findIndex(arr, condition) {
? ? let ret = -1;
? ? arr.forEach((item, index) => {
? ? ? ? for (var key in condition) {
? ? ? ? ? ? if (condition.hasOwnProperty(key) && condition[key] !== item[key]) { // 不進(jìn)行深比較
? ? ? ? ? ? ? ? return false;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? ret = index;
? ? });
? ? return ret;
},
md目錄的展示和錨點(diǎn)定位
<div id="menu">
? ? <ul class="nav-list">
? ? ? ? <li v-for="(nav, index) in contentMenu" :key="index">
? ? ? ? ? ? <a :href="'#' + nav.index" :class="{'active': highlightIndex === nav.index}" @click="handleHighlight(nav.index)" :key="nav.index">{{nav.title}}
? ? ? ? ? ? </a>
? ? ? ? ? ? <template v-if="nav.children.length > 0">
? ? ? ? ? ? ? ? <ul class="nav-list">
? ? ? ? ? ? ? ? ? ? <li v-for="(item, index) in nav.children" :key="index">
? ? ? ? ? ? ? ? ? ? ? ? <a :href="'#' + item.index" :class="{active: highlightIndex === item.index}" :key="item.index" @click="handleHighlight(item.index)">{{item.title}}
? ? ? ? ? ? ? ? ? ? ? ? </a>
? ? ? ? ? ? ? ? ? ? </li>
? ? ? ? ? ? ? ? </ul>
? ? ? ? ? ? </template>
? ? ? ? </li>
? ? </ul>
</div>
平滑滾動
在md的目錄中,a標(biāo)簽里已經(jīng)設(shè)置了href值,進(jìn)行了錨點(diǎn)定位,在點(diǎn)擊目錄綁定的事件里做了平滑處理
handleHighlight(item) {
? ? this.highlightIndex = item;
? ? let jump = document.querySelectorAll('.jump');
? ? //? 這里的60是header的高度值
? ? let total = jump[item].offsetTop - 60;
? ? let distance = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
? ? // 平滑滾動,時(shí)長500ms,每10ms一跳,共50跳
? ? let step = total / 50;
? ? if (total > distance) {
? ? ? ? smoothDown();
? ? } else {
? ? ? ? let newTotal = distance - total;
? ? ? ? step = newTotal / 50;
? ? ? ? smoothUp();
? ? }
? ? function smoothDown() {
? ? ? ? if (distance < total) {
? ? ? ? ? ? distance += step;
? ? ? ? ? ? document.body.scrollTop = distance;
? ? ? ? ? ? document.documentElement.scrollTop = distance;
? ? ? ? ? ? setTimeout(smoothDown, 10);
? ? ? ? } else {
? ? ? ? ? ? document.body.scrollTop = total;
? ? ? ? ? ? document.documentElement.scrollTop = total;
? ? ? ? }
? ? }
? ? function smoothUp() {
? ? ? ? if (distance > total) {
? ? ? ? ? ? distance -= step;
? ? ? ? ? ? document.body.scrollTop = distance;
? ? ? ? ? ? document.documentElement.scrollTop = distance;
? ? ? ? ? ? setTimeout(smoothUp, 10);
? ? ? ? } else {
? ? ? ? ? ? document.body.scrollTop = total;
? ? ? ? ? ? document.documentElement.scrollTop = total;
? ? ? ? }
? ? }
}
主內(nèi)容滾動,目錄高亮
在閱讀md內(nèi)容時(shí),隨著滾動條的變化,目錄的高亮項(xiàng)也隨著變化
mounted() {
? ? this.$nextTick(function() {
? ? ? ? window.addEventListener('scroll', this.onScroll);
? ? });
},
methods: {
? ? onScroll() {
? ? ? ? let top = document.documentElement ? document.documentElement.scrollTop : document.body.scrollTop;
? ? ? ? let items = document.getElementById('content').getElementsByClassName('jump');
? ? ? ? let currentId = '';
? ? ? ? for (let i = 0; i < items.length; i++) {
? ? ? ? ? ? let _item = items[i];
? ? ? ? ? ? let _itemTop = _item.offsetTop;
? ? ? ? ? ? if (top > _itemTop - 75) {
? ? ? ? ? ? ? ? currentId = _item.id;
? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? break;
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? if (currentId) {
? ? ? ? ? ? //? 這里的currentOId是字符串,必須轉(zhuǎn)換成數(shù)字,否則高亮項(xiàng)的全等無法匹配
? ? ? ? ? ? this.highlightIndex = parseInt(currentId);
? ? ? ? }
? ? }
}
Summary
以上就是如何讓md在網(wǎng)頁上良好展示的全部功能,谷歌了多次,感謝分享經(jīng)驗(yàn)的艾瑞巴dei,開源萬歲~!
待改進(jìn)的點(diǎn)有:
如何避免非標(biāo)題的#的正則匹配,比如// #這不是標(biāo)題?格式的內(nèi)容
md中h1和h2標(biāo)簽的處理,md中是允許html的,現(xiàn)在不能滿足匹配h1、h2的標(biāo)簽