2012年3月25日

淺談 Grails Resources Plugin 網站靜態資源最佳化

Resources plugin 在 Grails 2.x 成為預設的套件,有許多其它來不及升級的 plugin,也因為啟用 Resources 的關係而導致 bug 出現,許多人建議乾脆把它關掉吧!這篇文章希望可以幫助開發者更多認識 Resources,善用它,而不要將它給關掉了 : )

Resources plugin 是用來處理網站的靜態資源,例如 *.js 及 *.css,由於 modern web design 在 front-end 用了愈來愈多 CSS/JavaScript,使得這些資源是否最佳化也會造成網站效能的影響。

在 Grails 1.3.x 需要自己安裝 resource plugin,這只需要一行指令:

grails install-plugin resources

在 BuildConfig 可以看到 plugins 區塊多了 resources 的宣告。

plugins {
    // ...
    compile ":resources:1.0.2"
}

Resources 是在編譯、建置階段處理靜態資源,因此並不會讓執行期速度變慢,雖然在 development 階段,它可能會多消耗一些處理效能,偶爾也會造成資源載入不正常。但是到 production 階段,它的效果通常都會帶來正面的幫助。

用 Grails 2.x 建立的網站專案,可以發現 grails-app/views/layout/home.gsp 已經使用 <g:layoutResources /> 的宣告。這是啟用 Resources plugin 的必要步驟,在 View 的 *.gsp 加上宣告,讓資源能在適當的位置載入。

「適當位置」包括在 </head> 以及 </body> 之前,例如:

<html>
   <head>
      <g:layoutTitle/>
      <r:layoutResources/>
   </head>
   <body>
      <g:layoutBody/>
      <r:layoutResources/>
   </body>
</html>

接下來,將所需要的資源以宣告方式加入 *.gsp,例如:


    <head>
        <!-- require modules -->
        <r:require modules="jquery"/>
        <r:layoutResources/>
    </head>


在 Grails 2.x 已經預設提供 jquery plugin,而新版的 jquery plugin 有針對 Resources plugin 提供適當設定,因此在 <r:require ... /> 可以直接加入 jqeury 的 modules 設定。

對於專案本身的資源,必須建立 grails-app/conf/ApplicationResources.groovy 設定檔,將相關的資源分類定義成 module。例如以下的範例,是我自行將 Compass 編譯產生的 .css 定義成 "compass" 這個 module。

modules = {
    application {
        resource url: 'js/application.js'
    }


    compass {
        resource url: [dir: '/stylesheets', file: 'screen.css'],
            attrs: [media: 'screen, projection']
        resource url: [dir: '/stylesheets', file: 'print.css'],
            attrs: [media: 'print']
        resource url: [dir: '/stylesheets', file: 'ie.css'],
            attrs: [media: 'screen'],
            wrapper: { s -> "<!--[if IE]>$s<![endif]-->" }
        resource url: [dir: '/stylesheets', file: 'ie6.css'],
            attrs: [media: 'screen'],
            wrapper: { s -> "<!--[if lt IE 7]>$s<![endif]-->" }
    }

因此,若網站頁面需要用到 compass 模組,就只需要在某些 *.gsp 加上:


<r:require modules="jquery, compass"/>


這麼做有什麼好處呢?當網站用到的資源多到某個程度,利用 Resources plugin modules 定義進行集中分類、版本管理,就可以讓靜態資源更容易維護。

以下是 jquery-colorbox 及 codemirror 的資源定義,在 modules 宣告中可以混合 *.css 及 *.js,將歸屬於同一個類別的資源定義在一起(但資料夾可以分開,例如 /css 及 /js)。

    colorbox {
        resource url: [dir: '/css', file: 'colorbox.css'], attrs: [media: 'screen']
        resource url: [dir: '/js', file: 'jquery.colorbox-min.js']
    }


    codemirror {
        resource url: [dir: '/codemirror/lib', file: 'codemirror.css'],
            attrs: [media: 'screen']
        resource url: [dir: '/codemirror/lib', file: 'codemirror.js']
        resource url: [dir: '/codemirror/mode/rst', file: 'rst.js']
        resource url: [dir: '/js', file: 'jquery.codemirror.js']
        resource url: [dir: '/codemirror/lib/util', file: 'runmode.js']
    }

但是 Resources plugin 如果功能只是這樣,那它存在的必要性其實就不高,也不會變成 Grails 2.x 的預設功能。因為在 Grails 中,使用 taglib(自訂標籤)同樣可以輕鬆達到相同目的,例如利用 SimpleTag 自行定義 <res:jquery />、<res:codemirror /> 等標籤,用起來也不會比較麻煩。

所以,Resources plugin 對資源進行定義、管理,其實只是第一個步驟而已,我們可以(選擇性)加裝其它以 Resources plugin 為基礎建立的 Plugins,來達到資源自動最佳化的需求。

以下的 Plugin 安裝指令,應該從套件名稱就能猜到它們的用途。

# Compress resources using GZip/Deflate
grails install-plugin zipped-resources

# Cache resources in browsers
grails install-plugin cache-headers
grails install-plugin cached-resources

# Delivery resources using CDN
grails install-plugin cdn-resources

# Process less css
grails install-plugin lesscss-resources

# Pre-compress resources with YUI Compressor
grails install-plugin yui-minify-resources

zipped-resources 及 cached-resources 是 GrailsRocks 提供的 Plugins(開放源碼授權),只要安裝上去,就能立即得到靜態資源被壓縮、快取的效果。對於瀏覽器來說,差別只是存取 *.css 及 *.js 的檔案名稱被編碼過,其它並無副作用。

cdn-resources 可以和 zipped/cached 混搭使用,顧名思義就是透過 CDN(Content-Delivery-Network)來分散、加速瀏覽器對資源的存取,並減少網站伺服器的處理及頻寬負擔。它的原理相當簡單,開發者只要申請 Amazon CloudFront 或其他 CDN 服務,透過 CDN 來 mirror 網站的檔案,然後將 CDN URL 設定好,Grails 最後生成的 html 就會自動以 CDN URL 取代靜態資源原本的 URL。

使用 cdn-resources 需要以下的設定:

grails.resources.cdn.enabled = true
grails.resources.cdn.url = "http://cdn.your-web-name.com/"

因為有 enabled 選項可以自訂,所以建議只針對 production 開啟 cdn,至於 development 階段本來就沒有必要用 cdn,用了只會讓網站的開發、測試變得無法進行。

一般來說,使用 CDN 對於開發初期、內容不斷更新的網站,會帶來一些影響,因為大多 CDN 服務有 TTL(Time-To-Live)的限制,也就是內容可能已更新,但 CDN 伺服器仍然使用舊資料的快取來回應瀏覽器,造成網站發佈更新後的結果不一致、需要等候資源被更新(以 Amazon Cloud Front 來說,可能長達 24 小時)。

但 cdn-resources 與 zipped/cached-resources 搭配使用,理論上是能夠避免這種問題發生,因為 *.css / *.js 的檔案名稱會經過編碼,所以 JavaScript/CSS 靜態資源如果有更新,理論上就會產生新的檔案名稱,此時 CDN 服務就會重新讀取發佈的資料。當然,這在開發階段並不容易測試效果。

最後一個建議嘗試的套件是 yui-minify-resources,這個套件的使用方式也非常簡單,只要安裝好就會立即得到效果。它是利用 YUI Compressor 將 *.css/*.js 檔案進行壓縮處理,得到 *.min.css/*.min.js 最佳化的結果。

這裡也建議只將 yui-minify-resources 用於 production,如此一來,在 development 階段就能使用原始的 .css / .js 檔案以利除錯,而打包成 war 發佈之後又能自動化進行壓縮,可以同時兼顧 production 及 development 不同階段的需求。

回到文章一開始說到的「問題」,也就是許多舊的 Grails Plugins 並不支援 Resources plugin,主要原因是這些 plugin 附帶的靜態資源並沒有提供 resources 設定,造成在啟用 resources 時缺少檔案,瀏覽器讀不到(404 error)。

但是 2.x 遲早會成為 Grails 的主流版本,而 Resources plugin 帶來的利大於弊,所以這也是時候該淘汰那些不再繼續維護、更新的套件了;因為有了 Resources plugin,有一些 Grails plugin 僅有提供 3rd party js/css library 這種簡單功能,可以考慮自行打包 plugin 或藉由 resources modules 的宣告來達成。

Grails 是 VMWare / SpringSource 建立的開發框架,旨在提供 Java EE 開發者能利用 Groovy 程式語言、Rails 風格,簡化 Modern Web Application 的專案開發工作,既能延續利用現有的 Java Library,滿足企業級的開發需求,又能兼顧團隊對敏捷開發的追求。

如果您對 Grails 開發框架有興趣,歡迎加入我們的 Groovy Taiwan 社群。

沒有留言:

張貼留言

lyhcode by lyhcode
歡迎轉載,請務必註明出處!