RSS

2011/07/22

可以觸摸的網頁

HTML5 之中定義了對於 Touch Device 相應的功能. 其中最重要的大概是在 JavaScript 之中的 touch event.

Touch event 跟 mouse event 某程度上相當相似. 三個 event 分別是 touchstart, touchmovetouchend. 這就好比 mouse event 當中的 mousedown, mousemovemouseup 三個 event.

不過, touch event 跟 mouse event 的最大分別, 就是它本身設計支援 multi-touch, 所以每個 event handler 接收回來的 event 會包含每一隻手指(或接觸點)相關的資訊.

先拿 touchstart event 做例子:


document.getElementById('someid')
.addEventListener('touchstart', function(event){
var firstFinger = event.targetTouches[0];
document.getElementById('messageBox').innerHTML = 'point ' + firstFinger.pageX + ',' + firstFinger.pageY + ' touched!';

}, false);


以上例子不太實用, 不過旨在先提供一點概念. 首先留意到的, 是 event.targetTouches 是一個 array, 代表著接觸這個 DOM element 的每一隻手指. (另外還有 event.touches 代表所有手指, 以及 event.changedTouches 代表著跟這個 event 有關的所有手指, 這個稍後再說明)

假設只會有 single touch 的情況下, 我可以直接抽起 array 之中的第一個項目使用. 在以上例子, 就當成是普通 mouse event 一樣, 利用 pageXpageY 找出座標使用.

touchmove 情況差不多, 就當手指在對應 DOM element 上移動時, touchmove event 就會包含每隻手指的位置. 舉個例:


document.getElementById('someid')
.addEventListener('touchmove', function(event){
var centroid = { x: 0, y: 0 };
for(var i=0; i<event.targetTouches.length; ++i) {
centroid.x += event.targetTouches[i].pageX;
centroid.y += event.targetTouches[i].pageY;
}
centroid.x /= event.targetTouches.length;
centroid.y /= event.targetTouches.length;

document.getElementById('messageBox').innerHTML =
'touch centroid: ' + centroid.x + ',' + centroid.y;

event.preventDefault();
}, false);



以上例子每當手指移動時, 就會計算出所有手指的中間位置然後顯示出來. 最主要的部份, 還是經過 event.targetTouches 這個 array 找出所有點然後處理.

使用 touchmove 時必須留意, 現時大部份 mobile browser 都會對應 touchmove 做 scrolling 效果, 尤其是當到達頁面盡頭時候的 bounch back 效果會影響到 touchmove event 的運作. 所以建議有需要時在最後加上 event.preventDefault() 去避免問題發生.

最後到 touchend event, 使用方法就有點不同了:


document.getElementById('someid')
.addEventListener('touchend', function(event){
var touch = event.changedTouches[0];
document.getElementById('messageBox').innerHTML
= 'touch end: ' + touch.pageX + ',' + touch.pageY;
}, false);


由於當 touchend 發生之時手指已經離開螢幕, 所以要知道真正 trigger touchend event 的手指位置的話, 就要靠 event.changedTouches 了.

組合三個 event 就可以為網頁設計 gesture 了. 方法是利用 touchstart 記錄 gesture 的開始, 用 touchmove 去記錄軌跡, 最後在 touchend event 分析 gesture 並作出反應. 舉例說, 要處理 single touch gesture 的話, 可以這樣做:


var Touch = {
startPos: null,
endPos: null,
init: function(elementID) {
document.getElementById(elementID)
.addEventListener('touchstart',
function(event) { Touch.start(event); }, false);
document.getElementById(elementID)
.addEventListener('touchmove',
function(event) { Touch.update(event); }, false);
document.getElementById(elementID)
.addEventListener('touchend',
function(event) { Touch.stop(event); }, false);
},
start: function(event) {
var touch = event.targetTouches[0];
this.startPos = { x:touch.pageX, y:touch.pageY };
},
update: function(event) {
var touch = event.targetTouches[0];
// TODO: record gesture
},
stop: function(event) {
var touch = event.changedTouches[0];
this.endPos = { x:touch.pageX, y:touch.pageY };

var gesture = this.analyseGesture();

alert("The gesture is: " + gesture);
},
analyseGesture: function() {
if(this.startPos && this.endPos) {
if(this.endPos.x > this.startPos.x+200)
return "swipe right";
else if(this.endPos.x < this.startPos.x-200)
return "swipe left";
else
return "unknown";
}
}
};


以上例子很懶惰的只利用開始及結束的位置去估計是向左還是向右 swipe, 要更準確的話就要想一想應該在 touchmove 時記錄甚麼來分析了. 要使用這個例子的話, 可以把 Touch.init 加到 onload event 那裡:


window.onload = function() { Touch.init('someElementID'); };


(有關 Object function 在 event handler 上的應用, 可參考上一篇 令人又愛又恨的 JavaScript function :P)

可惜的是, 現時只有少部份軟件在 JavaScript touch event 之中支援 multi-touch, 期待將來 multi-touch 為 mobile web page 所帶來的更好的 user interface.

有關 touch event, 我推介 HTML5ROCKS 的相關介紹, 當中有提及更多 touch event 相關資料, 包括已定義但未有軟件支援的部份, 以及各主要軟硬件的支援情況, 做得相當不錯.

2011/06/30

令人又愛又恨的 JavaScript function

JavaScript 的其中一個特別之處, 就是 function 本身是一種 object 這個設計. 習慣寫 procedural 及 OOP 的人一時間未必能夠完全消化箇中用處. 寫 C 的高手, 或者是學過 LISP 的人, 也許會比較容易明白. 不過同一時間, 當 function 跟 object 一齊使用時, 又帶出了更加麻煩的 binding 問題.

JavaScript function 簡介

要定義 JavaScript function, 最基本的方法就是直接用 function keyword 來定義


function someFunc() {
// ...
}


這樣做的話, 其實等同於定義一個叫 someFunc 的 variable, 並指定一個 function object 作 value:


var someFunc = function () {
// ...
};


就因為 function 的名稱其實就是一個 variable, 我們可以把它用作另一個 function 的 parameter 甚至是從一個 function return 出去. 細想一下, 在 JavaScript 設定 event handler 時就正正是利用到這一點:


window.onload = function() { alert("Hi!"); };


當然, 除了用作 event handler 之外還有很多用途, 例如我可以寫一個這樣的 function :


function each(inputArray, inputFunction) {
var i;
for(i = 0; i<inputArray.length; ++i) {
inputFunction(inputArray[i]);
}
};


這個 function 就可以指定一個 function 去處理一個 array 的每一個項目, 例如我可以這樣用:


each([ 1, 2, 3 ], function(item) { document.write(item); });


類似的工具在不同的 JavaScript library 都有提供, 只要能夠充份的運用它們, 就能快速地完成想做的工作.
例如在 prototype library 之中我可以這樣寫:


var squares = [ 1, 2, 3 ].collect(function(item) { return item * item; });
// squares = [ 1, 4, 9 ]


當 function 遇上 object

寫 JavaScript 的其中一個良好習慣, 就是把要寫的東西都盡量歸納到自定的 object 之內. 順理成章的, 就會把 function 都定義到 object 之內了. 舉個例子:


var myPopup = {
message: "Hello!",
pop: function () { alert(this.message); }
};


假設當我們希望把它用作某些 event 的 event handler 的話, 很直接的想法就是這樣做:


window.onload = myPopup.pop;


不過這樣做的話不會出現我們所希望的結果. 為甚麼呢? 這是因為當我們把 function 當作 value 使用的時候, 它跟原本 object 之間的關聯就消失了. 想像一下, 以上例子其實跟以下這個一樣的:


window.onload = function () { alert(this.message); };


這樣看的話, 就即時發現 this 在這裡並不是指 myPopup 這個 object 了. (而事實上, 這個 this 在這裡是 JavaScript 的 window object)

要解決這個問題, 最容易的做法是定義一個 wrapper function:


window.onload = function() { return myPopup.pop(); };


這樣做就可以肯定 pop function 是經 myPopup 執行的. 要是不喜歡這種 wrapper 的話, 也可以借用一下 object template 的 lexical closure (類似 local scoping):


var PopupClass = function (message){
this.message = message;
var thisObject = this;
this.pop = function () { alert(thisObject.message); }
};
var myPopup = new PopupClass("Hello");
window.onload = myPopup.pop;


很複雜呢! 這裡做的, 是令到 pop function 可以用 thisObject 這個內部的 variable 去使用原來用不到的 this. 這樣做就可以用回 window.onload = myPopup.pop; 這種比較好看的寫法了.

再看看另一個情況, 如果這次是把 function 用作另一個 function 的 parameter 呢?


var myPopup = {
message: "Hello ",
pop: function (name) { alert(this.message + name); }
};
function popname(popFunc, name) {
popFunc(name);
}
popname(myPopup.pop, "Mary"); // undefinedMary
popname(myPopup.pop, "Tom"); // undefinedTom



同樣道理, 我們可以用 wrapper 解決:


popname(function() { myPopup.pop("Mary"); });
popname(function() { myPopup.pop("Tom"); });


但你會發現, 每次用到這個 function 的時候都要加一個 wrapper, 很麻煩又容易出錯. 我們可以預先定義一個工具, 再利用 function object 本身的 apply function:


function bind(func, object) {
return function() { return func.apply(object, arguments); };
}
popname(bind(myPopup.pop, myPopup), "Mary");
popname(bind(myPopup.pop, myPopup), "Tom");


同一個 function 更可以在程式中重覆使用. 除了 apply 之外, 還有類似的 call, 但在這個例子不適用.

JavaScript library

大部份的 JavaScript library 都提供了相關的工具, 相當方便. 例如 prototype 就直接定義了一個 bind function:


popname(myPopup.pop.bind(myPopup), "Mary");


而 jQuery 就有 proxy function 可以用:


popname($.proxy(myPopup.pop, myPopup), "Mary");


function binding 在 JavaScript 之中是個很重要的概念, 不過同一時間, 亦是一個很容易過份使用的工具. 對於初學者來說, 最重要的是先弄清楚甚麼時候需要用到. 熟習之後才研究甚麼時候需要/不需要用吧.

(BTW, 在這裡還要說清一點, 在 OOP 的世界, 我應該說 "method" 而不是 "function". 不過為免令到一些對 OOP 認識比較少的讀者看得一頭霧水, 還是決定用 "function" 一字)

2011/06/29

借用 Closure Compiler 來 debug JavaScript

JavaScript 令人頭痛的其中一個地方, 就是 browser compatibility 的問題. 有很多時候, 在一個 browser 運作正常的 script, 在另一個 browser 會出現問題.

很多時進行開發的人都只專注於其中一個運行環境, 待完成後才去作較大規模的測試. 現時很多網站都大量使用 JavaScript, 很容易就會出現過千行的code. 這時候問題一出現就會很頭痛, 因為很多時候這類 browser compatibility 的問題並不會產生甚麼有用的錯誤訊息.

舉個例子, 寫 PHP 的人很容易就習慣在建立 array 時在每個 item 後面都加上逗號, 例如是這樣:


<?php

$myarr = array(
'item1',
'item2',
'item3',
);
?>


這樣做的原因, 是可以很方便的加、減或移動項目. 但是 JSON 就不可以這樣做了:


var myarr = [
'item1',
'item2',
'item3'
];


最後的逗號一定要移走, 因為在某些 browser 會以為逗號之後有一個空的項目, 這樣會令整條 script 不能正常運作. 在加減項目, 或是移動項目時就變得要留意這個逗號了.

由於這個問題只會在某些 browser 出現, 若果埋沒在 code 海之中就很難去找出來了. 這個時候, 可以借用一下 Google 的 Closure Compiler 來幫忙了.

Closure Compiler 本身的作用, 是把 JavaScript 重新編譯, 以達到壓縮的效果. 不過所謂的"編譯", 有很大的部份只是把 variable, function, 或 object 以較短的名稱來取代. 由於要做到這效果, 必須先分析 script 的結構, 所以它可以找出 JavaScript 之中所潛在的 syntax error. 借用一下, 就可以用來 debug JavaScript.

就用以上的例子來說用一下吧. 首先到這個 Closure Compiler UI 界面. 在左手邊的 textarea 內把 // ADD YOUR CODE HERE 以下的 code 換成這個:


var myarr = [
'item1',
'item2',
'item3',
];


要留意這裡我故意在最後一個 item 加上逗號.

輸入好之後, 就按一下 "Compile", 然後就會看到右手邊顯示了有一個 Error. 按一下那個 tab 就會看到錯誤訊息:

JSC_TRAILING_COMMA: Parse error. Internet Explorer has a non-standard intepretation of trailing commas. Arrays will have the wrong length and objects will not parse at all. at line 6 character 1

找到錯誤的地方, 就能對症下藥了.

我個人認為, 在這個網絡速度那麼快的時代, Closure Compiler 本身的作為壓縮 JavaScript 的用途實在不太大(但可以間接令JavaScript難以解讀, 有著輕輕的一重保護作用). 不過可以利用一下來找出潛在的 syntax error, 又相當的不錯.

2011/03/31

快速開發 PHP web app - CakePHP

製作 web app, 一般來說主要都是分兩個部份. 先是設定好 database 儲存資料, 然後就是製作介面, 令用家可以透過介面處理資料.

這類介面一般提供資料儲存的四個主要工作功能: 建立, 查閱, 更新 以及 刪除, 亦即是所謂的 CRUD (create, read update delete).

要是為每一個 web app 都重新寫一遍以上的功能, 會是一個又花時間又重覆的工序. 若果懂得利用 open source framwork 的話, 則事半功倍.

CakePHP 所提供的, 是 MVC 加上 ORM 的模型. 其概念是從 Ruby on Rails 而來的.

使用 CakePHP 的方法, 首先是在 Model 設定每一個 database table, 特別是 table 與 table 之間的關聯. 然後在相應的 Controller 中, 為每一個 View, 寫一個 function. 這個 function 的角色, 主要是從 Model 抓來需要的資料, 然後送到特定的 View 作顯示.

舉個例, 如果我要為一個叫 users 的 table 作一個叫 view 的頁面, 我需要做三件事:

第一, 建立檔案 app/models/user.php


<?php

class User extends AppModel {
var $name = 'User';
}

?>


這就表示我有一個叫 User 的 model, 而 CakePHP 就會自動把這個 model 對應 database 上的 users table. Model 及 database table 之間的對應是自動的, 但是亦可以自行設定.

第二, 建立檔案 app/controllers/users_controller.php


<?php

class UsersController extends AppController {
function view($id) {
$this->User->id = $id;
$data = $this->User->read();
$this->set('data', $data);
}
}

?>


這就表示, view 這個 View 需要一個 parameter ($id), 然後我要從 User Model 讀取 id 是 $id 的資料, 送到 View 的 $data. View 跟 URL 是相對的, CakePHP 會自動把 URL 的要求送到 Controller 中對應的 function.
上面這個 function 對應的 URL 就是 http://<some domain>/<app folder>/users/view/<id>

最後就是建立檔案 app/views/users/view.ctp , 這個檔案就是一個 template (所以是 .ctp 不是 .php), 舉例說可以很簡單的把資料顯示出來:


<?php

foreach($data['User'] as $key => $value) {
echo "

$key = $value

";
}

?>


要留意的是, 這裡只包括內容的部份, 每個 View 都需要配上一個 layout 才完整, 不指定的話就是很普通的 HTML layout, 實際使用時當然要自行 customize.

使用 layout 的好處, 就是可以因應不同情況選擇不同的 layout, 例如是對應 web 及 mobile platform 使用不同的 layout, 或者是因為用戶選擇而導出 PDF 版本或 RSS 版本等等.

CakePHP 的功能有很多, 以上只是皮毛的概念, 當中還有很多其他東西需要留意的. 例如是設定檔, 以及更進一步 modulize MVC 三方功能的 Behaviors(Model 用), Components(Controller 用) 以及 Helpers/Elements(View 用). 有興趣的不妨看看 CakePHP 提供的 cookbook (因為是cake, 所以就是 cookbook 囉).

若果要求是很簡單的 CRUD 的話, 更可以考慮使用他們的 bake 功能. 這就像焗蛋糕一樣, 只需要準備好材料(database table 及其之間的關聯), 再進行一點步驟(在 console 執行 bake 指令), 就能夠自動產生相應的 CRUD 介面. 不過, 自動產生的介面限制不少, 我還是比較建議自行從 MVC 三方落手.

雖然說利用這類 framwork 工作會很方便, 不過也請盡量正確地使用它. 舉例說, 使用 MVC 模型時, 很容易就不自覺地把 business logic 跟 application logic 混合在一起, 令到 controller 異常地複雜及混亂, 但是 model 郤空空如也. 結果白白浪費了這麼好用的 framework, 將來需要擴充是亦會令人相當頭痛. 但另一方面, 要是能夠好好的熟習這類 framework, 要開發 web app 時就更加得心應手.

2010/12/14

在網頁中插入音樂

一直以來, 要在網頁上播放音樂都是個令人頭痛的問題. 一個不負責任的 developer 可以很隨便的用一個 <embed> tag 來處理, 迫使讀者要在特定環境加特定的軟件才可以播放.

[後補: 為免誤會, 我在此呼籲... 請各位必須避免在讀者不知情的情況下播放音樂. 例如是當進入頁面時自動播放音樂之類. 當你嚇到使用者的一刻就是他們離開的一刻.]

<embed> 的最大問題, 是它並不是一個 HTML 4.01 的 tag. 不過這個 tag 在 HTML 5 重新出現, 結果就像是把不負責任的行為合理化一樣...

<embed> 基本用途就是 embeded content, 亦即是瀏覽器會交由 plugin 去處理內容, 一般來說無法控制顯示的結果. 例如你 embed 的是一個 mp3 檔案的話, 用 IE 時可能會出現 Windows Media Player plugin, 用 Safari 時就大概會出現 Quicktime plugin. 要是使用者沒有安裝 plugin, 又沒有安裝權限的話, 那就只有說句不好意思了..

比較好的選擇, 是 HTML 4.01 已有的 <object> tag. 利用 <object> tag 配合 <paramgt; tag 的話, 可以控制的東西比較多. 例如是指定使用某一個 plugin, 要某一類的控制項等等.

可惜的是, 這個方法仍然是局限於 plugin 的使用. 而且, 不同的 plugin, 甚至是不同的瀏覽器所需要的設定都不同. 基於方便使用者的考量, 我會傾向選擇一個大部份使用者都有安裝的 plugin 來使用, 例如是, Flash.

利用 Flash-based 的音樂播放器. 好處是當大部份使用者都有安裝 Flash player 的情況下, 幾乎可以肯定大部份讀者都能夠享用你的音樂. 而且, 載入 Flash player 的 HTML 已經比常成熟, 對於 developer 來說也相當方便.

當然, 在 HTML 5 的年代, 我們可以用 <audio> tag 解決以上一切問題.... 是嗎?

使用 <audio> tag 的結果, 是瀏覽器會自行處理及播放音樂檔. Plugin 的問題是解決了, 但是瀏覽器的問題始終存在. 其中最大的問題是, 現時沒有一種音樂格式是全部主流瀏覽器都可以播放的. 例如說 Firefox 3.x 及 Opera 不會播放 mp3, IE 9 及 Safari 5 不會播放 ogg (要加 plugin). 難道要把音樂轉成巨大的 wave 格式嗎?

現時最好的方法, 就是同時提供 ogg 及 mp3 格式. 至於將來, 就看完全開放的 WebM 格式是否能夠普及化了. 不過當然還要顧及未能支援 HTML 5 的瀏覽器, 要提供 Flash player 作後備之用.

好了, 長篇大論之後就是萬眾期待的 code 了...




<audio> tag 本身可以加 src 屬性. 不過因為要顧及其他 browser 的關係, 就改為加入多個 <source> tag 讓瀏覽器決定. 其中的 type 屬性 可有可無. 如果瀏覽器不支援 <audio> tag, 就會按 <audio> tag 內的 Flash player code 顯示(或以任何其他信息取代).

有一點必需注意的, 就是 <audio> tag 的 controls 屬性會令瀏覽器顯示控制項. 而這個控制項的樣式全由瀏覽器決定. 如果要自行設計控制項的話, 就必須自行使用 HTML + CSS 設計, 然後用 JavaScript 來控制. 使用 JavaScript 可以控制播放的位置及音量等, 這亦是相比起用 Flash player, 使用 HTML 5 播放音樂的最大優點.

有時候, 我們要知道究竟現在的瀏覽器是否支援 <audio> tag. 我們可以利用 audio element 的 canPlayType function. 這個 function 本身可以查詢瀏覽器是不支援某一類的媒體, 只要這個 function 存在, 就表示瀏覽器支援 <audio> tag.

舉個例, 假如 HTML 的部份是這樣:






而我們希望, 當瀏覽器不支援 <audio> tag 的時候, 就提整個 <div> 的內容改為一個 Flash player 的話, 可以這樣做:


var audioElem = document.getElementById("audioPlayer");

if(!audioElem.canPlayType) {
loadFlashPlayer("audio");
}


這裡假設了有一個叫 loadFlashPlayer 的 function 會在 element id 叫 audio 的 element 載入一個 Flash player. 實際情況視乎選用的 Flash player 而定.

只不過是一個 <audio> tag, 也有這麼多的考慮. 可想而知當要用 <video> tag 時會有幾頭痛. 不知道將來 WebM 格式如果能夠普及的話, 會不會改善問題呢? 就讓我們拭目以待吧.