Sencha TouchでのクロスドメインなJSONP通信

12/1に、福岡RubyKaiki01で、ぜんぜんRubyとは関係ないSencha Touchの話をしてきました。そのときに紹介したJSONPのコードについて。
JSONPについては、サーバーの記述も必要なので、本には書いてなかった、というか書かないから調べてなかったので。

Sencha Touchとは?

HTML5+JavaScript+CSS3用のモバイルアプリケーションフレームワークです。HTML5とはいえ、WebKit用なので、Firefoxなどではうまく動きません。
どんな感じかは、次のKitchenSinkをChromeSafariで見てみてください。
http://cdn.sencha.io/touch/sencha-touch-2.1.0/examples/kitchensink/index.html


サンプルのソースを見るとわかりますが、基本的にはHTMLのタグを書くことはなく、ほぼJavaScriptのみを記述します。

実行準備

Sencha Touchのライブラリはここからダウンロードします。
http://www.sencha.com/products/touch/download/


アプリケーションなどの雛形を作成したり、公開用パッケージを作成するのにSencha Cmdを使います。
http://www.sencha.com/products/sencha-cmd/download


Sencha CmdはAntを使っているので、たぶんJavaがインストールされている必要があります。入れてない人はJDKをインストールしてください。
http://www.oracle.com/technetwork/java/javase/downloads/index.html


環境変数JAVA_HOMEにJDKをインストールしたパスを設定しておく必要があるかもしれません。
追記:2012/12/05
アプリケーションの生成やパッケージングではJAVA_HOMEの設定は不要です。Androidのネイティブパッケージを作成するときに必要になります。

まずはリスト表示

Sencha Touchライブラリのフォルダで次のコマンドを実行します。

> sencha generate app ListApp ..\list

最後のパラメータは生成されるフォルダなので、適当に。


そしたら、app/view/Main.jsに次のようにitemsのところにBooksの項目を追加します。requiresには、StoreとListを追加する必要があります。JsonPは、あとでJSONPを使うときに必要です。

    requires: [
        'Ext.TitleBar',
        'Ext.Video',
        'Ext.data.Store',
        'Ext.dataview.List',
        'Ext.data.proxy.JsonP'
    ],
    config: {
        tabBarPosition: 'bottom',

        items: [
            {
                title: 'Books',
                iconCls: 'bookmarks',
                xtype: 'list',
                items: {
                    docked: 'top',
                    xtype: 'titlebar',
                    title: 'Books'
                },                
                store:{
                    fields: ['title', 'author'],
                    data:[
                        {title: 'Rubyいいよいいよー', author: 'まっつ'},
                        {title: 'あじゃいるあじゃいる', author: 'かくたに'}
                    ]
                },
                itemTpl: '<div><div>{title}</div>' +
                        '<div style="color:gray">{author}</div></div>'
            },
            {
                title: 'Welcome',
                iconCls: 'home',


これで、ブラウザでアクセスすると、次のように表示されます。

ただし、Sencha Cmdで生成したアプリケーションは、起動にAjaxを使っているので、Webサーバーを通す必要があります。nginxがインストールや設定が楽でいいです。

JSONP用のサーバーを用意する

JSONP用には、ポートを変えて動くWebサーバが必要です。nginxをポート変えて動かしてもいいのですが、やはりなんらか動的に動いたほうがいいので、Javaでサーバーを書きます。ここでRubyで書いてたらRubyKaigi的によかったのだけど。Railsでやろうとしたのが間違いで、最初からこうやってWebサーバーごと書けばよかったのですね。

package Senchaサーバー;

import java.io.*;
import java.net.*;
import java.util.Random;

public class JSONPサーバー {
    public static void main(String[] args) throws IOException{
        String[] objs = {"Ruby", "Rails", "JavaScript", "Java", "Haskell", "かくたに"};
        String[] suffix = {"かっこいい", "さいこー", "いかす", "クール!"};
        String[] author = {"デビッド", "まっつ", "きしだ", "はるやま"};
        ServerSocket ss = new ServerSocket(8880);
        for(;;){
            try(Socket accept = ss.accept();
                InputStream is = accept.getInputStream();
                InputStreamReader isr = new InputStreamReader(is);
                BufferedReader br = new BufferedReader(isr);
                OutputStream os = accept.getOutputStream();
                PrintWriter pw = new PrintWriter(os))
            {
                String method = br.readLine();
                String path = method.split(" ")[1];
                String callback = "callback";
                int idx = path.indexOf("?");
                if(idx >= 0){//パラメータの分解
                    String query = path.substring(idx + 1);
                    String[] params = query.split("&");
                    for(String param : params){
                        String[] sep = param.split("=");
                        if(sep.length >= 2 && sep[0].equals("callback")){
                            callback = sep[1];
                        }
                    }
                }
                //出力
                while(!br.readLine().isEmpty() ){}//ヘッダーを読み飛ばす
                pw.println("HTTP/1.1 200 OK");
                pw.println("Content-Type: application/x-javascript");
                pw.println();
                pw.println(callback + "({books:[");
                Random r = new Random();
                for(int i = 0; i < 3; ++i){
                    pw.printf("{title:'%s%s', author:'%s'},%n",
                            objs[r.nextInt(objs.length)],
                            suffix[r.nextInt(suffix.length)],
                            author[r.nextInt(author.length)]);
                }
                pw.println("]})");
            }
        }        
    }
}


これを実行すると、こんな感じのJSONが吐かれます。

callback({books:[
{title:'Railsかっこいい', author:'まっつ'},
{title:'かくたにいかす', author:'きしだ'},
{title:'Javaさいこー', author:'まっつ'},
]})

callbackパラメータで受け取った名前の関数呼び出しの形にしておくところがミソです。
データ内容に意図はありません。ランダムです。

JSONPでデータを受け取る

さて、それではJSONPでデータを受け取るようにします。先ほどのMain.jsのdata部分をproxyに書き換えます。

                store:{
                    fields: ['title', 'author'],
                    proxy:{
                        type: 'jsonp',
                        url: 'http://localhost:8880/book',
                        reader: {
                            type: 'json',
                            rootProperty: 'books'
                        }
                    },
                    autoLoad: true
                },


このようにすると、読み込み時に次のようなURLが呼び出されます。

/book?_dc=1354558750232&page=1&start=0&limit=25&callback=Ext.data.JsonP.callback1

今回は関係ないけど、pageやlimitなどのパラメータも渡ってくるので、適切に処理する必要があります。


これを実行すると、次のように受け取ったデータが表示されます。

同一ドメインの場合は、typeをjsonpではなくajaxにすると、通常のAjax通信になります。


このように、Sencha Touchでは簡単にJSONPで他サーバーから受け取ったデータを表示することができます。


Sencha Touchの概要はこの本で(^^;