22
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

React + ES6 + Webpack で JSON Visual Editor を作ってみる

Last updated at Posted at 2016-05-12

はじめに

ES6 や React、Webpack といった技術の勉強のために簡単なシンプルページアプリケーションを作って、その過程をここに記録として書いていきます。時系列に何の整理もせず書いていくので分かりづらいかもしれないですが、ソースコードも公開するので、何かの参考になれば幸いです。

作るもの

JSON を見やすく可視化して、かつ編集も可能な JSON Visual Editor を作ります。これの元となるのはずいぶん前に作った「JavaScript のオブジェクトを可視化するやーつ」で、もっと使いやすくしたかったのと、React の勉強にちょうど良さそうなので、作ってみることにしました。

json-visual-editor.png

環境構築

今回は、React を ES6 (Babel) で、スタイルは Stylus で書いて、Webpack でビルドします。Gulp などは使用せず、npm-scripts でタスクを実行します。

これを1から構築するのではなく、これに似た構成のプロジェクトをベースに構築しました。

これをフォークしたのがこちらです。

この JSON Visual Editor は、このリポジトリでオープンソースとして公開します。

ビルド

README に記載の通りにビルドします。

Bootstrap の削除

$ npm run dev

でビルドしたら、Bootstrap に関係するエラーが出たので、Bootstrap を削除しました

src ディレクトリ追加

src/ ディレクトリを作ってそこにソースを入れるようにします。試しに Page コンポーネントを作って、src/index.jsx から読み込んでみます。

src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';
import Page from './Page';

export class App extends React.Component {
  render() {
    return (
      <Page />
    );
  }
}

ReactDOM.render(<App/>, document.querySelector("#myApp"));
src/Page.jsx
import React from 'react';
import ReactDOM from 'react-dom';

export default class Page extends React.Component {
  render() {
    return (
      <div>Page</div>
    );
  }
}

Page クラスに default キーワードをつけず、エラーとなってしまいハマりました。

また、webpack の設定も変更しました。

Stylus の設定

スタイルシートの言語は Stylus が好きなので、その設定を行います。stylus-loader という Webpack のプラグインがあるので、READMEに従ってインストール。

現在の構成では、プラグインの指定を webpack.loaders.js ファイルで行います。

webpack.loaders.js
  ...
  {
    test  : /\.styl$/,
    loader: 'style!css!stylus'
  },
  ...

もともと css の設定が記述されていましたが、削除しました。

Stylus のファイルは src/styles/ 以下に置きます。ルートとなる main.styl を src/index.jsx で import します。

src/index.jsx
import './styles/main.styl';

今回の修正は、こちらで確認できます

追記:構成をちょっと変えました

CSS フレームワーク「Material Design Lite」のインストール

多少は綺麗なスタイルにしたいので、Material Design Lite を導入。Material-UI という選択肢もあったけど、React との結びつきが強そうだったので、スタイルだけを利用できる MDL に。

$ npm install --save material-design-lite

でインストールして、Stylus で組み込む。

src/styles/index.styl
@import "../../node_modules/material-design-lite/material.min.css"
@import url("https://fonts.googleapis.com/icon?family=Material+Icons")

JavaScript も。

src/index.jsx
import '../node_modules/material-design-lite/material.min.js';

ESLint のインストール(2016/06/01 追記)

まずは、ESLint のインストール。

$ npm install -g eslint
$ eslint --init

対話形式で設定ファイル(.eslintrc)を生成することができます。

Webpack から利用するので、eslint-loader もインストール。

$ npm install --save-dev eslint-loader

webpack.config.js で、eslint-loader を読み込む設定を追加(preLoadersの部分)。

webpack.config.js
  module: {
    loaders: loaders,
    preLoaders: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'eslint-loader'
      }
    ]
  },

React での UI の構築

ひとまず環境は整ったので、次に UI を構成してみます。出来上がったレイアウトはこちら。

Screen Shot 2016-05-21 at 0.14.58.png

左側がテキストエリアで、ここに JSON を記述すると、右側のエリア(このスクリーンキャプチャーだとテキストが書いてあるエリア)に、その構造をあらわすテーブルが表示されます。テキストエリアには、JSON をコピーしたりクリアするボタン、JSON の文字数を示す文字列が存在します。

React のコンポーネント構成は下記の通り。

まだ、UI の動作の実装、データのバインディングはできていません。この時点のソースコードはこちら

UI の起点となる Page.jsx では、全体のレイアウトを定義しています。この中で、JSON を記入する TextArea、操作用のボタン等を配置する ControlsArea、記入された JSON をビジュアル化する VisualizedData を組み込んでいます。

この構造がベストかどうかは現時点ではわかりません。今後、実装していく中で調整が入ると思われます。

データのハンドリング

テキストエリアに記入した JSON を App コンポーネントで管理して、更新されるとそれに従って UI が更新されるようにします。

データの定義

ルートコンポーネントである App でデータを保持するようにします。

index.jsx
export class App extends React.Component {

  // コンストラクタでデータを保持する変数を定義。
  constructor(props) {
    super(props);
    this.state = {
      data: null
    };
    // this をバインド。
    this.updateData = this.updateData.bind(this);
  }

  // データを更新するためのメソッド。
  updateData(newData) {
    this.setState({data: newData});
  }

  // data でデータを渡して、updateData でデータ更新用メソッドを渡す。
  render() {
    return (
      <Page data={this.state.data} updateData={this.updateData} />
    );
  }
}

ReactDOM.render(<App/>, document.querySelector("#myApp"));

コンストラクタで定義した data を下記のように子コンポーネントに渡します。

index.jsx
      <Page data={this.state.data} updateData={this.updateData} />

このように、Page 以下のコンポーネントに、data を渡します。同時に、TextArea で JSON が変更された時に data を更新するためのメソッド updateData() を渡して、データを更新できるようにします。

また、これは React とは関係ないですが、

index.jsx
    // this をバインド。
    this.updateData = this.updateData.bind(this);

独自のメソッド内の thisReact.component ではないので、.bind()this が使えるようにします。(参考:Why this.setState is undefined in React ES6 class? · Issue #283 · goatslacker/alt

データの更新

Page に渡されたデータは、this.props.data でアクセスできます。これを、TextArea と VisualizedData に渡します(現時点では、TextArea に渡す必要はないですが)。また、TextArea には、データ更新用のメソッドも同様に渡します。

Page.jsx
    ...
    <TextArea data={this.props.data} updateData={this.props.updateData} />
    ...
    <VisualizedData data={this.props.data} />
    ...

TextArea では、3秒ごと(暫定)に記入した JSON を VisualizedData で表示するように、渡された更新用メソッドを呼び出します。

TextArea.jsx
export default class TextArea extends React.Component {

  constructor(props) {
    super(props);
    let text = (props.data === null) ? "" : JSON.stringify(props.data)
    this.state = {
      text: text
    };
  }

  componentDidMount() {
    setInterval((() => {
      let text = this.refs.jsonText.value;
      try {
        let data = JSON.parse(text);
        this.props.updateData(data);
      } catch(e) {
        console.log('Not JSON: '+text);
      }
    }).bind(this), 3000);
  }

  render() {
    return (
      <textarea placeholder="Write JSON code here."
                defalutValue={this.state.text}
                ref="jsonText"></textarea>
    );
  }
}

コンポーネントが DOM に追加された時に呼び出される componentDidMount() で周期的に this.props.updateData() を呼び出すようにしてデータを更新しています。<textarea> 内のテキストは this.refs.jsonText.value で呼び出していますが、これは ref="jsonText" と指定することで参照できるようになっています。

更新されたデータの表示

TextArea で this.props.updateData() を呼び出すと、App コンポーネントの updateData() が呼び出され、データが更新されます。

index.jsx
  updateData(newData) {
    this.setState({data: newData});
  }

これにより、子コンポーネントに変更が伝わり、表示が更新されます。

VisualizedData.jsx
export default class VisualizedData extends React.Component {
  render() {
    return (
      <div>{JSON.stringify(this.props.data)}</div>
    );
  }
}

VisualizedData では、渡されたデータを JSON の文字列に変換して表示しています。特に、更新を監視するような処理はありません。

今後、文字列ではなくデータ構造をテーブルで可視化する処理を追加します。

データのビジュアライズ

VisualizedData にデータが渡ってきたので、これをテーブル形式で表示します。Object データをテーブルに変換するコンポーネント ObjectType はこんな感じです。

ObjectType.jsx
export default class ObjectType extends React.Component {
  render() {
    let data = this.props.data;
    let result = null;
    if (data === null) {
      // null
      result = (<span className="null">null</span>);
    }
    else if (typeof(data) === typeof({}) && data !== null) {
      // Object or Array
      let rows = Object.keys(data).map((name) => {
        return (
          <tr>
            <th>{name}</th>
            <td><ObjectType data={data[name]} /></td>
          </tr>
        );
      });
      result = (
        <table>
          <tbody>
            {rows}
          </tbody>
        </table>
      );
    }
    else if (typeof(data) === typeof(1)) {
      // Number
      result = (<span className="number">{data}</span>);
    }
    else if (typeof(data) === typeof("a")) {
      // String
      result= (<span className="string">"{data}"</span>);
    }
    else if (typeof(data) === typeof(true)) {
      // Boolean
      result = (<span className="boolean">{(data)?'true':'false'}</span>);
    }
    else {
      // something else
      result = (<span className="undefined">{data}</span>);
    }
    return result;
  }
}

渡されたデータは this.props.data に入っていて、型を判断して Object 型の場合には、テーブル(<table>)で描画します。オブジェクトの各要素ごとにキー(配列の場合はインデックス番号)と値をテーブルの行として描画するのがこの部分。

      // Object or Array
      let rows = Object.keys(data).map((name) => {
        return (
          <tr>
            <th>{name}</th>
            <td><ObjectType data={data[name]} /></td>
          </tr>
        );
      });

ここで、値を描画する部分で再帰的に <ObjectType /> を呼び出しています。すべての行を rows 配列に保持したら、<table> で括って返します。

      // result は render() の戻り値
      result = (
        <table>
          <tbody>
            {rows}
          </tbody>
        </table>
      );

これにスタイルをあてると、データがテーブル形式で表示されます。

ここまでで、記入した JSON がビジュアライズされるところまでできました。他にも細かい点が実装できていないですが、ひとまず GitHub Pages にデプロイしてみます。

GitHub Pages へのデプロイ

Webpack は、ビルドするとアセットを bundle.js というファイルにパッキングします。現在の設定では、

$ npm run build

を実行すると、public/ ディレクトリに(.map ファイルとともに)保存されます。この中にある index.html を開くと bundle.js が読み込まれて、アプリケーションとして動作します。

これを GitHub Pages に簡単にデプロイするために、gh-pages を使います。

$ npm install gh-pages --save-dev
$ npm run build
$ gh-pages -d public

これだけで、https://ogaoga.github.io/json-visual-editor/ にデプロイされました!

この方法だと手動でデプロイする必要があるため、master ブランチに push すると自動的に Travis CI でテストして、デプロイするようにしたいと思います。 Travis CI からデプロイするのは意外と大変そうなので後回しにしました。

UI の状態コントロール

JSON の入力に応じて、データの表示だけではなく周辺の UI も状態を制御する必要があります。

<textarea> を state で管理する。

最初の実装では、<textarea> に記入された文字列を value 属性で管理していなかったので、React の公式ドキュメントに沿って変更します。

TextArea.jsx
        <textarea id="json-text"
                  placeholder="Write JSON code here."
                  value={this.state.text}
                  onChange={this.onChange}
                  ref="jsonText"></textarea>

これだけだと入力時に内容が更新されないので、onChange イベントで onChange メソッドを呼び出して、value を入力された文字列で更新する処理を追加します。

TextArea.jsx
  onChange(event) {
    this.setState({text: event.target.value});
  }

これで、<textarea> の変更が伝播するようになります。

コンポーネント構成を組み替える。

Copy ボタンや文字数カウンタなどを管理している ControlArea コンポーネントを、TextArea コンポーネントの子コンポーネントとなるように変更します。これにより、文字列を簡単に渡せるようになり、文字数の表示やボタンの disabled の制御が簡単になります。

TextArea.jsx
        <ControlsArea text={this.state.text}
                      clearText={this.clearText} />

text 属性で入力されたテキストを渡すのと同時に、clearText というメソッドを追加して、Clear ボタンを押した時に文字列を空にできるようにします。

TextArea.jsx
  clearText() {
    this.setState({text: ''});
  }

文字数カウントの実装

ControlArea に入力された文字列が渡されるようになったので、これをもとに文字数カウントを表示させます。

ControlsArea.jsx
        <div className="float-right control-count">
          <span className="text-count">{this.props.text.length}</span>
        </div>

ボタンの disabled の制御

Copy ボタンと Clear ボタンは、文字列が入力されている時だけ有効なので、disabled 属性にこの条件を与えてボタンを制御します。

ControlsArea.jsx
          <button id="copy-to-clipboard"
                  data-clipboard-target="#json-text"
                  disabled={this.props.text.length==0}
                  className="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect">Copy</button>

Clear ボタンの実装

Clear ボタンを押すと文字列が空になるようにするために、TextArea コンポーネントでそのメソッドを定義して、そのインタフェースを ControlsArea に渡します。これを、<button>onClick で呼び出すことで、文字列を空にできます。

ControlsArea.jsx
          <button className="mdl-button mdl-js-button mdl-button--raised mdl-js-ripple-effect"
                  disabled={this.props.text.length==0}
                  onClick={this.props.clearText}>Clear</button>

このように、あくまで渡された文字列データをもとに各種の制御を行い、<textarea> との依存関係をつくらないことがポイントです。また、親コンポーネントのデータを更新したい場合は、そのインタフェースを渡して、直接操作させないことも重要です。

これらの変更はこちらをご確認ください

Redux の導入

UI コンポーネントが増えてきたり構造が複雑になってくると、ルートのコンポーネントで管理しているデータを操作するメソッドを props で渡したり、コンポーネント間で state のデータをやり取りするのが面倒になってきます。また、全然依存がないコンポーネントがデータやメソッドをバケツリレーしなければならないのも気持ち悪いです。

これを解決するために、Redux というフレームワーク導入します。これにより、各コンポーネントの state で管理していたデータを一元管理して、そのデータを操作するメソッドをどのコンポーネントからでも呼び出せるようにして、依存関係を減らします。

Redux の導入はこれだけでも長くなるので、別の記事に分けました。こちらも併せてご覧ください。

React + ES6 + Webpack で JSON Visual Editor を作ってみる(Redux 導入編) - Qiita


現在はここまで。開発の進捗に沿って追記していきます。


参考

22
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?