vue-生成文章目录

正如你现在所看到右侧的文章目录一般;

记录一下怎么生成文章目录、跳转、监听滚动到哪个目录。

思路

大概思路:

在文章页面渲染完毕后,即vue的mounted()事件中,获取文章中标签,并得出其为几级标题和其id;

http://blog.zhanghaoran.ren文章页面渲染顺序如下:

  1. 由服务端返回文章内容(markdown格式);和标签等其它信息
  2. 渲染文章内容到页面
    1. 由marked库将服务端传回的markdown内容解析并生成html格式数据;
    2. 由highlight.js库对渲染在页面上的内容进行代码高亮;
  3. 获取页面中图片、给文章内链接设置在新标签页打开;
  4. 获取当前dom中 文章内容区域所有h标签,生成文章目录树。

获取文章目录

<template>
    <div class="main">
        ...
        <div class="articleContent" v-html="content" ref="content" id="content">
            </div>
        
        <toc :active="active" :headers="toc"></toc>
        ...
    </div>
</template>


<script>
    export default{
        data() {
            return{
                active: 0,
                toc: '',
                content: '文章内容,html格式'
            }
        }
        mounted() {
            this.toc = this.get_toc(this.content);
        },
        methods: {
            get_toc (content) {
                // 获取h1-h4
                let re = /<h([1-4])[^>]+>/g
                let result = []
                while (true) {
                    let match = re.exec(content)
                    if (!match) {
                        break
                    }
                    result.push(match[0])
                }
                result = result.map(header=>{
                    return {
                        class: header.split('h')[1].split(' ')[0],
                        text: header.split('"')[1]
                    }
                })
                // 拿到文章h1-h4目录
                return result
            },
        }
    }
</script>

渲染文章目录

<template>
    <ul id='toc'>
        <li v-for='(header,i) in headers' :style='styleArr[i]' :class='i===active?"active":""'>
            <a @click="jumpTo(header.text)">{{header.text.replace(/-\d/, '')}}</a>
        </li>
    </ul>
</template>

<script>
    export default {
        name: 'toc',
        data () {
            return {
            }
        },
        props: ['headers', 'active'],
        computed: {
            styleArr: function () {
                let headers = this.headers
                return headers.map(header => {
                    return {
                        // padding 层级
                        paddingLeft: `${(header.class - 1) * 10}px`,
                        // 层级 字体大小
                        fontSize: header.class <= 2 ? '16px' : header.class <= 4 ? '15px' : header.class <= 6 ? '15px' : '15px'

                    }
                })
            }
        },
        methods:{
            // 跳转目录
            jumpTo(objId){
                try{
                    document.documentElement.scrollTop = this.getElementPageTop(document.getElementById(objId));
                }catch (e) {
                    console.log(e)
                }
            },
            // 获取点击的目录距离文档顶部位置
            getElementPageTop(element){
                try{
                    var actualTop=element.offsetTop;
                    var parent=element.offsetParent;
                    while(parent!=null){
                        actualTop+=parent.offsetTop+parent.clientTop;
                        parent=parent.offsetParent;
                    }
                    return actualTop - 66;
                }catch (e) {
                    console.log(e)
                }

            },
        }
    }
</script>

<style scoped>
    #toc{
        padding-left:10px;
        transition:all .3s;
        width:95%;
        color:rgb(97,97,97);
        list-style-type:none;
        background-color:#fff;
    }
    #toc > li{
        text-align:left;
        cursor:pointer;
        letter-spacing: 1px;
        padding: 0 5px;
    }
    #toc > li > a{
        color:#333;
        width: 100%;
        display: block;
        overflow: hidden;
        text-overflow:ellipsis;
        white-space: nowrap;
    }
    #toc > li > a:hover{
        color: #1a1a1a;
        font-weight: bolder;
    }
    #toc > li.active a{
        color: #1a1a1a;
        font-weight: bolder;
    }
</style>

监听滚动事件,给可视窗中第一个目录添加active样式

<script>
    export default{
        data() {
            return{
                active: 0,
                toc: '',
                content: '文章内容,html格式',
                hs: []
            }
        }
        mounted() {
            this.hs = this.hs.length ? this.hs : [].slice.call(document.getElementById('content').querySelectorAll('h1,h2,h3,h4'))
            window.addEventListener('scroll', this.throttle(this.activeHeaders));
        },
        methods: {
            // 节流, 频繁跳转目录和滚动页面会造成页面卡顿,故设置为4秒执行一次
            throttle (fn, delay = 4000) {
                // 设置变量默认为true
                let flag = true;
                // 为了保证this指向,返回一个箭头函数
                return (...args) => {
                    // 判断如果已经在执行就直接return
                    if (!flag) return;
                    // 否则就是没有执行,将状态赋值为false
                    flag = false;
                    // 设置定时器,设置时间
                    setTimeout(() => {
                        // 调用apply方法确保this指向问题
                        fn.apply(this, args);
                        // 最后将状态重新更改为true,以便程序下次执行
                        flag = true;
                    }, delay)
                };
            },
            activeHeaders () {
                if(document.documentElement.clientWidth>=1000){
                    let hs = this.hs;
                    for (let h of hs) {
                        let scrollTop = document.documentElement.scrollTop + 56;
                        let offsetTop = h.offsetTop;
                        let innerHeight = window.innerHeight;
                        if (offsetTop < scrollTop + innerHeight && offsetTop > scrollTop) {
                            this.active = hs.indexOf(h)
                            break
                        }
                    }
                }
            },
        },
        beforeDestroy() {
            window.removeEventListener('scroll', this.throttle)
        },
    }
</script>

真开心,终于有文章目录了,写了好久….