光明山

vuePress-theme-reco 山妖    2016 - 2020
光明山

Choose mode

  • dark
  • auto
  • light
首页
Category
  • 例子
  • 移动端
  • nginx
  • 框架
  • 提示
  • vue
  • webpack
  • 小程序
Tag
TimeLine
External
author-avatar

山妖

15

Article

16

Tag

首页
Category
  • 例子
  • 移动端
  • nginx
  • 框架
  • 提示
  • vue
  • webpack
  • 小程序
Tag
TimeLine
External

使用vuepress创建一个仿element-ui文档的说明文档

vuePress-theme-reco 山妖    2016 - 2020

使用vuepress创建一个仿element-ui文档的说明文档


山妖 2020-05-08 vuepressmarkdown

# 前要

在项目中,组件我们通常要写很多,但每个组件怎么用,一般不会去类似element-ui的说明文档。所以新人接手时,要不就是直接忽略掉了通用组件,要不就是要看半天才知道一个复杂的组件怎么用。

在19年上半年,我写了一个小组件库,目的是为了统一公司多个工程的通用组件以及样式,并且给出说明文档。 组件地址 但这个小组件库的说明文档有点问题,同一个页面,只能允许一个vue。

最近整理自己的东西时,发现以前写的东西都有一些零碎,也没有一个统一的地方存放,所以采用vuepress写了一个博客,并且在这里实现了类似element-ui可以看到代码效果的说明文档。

# 内容

# 看一下效果

Hello Word

已经全局引入了element-ui


<template>
  <div class="hello">
    <h3>{{ msg }}</h3>
    <p class='doc'>
      已经全局引入了element-ui
    </p>
    <el-input v-model='txt' />
    <br/>
    <div>{{txt}}</div>
  </div>
</template>
<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Hello Word',
      txt: ''
    }
  },
  methods: {
  }
}
</script>
<style>
h3 {
  margin: 40px 0 0;
}
.doc {
  color: #42b983;
}
</style>

展开

# 原理说明

vuepress是支持在markdown文件里面直接写vue代码的,那么我们要实现代码和代码效果共存时,最简单的办法,把代码写两份,一份放在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 ,这个哥们就是用的这种方法

方法三:

一步到位,在vuepress插件里,先将代码块作为vue组件进行编译,再把编译后的组件插入页面里面。代码看 /docs/.vuepress/plugins/demo里面。

## 主代码

在config.js里面引入插件

[require('./plugins/demo/')]

plugins/demo/index代码如下:

/**
 * 提供 ::: 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代码如下:

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 标签
  // todo: 优化这段逻辑
  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/fence代码如下:

// 覆盖默认的 fence 渲染策略
module.exports = 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><pre><code class="html">${md.utils.escapeHtml(token.content)}</code></pre></template>`;
    }
    return defaultRender(tokens, idx, options, env, self);
  };
};

plugins/demo/util代码如下:

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', // TODO:这里有待调整
    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}
  `;
  // todo: 这里采用了硬编码有待改进
  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
};

plugins/demo/containers代码如下:

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}>`;
      }
    });
  };
}
  • 看一下效果
  • 原理说明