ログ日記

作業ログと日記とメモ

Wicketで素のAjax

WicketはAjaxBehaviorなどを使えば簡単にAjaxが実装できる。
しかし、内部システムだとこれでも良いが、一般公開のページだと都合が悪い。なぜならWicketに搭載されているAjaxクラスを使うと、ページは必ずステートフルになるからだ。
ステートフルのページは検索エンジンにクロールされたときに面倒な問題が起きる。*1
一般公開用のページはできる限りステートレスにしたい。


最近はjQueryを使っている。wiqueryというのもあるのだが、ステートレスなコンポーネントとステートフルなコンポーネントが混ざっていたような。(ちょっと使っただけなのであまり覚えてない)


問題はJavaScriptWicketとの連携。
必要なことは二つ。

  1. JavaからJavaScriptへのデータの受け渡し。
  2. JavaScriptからJavaへのデータの受け渡し。


1 の方は、最低でもAjax送信先となるページのURLを渡さないといけない。コンテキストパスやページクラス名が変わったからといってJavaScriptを書き換えるのは大変だ。取りあえず二つのコンポーネントを作った。
まずページクラスのURLをJavaScriptに変換する専用のクラス。

public class AjaxTargetHiddenField extends HiddenField<String> {
    private static final long serialVersionUID = -4517272943890825002L;
    public AjaxTargetHiddenField(String id, Class<? extends Page> page) {
        super(id);
        setModel(new Model<String>((String)urlFor(page, new PageParameters())));
    }
}

これはHiddenのvalueに入れているだけ。
もう一つ

public class JsonComponent extends WebComponent{
    /**
     * 
     */
    private static final long serialVersionUID = -2357713088979921265L;
    private final String HEADER = "\n/*<![CDATA[*/\n";
    private final String FOOTER = "\n/*]]>*/\n";
    private final String varName;
    public JsonComponent(String id, IModel<?> model, String varName) {
        super(id, model);
        this.varName = varName;
    }

    @Override
    public void onComponentTagBody(MarkupStream markupStream, ComponentTag openTag) {
        replaceComponentTagBody(markupStream, openTag,
            HEADER + "var " + varName + " = "
            + JSON.encode(getDefaultModelObject(), true) + FOOTER);
    }
}

jsonicを使って、JavaのオブジェクトをJavaScriptに変換する。これはHTMLでは

<script type="text/javascript" wicket:id="fooValues"></script>

のように書くと、JavaのオブジェクトをJSON形式で利用できる。これでJavaからidやラベルやその他諸々のデータをJavaScriptから使うための準備は整った。




次は 2 のAjaxで呼び出されるPageクラス。
Ajaxで呼び出されるページがHTMLを返してページの一部をまるごと書き換えるなら、特に何の処理も必要ない。
HTMLのテンプレートをいきなりdivから書けば良い。これはWicketのPanelと相性がいい。動的に表示を切り替えたい部分をPanelにしておけば、最初にページを描写するときもAjaxでページを描写するときも同じPanelが使える。
Ajaxの結果としてJsonを返したい場合は次のクラスを使う。

public abstract class JsonPage extends WebPage {
    private static final String CONTENT_TYPE = "application/json";

    @Override
    protected void configureResponse(WebResponse response) {
        super.configureResponse(response);
        response.setContentType(CONTENT_TYPE);
    }
    @Override 
    public final boolean hasAssociatedMarkup() {
        return false;
    }
    @Override
    public final void renderPage() {
    }

    public JsonPage(PageParameters params) {
        getRequestCycle().scheduleRequestHandlerAfterCurrent(
            new TextRequestHandler(CONTENT_TYPE, "UTF-8", JSON.encode(getJsonObject(params), true)));
    }
    abstract protected Object getJsonObject(PageParameters params);
}

これはJson専用ページの親クラス。このクラスを継承してgetJsonObjectで適当なオブジェクトを返せば、それがそのままJsonとしてレンダリングされる。




最後に、ちょっとハマったこと。
deploymentモードでステートレスなページだけを遷移しているとセッション用のCookieが発行されないようだ。
このとき、Ajaxの通信結果としてセッションに何かを格納するという処理を書いても動作しない。session id はまだ発行されていないのだから、当然Ajax通信時にCookieが送られないしWicket側で受け取りもできない。
以下のコードを書いて対応した。

        if (!WebApp.isAgent(AppSession.get().getClientInfo().getUserAgent()))
            AppSession.get().bind();

Ajaxからセッションを利用するページで、強制的にbindする。
isAgentというのはサーチエンジン用メソッドで
https://cwiki.apache.org/WICKET/seo-search-engine-optimization.html
ここに書いてある。

*1: jsessionidが付くとか毎アクセスごとにセッションが生成されるとか