HTML DOM 的高级应用,核心是 “让 DOM 技术成为解决复杂业务问题的核心工具”—— 不再局限于基础交互或组件封装,而是结合可视化、富文本、跨端适配等场景,通过 DOM API 与其他技术栈(如 Canvas、Web API)的协同,实现企业级产品的核心功能。本文围绕 “可视化图表交互”“富文本编辑器核心模块”“跨端 DOM 适配”“DOM 与 Web API 深度协同” 四大高频高级应用场景,结合真实业务案例,讲解 DOM 技术的落地思路与实践技巧。
一、应用场景 1:可视化图表交互(基于 DOM+SVG 的可交互数据图表)
在数据可视化场景中,SVG 因 “矢量可缩放”“支持 DOM 操作” 的特性,成为轻量级图表的首选方案。本场景以 “可交互折线图” 为例,实现 “hover 显示数据详情”“点击切换数据系列”“拖拽调整数据点” 三大核心交互,展现 DOM 在可视化中的高级应用。
业务需求
技术方案
完整实现代码
<!DOCTYPE html><html><head> <meta charset="UTF-8">
<"wap.24luxiang.com.cn">
<"wap.24luxiang.cn">
<"wap.24kanqiu.cn">
<"wap.24cbazhibo.cn">
<"wap.laogaozhibo.net">
<"wap.laogaozhibo.com.cn">
<"wap.maiqiuzhibo.com">
<"wap.maiqiuzhibo.cn">
<"wap.cnhjzsw.cn">
<"wap.kdzpw.com"><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> <h2>月度销售额与利润趋势图</h2> <!-- SVG图表 --> <svg id="chartSvg"> <!-- 坐标轴与折线将通过JS动态生成 --> </svg> <!-- 图例 --> <div> <div data-series="sales"> <span class="legend-color legend-sales"></span> <span>销售额(万元)</span> </div> <div data-series="profit"> <span class="legend-color legend-profit"></span> <span>利润(万元)</span> </div> </div> <!-- 详情弹窗 --> <div 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); // 计算弹窗位置(基于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>
