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

2015年3月22日 0 開発 ,

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 などで教えていただけるとありがたい。


REPLY

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です