自定義 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. 總結

到這里基本完工了,很多的知識點都是現學現賣的,如果哪里講的不對希望大家指出,哪里講得不好希望大家多多包涵。

在這里需要感謝 方應杭 方方老師提供的思路。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容