Pages

搜尋此網誌

2013年12月25日 星期三

企業級開發框架:extjs 與 grails 的完美組合

企業級開發框架:extjs 與 grails 的完美組合

本篇的重點在於說明 extjs 作為 web 應用程式前端的 framework,如何與目前常用的 full stack framework 進行整合,在此將以 grails 為例,當然不只可以跟 grails 整合,其他像 RoR 或者 .net,甚至是 node.js 都可以作為 extjs 的後端服務提供者。

透過 grails 這樣的整合範例,希望可以讓讀者體會不只能夠快速開發,一旦應用程式大到一定程度,也可以很方便的維護,並且在開發流程中的循環都可以順暢的不停轉動。


前後端分工

開發大型軟體,或是時程上較趕的時候最怕等來等去,在開發應用程式時,最需要確認的是資料庫的設計,一旦定義好之後,如何快速完成 model 並且將測試資料建立完成,以便進行測試,透過 grails 與 extjs 剛好可以完美的解決此問題,前幾篇介紹到的關於 extjs model 類別的使用,其概念與 grails 剛好一拍即合,同樣以之前 extjs mvc 為例 裡面用到的 Item,batch,以及 itemImage,在 grails 中宣告如下:

package finder
class Item {
        String name
        String title=""
        String description=""
        static hasMany=[itemImages:ItemImage]
}

package finder
class Batch {
        String name
        static belongsTo =[item:Item]
}

package finder
class ItemImage {
    Item item
    String name
}

grails 可以把它看做 java 中的 RoR,因此也有「約定優於配置」的特性,以往在傳統 java 對於 O/R mapping 這樣的技術,往往需要大量的 xml 定義,在 grails 只要將寫好的 model 放在 grails 下的 model 資料夾,而三個資料表定義就像上面的程式碼一樣,輕鬆簡單!不需要在對資料庫進行 table create,一旦 grails 啟動就會檢查資料庫是否有對應的資料表,判斷若是 develop mode 將會使用虛擬資料庫,在記憶體中就會建立好三個 table,不需要有實體就可以開始對你的應用程式開始進行測試,一旦開發完成,只要進行設定轉換為實體資料庫即可,接著在 grails 中有個類別 BootStrap 在這裡可以定義你要測試的初始資料,以便進行相關應用開發,如下:

import finder.*

class BootStrap {

  def init = { servletContext ->

        environments {
            development {
                def item1 = new Item(name:"item1").save(failOnError: true, flush: true)
                def batch1 = new Batch(name:"batch1",item:item1).save(failOnError: true, flush: true)
                def itemImage1 = new ItemImage(name:"itemImage1.jpg",item:item1).save(failOnError: true, flush: true)
            }
        }
  }
  def destroy = {
  }
}

一旦伺服器啟動就會執行在 BootStrap 中的程式碼,如果我們在此區塊撰寫新增資料的程式,每次啟動 grails 都會有新的資料可以進行測試,反覆測試的過程中將免去每次都要建立測試資料的麻煩,並且有預設的設定值也可以在此定義,資料準備好了,前後端就可以分開進行,接著來看如何快速定義好 extjs 與 grails 的溝通橋樑。

以 RESTful 進行前後端溝通

extjs 4 有個新的 proxy type:rest,一但定義為 rest proxy,在資料操作上將會根據你對前端資料的更新動作給予不同的 http method,如下:

  • 新增:POST
  • 修改:UPDATE
  • 刪除:DELETE
  • 查詢:GET

我們會用到另一個敏捷開發特性:Don’t Repeat Yourself(DRY),在 Grails 有另一個設定檔 URLMappings 可以讓我們設定根據前端 request 的 http method 導入至特定後端 controller method,該檔案設定如下:

class UrlMappings {
    static mappings = {
        "/$controller/$action?/$id?"{
			constraints {
			}
		}
		"/rest/$controller/$id"{
			action = [GET: "show", PUT:"update", DELETE:"delete"]
			constraints {
			}
		}
		"/rest/$controller"{
            action = [GET:"listAll", POST: "create"]
            constraints {
            }
        }
        "/"(view:"/home/index")
        "500"(view:'/error')
    }
}

可以看到在 URLMappings 的設定中:

"/rest/$controller/$id"{
    action = [GET: "show", PUT:"update", DELETE:"delete"]
    constraints {
    }
}

"/rest/$controller"{
    action = [GET:"listAll", POST: "create"]
    constraints {
    }
}

代表如果有傳入 id 則是上述的第一種 mapping 方式,根據 http method 的不同對應到不同的 controller 的 method;若沒有 id 則是第二種,實際代表的網址可能為 http://localhost/rest/item/1 或者 http://localhost/rest/batch/,就會根據 UrlMappings 的定義觸動在 controller 中的 method,範例如下:

package finder
import grails.converters.JSON
class ItemController {

    def listAll = {
        def items=Item.list()
        render (contentType: 'text/json') {
      [
                items: items,
                total: items.size()
            ]
        }
    }

    def show = { Long id ->
        def item=Item.findById(id)
        render (contentType: 'text/json') {
      [
                item: item
            ]
        }

    }

    def create = {
        ...
    }
    def update = {
        ...
    }
    def delete={
        ...
    }
}

剛剛提到對應的 controller method 就如同上面程式碼中的 listAll,show 等等,到這邊,後端的 server 算是已經準備好,可以開始進行測試,是否發現跟一般 java 比,簡潔很多,寫起來還有點像 javascript?實際上 Grails 骨子裡還是 java,執行時會編譯為 class,因為搭配了 java 中的動態語言 groovy 才有這樣的效果,且並沒有捨棄 java 多年累積廣大的第三方套件,當你需要時皆可以引入,不需重新造輪。

extjs:store.sync() - 簡化更新

後端 server 快速準備好後,在 extjs 更加簡化呼叫更新資料請求的程序,在 store 的類別提供一個 method 為 sync(),作用在於一旦 store 載入後,只要對 store 執行 insert,remove,insert 確定更新完成後,一旦執行就會對後端 server 發出 http request,所以,你不用勞你費心,extjs 已幫你完成相關程序,範例 controller 如下:

Ext.define('Frontend.controller.common.Standard', {
    extend: 'Ext.app.Controller',

    doRead: function() {
        this.store.load();
    },
    doCreate: function() {
        this.store.insert(0, this.model);
    },
    doDelete:function(){
        var selection = this.grid.getSelectionModel().getSelection()[0];
        if (selection) {
            this.store.remove(selection);
        }
    },
    doUpdate: function() {
            //更新對 store 的異動
        this.store.sync({
            success : function(){
                console.log("success");
                Ext.Msg.alert('Status', '更新成功');
            },
            failure : function(response, options){
                console.log("failure");
                Ext.Msg.alert('Status', '更新失敗');
            }  
        });
    }
});

快速前端元件建立

即使用像 grails 這樣的 full stack framework 對於前端介面還是需要自己重頭刻起,若是搭配 sencha architect 將可以補齊這方面的不足:快速建立前端介面,並且為了敏捷快速的開發,一旦介面拉好,就可以儘快確認需求與操作介面,所完成的介面就可以開始著手開發,介面的變動也可以在 architect 中完成,還記得之前有介紹過在 extjs 中的每個小元件都可以作為類別存在,並且 controller 若以每個元件為目標設計,透過混和(mixins)的特性組合 controller 就可以快速調整介面的呈現與互動。

extjs develop mode & test

一個好的框架,必須還要能夠方便測試,在 extjs 中可以很方便的指定某個類別作為初始的 view,可以參考上一篇 Sencha Architect 快速開發 extjs中「方便進行測試與開發」的介紹,即使你沒有用 Architect,也可以自行定義,別忘了利用這樣的特性對開發中的介面進行測試。

extjs production mode

應用程式開發到一個階段,就會從 develop 進階到所謂的 production mode,其目的就是要盡量加速資源的載入,在前端的世界就是要將所有的 js 檔最小化,並且合為一個 js 檔,雖然 extjs 有動態載入,實際在 production 模式這樣是很耗效能的,如果我們要自行利用 minify 工具進行壓縮,在 extjs 中各類別的相依性就無法顧慮到,並且可能因為組成檔案順序不正確造成衝突,所幸,extjs 也注意到這樣的問題,提供 Sencha cmd 來處理 minify js 的程序,並且可以搭配 Architect 使用,步驟如下:

  1. 利用 sencha cmd 產生 extjs 專案

    sencha -sdk {extjs_home} generate app {projectName} {projectLocation}
    
  2. 修改 sencha 設定檔:修改 {projectLocation}/.sencha/app/sencha.cfg,加入下面兩行 :

    app.dir={projectLocation}
    app.classpath=${app.dir}/app.js,${app.dir}/app
    
  3. 進到 {projectLocation} 執行 production 編譯

    sencha app build production
    

如此一來就會將執行完的結果產出在 {projectLocation}/build 底下,就是這們簡單!

resource 控管

extjs 所完成的介面在 grails 中將作為 resource 存在,且對 grails 而言屬於靜態檔案,因此可以進行快取來加速資源載入,而在 grails 有一設定檔 ApplicationResources 專門在定義要載入的 resource,在設定時必須考慮 develop 與 production 的不同,設定方式如下:

import org.codehaus.groovy.grails.web.context.ServletContextHolder as SCH
modules = {
    // develop mode 使用
    extjs4_dev {
        defaultBundle 'finder_dev'

        resource url: 'extjs4_dev/resources/ext-theme-neptune/ext-theme-neptune-all.css'        
        resource url: 'ext/ext-all.js'
        resource url: 'ext/ext-theme-neptune.js'
        resource url: 'app.js'

        getFilesForPath('app').each {
            resource url: it
        }
    }   
    // production mode 使用
  extjs4 {
        defaultBundle 'finder'
        resource url: 'extjs4/resources/finder_extjs-all.css'
        resource url: 'extjs4/all-classes.js'
    }   
}

// 載入 path 參數底下所有的檔案作為 resource
def getFilesForPath(path) {
    def webFileCachePaths = []
    def servletContext = SCH.getServletContext()

    if(!servletContext) return webFileCachePaths
    def realPath = servletContext.getRealPath('/')
    def appDir = new File("$realPath/$path")
    appDir.eachFileRecurse {File file ->
        if (file.isDirectory() || file.isHidden()) return
        webFileCachePaths << file.path.replace(realPath, '')
    }
    webFileCachePaths
}

經由這樣的設定,grails 會自動將 block 中所定義的 js 檔自動合為單一 js 檔,接著我們只要在 grails 中特有的 gsp 加入下列判斷:

<g:if env='development'>
    <r:require modules="extjs4_dev"/>
</g:if>
<g:else>
    <r:require modules="extjs4"/>
</g:else>

就會根據不同的開發模式載入不同的 resource 組合。

打完收工,期待下次在相會!

這是個想法,目前我們也正在投入這樣的應用,預期可以帶來不一樣的開發方式,軟體開發方式不停的在進步,也許還有很多團隊還在使用老舊的方法,這樣的組合,除了可以敏捷快速的開發,利用 extjs 所提供的方便性,相信可以帶來效率的提升,特別是前端的物件建立與操作,表單式的應用程式非常適合,筆者也曾在企業進行 extjs 的教育訓練,歡迎有興趣的讀者可以互相切磋。

系列文章到此告一段落,期待下次在與大家分享!

張貼留言