记录--原生 canvas 如何实现大屏?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
前言
可视化大屏该如何做?有可能一天完成吗?废话不多说,直接看效果,线上 Demo 地址 lxfu1.github.io/large-scree…。
看完这篇文章(这个项目),你将收获:
- 全局状态真的很简单,你只需 5 分钟就能上手
- 如何缓存函数,当入参不变时,直接使用缓存值
- 千万节点的图如何分片渲染,不卡顿页面操作
- 项目单测该如何写?
- 如何用 canvas 绘制各种图表,如何实现 canvas 动画
- 如何自动化部署自己的大屏网站
效果
实现
项目基于 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"
},
启动
- clone项目
git clone https://github.com/lxfu1/large-screen-visualization.git
- pnpm 安装
npm install -g pnpm
- 启动:
pnpm start
即可,建议配置 alias ,可以简化各种命令的简写 eg:p start
,不出意外的话,你可以通过 http://localhost:3000/ 访问了 - 测试:
p test
- 构建:
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 如何实现大屏?的更多相关文章
- 原生js实现头像大屏随机显示
效果如下图所示: 一.html部分 <div class="myContainer"> <ul> <li class="first" ...
- echarts解决一些大屏图形配置方案汇总
本文主要记录使用echarts解决各种大屏图形配置方案. 1.说在前面 去年经常使用echarts解决一些可视化大屏项目,一直想记录下使用经验,便于日后快速实现.正好最近在整理文档,顺道一起记录在博客 ...
- 利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果
利用canvas阴影功能与双线技巧绘制轨道交通大屏项目效果 前言 近日公司接到一个轨道系统的需求,需要将地铁线路及列车实时位置展示在大屏上.既然是大屏项目,那视觉效果当然是第一重点,咱们可以先来看看项 ...
- 开源大屏工具 DataGear 的使用
记录一款好用的大屏工具,DataGear,官方标记为"开源免费的数据可视化分析平台". 其支持的数据集可以为SQL或HTTP API等,SQL支持MySQL等关系型数据库及Hive ...
- 详细剖析pyecharts大屏的Page函数配置文件:chart_config.json
目录 一.问题背景 二.揭开json文件神秘面纱 三.巧用json文件 四.关于Table图表 五.同步讲解视频 5.1 讲解json的视频 5.2 讲解全流程大屏的视频 5.3 讲解全流程大屏的文章 ...
- 第六章 大数据,6.3 突破传统,4k大屏的沉浸式体验(作者: 彦川、小丛)
6.3 突破传统,4k大屏的沉浸式体验 前言 能够在 4K 的页面上表演,对设计师和前端开发来说,即是机会也是挑战,我们可以有更大的空间设计宏观的场景,炫酷的转场,让观众感受影院式视觉体验,但是,又必 ...
- 使用webgl(three.js)搭建3D智慧园区、3D大屏,3D楼宇,智慧灯杆三维展示,3D灯杆,web版3D,bim管理系统——第六课
前言: 今年是建国70周年,爱国热情异常的高涨,为自己身在如此安全.蓬勃发展的国家深感自豪. 我们公司楼下为庆祝国庆,拉了这样的标语,每个人做好一件事,就组成了我们强大的祖国. 看到这句话,深有感触, ...
- Qt编写项目作品大全(自定义控件+输入法+大屏电子看板+视频监控+楼宇对讲+气体安全等)
一.自定义控件大全 (一).控件介绍 超过160个精美控件,涵盖了各种仪表盘.进度条.进度球.指南针.曲线图.标尺.温度计.导航条.导航栏,flatui.高亮按钮.滑动选择器.农历等.远超qwt集成的 ...
- 使用DataV制作实时销售数据可视化大屏(实验篇)
课时1:背景介绍 任务说明 ABC是一家销售公司,其客户可以通过网站下单订购该公司经营范围内的商品,并使用信用卡.银行卡.转账等方式付费.付费成功后,ABC公司会根据客户地址依据就近原则选择自己的货仓 ...
- 15分钟构建超低成本数据大屏:DataV + DLA
第一步:准备低成本存储的业务数据和DLA表 OSS(https://www.aliyun.com/product/oss)是云上低成本数据存储的优选方案 DLA(https://www.aliyun. ...
随机推荐
- CF1822F Gardening Friends
题目链接 题解 知识点:树的直径,枚举. 考虑一个结论:树上任意点的最远点一定是树的直径的端点. 那么对于一个根节点,只要知道了树的直径,那么我们就可以立即得到最远距离,即乘 \(k\) 树的价值. ...
- NC51222 Strategic game
题目链接 题目 题目描述 Bob enjoys playing computer games, especially strategic games, but sometimes he cannot ...
- 【Unity3D】Photon环境搭建
1 简介 Photon 是一个泛用性的 ScoketServer 套装软件,可用于多人在线游戏.聊天室.大厅游戏,并同时支持 Windows.Unity3D.iOS.Android.Flash 等 ...
- 【Android】使用ContentProvider实现跨进程通讯
1 前言 ContentProvider 即内容提供器,是 Android 四大组件之一,为 App 存取数据提供统一的对外接口,让不同的应用之间可以共享数据. 如图,Server 端通过 C ...
- Vuex中的核心方法
Vuex中的核心方法 Vuex是一个专为Vue.js应用程序开发的状态管理模式,其采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.每一个Vuex应用的核心就是 ...
- Swoole从入门到入土(20)——WebSocket服务器[协程版本]
本篇让我们先用一段示例代码开路: <?php Co\run(function () { $server = new Co\Http\Server('0.0.0.0', 9501, false); ...
- Delphi 实现刘谦春晚魔术
看了博友的C# 实现刘谦春晚魔术很好,改成了delphi版的. 1 program Project1; 2 3 {$APPTYPE CONSOLE} 4 {$R *.res} 5 6 uses 7 S ...
- win32 - 将线程重定向到另一个函数(附带Suspend的解释)
Suspend: 挂起指定的线程 备注:不要永远挂起线程, 因为在Win32中,进程堆是线程安全的对象,并且由于在不访问堆的情况下很难在Win32中完成很多工作,因此在Win32中挂起线程极有可能使进 ...
- LibModbus库开发笔记(一):libmodbus库介绍、编译和基础工程模板
前言 本文章讲解libmodbus. libModbus介绍 libmodbus是一个免费软件库,可根据Modbus协议发送/接收数据.该库用C编写,并支持RTU(串行)和TCP(以太网) ...
- java+mysql数据库实现的学生管理系统
说明: java+mysql数据库实现的学生管理系统 功能 实现增加学生.删除学生.修改学生.学生列表.查询学生功能 截图: 开发工具/技术 java eclipse 价格:50元,有需要联系 微信 ...