webpack loader—在md文件中写vue

2,844 阅读5分钟

一、需求

一般在开发公司内部或者自己的组件库的时候,往往需要编写一些demo用来调试组件,

然后在编写文档的时候,我们又需要给对应的组件编写示例代码和demo,就显得很麻烦。

所以我想把两件事一起做了!

二、思路

1、把文档中的示例代码run起来,作为我们的调试代码

2、markdown-it可以把md标记转换为html代码

3、在webpack loader中可以使用markdown-it来转换代码,将整个md文件转换为vue单文本组件

4、在loader中提取vue代码标记为子组件,然后再次请求当前loader返回vue代码用作当前md文件的子组件实际代码

三、编写loader

不得不在这里吐槽一下,webpack编写loader不能热更新,只能修改一下run一下,就跟写html页面一样,改一下刷新一下(如果有好的调试办法,请安利一下)。

1、我们跳过配置阶段,重点就是新增loaderresolveLoader

// webpack.conf.js

{
    test: /\.md$/,
    use: [{
        loader: 'vue-loader'
    },{
        loader: 'markdone-vue-loader'
    }]
}
.
.
.
resolveLoader: {
    alias: {
        'markdone-vue-loader': resolvePath('../lib/index.js')
    }
},

2、在loader中调用markdown-itmarkdown-it-container来转换md标记,然后将将vue代码标记为子组件

md.use(container, 'demo', {
    validate: (params: string): boolean => {
        return containerReg.test(params.trim());
    },
    render: (tokens: any, idx: number): string => {
        // 开标签
        if (tokens[idx].nesting === 1) {
            // 当前md文件中唯一组件名
            const componentName: string = getName();
            // 替换:::demo中vue代码为组件
            return `<div class="${demoWrapperClass}">
                <${componentName}></${componentName}>
            `;
        }else{
            // 闭标签
            return `</div>\n`;
        }
    }
});

/**
 * 原始代码:
 * 
 * ### 一些markdown标记...
 * 
 * :::demo 一些代码简介 
 *  ``` vue
 *      <template>
 *          <div>
 *              vue content
 *          </div>
 *      </template>
 *      <scrip>
 *          export default {
 *              name: 'demo'
 *          };
 *      </scrip>
 *  ```
 * :::
 * 
 * ###一些markdown标记...
 * 
 * 
 * ===> 转换结果
 * 
 * <h3>一些markdown标记...<h3>
 *  <component0></component0>
 *  <component1></component1>
 * <h3>一些markdown标记...<h3>
 */
 

3、然后我们把md转换成的html放在<template></template>内,并返回代script的字符串,把步骤二标记的<component0></component0><component1></component1>等组件局部注册,从而让我们的md变成一个类似vue单文本组件。

// 每转换一次`vue`代码,生成对应的唯一组件名、引入路径等
function getName(){
    // 生成唯一组件名,防止与全局冲突
    const componentName: string = `${uniqComponentName}${componentIndex}`;
    // 生成新的请求参数请求当前md文件对应的vue代码
    const request: string = `${resourcePath}?fence&componentIndex=${componentIndex++}`;
    // 引入组件
    srciptImport += `import ${componentName} from ${loaderUtils.stringifyRequest(loaderContext, request)};`;
    // 局部注册组件
    components.push(`'${componentName}': ${componentName}`);
    return componentName;
}

// markdown将md文件转换为html代码
const code: string = md.render(source);

const ret = `
    <template>
        <div class="demo">
            ${code}
        </div>
    </template>

    <script>
        ${srciptImport}
        export default {
            name: 'ComponentDoc',
            components: {
                ${components.join(',')}
            }
        };
    </script>
`;

/**
 * 最终我们的md文件生成结果:
 * 
 * <template>
 *  <div class="demo">
 *      ${code}
 *  </div>
 * </template>
 * 
 * <script>
 *  import component0 from './当前md文件.md?fence&componentIndex=0'
 *  import component1 from './当前md文件.md?fence&componentIndex=1'
 * 
 *  export default {
 *      name: 'ComponentDoc',
 *      components: {
 *          component0,
 *          component1
 *      }
 *  };
 * </script>
 */

4、步骤三已经生成了我们的期望单文本vue组件了,然后底部js运行的时候import component0 from './当前md文件.md?fence&componentIndex=0',会再次请求这个md文件(我在请求参数中添加了fence参数用于loader区分,componentIndex用于分辨我们到底要返回哪一个vue),这一次我们仅需提取我们实际的vue代码并返回,这个返回就是我们步骤三中注册的子组件代码。

// 请求md文件中vue代码块
if (queryParams.fence) {
    // 请求md文件中的第n个vue代码块,从0开始
    const index: number = typeof queryParams.componentIndex === 'string'
        ? Number(queryParams.componentIndex)
        : 0;
    // 从md文件中匹配特殊标记的vue代码
    const matches: null | RegExpMatchArray = source.match(/:::demo[\s\S]*?:::/ig);
    if (!matches || !matches[index]) {
        console.log(`${colors.warn('[md-vue-loader]')}: 请求${resourcePath}中的第${index}${colors.error(':::demo')}标记块失败。`);
        return '';
    }

    const vueBlocks: null | RegExpMatchArray = matches[index].match(/```([\s\S]*?)(<[\s\S]*)```/i);
    if (vueBlocks && vueBlocks[2]) {
        return vueBlocks[2];
    }

    console.log(`${colors.warn('[md-vue-loader]')}: 请求${resourcePath}中的第${index}${colors.error(':::demo')}标记块中的vue代码块失败。`);
    return '';
}

5、由于步骤三和步骤四实际都是返回的vue代码,所以我们在webpack配置loader的时候,需要先md-vue-loader,再vue-loader,这样就省去了我们再去处理md文件中的scopedbabel的事情。最终,我们的md文件中的vue代码其实和正常写单文本组件毫无差别(就是在markdown文件中写vue代码没有代码提示,有点别扭)。

6、至此我们的md-vue-loader主要功能基本已实现。我们添加一些参数用作自定义配置,一个比较完善的loader就诞生了。

四、实现并上传至npm

按照上面的逻辑,我已实现一个初级版本,完善了一些参数配置,并上传至npm,由于md-vue-loader名称被占用,所以我取名为markdone-vue-loader。仓库代码为github

五、转换效果

## Button 组件

一、`Button` 组件的基本使用

:::demo #### Button 组件基础用法:

``` vue

<template>
    <div class="demo">
        <Button>默认按钮</Button>
        <Button type="primary">主要按钮</Button>
        <Button type="success">主要按钮</Button>
        <Button type="info">主要按钮</Button>
        <Button type="warning">主要按钮</Button>
        <Button type="danger">主要按钮</Button>
    </div>
</template>

<script>
import Button from "./components/Button";

export default {
    name: 'ButtonDemo',
    components: {
        Button
    }
};
</script>

<style scoped >
    .demo{
        padding-bottom: 20px;
        border-bottom: 1px solid #333;
        width: 100%;
        position: relative;
    }
</style>

```
:::

## Button 组件

二、`Button` 组件的基本使用

:::demo #### Button 组件disabled用法:
```vue

<template>
    <div class="demo">
        <Button disabled>默认按钮</Button>
        <Button type="primary" disabled>主要按钮</Button>
        <Button type="success" disabled>主要按钮</Button>
        <Button type="info" disabled>主要按钮</Button>
        <Button type="warning" disabled>主要按钮</Button>
        <Button type="danger" disabled>主要按钮</Button>
    </div>
</template>

<script>
import Button from "./components/Button";

export default {
    name: 'ButtonDemo',
    components: {
        Button
    }
};
</script>

<style scoped >
    .demo{
        padding-bottom: 20px;
        border-bottom: 1px solid #333;
        width: 100%;
        position: relative;
    }
</style>
```
:::

六、写在最后

参考:

1、vue-loader

2、element