在 Node.js 中使用 Konva
Konva 能在没有浏览器的 Node.js 里跑,用来在服务端生成图片—— 海报合成、分享卡片、报表快照、缩略图预渲染都属于这一类。
Konva 10 的服务端支持与 9.x 完全不同:入口从 konva/cmj 换成了
konva/canvas-backend,并且多了一个基于 Skia 的 konva/skia-backend。
照着旧文章写 require('konva/cmj') 会直接报 Package subpath './cmj' is not defined。
安装
后端库是可选的对等依赖(peerDependenciesMeta 里两个都标了 optional),
装 Konva 时不会自动带上,必须自己选一个装:
npm install konva canvas
# 或者用 Skia 后端
npm install konva skia-canvas
| 后端 | 入口 | 说明 |
|---|---|---|
| node-canvas | konva/canvas-backend | 最常用,生态成熟,原生模块需编译或下载预编译包 |
| skia-canvas | konva/skia-backend | 基于 Skia,文字与路径渲染质量更好,体积更大 |
下文以 canvas 为例,skia-canvas 只需把导入路径换掉。
两个 import 缺一不可
这是最容易踩的一步。后端入口只装环境适配,不带图形类:
import Konva from 'konva'; // ① 带来 Rect / Circle / Text / Filters
import 'konva/canvas-backend'; // ② 装上 Node 渲染后端
这是 Konva 官方 CHANGELOG 在 10.0.0 的迁移说明里给出的写法。
反过来写也一样能跑(实测两种顺序都正常)——两个模块操作的是同一个单例对象,
从哪个模块拿 Konva 引用都行。
少了第①行,你能拿到 Konva.Stage,但 Konva.Rect 是 undefined,
报错是 Konva.Rect is not a constructor——看起来像装错了包,
实际只是少导了一个模块。这一点在源码注释里写得很明白:
canvas-backend 导出的是"不含图形与滤镜的 Konva 核心对象,
import 'konva' 会往同一个对象上补齐它们"。
反过来,只 import 'konva' 而不导后端,new Konva.Stage() 会抛
Konva.js unsupported environment.——Konva 检测不到 DOM,
也没人告诉它该用什么来创建画布。
两个入口都是 ESM。 CommonJS 项目里要用动态 import(),
或者把文件扩展名改成 .mjs、在 package.json 里设 "type": "module"。
一个完整的例子
服务端没有 DOM,所以 Stage 不传 container:
import Konva from 'konva';
import 'konva/canvas-backend';
import fs from 'node:fs';
const stage = new Konva.Stage({ width: 400, height: 200 });
const layer = new Konva.Layer();
layer.add(new Konva.Rect({
x: 0, y: 0, width: 400, height: 200, fill: '#f5f5f5',
}));
layer.add(new Konva.Text({
x: 24, y: 80, text: '服务端渲染', fontSize: 36, fill: '#333',
}));
stage.add(layer);
// 方式一:拿 base64 再自己写文件
const base64 = stage.toDataURL().split(',')[1];
fs.writeFileSync('out.png', Buffer.from(base64, 'base64'));
// 方式二:走 node-canvas 的流,大图更省内存
stage.toCanvas().createPNGStream().pipe(fs.createWriteStream('out2.png'));
stage.toCanvas() 返回的就是 node-canvas 的 Canvas 实例,
它的 createPNGStream() / createJPEGStream() 都可以直接用。
大尺寸图片走流比先拼 base64 字符串省得多——base64 会让内存里同时存在
像素数据、二进制 Buffer 和一个比二进制还大三分之一的字符串。
加载图片用 node-canvas 的 loadImage,不要指望 Konva.Image.fromURL
在服务端按浏览器那套工作:
import { loadImage } from 'canvas';
const img = await loadImage('./logo.png'); // 也支持 http(s) URL 和 Buffer
layer.add(new Konva.Image({ image: img, x: 20, y: 20 }));
常见问题
为什么报 Konva.Rect is not a constructor?
少了 import 'konva'。后端入口 konva/canvas-backend 只负责告诉 Konva
"用 node-canvas 来创建画布和图片元素",它导出的核心对象上没有任何图形类。
两行都要写。顺序不重要,两种写法实测都能跑——它们操作的是同一个单例对象,
后导入的模块往同一个对象上补东西。建议照官方 CHANGELOG 的形式写
(import Konva from 'konva' 在前),这样和你在别处看到的示例一致。
为什么报 Konva.js unsupported environment?
反过来的情况:只 import 'konva' 没导后端。
Konva 在构造 Stage 时会尝试创建一个 canvas 元素,
在 Node 里既没有 document 也没有注册过替代实现,于是直接抛错。
如果你在一个同时跑浏览器和服务端的代码库里遇到这个, 检查是不是 SSR 阶段执行到了本该只在客户端跑的绘图代码。
中文显示成方框怎么办?
node-canvas 不自带字体,它通过系统的 fontconfig 查找。
本机开发时通常没问题——macOS 和完整的桌面版 Linux 都预装了中文字体。
问题几乎都出在容器里:node:20-slim、node:20-alpine
这类精简镜像不含任何中文字体,中文会渲染成方框(tofu)。
两个办法,推荐第二个:
# 办法一:在镜像里装字体
RUN apt-get update && apt-get install -y fonts-noto-cjk && rm -rf /var/lib/apt/lists/*
// 办法二:把字体文件放进仓库,显式注册(可复现,不依赖镜像)
import { registerFont } from 'canvas';
registerFont('./fonts/NotoSansSC-Regular.otf', { family: 'Noto Sans SC' });
// 注册必须在创建任何 canvas 之前完成
const text = new Konva.Text({ text: '中文', fontFamily: 'Noto Sans SC' });
registerFont 必须在第一次创建画布之前调用,之后再注册不生效。
注意字体文件的授权——思源黑体(Noto Sans CJK / Source Han Sans)是 OFL,
可以随镜像分发;微软雅黑、苹方不行。
服务端和浏览器渲染出来的一样吗?
不一样,不要按像素比对。 差异来自三处:
字体不同(服务端装的字体和用户机器上的几乎不可能一致),
抗锯齿算法不同(Skia、Cairo、浏览器各有各的实现),
以及文字度量不同——text.width() 在两边会返回不同的值,
依赖它做的居中和换行计算结果也就不同。
所以「服务端预渲染 + 前端展示」这种方案,要么全交给服务端出图, 要么接受两边有肉眼可见的细微差别。想做视觉回归测试的话, 基线图必须在同一套环境里生成。
能在服务端用滤镜和 cache 吗?
可以,Konva.Filters 的 20 个滤镜在 Node 里全部可用,
cache() 也正常工作——它们都只依赖 getImageData / putImageData,
不依赖 DOM。
但要注意性能:滤镜是纯 JS 的逐像素循环,在服务端阻塞事件循环。 浏览器里卡一帧只是掉帧,服务端卡住就是这段时间内所有请求都在排队。 大图加滤镜应该丢进 worker 线程或独立的渲染进程。
国内环境注意事项
canvas 是原生模块,npm install 时会先尝试从 GitHub Releases
下载预编译的二进制。国内访问 GitHub 不稳,这一步经常超时,
然后回退到本地编译——而本地编译要求先装好一整套图形库,
多数人的机器上并没有。
优先走镜像:
npm config set canvas_binary_host_mirror https://registry.npmmirror.com/-/binary/canvas
npm install canvas
如果只能本地编译,先装系统依赖:
# macOS
brew install pkg-config cairo pango libpng jpeg giflib librsvg
# Debian / Ubuntu
apt-get install -y build-essential libcairo2-dev libpango1.0-dev \
libjpeg-dev libgif-dev librsvg2-dev
Docker 里尤其容易连环踩坑:基础镜像既没有编译工具链、
也没有图形库、还没有中文字体,三样都要补。
如果构建时间敏感,更省事的做法是用多阶段构建,
在完整镜像里装好 node_modules 再拷进精简镜像——
但要注意原生模块的二进制与运行时镜像的 libc 必须匹配
(Alpine 用 musl,Debian 用 glibc,两者不能混),
这也是 Alpine 上跑 node-canvas 格外麻烦的原因。
skia-canvas 在这一点上省心一些,它提供的预编译包覆盖面更广。