自定義 loader 讀取 *.vue 檔案原始碼

譚上彪發表於2020-12-11

相關依賴版本:

  • 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>
    
    
  2. 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. 總結

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

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

相關文章