WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

Opera Unite サービスの作り方

Opera 10 ベータ版に搭載された画期的な新機能、 Opera Unite はもう試されたでしょうか。 JavaScript で組んだ Web アプリケーションをブラウザ内蔵の Web サーバーで動作させ、それに独自ドメイン(operaunite.com のサブドメイン)を割り当ててインターネットに公開するという、まったく新しい Web アプリケーションの形を提案してくれています。標準で Web サーバー、各種ファイル共有、付箋、チャットといったアプリケーションがプリセットされていますが、自分で独自のサービスを構築することもでき、可能性は無限大です。

しかし、 Opera Unite 自体はいろいろなサイトで取りあげられたものの、肝心の独自サービスの作り方を日本語で解説したページはあまり見受けません。せっかくの素晴らしい機能がこれではもったいないので、週末にいろいろと実験して、記事にまとめてみました。とりあえずウォークスルー的な感じで、 Hello World からローカルファイルにアクセスするサービスまで、ひととおりまとめています。 Opera Unite の面白い所をできるだけ網羅しましたので、ぜひご覧ください!

まずは既存のサービスを使ってみる

まだ Opera Unite をまったく使っていない方も多いでしょうから、まずはそのインストール方法や使い方を簡単にご説明しましょう。ぜひ実際に Opera Unite の世界を体験してください。インストール方法は以下のとおりです。

  1. Opera Unite の利用には My Opera のアカウントが必要なので、もしなければこちらのページで取得しておいてください。 Opera Unite を設定するときに取得することもできますが、あらかじめやっておいたほうが間違いがないと思います。
  2. 最新版の Opera 10 βを Opera Desktop Team のブログからダウンロードし、インストールします。 Opera Unite のページにあるダウンロードリンクはバージョンが古いので、こちらのほうがお勧めです。
  3. メインメニューの [ツール]-[Opera Unite Server]-[Enable Opera Unite] を選択します。
  4. 「Welcome to Opera Unite!」というダイアログが開くので、「次へ」をクリックします。
  5. My Opera アカウントを作成するダイアログが開くので、「I already have an account」をクリックします。
  6. ログインダイアログが開くので、最初に取得した My Opera のアカウント名とパスワードを入力します。
  7. Opera Unite Settings のダイアログが開くので、コンピュータ名を入力simasu

。ここで選択したコンピュータ名は公開するサービスのドメイン名の一部になるので、複数のマシンで Opera Unite を動かす場合、互いに重ならないように設定してください。

以上の操作を終えると、 Opera Unite のホーム画面が表示されます。もし表示されない場合は、メインメニューの [ツール]-[Opera Unite Server]-[Manage Services] を選択し、サイドパネルの家のアイコンをダブルクリックしてください。

この状態ではまだサービスがなにも起動していないので、試しにファイル共有サービスである「File Sharing」を使ってみましょう。インターネット経由で簡単にローカルのファイルを共有できるサービスです。サイドバネルにある「File Sharing」をダブルクリックすると設定ダイアログが開くので、共有するディレクトリを指定して「OK」を押してください。これだけでサービスが起動し、使える状態になります。

ファイル共有サービスですので、もちろん他のマシンからもアクセスできます。右にあるラジオボタンを Limited に変更した後、その上のテキストボックスにある URL に他のマシンからアクセスしてみてください。 Opera 以外のブラウザでも大丈夫です。以下のように、ローカルの Opera とまったく同じようにサービスが利用できます(ただ、現在は Opera のプロキシーが非常に重いようで、時間帯によってはなかなか繋がってくれません。正式公開までになんとかなるといいのですが・・・)。

このように、気軽に Web サービスを起動して、ローカルにあるデータをインターネット経由で共有できるのが Opera Unite の機能です。しかもサービスを自分で構築することもできるのですから、アイディア次第でとても強力な情報ツールになるでしょう。

Opera Unite サービスを作ってみる

Opera Unite を使う環境を整えたところで、さっそく独自の Opera Unite サービスを構築してみましょう。必要なのは Opera と任意のテキストエディタのみ。というか、 Opera はテキストエディタにもなるので、その気になれば Opera だけでも開発可能です。この手軽さが Opera Unite の最大の魅力ですね。

以降で作成するサービスの全ソースコードはこちらで参照できますので、併せてご参照ください。また、あくまでサンプルなので、エラーチェックやセキュリティーはほとんど考えていません。あらかじめご了承ください。

定番 Hello World! を作る

それでは、手始めとして基本中の基本である「Hello World!」サービスを作ってみましょう。ブラウザでアクセスすると、単に「Hello World!」と表示するだけのサービスです。 Opera Unite サービスの作成手順は本当に簡単で、適当なディレクトリを作成して以下の 2 つのファイルを作るだけです。

ファイル名 内容
config.xml サービスの基本設定
index.html サービス起動時に解釈される HTML ファイル

まずは config.xml から見ていきましょう。 Hello World サービスの config.xml は以下のようになります。

<?xml version="1.0" encoding="utf-8"?>
<widget network="private public">
  <widgetname>Test Service</widgetname>
  <description>Opera Unite のテスト</description>
  <feature name="http://xmlns.opera.com/webserver">
    <param name="type" value="service"/>
    <param name="servicepath" value="test"/>
  </feature>
  <feature name="http://xmlns.opera.com/fileio" />
</widget>

この中で、 widgetname はサービス名で、サイドパネルに表示されるものです。 description はサービスの説明文で、前出のホーム画面などに表示されます。そして、二番目の param タグ(name 属性が servicepath となっているもの)の value 属性がサービスのパスになります。従って、このサービスのトップページは

http://admin.コンピュータ名.ユーザー名.operaunite.com/test/

になるわけですね。その他のタグはほぼ決まり文句なので、このまま覚えてしまえば良いでしょう。実はこの config.xml は Opera Widgets とほぼ同じもので、詳細はこちらのページで解説されています。また、古い情報ですがこのブログでもこちらで記事にしていますので、ご参照ください。

次は index.html ですが、多くの方が「サーバーなのになぜHTML?」と不思議に思うことでしょう。これは私の推測ですが、おそらく Opera の内部では、個々のサービスが見えないブラウザ・タブなのだと思います(もう少し正確に言うと、見えない Opera Widget です)。そのタブに index.html を読み込み、そこに記述された JavaScript によってリクエストの処理を行う、という仕組みなのでしょう。

そのようなわけで、 index.html には最低限のタグとリクエスト処理用の JavaScript を記述することになります。 Hello World の場合は以下のようになります。

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>Test Service</title>
  <script type="text/javascript">
 
// リクエストハンドラの登録
// 後々のため、handlers 配列に追加するだけでハンドラを増やせるようにしている。
var handlers = [];
window.onload = function () {
  for(var i = 0 ; i < handlers.length ; ++i) {
    var h = handlers[i];
    opera.io.webserver.addEventListener(h[0], h[1], false);
  }
}
 
function helloHandler(event) {
  var response = event.connection.response;
  response.write([
    '<!DOCTYPE HTML>',
    '<html><head><title>hello</title></head>',
    '<body>Hello World!</body></html>'].join('\n'));
  response.close();
}
 
handlers.push(['_index', helloHandler]); // サービスのトップページのハンドラ
handlers.push(['hello',  helloHandler]);
 
  </script>
  <script type="text/javascript" src="template.js"></script>
  <script type="text/javascript" src="script.js"></script>
</head>
</html>

表示が必要ないので BODY タグを省略していますが、その他は見慣れた HTML ファイルであることがおわかりいただけるでしょう。

ひとつの大きな違いは onload イベント内の処理です。通常の Web ページでは必要な DOM 要素にイベントハンドラを登録するところですが、代わりにリクエストを処理するハンドラを opera.io.webserver に登録しています。登録には DOM イベントハンドラと同じ addEventListener が使われますが、引数の意味が少し違います。

addEventListener(パス, ハンドラ関数, false)

第一引数で指定されたパスがアクセスされると、第二引数で指定したハンドラ関数が呼び出されます。したがって、ここでは "/test/" もしくは "/test/hello" がアクセスされたら helloHandler が呼ばれるというわけですね。

helloHandler の引数は WebServerRequestEvent クラスのオブジェクトで、 DOM イベントのものとは全く異なります。以下、 helloHandler で使用しているメンバのみ簡単にご説明します。

event.connection
WebServerConnection クラスのオブジェクト。メンバにはコネクションの状態を取得するものや、リクエストの情報を保持した request 、レスポンス出力のための response などがあります。
event.connection.response
WebServerResponse クラスのオブジェクト。文字列を出力する write メソッドをはじめとして、レスポンス出力のために必要な多くのメソッドを持っています。

これで実装は終了なので、さっそくサービスを起動してみましょう。先ほど作成した config.xml を Opera にドラッグ&ドロップするだけです。確認ダイアログが表示された後にサイドパネルに「Test Service」が追加され、以下のように Hello! World と書かれたページが表示されるはずです。

記念すべき最初の Opera Unite サービス完成しました!以降はこれをベースにして、さまざまな機能を試していきましょう。

Markuper ライブラリを利用する

Hello World では JavaScript で表示内容を直接生成していましたが、出力する HTML が複雑になると、この方法はとても面倒です。この問題を解決するため、 Opera Unite 向けのテンプレートエンジンとして Markuper ライブラリが提供されています。

http://dev.opera.com/libraries/markuper/

今度はこのライブラリを利用して、リクエストの詳細情報を表示するページを作ってみましょう。ライブラリのソースが上記ページの「JavaScript code」のリンクからダウンロードできますので、それを template.js という名前で config.xml と同じディレクトリに保存してください。

index.html にこれ以上スクリプトを書くのは大変なので、独立した .js ファイルを作りましょう。名前はなんでもいいのですが、ここでは script.js としておきます。内容は以下です。

function requestInfoHandler(event) {
  var connection = event.connection;
  var template   = new Markuper('request-info.html', { connection : connection });
  connection.response.write(template.parse().html());
  connection.response.close();
}
handlers.push(['request-info', requestInfoHandler]);

ここでのキモは、 Markuper オブジェクトを生成している部分です。第一引数で渡されたファイル(内容は後述)がテンプレートとして読み込まれ、第二引数のオブジェクトがテンプレート内部で参照するデータとして使われます。その後、 template.parse().html() でテンプレートの解析と HTML への変換を行い、それをレスポンスに書き出しています。

テンプレートとなる request-info.html の内容は以下のとおりです。これも config.xml と同じディレクトリに作成してください。

<!DOCTYPE HTML>
<html>
 
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>リクエスト情報の確認</title>
</head>
 
<body>
  <h1>リクエスト情報の確認</h1>
 
  <h2>コネクション情報</h2>
  <table border="1">
    <tbody>
      <tr data-list="connection">
        <th>{{connection[].key}}</th>
        <td>{{connection[].value}}</td>
      </tr>
    </tbody>
  </table>
 
  <h2>リクエスト情報</h2>
  <table border="1">
    <tbody>
      <tr data-list="connection.request">
        <th>{{connection.request[].key}}</th>
        <td>
          <span data-keep-if="connection.request[].value">
            {{connection.request[].value}}
          </span>
        </td>
      </tr>
    </tbody>
  </table>
</body>
 
</html>

二重の波カッコ {{ }} で囲まれた部分は変数展開で、 Markuper コンストラクタの第二引数に指定されたオブジェクト内の値を参照しています。展開の際には HTML の特殊文字がすべてエスケープされるので安心です。

また、いくつかのタグに data-listdata-keep-if といった見慣れない属性がついていますが、これは Markuper のコマンド呼び出しです。デフォルトで以下のものが定義されています。

data-list
配列やオブジェクトのイテレート。指定された配列やオブジェクトの要素の数だけそのタグを繰り返します。タグの内部では、 {{配列またはオブジェクト[]}} とすることで現在の要素を参照できます。
data-keep-if
条件判定で、指定された値が真であれば、そのタグを出力します。
data-remove-if
条件判定で、指定された値が偽であれば、そのタグを出力します。
data-import
指定されたテンプレートファイルをタグの内部に挿入します。

さらに registerDataAttribute メソッドを使えば独自のコマンドを追加できます。詳しくはドキュメントを参照してください。

script.js, template.js という 2 つの外部のスクリプトファイルを作ったので、それを起動時に読み込むようにしましょう。 index.html に script タグを追加するだけです。

<!DOCTYPE HTML>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <script type="text/javascript">
    // 略...
  </script>
  <script type="text/javascript" src="template.js"></script>
  <script type="text/javascript" src="script.js"></script>
</head>
</html>

以上で完成です。さっそくブラウザでアクセスして・・・といきたいところですが、この状態で該当 URL にアクセスしても 404 エラーになってしまうはずです。 Opera Unite では、スクリプトの変更を反映するためにはサービスを再起動する必要があるのです。

サービスを再起動するには、サイドパネルで再起動したいサービス(ここでは「Test Service」)を選択し、サイドパネル上部にあるボタンでサービスを Stop → Start させてください。これで変更が反映されたはずですので、 "/test/request-info" にアクセスしてみましょう。以下のようにリクエストの情報が表示されれば成功です。

Markuper ライブラリを使うと、ロジックとビューの分離が簡単に実現できます。ある程度の規模になると、このような構成は必須になってくるので、ぜひ活用してください。

Preference Store で簡易メモを作成

Opera Unite のサービス内では、「Preference Store」という簡易 key-value ストレージが利用できます。これは Opera Widgets のそれとまったく同じもので、こちらの記事でその使い方をご説明しています。基本的には setPreferenceForKey で値を設定し、 preferenceForKey で値を取得するだけのシンプルなものです。これを使って簡単なメモ帳を作成してみましょう。

今回はまずビューの方から。 memo.html というファイルを作成し、内容を以下のようにしてください。

<!DOCTYPE HTML>
<html>
 
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>簡易メモ</title>
</head>
 
<body>
<h1>簡易メモ</h1>
<form method="POST" action="memo" enctype="multipart/form-data">
  <textarea name="data" cols="80" rows="20">{{data}}</textarea><br>
  <input type="submit" value="送信">
</form>
</body>
 
</html>

あまり説明の必要もないかと思いますが、フォームにテキストエリアをひとつ設置して、そこに data というテンプレートパラメータの内容を展開しています。そしてフォームの送信ボタンが押されると、 "/test/memo" に POST するようになっています。フォームがマルチパートになっているのは、文字化けを回避するためです(後述)。

次にスクリプトです。 script.js の最後に以下のコードを追加してください。

function memoHandler(event) {
  var connection = event.connection;
  var request    = connection.request;
  var response   = connection.response;
  if(request.method == 'POST' && connection.isOwner && request.bodyItems.data) {
    widget.setPreferenceForKey(decodeURIComponent(request.bodyItems.data[0]), 'memoData');
  }
  var template = new Markuper('memo.html', {
    data : widget.preferenceForKey('memoData') || ''
  });
  response.write(template.parse().html());
  response.close();
}
handlers.push(['memo', memoHandler]);

event.connection.request は先ほどの request-info でその内容を表示しましたが、リクエストの各種情報を保持したオブジェクトです。詳細は リファレンス を参照していただくとして、ここではこの記事で使うメンバのみをご説明します。

uri
リクエストされたパスが格納されています。このハンドラであれば、 "/test/memo" などとなっているはずです。
method
リクエストの HTTP メソッドです。 "GET", "POST", "DELETE" など。
bodyItems
POST されたフォームデータが格納されています。パラメータ名をキーとしたオブジェクト(ハッシュ)になっていて、各要素の値は常に配列です(同じ名前のデータが複数存在する場合があるため)。値は URL エンコードされているので decodeURIComponent で復元してください。送信データに日本語(マルチバイト文字)が入っていると文字化けしてしまいますが、これはフォームをマルチパートにするか、もしくは bodyItems の代わりに body を自力で解析することで回避できます。
files
マルチパートフォームでアップロードされたファイルを保持する File クラスのオブジェクトです。「ローカルファイルシステムにアクセスする」で使っていますので、そちらをご参照ください。

また、条件判定で使用している connection.isOwner は、クライアントがサーバーと同じ Opera で、かつ http://admin.〜 の URL でアクセスしている時のみ真になります。今回は、とりあえずこれを使って第三者がデータを書き込むことを防いでいます。手抜きですいません(^^ゞ

さて、忘れかけましたが、本題の Preference Store の処理を見てみましょう。前述のとおり、 Preference Store へのアクセスに使うメソッドは以下の 2 つだけです。

widget.setPreferenceForKey(保存する文字列, キー)
widget.preferenceForKey(キー)

今回のコードでは、フォームが POST され、かつ http://admin.〜 によるアクセスであれば、 setPreferenceForKey を使って送信された文字列を memoData というキーに保存しています。そして、(リクエストが GET か POST かに関わらず) preferenceForKey を呼び出して memoData の値を読み出し、それをテンプレートパラメータとして渡しています。

以上で簡易メモが完成です。サービスを再起動して、 "/test/memo" にアクセスしてみてください。テキストエリアに適当な文字列を入力して送信すると、その文字列が Preference Store に保存され、以降はリロードしてもその文字列が表示されるようになります。

Preference Store は非常にシンプルで検索機能などもないので、あまり複雑なデータを保存するのには不向きです。その代わり、サービスごとに完全に独立しており、他のサービスに影響を与えることも覗かれる心配もなく、安心して利用できます。また、後述のローカルファイルシステムへのアクセスと違い、サービスのインストール時にダイアログを出すこともありません。うまく使い分けて活用してください。

外部コンテンツへのアクセス

Opera Unite で外部コンテンツにアクセスするには、通常の XMLHttpRequest を利用します。これを使えば外部 API とのマッシュアップサイトなども構築可能なので、うまく活用したいところですね。ここでは例として、このブログの ATOM フィードを取得して、それをそのまま返すハンドラを作ってみましょう。

取得したデータをそのまま返すのでビューは必要ありません。 script.js に以下のコードを追加するだけで完成です。

function externalHandler(event) {
  var response = event.connection.response;
  xhr = new XMLHttpRequest();
  xhr.open('GET', "http://webos-goodies.jp/atom.xml", false);
  xhr.onreadystatechange = function() {
    if(xhr.readyState == 4 && xhr.status == 200) {
      response.setResponseHeader('Content-Type', 'application/xml; charset=utf-8');
      response.write(xhr.responseText);
      response.close();
    }
  }
  xhr.send(null);
}
handlers.push(['external', externalHandler]);

XMLHttpRequest に慣れている方なら、なにも迷うことなく理解できでしょう(もし XMLHttpRequest を使ったことがなければ、こちらの記事を参考にしてください)。

興味深いのは、非同期リクエストが正しく機能している点です。コールバックが呼ばれるのは明らかにリクエストハンドラの終了後なので、それまで(response.close が呼ばれるまで)コネクションが維持されていることになります。時間が足りなくて実際には試していませんが、この挙動をうまく利用すれば COMMET サーバーも実現できるかもしれません。どなたか、試してみませんか(笑)

ローカルファイルシステムにアクセスする

さて、最後は大物、ローカルファイルにアクセスするサービスを作ってみましょう。あまり複雑なものは無理なので、指定したディレクトリ直下のファイルのダウンロードとアップロードができるものを目指します。

Opera Unite のサービスからローカルファイルシステムにアクセスするには、 config.xml を以下のように変更する必要があります。

<?xml version="1.0" encoding="utf-8"?>
<widget network="private public">
  <widgetname>Test Service</widgetname>
  <description>Opera Unite のテスト</description>
  <feature name="http://xmlns.opera.com/webserver">
    <param name="type" value="service"/>
    <param name="servicepath" value="test"/>
  </feature>
  <feature name="http://xmlns.opera.com/fileio">
    <param name="folderhint" value="documents" />
  </feature>
</widget>

変更点は、二番目の feature タグの中に param を追加したことです。これによってサービスのインストール時にダイアログが開き、共有するディレクトリを指定できます。 value 属性はディレクトリ選択ダイアログの初期値を指定するもので、 documents のほかに home, pictures, music, video, downloads, desktop が指定できます。それぞれが具体的にどのディレクトリに対応するかは OS 依存ですので、まあ、試してみてください。

config.xml を変更したら、それを反映するためにはサービスを再インストールする必要があります。サイドパネルで「Test Service」をドラッグ&ドロップでごみ箱に移動し、その後 config.xml を Opera のウインドウ内にドラッグ&ドロップしてください。すると設定ダイアログが開くので、適当な共有ディレクトリを指定してください。

これだけではまだファイルアクセス API (File I/O API) が有効になっただけなので、次はそれを使ったリクエストハンドラを記述しましょう。 script.js に以下のコードを追加してください。

function fileHandler(event) {
  var connection = event.connection;
  var request    = connection.request;
  var response   = connection.response;
  var shared     = opera.io.filesystem.mountSystemDirectory('shared');
  var paths      = request.uri.split('?')[0].replace(/^\/+|\/+$/g, '').split('/');
  if(connection.isOwner) {
    if(paths.length <= 2) {
 
      if(request.method == "POST") {
        var stream = shared.open(request.files[0].name, opera.io.filemode.WRITE);
        stream.writeFile(request.files[0]);
        stream.close();
      }
      shared.refresh();
      var files = [];
      for(var i = 0 ; i < shared.length ; ++i) {
        if(shared[i].isFile)
          files.push({ name: shared[i].name, displayName: decodeURIComponent(shared[i].name) });
      }
      var template = new Markuper('file.html', { files: files });
      response.write(template.parse().html());
 
    } else if(paths.length == 3 && paths[2].indexOf('.') != 0) {
 
      var file = shared.resolve(paths[2]);
      var type = opera.io.webserver.getContentType(file.name);
      response.setResponseHeader('Content-Type', type);
      response.writeFile(file);
 
    }
  }
  response.close();
}
handlers.push(['file', fileHandler]);

だいぶ長いのですべては網羅できませんが、ポイントだけかいつまんでご説明します。まずはファイルシステムをマウントしている部分です。

var shared = opera.io.filesystem.mountSystemDirectory('shared');

これにより、ユーザーが指定したディレクトリを Opera Unite 内の仮想ファイルシステムの '/shared' にマウントします。 mountSystemDirectory の返り値はそのディレクトリを表す File オブジェクトです(クラス名に反して、 File クラスはディレクトリへのアクセスにも使います)。

次は、アップロードされたファイルを保存している部分です。

var stream = shared.open(request.files[0].name, opera.io.filemode.WRITE);
stream.writeFile(request.files[0]);
stream.close();

open メソッドは第一引数で指定されたファイルを開き、 FileStream オブジェクトを返します。第二引数はアクセスモードです。 writeFile は引数で指定された File オブジェクトの内容を読み込み、それをオープン中のファイルにそのまま書き込むメソッドです。これにより、アップロードされたファイル (request.files[0]) をローカルに保存しています。 close メソッドはもちろんファイルを閉じるメソッドです。

途中を抜かして、ローカルファイルをレスポンスに出力している部分を見てみましょう。

var file = shared.resolve(paths[2]);
var type = opera.io.webserver.getContentType(file.name);
response.setResponseHeader('Content-Type', type);
response.writeFile(file);

resolve メソッドは指定されたパスを指す File オブジェクトを取得するメソッドです。 open メソッドと似ていますが、ファイルを開くわけではなく、返り値も File オブジェクトです。そして、取得した File オブジェクトを responsewriteFile メソッドに渡し、ファイル内容をそのままレスポンスに出力しています。

また、 opera.io.webserver.getContentType メソッドでファイル名に対応する mime-type が取得できますので、それをレスポンスの Content-Type ヘッダに設定しています。

最後に、共有ディレクトリ内のファイルを検索している部分です。

shared.refresh();
var files = [];
for(var i = 0 ; i < shared.length ; ++i) {
  if(shared[i].isFile)
    files.push({ name: shared[i].name, displayName: decodeURIComponent(shared[i].name) });
}

File オブジェクトには配列と同じ添字演算子 [] と length プロパティーがあり、それを使ってディレクトリ内のファイルをリストアップしています。これらにアクセスするためには、事前に refresh メソッドを呼び出しておく必要があるので注意してください。配列添字演算子で得られるオブジェクトは対象ファイルを表す File オブジェクトですので、そのメンバを通してファイル名などが取得できます。

あとは、ビューとなる HTML テンプレートも作らなければなりませんね。 file.html という名前で以下の内容を保存してください。

<!DOCTYPE HTML>
<html>
 
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <title>ファイル共有</title>
</head>
 
<body>
<h1>ファイル共有</h1>
<ul>
  <li data-list="files">
	<a href="/test/file/{{files[].name}}">{{files[].displayName}}</a>
  </li>
</ul>
<form action="/test/file" method="post" enctype="multipart/form-data">
  <input type="file" name="file">
  <input type="submit" value="送信">
</form>
</body>
 
</html>

これで完成です。サービスを再起動して "/test/file" にアクセスすると、以下のように共有ディレクトリ内のファイルがリストアップされます。

各ファイルをクリックするとファイルがダウンロードされますし、フォームでファイルをアップロードすれば共有ディレクトリに保存されます。他の言語では当たり前のことですが、 JavaScript でファイルが自由に保存できるというのは、なかなか感動できますね(笑)。

かなり駆け足になってしまいましたが、以上のコードでローカルファイルへの一連のアクセスが可能です。このほかにも File / FileStream オブジェクトにはさまざまなメンバがあり、さらにきめ細かなアクセス(ファイルポインタを指定してのランダムリードも可能)やファイルの移動・コピー、ディレクトリの作成など多彩な操作が可能です。それらの詳細は File I/O API のドキュメントを参照してください。

パッケージングして配布する

サービスを作成したら、他のユーザーが簡単にインストールできるようにパッケージングしましょう。 Opera Unite サービスのパッケージは .ua という拡張子のファイルですが、実体は ZIP ファイルです。したがって、 ZIP 形式をサポートした圧縮ツールで config.xml のあるディレクトリをアーカイブし、その拡張子を .ua に変更するだけでパッケージング完了です。

例えば、コマンドラインで ~/unite 以下にあるサービスを ~/test.ua にパッケージングするなら、以下のようにします。 unite ディレクトリ自体はアーカイブに含めない点に注意してください。

cd ~/unite
zip -r ~/test.ua *

このようにして作成した .ua ファイルは、 Opera Unite 公式サイトの Upload ページで申請することで、同サイトのサービスディレクトリにて公開してもらえます(審査に多少時間がかかるようです)。

ただ、内輪だけで使うサービスだったり、開発中のサービスをテスト公開する場合など、公式ディレクトリには登録したくないこともあるでしょう。そんなときは .ua ファイルを自分の Web サーバーにアップロードし、 Content-Type が "application/x-opera-uniteapplication" になるように設定します。 Apache であれば、 .htaccess に以下の一行を書けば OK です。

AddType application/x-opera-uniteapplication .ua

オープンソース限定ですが、 Google Code Project Hosting を使用する方法もお勧めです。ご参考までに、記事中で作成したサービスの .ua ファイルを以下に公開してあります。

http://webos-goodies.googlecode.com/svn/trunk/blog/articles/getting_started_with_opera_unite/TestService.ua

※クリックするとインストールが始まるので注意してください。

Tips

最後に、 Opera Unite のサービスを構築する上で便利な Tips をいくつか挙げておきます。

静的なファイルの公開

記事中では使いませんでしたが、単に静的なファイルを公開するだけなら、 JavaScript でリクエストハンドラを実装する必要はありません。 config.xml と同じディレクトリに public_html というディレクトリを作成し、その中に保存すればそのまま公開されます。

public_html の内容はそのままサービストップページに反映されますので、例えば "public_html/foo.png" というファイルは、以下の URL でアクセスできます。

http://コンピュータ名.ユーザー名.operaunite.com/サービス名/foo.png

静的な画像やスタイルシート、クライアントサイドで使う JavaScript などは、この方法で公開するとよいでしょう。

グローバル変数を使う

前述のとおり Opera Unite のサービスはひとつのブラウザ・タブなので、 JavaScript のグローバル変数はサービスの終了まで保持されます。ですので、グローバル変数を一時的な記憶領域として活用できます。

例えばセッションオブジェクトや各種キャッシュなどはグローバル変数に格納するデータの第一候補でしょうね。その他、サービス終了時に消えてしまってもいいデータは、 Preference Store ではなくグローバル変数に保存するようにしましょう。

setInterval によるバックグラウンド処理

これも Opera Unite サービスがひとつのブラウザ・タブであることから可能になるテクニックです。 setTimeout や setInterval メソッドでコールバックを登録しておけば、通常の Web ページと同様に指定した間隔でコールバックが呼び出されます。

この機能を利用すれば、重い処理をリクエストの完了後に実行したり、特定の Web ページの内容を定期的にダウンロードしてローカルファイルに保存するようなサービスが実現可能です。いろいろと面白いことができそうですね。

ただし、コールバックの処理中はリクエストへの応答ができなくなるので、あまり時間のかかるような処理は、何回かに分けて実行するような工夫が必要です。 Web Worker があればなぁ・・・。

セッション ID について

Opera Unite では、同じマシンにインストールされたサービスはドメイン名を共有するので、 Cookie は本質的に安全ではありません。あまり使わない方が良いかと思います。

セッション ID については Opera Unite サーバーが unite-session-id という名前で自動的に発行してくれるので、それを使うのが良いでしょう。値を読み込む時は cookie ヘッダから普通に読み込めば OK です。

ここらへんを書いていた時は眠くて頭まわらなかったのですが、 unite-session-id は全サービス共通なので、もし悪意のあるサービスがインストールされていると簡単に盗まれてしまいますね。万全を期すなら、独自に署名を付けた Cookie をサービスのトップディレクトリに保存するべきなのかなぁ・・・。

まあ、プリセットされているサービスは普通に unite-session-id を使っているので、そこまで気にする必要はないのかもしれません。ただ、一方でローカルファイルへのアクセスを指定したディレクトリに限定するなど、セキュリティーにこだわっている部分もあるので、ちょっとちぐはぐな気がします。

Dragonfly でデバッグする

JavaScript の例外などは Opera のエラーコンソールに表示されるので、簡単なバグであれば問題を突き止められます。しかし、サービスが複雑になってくると、それでは限界があるでしょう。そんなときは、 Opera 内蔵の開発者向けツールである Dragonfly を活用しましょう。 JavaScript のステップ実行などの強力なデバッグ機能が利用できます。

Dragonfly を利用するには、まずデバッグ対象のサービスを起動した後、メインメニューの [ツール]-[詳細ツール]-[開発者用ツール] を選択します。すると Dragonfly の画面がウインドウ下部に表示されますので、その右上のトンボのメニューからデバッグするサービスを選べば、通常の Web ページと同様にデバッグできます。サービスの index.html に title タグを入れておくと、トンボメニューでの識別がしやすくなって便利です。

あとは、スクリプトのタブでソースコードの行番号部分をクリックすればブレークポイントが設定できます。その状態でデバッグ対象のページを開くと、ブレークポイントの場所で実行が停止し、ステップ実行や変数の表示などができます。 Dragonfly の詳しい使い方については、 Opera Dragonfly 入門 (Japanese) などを参照してください。

以上、本日は Opera Unite で独自サービスを構築する方法をご紹介しました。かなり駆け足になってしまったので、わかりにくい部分も多々あるかと思います。不明な点はコメントしていただければ、私がわかる範囲でお答えしようと思います。ぜひ、皆さんも独自の Opera Unite サービス構築に挑戦してみてください!


  • 2009/7/1 - セッションキーに関する記述を訂正。
  • 2009/7/2 - パッケージの拡張子が .uc になっていたのを、 .us に修正。 Thanks to t_ashula!
  • 2009/7/3 - 簡易メモで日本語が化けていたので、サンプルを修正して記述を追加。
  • 2009/7/3 - URL エンコーディングをかけているのは Preference Store ではなく request.bodyItems だったので、サンプルと記述を訂正。
  • 2009/9/22 - unite://〜 を http://admin.〜 に、 .us を .ua に、 application/x-opera-uniteservice を application/x-opera-uniteapplication に、それぞれ変更。
関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles