前言
在项目中,组件我们通常要写很多,但每个组件怎么用,一般不会去类似element-ui的说明文档。所以新人接手时,要不就是直接忽略掉了通用组件,要不就是要看半天才知道一个复杂的组件怎么用。
在19年上半年,我写了一个小组件库,目的是为了统一公司多个工程的通用组件以及样式,并且给出说明文档。 组件地址 但这个小组件库的说明文档有点问题,同一个页面,只能允许一个vue。
最近整理自己的东西时, 起了完善这块的想法,查看了一下网上现有的解决方案,采用vuepress框架,再结合高人的实现,实现了可以在网页同时展示效果和代码的实例,有兴趣的可以继续看。
原理
vuepress是支持在markdown文件里面直接写vue代码的,那么我们要实现代码和代码效果共存时,最简单的办法,把代码写两份,一份放在vue的v-pre 里展示代码, 一份直接用来展示效果。 人类的进步很大一部分是原因是来源于偷懒,不想一份代码写两遍,copy一下,也让文档不好看。偷懒的方法来了……
现有的方案
方法一:
借助 Vuepress 会自动注册 components 目录下组件的特性,或者通过 enhanceApp.js 钩子自己注册示例代码文件,然后使用 <<< @/filepath 语法将示例代码文件引入 这个方法不好的地方在于组件需要全局注册
<color-picker-basic-demo></color-picker-basic-demo>
## 示例代码如下
```html
<<< @/docs/.vuepress/components/color-picker-basic-demo.vue
方法二:
vuepress 也是有生命周期的,我们可以写一个vuepress插件,在插件里把代码进行拆分组装后,按格式存放在data-里面,然后在vuepress的更新时,使用vue.extend创建实例,并挂载到一个对应的元素上。可以在git 上搜一下vuepress-plugin-demo-block-master ,这个哥们就是用的这种方法
个人不太喜欢这种方法,需要将自已使用bable转换es6的代码,引入比如elementui之类的组件库,也有点麻烦。
方法三:
将打包的时候,就将vue的代码生成好,不使用vuepress的话,就自己写loader,类似我td-addon里面用的方式,但要完善相应的代码。 使用vuepress的代码,vuepress是支持插件扩展的,在它的配置文件 ....../.vuepress/config.js,添加plugins,就可以写自己的插件了,这个插件,就可以针对markdown文件进行预处理。预处理的大概思路是:
1、设定需要预处理的标识,如我用的 ::: demo 用于区分哪些代码是要走预处理的。
2、获取需要代码、效果并存的vue代码
3、将获取到的代码,一部分直接插入到v-pre里面,一部分能过'@vue/component-compiler-utils' 里面的compileTemplate方法进行编译后,再作为组件插入到对应的插槽里。
这个说起来不太复杂,实现还是有点小复杂的,不多说,直接上关健代码
config.js里面的配置
plugins: [
[require('./plugins/demo/')]
],
plugins/demo/index.js
/**
* 提供 ::: demo xxx ::: 语法,用于构建 markdown 中的示例
*/
const path = require('path')
const renderDemoBlock = require('./render')
const demoBlockContainers = require('./containers')
module.exports = (options = {}, ctx) => {
return {
chainMarkdown(config) {
config.plugin('containers')
.use(demoBlockContainers(options))
.end();
},
extendMarkdown: md => {
const id = setInterval(() => {
const render = md.render;
if (typeof render.call(md, '') === 'object') {
md.render = (...args) => {
let result = render.call(md, ...args);
const { template, script, style } = renderDemoBlock(result.html);
result.html = template;
result.dataBlockString = `${script}\n${style}\n${result.dataBlockString}`;
return result;
}
clearInterval(id);
}
}, 10);
}
}
}
plugins/demo/render.js
const {
stripScript,
stripStyle,
stripTemplate,
genInlineComponentText
} = require('./util.js');
module.exports = function (content) {
if (!content) {
return content
}
const startTag = '<!--pre-render-demo:';
const startTagLen = startTag.length;
const endTag = ':pre-render-demo-->';
const endTagLen = endTag.length;
let componenetsString = ''; // 组件引用代码
let templateArr = []; // 模板输出内容
let styleArr = []; // 样式输出内容
let id = 0; // demo 的 id
let start = 0; // 字符串开始位置
let commentStart = content.indexOf(startTag);
let commentEnd = content.indexOf(endTag, commentStart + startTagLen);
while (commentStart !== -1 && commentEnd !== -1) {
templateArr.push(content.slice(start, commentStart));
const commentContent = content.slice(commentStart + startTagLen, commentEnd);
const html = stripTemplate(commentContent);
const script = stripScript(commentContent);
const style = stripStyle(commentContent);
const demoComponentContent = genInlineComponentText(html, script); // 示例组件代码内容
const demoComponentName = `render-demo-${id}`; // 示例代码组件名称
templateArr.push(`<template><${demoComponentName} /></template>`);
styleArr.push(style);
componenetsString += `${JSON.stringify(demoComponentName)}: ${demoComponentContent},`;
// 重新计算下一次的位置
id++;
start = commentEnd + endTagLen;
commentStart = content.indexOf(startTag, start);
commentEnd = content.indexOf(endTag, commentStart + startTagLen);
}
// 仅允许在 demo 不存在时,才可以在 Markdown 中写 script 标签
let pageScript = '';
if (componenetsString) {
pageScript = `<script>
export default {
name: 'component-doc',
components: {
${componenetsString}
}
}
</script>`;
} else if (content.indexOf('<script>') === 0) {
start = content.indexOf('</script>') + '</script>'.length;
pageScript = content.slice(0, start);
}
// 合并 style 内容
let styleString = '';
if(styleArr && styleArr.length > 0) {
styleString = `<style>${styleArr.join('')}</style>`
} else {
styleString = `<style></style>`
}
templateArr.push(content.slice(start));
return {
template: templateArr.join(''),
script: pageScript,
style: styleString
}
};
plugins/demo/containers.js
const mdContainer = require('markdown-it-container');
module.exports = options => {
const {
component = 'demo-block'
} = options;
const componentName = component
.replace(/^\S/, s => s.toLowerCase())
.replace(/([A-Z])/g, "-$1").toLowerCase();
return 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] : '';
const content = tokens[idx + 1].type === 'fence' ? tokens[idx + 1].content : '';
const encodeOptionsStr = encodeURI(JSON.stringify(options));
return `<${componentName} :options="JSON.parse(decodeURI('${encodeOptionsStr}'))">
<template slot="demo"><!--pre-render-demo:${content}:pre-render-demo--></template>
${description ? `<div slot="description">${md.render(description).html}</div>` : ''}
<template slot="source">
`;
}
return `</template></${componentName}>`;
}
});
};
}
plugins/demo/util.js
const { compileTemplate } = require('@vue/component-compiler-utils');
const compiler = require('vue-template-compiler');
function stripScript(content) {
const result = content.match(/<(script)>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
}
function stripStyle(content) {
const result = content.match(/<(style)\s*>([\s\S]+)<\/\1>/);
return result && result[2] ? result[2].trim() : '';
}
// 编写例子时不一定有 template。所以采取的方案是剔除其他的内容
function stripTemplate(content) {
content = content.trim();
if (!content) {
return content;
}
return content.replace(/<(script|style)[\s\S]+<\/\1>/g, '').trim();
}
function pad(source) {
return source
.split(/\r?\n/)
.map(line => ` ${line}`)
.join('\n');
}
function genInlineComponentText(template, script) {
// https://github.com/vuejs/vue-loader/blob/423b8341ab368c2117931e909e2da9af74503635/lib/loaders/templateLoader.js#L46
const finalOptions = {
source: `<div>${template}</div>`,
filename: 'inline-component',
compiler
};
const compiled = compileTemplate(finalOptions);
// tips
if (compiled.tips && compiled.tips.length) {
compiled.tips.forEach(tip => {
console.warn(tip);
});
}
// errors
if (compiled.errors && compiled.errors.length) {
console.error(
`\n Error compiling template:\n${pad(compiled.source)}\n` +
compiled.errors.map(e => ` - ${e}`).join('\n') +
'\n'
);
}
let demoComponentContent = `
${compiled.code}
`;
script = script.trim();
if (script) {
script = script.replace(/export\s+default/, 'const democomponentExport =');
} else {
script = 'const democomponentExport = {}';
}
demoComponentContent = `(function() {
${demoComponentContent}
${script}
return {
render,
staticRenderFns,
...democomponentExport
}
})()`;
return demoComponentContent;
}
module.exports = {
stripScript,
stripStyle,
stripTemplate,
genInlineComponentText
};
看效果: