从 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 变得更好,让数据可视化社区活跃起来!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/198867.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Web】BJDCTF 2020 个人复现

目录 ①easy_md5 ②ZJCTF&#xff0c;不过如此 ③Cookie is so subtle! ④Ezphp ⑤The Mystery of IP ①easy_md5 ffifdyop绕过SQL注入 sql注入&#xff1a;md5($password,true) 右键查看源码 数组绕过 ?a[]1&b[]2 跳转到levell14.php 同样是数组绕过 param1[…

Jmeter-分布式压测(远程启动服务器,windows)

1 前提条件 JDK已部署&#xff0c;版本一致Jmeter已部署&#xff0c;版本一致多台服务器连接的同一网络(例如&#xff1a;同一wifi)防火墙处于关闭状态&#xff08;或者对应默认端口处于开放状态&#xff09;虚拟网络适配器都处于关闭状态查找到每一台服务器的IP 2 主服务器配…

深信服AD负载均衡频繁掉线故障分析

一个由114.114.114.114引起的AD异常 客户反馈深信服负载均衡链路频繁掉线&#xff0c;具体故障现象如下 可以获取到IP地址、网关 两分钟掉一次&#xff0c;持续一个多月&#xff0c;求IT的心理阴影面积&#xff01; 链路监视器只设置了一个114.114.114.114 处理流程&#xff…

【古月居《ros入门21讲》学习笔记】12_服务端Server的编程实现

目录 说明&#xff1a; 1. 服务模型 说明 2. 实现过程&#xff08;C&#xff09; 创建服务器代码&#xff08;C&#xff09; 配置服务器代码编译规则 编译 运行 3. 实现过程&#xff08;Python&#xff09; 创建服务器代码&#xff08;Python&#xff09; 运行效果 说…

springboot3.2 整合 mybatis-plus

springboot3.2 整合 mybatis-plus springboot3.2 正式发布了 迫不及待地的感受了一下 结果在整个mybatis-plus 的时候遇到了如下报错 java.lang.IllegalArgumentException: Invalid value type for attribute factoryBeanObjectType: java.lang.String. ____ _ …

Springboot实现增删改差

一、包结构 二、各层代码 (1)数据User public class User {private Integer id;private String userName;private String note;public User() {super();}public User(Integer i, String userName, String note) {super();this.id i;this.userName userName;this.note note;…

Databend 开源周报第 121 期

Databend 是一款现代云数仓。专为弹性和高效设计&#xff0c;为您的大规模分析需求保驾护航。自由且开源。即刻体验云服务&#xff1a;https://app.databend.cn 。 Whats On In Databend 探索 Databend 本周新进展&#xff0c;遇到更贴近你心意的 Databend 。 支持追加流 Da…

springboot 外部化配置

背景:修改jar包中的配置比较麻烦 项目部署的时候放一个配置文件在jar包外 配置文件优先级: 1.jar包内的application.properties/yaml 2.jar包内的application-{profile}.properties/yaml 3.jar包外的application.properties/yaml 4.jar包外的application-{profile}.properties…

Linux ps命令详解:如何查看进程的PID、占用的CPU和内存使用率、虚拟内存大小等信息(附实例教程和注意事项)

Linux ps命令介绍 Linux ps命令&#xff0c;全称为process status&#xff0c;是一个非常实用的命令&#xff0c;用于显示当前进程的状态。它的功能类似于Windows的任务管理器。通过ps命令&#xff0c;我们可以查看到进程的PID、占用的CPU和内存使用率、虚拟内存大小、实际内存…

SpringBoot整合MongoDB: 构建高效的数据存储应用

文章目录 1. 引言2. MongoDB简介3. 准备工作4. SpringBoot中配置MongoDB5. 创建MongoDB实体类6. 使用Spring Data MongoDB进行数据操作7. 编写Service层8. 控制器层9. 测试10. 拓展10.1. 复杂查询10.2. 数据分页10.3. 索引优化 11. 总结 &#x1f389;SpringBoot整合MongoDB: 构…

springboot 自定义starter逐级抽取

自定义starter 背景:各个组件需要引入starter 还有自己的配置风格 –基本配置原理 &#xff08;1&#xff09;自定义配置文件 导入配置可以在配置文件中自动识别&#xff0c;提示 导入依赖后可以发现提示 &#xff08;2&#xff09;配置文件实现 –让配置文件对其他模块生…

WebUI自动化学习(Selenium+Python+Pytest框架)003

1.元素操作 在成功定位到元素之后&#xff0c;我们需要对元素进行一些操作动作。常用的元素操作动作有&#xff1a; &#xff08;1&#xff09;send_keys() 键盘动作&#xff1a;向浏览器发送一个内容&#xff0c;通常用于输入框输入内容或向浏览器发送快捷键 &#xff08;2…

九、LuaTable(表)

文章目录 一、定义二、Table(表)的构造三、Table 操作&#xff08;一&#xff09;Table连接&#xff08;二&#xff09;插入和移除&#xff08;三&#xff09;Table 排序&#xff08;四&#xff09;Table 最大值 一、定义 table 是 Lua 的一种数据结构用来帮助我们创建不同的数…

Mysql中的引擎介绍(InnoDB,MyISAM,Memory)

MySQL引擎就是指表的类型以及表在计算机上的存储方式。 MySQL数据库及其分支版本主要的存储引擎有三种&#xff0c;分别是 InnoDB、MyISAM、 Memory&#xff0c;还有一些其他的&#xff0c;CSV、Blackhole等&#xff0c;比较少见&#xff0c;可以使用SHOW ENGINES语句来查看。结…

揭开 BFC 的神秘面纱:前端开发必知必会

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

Vue3的transition标签以及animate.css使用详解

一&#xff1a;前言 在项目开发中&#xff0c;有一种特殊情况是使用动画过渡去完成某个效果。比如淡入淡出&#xff0c;或者在动画完成后执行某些操作等。在以前开发中我们通常会选择使用 CSS3 进行研发。但是这样会有很多不好的地方&#xff0c;比如最原始化的封装&#xff0c…

Spring Boot实现图片上传和展示

Spring Boot实现图片上传和展示 本文将介绍如何使用Spring Boot框架搭建后端服务&#xff0c;实现接收前端上传的图片并保存到resources/images目录下。同时&#xff0c;我们还将展示如何在前端编写一个HTML页面&#xff0c;实现上传图片和从resources/images目录下获取图片并…

Minecraft Modding 模组制作-自定义方块

目录 自定义方块一般方块定义物品所属类注册方块注册方块对应物品添加材质添加各面不同的材质本地化 更多样的方块的实现方式会在之后陆续更新参考打赏 注意&#xff1a;本文代码只表现个人实现方式及习惯&#xff0c;本文解释只体现个人理解&#xff0c;不一定符合规范&#x…

【并发编程】ConcurrentHashMap底层结构和原理

&#x1f4eb;作者简介&#xff1a;小明Java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

MySQL进阶知识:二

目录 视图 基本语法 视图的更新 视图的作用 存储过程 介绍 存储过程基本语法 存储过程的变量 系统变量 用户自定义变量 局部变量 存储过程的判断逻辑 存储过程的参数 存储过程中的流程控制 存储过程中的循环 while的基本语法 repeat的基本语法 loop的基本语法…