Open lalalazero opened 4 years ago
首先,对于使用 webpack 的项目,需要对 .md 结尾的 markdown 文件配 loader,elementui 是这么配置的 /build/webpack.demo.js
{
test: /\.md$/,
use: [
{
loader: 'vue-loader',
options: {
compilerOptions: {
preserveWhitespace: false
}
}
},
{
loader: path.resolve(__dirname, './md-loader/index.js')
}
]
},
所以可以找到它手写的 markdown 文件的 loader,第一次看的时候我并看不懂,所以我尝试自己抄他的然后写一个 loader。
因为我的项目是基于 vue-cli 工具的,所以我的配置不是在 webpack.config.js 里面,但是也差不多。我找了下 vue-cli 的文档,大概是下面这样配置的:
// vue.config.js
module.exports = {
// ... 省略其他
chainWebpack: config => {
config.module
.rule('markdown')
.test(/\.md$/)
.use('vue-loader')
.loader('vue-loader').tap(options => {
return Object.assign({}, options, {
compilerOptions: {
preserveWhitespace: false
}
})
})
.end()
config.module
.rule('markdown')
.test(/\.md$/).use(require.resolve('./md-loader.js'))
.loader(require.resolve('./md-loader.js'))
.end()
}
}
配好了之后(这里有个小知识点:require.resovle 和 require 的区别),在 md-loader.js 里面写这样的内容:
module.exports = function(){
return `<template><div>hello world</div></template>` // 这就是一个最简单的 .vue 组件
}
然后 App.vue 里面随便写一下
<template>
<div id='app'>
<button-example />
</div>
</template>
<script>
export default {
components: {
'button-example': () => import("../docs/button.md") // 随便一个 .md 文件
}
}
</script>
运行之后,页面效果是这样的 因为在我们配置的 md-loader.js 里面,写死了返回一个 .vue 组件,内容就是 hello world。 到这一步,说明我们的 .md 文件的 loader 配置成功了。下一步就是把 markdown 文件的内容实际的渲染出来。
一个具体的介绍 button 组件的 markdown 文件的内容大概如下:
# 一级标题:button 组件 ## 二级标题: 基础用法 ::: demo 组件代码示例 ```html <z-view-button>按钮文字</z-view-button> ``` :::
注意里面花里胡哨的符号
# ## ::: ```
elementui 用到了 markdown-it
markdown-it-container
markdown-it-container
这几个库。他们的基础用法就是把 # 一级标题
渲染成 <h1>一级标题</h1>
根据官网例子和 elementui
代码,以下的符号都有特殊含义
::: 符号定义了一个 container,``` 定义一个 code fence (代码片段)
这里 elementui
重写了 markdown-it-container
的 container 和 fence 规则,把 :::demo 这样的一个块变成了一个
并且对于里面的 code fence 变成了这样返回
因此仿照 elementui ,我是这样写 md-loader.js 的。
function setCustomContainer(md) {
md.use(mdContainer, 'demo', {
validate(params) {
return params.trim().match(/^demo\s+(.*)$/);
},
render(tokens, idx) {
const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
if (tokens[idx].nesting === 1) {
const description = m && m.length > 1 ? m[1] : '';
let str1 = description ? `<template slot="description">${md.render(description)}</template>` : `<template slot="description">description</template>`
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : 'content';
return `<demo-block>
${str1}
<!-- zViewDemo ${content} zViewDemo -->
`;
}
return '</demo-block>';
}
})
}
上面这个方法会把以下的 markdown 内容转成 <demo-block />
组件输出
:::demo 组件代码示例 ```html <z-view-button>button</z-view-button> ``` :::
经过转换之后输出是这样
<demo-block>
<template slot='description>组件代码示例</template>
<!-- zViewDemo <z-view-button>button</z-view-button> zViewDemo -->
</demo-block>
至于中间为什么要标记 <!-- zViewDemo 下面会讲到
function overWriteFenceRule(md) {
const defaultRender = md.renderer.rules.fence;
md.renderer.rules.fence = (tokens, idx, options, env, self) => {
const token = tokens[idx];
// 判断该 fence 是否在 :::demo 内
const prevToken = tokens[idx - 1];
const isInDemoContainer = prevToken && prevToken.nesting === 1 && prevToken.info.trim().match(/^demo\s*(.*)$/);
if (token.info === 'html' && isInDemoContainer) {
return `<template slot="highlight"><pre v-pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
}
return defaultRender(tokens, idx, options, env, self);
};
}
前面我们说 fence 就是一个 container 里面的代码块,也就是
:::demo ```html 这里是 fence ```
通过重写 fence 方法,我们对返回的内容用一个 <template slot='highlight'><pre v-pre><code></code>这里是转义后的 fence 内容</pre></temolate>
包裹起来
这里有两个作用:
<pre><code>
标签是为了 highlight.js
插件做代码高亮我们编写组件示例无非就是两部分:
背景
轮子的示例一般要展示组件自身和相应的代码高亮。比如像这样: 在我自己写轮子官网的过程中,也遇到了这样的需求。以下记录了自己学习的过程。
思路1:手动转义 < > 符号
手写的意思就是,button 组件写一个 button-example.vue,里面会用到我写的 z-view-button.vue 组件,还会有对应的示例。遇到的第一个问题是示例代码如果不转义 < > 符号,就会被 vue 编译,视为 z-view-button 组件。
我也尝试过用 v-pre 也并没有用
只有明确的转义才会成功。
很明显,思路1这样的办法很挫。不可能每个示例代码里面的 < > 我都去转义,而且还要再另外做代码高亮。
思路2: hightlight.js 自动转义 < >和代码高亮
把示例代码写成一个字符串,然后在 button-example.vue 组件 mounted 之后,首先用 highlight.js 把字符串的 < > 转义,然后再语法高亮。代码类似:
实际渲染
这样勉强可以达到想要的效果,但是感觉也很蠢。于是我去看了 elementui 的做法
思路3:elementui 的做法—markdown文件转vue组件
elementui 的做法是组件的示例用法都用 markdown 来写,解析这个 markdown 文件输出是一个 vue 组件,里面包含轮子和相应的代码示例。这个过程就在 md-loader/index.js 文件中。