ラベル Web Components の投稿を表示しています。 すべての投稿を表示
ラベル Web Components の投稿を表示しています。 すべての投稿を表示

2013年12月7日土曜日

Skeleton Project for Google App Engine/Go + Dart

2016/4/30更新

この記事の内容はすでに古く、またGithub上にリポジトリにおいてあるコードも最新のDartに合わせて変わっているため参考にならない点に注意。

Dartも1.0になったことだしそろそろ試す人が増えてきてもいいはず。ということで、Google App Engine上でDartなウェブアプリを作りたい人のためにスケルトンのプロジェクトを作った。バックエンドの言語はGoでフロントエンドはDart。動作確認はDartiumでしかしてないので、他のブラウザで動かしたい人はどっかでdart2js通せばいける…はず? 正直dart2jsほとんど試したことないからよくわからない。

スケルトンプロジェクトについて

Github上にリポジトリを作ったのでそれをCloneするなりForkするなり好きにやってもらえればいいはず。

ちなみに今回は実際にApp Engine上にデプロイしてあるので、そっちでどんなもんか確認することもできる。URLはgae-go-dart-skeleton.appspot.comで、Dartiumでアクセスすればちゃんと見れるはず。なおローカルからDartium上で動かすとこんな感じ(Windows 8.1 + GAE/Go 1.8.8 + Dart SDK 1.0.0.3_r30188)。

スケルトン

スケルトンプロジェクトの動かし方

gae-go-dart-skeletonを動かすには以下の手順を踏む必要がある。

  1. GAE/GoのSDKとDartのSDKをダウンロードしてきてパスを通す
  2. git clone git@github.com:rillomas/gae-go-dart-skeleton.gitする。あるいは自分のとこにForkしてそっちをcloneする
  3. pub getして必要なパッケージを落としてくる
  4. dev_appserver.pyを使ってローカルで動かす
  5. (デプロイする場合は)appcfg.pyでアプリをアップする

pub get

pubはDartに標準でついてくるパッケージマネージャで、pubspec.yamlファイルに必要なものを書くと落としてきてくれる。今回のpubspec.yamlはこんな感じ。

dependencies:
  logging: any
  polymer: any

Polymerとログ用のパッケージしか書いてないけど、それ以外に必要なパッケージはpub側で判断してもってきてくれるからこれで十分。でpubspec.yamlが置いてあるディレクトリでpub getするとこんな感じに実行される。

$ pub get
Resolving dependencies............................
Downloading polymer 0.9.1+1 from hosted...
Downloading polymer_expressions 0.9.1 from hosted...
Downloading shadow_dom 0.9.1 from hosted...
Downloading observe 0.9.1+1 from hosted...
Downloading html5lib 0.9.1 from hosted...
Got dependencies!

ルートディレクトリにpackagesってディレクトリが作られて、そこにパッケージがすべてダウンロードされてくるはず。あとは必要なところにpackagesへのリンクが自動的に張られる。それ以外にもpubspec.lockていうファイルが作られるけど、それはpubがバージョンの管理用に自動生成するファイルなのであまり気にしなくて大丈夫。

App Engine関係

App Engine関係は公式ドキュメントがあるからそっちでよろしく。dev_appserver.pyまわりはここ、appcfg.pyはこことか。

コードについて

今回のはPolymerを使った極めて単純なウェブアプリなんだけど、クライアントサイドで完結するようなアプリだと面白くないのでサーバー側のREST APIを叩いてdatastoreからデータを持ってきてそれを表示している。

サーバーサイド

サーバー側は来訪者の情報を提供するためのvisitorInfo APIを用意している。パスはapi/1/visitorInfoで、そこにGETをかければjsonが返ってくるし、POSTすればサーバー側の数値を書き換えることができる。実際アップしてあるアプリのAPIにアクセスすると現在の来訪者数累計が取得できるはず。やっている処理の内容はserver/main.goの以下のあたり。

func init() {
    http.HandleFunc("/api/1/visitorInfo", handleVisitorInfoRequest)
}

func handleVisitorInfoRequest(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Debugf("Method: %s Url:%s ContentLength: %d\n", r.Method, r.URL, r.ContentLength)
    if (r.Method == "GET") {
        getVisitorInfo(c,w,r)
    } else if (r.Method == "POST") {
        setVisitorInfo(c,w,r)
    }
}

getVisitorInfo()の中でdatastoreからデータの取得とGETへの応答、setVisitorInfo()のなかでPOSTで送られてきたデータのdatastoreへの格納をやっている。datastoreへのget/setは固定の文字列キーでやってるので基本的に間違いはないはずなんだけど、初回にまだ何もsetされる前にgetが走る可能性があるのでNoSuchEntityエラーだけは現状握りつぶしてる。

const (
    visitorInfoKind string = "visitorInfo"
    visitorInfoKey string = "theVisitorInfoKey"
)

type VisitorInfo struct {
    Count int // total number of visitors
}

func getVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)
    info := new(VisitorInfo)
    err := datastore.Get(c,k, info)
    // ignore the NoSuchEntityError and return the default value if we don't have the entity stored yet
    if err != nil && err != datastore.ErrNoSuchEntity {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    outBuf, err := json.Marshal(info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    size, err := w.Write(outBuf)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    c.Debugf("Wrote %d bytes as response\n", size)

}

func setVisitorInfo(c appengine.Context, w http.ResponseWriter, r *http.Request) {
    buf, err := ioutil.ReadAll(r.Body)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    var info VisitorInfo
    json.Unmarshal(buf, &info)
    k := datastore.NewKey(c,visitorInfoKind, visitorInfoKey, 0, nil)

    _,err = datastore.Put(c,k,&info)
    if err != nil {
        c.Errorf("%s", err.Error())
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
}

クライアントサイド

クライアント側ではMainApp(index.htmlから参照されてるWeb Component)が表示された段階でサーバーに対してGETを発行して、結果をもとに自身の内容を更新している。コード的にはclient/lib/components/main_app.dartの以下のあたり。

    @override
    void enteredView() {
        super.enteredView();

        var domain = ServerChannel.generateRootDomain(window.location);
        ServerChannel.getVisitorInfo(domain).then((info) {
            info.count++;
            visitorInfo = info;

            // update the visitor info on the server side
            ServerChannel.sendVisitorInfo(info, domain);
        });
    }

enteredView()はWeb Component側がdocumentに挿入されたときに呼ばれてくる関数で、今回はそこでvisitorInfo APIへGETを投げている。サーバーから今までの来訪者数を取得して、自分の分を加算して表示、更にサーバー側の来訪者数をPOSTで更新するところまで通してやっている。

ちなみにこのやり方だと来訪者数が一瞬だけゼロ表示されてしまうので、情報がもらえるまでは表示そのものを隠すとかした方が美しいのかも。めんどうだからやってない。

実際にHttpリクエストを投げる部分はすべてServerChannelというクラスで隠蔽している。コード的にはclient/lib/services/server_channel.dart

class ServerChannel {

    static Future<VisitorInfo> getVisitorInfo(String domain) {
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("getting from ${url}");
        var task = new AsyncGet<int>();
        var f = task.request(url, (res) {
            Logger.root.info("response: $res");
            int count = -1;
            if (res.isEmpty) {
                Logger.root.warning("received empty string");
                return count;
            }
            Map map = JSON.decode(res);
            var info = new VisitorInfo.fromJson(map);
            return info;
        });
        return f;
    }

    static void sendVisitorInfo(VisitorInfo info, String domain) {
        var request = new HttpRequest();
        var url = "${domain}${visitorInfoApiPath}";
        Logger.root.info("Sending info to ${url}");
        request.open("POST", url);
        var str = JSON.encode(info);
        request.send(str);
    }

    // Generate the root domain from location information
    static String generateRootDomain(Location location) {
        return "${location.protocol}//${location.host}/";
    }

    static const string visitorInfoApiPath = "api/1/visitorInfo";
}

内部で使っているAsynGetていう独自クラスがFutureやらCompleterやらを駆使して非同期なコードをDartっぽくなるようにラップしてる。

class AsyncGet<Output> {
    Future<Output> request(String url, Output process(String response)) {
        HttpRequest.request(url).then((r) {
            if (r.readyState == HttpRequest.DONE &&
                (r.status == 200)) {
                Output out = process(r.responseText); // convert the raw response text
                _completer.complete(out);
            }
        });
        return _completer.future;
    }

    Completer<Output> _completer = new Completer<Output>();
}

ServerChannelを経由してサーバーから取得できた来訪者情報は、visitorInfo変数に代入された段階で自動的にバインド先に反映される。これはPolymerのおかげ。その辺のバインドが書いてあるのはMainAppのビューに相当するclient/lib/components/main_app.html。

<polymer-element name="main-app">
    <template>
        <h2>Welcome to my Dart Skeleton</h2>
        <p>You are the #{{visitorInfo.count}} visitor!</p>
    </template>
    <script type="application/dart" src="main-app.dart"></script>
</polymer-element>

自分でDOMいじる必要がないから楽。

雑感

App Engine + Dartでもわりと普通に動いてる印象。ただこっちで試したところ、Channel APIをjs-interopでラップするとデプロイしたときに"Uncaught Closure call with mismatched arguments: function 'call'"とかいう謎のエラーでうまく動かないことがわかってる。でもなぜかローカルの環境だとちゃんと動く。

DartにPolymerが無くてWeb UIだった頃から追ってるけど、Polymerは素直に書けてちゃんと動くしいいと思う。Web UIの頃はところどころ未完成でいまいちだった。個人的には@observableとかのアノテーションを書くのはちょっと違和感があるんだけど、たぶん慣れの問題。あとは早めにWeb AnimationsとPolymer UI Elementsを移植してもらえると助かるかな。

そんな感じ。Dart VMはいずれChrome stableにマージされる予定らしいし、dart2jsの性能もメリメリ向上してるのでみんなどんどんDart書くといいよ。

Dart Advent Calendar 2013 12月7日担当

2013年6月14日金曜日

Playing with Dart Web UI again

最後にWeb UIを触ってからしばらく経って、Dart自体のライブラリやらも色々と大きな変更があったようなのでまた久しぶりにWeb UIまわりを触ってみた。作ったものは前回とおなじというか、それを修正・拡張したもの。使ったのはDart Editor version 0.5.16_r23799とWeb UI version 0.4.11+1で、できあがったサンプルはここ

できたもの

動物の名前と色をリストで表示するだけのもの。


表示した直後

スライダー動かすと色も変わる

前と比べてどう変わったか

前やったときにはできなかった(もしくはめんどくさそうでやらなかった)ことがいくつかできるようになってた。具体的には以下の2つ。

  • 子のコンポーネントと親のコンポーネントをイベントでつなげられるようになった

    前試したときはちょうどStreams APIまわりができたばっかりくらいのタイミングで、それをどうカスタムなイベントに転用すればいいかよくわからなかった。その後情報がいくらか整備されてEventStreamProviderとCustomEvent使えばできるようになることがわかった、ていう感じ。個人的にこれは結構大きい。

  • input type="range"とのバインドができるようになった

    前ためしたときは確かinput type="range"とのバインドはうまく動かなかったけど、今回試したら普通に動いてた。

大きな変更があったのはanimal-summaryとcolor-editorの中だけなので、そこだけ解説する。

animal-summary

<!doctype html>
<html>
    <head>
        <link rel="import" href="color-editor.html">
    </head>
    <body>
        <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
            <template>
                <div>{{header}}</div>
                <color-editor id="pallet" model="{{model.color}}"></color-editor>
                <canvas id="summary" width="300" height="200"></canvas>
            </template>
            <script type="application/dart">
            import 'dart:json' as json;
            import 'package:web_ui/web_ui.dart';
            import 'package:dart_web_ui_sample/src/AnimalSummary.dart';
            import 'package:dart_web_ui_sample/src/Color.dart';

            class AnimalSummaryViewModel extends WebComponent {

                AnimalSummaryViewModel() {
                }

                String get header => "${model.name}:${model.count}";

                void increment(e) {
                    model.count++;
                }

                void created() {
                }

                void inserted() {
                    _initializeCanvas(model.color);

                    var p = query("#pallet");
                    p.xtag.onColorChanged.listen((event) {
                        // sync canvas color with the editor color
                        String str = event.detail;
                        var obj = json.parse(str);
                        Color c = new Color(obj["r"], obj["g"], obj["b"]);
                        _initializeCanvas(c);
                    });
                }

                void _initializeCanvas(Color color) {
                    var c = query("#summary");
                    var ctx = c.getContext('2d');
                    ctx.fillStyle = color.cssString;
                    ctx.fillRect(0,0,300,200);
                }

                AnimalSummary model;
            }
            </script>
        </element>
    </body>
</html>

前と変わったところで重要なのはinserted()の中。inserted()はanimal-summaryがDOMに追加された段階で呼ばれてくるんだけど、上のコードではその段階でcolor-editorが独自に定義したonColorChangedイベントにリスナーを登録してる。これの意図はcolor-editorの中で色に変更が加わったら、それをanimal-summary側で察知して表示されてる色を更新したい、というもの。

途中であらわれる謎のxtagというプロパティはこの辺に記述がある。queryから返ってきたElementはただのdivなので、自分で独自に定義したonColorChangedにアクセスするにはxtagを経由して実際のWeb Componentインスタンスにアクセスしないといけない、らしい。

ちなみに、そのうちinsertedとかはなくなるかも

color-editor

<!doctype html>
<html>
    <body>
        <element name="color-editor" constructor="ColorViewModel" extends="div">
            <template>
                <ul>
                    <li>R:<input type="range" bind-value="red" min="0" max="255" ><input type="text" readonly bind-value="red"></li>
                    <li>G:<input type="range" bind-value="green" min="0" max="255" ><input type="text" readonly bind-value="green"></li>
                    <li>B:<input type="range" bind-value="blue" min="0" max="255" ><input type="text" readonly bind-value="blue"></li>
                </ul>
            </template>
            <script type="application/dart">
            import 'dart:html';
            import 'dart:async';
            import 'dart:json' as json;
            import 'package:web_ui/web_ui.dart';
            import 'package:dart_web_ui_sample/src/Color.dart';

            class ColorViewModel extends WebComponent {
                ColorViewModel() {
                }

                static const EventStreamProvider<CustomEvent> colorChangedEvent = 
                    const EventStreamProvider<CustomEvent>('colorChanged');

                Stream<CustomEvent> get onColorChanged => colorChangedEvent.forTarget(this);

                String get red => "${model.red}";
                set red(String r) {
                    int nr = int.parse(r);
                    if (model.red == nr) {
                        return;
                    }
                    model.red = nr;
                    _notifyColorChange(model);
                }

                String get green => "${model.green}";
                set green(String g) {
                    int ng = int.parse(g);
                    if (model.green == ng) {
                        return;
                    }
                    model.green = ng; 
                    _notifyColorChange(model);
                }

                String get blue => "${model.blue}";
                set blue(String b) {
                    int nb = int.parse(b);
                    if (model.blue == nb) {
                        return;
                    }
                    model.blue = nb;
                    _notifyColorChange(model);
                }

                void created() {
                }

                void inserted() {
                }

                void _notifyColorChange(Color newColor) {
                    var str = json.stringify(newColor);
                    CustomEvent ev = new CustomEvent('colorChanged', canBubble: true, cancelable: true, detail: str);
                    dispatchEvent(ev);
                }

                Color model;
            }
            </script>
        </element>
    </body>
</html>

こっちで重要なのはEventStreamProviderとCustomEventでカスタムなイベントを定義してるところと、notifyColorChangeで実際にイベントを発行してるところ。

定義するところはもう作法が決まってるので、作法にのっとって書けば大丈夫。個人的にこの辺はWPFでDependencyPropertyを定義するときと少し似てるなーと思った。イベントを発行するところも大体作法通りなんだけど、1つ注意が必要なのはdetailの引数を渡すところ。これはドキュメントを見るとObject型を受け付けてるんだけど、実際は勝手に文字列に変換されるっぽい。最初インスタンスをそのまま渡したらリスナー側で例外が飛んで困った。そのため、今は先にこちらでjson文字列に変えてから渡してる。もちろんリスナー側でもjson文字列が来ることを前提でparseしてる。

なお、Colorをjson化するためColor側ではtoJson()ていう専用の関数を定義している。stringifyの中でそのtoJsonが呼ばれてるっぽい。

感想

しばらく放置してた間に色々変化があったようで、当初やりたかったことができるようになってきた印象。もう少し遊んでみるかも。

2012年12月12日水曜日

Playing with Dart Web UI

以前DartでのWeb Componentsの記事を書いてからまだ一月ちょっとくらいしか経ってないけど、Web Componentsパッケージ側で色々と変化があったせいですでに前の情報が役立たずになってしまっている。ということで最新情報で改めて書く。

何が変わったか

以下の通り。詳しくはDartの開発者が書いた記事へ。

  • そもそもパッケージの名前がweb_componentsからweb_uiに変わった。
  • バインディングやらの記法が変わった
  • Web Componentsとは直接関係ないけど、パッケージの構成が少し変わった。

逆にdwc.dartでのコンパイル方法やらDartiumでの閲覧方法は変わってないのでそこは前回と同じでいける。

コードの修正版

パッケージにしたものをGithubのリポジトリにアップした。ここではそれぞれをもう少し詳しく見ていく。

index.html

このindex.htmlはこのサンプルのエントリーポイントで、Dartのmainが記述されてる。以前は一つのindex.htmlに全部まとめてたんだけど、コンポーネント間の依存関係がうまく解決できなかったので今回は細かくファイルを分けた。

<!doctype html>
<html>
    <head>
        <title>Dart Web UI Sample</title>
        <link rel="stylesheet" type="text/css" href="stylesheets/bootstrap.min.css">
        <link rel="shortcut icon" type="image/png" href="images/favicon.png">
        <link rel="components" href="components/animal-summary.html">
        <script type="text/javascript" src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> 
    </head>
    <body>
    <div id="navigationArea" class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">Dart Web UI Sample</a>
            </div>
        </div>
    </div>
    <div id="animalListArea">
        <ul>
            <template iterate='a in animalList'>
                <animal-summary model="{{a}}" ></animal-summary>
            </template>
        </ul>
    </div>
    <script type="application/dart">
    import 'package:dart_web_ui_sample/src/AnimalSummary.dart';
    import 'package:dart_web_ui_sample/src/Color.dart';

    List<AnimalSummary> animalList = <AnimalSummary>[
        new AnimalSummary("aardvark",0, new Color(140,70,20)),
        new AnimalSummary("giraffe",1, new Color(255,255,0))
        ];
  
    void main() {}
    </script>  
    </body>
</html>
dwc.dartでコンパイルしてからDartiumで開いたindex.html

記法やらを新しくしたのと、色を示すColorクラスを追加したのが前と違う点。

animal-summary.html

AnimalSummaryクラスのViewModelとViewに相当するのがこのhtmlファイル。templateタグ内のマークアップがViewで、その下にDartで書かれているクラスがViewModel。

<!doctype html>
<html>
 <head>
  <link rel="components" href="color-editor.html">
 </head>
 <body>
     <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
         <template>
             <div>{{header}}</div>
             <color-editor model="{{model.color}}"></color-editor>
             <canvas id="summary" width="300" height="200"></canvas>
         </template>
         <script type="application/dart">
         import 'package:web_ui/web_ui.dart';
         import 'package:dart_web_ui_sample/src/AnimalSummary.dart';

         class AnimalSummaryViewModel extends WebComponent {

             AnimalSummaryViewModel() {
             }

             String get header => "${model.name}:${model.count}";

             void increment(e) {
                 model.count++;
             }

             void created() {
                print("created.");
             }

             void inserted() {
                 _initializeCanvas("#summary");
                 print("inserted.");
             }

             void _initializeCanvas(String id) {
                 var c = query(id);
                 var ctx = c.getContext('2d');
                 ctx.fillStyle = model.color.cssString;
                 ctx.fillRect(0,0,300,200);
             }

             AnimalSummary model;
         }
         </script>
     </element>
 </body>
</html>

Viewとしてはdiv、canvas、それと別に定義したcolor-editorというComponentを使用している。Canvasに関してはViewModel側でinserted関数を実装することで挿入されたタイミングに合わせて初期化を行なっている点が重要。

試したかんじtemplate内の子要素についてはinsertedのタイミングならqueryで取得できる。ただcreatedでqueryしようとしても現状はnullしか返ってこない。

color-editor.html

color-editorはanimal-summaryのtemplateで使っている色のエディタ。本当はスライダー(input type="range")を使いたいんだけど、現状はWeb UI側がスライダーとのデータバインディングをサポートしてないみたいで今はテキストボックスを使ってる。

<!doctype html>
<html>
 <body>
     <element name="color-editor" constructor="ColorViewModel" extends="div">
         <template>
          <ul>
              <li>Red:<input type="text" bind-value="red" /></li>
              <li>Green:<input type="text" bind-value="green" /></li>
              <li>Blue:<input type="text" bind-value="blue" /></li>
          </ul>
         </template>
         <script type="application/dart">
         import 'package:web_ui/web_ui.dart';
         import 'package:dart_web_ui_sample/src/Color.dart';

         class ColorViewModel extends WebComponent {
             ColorViewModel() {
             }

             String get red => "${model.red}";
             String set red(String r) => model.red = int.parse(r);

             String get green => "${model.green}";
             String set green(String g) => model.green = int.parse(g);

             String get blue => "${model.blue}";
             String set blue(String b) => model.blue = int.parse(b);

             void created() {

             }

             void inserted() {
              print("r: ${model.red} g: ${model.green} b: ${model.blue}");
             }

             Color model;
         }
        </script>
     </element>
 </body>
</html>

現状は値をViewModel側に渡すタイミング(WPFで言うところのUpdateSourceTrigger)を制御できないので、一文字消すとその段階でViewModelへの値の引渡しが行われる。そのせいで、テキストボックス内の数字を全部消すとnullか何かが渡ってきてエラーが起きる。

Redのテキストボックスを空にした瞬間にエラーが飛ぶ

本当ならテキストボックスからフォーカスが外れた段階で値の引渡しをやりたい。WPFみたいにタイミングいじれるようになるのか?

Color.dart

ColorはColorViewModelのModelに相当するクラス。見たまんまなので特にいうことはない。あえていえばcssStringというプロパティでCSS用の文字列を返すようにした、という点。

library dart_web_ui_sample;

class Color {
    Color(this.red, this.green, this.blue);
    int red;
    int green;
    int blue;
    String get cssString => "rgb(${red},${green},${blue})";
}

AnimalSummary.dart

本サンプルの主役を張ってるクラス。これも見たまんま。

library dart_web_ui_sample;
import 'package:dart_web_ui_sample/src/Color.dart';

class AnimalSummary {
    AnimalSummary(this.name, this.count, this.color);
    String name = "No Name";
    int count = 0;
    Color color;
}

感想

着実に良くなってる感じ。記法はシンプルになったし、子要素へのqueryも前までできなかったけどできるようになった。書いてて気になったのは下の点くらい。

  • WPFでいうところのUpdateSourceTriggerみたいに値を引渡すタイミングをいじりたい。
  • WPFでいうところのConverter的な仕組みで文字列 to 数値の変換のような働きをViewModelの外に出したい。今回でいえば色の数値をViewModelの中でint.parse()してるのをConverter的なクラスにやらせて、ColorViewModelはあくまでintしかインタフェースを持たないようにしたい。
  • ColorViewModelでの色の変更をAnimalSummaryViewModelに伝えてCanvasの色も更新したいんだけどどうすりゃいいんだ。

でもやっぱhtmlでも部品ごとにhtmlファイル分けて、それぞれで完結するViewとViewModelを書けるのは気持ちいい。

2012年11月4日日曜日

Playing with Dart Web Components

2012/12/27更新:この記事は一部情報が古いので、こちらと併せて読むといいかも。

Dart Web Componentsの記事が10月にアップされて、それからしばらくはバグってたりでまともに使えなかったんだけど、今日試したらなんとか使えた。で、Web Componentsマジさいこーってなったのでちょっとここで参考用に情報を残しておこうと思う。試した環境はUbuntu Linux 12.10(64 bit)。

手順

注:最近はDartで互換性を捨てる変更がいろいろと入ってるため、タイミングによっては同じやり方でうまく行かないかもしれない

Web Componentsを試すにはいろいろと手順が必要で、順番的には以下の通りになる。

  1. Dart SDK、Dartiumの準備
  2. テスト用のパッケージの生成
  3. htmlの記述、およびdwc.dartによるコンパイル
  4. Dartiumでの閲覧

それぞれ順を追って説明する。

Dart SDK、Dartiumの準備

まずすべきは最新のDartiumとSDKを落としてくること。Editorごと落とすと全部含まれているのでそれが楽かも。場所はここ(64bit版)。なぜ最新版でないといけないかというと、安定版では最新のDartの変更が取り込まれていないため、あとの手順で失敗してしまうから。(2012/11/4現在)

Editorを落としたら解凍してでてきたdartディレクトリにパスを通すんだけど、自分は~/binの下にdartディレクトリをおいて、~/.bashrcに以下のように書いてる。

export PATH=$PATH:~/bin/dart/dart-sdk/bin

これで必要な実行ファイル、特にdartとpubにパスが通る。

テスト用パッケージの生成

Dartはコードのパッケージ化を推進していて、今回使うWeb Componentsももちろんパッケージ化されている。パッケージ化されたコードを使う側もパッケージ化したほうが何かと都合がいいので、その作法に従うことにする。パッケージ化の詳細な手順は公式なドキュメントに任せるとして、ここでは最低限必要な部分だけを説明する。

  1. パッケージのルートディレクトリの生成

    パッケージ関連のファイルを格納する適当なディレクトリを作る。今回はdart-web-components-testとする。

  2. pubspec.yamlの作成

    pubspec.yamlというファイルはパッケージの情報、例えば名前やら依存する他のパッケージやらの情報を記述するファイルで、Web Componentsのパッケージを持ってくるためにこれを記述しなければならない。といっても内容は単純で、以下のとおり。

    name: dart-web-components-test 
    dependencies:
      web_components: any 
    

    パッケージの名前と、あとweb_componentsというパッケージに依存してることを示している。内容はこれだけ。

  3. pubによる依存パッケージのダウンロード

    今度は先ほど作ったpubspec.yamlを使って依存するパッケージをダウンロードする。dart-web-components-testのしたにpubspec.yamlを置いて、以下のコマンドをうつ。

    $ pub install
    Resolving dependencies...
    Downloading web_components 0.2.5+2 from hosted...
    Downloading js 0.0.7 from hosted...
    Downloading html5lib 0.0.19 from hosted...
    Dependencies installed!
    $ 
    

    これでWeb Componentsを使うのに必要なパッケージがダウンロードされて、自動的に作られたpackagesディレクトリの下に置かれる。ラクチン。ちなみにpackagesの中身は下のようになってるはず。

    $ ls packages/
    args  html5lib  js  logging  unittest  web_components
    

htmlの記述、およびdwc.dartによるコンパイル

必要なパッケージがそろったので、今度は実際にコードを書いていく。Web Componentsなコードの書き方はこの記事に詳しく書いてあるのでそっちを見るのをおすすめする。MVVM(Model-View-View Model)なコードに経験がある人ならあっさり理解できると思うけど、そうでない人にはもしかしたらちょっとわかりづらいかも。幸い俺は経験があるので問題なかった。

index.html

以下にサンプルのコードを示す。内容は動物のリストを表示するという単純なもの。中ではWeb Componentsの要素(element, template, iterate等)を色々と使ってる。

<!doctype html>
<html>
    <head>
        <title>Dart Web Components</title>
        <link rel="stylesheet" type="text/css" href="dart-web-components-test/bootstrap.min.css">
        <script src="http://dart.googlecode.com/svn/branches/bleeding_edge/dart/client/dart.js"></script> 
    </head>
    <body>
    <!-- Summary of Animal -->
    <element name="animal-summary" constructor="AnimalSummaryViewModel" extends="div">
        <template>
            <div>{{header}}</div>
            <button data-action="click:increment">Click to increment</button>
            <input type="text" data-bind="value:name" placeholder="type name here">
        </template>
        <script type="application/dart">
        import 'package:web_components/web_component.dart';

        /// Model class of animal
        class AnimalSummary {
            AnimalSummary(this.name, this.count);
            String name = "No Name";
            int count = 0;
        }

        /// View Model class of animal
        class AnimalSummaryViewModel extends WebComponent {

            /// read-only header
            String get header => "${_animal.name}:${_animal.count}";

            void increment(e) {
                _animal.count++;
            }

            String get name => _animal.name;
            String set name(String s) => _animal.name = s;

            /// accessor for the underlying model
            AnimalSummary get model => _animal;
            AnimalSummary set model(AnimalSummary ts) => _animal = ts;

            AnimalSummary _animal;
        }
        </script>
    </element>

    <div id="navigationArea" class="navbar">
        <div class="navbar-inner">
            <div class="container">
                <a class="brand" href="#">Dart Web Components</a>
            </div>
        </div>
    </div>
    <div id="animalListArea">
        <ul>
            <template iterate='a in animalList'>
                <animal-summary data-value="model: a"/>
            </template>
        </ul>
    </div>
    <script type="application/dart">

    // create a list of animals to show
    List<AnimalSummary> animalList = <AnimalSummary>[
        new AnimalSummary("aardvark",0),
        new AnimalSummary("giraffe",1)
        ];
  
    void main() {}
    </script>  
    </body>
</html>

コードが書けたら今度はそれを一旦コンパイルしないといけない。これは最終的には不要になる(と期待している)けど、現状は必要みたい。ということで以下のコマンドをうつ。

$ dart --package-root=packages/ packages/web_components/dwc.dart index.html 
Total time spent on index.html                               -- 270 ms
$ ls
_index.html.animal_summary.dart  _index.html_bootstrap.dart  packages
_index.html.dart                 bootstrap.min.css           pubspec.lock
_index.html.html                 index.html                  pubspec.yaml

これでindex.htmlの解析がおこなわれて、無数のよくわからないファイル(_index.html*)が生成される。

Dartiumでの閲覧

これで必要なファイルが揃ったのでいよいよDartiumを起動して結果を確認する。Dartiumを起動するときは下のコマンドを使用する。

$ ~/bin/dart/chromium/chrome --user-data-dir --enable-experimental-webkit-features --allow-file-access-from-files &

これでWeb Componentsが有効化されたDartiumが起動する。ついでにローカルファイルアクセスを有効化するフラグもつけた。この状態で先ほど生成された_index.html.htmlを開くと下のようになる。

テキストボックス上で名前をいじれば表示されている名前も変わるし、ボタンを押せば数字も増える。

AnimalSummaryViewModelのプロパティと各種のView、例えば名前とテキストボックスがバインド(Web Component的には別の用語かも)されていて、名前が変更された場合は自動的にその変更が反映されるようになっている。

またulの中のli要素も"iterate='a in animalList'"という書き方でリストから自動的に生成させることができる。その生成される要素もelementとして定義したanimal-summary要素で、すごいすっきりと宣言的に書くことができている。

まとめ

ということでDart Web Componentsを試してみた。JavaScriptではBackbone.jsとかで似たようなことはできるっぽいけど、やっぱり公式にサポートしてもらえると大変助かる。あと最近はC# + WPF + MVVMを書くことが多かったので、Webでも似たような書き方ができるというのは非常にありがたい(というかそうでないとめんどくさすぎてヤル気が起きないくらい)。今後もWeb Componentsには頑張ってほしい。