2015/10/19

watch.js

現在處於 node.js 幼幼班的階段,變覺得 node.js 的發明實在太方便,尤其是自己對 JavaScript 有一定的熟悉度所以學起來很愉悅,今天的 case 是,之前有介紹過 r.js,未來會朝向所有相關的 js 檔案全部壓成一個減少 server request,在開發階段會面臨一個問題就是東西有改動就要 compile,每次都要手動執行也挺累的,今天看到一個套件名為 watch,他可以偵測資料夾內檔案新增、修改、刪除的事件,於是寫了個 script 來幫我監聽並且做 auto compile。

~/watch/project.js

var watch = require('watch');
var path = require('path');
var exec = require('child_process').exec;
var monitorPath = '/var/www/project/assets/js/';

watch.createMonitor(monitorPath + 'src/', function(monitor) {
    monitor.on('changed', function(f) {
        var baseName = path.basename(f);
        var body = path.basename(f, '.js');
        var ext = path.extname(f);

        if (ext === '.js') {
            var command = 'r.js -o ' + monitorPath + 'build.js name=src/' + body + ' out=' + monitorPath + 'build/' + baseName + ' optimize=none';

            console.log('start compile ' + baseName);
            exec(command, function(error, stdout) {
                console.log(stdout);
            });
        }
    });
});

我在根目錄下面建立了一個 watch 的目錄,日後有新的專案的話就會多建一個檔案來監控,東西都設定完成後,只要在該目錄下 node project.js,watch 就會自動啟動了,我這邊編譯先把 uglifying 關掉,因為他會影響編譯速度,如果檔案已經確定是 final 不會變動的話,在去手動執行有 uglifying 的指令,如果在 windows 遇到錯誤訊息,只要把 command 改成 r.js.cmd 就可以了。

2015/10/13

r.js

用了一陣子 requirejs,之前都使用一些奇淫巧技讓每個頁面可以依照相依性導入各自頁面的 javascript,但其實這只達到了 requirejs 的非同步載入而已,檔案一多病沒有減少 request,requirejs 最強大的功能應該是他的 r.js 功能,他可以把相依性依照你設定的方式載入,並且合併成一個檔案,今天來示範一下我現在的布局。

目錄結構

- js/
 - build/
 - jquery/
 - src/
 - tool/

將所有的 javascript 檔案放在 js 目錄下,使用了四個目錄:

  • build/ (compiler 過後的檔案)
  • jquery/ (jQuery 相關檔案)
  • src/ (compiler 前的檔案)
  • tool/ (一些共用的工具檔案)

今天要示範我們要在 index.html 引入 index.js,index.js 要包含 jqueryjquery.formtools/func.js,並且依照相依性載入,首先我們建立 js/build.js

js/build.js
({
    baseUrl: './',
    paths: {
        'jquery': 'jquery/jquery.min',
        'form': 'jquery/jquery.form'
    },
    shim: {
        'form': ['jquery'],
        'src/index': ['form']
    },
    waitSeconds: 20
})

build.js 就是所有要 compiler 檔案的預設檔案,可以設定路徑、相依性等相關規範,我在這邊會將所有跟 jquery 相關的 plugin 都 depends on jquery,這樣當我各個 js 隨便調用任一個 jquery 的 plugin 時就會預載 jquery,接下來我們建立 src/index.js

src/index.js
requirejs(['tool/func'], function(func) {
    $(function() {
        func.init();
    });
});

我把 plugin 的調用寫在 build.js 裡面,工具的調用寫在各自的 js 裡面,這樣彈性變很大。

tool/func.js
define(function() {
    return {
        init: function() {
            console.log('func');
        }
    }
});

func.js 放個簡單的 funtion 讓 index.js 測試調用,接下來我們執行 r.js

$ r.js -o build.js name=src/index out=build/index.js

Tracing dependencies for: src/index
Uglifying file: D:/req/js/build/index.js

D:/req/js/build/index.js
----------------
D:/req/js/jquery/jquery.min.js
D:/req/js/jquery/jquery.form.js
D:/req/js/tool/func.js
D:/req/js/src/index.js

r.js 透過 build.js 執行了 src/index compiler 到 build/index.js,並且是依照我要的 order,接下來將結果放到 index.html 裡面。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>r.js</title>
    <script data-main="js/build/index" src="js/require.js"></script>
</head>
<body>

</body>
</html>

執行網頁後在 console 得到 func 的 output,如果東西在開發階段,可以加一個參數讓 compiler 的檔案不作最佳化, $ r.js -o build.js name=src/index out=build/index.js optimize=none,這樣就可以方便除錯了。

假設我們今天編譯了一個公用的工具,一旦你有修改到那隻工具,就要面臨每個檔案都必須重新編譯的問題,我們可以另外設一個 config 檔案來做群組的編譯。

builds.js
({
    paths: {
        'jquery': '../jquery/jquery.min',
        'form': '../jquery/jquery.form',
        'init': '../tool/func.js',
    },
    shim: {
        'form': ['jquery'],
        'src/index': ['form']
    },
    baseUrl: './',
    appDir: 'src/',
    dir: 'build/',
    waitSeconds: 20,
    modules: [
        {name: 'index'},
        {name: 'other'}
    ]
})

設定完畢後執行 r.js -o builds.js,就會將 modules 裡面的內容全部重新跑一遍。

2015/10/07

CodeIgniter Migration

一般的程式設計師設計資料庫常常會使用 PHPMyAdmin 或者是一些軟體直接在 DB 上做表單的建立或管理,一個人的專案可能還 ok,但如果遇到 co-work,或者是測試機與正式機不同機器,很難避免粗心漏掉內容的問題,最好的解決方案就是用程式產生相關的內容,PHP 兩個熱門的框架 Laravel 以及 CodeIgniter 都有提供 Database Migration 的功能,今天來初探 CodeIgniter 的 Migration 部分。

CodeIgniter 所有 Migration 的內容只要看過上面兩個連結就可以做完,讓我們來 step by step 案例。

前置作業

application/config/migration.php

這個檔案是設定 migration config 的位置,有幾個重點要設定

$config['migration_enabled'] = true;

miagration_enabled 的部分開啟。

$config['migration_type'] = 'sequential';

修改 migration_typesequential,看個人習慣,這個設定是指 migration 的檔案取名方式,如果是 timestamp 檔名取法則用日期當做順序,例: 20121031104401_add_blog.php,而 sequential 的話就是按照檔名排序,例:001_add_blog.php,其他設定如 miagration 資料夾位置,這邊我用 default,所以我們建立一個目錄 application/migrations/

實做

application/migrations/001_create_users_table.php
class Migration_Create_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->add_field([
            'id' => [
                'type' => 'int',
                'unsigned' => true,
                'auto_increment' => true,
            ],
            'username' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'password' => [
                'type' => 'varchar',
                'constraint' => 50,
            ],
            'gender' => [
                'type' => 'tinyint',
                'constraint' => 1
            ],
            'created_at' => [
                'type' => 'datetime'
            ],
            'updated_at TIMESTAMP DEFAULT NOW() ON UPDATE NOW()'
        ]);

        $this->dbforge->add_key('id', true);
        $this->dbforge->create_table('users');
    }

    public function down()
    {
        $this->dbforge->drop_table('users');
    }
}

class name 一定要是 Migration_ 加上字首大寫並且去掉序列的檔名,不然不會動,up 是建立的部分,down 則是給 rollback 用的,所以我們這邊的 up 是 create 某個 table 的話,down 就是 drop 某個 table,以此類推,add_key 的部分有加 true 會是 primary key,沒有的話就是一般的 index,基本上欄位的設定就是把 sql 的欄位建立方式填進去並且給值,constraint 不給的話預設會是最大,如果在 MySQL 裡面試是非題的話就是用 true false 給值,像是 null => false 則預設不能 null,如果沒下的話預設值看 MySQL 怎麼設定。

application/controllers/Migrate.php
class Migrate extends CI_Controller
{
    public function start()
    {
        $this->load->library('migration');

        if ($this->migration->latest() === false) {
            echo $this->migration->error_string();
        } else {
            echo 'migrated'.PHP_EOL;
        }
    }
}

我們建立一個 controller 來執行 migration,網路上有很多方法可以讓這個 controller 只能透過 cli 執行,但我實際跑過,如果你是下 $this->migration->latest() 的話,從 browser 重複執行並不會影響建構好的 table,所以我是沒有打算放,而且客戶常常都只有給 FTP 哪有辦法走 cli 模式。

建立完畢後我在本機的 cli 跑 php index.php migrate start 得到:

"chan" Sid: S-1-5-21-759891990-2490853520-3465201229-1000
migrated

打開資料庫看得到了 migrations 以及 users table,這樣就是順利完成了,接下來我們來模擬幾個場景。

application/migrations/002_add_ip_to_users_table.php
class Migration_Add_ip_to_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'ip' => [
                'type' => 'varchar',
                'constraint' => 100,
                'null' => false,
            ]
        ];

        $this->dbforge->add_column('users', $field, 'password');
    }

    public function down()
    {
        $this->dbforge->drop_column('users', 'ip');
    }
}

這個場景就是在 users 裡面增加一個欄位,add_column 的第三個欄位等於 MySQL 的 after

application/migrations/003_drop_gender_from_users_table
class Migration_Drop_gender_from_users_table extends CI_Migration
{
    public function up()
    {
        $this->dbforge->drop_column('users', 'gender');
    }
}

users table 移掉 gender 這個 column。

004_modify_password_from_users_table.php
class Migration_Modify_password_from_users_table extends CI_Migration
{
    public function up()
    {
        $field = [
            'password' => [
                'name' => 'password',
                'type' => 'varchar',
                'constraint' => 100,
            ]
        ];

        $this->dbforge->modify_column('users', $field);
    }
}

password 的長度增加到 100,在 MySQL 如果要改一個欄位的內容 alter 的語法你必須把其他欄位設定都在打一次,所以不要只有打 'constraint' => 100 而已,這樣會出事。

2015/10/06

CodeIgniter Drivers

Codeigniter 裡面有一個功能叫 drivers,但官方的文件著墨很少,網路上討論的文章也不算多,但我覺得以一個不是使用 namespace 那麼方便做 DI 以及調用資源的 framework 來說,這個功能還頗好應用的,一般人使用 CI 都會用他的 library 功能,把自己的 class 放在這邊調用,但大部分就是一層式的沒有階級觀念,就不用提 interface 了,dirvers 可以靈活的做到調用,先來講怎麼設定 driver 環境,以各家第三方登入為例。

首先我們先建立目錄

  • Auth
  • Auth/Auth.php
  • Auth/drivers
  • Auth/drivers/Auth_facebook.php
  • Auth/drivers/Auth_google.php

命名規則就是檔名必須跟目錄名稱相同,第一個字首要大寫(包含初始的目錄),只要任何一個名稱取錯都不會動,我試過了。

Auth.php
class Auth extends CI_Driver_Library
{
    protected $valid_drivers;

    public function __construct()
    {
        $this->valid_drivers = array('facebook', 'google');
    }

    public function login()
    {
        echo 'login from auth driver'.PHP_EOL;
    }
}

$valid_drivers 是必要參數,他表示允許調用的 dirver 名稱,呼叫的時候不需要帶 parent name,這樣寫起來也比較清爽。

Auth_facebook.php
class Auth_facebook extends CI_Driver
{
    public function login()
    {
        echo 'login from auth facebook'.PHP_EOL;
    }
}

基本上被調用的 driver 子項目很簡單,繼承 CI_Driver 就可以了,我們接著建立一個叫 Login 的 controller。

application/controllers/Login.php
class Login extends CI_Controller
{
    public function index()
    {
        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->facebook->login();
    }
}

在 command line 執行 php index.php login index 會得到

login from auth driver
login from auth facebook

寫到這就知道前面部屬好的話,之後要調用各家的 login 只要在 Auth/drivers 下增加,並且加入 $valid_drivers 設定即可,根據 SOLID 原則,會變動的 config 內容不應該存在在 class 裡面,所以我們在 config 增加一個 auth.php

application/config/auth.php
$config['valid_drivers'] = array('facebook', 'google');

然後將 Auth.php 改寫如下。

class Auth extends CI_Driver_Library
{
    protected $valid_drivers;
    protected $ci;

    public function __construct()
    {
        $this->ci =& get_instance();
        $this->ci->config->load('auth', true);
        $this->valid_drivers = $this->ci->config->item('valid_drivers', 'auth');
    }

    public function login()
    {
        echo 'login from auth driver'.PHP_EOL;
    }
}

這樣的話之後要加內容只要改 config 就好,但這樣的彈性還不是最好的,我們應該要讓 Login controller 可以接受變數選擇調用哪個登入才對,來對他動一點手腳。

class Login extends CI_Controller
{
    public function index($method = null)
    {
        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->{$method}->login();
    }
}

然後執行 php index.php login index facebook,這樣會得到跟上面一樣的結果,但我們應該更嚴謹一點,檢查該 method 是否存在,在來強化一下。

class Login extends CI_Controller
{
    public function index($method = null)
    {
        $this->config->load('auth', true);

        if (!in_array($method, $this->config->item('valid_drivers', 'auth'))) {
            die('method not exists');
        }

        $this->load->driver('auth');
        $this->auth->login();
        $this->auth->{$method}->login();
    }
}

如果亂執行一通 php index.php login index twitter,會得到 method not exists,這大致上是 drivers 的全貌,怎麼變化應用讓功能更有彈性就要視不同的情況去配置了。