2011/12/20

Web Storage APIを別空間から使ってみる - HTML5 Adv




12月20日のHTML5 Advent Calendar担当の@shoitoです。

今回は、以前にも本ブログで軽く紹介したことがある、Web Storage APIをFlash側と共有する簡単な方法について紹介します。業務でHTML5(File APIやWeb Storage, Application Cache...)以外にも、Flash/Flexもまだ現役で使用中(主にメンテ)なので、その両方に関係するネタを扱うことにしました。

ページの一部にFlashが埋め込まれているようなページで、HTML部分とFlash部分の共有ストレージ領域を設けられないかと思って、Web Storage APIに今回白羽の矢が立ちました。

多くのブラウザで利用できるAPIであることが大事ですので、Web Storage APIについて確認してみると、IE6, 7以外のほとんどのブラウザで使えることがわかります。

イメージ的には下記のようになりますが、Web Storage APIはもちろんJavaScript APIなので、ページ内のJavaScriptから簡単に扱えます。しかし、Flash側からWeb Storageのストレージ領域にアクセスするAPIがありません。そこでActionScriptでWeb Storage APIを実装します。青の矢印部分。

まず、ActionScriptのAPIを実装するために、W3Cで公開されているWeb Storage API仕様を確認すると、引数、戻り値の方は基本的にvoidかDOMStringなので扱いやすそうです。
Web Storage API自体シンプルなので、メソッドの数も多くはありません。なお、Local StorageとSession Storageはインターフェースは同じで、スコープが違うだけです。

次に、実装に用いるflash.external.ExternalInterfaceクラスのリファレンスに目を通すと下記のように説明されています。
ActionScript から、HTML ページに対して次のことを実行できます。
  • 任意の JavaScript 関数を呼び出す 
  • 引数の数を名前と共に渡す 
  • ブール(Boolean)、数値(Number)、ストリング(String)などの各種データ型を渡す 
  • JavaScript 関数からの戻り値を受け取る 
HTML ページの JavaScript から、次のことを実行できます。
  • ActionScript 関数を呼び出す
  • 標準の関数呼び出しの表記法を使用して、引数を渡す
  • JavaScript 関数に値を戻す
注目すべき点を太字にしましたが、ExternalInterfaceを利用することで、ActionScriptからJavaScript APIを利用できることが分かります。下記のようなイメージになります。

これに着目して、実装したActionScript APIが下記になります。
package com.google.code.as3webstorage {
    import flash.external.ExternalInterface;

    public class LocalStorage {
        public static function length():uint {
            return ExternalInterface.call("function() { return localStorage.length; }");
        }

        public static function key(index:uint):* {
            return ExternalInterface.call("localStorage.key", index);
        }

        public static function getItem(key:String):* {
            return ExternalInterface.call("localStorage.getItem", key);
        }

        public static function setItem(key:String, data:*):void {
            ExternalInterface.call("localStorage.setItem", key, data);
        }

        public static function removeItem(key:String):void {
            ExternalInterface.call("localStorage.removeItem", key);
        }

        public static function clear():void {
            ExternalInterface.call("localStorage.clear");
        }
       
        public static function available():Boolean {
            return ExternalInterface.available && ExternalInterface.call("function() { return typeof localStorage != 'undefined'; }");
        }
       
        public static function addStorageEventListener(func:Function, useCapture:Boolean = false):void {
            ExternalInterface.call("as3webstorage.assignSwf", ExternalInterface.objectID);
            ExternalInterface.addCallback("callbackToAs", func);
            ExternalInterface.call("as3webstorage.addStorageEventListener", "callbackToAs", useCapture);
        }
    }
}
ActionScriptに見慣れていない方には分かりにくいと思います。
どういうことをしているかと言うと、getItem()やsetItem()など各メソッドの中で、JavaScriptによるWeb Storage API実装を呼ぶようにしています(ラップしているだけ)。
call()メソッドの第一引数にJavaScriptコードの文字列表現、第二引数に各Web Storage APIに渡す引数を指定しています。こうすることで、ExternalInterfaceを通して、JavaScript <---> ActionScript連携が実現できます。しかし、これだけだとWeb Storage APIのStorageイベントを扱うことができません。

そこで、addStorageEventListener()用のコールバックをJavaScript側に実装します。
(function() {
    if (this.as3webstorage) return;
   
    var as3webstorage = this.as3webstorage = {
        swf: null,
       
        assignSwf: function(swfId) {
            this.swf = document.all ? window[swfId] : document[swfId];
        },

        addStorageEventListener: function(callback, useCapture) {
            window.addEventListener("storage", function(event) {
                var returnObject = {
                    "key": event.key,
                    "oldValue": event.oldValue,
                    "newValue": event.newValue,
                    "uri": event.uri,
                    "source": undefined,
                    "storageArea": undefined
                };

                as3webstorage.toNativeFunc(as3webstorage.swf, callback).apply(null, [returnObject]);
            }, useCapture);
        },
       
        toNativeFunc: function(obj, functionName) {
            return function() {
                var parameters = [];
                for(var i = 0; i < arguments.length; i++) {
                    parameters[i] = "_" + i;
                }
               
                return Function(
                    parameters.join(','),
                    'this["' + functionName + '"](' + parameters.join(',') + ')'
                ).apply(obj, arguments);
            };
        }
    };
})();
サンプルを作ったので下記で試せます。
http://dl.dropbox.com/u/227786/code/flex/as3webstorage/as3localstorage.html
本当にWeb Storageが使われているかはデベロッパーツールなどを使って確認してください。

ExternalInterfaceでJavaScript <---> ActionScript間のデータ交換はXML形式で行われるので、オブジェクトによってはデシリアライズできないものもあります。
そのため、シンプルなインターフェースなら今回のように実装することが可能なのですが、Indexed Database APIのような複雑なものだと難しそうです。

あ、これって誰得?ActionScriptの話が多くない?という感じで終わってすいません。
HTML5 Advent Calendar 2011はまだ続きますのでお楽しみに。

関連記事
Flash + HTML 5 : Offline Flash Apps using DataStore API
http://blog.flexgeek.in/2010/05/flash-html-5-offline-flash-apps-using-datastore-api/ 
HTML5 Web Storage APIをFlash/Flexから使う - as3webstorage
http://blog.air-life.net/2010/06/as3webstorage.html 
HTML5 Geolocation APIをFlash/Flexから使う - as3geolocation
http://blog.air-life.net/2010/06/as3geolocation.html