使用nodejs搭建图片服务器(一)
背景
当我们开发一个Web项目的时候,为了将图片管理与web服务分离开,通常都会搭建一个图片服务器。
之所以选择nodejs是因为使用nodejs来搭建web项目相当简单而且快速,虽然这个图片服务器很简单,也有很多人会认为使用nodejs来当图片服务器不合适,但是当我们的应用没有达到非常大的程度的情况下,其实nodejs还是够用的。
会使用到的工具如下:
- nodejs
- express(nodejs mvc框架)
- body-parser(express middleware)
- gm(nodejs中用来处理图片的)
- uuid(nodejs中用于生成uuid)
- underscore(nodejs数据处理库)
- ImageMagick(gm会调用该程序处理图片)
那么接下来就来尝试实现这个简易的图片服务器吧, ^_^
搭建项目
首先要使用express来搭建项目,由于图片服务器的功能相对简单,只有2个功能:1、获取图片资源 2、上传图片,因此对应express只需要使用到bodyParser这样的组件,代码大致如下:
//app.js var app = require('express')(); process.app = app;//方便在其他地方使用app获取配置 require('./config')(__dirname, app);//所有配置 var mode = app.get('mode'); require('./controller/' + mode + 'Controller.js'); var config = app.get(mode); require('http').createServer(app).listen(config.port, function () { console.log('%s已经启动,正监听:%s', config.name, config.port); }); //config.js var path = require('path'); var OPTIONS = { targetDir: function (app) { return path.join(app.get('rootDir'), 'img'); }, read: { name: '<<图片服务器(读取)>>', port: 80, 'default': 'default.jpg', sizeReg: /(\w+)-(\d+)-(\d+)\.(\w+)$/ }, save: { name: '<<图片服务器(保存)>>', port: 9990 }, mode: 'read', contentType: { 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'png': 'image/png' } }; module.exports = function (rootDir, app) { app.set('rootDir', rootDir); var $ = require('underscore'); $.each(OPTIONS, function (v, k) { app.set(k, typeof v === 'function' ? v(app) : v); }); app.use(require('body-parser')()) };
上面独立出了config.js是为了将所有的配置放在一起方便维护,其次配置中的mode是为了区分当前是一个读取还是存储服务器。
获取图片
这是图片服务器的其中一个功能,用户根据url获取图片服务器内存储的图片,当后台接收这个请求后,首先判断图片上会否存在,如果存在则返回对应的图片否则返回默认的图片,大致代码如下:
var path = require('path'); var fs = require('fs'); var gm = require('gm').subClass({ imageMagick: true });//默认的情况下 gm使用的是另外一个图片处理程序 var app = process.app; var config = app.get('read'); var targetDir = app.get('targetDir'); app.get('/:filename', function (req, res) { var filePath = path.join(targetDir, req.params.filename); fs.exists(filePath, function (exists) { res.sendfile(exists ? filePath : path.join(targetDir, config.default)); }); });
使用以上的代码便可以对图片进行读取了,但是只能对targetDir目录下的文件进行读取且没有对文件类型进行判断,对文件类型的判断比较简单只要在方法执行之前对文件扩展名进行判断即可,至于增加了文件夹结构的话,跟直接从targetDir目录下读取是差不多的流程,稍微调整一下代码:
var contentTypes = app.get('contentType'); app.get('/:filename', function (req, res) { sendFile([], req.params.filename, res); }); app.get('/:folder/:filename', function (req, res) { sendFile([req.params.folder], req.params.filename, res); }); app.get('/:folder1/:folder2/:name', function (req, res) { sendFile([req.params.folder1, req.params.folder2], req.params.filename, res); }); function sendFile(folders, filename, res) { var ext = path.extname(filename).substr(1); if (!contentTypes[ext]) return res.sendfile(getFilePath()); folders.push(filename); var filePath = getFilePath(path.join.apply(path, folders)); fs.exists(filePath, function (exists) { res.sendfile(exists ? filePath : getFilePath()); }); } function getFilePath(filename) { return path.join(app.get('targetDir'), filename || config.default); }
接下来假设一个场景,如果上传了一张800*600的图片,但是在页面上显示的时候,也许我只想显示一张200*150的缩略图,这时候gm就该登场了,我们可以使用gm来为800*600的图片临时生成一张200*150的图片,并以buffer的形式回传给客户端,大致代码如下:
//判断请求图片是否存在 if (exists) return res.sendfile(filePath); var groups = filename.match(config.sizeReg); if (!groups) return res.sendfile(getFilePath()); folders[len] = groups[1] + "." + groups[4]; filePath = getFilePath(path.join.apply(path, folders)); var width = parseInt(groups[2]); var height = parseInt(groups[3]); //判断原始图是否存在 fs.exists(filePath, function (exists) { if (!exists) filePath = getFilePath(); var gm = gm(filePath); if (width > 0 && height > 0) gm.resize(width, height); gm.toBuffer(function (err, buffer) { if (err) return res.sendfile(getFilePath()); res.set('Content-Type', contentTypes[ext]); res.send(buffer); }); });
上传图片
由于图片服务器只提供最简单的功能,支持文件上传,但是并不会对上传的文件制作水印以及其他的处理,也不会将此功能开放到外网,因此图片上传服务器是在内网的,只能从web服务器那边处理图片以后上传到图片服务器,图片服务器对其进行存储并返回存储后的图片路径,大致代码如下:
var buf = require('buffer'); var fs = require('fs'); var path = require('path'); var util = require('util'); var gm = require('gm').subClass({ imageMagick: true }); var uuid = require('uuid'); var app = process.app; var contentTypes = app.get('contentType'); /* 请求包含如下参数: @ext 图片扩展名 @buffer 图片buffer数据 @folder 文件夹,格式:/aa/bb */ app.post('/', function (req, res) { var ext = req.body.ext; var buffer = req.body.buffer; if (!(ext && buffer && contentTypes[ext])) return res.json({ success: false }); var pathArgs = req.body.folder.replace(/\n/g, ''); if (pathArgs) pathArgs = pathArgs.substr(1).split('/'); else pathArgs = ['']; createDir(pathArgs); pathArgs.push(''); var filePath = createPath(pathArgs, ext); gm(new buf.Buffer(JSON.parse(buffer))).write(filePath, function (err) { if (err) return res.json({ success: false }); res.json({ success: true, data: util.format('\\%s.%s', pathArgs.join('\\'), ext) }); }); }); function createDir(pathArgs) { if (pathArgs[0]) { var dir = path.join(app.get('targetDir'), path.join.apply(path, pathArgs)); var exists = fs.existsSync(dir); if (!exists) fs.mkdirSync(dir); } } function createPath(pathArgs, ext) { var args = [app.get('targetDir')]; pathArgs[pathArgs.length - 1] = uuid.v1().replace(/-/g, ''); args.push.apply(args, pathArgs); var filePath = path.join.apply(path, args) + '.' + ext; return fs.existsSync(filePath) ? createPath(pathArgs, ext) : filePath; }
结尾
到这里我们就完成了一个nodejs版本的图片服务器了,因为尽可能将不需要的功能剥离到了其他的项目中去,因此图片服务器的功能就变得很简单、单一了。
对于图片获取服务器生成临时大小的文件,可以先缓存起来,并结合leasing模式进行管理,频繁使用的情况下可以翻倍增加它的租约,到期则直接释放。
而图片存储服务器,如果对于某些应用上传的图片都会生成固定系列大小的情况下, 可以使用gm对原始图进行二次处理,根据规则批量生成各种大小的图,至于图片的名字可以采用一些规则,这样返回的话,依然是存储后的原始图,而且格式则可以通过获取服务器获取,无需再进行缓存了。
由于大部分的代码已经提供了,请不要再跟我要什么源码之类的哦,大家可以自己尝试搭建一下,今天就到这里了,如果有什么问题请告知我,谢谢各位。