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

前言

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

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

记录--原生 canvas 如何实现大屏?的更多相关文章

  1. 原生js实现头像大屏随机显示

    效果如下图所示: 一.html部分 <div class="myContainer"> <ul> <li class="first" ...

  2. echarts解决一些大屏图形配置方案汇总

    本文主要记录使用echarts解决各种大屏图形配置方案. 1.说在前面 去年经常使用echarts解决一些可视化大屏项目,一直想记录下使用经验,便于日后快速实现.正好最近在整理文档,顺道一起记录在博客 ...

  3. 利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果

    利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果 前言 近日公司接到一个轨道系统的需求,需要将地铁线路及列车实时位置展示在大屏上.既然是大屏项目,那视觉效果当然是第一重点,咱们可以先来看看项 ...

  4. 开源大屏工具 DataGear 的使用

    记录一款好用的大屏工具,DataGear,官方标记为"开源免费的数据可视化分析平台". 其支持的数据集可以为SQL或HTTP API等,SQL支持MySQL等关系型数据库及Hive ...

  5. 详细剖析pyecharts大屏的Page函数配置文件:chart_config.json

    目录 一.问题背景 二.揭开json文件神秘面纱 三.巧用json文件 四.关于Table图表 五.同步讲解视频 5.1 讲解json的视频 5.2 讲解全流程大屏的视频 5.3 讲解全流程大屏的文章 ...

  6. 第六章 大数据,6.3 突破传统,4k大屏的沉浸式体验(作者: 彦川、小丛)

    6.3 突破传统,4k大屏的沉浸式体验 前言 能够在 4K 的页面上表演,对设计师和前端开发来说,即是机会也是挑战,我们可以有更大的空间设计宏观的场景,炫酷的转场,让观众感受影院式视觉体验,但是,又必 ...

  7. 使用webgl(three.js)搭建3D智慧园区、3D大屏,3D楼宇,智慧灯杆三维展示,3D灯杆,web版3D,bim管理系统——第六课

    前言: 今年是建国70周年,爱国热情异常的高涨,为自己身在如此安全.蓬勃发展的国家深感自豪. 我们公司楼下为庆祝国庆,拉了这样的标语,每个人做好一件事,就组成了我们强大的祖国. 看到这句话,深有感触, ...

  8. Qt编写项目作品大全(自定义控件+输入法+大屏电子看板+视频监控+楼宇对讲+气体安全等)

    一.自定义控件大全 (一).控件介绍 超过160个精美控件,涵盖了各种仪表盘.进度条.进度球.指南针.曲线图.标尺.温度计.导航条.导航栏,flatui.高亮按钮.滑动选择器.农历等.远超qwt集成的 ...

  9. 使用DataV制作实时销售数据可视化大屏(实验篇)

    课时1:背景介绍 任务说明 ABC是一家销售公司,其客户可以通过网站下单订购该公司经营范围内的商品,并使用信用卡.银行卡.转账等方式付费.付费成功后,ABC公司会根据客户地址依据就近原则选择自己的货仓 ...

  10. 15分钟构建超低成本数据大屏:DataV + DLA

    第一步:准备低成本存储的业务数据和DLA表 OSS(https://www.aliyun.com/product/oss)是云上低成本数据存储的优选方案 DLA(https://www.aliyun. ...

随机推荐

  1. 如何在.NET Core中为gRPC服务设计消息

    如何在.NET Core中为gRPC服务设计消息 使用协议缓冲区规范定义gRPC服务非常容易,但从需求转换为.NET Core,然后管理服务的演变时,需要注意几件事. 创建gRPC服务的核心是.pro ...

  2. Power BI 4 DAY

    目录 数据化结构 其他数据结构 列表嵌套列表 记录嵌套列表 M函数计算方式 运算符 爬取网页 数据化结构 其他数据结构 复合数据结构的列表 let source = { 1, //数值 "B ...

  3. 遍历用for还是foreach?

    遍历用for还是foreach?这篇文章帮你轻松选择! 在编程的世界里,我们经常需要对数据进行循环处理,常用的两种方法就是:for循环和foreach循环.想象你站在一条装满宝贝的传送带前,你要亲手检 ...

  4. ORA-39087: Directory Name Is Invalid

    说明 有时我们在Oracle数据库服务器执行expdp/impdp过程中会碰到这个错误:ORA-39087: Directory Name Is Invalid,意思是我们指定的directory参数 ...

  5. OCP试题解析之053-61 RMAN set command id to

    61.You frequently have multiple RMAN sessions running, and you want to be able to easily identify ea ...

  6. cronet 的简单学习

    官方的解释 "Cronet is the networking stack of Chromium put into a library for use on mobile. This is ...

  7. 以二进制文件安装K8S之环境准备

    为了k8s集群能正常运行,需要先完成4项准备工作: 1.关闭防火墙 2.禁用SeLinux 3.关闭Swap 4.安装Docker 关闭防火墙 # 查看防火墙状态 getenforce #关闭防火墙, ...

  8. 【Java复健指南13】OOP高级04【告一段落】-四大内部类

    四大内部类 一个类的内部又完整的嵌套了另一个类结构. class Outer{ //外部类 class lnner{ //内部类 } } class Other{//外部其他类 } 被嵌套的类称为内部 ...

  9. Spark任务性能调优总结

    一.shuffle调优 大多数Spark作业的性能主要就是消耗在了shuffle环节,因为该环节包含了大量的磁盘IO.序列化.网络数据传输等操作.因此,如果要让作业的性能更上一层楼,就有必要对shuf ...

  10. 解决网页无法复制粘贴选中的问题 显示vip无法复制解决方案

    方法:先是按F12打开控制台点击console输入以下代码!!!! 解决网页禁止鼠标右键,无法被选中的 第一种: javascript:(function() { function R(a){ona ...