HTML DOM 高级应用:从技术到业务的深度落地实践

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>

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容