在很多的后台管理系统或者门户系统中,经常会涉及到文档的展示,比如说vue的官方网站就存在大量的文档展示。这类文档一般是由PM写好的markdown文档,而前端开发人员就需要快速地将这些md文档部署到我们项目的页面上去。这就涉及到如何将md文档部署到网页上去,展示出和md编辑器一样的效果。
遇到上述需求,我们脑子里的第一想法是看是否由相关的插件,答案是肯定的。网络上有很多解析markdown文档的插件,比如说vue-markdown,marked.js,showdown,mavonEditor……我在项目中选用了vue-markdown,下面说说如何使用。
npm install text-loader --save-dev
// or
yarn add text-loader
module:{
rules:[
{
test: /.md$/,
use: 'text-loader',
}
]
}
npm install vue-markdown --save-dev
// or
yarn add vue-markdown
<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文档时,因为文档最后要部署到网页上。所以在写图片或者视频的路径时要采用线上的地址。
如果项目的静态资源就在项目的域名下,可采用以下写法:
显示图片
<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>
到上面这一步,我们仅仅是把使用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需要的数据。整体的过程如下:
我将在这一过程中需要用到的函数封装在了一个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文档改动的频率是比较高的,文档的数量也是比较多的。如果我们将这些文档存放在前端项目的静态资源中,那每次文档的改动都需要将项目重新打包发布,非常麻烦。在我的项目采用的方案是将文档、图片等资源上传到服务器,然后需要展示文档时根据mediaId去获取,后端直接返回读取文档的字符串,然后再传递给插件渲染,生成目录等。这样就实现了用户完全自己去掌控md文档的变动,不再需要前端打包发布。
以上就是一个简单的md文档部署方案,可改进的地方还有很多,这里仅给遇到相似需求的朋友提供一个思路。如有错误,还请指正。
参考文章