原生 canvas 如何实现大屏?

news2025/8/13 13:15:10

前言

可视化大屏该如何做?有可能一天完成吗?废话不多说,直接看效果,线上 Demo 地址 lxfu1.github.io/large-scree…。

看完这篇文章(这个项目),你将收获:

  1. 全局状态真的很简单,你只需 5 分钟就能上手
  2. 如何缓存函数,当入参不变时,直接使用缓存值
  3. 千万节点的图如何分片渲染,不卡顿页面操作
  4. 项目单测该如何写?
  5. 如何用 canvas 绘制各种图表,如何实现 canvas 动画
  6. 如何自动化部署自己的大屏网站

实现

项目基于 Create React App --template typescript搭建,包管理工具使用的 pnpm ,pnpm 的优势这里不多介绍(快+节省磁盘空间),之前在其它平台写过相关文章,后续可能会搬过来。由于项目 package.json 里面有限制包版本(最新版本的 G6 会导致 OOM,官方短时间能应该会修复),如果使用的 yarn 或 npm 的话,改为对应的 resolutions 即可。

 "pnpm": {
    "overrides": {
      "@antv/g6": "4.7.10"
    }
  }
复制代码
"resolutions": {
  "@antv/g6": "4.7.10"
},
复制代码

启动

  1. clone项目
git clone https://github.com/lxfu1/large-screen-visualization.git
复制代码
  1. pnpm 安装 npm install -g pnpm
  2. 启动: pnpm start 即可,建议配置 alias ,可以简化各种命令的简写 eg:p start,不出意外的话,你可以通过 http://localhost:3000/ 访问了
  3. 测试:p test
  4. 构建:p build

强烈建议大家先 clone 项目!

分析

全局状态

全局状态用的 valtio ,位于项目 src/models目录下,强烈推荐。

优点:数据与视图分离的心智模型,不再需要在 React 组件或 hooks 里用 useState 和 useReducer 定义数据,或者在 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 状态,所以该组件会进行更新。有没有感觉非常简单?一些高级用法建议大家去官网查看,不再展开。

函数缓存

为什么需要函数缓存?当然,在这个项目中函数缓存比较鸡肋,为了用而用,试想,如果有一个函数计算量非常大,组件内又有多个 state 频繁更新,怎么确保函数不被重复调用呢?可能大家会想到 useMemo``useCallback等手段,这里要介绍的是 React 官方的 cache 方法,已经在 React 内部使用,但未暴露。实现上借鉴(抄袭)ReactCache,通过缓存的函数 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);
  });
});
复制代码

可以看出,即使我们调用了 2 次 cacheFn,由于入参不变,fn 只被执行了一次,第二次直接返回了第一次的结果。

项目里面在做 circle 动画的时候使用了,因为该动画是绕圆周无限循环的,当循环过一周之后,后的动画和之前的完全一致,没必要再次计算对应的 circle 坐标,所以我们使用了 cache ,位于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 ,同时把 schduler 调用去掉,直接改为同步绘制。当节点数量在 500 W 的时候,如果没有开启切片,页面白屏时间在 MacBook Pro M1 上白屏时间大概是 8.5 S;开启分片渲染时页面不会出现白屏,而是从左到右逐步绘制背景图,每个任务的执行时间在 16S 左右波动。

  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 也会造成阻塞,并且会堆积,并且任务执行的时候没有等,最终渲染状态和预期不一致,所以 task 的拆分也很重要。

单测

这里不想多说,大家可以运行 pnpm test看看效果,环境已经搭建好;由于项目里面用到了 canvas 所以需要 mock 一些环境,这里的 mock 可以理解为“我们前端代码跑在浏览器里运行,依赖了浏览器环境以及对应的 API,但由于单测没有跑在浏览器里面,所以需要 mock 浏览器环境”,例如项目里面设置的 jsdom、jest-canvas-mock 以及 worker 等,更多推荐直接访问 jest 官网。

// 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;
复制代码

自动化部署

开发过项目的同学都知道,前端编写的代码最终是要进行部署的,目前比较流行的是前后端分离,前端独立部署,通过 proxy 的方式请求后端服务;或者是将前端构建产物推到后端服务上,和后端一起部署。如何做自动化部署呢,对于一些不依赖后端的项目来说,我们可以借助 github 提供的 gh-pages 服务来做自动化部署,CI、CD 仅需配置对应的 actions 即可,在仓库 settings/pages 下面选择对应分支即可完成部署。

例如项目里面的.github/workflows/gh-pages.yml,表示当 master 分支有代码提交时,会执行对应的 jobs,并借助 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
复制代码

总结

写文档不易,如果看完有收获,记得给个小星星!欢迎大家 PR!

  • Ant Design Charts
  • 示例仓库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/14986.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

多篇《Nature》和《Science》关于马约拉纳费米子的研究论文近日被撤稿

马约拉纳费米子&#xff08;英语&#xff1a;Majorana fermion&#xff09;是一种假设粒子&#xff0c;它的反粒子就是它本身&#xff0c;1937年&#xff0c;埃托雷马约拉纳发表论文假想这种粒子存在&#xff0c;因此而命名。与之相异&#xff0c;狄拉克费米子&#xff0c;指的…

当大火的文图生成模型遇见知识图谱,AI画像趋近于真实世界

导读 用户生成内容&#xff08;User Generated Content&#xff0c;UGC&#xff09;是互联网上多模态内容的重要组成部分&#xff0c;UGC数据级的不断增长促进了各大多模态内容平台的繁荣。在海量多模态数据和深度学习大模型的加持下&#xff0c;AI生成内容&#xff08;AI Gen…

(七)文件——PHP

文章目录第七章 文件1 文件包含**1.1 include()函数****1.2 require()函数**2 文件的读取和写入**2.1 文件模式****2.2 文件读取****2.3 文件写入**3 文件上传3.1 创建表单3.2 创建脚本3.3 实例第七章 文件 1 文件包含 您可以在服务器执行PHP文件之前将其内容包含到另一个PHP…

支付宝"手机网站支付"主域名申请了,二级域名还要申请吗

微信商城小程序里可以用支付宝付款吗&#xff1f;当然可以了&#xff0c;不过需要去支付宝官方网站去申请一个接口&#xff0c;注意选对产品&#xff0c;支付宝提供了很多种接口&#xff0c;微信里要用到的支付宝付款属于“手机网站支付”这个产品&#xff0c;重要的事情说三遍…

一文讲解,Linux内核——内存管理(建议收藏)

一.内存的基础知识&#xff1a; 1.写程序到程序运行的过程&#xff1a; &#xff08;1&#xff09;编译&#xff1a;由编译程序将用户代码编译成若干个目标模块&#xff08;把高级语言翻译成机器语言&#xff09; &#xff08;2&#xff09;链接&#xff1a;由链接程序将编译…

头歌平台-MongoDB 之滴滴、摩拜都在用的索引

第1关&#xff1a;了解并创建一个简单索引 > use test switched to db test > db.student.insert([{_id:1,name:"王小明",age:15,score:90},{_id:2,name:"周晓晓",age:18,score:86},{_id:3,name:"王敏",age:20,score:96},{_id:4,name:&qu…

kudu集群数据节点(tserver)扩容(缩容)

背景&#xff1a; 4个数据节点有数据倾斜&#xff0c;rebalance后依旧如此&#xff0c;检查分片数量和分布情况也是均衡的。最后发现相同的分片在其中一个节点存储消耗的磁盘资源比其他节点要大很多&#xff0c;导致了这个节点磁盘消耗较快。于是决定更换节点的磁盘 操作计划如…

【freeRTOS】操作系统之一-任务调度

一. 任务调度 ​ FreeRTOS操作系统支持三种调度方式&#xff1a;抢占式调度&#xff0c;时间片调度和合作式调度。实际应用主要是抢占式调度和时间片调度&#xff0c;合作式调度用到的很少。 挂起/阻塞/就绪/运行 三种基本状态 进程在运行中不断地改变其运行状态。通常&…

智能网联汽车网络安全攻击与防御技术概述

作者 | 王博文 上海控安可信软件创新研究院研究员 来源 | 鉴源实验室 01 引言 在汽车电动化、网联化、智能化和共享化等新四化的发展趋势下&#xff0c;智能网联汽车&#xff08;Intelligent Connected Vehicles&#xff0c;ICVs&#xff09;已经是新时代的必然产物。在智能网…

【学习笔记】浅谈闵可夫斯基和

学这东西主要是 这道题 要用 233 定义&#xff1a;给定两个凸包AAA,BBB&#xff0c;定义C{ab∣a∈A,b∈B}C\{ab|a\in A,b\in B\}C{ab∣a∈A,b∈B} &#xff0c;其中aaa,bbb均为坐标。 不好意思图是嫖的 首先我们感性认识一下。可以直接将一个凸包的顶点换成另一个凸包&#x…

实现登陆模块时Cookie,Session,Token的理解

目录引言一、简化版登陆&#xff08;不保存登录状态&#xff09;二、Session 登陆三、CookieSession 实现登录四、Token登陆引言 先思考问题&#xff1a; 1、如何实现登陆&#xff1f; &#xff08;1&#xff09;表单输入账号密码 &#xff08;2&#xff09;后台数据库验证 …

治愈系书单|林曦《只生欢喜不生愁》

《只生欢喜不生愁》是水墨画家林曦的艺术生活随笔集&#xff0c;谈艺术之道&#xff0c;论无用之美。      全书按主题分为六辑&#xff1a;“艺可通道”“观照世间”“得自在禅”“心安之所”“写给时间”“赤子之心”&#xff0c;文字轻盈、意境优雅。林曦以手艺人之道&a…

Java#13(String Builder介绍及其常用办法)

目录 一.String Builder 1.理解:可以将String Builder当作一个容器,创建之后的内容是可以变的 2.作用:可以提高字符串的操作效率 二. StringBuilder中的常用方法 1.public StringBuilder append (任意类型) 添加数据&#xff0c;并返回对象本身 2.public StringBuild…

Keysight是德科技e5063A网络分析仪-安泰测试

Keysight E5063A ENA 是一款经济适用的台式矢量网络分析仪&#xff0c;可用于测试简单的无源元器件&#xff0c;例如频率最高达到 18 GHz 的天线、滤波器、电缆或连接器。 作为业界闻名的 ENA 系列中的一员&#xff0c;它传承了一致的测量框架&#xff0c;以便提高效率和生产率…

大学生静态HTML鲜花网页设计作品 DIV布局网上鲜花介绍网页模板代码 DW花店网站制作成品 web网页制作与实现

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

RabbitMQ初步到精通-第二章-RabbitMQ介绍

第二章 RabbitMQ介绍 1、RabbitMQ简介 RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。RabbitMQ最初起源于金融系统&#xff0c;用于在分布式系统中存储转发消息&#xff0c;在易用性、扩展性、高可用性等方面表现不俗。 rabbit- 兔子&#xff0c;兔子的特点是什么? 轻…

图解https读书笔记

7.确保Web安全的HTTPS 7.2 HTTP 加密 认证 完整性保护 HTTPS https http ssL(Secure socket layer)client: 使用servcer端的publickey对消息加密-----》server&#xff1a;收到消息&#xff0c;用privatekey进行解密参考&#xff1a;彻底搞懂HTTPS的加密原理_峰子2012的博…

VS+QT错误集合

主要是使用VSQT时遇到等一些问题&#xff0c;现在使用等是VS2015&#xff08;专业版&#xff09; QT5.13.1 这章主要解决遇到的无法解析等外部符号这类问题 1、LNK2001 无法解析的外部符号 "public: virtual struct QMetaObject const * __cdecl Process::metaObject…

【分析笔记】Linux 4.9 backlight 子系统分析

相关信息 内核版本&#xff1a;Linux version 4.9.56 驱动文件&#xff1a;lichee\linux-4.9\drivers\video\backlight\backlight.c 驱动作用 对上&#xff0c;面对应用层提供统一的设备节点入口同级&#xff0c;面对驱动层提供设备驱动加载卸载通知事件&#xff0c;以及背光…

闲人闲谈PS之三十二——业务工程预算和PS成本计划

惯例闲话&#xff1a;年底&#xff0c;又要开始忙了&#xff0c;今年这一年&#xff0c;收获还不错&#xff0c;至少规划了第三年实现的目标——工程行业彩虹图&#xff0c;在今年居然奇迹般的实现了&#xff0c;看样子闲人还是保守了。还是应验了那句话&#xff0c;只要标准化…