📊基于Vue对Echarts5进行基础封装-按需引入

写在开头

Echarts 一款强大的可视化库,当前已经迭代到 5.5.x 版本了,应用十分广泛。相信各位也都熟悉哈,俺就不过多介绍了。😋

这次要分享的是在Vue2项目中关于Echarts5的一些基本封装情况,请诸君按需食用。

前置准备

首先,咱先来创建一个 Vue2 项目:

js 复制代码
vue create your-project-name

(在创建过程中,你可以根据提示选择合适的预设。)

再把 Echats 给整进来:

js 复制代码
npm install echarts

启动项目:

js 复制代码
npm run serve

基本使用

先来整一个基础的案例看看 Echarts 下载是否成功,在 App.vue 文件:

js 复制代码
<template>
  <div id="app">
    <div id="chart" style="width: 600px; height: 400px"></div>
  </div>
</template>

<script>
import * as echarts from "echarts/core";
// 按需引入!!!
import { PieChart } from "echarts/charts";
import { TitleComponent } from "echarts/components";
import { CanvasRenderer } from "echarts/renderers";

// 注册
echarts.use([
    PieChart,
    CanvasRenderer,
    TitleComponent,
]);

export default {
    name: "App",
    mounted() {
        const chartDom = document.getElementById("chart");
        const myChart = echarts.init(chartDom);
        const options = {
            title: {
                text: "饼图示例",
            },
            series: [
                {
                    type: "pie",
                    radius: "50%",
                    data: [
                        { value: 1048, name: "语文" },
                        { value: 735, name: "数学" },
                        { value: 580, name: "英语" },
                        { value: 484, name: "物理" },
                        { value: 300, name: "化学" },
                    ],
                },
            ],
        };
        myChart.setOption(options);
    },
};
</script>

如果能正常看饼图的渲染,则说明成功下载引入 Echarts 了。

这里需要特别注意的一点是,咱们使用 Echarts 的按需引入形式,需要从对应的包导出特定功能进行注册后才能使用,如果你使用中有发现异常,请注意查看控制台报错信息。👀

按需引入更多细节情况:传送门

基础封装

观察上述的基础使用,每次咱们创建一个图表时,基本都要引入相关的 Echarts 包并进行注册,接着获取 DOM,初始化 Echarts,这一系列操作颇为繁琐。而且,还要留意 Echarts 实例的相关情况,如实例不被 Vue 依赖收集,实例使用后需要进行销毁操作,整个过程反复且麻烦。还有如果同一个页面要创建多个图表,就更麻烦了,不仅要处理每个图表各自的实例问题,还要保证它们之间互不干扰。

Em......😗讲那么多,就是需要咱们来对 Echarts 进行一个基础封装,以便更好的使用。咱们在使用过程中其实最主要关注 options 参数就行,当 options 参数发生变化时,图表也自动随之改变,其他的操作基本都一样,全给它们整合封装在组件内去统一处理。

得,直接贴代码瞧瞧吧。👉👉👉

先创建 components/Echarts/install.js 文件:

js 复制代码
/**
 * @file 该文件用于按需引入 ECharts 的图表与组件
 * @author xxx
 * @recentUpdate 2024-11-05 15:00
 * @notice Echarts图表与组件可在其源码下的 echarts/lib/export 找到
 */

import * as echarts from 'echarts/core';
import { PieChart } from 'echarts/charts';
import { TitleComponent } from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
    // 图表
    PieChart,
    // 渲染器
    CanvasRenderer,
    // 组件
    TitleComponent,
]);

export default function installer(dom) {
    return dom ? echarts.init(dom) : echarts;
}

这个文件用于统一控制 Echarts 的图表和组件的引入与注册。

再创建 components/Echarts/ChartRender.vue 文件:

js 复制代码
<template>
    <div ref="container" class="container" />
</template>

<script>
import installer from './install.js';

export default {
    props: {
        options: {
            type: Object,
            required: true
        }
    },
    data() {
        return {
            // 避免依赖收集
            $chart: null,
            timer: null
        };
    },
    watch: {
        options: {
            handler() {
                if (!this.options) return;
                this.renderChart();
            },
            immediate: true,
            deep: true
        }
    },
    methods: {
        /** @description 初始化或渲染图表 **/
        renderChart() {
            if (!this.$chart) {
                this.initChart();
            } else {
                this.setOptions();
            }
        },
        /** @description 初始化图表 **/
        initChart() {
            this.$nextTick(() => {
                if (this.$chart) this.$chart.dispose();
                this.$chart = installer(this.$refs.container);
                /* 事件注册 - start */
                // 点击事件,可以节流代理一下点击事件
                this.$chart.on(
                    'click',
                    params => this.$emit('chartClick', params)
                );
                // 双击事件
                this.$chart.on('dblclick', params => this.$emit('chartDblClick', params));
                // 渲染完成事件
                this.$chart.on('finished', () => this.$emit('chartFinished', this.$chart));
                /* 事件注册 - end */
                // 渲染图表
                this.setOptions();
            });
        },
        /** @description 渲染图表,进行防抖处理,防止短时间内options多次修改,造成重复渲染性能的浪费 **/
        debouncedSetOptions() {
            if (!this.$chart) return;
            this.$chart.clear();
            this.$chart.setOption(this.options, { notMerge: true });
        },
        setOptions() {
            // 不可直接使用lodash的防抖,会造成初始渲染多组件的场景中,防抖函数内部的timer互相污染的问题
            this.debounce(this.debouncedSetOptions, 200)();
        },
        /** @description 防抖函数 **/
        debounce(func, delay) {
            return (...args) => {
                if (this.timer) {
                    clearTimeout(this.timer);
                }
                this.timer = setTimeout(() => {
                    func.apply(this, args);
                    this.timer = null;
                }, delay);
            };
        }
    },
    beforeDestroy() {
        this.timer = null;
        if (!this.$chart) return;
        this.$chart.off('click');
        this.$chart.off('dblclick');
        this.$chart.off('finished');
        this.$chart.dispose();
        this.$chart = null;
    }
};
</script>

<style scoped>
.container {
    width: 100%;
    height: 100%;
    flex: 1;
}
</style>

App.vue 文件具体使用情况:

js 复制代码
<template>
  <div id="app">
    <div style="width: 600px; height: 400px">
        <ChartRender :options="options" />
    </div>
  </div>
</template>

<script>
import ChartRender from './components/Echarts/ChartRender.vue';

export default {
    name: "App",
    components: {
        ChartRender
    },
    data() {
        return {
            options: {
                title: {
                    text: "饼图示例",
                },
                series: [
                    {
                        type: "pie",
                        radius: "50%",
                        data: [
                            { value: 1048, name: "语文" },
                            { value: 735, name: "数学" },
                            { value: 580, name: "英语" },
                            { value: 484, name: "物理" },
                            { value: 300, name: "化学" },
                        ],
                    },
                ],
            },
        };
    },
};
</script>

这样子就简单多了😀,如果需要多个图表,直接复制并提供多个 options 参数就能轻松完成。而且你更改去 options 参数,图表也会自动进行重新渲染,并且组件内部还进行了防抖处理,短时间内多次更改 options 参数仅会进行一次渲染。

可以如下简单测试是否有问题:

js 复制代码
handleClick() {
    this.options.title.text = '标题修改';
    setTimeout(() => {
        this.options.title.text = '标题修改2';
    }, 100);
}

注意:🔉

在上述组件中使用了防抖函数(debounce),小编将函数中的 timer 变量提升到 data 里。这样做主要是为了避免在多个图表组件同时进行实例化 时,出现只有最后一个图表正常渲染的问题,因为这里我们需要保证每个图表组件都有独立的防抖处理,互相的 timer 不能影响。
$chart:通过以 $ 符号开头,能避免属性被 Vue 进行依赖收集,具体可以查看小编的另一篇文章。传送门

自适应尺寸

涉及到图表的需求,很多时候咱们都需要考虑尺寸自适应的问题;Echarts 也提供 echartsInstance.resize() 方法给我们处理尺寸变化的情况,但具体使用过程还需我们结合一些 JS 的 API 来监听处理,挺麻烦的。😑

小编这里选择使用 resize-detector 包来处理,它能帮助咱们跨浏览器、基于事件的形式检测元素大小的变化。

先安装它:

js 复制代码
npm install resize-detector

具体在 ChartRender.vue 文件中使用:

js 复制代码
<template>
    <div ref="container" class="container" />
</template>

<script>
import installer from './install.js';
import { addListener, removeListener } from 'resize-detector';

export default {
    // ...
    methods: {
        initChart() {
            this.$nextTick(() => {
                if (this.$chart) this.$chart.dispose();
                this.$chart = installer(this.$refs.container);
                // 尺寸自适应
                addListener(this.$refs.container, this.resizeHandler);
                // ...
            });
        },
        /** @description 图表自适应尺寸,一样进行防抖处理 **/
        resizeHandler() {
            this.debounce(this.debounceResizeHandler, 200)();
        },
        debounceResizeHandler() {
            this.$emit('chartResize');
            this.$chart.resize();
        },
        // ...
    },
    beforeDestroy() {
        this.timer = null;
        if (!this.$chart) return;
        // 移除
        removeListener(this.$refs.container, this.resizeHandler);
        // ...
    }
};
</script>

效果:

导出图片

最后,咱们也可以将 Echarts 一些特有的功能做一层简单封装,方便后续的使用,如导出图表的图片。

js 复制代码
/** @description 导出图片 **/
downloadPicture(name) {
    if (!this.$chart) return;
    const base64 = this.$chart.getDataURL({
        type: 'png',
        pixelRatio: 1.5,
        backgroundColor: '#fff'
    });
    const fileName = name || new Date().getTime();
    const link = document.createElement('a');
    link.download = fileName;
    link.style.display = 'none';
    link.href = base64;
    document.body.appendChild(link);
    link.click();
    URL.revokeObjectURL(link.href);
    document.body.removeChild(link);
}

具体使用:

js 复制代码
<template>
    <div id="app">
        <div style="width: 100%; height: 400px;border: 1px solid red;">
            <ChartRender ref="chartRender" :options="options" />
        </div>
        <button @click="handleClick">下载图片</button>
    </div>
</template>

<script>
import ChartRender from './components/Echarts/ChartRender.vue';

export default {
    // ...
    methods: {
        handleClick() {
            this.$refs.chartRender.downloadPicture('这是图片名称');
        }
    }
};
</script>

这样子就很方便啦,更多其他功能就期待你自己去探究咯。😋

完整源码

ChartRender.vue 文件:

js 复制代码
<template>
    <div ref="container" class="container" />
</template>

<script>
import installer from './install.js';
import { addListener, removeListener } from 'resize-detector';
import { throttle, cloneDeep } from 'lodash';

export default {
    props: {
        options: {
            type: Object,
            required: true
        }
    },
    data() {
        return {
            $chart: null,
            timer: null
        };
    },
    watch: {
        options: {
            handler() {
                if (!this.options) return;
                this.renderChart();
            },
            immediate: true,
            deep: true
        }
    },
    methods: {
        renderChart() {
            if (!this.$chart) {
                this.initChart();
            } else {
                this.setOptions();
            }
        },
        initChart() {
            this.$nextTick(() => {
                if (this.$chart) this.$chart.dispose();
                this.$chart = installer(this.$refs.container);
                addListener(this.$refs.container, this.resizeHandler);
                this.$chart.on(
                    'click',
                    throttle(params => this.$emit('chartClick', params), 350, {
                        leading: true,
                        trailing: false
                    })
                );
                this.$chart.on('dblclick', params => this.$emit('chartDblClick', params));
                this.$chart.on('finished', () => this.$emit('chartFinished', this.$chart));
                this.setOptions();
            });
        },
        debouncedSetOptions() {
            if (!this.$chart) return;
            const deepCloneOptions = cloneDeep(this.options);
            this.$chart.clear();
            this.$chart.setOption(deepCloneOptions, { notMerge: true });
        },
        setOptions() {
            this.debounce(this.debouncedSetOptions, 200)();
        },
        resizeHandler() {
            this.debounce(this.debounceResizeHandler, 200)();
        },
        debounceResizeHandler() {
            this.$emit('chartResize');
            this.$chart.resize();
        },
        /** @description 提供给外部手动调用渲染时使用 **/
        setOptionsOutside(options) {
            const deepCloneOptions = cloneDeep(options);
            this.$chart.clear();
            this.$chart.setOption(deepCloneOptions, { notMerge: true });
        },
        /** @description 清空当前实例,会移除实例中所有的组件和图表 **/
        clearChart() {
            this.$chart && this.$chart.clear();
        },
        downloadPicture(name) {
            if (!this.$chart) return;
            const base64 = this.$chart.getDataURL({
                type: 'png',
                pixelRatio: 1.5,
                backgroundColor: '#fff'
            });
            const fileName = name || new Date().getTime();
            const link = document.createElement('a');
            link.download = fileName;
            link.style.display = 'none';
            link.href = base64;
            document.body.appendChild(link);
            link.click();
            URL.revokeObjectURL(link.href);
            document.body.removeChild(link);
        },
        debounce(func, delay) {
            return (...args) => {
                if (this.timer) {
                    clearTimeout(this.timer);
                }
                this.timer = setTimeout(() => {
                    func.apply(this, args);
                    this.timer = null;
                }, delay);
            };
        }
    },
    beforeDestroy() {
        this.timer = null;
        if (!this.$chart) return;
        removeListener(this.$refs.container, this.resizeHandler);
        this.$chart.off('click');
        this.$chart.off('dblclick');
        this.$chart.off('finished');
        this.$chart.dispose();
        this.$chart = null;
    }
};
</script>

<style scoped>
.container {
    width: 100%;
    height: 100%;
    flex: 1;
}
</style>

install.js 文件:

js 复制代码
/**
 * @file 该文件用于按需引入 ECharts 的图表与组件
 * @author xxx
 * @recentUpdate 2024-11-05 15:00
 * @notice Echarts图表与组件可在其源码下的 echarts/lib/export 找到
 */

import * as echarts from 'echarts/core';
import {
    PieChart,
    LineChart,
    BarChart,
    // ScatterChart,
    // RadarChart,
    // MapChart,
    // FunnelChart,
    // SunburstChart,
    // CustomChart
} from 'echarts/charts';
import {
    TitleComponent,
    TooltipComponent,
    LegendComponent,
    GridComponent
    // DataZoomComponent,
    // DataZoomInsideComponent,
    // DataZoomSliderComponent,
    // VisualMapComponent,
    // PolarComponent,
    // MarkLineComponent,
} from 'echarts/components';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
    // 图表
    PieChart,
    LineChart,
    BarChart,
    // ScatterChart,
    // RadarChart,
    // MapChart,
    // FunnelChart,
    // SunburstChart,
    // CustomChart,
    // 渲染器
    CanvasRenderer,
    // 组件
    TitleComponent,
    TooltipComponent,
    LegendComponent,
    GridComponent
    // DataZoomComponent,
    // DataZoomInsideComponent,
    // DataZoomSliderComponent,
    // VisualMapComponent,
    // PolarComponent,
    // MarkLineComponent,
]);

export default function installer(dom) {
    return dom ? echarts.init(dom) : echarts;
}

至此,本篇文章就写完啦,撒花撒花。

相关推荐
cwj&xyp13 分钟前
Python(二)str、list、tuple、dict、set
前端·python·算法
dlnu201525062215 分钟前
ssr实现方案
前端·javascript·ssr
古木201920 分钟前
前端面试宝典
前端·面试·职场和发展
轻口味2 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王2 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发3 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀3 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪3 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef5 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端