从 0 到 1 开发一个 node 命令行工具
G2 5.0 推出了服务端渲染的能力,为了让开发者更快捷得使用这部分能力,最写了一个 node 命令行工具 g2-ssr-node:用于把 G2 的 spec 转换成 png、jpeg 或者 pdf 等。基本的使用如下:
$ g2-ssr-node g2png -i ./bar.json -o ./bar.png
其中 bar.json是一个如下绘制条形图的 G2 spec:
{
"type": "interval",
"data": [
{ "genre": "Sports", "sold": 275 },
{ "genre": "Strategy", "sold": 115 },
{ "genre": "Action", "sold": 120 },
{ "genre": "Shooter", "sold": 350 },
{ "genre": "Other", "sold": 150 }
],
"encode": {
"x": "genre",
"y": "sold"
}
}
最后得到如下的图片 bar.png:
那接下来就来看看如何从 0 到 1 实现 g2-ssr-node。
初始化环境
首先新建一个文件夹并且用 npm 初始化项目:
$ mkdir g2-ssr-node && cd g2-ssr-node && npm init -y
在得到的 package.json 中增加一个 bin 字段,该字段指向了包里面的可执行文件,也就是最后运行 $ g2-ssr-node 时候执行的文件。
{
"bin": "bin/g2-ssr-node.js",
}
接下来新增 bin 目录、新增 g2-ssr-node.js,并且输入以下的内容:
#!/usr/bin/env node
console.log('hello world!')
其中第一行说明用 node 来执行 g2-ssr-node.js 文件,类似于当运行 $ g2-ssr-node的时候调用 $ node g2-ssr-node.js。
这之后在项目根目录下运行 npm link 会把当前包安装到全局。如果打开控制台输入 g2-ssr-node能打印出 hello world!, 那么说明开发环境已经搭建好了,可以进一步写代码了。
安装 commander
首先安装 commnder 工具包来简化开发命令行的成本,比如帮助解析参数、展现使用错误和实现提示信息等。
npm i commander -D
本文实现的 g2-ssr-node 有一个子命令:g2png,将 G2 的 spec 转换成图片。该子命令有两个参数:
- -i, --input <filename>: 指定包含需要转换的 spec 的文件地址
- -o, --output <filename>: 指定输出图表的文件地址
修改 bin/g2-ssr-node.js 如下用于满足上述需求:
#!/usr/bin/env node
const { Command } = require("commander");
const process = require("node:process");
const { version } = require("../package");
const { g2png } = require("./g2png.js");
const program = new Command();
program
.name("g2-ssr-node")
.description("CLI for ssr of @antv/g2")
.version(version);
program
.command("g2png") // 添加子命令
.description("Convert a G2 spec to an PNG image") // 添加对子命令的描述
.option("-i, --input <filename>", "filename for the input spec") // 声明参数
.option("-o, --output <filename>", "filename for the output image") // 声明参数
.action((options) => g2png(options).then(() => process.exit())); // 真正的执行函数
program.parse(); // 解析
然后新增 bin/g2png.js这个文件,并且导出g2png(options)这个函数,用于根据指定的配置渲染图片。
// bin/g2png.js
function g2png(options) {
console.log(options)
}
module.exports = { g2png };
如果一切顺利的话,运行如下测试命令:
$ g2-ssr-node g2png -i ./bar.json -o ./bar.png
会在控制台输出:{ input: './bar.json', output: './bar.png' }。
当然也可以运行 g2-ssr-node g2png --help看看控制台输出的帮助信息是否符合预期:
Usage: g2-ssr-node g2png [options]
Convert a G2 spec to an PNG image
Options:
-i, --input <filename> filename for the input spec
-o, --output <filename> filename for the output image
-h, --help display help for command
如果没有问题的话,就可以进入下一步,实现 g2png 函数。
实现 g2png 函数
g2png 主要包含三个步骤:
- 根据 input 地址读取 JSON 内容,解析成 JavaScript 对象,得到需要渲染的 spec。
- 将得到的 spec 通过 renderImage 函数渲染成 node canvas 中的 canvas 对象。
- 将 canvas 对象转换成 png 流并且写入 output 地址。
首先我们安装 node canvas。 node canvas 是一个实现了 canvas 标准的 node 库,主要用于做 canvas 的服务端渲染,更多的 API 参考其文档。
$ npm i canvas -D
修改 bin/g2png.js代码如下:
const fs = require("fs");
const { renderImage } = require("./renderImage.js");
function readJSONSync(input) {
const data = fs.readFileSync(input, "utf-8");
return JSON.parse(data);
}
async function g2png({ input, output }) {
console.log(`Start converting ${input} to ${output} ...`);
// 读取并且转化 Spec
const spec = await readJSONSync(input);
// 将 Spec 渲染成 node canvas 中的 canvas
const canvas = await renderImage(spec);
// 将 canvas 转化成 png 流并且写入 output 地址
const out = fs.createWriteStream(output);
const stream = canvas.createPNGStream();
stream.pipe(out);
return new Promise((resolve, reject) => {
out
.on("finish", () => {
console.log(`Convert ${input} to ${output} successfully.`);
resolve();
})
.on("error", () => reject());
});
}
module.exports = { g2png };
那么接下来我们来看看 renderImage 函数的实现。
实现 renderImage 函数
首先新建 bin/renderImage.js文件,并且输入如下的代码。这段代码将一个 node-canvas 创建的 canvas 对象传给了 G,用于创建一个画布,然后 G2 将 spec 渲染到这个画布上,并且将 canvas 返回。
// bin/renderImage.js
const { createCanvas } = require("canvas");
const { stdlib, render: renderChart } = require("../dist/g2.js");
const { Canvas } = require("../dist/g");
const { Renderer } = require("../dist/g-canvas");
async function renderImage(options) {
// 创建 canvas
const { width = 640, height = 480, ...rest } = options;
const [gCanvas, canvas] = createGCanvas(width, height);
// 根据 spec 和 context 渲染图表到 canvas 上
const spec = { ...rest, width, height };
const context = {
canvas: gCanvas,
library: stdlib,
createCanvas: () => createCanvas(300, 150),
};
await new Promise((resolve) => renderChart(spec, context, resolve));
return canvas;
}
function createGCanvas(width, height, type) {
const canvas = createCanvas(width, height, type);
const offscreenCanvas = createCanvas(1, 1);
const renderer = new Renderer();
// 移除一些和 DOM 相关的交互
const htmlRendererPlugin = renderer.getPlugin("html-renderer");
const domInteractionPlugin = renderer.getPlugin("dom-interaction");
renderer.unregisterPlugin(htmlRendererPlugin);
renderer.unregisterPlugin(domInteractionPlugin);
return [
new Canvas({
width,
height,
canvas,
renderer,
offscreenCanvas,
devicePixelRatio: 2, // 解决高分辨率屏不清晰的问题
}),
canvas,
];
}
module.exports = { renderImage };
这里大家可能注意到了在上面代码中并没有直接从 G2 或者 G 中导入我们需要的函数,那么接下来就来看看为什么。
// renderImage.js
// 从 dist 导出
const { stdlib, render: renderChart } = require("../dist/g2.js");
const { Canvas } = require("../dist/g");
const { Renderer } = require("../dist/g-canvas");
打包 commonjs
JavaScript 常见的模块系统有两种 ESM 和 commonjs,而低版本的 Node 只支持 commonjs。虽然 G2 和 G 都提供了 commonjs 的版本,但是它们的依赖却不一定,比如 D3.js 只提供了 ESM 版本。为了让 g2-ssr-node 能在低版本的 Node 中运行,需要把 G2 和 G 以及它们的依赖都打包成 commonjs。
首先分别创建以下三个文件,并且输入以下的内容:
// @antv/g2.js
export * from '@antv/g2';
// @antv/g.js
export * from '@antv/g';
// @antv/g-canvas.js
export * from '@antv/g-canvas'
然后使用 Rollup 和它的一系列插件来将上述的文件打包成 commonjs 模块:
npm i rollup @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-terser -D
配置文件如下:
// rollup.config.js
const resolve = require("@rollup/plugin-node-resolve");
const cjs = require("@rollup/plugin-commonjs");
const json = require("@rollup/plugin-json");
const terser = require("@rollup/plugin-terser");
const common = {
plugins: [resolve(), cjs(), json(), terser()],
external: ["@antv/g"],
};
module.exports = [
{
input: "antv/g2.js",
output: {
file: "dist/g2.js",
format: "cjs", // 打包成 commonjs
},
...common,
},
{
input: "antv/g.js",
output: {
file: "dist/g2.js",
format: "cjs", // 打包成 commonjs
},
...common,
},
{
input: "antv/g-canvas.js",
output: {
file: "dist/g2-canvas.js",
format: "cjs", // 打包成 commonjs
},
...common,
},
];
然后在控制台输入 npx rollup -c就会发现已经多了一个 dist 文件夹,这之后就可以验证是是否成功了。
验证是否成功
第一步,在项目根目录下创建一个 bar.json文件,并且输入以下内容:
{
"type": "interval",
"data": [
{ "genre": "Sports", "sold": 275 },
{ "genre": "Strategy", "sold": 115 },
{ "genre": "Action", "sold": 120 },
{ "genre": "Shooter", "sold": 350 },
{ "genre": "Other", "sold": 150 }
],
"encode": { "x": "genre", "y": "sold" },
"viewStyle": { "plotFill": "white" }
}
接下来运行:
$ g2-ssr-node g2png -i ./bar.json -o ./bar.png
如果没有问题的话,就会在项目的根目录出现一张名叫 bar.png的图片。
小结
到这里简单版本的 g2-ssr-node 就已经开发完成了,发包的过程就不在这里赘述了。最后大体的代码结构如下:
- antv
- g2.js
- g.js
- g-canvas.js
- dist
- g2.js
- g.js
- g-canvas.js
- bin
- g2-ssr-node.js
- g2png.js
- renderImage.js
- rollup.config.js
- bar.json
- package.json
除了更多的能力之外(比如将 G2 spec 转换成 jpeg 和 pdf,以及通过 API 的形式调用),还需要考虑 Test、Lint、CI 和文档一些相关的问题。完整的代码、能力和文档可以在 g2-ssr-node 查看。当然感兴趣的小伙伴也可以提 PR,去实现将 G2 spec 转换成 SVG 命令,参考 SVG Output。
最后,g2-ssr-node 算是 G2 5.0 生态中的新成员,而目前 G2 5.0 在收集相关的一些生态,当然也可以在这里提出一些想法,组团来实现。期望大家一起,让 G2 变得更好,让数据可视化社区活跃起来!