vuepress 编写仿element 说明文档

1,431 阅读4分钟

前言

  在项目中,组件我们通常要写很多,但每个组件怎么用,一般不会去类似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
};

看效果:

  wfwfwf.github.io/vue-blog/di…