Skip to content

useEcharts 组合式函数 —— 渐进式封装总结

目标:在 Vue3 中把「初始化、自适应、事件、卸载」等繁琐逻辑收拢成一个 可复用、零样板、类型安全 的组合式函数。
适合:已经会用 ECharts,但想进一步“偷懒”的同学。


Step 0 裸用 ECharts(痛点先行)

ts
onMounted(() => {
  const chart = echarts.init(document.getElementById('main'))
  chart.setOption(option)
  window.addEventListener('resize', () => chart.resize())
})
onUnmounted(() => {
  /* 记得 dispose?记得 removeListener? */
})

痛点

  • 每次写监听、卸载、防抖,样板 > 业务。
  • 主题、loading、事件注册散落在各处。
  • SSR / keep-alive 场景容易忘处理。

Step 1 只把「初始化」封装起来

ts
export const useEchartsBasic = (el: Ref<HTMLElement | null>) => {
  const ins = shallowRef<ECharts>()
  onMounted(() => ins.value = echarts.init(el.value!))
  return { ins }
}

✅ 解决:拿到实例。
❌ 没处理 resize / dispose / 主题 / loading…


Step 2 加入「响应式自适应」

ts
export const useEchartsResize = (el, theme = 'default') => {
  const ins = shallowRef<ECharts>()
  const resize = useDebounceFn(() => ins.value?.resize(), 200)
  
  onMounted(() => {
    ins.value = echarts.init(el.value, theme)
    window.addEventListener('resize', resize)
    nextTick(resize)          // 首屏
  })
  onActivated(resize)         // keep-alive 恢复
  onBeforeUnmount(() => {
    window.removeEventListener('resize', resize)
    ins.value?.dispose()
  })
  return { ins, resize }
}

Step 3 把「loading / 事件 / 数据」全部收拢

ts
export const useEchartsCore = (el, opts = {}) => {
  const { ins, resize } = useEchartsResize(el, opts.theme)
  
  const setOption = (option, events = []) => {
    ins.value?.setOption(option)
    events.forEach(e => {
      if (e.type === 'zr') ins.value?.getZr().on(e.name, e.cb)
      else ins.value?.on(e.name, e.query || {}, e.cb)
    })
  }
  const showLoading = (t='default', o={}) => ins.value?.showLoading(t, o)
  const hideLoading = () => ins.value?.hideLoading()
  const clear = () => ins.value?.clear()

  return { ins, setOption, showLoading, hideLoading, clear, resize }
}

Step 4 最终形态(带类型 & 统一 safeCall)

javascript
/* useEcharts.js */
import {
  getCurrentInstance,
  nextTick,
  onActivated,
  onMounted,
  shallowRef,
  unref,
} from 'vue'

/* ---------- 极简防抖 ---------- */
function debounce(fn, wait = 200) {
  let timer
  return function (...args) {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), wait)
  }
}

/* ---------- 事件注册 ---------- */
function useEvents(instanceRef, eventParams) {
  if (!Array.isArray(eventParams)) {
    return
  }

  eventParams.forEach((p) => {
    if (typeof p?.callback !== 'function') {
      return
    }

    const inst = unref(instanceRef)
    if (!inst) {
      return
    }

    if (p.type === 'zrender') {
      // zrender 事件
      inst.getZr().on(p.name, ev => !ev.target && p.callback(ev))
    }
    else {
      // echarts 事件
      inst.on(p.name, p.query || '', p.callback)
    }
  })
}

/* ---------- 主函数 ---------- */
export function useEcharts(elementRef, opts = {}) {
  const { appContext } = getCurrentInstance()
  const $echarts = appContext.config.globalProperties.$echarts
  if (!$echarts) {
    return null
  }

  const instance = shallowRef(null)

  const getInstance = () => instance.value
  const safeCall = fn => getInstance()?.[fn]?.()

  const resize = () => getInstance()?.resize()
  const debouncedResize = debounce(resize, 100)

  /* 初始化 */
  onMounted(async () => {
    await nextTick()
    const el = unref(elementRef)
    if (el) {
      instance.value = $echarts.init(el, opts.theme || 'default', opts)
    }
    window.addEventListener('resize', debouncedResize)
    resize()
  })

  onActivated(resize)

  /* 对外 API */
  return {
    echarts: $echarts,
    getInstance,
    setOption: (option, events) => {
      getInstance()?.setOption(option)
      useEvents(instance, events)
    },
    showLoading: (type = 'default', cfg = {}) =>
      getInstance()?.showLoading(type, cfg),
    hideLoading: () => getInstance()?.hideLoading(),
    clear: () => {
      if (getInstance()) {
        window.removeEventListener('resize', debouncedResize)
        getInstance().dispose()
        instance.value = null
      }
    },
    resize,
    appendData: payload => getInstance()?.appendData(payload),
    getDom: () => safeCall('getDom'),
    getWidth: () => safeCall('getWidth'),
    getHeight: () => safeCall('getHeight'),
    getOption: () => safeCall('getOption'),
  }
}

亮点总结

维度实现收益
零样板一行 const { setOption, resize } = useEcharts(domRef)开发者只关心数据
自适应内置防抖 + onActivated首屏、keep-alive、窗口变化自动 resize
事件setOption(opt, [{name:'click', cb}])声明式注册,自动随组件卸载
安全safe() 统一可选链彻底解决 Cannot read properties of null
可扩展额外参数通过 opts 透传主题、renderer、devicePixelRatio 随传随走

Released under the MIT License.