@mizchi / Increments Inc
- id:mizchi | 竹馬光太郎
- Qiitaの方から来ました
- 業務エンジニア歴3年
- 積みゲーが終わらん
新卒で最初に書いた言語は Haskell と Clojure とあるゲームのUnityからHTML5への移植をして以来、SPAの設計を考え続けている
- VirtualDOM Advent Calendar 2014 - Qiita の主催
- あなたがReactを使うべき理由
- なぜ仮想DOMという概念が俺達の魂を震えさせるのか - Qiita
- #13 Virtual DOM | mozaic.fm のゲスト
- 「俺がプロダクションで使いたいから」に決まってんじゃん!
- みなさんご協力ありがとうございました!!!!
yaotti「XXX を AtomShellでWindows 向けに作れない?」 mizchi「あぁ^~いいっすね〜」 mizchi「プロトで React 使ったけどこんままいきましょう」 => Go
- 人柱になった
- というわけでReactを現場で使ってみた話をします
(みんなVirtual DOM は予習済みだよね?)
Real World Virtual DOM
- 第一章: Kobito on AtomShell
- 第二章: Arda - MetaFlux Framework
- 第三章: Isormorphicの実践
- 最後に: Virtual DOM をとりまく現実
- Incrementsが開発
- Markdownでメモを書けるMacアプリ
- Qiita(またはQiita:Team)への同期機能がある
- Objective-C
Mac版
- Kobito を AtomShell で実装してWindows版だそうぜっていうプロジェクト
- 今日が初公開
- 開発動機: WindowsのKobitoがない
- デスクトップアプリ
- 既存のKobitoのクローンではなく、いくつかの課題を解決しつつ開発
- View は React Component + 自作Flux Framework
(画面は開発中のものです)
(画面は開発中のものです)
デモ
- Qiita / Qiita:Team との同期機能を強化
- ローカルに閉じたInboxの追加
- 単純なメモツールとしての使い勝手を強化
- Vim キーバインドモードの追加(開発者の趣味)
- リリース予定: 2015年 4月~5月
- => Kobito for Windows Newsletter
- 企画 / プロト 10月後半 ~
- 設計 - 11月 ~
- 実装 - 12月~
- バグ洗い出しとリファクタ(いまここ)
エンジニア1人(mizchi) 1月からマークアップ1人
- 単なる**「データバインド付きテンプレートエンジン」**で、描画後再利用可能なコンポーネント
- 必要十分に速い(さすがに職人芸的なDOMチューニングには劣る)
- 今まで苦労していた状態遷移が死ぬほど単純化される
- 情報が十分にある(ただし海外中心)
- ヘッドレス環境(node)でのテストケースの書きやすさ重視 他の環境ならもっと小さいライブラリを使っていたかも。Qiita にもまだ入れてない。
- 設計が単純化された結果、アプリケーションドメイン層が明確に意識できるように
- Pure JavaScript, いわゆる「Isomorphic化」可能な箇所に注力できる(あとで詳しく話す)
- 開発したコンポーネント群を順次フィードバックしていきたい
- たとえば…
Markdown ハイライト付きのエディタ
Atom の Cmt+T 的なインクリメンタルサーチ
- Atom Editorの基盤
- Multi Platform (Win/Mac/Debian)
- デスクトップアプリの為のChromium ラッパー
- nodeのモジュールを呼べる
- クロスオリジンを超えれる
- デスクトップアプリとして配布できる
- ブラウザストレージの上限を任意に増やせる
- (Blink以外の動作確認をサボれる)
- Web界隈だとWindowsの知見をためても活用しにくく、WPFの知識を蓄える動機がない
- node/HTMLのノウハウを活かせる
- HTML/JSでQiitaとコンポーネントを共有できる
- やることは実質SPA だが…
- ネイティブアプリとしてのUXを期待される
- そのための React
- Reactの仮想DOMに最適化されたJavaScript の文法拡張
var div = <div/>;
みたいなやつ
- JavaScriptの知識を要求しすぎる
items.map(item => <Item data=item.data/>)
がリスト要素作ってるのわかる?
- 非JSエンジニアと協調するには厳しい
- 他のAltJSと相性がよくない
- 今回はCoffeeScriptとTypeScriptを使っているので最悪
- テンプレートと密結合しすぎる
- ViewModel を強く意識してテンプレートとプロパティを分離を試みた
- jadeテンプレートからReactのVirtual DOM が吐ける
- jadeの開発元が提供しているので、メンテされるだろうという期待がある
.container
h1(onClick=onClickTitle)= This is title
↓
React.createElement('div', {className: 'container'}, [
React.createElement('h1', {onClick: onClickTitle}, 'This is title')
])
// ヘッダ省略
いわゆるhaml系テンプレート
- JS詳しくない人「なんかよくわからんプロパティがあるが触れる」程度に落ち着く
- (Qiita本体はslim っていう背景があるかも)
- UI層: React Component / Dispatcher
- CoffeeScriptで高速にTry & Error を回す
- テンプレートはreact-jade
- Store層 / Domain層
- TypeScript の common.jsモード
- CoffeeScript に require される(逆はない)
- 単方向データフロー
- 状態管理コストが低いVirtualDOMに向いた設計を実現する思想. (実装ではない)
- 詳しくは誰かが話してくれ(る/た)でしょう or ぐぐれ
- Fluxxor
- Reflux
- Alt
- Fluxible
- Facebook's flux
- Deloerean
- etc...
- 薄い
- どの実装もIdiomatic
- どれが生き残るかわからん
- mizchi/arda - Github
- 元々は Kobito on Atom Shell の状態管理と画面遷移を抽象化したもの
- そこそこテスト書いて、だいぶドッグフーディングしているので実用に耐えうるはず
- J・R・R・トールキンの「指輪物語」の世界の名前であり地球そのものでもある
- VirtualDOMの仮想な世界と現実が融合する場所ぐらいのニュアンス
- ぶっちゃけ短けりゃなんでもよかった
- 既存のFlux実装は「画面遷移」が表現しにくかった
- react-routerが使いにくかった/目的が違った
- Store層をTypeScriptフレンドリーに型で保護できるように分離したかった
- Store/View/Dispatcherの塊を「Context」という単位で管理
- Contextのスタックで状態を表現
- React のState/Props の概念は継承
- すべての状態遷移をPromise化
- 単なるFluxではなくFluxを内包したより上位のFramework
- Viewは単なるReact.Component
- Dispatcherは単なるEventEmitter
- StoreはEventを受けて状態を更新
- Router から初期入力(Props)を受けて初期化される
- Propsから初期State(Context内状態)を作る
- Props と State から、Componentに渡すプロパティ(ComponentProps)を生成
- Component に渡す
- 状態が更新されたら3に戻る
- pushContext
- popContext
- replaceContext
- APIで察して
- Contextの生成と破棄を担当(SPAはそこらへん厳しい)
class Clicker extends Arda.Component
render: -> React.createElement 'button', {onClick: @onClick.bind(@)}, @props.cnt
onClick: -> @dispatch 'clicker:++'
class ClickerContext extends Arda.Context
@component: Clicker
initState: (props) -> cnt: 0
expandComponentProps: (props, state) -> cnt: state.cnt
delegate: (subscribe) ->
super
subscribe 'clicker:++', =>
@update((s) => cnt: s.cnt+1)
router = new Arda.Router(Arda.DefaultLayout, document.body)
router.pushContext(ClickerContext, {})
class Clicker extends Arda.Component
render: -> React.createElement 'button', {onClick: @onClick.bind(@)}, @props.cnt
onClick: -> @dispatch 'clicker:++' #<= EventEmitterへ発火
class ClickerContext extends Arda.Context
@component: Clicker
initState: (props) -> cnt: 0
expandComponentProps: (props, state) -> cnt: state.cnt
delegate: (subscribe) ->
super
subscribe 'clicker:++', => #<= EventEmitterのEvent受信
@update((s) => cnt: s.cnt+1)
router = new Arda.Router(Arda.DefaultLayout, document.body)
router.pushContext(ClickerContext, {})
Event は一方通行
class Clicker extends Arda.Component
render: -> React.createElement 'button', {onClick: @onClick.bind(@)}, @props.cnt
onClick: -> @dispatch 'clicker:++'
class ClickerContext extends Arda.Context
@component: Clicker
initState: (props) -> cnt: 0 #<= 初期状態
expandComponentProps: (props, state) -> cnt: state.cnt #<= ComponentのProps
delegate: (subscribe) ->
super
subscribe 'clicker:++', =>
@update((s) => cnt: s.cnt+1) #<= 状態の更新
router = new Arda.Router(Arda.DefaultLayout, document.body)
router.pushContext(ClickerContext, {})
Mutable なのは State だけ
- 型によって仕様が明確になる
- Props は画面を再構築するのに必要な情報
- State はその画面の中で変化する状態
- ComponentProps は 実際にComponent に渡されるもの
- 関心の分離
- Componentが知るべき状態だけに変形したい
- 型で保護しにくいComponent に直接 Props と State を渡すのは嬉しくない
- Stateとして何かの id だけ持って DBやネットワークを叩くと、結果に再現性がなく State として持ちたくない
buildTimelineByGroupId(state.selectedGroupId).then((items) = {
this.render(items); // ここを持ちたくない
});
- ComponentPropsが同じなら必ず同じビューを状態を再現できる(とする)
- Component と Props の組み合わせの URLへのシリアライズ/デシリアライズ を実装すれば Browser Hisotry に対応可能
- AgnosticにしたいのでArdaではブラウザヒストリーを関知しない
- arda.d.ts の型定義ファイルがAPIドキュメントを兼ねてる
- Arda自身はcoffeescriptで記述
- 最初はtypescriptで書いたが、メタプロだらけで型が生きず、代わりにテストを多めに書いた
- Context を TypeScript で型で保護する。
- Component は CoffeeScript で雑に書いて Event をdispatch する
- Eventの購読側はTypeScript で書いているが、受け取る引数についてはお約束程度
Arda with TypeScript
interface Props {firstName: string; lastName: string;}
interface State {age: number;}
interface ComponentProps {greeting: string;}
class MyContext extends Arda.Context<Props, State, ComponentProps> {
initState(props){
return new Promise<State>(done => {
setTimeout(done({age:10}), 1000)
})
}
expandComponentProps(props, state) {
return {greeting: 'Hello, '+props.firstName+', '+state.age+' years old'}
}
}
# 中略
router.pushContext(MyContext, {firstName: 'Jonh', lastName: 'Doe'})
- 既存のFluxの弱い点をカバーできたと思う
- 自分にとっては最高なんで流行らせたい
- APIも覚えることも少ないので使ってくれ🙏
- 「同じライブラリがnodeでもブラウザでも動けばいいよね」という発想
- browserify/webpackによって実現可能になった
- たとえnode(iojs)は使わなくても、単体テストはnodeでやるのが簡単で高速
- フロントエンドの各種プリコンパイラやタスクランナーもnode
- 起動コストが高く不安定なヘッドレスブラウザ(phantomjs)の使用を極力避けたい
- node の
global
と ブラウザのwindow
が共存する特殊な環境 - 成果物はいずれQiitaへ持ち込みたい
というわけでKobito on Atom Shell では Isomorphic を強く意識して設計した
- ストレージ
- DOM
- mongodb風のAPIを持った永続ストレージ
- 保存先を切り替えて実行環境を選べる(IndexedDB/オンメモリ/MongoDb)
- 採用理由: 元々 meteor の一部でよくテストされている
- テスト環境下ではオンメモリモードにして起動し、テストケースごとに生成/破棄
- mizchi/minimongo-schema スキーマ定義のJSONからDB初期化
- mizchi/factory-dog ↑用のスキーマからダミーオブジェクトの生成(雑なfactory-girl実装)
- mizchi/mz-repository リポジトリパターン実装
- mizchi/noo ES6Proxyを用いた rspec の null object っぽいやつの実装
schema.databases[0].type = 'memoryDb'
global.stubDatabases = -> # helper
beforeEach ->
initDatabasesBySchema(schema).then ([db]) ->
global.db = new Repository.Database(db)
global.Item = db.getCollection('items')
global.Team = db.getCollection('teams')
afterEach ->
delete global.db
delete global.Item
delete global.Team
- ブラウザ環境がなくても動く(Server Side Rendering の為)
- jsdom でも結構動く
var Component = React.createClass({
render: function(){return React.createElement('div', {}, 'this is title');}
});
var html = React.renderToString(React.createFactory(Component)());
assert.ok(html.indexOf('this is title') > -1);
componentWillMount
まで呼ばれるのがポイント(componentDidMount
は呼ばれない)
jsdom = require('jsdom').jsdom;
global.document = jsdom('<html><body></body></html>');
global.window = document.parentWindow;
global.navigator = window.navigator;
React = require('react/addons');
var el = React.createElement('div');
component = React.addons.TestUtils.renderIntoDocument(el)
サーバー(node)でクリックイベント発火もテストできる。
参考: JSDOMとReact.addons.TestUtilsでReactをヘッドレスにテストする - Qiita
- src/(.ts, .jade, .coffee) を相対パスを維持したままコンパイルし lib/(**.js)へ
- browserifyで lib/index.js を 全部入り(node_modules以下の依存含む)の bundle.js としてビルド
(gulpで拡張子ごとに監視して差分ビルド)
src/
- main.coffee
- foo.ts
- template.jade
lib/
- main.js
- foo.js
- template.js
public/
- bundle.js # lib node_modules の依存全部入り
- index.html
node_modules/
- ...
test/
- main-test.coffee
- 用途に応じた実行方式の切り替え
- モジュラリティの向上
- 配布用にビルド済みのbundle.jsを使ってサイズ削減(85MB -> 1.8MB)
元サイズが大きい理由は、node_modules/* の依存が全部入っているせい。
- AtomShell内蔵のnodeを使って、lib/indexから相対パスで解決。
- やや時間が掛かるbrowserifyをスキップできる
- ブラウザでbundle.jsを読み込むindex.html から普通に起動するだけ
- クロスオリジン制約にひっかからないもの、ネイティブを呼ぶ機能以外は実行可能
- 何かに使えないか考えている…(体験版とか?)
- lib 以下のファイルを
test/**/*
から相対パスでrequireして実行 - ヘッドレスなのでとにかく速いし安定する
- ブラウザビルドと同じように構築
- AtomShell用のSeleniumアダプターの設定をサボることができた
webpackはいろんなことが出来過ぎて、node にない挙動が可能なので Isomorphic 性を守るためにあえてbrowserifyを使っている。
- 画面に変化を起こす/起こし続けるのが圧倒的に楽
- とはいえ周辺ライブラリが枯れてない
- Issueでバグ報告しまくったり自分でforkしてパッチあてたりしてる
- サイズがやや大きい(.min.js で127k)(jQueryと同じぐらい)
- より小さな実装
virtual-dom/deku/mithril/riot
も考慮にいれるべきかも? - とはいえ一番枯れてる
「どういう設計がいいかわからん」
- Ardaの大規模向けプロジェクトスケルトン置いとくんでどうぞ
- mizchi-sandbox/arda-starter-project
- 実際の Kobito on AtomShell とほぼ同様
- 思想の段階でコンフリクトしているので協調が難しい
- 既存資産の以降からの、一番のボトルネックであることは否めない
- 使えないわけではなく リードオンリー だと考えると自然
- その50行のスパゲティコード、Reactだったら10行のComponentにならない?
- そういう視点を常に持つ
- 手を動かそう!
<div key='hogefuga'></div>
でユニークなkey属性を持つ仮想DOMなら消えない- コンテナの中をjQueryの領域とする
- スクロール量の読み出しと更新
- 擬似クリックイベントの発火
- aタグを全てオーバーライドしてAtomShellの外に出てしまわないように
- よりサーバーサイド言語の発想に近くなる
- デザイナーにとっての学習コストは上がり、エンジニアにとっては下がる
- 適切な分業体制が必要
- やるべきことはサーバーサイドnodeエンジニアと全く同じ(実行環境が異なる)
- 画面の構築に必要な発想は、ネイティブのアプリケーションエンジニアと同じ
- 自分はゲーム開発とAndroidの経験が生きた
- ストレージを扱うとデータ管理がシビアに
- ドメイン駆動を意識する
- RP/FRP
- もっともよく聞く
- 同じ感想だがそもそも要求が複雑化しているので…
- とはいえVirtualDOM は設計の単純化方向に働くので現実的に採用可能
- 現場で使ってみたけど大丈夫
- Reactは設計の単純化に方向に働く
- フロントエンドはIsomorphic化され, node(iojs)のスキルによって効率化される
- Arda によって画面遷移を管理し、型による「硬さ」を調節できるようにした
- 4月中に出したい
- => Kobito for Windows Newsletter
- デザイナの手が足りなくて辛い
- Qiita/Kobito のデザインしたい人きてくれ!!!