在写博客的过程中,我时常会遇到一些很有意思的现象。前不久我偶然发现有个网站,只要在图片链接后面加上一个参数,图片上就会立刻显示出对应的文字。比如在 URL 中写上 ?text=Hello
,返回的图片就会带着"Hello"字样,简直像是有人在后台帮我随时做了一张新图片一样。这个小小的发现勾起了我的好奇心,我想弄清楚这背后的原理,并且自己动手复现一个类似的功能。于是,我开始了这段探索之旅。

起初,我的直觉告诉我,这绝对不是一张提前存好的静态图片,而是一种动态生成的结果。毕竟文字是随参数变化的,肯定要经过实时处理。那么要么是服务器在接收到请求时临时绘制一张图片并返回,要么就是返回了一个 SVG 矢量文件,浏览器把它当作图片来渲染。

在我脑海里,整个过程大概就是这么运转的:用户在浏览器中请求一个带参数的 URL,服务器收到参数后通过代码逻辑生成对应的图片,把文字渲染上去,再以 PNG 或 SVG 的形式返回给浏览器。看似只是一个小功能,但里面其实隐藏了很多有意思的技术点,比如服务器端如何处理图片绘制、不同语言的图形库如何选择、返回格式对性能的影响等等。
带着这样的思路,我决定先尝试自己写一个最小化的实现。我主要用 Node.js 比较多,所以自然想到用 Node 来搭建一个小服务。Node 有个非常好用的库叫做 canvas
,它能够模拟浏览器里的 Canvas API,让我们可以在服务端生成带有图形和文字的 PNG 图片。我的计划就是:搭建一个最简单的 Express 服务,监听请求,把 text
参数解析出来,然后调用 canvas
绘制文字,最后返回 PNG 数据。听起来很直接,但真正实现起来还是遇到了一些小问题。
我先写了个基础框架,创建一个 server.js
文件:
js
const express = require('express');
const { createCanvas } = require('canvas');
const app = express();
const port = 3000;
app.get('/image', (req, res) => {
const text = req.query.text || 'Hello World';
// 创建画布
const canvas = createCanvas(600, 200);
const ctx = canvas.getContext('2d');
// 设置背景
ctx.fillStyle = '#f0f0f0';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// 设置文字样式
ctx.fillStyle = '#333';
ctx.font = '30px Arial';
ctx.fillText(text, 50, 100);
// 输出为 PNG
res.setHeader('Content-Type', 'image/png');
canvas.pngStream().pipe(res);
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
这一段代码其实就是最核心的流程。首先我用 createCanvas
创建了一张画布,指定宽高,然后拿到 2D 上下文 ctx
来绘制内容。背景我随便填了个灰色,接着设置了字体和颜色,把用户传过来的 text
绘制在画布上。最后,我通过设置 Content-Type
为 image/png
,再把画布流返回给响应,这样浏览器拿到的就是一张真正的 PNG 图片。等我在浏览器里访问 http://localhost:3000/image?text=测试
时,页面果然显示出了一张图片,上面写着"测试"两个字。第一次成功的时候,我还挺有成就感的。
不过问题很快来了。我发现返回的字体有点奇怪,显示出来的中文字体很难看,似乎没有加载到合适的字体文件。原来在服务端使用 canvas
时,它并不会自动读取系统的字体配置,需要我们自己手动指定。于是我查了下文档,发现可以用 registerFont
方法引入指定的字体。于是我在代码里加上:
js
const { createCanvas, registerFont } = require('canvas');
registerFont('C:/Windows/Fonts/msyh.ttc', { family: 'Microsoft YaHei' });
这样一来,我就能在绘制时写成 ctx.font = '30px "Microsoft YaHei"'
,中文就会变得好看很多。当然这个路径在 Linux 或 Mac 上是不一样的,如果要做成通用服务,最好把字体文件打包在项目里,直接读取相对路径,这样就不会依赖系统字体了。
接着我又想到,既然能绘制文字,那是不是也可以加入一些别的元素,比如自动调整文字大小,居中显示,甚至加个边框或者渐变背景。于是我在 canvas
上玩了一番。比如渐变背景的实现可以这么写:
js
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
gradient.addColorStop(0, '#ff7e5f');
gradient.addColorStop(1, '#feb47b');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
这样返回的图片就不再是单调的灰色背景,而是一个左右渐变的橙色背景。再搭配上黑色的文字,效果瞬间就丰富多了。我还试着用 ctx.strokeRect
给图片加上边框,感觉这个功能如果要用在生成个性化海报、临时封面图或者验证码之类的场景,会很有趣。
随着功能一点点增加,我觉得应该给整个项目的结构梳理一下,否则以后维护起来会很混乱。

结构其实非常简单,核心就是一个 server.js
文件,里面依赖 express
来做 HTTP 服务,依赖 canvas
来做绘图。额外的资源就是字体文件,保证文字显示的美观。这样一来,整个项目看上去就清晰明了了。
在实现过程中,我也顺便回顾了一些和 HTTP 相关的知识点。比如 Content-Type
头设置的重要性,如果不写成 image/png
,浏览器可能会把它当作普通文本来渲染,结果一堆乱码。还有 URL 参数的解析,Express 已经帮我处理好了 req.query.text
,如果要扩展成更多参数,比如 color
、size
、bg
等,其实只要多读取几个参数就可以了。这让我意识到,这种动态生成图片的功能完全可以做成一个小 API 服务,任何人只要传不同参数,就能得到个性化的图片。
我甚至想到了很多有趣的用途:比如给论坛或博客生成动态的签名图,每次显示不同的心情语录;给短链接服务加上一个临时生成的预览图;或者在前端项目里快速做一个带文字的占位图,省得自己再去用 Photoshop 画一张。