2017/10/20

解構賦值比較

PHP 有一個可以替換變數的 function 叫 list,使用範例如下

test.php
$arrs = ['chan', 37];
list($name, $age) = $arrs;
var_dump($name, $age);
$ php test.php
string(4) "chan"
int(37)

在 node.js 的世界的話,用法是這樣的

test.js
const arrs = ['chan', 37];
const [name, age] = arrs;
console.log(name, age);
$ node test.js
chan 37

python 的話超簡單

test.py
arrs = ['chan', 37]
name, age = arrs
print(name, age)
$ python test.py
('chan', 37)

在 PHP 以及 js 的世界其實我自己用的不多,其他人的範例 code 也不算到常見,但在 python 的世界出現的頻率還算不低

2017/10/19

動態呼叫 module - python

在寫程式上我們會常遇到使用不同的參數動態的呼叫他的 class,在 PHP 會使用 call_user_fun 或 call_user_func_array 來達到這個目的,而 node.js 載入 module 本來就是可以使用變數所以相對簡單,舉例來說

tester.js
module.exports = {
    test() {
        console.log('test');
    }
};
main.js
moduleName = 'tester';
const tester = require(`./${moduleName}`);

tester.test();
$ node main.js
test

不過 python 載入模組的方式無法使用字串,但可以透過 __import__ 以及 getattr 這兩個方式組合達到我們要的目的,檔案的結構如下:

transportation/
	__init__.py
	base.py
	bike.py
	car.py
	main.py
base.py
class Base(object):
    def go(self):
        print('go with {excute_name}'.format(excute_name=self.excute_name))

這隻 module 簡單來講就是 car 以及 bike 共同繼承的工具,目的在測試這樣的載入方式使用繼承會不會有問題

car.py
from base import Base

class Car(Base):
    excute_name = 'car';

基本上只是初始化一個 module,放一個 property

bike.py
from base import Base

class Bike(Base):
    excute_name = 'bike'

同上

main.py
# -*- coding: utf-8 -*-

transportations = [
    'car',
    'nope',
    'bike'
]

for transportation in transportations:
    class_name = transportation.capitalize() # 字首改大寫即為 class name
    module_name = 'transportation.{name}'.format(name=transportation)

    try:
        module = __import__(module_name, globals(), locals(), [class_name])
        collector = getattr(module, class_name)()
        collector.go()
    except Exception as e:
        print(str(e))

我們把要呼叫的 module 寫成一個 array 跑迴圈,中間我故意放了一個不存在的 module,程式碼裡面有使用 try exception,所以不用怕出錯會中斷,執行程式結果如下

$ python main.py
go with car
No module named nope
go with bike

2017/08/07

Prompt In All Language

最近喜歡刻一些互動的小工具,程式碼將需求分析後在 terminal 與 user 互動,confirm 內容後動作是常見的,這邊紀錄一下三個語言的 prompt 方法

prompt.php
echo 'Are you sure? [y/n]: ';

$handle = fopen('php://stdin', 'r');
$answer = trim(fgets($handle));

if ($answer === 'y') {
    echo 'Yes!'.PHP_EOL;
} else {
    echo 'Canceled!'.PHP_EOL;
}

fclose($handle);
prompt.js
const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

rl.question('Are you sure? [y/n]: ', (answer) => {
    if (answer.trim() === 'y') {
        console.log('Yes!');
    } else {
        console.log('Canceled!');
    }

    process.exit();
});
prompt.sh
#!/bin/bash

read -p 'Are you sure? [y/n]: ' answer

if [[ $answer == 'y' ]]; then
    echo "Yes!"
else
    echo "Canceled!"
fi

2017/07/25

Express MVC

最近心血來潮又把 express 玩了一下,寫篇文章來紀錄一下整個過程,會用到的工具如下:

  1. express 4
  2. node-orm2
  3. nodemon

首先安裝 express-generator,這是一個可以自動產生 express 需要內容的工具

$ sudo npm i -g express-generator

安裝好之後,我們切換到我們想要的目錄來產生專案資料夾,我選用 twig 當作我的樣板引擎,我在 PHP 使用 CI 的時候也是選用同一個引擎,可以無痛轉移

/var/www $ sudo express -v twig project

/var/www $ cd project
/var/www/project $ sudo npm i
/var/www/project $ sudo npm i orm mysql --save

專案目錄產生後,我們可以使用 nodemon 來監控目錄,也可以達到修改檔案後自動重起的效果

/var/www/project $ sudo nodemon bin/www

這時候打開 http://10.10.10.16:3000 (我的 vm 測試 IP)就可以看到預設網址,如果希望改 port 的話可以在 env 設定 port,或者直接去改 bin/www 這個檔案

打開 app.js 這是預設的內容

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var index = require('./routes/index');
var users = require('./routes/users');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'twig');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

所有的動作都在這隻 app.js 設定,我們來建立一個 product routes 檔案

routes/product.js
const express = require('express');
const router = express.Router();

router.get('/', function(req, res, next) {
    res.render('product/index');
});

module.exports = router;
view/product/index.twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h3>This is product page</h3>
</body>
</html>

將 app.js 加上這個 routes 的設定

app.js
var index = require('./routes/index');
var users = require('./routes/users');
var product = require('./routes/product');
var index = require('./routes/index');
var users = require('./routes/users');
var product = require('./routes/product');

app.use('/', index);
app.use('/users', users);
app.use('/product', product);

這時網址改成 http://10.10.10.16:3000/product,就會看到 This is product page 這幾個字

接下來我們進行資料庫的串連,在 MySQL 內建立一個叫 express 的資料庫,另外建立一個 products 的 table 並且塞入資料

CREATE TABLE `products` (
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`name` VARCHAR(50) NOT NULL
) ENGINE=InnoDB CHARACTER SET utf8 COLLATE utf8_unicode_ci;

接下來我們來設定資料庫連接,因為我不喜歡全部的東西都塞在 app.js,所以我另外開一個資料夾來放設置檔

config/db.js
module.exports = (app, orm) => {
    app.use(orm.express("mysql://root:123456@127.0.0.1/express", {
        define: function(db, models, next) {
            models.product = db.define('products', {
                id: Number,
                name: String
            });
            next();
        }
    }));
};

修改 app.js 啟動這個設定,只要放在 app 以及 orm 初始化後即可

app.js
require('./config/db.js')(app, orm);

我們先將 routes/product.js 修改成以下內容,變可以從網址先看到結果

const express = require('express');
const router = express.Router();

router.get('/', function(req, res, next) {
    req.models.product.all((err, data) => {
        res.json(data);
    })
});

module.exports = router;
http://10.10.10.16:3000/product
[{"id":1,"name":"product a"},{"id":2,"name":"product b"}]

這是最基本的用法,但我覺得這樣可重複利用性太低了,所以我們來建立類似 model 機制吧

routes/product.js
const express = require('express');
const router = express.Router();
const Product = require('../model/product.js');

router.get('/', async(req, res, next) => {
    try {
        const product = new Product(req.models.product);
        res.json(await product.getAllNews());
    } catch (e) {
        res.send(e);
    }
});

module.exports = router;
model/product.js
class Product {
    constructor(model) {
        this.model = model
    }

    async getAllNews() {
        return new Promise((resolve, reject) => {
            this.model.all((err, data) => {
                if (err) {
                    reject(err);
                }

                resolve(data);
            })
        })
    }
}

module.exports = Product;

這樣的執行結果會一模一樣,但把東西拆乾淨模組化了,再修改一次在 view 呈現吧

routes/product.js
const express = require('express');
const router = express.Router();
const Product = require('../model/product.js');

router.get('/', async(req, res, next) => {
    try {
        const product = new Product(req.models.product);
        res.render('product/index', {
            products: await product.getAllNews()
        });
    } catch (e) {
        res.send(e);
    }
});

module.exports = router;
views/product/index.twig
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
    <h3>This is product page</h3>
    {% for product in products %}
    <div>product: {{ product.name }}</div>
    {% endfor %}
</body>
</html>

以上就是這兩天試玩 express 的整理,沒有實現所謂的 controller 部份,就先把 routes 當 controller,當然一定會有更多更好的方法,再慢慢探索,在此也感謝陳默司大大的指導

2017/06/28

Monitor File Change And Execute Code

現在寫 PHP 的時候都會試著在 node.js 以及 shell script 上實現,所以會不斷的嘗試程式碼,每次要看執行結果都要下一次 command 也挺累的,寫了一隻 w.js 來監控這三種檔案的變動並帶入對應的呼叫方式來節省時間

此功能需安裝 watch,執行方法為 node w.js xxx.php

w.js
const watch = require('watch');
const { exec } = require('child_process');
const path = require('path');
const argvs = process.argv;
const target = argvs[2];
const fs = require('fs');
const commandMap = {
    sh: 'bash',
    php: 'php',
    js: 'node'
};

if (target === undefined) {
    console.log('Need file argument!');
    return;
}

if (!fs.existsSync(target)) {
    console.log(`${target} is not exists!`);
    return;
}

let ext = path.extname(target).replace(/\./g, '');

if (Object.keys(commandMap).indexOf(ext) === -1) {
    console.log(`${ext} is not legal!`);
    return;
}

console.log(`Start to watch "${target}"!`);

watch.createMonitor('./', (monitor) => {
    monitor.on('changed', (file) => {
        if (file === target) {
            exec(`clear; ${commandMap[ext]} ${file}`, (error, stdout, stderr) => {
                if (error) {
                    console.error(`exec error: ${error}`);
                    return;
                }

                console.log(stdout);
                console.log(stderr);
            });
        }
    });
});

實際使用畫面大概是這樣

w.js 示意圖

2017/06/27

Random String

公司有一個需求想要產生 unique id,雖然 PHP 有 uniqid 可以使用,但他最基本的長度是 13 個字元,所以決定自己刻一個利用 0-9、a-z 產生的亂數密碼,會有 PHP、nodejs 以及 shell script 版本。

PHP
<?php

function code($length = 10)
{
    $codeString = '0123456789abcdefghijklmnopqrstubwxyz';
    $codeStringSplit = str_split($codeString);
    $codeStringSize = count($codeStringSplit);
    $code = '';

    for ($i = 0; $i < $length; ++$i) {
        $rand = mt_rand(0, $codeStringSize - 1);
        $code .= $codeStringSplit[$rand];
    }

    return $code;
}

for ($i = 0; $i < 10; $i++) {
    echo code().PHP_EOL;
}
node.js
function code(length) {
    var stringLength = length || 10;
    var codeString = '0123456789abcdefghijklmnopqrstubwxyz';
    var codeStringSplit = codeString.split('');
    var codeStringSize = codeStringSplit.length;
    var code = '';
    var min = 0;
    var max = codeStringSize - 1;

    for (var i = 0; i < stringLength; ++i) {
        var rand = Math.floor(Math.random() * (max - min + 1)) + min;

        code += codeStringSplit[rand];
    }

    return code;
}

for (var i = 0; i < 10; ++i) {
    console.log(code());
}
bash
#!/bin/bash

function code() {
    result=""
    length=10
    codeString="0123456789abcdefghijklmnopqrstubwxyz";
    declare -a array

    for (( i = 0; i < ${#codeString}; i++ )); do
        array+=(${codeString:$i:1})
    done

    if [[ -n $1 ]]; then
        declare -i length=$1
    fi

    max=$((${#array[@]} - 1))
    
    for (( i = 0; i < ${length}; i++ )); do
        rand=$(shuf -i 0-${max} -n 1)
        result="${result}${array[${rand}]}"
    done

    echo ${result}
}

for (( j = 0; j < 10; j++ )); do
    code
done

2017/02/27

Reverse Proxy For Docker

前言

現在很流行用 docker 處理一些服務,以我這行來講用來處理 LAMP 或者是 LNMP 最常見,也可能是產生個 gitlab 之類的,docker 最大的優點是服務都跑在 container,安裝快啟動也快,要幾個有幾個,寫好 config 像是 Dockerfile 之類的設定還可以一個指令搞定,讓你的電腦可以保持很乾淨,網站使用 80 port 當作預設的入口,但每個 server port 都是唯一的,所以假設你有很多網站服務,可能會指派很多不同的 port,像是 localhost:66666 或者是 localhost:66667,但有些測試你可能想要盡可能逼真,希望是用網址來跑服務的話,我們可以在 80 port 假一個反向的 proxy server,讓你可以將 domain 指定到帶 port 的 IP,下面會示範 nginx 以及 apache 的設定方式

Docker

我測試的環境是 Windows,我先使用 Vagrant 安裝一個 ubuntu 16 的 VM,IP 為 10.10.10.22,之後將會在這個 VM 裡面跑 nginx 以及 apache servie,還有 docker

我 container 使用的 image 是 novice/lnmp,我先啟動兩個 web service 服務

$ sudo docker run -itd --name web1 -p 33333:80 novice/lnmp
$ sudo docker run -itd --name web2 -p 33334:80 novice/lnmp

我將 33333 port 給 nginx 這個 web service 使用,33334 給 apache 使用,然後我們將 Windows 的 hosts 設定一下

10.10.10.22 docker.web1
10.10.10.22 docker.web2

接著我進到 apache container 的 /var/www 目錄,加上一個 hello.php

<?php

echo 'hello I am web1';

同樣在 nginx 的 container /var/www 加入 hello.php。

<?php

echo 'hello I am web2';

在網址輸入 http://docker.web1:33333/hello.php 以及 http://docker.web2:33334/hello.php 各別會看到 hello I am web1 以及 hello I am web2,不過其實目前兩個網址互換得到的結果也會一樣,因為現在主要是因為不同的 port 帶出不同的 web service,而我們要做的事情就是把 port 拿掉以後還會成立

nginx

先在這個 VM 安裝 nginx。

$ sudo apt-get install nginx

安裝好之後,我們在 /etc/nginx/sites-available的目錄建立一個 proxy.conf內容如下

server {
    listen 80;
    sendfile off;
    server_name docker.web1;

    location / {
        proxy_pass http://127.0.0.1:33333;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host $server_name;
    }
}

server {
    listen 80;
    sendfile off;
    server_name docker.web2;

    location / {
        proxy_pass http://127.0.0.1:33334;
        proxy_redirect     off;
        proxy_set_header   Host $host;
        proxy_set_header   X-Real-IP $remote_addr;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host $server_name;
    }
}

接著設定 config,然後將服務重啟

$ sudo ln -s /etc/nginx/sites-available/proxy.conf /etc/nginx/sites-enabled/proxy.conf
$ sudo service nginx restart

此時如果你將 port 拿掉還是可以看到正確結果的話表示成功了,說明一下 sendfile off 主要是因為我使用 VM,這個不關的話會一直看到 cache,如果你是直接在 Linux 環境的話應該是不用特別設定

Apache

首先來安裝 apache,記得先將 nginx 關掉,並且確認兩個 proxy 必要的 module 有開

$ sudo service nginx stop
$ sudo apt-get install apache2
$ sudo a2enmod proxy
$ sudo a2enmod proxy_http

接著在 /etc/apache2/sites-available加入 proxy.conf,內容如下

<VirtualHost *:80>
    ServerName docker.web1
    ProxyPass / http://127.0.0.1:33334/
    ProxyPassReverse  / http://127.0.0.1:33334/
</VirtualHost>

<VirtualHost *:80>
    ServerName docker.web2
    ProxyPass / http://127.0.0.1:33333/
    ProxyPassReverse  / http://127.0.0.1:33333/
</VirtualHost>

接著我們製作 link,以及重起 apache

$ sudo a2ensite proxy.conf
$ sudo service apache2 restart

如果那兩個網址重整以後一樣可以看到對應的文字,表示 apache reserse proxy 也成功了