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 随传随走 |