参考vue-markdown-loader
参考Element源码系列——Vue加载Markdown格式组件上篇
(如有侵权,请联系本人,会尽快修改)
愿景:开发一套内部ui组件库,适用于B、S
不废话,直接开始
1.项目是基于ts、vue2.x
2.安装相关依赖
markdown-it 渲染 markdown 基本语法
markdown-it-anchor 为各级标题添加锚点
markdown-it-container 用于创建自定义的块级容器
vue-markdown-loader 核心loader
transliteration 中文转拼音
cheerio 服务器版jQuery
highlight.js 代码块高亮实现
3.工具类-strip-tags.ts
'use strict';
var cheerio = require('cheerio'); // 服务器版的jQuery
/**
* 在生成组件效果展示时,解析出的VUE组件有些是带<script>和<style>的,我们需要先将其剔除,之后使用
* @param {[String]} str 需要剔除的标签名 e.g'script'或['script','style']
* @param {[Array|String]} tags e.g '<template></template><script></script>''
* @return {[String]} e.g '<html><head><template></template></head><body></body></html>'
*/
exports.strip = function(str, tags) {
var $ = cheerio.load(str, {decodeEntities: false});
if (!tags || tags.length === 0) {
return str;
}
tags = !Array.isArray(tags) ? [tags] : tags;
var len = tags.length;
while (len--) {
$(tags[len]).remove();
}
return $.html(); // cheerio 转换后会将代码放入<head>中
};
/**
* 获取标签中的文本内容
* @param {[String]} str e.g '<html><body><h1>header</h1></body><script></script></html>'
* @param {[String]} tag e.g 'h1'
* @return {[String]} e.g 'header'
*/
exports.fetch = function(str, tag) {
var $ = cheerio.load(str, {decodeEntities: false});
if (!tag) return str;
return $(tag).html();
};
/**
* 由于cheerio在转换汉字时会出现转为Unicode的情况,所以我们编写convert方法来保证最终转码正确
* @param {[String]} str e.g 成功
* @return {[String]} e.g 成功
*/
exports.convert = (str) => {
str = str.replace(/(&#x)(\w{4});/gi, function($0) {
return String.fromCharCode(parseInt(encodeURIComponent($0).replace(/(%26%23x)(\w{4})(%3B)/g, '$2'), 16));
});
return str;
}
/**
* 由于v-pre会导致在加载时直接按内容生成页面.但是我们想要的是直接展示组件效果,通过正则进行替换
* hljs是highlight.js中的高亮样式类名
* @param {[type]} render e.g '<code v-pre class="test"></code>' | '<code></code>'
* @return {[type]} e.g '<code class="hljs test></code>' | '<code class="hljs></code>'
*/
exports.wrap = (render) => {
return function() {
return render.apply(this, arguments)
.replace('<code v-pre class="', '<code class="hljs ')
.replace('<code>', '<code class="hljs">');
};
}
4.vue.config.js 配置
const hljs = require('highlight.js');
const slugify = require('transliteration').slugify
const striptags = require('./md-loader/strip-tags.ts'); // 引入工具类
const md = require('markdown-it')();
const vueMarkdown = {
raw: true,
// 定义处理规则
preprocess: function(MarkdownIt, source) {
// 对于markdown中的table,
MarkdownIt.renderer.rules.table_open = function() {
return '<table class="table">';
};
// 对于代码块去除v-pre,添加高亮样式
MarkdownIt.renderer.rules.fence = striptags.wrap(MarkdownIt.renderer.rules.fence);
// ```code`` 给这种样式加个class code_inline
const code_inline = MarkdownIt.renderer.rules.code_inline
MarkdownIt.renderer.rules.code_inline = function(...args){
args[0][args[1]].attrJoin('class', 'code_inline')
return code_inline(...args)
}
return source;
},
use: [
[require('markdown-it-anchor'), {
level: 2, // 添加超链接锚点的最小标题级别, 如: #标题 不会添加锚点
slugify: slugify, // 自定义slugify, 我们使用的是将中文转为汉语拼音,最终生成为标题id属性
permalink: true, // 开启标题锚点功能
permalinkBefore: true // 在标题前创建锚点
}],
// 'markdown-it-container'的作用是自定义代码块
[require('markdown-it-container'), 'demo', {
// 当我们写::: demo :::这样的语法时才会进入自定义渲染方法
validate: function(params) {
return params.trim().match(/^demo\s*(.*)$/);
},
// 自定义渲染方法,这里为核心代码
render: function(tokens, idx) {
var m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
// nesting === 1表示标签开始
if (tokens[idx].nesting === 1) {
// 获取正则捕获组中的描述内容,即::: demo xxx中的xxx
var description = (m && m.length > 1) ? m[1] : '';
// 获得内容
var content = tokens[idx + 1].content;
// 解析过滤解码生成html字符串
var html = striptags.convert(striptags.strip(content, ['script', 'style'])).replace(/(<[^>]*)=""(?=.*>)/g, '$1');
// 获取script中的内容
var script = striptags.fetch(content, 'script');
// 获取style中的内容
var style = striptags.fetch(content, 'style');
// 组合成prop参数,准备传入组件
var jsfiddle = { html: html, script: script, style: style };
// 是否有描述需要渲染
var descriptionHTML = description
? md.render(description)
: '';
// 将jsfiddle对象转换为字符串,并将特殊字符转为转义序列
jsfiddle = md.utils.escapeHtml(JSON.stringify(jsfiddle));
// 起始标签,写入demo-block模板开头,并传入参数
return `<demo-block class="demo-box" :jsfiddle="${jsfiddle}">
<div class="source" slot="source">${html}</div>
${descriptionHTML}
<div class="highlight" slot="highlight">`;
}
// 否则闭合标签
return '</div></demo-block>\n';
}
}],
[require('markdown-it-container'), 'tip'],
[require('markdown-it-container'), 'warning']
]
}
module.exports = {
chainWebpack: config => {
config.module.rule('md')
.test(/\.md$/)
.use('vue-loader')
.loader('vue-loader')
.options({
compilerOptions: {
preserveWhitespace: false,
},
})
.end()
.use('vue-markdown-loader')
.loader('vue-markdown-loader/lib/markdown-compiler')
.options(vueMarkdown)
}
}
5.使用到的demo-block.vue
<template>
<div class="docs-demo-wrapper">
<div :style="{height: isExpand ? 'auto' : '0'}" class="demo-container">
<div span="14">
<div class="docs-demo docs-demo--expand">
<div class="highlight-wrapper">
<slot name="highlight"></slot>
</div>
</div>
</div>
</div>
<span class="docs-trans docs-demo__triangle" @click="toggle">{{isExpand ? '隐藏代码' : '显示代码'}}</span>
</div>
</template>
<script lang='ts'>
import {Component, Vue} from 'vue-property-decorator'
@Component({
})
export default class extends Vue {
private isExpand: boolean = false
private toggle() {
this.isExpand = !this.isExpand;
}
}
</script>
<style lang="less" type="text/less">
.demo-container {
transition: max-height .3s ease;
overflow: hidden;
}
.docs-demo {
width: 100%;
height: auto;
box-sizing: border-box;
font-size: 14px;
background-color: #F7F7F7;
border: 1px solid #e2ecf4;
border-top: none;
pre code {
font-family: Consolas,Menlo,Courier,monospace;
line-height: 22px;
border: none;
}
}
.docs-trans {
width: 100%;
text-align: center;
display: inline-block;
color: #C5D9E8;
font-size: 12px;
padding: 10px 0;
background-color: #FAFBFC;
}
.docs-demo__code,
.highlight-wrapper,
.docs-demo__meta {
padding: 0 20px;
overflow-y: auto;
}
.docs-demo__code {
border-bottom: 1px solid #eee;
}
.docs-demo.docs-demo--expand .docs-demo__meta {
border-bottom: 1px dashed #e9e9e9;
}
.docs-demo.docs-demo--expand .docs-demo__triangle {
transform: rotate(180deg);
}
.highlight-wrapper {
display: none;
p,
pre {
margin: 0;
}
.hljs {
padding: 0;
}
}
.docs-demo.docs-demo--expand .highlight-wrapper {
display: block;
}
.docs-demo__code__mobi {
height: 620px;
margin: 20px 0;
}
.docs-demo__code__mobi__header {
border-radius: 4px 4px 0 0;
background: -webkit-linear-gradient(rgba(55,55,55,.98),#545456);
background: linear-gradient(rgba(55,55,55,.98),#545456);
text-align: center;
padding: 8px;
img {
width: 100%;
}
.url-box {
height: 28px;
line-height: 28px;
color: #fff;
padding: 0 3px;
background-color: #a2a2a2;
margin: 10px auto 0;
border-radius: 4px;
white-space: nowrap;
overflow-x: auto;
}
}
.docs-demo__code__mobi__content {
iframe {
width: 100%;
border: 0;
height: 548px;
}
}
</style>
6.main.ts
import demoBlock from './components/demo-block.vue'
Vue.component('demo-block', demoBlock);
7.编写组件button.md
8.展示如图