手摸手教学如何使用tsx + vue3.x + echarts + vueuse封装高级特性的可视化组件

前言

在日常开发任务中,chart 图表是打交道最多的库之一。并且有许多优秀的开源二次封装库,但是这里就不做赘述。

今天带大家学习一波,如何基于 vue3.x, vueuse, echarts 封装一个具有以下高级特性的 chart 组件:

  • 异步渲染
  • 贴花配置
  • 加载配置
  • 视窗区域渲染
  • 自动更新尺寸
  • 节流更新图表
  • 自动根据 options 更新更新图表
  • 自动销毁
  • useChart 方法
  • 还有一些不值一提的基础特性...

由于篇幅的原因,只贴出一些核心的实现细节,如果需要查看具体代码,可以点击查看:

项目初始化

如何初始化项目这里就不做赘述,直接进入主题。

不会的同学,可以去看对应的官网文档:

上手起步

创建一个 RChart 文件夹,包含以下的文件与文件夹:Chart.tsx, types.ts, props.ts, index.scss, hooks

sh 复制代码
RChart
├── Chart.tsx
├── types.ts
├── props.ts
├── index.scss
└── hooks
    ├── useChart.ts

props.ts

Q: 为什么需要把 props 配置项单独提出来呢?

A:一部分原因是为了更好的支持 ts,另一个原因是为了更好的维护与方便后续的拓展。

ts 复制代码
const props = {
  /**
   *
   * @description
   * 是否开启 IntersectionObserver 监听,用于监听图表是否在可视区域内再进行渲染。
   * 默认监听图表容器是否在可视区域内,也可以配置 intersectionObserverTarget 属性监听指定元素。
   *
   * 该方法需要浏览器支持 IntersectionObserver API。
   *
   * @default true
   */
  intersectionObserver: {
    type: Boolean,
    default: true,
  },
  /**
   *
   * @description
   * 指定 IntersectionObserver 监听的目标元素。
   *
   * 该属性需要开启 intersectionObserver 才能生效。
   *
   * @default null
   */
  intersectionObserverTarget: {
    type: Object as PropType<MaybeComputedElementRef<MaybeElement>>,
    default: null,
  },
  /**
   *
   * @description
   * IntersectionObserver 配置项。
   *
   * 该属性需要开启 intersectionObserver 才能生效。
   *
   * @see https://www.vueusejs.com/core/useIntersectionObserver/
   *
   * @default {threshold:0.1}
   */
  intersectionOptions: {
    type: Object as PropType<UseIntersectionObserverOptions>,
    default: {
      threshold: 0.1,
    },
  },
  /**
   *
   * @description
   * chart 默认宽度,默认为 100%。
   *
   * 但是,如果未获取到实际宽度,那么会以 200px 宽度填充。
   *
   * @default 100%
   */
  width: {
    type: String,
    default: '100%',
  },
  /**
   *
   * @description
   * chart 默认高度,默认为 100%。
   *
   * 但是,如果未获取到实际高度,那么会以 200px 高度填充。
   *
   * @default 100%
   */
  height: {
    type: String,
    default: '100%',
  },
  /**
   *
   * @description
   * 是否启用自动调整大小,默认跟随图表容器尺寸变化。
   *
   * @default true
   */
  autoResize: {
    type: Boolean,
    default: true,
  },
  /**
   *
   * @description
   * 是否启用 chart 无障碍模式。
   * 启用该配置项后会覆盖 options 中的 aria。
   *
   * @default false
   */
  showAria: {
    type: Boolean,
    default: false,
  },
  /**
   *
   * @description
   * chart 图表配置项。
   *
   * @default {}
   */
  options: {
    type: Object as PropType<echarts.EChartsCoreOption>,
    default: () => ({}),
  },
  /**
   *
   * @description
   * chart 渲染成功回调函数。
   *
   * @default null
   */
  onSuccess: {
    type: [Function, Array] as PropType<MaybeArray<(e: ECharts) => void>>,
    default: null,
  },
  /**
   *
   * @description
   * chart 渲染失败回调函数。
   *
   * @default null
   */
  onError: {
    type: [Function, Array] as PropType<MaybeArray<() => void>>,
    default: null,
  },
  /**
   *
   * @description
   * chart 渲染结束后的回调函数,不论是否成功都会执行。
   */
  onFinally: {
    type: [Function, Array] as PropType<MaybeArray<() => void>>,
    default: null,
  },
  /**
   *
   * @description
   * 手动指定 chart 主题配置项。
   *
   * @default null
   */
  theme: {
    type: String as PropType<ChartTheme>,
    default: null,
  },
  /**
   *
   * @description
   * 是否自动跟随模板主题切换。
   * 该配置项会覆盖 theme 配置项。
   *
   * @default true
   */
  autoChangeTheme: {
    type: Boolean,
    default: true,
  },
  /**
   *
   * @description
   * 手动拓展 chart 图的相关组件。
   *
   * 该配置项不支持动态调用,及时动态更新了该属性,也不会生效。
   * 并且,该配置项必须在 RChart 组件初始化时候配置。
   *
   * @default []
   */
  use: {
    type: Array as PropType<EChartsExtensionInstallRegisters[]>,
    default: () => [],
  },
  /**
   *
   * @description
   * 是否开启 watch 监听 options 配置项。
   *
   * @default true
   */
  watchOptions: {
    type: Boolean,
    default: true,
  },
  /**
   *
   * @description
   * 是否启用 chart 加载动画。
   *
   * @default false
   */
  loading: {
    type: Boolean,
    default: false,
  },
  /**
   *
   * @description
   * chart 加载动画配置项。
   *
   * @default {}
   */
  loadingOptions: {
    type: Object as PropType<LoadingOptions>,
    default: () => loadingOptions(),
  },
  /**
   *
   * @description
   * 手动设置 autoResize 监听的元素。
   * 该元素必须是一个有效的 DOM 元素,并且需要开启 autoResize 才能生效。
   *
   * 默认以图表容器元素作为监听对象。
   *
   * @default null
   */
  autoResizeObserverTarget: {
    type: Object as PropType<MaybeComputedElementRef<MaybeElement>>,
    default: null,
  },
  /**
   *
   * @description
   * 是否开启 watchThrottle 监听 options 配置项更新。
   * 该配置项适合在需要频繁更新 chart options 的场景下使用。
   *
   * 但是该配置项需要开启 watchOptions 才能生效。
   *
   * @default 500
   */
  watchOptionsThrottleWait: {
    type: Number,
    default: 500,
  },
  /**
   *
   * @description
   * 是否将渲染放置下一个队列。
   *
   * @default true
   */
  nextTick: {
    type: Boolean,
    default: true,
  },
  /**
   *
   * @description
   * 设置 setOptions 方法配置项。
   *
   * @default {notMerge:false,lazyUpdate:true,silent:false,replaceMerge:[]}
   */
  setChartOptions: {
    type: Object as PropType<SetOptionOpts>,
    default: () => ({
      notMerge: false,
      lazyUpdate: true,
      silent: false,
      replaceMerge: [],
    }),
  },
  /**
   *
   * @description
   * RChart 注册挂载成功后触发的事件。
   * 可以结合 useChart 方法中的 register 方法使用,然后便捷的使用 hooks。
   *
   * @default null
   */
  onRegister: {
    type: [Function, Array] as PropType<
      MaybeArray<(chartInst: ECharts, render: VoidFC, dispose: VoidFC) => void>
    >,
    default: null,
  },
}

具体的每一个配置项功能都写了详细的注释,这里就不做赘述。

实现细节

贴花、加载

利用组件的特性,我们可以将这两个 EChart 特性封装为配置项的形式去使用。

tsx 复制代码
// 监听是否启用了贴花
watch(
  () => props.showAria,
  () => {
    destroyChart()
    updateChartTheme()
  },
)
// 监听 loading 变化
watchEffect(() => {
  props.loading
    ? echartInst?.showLoading(props.loadingOptions)
    : echartInst?.hideLoading()
})

没想到吧,就这么简单的实现了。

异步渲染

当渲染的数据过多、一次性渲染的图表过多,可能会导致页面卡顿,这时候就需要异步渲染。

其实核心的思想就是利用 nextTick api 去实现,将一些关键的操作丢至下一个队列中执行。

tsx 复制代码
const renderChart = (theme: string = echartTheme) => {
  // 省略部分代码...

  try {
    // 省略部分代码...

    // 是否强制下一队列渲染图表
    if (props.nextTick) {
      echartInst.setOption({})

      nextTick(() => {
        options && echartInst?.setOption(options)
      })
    } else {
      options && echartInst?.setOption(options)
    }
  } catch (e) {
    // 省略部分代码...
  } finally {
    // 省略部分代码...
  }
}

自动更新尺寸

ECharts 天生支持了 autoResize 方法,我们可以利用 vueuse 提供的 useResizeObserver 方法去实现。

当然,还可能需要考虑到,如果频繁触发更新,肯定也是不是一个理智的行为,还可以在此基础上添加一个节流锁。我们可以用 lodash-es 提供的 throttle 方法去实现。

tsx 复制代码
const resizeChart = () => {
  if (echartInst) {
    echartInst.resize()
  }
}

const mount = () => {
  // 省略部分代码...

  if (props.autoResize) {
    if (!resizeThrottleReturn) {
      resizeThrottleReturn = throttle(resizeChart, 500)
    }

    /**
     *
     * 监听内容区域尺寸变化更新 chart。
     * 如果没有传入 autoResizeObserverTarget 属性,则默认监听容器尺寸变化。
     */
    if (!resizeObserverReturn) {
      resizeObserverReturn = useResizeObserver(
        props.autoResizeObserverTarget || rayChartWrapperRef,
        resizeThrottleReturn as AnyFC,
      )
    }
  }
}

视窗区域渲染

同样的,我们可以利用 vueuse 提供的 useIntersectionObserver 方法去实现。

tsx 复制代码
// 如果配置启用 intersectionObserver,则监听图表是否在可视区域内
if (props.intersectionObserver) {
  intersectionObserverReturn = useIntersectionObserver(
    props.intersectionObserverTarget || rayChartWrapperRef,
    ([entry]) => {
      targetIsVisible.value = entry.isIntersecting
    },
    props.intersectionOptions,
  )
}

当然,该配置项需要浏览器支持 IntersectionObserver API。并且,在初始化渲染了 chart 后需要移除,避免重复监听。

tsx 复制代码
// 初始化完成后移除 intersectionObserver 监听
const mount = () => {
  // 省略部分代码...

  intersectionObserverReturn?.stop()
}

经过如上的操作,我们就实现了视窗区域渲染。组件仅仅会在视窗区域内渲染,避免了一次性渲染过多图表导致页面卡顿。

自动销毁

在组件销毁的时候,我们需要手动销毁 chart 实例,避免内存泄漏。仅需在 vue 组件卸载之前调用 onBeforeUnmount 钩子即可。

tsx 复制代码
const unmount = () => {
  // 省略部分代码...
}

onBeforeUnmount(() => {
  unmount()
  watchThrottledCallback?.()
})

节流更新图表

该方法可以基于 vueuse 提供的 watchThrottled 方法去实现。

tsx 复制代码
watchEffect(() => {
  /** 监听 options 变化 */
  if (props.watchOptions) {
    watchThrottledCallback = watchThrottled(
      () => props.options,
      (ndata) => {
        // 重新组合 options
        const options = combineChartOptions(ndata)
        const setOpt = Object.assign(
          {},
          props.setChartOptions,
          defaultChartOptions,
        )
        // 如果 options 发生变动更新 echarts
        echartInst?.setOption(options, setOpt)
      },
      {
        // 深度监听 options
        deep: true,
        throttle: props.watchOptionsThrottleWait,
      },
    )
  } else {
    watchThrottledCallback?.()
  }
})

主题注册

ECharts 中,我们可以通过 echarts.registerTheme 方法去注册主题。

并且,ECharts 官方还提供了主题编辑器,我们只需要按照以下步骤即可完成:

  1. 配置、选择主题
  2. 点击下载主题
  3. 选择 json 类型,然后复制
  4. 选择一个存放主题包的文件夹
ts 复制代码
export const setupChartTheme = () => {
  // 获取所有主题
  const themeRawModules: Record<string, ChartThemeRawModules> =
    // 该地址换位你的主题包存放地址
    import.meta.glob('@/app-config/echart-themes/**/*.json', {
      eager: true,
    })
  const regex = /\/([^/]+)\.json$/

  const rawThemes = Object.keys(themeRawModules).reduce((pre, curr) => {
    const name = curr.match(regex)?.[1]

    if (name) {
      pre.push({
        name,
        theme: themeRawModules[curr].default,
      })

      return pre
    } else {
      throw new Error(`[RChart Theme Error]: name ${curr} is invalid!`)
    }
  }, [] as ChartThemeRawArray[])

  return rawThemes
}

调用 setupChartTheme 方法,我们就可以获取到所有的主题配置项。

tsx 复制代码
import { use, registerTheme, init } from 'echarts/core' // echarts 核心模块

// 获取 chart 主题
const echartThemes = setupChartTheme()

// 注册主题
echartThemes.forEach((curr) => {
  registerTheme(curr.name, curr.theme)
})

useChart 方法

vue 中,如果我们需要使用组件实例上的方法,我们需要进过以下的步骤去使用:

vue 复制代码
<template>
  <RChart ref="chartRef" />
</template>
<script setup lang="ts">
import { ref } from 'vue'

import type { RChartInst } from 'RChart/types'

const chartRef = ref<RChartInst>()

const isDispose = () => {
  return !chartRef.value?.getDom()
}
</script>

不仅仅需要定义 ref,并且还要关注组件的生命周期,还有则是这么写的代码好像没有那么的优雅,也不方便拓展。

所以,我们需要一个 useChart 方法。

ts 复制代码
const useChart = () => {
  // 省略部分代码...

  return [
    register,
    {
      getChartInstance,
      isDispose,
      dispose,
      render,
    },
  ] as const
}

然后我们现在来对比一下同样的功能代码编写:

vue 复制代码
<template>
  <RChart @register="register" />
</template>
<script setup lang="ts">
import { useChart } from 'RChart/hooks/useChart'

const [register, { getChartInstance, isDispose, dispose, render }] = useChart()

const isDispose = () => {
  return isDispose()
}
</script>

使用

当我们封装好该组件后,可以快速上手体验一下。

vue 复制代码
<template>
  <RChart @register="register" :options="options" />
</template>
<script setup lang="ts">
import { useChart } from 'RChart/hooks/useChart'

const [register, { getChartInstance, isDispose, dispose, render }] = useChart()
const options = ref({
  tooltip: {
    trigger: 'axis',
    axisPointer: {
      type: 'cross',
      label: {
        backgroundColor: '#6a7985',
      },
    },
  },
  legend: {
    data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine'],
  },
  grid: {
    left: '3%',
    right: '4%',
    bottom: '3%',
    containLabel: true,
  },
  xAxis: [
    {
      type: 'category',
      boundaryGap: false,
      data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
    },
  ],
  yAxis: [
    {
      type: 'value',
    },
  ],
  series: [
    {
      name: 'Email',
      type: 'line',
      stack: 'Total',
      areaStyle: {},
      emphasis: {
        focus: 'series',
      },
      data: [120, 132, 101, 134, 90, 230, 210],
    },
    {
      name: 'Union Ads',
      type: 'line',
      stack: 'Total',
      areaStyle: {},
      emphasis: {
        focus: 'series',
      },
      data: [220, 182, 191, 234, 290, 330, 310],
    },
    {
      name: 'Video Ads',
      type: 'line',
      stack: 'Total',
      areaStyle: {},
      emphasis: {
        focus: 'series',
      },
      data: [150, 232, 201, 154, 190, 330, 410],
    },
    {
      name: 'Direct',
      type: 'line',
      stack: 'Total',
      areaStyle: {},
      emphasis: {
        focus: 'series',
      },
      data: [320, 332, 301, 334, 390, 330, 320],
    },
    {
      name: 'Search Engine',
      type: 'line',
      stack: 'Total',
      label: {
        show: true,
        position: 'top',
      },
      areaStyle: {},
      emphasis: {
        focus: 'series',
      },
      data: [820, 932, 901, 934, 1290, 1330, 1320],
    },
  ],
})
</script>

最后

这里就不做赘述,想给大家提供一个封装的思路。

感谢大家的阅读,谢谢~~~

相关推荐
mldong2 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排2 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60613 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角4 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!4 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰4 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
mayaairi6 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js
kyriewen6 小时前
面试官问我:AI 都能写代码了,前端凭什么还值 25K
前端·javascript·人工智能
风骏时光牛马7 小时前
AI源码分析:拆解模型底层实现逻辑
前端
IT_陈寒7 小时前
React子组件莫名其妙重渲染?你可能漏了这个Hook
前端·人工智能·后端