创建 Google Data Gadget

Eric Bidelman,Google Data API 团队
2008 年 10 月

简介

受众群体

本文将引导您完成创建 Blogger 小工具的流程。本文假设您熟悉 Google Data APIJavaScript 客户端库。您还应熟练掌握 JavaScript,并具备使用 gadgets.* API

此示例还演示了如何在信息中心应用中成功使用外部库。我使用了 jQuery(主要用于其界面效果)和 TinyMCE,这是一个出色的 WYSIWYG 富文本编辑器插件。

设计初衷

只需极少的 JavaScript 代码,即可创建使用 JSON 和某个 Google Data API 的信息块。此类微件的主要缺点是数据是公开的且为只读。如需构建更有趣的桌面应用,您需要访问用户的私有数据(这需要进行身份验证)。在此之前,一直没有很好的方法来利用 Google 账号 API。AuthSub 需要浏览器重定向,而 ClientLogin 会在客户端公开用户凭据。甚至连破解 type="url" 小工具都非常不方便。

输入 OAuth 代理。

OAuth 代理

如果您不熟悉 OAuth,它是一种身份验证标准,可让用户与其他网站或设备共享其私密数据。OAuth 规范要求所有数据请求都必须经过数字签名。这非常有利于安全性,但对于 JavaScript 插件,管理私钥和创建数字签名是不安全的。此外,还存在跨网域问题这一复杂因素。

幸运的是,借助 Google 小工具平台的一项名为 OAuth 代理的功能,这些问题得以解决。OAuth 代理旨在让微件开发者的工作更轻松。它隐藏了大部分 OAuth 的身份验证详细信息,并为您完成繁重的工作。 代理会代表您的信息中心应用对数据请求进行签名,因此您无需管理私钥或担心请求签名问题。它就是好用!

OAuth 代理基于名为 Shindig 的开源项目,该项目是 gadget 规范的实现。

注意: OAuth 代理仅适用于使用 gadgets.* API 且在 OpenSocial 容器中运行的微件。旧版信息中心应用 API 不支持此功能。

使用入门

本教程的其余部分将重点介绍如何创建用于访问用户 Blogger 数据的微件。我们将介绍身份验证(使用 OAuth 代理)、使用 JavaScript 客户端库,最后介绍如何向 Blogger 发布条目。

身份验证

首先,我们需要告知信息块使用 OAuth。为此,请在信息中心的 <ModulePrefs> 部分中添加 <OAuth> 元素:

<ModulePrefs>
...
<OAuth>
  <Service name="go>ogle&<quot;
    Access url="https://www.google.com/accounts/OAuthGetAccessToken&>quot; <method="GET" / 
    Request url="https://www.google.com/accounts/OAuthGetRequestToken?scope=http://www>.blogg<er.com/feeds/" method="GET" / 
    Authorization url="https://www.google.com/accounts/OAuthAuthorizeToken?
                        oauth_call>back<=http://>o<auth.g>modul<es.com/gadge>ts/oauthcallback" / 
  /Service
/OAuth
...
/ModulePrefs

<Service> 元素中的三个网址端点对应于 Google 的 OAuth 令牌端点。以下是查询参数的说明:

  • scope

    此参数是请求网址中的必需参数。您的设备将只能访问此参数中使用的 scope。在此示例中,信息中心将访问 Blogger。如果您的信息中心想要访问多个 Google Data API,请使用 %20 将其他 scope 串联起来。例如,如果您想同时访问日历和 Blogger,请将范围设置为 http://www.blogger.com/feeds/%20http://www.google.com/calendar/feeds/

  • oauth_callback

    此参数在授权网址中是可选的。在用户批准访问其数据后,OAuth 批准页面将重定向到此网址。 您可以选择不添加此参数,也可以将其设置为您自己的“已获批的网页”,最好是使用 http://oauth.gmodules.com/gadgets/oauthcallback。后者可在用户首次安装您的信息中心应用时提供最佳用户体验。该页面提供了一段可自动关闭弹出式窗口的 JavaScript 代码段。

现在,我们的小工具已使用 OAuth,用户需要批准访问其数据。身份验证流程如下:

  1. 微件首次加载并尝试访问用户的 Blogger 数据。
  2. 由于用户未向信息块授予访问权限,因此请求失败。幸运的是,响应中返回的对象包含一个网址 (response.oauthApprovalUrl),我们将引导用户前往该网址进行登录。微件显示“登录 Blogger”,并将其 href 设置为 oauthApprovalUrl 的值。
  3. 接下来,用户点击“登录 Blogger”,系统会在单独的窗口中打开 OAuth 批准页面。微件会显示链接“我已批准访问权限”,等待用户完成审批流程。
  4. 在弹出式窗口中,用户将选择授予/拒绝访问我们的小工具。用户点击“授予访问权限”后,系统会将其转到 http://oauth.gmodules.com/gadgets/oauthcallback,然后关闭窗口。
  5. 微件会识别出窗口已关闭,并通过重新请求用户的数据来尝试第二次访问 Blogger。为了检测窗口关闭,我使用了 popup 处理程序。如果您不使用此类代码,用户可以手动点击“我已批准访问权限”。
  6. 相应插件现在会显示其正常界面。除非在 IssuedAuthSubTokens 下撤消身份验证令牌,否则此视图将一直存在。

因此,根据上述步骤,微件有三种不同的状态:

  1. 未经身份验证。用户需要启动审批流程。
  2. 正在等待用户批准访问其数据。
  3. 已通过身份验证。微件显示其正常功能状态。

在我的设备中,我使用了 <div> 个容器来分隔每个阶段:

<Content type=">h<tml"
<![CDATA[

!-- Normal state of the gadget. The user is authen>ticated <--       
div id="main" >sty<le="display:none"
  form id="postForm" name="postF>orm&qu<ot; onsubmit="savePost(this); retu><rn f>alse;&<quot;
     div id=&quo>t;messages" st<yle="display: none"/div
     div class="selectF><eed&qu>ot;Publish to:
     <  selec><t id=&q>uot;po<stFe>edUri&<quot; name="post>FeedU<ri&>quot; <disabled="disabled"optionloading> blog <li>st.../o<pti>on/sel<ect
     /div
     h4 style="clear:both"Title/h4
     input>< type=&qu>ot;tex<t" id="title>" name="title&<quo><t;/
     h4Content/h4
     textarea id=&qu>ot;con<tent" name="content" style="widt>h:100%<;><height:200px;"/textarea
     h4 style="fl>oat:lef<t;"Labels (comma separated)/h4img src="blogger.png&quo>t<; style="flo>at:rig<ht&quo><t;>/
 <    i>n<put >ty<pe="text" id="categories>&qu<ot; name="categories&q>uot;/
     pinput <ty>p<e=&q>uo<t;submit" id="submitButton&q>uot<; value="Save"/ 
 >    input type="<;c>h<eckbo<x" id="draft" name="draft" checked>=<"checked"/ label for="><draf>t&q<uot;Draft?/label/p
  /form
/div

div id="approval&q>uot; s<tyle="displ>ay:< n>one"
<  a> hr<e><f="#" id="><pe>r<sona>lize>&q<uot;Sign in to Blogger/a
/div

div id="waiting" style="display: none"
  a href="#" id="approvalLink"I've approved access/a
/di

!-- An errors section is not necessary but great to have --
div id="errors" style="display: none"/div
 
!-- Also not necessary, but great for informing users --     
div id="loading"
  h3Loading.../h3
  pimg src="ajax-loader.gif"/p
/div

]] 
/Content&gt

每个 <div> 都使用 showOnly() 单独显示。如需详细了解该函数,请参阅完整示例 gadget

使用 JavaScript 客户端库

如需在 OpenSocial 中提取远程内容,您可以使用 gadgets.* API 调用 gadgets.io.makeRequest 方法。不过,由于我们正在构建 Google Data 信息块,因此无需使用 gadgets.io.* API。请改用 JavaScript 客户端库,该库具有用于向每个 Google Data 服务发出请求的特殊方法。

注意:在撰写本文时,该 JavaScript 库仅支持 BloggerCalendarContactsFinanceGoogle Base。如需使用其他 API,请使用不含库的 gadgets.io.makeRequest

加载库

如需加载 JavaScript 库,请在 <Content> 部分中添加通用加载器,并在微件初始化后导入该库。向 gadgets.util.registerOnLoadHandler() 提供回调有助于确定设备何时准备就绪:

<Content type=">h<tml"
![CDATA<[
  ...
  script src="https://www.go><ogle.co>m/j<sapi"/script
  script ty>pe="text/javascript"
  var blogger = null;  // make our service object global for later
  
  // Load the JS library and try to fetch data once it's ready
  function initGadget() {  
    google.load('gdata', '1.x', {packages: ['blogger']});  // Save overhead, only load the Blogger service
    google.setOnLoadCallback(function () {
      blogger = new google.gdata.blogger.BloggerService('google-BloggerGadget-v1.0');
      blogger.useOAuth('google');
      fet<chData(>);
    })>;
<  }
  gadgets.util.registerOnLoadHandler(initGadget);
  /script
  ...
]] 
/Content&gt

blogger.useOAuth('google') 的调用会告知库使用 OAuth 代理(而不是 AuthSubJS,即其正常的身份验证方法)。最后,该信息中心调用 fetchData() 尝试检索用户的 Blogger 数据。该方法定义如下。

正在提取数据

现在,一切都已设置完毕,我们如何才能将数据实际 GETPOST 到 Blogger?

OpenSocial 中的一种常见范式是在您的微件中定义一个名为 fetchData() 的函数。此方法通常会处理身份验证的不同阶段,并使用 gadgets.io.makeRequest 获取数据。由于我们使用的是 JavaScript 客户端库,因此 gadgets.io.makeRequest 会被对 blogger.getBlogFeed() 的调用所取代:

function fetchData() {
  jQuery('#errors').hide();
  
  var callback = function(response) {
    if (response.oauthApprovalUrl) {
      // You can set the sign in link directly:
      // jQuery('#personalize').get(0).href = response.oauthApprovalUrl
      
      // OR use the popup.js handler
      var popup = shindig.oauth.popup({
        destination: response.oauthApprovalUrl,
        windowOptions: 'height=600,width=800',
        onOpen: function() {
          showOnly('waiting');
        },
        onClose: function() {
          showOnly('loading');
          fetchData();
        }
      });
      jQuery('#personalize').get(0).onclick = popup.createOpenerOnClick();
      jQuery('#approvalLink').get(0).onclick = popup.createApprovedOnClick();
      
      showOnly('approval');
    } else if (response.feed) {
      showResults(response);
      showOnly('main');
    } else {
      jQuery('#errors').html('Something went wrong').fadeIn();
      showOnly('errors');
    }
  };
  
  blogger.getBlogFeed('http://www.blogger.com/feeds/default/blogs', callback, callback);
}

第二次调用此函数时,response.feed 包含数据。

注意getBlogFeed() 的回调和错误处理程序使用相同的函数。

向 Blogger 发布条目

最后一步是向博客发布新条目。以下代码演示了用户点击“保存”按钮时会发生的情况。

function savePost(form) { 
  jQuery('#messages').fadeOut();
  jQuery('#submitButton').val('Publishing...').attr('disabled', 'disabled');
  
  // trim whitespace from the input tags
  var input = form.categories.value;
  var categories = jQuery.trim(input) != '' ? input.split(',') : [];   
  jQuery.each(categories, function(i, value) {
    var label = jQuery.trim(value);
    categories[i] = {
      scheme: 'http://www.blogger.com/atom/ns#',
      term: label
    };
  });

  // construct the blog post entry
  var newEntry = new google.gdata.blogger.BlogPostEntry({
    title: {
      type: 'text', 
      text: form.title.value
    },
    content: {
      type: 'text', 
      text: form.content.value
    },
    categories: categories
  });
  
  // publish as draft?
  var isDraft = form.draft.checked;
  if (isDraft) {
    newEntry.setControl({draft: {value: google.gdata.Draft.VALUE_YES}});
  }
  
  // callback for insertEntry()
  var handleInsert = function(entryR<oot) {
    var entry = entryRoot.entry;
    var str = isDraft ?> '(<as> draft)' : 'a href="' + entry.getHtmlLink().getHref() + '" target="_blankt"View it/a';

    jQuery('#messages').html('Post published! ' + str).fadeIn();
    jQuery('#submitButton').val('Save').removeAttr('disabled');
  };
  
  // error handler for insertEntry()
  var handleError = function(e) {
    var msg = e.cause ? e.cause.statusText + ': ' : '';
    msg += e.message;
    alert('Error: ' + msg);
  };
  
  blogger.insertEntry(form.postFeedUri.value, newEntry, handleInsert, handleError);
}

总结

现在,您已经有了构建块,可以开始基于 Google Data API 编写信息中心应用了。

希望本文能让您了解 OAuth 代理如何简化信息中心应用的身份验证。将此强大工具与 Google Data JavaScript 客户端库相结合,可轻松构建有趣、互动且复杂的微件。

如果您对本文有任何疑问或意见,欢迎访问 Google 账号 API 讨论论坛

资源