文档是如何工作的 – vue-markdown-loader
当初写 Mint UI 时就遇到了要用 Vue 写文档的问题:如何才能在写 Markdown 时也能写 Vue 组件的 Demo。虽然后来并没有在 Mint UI 的文档里写 Demo。最开始在 Element 的内部版本里,找遍了各种 Vue 的 Markdown 相关插件,要么是在 template 里定义 Markdown 格式,要么就是有一个 Markdown 的组件。都不能做到纯粹的写 Markdown 文件,并且还能写 Demo。
后来想到或许可以尝试把 Markdown 文件转成 Vue 组件。毕竟可以在 Markdown 里写 HTML,那么完全可以作为 Vue 的模板。后来就有了 vue-markdown-loader,一个把 Markdown 转成 Vue 组件的 webpack loader,搭配 vue-router 就能搭建一个可以在 Markdown 里写 Vue 代码的文档网站。
引用自—https://segmentfault.com/a/1190000007026819
在写之前,我们先整理下需求,只有理解了需求才可以生产出更好的代码!这也是本文中最重要的部分.
我们的最终目的是做一个这样的教程网站. http://element.eleme.io/#/zh-CN/component/alert
为了提升开发效率,我们需要将markdown格式的文件可以通过 import md from ‘path/xx.md’的导入形式加载到相对应组件中,这时markdown文件就是一个组件.
那么我们第一个需求就是拦截import,并且解析markdowm语法!
到这里时,看似没有啥毛病.但是别忘了我们要做的是教程网站. 光有代码可不行,我们还需要有效果呀!
所以我们第二个需求就是在析markdown中也可以写Vue的组件!
解决了这个问题后,我们只需要设计好网页的模板,并通过路由调用不同的md文件,这样就可以很方便的完成一套教程网站了
方便起见,我们就不再自己搭建环境了,直接进入主题.
vue init webpack markdown
cd markdown
npm install
markdown-it 渲染 markdown 基本语法
markdown-it-anchor 为各级标题添加锚点
markdown-it-container 用于创建自定义的块级容器
vue-markdown-loader 核心loader
transliteration 中文转拼音
cheerio 服务器版jQuery
highlight.js 代码块高亮实现
striptags 利用cheerio实现两个方法,strip是去除标签以及内容,fetch是获取第一符合规则的标签的内容
先在build
目录下新建一个strip-tags.js
文件.
// strip-tags.js
'use strict';
var cheerio = require('cheerio'); // 服务器版的jQuery
/**
* 在生成组件效果展示时,解析出的VUE组件有些是带''
* @return {[String]} e.g ''
*/
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 转换后会将代码放入中
};
/**
* 获取标签中的文本内容
* @param {[String]} str e.g 'header
'
* @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();
};
工具类写完后,我们就开始配置webpack吧,打开webpack.base.conf.js
// webpack.base.conf.js
const md = require('markdown-it')(); // 引入markdown-it
const slugify = require('transliteration').slugify; // 引入transliteration中的slugify方法
const striptags = require('./strip-tags'); // 引入刚刚的工具类
/**
* 由于cheerio在转换汉字时会出现转为Unicode的情况,所以我们编写convert方法来保证最终转码正确
* @param {[String]} str e.g 成功
* @return {[String]} e.g 成功
*/
function convert(str) {
str = str.replace(/()(\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 '
' | '
'
* @return {[type]} e.g '
'
*/
function wrap(render) {
return function() {
return render.apply(this, arguments)
.replace(''''
, ''
);
};
}
在module .rules中添加一个新的loader,
{
test: /\.md$/,
loader: 'vue-markdown-loader',
options: {
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 = 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 `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']
],
// 定义处理规则
preprocess: function(MarkdownIt, source) {
// 对于markdown中的table,
MarkdownIt.renderer.rules.table_open = function() {
return '<table class="table">';
};
// 对于代码块去除v-pre,添加高亮样式
MarkdownIt.renderer.rules.fence = wrap(MarkdownIt.renderer.rules.fence);
return source;
}
}
},
根目录下新建一个info.md文件用以测试
//info.md
### 基本用法
页面中的非浮层元素,不会自动消失。
:::demo Alert 组件提供四种主题,由`type`属性指定,默认值为`info`。
```html
<template>
<el-alert
title="成功提示的文案"
type="success">
el-alert>
<el-alert
title="消息提示的文案"
type="info">
el-alert>
<el-alert
title="警告提示的文案"
type="warning">
el-alert>
<el-alert
title="错误提示的文案"
type="error">
el-alert>
template>
```
:::
在components中的HelloWorld.vue中引入md文件
// HelloWorld.vue
<template>
<div class="hello">
<info>info>
div>
template>
<script type="text/babel">
import info from '../../info.md'; // 导入md文件
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
},
components:{
info // 注册组件
}
}
script>
<style>
@import 'highlight.js/styles/color-brewer.css'; //导入高亮样式
.hello {
margin: 20px auto;
width: 50%;
}
a {
color: #409EFF;
text-decoration: none;
}
code {
background-color: #f9fafc;
padding: 0 4px;
border: 1px solid #eaeefb;
border-radius: 4px;
}
.hljs {
line-height: 1.8;
font-family: Menlo, Monaco, Consolas, Courier, monospace;
font-size: 12px;
padding: 18px 24px;
background-color: #fafafa;
border: solid 1px #eaeefb;
margin-bottom: 25px;
border-radius: 4px;
-webkit-font-smoothing: auto;
}
style>
配置到到这里,md文件其实已经可以显示在页面上了。但是离在md文件中调用Vue组件还需要编写一个demo-block组件,由于篇幅原因,关于demo-block组件我们在下篇文章再做介绍.
不得不说,一个好的开发架构可以为开发省去非常多时间了.文章中vue-markdown-loader的使用固然重要,但是更重要的是架构的设计思想!
感谢您的阅读!
感谢element团队的贡献!
跳转在这里Element源码系列——搭建开发环境