banseivlog

大器晩成型 - 仕事中の覚え書きや反省文を書く程度のブログです

jQuery,Reactを使わずにvirtual-domを使ってLPを作った理由

TL;DR

jQueryとReactの代わりにvirtual-dom使ってLP的なサイトを実装してみたけど案外良かったよ。

jQueryやReactを使わない選択肢

とあるサイトをリニューアルをしました その際、動的コンテンツの表示をするのに何を使うかを考えました。

  • jQuery: 今まで使っててて、LPとか作る際の主流。
  • React: 流行ってる、ただ実装に対して too much な機能(差分updateと状態変化くらいでよかった)
  • virtual-dom: 欲しい機能だけ有している。随所でmount処理入れてあげればコードも健全に保てそう。

ちなみにサイトの仕様はこんな感じ。

  • Java製, freemarkerでのサーバー吐き出しとAPI経由の動的吐き出しの入り乱れたコンテンツ
  • 複雑なロジックはなく、特別に実装するのはモーダルやトラッキングログだったりちょっとしたインタラクションだけ
  • IEのサポートは新しめ

ということで、流行りのReactを使わずにvirtual-dom単体で実装することにしました。 他にも、自分が受け持っているWebアプリケーションの方でdomを書くときに割とvirtual-domの書き方と似通った実装をしていたので、他チームメンバーにもスケールするような実装を目指していたので比較的全体をシンプルに完結させたかったのもあります(教育コスト的に)。

実装

実装はシンプルで、サーバー側でレンダリングしたものに関してはpureなJavaScriptでロジックを書き、動的に出すものに関してはvirtual-domを使って実装しました。

また主要なライブラリをまとめるとこんな感じになります。

  • virtual-dom: UI周りの実装
  • es6-promise: 言わずもがな
  • eventemitter3: イベント駆動の基礎部分
  • axios: API通信
  • etc...

virtual-domについて

virtual-dom

特に説明は不要かと思いますが, 仮想的なDOMの状態を定義して、状態の更新を効率的に行ってくれています。

割と雑に説明しましたが、実作業として実装者がするのは定義の部分です。

具体的には

// 定義
var tree = render(count);
var rootNode = createElement(tree);
document.body.appendChild(rootNode);
function render(count)  {
    return h('div', {
        style: {
            textAlign: 'center',
            lineHeight: (100 + count) + 'px',
            border: '1px solid red',
            width: (100 + count) + 'px',
            height: (100 + count) + 'px'
        }
    }, [String(count)]);
}

上のようにDOMをvirtual-domに沿って定義して、

// 状態更新
function update() {
  count++;
  var newTree = render(count);
  var patches = diff(tree, newTree);
  rootNode = patch(rootNode, patches);
  tree = newTree;
}

下のように状態の更新を実装します。 一件状態の更新が複雑に見えそうですが、我々が意識するのは基本的には仮想DOMの定義だけでいいのでシンプルかつきれいな状態で実装することができます。

モジュールの設計

今回virtual-domをモジュールごとに定義して、それぞれ予めhtml上にある適当な場所に対してDOMをマウントさせるという実装を行いました。

その際、都度書くような実装は簡略化したClassにまとめてしまい、仮想DOMの実装に集中させました。

import h from 'virtual-dom/h';
import diff from 'virtual-dom/diff';
import patch from 'virtual-dom/patch';
import createElement from 'virtual-dom/create-element';

export class Component {
  // ... React.Compomnentを簡素にしたような実装
}

classはReact風に書いています。 あとはこのComponentを都度継承してrenderをオーバーライドさせれば仮想DOMの定義に集中できます。

// <= <div id="hoge"></div>
import h from 'virtual-dom/h';
class Hoge extends Component {
  render() {
    return h('div', ['HOGE']);
  }
}
new Hoge(document.getElementById('hoge'));

// => <div id="hoge"><div>HOGE</div></div>

データ周り

データの取扱周りはEventEmitter3を使ってイベント駆動っぽくシンプルに実装しました

# Data flow
[ Component ] --> [ Action ] --> [ Service Factory ] <---> [ WebAPI ]
     ^            [        ]            |
     |            [ STATE  ]            |
     |            [        ]            |
     +----------> [ Store  ] -----------+

大したことをしていないので詳細のコードは割愛しますが、

FluxでいうところのActionとStoreはあえて分けず、Repogitory的な扱いにしてComponentからは直接その更新をみるのと参照だけしておきます。

ビジネスロジック的なところはServiceFactoryにてゴニョゴニョいじって、都度Storeに結果だけを格納しています。

そんな形で、複雑なロジック部分もちょっとシンプルに実装させています。

こうして、シンプルなLPチックなサイトをvirtual-domを使って実装しました。

その他

コードをES2015+で開発するために babeljs を使っています。

特に今回は他にもチーム内で幾つか別プロジェクトで babel環境を整えたかったので 一つの babel-preset を作って複数リポジトリでコーディングの基準を守れるようにしました。

babel-preset-ryosuga ←簡単に babel-preset を作ることができるので、チームが大きく複数プロジェクトにまたがる場合にまとめてみると良いと思います。(単純にメンテナンスが楽になります)

// index.js [babel-presetの参考]
module.exports = {
  presets: [
    require('babel-preset-**')
  ],
  plugins: [
    require('babel-plugin-**'),
    require('./path/to/file') // 独自の実装も含められる
  ]
};

反省など

Q なぜjQueryじゃないのか
A DOMの操作をjQueryでやりたくなかった、ただそれだけです。

Q Reactで良かってのではないか
A 良かった。少しの反骨心とちょっとしたファイルサイズへのこだわりに少し後悔している。

minified jquery 3.x (slim): 69KB
minified react: 45KB
minified virtual-dom: 18KB

毎回開発の粒度が短いので、細かい技術選定をできていないのですが、割と今回のvirtual-domは複雑なロジックを含まないLPとしては最小構成で楽に開発ができるものになったんじゃないかなと個人的には及第点になったんじゃないかなと思いました。(実際は少しロジカルな部分がある1面の実装をしてますが)

ただ次に開発するときは、他のパッケージ依存をなるだけ少なくしてReactを使うと思います。

今の時代、jQuery以外にも選択肢はたくさんあるということに感謝です。 後ろに続く人たちが幸せになれる実装を心がけて明日もまたコードを書き続けて行きます…