自定义 loader 读取 *.vue 文件源码

相关依赖版本:

  • node v10.15.0

  • npm v6.4.1

  • yarn v1.22.10

  • vue-cli v4.5.9

  • @vue/compiler v3.0.4

GitHub: vue-source-demo

1. 前言(需求)

就是想读取 *.vue 文件的源码并高亮展示到页面上,又不想用第三方的依赖(其实是找不到)。

2. 实现思路

通过 vue-loader 自定义块 功能,获取目标文件的文件路径,然后通过 fs 读取源码,再用 @vue/compiler-core 的 API baseParse将读取到的内容转换成 AST 语法抽象树,然后将 fs 读取的内容中 抽离出 自定义块内容 和 需要的源码,最后再将以上两个内容重新挂到组件对象上,直接读取组件相应的字段就可以。

完美,关机,下班。

3. 实现

现在思路已经非常的清晰,时候实现它了。

3.1 项目初始化

vue-cli 创建快速模板搭建项目,这里用的是 2版本的 vue,后面再用 vite + vue3 实现一个。

image-20201210225929248

项目跑起来是下面这个样子的,这里大家应该都会的,就不多赘述了。

image-20201210231214294

3.2 自定义块

这里参考 vue-loader 官网的例子,非常的简单。不懂的同学,可以去官网查看。

  1. 创建loader文件 plugins/docs-loader.js
module.exports = function (source, map) {
    this.callback(
        null,
        `export default function (Component) {
            Component.options.__docs = ${
                JSON.stringify(source)
            }
        }`,
        map
    )
}
  1. 创建 vue.config.js 配置规则使用上面定义好的 loader
const docsLoader = require.resolve('./plugins/docs-loader.js')

module.exports = {
    configureWebpack: {
        module: {
            rules: [
                {
                    resourceQuery: /blockType=docs/,
                    loader: docsLoader
                }
            ]
        }
    }
}

注:修改了配置相关文件需要重跑一下项目

  1. 使用

src/components/demo.vue

<docs>
    我是ComponentB docs自定义快 内容
</docs>

<template>
    <div>
        ComponentB 组件
    </div>
</template>

<script>
    export default {
        name: "ComponentB"
    }
</script>

<style scoped>

</style>

src/App.vue

<template>
    <div id="app">
        <demo/>
        <p>{{demoDocs}}</p>
    </div>
</template>

<script>
    import Demo from './components/demo'

    export default {
        name: 'App',
        components: {
            Demo
        },
        data () {
            return {
                demoDocs: Demo.__docs
            }
        }
    }
</script>

效果:

image-20201210232732127

Demo 组件在控制台输出效果会更明显一点:

image-20201210232901114

3.4 获取文件路径并显示内容

在获取文件的路径的时候,瞎泽腾了好久(此处省略好多个字),结果 webpack 的英文官网是有提到。于是就去打印一下 loaderthis ,真的什么都有,早知道早点打印出来看了,害!!! 留下了没技术的眼泪。

image-20201210234040621

现在已经拿到目标文件的完整路径了,开始搞事情!给我们自定义的 loader 稍微加一点细节:

搞事前需要安装一下相关依赖:

yarn add -D @vue/compiler-core
const fs = require('fs');
const {baseParse} = require('@vue/compiler-core');

module.exports = function (source, map) {
    // 1. 获取带有 <docs /> 标签的文件完整路径
    const {resourcePath} = this
    // 2. 读取文件内容
    const file = fs.readFileSync(resourcePath).toString()
    // 3. 通过 baseParse 将字符串模板转换成 AST 抽象语法树
    const parsed = baseParse(file).children.find(n => n.tag === 'docs')
    // 4. 标题
    const title = parsed.children[0].content
    // 5. 将 <docs></docs> 标签和内容抽离
    const main = file.split(parsed.loc.source).join('').trim()
    // 6. 回到并添加到 组件对象上面
    this.callback(
        null,
        `export default function (Component) {
          Component.options.__sourceCode = ${JSON.stringify(main)}
          Component.options.__sourceCodeTitle = ${JSON.stringify(title)}
        }`,
        map
    )
}

完成以上步骤,记得重跑项目。现在我们来看看效果如何:

image-20201210235104113

em... 不错,Demo 组件该有的都有了。再用 pre 标签显示出来看:

image-20201210235401173
<template>
    <div id="app">
        <demo/>
        <p>{{sourceCodeTitle}}</p>
        <pre v-text="sourceCode"></pre>
    </div>
</template>

<script>
    import Demo from './components/demo'

    export default {
        name: 'App',
        components: {
            Demo
        },
        data () {
            return {
                sourceCodeTitle: Demo.__sourceCodeTitle,
                sourceCode: Demo.__sourceCode
            }
        },
        mounted() {
            console.log('Demo', Demo)
        }
    }
</script>

到这里需求好像已经全部实现,很是轻松,作为一个刚毕业五个月的干饭人怎么能止步在这里呢!我决定让这平平无奇的代码高亮起来,让他变得漂漂亮亮的。

3.5 代码高亮

代码高亮用了一个 star 比较高的 highlightjs

安装:

yarn add highlight.js

使用:

src/App.vue

<template>
    <div id="app">
        <demo/>
        <p>{{sourceCodeTitle}}</p>
        <pre>
            <code class="language-html" ref="code" v-text="sourceCode" />
        </pre>
    </div>
</template>

<script>
    import Demo from './components/demo'
    import highlightjs from 'highlight.js'
    import 'highlight.js/styles/vs2015.css'

    export default {
        name: 'App',
        components: {
            Demo
        },
        data () {
            return {
                sourceCodeTitle: Demo.__sourceCodeTitle,
                sourceCode: Demo.__sourceCode
            }
        },
        async mounted() {
            await this.$nextTick()
            this.init()
        },
        methods: {
            init () {
                const codeEl = this.$refs.code
                highlightjs.highlightBlock(codeEl)
            }
        }
    }
</script>

效果:

image-20201211001635863

代码高亮了,是喜欢的颜色。亮是亮起来了,但是写得是一次性代码,不大符合干饭人的要求,是不是可以封装一个公共组件专门来看组件的效果和源码的呢!

3.6 组件封装

封装组件之前需要构思一下这个组件应该长什么样呢?带着样的一个疑问,去浏览了各个优秀轮子的文档页面,画出了下面的设计图:

image-20201211002904439

开始全局组件封装:

  1. src/components/component-source-demo/src/index.vue
<template>
    <div class="component-source-demo">
        <h2 class="component-source-demo__title">{{title || component.__sourceCodeTitle}}</h2>
        <div class="component-source-demo__description">{{description}}</div>
        <div class="component-source-demo__component">
            <component :is="component" :key="component.__sourceCodeTitle"/>
        </div>
        <div class="component-source-demo__action">
            <button type="button" @click="handleCodeVisible('hide')" v-if="codeVisible">隐藏代码 ↑</button>
            <button type="button" @click="handleCodeVisible('show')" v-else>查看代码 ↓</button>
        </div>
        <div class="component-source-demo__code" v-show="codeVisible">
      <pre>
        <code class="html" ref="code" v-text="component.__sourceCode"/>
      </pre>
        </div>
    </div>
</template>

<script>
    import {highlightBlock} from 'highlight.js';
    import 'highlight.js/styles/vs2015.css'

    export default {
        name: "component-source-demo",
        props: {
            title: String,
            description: String,
            component: {
                type: Object,
                required: true
            }
        },
        data() {
            return {
                codeVisible: true
            }
        },
        async mounted() {
            await this.$nextTick()
            this.init()
        },
        methods: {
            init () {
                const codeEl = this.$refs.code
                highlightBlock(codeEl)
            },
            handleCodeVisible(status) {
                this.codeVisible = status === 'show'
            }
        }
    }
</script>

<style scoped>

</style>

  1. src/components/component-source-demo/index.js
import ComponentSourceDemo from './src/index'

ComponentSourceDemo.install = (Vue) => Vue.component(ComponentSourceDemo.name, ComponentSourceDemo)

export default ComponentSourceDemo

使用:

  1. src/mian.js 全局注册组件

    image-20201211004750178
  2. src/App.vue

<template>
    <div id="app">
        <component-source-demo :component="Demo"/>
    </div>
</template>

<script>
    import Demo from './components/demo'

    export default {
        name: 'App',
        data () {
            return {
                Demo
            }
        }
    }
</script>

代码非常的清爽,舒服!!! 效果也非常的棒,甲方很满意。

03
感觉还是有点美中不足,如果有很多个需要展示的组件呢。那岂不是要写很多的重复代码,作为优秀的干饭人是不允许这种情况出现的,代码还需再优化一下。

3.7 代码优化

3.7.1 组件自动引入

src/App.vue

<template>
    <div id="app">
        <component-source-demo
                v-for="item in componentList"
                :key="item.name"
                :component="item"
        />
    </div>
</template>

<script>
    export default {
        name: 'App',
        data () {
            return {
                componentList: []
            }
        },
        mounted() {
            this.autoImportComponents()
        },
        methods: {
            autoImportComponents () {
                const moduleList = require.context('./components/demo', false, /\.vue$/)
                const requireAll = requireContext => requireContext.keys().map(requireContext)
                let targetModuleList = requireAll(moduleList)
                this.componentList = targetModuleList.map(module => {
                    return module.default
                })
            }
        }
    }
</script>
image-20201211012252290
image-20201211012523830

现在只需往 components/demo 添加的新的组件,我们只需刷新一下webpack 就会帮我们自动读取组件了。

4. 总结

到这里基本完工了,很多的知识点都是现学现卖的,如果哪里讲的不对希望大家指出,哪里讲得不好希望大家多多包涵。

在这里需要感谢 方应杭 方方老师提供的思路。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容