:::

Blogger範本應用CSS Sprites技術記事

Blogger範本應用CSS Sprites技術記事

padding_header_footer

blogger-icon-set subscribe-icon-set

最近我在學習CSS Sprites技巧,並嘗試將此技巧應用於「布丁布丁吃什麼?」blog中。以下記錄開始修改的機緣、簡單介紹CSS Scrites的原理,然後一一記錄我如何將CSS Scrites應用於Blogger的範本中的過程。


機緣

最近在修改Blogger範本之後,就想說應該拿個什麼測速工具之類的檢測一下Blog有什麼問題。赫然想起之前電腦玩物介紹了Google Page Speed Online,他可以提供網站速度評測指標的分析與指導,似乎頗值得拿來參考。

image

分析之後,Page Speed Online指出「布丁布丁吃什麼?」最優先需要修改的建議是「將圖片合併到CSS合併圖片」,也就是它建議我應用CSS sprites技巧來改善網頁的讀取速度。

image

我用Firebug檢查了一下圖片的請求狀況,發現光是布丁的自我簡介(2011年版)就有56個請求(如上圖)。趁著改良Blog的機會,我也想來練習做做看CSS Sprites,提昇自己的程設能力。


CSS Scrites原理

CSS Scrites是一種提高網頁讀取速度的技巧。其原理是降低圖片請求(request)數量,以節省請求時額外消耗的速度。

概要作法是將網頁中多張圖片結合起來,再透過CSS語法調整,讓每個位置都只顯示該部分的圖片。應用CSS Scrites之後,原本網頁需要讀取多張圖片時需要跟伺服器請求的數量,會因為合併成一張圖片,而大幅降低了請求數量,因此也節省了多次請求而消耗的速度。

其原理很容易懂,但是實作的時候卻不容易。這需要熟悉HTML跟CSS語法才能進行,而且也需要分辨哪些圖片可以應用CSS Scrites,或是哪些不行。

Page Speed Online有給我們一些建議,我嘗試翻譯如下:

  • 合併會一起讀取的圖片:建議合併時常在同一頁面中同時讀取的圖片。例如,每一頁都會用到的同一組圖示,就適合進行合併。相反的,每一次讀取都會改變的動態圖片,例如大頭貼照片、或是在頁面中會時常變更的圖片,就不建議進行合併。
  • 優先合併GIF跟PNG圖片:GIF跟PNG圖片使用無損壓縮法,因此合併時並不會因此降低合併圖片的品質。
  • 優先合併小型圖片:每一個圖片請求都會需要固定的額外請求時間(request overhead),即使是下載小型圖片,瀏覽器也會需要為此耗費額外的請求時間。藉由合併小型圖片,將可以從每一次請求一張圖片到一次請求就讀取整張合併的圖片,因此降低了額外請求的時間。
  • 合併可以快取的圖片:建議合併快取時間(caching lifetime)較長的圖片。如果圖片已經被瀏覽器快取,那瀏覽器就不需要再次下載該張圖片,以提高讀取的效率。
  • 使用CSS Sprite服務:合併圖片時,可以使用SpriteMe之類的服務,讓你輕易應用CSS sprites。
  • 最小化合併圖片中的空白處:為了顯示圖片,瀏覽器必須解壓縮並解碼該圖片。圖片的尺寸通常是跟圖片的解析度成正比。因此,當合併圖片中的空白處過多的時候,即使沒有明顯改變圖片的檔案大小,但是沒有顯示的像素依然會佔據記憶體的用量,造成瀏覽器回應速度變慢。
  • 合併使用同樣色彩的圖片:合併圖片如果超過256色,將會讓PNG從palette type改成使用truecolor type,並造成合併圖片檔案變大。為了產生最佳化的合併圖片,要合併的圖片最好都使用相同的256色。如果你的圖片還有調整的空間,建議考慮想辦法讓你的合併圖片色彩數量降低到256色。

SpriteMe的安裝與分析建議

image

既然Page Speed Online都建議我先從SpriteMe開始了,那就先用SpriteMe看看有什麼好的建議吧。

安裝SpriteMe書籤

SpriteMe是一個書籤小工具(Bookmarklet)請把下面的連結拖曳到書籤列上吧。

SpriteMe

使用SpriteMe

image

打開你要分析的網頁,這邊我一樣以布丁的自我簡介(2011年版)來做做看。SpriteMe將網頁中的圖片分成「Suggested Sprites」(建議合併)與「Non-Sprited Images」(不合併圖片)這兩種。以下是詳細的列表:

image

接著讓我們來看看SpriteMe為什麼建議合併與不建議合併的理由。

合併建議1:合併不重複的圖片

第一項是「vertical, varied width」(垂直的,多變的寬度),直接翻譯還真是看不懂是什麼意思,但仔細一看他列出的圖片,大多都是寬度、高度不等,而且在CSS中都是不重複(no-repeat)的圖片,簡單來說就是建議合併的大雜匯啦。

以下舉幾張例子:

  • image
    頁首標題前的箭頭
  • image
    擺在頁首的底部圖片
  • image 
    日期前的箭頭
合併建議2:X軸重複、寬度相等

這一個建議很特別,他分析出兩張寬度相等(760px)的背景圖,而且他們也都是設定為X軸重複(repeat-x),因此也適合合併成一張圖。

這兩張圖各別是:

  • image
    頁首的背景
  • image
    頁尾的背景

他們都Y軸的漸層效果。仔細一看,似乎這背景圖也不需要這麼寬,就能用X軸重複達到填滿的效果了。

不合併的建議

除了合併的建議之外,SpriteMe也給了不合併的建議。我把圖片的長寬尺寸與理由列舉如下:

  • bg_body.gif 5x600:因為X軸重複,而且太短
  • bg_sidebar_arrow.gif 80x98:Y軸重複而且太窄
  • icon_list_item_left.gif 9x7:因為包含他的區塊(block)比他高且比他寬。(意思是這種情況還是用背景圖來顯示比較好)
  • bg_sidebar.gif 5x464:因為看不到。(我動態地把導覽列隱藏起來的關係)

SpriteMe的使用過程

SpriteMe不僅僅是分析建議很詳細,就連使用起來也很容易。

建立合併

image

上面提到SpriteMe建議我合併X軸重複、寬度相等的圖片,而該區右上角有個「make sprite」按鈕,就能夠自動產生CSS Sprite的效果。

image

按下去之後稍待一會,圖片就合併成一張了。打開項目的詳細事項,裡面記載著SpriteMe調整過的元素內容。點選該元素,他會在元素外圍描繪上紅色的框線。

image

他同時也提供了一張合併後的圖片,如上圖。儘管我很好奇的是,不知道為什麼SpriteMe合併之後的圖片間會有這麼多空白間隔(padding)。可能是預留排版出錯時的緩衝空間吧?

image

SpriteMe直接將合併之後圖片的語法寫在受到調整的元素中。上圖是頁首背景圖片直接套用了SpriteMe的合併圖片,可以看到他以background-image跟background-position設定直接寫在元素的style屬性中了。

輸出CSS

image

雖然右上角有個「export CSS」功能,可以把合併後的圖片與語法輸出成CSS。只是在Chrome裡面發生了JavaScript錯誤而無法執行,後來我改用Firefox 4來操作,就能夠開啟SpriteMe Export CSS網頁。

image

Export CSS網頁中,先告訴我剛剛我合併的圖片網址。

然後下面列出了這個網頁使用的CSS檔案,並嘗試在這些檔案中找尋剛剛修改的元素設定位置。可惜因為Firefox的跨網域限制,SpriteMe沒辦法自動幫我分析這些CSS檔案的內容。

接著他列出CSS的建議修改方式,包括刪掉原本的圖片,並替換上新的圖片。這個建議可以讓我輕易地修改CSS檔案,非常地實用。其內容如下:

DIV id=header class=header section
{
  background-image: url("http://www.blogblog.com/thisaway/bg_header.gif")
  background-image: url("http://www.jaredhirsch.com/coolrunnings/public_images/a98ceddb07/spriteme2.png");
  background-position: 0px -18px;

}
DIV id=footer class=footer section
{
  background-image: url("http://www.blogblog.com/thisaway/bg_footer.gif")
  background-image: url("http://www.jaredhirsch.com/coolrunnings/public_images/a98ceddb07/spriteme2.png");
  background-position: 0px -118px;

}

當然,我會把SpriteMe產生的合併圖片下載之後,上傳到自己的空間,然後再把之間的網址改成我的空間,這樣才不會造成SpriteMe伺服器的負擔。

image

修改完成之後,圖片的請求數從原本的56個降低為55個囉。

分組寬度相當、顏色相近的圖片來建立合併圖片

image

如果直接採用SpriteMe的建議,把寬度、高度不等的圖片直接合併,就會出現像上面的合併圖片。在Page Speed Online建議合併圖片要盡量降低空白處,而上圖很明顯的違反了這個建議。多次嘗試之後,我發現SpriteMe只會將圖片垂直排列來合併。因此,如果只將寬度相當的圖片進行合併,就能夠將合併圖片的空白處降低到最小。

image

上圖是SpriteMe預設的建議,圖片的寬度從760px到10px都有,合併起來將會出現相當多的空白。還好,SpriteMe也提供了讓使用者自訂合併圖片的功能,請按下上面的「new sprite」按鈕。

image

這時候上面就會出現新的合併列表,但是是空的。

image

你可以從下面的圖片,將寬度差不多的圖片拖曳到這個區塊,SpriteMe就會依照你指定的圖片建立新的合併圖片。

image

我將合併圖片分成三組,個別是寬度為10px的圖片、寬度為760px的圖片,以及寬度為47px到54px之間的圖片來進行合併。

合併之後的結果如下:

  • image
  • image
  • image

這樣子空白處就減少很多囉。

另外Page Speed Online也建議將合併圖片的顏色數量降低到256色之內,這也是分組時的一個參考依據。

SpriteMe忽略了<img>圖片

仔細比較一下Page Speed Online給的建議,會發現SpriteMe還忽略了很多圖片。再細部分析一下,這些圖片都是以<img>圖片標籤顯示的內容。

image

上面的「訂閱所有留言」功能就用了大量的<img>標籤,而且都是固定常出現的小型圖片。Page Speed Online建議我合併這些圖片,但是SpriteMe並沒有分析到這邊。

為了要讓SpriteMe偵測到這些圖片,我必須先把<img>中src指定的圖片,改成以background-image背景圖片的方式來顯示。

原本我是想在<img>直接設定背景圖片,但是效果卻不如預期。Firefox中,只有將<img>顯示型態設為block的時候,才能順利顯示背景圖。因此,我決定將<img>改成<div>,並以CSS的background-image來顯示圖片。


<img>改成<div>背景圖

以下我以這個「訂閱所有留言」的功能來說明修改的過程。這是一個寫在小工具區的HTML程式碼,內容如下:

<div class="subscribe-wrapper subscribe-type-COMMENT">
    <div style="display: none;" id="SW_READER_LIST_Subscribe1COMMENT" class="subscribe expanded subscribe-type-COMMENT">
        <div class="top">
            <span onclick="return(_SW_toggleReaderList(event, &quot;Subscribe1COMMENT&quot;));" class="inner">
                <img src="http://img2.blogblog.com/img/widgets/arrow_dropdown.gif" class="subscribe-dropdown-arrow" />
                <img border="0" align="absmiddle" src="http://img1.blogblog.com/img/icon_feed12.png" class="feed-icon" alt="" />
                訂閱所有留言
            </span>
        <div class="feed-reader-links">
            <a target="_blank" href="http://www.google.com/ig/add?source=bstp&amp;feedurl=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link">
                <img src="http://img1.blogblog.com/img/widgets/subscribe-google.png" />
            </a>
            <a target="_blank" href="http://www.bloglines.com/sub/http://pulipuli.blogspot.com/feeds/comments/default" class="feed-reader-link">
                <img src="http://img1.blogblog.com/img/widgets/subscribe-bloglines.png" />
            </a>
            <a target="_blank" href="http://www.netvibes.com/subscribe.php?url=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link">
                <img src="http://img1.blogblog.com/img/widgets/subscribe-netvibes.png" />
            </a>
            <a target="_blank" href="http://www.newsgator.com/ngs/subscriber/subext.aspx?url=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link">
                <img src="http://img1.blogblog.com/img/widgets/subscribe-newsgator.png" />
            </a>
            <a target="_blank" href="http://add.my.yahoo.com/content?url=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link">
                <img src="http://img1.blogblog.com/img/widgets/subscribe-yahoo.png" />
            </a>
            <a target="_blank" href="http://pulipuli.blogspot.com/feeds/comments/default" class="feed-reader-link">
                <img align="absmiddle" src="http://img1.blogblog.com/img/icon_feed12.png" class="feed-icon" />
                Atom
            </a>
        </div>
    </div>
    <div class="bottom"></div>
</div>
<div onclick="return(_SW_toggleReaderList(event, &quot;Subscribe1COMMENT&quot;));" id="SW_READER_LIST_CLOSED_Subscribe1COMMENT" class="subscribe">
    <div class="top">
        <span class="inner">
            <img src="http://img2.blogblog.com/img/widgets/arrow_dropdown.gif" class="subscribe-dropdown-arrow" />
            <span onclick="return(_SW_toggleReaderList(event, &quot;Subscribe1COMMENT&quot;));">
                <img border="0" align="absmiddle" src="http://img1.blogblog.com/img/icon_feed12.png" class="feed-icon" alt="" />
                訂閱所有留言
            </span>
        </span>
    </div>
    <div class="bottom"></div>
    </div>
</div>

 

程式碼有點長,不過構造還算簡單。大致上需要改的有兩種類型,以下一一敘述作法。

修改顯示類型為block(區塊)圖片

image

有些<img>被賦予了display:block;的設定,表示他會跟<div>一樣以block(區塊)的樣式顯示。在「訂閱所有留言」中,下拉選單的各種圖示都是以這種形式呈現。

這種形式的<img>圖片可以很容易地修改成<div>背景圖,也不容易影響排版。

讓我們先看看image Add to Google這個圖示的原始碼:

   1: <a target="_blank" 
   2:     href="http://www.google.com/ig/add?source=bstp&amp;feedurl=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" 
   3:     class="feed-reader-link">
   4:     <img src="http://img1.blogblog.com/img/widgets/subscribe-google.png" />
   5: </a>

這是一個<a>連結標籤包含著<img>圖片標籤的元素。在其他的CSS當中,此處的<img>被設定為display:block;,因此我們可以考慮直接把這種<img>換成<div>,並加上額外的CSS設定。

修改的過程有幾個步驟:

  1. 在<img>後面建立<div>,並給予適當的class名稱,以便後續CSS設定中可以正確地選擇到該<div>。這邊要注意的是,必須完整撰寫<div></div>標籤,而不能用<div/>這種空標籤喔。
  2. 增加額外的CSS設定,包括:
    • background-image: url(圖片網址):指定<img>讀取的圖片
    • background-repeat: no-repeat:不重複圖片
    • width & height:根據圖片大小設定
  3. 移除原本的<img>

以下是修改之後的元素程式碼與CSS設定:

   1: <a target="_blank" href="http://www.google.com/ig/add?source=bstp&amp;feedurl=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link">
   2:     <div class="subscribe-google"></div>
   3: </a>
   4: <style type="text/css">
   5: .feed-reader-link .subscribe-google {
   6:     background-image: url(http://img1.blogblog.com/img/widgets/subscribe-google.png);
   7:     background-repeat: no-repeat;
   8:     width: 104px;
   9:     height: 17px;
  10: }
  11: </style>

儘管大致上改到如此就可以告一段落,但是這段HTML還有繼續改善的空間。

由於<a>標籤中只包含<img>(後來被我換成<div>了)一個元素,因此我們可以考慮將樣式直接套用到<a>中,省略掉多餘的<div>。

更進一步修改之後的程式碼如下。必須注意的是,CSS中選擇器改成指定<a>的class名稱,並加入顯示類型display: block的設定囉:

   1: <a target="_blank" href="http://www.google.com/ig/add?source=bstp&amp;feedurl=http%3A%2F%2Fpulipuli.blogspot.com%2Ffeeds%2Fcomments%2Fdefault" class="feed-reader-link subscribe-google">
   2: </a>
   3: <style type="text/css">
   4: .feed-reader-link.subscribe-google {
   5:     background-image: url(http://img1.blogblog.com/img/widgets/subscribe-google.png);
   6:     background-repeat: no-repeat;
   7:     width: 104px;
   8:     height: 17px;    display: block;
   9: }
  10: </style>

修改之後的程式碼又更簡潔了。實際應用的時候,CSS設定會集中在其他檔案中,而不會像上面一樣跟HTML寫在一起。

修改顯示類型為inline(同軸)的圖片

image

<img>圖片的預設顯示類型是inline,意思是會跟文字一樣一起排列。而<div>卻是block顯示類型,會強制將後面的內容換行。

有些時候,<img>會以原始的inline跟其他文字一起排列,這種情況要改成<div>就比較困難,通常還要搭配float浮動樣式、margin外距調整、以及搭配一些CSS修改經驗才能達成。

這次我要修改的是「訂閱所有留言」左邊的RSS圖片,這張圖片與「訂閱所有留言」文字一起排列,是以預設的inline顯示類型呈現。它的原始碼如下:

   1: <span 
   2:  onclick="return(_SW_toggleReaderList(event, &quot;Subscribe1COMMENT&quot;));">
   3:     <img border="0" 
   4:      align="absmiddle" 
   5:      src="http://img1.blogblog.com/img/icon_feed12.png" 
   6:      class="feed-icon" alt="" />
   7:     訂閱所有留言
   8: </span>

我為它進行的修改步驟為:

  1. 在<img>後面建立<div>,並給予適當的class名稱,以便後續CSS設定中可以正確地選擇到該<div>。
  2. 增加額外的CSS設定,包括:
    1. background-image: url(圖片網址):指定<img>讀取的圖片
    2. background-repeat: no-repeat:不重複圖片
    3. width & height:根據圖片大小設定
    4. float: left:因為該圖片位於文字的左方,所以用此設定
    5. margin 調整外距
  3. 移除原本的<img>

修改後的程式碼與新增的CSS設定如下:

   1: <span 
   2:  onclick="return(_SW_toggleReaderList(event, &quot;Subscribe1COMMENT&quot;));">
   3:     <div class="feed-icon"></div>
   4:     訂閱所有留言
   5: </span>
   6: <style type="text/css">
   7: .inner .feed-icon {
   8:     background-image: url(http://img1.blogblog.com/img/icon_feed12.png);
   9:     background-repeat: no-repeat;
  10:     width: 12px;
  11:     height: 12px;
  12:     float: left;
  13:     margin-top: 2px;
  14:     margin-left: 7px;
  15: }
  16: </style>

image

修改之後,因為加上了margin外距調整,感覺比之前的<img>更順眼了點呢。

顯示圖片讓SpriteMe能夠偵測

即使把<img>改成<div>的背景圖片了,還要記得把他們顯示出來(visible),SpriteMe才能夠偵測並判斷它是否適合成為合併的圖片,否則會被歸類成不適合合併的圖片。

image

在分析之前,我先將「訂閱所有留言」的選單打開,再使用SpriteMe的功能,如上圖。

image

這下子SpriteMe總算偵測到剛剛我修改的<div>背景圖,而且將它列入建議合併的圖片中了。

其他CSS Sprites技巧

SpriteMe是分析背景圖片(background-image)以達到CSS Sprites技巧的效果,不過除了背景圖片之外,還有其他技巧可以使用。

CSS擁有無限的可能性,而且隨著瀏覽器跟標準不斷地改變,未來也可能會有更好用的技巧出現也說不定吧。


真的有必要做CSS Sprites嗎?

儘管CSS Sprites能夠降低圖片請求數量、提昇網頁讀取的速度,但任何技術都不是萬靈丹,在使用CSS Sprites的時候我也發現到一些限制,在此提出來跟大家討論一下:

合併後的圖片難以管理

現在我們將多張圖片合併成一張大型圖片,用CSS Sprites設定背景圖片與背景位置。如果未來需要變更其中一張圖片的高度,這不僅要把之前所有的圖片都找回來,還需要修正下面的圖片的背景位置。

為了避免這種情況發生,在使用CSS Sprites技巧時,盡量挑選不會變更的圖片,或是將可能會一起變更的圖片一起合併,要改的時候也一起改。

最後,要記得保留合併前的舊圖片,以免未來要重新合併時找不到圖片。我在CSS樣式檔中,就會將舊圖片的連結先註解掉,讓未來還有回復成原本圖片的機會:

   1: #header {
   2:   background-color: #634320;
   3:   /*background-image: url(http://www.blogblog.com/thisaway/bg_header.gif);*/
   4:   background-image: url(http://dl.dropbox.com/u/717137/blogger/img/bg_header_footer.png);
   5:   background-position: 0px -18px;
   6:   background-repeat: repeat-x;
   7: }
將<img>轉換成<div>的人工成本與風險相當高

SpriteMe不會分析<img>中的圖片,而需要我們手動將<img>轉換成<div>背景圖,才能順利讓SpriteMe分析。前面我也簡單地敘述了兩種轉換的過程,不過實際使用時一定會遇到許多更棘手的情況。

最大的問題仍是在<img>以inline顯示類型與<div>的block顯示類型基本上就有很大的差別。由於<div>一定要設定為block才能設定寬度與高度,所以不能單純轉換為inline顯示類型。

當然,還有許多CSS設計技巧可以迴避掉類似的問題,但不論是哪種方法,都需要相當有經驗的CSS設計師才能做到。隨隨便便套用CSS設定,都會帶來版面構造破壞的風險。

區塊有重複延展的需求時,盡量不要做CSS Sprites

前面SpriteMe的建議都是針對不重複、或是針對X軸重複的圖片建議合併,但是有時候SpriteMe的建議也不是萬能,它並不能預測到你這個區塊未來是否有需要延展的空間。

有一種CSS設計,是在指定不重複(background-repeat: no-repeat)的背景圖片(background-image),同時搭配背景顏色(background-color)的情況。這種設計並不是讓背景圖片重複來填滿背景,而是用背景顏色來填滿,但只有特定的地方顯示背景圖片而已。

image

「布丁布丁吃什麼?」的範本中時常出現這種設計,例如上圖的頁首區塊。它的CSS設定如下:

   1: #header {
   2:   background-color: #634320;
   3:   background-image: url(http://www.blogblog.com/thisaway/bg_header.gif);
   4:   background-repeat: repeat-x;
   5: }

image

如果當頁首資料量太多的時候(我繞了好多遠路),背景還能順利地延展開來,不會讓版面變得很奇怪。

image

但是如果照前述的方式將頁首區塊的背景合併成CSS Sprites,當資料量一多的時候,就會發現頁首喪失了延展性,背景變得很奇怪了。

即使不是資料量變多,而只是單純地縮小視窗寬度,資料自動往下擠壓而造成額外的高度需求,那也可能發生類似的破版畫面。

儘管有很多技巧可以避免上述的問題,不過這邊我想說的是,當區塊有擴增、延展的需求時,不要輕易地將它的背景圖片做成CSS Sprites,以免徒增版面發生錯誤的風險。


成果與結語

image

經過CSS Sprites調整之後,布丁的自我簡介(2011年版)從56個圖片請求數降低到了47個請求數。老實說,感覺成效並不明顯。

image

另一方面主要的原因在於大多數圖片並不是我能掌控的範圍,像是Google Friend Connect的大頭像、Plurk的顯示圖片。結果Page Speed Online還是建議我去合併這些圖片,我也沒輒了。

附帶一提,我並沒有特別去注意下載速度與圖片壓縮的數量,因為實際上都是小圖片的整併,比較這一點點差距沒有多大意義。

總之,經過這次的把玩,總算對CSS Sprites這個技巧有更深入的了解。以後在設計網頁的時候,不妨預先考量到可以進行CSS Sprites的轉換空間,將<img>以<div>替代,然後最後再利用SpriteMe來動手術,努力提昇提昇網頁讀取效率吧。

(more...)

Blogger範本之網頁類型(pageType)應用

布丁布丁吃布丁

Blogger範本之網頁類型(pageType)應用

image

Blogger的範本是以Blogger特殊的範本語法跟HTML語法混合撰寫而成,在Blogger版面配置中有相關說明。常常會有人用「Blogger Hack」來形容修改範本的行為,而我自己把玩Blogger範本也有一段時間,偶爾也會跟大家分享一些Blogger的程式碼。

這篇要跟大家介紹的是Blogger範本程式中最常用到的「網頁類型」(page type),以及如何偵測不同的網頁類型來對版面進行微調。


網頁類型之index、archive、item

「網頁類型」是Blogger在不同情境下的狀態參數,名稱是blog.pageType,可以利用以下語法來顯示網頁類型:

<data:blog.pageType/>

在Blogger的資料標記說明中,網頁類型有「index」(包含首頁、搜尋、標籤label列表)、「archive」(每年、每月封存的文章列表)、「item」(文章全文,Google翻譯做項目)、「static_page」(靜態網頁)等四種。

實際上在使用網頁類型時,會搭配Blogger範本語法中的條件標記<b:if></b:if>跟<b:else/>來使用。舉例來說,我想要在item類型中的顯示一段話,在index類型中顯示另外一段話的話,可以插入以下語法:

<b:if cond='data:blog.pageType == &quot;item&quot;'>
這段文字會出現在item網頁類型。
</b:if>
<b:if cond='data:blog.pageType == &quot;index&quot;'>
這段文字會出現在index網頁類型。
</b:if>

其中「&quot;」是指「雙引號"」的脫逸化後的字串,同等於雙引號。只是在Blogger範本處理過程中,會自動將雙引號換成&quot;。大家在使用的時候請不要覺得奇怪喔。

區隔index中的首頁與其他網頁

預設的網頁類型雖然有四種,但是index類型包含了首頁、搜尋與列表,這樣的混雜令人難以使用。

舉例來說,我會希望我的Blogger首頁顯示文章內文摘要、而搜尋跟標籤(label)列表只要顯示標題就好。就像下表左右兩張圖的區別一樣。但是光用網頁類型卻無法區隔這兩者的差別。

image image

首頁

搜尋、標籤列表

其實,Blogger範本中還有另一種資料標記可以用來區隔是否是首頁,那就是「blog.homepageUrl」(Blog首頁的網址)跟「blog.url」(現在網頁網址)。blog.homepageUrl在各種類型的網頁中都是固定的參數,表示你的Blog首頁網址,例如「http://pulipuli.blogspot.com/」;blog.url則會隨著你現在瀏覽的網頁有所不同,例如「http://pulipuli.blogspot.com/search/label/Second%20Life」。

聰明的你應該發現到了,只有當頁面是首頁的時候,blog.url才會等於blog.homepageUrl。因此我們可以利用條件標記來判斷這是否是首頁,例如:

<b:if cond='data:blog.url == data:blog.homepageUrl'>
這是首頁。
<b:else/>
這不是首頁,可能是搜尋、標籤列表,或是文章全文、封存列表。
</b:if>

如果再加上網頁類型的判斷,就可以將範圍縮小到只區隔首頁跟搜尋、標籤列表這兩種類型。語法如下:

<b:if cond='data:blog.pageType == &quot;index&quot;'>
    <b:if cond='data:blog.url == data:blog.homepageUrl'>
        這是首頁。
        <b:else/>
        這是搜尋或標籤列表。
    </b:if>
</b:if>

於是這樣子,我們就能夠分辨清楚首頁與搜尋、標籤列表這兩種類型的網頁了。

重新歸類網頁類型

除了原本的四種網頁類型之外,上述的方法又可區分出首頁與搜尋、標籤列表這兩種類型。但對我來說,實際上我只把Blogger的網頁類型區分成四種:首頁、列表、文章、靜態網頁。以下一一敘述其情境、需求,以及Blogger範本語法的用法。

首頁

image

首頁,Blog一進來就會看到的網頁,是整個Blog的門面。

  • 舉例網址:http://pulipuli.blogspot.com/
  • 文章數量:我會在首頁擺放五篇左右的最新文章
  • 文章內文(post content):僅顯示文章的摘要內文(用Blogger的繼續閱讀功能,或是以JavaScript來實作)。
  • 導覽列(sidebar)預設狀態:顯示
  • 小工具(widget):列出所有的小工具,供使用者取用完整的功能。
  • 網頁類型(page type):index

首頁被歸類在網頁類型的「index」中,但透過blog.url跟blog.homepageUrl來判斷,就能實作判斷首頁的語法。Blogger範本判斷首頁的語法如下:(如果只要知道首頁的話,就不需要blog.pageType的判斷)

<b:if cond='data:blog.url == data:blog.homepageUrl'>
    這是首頁。
</b:if>
列表

image

列表是使用搜尋功能、瀏覽標籤以及網頁存檔的時候顯示的文章列表。雖然Blogger把首頁跟搜尋、標籤列表混在一起當做index網頁類型,但實際上,我們使用搜尋、標籤跟網頁存檔的時候,通常會因為單一頁面顯示太多篇文章,導致網頁讀取速度太慢。而此時使用者專注於搜尋,導覽列裡一些小工具也不需要顯示或執行,以免拖慢網頁速度。

我以前常常覺得Blogger內建的搜尋速度實在是太慢,這是因為搜尋頁面中會顯示的文章太多,加上圖片讀取、JavaScript執行的時間,就算是Google Chrome開起來也常常覺得會有即將當機的感覺。以前我能力不足,只會用JavaScript勉強修正,但現在知道怎麼利用Blogger範本之後,這個問題就迎刃而解了!

在Blogger範本中,要使用列表的條件標籤語法如下:

<b:if cond='data:blog.pageType == &quot;index&quot;'>
    <b:if cond='data:blog.url != data:blog.homepageUrl'>
        這是列表(搜尋、標籤)。
    </b:if>
</b:if>
<b:if cond='data:blog.pageType == &quot;archive&quot;'>
    這是列表(封存)。
</b:if>
文章

image

文章就是Blog單篇文章的全文。

  • 舉例網址:
    文章全文 http://pulipuli.blogspot.com/2011/06/oped.html
  • 文章數量:一篇
  • 文章內文:全部顯示
  • 導覽列預設狀態:隱藏
  • 小工具:列出全部的小工具,但延遲載入。
  • 網頁類型:item

我個人不喜歡閱讀文章時,旁邊還有一串導覽列,造成文章閱讀起來容易分心、還佔了版面的位置。所以我希望能在文章類型的頁面中隱藏導覽列,讓使用者能專心閱讀。

Blogger範本中可以直接用blog.pageType來偵測文章的網頁類型,語法如下:

<b:if cond='data:blog.pageType == &quot;item&quot;'>
    這是文章。
</b:if>
靜態網頁

靜態網頁通常是放一些長期顯示的訊息,例如詳細的「關於我」介紹、「與我聯絡」的細節。我目前並沒有使用靜態網頁,因為我覺得隨著時間一次又一次建立的網頁典藏比較有趣。因此,下面只是我推測靜態網頁可能的情境與需求而已:

  • 文章數量:一篇
  • 文章內文:全部顯示
  • 導覽列預設狀態:顯示
  • 小工具:列出全部的小工具。
  • 網頁類型:static_page

Blogger範本中也可以直接用blog.pageType來偵測靜態網頁的網頁類型,語法如下:

<b:if cond='data:blog.pageType == &quot;static_page&quot;'>
    這是靜態網頁。
</b:if>

網頁類型改變顯示內容的應用

上述的四種情境,在文章內文、導覽列預設狀態跟小工具的顯示上皆有不同的需求。那麼要如何針對不同的網頁類型,改變成該類型的顯示內容呢?答案是依照網頁類型載入不同的CSS樣式檔與JavaScript腳本檔。

全網站與類型專用的檔案

實務上,我會建立一個應用於全網站的blogger.css樣式檔以及blogger.js腳本檔,然後再依照不同網頁型態建立各自型態使用的檔案。不同網頁型態的檔案名稱字尾有所不同,我的作法是:

  • 首頁:-index
  • 列表:-list
  • 文章:-item
  • 靜態網頁:-static-page

例如文章就會有專屬的blogger-item.css樣式檔跟blogger-item.js腳本檔。

全網站的檔案跟專屬的檔案有什麼差別呢?我以「布丁布丁吃什麼?」blog右邊的導覽列為例。在blogger.css中定義主要內文與導覽列寬度的CSS語法如下:

div#main-wrapper div#main {
    width: 66%;
}
div#sidebar-wrapper {
    width: 30%;
}

因此全網站預設導覽列會在主要內容右邊,佔據大概30%的寬度。其中,兩者相加沒有100%的原因是避免擠壓到邊線排版。

而在文章網頁類型中,我希望導覽列能隱藏,而主要內文的寬度能最大化。因此在blogger-item.css中定義主要內文與導覽列寬度的CSS語法如下:

div#sidebar-wrapper {
    display:none !important;
    width: 0% !important;
}

div#content-wrapper div#main {
    width: 97% !important;
}

如此一來,當載入blogger-item.css的時候,CSS語法的「!important」就會強制覆寫blogger.css的定義,讓主要內文的寬度擴展到97%,並將導覽列隱藏起來。

至於JavaScript腳本檔的方面,雖然我沒有特別說明,不過對於常常在玩JavaScript的人來說,看到這邊應該就能夠舉一反三了吧。

載入檔案語法

建立好各自的檔案之後,在Blogger範本中,可以在<head></head>之間載入全網站的樣式檔與腳本檔,並加入判斷式載入各種不同網頁類型專屬的樣式檔與腳本檔。假設你的所有檔案都上傳到了http://yourhost/,並且可以直接取用,那麼範本的語法如下:

<!-- 載入全網站的樣式檔與腳本檔 -->
<script src='http://yourhost/blogger.js' type='text/javascript'/>
<link href='http://yourhost/blogger.css' rel='stylesheet' type='text/css'/>

<b:if cond='data:blog.pageType == &quot;index&quot;'>
    <b:if cond='data:blog.url == data:blog.homepageUrl'>
        <!-- 載入首頁的樣式檔與腳本檔 -->
        <script src='http://yourhost/blogger-index.js' type='text/javascript'/>
        <link href='http://yourhost/blogger-index.css' rel='stylesheet' type='text/css'/>
    <b:else/>
        <!-- 載入列表的樣式檔與腳本檔 -->
        <script src='http://yourhost/blogger-list.js' type='text/javascript'/>
        <link href='http://yourhost/blogger-list.css' rel='stylesheet' type='text/css'/>
    </b:if>
</b:if>
<b:if cond='data:blog.pageType == &quot;archive&quot;'>
    <!-- 載入列表的樣式檔與腳本檔 -->
    <script src='http://yourhost/blogger-list.js' type='text/javascript'/>
    <link href='http://yourhost/blogger-list.css' rel='stylesheet' type='text/css'/>
</b:if>
<b:if cond='data:blog.pageType == &quot;item&quot;'>
    <!-- 載入文章的樣式檔與腳本檔 -->
    <script src='http://yourhost/blogger-item.js' type='text/javascript'/>
    <link href='http://yourhost/blogger-item.css' rel='stylesheet' type='text/css'/>
</b:if>
<b:if cond='data:blog.pageType == &quot;static_page&quot;'>
    <!-- 載入靜態網頁的樣式檔與腳本檔 -->
    <script src='http://yourhost/blogger-static-page.js' type='text/javascript'/>
    <link href='http://yourhost/blogger-static-page.css' rel='stylesheet' type='text/css'/>
</b:if>

實際上可能不會應用到這麼多種類型的檔案,像我自己沒有使用靜態網頁,所以並沒有判斷static_page。

不要直接修改Blogger範本的理由

有了條件標記之後,其實也能夠直接針對Blogger範本內文進行變更。例如,在某些類型中不輸出導覽列的標記內容。

在大部分情況中,我並不建議這樣做。一來是管理上比較不方便。網頁的樣式與腳本都經過全網站與各別類型的檔案分開管理,這樣子可以明確地知道在什麼情境下會有什麼樣的效果。

二來,由於Blogger範本的條件標記不支援多重條件,因此當條件變多的時候,就得重複撰寫條件的內容。例如我將搜尋、標籤列表與存檔網頁都視為列表類型,但實際上他們的條件判斷都不一樣,因此內容都必須重複撰寫。上面的檔案引用語法中blogger-list.css跟blogger-list.js寫了兩次,就是這個意思。

不過,如果是判斷文章內文是否顯示的時候,我會建議你直接寫在Blogger範本中。

各種類型中的文章內文顯示

預設的Blogger範本中,顯示文章內文的語法通常會這樣表示:

<div class='post-body entry-content'>
    <data:post.body/>
</div>

其中,post.body會包含文章全部的內文。像我Blog每篇往往動輒二、三千字,一個列表網頁展示個十篇文章左右的數量,文章內文就會大到難以下載。

因此我建議只有在首頁、文章跟靜態網頁這三種網頁類型的時候才顯示文章內文。語法如下:

<div class='post-body entry-content'>
    <b:if cond='data:blog.url == data:blog.homepageUrl'>
        <data:post.body/>
    </b:if>
    <b:if cond='data:blog.pageType == &quot;item&quot;'>
        <data:post.body/>
    </b:if>
    <b:if cond='data:blog.pageType == &quot;static_page&quot;'>
        <data:post.body/>
    </b:if>
</div>

在上面各種網頁類型需求中,我對首頁的需求是想要文章內文只顯示摘要。如果你有用Blogger預設的語法來插入繼續閱讀的摘要點,那麼這樣子就足以讓他在首頁時只顯示摘要。如果你跟我一樣,不習慣每次寫文章都還要手動設定摘要點,那麼你可以用我之前寫的JavaScript自動摘要程式看看。


結語

經過這一串的研究與改造之後,希望「布丁布丁吃什麼?」blog能帶給大家更好的瀏覽體驗。不過與其說是對大家好,不如說是對我自己的需求來改善。因為我常常需要回頭找blog寫過的東西,但是以前沒有針對網頁類型判斷的時候,過大的網頁內文量總是會讓網頁讀到快當掉,令我非常難過。現在讀取速度應該是比較好了吧?不知道大家的看法如何呢?

歡迎一起討論Blogger Hack的技巧吧!

(more...)

輔大動漫社OPED點播單

布丁布丁吃布丁

輔大動漫社OPED點播單

clip_image002

  • 題名:輔大動漫社OPED點播單
  • 建置時間:2006~2007,2010

我大學時參加了輔大動漫社,並在動漫社中開啟了「動漫OPED放映會」的活動。活動內容很簡單,大家點播想要播放的影片片段(甚至是自己做),然後湊齊這些影片,最後借個演講廳來播放。我已經畢業將近四年,但是這個活動仍然大受學弟妹好評,而且像是傳統一樣地延續了下去,讓我感到非常地意外。

OPED點播單是一個資料庫系統,社員可以利用瀏覽器在網頁上進行點播,而活動負責人則可以在點播單上篩選出最終選入的歌單。程式語言是PHP,資料庫是MySQL,這是一個很小型,但對活動進行有相當大幫助的一個系統。

以往活動中都是由我個人來維護OPED點播單,後來我將之以InnoSetup包裝成安裝檔,供學弟妹輕易地在Windows系統上建立OPED點播單的伺服器。

儘管只是一個小型的系統開發,而且不是正式的應用,不過也算是我個人的作品之一,所以在此留下記錄。


前言

本系統是由輔漫老人布丁開發,為了協助動漫OPED放映會進行而做的點播系統(以下簡稱OPED點播單)。OPED點播單本身以PHP與MySQL架構而成,我把他跟XAMPP與AMPstart結合成可直接執行的網頁伺服器版本,方便對於伺服器設定不熟的學弟妹們,也能夠自行動漫OPED點播單。

安裝需求

網路環境

以下兩者其中之一即可擔任伺服器

  1. 固定IP
  2. ADSL
硬體需求
  • 平時電腦即是長時間開機,就當作是P2P附加的流量吧。流量應該1天不到10KB。
  • Windows 2000/XP (Windows 7上測試起來好像會有點問題)
  • Pentium III以上等級
  • 所需硬碟空間約300MB

OPED點播單下載與安裝

  1. 下載OPED點播單(大約50MB) 
  2. 請直接執行OPEDrequestSetup.exe,開啟安裝程式

OPED點播單安裝精靈

clip_image002[4]

安裝精靈操作很簡單,一直按下一步也可以完成。就不用多說了。

clip_image004

安裝完成之後,安裝精靈會提示您「安裝DynDNS Updater」,請打勾並且完成安裝。

安裝動態網址DynDNS Updater

動態網址讓我們可以用fjuacg.servebbs.org來連到你的電腦,而不用在意你到底是固定IP還是ADSL的浮動IP。

clip_image002[8]

如果你沒有在安裝精靈中安裝DynDNS Updater,你也可以點選「開始/程式集/輔大動漫OPED點播單/安裝DynDNS Updater」來安裝

開始安裝之後的操作介紹如下:

clip_image004[6]

clip_image006[4]

image

使用預設設定即可。

image

Intstall the DynDNS Updater as a Windows Service請留空白。

image

Destination Folder是安裝的資料夾,你可以自行決定安裝位置。

image

Run DynDNS Updater請打勾。然後系統會開啟DynDNS Updater。

image

填入帳號與密碼。

image

以後開機的時候就會自動啟動DynDNS Updater。

設定防火牆

OPED點播單使用50080連接埠來提供服務,以避免跟原本電腦正常服務使用的連接埠相衝突。為了避免被防火牆阻擋,我們要手動開啟50080連接埠。如果最後無法開啟OPED點播單網頁,那麼大多數問題都是被防火牆阻擋了。

防火牆大致上分成軟體跟硬體兩種,除非你電腦網路是透過硬體的無線基地台(或是其他router),那麼一般情況下都只要設定軟體防火牆即可。

因為軟體防火牆眾多,難以確定實際上你安裝的是哪一種,故以下只說明講解Windows防火牆(Windows XP版本)跟卡巴斯基Kaspersky Anti-Virus 6.0防火牆的設定方式,如果是其他情況的話可以再來問我。

Windows防火牆開啟50080連接埠
  1. 開啟「控制台」中的「Windows防火牆」
    clip_image002[10] 
  2. 進入「例外」(因為我預設沒開啟Windows防火牆,所以下面的訊息應該跟你的不太一樣)
    clip_image004[8] 
  3. 點下「新增連接埠」
    clip_image006[6] 
  4. 請填入「名稱: OPEDrequest」、「連接埠編號: 50080」、選擇「TCP」,如下:image 
  5. 在「程式和服務」裡面找到「OPEDrequest」已經新增且勾選,按下「確定」之後,就完成設定防火牆的工作。
    clip_image012[4]

卡巴斯基Kaspersky Anti-Virus 6.0開啟50080連接埠
  1. 開啟卡巴斯基主程式,點選「駭客防護」
    clip_image002[12] 
  2. 點選「防火牆」
    clip_image004[10] 
  3. 進入「設定」
    clip_image006[8]
  4. 進入「封包篩選規則」
    clip_image008 
  5. 按下右邊的「加入」
    clip_image010[4] 
  6. 規則名稱輸入「OPEDrequest」,勾選「本機連接埠」,再點下「輸入連接埠」
    clip_image012[6] 
  7. 輸入「50080」,按下「確定」。
    clip_image014 
  8. 按下「確定」
    clip_image016 
  9. 確定清單當中出現了「OPEDrequest」之後,切換到「區域」
    clip_image018[4] 
  10. 隱形模式必須全部都取消打勾,然後按下「確定」
    clip_image020 
  11. 按下「確定」,設定完畢
    clip_image022[4]

啟動OPED點播單

  1. 請點「開始/程式集/輔大動漫OPED點播單/啟動OPED點播單」
    image 
  2. 程式會自動幫你開啟伺服器,請等候30秒,網頁會自動開啟。
    clip_image002[14]
  3. 網址分成兩種:
    1. 內部測試用網址:http://localhost:50080
      只有你自己能夠連線,其他人無法開啟。僅供測試用。備份/還原資料時,必須使用此網址。
    2. 正式網址:(問問輔漫幹部要不要開放吧,畢竟這算是私下的小活動)
      如果要其他人能看到你的網頁,必須用此網址。
      使用此網址必須啟動DynDNS Updater,並照上述步驟完成設定即可。
  4. 如果看到以下網頁,表示你開啟成功了。
    clip_image004[12]
  5. 但如果看到以下網頁,則表示你開啟失敗。可能是防火牆沒有設定的緣故,請聯絡布丁處理這個狀況。
    clip_image006[10]

關閉OPED點播單

clip_image002

請在Windows工具列右下角工具圖示當中,找到AMPstart的圖示。並按下右鍵,選擇「Exit」,則能夠關閉OPED點播單。

新增OPED點播單代次

確定OPED點播單可以開啟之後,我們才可以進一步地去設定OPED點播單的內容。本說明僅教你如何新增OPED點播單代次,其餘的新增、修改功能,就讓你自行去摸囉。

  1. 進入OPED點播單左下角的「管理介面」
    clip_image002[16]
  2. 點選「選擇使用」下拉選單,選擇「新增」
    clip_image004[14]
  3. 請把右欄資資料都填寫完畢,如果不會填沒關係,稍候可以再來修改。然後按下「新增」,系統會跳出確認視窗,按下「確定」。
    clip_image006[12]
  4. 按下「確定」
    clip_image008[4]
  5. 輸入預設密碼:fjuacg
    clip_image010[6]
  6. 新增完畢,點選標題回到首頁。
    image
     
  7. 這樣就是新一代的OPED點播單囉。
    clip_image015[4]

備份/還原OPED點播單

clip_image002[18]

請先啟動OPED點播單之後,再來開啟「開始 > 所有程式 > 輔大動漫OPED點播單 >備份還原OPED點播單」。

clip_image004[16]

隨後會開啟此網頁,網址固定是「http://localhost:50080/export.php」。您可以在

此下載備份檔案,或是上傳檔案以進行還原的動作。還原時會把原本的資料全部刪掉、再新增還原的資料,所以以前的資料會毀掉,請務必注意。

必須注意的是,備份/還原資料的功能,只有安裝電腦本機端才能執行喔。 (more...)

軟體構築美學──棕地應用程式開發指導原則讀後感想

布丁布丁吃布丁

軟體構築美學──棕地應用程式開發指導原則讀後感想

image

Kyle Baley、Donald Belcham 著;蔡煥麟、張簡才祿 譯

  • 出版商:悅知
  • 出版日期:2010-10-20
  • 台幣定價:$650
  • 語言:繁體中文
  • 頁數:528
  • ISBN:9866348784
  • EAN:9789866348785
  • 網路書店連結:天瓏網路書店博客來書籍館

某天我在逛天瓏書局時意外翻到這本書,書中提及了許多我以前就很在意的議題,包括程式開發的環境配置、如何將物件導向設計模式應用到開發中。更重要的是,該書要改善的「棕地應用程式」正是適用我多次開發的真實情境。讀完之後,我發現這本書給我了比預期還要多的啟發,所以我想藉此機會寫一下這本書的讀後感想,也作為整理自己所學的一個記錄。


標題與內容

這本書內文翻譯得很好,但就是「軟體構築美學」這個標題下的太差,很難一眼就搞懂作者到底要講什麼,而且也很容易跟其他書籍混淆。

原文的標題是「Brownfield Application Development in .NET」,直譯就是「棕地應用程式──以.NET語言開發」,但就市場銷售來說,這個讓人無法直覺理解的標題也不太妥當。

先不論標題如何,該書就是環繞在「棕地應用程式」的改善上。作者是以.NET程式語言開發,時常使用Visual Studio IDE來講解,主要是開發安裝在桌面端的應用程式。但是作者是以概念性、原則性的方式說明各種作法的含意,因此很容易讓人將作者的開發技巧轉換到其他的程式語言上。

該書分成兩大部分,Part I說明開發環境,而Part II才是開始寫程式碼。以下我稍微摘要各章的內容,並加入我自己的感想,讓大家對該書有一些基本的認識。


認識棕地應用程式

建築領域所說的棕地(brownfield),指的是一塊年久失修的荒蕪建地,如果將這塊地好好整理一番,則仍可能恢復其利用價值。對軟體開發人員來說,棕地應用程式(brownfield application)指的是一個既有的軟體專案,此專案可能因為用了一些不好的開發方式、結構、或設計而產生許多問題,但若經過仔細的整理和重構(refactoring),還是有機會起死回生。

以上引用自該書的第一章的第一段。簡單來說,接手別人寫好的程式碼繼續改進,通常就是一個棕地應用程式。作者更進一步地說明棕地應用程式的三個基本元素:

  1. 既有程式碼:棕地應用程式中會有大量的、已經寫好的程式碼。但是可能因為有些問題,而現在有一群專屬的團隊在積極地想改善它。
  2. 差勁實務作法所造成的汙染:程式中到處都是看起來不太正常的技術債(technical debt)需要改善,大多都是前人一再拖延而沒有寫好的地方。
  3. 仍有改善或重複使用的潛力:儘管有些問題,但棕地應用程式仍有改善的價值。反之,如果該應用程式已經沒有救了,那麼它會被歸類到「老舊系統」。

我翻到這邊的時候就深深地被這本書吸引住。我認為現在進行開發工作很少是重新開始(該書稱之為綠地應用程式),而是幾乎都是接手別人的程式碼,再來開發自己想要的功能,例如我以開放原始碼系統DSpace為基礎進行開發就是如此。嚴格說來,這並不符合上述的棕地應用程式定義,因為DSpace寫作架構良好,並沒有明顯的污染問題,至多只是應用功能無法達到專案的需求,因此仍需要開發。

大多數時候,公司的工程師要接手其他工程師開發的系統,實驗室的學弟要接手學長的論文系統,而這些以少數人力開發出來的程式,都是屬於典型的棕地應用程式。我們遇到棕地應用程式仍然是比重新開發還來得多,不可忽視棕地應用程式的問題。


Part I 開發環境

Part I 開發環境包括以下五章:

  • 第02章:棕地專案的版本控制
  • 第03章:持續整合
  • 第04章:自動化測試
  • 第05章:軟體度量與程式碼分析
  • 第06章:瑕疵管理

對我來說很意外的是,在我過去的認知中,講到開發環境,要嘛就是指非常針對特定程式語言的IDE、要嘛就是專案開發的方法論,但是該書講的卻是更實在的開發環境──能夠建立信心的開發環境。

程式開發的環境

該書先探討許多棕地專案都會使用的版本控制系統VCS,指出傳統簽出、簽入式VCS與具備合併、分支、標籤功能的VCS應該如何使用。

持續整合則是一臺時常進行自動化編譯與測試的伺服器。它負責告訴專案成員現在在VCS中的程式碼是否可用。而持續整合用在不需要編譯的直譯式程式語言上的時候,我認為可以用自動化測試來取代編譯,這也是Ruby on Rails教我的想法。

自動化測試則是基於敏捷開發的概念,藉由單元測試與整合測試來為應用程式抓出臭蟲並改善。

以上三章提到的都是非常重要的議題,而比起單純的專案管理方法論,這些技巧更是能夠確實地提昇專案開發的穩定度,帶給工程師、專案經理、老闆與客戶們信心。讓程式開發就像是坐著一輛穩定的跑車,能夠讓駕駛安心地開往目的地的感覺。

開發以外的環境

更進一步的,作者導入軟體度量與程式碼分析的概念,講述我們常常聽到的「程式碼涵蓋範圍」、「循環複雜度」、「類別耦合度」等指標要如何用於專案開發。

然後作者在瑕疵管理中敘述如何面對瑕疵,以及「達到零瑕疵」原則對於提昇專案組織文化的價值所在,這讓我對於瑕疵報告系統又有了全新的認知。

作者不僅僅只是對程式開發的環境提出指導,也更關注在進行開發的工程師、專案經理遇到程式碼以外的實務問題。我並沒有太多與他人合作開發的經驗,但仍不難想像實際開發時專案成員對於瑕疵所帶來的恐懼以及會如何應對。

正視開發環境中會遇到的問題,並逐步著手改善。這是我認為該書相當有價值的原因之一。而且光是Part I 開發環境,就佔了這本書的一半。


Part II 程式碼

經過半本書漫長兩百多頁的開發環境,Part II總算要關注應用程式真正要處理的程式碼了。在Part II開發環境中包括以下六章:

  1. 第07章:在專案中導入好的物件導向實務
  2. 第08章:應用程式的重新分層
  3. 第09章:鬆散一些:降低程式碼的依賴性
  4. 第10章:重整使用者介面
  5. 第11章:重構資料存取
  6. 第12章:管理系統外部的依賴

在剩餘的兩百多頁中談論如此多的議題,該書並沒有辦法相當地深入探討,但是他卻比專門探討程式開發(例如設計模式)的書籍更加地實用──由於廣泛地討論各種議題,因此適合作為程式開發的入門、回顧、反省,並且更能將這些開發技巧應用在實務上會遇到的各種問題。

可測試的物件導向設計

一開始,作者像是老生常談一樣地帶我們複習了物件導向,而且是「適合實務的物件導向用法」──是的,因為太多人還是用程序導向在寫物件導向的程式。封裝、繼承、抽象化、多型等物件導向基本概念,你學過,我學過,龍五也學過,不過很多學校課程也只會教到這邊而已。

實務上到底應該怎麼用物件導向呢?作者給我們幾個好的程式應該有的能力指標:

  1. 可維護性:一切好的程式應該有的基礎出發點。
  2. 可讀性:讓程式碼容易讓人閱讀。接手棕地應用程式的工程師,閱讀程式碼的時間通常比動手寫的時間還要多。
  3. 可測試性(Designing for testability):程式能夠進行個別的、自動化的測試。
  4. 擴充性:讓程式碼在需要加入新行為時很容易修改。
  5. 可逆性(reversibility):任何設計決策都能輕易回復。
  6. 調適性(adaptability):確保變動發生時不用大幅度重寫。

就如許多程設教科書會講的一樣,這些能力指標也是相當抽象的概念。作者介紹了各種好的程式的開發原則與作法,其中最實在的概念,大概就是「可測試性」了。儘管Part II廣泛地討論各種程式碼設計原則,但大多都會回到「想辦法讓程式可以被自動化測試」的概念上,我也覺得這是一個相當實際的指標。

以實務問題來應用設計模式

第8章討論應用程式的分層、第9章教讀者如何降低程式碼的依賴性,作者從實務中會遇到的問題著手,引進各種設計模式來解決這些問題。

許多以「設計模式」為主題的程設書籍,例如Head First的深入淺出設計模式,大多是為講解「設計模式」而講解,儘管深入淺出設計模式真的寫得很好,但看完之後還是會覺得不知道何時該應用於哪種實務問題上。

相反地,該書作者以設計模式來解決棕地應用程式會遇到的問題,這讓我對設計模式有了更具體的印象。作者不是很深入地探討設計模式,不同程式語言也會有不同的實作方式,不過這是一個很好的入門、索引。畢竟我們想要學的程設技巧,都是為了解決實務上會遇到的問題,而不是為了使用設計模式才想要去學設計模式。

儘管設計模式的作法沒有深入的探討,但是作者仍有具體的程式碼舉例。作者在降低程式碼的依賴性一章以電影評論的拼字檢查功能來舉例,帶著讀者一步一步將程式拆解,以依賴反轉原則(dependency inversion principle)設計介面(interface)、以依賴注入(dependency injection)降低耦合度,將一個隨處可見的棕地應用程式改成更為良好的程式。

不得不說令我汗顏的是,儘管我以為我自己已經對設計模式、正確的物件導向有所了解,但是在看該章一開始介紹的棕地應用程式時,我真的覺得自己也只會寫出那樣的程式碼,而且不知道其中的問題所在。當作者一一剖析之後,我才有如夢初醒的感覺,同時也深深地反省著自己的功力不足。

使用者介面、資料存取與系統外部依賴的重構

許多程設的教科書只會講到上面的原則性理論,而本書還深入使用者介面、資料存取的設計、以及使用系統外部依賴的議題。

這邊談到的使用者介面並不是介面設計,而是在討論很多工程師都會把商業邏輯(business logic)跟介面寫在一起的問題。這在開發AJAX應用的時候更是讓人苦惱不已,某些邏輯判斷到底要寫在JavaScript,還是寫在後端程式中呢?作者帶我們認識了MVP(Model-View-Presenter)與MVC(Model-View-Controller)設計模式,而在會保留使用者狀態的應用程式中,適合使用MVP,例如桌面端應用程式;傳統的網頁程式則是普遍採用MVC。依此推論,我想以保留使用者狀態為賣點的AJAX應用,應該也可以用MVP的方式來設計也說不定。

在第11章資料存取主要討論DAL(Data Access Layer,也就是MVC中的Model),作者斥責預存程序(store procedure,在資料庫中寫的邏輯判斷程式),建議商業邏輯應該集中,並且大力讚賞ORM(Object-relational mapping)。說真的,我在用過Active Record這種ORM的設計之後,也認為作者的建議實在是中肯到不行。

外部系統依賴的問題中,作者建議以Proxy、Adapter、Facade等設計模式來設計「反腐敗層」(anti-corruption layer)。而如果將目光縮小到專案內部,內部依賴也是可以利用作者介紹的各種設計模式,將內部元件區隔、模組化,以降低內部依賴的程度。

以上三個議題都比前面的原則性指導更貼近實務操作,作者將各種會遇到的問題一一剖析、探討,讓程設從實務需要的技巧更深入到程式的品質。隨著實例看作者是如何改善、為何要如此修改的敘述,讓人獲益良多。


持續改善

該書的最後一章並不談程式,而是回歸到了整體開發環境中的基礎點:要如何持續改善。

該書談論了各種程設的技巧與作法,儘管有其價值所在,但每個看過的人應該都會先在心中打退堂鼓,抱著:「我真的做得到嗎?」「這種作法真的能持續下去嗎?」的疑惑。至少我就是這樣覺得。

在最後一章中,作者點出這個困惑。作者並不是要實務上立刻大轉彎,馬上加入版本控制、持續整合、自動化測試等機制,而是建議讀者緩慢、持續漸進地改善。

我們需要清楚的知道目標,並計劃如何達成目標,而不是「我們想要讓事情變得更好,所以就加入單元測試」,而是要不斷的推動這個專案,讓最後的結果可以盡量貼近我們當初的期望。

然後最後這一章舉了相當多案例,帶讀者回顧整本書的內容要如何應用在實務上。即使是像作者如此老練的高手,還是不斷在專案中碰得一鼻子灰。但是堅持品質、持續改善,儘管專案成果可能很難讓客戶理解這之中的好處,但是團隊的成員素質卻會因此逐步提昇、變得更有自信。

看完持續改善一章之後,我就決定在把這本書還回圖書館之後,自己再去買一本擺入書櫃中。當我要開始開發專案時,或是專案碰到困惑時,我想這本書都可以給我一些實務上的建議。更重要的是,我希望藉由該書在書櫃上的身影,讓我不會忘記對於持續改善的堅持。


優良的翻譯

最近讀了一些歐萊禮的程式書籍,說實在的,歐萊禮的翻譯通常都會讓人覺得奇怪。有些是翻譯的不錯,不過像是這種比較實務性質、又牽扯到各式各樣議題的書本,我通常會有需要腦內自動校正翻譯的覺悟。

但是這本悅知文化出的書中,譯者蔡煥麟跟張簡才祿翻譯得真的很好,我幾乎找不到看起來很奇怪的地方,閱讀起來毫無阻礙。不僅是程設專案的專有名詞翻譯貼切(時常標註英文原名也是相當的貼心),而且作者幽默的笑點也能讓人看了會心一笑。這讓該書中的眾多案例讀起來像是小說般的有趣。

以下節錄一段讓我印象最深刻的敘述,這是持續整合中的假設案例:

現在,假設我們開發流程已經有自動化建置程序,只要有人簽入程式碼,就會自動執行下列動作:
  1. 取得最新版程式碼
  2. 編譯程式碼
  3. 透過電子郵件通知團隊這次的建置結果
所以當傑克森簽入程式碼的時候,我們的自動化建置程序便會啟動,並且立即在第2個步驟(編譯程式碼)出現錯誤。然後團隊成員會收到電子郵件,知道程式碼出了問題了。我們甚至設定讓電子郵件的內文顯示最後一次簽入的人是誰,在這個例子當中就是傑克森。
現在得用上我們的建置失敗規則了。麥可、小劉、阿輝,以及其他團隊成員都停下了手邊的開發工作,也許去喝杯咖啡什麼的。他們多半會閒晃到傑克森的辦公室隔間開點小玩笑,也許送他一個小東西,例如,一隻象徵失敗的橡皮鴨,暗示他就是最近造成一次建置失敗的人。傑克森看見一群人朝他這邊走來,嗅到了嘲弄的味道,才想起自己之前忘記取出新版的程式碼,於是趕緊做了必要的修補。在第一個人踏入他的隔間之前,他就已經迅速修正完畢,並執行了正確的簽入程序。現在,他可以心安理得地等待命運之神的降臨。

感想

在寫論文的時候,我嘗試自己進行開發一個綠地應用程式專案。儘管在開發之前我讀了一些程設的書,學習設計模式與專案管理,但是開發過程中,我還是得承認自己真的禁不住「走捷徑」的誘惑,在程式碼中留下了一堆技術債,而我只能絕望地看著程式碼向下沈淪。現在,他已經是個標準的棕地應用程式,雖然我不知道未來會不會有人繼續想要改善它就是。

反省當初,儘管我在論文中找來了PHP與JavaScript的IDE,但是並沒有建構起良好的開發環境。Dropbox雖然有版本控制,但跟VCS比起來還是有很大的差距。自動化測試只有Model可以測,我到最近才知道,原來JavaScript跟前端UI也有一套自動化測試的方法。更進一步的,應該要搭配程式碼覆蓋率與持續整合的作法,鞏固程式碼的穩定性。

雖然我希望將我的論文系統以開放原始碼的形式發佈,但他真的很不像是外面人家做的開放原始碼專案。下次起頭時,我還是應該好好看看別人是怎麼做的,以開放原始碼專案的形式好好地開始規劃。

最後,看完這本書之後,我有一種想要再執行一個開發專案的衝動。我想實作這本書中介紹的各種方法,只要我熟悉這些方法,我就能夠帶給團隊其他的工程師,提高大家的程設能力。

題外話,雖然該書是以.NET來撰寫,但書中多次提到Ruby on Rails框架在自動化測試上的好處,而我也很想做做看大家認為能夠快速開發的Ruby on Rails,到底會是多麼快速。

還有好多想要做,還有好多想要學的事情呢。


結語

「軟體構築美學」一書,並不是專案管理的書。專案管理是成功專案的一部分,但是專案管理看再多,程設的等級還是很難進步,開發系統依然像是高空走吊橋,令人驚心膽跳。

現實環境中,很多開發環境都會讓人難以確實地提昇程設技巧。實務上的問題會變成以交際手段來搓過,工程師的能力還是很難提昇。我們這邊的開發團隊,儘管老師給了我們很大的自由,但我覺得在程設開發的能力上還是不夠。在我們這樣有自由、有時間的環境都是如此,在外面中小企業中,沒有經驗的程設顧問、老闆也不見得願意提供資源、而其他工程師也不一定想要配合,這些情況都很容易想像的出來。

即使如此,我還是想要進行更好的程式開發,用有效率、穩定的方式開發出高品質的系統。現在的我,並沒有能力帶領大家。不過,我想還是可以先將「軟體構築美學」作為目標,一步一步地踏實前進,想辦法增進自己的實力吧。

(more...)