编者注:今天呢我们请来了 @有马 同学为我们分享他在做某数据可视化大屏项目的时候使用服务端渲染大屏动画的经验。说到服务端渲染大家一般都想到 Vue 的 SSR 或者 React 的同构吧,不过动画也是可以在服务端渲染的哦!所以让我们赶快进入正文看看到底是怎么实现的吧~


介绍

ThinkJS 是一个基于 koa@2.0 的企业级服务端开发框架,本项目中除基本的 HTTP 服务外,还使用了定时任务和 websocket 功能。

Sprite.js 是一个跨平台的 2D 绘图对象模型库,它支持 Web、Node、桌面应用和微信小程序的等多端图形绘制及动画实现。Sprite.js 使用 node-canvas 进行服务端渲染,这意味着我们可以在 node 环境中使用 Sprite.js,并将绘制好的图形保存成 png,或将动画保存成 gif。在本文中的项目中主要使用了以下特性:

  • Scene(场景):sprite.js 通过创建场景 scene 实现 layer管理;
  • Layer(图层):每层 layer 是一个封装过的 canvas 2D 对象;
  • Sprite(精灵):一个拥有盒模型的可渲染对象。sprite.js 默认支持的精灵类型有四种,分别是Sprite、Label、Path和Group,其中Sprite是最基础的精灵;

?️ 疑问

为什么进行 canvas 服务端渲染呢?

本项目的需求是实现峰值每小时百万级的实时数据的大屏展示,为了能达到最好的展示效果,并且能回溯历史态势,我们决定使用前端、服务端代码同构,前端进行实时数据的动画展示, 服务端同时渲染数据攻击路径,具体策略如下:

  • 服务端作为 websocket 客户端,接收 websocket 上游的数据,使用 sprite.js 绘制图像,通过 ThinkJS 定时任务拍快照,并将图片上传到 CDN 后保存 URL;
  • 同时,服务端也作为 websocket 服务端,把上游的数据过滤后发送给前端,前端将接收到的数据通过sprite.js 实时绘制到 canvas 上。
  • 前端回溯历史态势时,需请求服务端取得历史快照。服务端将请求时间内的快照合并为一张,上传到 CDN后将URL返回给前端,并由前端绘制到 canvas 上。

? 开发前的爬坑之旅

在实现这套方案的过程中爬了不少坑,其中最大的坑是 node-canvas 挖的?,爬坑的路上,我一度弄挂了服务器(幸亏只是个docker容器)。

安装 node-canvas

node-canvas 是一个使用 Cairo 支持的 Node.js 环境的 canvas 实现,打开它的开发者列表页面,你会看到一个熟悉的名字 TJ Holowaychuk。目前遇到的这几个问题也是在多次更换服务器的过程中发现的,希望大家留意,免得以后被坑哭。

缺少预编译的二进制文件

node-canvas 只有在 node 服务端才会用到,所以 sprite.js 的依赖中没有添加它,需要我们手动执行 npm i canvas@next 安装到项目中,默认会安装最新版本,安装时它会根据系统架构决定在预编译项目中下载相应的二进制文件,如果你遇到了图1所示错误,有两种解决方法:

  1. 编译安装 node-canvas ,官方文档上写清楚了不同操作系统编译需要的依赖;
  2. 安装最近有预编译二进制文件的版本,目前是 canvas@2.0.0-alpha.14;

缺少 GLIBC_2.14

在解决完上个问题后你可能还会遇到这个问题

Error: /lib64/libc.so.6: version `GLIBC_2.14` not found
复制代码

这表示服务器操作系统上没有 GLIBC_2.14 的库,先了解下 GLIBC 是什么:

GLIBC是GNU发布的libc库,即C运行库。GLIBC是Linux系统中最底层的API,几乎其它任何运行库都会依赖于GLIBC。GLIBC除了封装Linux操作系统所提供的系统服务外,它本身也提供了许多其它一些必要功能服务的实现。

看完这段介绍,你就应该明白你即将面对的是什么级别的依赖缺失,去搜一下相关词条,多少人因为它重装了系统。

查看系统内核是否支持 GLIBC_2.14 可以用这条命令

strings /lib64/libc.so.6 | grep GLIBC

如果结果中确实没有 GLIBC_2.14 关键字,可以尝试以下两种方式解决这个问题:

  1. 在你使用的操作系统上添加 GLIBC 的源,然后安装对应版本的 GLIBC;
  2. 选择一个 GLIBC 版本 >= 2.14 的操作系统,如 CentOS 7。

如果没有找到服务器系统内核对应的源,也不要尝试编译安装这个库,运维的同事说有些老版本内核就不支持 GLIBC_2.14。然后请读一下下面这句话:

由于 GLIBC 囊括了几乎所有的 UNIX 通行的标准,可以想见其内容包罗万象。而就像其他的 UNIX 系统一样,其内含的档案群分散于系统的树状目录结构中,像一个支架一般撑起整个操作系统。

所以最好的方式还是直接用支持的操作系统。

缺少字体文件

在安装好 node-canvas 后,可以使用下面这段代码进行测试。如果输出图片上文字显示为下图所示的长方形,这表示你使用的系统缺少字体文件。碰巧你又有渲染文本的需求,就需要解决这个问题。

一般 PC 上会有很多字体文件,但没有界面的服务器环境可能会缺少字体文件,因此需要至少安装一种字体,操作方法可以参考这篇文章。操作完后运行下面的代码,会生成一张图片,如果能正确显示文字说明成功安装了字体文件。

// label.js
const {Scene,Label} = require('spritejs');
const fs = require('fs');
const writeFileAsync = think.promisify(fs.writeFile, fs);(function () {// 创建scene和layerconst scene = new Scene('#paper', {resolution: [1200, 1200]});const fglayer = scene.layer('fglayer');// 创建label并设置属性const text1 = new Label('Hello World !');text1.attr({anchor: [0.5, 0.5],pos: [600, 600],font: 'bold 48px Arial',color: '#ffdc45',});// 将label添加到layer上,并将将canvas存为图片fglayer.append(text1);await fglayer.prepareRender();await writeFileAsync('spritejs.png', fglayer.canvas.toBuffer());
}());
复制代码

? 服务端渲染

服务端渲染 canvas 的关键操作是图片的输出和合并,理解并灵活运用这两个过程,能够满足大部分 canvas 服务端渲染的场景。

图片输出

本文项目的方案,服务端得到新数据后创建 sprite 并添加到 layer 上,构造出的 sprite 只应该完成一个任务,就是在 layer 留下图像,然后就删除掉(如果不删掉内存会吃不消的)。要完成这个过程需要重写 layer 上的 clearContext 方法,这样才能保留 sprite 绘制的图案。

// 重写 clearContext 方法确保 sprite,label,path,等元素移除后保留图像
layer.clearContext = () => {}// 通过数据生成新的 sprite 元素
const sprite = drawSomething(data);
// 绘制到 layer 上
layer.append(sprite);
// 确保 sprite 绘制到 layer 上后
await layer.prepareRender();
// 将 sprite 元素移除,因为重写了 clearContext 方法,移除后图像仍在 layer 上
sprite.remove();// 如果要清空 layer
const {width, height} = layer.context.canvas;
layer.context.clearRect(0, 0, width, height);
复制代码

sprite.js 的 scene 对象上是有快照方法的,它对当前的 scene 截屏并返回 canvas 对象,我们可以在这个 canvas 对象上调用 toBuffer 方法获得图像二进制数据,然后使用 node.js 中 fs 模块提供的同步写方法生成一张 png 图。

const canvas = await scene.snapshot()
await fs.writeFile('snap.png', canvas.toBuffer())
复制代码

如果不想对整个 scene 拍快照,只想对特定的某个 layer 拍照快,可以通过 layer 获得 canvas 对象,然后使用同样的方式对layer进行拍摄快照。

async function snapshot(layer) {await layer.prepareRender();return layer.canvas.toBuffer();
};
复制代码

快照图片量很大的时候,需要定时将快照上传到 cdn 或者单独的文件服务器,然后在数据库中保存图片的 url。这个过程用到了 ThinkJS 的定时任务,可以在 src/config/crontab.js 中添加如下配置,然后编写对应的处理方法。如果想确保在某个时间进行定时任务,例如在 5 分钟整数倍时执行任务,可以设一个更细粒度的定时器,然后在处理方法中判断,如果不是 5 分钟的倍数则不执行。

// src/config/crontab.js
module.exports = [{enable: true,interval: '1m', // 每1分钟执行一次handle: 'crontab/snapshot'}
];// src/controller/crontab.js
module.exports = class extends think.Controller {async snapshotAction() {// 拒绝非定时任务启动if (!this.isCli) return;const now = new Date();// 如果不是 5 分钟的整数倍,则不执行任务if (now.Minutes() % 5) {return;}// 下面实现拍快照 -> 上传 cdn -> 存数据库的逻辑// ...}
}
复制代码

图片处理

使用 sprite.js 可以在服务端组合,合并图片,添加滤镜等,这个方案中简单地将多张相同类型的图片合为一张。sprite.js 实现了前后端通用的预加载功能,可以预加载图片,然后在 sprite.js 中使用,下面的代码就实现了这个过程,具体可以参考 sprite.js 文档图片异步加载。

const spritejs = require('spritejs');
const fs = require('fs');
const writeFileAsync = think.promisify(fs.writeFile, fs);(async function() {const {Scene, Sprite} = spritejs;const scene = new Scene('#paper', {resolution: [1200, 1200]});// 预加载图片await scene.preload('https://p3.ssl.qhimg.com/t01ccaee34d3f92a10c.png','https://p2.ssl.qhimg.com/t01eb096408038e7496.png');// 是否代理DOM 事件,如果这个参数设置为false,那么这个 Layer 将不处理DOM事件// 可以提升性能const layer = scene.layer('fglayer', {handleEvent: false});const sprite = new Sprite();// 在 sprite 元素上添加多个 texture// http://spritejs.org/#/zh-cn/doc/attribute?id=texturessprite.attr({textures: [{src: 'https://p3.ssl.qhimg.com/t01ccaee34d3f92a10c.png'},{src: 'https://p2.ssl.qhimg.com/t01eb096408038e7496.png'}]});// 添加到 layer 上layer.append(sprite);const buffer = await snapshot(layer);await writeFileAsync('test.png', buffer);layer.remove(sprite);
})();
复制代码

websocket

ThinkJS 使用 master/workers 多进程模型,如果项目没有用到 websocket,master 接收到请求后是以 Round Robin 算法交给 workers 处理,这样基本保证负载均衡。如过项目前后端需要 websocket 通信,在 ThinkJS 中需要配置 stickyCluster: true,添加这个配置后,master 会做 IP Hash,这样确保来自同一个客户端的请求会被相同的 worker 处理,从而成功建立起 websocket 通信,这样会牺牲一部分性能,详细了解多进程模型,请看《细谈ThinkJS多进程模型》。

由于我们的项目是数据可视化大屏项目,一般没有什么访问量,因此在这个项目中只启动了一个 worker,将 stickyCluster 设置为 false,也能成功建立 websocket 通信。因为只有一个 worker 干活,所有的请求必然都交给了它。

虽然前端可以直接跟 websocket 上游服务建立通信,但是为什么没有这么做(目前是跟 ThinkJS 服务建立 websocket 通信),主要是考虑在 ThinkJS 服务中可以通过制定一套策略处理数据,决定服务端渲染以及前端实时数据展示,这样前端大屏页面就可以只关注绘图工作。

? 总结

这是第一次做 ThinkJS 和 Sprite.js 结合的服务端渲染大屏项目,对我们来说是一次新的尝试,但是技术解决的是怎样实现的问题,实现什么样的展示?以及为什么这么展示?仍是可视化展现过程中需要先行思考的问题。

ThinkJS 和 Sprite.js 服务端渲染实践相关推荐

  1. Vuex 数据流管理及Vue.js 服务端渲染(SSR)

    Vuex 数据流管理及Vue.js 服务端渲染(SSR)项目见:https://github.com/smallSix6/fed-e-task-liuhuijun/tree/master/fed-e- ...

  2. Nuxt.js 服务端渲染从安装到部署

    Nuxt.js 服务端渲染方案 了解 Nuxt.js 的作用 掌握 Nuxt.js 中的路由 掌握 layouts.pages 以及 components 的区别 能够在 Nuxt.js 项目中使用第 ...

  3. Vue.js 服务端渲染

    服务端渲染 SSR 完全指南 在 2.3 发布后我们发布了一份完整的构建 Vue 服务端渲染应用的指南.这份指南非常深入,适合已经熟悉 Vue, webpack 和 Node.js 开发的开发者阅读. ...

  4. 开发函数计算的正确姿势 —— 移植 next.js 服务端渲染框架

    为什么80%的码农都做不了架构师?>>>    首先介绍下在本文出现的几个比较重要的概念: 函数计算(Function Compute): 函数计算是一个事件驱动的服务,通过函数计算 ...

  5. Node项目部署到阿里云服务器(ECS),以Nuxt.js服务端渲染项目为例

    1.前言 最近打算业余时间搭个网站,选择的技术栈为node+mongodb+Nuxt.js(基于vue,用于创建服务端渲染 (SSR) 应用),以下不会教科书式讲解,只是提供整体思路.参考资料以及关键 ...

  6. js 操作vuex数据_请教个有关 Vue.js 使用 Nuxt.js 服务端渲染,使用 Vuex 取数据的时候报错...

    查过资料没有什么结果,首先怀疑的是 SSR 的问题,但是简单的测试感觉不是 SSR 的问题.没有找到原因,希望在这里能得到解惑! 使用 Nuxt.js 做服务端渲染,前后端分离,Token 存储在 l ...

  7. React SSR 服务端渲染实践指南

    年前因为工作原因需要对原有 React 项目进行服务端渲染的改造,下面是我对之前工作经验的一些总结分享,希望可以对大家有所帮助. 适用场景 首先我们来了解一下 SSR 可以做什么,可以解决什么问题,诞 ...

  8. php和asp渲染页面,Vue.js与 ASP.NET Core 服务端渲染功能

    在前端使用 Vue.js,Vue 服务端渲染直到第二个版本才被支持. 在本例中,我想展示如何将 Vue.js 服务端渲染功能整合 ASP.NET Core. 我们在服务端使用了 Microsoft.A ...

  9. 美少女秃头思考:react服务端渲染

    富婆来报道,今天想问题想不出来,随手抓了一下头发,没想到啊没想到,我那浓(mei)密(sheng)茂(ji)盛(gen)的秀发又少了好几根,一定要改掉这个想不出来问题就揪头发的坏习惯.你们遇到问题想不 ...

最新文章

  1. libuvc介绍及简单使用
  2. C++知识点42——下标运算符[]的重载及string类的实现
  3. 解决:Cannot resolve plugin org.apache.maven.plugins:maven-compiler-plugin:2.3.2问题
  4. 如何在Java中将InputStream读取/转换为String?
  5. 【Flink】FLink 1.12 版本的 Row 类型 中的 RowKind 是干嘛的
  6. leetcode python3 简单题234. Palindrome Linked List
  7. Apache Rewrite 理解
  8. sicily 1282. Computer Game
  9. Antlr4 简单入门
  10. C++ 类型转换归纳
  11. 2022.5.23-5.29 AI行业周刊(第99期):AI创业道路
  12. Windows Qt设置环境变量
  13. 宝塔搭建实测-基于ThinkPHP5.1的wms进销存源码
  14. android js桥接,聊一聊桥接(JSBridge)的原理(下)
  15. mysql加载audit失败_MySQL5.5 安装mcafee mysql-audit插件 不成功
  16. centos格式化优盘命令_centos 格式化分区
  17. GAN 的训练、调参实践
  18. 电源开关电源200W 12V 24V,电源架构PFC+LLC+同步整流,高效率高功率因数
  19. 大鱼号怎么赚钱,95%的新手都不知道这样做!
  20. 人生观,世界观,价值观树立的方式

热门文章

  1. 使用 IntraWeb (4) - 页面布局之 TIWRegion
  2. [转]WebGL All in One 全傻瓜简介
  3. Spring中的BeanDefinition
  4. [NOIP2011] 玛雅游戏
  5. 获取mssqlserver数据库表的字段名称,字段说明,数据类型,主键等表的信息
  6. CodeForces 598A Tricky Sum
  7. Android学习记录--Switch开关按钮的应用
  8. [一步一步MVC]第二回:还是ActionFilter,实现对业务逻辑的统一Authorize处理 OnActionExecuting内如何获取参数...
  9. ArcGIS For Flex学习之Mapping---Map Extent and Mouse Coordinates
  10. centos6.2系统下安装配置FastDFS步骤