2008-07-24

結合發表文章的日曆模組 (Feed Calendar)

一年多前參考國外某些人作法修改,做了一個 Blogger 用的整合日曆模組,針對發表過的文章,其實 Blogger 有內建一個網頁元素叫做「網誌存檔 Archive」,先將其屬性改成每日存檔,系統就會針對每天的文章做出一個靜態的頁面連結,然後用 Javascript 做出日曆、判斷哪些天有文章、把日前和連結整合在一起,再利用 Javascript 做出一些效果,達到還能在不同月份切換。比起當時很多人用外掛、其他網路服務幫忙,這個方法相對先進多了,速度又快,剛開始我還蠻滿意的,不過其中還是有不少差強人意的地方。首先,它利用了 Blogger 內建的 Daily Archive,所以設定和修改比較麻煩,而且系統限制只能用一個 Archive,所以有了日曆之後功能排擠,沒辦法再有其他網誌存檔的功能。第二個問題是原理,原來的辦法要求抓出「所有」的當日文章、產生靜態連結網址,然後再一個個塞進日曆中,雖然讓做出來的日曆在月份切換時反應很快,但看原始碼會看到所有文章的存檔連結,可想而知一旦時間一長文章量爆多,幾百幾千篇的文章都產生靜態連結放在原始檔裡,那應該很恐怖(所以我另一個 Blog 不敢用這個日曆)。最後,用 Daily Archive 做出來的日曆還有個討厭的問題,即使當天只有單篇文章,它不能直接顯示該文章的標題或永久連結,還是只能幫你連結到 Daily Archive 的頁面,等於要多花一個步驟多點一次連結才能看到日曆上當日的全文,感覺還挺不直覺的。結合以上要求,最好是日曆只即時抓要顯示的當月文章連結、與「網誌存檔」拖勾、日曆上還可以顯示該文章的永久連結和標題(當然如果一天有兩篇以上文章,問題也要解決),還能兼顧效能和美觀,這樣才是個功能性好的日曆。

後來經網友 LVCHEN 的提醒、看到了他做的「日曆文章列表」,哇!原來 Feed 裡新多加能指定時間範圍的參數啊!結合 JSON 的技巧,不就可以輕易地取得特定某個月的當月文章嗎?有了新的資料來源,修改之前的日曆程式,不就可以解決以上的所有問題嗎?知道之後整個寫程式的熱血又來了,花了一兩天測試修改,終於做出這個新的日曆模組。整段程式碼不到 5K(比前個版本更小)、如果搭配前篇文章提到壓縮 Javascript 的技巧,還可以壓到 3K 以下!和之前以網誌存檔當資料來源做出來的 Archive Calendar 原理不同,這次是利用 Feed 來搞,所以就叫它 Feed Calendar 囉!如果你用過之前的程式,記得先手動移除修改的部份、備份完整的原始樣板,然後再開始下面的動作。(修改程式碼還是需要一點程度,也有一定風險,如果你是什麼都不懂的使用者,請直接參考 LVCHEN 的安裝外掛,比較簡單風險又低,效果也差不多~)

第一個步驟,先塞入這個日曆外觀顏色的 CSS 樣式定義。如果你用過之前日曆,那就不用改啦,因為我用了一樣的定義,打開版面配置、修改樣板原始碼的 HTML,放在 <head> 標籤內、定義 CSS 樣版的區段裡:

/* Feed Calendar Styles */
#Calendar {
  margin: 0px;
}
#Calendar .act {
  color: #fff;
  padding: 4px;
}
#CalendarTable table {
  border-collapse: collapse;
  padding: 0px;
  border: 0px;
}
#CalendarTable table th {
  padding: 1px;
  color: #777;
  margin: 0;
}
#CalendarTable table td {
  height: 25px;
  color: #999;
  text-align: center;
  padding: 1px;
  margin: 0;
}
#CalendarTable table td a {
  display: block;
}
#CalendarTable .Today {
  color: #fff;
  background: #777;
}
#CalendarTable .Today a {
  color: #fff;
}
#CalendarTable .Weekend {
  color: #997777;
}

如果對顏色、字型大小、靠左靠右有特別需求的,請自行改上面的樣式。第二個步驟是重點,就是抓 Feed、產生日曆主要的 Javascript 程式碼,一樣是貼在 <head> 標籤後面,如果你之前也有 Hack 放過 Javascript,放在一起就好:

<script type='text/javascript'>
//<![CDATA[
<!-- Script functions for generating Feed Calendar: generateCalendar(), collectPost(), BrowsePrev(), BrowseNext(),  BackToday() -->
var baseURL = '';
var currentDay = new Date();
var today = new Date();
var monthLabels = new Array('01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12');
var monthDays = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
var weekLabels = new Array('一','二','三','四','五','六','日');

function generateCalendar(){
  var thisYear = currentDay.getFullYear();
  var thisMonth = monthLabels[currentDay.getMonth()];
  var thisDay = today.getDate();
  var nDays = monthDays[currentDay.getMonth()];
  if (currentDay.getMonth() == 1 &&(((thisYear % 4 == 0) && (thisYear % 100 != 0)) || (thisYear % 400 == 0)))
    nDays = 29;
  var IsitNow = currentDay;
  IsitNow.setDate(1);
  var startDay = IsitNow.getDay() - 1;
  if (startDay < 0)
    startDay = 6;
  var sCalendarCode = '<table><tr>';
  for (var index=0;index<7;index++)
    sCalendarCode+='<th style="width:25px;">'+ weekLabels[index]+'</th>';
  sCalendarCode+='</tr>';
  var nTableCol=0;
  for (index=0;index<startDay;index++) {
    if (nTableCol == 0)
      sCalendarCode += '<tr>';
    sCalendarCode+='<td>&nbsp;</td>';
    nTableCol++;
  }
  for (index=1;index<=nDays;index++) {
    if (nTableCol==0)
      sCalendarCode+='<tr>';
    if (index==thisDay && today.getMonth()==currentDay.getMonth() && today.getFullYear()==currentDay.getFullYear())
      sCalendarCode+='<td id="Day'+index+'" class="Today">';
    else {
      if (nTableCol < 5)
        sCalendarCode+='<td id="Day'+index+'">';
      else
        sCalendarCode+='<td id="Day'+index+'" class="Weekend">';
    }
    sCalendarCode+=index;
    sCalendarCode+='</td>';

    if (nTableCol==6) {
      sCalendarCode+='</tr>';
      nTableCol=0;
    }
    else
      nTableCol++;
  }
  if (nTableCol>0) {
    for (index=0;index<(7-nTableCol);index++) {
      sCalendarCode+='<td>&nbsp;</td>';
    }
    sCalendarCode+='</tr>';
  }
  sCalendarCode+='</table>';
  document.getElementById('CalendarTable').innerHTML = sCalendarCode;

  var sFeedURL = baseURL + '/feeds/posts/summary?orderby=published&published-min='+thisYear+'-'+thisMonth+'-01T00:00:00&published-max='+thisYear+'-'+thisMonth+'-31T23:59:59&max-results=50&alt=json-in-script&callback=collectPost';
  var script = document.createElement('script');
  document.getElementById('CalendarCaption').innerHTML = '<span class="loading">Loading <blink>...</blink></span>';
  script.setAttribute('src', sFeedURL);
  script.setAttribute('type', 'text/javascript');
  document.documentElement.firstChild.appendChild(script); 
}

function collectPost(json) {
  document.getElementById('CalendarCaption').innerHTML = currentDay.getFullYear()+'-'+monthLabels[currentDay.getMonth()];
  var entries = json.feed.entry;
  if (entries == undefined)
   return;
  var nDay = 0, nCount = 0, nActual = 0;
  var posts = new Array();
  for (var i = 0, post; post = entries[i]; i++) {
    nDay = parseInt(post.published.$t.substr(8,2),10);
    if (i>0&&nDay==parseInt(entries[i-1].published.$t.substr(8,2),10)) {
      var actualDay = post.published.$t.substr(0,10);
      var actualTimezone = post.published.$t.substr(23,6);;
      posts[nActual-1][1] = posts[nActual-1][1]+', '+post.title.$t;
      posts[nActual-1][2] = baseURL +'/search?updated-min='+actualDay+'T00%3A00%3A00'+encodeURIComponent(actualTimezone)+'&updated-max='+actualDay+'T23%3A59%3A59'+encodeURIComponent(actualTimezone);
    } else {
      posts[nActual] = new Array(3);
      posts[nActual][0] = nDay;
      posts[nActual][1] = post.title.$t;
      var j = 0;
      while (j < post.link.length && post.link[j].rel != "alternate")
        j++;
      posts[nActual][2] = post.link[j].href;
      nActual++;
    }
  }   
  for (i=0;i<nActual;i++) {
    posts[i][1] = posts[i][1].replace('\"', '&#34').replace('\'', '&#39');
    document.getElementById('Day'+posts[i][0]).innerHTML = '<a title="'+posts[i][1]+'" href="'+posts[i][2]+'" target="blank_">'+posts[i][0]+'</a>';
  }
}

function BrowsePrev() {
  var thisMonth = currentDay.getMonth()-1;
  var thisYear = currentDay.getFullYear();
  if (thisMonth<0) {
    thisMonth = 11;
    thisYear = thisYear-1;
  }
  thisMonth = monthLabels[thisMonth];
  currentDay = new Date(thisYear+'/'+thisMonth+'/1 00:01');
  generateCalendar();
}

function BrowseNext() {
  var thisMonth = currentDay.getMonth()+1;
  var thisYear = currentDay.getFullYear();
  if (thisMonth>11) {
    thisMonth = 0;
    thisYear = thisYear+1;
  }
  thisMonth = monthLabels[thisMonth];
  currentDay = new Date(thisYear+'/'+thisMonth+'/1 00:01');
  generateCalendar();
}

function BackToday() {
  currentDay = new Date();
  generateCalendar();
}
//]]>
</script>

好啦!樣板原始碼的修改到此搞定存檔,接下來安排這個新日曆模組的位置。換到「網頁元素」的設定,在你想塞入日曆的地方新增一個網頁元素、選擇 HTML/JavaScript 類型。接下來給個標題,然後貼入以下的 HTML 程式:

<center>
  <table border="0" id="Calendar" cellpadding="0" cellspacing="0">
    <caption>
      <a href="#" onclick="javascript:BrowsePrev();return false;" title="Previous Month">&lt;&lt;</a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <a href="#" onclick="javascript:BackToday();return false;" title="Back to Today"> <span id="CalendarCaption"> </span></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
      <a href="#" onclick="javascript:BrowseNext();return false;" title="Next Month">&gt;&gt;</a>
    </caption>
    <tr>
      <td id="CalendarTable" class="act"> </td>
    </tr>
  </table>
  <script type="text/javascript">
    generateCalendar();
  </script>
</center>

儲存搞定!接下來就可以檢查新的日曆模組能不能動囉~當然,根據以上的原理來說,這個新的日曆模組應該沒啥問題(我自己所有的 Blog 也都裝來用啦),不過如同大部份透過 Feed 抓文章的 Hack 一樣,由於要仰賴 Feed 即時連線抓取資料來源,這點會受到網路影響,所以快速猛點連結時切換的反應會比較慢(這是 Feed 的原罪,沒辦法),所以如果日曆有出現、但該日期沒有文章連結,請先檢查 Feed 的內容是不是正常,如果貼完程式發現怎麼只有兩個箭頭,那就是程式貼錯地方或貼漏了,請自己再仔細檢查一下。如果想要調整日曆裡的 Layout 這類「客製化」的問題(像是一週第一天是禮拜天、週末的顏色或標題調整),請先檢查之前那篇文章的回應斟酌修改(如果看不懂、那還是不要以身試法啦)。

BTW,根據前一篇文章提到的瘦身法,步驟二中的那一狗票 Javascript 用 Compresser 一整個壓縮後再塞回去(建議就壓 //<![CDATA[ 和 //]]> 這兩行中間的那些程式碼,處理完再貼回原來的位置),可以讓程式碼更小更節省讀取的時間,缺點當然是變得不可讀、修改不易,所以建議原始 Javascript 套用過沒問題後,再去進行壓縮瘦身。

回應: 53

 

2008-07-16

優化網站內的 Javascript

時代不一樣了,老早在寫「網頁程式」的時候,瀏覽器端只要 Parse 單純的 HTML(或下載顯示圖片),資料的處理,都由瀏覽器透過 POST/GET 方法和伺服器發出需求,伺服器處理完畢做完回應給瀏覽器才算完成。現在的網站服務大量套用 AJAX 和 Javascript,很多處理在 Client(瀏覽器)端就能先做,就算丟給伺服器處理也可以「非同步」完成,帶來的好處除了伺服器的負擔變輕、瀏覽器這邊也能夠有更快的反應速度(也可以搞一些畫面效果)。但也因為越來越多的 Javascript 被使用,即使是非同步在執行,應該也會發現處理的 Loading 被分擔到使用者端的瀏覽器上,這也是為什麼連到所謂「Web 2.0」相關的服務和網站時,查看自己的 CPU Loading,常常導致滿載的程式都是瀏覽器,使用效能差一點的電腦,連這類網站常常 Lag 到不行。我想每個架站、寫 Blog 或提供網路服務的人,應該都希望使用者或讀者在連線瀏覽時,不會有「連上你的網站要等很久」的感覺,除了網路頻寬外,如何優化網站程式碼也是一個重要的課題。除了增加伺服器端的處理效率(調整硬體或資料庫),交付給使用者執行的 Javascript 程式,或許也是增加效能的關鍵。

檢視使用的程式碼(Code Review)

我想看我 Blog、參考那些 Blogger Hack 的人會發現,首先,我會避免用外部 Include 進樣板範本的 Javascript 檔(像這類語法:<script type="text/javascript" src="xxx.js"></script>),因為一旦你外部引用的檔案掛了,等同於瀏覽器不能執行該 Javascript 的相關功能,利用該 Javascript 去實現操作介面或網頁模組全不能動,那連帶的衝擊有多大可想而知(當然,如果你網頁和引用的 Javascript 可以放在同一台機器上,同生死共患難,那反倒建議儘量用引用的比較好管理)。其次,我很多 Hack 其實也是參考人家的,但,我會「取其精華」、只保留該功能用得到的變數和函式,這樣做主要是簡化程式碼、減少載入時間,另一方面也讓自己能掌握每個函式的到底在幹嘛,能夠進一步優化和改寫(尤其很多特定功能的 Javascript,裡面殘留作者「懶得刪除」的無用程式碼,自己看了很礙眼)。不過由於 Script 的結構容忍度高(鬆散不嚴謹),不像要 Compile 的程式語言在 Build 的時候會做檢查,自己寫的時候也都只做到「能跑就好」,因此很多地方可能效率太差、沒測試到就會發生 Bug。後來看到 Will 的一篇文章:驗證你的 JavaScript 程式:JSLint,該文提到一個線上驗證 Javascript 程式的服務:JSLint,它會用嚴謹的標準去檢驗自己寫 Javascript,像是短少分號啦、變數沒有宣告之類的,小地方可以糾正自己不良的習慣,進而減少 Javascript 在客戶端出錯的機會,認為寫程式是一門「藝術」的人一定要參考看看。

程式壓縮瘦身

如果這個外掛的 .js 是自己能掌握的(就是放在自己的空間),或許可以先「壓縮」,經過「瘦身」以後放上網路,這樣檔案會變小,使用者下載讀取的速度也會變快。參考了這篇文章:「上線前用 JSMin 壓縮你的 JavaScript 檔案」中提到的 JSMin,還有線上的壓縮工具 Packer,以及很多 Javascript Library 愛用的 Javascript Compressor,這些都可以讓你自己外掛或自行撰寫的 Javascript 達到壓縮的效果(減少不必要的字元),檔案變小、程式碼變少自然載入就會變快,這樣也是一種優化的好辦法。

找出瓶頸、調整位置延緩執行

好吧,對不會改寫 Javascript、或迫不得已一定要引用別的外部 .js 檔的人來說,難道沒有優化的辦法嗎?有!在重灌狂人這篇文章提到,有個 Firefox 用的套件:Firebug 可以幫網頁測速度,抓出拖慢網站的元兇,如果自己網站用了一堆外掛 Widget,這是一個可以找出拖累網站瀏覽速度元兇的好辦法。那,找到元兇該怎麼辦呢?最簡單的方法就是不要用了(..XD),要不,就是把該功能儘量移到程式碼下方或後面,至少前面的部份能夠先順利顯示出來(AJAX 的好處)。那,如果該段程式碼就是要在前面、不能改動位置,還有一招,可以在 JavaScript 的宣告標籤裡加上 defer 屬性(一樣是從 Will 的文章,這篇:「不要讓 JavaScript 拉長你網站的反應時間 」學來的),例如:

<script type="text/javascript" src="xxx.js" defer="defer"></script>

不過以上各種優化的招式,請先確定真的知道自己在做什麼(那種什麼程式都不懂的小朋友不要學,叔叔有練過,這有危險性,不知道亂套用只會讓你的網站掛掉),而我檢視自己的 Blog,其實能夠調整的地方並不多(一方面有優化過了,另一方面我不用 include JS 的語法,都用 inline 的 Javascript,無從調整起),不過還有個地方可以玩,就拿來當作範例:我網站裡有裝 Google Analytics、一項 Google 提供用來分析流量的站長工具,沒辦法,它就要我一定要放一段 Code 才能啟用該項功能,所以套用以上提到的 defer 屬性,變成這樣:

<!-- Used for Google Analytics  -->
<script src='http://www.google-analytics.com/urchin.js' type='text/javascript' defer="defer"/>

放的位置不變,照理來說該 Script 會比較慢執行才是。接下來是 Google Analytics 會呼叫的函式 (就是有呼叫 urchinTracker 函式的那一段),本來人家是要擺在 <body> 的後面,我給它搬到 </body> 的前面,這樣就算是「最後執行」了。這個 Google Analytics 的範例用了幾個優化網站瀏覽的技巧,而且(應該)不會影響到顯示和相關功能,雖然我認為自己 Blog 的顯示效率已經一些搞一堆外掛的 Blog 好一些,但有時候還是會卡卡的不大滿意,看來有機會應該對整個網站程式碼做總體檢,用以上的工具和原則檢視一番,對網頁瀏覽時的效能說不定更進一步的明顯提昇。

回應: 10