找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2481

积分

0

好友

344

主题
发表于 3 天前 | 查看: 18| 回复: 0

看完这篇文章,你将系统性地掌握如何运用现代前端技术栈,从零开始构建一个高性能的数据可视化大屏项目。我们将涉及全局状态管理、函数缓存、Canvas动画、大规模数据分片渲染以及自动化部署等核心知识。线上 Demo 地址:https://lxfu1.github.io/large-screen-visualization/

数据可视化大屏建模方法示意图

你将收获:

  1. 全局状态管理新思路:5分钟上手,告别繁琐的状态提升与传递
  2. 高性能函数缓存:入参不变时,直接返回缓存结果,避免重复计算
  3. 千万级节点渲染优化:通过分片渲染技术,实现流畅的页面交互
  4. 前端项目单元测试:搭建测试环境,编写有效的测试用例
  5. Canvas 实战技巧:绘制复杂图表与实现平滑动画
  6. 自动化部署流程:利用 GitHub Actions 轻松部署静态网站

实现

项目基于 Create React App --template typescript 搭建,包管理工具使用 pnpm。pnpm 以其安装速度快和节省磁盘空间而著称。由于项目中限制了特定包的版本(最新版本的 @antv/G6 可能导致内存溢出,官方预计会修复),如果你使用 yarn 或 npm,请将配置项改为对应的 resolutions

pnpm 配置:

"pnpm": {
  "overrides": {
    "@antv/g6": "4.7.10"
  }
}

npm/yarn 配置:

"resolutions": {
  "@antv/g6": "4.7.10"
},

启动

  1. 克隆项目
    git clone https://github.com/lxfu1/large-screen-visualization.git
  2. 安装 pnpm npm install -g pnpm
  3. 启动项目:运行 pnpm start 即可。建议配置别名以简化命令,例如 alias p=pnpm,之后可使用 p start。正常情况下,你可以通过 http://localhost:3000/ 访问应用。
  4. 运行测试p test
  5. 构建项目p build

强烈建议先克隆项目,边操作边理解!

核心技术点分析

全局状态管理:Valtio

项目使用 Valtio 进行全局状态管理,相关代码位于 src/models 目录。Valtio 提供了数据与视图分离的心智模型,让你无需在 React 组件或 Hooks 中纠结于 useStateuseReducer,也无需在 useEffect 中处理初始化请求,或烦恼该用 Context 还是 Props 传递数据。

优点:心智模型简单,代码清晰。
缺点:基于 Proxy 实现,对低版本浏览器(如 IE)不友好。不过,数据可视化大屏通常无需考虑此类浏览器。

状态定义示例:

import { proxy } from “valtio”;
import { NodeConfig } from “@ant-design/graphs”;

type IState = {
  sliderWidth: number;
  sliderHeight: number;
  selected: NodeConfig | null;
};

export const state: IState = proxy({
  sliderWidth: 0,
  sliderHeight: 0,
  selected: null,
});

状态更新:

import { state } from “src/models”;
state.selected = e.item?.getModel() as NodeConfig;

状态消费:

import { useSnapshot } from “valtio”;
import { state } from “src/models”;

export const BarComponent = () => {
  const snap = useSnapshot(state);
  console.log(snap.selected)
}

当选中图谱节点时,由于 BarComponent 组件订阅了 selected 状态,该组件会自动更新。是不是非常简单?更多高级用法建议查阅官方文档。

函数缓存:提升重复计算性能

为什么需要函数缓存?设想一个计算量巨大的函数,其所在的组件又有多个状态频繁更新,如何确保该函数不被重复调用?你可能会想到 useMemouseCallback 等 React Hooks。这里我们介绍一种借鉴 React 内部 cache 方法的实现(React 官方尚未暴露此 API)。

其核心原理是:通过被缓存的函数 fn 及其参数列表构建一个 cacheNode 链表,基于链表最后一项的状态来存储函数 fn 与对应参数的计算结果。

代码位于 src/utils/cache

interface CacheNode {
  /**
   * 节点状态
   *  - 0:未执行
   *  - 1:已执行
   *  - 2:出错
   */
  s: 0 | 1 | 2;
  // 缓存值
  v: unknown;
  // 特殊类型(object,fn),使用 weakMap 存储,避免内存泄露
  o: WeakMap<Function | object, CacheNode> | null;
  // 基本类型
  p: Map<Function | object, CacheNode> | null;
}

const cacheContainer = new WeakMap<Function, CacheNode>();

export const cache = (fn: Function): Function => {
  const UNTERMINATED = 0;
  const TERMINATED = 1;
  const ERRORED = 2;

  const createCacheNode = (): CacheNode => {
    return {
      s: UNTERMINATED,
      v: undefined,
      o: null,
      p: null,
    };
  };

  return function () {
    let cacheNode = cacheContainer.get(fn);
    if (!cacheNode) {
      cacheNode = createCacheNode();
      cacheContainer.set(fn, cacheNode);
    }
    for (let i = 0; i < arguments.length; i++) {
      const arg = arguments[i];
      // 使用 weakMap 存储,避免内存泄露
      if (
        typeof arg === “function” ||
        (typeof arg === “object” && arg !== null)
      ) {
        let objectCache: CacheNode[“o”] = cacheNode.o;
        if (objectCache === null) {
          objectCache = cacheNode.o = new WeakMap();
        }
        let objectNode = objectCache.get(arg);
        if (objectNode === undefined) {
          cacheNode = createCacheNode();
          objectCache.set(arg, cacheNode);
        } else {
          cacheNode = objectNode;
        }
      } else {
        let primitiveCache: CacheNode[“p”] = cacheNode.p;
        if (primitiveCache === null) {
          primitiveCache = cacheNode.p = new Map();
        }
        let primitiveNode = primitiveCache.get(arg);
        if (primitiveNode === undefined) {
          cacheNode = createCacheNode();
          primitiveCache.set(arg, cacheNode);
        } else {
          cacheNode = primitiveNode;
        }
      }
    }
    if (cacheNode.s === TERMINATED) return cacheNode.v;
    if (cacheNode.s === ERRORED) {
      throw cacheNode.v;
    }
    try {
      const res = fn.apply(null, arguments as any);
      cacheNode.v = res;
      cacheNode.s = TERMINATED;
      return res;
    } catch (err) {
      cacheNode.v = err;
      cacheNode.s = ERRORED;
      throw err;
    }
  };
};

如何验证其效果?可以查看位于 src/__tests__/utils/cache.test.ts 的单元测试:

import { cache } from “src/utils”;

describe(“cache”, () => {
  const primitivefn = jest.fn((a, b, c) => {
    return a + b + c;
  });

  it(“primitive”, () => {
    const cacheFn = cache(primitivefn);
    const res1 = cacheFn(1, 2, 3);
    const res2 = cacheFn(1, 2, 3);
    expect(res1).toBe(res2);
    expect(primitivefn).toBeCalledTimes(1);
  });
});

从测试可以看出,尽管调用了两次 cacheFn,但由于入参相同,原始函数 primitivefn 仅执行了一次,第二次调用直接返回了缓存的结果。

项目中,在实现圆形背景动画时使用了缓存。因为该动画是绕圆周无限循环的,循环一周后,后续的计算与之前完全一致,无需重复计算坐标。相关代码位于 src/components/background/index.tsx

  const cacheGetPoint = cache(getPoint);
  let p = 0;
  const animate = () => {
    if (p >= 1) p = 0;
    const { x, y } = cacheGetPoint(p);
    ctx.clearRect(0, 0, 2 * clearR, 2 * clearR);
    createCircle(aCtx, x, y, circleR, “#fff”, 6);
    p += 0.001;
    requestAnimationFrame(animate);
  };
  animate();

分片渲染:应对海量数据绘制

你检查过页面元素吗?项目的背景图并非静态图片,而是通过 Canvas 实时绘制的!用 Canvas 绘制数量庞大的小圆点,会阻塞页面操作吗?当数据量极大时,确实会。你可以尝试将 NodeMargin 设置为 0.1,并移除调度器,改为同步绘制。当节点数量达到 500 万时,若不开启分片,在 MacBook Pro M1 上页面白屏时间约为 8.5 秒;而开启分片渲染后,页面不会白屏,背景图会从左到右逐步绘制,每个分片任务的执行时间控制在 16ms 左右。

分片渲染性能分析截图

分片调度器核心代码:

  const schduler = (tasks: Function[]) => {
    const DEFAULT_RUNTIME = 16;
    const { port1, port2 } = new MessageChannel();
    let isAbort = false;

    const promise: Promise<any> = new Promise((resolve, reject) => {
      const runner = () => {
        const preTime = performance.now();
        if (isAbort) {
          return reject();
        }
        do {
          if (tasks.length === 0) {
            return resolve([]);
          }
          const task = tasks.shift();
          task?.();
        } while (performance.now() - preTime < DEFAULT_RUNTIME);
        port2.postMessage(“”);
      };
      port1.onmessage = () => {
        runner();
      };
    });
    // @ts-ignore
    promise.abort = () => {
      isAbort = true;
    };
    port2.postMessage(“”);
    return promise;
  };

分片渲染的优势在于不阻塞用户操作,但会延长任务的总执行时间。是否启用需权衡数据量。需注意,如果单个分片的实际执行时间超过 16ms,仍会造成阻塞,并且任务会堆积,可能导致最终渲染状态与预期不符,因此合理拆分任务至关重要。

单元测试环境搭建

关于单测,这里不做深入展开。你可以运行 pnpm test 查看效果,测试环境已预先配置好。由于项目使用了 Canvas,需要模拟(Mock)浏览器环境。可以这样理解:我们的前端代码依赖浏览器环境及 API,但单测并非在真实浏览器中运行,因此需要模拟这些环境。项目中使用了 jsdomjest-canvas-mock 来模拟 windowCanvasWorker 等。更多细节推荐直接查阅 Jest 官网。

环境 Mock 示例 (src/setupTests.ts):

// jest-dom adds custom jest matchers for asserting on DOM nodes.
import “@testing-library/jest-dom”;

Object.defineProperty(URL, “createObjectURL”, {
  writable: true,
  value: jest.fn(),
});

class Worker {
  onmessage: () => void;
  url: string;
  constructor(stringUrl) {
    this.url = stringUrl;
    this.onmessage = () => {};
  }

  postMessage() {
    this.onmessage();
  }
  terminate() {}
  onmessageerror() {}
  addEventListener() {}
  removeEventListener() {}
  dispatchEvent(): boolean {
    return true;
  }
  onerror() {}
}
window.Worker = Worker;

自动化部署:GitHub Pages + Actions

对于前端项目,最终都需要部署。目前流行的是前后端分离,前端独立部署。对于不依赖后端的纯静态项目,我们可以借助 GitHub 提供的 GitHub Pages 服务实现自动化部署,CI/CD 只需配置对应的 Actions 工作流即可。在仓库的 Settings -> Pages 页面,选择承载页面的分支(如 gh-pages)即可完成部署。

GitHub Pages 设置界面截图

例如,项目中的 .github/workflows/gh-pages.yml 文件定义了:当 master 分支有代码推送时,会自动执行构建任务,并借助 peaceiris/actions-gh-pages@v3 将构建产物同步到 gh-pages 分支。

name: github pages

on:
  push:
    branches:
      - master # default branch

env:
  CI: false
  PUBLIC_URL: ‘/large-screen-visualization’

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - run: yarn
      - run: yarn build
      - name: Deploy
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: ./build

希望这篇结合了 React 状态管理、Canvas 绘图、性能优化与 开源实战 部署的详细指南,能帮助你高效构建属于自己的数据可视化大屏应用。如果你在实践过程中有更多心得,欢迎在 云栈社区 与更多开发者交流探讨。




上一篇:主流Linux远程连接工具对比:哪款更适合你的服务器管理需求?
下一篇:Qoder @database功能详解:JetBrains插件自动关联数据库Schema,提升后端开发效率
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 02:49 , Processed in 0.419462 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表