JSONPライブラリ作った その2

[javascript][JSONP]JSONPライブラリ作った
http://d.hatena.ne.jp/futa23/20080203/p1

//関数を指定しつつ、スコープのバインド付き(UI部品をcallbackとして指定する場合)
<input type="button" onclick="new JSONP2('http://del.icio.us/feeds/json/futa23?', grid.build, grid)" value="JSONP呼び出し">


これ、今頃気づいたんだけど・・・
prototype.js使っているなら、Function.prototype.bindを使えば
3番目の引数のscopeっていらないじゃん。


これでOK。

//最後の引数は、bindの引数に指定。
<input type="button" onclick="new JSONP2('http://del.icio.us/feeds/json/futa23?', grid.build.bind(grid))" value="JSONP呼び出し">


scopeは必須の引数じゃなくてもいいので、使う際は

initialize: function(src, callback, scope){
//を
initialize: function(src, callback){
//に変更するだけ。

JSONPライブラリ作った・・・けどIE7で動かない?

あれ?IE7でしか動かないorz
Operaは大丈夫だけどIE7だと動かない?
動かないというかHTMLが表示できない。
なんで?
IE7はHTMLのチェックが厳しすぎるイメージ。

追記

OperaだとJSONPは同期処理になってしまうんだね。
その回避法。
TAKESAKO @ Yet another Cybozu Labs: Operaでも非同期リクエストが並列処理できる img-JSONP
http://labs.cybozu.co.jp/blog/takesako/2007/06/opera_img-jsonp.html

非同期リクエストをimgタグで飛ばして、結果をonerrorハンドラで受けとって、ブラウザのキャッシュを再利用するという方法。


すんごい回避方法だなぁ。
これも組み込んじゃう?

JSONPライブラリ作った

Javascriptを触りだして9ヶ月経つ。
昔のコードを見ると恥ずかしくなるなぁ(゚ε゚)キニシナイ!

クロスドメインでアクセスするための非同期通信としてJSONPはすごく便利だけど

ちょっとなぁってところが3つある。

  1. callback関数名を渡すところ
  2. scriptタグのremoveChildを書くのが面倒くさい
  3. callback関数のスコープ


1つ目

<script type="text/javascript" charset="UTF-8" src="http://del.icio.us/feeds/json/futa23?callback=コールバック関数名"/>

初めてJSONPを知ったJSONscriptRequestでもコールバック関数をURL内に文字列として指定してる。


文字列で指定するんじゃなくて関数で指定したい!
例えば、こんな風に指定できたほうが自然だよね。

new JSONP2('http://del.icio.us/feeds/json/futa23?', function(json){alert(json)})


2つの目

function callback(json){
 //描画
 //scriptタグの削除
}

scriptタグをappendChildして関数をコールするのがJSONPだけど
そのscriptタグの削除をcallback関数で書かないといけないよね?
まぁ別に放置しててもいいけどちょっと精神衛生上気持ち悪い。
scriptタグのremoveChildはライブラリ内でうまいことやっといてほしい。
ある関数を実行し終わったら必ず特定の処理をする、
この場合callback関数を実行したらJSONPコールに使ったscriptタグの削除。
つまりAOPみたいなことできないものか。


3つ目
GUIを描画する部品とか作ってJSONPでデータをもらうと
GUI部品をnewして

  //DIV#target=描画ターゲット
 grid = new GridBuilder($('target'));

そのインスタンスの描画メソッドにJSONPのデータをもらって描画って流れになると思う。
以下の例では、gridというインスタンスのbuildメソッドを呼び出してる。

<script type="text/javascript" charset="UTF-8" src="http://del.icio.us/feeds/json/futa23?callback=grid.build"/>

1つ目みたいな書き方をしつつ、スコープがgridになるようにしたい。

ということで作ってみた!JSONP2クラス。

JSONP2ってクラス名になってる。
callbackパラメータ名はcallback固定にしてあるけど、
こういうのもオプションで外から指定できるようにハッシュで引数を用意するのもアリ。

/*******************************************
* JSONP call<br/>
* @auther futa23<br/>
* @version 2008/02/03
********************************************/
JSONP2.counter = 1;
function JSONP2(src){
  this.initialize.apply(this, arguments);
}
JSONP2.prototype = {
  scriptTag: '',
  headTag: '',
  initialize: function(src, callback, scope){
      this.scriptTag = document.createElement("script");
    this.headTag = document.getElementsByTagName("head").item(0);

    this.scriptTag.id = "jsonp" + JSONP2.counter++;
    this.scriptTag.setAttribute('type', 'text/javascript');
    this.scriptTag.setAttribute('charset', 'UTF-8');
    var noCache = '&_noCache=' + new Date().getTime();
    var url = src + noCache;
    
    switch(typeof callback) {
      case 'string':
        url += '&callback=' + callback;
        break;
      case 'function':
        var callbackName = 'callback' + this.scriptTag.id;
        var self = this;
        scope = scope || window;
        window[callbackName] = function(json) {
                    return function() {
                        callback.apply(scope, arguments);
                        window[callbackName] = undefined;
                        try{ delete window[callbackName]; } catch(e){}
                        if (self.headTag) self.remove();
                    }(json);//これで実行される
        }      
        url += '&callback=' + callbackName;
        break;
      default:
        break;
    }
    
    this.scriptTag.setAttribute('src', url);
      this.headTag.appendChild(this.scriptTag);
  },
  remove: function(){
    this.headTag.removeChild(this.scriptTag);
  }
}


使い方。
1番目はAPIのURL。(これだけ必須)
2番目はメソッドの指定。
3番目はスコープの指定。protoype.jsのFunction.prototype.bindみたいなイメージ。

//URLにcallbackまで指定する場合(windowにcallbackとしてhundlerという関数を定義している場合)
<input type="button" onclick="new JSONP2('http://del.icio.us/feeds/json/futa23?callback=hundler')" value="JSONP呼び出し">

//上記例だとcallbackがわかりにくいので、callback関数名を指定
//switch文の分岐のstringの場合
<input type="button" onclick="new JSONP2('http://del.icio.us/feeds/json/futa23?', 'hundler')" value="JSONP呼び出し">

//関数を指定しつつ、スコープのバインド付き(UI部品をcallbackとして指定する場合)
<input type="button" onclick="new JSONP2('http://del.icio.us/feeds/json/futa23?', grid.build, grid)" value="JSONP呼び出し">


コールバック関数名を自動生成して、window[自動生成したコールバック関数名]としてURLに指定してる。
自動生成した関数では、引数でもらったコールバック関数をラップした関数を返して実行してる。
ラップした関数内で、引数でもらったコールバック関数の呼び出しとscriptタグの削除などを順にやってる。
その他、callback関数名を文字列で指定しても動くように引数のcallbackをtypeofで評価して
型がfunctionのときだけ上記処理をしてる。
文字列のときは、そのままURLに追加だけ。
あとはブラウザのキャッシュ抑制のための変数とか勝手に入れてる。


って、そういやjQueryJSONPコールがサポートされてるんだった。
jQuery で JSONP 2通り - てっく煮ブログ
これを参考にすればよかったorz
まさに車輪の再発明

$.ajax({
    url : "http://www.example.com/jsonp.cgi",
    dataType : "jsonp",
    data : {
        param1 : "value1"
    },
    success : function(json){
        // ロード完了時にここが呼ばれる
    },
    error : function(){
        alert('error');
    }
});

errorとか参考にさせてもらおっと。

コピーライト

しょーもないソースなんで自由に使ってください。
prototype.jsとか使ってないので単体で動きます。

感想

自分で作ったやつをリファクタリングしたくなってきた(´・ω・`)
カリー化ってのが初めてちゃんとわかったかも。

追記

第1引数だけだと、callback関数がわからないので
さすがにscriptタグの削除はできないです。
setTimeoutで10秒とか長めな遅延実行させてもいいけど、
そこまでして消さなくてもいいかなぁと。

追記2

[javascript][JSONP]JSONPライブラリ作った その2
http://d.hatena.ne.jp/futa23/20080217/p1

エロくないやつも作ってみた



mixiの日記一覧や日記の個別ページで、サムネイル画像をクリックすると
オーバーレイされてオリジナルの大きな画像が表示されます。
おまけでプロフィルページの「ほかの写真を見る」の横にアイコンが出て来るので
クリックするとプロフィール画像が全てオーバーレイ表示されます。
表示された画像をクリックすると、オーバーレイが終了します。


mixiImager -- Userscripts.org
http://userscripts.org/scripts/show/12959
あまりソースはきれいじゃないのはご愛敬。


ほんとはLightBox.js(http://www.huddletogether.com/projects/lightbox/
みたく中央に枠をつけて表示させたかったんだけど
画像のwidthとheightが取得できなかった。
なんでだろう?


急にテキストエリアで改行できなくなったorz
なんで?

やっぱり技術革新の源はエロだね

前から書いてみたかったGreasemonkeyを書いてみた。
あと、ついでにXPathも使ってみた。
初めてのことづくしで丸一日かかったorz
よくわかってないので、おかしなソースを書いているかもしれないがキニシナイヽ(`Д´)ノ
download->DMMimager -- Userscripts.org
早速、試したい方はこちら(成人リンク)*1

できること

  1. DMMでの全サムネイルのオートロード
  2. DMMでのジャケット写真の先読み

「DMMでの全サムネイルのオートロード」
DMMでは「全部見る」というリンクをクリックしないと
全てのサムネイルが見れないのを、勝手にajaxでロードさせます。

「DMMでのジャケット写真の先読み」
ジャケット写真にmouseoverさせると、リンク先の拡大写真を表示する。mouseoutで消える。
拡大写真のページはウィンドウサイズを勝手に変更してきやがりますが、それから解放される。

しょーもない技術的な説明

「全部見る」というTextNodeを持つAncherを取るためにXPathを使いました。

$X('//A[contains(string(.),"\u5168\u90E8\u898B\u308B")][1]')
\u5168\u90E8\u898B\u308B

は「全部見る」の16進数表記。こうしないと動きませんでした。


$XはXPathを使うためのユーティリティ関数。
私製版、GreasemonkeyでXPathを楽に使う関数 @蕪浅録奏から頂きました。
ありがとうございます。
$X内で使ってるlog関数はfirebugのコンソールに出すためのログ関数。
無駄に指定しなきゃいけないnullとか無駄に長いsnapshotLengthやsnapshotItemから解放されます。

function $X(xpath, node) {
        var node = node || document
	log('xpath=' + xpath);
        var nodesSnapshot = document.evaluate(xpath, node, null,
            XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
        var data = []
        for (var i = 0; i < nodesSnapshot.snapshotLength; i++) {
            data.push(nodesSnapshot.snapshotItem(i))
        }
        return (data.length >= 1) ? data : null
}

var logEnable = false;
function log(str) {
	if(console.log && logEnable) console.log(str);
}

その他

ロードするHTMLページをさらにXPathするために、
HTMLドキュメント化する必要があったのでLDRizeからソースを拝借しました。
thanks to id:swdyh!
でも実はよくわかってないw

function createDocumentFragmentByString(str) {
	var range = document.createRange()
	range.setStartAfter(document.body)
	return range.createContextualFragment(str)
}
function createHTMLDocumentByString(str) {
	var html = str.replace(/<!DOCTYPE.*?>/, '').replace(/<html.*?>/, '').replace(/<\/html>.*/, '')
        var htmlDoc  = document.implementation.createDocument(null, 'html', null)
        var fragment = createDocumentFragmentByString(html)
        htmlDoc.documentElement.appendChild(fragment)
        return htmlDoc
}

バグ

  1. 一覧画面でもジャケット写真と勘違いして取ってきてしまいます
  2. 商品ページによっては、サムネイルの配置が崩れます

1つ目のバグはジャケット写真を1つ目のJPEG画像で取っているため。
回避方法が浮かばないのと、実害がないので無視。
その他、バグがあったら教えてください。

*1:人気ランキング1位だそうです

サードパーティのjarの設定方法(orz訂正)

前回悩んだところはTECHSCOREに載ってましたorz


Maven-TECHSCORE-
3.1.2. groupId, artifactId, version に指定する値の調べ方
http://www.techscore.com/tech/ApacheJakarta/Maven/3-2.html#maven-3-1-2


Maven Repo Search
http://maven.ozacc.com/
で検索すると、groupId, artifactId, versionに出てくるらしい。


でも、ここで検索しても取得できないことがあるみたいだから
そういうときは
http://repo1.maven.org/maven2/
に直接見に行ったほうがいいかなと思う。

サードパーティのjarの設定方法

mavenを初めて触った。
悩んだところをメモ。
依存するサードパーティのjarの設定の仕方がいまいちわからなかった。
でも、たぶん自分の想像通りだと思うので書きます。


ところで、3,4年前にantを少しだけ触ったことがあるけど、
コンパイルと実行くらいしか使ってなかったんよね。
しかも、そのときやりたかったのは、実行結果をファイルに出したかっただけ。
System#setOutでできたのに。
それを知らず・・・orz


標準出力をファイルにするには? - Java Solution
http://www.atmarkit.co.jp/bbs/phpBB/viewtopic.php?topic=18777&forum=12&2


でも、意外にSystem#setOutのこと知ってる人少ないと思う。



それはさておき。


maven
antの後継だってことしか知らなかったので、良さそうなサイトを探してみた。


Maven2のTipsを集めるWiki - CookBook
http://wiki.fdiary.net/maven2/?CookBook


ここに書いてあるように、
Javaで開発するときのお薦めフォルダ構成を作ってくれたり
eclipseのプロジェクトファイルを作ってくれたり
その他もろもろ便利。
Javaで開発する際は、まずmaveneclipseのPJを作るところからやるべきだね。


というわけで、
とりあえず、上から順に試してみた。
ソースはWebDav+SVNで管理してるので、wgetで取得。
mavenお勧め構成になってないので、mvしたりrmしたりきれいにする。


で、pom.xmlというXML構文の設定ファイル作成へ移る。


Maven centralリポジトリ以外で提供されている依存ライブラリを設定する
http://wiki.fdiary.net/maven2/?CookBook#l11
ここに書かれてることで少し悩んだ。


mavenってのはyumみたいにリポジトリという場所でjarを管理してる(ものがある。)
あと、rubyperlなんかとも同じ。
デフォルトのリポジトリはここ。
http://repo1.maven.org/maven2/


ここに、いろんなjarが詰まってる。
commons-langだと、リポジトリトップ下にcommons-lang/commons-lang/2.3/
って感じで管理されてる。
1つ目のcommons-langがgroupId、2つ目のcommons-langがartifactIdとなる。
最後はもちろんバージョン。


このリポジトリで管理してるjarは

    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.3</version>
      <scope>provided</scope>
    </dependency>

以下に書けば、mvn compile時に勝手にダウンロードしてくれる。


リポジトリにないjarは、ローカルに落したjarをmvn installすると、
ローカルのリポジトリに追加される模様。
この場合のmvn installで指定するgroupIdやartifactIdは一意になれば自由っぽい。
ここで指定したgroupIdやartifactIdをで書いてやると、ローカルのリポジトリを見に行く模様。


あとは、コンパイルするJavaのバージョンやら文字コードやらを指定してmvn compile。

    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <configuration>
        <encoding>SHIFT-JIS</encoding>
        <source>1.6</source>
        <target>1.6</target>
      </configuration>
    </plugin>


今のところここまで。
また何かあったら書きます。