使用 Node.js 开发一个简单的图片服务器
背景
之前在开发实验室官网(https://www.xiyoumobile.com)的时候,由于图片特别多,学校服务器走电信和教育网,带宽也不够,在某些网络环境图片加载十分缓慢,而且有时候主页打开需要 10s+的时间,所以考虑从图片压缩上节省网络带宽
目前业界的图片压缩方案有以下几种,他们的压缩比都很高,都可以有效减小图片文件的体积
方案 | 提出者 | 编码类型 | 开源 | 官网 | 浏览器支持情况 |
---|---|---|---|---|---|
WebP | VP8 | 是 | https://developers.google.com/speed/webp/ | Chrome、Opera、Edge、Android,及其他基于 Chromium 内核的浏览器 | |
BPG | Fabrice Bellard | HEVC | 是 | http://bellard.org/bpg | 无 |
TPG | 腾讯 | AVS2(类似 AVC) | 否 | 未知 | QQ 浏览器 |
JPEG XR | 微软 | 未知 | 否 | (微软已投奔 WebP) | IE、Edge |
根据 TPG 推出时腾讯的微信公众号文章来看,TPG 的压缩算法相比 WebP 而言,能够使体积变得更小,图片更清晰,但是适用范围还是太局限了,所以还是选择了 WebP
但主要问题是前端代码中已经固定了某些静态图片资源的 URL 及扩展名,数据库中保存的 HTML 中的图片引用也是如此,即使把图片全部转换为 WebP 格式,如何保证不改动 HTML 及 CSS 代码,就能够对支持的浏览器返回 WebP 格式图片,而不支持的浏览器返回 JPG/PNG?
于是经过一段时间摸索和整理,就有了现在用在我个人网站的 coderyuan-image-server:https://github.com/yuanguozheng/coderyuan-image-server
基本原理及流程
判断浏览器是否支持 WebP
首先引用一段来自 w3 的定义(RFC 2616):https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
The Accept request-header field can be used to specify certain media types which are acceptable for the response. Accept headers can be used to indicate that the request is specifically limited to a small set of desired types, as in the case of a request for an in-line image.
Accept = "Accept" ":"
#( media-range [ accept-params ] )
media-range = ( "*/*"
| ( type "/" "*" )
| ( type "/" subtype )
) *( ";" parameter )
accept-params = ";" "q" "=" qvalue *( accept-extension )
accept-extension = ";" token [ "=" ( token | quoted-string ) ]
这里定义了 HTTP 请求头中 Accept 字段的含义——表示 Response 可以接受的媒体类型(MIME-Type)
如果支持 WebP,那么 Accept 中将会包含image/webp
这样的字符串
于是乎,我们就可以根据浏览器请求图片资源时的 Accept 字段来区分是否真正支持 WebP 格式
为什么不使用 User-Agent?
UA 一般用于区分浏览器类别、操作系统、网络状况等,通过这些字段区分是否支持 WebP,是不可靠的。
举个例子,Edge 浏览器第一版是不支持 WebP 的,现在及以后的版本就会支持了,如果通过 UA 来区分,成本就会十分高昂
完整的图片请求流程
图片上传
为了能够支持根据Accept
自动返回对应的图片,以达到节省网络带宽,在图片采集上也得进行定制,即:在上传原图(如:PNG、JPG)的同时,还应对图片进行转码,生成对应的 WebP 文件,并按照一定的规则重命名,以便 Image Server 查找
如:上传文件为:image.png
,保存这个图片的同时,应当再转码一个 WebP 格式文件,保存为:image.webp
或者image.png.webp
那么在请求这个文件的时候,附加一个.webp
的扩展名即可
技术框架
由于比较熟悉 Node.js,而且它的异步非阻塞 I/O 能够在有限的资源下,带来良好的性能,所以我在这里选择了 Node.js 来开发图片服务器(Image Server),其中包括图片上传、转换和路径解析、图片传输模块。当然,选择其他的语言及相关的框架也是完全没有问题的,比如:Java、Go
express
做过 Node.js 开发的人应该都不陌生,可谓是 Node 里最著名的 Web 框架,使用它来构建一个 Web 服务,并处理 HTTP 请求的 Header、参数(包括上传文件)、路由等十分方便
Github:https://github.com/expressjs/express/
node-static
node-static 用于做静态文件服务器,主要用于根据请求的 URL、Accept 等参数,返回给客户端(浏览器)所对应的图片,从而实现同一 URL,Chrome 返回 WebP,而 Safari/FireFox 返回 PNG 的效果
Github:https://github.com/cloudhead/node-static
为什么不使用 Express 自带的 serve-static?
查阅 node-static 的源码,发现它支持的功能更适用,如:断点续传、gzip 压缩等
没有对 Express 模块的依赖,如果做单独使用,功能支持的比 serve-static 更好
性能更好一些
实现
图片解析及传输服务
图片传输服务主要用于向服务器发送请求的客户端传输其能够支持的图片文件流,所以需要实现请求路径的解析和实际文件的匹配,最后将文件流封装在 HTTP 响应报文当中
这里只需要引入 Node 自带的url
和path
即可完成对请求 URL 的解析,非常方便
代码如下:
1 | const URL = require('url'); |
由path.parse
解析所得到的对象继承自ParsedPath
类,包含 5 个字段,root
、dir
、base
、ext
、name
,其解释如下
1 | /** |
所以我们拿到 name 以后,就可以根据需要的扩展名拼接成一个完成的绝对路径,从而实现文件检索和分发的工作
这里来列出对不同Accept
值进行不同文件分发的代码
1 | // If HTTP header accepts contains 'image/webp' (like Chrome), return webp file. |
这样,基本的解析及分发模块就完成了,后面再补上读取配置和对错误的处理,如:文件不存在等,就比较完整了
文件上传服务
接管 Express 的文件上传
文件上传服务业务逻辑相对复杂一些,首先我们需要配置好为 Express 准备的 multer 模块,由于 multer 的参数非常丰富,我们只需要配置限制大小、过滤器方法和目标路径
1 | const multer = require('multer'); |
由于暂时只支持单张图片上传,所以这里使用了 single,如果需要,可以进行一下修改,也非常简单
使用了 multer 以后,只需要将 Express 的 Request、Response 交给 multer 处理即可,文件会根据之前的配置上传到相应的目录中去,如下所示:
1 | app.use('/', (req, res) => { |
更多的用法,可以参考 multer 的官方文档:https://github.com/expressjs/multer
使用 GraphicsMagick 添加图片水印
这里开发了一个支持水印的功能,基本原理是根据上传图片的大小,和设计好的水印图片(半透明)的大小,计算出一个水印图片的目标尺寸,经过缩放以后,将两张图片利用 GraphicsMagick 进行合并,最终变成一张带有水印的图片,这里简单列举拼合图片的操作代码:
1 | const gm = require('gm'); |
推荐使用经过开发者封装过的 gm 组件,可以很方便的帮你调用 GraphicsMagick 工具命令(前提是系统已经装了 GraphicsMagick),当然也可以自己使用 Node 的代码来调用 GraphicsMagick 或 ImageMagick 的命令,成本相对比较高
gm 组件 Github:https://github.com/aheckmann/gm
GraphicsMagick 官网:http://www.graphicsmagick.org/
生成 WebP 格式文件
这里我们可以使用 Node 组件 cwebp(也叫 node-webp),可以使用操作 cwebp 命令,从而使用 PNG、JPG 等图片生成 WebP 格式的图片,代码非常简单:
1 | const CWebp = require('cwebp').CWebp; |
node-webp 官方 Github:https://github.com/Intervox/node-webp
回调可供访问的完整 URL
经过上面的步骤,我们上传的图片已经符合最初的预期目标,但不要忘了,将最终生成的文件名,拼接上你的图片服务器前缀,并以 JSON 或其他格式回传给调用方(这里不再列举代码了)
到此,我们的图片服务器主要编码基本都已经完成了,可以根据项目需要设计一些配置、Log 等模块以保证服务的可配置和稳定性,完整的图片服务器代码,可以参考我的 Github:https://github.com/yuanguozheng/coderyuan-image-server
部署
在我的代码中,将图片上传服务和图片解析传输服务分为了两个 TCP 端口运行,以方便部署时进行 nginx 反向代理的配置,这里贴一下 nginx 配置文件的图片部分:
1 | server { |
而基于 Node 的部分,使用 forever 或者 pm2,直接运行app.js
即可
1 | // app.js |
遇到一个坑: 由于 nginx 会对图片这种静态文件进行缓存,所以在反向代理时,如果没有进行明确配置,会将缓存文件存入proxy_temp
目录中,nginx 进程往往没有这个目录的写入权限(由于使用了 nobody 运行),所以需要开放这个目录的读写权限,能够使 nginx 正常读写 cache,否则将造成在 Safari 浏览器上也返回 WebP 格式文件的错误!!
后继优化
安全性的保证还不够充分,适合个人网站使用,但可以改进用户认证的方案
可以考虑支持文件传入 cdn 平台,满足拥有 cdn 的使用场景
支持多图传输