markdown线上部署方案

孙星鹏
2023-12-01

背景

在很多的后台管理系统或者门户系统中,经常会涉及到文档的展示,比如说vue的官方网站就存在大量的文档展示。这类文档一般是由PM写好的markdown文档,而前端开发人员就需要快速地将这些md文档部署到我们项目的页面上去。这就涉及到如何将md文档部署到网页上去,展示出和md编辑器一样的效果。

markdown展示

遇到上述需求,我们脑子里的第一想法是看是否由相关的插件,答案是肯定的。网络上有很多解析markdown文档的插件,比如说vue-markdown,marked.js,showdown,mavonEditor……我在项目中选用了vue-markdown,下面说说如何使用。

  1. 要实现markdown,首先我们得安装text-loader解析导入的md文档
npm install text-loader --save-dev 
// or
yarn add text-loader
  1. 进行webpack module配置
module:{
    rules:[
    {
       test: /.md$/,
       use: 'text-loader',        
    }
  ]
}
  1. 安装md解析器vue-markdown
npm install vue-markdown --save-dev
// or
yarn add vue-markdown
  1. 在Vue文件里面引入md文件
<template>
  <div>
    <h1>
      Ninecat-ui
    </h1>
    <vue-markdown>
      {{ about }}
    </vue-markdown>
  </div>
</template>

<script>
import VueMarkdown from 'vue-markdown'
import about from '../../markdown/about.md'
export default {
  name: 'About',
  components: {
    VueMarkdown
  },
  data () {
    return {
      about: about
    }
  }
}
</script>

完成上面几步,我们就可以实现最核心的md展示功能了。

关于vue-markdown更多的用法,可参考官方文档vue-markdown

md文件中添加资源文件

在写md文档时,因为文档最后要部署到网页上。所以在写图片或者视频的路径时要采用线上的地址。

如果项目的静态资源就在项目的域名下,可采用以下写法:
显示图片

<img src="/static/images/5-vue/test.jpg" width="300" />

显示视频

<video src="/static/images/5-vue/20190509_191927.mp4" controls width="300" ></video>

显示能下载文件

源代码: <a href="/static/images/5-vue/test.rar" >下载</a> html文件: <a href="/static/images/5-vue/test.html" download>下载</a> js文件: <a href="/static/images/5-vue/test.js" >下载</a>

markdown样式

到上面这一步,我们仅仅是把使用md语法写的文档在网页上使用dom展示出来,现在的样式还是标签的基本样式,所以整体页面还比较丑,接下来我们美化一下。

关于md文档的css文件网上比较多,比如说vue官方文档的样式,Typora的各种主题样式,可以自己选择一个下载下来引入到项目当中。下载下来的文件也可以根据自己的需求进行修改。
Typora主题样式
有哪些Markdown的CSS样式表推荐?
简单又好看,你的 Markdown 文稿也能加上个性化主题

至此,我们完成了页面的美化。

文档目录

通常我们在网页上查看文档时都会有相应的文档目录,阅读时滑动文档,当前文档位置对应的目录样式也会跟着改变,点击目录跳转到文档相应的位置。

vue-markdown有自动生成目录的配置选项,我在项目中选择了自己来生成文档目录。

首先编写一个生成目录的组件DocMenu.vue:

<template>
    <ul class="menu" ref="docMenu">        
        <li v-for="(item, index) in menuItems" :key="index">
            <a :href="`#${item.id}`" :class="item.level" class="menu-item">{{item.title}}</a>
        </li>        
    </ul>
</template>

<script>
export default {
    props: {
        menuItems: Array
    }
}
</script>

<style lang="less" scoped>
    .menu {
            width: 14%;
            max-height: 85%;
            overflow-y: auto;
            overflow-x: hidden;
            padding: 20px;
            background-color: #ffffff;
            position: fixed;
            top: 70px;
            right: 2%;
            border-radius: 3px;
            box-shadow: 1px 1px 5px rgb(114, 111, 111);


            li {
                height: 20px;
                line-height: 20px;
                margin: 13px 0 13px 0;
                

                a {
                    text-decoration: none;
                    color: #303133;
                    font-weight: 700;
                    display: block;
                    width: 100%;
                    height: 100%;
                    text-overflow: ellipsis;
                    white-space: nowrap;
                    overflow: hidden;
                    
                    &:hover {
                        color: #409eff;
                    }
 
                }

                .depth1 {
                    font-size: 16px;
                }

                .depth2 {
                    color: #666666;
                    font-size: 14px;
                    padding-left: 15px;
                }

                .depth3 {
                    color: #666666;
                    font-size: 12px;
                    padding-left: 30px;
                }

                .depth4 {
                    color: #666666;
                    font-size: 10px;
                    padding-left: 40px;
                }

                .current {
                    color: #409eff;
                }
            }
        }
</style>

DocMenu.vue组件采用了锚点来进行文档的跳转。

接下来就是根据文档的dom结构来生成DocMenu.vue需要的数据。整体的过程如下:

  1. 在页面渲染好之后开始获取需要作为目录的那些h1-h6的dom结构。获取到的这些dom有2个用处:(1)收集他们的top,用于滑动时计算当前的目录;(2)根据dom的信息生成需要传递给DocMenu.vue的数据。
  2. 根据上面的数据生成需要传递给DocsMenu组件的数据。
  3. 收集上述dom的top值
  4. 收集需要改变样式的DocsMenu的a标签
  5. 为VueMarkdown组件的父元素添加srcoll事件
  6. 在文档容器组件的mounted中为window添加resize事件,resize之后重新获取dom的top值

我将在这一过程中需要用到的函数封装在了一个mark2htmlUtil.js文件当中。

// mark2htmlUtil.js
function getMenuDoms(vm, refName, baseRoute) { // 获取作为菜单目录显示的dom
    var nodeList = vm.$refs[refName].$el.children
    var titleTags = ['H1', 'H2', 'H3', 'H4'] // 这里根据自己需要显示的目录层级来决定
    nodeList = Array.from(nodeList) // 将伪数组转化为数组
    nodeList = nodeList.filter(node => titleTags.indexOf(node.nodeName.toUpperCase()) !== -1) // 找出所有h1-h4标签
    nodeList.forEach(node => { // 为每个菜单项加上id
        node.id = baseRoute +'#'+node.innerHTML
    })
    vm.menuDoms = nodeList            
}

function getMenuItems(vm) { // 收集需要传递给DocsMenu组件的数据
    
    vm.menuItems = vm.menuDoms.map(item => ({ 
        id: item.id,
        title: item.id.split('#')[1],
        level: 'depth' + item.nodeName[1]
    }))
}

function getMenuAnchors(vm, refName) { // 收集需要改变样式的DocsMenu的a标签
    var menuAnchors = vm.$refs[refName].getElementsByClassName('menu-item')
    vm.menuAnchors = Array.from(menuAnchors) // 将伪数组转化为数组
}

function getTops(vm) { // 收集菜单目录元素的offsetTop
    vm.tops = []
    vm.menuDoms.forEach(node => { 
        vm.tops.push(node.offsetTop)
    })
}

function changeDocMenuCurrentStyle(vm, refName) { // 滑动过程中改变当前目录的样式
    var tops = vm.tops
    var currentTop = vm.$refs[refName].scrollTop
    var menus = vm.menuAnchors // 目录中的选项
    menus.forEach(element => {
        element.classList.remove('current')
    })

    var currentIndex = tops.findIndex((item, index) => {
        var condition1 = currentTop >= tops[index] && currentTop < tops[index+1]
        var condition2 = currentTop >= tops[index] && index + 1 == tops.length // 最后的目录
        return condition1 || condition2
    } )
    if(currentIndex !== -1) {
        menus[currentIndex].classList.add('current')
    }
}



export default {
    getMenuDoms, 
    getMenuItems, 
    getMenuAnchors, 
    getTops, 
    changeDocMenuCurrentStyle,
}

显示md文档页面的组件:

<template>
    <div class="docs">
        <div class="doc-container" ref="doc" @scroll="scrollHandler">
            <VueMarkdown  
            class="content markdown" 
            ref="docContent" 
            :source="dataOnShow"></VueMarkdown>
            <DocsMenu :menuItems="menuItems" />
        </div>
    </div>
</template>

<script>
import VueMarkdown from 'vue-markdown'
import DocsMenu from '@/components/doc/DocsMenu'
import mark2htmlUtil from '@/utils/mark2htmlUtil'
import DPMenu from '@/components/common/DPMenu'
export default {
    name: 'Doc',
    data() {
        return {
            dataOnShow: '',
            menuDoms: [], // 存放会作为菜单目录的dom
            menuItems: [], // 存放需要传递给DocsMenu组件的数据
            menuAnchors: [], // 存放目录中的a元素,用于滑动时改变当前目录样式
            tops: [], // 存放菜单目录元素的offsetTop
        }
    },
    methods: {
        scrollHandler() {
            mark2htmlUtil.changeDocMenuCurrentStyle(this, 'doc')
        },
        generateDocMenu() {
            this.$refs.doc.scrollTop = 0 // 将滑动过的页面重新置顶
            this.$nextTick(() => {
                mark2htmlUtil.getMenuDoms(this, 'docContent', this.$route.path)
                mark2htmlUtil.getMenuItems(this)
                
                var timer = setTimeout(() => { 
                    // 由于图片是异步获取的,所以需等到图片渲染在页面上再收集tops
                    mark2htmlUtil.getTops(this)
                    // 菜单dom依赖于menuItems获取之后再生成,延迟收集dom
                    mark2htmlUtil.getMenuAnchors(this, 'doc')
                    clearTimeout(timer)
                }, 1000)
                
            })
        },
    },
    components: {
        VueMarkdown,
        DocsMenu,
        DPMenu
    },
    created() {
    },
    mounted() {
        this.generateDocMenu()
        window.addEventListener('resize', () => { // 窗口大小改变后重新收集offsetTop
            mark2htmlUtil.getTops(this)
        })
    }
}
</script>

上述过程中对于滑动改变当前目录样式的处理函数可以优化,采用index===currentIndex的方式。

md文件存放的位置

通常而言,这些md文档改动的频率是比较高的,文档的数量也是比较多的。如果我们将这些文档存放在前端项目的静态资源中,那每次文档的改动都需要将项目重新打包发布,非常麻烦。在我的项目采用的方案是将文档、图片等资源上传到服务器,然后需要展示文档时根据mediaId去获取,后端直接返回读取文档的字符串,然后再传递给插件渲染,生成目录等。这样就实现了用户完全自己去掌控md文档的变动,不再需要前端打包发布。

总结

以上就是一个简单的md文档部署方案,可改进的地方还有很多,这里仅给遇到相似需求的朋友提供一个思路。如有错误,还请指正。

参考文章

 类似资料: