[Web] Responsive Images On-Demand

這應該是 Responsive Images 系列最後一篇,雖然想提一下 Icons 不過還是另開篇幅好了。這是比較花俏的東西,實用不實用就看個人了。

其實只是要練習 NodeJS 而已(誒

前言

之前提過 resrc.it 這個服務,他的做法就是搭配 Javascript 來動態的將圖片,依照目前所需要的尺寸,讓伺服器端直接回傳圖片。如果嫌自己的機器不夠忙,或是真有特殊需求的話,這樣做似乎也不錯。

當然,我們的目標應該不是這樣,理論上會這麼做,就是不想要在發佈時還要叫 Grunt 做一堆事情(設定也煩,

  • 不需要 Grunt task 幫你做圖(不預先產出
  • 使用者自行上傳的圖片可以不用一直 Watch
  • 適用任何尺寸
  • 跟 Media Query 無關

起手式

首先,你可能要準備一個伺服器端的環境,由於我不太會用 NodeJS,所以我們這邊以 NodeJS 為例子(疑,

這裡只會讓你的服務看起來可以動,至於如何讓他放到正式站可以使用,就看個人造化了。我們安裝好 sailsjs 環境之後,利用 sails 來開啟一個新的專案,並且,產生一個新的 Controller 叫做 resimg

sails new responsive-images

Responsive Images Project

開好之後,我們先編輯一下 package.json,增加三個東西,

    "imagemagick": "0.1.3",
    "jpegtran-bin": "~0.2.2",
    "optipng-bin": "~0.3.1"

然後先安裝一下,

npm install

然後就可以啟動這個專案來看看可不可以動,

npm start

Start project

接著產生一個 Controller 叫做 resimg 備用,

sails generate controller resimg

Generate controller

路由規則

雖然說我也想偷學 resrc.it 來做,但是想想還是簡單一點好了,

http://localhost:1337/{ $width }/{ $pixelratio }/{ $url }

這是我們這個專案需要用到的一個路由規則,簡單來說是這樣,

  • 第一段指定圖片寬度
  • 第二段指定顯示裝置的密度(display screen disney density
  • 第三段指定圖片的來源 URL

當然,我們需要彈性,也就是說不給顯示裝置的密度也可以,所以我們使用正規表式是來解決,

'get ((?:/)([0-9]+))?((?:/)([0-9.]+))?((?:/)(https?://[\\w\\-_]+(?:.[\\w\\-_]+)+(?:/[\\w\\d\\-_]+)+.(jpg|jpeg|png|gif)$))?': 'ResimgController.generate',

上面的正規沒有包含 gif 檔案格式,如果需要,請自己加上去。至於正規做了什麼事情,

不要問!

這一串要加在哪裡呢?

cd config/
vim routes.js

Routes

幕後黑手

設定好路由之後,我們可以測試一下路由是不是正常,首先,先編輯一下 ResimgController

cd api/controllers
vim ResimgController.js

我們需要加上一個方法,叫做 generate

generate: function( req, res ) {

    res.json('ok');
}

由於我們剛才有設定路由,所以你可以用瀏覽器來測試,

Route test

如果有錯誤請自行料理(疑,接著,我們先到 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');
                    });
                }

最後他就會返回縮好的圖片給你,

Demo

這張圖片呢,原始尺寸是 3888 x 2592,大小是 9.1MB,從 flickr 直接取下來縮圖,縮出來的圖片是 1600 寬等比例,所需時間是 9.98 秒。

如果這張圖片丟給 resrc.it 測試的話,約莫是 6 秒!

前端

前端要做的事情其實很簡單,

  • 使用 onresize 事件監聽視窗
  • 取出你想要改變的圖片物件
  • 依照比例算出需要的尺寸
  • 呼叫 API(就是把 <img> 的來源,換成我們剛剛的路由規則

當然,代誌不是憨人想得那麼簡單。還是有一些地方需要下點功夫,

  • 當我們使用 onresize 的時候,可能得考慮效能問題
  • 取代圖片的計算,也需要考量效能問題
  • 當圖片來源被置換時,需要解決圖片在置換讀取的等待時間
  • 當圖片失敗的時候,需要有解套的方法

嗯,前端的事情大概就是這樣,如果你需要快速反應時間,還是不建議動態去縮圖啦(交給 Grunt 就好了

小結

其實我不是很會寫 NodeJS,所以哪裡寫壞掉一定是駭客入侵

抽空再來說 Responsive Icons 跟 SVGs 好了。這個東西真的只是單純做出來自嗨用的!我也有一台虛擬機專門跑這件事情,原因是因為我懶得縮圖還要跑那麼多指令(或是用 Grunt,遠端的圖片全部透過 API 由機器上自己呼叫自己把縮圖跟最佳化全部做完了。

自己呼叫自己看起來好像蠻蠢的,

新的一年從 DIY 開始你不覺得挺不錯的嗎(誒

Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei