アカベコマイリ

HEAR NOTHING SEE NOTHING SAY NOTHING

React.js を利用したときのデザイナー協業について考えてみる

March 22, 2015開発JavaScript, React.js

React.js を利用したときのデザイナー協業について考えてみる。この間、Twitter でこのような意見をいただいた。

JavaScript フレームワークで AngularJSvue.jsRactive.js のように UI の定義が HTML 的なテンプレートだと Web デザイナーにとって抵抗感が少なく協業しやすいのかもしれない。独自の属性や繰り返しなどのテンプレート記法があったとても HTML (厳密にはテンプレートだが話を簡単にするため以降、こう呼ぶ) → JavaScript という主従関係が重要なのだろうか。

React.js はテンプレートとロジックを JSX ファイル内へコンポーネントとして一緒に定義する。つまり主従は JavaScript → HTML になる。これをデザイナーから見るとテンプレートは理解できても自分の領分ではないロジック部分が一緒なのは邪魔に感じられるだろう。

というわけで JSX を使いつつ、なるべくテンプレート部分だけ分離できないものか検討してみたところ、コンポーネントの render 関数の内容をくくり出すぐらいしか思いつかなかった。実例として開発中の mw.js サンプル (音楽プレーヤー) に適用してみた。

まず React コンポーネントを以下のように定義。JSX 部分を持たないので拡張子は .js にしている。

var React         = require( 'react' );
var MusicListView = require('../view/MusicListView.jsx');

var MusicListViewModel = React.createClass( {
    render: function() {
        return MusicListView( this, this.props.musics, this.props.current );
    }

    _onSelectMusic: function( music ) {
    },

    _onSelectPlay: function( music ) {
    }
} );

module.exports = MusicListViewModel;

render で呼び出している関数を JSX ファイルとして定義。

var React    = require( 'react' );
var TextUtil = require( '../model/util/TextUtility.js' );

/**
 * 音楽リスト用コンポーネントを描画します。
 *
 * @param {ReactClass} component コンポーネント。
 *
 * @return {ReactElement} React エレメント。
 */
module.exports = function( component, musics, current ) {
    var items = musics.map( function( music, index ) {
        var selected = ( current && current.id === music.id ? 'selected' : null );
        return item( component, index, music, selected );
    }, component );

    return (
        <div className="music-list">
            <table className="musics">
                <thead>
                    <tr><th>#</th><th>Title</th><th>Artis</th><th>Album</th><th>Duration</th></tr>
                </thead>
                <tbody>
                    {items}
                </tbody>
            </table>
        </div>
    );
};

/**
 * 音楽リストのアイテムを描画します。
 *
 * @param {ReactClass} component コンポーネント。
 * @param {Numbet}     index     リスト上のインデックス。
 * @param {Music}      music     音楽情報。
 * @param {Boolean}    selected  音楽情報が選択されているなら true。
 *
 * @return {ReactElement} React エレメント。
 */
function item( component, index, music, selected ) {
    return (
        <tr 
            key={music.id}
            className={selected}
            onClick={component._onSelectMusic.bind( component, music )}
            onDoubleClick={component._onSelectPlay.bind( component, music )}>
            <td className="number">{index + 1}</td>
            <td>{music.title}</td>
            <td>{music.artist}</td>
            <td>{music.album}</td>
            <td>{TextUtil.secondsToString( music.duration )}</td>
        </tr>
    );
}

デザイナーには JSX ファイルだけ編集してもらう運用を想定。どうしても JavaScript 部分は残ってしまうが素のコンポーネントよりはだいぶマシになったのではなかろうか。JSX を読み込む側で表示用プロパティを作りこんで引数指定すれば更に簡素化できる。

render 部分だけ分割しておくことでモック作成しやすくなるかもしれない。例えば以下のように JSX を結合するだけのコンポーネントを実装し、その state にテスト用データを入れておくとか。

var ToolbarView   = require( '../view/ToolbarView.jsx' );
var MusicListView = require( '../view/MusicListView.jsx' );

var MainViewModel = React.createClass( {
    getInitialState: function() {
        var musics = [ { /* テスト用データ */ } ];
        return {
            musics:  [],
            current: musics[ 0 ]
        };
    },

    render: function() {
        return (
            <article className="app">
                {ToolbarView( this, this.state )}
                {MusicListView( this, this.state.musics, this.state.current )}
            </article>
        );
    }
} );

JSX 自体がちょっとね...という場合は react-jade などを利用するのもアリ。

この案は Browsetify によるファイル結合に依存している。最近の Web 開発ではデザイナーも SCSS などを利用するだろうから、それらとあわせて gulp や Grunt で自動ビルド環境を構築するのがよいだろう。

gulp.watch と watchify (前に試してよい感じだったので常用してる) でファイル監視 & 自動ビルド実行できると更によい。監視タスクを走らせてブラウザを開き、以降は JSX と CSS/SCSS を編集するだけ、というところまで持ってゆけるはず。

今回サンプルとして引用した nw.js プロジェクトではそのようにタスクが組んである。監視タスクと nw.js を起動、なにか修正したら nw.js だけリロードする (Web 開発ならここがブラウザになる) 感じで開発している。

もしよりよい方法があれば Twitter などで教えていただけるとありがたい。