真正高复用的数据看板组件必须结合 custom elements 和 shadow dom 封装边界,因为 仅存 html 片段,无响应性、无样式隔离、无生命周期控制,易导致 css 冲突、js 查询错乱、图表初始化失败及内存泄漏。

直接用 <template></template> 写死结构 + 手动克隆填充,做不出真正高复用的数据看板组件——它缺响应、缺隔离、缺生命周期控制。真要复用,必须结合 Custom Elements 和 Shadow DOM 封装边界。
为什么不能只靠 <template></template> 做数据看板组件
<template></template> 只是“存 HTML 片段”的容器,不带逻辑、不响应属性变化、样式不隔离。你把它 clone 进页面后,所有 class、id、内联样式都暴露在全局作用域里,一张看板里放两个同名卡片,CSS 互相污染,JS 查询器(如 document.querySelector('.chart'))会随机命中其中一个。
常见错误现象:
- 多个看板卡片共用同一个
id="chart1",ECharts 初始化报错Cannot initialize ECharts instance: container is not found - 修改一个卡片的
title属性,其他卡片标题也跟着变(因为用了全局变量或共享 DOM 引用) - 缩放浏览器窗口时,只有第一个图表 resize,其余失真(因监听了全局
window.resize,没绑定到各自容器)
Custom Elements + <template></template> 的正确组合方式
把 <template></template> 当作静态结构仓库,用 Custom Elements 控制实例化、数据注入和生命周期。关键不是“怎么写模板”,而是“谁来管它”。
实操建议:
- 在 HTML 顶部定义
<template id="dashboard-card"><div class="card"> <h3 data-bind="title"></h3> <div class="chart-container" data-bind="chartId"></div> </div></template>,只放结构,不写 JS 或内联事件 - 组件类中用
this.attachShadow({ mode: 'open' })创建隔离环境,再用document.getElementById('dashboard-card').content.cloneNode(true)注入,避免全局样式穿透 - 用
static get observedAttributes()声明['title', 'api-url', 'refresh-interval'],属性变更时触发attributeChangedCallback,而不是靠外部反复调用方法 - 图表初始化必须在
connectedCallback中执行,且先检查this.shadowRoot.querySelector('.chart-container').offsetWidth > 0,防止容器尺寸为 0 导致 ECharts 渲染失败
数据加载与图表更新必须绑定到单个实例
轮询不能写在全局 setInterval 里——那样所有卡片共享一个定时器,一个卡住,全部卡死;也不能在 connectedCallback 里直接 fetch 后渲染,否则组件被移除又重挂载时会重复请求。
实操建议:
- 每个组件实例维护自己的
this._abortController = new AbortController(),在disconnectedCallback中调用this._abortController.abort() - 用
setTimeout模拟节流:首次加载后,设this._timer = setTimeout(() => this._fetchData(), parseInt(this.getAttribute('refresh-interval') || '5000')),下次刷新前先clearTimeout(this._timer) - ECharts 实例必须存在组件实例上(
this._chart = echarts.init(container)),resize 监听也要绑定到该实例:window.addEventListener('resize', () => this._chart?.resize()),并在disconnectedCallback中移除
容易忽略的兼容性与销毁细节
多数人写了能显示的组件就停了,但真实部署中,卡片频繁增删、路由切换、动态配置拖拽排序,这些场景下内存泄漏和残留监听器才是大坑。
注意:
- Safari 15.6 及更早版本不支持
shadowRoot.querySelector(':scope .chart-container'),得用this.shadowRoot.querySelector('.chart-container')—— 别依赖伪类简化写法 -
disconnectedCallback不一定被调用(比如页面直接刷新),所以轮询定时器、WebSocket 连接、eventListener 都得在connectedCallback中配对清理,不能只靠生命周期钩子 - 如果看板支持拖拽排序(如用 SortableJS),别把
Sortable.create()放在组件内部——它操作的是父容器,应由外层看板容器统一管理,组件只负责响应位置变更后重新 fetch 数据
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











