undefined

bokuweb.me

ファミコンのエミュレータを書いた

f:id:bokuweb:20170919214338p:plain

概要

ファミコンのエミュレータをJSでだらだらと作ってた。そこそこ遊べるようになったので公開しておく。技術的な内容は、またどこかで発表したり、Qiitaなどにまとめたい。(忘れないうちに。需要があるかは怪しいが。)

随分昔に作ってみたいなーと思いFPGAでの実装を開始したんだけど、早々に挫折した覚えがある。今思うとFPGAの場合タイミングの問題が付き纏うのでJSで書くより圧倒的に難易度も高いし、ハードエミュレータを実装するにしても前段階としてソフトウェミュレータを実装するのが定石っぽいので無謀だったっぽい。

ひとまずMapper0という基本的なカセット形式のみに対応し、スーパーマリオブラザーズがそこそこ遊べるくらいを目標とした。

成果物

github.com

ファミコンのスペック

  • MPU 6502(RP2A03), 8bit
  • WRAM2KB
  • VRAM 2KB
  • 最大発色数 52色
  • 画面解像度 256x240

MPUは6502にAPUと呼ばれるオーディオプロセッサを搭載したカスタム品。メモリマップを覗くとわかるがAPUがブチ込まれた感が表現されていて良い。結構無茶したんじゃなかろうか。

解像度は256x240。デモを見せた人が口をそろえて「小さい」というが確かに小さい。

上記に加えてPPU(ピクチャープロセッシングユニット)という独自ICが実装されていて、各ハードウェアをひとつずつ再現していくことになる。

過程

Sprite2png

まずはどのように描画すべきなのか理解するためにカセットの中のスプライト領域をpngで出力するツールを書いてみた。

github.com

たとえばスーパーマリオブラザーズのスプライトは以下。これだけであの世界が構築されているのはすごい。

f:id:bokuweb:20170919222738p:plain

Hello world

まずはHello worldだけどCPUとPPU(ピクチャープロセッシングユニット)の背景レンダリングくらいは出来上がっていないといけないのでHello worldまでもそこそこ大変。

このツイート前はcanvasに描画してたんだけど、上のtweetは遊びで1div + cssでレンダリングしたときの。10FPS前後出てた。

ROMは以下で手に入る。C言語版もあるのでわかりやすい。

NES研究室

GIKO005

Hello worldのあとは「ギコ猫でもわかるファミコンプログラミング」を順にやっていくとよい。

gikofami.fc2web.com

これはGIKO005のスプライトを表示するサンプル。パレットがまだ実装されていないのか色がついていない。

GIKO013

これはGIKO013。GIKO013はAPUのサンプルでAPU(オーディオプロセッシングユニット)にどのような矩形波を出力すべきかが書き込まれるのでWebAudioで矩形波を作って音を鳴らしている。WebAudio自体あまり触ったこと無いし、音楽の知識もないのでここは本当にきつかった。

ここまでのサンプルで1Playerのキーパッドや背景スクロール、キャラクター移動などは実装済だった。 これだけ動いていたのでかなり順調で「CPUなんて完璧では?」と思ったりもしたが、この後に大量のバグと不可解な挙動に遭遇する。逆に言うとCPUがボロボロでもこの程度は動く。

GIKO016

縦スクロールのサンプルなんだけど全然だめだった。このあたりからパッと見つかるようなバグは減っていて、ひたすらアセンブラを読みながらデバッグするようになってた。結局「あとちょっとだから」っといって最後までやらなかったんだけど、デバッガの実装を早めにやっておけばよかった。CPUができていれば難しくない。結局全然「あとちょっと」じゃなかった。

GIKO017

これは横スクロールのサンプルで身体がないのは8 * 16形式のスプライトに対応してないから。マリオは8 * 8で動いたので、結局まだ8 * 16は対応してない。比較的このサンプルはすんなり動いた。

nestest

nes用のテストROMを発見してテストが通るようにデバッグを開始した。このROMの存在は知ったのはマリオに着手し始めたあとなので、この記事の内容の順番は実際とは多少前後している。このROMは最高でもっと早く試すべきだった。注意点としてはテスト対象のCPU上でこのテストROMが走るため、エラーとなった箇所を鵜呑みにはできない点。とは言えこいつのおかげでかなり進んだ。

CPU / PPU / keypadを早めに優先してこいつをGIKOたちより先に動作させるのが手順としては良さそう。

http://www.qmtpro.com/~nes/misc/nestest.log

テストログも落ちているのでデバッグもしやすい。

Super mario bros

だれもいない。

マリオが出たが、1マス浮いてる。ガタガタ。

味がある。

パレットをいじっていたらクリボーにコインの点滅が移ってしまった。

斜めになりながらものおばあちゃんを思い出した

重力無視してた。左上のスコアの0に黒いものが移っているけど、これはSprite0と呼ばれるもので、こいつが描画された瞬間PPUのあるビットがtrueになる。この画像では描画位置ズレて見えてしまってるが、本当はコインの裏くらいに隠してあるっぽい。

多分プレイ中はスコアやタイムなどのヘッダは固定されていて、コインより下の部分のみスクロール処理が必要なため、この位置においてあるんだと思う。CPUはPPUのsprite 0 hit フラグが立つのをポーリングしていてこいつがtrueになったらスクロールなどを始めるんだと思う。

ちなみにマリオのソースが以下で読める。

A Comprehensive Super Mario Bros. Disassembly · GitHub

未実装

未実装な箇所はたくさんあって例えば以下。 Audioはまだまだバグってるっぽくて変な音なる場合がある。

  • 8 * 16 スプライト
  • 各Mapper
  • Noise audio
  • 2 player keypad
  • DCM

Mapper3くらいは対応してやりたい。

現状

ファイナルファンタジー3の高速飛空艇はCPUのバグを突いてあの速度を実現しているらしく、どんな仕組みなのか解析したいと思ってたけど、そもそもファイナルファンタジー3を単に動かすだけでも道のりは遠そう。まだ実装しなきゃいけない箇所が結構ある。 ひとまずJS版はもういいや、という気持ちになったので今はRust + wasmで書き直してる。楽しいけどRust難しい。

詳細をどっかにまとめようかとは思うが、需要ありますか?

Rust+wasmでライフゲーム

Rustとwasmの入門にライフゲームを書いてみた

成果物

github.com

以下のURLで動作を確認できますが、わらわらしてますので苦手な方は注意してください。 windowサイズを小さくすると60FPSでて楽しいです。

Game of life with rust + wasm

環境構築

環境構築は以下の記事を参考にさせてもらっています。

sbfl.net

また「Think Web」の「Rust + WebAssembly でブラウザで動くLisp処理系を作る」も合わせて参考にさせてもらってます。

techbooster.booth.pm

実装

JS側からRust側へポインタを渡しておき、JS側のrequestAnimaionFrameからRust側から公開されているupdate関数を叩き、更新されたメモリをcanvasに反映するという構成を取っています。

Rust側とメモリを共有

簡略化してますが以下のようにwindowサイズ分の領域を確保して初期化(実際にはランダムにtrue / falseで埋めてますが省略)ポインタとサイズをupdateに渡してます。

  • js
const bufsize = window.innerHeight * window.innerWidth;
const bufptr = Module._malloc(bufsize);

Module._memset(bufptr, 0, bufsize);
let buf = new Uint8Array(Module.HEAPU8.buffer, bufptr, bufsize);

  ... 省略 ...
  
  update(bufsize, buf.byteOffset, column);

Rust側でupdateは以下のようになっていて、もらったポインタ、サイズからsliceを作成して次のステートを作成、バッファに戻す、という処理を行っています。

  • Rust
#[no_mangle]
pub extern "C" fn update(len: usize, ptr: *mut bool, col: usize) {
    let row = len / col;
    let buf: &mut [bool] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    let game: Vec<bool> = Game::new(buf, row, col).next();
    buf.clone_from_slice(game.as_slice())
}

また、jsからupdateをを使用するために、公開する関数をbuild時に指定してやる必要があります。 具体的には -C link-args="-s EXPORTED_FUNCTIONS=['_update']"のように指定する必要があります。 実際buildオプションは以下から確認できます。

github.com

Rust側のGame

数十行なので載せときます。 もらったSliceをVec<Vec>に変換して、あとはゲームのルールに従い次のステートを算出しています。

この間、コンパイラにはめちゃめちゃ叱られたし、未だ書ける気がしてこないが、入門にはいい題材だったっぽい。 パターンマッチ好きです。

type Field<T> = Vec<Vec<T>>;

pub struct Game {
    field: Field<bool>,
}

impl Game {
    pub fn new(buf: &[bool], row_size: usize, col_size: usize) -> Game {
        let field = Game::create(buf, row_size, col_size);
        Game { field }
    }

    pub fn next(self) -> Vec<bool> {
        self.field
            .iter()
            .enumerate()
            .map(|(y, r)| self.next_row(r, y))
            .flat_map(|x| x)
            .collect()
    }


    fn next_row(&self, row: &Vec<bool>, y: usize) -> Vec<bool> {
        row.iter()
            .enumerate()
            .map(|(x, _)| self.next_cell(y as i32, x as i32))
            .collect()
    }

    fn next_cell(&self, y: i32, x: i32) -> bool {
        let alive_num = [
            (y - 1, x - 1),
            (y, x - 1),
            (y + 1, x - 1),
            (y - 1, x),
            (y + 1, x),
            (y - 1, x + 1),
            (y, x + 1),
            (y + 1, x + 1),
        ].iter()
            .map(|&(y, x)| self.get_cell_state(y, x))
            .filter(|cell| *cell)
            .collect::<Vec<_>>()
            .len();
        match alive_num {
            3 => true,
            2 if self.is_alive(y as usize, x as usize) => true,
            _ => false,
        }
    }

    fn is_alive(&self, y: usize, x: usize) -> bool {
        self.field[y][x]
    }

    fn create(buf: &[bool], row_size: usize, col_size: usize) -> Field<bool> {
        (0..row_size)
            .into_iter()
            .map(|i| {
                let start = i * col_size;
                let end = start + col_size;
                buf[start..end].to_vec()
            })
            .collect()
    }

    fn get_cell_state(&self, row: i32, column: i32) -> bool {
        match self.field.iter().nth(row as usize) {
            Some(r) => {
                match r.iter().nth(column as usize) {
                    Some(c) => *c,
                    None => false,
                }
            }
            None => false,
        }
    }
}

速度

Rustのコードが完成してから、そのコードをJSにざくっと移植して、速度を測ってみたところ約5倍ほどwasmの方が早い結果となってる。 この結果はJSの最適化がされていないのが主要因だと思ってるので眉唾なんですが、フィボナッチで比較した際3倍程度との記事を見かけたことがあるので最適化を施していくとその辺に落ち着くのかもしれない。この辺は宿題。

今後

書く量が圧倒的が足りないのでテーマアップして継続していきたい。ひとまずはテトリスとか、ファミコンエミュレータを考えてる。キーボードも自作したいしテーマはが尽きなそう。時間が足りない。

wasm化したOpenCVでカメラ入力に笑い男を加えて描画する

OpenCVで試したいことがあり、OpenCV + wasmで入門がてら顔認識を試して遊んでみました。

OpenCVのビルド

wasmへのビルドは参考になるような記事を見つけられず、いろいろ試したものの成功しなかったんですが、ビルドを成功させているリポジトリが発見でき手順通り(cloneするリポジトリ名のみ間違ってて修正しましたが)にやることでビルドできました。

github.com

例えば顔認識分類器のデータの追加変更や任意のモジュールの追加、削除などを行うことを考えると自前でビルドできないと今後きつそうなんですが、正直良くわからないってのが現状です。このあたりは課題。 ビルド筋を鍛えようにも取っ掛かりもないような状態なので、おすすめの資料などがあれば、是非教えていただけると嬉しいです。

wasm

Rustでいろいろ試しているが、その際以下の記事をよく参考にさせてもらったりしてます。

sbfl.net

具体的には以下の用な感じで事前にビルドしたopencvを読み込んで使用する。

    "use strict"
    const name = "wasm/cv-wasm";
    const Module = {
      preRun: [],
      postRun: [],
      wasmBinaryFile: `${name}.wasm`,
      print: text => console.log(text),
      printErr: text => console.error(text),
    };
    fetch(`${name}.wasm`)
      .then(res => res.arrayBuffer())
      .then(buffer => {
        Module.wasmBinary = buffer;
        const script = document.createElement("script");
        script.src = `${name}.js`;
        script.addEventListener('load', (e) => {
          const main = document.createElement('script');
          main.src = 'main.js';
          document.body.appendChild(main);
        });
        document.body.appendChild(script);
      });

入力画像の顔認識

上記のリポジトリにはありがたいことに多くのサンプル、テストが含まれており、その中の顔認識のサンプルを書き直しながら試していきました。

github.com

wasmに対応したブラウザであれば以下で試すことができます。(loadに時間かかります)

https://bokuweb.github.io/cv-wasm-face-detect-sample/index.html

検知できれば以下のように顔部分が赤枠でマークされると思います。

f:id:bokuweb:20170727205411p:plain

カメラ入力の対応と笑い男のレンダリング

ここまでできれば、あとはカメラ入力に対応するだけです。 navigator.getUserMediaで映像を取得しvideoタグに繋いで、rafvideoからcanvsに描画し、顔が検出されていれば、座標とサイズを元に笑い男を描画しています。以下から試すことができますが、雑にcustomElements.defineを使用したがために先のサンプルとは違い動くのは最新のchromeぐらいだと思います。飽きたので最大笑い男数1。

https://bokuweb.github.io/wasm-cv-with-laughing-man/ (loadに時間かかります、カメラ必要)

ちゃんと動くとこんなこんな感じ。

f:id:bokuweb:20170727210000g:plain

顔を傾けると結構認識できなかったり、服のシワをご検知したりします。精度をあげる方法はいくつかあるようでちらっと見かけはしましたが、最終目的としては顔認識したいわけではないので、ひとまずここまで。MacBook Pro (Retina 13-inch、Early 2015) 2.9 GHz Intel Core i5 RAM 8 GB + chrome 59で4FPSとかそんなもんでした。画像を200 × 200くらいにすると60FPSでてた。参考まで。

実装して学ぶRxJS

実際にいくつかのオペレータを実装してみたらRxの気持ちがわかるかと思い実践してみました。 簡素化するために以下の方針とします。

  • unsubscribeしない
  • errorハンドリングしない

実装してみたのは以下です。

  • of
  • map
  • subject
  • filter
  • delay
  • fromPromise
  • combineLatest
  • switchMap

Observable

何はともあれ、まずはObservableを実装します。

class Observable {
  constructor(producer) {
    this.subscribe = producer
  }
}

コードはこれだけで、producerを受け取って、自身のsubscribeに接続します。 producerobserverを引数にとって、次に、どんなタイミングで、どんな値を流すか決定する関数です。 現時点ではイメージもわかないと思うので次ofを眺めたほうがわかりやすいかと思います。

of

次にofを実装します。ofは引数で受け取った値を順に流していくだけの最もシンプルなOperatorの一つです。

github.com

Observable.prototype.of = function (...values) {
  const producer = observer => {
    values.forEach(v => observer.next(v));
    observer.complete();
  }
  return new Observable(producer)
}

observerを引数にとるproducerを作成し、引数の値を順にobserver.nextで流し。完了すればobserber.completeします。 使用例は以下のようになります。1,2,3と値が流れ、completeします。冒頭で述べましたが、observerは本来errorをハンドリングする関数を含みますが簡素化のため削除しています。

const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

new Observable().of(1,2,3).subscribe(observer);

実際の動作を以下で確認することができます。

runkit.com

observersubscribeすることで初めて、producerが実行され、値が流れます。

map

もうひとつシンプルかつ、多用するoperatorであるmapを実装してみます。

Observable.prototype.map = function (f) {
  const producer = observer => {
    return this.subscribe({
      next(value) { observer.next(f(value)) },
      complete() { observer.complete() }
    })
  };
  return new Observable(producer);
}

関数を引数にとり、producerの中で受け取った関数を適用した値をobserver.nextで流します。 先程のofと合わせて以下のように使用します。

const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

new Observable().of(1,2,3).map(x => x * x).subscribe(observer); // 1, 4, 9, complete

1, 4, 9と値が流れcompleteします。 動作は以下で確認できます。

runkit.com

Subject

ここまで来るとあとは応用で、粛々とOperatorを追加していくだけなんですが、先にHotであるSubjectを実装しておきます。

class Subject extends Observable {
  constructor() {
    super(function (observer) {
      this.observers.push(observer);
    });
    this.observers = [];
  }

  next(x) {
    this.observers.forEach((observer) => observer.next(x));
  }

  complete() {
    this.observers.forEach((observer) => observer.complete());
  }
}

SubjectObservableを継承しsubscribeされると配信先を自身のリストに登録されるようにします。 また、登録された配信先に値を流せるようnextを生やします。nextでは登録済の配信先全てに値を流します。

const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

const subject$ = new Subject();
subject$.subscribe(observer);
subject$.next('hoge'); // hoge
subject$.next('fuga'); // fuga
subject$.complete();   // complete

良さそう。

runkit.com

filter

後は上記の応用でほとんどのものは実装できます。 filterは名前の通りなんですが、マーブルダイアグラムがあるとよりわかりやすいですね。

f:id:bokuweb:20170413231104p:plain

Observable.prototype.filter = function (f) {
  const producer = observer => {
    return this.subscribe({
      next(value) {
        if (f(value)) observer.next(value)
      },
      complete() { observer.complete() }
    })
  };
  return new Observable(producer);
}
const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

new Observable().of(1,2,3).map(x => x * x).filter(x => x % 2 === 0).subscribe(observer); // 4, complete

delay

こちらも名前の通り指定時間出力を遅延させるoperatorです。

Observable.prototype.delay = function (time) {
  const producer = observer => {
    return this.subscribe({
      next(value) {
        setTimeout(() => observer.next(value), time)
      },
      complete() {
        setTimeout(() => observer.complete(), time)
      }
    })
  };
  return new Observable(producer);
}

1秒後に1が流れてきます

const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

new Observable().of(1).delay(1000).subscribe(observer); // 1 after 1sec

fromPromise

promiseからobservableに変換するoperator

Observable.prototype.fromPromise = function (promised) {
  const producer = observer => {
    return this.subscribe({
      next(value) {
        promised.then((a) => {
          observer.next(a)
        })
      },
      complete() {
        promised.then(() => {
          observer.complete()
        })
      }
    })
  };
  return new Observable(producer);
}

5秒後に1が流れてきます

const observer = {
  next: (v) => console.log(v),
  complete: () => console.log('complete'),
}

new Observable()
  .of(null)
  .fromPromise(new Promise(resolve => {
    setTimeout(() => { resolve(1)}, 5000)
  }))
  .subscribe(observer); // 1 after 5sec

combineLatest

頻出ですが、このあたりから結構複雑ですね。 みんなだいすきcombineLatest

本来combineLatestは最後の引数が値を変換するtransform関数なんですが、省略も可能になっていて、今回はObservableの結合のみ行うOperatorとして実装しています。

f:id:bokuweb:20170413224908p:plain * http://rxmarbles.com/#combineLatestより

Observable.prototype.combineLatest = function (...observables) {

  const length = observables.length + 1;
  const producer = outObserver => {
    const values = [...Array(length)].map(_ => undefined);
    const hasValue = [...Array(length)].map(_ => false);
    const hasComplete = [...Array(length)].map(_ => false);

    const next = (x, index) => {
      values[index] = x;
      hasValue[index] = true;
      if (hasValue.every(x => x === true)) outObserver.next(values);
    };

    const complete = (index) => {
      hasComplete[index] = true;
      if (hasComplete.every(x => x === true)) outObserver.complete();
    };

    observables.forEach((observable, index) => {
      observable.subscribe({
        next: (x) => next(x, index + 1),
        complete: () => complete(index + 1),
      });
    });
    this.subscribe({
      next: (x) => next(x, 0),
      complete: () => complete(0),
    });
  };
  return new Observable(producer);
}

以下使用例。

new Observable()
   .of(0)
   .combineLatest(
     new Observable().of(1, 4),
     new Observable().of(2),
     new Observable().of(3).delay(1000),  
   )
   .subscribe(observer);  // [0, 4, 2, 3] after 1sec

動作を文書で説明するのがなかなか難しいOperatorだと思います。 1の値が流れていってしまうことに注意してください。初回のみ各streamの値が出揃うまで値は流れません。 次回以降値が流れてくる度に、他のstreamの最新の値と合わせて配列にパッキングされます。

以下のマーブルダイアグラムを動かしてみるのが一番しっくりくるかもしれません。

rxmarbles.com

動作確認は以下。

runkit.com

こっちはRx

runkit.com

switchMap

こちらも頻出、みんなだいすきswitchMapです。 実装も泥臭くて若干怪しいですが、おもちゃレベルでは動いてそうです。

switchMapは分かりやすいマーブルダイアグラムがないですね。。

Observable.prototype.switchMap = function (f) {
  const producer = outObserver => {
    let i = 0;
    let hasSourceCompleted = false;
    const completed = [];
    this.subscribe({
      next: (x) => {      
        i++;
        completed[i] = false;
        f(x).subscribe({
          next: ((index, y) => {
            if (index === i) {
              outObserver.next(y)
            }
          }).bind(this, i),
          complete: ((index) => {
            completed[index] = true;
            if (hasSourceCompleted && completed.every(x => x)) outObserver.complete();
          }).bind(this, i),
        });
      },
      complete: (() => {
        hasSourceCompleted = true;
        if (hasSourceCompleted && completed.every(x => x)) outObserver.complete();
      }),
    });
  };
  return new Observable(producer);
};

RxのswitchMapPromiseも展開してつないでくれますが、今回はObservableのみ対応しています。 使用例は以下。

const observer = {
  next(value) { console.log(value) },
  complete() { console.log('Done') }
}

new Observable().of(1, 2).switchMap((v) => {
  if (v === 1) return new Observable().of(v).delay(400);
  if (v === 2) return new Observable().of(v).delay(200);
}).subscribe(observer); // 2 after 200ms

12の値が順次流れてきて、1のときは400ms後に1が返るObservableが、2のときは200ms後に2が返るObservableがreturnされます。 200ms後に2が400ms後に1が流れてきそうですが、2switchMapに流れてきた時点でstreamは200ms後に2が返るObservableswitchされるのでobserverまで1が流れてくることはありません。

動作確認用

runkit.com

こっちはRx

runkit.com

さいごに

業務ではAngularを使用しているため、Observableの扱いにはいつも悩んでいて、もう少し仲良くなるために今回は実装してみました。 若干複雑なOperatorもありますが、mapfilterはかなりシンプルで、仕組みを知るにはちょうどいい題材ではないかと思います。 また趣味ではredux-observableを使用していて、わりと気に入っているのでもう少し使いこなせるようになりたいですね。

Angular2 RC5への更新とステート管理の変更、power-assertによるテストまで試す

概要

以前触ってみたときはRC3でRC5が出たらもう一回触るかってことで、以前作ったサンプルのRC5への更新、ステート管理の変更、ユニットテストについて試してみた。以前の記事は以下。

blog.bokuweb.me

RC5への更新

情報収集をするとNgModuleが追加されたことが大きいようで、コンポーネントごとにdirectivespipesでの指定を行う必要がなくなり、stableでこの方法は廃止になるとのこと。現状、このサンプルにおいてはRC3のコードのまま動作するしwarningもでなかった。

詳細は以下で確認すると良さそう。

ng2-info.github.io

ng2-info.github.io

NgModuleの導入

まずは@NgModuleを使用してモジュールを作ることになる。前回コンポーネントごとに記述していたディレクティブはdeclarationsに記述することになる。これによりモジュール配下のコンポーネントにおいて、ここで記述したディレクティブが使用できるようになる。サンプルでは未使用だがパイプなどもここにまとめて記述することになる。

providerにはDIプロバイダ、bootstrapにはエントリポイントとなるコンポーネントを指定する。

  • app/main.ts
import { NgModule, ApplicationRef } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 
import { CommentList } from '../components/comment-list';
import { CommentForm } from '../components/comment-form';
import { CommentItem } from '../components/comment-item';
 
import { HTTP_PROVIDERS } from '@angular/http';
import { CommentBox } from '../components/comment-box';

@NgModule({
   declarations: [
     CommentBox,
     CommentList,
     CommentForm,
     CommentItem,
   ],
   providers: [
     HTTP_PROVIDERS,
   ],
   imports: [BrowserModule],
   bootstrap: [CommentBox]
 })
 class AppModule {
 }
 
 platformBrowserDynamic().bootstrapModule(AppModule);

あとは各コンポーネントのdirectivesを削除し、動作した。 前回のRC3からRC5への差分は以下。

github.com

ステート管理の見直し

前回のサンプルを作成したときには、containerにステートを持たせて管理する方法をとっていた。ただ、ReactReduxなどを使っていて、コンポーネントに状態を閉じた場合、後々いい結果にならないケースが多い、印象があるので模索していたところ以下の記事を見つけたので今回のサンプルに反映してみることにした。特にコンポーネント数が多く、コンポーネントのステートが様々なコンポーネントに影響するような構成の場合、アプリケーションのステートを局所化する手段はあったほうがいいと思う。

vsavkin.com

概要

基本的にはReduxライクな構成でイミュータブルなステートをアクション経由で変更するというもの。そこにRxJS(というよりObservableか)を使用することでaction発行毎にreducerが走る問題などを解決しつつ、RxJSの恩恵を受ければ状態管理シンプルにできるよ。という趣旨に見える。

Actions

アクションを用意する。ひとまずpayloadにデータを放り込んでる。

  • actions/action
import { Comment } from '../interfaces/comment';

export class AddCommentAction {
  public payload: {
    id: number;
    text: string;
    author: string;
  }

  constructor(comment: Comment) {
    this.payload = comment;
  }
}

export class FetchCommentAction {
  public payload: {
    comments: Comment[];
  }

  constructor(comments: Comment[]) {
    this.payload = { comments };
  }
}

export type Action = AddCommentAction | FetchCommentAction;

Observable

ActionをもらってアプリケーションステートのObservableを返すステート関数を用意する。 Reduxでのreducerに相当する関数。ただし、reducerと違いこの関数自体は一度しか呼ばれないことを利点として上げている。 これによりすべてのReducerが呼ばれたり、Selectorが呼ばれたりしてパフォーマンスが落ちることを避けられるというかな?

  • store/comments.ts
import { AddCommentAction, FetchCommentAction, Action } from '../actions/action';
import { Comment } from '../interfaces/comment';
import { Observable } from 'rxjs';

export function comments(initState: Comment[], actions: Observable<Action>): Observable<Comment[]> {
  return actions.scan((state: Comment[], action: Action) => {
    if (action instanceof AddCommentAction) {
      const { id, text, author } = action.payload;
      const newComment = { id, text, author };
      return [...state, newComment];
    } else if (action instanceof FetchCommentAction) {
      return action.payload.comments;
    } else {
      return state;
    }
  }, initState);
}

次にrootReducerに相当する各ステート関数をcombineするstateFnを用意する。このサンプルではcombineする必要はないけど、参照記事ではfilter機能などを実装しているため、複数のステート関数をcombineしている。このサンプルではあまり意味を成さないものになっているので詳細は参照記事を確認してください。

  • store/index.js
import { AddCommentAction, Action } from '../actions/action';
import { Comment } from '../interfaces/comment';
import { Observable, BehaviorSubject } from 'rxjs';
import { comments } from './comments';

export function stateFn(initState: Comment[], actions: Observable<Action>): Observable<Comment[]> {
  const appStateObs = comments(initState, actions);
  return wrapIntoBehavior(initState, appStateObs);
}

function wrapIntoBehavior(init, obs) {
  const res = new BehaviorSubject(init);
  obs.subscribe(s => res.next(s));
  return res;
}

一つ注意点があって、wrapIntoBehaviorを実行しないとコンポネーントがsubscribeしたときにemitされるまでデータが受け取れないことになる。その問題をBehaviorSubjectで解決しているとのこと。

このあたりRxJSの理解力が乏しくてアレなんだが、BehaviorSubjectの挙動としては以下のようなものらしい。

直前にonNextで渡された値を保持し、subscribe()した直後に保持していた値を流します。その後の動作は後述のPublishSubjectと同等です。

f:id:bokuweb:20160818101059p:plain

Rxで知っておくと便利なSubjectたち - Qiitaより

この辺りの挙動はテストを書いてみるとわかりやすく、参照記事と同様のものを書いてみた。 後述するが、テストまわりは不明点周りで現状かなり辛い印象をもったが、この辺りはAngularから切り離されているためテストも容易だった。

import assert = require('power-assert');
import Rx = require('rxjs');
import { stateFn } from '../store/index';
import { AddCommentAction, Action } from '../actions/action';

describe ('store test.', () => {
  it('should create a new comment', () => {
    const actions = new Rx.Subject<Action>();
    const states = stateFn([], actions);
    actions.next(new AddCommentAction({ id: 1, text: 'text', author: 'author' }));
    actions.next(new AddCommentAction({ id: 2, text: 'foo', author: 'bar' }));
    states.subscribe(s => {
      assert.equal(s.length, 2);
      assert.deepEqual(s[0], {
        id: 1, text: 'text', author: 'author',
      });
      assert.deepEqual(s[1], {
        id: 2, text: 'foo', author: 'bar',
      });
    });
  });
});

Application and View Boundary

上記のactionやstateをView側で扱う準備をする。 この辺り理解が怪しいんだが、Angular 2のDIは基本的に、 トークン に対して値をセットしており、OpaqueTokenはProviderのトークンとして使いやすいインスタンスを提供してくれると書いてある。詳細は以下を参照すると良さそう。

import { OpaqueToken } from '@angular/core';
import { Subject } from 'rxjs';
import { Action } from '../actions/action';
import { stateFn } from '../store';

export const initState = new OpaqueToken("initState");
export const dispatcher = new OpaqueToken("dispatcher");
export const state = new OpaqueToken("state");

export const stateAndDispatcher = [
  {
    provide: initState,
    useValue: [],
  }, {
    provide: dispatcher,
    useValue: new Subject()
  }, {
    provide: state,
    useFactory: stateFn,
    deps: [initState, dispatcher]
  }
];

そして、こいつを@NgModuleで登録してやる。

...
import { stateAndDispatcher } from '../store/state-and-dispatcher';

@NgModule({
  declarations: [
    CommentBox,
    CommentList,
    CommentForm,
    CommentItem,
  ],
  providers: [
    HTTP_PROVIDERS,
    stateAndDispatcher,
  ],
  ...

View

以下のようにInjectしてdispatchと、stateを使用している。

  • components/comment-box.ts
import { Component, OnInit, Inject } from '@angular/core';
import { CommentService } from '../service/comment';
import { Comment } from '../interfaces/comment';
import { Observable, Observer } from 'rxjs';
import { Action } from '../actions/action';
import { state, dispatcher } from '../store/state-and-dispatcher';
import { AddCommentAction, FetchCommentAction } from '../actions/action';

@Component({
  selector: 'comment-box',
  providers: [CommentService],
  template: `
    <div class="commentBox">
      <h1>Comments</h1>
      <comment-list [comments]="comments"></comment-list>
      <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form>
    </div>
  `,
})
export class CommentBox implements OnInit {

  comments: Comment[];

  constructor(private commentService: CommentService,
              @Inject(state) private state: Observable<Comment[]>,
              @Inject(dispatcher) private dispatcher: Observer<Action>) {
    this.state.subscribe(comments => this.comments = comments);
  }

  ngOnInit() {
    this.commentService
      .startIntervalFetch()
      .subscribe(comments => this.dispatcher.next(new FetchCommentAction(comments)));
  }

  handleCommentSubmit(comment) {
    comment.id = this.comments.length;
    this.commentService
      .add(comment)
      .subscribe(res => this.dispatcher.next(new AddCommentAction(comment)));
  }
}

書いてて思ったけど、このコンポーネントはSmartComponentとしての責務に専念して、templateへの記述は最小限に抑えるべきな気がする。 あとこういった構成の場合複雑な非同期処理が必要となった場合(API設計が悪いとか、BFFが、とかはひとまず置いておいて)、シンプルに対応できるかというのが頭をよぎるんだが、この場合サービス側でごにょごにょやる感じになるのかな。

これはAngularではなくRxJS力の低さの問題なんだけど、非同期処理をチェーンさせる(たとえばユーザーリストをフェッチ後、ユーザidからお友達リストを取ってここなくてはならない、など)場合、どう書くのがシンプルなのかわからない。

テスト

mocha + power-assertでテストが書けるとのことなので一番シンプルなコンポーネントで試してみた。 とは言え、現状不明点が多く、「こうしたらなんか動いた」的な状態に近い。

テスト対象

  • components/comment-list.ts
import { Component, Input } from '@angular/core';

@Component({
  selector: 'comment-list',
  template: `
    <div class="comment-list">
      <div *ngFor="let comment of comments">
        <comment-item [author]="comment.author" [text]="comment.text"></comment-item>
      </div>
    </div>
    `
})

export class CommentList {
  @Input() comments;
}

テスト

  • test/comment-list.test.ts
import 'es6-shim';
import 'reflect-metadata';
import 'zone.js/dist/zone';
import 'zone.js/dist/long-stack-trace-zone';
import 'zone.js/dist/async-test';
import 'rxjs';
import { TestBed } from '@angular/core/testing';
import { Component } from '@angular/core';
import assert = require('power-assert');
import { CommentList } from '../components/comment-list';
import { CommentItem } from '../components/comment-item';
import { Comment } from '../interfaces/comment';
import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
    BrowserDynamicTestingModule,
    platformBrowserDynamicTesting()
);

describe ('comment-list test.', () => {
  @Component({
    template: `
      <comment-list [comments]="comments"></comment-list>
    `,
  })
  class TestCmp {
    comments: Comment[] = [
      { id: 0, text: 'text1', author: 'author1' },
      { id: 1, text: 'text2', author: 'author2' }
    ];
  }

  beforeEach(() => {
    // setup
    TestBed.configureTestingModule({
      declarations: [TestCmp, CommentList, CommentItem]
    });
  });

  it('component test', (done) => {
    // compile test component
    TestBed.compileComponents().then(() => {
      // create component instance
      const fixture = TestBed.createComponent(TestCmp);
      fixture.detectChanges();
      const el = fixture.debugElement.nativeElement as HTMLElement;
      const authors = el.querySelectorAll('comment-item h2.comment-author');
      const text = el.querySelectorAll('span');
      assert.equal(el.querySelectorAll('comment-item').length, 2);
      assert.equal(authors.length, 2);
      assert.equal(authors[0].textContent.trim(), 'author1');
      assert.equal(text.length, 2);
      assert.equal(text[0].textContent.trim(), 'text1');
      assert.equal(text[1].textContent.trim(), 'text2');
      done();
    });
  });
});

以下にも記述があるが、ユニットテスト用のモジュールを管理するTestBed APIが追加されたらしく、こいつを使ってテスト用のコンポーネントを作成したりしてテストしていくことになりそう。

ng2-info.github.io

最初はTestBed.createComponent(CommentList)として、createComponentに直接テスト対象のコンポーネントを追加していたんだけど、この場合@Inputをどのように解決するのかわからず、上位コンポーネントとしてTestCmpを作成し、こいつを渡すことにした。以下のモジュールのテストを参考にしていて、これならspystubなどを仕込むのも簡単にできそう。

github.com

また、以下の記事もユニットテストやマーブル記法のテストまで触れてあって参考になった。

qiita.com

beforeEachでは TestBed.configureTestingModule({declarations: [TestCmp, CommentList, CommentItem]});としてテスト用のモジュール設定を行っている。これは@NgModuleに相当するところのようで、使用するディレクティブなどはここで設定しておく必要がある。(子や孫なども含めて)。

    TestBed.compileComponents().then(() => {
      // create component instance
      const fixture = TestBed.createComponent(TestCmp);
      fixture.detectChanges();
      const el = fixture.debugElement.nativeElement as HTMLElement;

あとは上記のようにすることでレンダリング後のHTMLElementがもらえるので、querySelectorで検索してテストしてみた。

ここまでやって思ったのはShallowRenderingまたはそれに相当するものが欲しい。あるのかどうかわからないが。 そうしないとすべてのコンポーネントが展開されてしまい、不味いことになる。

すでにShallowRenderingのようなものがあって見つけられていないのか、それとも、TestBed.configureTestingModuleのdeclarationsにモックを登録したりTestBed.overrideComponentを上手く使ってやりくりしたりするのかよく分かっていない。上記の記事にも書いてあるけど、とにかく今は情報が少なくて辛い。

また、今回は雑にquerySelectorを使っているけど、Enzyme的なView用テストユーティリティがあると良さそう。すでにあるのかもしれないけど・・。

追記

ここにテストについて結構書いてある

habrahabr.ru

モダンCSSフレームワーク『Bulma』のReactコンポーネントセット『ReBulma』を作った

概要

今年に入ってからのんびりTwitterClient(https://github.com/bokuweb/tsukiakari)を作っていて、こいつに使いたいなーと思いコンポーネントセット作った。といっても、まだかなり雑でまだまだやることがありそう。

以下のような記事もあって様々なReactコンポーネントセットがあるんだが、あまり気にいるものがなく作ってみることにした。有名ところはMateri-UIあたりになるんだろうけど、正直自分はMaterialデザインの色とか見た目があまり好きじゃないし、Material-UIの挙動が?(現在は改善されてるかも)だったり、Styleのみを閉じたシンプルなものが欲しいというのも理由のひとつになっている。

qiita.com

Bulma

BulmaFlexboxベースのモダンCSSフレームワークで現在V0.1.0RC。どんな感じなのかは以下の公式を眺めるとよい。先日もGithub Trendに入っていたので注目度は高いっぽい。

bulma.io

紹介記事もいっぱいでてくるし、以前作った(http://bokuweb.github.io/slack-list-ja/)でも試しに使っている。

coliss.com

作ったもの

作ったものは以下。

github.com

コンポーネントは以下から確認できます。サンプルコードは直接編集できるので、適当にいじってみてください。

re-bulma styleguide

一例を載せると以下のような感じ。

f:id:bokuweb:20160729112449p:plain

気をつけた点など

グローバルスコープを汚染しないこと

自分はもともとこういったCSSフレームワークが好きじゃなくて、大きな理由のひとつにグローバルスコープの汚染が上げられと思う。 これはどのCSSフレームワークも大きな差はないと思うけど、例えばBulmaの場合も以下のような記述があって、コンポーネント外にもスタイルは染みだしてしまう。

body {
  color: #69707a;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.428571428571429;
}

a {
  color: #1fc8db;
  cursor: pointer;
  text-decoration: none;
  -webkit-transition: none 86ms ease-out;
  transition: none 86ms ease-out;
}

a:hover {
  color: #222324;
}

なので、今回はこういったグローバルなスタイルを削除し、コンポーネントに閉じつつ見た目を保っている(つもり。だけど、やはり多少の差異は避けられないような気もしている。例えばfont-size: inheritが設定されたコンポーネントなどは、グローバルにfont-sizeを指定していないのでBulmaの見本とくらべて大きくなっていると思う。)。これで、コンポーネントのスタイルがコンポーネント外のスタイルに影響を与えることはひとまず、無くなっていると思う。

擬似スコープ

csjs

擬似スコープを作成するのには、今回はcsjsを使ってみることにした。こういったことを行うツールで有名所はCSS Modulesなんだろうけど、自分が食わず嫌いの天邪鬼で(CSS Modulesを使った際に外部にコンポーネントとして提供する手順がぱっと見よくわからなかったのもあるが)今回はcsjsを使った。

github.com

csjsはテンプレートストリングを使って以下のように使用する。

const styles = csjs`
  .title {
    font-size: 15px;
  }
`;

class Foo extends Component {
  render() {
    return (
      <h1 className={styles.title}>Bar</h1>
    );
  }
}

すると実際には以下のようにclassNamesuffixが付加され擬似スコープ化されるというもの。あとはcsjsを経由して得られるcssを何らかの方法でinjectする必要がある。(後述するが今回はsubstack/insert-cssで内の放り込んでいる。)

  render() {
    return (
      <h1 className="title_3YGtO7">Bar</h1>
    );
  }
問題点

上記のようにcsjsでランダムなsuffixをつけて、スタイルを上書きしたい場合は上位からclassNameを渡して設定させようとしていたんだけど、これだと詳細度の関係で思うようにスタイルの上書きができなかったり、入れ子になっているコンポーネントまでclassNameが渡せず、細かいカスタマイズができない、という話しがあって結局現在はcsjsを介して固定(___re-bulma_)のPrefixをつけるようにした。擬似スコープを作るという意味ではこのへんでも十分かもしれない。

npm iのみで済ませたい

これは些細な内容なんだけど、自分は最高に怠惰なのでnpm iした後に、別途<head />にcssを仕込むの嫌いで、上でも少し触れたようにsubstack/insert-csshead内に放り込んでいる。それだけのためにinsert-cssに依存してruntimeで挿入するのか?お前のトレードオフは間違っている。と言われたら「ですよねー」という感情しかわかない気もするけど、みんなコンポーネントのcssを別途、<head />にしこむことについてどんな印象を持っているんだろうか。

課題

容量

現在スタイル関係のファイルで百数十KBあって、それにコンポーネント(各数KB)あるのでかなりきつい。そもそもこうやって作ってもたものの本当に使うのコンポーネントって、ほんの一部でほとんどが無駄だと思う(Progressとか出番がイメージできない)。

なので課題としてlodashのようにコンポーネントの単位でインストールできるようにすれば今後使いやすいかもしれないと思ったりもしている。npm i re-bulma.button のようにインストールするイメージ。

そもそも、じゃあこういったUIキットみたいなものをプロダクションで自分が採用するかといったら、使用するケースは少ないと思うが、<Button />、とか<Input />程度の粒度で公開されていれば、使用できるケースも増えるかもしれない。

CSSのリファクタリング

今はcsjsの制約があって、本家のcssからグローバルな定義を解決しつつ機械的に移植したためcssで書いていて、散らかっているんだけど、リファクタリングしながらstylusで書いてもいいかもと思っている。

蛇足

名前は素直にreact-bulmaとかでいいかとおもったけど、こういったものをみんな作ろうと思うのか何なのかわからないけどreact-bulmabulma-reactも抑えられていて、re-bulmaにした。先に名前だけ抑えて何も作らないのは勘弁して欲しい。その過程でyamchaというのも発見した。ロゴはsvg。

github.com

さいごに

スローペースかもしれないけど、ぼちぼち改善していくのでよければ試してみてください。さくっとモックみたいなのを作るケースには役だつかも。

github.com

golang入門するためにlifegame書いてみた

タイトルとおりなんだけど、golang使ってみたいと思うケースが出てきて、入門のためまずはlifegameを書いてみた。

ルール

ライフゲーム - Wikipedia

結果

github.com

f:id:bokuweb:20160713185814g:plain

16/7/16修正しました

package main

import (
    "fmt"
    "math/rand"
    "time"
)

type game struct {
    field  [][]bool
    row    int
    column int
}

func newGame(row, column int) *game {
    rand.Seed(time.Now().UnixNano())
    p := new(game)
    p.row = row
    p.column = column
    p.field = make([][]bool, row)

    for r := 0; r < row; r++ {
        p.field[r] = make([]bool, column)
        for c := 0; c < column; c++ {
            if rand.Intn(10) == 0 {
                p.field[r][c] = true
            }
        }
    }
    return p
}

func (p *game) count(r, c int) int {
    if r < 0 || p.row <= r {
        return 0
    }
    if c < 0 || p.column <= c {
        return 0
    }
    if p.field[r][c] {
        return 1
    }
    return 0
}

func (p *game) updateCell(r, c int) bool {
    count := p.count(r-1, c-1) +
        p.count(r-1, c) +
        p.count(r-1, c+1) +
        p.count(r, c-1) +
        p.count(r, c+1) +
        p.count(r+1, c-1) +
        p.count(r+1, c) +
        p.count(r+1, c+1)

    if count == 2 {
        return p.field[r][c]
    } else if count == 3 {
        return true
    } else {
        return false
    }
}

func (p *game) render() {
    fmt.Print("\033[0;0H")
    for r := 0; r < p.row; r++ {
        for c := 0; c < p.column; c++ {
            cell := " "
            if p.field[r][c] {
                cell = "█"
            }
            fmt.Print(cell)
        }
        fmt.Println()
    }
}

func (p *game) update() {
    field := make([][]bool, p.row)
    for r := 0; r < p.row; r++ {
        field[r] = make([]bool, p.column)
        for c := 0; c < p.column; c++ {
            field[r][c] = p.updateCell(r, c)
        }
    }
    p.field = field
}

func main() {
    game := newGame(30, 90)
    game.render()
    ticker := time.Tick(time.Second)
    for {
        <-ticker
        game.update()
        game.render()
    }
}

その他

ひとまず、以下の記事を一通りやってみた。分かりやすかった。

blog.amedama.jp

普段なら3項演算子で書くところも、諦めて愚直にifで書く必要があるらしい。

qiita.com

テスト書いてないけど、プライベートな関数をテストしたかったら以下の記事のようにするんだろうか? httpパッケージなんかを見るとexport_test.goみたいなファイルを作ってexportしてるように見える。

medium.com

lodashを移植したら勉強になるのかと思ったりしたけど、そういったライブラリの需要はあるんだろうか?それともすでに同様のライブラリあるんだろうか。

追記

ご指摘いただきました。ありがとうございます。認識していなかったです。

qiita.com

Angular2でReactのチュートリアルを試してみる

概要

一回触ってみたいと思っていたAngular2をようやく触ってみた。最近は新しいフレームワークやライブラリを触る場合はゲームを作ってみるか、Reactのチュートリアルをやるようにしていて、今回はReactのチュートリアル(コメントフォームのやつ)をAngular2でやってみることにした。

基本的には環境構築周りは以下のシェルスクリプトマガジンとAngularのSlackチームng-japanのビギナー用に紹介されていた手順を踏んでいる。

シェルスクリプトマガジン vol.37

シェルスクリプトマガジン vol.37

  • 作者: 當仲寛哲,岡田健,佐川夫美雄,大岩元,松浦智之,後藤大地,白羽玲子,水間丈博,濱口誠一,すずきひろのぶ,花川直己,しょっさん,法林浩之,熊野憲辰,桑原滝弥,USP研究所,ジーズバンク
  • 出版社/メーカー: USP研究所
  • 発売日: 2016/04/25
  • メディア: 雑誌
  • この商品を含むブログを見る

Join ng-japan on Slack!

なにか間違いやより良い方法などありましたらご指摘願います。

バージョン

package.jsonも最後にのせるが

Angular: 2.0.0-rc.3 Typescript: 1.8.10

で試している。

Repository

github.com

作業ログ

インストール

Angular2関連のパッケージは@angular/xxxxxという形式になっているよう。すこし前まではangular2/xxxxxだったのかな。 一点注意が必要なのはRx.jsのバージョンが上がって、symbol-observableが必要になった点でしょうか。その他はほぼ、slackで紹介されていた手順だと思う。

stackoverflow.com

$ npm init -y

$ npm i -S es6-shim systemjs symbol-observable zone.js reflect-metadata rxjs @angular/common @angular/compiler @angular/core @angular/platform-browser @angular/platform-browser-dynamic

$ npm i -D concurrently lite-server typescript typings

npm scriptの記述。

  • package.json
"scripts": {
  "start": "concurrently \"npm run tsc:w\" \"npm run lite\" ",
  "tsc": "tsc",
  "tsc:w": "tsc -w",
  "lite": "lite-server",
  "typings": "typings",
  "postinstall": "typings install"
},

typescriptの設定。

  • tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "system",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "removeComments": false,
    "noImplicitAny": false
  },
  "exclude": [
    "node_modules",
    "typings/main",
    "typings/main.d.ts"
  ]
}

型情報のインストール

$ node_modules/.bin/typings init
node_modules/.bin/typings install -GS es6-shim jasmine --source dt
touch index.html
mkdir scripts
touch scripts/main.ts
mkdir -p components/home
touch components/home/home.ts
touch system-config.ts
touch bs-config.json
  • bs-config.json
{
  "port": 8000,
  "files": ["./**/*.{html,htm,css,js}"],
  "server": { "baseDir": "./" }
}

system.jsの設定。この辺りよく作法を理解していないので要復習。

  • system-config.js
System.config({
  map: {
    '@angular': 'node_modules/@angular',
    'rxjs': 'node_modules/rxjs',
    'scripts/main': 'app/main.js',
    'symbol-observable': 'node_modules/symbol-observable',
  },
  packages: {
    '@angular/core':  { main: 'index' },
    '@angular/common':  { main: 'index' },
    '@angular/compiler':  { main: 'index' },
    '@angular/http':  { main: 'index' },
    '@angular/router':  { main: 'index' },
    '@angular/platform-browser':  { main: 'index' },
    '@angular/platform-browser-dynamic':  { main: 'index' },
    'symbol-observable': { defaultExtension: 'js', main: 'index' },
    'rxjs':  { main: 'Rx' },
    'components':  { main: 'index' }
  }
});
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <base href="/">
        <title>ng2 comment system</title>
    </head>
    <body>
        <comment-box>Loading...</comment-box>
        <script src="node_modules/es6-shim/es6-shim.js"></script>
        <script src="node_modules/reflect-metadata/Reflect.js"></script>
        <script src="node_modules/systemjs/dist/system.src.js"></script>
        <script src="node_modules/zone.js/dist/zone.js"></script>
        <script>
         System.import('system-config.js')
               .then(function () {
                   System.import('app/main');
               })
               .catch(console.error.bind(console));
        </script>
    </body>
</html>
  • app/main.ts
import {bootstrap} from '@angular/platform-browser-dynamic'
import {AppComponent} from '../components/home/home'

bootstrap(AppComponent);
  • components/home.ts
import {Component} from '@angular/core'
@Component({
  selector: 'my-app',
  template: `
    <h1>Hello ng2</h1>
    `
})
export class AppComponent {
}

これでnpm startするとブラウザが開いて、Hello ng2 と表示されるはず。

Commentボックスの実装

app/main.tsから呼ぶのをcomponents/comment-boxに修正。

  • scripts/main.ts
import { bootstrap } from '@angular/platform-browser-dynamic';
import { CommentBox } from '../components/comment-box';

bootstrap(CommentBox);

index.htmlから呼ぶコンポーネント名を修正

<ng-app>を<comment-box>に変更

 <body>
   <comment-box>Loading...</comment-box>
          ...

components/home.tscomponents/comment-box.txに修正して、以下のように修正した。

comment-box

  • components/comment-box.ts
import { Component, OnInit } from '@angular/core';
import { CommentList } from './comment-list';
import { CommentForm } from './comment-form';
import { CommentService } from '../service/comment';
import Comment from '../interfaces/comment';

@Component({
  selector: 'comment-box',
  providers: [CommentService],
  directives: [CommentList, CommentForm],
  template: `
    <div class="commentBox">
      <h1>Comments</h1>
      <comment-list [comments]="comments"></comment-list>
      <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form>
    </div>
  `,
})
export class CommentBox implements OnInit {

  comments: Comment[];

  constructor(private commentService: CommentService) {
  }

  ngOnInit() {
    this.commentService
      .startIntervalFetch()
      .subscribe(comments => this.comments = comments);
  }

  handleCommentSubmit(comment) {
    comment.id = this.comments.length;
    this.commentService
      .add(comment)
      .subscribe(res => this.comments.push(res));
  }
}

こうなるまで結構右往左往したんだけど、ひとまず結果だけを載せておく。 また、ポイントとなりそうな箇所を以下に記載しておく。

コンポーネント

Angular2ではコンポーネントはDecoratorを使用して以下のような形で記述するらしい。

@Component({
  selector: 'comment-box',
  providers: [CommentService],
  directives: [CommentList, CommentForm],
  template: `
    <div class="commentBox">
      <h1>Comments</h1>
      <comment-list [comments]="comments"></comment-list>
      <comment-form (onCommentSubmit)="handleCommentSubmit($event)"></comment-form>
    </div>
  `,
})
export class CommentBox implements OnInit {
 ...省略
selector

他のコンポーネントからこのコンポーネントを呼ぶ場合はこの名前を使って呼ぶ。上記の場合であれば<comment-box></comment-box>とする。(<comment-box />とは書けないよう。)また、カスタムディレクティブの場合は使用するディレクティブを次のdirectivesで指定する必要があるっぽい。

directives

使用するカスタムディレクティブをここで指定する。この場合はCommentList, CommentFormというディレクティブを使用する。

providers

Injectするサービスを指定する。Viewに関心のない処理などはサービスとして切り出してInjectするらしい。このコメントシステムであればサーバとAjax通信する処理をサービス(CommentService)として切り出して、Injectしている。Injectされる側(CommentService)は後述する。

templete

テンプレートストリングを使用して、以下のように書く。

  template: `
    <div>Hello</div>
  `

また、templateUrlを使って次のように別ファイルのhtmlを使用するこもできるっぽい。

templateUrl: 'foo/bar.html'

どっちが主流なのだろうか。デザイナーとの協業であれば後者のほうがやりやすいだろうし、チームの体制に左右されるだろうか。テンプレートストリングだとエディタのシンタックスハイライトとかインデントが死んでてみんなどうしているのって感じ。

class

デコレートされる側のclassは以下のようになっている。

export class CommentBox implements OnInit {

  comments: Comment[];

  constructor(private commentService: CommentService) {
  }

  ngOnInit() {
    this.commentService
      .startIntervalFetch()
      .subscribe(comments => this.comments = comments);
  }

  handleCommentSubmit(comment) {
    comment.id = this.comments.length;
    this.commentService
      .add(comment)
      .subscribe(res => this.comments.push(res));
  }
}

OnInitってのを継承していて、これはLifecycle Hooksを使うためのもの。今回はReactcomponentWillMount相当する(?あってる?)タイミングでサーバへリクエストを投げたかったので、OnInitを使用している。以下によくまとまっていた。

blog.yuhiisk.com

constructorでは後述するcommentService(主にサーバとのAjax通信するサービス)を受け取って、ngOnInitでポーリングする処理を呼んでいる。handleCommentSubmitでは子コンポーネントのcomment-formからのデータを受け取りcommentServiceに渡すことで、コメントを投稿している。startIntervalFetch()add()ともにobservableが返ってくるのでsubscribeしてcommentsを書き換えたり、追加したりしている。

comment-list

基本的にはcomment-boxでの知識があればほぼほぼ書けた。

  • components/comment-list.ts
import { Component, Input } from '@angular/core';
import { CommentItem } from './comment-item';

@Component({
  selector: 'comment-list',
  directives: [CommentItem],
  template: `
    <div class="comment-list">
      <div *ngFor="let comment of comments">
        <comment-item [author]="comment.author" [text]="comment.text"></comment-item>
      </div>
    </div>
    `
})

export class CommentList {
  @Input() comments;
}

新しいポイントは以下。

@Input()

親コンポーネントからディレクティブに対する入力は@input()で定義する必要がある。

*ngFor

みたまんま繰り返しを行う記述でstructural directivesと呼ぶらしい。他には、*ngIf="condition"等が用意されている。ちょっと前まではletではなく#と書いていたようで、#を使うと現在では動作はするが、#" inside of expressions is deprecated. Use "let" instead!という警告がでる。

ここではcommentsの要素数分繰り返し<comment-item></comment-item>に渡している。

詳細は以下を見ると良さそう。

angular.io

comment-form

フォーム部分については以下のようになった。

  • components/comment-form.ts
import { Component, Output, EventEmitter } from '@angular/core';
import { CommentList } from './comment-list';
import { CommentService } from '../service/comment';
import Comment from '../interfaces/comment';

@Component({
  selector: 'comment-form',
  template: `
    <div class="comment-form">
      <input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" />
      <input type="text" value={{text}} (keyup)="onTextChange($event)" placeholder="Say something..." />
      <input type="submit" value="Post" (click)="handleSubmit()" />
    </div>
    `,
  styles: [`
    .comment-form {
      margin-top: 50px;
    }
  `]
})
export class CommentForm {
  @Output() onCommentSubmit: EventEmitter<any> = new EventEmitter();

  public author: string
  public text: string

  onAuthorChange(e: KeyboardEvent): void {
    this.author = (<HTMLInputElement>event.target).value;
  }

  onTextChange(e: KeyboardEvent): void {
    this.text = (<HTMLInputElement>event.target).value;
  }

  handleSubmit(): void {
    const author = this.author.trim();
    const text = this.text.trim();
    if (!text || !author) return;
    this.onCommentSubmit.emit({ author, text });
    this.text = '';
    this.author = '';
  }
}

新しい要素は以下。

styles

コンポーネントのスタイルはstyleで指定できる。

  styles: [`
    .comment-form {
      margin-top: 50px;
      }
  `]

Angular2Scoped CSSを実現しているという話しをちらっと聞いていたんだけどどのように実現しているか見たら以下のようになっていた。

スコープはどのように実現しているか確認すると以下のようになっていた。attributeを使用して擬似的にスコープを実現しているように見える。

  • html
<comment-form _nghost-dkv-3="">
    <div _ngcontent-dkv-3="" class="comment-form">
      <input _ngcontent-dkv-3="" placeholder="Your name" type="text" ng-reflect-value="">
      <input _ngcontent-dkv-3="" placeholder="Say something..." type="text" ng-reflect-value="">
      <input _ngcontent-dkv-3="" type="submit" value="Post">
    </div>
</comment-form>
  • css
.comment-form[_ngcontent-dkv-3] {
    margin-top: 50px;
}

またstyleUrlsによりcssファイルを使用することもできるっぽい。

styleUrls: ['app/style.css'],

バインディング

<input type="text" value={{author}} (keyup)="onAuthorChange($event)" placeholder="Your name" />のようにしてkeyupイベントで値をセットし、反映している。この辺りは以下のように書くことで1way2wayか選択できるっぽい。

  • 2way binding
<input type="text" [(ngModel)]=”author” placeholder=”Your name” />
  • 1way binding
<input type="text" value={{author}} placeholder="Your name" />

また、changeだとハンドラが呼ばれるのはblurのタイミングっぽくて、ReactでいうonChangeのタイミングで呼びたかったらkeyupイベントを使うのが良さそうに見える。

angular.io

@output

@input()があったように出力は@output()を使用するっぽい。ここではフォームの内容をEventEmitterを使って<comment-box></comment-box>まで引き上げている。多分、<comment-form></comment-form>に直接CommentServiceInjectすることもできるんだろうけど、Dumb ComponentSmat Componentを意識するとこういうことになるんじゃないかと思う。

comment-item

特筆すべき箇所はないが、この時点でmarkedを追加した。 あと、タグを使用する場合は[innerHTML]を使用するっぽい。

  • components/comment-item.ts
import { Component, Input } from '@angular/core';
import { parse } from 'marked';

@Component({
  selector: 'comment-item',
  template: `
    <div class="comment">
      <h2 class="comment-author">
        {{author}}
      </h2>
      <span [innerHTML]="rawMarkup()"></span>
    </div>
  `
})

export class CommentItem {
  @Input() author: string
  @Input() text: string

  rawMarkup():string {
    return parse(this.text, { sanitize: true });
  }

markedの追加

以下の手順で行った。

npm i -S marked
node_modules/.bin/typings -GS install marked --source dt

system-config.jsに追加

System.config({
  map: {
    ...省略
    'marked': 'node_modules/marked',
  },
  packages: {
    ...省略
    'marked': { main: 'index' },
  }

comment-service

CommentServiceに使用するHttpモジュールについては以下を参考にした。

qiita.com

angular.io

  • service/comment.ts
import { Injectable } from '@angular/core';
import Comment from '../interfaces/comment';
import { Http, Request, Response } from '@angular/http';
import { Observable } from 'rxjs';
import 'rxjs/add/operator/map';

@Injectable()
export class CommentService {

  constructor(private http: Http) {
  }

  startIntervalFetch(): Observable<Comment[]> {
    return Observable.interval(1000)
      .flatMap(() => this.http.get("http://localhost:3001/comments"))
      .map(res => res.json() as Comment[]);
  }

  add(comment: Comment): Observable<Comment> {
    return this.http
      .post("http://localhost:3001/comments", comment)
      .map(res => res.json() as Comment);
  }
}

startIntervalFetch()では1秒ごとにサーバにコメントを取りに行って、add()でコメントを追加している。 この時点でjson-serverをインストールしてやり取りはそっちと行っている。

@Injectable()

@Injectable()DIを可能にする。

使う側(components/comment-box.ts)は以下のようになる。

@Component({
  providers: [CommentService],
})
export class CommentForm {
  constructor(private commentService: CommentService) {
  }

package.json

package.jsonは最終的に以下。

{
  "name": "ng2-comment-tutorial",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "concurrently \"npm run tsc:w\" \"npm run lite\" \"npm run json\"",
    "tsc": "tsc",
    "tsc:w": "tsc -w",
    "lite": "lite-server",
    "typings": "typings",
    "json": "json-server --watch db.json --port 3001",
    "postinstall": "typings install"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@angular/common": "^2.0.0-rc.3",
    "@angular/compiler": "^2.0.0-rc.3",
    "@angular/core": "^2.0.0-rc.3",
    "@angular/http": "^2.0.0-rc.3",
    "@angular/platform-browser": "^2.0.0-rc.3",
    "@angular/platform-browser-dynamic": "^2.0.0-rc.3",
    "es6-shim": "^0.35.1",
    "marked": "^0.3.5",
    "reflect-metadata": "^0.1.3",
    "rxjs": "^5.0.0-beta.9",
    "symbol-observable": "^1.0.1",
    "systemjs": "^0.19.31",
    "zone.js": "^0.6.12"
  },
  "devDependencies": {
    "concurrently": "^2.1.0",
    "json-server": "^0.8.14",
    "lite-server": "^2.2.0",
    "typescript": "^1.8.10",
    "typings": "^1.3.0"
  }
}

 まとめ

  • 1.xのときよりシンプルになったような印象を受ける
  • 二の足を踏んでいたけど、導入障壁も高くないように感じた
  • ただ、逐一追っているわけではないがまだ破壊的な変更が入っているような話しを耳にする
    • 次に触るのはもう少し安定してからでいいかな、と感じた
    • その際はなにか独立したコンポーネント、例えばreact-resizable-and-movableあたりを移植してみたい
  • Singleton store脳な自分には状態を一元管理するような方法があるとありがたい
    • ng-reduxとか?あまりいい評判は聞かない。。。
  • テスト周りがわからん。rc.4からjasmineに限らずmochaも使用できるようになった?