HTML DOM的高级应用,核心是“让DOM技术成为解决复杂业务问题的核心工具”——不再局限于基础交互或组件封装,而是结合可视化、富文本、跨端适配等场景,通过DOM API与其他技术栈(如Canvas、Web API)的协同,实现企业级产品的核心功能。本文围绕“可视化图表交互”“富文本编辑器核心模块”“跨端DOM适配”“DOM与Web API深度协同”四大高频高级应用场景,结合真实业务案例,讲解DOM技术的落地思路与实践技巧。
一、应用场景1:可视化图表交互(基于DOM+SVG的可交互数据图表)
在数据可视化场景中,SVG因“矢量可缩放”“支持DOM操作”的特性,成为轻量级图表的首选方案。本场景以“可交互折线图”为例,实现“hover显示数据详情”“点击切换数据系列”“拖拽调整数据点”三大核心交互,展现DOM在可视化中的高级应用。
业务需求
展示“月度销售额”与“月度利润”两条数据系列的折线图;
鼠标hover到数据点时,显示当前月份、销售额、利润的详情弹窗;
点击图例可切换对应数据系列的显示/隐藏;
支持拖拽数据点调整销售额数值,实时更新图表与数据。
技术方案
用SVG绘制折线图(坐标轴、数据点、折线、图例),利用SVG的DOM特性绑定事件;
通过getBoundingClientRect()计算数据点位置,实现弹窗精准定位;
维护数据源与DOM的双向同步,拖拽数据点后实时更新SVG路径与数据。
完整实现代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DOM高级应用:可交互SVG折线图</title>
<style>
.chart-container {
max-width: 1000px;
margin: 40px auto;
padding: 0 20px;
font-family: "Microsoft YaHei", sans-serif;
}
.chart-title {
font-size: 20px;
color: #1f2937;
margin-bottom: 20px;
text-align: center;
}
/* SVG图表样式 */
.chart-svg {
width: 100%;
height: 500px;
border: 1px solid #e5e7eb;
border-radius: 8px;
background: #f9fafb;
}
.axis-line {
stroke: #6b7280;
stroke-width: 1;
}
.axis-text {
font-size: 12px;
fill: #4b5563;
text-anchor: middle;
}
.legend-item {
cursor: pointer;
}
.legend-text {
font-size: 14px;
fill: #4b5563;
margin-left: 8px;
}
.data-point {
cursor: pointer;
transition: r 0.2s;
}
.data-point:hover {
r: 6; /* hover时放大数据点 */
}
.data-point.dragging {
r: 8;
stroke: #1f2937;
stroke-width: 2;
}
/* 详情弹窗样式 */
.tooltip {
position: absolute;
padding: 8px 12px;
background: #1f2937;
color: white;
border-radius: 4px;
font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
pointer-events: none; /* 避免遮挡鼠标事件 */
opacity: 0;
transition: opacity 0.2s;
}
.legend-container {
display: flex;
justify-content: center;
gap: 24px;
margin-top: 16px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
display: inline-block;
vertical-align: middle;
}
.legend-sales {
background: #3b82f6;
}
.legend-profit {
background: #10b981;
}
</style>
</head>
<body>
<div class="chart-container">
<h2 class="chart-title">月度销售额与利润趋势图</h2>
<!-- SVG图表 -->
<svg class="chart-svg" id="chartSvg">
<!-- 坐标轴与折线将通过JS动态生成 -->
</svg>
<!-- 图例 -->
<div class="legend-container">
<div class="legend-item" data-series="sales">
<span class="legend-color legend-sales"></span>
<span class="legend-text">销售额(万元)</span>
<"zq-mobile.zhaopin.com/moment/80155854">
<"zhiq.zhaopin.com/moment/80155854">
<"zq.zhaopin.com/moment/80155086">
<"zq-mobile.zhaopin.com/moment/80155086">
<"zhiq.zhaopin.com/moment/80155086">
<"zq.zhaopin.com/moment/80152405">
<"zq-mobile.zhaopin.com/moment/80152405">
<"zhiq.zhaopin.com/moment/80152405">
<"zq.zhaopin.com/moment/80152289">
<"zq-mobile.zhaopin.com/moment/80152289">
</div>
<div class="legend-item" data-series="profit">
<span class="legend-color legend-profit"></span>
<span class="legend-text">利润(万元)</span>
</div>
</div>
<!-- 详情弹窗 -->
<div class="tooltip" id="tooltip"></div>
</div>
<script>
// 1. 初始化数据源(月份+销售额+利润)
const chartData = {
months: ["1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月"],
series: [
{
name: "sales",
label: "销售额",
color: "#3b82f6",
data: [120, 150, 130, 180, 160, 200, 190, 220, 240],
visible: true // 控制是否显示
},
{
name: "profit",
label: "利润",
color: "#10b981",
data: [30, 45, 35, 50, 48, 60, 55, 65, 72],
visible: true
}
]
};
// 2. 获取DOM元素与SVG相关配置
const svg = document.getElementById("chartSvg");
const tooltip = document.getElementById("tooltip");
const svgRect = svg.getBoundingClientRect();
// SVG绘图区域边距(避免内容贴边)
const margin = { top: 40, right: 40, bottom: 60, left: 60 };
const chartWidth = svgRect.width - margin.left - margin.right;
const chartHeight = svgRect.height - margin.top - margin.bottom;
// 记录拖拽状态
let draggingPoint = null;
let dragStartY = 0;
// 3. 创建SVG绘图组(平移到边距内)
const chartGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
chartGroup.setAttribute("transform", `translate(${margin.left}, ${margin.top})`);
svg.appendChild(chartGroup);
// 4. 绘制坐标轴(X轴:月份,Y轴:数值)
function drawAxes() {
// X轴:月份
const xAxisGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
xAxisGroup.setAttribute("transform", `translate(0, ${chartHeight})`);
// X轴轴线
const xAxisLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
xAxisLine.setAttribute("class", "axis-line");
xAxisLine.setAttribute("x1", 0);
xAxisLine.setAttribute("y1", 0);
xAxisLine.setAttribute("x2", chartWidth);
xAxisLine.setAttribute("y2", 0);
xAxisGroup.appendChild(xAxisLine);
// X轴刻度与文本(均匀分布)
const xStep = chartWidth / (chartData.months.length - 1);
chartData.months.forEach((month, index) => {
const x = index * xStep;
// 刻度线
const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
tick.setAttribute("class", "axis-line");
tick.setAttribute("x1", x);
tick.setAttribute("y1", 0);
tick.setAttribute("x2", x);
tick.setAttribute("y2", 6);
xAxisGroup.appendChild(tick);
// 文本
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("class", "axis-text");
text.setAttribute("x", x);
text.setAttribute("y", 24);
text.textContent = month;
xAxisGroup.appendChild(text);
});
chartGroup.appendChild(xAxisGroup);
// Y轴:数值(最大值取数据中的最大值+20,确保顶部有空间)
const maxValue = Math.max(...chartData.series.flatMap(s => s.data)) + 20;
const yAxisGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
// Y轴轴线
const yAxisLine = document.createElementNS("http://www.w3.org/2000/svg", "line");
yAxisLine.setAttribute("class", "axis-line");
yAxisLine.setAttribute("x1", 0);
yAxisLine.setAttribute("y1", 0);
yAxisLine.setAttribute("x2", 0);
yAxisLine.setAttribute("y2", chartHeight);
yAxisGroup.appendChild(yAxisLine);
// Y轴刻度与文本(5个刻度)
const yTicks = 5;
const yStep = chartHeight / yTicks;
const valueStep = maxValue / yTicks;
for (let i = 0; i <= yTicks; i++) {
const y = chartHeight - i * yStep;
const value = Math.round(i * valueStep);
// 刻度线
const tick = document.createElementNS("http://www.w3.org/2000/svg", "line");
tick.setAttribute("class", "axis-line");
tick.setAttribute("x1", 0);
tick.setAttribute("y1", y);
tick.setAttribute("x2", -6);
tick.setAttribute("y2", y);
yAxisGroup.appendChild(tick);
// 文本(左对齐,避免遮挡)
const text = document.createElementNS("http://www.w3.org/2000/svg", "text");
text.setAttribute("class", "axis-text");
text.setAttribute("x", -12);
text.setAttribute("y", y + 4);
text.setAttribute("text-anchor", "end");
text.textContent = value;
yAxisGroup.appendChild(text);
}
chartGroup.appendChild(yAxisGroup);
}
// 5. 绘制折线与数据点(核心:DOM与数据绑定)
function drawSeries() {
// 先清除已有的折线与数据点(避免重复绘制)
chartGroup.querySelectorAll(".series-line, .data-point").forEach(el => el.remove());
const xStep = chartWidth / (chartData.months.length - 1);
const maxValue = Math.max(...chartData.series.flatMap(s => s.data)) + 20;
chartData.series.forEach(series => {
if (!series.visible) return; // 隐藏的系列不绘制
// 生成折线路径(SVG path语法:M(x1,y1) L(x2,y2) ...)
let pathData = "";
series.data.forEach((value, index) => {
const x = index * xStep;
// Y轴坐标:数值越大,Y越小(SVG原点在左上角)
const y = chartHeight - (value / maxValue) * chartHeight;
if (index === 0) {
pathData += `M${x},${y}`; // 起点
} else {
pathData += ` L${x},${y}`; // 后续点
}
});
// 创建折线元素
const line = document.createElementNS("http://www.w3.org/2000/svg", "path");
line.setAttribute("class", "series-line");
line.setAttribute("d", pathData);
line.setAttribute("stroke", series.color);
line.setAttribute("stroke-width", 2);
line.setAttribute("fill", "none");
chartGroup.appendChild(line);
// 创建数据点(每个数据点绑定事件)
series.data.forEach((value, index) => {
const x = index * xStep;
const y = chartHeight - (value / maxValue) * chartHeight;
const point = document.createElementNS("http://www.w3.org/2000/svg", "circle");
point.setAttribute("class", "data-point");
point.setAttribute("cx", x);
point.setAttribute("cy", y);
point.setAttribute("r", 4);
point.setAttribute("fill", series.color);
// 绑定数据(便于后续交互)
point.dataset.series = series.name;
point.dataset.index = index;
point.dataset.value = value;
// 6. 数据点hover事件:显示详情弹窗
point.addEventListener("mouseover", function() {
const seriesName = this.dataset.series;
const index = parseInt(this.dataset.index);
const value = this.dataset.value;
const month = chartData.months[index];
const series = chartData.series.find(s => s.name === seriesName);
<"zhiq.zhaopin.com/moment/80152289">
<"zq.zhaopin.com/moment/80152223">
<"zq-mobile.zhaopin.com/moment/80152223">
<"zhiq.zhaopin.com/moment/80152223">
<"zq.zhaopin.com/moment/80152204">
<"zq-mobile.zhaopin.com/moment/80152204">
<"zhiq.zhaopin.com/moment/80152204">
<"zq.zhaopin.com/moment/80152201">
<"zq-mobile.zhaopin.com/moment/80152201">
<"zhiq.zhaopin.com/moment/80152201">
// 计算弹窗位置(基于SVG坐标转换为页面坐标)
const pointRect = this.getBoundingClientRect();
tooltip.style.left = `${pointRect.left + pointRect.width/2 - tooltip.offsetWidth/2}px`;
tooltip.style.top = `${pointRect.top - tooltip.offsetHeight - 8}px`;
tooltip.innerHTML = `
<div>月份:${month}</div>
<div>${series.label}:${value} 万元</div>
`;
tooltip.style.opacity = 1;
});
point.addEventListener("mouseout", function() {
tooltip.style.opacity = 0;
});
// 7. 数据点拖拽事件:调整数值(仅支持销售额)
if (series.name === "sales") {
point.addEventListener("mousedown", function(e) {
draggingPoint = this;
this.classList.add("dragging");
dragStartY = e.clientY; // 记录拖拽起始Y坐标
// 阻止默认行为,避免文本选中等干扰
e.preventDefault();
});
}
chartGroup.appendChild(point);
});
});
}
// 8. 全局拖拽事件:处理数据点拖拽更新
document.addEventListener("mousemove", function(e) {
if (!draggingPoint) return;
// 计算Y轴移动距离对应的数值变化(每移动10px,数值变化5)
const yDiff = dragStartY - e.clientY;
const valueChange = Math.round(yDiff / 10) * 5;
// 获取当前数据点的原始数据
const seriesName = draggingPoint.dataset.series;
const index = parseInt(draggingPoint.dataset.index);
const originalValue = parseInt(draggingPoint.dataset.value);
// 计算新数值(限制在50-300之间,避免异常值)
let newValue = originalValue + valueChange;
newValue = Math.max(50, Math.min(300, newValue));
// 更新数据源与DOM
const series = chartData.series.find(s => s.name === seriesName);
series.data[index] = newValue;
draggingPoint.dataset.value = newValue;
// 更新数据点位置
const maxValue = Math.max(...chartData.series.flatMap(s => s.data)) + 20;
const y = chartHeight - (newValue / maxValue) * chartHeight;
draggingPoint.setAttribute("cy", y);
// 更新拖拽起始Y坐标(确保连续拖拽)
dragStartY = e.clientY;
// 重新绘制折线(因为数据变化)
drawSeries();
});
// 拖拽结束:清除状态
document.addEventListener("mouseup", function() {
if (draggingPoint) {
draggingPoint.classList.remove("dragging");
draggingPoint =</doubaocanvas>