Confluence是团队知识管理中最常用的Wiki系统之一,但在自动化测试、前端联调或者文档截图流水线中,直接依赖真实的Confluence实例往往并不现实:环境搭建重、数据难以隔离、页面操作有副作用。一个更优雅的思路是,用Node.js实现一个Confluence Mock服务,把页面数据虚拟化,同时提供一个把Mock页面内容渲染成图片的接口,也就是所谓的Mock2Image能力,再把整套服务容器化后跑在Kubernetes上,形成一条随时可用的文档模拟链路。

一、Confluence Mock服务的接口设计
要模拟Confluence,先要理解它的核心REST API形态。Confluence官方的REST接口大致分为空间(Space)、页面(Page)和内容搜索三类。我们的Mock服务不需要完整复刻所有接口,只需要覆盖高频使用的几个:创建页面、按ID查询页面、按空间列出页面、删除页面,以及一个供Mock2Image使用的渲染接口。
先定义页面的数据结构。一个Mock页面至少要包含这些字段:页面ID、所属空间Key、标题、正文内容(用简化版的存储格式表示,比如HTML片段)、版本号和创建时间。在内存中使用一个Map存储即可,如果希望重启后数据仍在,可以加一个简单的JSON文件持久化。
const express = require('express');
const app = express();
app.use(express.json());
// 内存中的页面存储
const pages = new Map();
let idCounter = 1;
// 创建页面,模拟 POST /rest/api/content
app.post('/rest/api/content', (req, res) => {
const { spaceKey, title, body } = req.body;
if (!spaceKey || !title) {
return res.status(400).json({ message: 'spaceKey与title为必填项' });
}
const id = String(idCounter++);
const page = {
id,
type: 'page',
title,
space: { key: spaceKey },
body: { storage: { value: body || '', representation: 'storage' } },
version: { number: 1 },
createdAt: new Date().toISOString()
};
pages.set(id, page);
res.status(200).json(page);
});
// 按ID查询页面,模拟 GET /rest/api/content/:id
app.get('/rest/api/content/:id', (req, res) => {
const page = pages.get(req.params.id);
if (!page) {
return res.status(404).json({ message: '页面不存在' });
}
res.json(page);
});
// 按空间列出页面
app.get('/rest/api/space/:spaceKey/content', (req, res) => {
const list = [...pages.values()].filter(p => p.space.key === req.params.spaceKey);
res.json({ results: list, size: list.length });
});
app.listen(3000, () => console.log('Confluence Mock服务已启动,端口3000'));接口返回的JSON结构与Confluence官方API保持高度一致,这一点很重要。因为调用方(比如前端代码或测试脚本)通常是基于官方API的返回格式写了解析逻辑,Mock越贴近真实结构,切换到真实环境时改动就越小。version字段也不要省略,Confluence的更新机制依赖版本号递增,很多客户端SDK会读取它。
二、用Node Canvas实现Mock2Image渲染
Mock2Image是这个方案里最有意思的部分:把Mock出来的页面内容直接渲染成一张PNG图片。应用场景很实际,比如CI流水线中要生成文档预览图,或者测试报告需要附带页面截图,又不想启动无头浏览器这种重量级方案。Node.js下可以借助node-canvas库直接绘制,性能比Puppeteer好一个量级。
渲染逻辑分两步:第一步解析页面的storage内容(我们限定为简单HTML),提取标题、段落和列表;第二步在画布上逐行绘制。简化处理时可以用正则来拆分段落,避免引入完整的HTML解析器。
const { createCanvas, registerFont } = require('canvas');
function renderPageToImage(page, outputPath) {
const width = 800;
const lineHeight = 32;
const padding = 40;
// 从storage内容中拆出段落
const paragraphs = (page.body.storage.value || '')
.split(/<\/?(p|h1|h2|li)>/)
.map(s => s.replace(/<[^>]+>/g, '').trim())
.filter(Boolean);
const height = padding * 2 + 60 + paragraphs.length * lineHeight;
const canvas = createCanvas(width, height);
const ctx = canvas.getContext('2d');
// 背景与边框
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
ctx.strokeStyle = '#dddddd';
ctx.strokeRect(0.5, 0.5, width - 1, height - 1);
// 绘制页面标题
ctx.fillStyle = '#172B4D';
ctx.font = 'bold 26px sans-serif';
ctx.fillText(page.title, padding, padding + 26);
// 绘制正文段落
ctx.font = '16px sans-serif';
ctx.fillStyle = '#333333';
paragraphs.forEach((text, i) => {
ctx.fillText(text, padding, padding + 60 + i * lineHeight);
});
require('fs').writeFileSync(outputPath, canvas.toBuffer('image/png'));
return outputPath;
}
// 对外暴露渲染接口:传入页面ID,返回渲染好的图片
app.get('/mock2image/:id.png', (req, res) => {
const page = pages.get(req.params.id);
if (!page) return res.status(404).json({ message: '页面不存在' });
const buf = renderPage(page);
res.set('Content-Type', 'image/png');
res.send(buf);
});有一个坑必须提前说明:node-canvas依赖Cairo等原生库,而且默认字体里通常没有中文字体。如果页面内容包含中文,绘制出来会是方块或空白。解决办法有两个,一是在Docker镜像里安装fonts-noto-cjk字体包,二是用registerFont注册随代码分发的字体文件。长文本换行也要自己处理,node-canvas的fillText不会自动换行,需要配合measureText按宽度切分文本。
三、容器化与Kubernetes部署
服务写好之后,下一步是打包成Docker镜像。node-canvas的存在让镜像构建稍微复杂一些,需要在基础镜像上补装编译依赖。推荐直接使用带构建工具的Node基础镜像,一次apt安装解决所有问题。
FROM node:18-bullseye
# 安装node-canvas运行所需的原生库与中文字体
RUN apt-get update && apt-get install -y \
build-essential libcairo2-dev libpango1.0-dev \
libjpeg-dev libgif-dev librsvg2-dev \
fonts-noto-cjk \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]Kubernetes侧的编排很常规,一个Deployment管理Pod副本,一个Service暴露访问入口。需要注意两点:渲染图片是CPU密集操作,建议在资源限制里给容器设置合理的CPU request,避免图片渲染高峰期挤占其他服务;如果渲染请求并发较高,可以配置HPA根据CPU使用率自动扩缩容。
apiVersion: apps/v1
kind: Deployment
metadata:
name: confluence-mock
spec:
replicas: 2
selector:
matchLabels:
app: confluence-mock
template:
metadata:
labels:
app: confluence-mock
spec:
containers:
- name: mock
image: registry.ippipp.com/confluence-mock:1.0
ports:
- containerPort: 3000
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1"
memory: "512Mi"
readinessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: confluence-mock
spec:
selector:
app: confluence-mock
ports:
- port: 80
targetPort: 3000别忘了在Express里补一个/health健康检查端点,返回200即可,否则readinessProbe会一直失败导致Pod永远不就绪。部署完成后,通过kubectl port-forward svc/confluence-mock 8080:80就能在本地验证整套流程:先POST创建一个页面,再访问http://127.0.0.1:8080/mock2image/1.png拿到渲染好的图片。
四、工程实践建议
数据持久化方面,Map存储在Pod重启后会丢失。对于测试场景这反而可能是优点(每次都是干净环境),但如果有保留需求,建议引入Redis或者在容器里挂一个emptyDir卷写JSON文件,而不是给Mock服务上重型数据库,那违背了轻量的初衷。
接口兼容性建议用契约测试来保障。写一组固定用例,同时跑真实Confluence测试环境与Mock服务,对比两者响应结构的一致性,这样Confluence API升级时能第一时间发现Mock需要同步的地方。另外,如果后续需要更复杂的富文本渲染能力,可以在Mock2Image链路前面加一层Puppeteer渲染兜底,把node-canvas用于简单场景、无头浏览器用于复杂场景,按内容复杂度自动路由,兼顾性能与还原度。
最后,镜像体积值得留意。装完字体和编译依赖后镜像可能超过1GB,可以在CI里用多阶段构建,只在编译阶段安装build-essential,运行阶段只拷贝编译好的node_modules,通常能把镜像压缩到400MB左右,拉取速度和集群存储压力都会明显改善。
Node.jsConfluence Kubernetes修改时间:2026-09-14 04:24:50