vue组件库用markdown生成文档

前言:

开发vue组件库需要提供组件的使用文档,最好是有渲染到浏览器的demo实例,既能操作又能查看源代码。markdown作为常用的文档编写载体,如果能在里面直接写vue组件,同时编写使用说明就再好不过。流行的组件库element-ui的文档就是用markdown写出来的,看了看其处理md的程序后,自己也决定写一个类似的处理程序,研究一下其中的细节。

技术点

1.markdown-it

处理markdown最常用的工具是markdown-it,它能把我们写的markdown文件转换为html。类似于babel,markdown也有自己的插件系统,通过设置或者编写自定义插件改变渲染的路径。

2.webpack-loader

处理md文件可以使用自定义webpack-loader来处理,先把md内容转为合适html,然后再给vue-loader处理。

3.cheerio

使用markdown-it把md内容转为html之后,需要操作html,cherrio以类似jquery的方式操作html,简单方便。

4.hljs

代码需要高亮渲染,hijs的功能就是将代码处理成html,通过样式使其高亮显示出来。

步骤

1.配置webpack解析md

{
    test: /\.md$/,
    use:[
        {loader: 'vue-loader'},
        { loader: path.resolve(__dirname,'./markdown-loader/index.js') }
    ]
},

2.markdown-loader的入口

module.exports = function (source) {
	this.cacheable && this.cacheable();
	const {resourcePath=''}  = this
	const fileName = path.basename(resourcePath,'.md')
	// @符号在markdown中是特殊符号
	source = source.replace(/@/g, '__at__');

	var content = parser.render(source).replace(/__at__/g, '@');

	var result = renderMd(content,fileName)

	return result
};

3.添加插件markdown-it-container

markdown-it-container是一个插件,使用这个插件之后就可以在markdown中添加自己的标识,然后就能自定义处理标识里面的内容。在这里可以在把代码块放到标识内部,主要是防止markdown-it把vue组件转成html,由自己处理这些代码,最终返回想要的内容。

::: demo
​```html
<i class="kv-icon-close fs-24"></i>
<i class="kv-icon-link fs-24"></i>
​```
:::

上面就是插件的用法,demo由自己定义,初始注入的代码如下:

parser.use(require('markdown-it-container'), 'demo', {
	validate(params) {
		return params.trim().match(/^demo\s*(.*)$/);
	},
	// 把demo代码放到div.kv-demo里面
	render(tokens, idx) {
		const m = tokens[idx].info.trim().match(/^demo\s*(.*)$/);
		if (tokens[idx].nesting === 1) {
			const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
			// 先把demo中的代码放到demo-block的之中,然后程序继续render fence,按照上面的fence规则渲染出代码部分,作为隐藏的查看代码。
			return `<demo-block><div  class="kv-demo">${content}</div>`;
		}
		return '</demo-block>';
	}
})

render方法仿照的是npm包里的例子。其中的tokens是AST节点,可以从这个网址看到markdown-it解析的AST,对照着做判断。

根据自己的理解,因为html是有起始标签和结束标签,markdown-it的render也是成对的,也就是在标记的起始和结束都会调用render方法,所以在demo起始的时候返回了一个起始<demo-block> (这是个全局定义的vue组件),然后把代码放到内部;

markdown会继续处理demo标识内部“` 标识,这个标识在markdown-it中有自己的rules (rules.fence)来处理;然而我们的目标是把这个代码放到一个标签中渲染成html,然后作为查看源码的部分展示出来,这就需要自定义了:

// 先保存下来
const defaultRender = parser.renderer.rules.fence;
parser.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">${hljs.highlight('html',token.content.replace(/^(\s*)|(\s*)$/g,'')).value}</code></pre>
				</template>`;
	}

	return `<div class="code-common">${defaultRender(tokens, idx, options, env, self)}</div>` 
};

需要注意的是如果初始化parser的时候如果配置了 highlight: renderHighlight,调用defaultRender的时候会自动处理成高亮;否则需要就需要自己处理了,就是上面的hljs.highlight('html',token.....

做完以上部分之后,md的内容会被渲染成代码片断,内部包含普通的html标签和vue组件标签,大概如下:

<div>一些文字</div>
<demo-block>
	 <div  class="kv-demo">
			<ul class="icon-list">
  			<li v-for="name in icons" :key="name">
    			<span>
      		<i :class=" iconPre+ name"></i>
      		{{'kv-' + name}}
    			</span>
  			</li>
			</ul>
			<script>
  			export default {
   				 data() {
      				return {
        	 			icons: require('../icon.json'),
        				iconPre:'kv-icon-'
      				};
    			}
 				}
			</script>

			<style lang="scss">
  		.demo-icon {
      	.....
       }
       </style>
	  </div>
	  <template slot="highlight">
	  	......
	  </template>
</demo-block>

组装成vue模板

这个代码和vue的组件的代码不一致,是无法解析的,需要修正一下。

另外一篇文档中会有多个demo即多个export default,解决方案就是把各个demo提取成组件,注册当前文档这个vue组件中,把demo的部分替换组件的名字。

第一部分:组装当前文档为vue组件 ,同时挂载提取出来demo组件//github.com/blank-x/kv/blob/master/build/markdown-loader/index.js#L15

var renderMd = function (html,fileName) {
	......
}

第二部分:提取其中的demo为组件,//github.com/blank-x/kv/blob/master/build/markdown-loader/index.js#L57)

var renderVueTemplate = function (content) {
	......
}

结果类似于如下:

<template>
  <div class="demo-">
    <demo-block>
      <template slot="source">
        <kv-demo0></kv-demo0>
      </template>
      <template slot="highlight">
        <pre v-pre><code class="html">......</code></pre>
      </template>
    </demo-block>
   .......
    <demo-block>
      <template slot="source">
        <kv-demo1></kv-demo1>
      </template>
      <template slot="highlight">
        <pre v-pre><code class="html"><span class="hljs-tag">.......</code></pre>
      </template>
    </demo-block>
  </div>
</template> 
<script>
export default {
  name: "component-doc0",
  components: {
    "kv-demo0": {
      template: `<div class="kv-demo0"><kv-tag>标签一</kv-tag></div>`
    },
    "kv-demo1": {
      template: `<div class="kv-demo1">
      							<kv-tag :key="tag.name" v-for="tag in dynamicTags" 
      									closable :disable-transitions="false" @close="handleClose(tag)" :type="tag.color">
                    {{tag.name}}
                    </kv-tag>
                </div>`,
      data() {
        return {
          dynamicTags: [{
              name: "标签一",
              color: "primary"
          }]
        };
      },
      methods: {
        handleClose(tag) {
          this.dynamicTags.splice(this.dynamicTags.indexOf(tag), 1);
        }
      }
    }
  }
};
</script> 
<style lang="scss"  >
  .kv-tag {
    margin-right: 8px;
  }
</style>

组件kv-demo0 和kv-demo1 在components中定义;

在demo内部的scss会被提出来,放到了外层vue组件中,如果需要修改样式,可以参考如下写法:

.demo-tag .kv-demo1{
	//
}
.demo-tag .kv-demo0{
	//
}
tag  // md的名字
demo0  // 页面内第几个demo

未解决的问题

每一个demo中script标签和export之间的代码被丢弃。如果需要引入其他文件,可以在data中通过require引入;

最后

本代码仅为练手使用,未在实际开发中使用,如有不正之处望指正。

Tags: