2016/03/15

Node.js VS PHP PDO Insert MySQL

最近在考慮將公司某些專案的排程改到 node.js 上面,主要是程式本體的 driver 跟 MySQL 之間的效能,先講結論,如果有用 MVC 的人不要用內建的 ORM 跑大資料寫入的排程,雖然 ORM 慢是大家都知道的事情,畢竟他本來就是圖方便的工具,但沒想到大量資料寫入他可以慢到將近一倍,本測試執行 node.js、PHP PDO,一次跑 3000 個 insert 各跑十次,以下是寫法以及結果。

環境說明
  1. PHP 5.6.15
  2. MySQL Ver 15.1 Distrib 10.1.9-MariaDB
  3. node.js 5.8.0
  4. Windows 7 64bit
  5. 8GB Ram
套件說明
  1. npm-mysql
  2. drealecs/time-benchmark

程式寫法

node.js
console.time('add');
console.log('start');

var mysql = require('mysql');
var connection = mysql.createConnection({
    host     : 'localhost',
    user     : '',
    password : '',
    database : 'test',
});
var length = process.argv[2] || 100;
var counter = 1;

connection.connect();

var add = (counter) => {
    connection.query('INSERT INTO `users` SET ?', {name: 'test'}, (err, rows, fields) => {
        if (err) {
            console.log(err);
        }

        if (counter < length) {
            add(++counter);
        } else {
            console.log('all done');
            console.timeEnd('add');
            connection.end();
        }
    });
};

add(counter);
PHP PDO
<?php

include 'vendor/autoload.php';

echo 'start'.PHP_EOL;

use TimeBenchmark\Stopwatch;

$length = isset($argv[1]) ? $argv[1] : 100;
$counter = 1;
$dsn = 'mysql:host=localhost;dbname=test;charset=utf8';
$db = new PDO($dsn, '');

$stopwatch = Stopwatch::create();
$stopwatch->start();

$add = function($counter) use ($length, $db, $stopwatch, &$add) {
    $sql = "INSERT INTO `users`(`name`) VALUES(:name)";
    $stmt = $db->prepare($sql);
    $stmt->bindValue(':name', 'test');
    $stmt->execute();

    if ($counter < $length) {
        $add(++$counter);
    } else {
        $stopwatch->stop();
        echo $stopwatch->getElapsedMilliseconds().'ms'.PHP_EOL;
    }
};

$add($counter);

測試結果

node.js
  1. 75500.335ms
  2. 76234.479ms
  3. 60025.145ms
  4. 68833.407ms
  5. 71067.471ms
  6. 63238.021ms
  7. 63908.226ms
  8. 57938.820ms
  9. 66872.004ms
  10. 66817.116ms
PHP PDO
  1. 63311.331ms
  2. 66825.682ms
  3. 67424.741ms
  4. 67320.732ms
  5. 62134.213ms
  6. 65586.558ms
  7. 67117.711ms
  8. 54427.443ms
  9. 66046.604ms
  10. 67741.773ms

結論

其實數據沒有差很多,但 node.js 比較會飄,所以還是觀察一下吧。

2016/03/11

$.Deferred VS Promise

Promise 是非常好用的一個特性,主要用來處理 async 的工作,在 jQuery 上很早就有 deferred 的實作,而 JavaScript ES6 也把這個功能加了進來,而且寫法非常清爽,但 ES6 要普及應該還要一段時間,jQuery 的 deferred 比較沒有相容性的問題,在這邊列一下同樣功能怎樣在雙邊實現。

單一運用

$.Deferred

let response = () => {
    return $.Deferred(dfd => {
        setTimeout(() => {
            dfd.resolve('done');
        }, 2000)
    }).promise();
}

$.when(response()).then(response => {
    console.log(response);
});

// or

let response = () => {
    let dfd = $.Deferred();

    setTimeout(() => {
        dfd.resolve('done');
    }, 2000)

    return dfd.promise();
}

$.when(response()).then(response => {
    console.log(response);
});

Promise

let response = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('done');
        }, 2000);
    });
}

response().then(response => {
    console.log(response);
});

等待多重結果

$.Deferred

let results = [];

[2000, 3000, 4000].forEach(timer => {
    results.push($.Deferred(dfd => {
        setTimeout(() => {
            console.log('single done!');
            dfd.resolve();
        }, timer);
    }));
});

$.when.apply($, results).then(() => {
    console.log('all done');
});

promise

let promises = [];

[2000, 3000, 4000].forEach(timer => {
    promises.push(new Promise(resolve => {
        setTimeout(() => {
            console.log('single promise done!');
            resolve();
        }, timer);
    }));
});

Promise.all(promises).then(() => {
    console.log('promise all done');
});

2016/03/09

apt and yum

purpose apt aptitude yum
update repo list apt-get update aptitude update none
install package apt-get install package_name aptitude install package_name yum install package_name
intall from file dpkg -i package.deb yum install package.rpm
rpm -ivh package.rpm
remove package apt-get remove package_name aptitude remove package_name yum remove package_name
check outdated package apt-get -s upgrade aptitude -s upgrade yum check-update
check single outdated package apt-get -s upgrade packge_name
apt-cache policy package_name
aptitude -s upgrade package_name yum check-update package_name
update package apt-get upgrade package_name aptitude package_name yum update package_name
rpm -Uvh package_name
search package apt-cache search package_name aptitude search package_name yum search package_name
list installed package dpkg -l yum list installed
rpm -qa
show package information apt-cache show package_name aptitude show package_name yum info package_name
remove packages from the local cache directory apt-get clean aptitude clean yum clean all
Reference

ubuntu.com

2016/02/27

工作工具

前幾天公司電腦掛點,由於程式碼都有上版控,文件類現在都用雲端,軟體大多用簡單免費的工具,所以重建工作環境沒有花多少時間,但為了下次可以更快,把一些基本必備的東西列一下。

Editor
Development Environment
FTP
Terminal

Xshell

Database

HeidiSQL

Other Tool

2016/01/27

webpack

將 JavaScript 模組化打包減少 http request 是目前幾乎必備的技能,之前使用 requirejs 來做這一塊,後來玩了一下 webpack 後發現回不去了,以此篇做個設定與使用筆記。

webpack.config.js
var webpack = require('webpack');
var sourcePath = __dirname + '/assets/js/src';

module.exports = {
    entry: sourcePath + '/entry.js',
    output: {
        path: __dirname + '/assets/js/build',
        filename: 'bundle.js'
    },
    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery"
        }),
        new webpack.optimize.UglifyJsPlugin({
            compress: {
                warnings: false
            }
        })
    ],
    module: {
        loaders: [
            {test: /\.js$/, exclude: /node_modules/, loader: "babel", query: {presets: ['es2015']}}
        ],
    },
    resolve: {
        alias: {
            init: __dirname + '/assets/js/app/init.js'
        }
    }
};

web.config.js 是 webpack 的設定檔,將常用的東西規範好之後,執行 webpack 這個指令他便會照你的設定檔作動,entry 指的是來源檔案,如果是多個檔案要 compiler 的話可以使用物件寫法:

    entry: {
        'a': sourcePath + '/a.js',
        'b': sourcePath + '/b.js'
    },
    output: {
        path: __dirname + '/assets/js/build',
        filename: '[name].js'
    },

這樣的格式他便會照你的 key name gen 一個一模一樣的檔案在 output 資料夾,plugin 的部分我用 npm 安裝了 jQuery,並用 plugin 宣告讓他可以全域使用,不用每次都 require,壓縮的部分使用 UglifyJS 2,基本上如果不使用 plugin 的話在 command line 下 webpack --optimize-minimize 會有一樣的效果,記得如果有下壓縮編譯的話速度會慢一些,開發階段可以先關起來,module 的部分使用了 babel-loader,撰寫的程式碼都會經過 babel 的編譯,確定你的 ES6 語法可以跨瀏覽器使用,alias 的部分就是讓你少打字而已,另外如果在 command line 使用 webpack -w 便會執行監看模式,如果 entry 的檔案有所變動的話就會 auto compiler,但我喜歡自己下指令,因為我還是會稍微看一下編譯結果,接下來開一個 webpack 的資料夾,來安裝相關的模組。

npm init
npm i babel-loader babel-core babel-preset-es2015 babel-preset-react --save-dev
npm i uglify-js --save-dev
npm i jquery@1.12 --save
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>webpack test</title>
    <script src="assets/js/build/bundle.js"></script>
</head>
<body>

</body>
</html>
assets/js/app/init.js
module.exports = {
    hello: (name = 'Chan') => {
        return `Hello world, I am ${name}`;
    }
};
assets/js/src/entry.js
var init = require('init');

$(function() {
    console.log(init.hello());
});

在 command line 執行 webpack,開啟網頁就會得到,Hello world, I am Chan,我的 VIM 有使用 JSHint 檢查原始碼,他偵測到 ES6 會報錯,只要在根目錄放置設定檔即可。

.jshintrc
{
    "esnext": true
}

2016/01/11

PHP JSON Content Type

目前大家網路交換格式大多用 json,前幾天踩到一個雷,送出需求到一個網頁以後,該頁面會將 html 的內容回吐,格式像是:

{
 "status": "done",
 "html": "<ul><li><a href=\"#\">link</a></li></ul>"
}

但當 conosole.log() 的時候內容被截斷了,出現可能只有 link</a></li> 之類的,原因很簡單,我的 output PHP 沒有給 content type,所以造成 request 頁面雖然可以解析 json format,但其實是 jQuery 硬做掉的,平時還是不要偷懶,記得要 output 什麼內容就加上該對應的 content type 吧。

// PHP
header('Content-Type: application/json; charset=utf-8');

// CodeIgniter
$this->output->set_content_type('application/json')->set_output(json_encode(compact('result')));

// Laravel 5+
use Illuminate\Http\Response;

return response(compact('result'), $status)->header('Content-Type', 'application/json');

// Or

return response()->json(compact('result'));

2016/01/08

Preview Image Before Upload

後台上傳圖片時,若想要看到預覽圖片,早期只有 IE 有 function 可以吃到 localhost 的檔案,讓你看到你要上傳的圖片樣貌,現在 HTML5 有 FileReader 的 api 可以用,範例如下:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
    <script type="text/javascript" charset="utf-8">
    $(function() {
        var allows = ['jpg', 'jpeg', 'gif', 'png'];

        $('#file').on('change', function() {
            var reader = new FileReader();
            var ext = $(this).val().split('.').pop().toLowerCase();

            try {
                if ($.inArray(ext, allows) === -1) {
                    throw new Error('檔案格式錯誤,僅允許' + allows.join(', '));
                }

                reader.readAsDataURL(this.files[0]);
                reader.onload = function(e) {
                    $('#content').html('<img src="' + e.target.result + '">');
                }
            } catch (e) {
                alert(e.message);
            }
        });
    });
    </script>
</head>
<body>
<input id="file" type="file" name="file">
<div id="content"></div>
</body>
</html>

這個方法有幾個問題:

  1. 瀏覽器支援問題
  2. 真實呈現問題(假設我們有透過後端處裡圖片無法呈現最後樣貌)

基於以上兩點,我決定還是透過 ajax + php 的方法來做掉,我的方法是,檔案 onChange 的時候 upload 去 server 端,請 server 端處理 tmp_name,然後傳回 base64_encode 的結果,這樣的好處是檔案我沒有使用 move 搬移,server 會自動幫我定期清除那些暫存檔,使用 base64 編碼因此不需要衍生其他檔案,做法如下:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.3/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.form/3.51/jquery.form.min.js"></script>
    <script type="text/javascript" charset="utf-8">
    $(function() {
        $('#file').on('change', function() {
            $('#frm').ajaxSubmit({
                url: 'preview.php',
                success: function(response) {
                    if (response.status !== 'done') {
                        alert(response.message);
                    } else {
                        $('#content').html('<img src="' + response.message + '">');
                    }
                }
            });

            return false;
        });
    });
    </script>
</head>
<body>
<form id="frm" method="post" action="upload.php">
<input id="file" type="file" name="file">
</form>
<div id="content"></div>
</body>
</html>
php
<?php

header('Content-Type: application/json');

include 'vendor/autoload.php';

$status = 'done';
$message = '';
$allows = ['jpeg', 'png', 'gif'];
$file = $_FILES['file'];
$map = function($item) {
    return "image/{$item}";
};

try {
    if ($file['name'] === '') {
        throw new Exception("請選擇檔案");
    }

    $type = $file['type'];

    if (!in_array($type, array_map($map, $allows))) {
        throw new Exception('檔案格式錯誤,僅接受'.implode(', ', $allows));
    }

    $layer = PHPImageWorkshop\ImageWorkshop::initFromPath($file['tmp_name']);
    $layer->resizeInPixel(100, null, true, 0, 0, 'MM');
    $result = $layer->getResult();

    ob_start();
    call_user_func(str_replace('/', '', $type), $result);
    $image = ob_get_contents();
    ob_end_clean();

    $message = "data:{$type};base64,".base64_encode($image);
} catch (Exception $e) {
    $status = 'fail';
    $message = $e->getMessage();
}

echo json_encode(compact('status', 'message'));

我使用了 ImageWorkshop 來處理縮圖等等的功能,這個部分可以自行替換 solution。