Todo ウェブアプリを作ってみよう

Worker にウェブビューを実装する

最後に、ウェブビューを実装します。

Todo ウェブアプリを操作している様子

準備

準備の様子

(↑クリックで拡大表示)

この Todo ウェブアプリでは、ReactBootstrap を使って、ウェブビューを実装します。

React と Bootstrap の実際の使用にあたっては、react-railstwitter-bootstrap-rails という gem を使用します。まずは、この gem を使用する準備をしましょう。

ファイル Gemfile の最後に、react-railstwitter-bootstrap-rails の gem をインストールする記述を追加します。

source 'https://rubygems.org'

  :
(中略)
  :

gem 'react-rails', '~> 1.5.0'
gem 'twitter-bootstrap-rails', '~> 3.2.0'

bundle install コマンドで、追加の gem をインストールします。

bundle install

rails generate bootstrap:install コマンドを使って、twitter-bootstrap-rails を Rails から使えるようにします。

bundle exec rails generate bootstrap:install static

rails generate react:install コマンドを使って、react-rails を Rails から使えるようにします。

bundle exec rails generate react:install

これで、準備完了です。

ウェブビューの実装

ウェブビュー実装の様子

(↑クリックで拡大表示)

rails generate react:component コマンドを使って、コンポーネントのテンプレートを生成します。コンポーネント名は、MiniTodo とします。

bundle exec rails generate react:component MiniTodo

続いて、MiniTodo コンポーネントの中身を定義していきます。生成されたファイル app/assets/javascripts/components/mini_todo.js.jsx を次のように編集します。

var MiniTodo = React.createClass({
  requestMiniTodo: function (url, type, data) {
    $.ajax({
      url: url,
      dataType: 'json',
      type: type,
      data: data,
      success: function (result) {
        this.setState(result);
      }.bind(this),
      error: function (xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    });
  },
  getInitialState: function () {
    return {data: []};
  },
  componentDidMount: function () {
    this.requestMiniTodo(this.props.url, 'GET');
  },
  handleTodoSubmit: function (todo) {
    this.requestMiniTodo(this.props.url, 'POST', todo);
  },
  handleTodoChange: function (id, todo) {
    this.requestMiniTodo(this.props.url + '/' + id, 'PATCH', todo);
  },
  handleTodoDestroy: function (id) {
    this.requestMiniTodo(this.props.url + '/' + id, 'DELETE');
  },
  render: function () {
    return (
      <div className="container">
        <div className="panel panel-primary">
          <div className="panel-heading"><h1>Todos</h1></div>
          <div className="panel-body">
            <TodoForm onTodoSubmit={this.handleTodoSubmit} />
          </div>
          <TodoList data={this.state.data} onTodoChange={this.handleTodoChange} onTodoDestroy={this.handleTodoDestroy} />
        </div>
      </div>
    );
  }
});

var TodoForm = React.createClass({
  handleSubmit: function (e) {
    if (event.which !== 13) {
      return;
    }
    e.preventDefault();
    var title = React.findDOMNode(this.refs.title).value.trim();
    if (!title) {
      return;
    }
    this.props.onTodoSubmit({title: title, completed: false});
    React.findDOMNode(this.refs.title).value = '';
    return;
  },
  render: function () {
    return (
      <input className="form-control"
             placeholder="やらなければいけないこと"
             autoFocus={true}
             onKeyDown={this.handleSubmit}
             ref="title" />
    );
  }
});

var TodoList = React.createClass({
  handleTodoChange: function (id, todoItem) {
    this.props.onTodoChange(id, todoItem);
  },
  handleTodoDestroy: function (id) {
    this.props.onTodoDestroy(id);
  },
  render: function () {
    var todoItems = this.props.data.map(function (todoItem) {
      return (
        <TodoItem key={todoItem.id} data={todoItem} onTodoChange={this.handleTodoChange} onTodoDestroy={this.handleTodoDestroy} />
      );
    }, this);
    return (
      <ul className="list-group">
        {todoItems}
      </ul>
    );
  }
});

var TodoItem = React.createClass({
  handleCompletedChange: function (e) {
    e.preventDefault();
    var completed = React.findDOMNode(this.refs.completed).checked;
    this.props.onTodoChange(this.props.data.id, {title: this.props.data.title, completed: completed});
    return;
  },
  handleTodoItemDestroy: function (e) {
    this.props.onTodoDestroy(this.props.data.id);
  },
  render: function () {
    return (
      <li className="list-group-item">
        <div className="checkbox">
          <label>
            <input type="checkbox"
                   checked={this.props.data.completed}
                   onChange={this.handleCompletedChange}
                   ref="completed"
            />{this.props.data.title}
          </label>
          <button type="button" className="close" aria-label="Close" onClick={this.handleTodoItemDestroy}><span aria-hidden="true">&times;</span></button>
        </div>
      </li>
    );
  }
});

コントローラを実装します。rails generate controller コマンドで、コントローラのテンプレートを生成します。コントローラ名は、Todos とします。

bundle exec rails generate controller todos

ビューは、先ほど実装した MiniTodo コンポーネントでレンダリングするように、ファイル app/views/todos/index.html.erb を次の内容で作成します。

<%= react_component('MiniTodo', url: '/api/v1/todos') %>

ファイル config/routes.rb を編集して、ルーティングを設定します。

Rails.application.routes.draw do
  get 'hello' => 'hello#index', defaults: { format: 'json' }

  namespace :api, format: 'json' do
    namespace :v1 do
      resources :todos, :only => [:index, :create, :update, :destroy]
    end
  end

  root 'todos#index'
end

これで、Todo 管理ウェブアプリが完成しました。

Docker イメージの再作成

Docker イメージ再作成の様子

(↑クリックで拡大表示)

続いて、Docker イメージを再作成します。Docker イメージの再作成に用いる Dockerfile は、これまでのものをそのまま使用します。

Docker イメージ名のタグ(バージョン)は、1.1.0 から 1.2.0 に変更します。

docker build -t username/mini-todo:1.2.0 .

作成した Docker イメージを Docker Hub に登録します。

docker push username/mini-todo:1.2.0

デプロイ

デプロイの様子

(↑クリックで拡大表示)

ここまでで、デプロイの準備が整いました。すでに MAGELLAN で動いている Worker をバージョンアップするデプロイを実施しましょう。

デプロイは、ウェブブラウザーでの作業となります。ウェブブラウザーの画面をステージ DefaultStage1 に切り替えて、以下の手順に沿って作業してください。

今回は、ウェブブラウザーから Worker (MAGELLAN) にアクセスするため、MAGELLAN 標準設定を変更します。

まず、Worker (MAGELLAN) に認証なしでアクセスできるように変更します。

  1. Edit Stage ボタン画像 をクリックします。

    「Stage の編集」画面が表示されます。

  2. 「Authentication」フィールドの「認証なし」をチェックします。

  3. Save ボタン画像 をクリックします。

次に、Worker にサブドメインを割り当てます。

  1. 左サイドメニューから「DefaultProject1」のリンクをクリックします。

    プロジェクト DefaultProject1 の画面が表示されます。

  2. 「Client Versions」タブをクリックします。

    クライアントバージョンの管理画面に切り替わります。

  3. 「DefaultStage1」の行右端の 編集アイコン画像 をクリックします。

    「クライアントバージョンの編集」画面が表示されます。

  4. 「Domain」入力欄に、*****-minitodo と入力します。

    ***** の部分は、MAGELLAN のアカウント名に読み替えてください。アカウント名は、MAGELLAN コンソール右上の Logout ボタン 左横で確認できます。

  5. Save ボタン画像 をクリックします。

これで、ウェブブラウザーから、http://*****-minitodo.magellanic-clouds.net の URL で、Worker にアクセスする準備が整いました。

最後に、Worker を情報を更新し、Worker を再起動します。

  1. 「DefaultStage1」のリンクをクリックします。

    ステージ DefaultStage1 の管理画面が表示されます。

  2. 「Planning」タブをクリックします。

    Worker 情報を作成・編集する画面が表示されます。

  3. 「Image Name」入力欄に、username/mini-todo:1.2.0 と入力します。

  4. 更新した Worker 情報を保存します。入力内容に間違いがないことを確認して、Save ボタン画像 をクリックします。

  5. コンテナの起動ボタン をクリックして、コンテナを再起動します。

動作確認

ウェブブラウザーで、http://youraccount-minitodo.magellanic-clouds.net/ にアクセスして、動作確認します。

Todo ウェブアプリを操作している様子

おめでとうございます。

以上で終了です。ここで、Worker 開発環境は、終わらせておきましょう。ターミナルに戻り、docker exec -it worker-devenv su magellan したターミナルすべてで、exit コマンドを実行してください。その後、次のコマンドを実行してください。

docker stop mysql worker-devenv && docker rm mysql worker-devenv

このステップのまとめ

最後に、このステップで学んだことをまとめます。

  • ウェブブラウザーから Worker にアクセスするためには、MAGELLAN 標準設定の変更が必要
    • 「Stage の編集」画面で、「Authentication」を「認証なし」へ変更
    • 「クライアントバージョンの編集」画面で、サブドメインを割り当てる

なお、Worker の詳しい作り方は、「MAGELLAN 用ウェブアプリケーションの作り方」で解説しています。