邊做邊學 Facebook 開發 系列 2 - Facebook SDK With IFrame,Facebook Connect and XFBML

文/黃忠成


申請 Facebook Application

   在上一篇文章中,我們使用了 Facebook SDK 中的 IFrame 支援,成功的做出了列示使用者朋友及其相片的應用程式,現在讓我們繼續改進這個程式,讓它也能列出每個朋友的最新狀態。

   說來也簡單,Facebook SDK 提供了相當完整的 Facebook REST API Wrapper,要取得特定朋友的狀態,只要呼叫 Status.Get 函式就可以了。

public IList<user_status> Get(long uid);

其中的 uid 參數指的便是要取得狀態的使用者編號,在 Facebook 中,每個用戶會繫結至一個 long 型態的編號,只要擁有這個編號,我們就能取得該用戶的狀態、或是傳送邀請訊息給該用戶 (當然,前提得是朋友,且有賦與這些權限才行)。

   有了 Get 這個函式後,只要將原來的程式碼改成程式1的樣子,就能夠在列示朋友資料時,順便把狀態也列出來。

程式1

Default.aspx.cs

............
foreach (Facebook.Schema.user user in users)
{
         list.Add(new FacebookInfo()
         {
             name = user.name,
             pic_small = user.pic_small,
             uid = (long)user.uid,
             latestMessage = api.Status.Get((long)user.uid)[0].message
        });
}
.........

但這樣的做法並不聰明,因為只要朋友數量一多,此程式就得使用對應數量的 REST API 呼叫,一來一往間,犧牲的將是應用程式的效率。

   改良的方法有很多種,第一種是使用 Batch 機制,Facebook REST API 支援 Batch 機制,我們可以將多個 REST API 呼叫封成一個,一次送給 Facebook Server,而其將會一併處理後傳回所有的回傳值,這樣可以減少因多次呼叫 REST API 所需付出的網路來往次數。

程式2

Default.aspx.cs

.............
api.Status.Batch.BeginBatch();
int i = 0;
foreach (Facebook.Schema.user user in users)
{
     if (i % 20 == 0)
     {
        var result = api.Status.Batch.ExecuteBatch(true);
        if (result != null)
        {
            for (int j = 0; j < 20; j++)
            {
            if (((Facebook.Schema.status_get_response)result[j]).user_status.Count > 0)
                list[i - 20 + j].latestMessage = 
                ((Facebook.Schema.status_get_response)result[j]).user_status[0].message;
            }
            }
         api.Status.Batch.BeginBatch();
       }
       list.Add(new FacebookInfo()
       {
           name = user.name,
           pic_small = user.pic_small,
           uid = (long)user.uid
       });
       api.Status.Get((long)user.uid, 1);
       i++;
     }

     var remainResult = api.Status.Batch.ExecuteBatch(true);
     if (remainResult != null)
      {
          for (int j = 0; j < remainResult.Count; j++)
          {
           if (((Facebook.Schema.status_get_response)remainResult[j]).user_status.Count > 0)
              list[(list.Count / 20) * 20 + j].latestMessage =
           ((Facebook.Schema.status_get_response)remainResult[j]).user_status[0].message;
     }
}
....................

此法雖然可改善效率,但是還是不夠好,主要是應用程式的目的只是要列出使用者狀態,然後顯示在網頁上,這樣的需求 Facebook 早就有了現成、有效率的解決方案了,那就是 FBML/XFBML。

  那為何我還要把這兩個例子列出來討論呢?很簡單,FBML/XFBML 不是萬能的,多少有其力有未逮之處,了解如何呼叫 Facebook REST API 及使用 Batch 增進效能,是每個 Facebook Developer 必須知道的基礎知識,日後筆者還會介紹 FQL (Facebook Query Language),可以更加有效率的來取得使用者資料。

IFrame 的先天限制

   在與 Facebook Server 溝通時,IFrame 型態應用程式多半必須發出為數不少的 REST API 呼叫,這種行為會導致應用程式的效能緩慢,當然!我的意思並不是 IFrame 型態應用程式都會有效能低落的情況,這與架構面與手法有很大關係。以前面例子而言,即使我們把 REST API 呼叫以 Batch 模式封裝,在效能上仍顯差強人意,這是 IFrame 應用程式先天上的限制。

   那沒有辦法可以超越這個限制嗎?有的,第一種是使用 FBML,這是由 Facebook 所發明的一種標記語言,其運作方式與 IFrame 完全不同,當使用者瀏覽一個 IFrame 型態的 Facebook 應用程式時,Facebook Server 會輸出一個內含 IFrame 的網頁,這個 IFrame 就內嵌了我們所指定的網站,這也是為何我們可以在本機上開發及除錯 IFrame 應用程式的主要原因。但也因為如此,所以當 IFrame 應用程式需與 Facebook 溝通時,只能透過 REST API 來處理。

   反觀 FBML,當使用者瀏覽一個 FBML 型態的 Facebook 應用程式時,Facebook Server 會發出一個要求至我們指定的網站,取得回傳內容後交給 FBML Parser 來解譯,最終產出 HTML 給使用者,圖1是這個流程的示意圖。

圖1

這張圖裡隱含了一個重要的訊息,那就是我們所指定的網站,必須要是可視於 Facebook Server 的,也就是說無法像開發 IFrame 型態應用程式般於本機除錯,除非電腦中安裝了 Web Server,且 IP 是可視於 Facebook Server 的,更簡略的說!該電腦必須完全曝露在網際網路上。

   但也因為是這樣的架構,所以 FBML 程式可以省掉許多的 REST API 呼叫,因為這些呼叫大多數都被已定義的 FBML Tag 取代,少了 REST API 呼叫所產生的流量,速度自然可以提升非常多。

   只是 FBML 本身的限制相當多,當程式較為複雜時,多半時間會花在維持 FBML 與 Facebook、自身的 JavaScript、Post-Back 機制間的平衡,因此 FBML 只適合用來寫較簡單的網頁或是 Flash 為主的網頁 (Facebook 提供了 Flash 專用的 FBML Tag)。

使用 XFBML

   為了解決 IFrame 的這個限制,及讓 IFrame 能擁有與 Facebook 一致的 UI 介面,Facebook Developer Team 發明了 XFBML 機制,此機制可以讓 IFrame 也能使用多數的 FBML Tag,不需要使用 REST API 呼叫來達到原本使用 FBML 就能輕易達到的工作。

   要使用 XFBML,我們的網站必須準備好一個 xd_receiver.htm 檔案,其內容如下:

xd_receiver.htm

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title>Cross-Domain Receiver Page</title>
</head>
<body>
    <script src="http://static.ak.facebook.com/js/api_lib/v0.4/XdCommReceiver.js?2" type="text/javascript"></script>
</body>
</html>

接著除了設定畫布等資訊外,還要設定應用程式的 Connect URL。

圖2

然後在 MasterPage 中設定 XFBML 需用到的 xd_receiver.htm 網址。

Default.Master

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Default.master.cs" Inherits="FrameDemo.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
    <title></title>
    <asp:ContentPlaceHolder ID="head" runat="server">
    </asp:ContentPlaceHolder>
</head>
<body>   
    <script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>  
        <asp:ContentPlaceHolder ID="ContentPlaceHolder1" runat="server">
        </asp:ContentPlaceHolder>
   <script type="text/javascript">
       FB_RequireFeatures(["XFBML"], function() {
              FB.Facebook.init("<your api key>", "channel/xd_receiver.htm");
       });
    </script>
</body>
</html>

最後修改主網頁。

Default.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Default.Master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="FrameDemo.Frame1" %>

<%@ Register Assembly="Facebook.Web" Namespace="Facebook.Web.FbmlControls" TagPrefix="fb" %>
<%@ Register Assembly="Facebook.Web" Namespace="Facebook.Web" TagPrefix="cc1" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
    
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <p>
    </p>
    <form id="form" runat="server">
    
    <asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False" CellPadding="4"
        ForeColor="#333333" GridLines="None" OnSelectedIndexChanged="GridView1_SelectedIndexChanged">
        <RowStyle BackColor="#E3EAEB" />
        <Columns>
            <asp:ImageField DataImageUrlField="pic_small">
            </asp:ImageField>
            <asp:BoundField DataField="name" HeaderText="Name" />
            <asp:TemplateField>
                <ItemTemplate>
                    <fb:UserStatus ID="UserStatus1" runat="server" Uid='<%# Eval("uid")  %>' />
                </ItemTemplate>
            </asp:TemplateField> 
        </Columns>
        <FooterStyle BackColor="#1C5E55" Font-Bold="True" ForeColor="White" />
        <PagerStyle BackColor="#666666" ForeColor="White" HorizontalAlign="Center" />
        <SelectedRowStyle BackColor="#C5BBAF" Font-Bold="True" ForeColor="#333333" />
        <HeaderStyle BackColor="#1C5E55" Font-Bold="True" ForeColor="White" />
        <EditRowStyle BackColor="#7C6F57" />
        <AlternatingRowStyle BackColor="White" />
    </asp:GridView>
    
    </form>
</asp:Content>

Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Facebook.Web;
using Facebook.Rest;
using Facebook.Session;
using System.Configuration;

namespace FrameDemo
{
    public partial class Frame1 : System.Web.UI.Page
    {
        protected void Page_Load(object sender, EventArgs e)
        {
            if (!IsPostBack)
            {
                IFrameCanvasSession session = 
                   new IFrameCanvasSession(ConfigurationManager.AppSettings["APIKey"],
                   ConfigurationManager.AppSettings["Secret"]);
                Api api = new Api(session);
                List<FacebookInfo> list = new List<FacebookInfo>();
                var users = api.Friends.GetUserObjects();
                foreach (Facebook.Schema.user user in users)
                {
                    list.Add(new FacebookInfo()
                    {
                        name = user.name,
                        pic_small = user.pic_small,
                        uid = (long)user.uid
                    });
                }
                GridView1.DataSource = list;
                GridView1.DataBind();
            }
        }
        protected void GridView1_SelectedIndexChanged(object sender, EventArgs e)
        {
        }
    }

    public class FacebookInfo
    {
        public string name { get; set; }
        public string pic_small { get; set; }
        public string latestMessage { get; set; }
        public long uid { get; set; }
    }
}

在這個頁面中,我們使用了一個特別的 Server Control:UserStatus,這是由 Facebook SDK 所提供的 FBML 專用 Server Control,其主要功能是輸出對應的 FBML Tag,由於 XFBML 大部份的 Tag 都與 FBML 同名,因此 Facebook SDK 所提供的 FBML Server Control 同時可用於 FBML 及 XFBML,圖3 是此例的執行畫面,效率自然是非常的快。

圖3

圖4 是 Facebook SDK 所提供的 FBML Server Control 一部份列表。

圖4

相關說明可於以下網址取得。

FBML : http://developers.facebook.com/docs/reference/fbml/
XFBML: http://developers.facebook.com/docs/reference/javascript/FB.XFBML.parse/

 

IFrame 的進階例子 - 選取最愛的 Windows Phone

接下來讓我們來做個比較有互動性的例子,應用程式會列出幾個 Windows Phone 手機讓使用者選取,在使用者選定後按下按紐,開出張貼訊息至 Facebook 的對話視窗。

  這個例子很簡單,只是整合了 IFrame 與 Facebook JavaScript SDK 而已,下面是此程式的列表,影片為效果圖。

 

Default.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Default.Master" AutoEventWireup="true"
    CodeBehind="Default.aspx.cs" Inherits="FrameDemo.Default1" %>

<%@ Register Assembly="Facebook.Web" Namespace="Facebook.Web.FbmlControls" TagPrefix="fb" %>
<%@ Register Assembly="Facebook.Web" Namespace="Facebook.Web" TagPrefix="cc1" %>
<%@ MasterType VirtualPath="~/Default.Master" %>
<asp:Content ID="Content1" ContentPlaceHolderID="head" runat="server">
</asp:Content>
<asp:Content ID="Content2" ContentPlaceHolderID="ContentPlaceHolder1" runat="server">
    <form id="form1" runat="server">

    <script language="javascript" type="text/javascript">
        function selected() {
            var selOpt = document.getElementsByName(
                     "ctl00$ContentPlaceHolder1$RadioButtonList2");
            var selectValue = "";
            for (var i = 0; i < selOpt.length; i++) {
                if (selOpt[i].checked) {
                    selectValue = selOpt[i].value;
                    break;
                }
            }
            if (selectValue != "") {
                var attachment = { 'name': 'MyFirstApp', 
                 'href': 'http://apps.facebook.com/testapp_tw_r', 
                 'description': 'MyFirstApp' };
                FB.Connect.streamPublish('我選擇了' + selectValue, attachment);
            }
        }

        function feedSent() {
            submitForm();
        }
    </script>

    <asp:RadioButtonList ID="RadioButtonList2" runat="server">
        <asp:ListItem Value="HD2">HTC HD2</asp:ListItem>
        <asp:ListItem Value="HT2">HTC Touch 2</asp:ListItem>
        <asp:ListItem>I8000</asp:ListItem>
        <asp:ListItem Value="M20">Garmin Asus M20</asp:ListItem>
    </asp:RadioButtonList>
    <input type="button" id="selBtn" value="選取" onclick="selected()"/>
    </form>
</asp:Content>

Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace FrameDemo
{
    public partial class Default1 : System.Web.UI.Page
    {

        protected void Page_Load(object sender, EventArgs e)
        {

        }
    }
}

圖5、6、7 是執行結果。

圖5

圖6

圖7

程式的關鍵在於 selected 這個 JavaScript 函式,它先取得使用者所選取的手機,然後呼叫 FB.Connect.streamPublish 來發佈內容,第一個參數是要發佈的訊息,接著是 attachment,這是由 Facebook 所定義,專用於發佈用的 JSON 物件,以我們的例子而言,第一個 name 指的是應用程式名稱,第二個 href 則是顯示於對話框下面的連結,第三個參數 description 則是顯是於連結下方的描述。

streamPublish 不僅可以發佈文字內容,也可以發佈圖形內容,例如下例就能夠在發佈時加上圖片。

var attachment = { 'name': 'MyFirstApp', 
            'href': 'http://apps.facebook.com/testapp_tw_r', 'description': 'MyFirstApp',
             'media':  [{'type': 'image',                                               'src':  'http://www.htc.com/uploadedImages/Common/Shared_Image/Icons/HTC_HD2_Make_It_Mine.jpg',
'href': 'http://www.htc.com/tw/product/hd2/overview.html'}]
};

圖8

圖9

更多的 streamPublish 例子,請參考以下連結。

http://developers.facebook.com/docs/?u=facebook.jslib.FB.Connect.streamPublish

 

同樣的結果也可以使用 Facebook SDK 所提供的 REST API Wrapper 達成,不過這必須要使用者同意,讓 MyFirstApp 不需要詢問使用者就能夠發佈訊息,基本上我比較不贊成這樣的用法,以下影片為其效果。

 

圖10

要做到這點的關鍵有兩個,一是必須在 Default.Master.cs 中要求取得授權。

Default.Master.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using Facebook;
using Facebook.Web;
using Facebook.Schema;

namespace FrameDemo
{
    public partial class Default : Facebook.Web.CanvasIFrameMasterPage
    {
        protected void Page_Load(object sender, EventArgs e)
        {

        }

        public Default()
        {
            RequireLogin = true;
            this.RequiredPermissions = new List<Enums.ExtendedPermissions>() { 
                  Enums.ExtendedPermissions.publish_stream };
        }
    }
}

二是撰寫 Server 端的 Button Click 事件。

Default.aspx.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;

namespace FrameDemo
{
    public partial class Default1 : System.Web.UI.Page
    {

        protected void Page_Load(object sender, EventArgs e)
        {
        }

        protected void Button1_Click(object sender, EventArgs e)
        {
            Master.Api.Stream.Publish("我選擇了" + RadioButtonList2.SelectedItem.Text,
                                   new Facebook.Rest.attachment()
                                   {
                                       name = "MyFirstApp",
                                       description = "MyFirstApp",
                                       href = "http://apps.facebook.com/testapp_tw_r",
                                       media = new List<Facebook.Rest.attachment_media>() 
                                      {
                                        new Facebook.Rest.attachment_media_image()
                                          {
href="http://www.htc.com/tw/product/hd2/overview.html",                                                                              src="http://www.htc.com/uploadedImages/Common/Shared_Image/Icons/HTC_HD2_Make_It_Mine.jpg",
type= Facebook.Rest.attachment_media_type.image
}
}
}, null, null, (long)Master.Api.Users.GetInfo().uid);
     }
    }
}

不過說實話,我還是比較喜歡應用程式先詢問我再發佈訊息。

 

【範例資源下載】