md在web良好展現的處理,三種實現定位時平緩滾動的方法(vue + Element ui + mark.js)

項目地址 https://github.com/liangxh0523/md_web_displaycss

先上成果圖

網頁的佈局

網頁佈局分爲三部分,分別是html

  • 頭部header,固定定位
  • 側邊欄aside,固定定位,margin-top的值是header的高度
  • 內容contain,靜態定位, margin-top值爲header的高度,margin-left的值爲aside的寬度,是router-view的出口。分爲兩部分:
    • 主內容,顯示md轉換後的html頁面,margin-right值爲md目錄的寬度值
    • 提取markdown的h1和h2目錄,用於標題導航,固定定位

功能

爲了md能在網頁上良好的展現,應具有如下功能:git

  1. 點擊左側的菜單,能夠獲取到相應的md內容(字符串格式),將md內容轉成html,爲一級、二級標題加錨點id
  2. 爲html增長md的格式,引入一個css便可,參考網址
  3. 提取md中的一級二級標題,在右側顯示文章目錄
  4. 點擊右側文章目錄,左側內容可定位到相應的位置,還要作平滑滾動處理,加強用戶體驗,ps:主要講這個功能的三種實現方法,分別是
    • 原生JS的方法,優勢是兼容性好,幾乎全部瀏覽器都支持,缺點是代碼不優雅
    • css的scroll-behavior方法,優勢是代碼簡單、優美,缺點是不支持safari(可能更多,只測試了opera、chrome、firefox)
    • js的scrollIntoView方法,優勢是不須要錨點定位,代碼簡單。缺點也是瀏覽器兼容性問題,不支持safari
  5. 左側內容滾動時,右側目錄的激活項隨之動態變化

md轉成html

我使用了marked.js將md轉成htmlgithub

//  先安裝marked.js到本地
npm install marked --save
//  在組件內引入marked
import marked from 'marked';
//  marked的基本設置
marked.setOptions({
    renderer: rendererMD,
    gfm: true,
    tables: true,
    breaks: false,
    pedantic: false,
    sanitize: false,
    smartLists: true,
    smartypants: false
});
//  實例化
let rendererMD = new marked.Renderer();
watch: {
    '$route.path': {
        immediate:true,
        handler: function(newValue) {
            this.mode = newValue.slice(1);
            let index = 0;
            //  在rendererMD.heading裏有本身的做用域,用this訪問不到
            let mode = this.mode
            // 這裏的處理只是爲了同時展現多個demo,三種方法須要不一樣的處理方法
            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}>`;
                }
            };
            document.getElementsByTagName("html")[0].style['scroll-behavior'] = this.mode === 'css_demo' ? 'smooth' : '';
            this.compiledMarkdown = marked(str);
        }
    }
}
複製代碼

在html中,用v-html綁定此計算屬性便可web

<div class="markdown-body" ref="content" id="content" v-html="compiledMarkdown">
複製代碼

提取標題

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';
        })
        // 以致少一個#開始,緊接非換行符外任意個字符進行惰性匹配,而後是一個換行符
        .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級標題,將一級和二級過濾出來
    nav = tempArr.filter(_ => _.level <= 2);
    let index = 0;
    //  在此處加index值,這裏和標籤裏綁定的id是對應的
    nav = nav.map(_ => {
        _.index = index++;
        return _;
    });
    let retNavs = [];
    let toAppendNavList;
    navLevel.forEach(level => {
        // 遍歷一級和二級標題,將同一級的元素組成一個新數組
        toAppendNavList = this.find(nav, {
            level: level
        });
        if (retNavs.length === 0) {
            // 處理一級標題
            retNavs = retNavs.concat(toAppendNavList);
        } else {
            // 處理二級標題,把二級標題加到相應的父節點的children中
            toAppendNavList.forEach(_ => {
                _ = Object.assign(_);
                let parentNavIndex = this.getParentIndex(nav, _.index);
                return this.appendToParentNav(retNavs, parentNavIndex, _);
            });
        }
    });
    //  此處的retNavs就是處理後的樹
    return retNavs;
},
//  處理屬於同一級的標題,組成數組
find(arr, condition) {
    return arr.filter(_ => {
        for (let key in condition) {
            if (condition.hasOwnProperty(key) && condition[key] !== _[key]) {
                return false;
            }
        }
        return true;
    });
},
//  獲取此節點的父節點
getParentIndex(nav, endIndex) {
    //  從當前的index開始找 1.距離本身最近的(遞減體現) 2.level比自己小的(越小越高)
    for (var i = endIndex - 1; i >= 0; i--) {
        if (nav[endIndex].level > nav[i].level) {
            return nav[i].index;
        }
    }
},
//  找到同一個父節點的全部子節點
appendToParentNav(nav, parentIndex, newNav) {
    //  找到每個二級標題的傅標題的index值
    let index = this.findIndex(nav, {
        index: parentIndex
    });
    if (index === -1) {
        // 這裏處理的是三級及如下標題
        // 若是在一級標題裏沒找到父節點,就去每個一級標題裏的children裏找
        let subNav;
        for (var i = 0; i < nav.length; i++) {
            // 處理沒有父節點的狀況
            subNav = nav[i];
            subNav.children.length && this.appendToParentNav(subNav.children, parentIndex, newNav);
        }
    } else {
        nav[index].children = nav[index].children.concat(newNav);
    }
},
//  找符合條件的數組中的成員
findIndex(arr, condition) {
    let ret = -1;
    arr.forEach((item, index) => {
        for (var key in condition) {
            if (condition.hasOwnProperty(key) && condition[key] !== item[key]) { // 不進行深比較
                return false;
            }
        }
        ret = index;
    });
    return ret;
},
複製代碼

md目錄的展現和錨點定位

<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>
複製代碼

平滑滾動

這裏有三種方法實現平滑滾動chrome

1. js原生的方法

當點擊右側的滾動條時,處理點擊事件,根據點擊元素的offsetTop和scrollTop值的差,肯定須要滾動的距離npm

handleHighlight(item) {
    this.highlightIndex = item;
    if (this.mode === 'raw_js_demo') {
        this.rawJsScroll(item);
    } else if (this.mode === 'scroll_into_view') {
        let jump = document.querySelectorAll('.jump');
        jump[item].scrollIntoView({
            block: 'start',
            behavior: "smooth"
        });
    }
},
rawJsScroll(item) {
    let jump = document.querySelectorAll('.jump');
    //  這裏的60是header的高度值
    let total = jump[item].offsetTop - 60;
    let distance = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop;
    // 平滑滾動,時長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;
            window.scrollTo(0, distance);
            setTimeout(smoothDown, 10);
        } else {
            window.scrollTo(0, total);
        }
    }
    function smoothUp() {
        if (distance > total) {
            distance -= step;
            window.scrollTo(0, distance);
            setTimeout(smoothUp, 10);
        } else {
            window.scrollTo(0, total);
        }
    }
}
複製代碼

2. css的方式

若是是css的方式,滾動條的容器處的scroll-behavior屬性設置爲smooth值數組

document.getElementsByTagName("html")[0].style['scroll-behavior'] = this.mode === 'css_demo' ? 'smooth' : '';
複製代碼

因爲這裏有header,因此須要錨點定位時進行偏移處理,因此對於須要定位到的元素h一、h2前面插入一個元素,這個元素的標籤內綁定錨點值,它的padding-top和margin-top設置爲相反數,值爲頭部的高度,這樣就完成了定位元素的偏移處理。瀏覽器

rendererMD.heading = function(text, level) {
    if (level < 3) {
        return <div class="jump offset" id="${index++}"></div><h${level}>${text}</h${level}>`;
    } else {
        return `<h${level}>${text}</h${level}>`;
    }
};

div .offset {
    position: relative;
    padding-top: 40px;
    margin-top: -40px;
    visibility: hidden;
}
複製代碼

3. JS的scrollIntoView()方法

若是使用scrollIntoView()的方法,就不須要加錨點值進行定位了,在目錄項的點擊事件中,使用當前選中元素,設置scrollIntoView()方法的block和behavior值,直接平滑滾動到指定的位置bash

rendererMD.heading = function(text, level) {
    if (level < 3) {
        return <div class="jump offset" id="${index++}"></div><h${level}>${text}</h${level}>`;
    } else {
        return `<h${level}>${text}</h${level}>`;
    }
};
handleHighlight(item) {
    let jump = document.querySelectorAll('.jump');
    jump[item].scrollIntoView({
        block: 'start',
        behavior: "smooth"
    });
}
複製代碼

主內容滾動,目錄高亮

在閱讀md內容時,隨着滾動條的變化,目錄的高亮項也隨着變化,這裏有注意:

  • 在mounted的鉤子函數中增長scroll的監聽事件
  • 因爲scroll是全局性的,因此,離開當前組件以前,應該移除此監聽事件
mounted() {
    this.$nextTick(function() {
        window.addEventListener('scroll', this.onScroll);
    });
},
beforeDestroy() {
    window.removeEventListener('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;
                //  處理三種平滑滾動處理方式時的偏移量不一樣
                let height = this.mode === 'raw_js_demo' ? 75 : (this.mode === 'css_demo' ? 10 : 0);
                if (top > _itemTop - height) {
                    currentId = i;
                } else {
                    break;
                }
        }
        if (currentId) {
            //  這裏的currentOId是字符串,必須轉換成數字,不然高亮項的全等沒法匹配
            this.highlightIndex = parseInt(currentId);
        }
    }
}
複製代碼

Summary

以上就是如何讓md在網頁上良好展現的所有功能,谷歌了屢次,感謝分享經驗的艾瑞巴dei,開源萬歲~!

待改進的點有:

  • 如何避免非標題的#的正則匹配,好比// #這不是標題 格式的內容
  • md中h1和h2標籤的處理,md中是容許html的,如今不能知足匹配h一、h2的標籤

以爲有用的話請點贊~你的贊就是我更新最大的動力~!

相關文章
相關標籤/搜索