Category Archives: Mayaa

そろそろ2年間Mayaa使ってわかったことを書く

僕は、2年ほど前に、所属している会社の主要プロダクトであるのリニューアルに係わりました。その際、Mayaaを採用し、今日までにそれを運用してノウハウが蓄積してきたので、ここに発表しようと思います。

経緯

私たちはコンシューマ向けのビューを多く扱うので、プログラミング工数を要せずデザイナーがダイレクトにデザインをカスタマイズできる仕組みの実現が急務でした。

以前はテンプレートにJSPを採用していたため、デザインを作る(HTMLを書く)デザイナーと、プログラムに打ち込むJSPコーダーが必要という状況でした。一方ブログサイトなどでは、CSSレベルでのデザインカスタマイズをサポートしていることは多いですが、それでは自由度に限りが出てしまいます。フルHTMLレベルでのテンプレートエンジンが必要でした。

結果的に、この状況がどのように変化したのかは下記のエントリにも書きましたので、こちらも合わせて読んでいただけると幸いです。

プログラマーとデザイナーの境界が縮まってきた気がする

今回は技術よりのこと、及び運用していく上で培ったノウハウを紹介したいと思います。

JavaServerTemplates "Mayaa" とは

前置きが長くなってしまいましたが、自由なテンプレートの実現のため、Mayaaというライブラリを採用しました。

http://mayaa.seasar.org/

MayaaはSeasar2系の日本発オープンソースライブラリですが、Seasar2に関係なく使用することができます。JSPと同じレイヤーで動作し、JSPのようにServletからforwardして使うことができます。以下のページの図が分かりやすいです。

http://mayaa.seasar.org/documentation/about.html

Mayaaを導入する以前のテンプレートエンジンはこんな感じでした。

<% if (a != null) { %>
hello, <%= a %>
<% } %>

同じようなテンプレートをMayaaで書くとこんな感じです。

<div m:id="IF_A_EXIST">
    hello, <span m:id="A_HERE">dummy</span>
</div>

という感じで、普通のHTMLにm:idという属性が付いていますね。このようにしておくと、テンプレートファイルをそのままブラウザでプレビューすることが可能ですし、HTMLエディタ、例えば Dreamweaver で編集することができます。

Mayaaのすばらしさ

「テンプレートファイルをそのままブラウザでプレビューすることが可能です」といっても、簡単なようで簡単なことではありません。例えば画像の相対パスです。

<img src="images/xxx.jpg" />

ローカルではこのようになんの変哲もなく動く感じですが、サーバにデプロイしてみるとうまく動かないかもしれません。例えば、テンプレートは"view/hello.html"だけど、URLマッピングは別に管理されていて、"action/hello.do"かもしれません。そうすると、上記部分は

<img src="../view/images/xxx.jpg" />

<img src="http://something.example.com/view/images/xxx.jpg" />

のように書き換えるか必要があります。しかし、このようにしてしまったら、今度はローカルで画像が見られなくなったりします。Mayaaにはこの問題を解決するために、PathAdjusterという機構があります。

http://mayaa.seasar.org/documentation/path_adjust.html

詳しくは上記リンクに任せて割愛しますが、大ざっぱに言うと、一番目の記述を三番目の記述に実行時に書き換えてくれる機構です。これを応用すると、例えば「画像パスにtimestampを付与してmod_expireと組み合わせたクライアントキャッシュ対応」なんてこともできてしまいます。

その他にも、Webのフロントとして使う上でかゆいところに手が届く機能が結構付いているので気に入っています。

はまったこと

とはいえ、ここまで順風満帆というわけではありませんでした。例えばこんなころにハマりました。

最初が取っ付きにくい

よく言われます。これが故に、敬遠する方も多いようです。しかし、慣れてみると何も苦はなく、もうJSPには戻れません。ドキュメントが非常によく整備されていますので、とっつきやすさで言えば取っ付き易いほうだと思います。

とはいえ教育コストがかかります(mayaaファイルを覚えなければならない)

mayaaファイルという聞きなれない独自のファイルの記述が必要です。しかも、これを書くには、幾つかあるmayaaプロセッサーをマスターしていないと心もとないところです。

http://mayaa.seasar.org/documentation/processor_reference.html

そこで、mayaaファイルを書ける技術者の数は限られてしまいます。そのためには、「なるべくテンプレート(HTML)側に制御を委ねる」アプローチが必要かなと思います。(詳しくは後ほど書きます)

JavaScriptが気持ち悪い

mayaaでは、Javaオブジェクトとテンプレートとの橋渡しにJavaScript(Rhino)を使用します。Javaとそのまま埋め込めるJSPとは異なるので、「Javaのように見えて少し違う」書き方をしなければなりません。Javaだと思って書いていると、うっかり思いもよらないバグを作ってしまってハマります。これに対するアプローチは以下の二つかなあと思っています。

  • 出来る限りJavaに委譲する
  • 出来る限りJavaScriptに統一する

従来ならば、前者の書き方が理想だと思われますが、そのような方法を2年ほどやってきた結果、少々冗長さを感じています。ちょっとした変更でもサーバの再起動が必要なのも億劫です。(本番環境だと簡単に再起動できませんしね?)今はサーバーサイドJavaScriptも広く利用されています。今後は後者のアプローチも有効ではないかと思っています。 実際にやってみて失敗でした。JavaScriptのビルドや実行はJavaより遅いので多用するとパフォーマンス上の問題があります。JavaScriptはなるべく使わず可能な限りJavaに寄せるのが正解です(2014/07/02 追記)

また、別件ですが、mayaaファイルはXMLなので、以下のような冗談みたいな記述もあります

<m:if test="${hoge &gt; 0}">

本当はif (hoge > 0)と書きたいところなのですが、mayaaがxmlファイルなので、">"が書けずこのような書き方になってしまいます(笑)どうしても">"と書きたい場合は、""の中をCDATAにすればいいのですが、

<m:if test="${<![CDATA[hoge > 0]]>}">

さらに冗長になっちゃいました(笑)

負け惜しみとしてはmayaaのおかげで鍛えられて、もうltとgtがどっちかなんて迷うことはなくなりました(笑)

私たちが採用したノウハウ集

実際に使っていて培ったノウハウを発表します。

m:idに命名規則を使用する

mayaaには数多くのプロセッサーが存在しますが、HTML側から見ると、下記の4種類に集約されることがわかりました。

  • 変数を出力する
  • 属性を書き換える
  • 条件を満たす場合のみ表示する(条件を満たさなければ消える)
  • 特定の条件のもと繰り返す

それぞれを、"〜HERE"、"〜_TAG"、"IF〜"、"LOOP_〜"と命名するように統一しました。このようにすると、こんなこともできてしまいます。

Mayaaファイルを命名規則から一括作成するEmEditorマクロ

デザイナーとプログラマーで使う名前空間を分ける

Mayaaでは、動的なタグの紐付けにHTMLの"id"属性を使用できますが、"m:id"のように独自の属性を使用することができます。まずは有無を言わずそちらを採用します。

http://mayaa.seasar.org/documentation/equals_id_resolver.html

それでもどうしても、id属性やclass属性を使用したくなることがあります。これはMayaaに限らず一般論ですが。そこで、「プログラマーは全て大文字を使う」「デザイナーは全て小文字を使う」などの棲み分けをすると良いと思います。

xpath指定は使用しない

Mayaaではidの代わりにxpath指定でタグをコントロールすることができます。

http://mayaa.seasar.org/documentation/xpath.html

が、この方法は緊急時を除いて使用するべきではありません。あとからどうにもならなくなって泣けてきます。。。

また、パフォーマンス上の問題もありますので、できれば、templateResolverリストから外してしまうことをおすすめします。(2014/07/02 追記)

デバッグ用関数は値を返す

こんな関数を用意してdefault.mayaaから呼び出すようにするとデバッグで便利です。Javaのような豪華なデバッガがあれば本当はいいんですけどね

function debug(value, tag) {
    if (tag == null) tag = 'debug';
    java.lang.System.out.println(tag + "t" + tag);
    return value;
}

プログラムは文言を吐き出さない

以下のような書き方は、文言の変更にプログラムの修正が必要となってしまいます。

性別:<span m:id="USER_SE×_HERE">男性</span>

※上記コードは、中学生並みの恥ずかしがりではなく、変なサーチキーワードに引っかからないように一部改変しています。

例えば、「男性」という文言ではなく、画像にしたいかもしれません。英語化するかもしれません。以下の書き方が良いです。

性別:<span m:id="IF_USER_MAN">男性</span><span m:id="IF_USER_WOMAN">女性</span>

id一覧表を自動生成する

mayaaファイルはxmlファイルなので、比較的簡単にパースできます。XMLコメントもパースの対象にできるので、それを元にid一覧表を作るのは容易です。JavaDocを自作するイメージです。(ソース載せようとしたらちょっと長いので割愛します。SAXを使うとスムーズに実装できました。

http://www.atmarkit.co.jp/fxml/rensai/xmljava04/xmljava02.html

また、ドキュメントの出力部分では、以下を参考に、ここでもMayaaを使用しました(笑)

http://d.hatena.ne.jp/terazzo/20071116/1195250832

PathAdjusterをうまく使う!

Webサイトですから、ハイパーリンクを多用します。この時、いちいちリンク一つ一つにm:idを発行していたら大変です。そういう時は、PathAdjusterでなんとかしましょう。僕は、adjustRelativePathのオーバーライドで、拡張子が".html"の場合はsuper.adjustRelativePath(base, path)に引き継がず、独自で処理するようにしました。結果的に、リンクは以下のように書くことになっています。

<a href="member_input.html">会員登録</a><!-- 実行すると、servlet用のURLに変換される -->

このように、PathAdjusterをうまく使うことで、発行するidの数を減らすことは非常に強力です!

一つのmayaaファイルで複数のテンプレートを使い回す。

Mayaaは設定ファイル

  • org.seasar.mayaa.provider.ServiceProvider
  • org.seasar.mayaa.source.PageSourceFactory

などの記述で非常に柔軟にエンジンをカスタマイズできます。

前述したとおり、mayaaファイルはできるだけ少なくしたほうが得策です。そこで、一つのページをベースに複数の派生ページがデザインレベルで作れると幸せです。以下のエントリでその試みを成功させました。

Mayaa ファイルの共通化

そうこうしていると、「派生したページでちょっとだけm:idを追加したい」なんていう要望も発生してしまいました。これについても、EqualsIDInjectionResolverのカスタマイズで対応することができてしまいました。

http://ml.seasar.org/archives/mayaa-user/2010-January/000893.html

これは、当時での実現方法ですので今は、https://www.seasar.org/issues/browse/MAYAA-76
が実装されたのでさらに簡単にできると思います。

m:idの数を減らす

例えば、「名前の頭30文字を出力して残りは...にする」「名前の頭60文字を出力して残りは...にする」「名前の頭60文字を出力して残りは...にする」のように、パラメータが存在する制御をしたい時があります。そんなとき、

  • NAME_30_HERE
  • NAME_60_HERE

のようにいちいちつくっていたらヤボです。はじめのうちは本当にそんな感じで対応していたためミスもありました。

今は以下の方法でパラメータ化を実現しています。

function $p(name, defaultValue) {
    var attr = originalNode.getAttribute(
        Packages.org.seasar.mayaa.impl.engine.specification.SpecificationUtil.createQName(name)
    );
    if (attr != null) return attr.value;
    attr = originalNode.getAttribute(
        Packages.org.seasar.mayaa.impl.engine.specification.SpecificationUtil.createQName(originalNode.getDefaultNamespaceURI(), name)
    );
    return defaultValue;
}

テンプレート側では、以下のように書けばOKです。

<span m:id="FREE_HERE" m:free_no="20">自由項目20番</span>

m:を抜かすと実行後のマークアップにfree_noが残ってしまいますのでつけることをおすすめします(2014/07/03 追記)

コメントはこう書け!

JSPだとこんな風なコメントが使用できました。

<%-- ほげほげ --%>

開発が切羽詰ってくるとコメントも荒れてきます。うっかり外に出してはまずい言葉を書いてしまうかもしれません。JSPコメントならば出力されないのでセーフですが、mayaa+HTMLだと、この書き方ができないので、うっかりテンプレートに開発者しかしらない内部情報を書いては大問題です。そんな時はこう書きましょう。

<!-- ${/* FIXME:ここ意味がわからないけどなぜかそれっぽく動く>< */} -->

これでブラウザには出力されなくなります(笑)

最後に

今どきUIに強いプログラマー=モテです。さあ、ここまで読めば、君も立派なMayaa使い!

ちなみに、弊社の求人はこちらです↓

http://www.interfactory.co.jp/recruit/index.html

(2011/07/16)書き忘れたことを別エントリで追記しました

2年間Mayaaを使ってわかったこと その2

この記事は以前多くのブックマーク・コメントを頂きました。

これからブックマークされる方はこちら↓

MayaaでHTML5のスマートフォンページを作る際にはまったこと

僕はMayaaが好きです。仕事でもかなり使っています。

今回、スマートフォン(iPhoneおよびAndroid対応)向けECサイトのフロントエンドに、Mayaaを使用しました。そこではまったことを報告します。

metaタグにContentTypeが省略できるようになったため、ドキュメント判別の手段が減ってしまった

HTML5では、

<meta charset="UTF-8">

のような書き方が許されるようになりました。

従来は

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

という書き方をしていました。
これで何が問題なのかというと、Mayaa自体がこのContent-Type指定を読んでいてそれによって処理を判別(例えばapplication/xhtml+xmlかどうかなど)もしているので、動作が異なってしまう場合があります。

私たちの場合は、内部的な事情で、テンプレートファイルの拡張子に.xhtmlを使用していたため、すべてのContentTypeヘッダがapplication/xhtml+xmlになってしまい、ブラウザのバリデーションエラーで画面が真っ白になるという現象が発生しました。

回避方法は、前者の省略記法を使用せず、後者の従来通りの書き方をすることです。この書き方はHTML5文法としても正式です。

aタグがブロック要素を包含できるようになったのに、うまくいかない

この問題は意外と深かったです。テンプレートに

<a href="javascript:alert('hello');">
       <p>aaa</p>
       <p>aaa</p>
</a>

のように記述すると、実行時に勝手に以下のように書き換えられてしまいます。

<a href="javascript:alert('hello');">
       </a><p><a href="javascript:alert('hello');">aaa</a></p>
<p>aaa</p>

これは、Mayaa依存ライブラリのNekoHTMLが、パースした段階でDOMツリーを再構成してしまうためで、NekoHTMLがHTML5に対応していないため、XHTMLやHTML4以前の仕様に基づいて、DOMツリーを構成してしまいます。

この件について、現時点で一番良い回避法は、
http://ml.seasar.org/archives/mayaa-user/2011-March/000923.html
こちらで対応してくださった方法を使用することです。しかし、この場合は、imgやbrなども含めて必ず閉じタグを書かなければなりません。テンプレートコーディング者が第三者の場合などその徹底が難しい場合は使用が難しいと思います。

NekoHTMLについては、本家MLに問い合わせたところ、最新版でもHTML5への特別な対応はしていないとの返答をいただきました。

最終手段として、私は、NekoHTMLのソースを直接書き換えました。修正箇所は、Mayaaが使用しているNekoHTML-0.9.5をベースとして、

org.cyberneko.html.HTMLElements の、

184:     new Element(A, "A", Element.INLINE, BODY, null),

184:     new Element(A, "A", 0, BODY, null),

に書き換えることです。

NekoHTMLにはテストコードが付属しているので、実行してみたところ、2箇所でエラーになりました。

test:
[tester] Parsing test files and generating output...
[tester] Comparing parsed output against canonical output...
[tester] test36.html:5 strings don't match
[tester] [in: )A]
[tester] [out: (P]
[tester] test50.html:5 strings don't match
[tester] [in: )A]
[tester] [out: (P]
[tester] Finished with errors.

BUILD FAILED

これは、まさに今回の修正の対象だったので、テストケースを修正します。
/data/html/canonical/test36.html
修正前:

(HTML
(BODY
(A
Aname foo
)A
(P
(A
Aname foo
"Blah
)A
)P
)BODY
)HTML

修正後:

(HTML
(BODY
(A
Aname foo
(P
"Blah
)P
)A
)BODY
)HTML

/data/html/canonical/test50.html
修正前

(HTML
(BODY
(A
Ahref foo
)A
(P
(A
Ahref foo
"Blah
)A
)P
)BODY
)HTML

修正後

(HTML
(BODY
(A
Ahref foo
(P
"Blah
)P
)A
)BODY
)HTML

test:
[tester] Parsing test files and generating output...
[tester] Comparing parsed output against canonical output...
[tester] Done.

BUILD SUCCESSFUL

これで、独自にビルドしたnekohtmlのjarをアプリのlibにコピーし、nekohtml-0.9.5.jarを削除すれば差し替え完了です。

Mayaaファイルを命名規則から一括作成するEmEditorマクロ

業務でMayaa使っています。
私が所属する開発チームでは、Mayaaのプロセッサとひもづけるid(m:id)に、一定の命名規則を持たせています。

ルールは以下の4種類しかありません。

  1. その場に値を出力する(m:writeプロセッサ):"〜_HERE"
  2. そのタグの属性を変化させる(m:echo, m:attribute):"〜_TAG"
  3. その要素を条件によって出し分ける(m:if):"IF_〜"
  4. その要素を繰り返す(m:for or m:forEach):"LOOP_〜"

このようにしておけば、仮に〜_HEREがm:writeではなく、m:insertであったとしても、テンプレートを書くデザイナーさんには同じように見えるので、よって上記4ルールで徹底運用しています。

さて、日々の業務を楽にするための努力は欠かしません。名前が規則に従っているなら、プログラムも自動生成できてしまうのではないか?

Mayaaは、テンプレートに定義されていないm:idを記述すると、警告で教えてくれる機能があります。

[WARN] EqualsIDInjectionResolver - the injection ID(IF_*******) is not found on the template, /xxx/yyy/zzz.xhtml#410.

この文字列をコピペして食べさせれば、.mayaaファイルの雛形を生成するEmEditorマクロを作りました。

document.selection.Replace(
"[WARN] EqualsIDInjectionResolver - the injection ID(",
"",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll);
document.selection.Replace(
"\\) is not found on the template.*$",
"",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll | eeFindReplaceRegExp);
document.selection.Replace(
"^.*_HERE$",
"\t<!-- を出力します。 -->\n"
+ "\t<m:write m:id='\\0' value='${}' />",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll | eeFindReplaceRegExp);
document.selection.Replace(
"^.*_TAG$",
"\t<!-- します。属性が変化します。 -->\n"
+ "\t<m:echo m:id='\\0'>\n"
+ "\t\t<m:attribute name='' value='${}' />\n"
+ "\t</m:echo>",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll | eeFindReplaceRegExp);
document.selection.Replace(
"^IF.*$",
"\t<!-- の時にのみ表示します。 -->\n"
+ "\t<m:if m:id='\\0' test='${}'>\n"
+ "\t\t<m:echo><m:doBody /></m:echo>\n"
+ "\t</m:if>",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll | eeFindReplaceRegExp);
document.selection.Replace(
"^LOOP.*$",
"\t<!-- を繰り返します。 -->\n"
+ "\t<m:for m:id='\\0'\n"
+ "\t\t\tinit='${}'\n"
+ "\t\t\ttest='${}'\n"
+ "\t\t\tafter='${}'>\n"
+ "\t\t<m:echo><m:doBody /></m:echo>\n"
+ "\t</m:for>",
eeFindNext | eeFindReplaceCase | eeFindReplaceEscSeq | eeReplaceAll | eeFindReplaceRegExp);

便利です!
自分だけですが。。。

問題なのは会社でEmEditorを愛用しているのが僕だけということです。。。秀丸マクロ?書けません!