记录--原生 canvas 如何实现大屏?

news2024/11/17 0:33:41

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

可视化大屏该如何做?有可能一天完成吗?废话不多说,直接看效果,线上 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

本文转载于:

https://juejin.cn/post/7165564571128692773

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

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

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

相关文章

在线支付系列【13】微信支付之签名验签流程分析

有道无术&#xff0c;术尚可求&#xff0c;有术无道&#xff0c;止于术。 文章目录前言签名生成签名验证总结前言 在上篇文档中&#xff0c;我们简单实现了对接微信支付的几个接口。了解到wechatpay-apache-httpclient框架自动实现了签名和验签&#xff0c;接下来跟踪下源码&a…

LeetCode-136. 只出现一次的数字

目录题目分析哈希集位运算题目来源 https://leetcode.cn/problems/single-number/ 题目分析 题目有个条件可谓相当重要&#xff0c;即凡重复的元素最多重复一次&#xff08;原话&#xff1a;给定一个非空整数数组&#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个…

详解Mybatis-Plus中分页插件PaginationInterceptor, MybatisPlusInterceptor在SpringBoot中的使用

文章目录1. 描述1.1 MybatisPlusInterceptor1.2 InnerInterceptor2. 实现2.1 不带条件的分页查询2.2 带条件的分页查询2.3 简述Page类3. 注意事项1. 描述 1.1 MybatisPlusInterceptor 我们在开发的过程中&#xff0c;经常会遇到分页操作&#xff0c;其分为逻辑分页和物理分页…

Gif动态图片如何用静图制作?教你静图合成动图的方法

gif动图如何制作&#xff1f;相信对于gif动图大家都不陌生&#xff0c;在平时的聊天软件中、公众号文章中都可以看到。那么&#xff0c;要如何制作gif动图呢&#xff1f;下面&#xff0c;就给教大家两个在线gif制作&#xff08;https://www.gif.cn/&#xff09;的方法&#xff…

综合案例【商品管理系统-Java基础版】(附完整源码)

Java语言的一个超级简易的商品管理系统&#xff0c;适合初学者练手 源码包无法上传至资源&#xff08;blog已经超级完整啦&#xff09;如果还是需要完整源码src包可私分享 目录1 项目分析1.1 用户模块&#xff08;普通用户、管理员用户&#xff09;1.1.1前端系统&#xff08;普…

FluentReader为什么称之为高颜值的rss阅读器

Fluent Reader 这是一款 RSS 阅读器。对于很多年轻的网友来说&#xff0c;RSS 这个名词可能有点陌生。简单来说只要某个网站支持 RSS&#xff0c;你订阅了 RSS 后&#xff0c;一旦网站更新了内容&#xff0c;就会推送到你面前。这时候&#xff0c;我们就需要一款像 Fluent Rea…

全屋智能三国志

刚刚过去的春节假期&#xff0c;对我来说&#xff0c;是一个收集现实素材、感应社会变化的好机会。也确实发现了不少新趋势&#xff0c;一个结论是&#xff1a;智能家居正在酝酿新一轮的市场浪潮。以央视春晚作为切口&#xff0c;每年央视春晚的广告投放&#xff0c;都一定程度…

vscode 配置 plantuml

1、首先安装 plantuml 插件 2、安装 java 开发环境 如果是 mac 系统&#xff0c;直接执行 brew install java &#xff0c;然后按照下图执行下提示中的命令即可 如果是 windows 系统&#xff0c;需要去官网下载 java jdk&#xff0c;安装好之后添加运行路径到 path 中 https://…

基于 ShardingSphere 的分布式数据库负载均衡架构搭建实战

本文主要分为 3 部分&#xff0c;将依次介绍&#xff1a; 基于 ShardingSphere 的分布式数据库「负载均衡架构搭建」要点结合实际的「用户问题案例」&#xff0c;介绍引入「负载均衡」的影响介绍并展示 ShardingSphere 分布式数据库在云上的「一站式解决方案」 文章目录Shardi…

LED背光车载驱动IC 可支持48通道

特性电源电压范围-VDD:3.3V~5.5V-VLED:27V(max)48 个恒流输出通道- 通道恒流输出范围 0~50mA一 通道间电流输出偏差小于3%- 芯片间电流输出偏差小于 3%Low Knee Voltage:0.55V小于20mA0.6V 大于20mA最大支持4扫&#xff0c;内嵌行控制信号通道灰度实现-支持 PWM/PAMPWM驱动 -灰…

云帆文档管理系统版本更新说明:v4.6.0

一、新增及优化功能资料管理新增打包下载管理端增加资料查阅菜单&#xff0c;管理端和用户端用户有同样的查阅权限&#xff0c;方便其快速的查阅企业的资料信息前端下载文档格式添加下载水印&#xff0c;水印显示的是下载的用户和日期采购合同增加智能识别功能资料管理添加关键…

KPI考核系统实战之二:swagger框架

KPI考核系统实战之二&#xff1a;swagger框架一、asp.net core 3.1二、融合swagger1.Nuget安装Swashbuckle.AspNetCore2.Startup.cs 注册Swagger服务&#xff0c;使用swagger中间件一、asp.net core 3.1 使用Visual Studio 2022&#xff0c;搭建asp.net core weiapi开发平台&a…

煤矿皮带运行状态监测预警系统 yolov7

煤矿皮带运行状态监测预警系统通过yolov7网络模型深度学习技术&#xff0c;自动对传输皮带运行状态进行实时监测。当监测到皮带撕裂、跑偏、异物、堆煤等异常情况时&#xff0c;立即抓拍预警及时停止皮带同步回传违规信息到后台。介绍Yolo算法之前&#xff0c;首先先介绍一下滑…

PushKit/Callkit使用经验

前言&#xff1a;如果要求使用这两种库&#xff0c;请在查询资料并自己尝试后&#xff0c;多参考苹果官方的API文档&#xff1a;PushKit&#xff1a;https://developer.apple.com/documentation/pushkit?languageobjcCallKit&#xff1a;https://developer.apple.com/document…

人工智能与模式识别的意义(模式识别与图像处理课程作业)

人工智能与模式识别的意义(模式识别与图像处理课程作业一、 人工智能的意义二、 模式识别的意义2.1、文字识别2.2、语音识别2.3、指纹识别2.4、遥感2.5、医学诊断1、语音识别技术2、生物认证技术3、数字水印技术一、 人工智能的意义 人工智能的发送对于我们社会的各个方面都具有…

Word页面中四个直角

文章目录1、四个直角1&#xff09;代表页边距2&#xff09;页边距的设置3&#xff09;打开或关闭“裁剪标记”2、“裁剪标记”与图片1&#xff09;插入图片超过这个角能打印显示出来吗&#xff1f;3、“裁剪标记”与表格1、四个直角 1&#xff09;代表页边距 页面中的四个角代…

【LoRa网关以及LoRa自组网】以“有人物联网”为例

【LoRa网关以及LoRa自组网】以“有人物联网”为例0.参考资料1. LoRa 自组网协议的理解1.1【LoRa模块WH-L101-L-P-H10 】1.2【LoRa网关设置】1.3【节点、网关、服务器通讯】1.4【一些注意事项】1.5【专业名词】2.【LoRa点对点通讯 】LoRa网关可以实现多个LoRa节点的数据采集&…

咖啡商城|基于Springboot+Vue前后端分离咖啡商城系统

作者主页&#xff1a;编程指南针 作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、掘金特邀作者、多年架构师设计经验、腾讯课堂常驻讲师 主要内容&#xff1a;Java项目、毕业设计、简历模板、学习资料、面试题库、技术互助 收藏点赞不迷路 关注作者有好处 文末获取源…

微软开源的 5 个 yyds 课程!

微软在 GitHub 开源了五大课程&#xff0c;面向计算机专业或者入门编程的同学。分别是 Web 开发课程、机器学习课程、物联网课程、数据分析课程、Bash 课程。01为初学者准备的 Web开发课程这个 65k Star 的 Web 开发课程由 Microsoft 的 Azure Cloud Advocates 出品&#xff0…

prometheus登录认证

目标 登录Prometheus的9090端口页面的时候&#xff0c;需要输入用户名和密码&#xff0c;才能进入Prometheus页面。 设置密码 Prometheus配置密码不能是明文&#xff0c;必须经过bcrypt程序对密码进行Hash处理。 vim gen-pass.py 内容如下&#xff1a; import getpass import…