1. 需求背景

组件库要被人家使用,所以一定需要有使用demo和API解析。这两个工作是可以合并在同一份markdown文件里面的,所以我们需要提取markdown里面的代码片段,然后渲染成demo就可以,这块我们手写一个loader来处理,具体的逻辑在build/markdown-loader.js里面。

1.1. 添加loader

我们要转换.md文件为HTML标签,同时提取里面的代码块,然后对代码块做一些自定义处理,最后返回vue单文件组件模板,传递给vue-loader去解析。多loader是串联链式调用的,按逆序执行,所以我们在vue.config.js里面添加下面对.md的处理👇

config.module
  .rule('md')
  .test(/\.md/)
  .use('vue-loader')
  .loader('vue-loader')
  .options({
    compilerOptions: {
      preserveWhitespace: false
    }
  })
  .end()
  .use('markdown-loader')
  .loader(
    require('path').resolve(__dirname, './build/markdown-loader.js')
  )
  .end();

1.2. loader逻辑

markdown-it这个库可以把Markdown文档转换成HTML标签,配合markdown-it-container这个插件可以提取特定标签内内容。在steam-game-ui里面我们定义:::demo 代码块 :::里面的代码块需要被渲染成Vue组件。

const md = require('markdown-it')();
const VueTemplateComplier = require('vue-template-compiler');
const { parse, compileTemplate } = require('@vue/component-compiler-utils');
let componentCodeList = [];

md.use(require('markdown-it-container'), 'demo', {
  // 验证代码块为【:::demo :::】才进行渲染
  validate: function(params) {
    return params.trim().match(/^demo\s+(.*)$/);
  },
  // 代码块渲染
  render: function (tokens, idx) {
    const token = tokens[index];
      const tokenInfo = token.info.trim().match(/^demo\s*(.*)$/);
      if (token.nesting === 1) { // 开始标签
        // 获取demo的第一行代码描述,即::: demo xxx 中的xxx
        const desc = tokenInfo && tokenInfo.length > 1 ? tokenInfo[1] : '';
        // 获取demo中需要渲染的内容
        const nextIndex = tokens[index + 1];
        const content = nextIndex.type === 'fence' ? nextIndex.content : '';
        // 将content解析为vue组件基本属性对象
        let { template, script, styles } = parse({
          source: content,
          compiler: VueTemplateComplier,
          needMap: false
        });
        // 将content的内容分别提炼出来并转码,给codepen作为提交表单使用
        let rawCodepen = {
          html: (template && template.content || '').replace(/^(\/|\n)*/g, ''),
          js: (script && script.content || '').replace(/^(\/|\n)*/g, ''),
          css: (styles && styles.content || '').replace(/^(\/|\n)*/g, '')
        };
        let codepen = markdownIt.utils.escapeHtml(JSON.stringify(rawCodepen));
        // 将template转为render函数
        const { code } = compileTemplate({
          source: template.content,
          compiler: VueTemplateComplier
        });
        styleCodeList = styleCodeList.concat(styles);
        // 获取script的代码
        script = script ? script.content : '';
        if (script) {
          script = script.replace(
            /export\s+default/,
            'const exportJavaScript ='
          );
        } else {
          script = 'const exportJavaScript = {} ;';
        }
        // 将代码解析成vue组件存储,然后在渲染html中引用该组件
        const name = `st-demo-${componentCodeList.length}`;
        componentCodeList.push(`"${name}":(function () {
          ${code}
          ${script}
           return {
             ...exportJavaScript,
             render,
             staticRenderFns
          }
        })()`);
        // 将需要渲染的示例用code-block组件包裹替换插槽显示示例效果
        return `<code-block :codepen="${codepen}">
                  <div slot="desc">${markdownIt.render(desc)}</div>
                  <template slot="demo"><${name} /></template>
                  <div slot="code">`;
      }
      return `    </div>
                      </code-block> `;
    }
  }
});

核心逻辑如上,获取到每一块:::demo 代码块 :::里面的代码块,然后分别获取到代码块里面的HTML模板、脚本和样式,他们有两个作用,一是传给我们的自定义组件code-block,在里面我会对代码块做一些样式和功能性的封装;二是组合成codepen的提交参数,codepen是一个在线代码编辑平台,它可以通过表单提交代码然后跳转演示,同样的平台还有jsfiddlejsbin等。这里面我使用vue-template-compiler去提取模板变量,但是你也可以用别的方式去提取,比如cheerio👇

const cheerio = require('cheerio');
function fetch (str, tag) {
  var $ = cheerio.load(str, { decodeEntities: false });
  if (!tag) return str;
  return $(tag).html();
}
// ...
let codepen = {
    html: fetch(content, 'template'),
    js: fetch(content, 'script'),
    css: fetch(content, 'style')
};

因为一份文档里面有多个demo,所以我们需要一个componentCodeList去存储每个代码块,然后最终遍历插入,最终生成下面这个模板并返回

`<template>
  <div class="st-doc">
    ${markdownIt.render(source)}
  </div>
</template>
<script>
export default {
  name: 'st-doc',
  components: {
    ${componentCodeList.join(',')}
  }
}
</script>
<style>
  ${Array.from(styleCodeList, m => m.content).join('\n')}
</style>`

除了处理demo内的代码块,我们还可以对markdown-it生成的HTML标签做一些处理,它本身提供一些rule允许你对它进行一些操作,比如给table标签替换一个类名👇

markdownIt.renderer.rules.table_open = function() {
  return '<table class="st-doc__table">';
};

同时你也可以通过fence自己定义某些标签的渲染规则,比如如果在模板中用到插值{{ xx }},因为我们把整个文本返回给vue-loader处理,如果不对双大括号做处理的话,在显示纯文本的代码的时候,双大括号也会变解析成模板,在父组件内就会报错找不到该变量,所以需要写一个函数替换双大括号👇

markdownIt.renderer.rules.fence = genInlineLabel(markdownIt.renderer.rules.fence);
function genInlineLabel (render) {
  return function() {
      return render.apply(this, arguments)
      .replace('{{', '{ { ')
      .replace('}}', ' } }');
  };
}

1.3. CodeBlock组件

从上面看到我们把template,js,css提取出来之后都注入到了CodeBlock这个组件当中,这个组件主要就是做一些样式上的处理同时添加跳转codepen的方法👇

goCodepen() {
    // https://blog.codepen.io/documentation/api/prefill
    const { js, html, css } = this.codepen;
    const resourcesTpl =
        '<scr' +
        'ipt src="//unpkg.com/vue/dist/vue.js"></scr' +
        'ipt>' +
        '\n<scr' +
        `ipt src="//unpkg.com/steam-game-ui@${SteamUI.version}/lib/steam-game-ui.umd.min.js"></scr` +
        'ipt>';
    let jsTpl = (js || '').replace(/export default/, 'var Main =').trim();
    let htmlTpl = `${resourcesTpl}\n<div id="app">\n${html.trim()}\n</div>`;
    jsTpl = jsTpl
        ? jsTpl + "\nvar Ctor = Vue.extend(Main)\nnew Ctor().$mount('#app')"
        : "new Vue().$mount('#app')";
    let cssTpl = `html { font-size: 100px; } #app{ font-size: 14px; }\n${css ||
        ''}`;
    const data = { js: jsTpl, css: cssTpl, html: htmlTpl };
    const form =
        document.getElementById('fiddle-form') ||
        document.createElement('form');
    while (form.firstChild) {
        form.removeChild(form.firstChild);
    }
    form.method = 'POST';
    form.action = 'https://codepen.io/pen/define/';
    form.target = '_blank';
    form.style.display = 'none';

    const input = document.createElement('input');
    input.setAttribute('name', 'data');
    input.setAttribute('type', 'hidden');
    input.setAttribute('value', JSON.stringify(data));
    form.appendChild(input);
    document.body.appendChild(form);
    form.submit();
}

这里要注意的是,我们需要给codepen写一段注入的代码,去引用我们当前已发布的最新版本的代码包,因为组件库托管在npm上,所以我这里引用的是unpkg.com上的代码包,只要你的包在npm上发布了,都会被同步到unpkg.com中。最后我们在main.js中全局注册codeBlock组件Vue.component('code-block', codeBlock),这样子就可以在所有地方引用该组件了

1.4. 总结

整个流程是两部分:第一部分把Markdown文件转成HTML标签,第二部分提取:::demo 代码块 :::代码块里面的内容传给全局的codeBlock组件,然后把一二部分合成一个组件模板,传给vue-loader去渲染,在官网中我们只需要在路由里面引用Markdown文件就可以了。具体的代码逻辑在build/markdown-loader.js

powered by Gitbook该文件修订时间: 2019-09-12 00:10:40

results matching ""

    No results matching ""