Skip to content

Commit

Permalink
chore: update ssr docs (#5780)
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoiver authored Nov 10, 2023
1 parent 9263300 commit 68008df
Show file tree
Hide file tree
Showing 2 changed files with 104 additions and 184 deletions.
96 changes: 0 additions & 96 deletions __tests__/unit/ssr/index.ts

This file was deleted.

192 changes: 104 additions & 88 deletions site/docs/manual/extra-topics/ssr.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,61 +3,126 @@ title: 服务端渲染(SSR)
order: 12
---

服务端渲染(SSR)是指在非浏览器环境渲染出图表,比如在 NodeJs、Python、Java、PHP 等后端语言环境中,一般在后端语言中,最终出来的是一张没有交互和动画的图片。一般使用的场景
服务端渲染(SSR)是指在非浏览器环境渲染出图表,比如在 Node.js、Python、Java、PHP 等后端语言环境中,一般在后端语言中,最终出来的是一张没有交互和动画的图片。一般使用的场景如下

- 后端预渲成图片,提高页面打开的速度
- 脚本批处理,便于传播
- 服务端可视化服务
- 生成图片进行截图对比,用于代码单测
- ...

## 在 Node.js 中使用

## 原理
在 Node.js 生态中,以下库实现了浏览器环境中常见的渲染 API:

G2 可视化引擎的 SSR 原理很简单。G2 能绘制图表的核心是需要有 `Canvas` 的 API,在浏览器中,有浏览器标准的 Canvas 绘图接口,而在 SSR 中,只需要在对应的语言脚本中提供 Canvas 绘图 API 即可。
- [node-canvas](https://github.com/Automattic/node-canvas) 提供了基于 Cairo 的 Canvas2D API 实现
- [jsdom](https://github.com/jsdom/jsdom) 提供了 DOM API 实现

基于它们创建对应的渲染器,就可以让 G2 渲染得到 PNG 或者 SVG 结果。下面我们分别介绍基于这两种实现的示例代码。

## 在 NodeJS 中使用
### jsdom

正常在浏览器中使用 G2 的时候,可以参考文档[开始使用](/manual/introduction/getting-started),而在 NodeJS 中,只需要在创建 Chart 实例的时候,传入对应的构建 `Canvas` 对象的方法即可。整体代码如下:
[在线示例](https://stackblitz.com/edit/stackblitz-starters-6zfeng?file=index.js)

首先使用 JSDOM 创建一个容器 `container`,后续图表将渲染到这里,另外保存 `window``document` 对象供后续使用:

```js
const jsdom = require('jsdom');

const { JSDOM } = jsdom;
const { window } = new JSDOM(`<!DOCTYPE html>`);
const { document } = window;
const container = document.createElement('div');
```

然后创建一个 SVG 渲染器,移除掉依赖 DOM API 的插件后创建画布:

```js
const { Canvas } = require('@antv/g');
const { Renderer } = require('@antv/g-svg');

const renderer = new Renderer();
const htmlRendererPlugin = renderer.getPlugin('html-renderer');
renderer.unregisterPlugin(htmlRendererPlugin);
const domInteractionPlugin = renderer.getPlugin('dom-interaction');
renderer.unregisterPlugin(domInteractionPlugin);

const gCanvas = new Canvas({
renderer,
width,
height,
container, // 使用上一步创建的容器
document,
offscreenCanvas: offscreenNodeCanvas,
requestAnimationFrame: window.requestAnimationFrame,
cancelAnimationFrame: window.cancelAnimationFrame,
});
```

接着正常创建 G2 Chart,传入之前创建的画布和容器,详见参考文档[开始使用](/manual/introduction/getting-started)

```js
const { Chart } = require('@antv/g2');

const chart = new Chart({
width,
height,
canvas: gCanvas,
container,
});
```

最后渲染图表,从 JSDOM 中获取渲染结果并序列化成 SVG 字符串,随后可以选择保存成本地文件,这里示例代码就直接输出到控制台了:

```js
const xmlserializer = require('xmlserializer');

(async () => {
await chart.render();

const svg = xmlserializer.serializeToString(container.childNodes[0]);
console.log(svg); // '<svg>...</svg>

chart.destroy();
})();
```

值得一提的是目前在 G2 的集成测试中,由于 SVG 具有良好的跨平台兼容性,我们也使用了该技术用于[截图比对](https://github.com/antvis/G2/tree/v5/__tests__/integration/snapshots/static)

### node-canvas

[在线示例](https://stackblitz.com/edit/stackblitz-starters-evrvef?file=index.js)

基于 jsdom 的方案只能生成 SVG,如果想生成类似 PNG 格式的图片,可以使用 [node-canvas](https://github.com/Automattic/node-canvas) 渲染。

首先创建两个 node-canvas,分别用于渲染场景和度量文本宽度:

```ts
const { createCanvas } = require('canvas');
const nodeCanvas = createCanvas(width, height);
const offscreenNodeCanvas = createCanvas(1, 1);
```

然后创建一个 Canvas 渲染器和画布:

```ts
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { createCanvas } from 'canvas';
import { Canvas } from '@antv/g';
import { Renderer } from '@antv/g-canvas';
import { Plugin as DragAndDropPlugin } from '@antv/g-plugin-dragndrop';
import { Chart } from '../../../src';

function createNodeGCanvas(width: number, height: number): Canvas {
// Create a node-canvas instead of HTMLCanvasElement
const nodeCanvas = createCanvas(width, height);
// A standalone offscreen canvas for text metrics
const offscreenNodeCanvas = createCanvas(1, 1);

// Create a renderer, unregister plugin relative to DOM.
const renderer = new Renderer();
// Remove html plugin to ssr.
const htmlRendererPlugin = renderer.getPlugin('html-renderer');
renderer.unregisterPlugin(htmlRendererPlugin);
const domInteractionPlugin = renderer.getPlugin('dom-interaction');
renderer.unregisterPlugin(domInteractionPlugin);
renderer.registerPlugin(
new DragAndDropPlugin({ dragstartDistanceThreshold: 10 }),
);
return new Canvas({
width,
height,
// @ts-ignore
canvas: nodeCanvas,
renderer,
// @ts-ignore
offscreenCanvas: offscreenNodeCanvas,
});
}

const renderer = new Renderer();
// 省略移除 DOM 相关插件代码
const gCanvas = new Canvas({
width,
height,
canvas: nodeCanvas,
renderer,
offscreenCanvas: offscreenNodeCanvas,
});
```

接下来正常创建 G2 Chart 并渲染,完成后调用 node-canvas 提供的 [createPNGStream](https://github.com/Automattic/node-canvas#canvascreatepngstream) 方法创建一个包含 PNG 编码的 [ReadableStream](https://nodejs.org/api/stream.html#stream_class_stream_readable)。同样也提供了 [createJPEGStream](https://github.com/Automattic/node-canvas#canvascreatejpegstream)[createPDFStream](https://github.com/Automattic/node-canvas#canvascreatepdfstream) 导出 JPEG 和 PDF。

```ts
function writePNG(nodeCanvas) {
return new Promise<string>((resolve, reject) => {
const f = path.join(os.tmpdir(), `${Math.random()}.png`);
Expand All @@ -67,59 +132,10 @@ function writePNG(nodeCanvas) {
out.on('finish', () => resolve(f)).on('error', reject);
});
}

async function renderG2BySSR() {
const width = 600;
const height = 400;

const gCanvas = createNodeGCanvas(width, height);

// A tabular data to be visualized.
const data = [
{ genre: 'Sports', sold: 275 },
{ genre: 'Strategy', sold: 115 },
{ genre: 'Action', sold: 120 },
{ genre: 'Shooter', sold: 350 },
{ genre: 'Other', sold: 150 },
];

// Instantiate a new chart.
const chart = new Chart({
width,
height,
// Set the g canvas with node-canvas.
canvas: gCanvas,
// Set the createCanvas function.
// @ts-ignore
createCanvas: () => {
// The width attribute defaults to 300, and the height attribute defaults to 150.
// @see https://stackoverflow.com/a/12019582
return createCanvas(width, height) as unknown as HTMLCanvasElement;
},
});

// Specify visualization.
chart
.interval() // Create an interval mark and add it to the chart.
.data(data) // Bind data for this mark.
.encode('x', 'genre') // Assign genre column to x position channel.
.encode('y', 'sold') // Assign sold column to y position channel.
.encode('color', 'genre'); // Assign genre column to color channel.

// Render visualization.
await chart.render();

return writePNG(chart.getContext().canvas?.getConfig().canvas);
}

await renderG2BySSR();
```

也可以在 G2 的单测目录中找到对应的代码 [__tests__/unit/ssr/index.spec.ts](https://github.com/antvis/G2/tree/v5/__tests__/unit/ssr/index.spec.ts)


## 在其他服务端语言环境中使用

因为 G2 的代码是使用 JavaScript 编写和开发,所以无法直接在 Python、Java、PHP 等语言环境中使用,但是可以在服务中安装 NodeJS 环境,然后使用对应的后端语言命令行 API 去驱动上述的 NodeJS 代码去执行 SSR。
因为 G2 的代码是使用 JavaScript 编写和开发,所以无法直接在 Python、Java、PHP 等语言环境中使用,但是可以在服务中安装 Node.js 环境,然后使用对应的后端语言命令行 API 去驱动上述的 Node.js 代码去执行 SSR。

参考《[python 调用 node js](https://juejin.cn/s/python%20%E8%B0%83%E7%94%A8%20node%20js)》,其他语言类似。

0 comments on commit 68008df

Please sign in to comment.