字节笔记本

2026年2月23日

Vue3 + TypeScript + ECharts 移动端封装实践

上个月接触了一下移动端的图表,在学习(Ctrl+C)的过程中,遇到了不少坑,因此我想记录下来,分享一下。

封装 echarts

因为是初衷学习嘛,那肯定是自己造轮子来的香。

模板部分

html
<template>
  <div id="chart" ref="root" :style="{ height: height, width: width }"></div>
</template>

脚本部分

typescript
<script lang="ts">
import {
  defineComponent,
  PropType,
  ref,
  toRefs,
  nextTick,
  onMounted,
  onUnmounted,
  watch,
  shallowRef,
  watchEffect,
} from "vue";

import * as echarts from "echarts/core";
import { EChartsType, EChartsCoreOption } from "echarts/core";
// 引入 SVG 渲染器,注意引入 CanvasRenderer 或者 SVGRenderer 是必须的一步
import { SVGRenderer } from "echarts/renderers";
import { LabelLayout } from "echarts/features";
// 引入柱图表,图表后缀都为 Chart,我这里选择在组件中引入所有的组件,这样就不需要在每个引用的文件里面再次 import
import { PieChart, BarChart, LineChart } from "echarts/charts"; // 系列类型的定义后缀都为 SeriesOption
// 引入直角坐标系,标题,图例,提示框等组件,组件后缀都为 Component
import {
  GridComponent,
  TitleComponent,
  LegendComponent,
  TooltipComponent,
  DatasetComponent,
  DataZoomComponent,
} from "echarts/components";

// 注册必须的组件 经过测试,一个地方注册,全局都能使用
echarts.use([
  BarChart,
  PieChart,
  LineChart,
  LabelLayout,
  SVGRenderer,
  GridComponent,
  TitleComponent,
  LegendComponent,
  TooltipComponent,
  DatasetComponent,
  DataZoomComponent,
]);

export default defineComponent({
  name: "echarts",
  props: {
    width: {
      type: String,
      default: "100%",
      required: false,
    },
    height: {
      type: String,
      default: "250px",
      required: false,
    },
    option: {
      type: Object as PropType<EChartsCoreOption>,
      required: true,
    },
    autoResize: {
      type: Boolean,
      default: true,
      required: false,
    },
    loading: {
      type: Boolean,
      default: false,
      required: false,
    },
  },
  setup(props) {
    const root = ref<any>(null);
    // https://github.com/apache/echarts/issues/14339
    // 否则在动态改变 bar 高度时会报错
    const chart = shallowRef<EChartsType>();

    const { autoResize, loading } = toRefs(props);

    const init = () => {
      if (!root.value) return;
      chart.value = echarts.init(root.value, {}, { renderer: "svg" });

      function commit() {
        if (props.option && chart.value) {
          chart.value.setOption(props.option || {});
        }
      }
      // 兼容 option 默认就有的场景,一般项目场景是异步获取,此时 setOption 通过 watch 执行
      nextTick(() => {
        resize();
        commit();
      });
    };

    // 更新/设置配置
    const setOption = () => {
      nextTick(() => {
        if (!chart.value) {
          init();
          if (!chart.value) return;
        }
        chart.value.clear();
        chart.value.setOption(props.option || {});
      });
    };

    const showLoading = () => {
      chart.value && chart.value.showLoading();
    };

    const hideLoading = () => {
      chart.value && chart.value.hideLoading();
    };

    watchEffect(() => {
      if (loading.value) {
        showLoading();
      } else {
        hideLoading();
      }
    });

    const resize = () => {
      if (chart.value && !chart.value.isDisposed()) {
        // 有时候需要动态更改 chart 高度,这时候需要手动 resize 一下
        if (props.height.endsWith("px")) {
          let height = props.height.slice(0, props.height.length - 2);
          chart.value.resize({
            height: Number(height),
          });
        } else {
          chart.value.resize();
        }
      }
    };

    const cleanup = () => {
      if (chart.value) {
        chart.value.dispose();
        chart.value = undefined;
      }
    };

    watch(
      () => props.height,
      () => {
        nextTick(() => {
          resize();
          setOption();
        });
      }
    );

    watch(
      () => props.option,
      () => {
        if (!chart.value) {
          init();
        } else {
          setOption();
        }
      },
      { deep: true }
    );

    onMounted(() => {
      init();
      if (autoResize.value) {
        window.addEventListener("resize", resize);
      }
    });

    onUnmounted(() => {
      if (autoResize.value) {
        window.removeEventListener("resize", resize);
      }
      cleanup();
    });

    return {
      root,
      chart,
    };
  },
});
</script>

总结

由于项目的功能场景,我目前只支持以下几个简单功能:

  • [✔] resize(也就是支持 pc 和移动)
  • [✔] 异步请求 showloadinghideloading
  • [✔] 兼容初始值存在 option 和异步设置 option 两种
  • 允许使用 echarts 中的 dispatchAction 触发图表行为,由于篇幅有限,这里不展开

有几个小坑,其实文档都有写。真的要看文档,看文档,看文档。

常见问题

  1. Tab 标签页中图表绘制失败

    有时候图表会放在多个 tab 标签页里,那些初始隐藏的标签在初始化图表的时候因为获取不到容器的实际高宽,可能会绘制失败(我组件里面默认宽度是 100%,就会显示 100px)。

    • 因此在切换到该标签页时需要手动调用 resize 方法获取正确的高宽并且刷新画布
    • 或者在 props 中显示指定图表高宽(通过 js 去获取宽高然后重新赋值)
  2. 动态更改图表高度不触发 resize

    有时候想要重新设置 chart 图表高度,但是这并不会触发 resize(比方说 legend 在右侧,但是 ui 不同意使用 scroll 模式)。

    • 要不就还是手动 resize 一下
    • 我的做法是监听 props 里面高度的变化,重新触发 resize
  3. legend 样式无法满足需求

    有时候 echarts 中的 legend 无法满足项目样式需求时(UI 给样式太过特别,echart 中的 rich 难以满足)。

    • 建议直接在外层写一个 div,自己通过 css 样式去模拟

原文链接:https://juejin.cn/post/7130611311158771726 作者:Jxiang

分享: