在做后台管理系统或者数据演示项目时,经常会遇到这样一个需求:库里存着几十条甚至上千条记录,希望每条记录都能对应一张可视化图片,用来做列表封面、占位图或者演示素材。手动一张张做显然不现实,这时候就可以写一个Node.js脚本,从Supabase里把数据拉出来,自动生成Mock图片,甚至还能把生成结果回传到Supabase Storage,形成一条完整的自动化链路。本文就来完整拆解这个方案。

整体架构与依赖选型
这个方案的核心链路其实很简单,分三步:第一步通过supabase-js客户端连接Supabase数据库,把目标表的数据查询出来;第二步在Node.js进程内把每条记录渲染成图片;第三步可选,把生成的图片上传到Supabase Storage,拿到公开访问URL后再写回数据库字段。整个过程不需要起HTTP服务,一个普通脚本就能跑完。
依赖方面,连接数据库用官方的@supabase/supabase-js,这个库封装了REST接口,查询语法非常直观。图片生成有两个主流选择:一个是sharp,基于libvips,性能极强,适合做SVG转PNG这类操作,思路是先用字符串拼出SVG,再交给sharp栅格化;另一个是node-canvas,它提供了类似浏览器Canvas的2D绘图API,灵活度更高,但编译依赖较多,Windows环境下偶尔会有安装问题。如果生成的图片只是文字加色块这类简单Mock图,推荐优先用sharp,安装简单且速度飞快。
从Supabase查询数据
连接Supabase需要两个东西:项目URL和service_role或anon key,都能在Supabase控制台的Settings中的API页面找到。注意如果脚本只做读取,用anon key配合数据库的RLS策略就够了;如果涉及写入Storage或者更新表数据,建议用service_role key,并且这个key绝对不能暴露到前端代码里。
const { createClient } = require('@supabase/supabase-js');
const supabase = createClient(
'https://your-project.supabase.co',
process.env.SUPABASE_SERVICE_KEY
);
async function fetchRecords() {
// 分页查询,每次取100条,避免一次拉取过多
const { data, error } = await supabase
.from('products')
.select('id, name, price, category')
.range(0, 99);
if (error) {
throw new Error('查询失败: ' + error.message);
}
return data;
}这里有个容易被忽略的点:当表里数据量超过一千条时,select默认不会一次性返回全部数据。务必用range做分页循环,或者用.select('*', { count: 'exact' })先拿到总数再规划分批策略,否则生成到一半会发现少了一批数据,排查起来很浪费时间。
用Sharp渲染Mock图片
生成Mock图片最省事的方式是拼SVG字符串。SVG里可以随意控制背景色、文字内容、字号和位置,而且文本内容是从数据库来的,天然就是动态的。需要注意的是SVG里的特殊字符必须转义,特别是&、<这类符号,一旦出现就会导致SVG解析失败,sharp直接抛异常。
const sharp = require('sharp');
function escapeXml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
async function renderMockImage(record) {
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="800" height="400">
<rect width="100%" height="100%" fill="#1e293b"/>
<text x="40" y="180" font-size="42" fill="#f8fafc" font-family="sans-serif">
${escapeXml(record.name)}
</text>
<text x="40" y="240" font-size="28" fill="#94a3b8" font-family="sans-serif">
价格: ¥${record.price} | 分类: ${escapeXml(record.category)}
</text>
</svg>`;
const buffer = await sharp(Buffer.from(svg))
.png()
.toBuffer();
return buffer;
}如果需要更复杂的排版,比如多行文字自动换行、绘制图表线条,SVG写起来会比较费劲,这时换node-canvas更合适,它有measureText方法可以精确计算文字宽度,实现真正的自动换行。不过对于绝大多数Mock场景,一张背景加两行文字已经够用了。
批量上传到Storage并回写URL
图片生成好之后,直接上传到Supabase Storage的某个bucket里,然后用getPublicUrl拿到访问地址,再把地址更新回表的image_url字段,这样前端列表页就能直接显示生成的Mock图了。批量处理时建议加并发控制,一次性开太多请求容易触发Supabase的速率限制,用p-limit把并发压在5以内比较稳妥。
const pLimit = require('p-limit');
const limit = pLimit(5);
async function processAll(records) {
const tasks = records.map(record => limit(async () => {
const buffer = await renderMockImage(record);
const path = `mock/${record.id}.png`;
const { error: uploadError } = await supabase.storage
.from('mock-images')
.upload(path, buffer, { contentType: 'image/png', upsert: true });
if (uploadError) throw uploadError;
const { data } = supabase.storage
.from('mock-images')
.getPublicUrl(path);
await supabase
.from('products')
.update({ image_url: data.publicUrl })
.eq('id', record.id);
console.log(`完成: ${record.id}`);
}));
await Promise.all(tasks);
}最后补充两点实践建议:一是记得在Storage的bucket设置里开启Public策略,否则生成的URL无法直接访问;二是给整个流程加上失败重试和日志,某条记录渲染失败不应该中断整批任务,把失败的id收集起来单独重跑即可。这套方案跑通之后,扩展空间很大,比如换成根据记录内容生成二维码、生成带统计图表的数据卡片,都只是换一个SVG模板的事情。