Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active September 3, 2015 18:09
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mizchi/3c04122d17cb04081286 to your computer and use it in GitHub Desktop.
Save mizchi/3c04122d17cb04081286 to your computer and use it in GitHub Desktop.
Real World Virtual DOM 原稿

Real World Virtual DOM

React, Flux, Isormorphic… そして現実

  @mizchi / Increments Inc


自己紹介

left inline 70 %

  • id:mizchi | 竹馬光太郎
  • Qiitaの方から来ました
  • 業務エンジニア歴3年
  • 積みゲーが終わらん

今まで仕事してきた環境

inline 20pxinline 20pxinline

新卒で最初に書いた言語は HaskellClojure とあるゲームのUnityからHTML5への移植をして以来、SPAの設計を考え続けている


Virtual DOM についての活動


みなさん


魂震えてます?


僕がReactについて

情報発信していた理由


理由

  • 「俺がプロダクションで使いたいから」に決まってんじゃん!
  • みなさんご協力ありがとうございました!!!!

結果

yaotti「XXX を AtomShellでWindows 向けに作れない?」 mizchi「あぁ^~いいっすね〜」   mizchi「プロトで React 使ったけどこんままいきましょう」 => Go


「使ってみた」

  • 人柱になった
  • というわけでReactを現場で使ってみた話をします

(みんなVirtual DOM は予習済みだよね?)


今日のテーマ

Real World Virtual DOM

  • 第一章: Kobito on AtomShell
  • 第二章: Arda - MetaFlux Framework
  • 第三章: Isormorphicの実践
  • 最後に: Virtual DOM をとりまく現実

第一章

Kobito

on Atom Shell


Kobitoとは

  • Incrementsが開発
  • Markdownでメモを書けるMacアプリ
  • Qiita(またはQiita:Team)への同期機能がある
  • Objective-C

Mac版

inline fit


Kobito "on AtomShell"

  • Kobito を AtomShell で実装してWindows版だそうぜっていうプロジェクト
  • 今日が初公開

Kobito "on AtomShell"

  • 開発動機: WindowsのKobitoがない
  • デスクトップアプリ
  • 既存のKobitoのクローンではなく、いくつかの課題を解決しつつ開発
  • View は React Component + 自作Flux Framework

fit inline

(画面は開発中のものです)


fit inline

(画面は開発中のものです)


デモ


追加要素

  • Qiita / Qiita:Team との同期機能を強化
  • ローカルに閉じたInboxの追加
  • 単純なメモツールとしての使い勝手を強化
  • Vim キーバインドモードの追加(開発者の趣味)

Kobito for Windows


開発過程

  • 企画 / プロト 10月後半 ~
  • 設計 - 11月 ~
  • 実装 - 12月~
  • バグ洗い出しとリファクタ(いまここ)

エンジニア1人(mizchi) 1月からマークアップ1人


ライブラリの使用感

inlineinline


結局Reactとはなんだったのか


Just the UI

  • 単なる**「データバインド付きテンプレートエンジン」**で、描画後再利用可能なコンポーネント
  • 必要十分に速い(さすがに職人芸的なDOMチューニングには劣る)
  • 今まで苦労していた状態遷移が死ぬほど単純化される

仮想DOMとしてのReact選定理由

  • 情報が十分にある(ただし海外中心)
  • ヘッドレス環境(node)でのテストケースの書きやすさ重視   他の環境ならもっと小さいライブラリを使っていたかも。Qiita にもまだ入れてない。

「本質的な部分」に集中できる

  • 設計が単純化された結果、アプリケーションドメイン層が明確に意識できるように
  • Pure JavaScript, いわゆる「Isomorphic化」可能な箇所に注力できる(あとで詳しく話す)

Qiitaへのフィードバック(予定)

  • 開発したコンポーネント群を順次フィードバックしていきたい
  • たとえば…

Markdown ハイライト付きのエディタ

inline fit


Atom の Cmt+T 的なインクリメンタルサーチ

inline fit


AtomShellについて


AtomShell

  • Atom Editorの基盤
  • Multi Platform (Win/Mac/Debian)
  • デスクトップアプリの為のChromium ラッパー

AtomShellで作るメリット

  • nodeのモジュールを呼べる
  • クロスオリジンを超えれる
  • デスクトップアプリとして配布できる
  • ブラウザストレージの上限を任意に増やせる
  • (Blink以外の動作確認をサボれる)

AtomShellを選ぶ環境要因

  • Web界隈だとWindowsの知見をためても活用しにくく、WPFの知識を蓄える動機がない
  • node/HTMLのノウハウを活かせる
  • HTML/JSでQiitaとコンポーネントを共有できる

デスクトップアプリケーション

になるとどうなるか


跳ね上がる期待値

  • やることは実質SPA だが…
  • ネイティブアプリとしてのUXを期待される
  • そのための React

設計時に問題になったこと


問題1. JSX


JSXとは

  • Reactの仮想DOMに最適化されたJavaScript の文法拡張
var div = <div/>;

みたいなやつ


JSXの問題点. 1

  • JavaScriptの知識を要求しすぎる
    • items.map(item => <Item data=item.data/>) がリスト要素作ってるのわかる?
  • 非JSエンジニアと協調するには厳しい

JSXの問題点. 2

  • 他のAltJSと相性がよくない
  • 今回はCoffeeScriptとTypeScriptを使っているので最悪

JSXの問題点. 3

  • テンプレートと密結合しすぎる
  • ViewModel を強く意識してテンプレートとプロパティを分離を試みた

解決策: react-jade

jadejs/react-jade

  • jadeテンプレートからReactのVirtual DOM が吐ける
  • jadeの開発元が提供しているので、メンテされるだろうという期待がある

react-jade の例

.container
  h1(onClick=onClickTitle)= This is title

React.createElement('div', {className: 'container'}, [
  React.createElement('h1', {onClick: onClickTitle}, 'This is title')
])
// ヘッダ省略

いわゆるhaml系テンプレート


react-jade の結果

  • JS詳しくない人「なんかよくわからんプロパティがあるが触れる」程度に落ち着く
  • (Qiita本体はslim っていう背景があるかも)

開発言語

  • UI層: React Component / Dispatcher
    • CoffeeScriptで高速にTry & Error を回す
    • テンプレートはreact-jade
  • Store層 / Domain層
    • TypeScript の common.jsモード
    • CoffeeScript に require される(逆はない)

問題2.

どの設計を採用する?


Flux

  • 単方向データフロー
  • 状態管理コストが低いVirtualDOMに向いた設計を実現する思想. (実装ではない)
  • 詳しくは誰かが話してくれ(る/た)でしょう or ぐぐれ

乱立する Flux 実装

  • Fluxxor
  • Reflux
  • Alt
  • Fluxible
  • Facebook's flux
  • Deloerean
  • etc...

Flux実装の現実

  • 薄い
  • どの実装もIdiomatic
  • どれが生き残るかわからん

なんかしっくりするのがない


「もう自作するしかない!」


というわけで…


第二章:

Arda - MetaFlux


Arda

  • mizchi/arda - Github
  • 元々は Kobito on Atom Shell の状態管理と画面遷移を抽象化したもの
  • そこそこテスト書いて、だいぶドッグフーディングしているので実用に耐えうるはず

Ardaの由来

  • J・R・R・トールキンの「指輪物語」の世界の名前であり地球そのものでもある
  • VirtualDOMの仮想な世界と現実が融合する場所ぐらいのニュアンス
  • ぶっちゃけ短けりゃなんでもよかった

開発の動機

  • 既存のFlux実装は「画面遷移」が表現しにくかった
  • react-routerが使いにくかった/目的が違った
  • Store層をTypeScriptフレンドリーに型で保護できるように分離したかった

意識したもの

  • Store/View/Dispatcherの塊を「Context」という単位で管理
  • Contextのスタックで状態を表現
  • React のState/Props の概念は継承
  • すべての状態遷移をPromise化

位置づけ

  • 単なるFluxではなくFluxを内包したより上位のFramework

fit


モジュールを単純に

  • Viewは単なるReact.Component
  • Dispatcherは単なるEventEmitter
  • StoreはEventを受けて状態を更新

Context のデータフロー

  1. Router から初期入力(Props)を受けて初期化される
  2. Propsから初期State(Context内状態)を作る
  3. Props と State から、Componentに渡すプロパティ(ComponentProps)を生成
  4. Component に渡す
  5. 状態が更新されたら3に戻る

fit


Arda.Router

  • 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 だけ


Context を TypeScript で記述すると何が嬉しい?

  • 型によって仕様が明確になる
  • Props は画面を再構築するのに必要な情報
  • State はその画面の中で変化する状態
  • ComponentProps は 実際にComponent に渡されるもの

ComponentProps が存在する意図

  • 関心の分離
  • Componentが知るべき状態だけに変形したい
  • 型で保護しにくいComponent に直接 Props と State を渡すのは嬉しくない

たとえば

  • Stateとして何かの id だけ持って DBやネットワークを叩くと、結果に再現性がなく State として持ちたくない
buildTimelineByGroupId(state.selectedGroupId).then((items) = {
  this.render(items); // ここを持ちたくない
});

再現可能なビュー

  • ComponentPropsが同じなら必ず同じビューを状態を再現できる(とする)
  • Component と Props の組み合わせの URLへのシリアライズ/デシリアライズ を実装すれば Browser Hisotry に対応可能
  • AgnosticにしたいのでArdaではブラウザヒストリーを関知しない

他、詳しいAPI

  • arda.d.ts の型定義ファイルがAPIドキュメントを兼ねてる
  • Arda自身はcoffeescriptで記述
  • 最初はtypescriptで書いたが、メタプロだらけで型が生きず、代わりにテストを多めに書いた

Kobito on Atom Shell での Arda

  • 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'})

Arda の書き心地

  • 既存のFluxの弱い点をカバーできたと思う
  • 自分にとっては最高なんで流行らせたい
  • APIも覚えることも少ないので使ってくれ🙏

いますぐ npm install arda --save


第三章:

Isomorphicの実践


Isomorphicとは

  • 「同じライブラリがnodeでもブラウザでも動けばいいよね」という発想
  • browserify/webpackによって実現可能になった

なぜIsomorphicを意識して開発するか

  1. たとえnode(iojs)は使わなくても、単体テストはnodeでやるのが簡単で高速
  2. フロントエンドの各種プリコンパイラやタスクランナーもnode
  3. 起動コストが高く不安定なヘッドレスブラウザ(phantomjs)の使用を極力避けたい  

Qiita と Atom Shell 特有の事情

  • node の global と ブラウザの window が共存する特殊な環境
  • 成果物はいずれQiitaへ持ち込みたい

というわけでKobito on Atom Shell では Isomorphic を強く意識して設計した


Isomorphic の為の抽象化

  • ストレージ
  • DOM

ストレージの

Isomorphic化


minimongo

mWater/minimongo

  • mongodb風のAPIを持った永続ストレージ
  • 保存先を切り替えて実行環境を選べる(IndexedDB/オンメモリ/MongoDb)
  • 採用理由: 元々 meteor の一部でよくテストされている

他の候補


Isomorphic 的運用

  • テスト環境下ではオンメモリモードにして起動し、テストケースごとに生成/破棄

他、自作ライブラリ群

  • mizchi/minimongo-schema スキーマ定義のJSONからDB初期化
  • mizchi/factory-dog ↑用のスキーマからダミーオブジェクトの生成(雑なfactory-girl実装)
  • mizchi/mz-repository リポジトリパターン実装
  • mizchi/noo ES6Proxyを用いた rspec の null object っぽいやつの実装

mochaでの実際のコードの一部

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

React の

Isomorphic化


Headless React

  • ブラウザ環境がなくても動く(Server Side Rendering の為)
  • jsdom でも結構動く

renderToString(...)

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

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


Isomorphicによる

実行モード切り替えの実現


Kobitoの Isomorphic の実践

  • 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  



Isomorphic が可能にしたこと

  • 用途に応じた実行方式の切り替え
  • モジュラリティの向上

実行モード1: AtomShell:production

  • 配布用にビルド済みのbundle.jsを使ってサイズ削減(85MB -> 1.8MB)

  元サイズが大きい理由は、node_modules/* の依存が全部入っているせい。


実行モード2: AtomShell:development

  • AtomShell内蔵のnodeを使って、lib/indexから相対パスで解決。
  • やや時間が掛かるbrowserifyをスキップできる

実行モード3: ブラウザ実行

  • ブラウザでbundle.jsを読み込むindex.html から普通に起動するだけ
  • クロスオリジン制約にひっかからないもの、ネイティブを呼ぶ機能以外は実行可能
  • 何かに使えないか考えている…(体験版とか?)

実行モード4: 単体テスト

  • lib 以下のファイルを test/**/* から相対パスでrequireして実行
  • ヘッドレスなのでとにかく速いし安定する

実行モード5: End to End Test

  • ブラウザビルドと同じように構築
  • AtomShell用のSeleniumアダプターの設定をサボることができた

おまけ: browsrify vs webpack

webpackはいろんなことが出来過ぎて、node にない挙動が可能なので Isomorphic 性を守るためにあえてbrowserifyを使っている。


第四章

Virtual DOM をとりまく現実


実際Reactどうなん

  • 画面に変化を起こす/起こし続けるのが圧倒的に楽
  • とはいえ周辺ライブラリが枯れてない
  • Issueでバグ報告しまくったり自分でforkしてパッチあてたりしてる

Reactの懸念点

  • サイズがやや大きい(.min.js で127k)(jQueryと同じぐらい)
  • より小さな実装 virtual-dom/deku/mithril/riot も考慮にいれるべきかも?
  • とはいえ一番枯れてる

「どういう設計がいいかわからん」


React vs jQuery

  • 思想の段階でコンフリクトしているので協調が難しい
  • 既存資産の以降からの、一番のボトルネックであることは否めない
  • 使えないわけではなく リードオンリー だと考えると自然

そもそもjQuery必要?

  • その50行のスパゲティコード、Reactだったら10行のComponentにならない?
  • そういう視点を常に持つ
  • 手を動かそう!

「なにがなんでもjQueryプラグイン捨てられないんじゃ〜」

  • <div key='hogefuga'></div> でユニークなkey属性を持つ仮想DOMなら消えない
  • コンテナの中をjQueryの領域とする

Kobito on AtomShell でjQuery を使った箇所

  • スクロール量の読み出しと更新
  • 擬似クリックイベントの発火
  • aタグを全てオーバーライドしてAtomShellの外に出てしまわないように

現代的なJavaScriptエンジニアに求められるもの


Isomorphicの発想とnodeのスキル

  • よりサーバーサイド言語の発想に近くなる
    • デザイナーにとっての学習コストは上がり、エンジニアにとっては下がる
    • 適切な分業体制が必要
  • やるべきことはサーバーサイドnodeエンジニアと全く同じ(実行環境が異なる)

大規模SPAの設計

  • 画面の構築に必要な発想は、ネイティブのアプリケーションエンジニアと同じ
    • 自分はゲーム開発とAndroidの経験が生きた
  • ストレージを扱うとデータ管理がシビアに
    • ドメイン駆動を意識する
  • RP/FRP

「最近のJSは覚えることが多すぎてわからん」

  • もっともよく聞く
  • 同じ感想だがそもそも要求が複雑化しているので…
  • とはいえVirtualDOM は設計の単純化方向に働くので現実的に採用可能

今日のまとめ


  • 現場で使ってみたけど大丈夫
  • Reactは設計の単純化に方向に働く
  • フロントエンドはIsomorphic化され, node(iojs)のスキルによって効率化される
  • Arda によって画面遷移を管理し、型による「硬さ」を調節できるようにした

Kobito for Windows よろしくお願いします!


Increments では デザイナが足りない

  • デザイナの手が足りなくて辛い
  • Qiita/Kobito のデザインしたい人きてくれ!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment