背景
當我們開發一個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對原始圖進行二次處理,根據規則批量生成各種大小的圖,至於圖片的名字可以采用一些規則,這樣返回的話,依然是存儲后的原始圖,而且格式則可以通過獲取服務器獲取,無需再進行緩存了。
由於大部分的代碼已經提供了,請不要再跟我要什么源碼之類的哦,大家可以自己嘗試搭建一下,今天就到這里了,如果有什么問題請告知我,謝謝各位。