逐行分析 RayTemplate RChart 可视化组件,手膜手教学如何封装企业级可视化组件

283 阅读4分钟

RChart 组件

不同于其他基于业务层面封装的 Echart 组件,RChart 仅关注:

  • mount(注册)
  • unmount(注销)
  • watch options(配置项)
  • loading(加载动画)、aria(贴花)
  • autoResize(自动更新尺寸)
  • theme(主题)

换句话讲,RChart 只是帮你做了生命周期管理的事情,让你每次在需要使用 echarts 的时候,只需要去关注配置项的维护。

当然,该组件不仅仅只有以上的能力,这里只是挑一些核心的来举例。

预览

进入正题

由于该组件是 Ray Template 模板中的一个二次封装组件,所以结合了该模板的一些特性在其中。

组件实现

逻辑分析

echarts 注册逻辑

  1. 注册核心组件、渲染器
  2. 获取 DOM
  3. 调用 init 方法注册图表,并且返回一个实例
  4. 调用 setOption 方法设置配置项

echarts 卸载逻辑

  1. 调用 clear, dispose 方法释放资源
  2. 注销相关监听资源

现在我们知道了 echarts 的完整注册、卸载流程,我们继续分析,如何把它与 vue3.x 结合起来。

vue3.x 结合

我们应该先分析,需要先做那些问题是 init 时需要关注的:

  • 容器的初始尺寸
  • 主题
  • options 配置项

再来看,封装组件需要关注的:

  • 代理 optons 配置项
  • 代理 echarts instance 实例
  • 触发注册成功或失败的回调方法
  • lodaingariatheme 代理

代码实现(部分代码)

  • 注册 echarts
    const renderChart = (theme: string = echartTheme) => {
      /** 获取 dom 容器 */
      const element = rayChartRef.value as HTMLElement
      /** 获取配置项 */
      const options = combineChartOptions(props.options)
      /** 获取 dom 容器实际宽高 */
      const { height, width } = element.getBoundingClientRect()
      const { onSuccess, onError } = props

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

        /** 注册 chart */
        echartInst = echarts.init(element, theme, {
          /** 如果款度为 0, 则以 200px 填充 */
          width: width === 0 ? 200 : void 0,
          /** 如果高度为 0, 则以 200px 填充 */
          height: height === 0 ? 200 : void 0,
        })
        echartInstanceRef.value = echartInst

        /** 设置 options 配置项 */
        if (props.animation) {
          echartInst.setOption({})

          setTimeout(() => {
            options && echartInst?.setOption(options)
          })
        } else {
          options && echartInst?.setOption(options)
        }

        /** 渲染成功回调 */
        if (onSuccess) {
          call(onSuccess, echartInst)
        }
      } catch (e) {
        /** 渲染失败回调 */
        if (onError) {
          call(onError)
        }

        console.error('RChart render error: ', e)
      }
    }

该方法实现了基本的注册逻辑。并且能够在获取容器尺寸失败的时候,使用默认尺寸填充。

  • 注销 echarts
    const destroyChart = () => {
      if (echartInst && echartInst.getDom()) {
        echartInst.clear()
        echartInst.dispose()
        echartInstanceRef.value = void 0
      }
    }

该方法用于卸载图表释放资源。

  • 实现代理并且监听 options
    watchEffect(() => {
      /** 监听 options 变化 */
      if (props.watchOptions) {
        watchCallback = watch(
          () => props.options,
          (noptions) => {
            /** 重新组合 options */
            const options = combineChartOptions(noptions)
            const setOpt = Object.assign(
              props.setChartOptions,
              defaultChartOptions,
            )
            /** 如果 options 发生变动更新 echarts */
            echartInst?.setOption(options, setOpt)
          },
          {
            deep: true,
          },
        )
      } else {
        watchCallback?.()
      }
    })

允许配置 watchOptions 取消监听

  • 自适应尺寸实现
    if (props.autoResize) {
      resizeThrottleReturn = throttle(resizeChart, props.throttleWait)
      /** 监听内容区域尺寸变化更新 chart */

      resizeOvserverReturn = useResizeObserver(
        props.observer || rayChartWrapperRef,
        resizeThrottleReturn,
      )
    }

完整代码

完整代码这里不贴出来了,建议转移至该处查看:点击查看

使用

在调用该组件时,只需要关注于 options 的管理,即可实现动态更新图表状态。

defineComponent({
  name: 'REchart',
  setup() {
    const baseChartRef = ref<RayChartInst>()
    const chartLoading = ref(false)
    const chartAria = ref(false)
    const state = reactive({
      loading: false,
    })

    const baseLineOptions = 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],
        },
      ],
    })

    const handleLoadingShow = (bool: boolean) => {
      state.loading = bool
    }

    const handleAriaShow = (bool: boolean) => {
      chartAria.value = bool
    }

    const mountChart = () => {
      baseChartRef.value?.render()
    }

    const unmountChart = () => {
      baseChartRef.value?.dispose()
    }

    const handleUpdateTitle = () => {
      const createData = () => Math.floor((Math.random() + 1) * 100)

      baseLineOptions.value.series[0].data = new Array(7)
        .fill(0)
        .map(() => createData())
      baseLineOptions.value.series[1].data = new Array(7)
        .fill(0)
        .map(() => createData())
    }

    return {
      baseOptions,
      baseChartRef,
      chartLoading,
      handleLoadingShow,
      chartAria,
      handleAriaShow,
      basePieOptions,
      baseLineOptions,
      ...toRefs(state),
      mountChart,
      unmountChart,
      handleUpdateTitle,
    }
  },
  render() {
    return (
      <div class="echart">
           <RChart
            title="周销售量"
            ref="baseChartRef"
            autoChangeTheme
            options={this.baseLineOptions}
            showAria={this.chartAria}
            preset="card"
          />
      </div>
    )
  },
})

最后

后续将会不定时分析 Ray Template 的组件封装逻辑。

感谢大家的阅读,如果觉得该文章对您有所帮助,可以点个赞支持一下~

如果觉得模板不错,可以点一个小星星支持一下~