這應該是 Responsive Images 系列最後一篇,雖然想提一下 Icons 不過還是另開篇幅好了。這是比較花俏的東西,實用不實用就看個人了。
其實只是要練習 NodeJS 而已(誒
前言
之前提過 resrc.it 這個服務,他的做法就是搭配 Javascript 來動態的將圖片,依照目前所需要的尺寸,讓伺服器端直接回傳圖片。如果嫌自己的機器不夠忙,或是真有特殊需求的話,這樣做似乎也不錯。
當然,我們的目標應該不是這樣,理論上會這麼做,就是不想要在發佈時還要叫 Grunt 做一堆事情(設定也煩,
- 不需要 Grunt task 幫你做圖(不預先產出
- 使用者自行上傳的圖片可以不用一直 Watch
- 適用任何尺寸
- 跟 Media Query 無關
起手式
首先,你可能要準備一個伺服器端的環境,由於我不太會用 NodeJS,所以我們這邊以 NodeJS 為例子(疑,
- 準備 NodeJS
- 挑一個 Framework,我使用 sailsjs
- 準備需要的套件
- Coding
- Coding
- Coding
這裡只會讓你的服務看起來可以動,至於如何讓他放到正式站可以使用,就看個人造化了。我們安裝好 sailsjs
環境之後,利用 sails
來開啟一個新的專案,並且,產生一個新的 Controller 叫做 resimg
,
sails new responsive-images
開好之後,我們先編輯一下 package.json
,增加三個東西,
"imagemagick": "0.1.3",
"jpegtran-bin": "~0.2.2",
"optipng-bin": "~0.3.1"
然後先安裝一下,
npm install
然後就可以啟動這個專案來看看可不可以動,
npm start
接著產生一個 Controller 叫做 resimg
備用,
sails generate controller resimg
路由規則
雖然說我也想偷學 resrc.it
來做,但是想想還是簡單一點好了,
http://localhost:1337/{ $width }/{ $pixelratio }/{ $url }
這是我們這個專案需要用到的一個路由規則,簡單來說是這樣,
- 第一段指定圖片寬度
- 第二段指定顯示裝置的密度(display screen
disneydensity - 第三段指定圖片的來源 URL
當然,我們需要彈性,也就是說不給顯示裝置的密度也可以,所以我們使用正規表式是來解決,
'get ((?:/)([0-9]+))?((?:/)([0-9.]+))?((?:/)(https?://[\\w\\-_]+(?:.[\\w\\-_]+)+(?:/[\\w\\d\\-_]+)+.(jpg|jpeg|png|gif)$))?': 'ResimgController.generate',
上面的正規沒有包含 gif
檔案格式,如果需要,請自己加上去。至於正規做了什麼事情,
不要問!
這一串要加在哪裡呢?
cd config/
vim routes.js
幕後黑手
設定好路由之後,我們可以測試一下路由是不是正常,首先,先編輯一下 ResimgController
,
cd api/controllers
vim ResimgController.js
我們需要加上一個方法,叫做 generate
,
generate: function( req, res ) {
res.json('ok');
}
由於我們剛才有設定路由,所以你可以用瀏覽器來測試,
如果有錯誤請自行料理(疑,接著,我們先到 assets
資料夾下建立 resimgs
備用,
mkdir assets/resimgs
然後繼續料理 Controller,
generate: function( req, res ) {
var _width = req.params[1],
_dpr = req.params[3],
_url = req.params[5],
_ext = req.params[6];
if (_width === undefined
&& _dpr === undefined
&& _url === undefined) {
/**
* 全部都沒有東西,滾回首頁
*/
res.redirect('/');
}
if (_width === undefined) {
/**
* 沒有給寬度,滾回首頁
*/
res.redirect('/');
}
if (_url === undefined) {
/**
* 沒有給 URL,滾回首頁
*/
res.redirect('/');
}
/**
* 處理一下傳進來的東西
*/
_dpr = _dpr || 1;
_ext = _ext.toLowerCase();
/**
* 接著開始處理圖片
* 這裡並沒有強迫檢查圖片的格式,請自行想像
*/
var http = require('http'),
fs = require('fs'),
crypto = require('crypto'),
path = __dirname + '/../../assets/resimgs/',
filename = crypto.createHash('md5').update(_url).digest('hex'),
file = fs.createWriteStream( path + filename + '.' + _ext);
http.get(_url, function( response ) {
if (response.statusCode !== 200) {
res.json('file not found', 404);
}
response.on('data', function( data ) {
file.write(data);
}).on('end', function() {
file.end();
var im = require('imagemagick'),
dstFile = path + filename + '_' + _width + '_' + _dpr + 'x.' + _ext;
im.resize({
srcPath: path + filename + '.' + _ext,
dstPath: dstFile,
width: _width * _dpr
}, function(err, stdout, stderr) {
if (err) throw err;
var execFile = require('child_process').execFile,
optipngPath = require('optipng-bin').path,
jpegtranPath = require('jpegtran-bin').path;
if (_ext === "png") {
execFile(optipngPath, [dstFile], function(err, stdout, stderr) {
if (err) throw err;
res.json("ok");
});
} else if (_ext === "jpg" || _ext === "jpeg") {
execFile(jpegtranPath, ['-copy', 'none', '-optimize', '-outfile', dstFile, dstFile], function(err, stdout, stderr) {
if (err) throw err;
res.json("ok");
});
} else {
// Non-support image type.
}
});
});
}).on('error', function( err ) {
res.json(e.message, 403);
});
}
然後,如果沒錯的話,那麼我們用瀏覽器直接開還是會回傳 ok
,如果不是的話請去擲筊!當然,我們應該是要他回傳圖片樣式,而不是回傳 JSON,所以,稍微改寫一下成功的時候,把圖片返回,
if (_ext === "png") {
execFile(optipngPath, [dstFile], function(err, stdout, stderr) {
if (err) throw err;
res.writeHead(200, { 'Content-Type': 'image/png' });
res.send(fs.readFileSync(dstFile), 'binary');
});
} else if (_ext === "jpg" || _ext === "jpeg") {
execFile(jpegtranPath, ['-copy', 'none', '-optimize', '-outfile', dstFile, dstFile], function(err, stdout, stderr) {
if (err) throw err;
res.writeHead(200, { 'Content-Type': 'image/jpeg' });
res.send(fs.readFileSync(dstFile), 'binary');
});
}
最後他就會返回縮好的圖片給你,
這張圖片呢,原始尺寸是 3888 x 2592,大小是 9.1MB,從 flickr 直接取下來縮圖,縮出來的圖片是 1600 寬等比例,所需時間是 9.98 秒。
如果這張圖片丟給 resrc.it
測試的話,約莫是 6 秒!
前端
前端要做的事情其實很簡單,
- 使用
onresize
事件監聽視窗 - 取出你想要改變的圖片物件
- 依照比例算出需要的尺寸
- 呼叫 API(就是把
<img>
的來源,換成我們剛剛的路由規則
當然,代誌不是憨人想得那麼簡單。還是有一些地方需要下點功夫,
- 當我們使用
onresize
的時候,可能得考慮效能問題 - 取代圖片的計算,也需要考量效能問題
- 當圖片來源被置換時,需要解決圖片在置換讀取的等待時間
- 當圖片失敗的時候,需要有解套的方法
嗯,前端的事情大概就是這樣,如果你需要快速反應時間,還是不建議動態去縮圖啦(交給 Grunt 就好了
小結
其實我不是很會寫 NodeJS,所以哪裡寫壞掉一定是駭客入侵。
抽空再來說 Responsive Icons 跟 SVGs 好了。這個東西真的只是單純做出來自嗨用的!我也有一台虛擬機專門跑這件事情,原因是因為我懶得縮圖還要跑那麼多指令(或是用 Grunt,遠端的圖片全部透過 API 由機器上自己呼叫自己把縮圖跟最佳化全部做完了。
自己呼叫自己看起來好像蠻蠢的,
新的一年從 DIY 開始你不覺得挺不錯的嗎(誒