網站製作學習誌

記錄學習製作網站的一切

Weblet on Zend Framework

常常我們會需要在頁面上佈置一些可互動的 UI 元件,像是連動式下拉選單或是 CAPTCHA 驗證元件等等。這些元件既要能夠用 Ajax 和後端溝通,又要能夠融入一般表單的流程中,最重要的是它們還要能夠重複使用,而不是跟專案綁在一起。

在 Zend Framework 中,還沒提供這樣的機制讓我們使用;因此我便自己製作一個 Weblet 的功能,用以實現這個想法。

註:其實一開始我是用 Widget 這個名稱,不過 Widget 指的是 Web 頁面或視窗桌面上的小玩意;後來想到 let 這個字含有小東西的意思,因此就稱這個機制為 Weblet 。

概念

Weblet 主要概念就是要在 Web 頁面上放置一個 UI 元件,或是可獨立操作的小區塊,而它的資料處理邏輯和樣版則可以完全與專案分離。

例如一般會員網站上都會提供一個登入區塊,例如:

登入表單示意圖

而這個登入區塊通常都是直接用 POST 到某個處理 login 的 PHP 程式,或是 MVC 中的 Action 。如果我們可以把這個登入區塊的樣版與處理登入的邏輯獨立於專案之外,不就可以重複利用了?

而如果這個登入區塊也能夠有自己一套獨立的 AJAX 流程,不必與現有的 MVC 架構混合在一起,那麼在使用上的彈性就更大了。

在以往 Zend Framework 中處理這些元件 Ajax 的部份時,我們都需要在當前的 Action Controller 中建立一個 Action 來對應。而這個元件如果要重複使用的話,那我們還必須建立一個獨立的 Action Controller 來處理特別處理 Ajax Request ;每換一個專案也得將這個 Action Controller 一起搬過去,開發上相當麻煩。

所以如果我們能將處理這些元件的 Ajax 流程獨立於目前的 Action Controller 之外,也不需要特別建立一個 Action Controller 來對應,這樣不是很好嗎?也因此我就有了以下的構思:

  • Weblet 可以在任意的樣版上佈置 Weblet 的 HTML ,並支援 Ajax 呼叫。

  • Weblet 不須綁定 Action Controller ,所以也就不需要關連到特定的網址。

  • Weblet 可以執行不同的動作,也可以指定輸出的格式 (例如 HTML 、 JSON 或是圖片等) 。

簡單來說, Weblet 就是建構在 Zend Framework MVC 上的一個小型 MVC 架構,在 Model 及 Controller 上的運作是獨立在 Zend Framework 之外的。

流程與架構

依照上面的想法,我大致將實際的流程整理如下圖:

Weblet 流程

主要的重點如下:

  1. 利用 Zend_View 顯示 Weblet 的 View 層。

  2. 利用 Controller Plugin 管理 Post Back 後的 Weblet 的 Model 層及 Controller 層。

  3. Controller Plugin 也會判斷該次需求是否為 Ajax ,如果是的話,就不再執行原有的 Action Controller ,以加速 Ajax 效能。

因此依照上面流程,我規劃了以下幾個類別:

  • Wacow_Weblet_Abstract : Weblet 抽象類,之後專案裡要實作的 Weblet 都需要繼承此類別。而它主要是包含了 Weblet 的 Model 層及 Controller 層。

  • Wacow_Weblet_Broker : 用來管理 Weblet 群集的類別,負責建立、載入及設定 Weblet 。

  • Wacow_Application_Resource_Weblet :用來初始化 Weblet 相關物件的類別。

  • Wacow_Controller_Plugin_Weblet :控管 Weblet 流程的主要類別,會透過 Broker 來與 Weblet 溝通。

  • Wacow_Controller_Action_Helper_Weblet :可以在一般的 Action Controller 裡,透過這個 Helper 類別取得指定的 Weblet 。

  • Wacow_View_Helper_Weblet :可以在 View Script 中,透過這個 Helper 類別顯示指定的 Weblet 。

註:未來我打算再實作一個 Provider 類別來自動建立 Weblet 。

安裝

請先建立好一個 Zend Framework 專案,也就是在 Command Line 模式下輸入:

1
zf create project project_name

接著下載 Wacow_Weblet 套件,解開後放到 php.ini 中 include_path 指定的目錄,或是專案的 library 目錄裡即可。

然後在 application/configs/application.ini 的 [production] 區段中加入:

1
2
3
4
5
6
7
8
Autoloadernamespaces[] = "Wacow_"
pluginPaths.Wacow_Application_Resource_ = "Wacow/Application/Resource"
pluginPaths.Wacow_Weblet_ = "Wacow/Weblet"
resources.frontController.plugins.weblet.class = "Wacow_Controller_Plugin_Weblet"
resources.frontController.actionHelperPaths.Wacow_Controller_Action_Helper = "Wacow/Controller/Action/Helper"
resources.view.basePath = APPLICATION_PATH "/"
resources.view.helperPath.Wacow_View_Helper_ = "Wacow/View/Helper"
resources.weblet.webletPath.Application_Weblet_ = APPLICATION_PATH "/weblets"

這樣就安裝好 Weblet 套件了。

用法與範例

接下來我直接用範例說明,首先在 application/ 下建立這樣的目錄結構:

1
2
3
4
5
application/
  |
  |- weblets
       |
       |- scripts

其中, weblets 是放置 Weblet 類別, scripts 則是放置 Weblet 的 HTML 樣版。

以下我們直接用一個 Login Weblet 做為範例,請將以下的內容存為 application/weblets/Login.php :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
<?php
class Application_Weblet_Login extends Wacow_Weblet_Abstract
{
    // 如果 Weblet 有必要的資料項,要放在 $_data 這個屬性中
    protected $_data = array(
        'account' => null,
        'password' => null,
    );
    protected $_session = null;
    // 初始化
    public function init()
    {
        // 設定 Session 物件
        if (null === $this->_session) {
            $this->_session = new Zend_Session_Namespace('login');
        }
        // 已經登入的話,就取得 Session 中的 account 資訊
        if ($this->_session->isLogin) {
            $this->account = $this->_session->account;
        }
    }
    // 預設會執行的動作
    public function defaultAction() {}
    // 判斷帳號密碼
    public function checkAction()
    {
        // 已經登入就不需往下處理
        if ($this->isLogin()) { return; }
        // 前端傳給 Weblet 的資料
        $account = trim(strtolower($this->account));
        $password = trim($this->password);
        // 登入成功
        if ('jaceju' === $account
                && '123' === $password) {
            $this->_session->isLogin = true;
            $this->_session->account = $this->account;
        } else {
            $this->getView()->assign('message', '帳號或密碼錯誤');
        }
    }
    // 是否已經登入
    public function isLogin()
    {
        return $this->_session->isLogin;
    }
}

這個 Login 類別主要是模擬一個登入流程,它必須繼承 Wacow_Weblet_Abstract 這個抽象類,而 Application_Weblet_Login 這個類別名稱是因為我們在 application.ini 中加入了:

1
resources.weblet.webletPath.Application_Weblet_ = APPLICATION_PATH "/weblets"

這樣 Wacow_Weblet_Broker 就會知道要去哪裡載入 Weblet ,同時這個設定也會定義 Weblet Template 放置的路徑 (也就是在 weblet 類別所在目錄下的 scripts 目錄 ) 。

接下來說明 Application_Weblet_Login 的程式結構。

  • $_data :這個屬性定義於 Wacow_Weblet_Abstract 類別中,我們可以在子類別覆寫它; $_data 主要是用來定義讓前端的 HTML 和後端的 Weblet PHP 程式碼溝通的資料項目,也就是說從前端接收到的資料如果包含了 $_data 所定義的索引時,其對應的資料就會被載入到 $_data 中 ,這裡即為 account 及 password 。

  • $_session :此 Login 類別所需要的 Session 物件,這個屬性是自訂的。

  • init() : Wacow_Weblet_Broker 在建立 weblet 後,會立刻呼叫這個方法,以完成 weblet 的初始化;預設是空實作。

  • defaultAction() : Weblet 預設會執行的方法,可以透過更改 $_action 屬性來改變。 (稍後會提到如何更改 $_action 值)

  • checkAction() :此 Login 類別用來驗證使用者輸入的帳號與密碼,這個方法是自訂的。

  • isLogin() :此 Login 類別用來判斷使用者是否已經登入,這個屬性是自訂的。

註:這只是應用範例,開發時還是要按照實際需求來撰寫驗證帳號與密碼的方式。

接著是 HTML 樣板,請將以下內容存為 application/weblets/scripts/login.phtml :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<div class="webletLogin" id="<?php echo $this->weblet->id(); ?>">
<?php if ($this->weblet->isLogin()): ?>
<p>歡迎你, <?php echo $this->escape($this->weblet->account); ?></p>
<?php else: ?>
<fieldset>
<legend>登入</legend>
<form method="post" class="webletLoginForm" action="">
    <p><label>帳號</label><input type="text"
                               name="<?php echo $this->weblet->fn('account'); ?>"
                               value="<?php echo $this->weblet->account; ?>" /></p>
    <p><label>密碼</label><input type="password"
                               name="<?php echo $this->weblet->fn('password'); ?>" /></p>
    <p><input type="submit" value="登入" /></p>
    <?php echo $this->weblet->fields('login', 'html'); ?>
</form>
</fieldset>
<?php endif; ?>
</div>

這個樣版用到了幾個 Weblet 的特色:

  • $this->weblet :會自動指向我們目前調用的 Login 類別所對應的 weblet 物件。

  • $this->weblet->id() :取得 weblet 物件的 id 屬性值, id 的值可以自訂,稍後我們會談到如何更改這個值。

  • $this->weblet->fn('xxx') :用來產生 weblet[weblet-name][xxx] 這樣的 HTML 表單欄位名稱,主要是配合各式表單欄位使用。

  • $this->weblet->fields(action, format) :用來產生 weblet 相關資訊的隱藏欄位。第一個參數是 action 名稱,用以呼叫對應的 action ;第二個參數是 format ,主要是在透過 AJAX 呼叫時所要輸出的格式,預設可以使用 HTML 和 JSON 兩種格式。

有了這兩個檔案後,我們只要在主要的 View Script 上佈置 weblet 就可以了。

請開啟 application/views/scripts/index/index.phtml ,並在 <body> … </body> 之間加入以下程式碼:

1
<?php echo $this->weblet('login', 'login-01'); ?>

這行程式主要是透過 Wacow_View_Helper_Weblet 這個類別中的 weblet 方法來建立並顯示 Login Weblet ;第一個參數就是 weblet 的名稱,它會自動找出符合名稱的 Weblet ;而第二個參數就是 weblet 的 id ,主要是用來讓後端的 PHP 程式可以得知要對應的 weblet 。

接著開啟瀏覽器並瀏覽 Zend Framework 專案的主要頁面後,就會產生以下 HTML :

登入表單示意圖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div class="webletLogin" id="login-01">
<fieldset>
<legend>登入</legend>
<form method="post" class="webletLoginForm" action="">
    <p><label>帳號</label><input type="text"
                               name="weblet[login-01][account]"
                               value="" /></p>
    <p><label>密碼</label><input type="password"
                               name="weblet[login-01][password]" /></p>
    <p><input type="submit" value="登入" /></p>
        <input type="hidden" name="weblet[login-01][_name]" value="login" />
<input type="hidden" name="weblet[login-01][_format]" value="html" />
<input type="hidden" name="weblet[login-01][_action]" value="check" />
<input type="hidden" name="weblet[login-01][_guid]" value="4644df4f-d8fc-36c8-9338-0000098eaa0b" />
</form>
</fieldset>
</div>

最後可以試試看操作這個表單,若沒有問題的話,應該會如預期般顯示出「歡迎你, jaceju 」或是在表單下方顯示「帳號或密碼錯誤」。

加入 Ajax

在上面的例子中, Login 表單使用的還是傳統的 POST ,這裡我們可以改用 AJAX 來傳送。

首先在 application/weblets/scripts/login.phtml 的 </fieldset> 標籤下方加入以下程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php $this->headScript()->appendFile('http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'); ?>
<?php $this->headScript()->captureStart() ?>
$(function () {
$('form.webletLoginForm').live('submit', function () {
    $this = $(this);
    $container = $this.parents('div.webletLogin');
    $.ajax({
        type: 'POST',
        url: '<?php echo $this->baseUrl(); ?>/',
        data: $this.serializeArray(),
        success: function (html) {
            $container.replaceWith(html);
        },
        dataType: 'html'
    });
    return false;
});
});
<?php $this->headScript()->captureEnd() ?>

上面的 JavaScript 中我透過 jQuery 來傳送 AJAX Request ,如果成功的話就以回傳的 HTML 結果取代目前的 Weblet 畫面。

然後在 application/views/scripts/index/index.phtml 的 </body> 標籤上面加入:

1
<?php echo $this->headScript(); ?>

最後再瀏覽一次專案首頁,成功的話應該會載入 jQuery :

載入 jQuery

註:這裡我是透過 Firefox 配合 Firebug 套件來查看瀏覽器載入的資源。

如果這時我們直接按下「登入」鈕,那麼頁面就會透過 AJAX 更新:

AJAX 更新畫面

是不是很簡單呢?

當然這個架構還沒有被嚴格測試過,在正式使用上一定還有未盡完善之處,希望大家能給我一些建議,讓這個機制更加好用。

參考

Comments