20190707のReactに関する記事は5件です。

Reactをわかった気になる #1 Reactの特徴

本記事の目的

Reactという単語は聞いたことあるけれど概要はよく知らない人向けの記事になります(元々はバイト先のディレクターさんにReactを教えるためのスライドです)。
なのでReactの特徴やその大まかな意味などの上流の部分に触れていきます。
その中でも今回はReactの特徴について触れていこうと思います。

Reactとは

Reactとはユーザインタフェースを構築するためのJSライブラリです。
よくAngularjQueryと比較されますが、React自体はウェブフレームワークではなく「ライブラリ」ですので、実際の開発の際には他にも様々なライブラリを組み合わせて使うことが多いです。
Reactの開発はFacebookが行っていて2019年7月現在githubのスターは132,000以上もあるwebフロント界隈では主流になりつつあるライブラリです。

Reactの特徴

Reactの特徴は大きく分けて3つあります。

  • componentベース
  • virtialDOM
  • JSX

まず1つ目はcomponentベースです。
基本的にReactでは1つのwebページを複数のコンポーネントに分けて実装していきます。
コンポーネントに分けることで共通部品として使ったり、コンポーネントごとに状態を管理できるためソースコードの見通しが良くなります。
また、Atomicデザインと非常に相性がいいです。
→Reactのコンポーネントについて(後日公開予定)
スクリーンショット 2019-07-07 20.30.26.png
例えばReactのホームページでは上記のようにコンポーネントを分けて実装されていました(便宜上、元ページの両端をカットしています)。

2つ目にvirtualDOMがあげられます。
virtualDOMはJSで作られた仮想のDOMです。内容はJSONのため、ブラウザのDOMよりも軽量で扱いやすいという利点があります。
webページの1部分を動的に変化させるときにReactでは1度virtualDOMを作成し、もとのvirtualDOMと比較をして変化のある部分だけをブラウザに通知します。
そうすることでブラウザのDOMの更新による処理の負担を減らし、パフォーマンスの最適化を図ることができます。この技術によりSPA(Single Page Application)を現実的にしました。
→ReactのvirtualDOMについて(後日公開予定)
スクリーンショット 2019-07-07 20.53.32.png
※この図はこちらの記事を参考に作成させていただきました。

最後はJSXになります。
JSXはソースコードの記述方法に関する特徴になります。
JSXは簡単に言うとJSのコードの中にHTMLを書くことができる仕組みです。
ロジックとマークダウンが混在している点が初見の方にとっては違和感に感じる部分だと思います。
Reactではそもそも1つ目であげたとおり、webページをコンポーネント単位で実装していきます。
なのでJSファイルとHTMLファイルで分割して実装していくよりも、1つのコンポーネントにつき1つのJSXファイルで実装したほうが利点が大きいです。
→ReactのJSXについて(後日公開予定)

helloMessage.jsx
export default class HelloMessage extends React.Component {
  render() {
    const helloStyle = { color: 'red' };
    return (
      <div style={helloStyle}>
        Hello JSX
      <div>
    );
  }
}

上記の例だと、「Hello JSX」の文字が赤くなります。

終わりに

今回はReactの特徴について触れていきました。
次からは各特徴をもう少し深掘りして見たいと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Redux Formを使ってフォーム画面を作ってみる

React+Reduxでフォーム画面を作る時にRedux Formというライブラリが便利だと聞いたので実際に使ってみました。
redux-form

Redux Formはテキストボックスなどに入力された値を即座にstoreにdispatchしてくれるライブラリのようです。

ライブラリの導入

Redux Formを使うには当然ですがreduxが必要です。
まずは必要となるライブラリをインストールしましょう

$ npm install redux react-redux redux-form redux-logger

redux-loggerは別に無くても問題なく動作しますが、actionが呼び出される時にconsoleにログを表示してくれるのでデバッグに便利です。
次にindexを書き換えます。redux-loggerはミドルウェアのひとつなのでapplyMiddlewareが必要になります。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import logger from 'redux-logger';
import { createStore, applyMiddleware } from 'redux'
import { Provider } from 'react-redux';
import reducer from './reducer';
import App from './App';

const store = createStore(
  reducer,
  applyMiddleware(logger)
);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root'));

Reducerを作成

Redux Formの導入はreducer内に記載します。

Reducer.js
import { combineReducers } from "redux";
import { reducer as formReducer } from "redux-form";

const reducer = combineReducers({
  form: formReducer
});

export default reducer;

書くのはこれだけ。actionも書く必要ないっぽいです。便利。

コンポーネントの作成

いよいよコンポーネントを書いていきます。

App.js
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
import Form from "./Form";
import { connect } from "react-redux";

class App extends Component {
  showText() {
    window.alert(this.props.text.values.test);
  }

  render() {
    return (
      <div>
        <Field name="test" type="text" component={Form} />
        <button type="button" onClick={this.showText.bind(this)}>
          テキスト表示
        </button>
      </div>
    );
  }
}

const mapStateToProps = state => ({
  text: state.form.text
});

export default connect(mapStateToProps)(
  reduxForm({
    form: "text",
    enableReinitialize: true
  })(App)
);
Form.js
import React, { Component } from "react";

export default class Form extends Component {
  render() {
    const { input, name, type } = this.props;
    const disabled = false;

    return (
      <div>
        <input {...input} name={name} type={type} disabled={disabled} />
      </div>
    );
  }
}

App.jsをRedux Formとconnectし、Redux Formを利用したテキストボックスとアラートを呼び出す用のボタンを作りました。
Form.jsにテキストボックスのコードを記載し、App.jsでそれを呼び出して使用しています。

フォームの種類によって場合分けしたいときは以下のようにForm.js内で条件分岐させることも可能です。

Form.js
import React, { Component } from "react";

export default class Form extends Component {
  render() {
    const { input, name, type } = this.props;
    const disabled = false;

    return (
      <div>
        {type === "text" && (
          <input {...input} name={name} type={type} disabled={disabled} />
        )}
        {type === "select" && (
          <select {...input} name={name} type={type}>
            <option value={"one"}>ひとつめ</option>
            <option value={"two"}>ふたつめ</option>
          </select>
        )}
      </div>
    );
  }
}

テキストボックスに入力された値をその場ですぐ表示させるためには以下のように書く必要があるみたいです。

App.js
import React, { Component } from "react";
import { Field, reduxForm } from "redux-form";
import Form from "./FormItem";
import { connect } from "react-redux";

class App extends Component {
  render() {
    return (
      <div>
        <Field name="test" type="text" component={Form} />
        {this.props.text && this.props.text.values && (
          <p>{this.props.text.values.test}</p>
        )}
      </div>
    );
  }
}

const mapStateToProps = state => ({
  text: state.form.text
});

export default connect(mapStateToProps)(
  reduxForm({
    form: "text",
    enableReinitialize: true
  })(App)
);

&&を使って条件分岐している理由は、テキストボックスに何も入力されていない状態だとtextとvaluesがundefinedになってしまうせいでエラーになってしまうからです。

もう少し詳しくRedux Formについて調べていけばもっとスマートに書ける方法が見つかる気がする…

参考

Redux Form入門

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでAPIを作成して、Reactで取得したい

開発環境

  • laravel 5.8
  • docker

dockerは、LaradockでLaravel+Docker環境構築(mac)の方法で環境構築しました。

概要

LaravelでReactを使用できる方法から、Apiを作成してReactのJSXで表示したい

Reactへ切り替え

laravelのあるディレクトリへ移動

$ cd project

vue.jsからreactへ切り替え

$ php artisan preset react
React scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.

npmをインストール

$ npm install 

JSファイルの保存を監視とビルドのため実行(control+cで終了)

$ npm run watch

reactに切り替わっているか確認
resources/js/componentsがExample.vueからExample.jsになっていれば成功

Example.js編集

Example ComponentをReactに変更

Example.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

export default class Example extends Component {
    render() {
        return (
            <div className="container">
                <div className="row justify-content-center">
                    <div className="col-md-8">
                        <div className="card">
                            <div className="card-header">React</div>

                            <div className="card-body">I'm an example component!</div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

if (document.getElementById('example')) {
    ReactDOM.render(<Example />, document.getElementById('example'));
}

次にwelcome.blade.phpのbodyタグを下記のように修正する。

id=exampleの要素から、Exampleがクラスが呼び出される。

welcome.blade.php
<body>
        <div id="example"></div>
        <script src="{{mix('js/app.js')}}" ></script>
</body>

localhostでアクセスすると修正した内容が反映されています。

LaravelをApiとして使う準備

次に、laravelをapi化します。今回はapp/Http/Controllers/api配下にPostControllerを作成します。

$ php artisan make:controller api/PostController

apiのルーティングは、route/api.phpがあるので、以下のように記述します。

route/api.php
Route::group(['middleware' => ['api']], function() {
    Route::resource('post' , 'api\PostController');
});

modelとmigration作成

$ php artisan make:model Post -m

中身はname、contentカラムを追加します

public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            // name,contentカラム追加
            $table->string('name');
            $table->text('content');
            $table->timestamps();
        });
    }

シーダーの作成

$ php artisan make:seeder PostsTableSeeder
public function run()
    {
        \DB::table('posts')->insert([
            [
                'name' => '名前1',
                'content' => '内容1'
            ],
            [
                'name' => '名前2',
                'content' => '内容2'
            ],
            [
                'name' => '名前3',
                'content' => '内容3'
            ],
        ]);
    }

database/seeds/DatabaseSeederにPostsTableSeederを追記して

public function run()
{
  $this->call(PostsTableSeeder::class);
}

マイグレーションとシーダーを実行

$ php artisan migrate --seed

PostControllerのindexで作成したPostモデルを、jsonで返す

PostController.php
public function index() 
{
    $posts = Post::all();   
    return response()->json($posts, 200);
}

本来なら、postman等のツールで確認しますが、今回はブラウザでhttp://localhost/api/posts
を直接叩いて、jsonで帰ってきたら成功

react側でapiの利用

先ほど作成したPostモデルのデータをaxiosを使ってreact側で受け取ります。

axiosは、HTTPリクエストを送信するメソッドです。
今回はGETリクエストを送信します。

Example.jsを下記のように修正します。

Example.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';

export default class Example extends Component {

    constructor() {
        super();

        this.state = {
            posts: []
        };
    }
    componentDidMount() {
        axios
            .get('/api/posts')
            .then(response => {
                this.setState({posts: response.data});
            })
            .catch(() => {
                console.log('通信に失敗しました');
            });
    }

    renderPosts() {
        return this.state.posts.map(post => {
            return (
                <li key={post.key}>
                    {post.name}: {post.content}
                </li>
            );
        });
    }

    render() {
        return (
            <div className="container">
                <ul>
                    {this.renderPosts()}
                </ul>
            </div>
        );
    }
}

if (document.getElementById('example')) {
    ReactDOM.render(<Example />, document.getElementById('example'));
}

まず初めconstructorのstateに、取得するpostsを格納する為に、空配列を用意します。

constructor() {
     super();

     this.state = {
         posts: []
     };
 }

次に、コンポーネントがマウントされた直後に呼ばれるcomponentDidMountにaxiosを記述して、stateの中にapiから取得したpostsを格納します。例外処理は、コンソールでエラーを返すようにします。

componentDidMount() {
    axios
        .get('/api/posts')
        .then(response => {
            this.setState({posts: response.data});
        })
        .catch(() => {
            console.log('通信に失敗しました');
        });
}

最後に、renderPostsメソッドで配列をループして、JSXでこのメソッドを呼び出します。

renderPosts() {
    return this.state.posts.map(post => {
        return (
            <li key={post.key}>
                {post.name}: {post.content}
            </li>
        );
    });
}

render() {
    return (
        <div className="container">
            <ul>
                {this.renderPosts()}
            </ul>
        </div>
    );
}

これで、laravelでreactのセットアップから、apiからデータを取得して、jsxで表示するところまでできました。

スクリーンショット 2019-07-07 19.06.21.png

以上です。
次回は、Reduxの導入でもしたいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LaravelでAPIを作成して、ReactのJSXにそのデータを表示したい

開発環境

  • laravel 5.8
  • docker

dockerは、LaradockでLaravel+Docker環境構築(mac)の方法で環境構築しました。

概要

LaravelでReactを使用できる方法から、Apiを作成してReactのJSXで表示したい

Reactへ切り替え

laravelのあるディレクトリへ移動

$ cd project

vue.jsからreactへ切り替え

$ php artisan preset react
React scaffolding installed successfully.
Please run "npm install && npm run dev" to compile your fresh scaffolding.

npmをインストール

$ npm install 

JSファイルの保存を監視とビルドのため実行(control+cで終了)

$ npm run watch

reactに切り替わっているか確認
resources/js/componentsがExample.vueからExample.jsになっていれば成功

Example.js編集

Example ComponentをReactに変更

Example.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';

export default class Example extends Component {
    render() {
        return (
            <div className="container">
                <div className="row justify-content-center">
                    <div className="col-md-8">
                        <div className="card">
                            <div className="card-header">React</div>

                            <div className="card-body">I'm an example component!</div>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

if (document.getElementById('example')) {
    ReactDOM.render(<Example />, document.getElementById('example'));
}

次にwelcome.blade.phpのbodyタグを下記のように修正する。

id=exampleの要素から、Exampleがクラスが呼び出される。

welcome.blade.php
<body>
        <div id="example"></div>
        <script src="{{mix('js/app.js')}}" ></script>
</body>

localhostでアクセスすると修正した内容が反映されています。

LaravelをApiとして使う準備

非同期で処理できるようにしたので、laravelをapi化します。今回はapp/Http/Controllers/api配下にPostControllerを作成します。

$ php artisan make:controller api/PostController

apiのルーティングは、route/api.phpがあるので、以下のように記述します。

route/api.php
Route::group(['middleware' => ['api']], function() {
    Route::resource('post' , 'api\PostController');
});

modelとmigration作成

$ php artisan make:model Post -m

中身はcontentカラムを追加します

public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->bigIncrements('id');
            // name,contentカラム追加
            $table->string('name');
            $table->text('content');
            $table->timestamps();
        });
    }

シーダーの作成

$ php artisan make:seeder PostsTableSeeder
public function run()
    {
        \DB::table('posts')->insert([
            [
                'name' => '名前1',
                'content' => '内容1'
            ],
            [
                'name' => '名前2',
                'content' => '内容2'
            ],
            [
                'name' => '名前3',
                'content' => '内容3'
            ],
        ]);
    }

database/seeds/DatabaseSeederにPostsTableSeederを追記して

public function run()
{
  $this->call(PostsTableSeeder::class);
}

マイグレーションとシーダーを実行

$ php artisan migrate --seed

PostControllerのindexで作成したPostモデルを、jsonで返す

PostController.php
public function index() 
{
    $posts = Post::all();   
    return response()->json($posts, 200);
}

本来なら、postman等のツールで確認しますが、今回はブラウザでhttp://localhost/api/posts
を直接叩いて、jsonで帰ってきたら成功

react側でapiの利用

先ほど作成したPostモデルのデータをaxiosを使ってreact側で受け取ります。

axiosは、HTTPリクエストを送信するメソッドです。
今回はGETリクエストを送信します。

Example.jsを下記のように修正します。

Example.js
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';

export default class Example extends Component {

    constructor() {
        super();

        this.state = {
            posts: []
        };
    }
    componentDidMount() {
        axios
            .get('/api/posts')
            .then(response => {
                this.setState({posts: response.data});
            })
            .catch(() => {
                console.log('通信に失敗しました');
            });
    }

    renderPosts() {
        return this.state.posts.map(post => {
            return (
                <li key={post.key}>
                    {post.name}: {post.content}
                </li>
            );
        });
    }

    render() {
        return (
            <div className="container">
                <ul>
                    {this.renderPosts()}
                </ul>
            </div>
        );
    }
}

if (document.getElementById('example')) {
    ReactDOM.render(<Example />, document.getElementById('example'));
}

まず初めconstructorのstateに、取得するpostsを格納する為に、空配列を用意します。

constructor() {
     super();

     this.state = {
         posts: []
     };
 }

次に、コンポーネントがマウントされた直後に呼ばれるcomponentDidMountにaxiosを記述して、stateの中にapiから取得したpostsを格納します。例外処理は、コンソールでエラーを返すようにします。

componentDidMount() {
    axios
        .get('/api/posts')
        .then(response => {
            this.setState({posts: response.data});
        })
        .catch(() => {
            console.log('通信に失敗しました');
        });
}

最後に、renderPostsメソッドで配列をループして、JSXでこのメソッドを呼び出します。

renderPosts() {
    return this.state.posts.map(post => {
        return (
            <li key={post.key}>
                {post.name}: {post.content}
            </li>
        );
    });
}

render() {
    return (
        <div className="container">
            <ul>
                {this.renderPosts()}
            </ul>
        </div>
    );
}

これで、laravelでreactのセットアップから、apiからデータを取得して、jsxで表示するところまでできました。

スクリーンショット 2019-07-07 19.06.21.png

以上です。
次回は、Reduxの導入でもしたいと思っています。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React + Express + Watson Assistantで自動応答ボットを作る

この記事を書いたきっかけ

今まで何となく避けて通ってきたReactを勉強する事になりました。
せっかくなので何か成果物を作りたいなーと思ったので、Watsonを使った自動応答ページを作ってみる事に

完成品

React側のソース(github)

sample.gif

構成

ザックリ言うと、フロントはReact、APIを呼び出すサーバにはExpressを使っています。
構成.png

デモにあったの天気の取得は、裏側でAssistantIBM Cloud FunctionsThe Weather Company Dataという小技を使っています。
関連エントリ
* Watson AssistantからIBM Cloud functionsを使う時の考慮点

React側の構成

package.json(一部抜粋)

  "dependencies": {
    "bootstrap": "^4.3.1",
    "dotenv": "^8.0.0",
    "react": "^16.8.6",
    "react-bootstrap": "^1.0.0-beta.9",
    "react-dom": "^16.8.6",
    "react-icons": "^3.7.0",
    "react-loading-overlay": "^1.0.1",
    "react-scripts": "3.0.1",
    "react-spinners": "^0.5.4"
  },
  1. react-bootstrap
    なれたbootstrapでフロントを開発したかったので利用
  2. react-spinnersreact-loading-overlay
    問合せ中のローディング画面用のパッケージ
  3. dotenv
    APIサーバ等をenvファイルに持たせるためのパッケージ

ソースコード

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <link
      rel="stylesheet"
      href="https://maxcdn.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
      crossorigin="anonymous"
    />
    <title>React App</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <div id="inputLabel"></div>    
  </body>
</html>

App.js(実際の入出力フォーム)

import React from 'react';
import ReactDOM from 'react-dom';
import './App.css';
import { FaRobot } from 'react-icons/fa';
import LoadingOverlay from 'react-loading-overlay';
import ScaleLoader from 'react-spinners/ScaleLoader'

function App() {
  /**
   * 入力テキストとボタンをrenderするコンポーネント
   */
  class Input extends React.Component{
    constructor(props){
      super(props);
      this.state = {
        textValue  : "",
        messages   : [],
        session_id : "",
        isActive : false
      }
      this.handleClick = this.handleClick.bind(this);
      this.changeText  = this.changeText.bind(this);
      this.createWatsonAssistantSession();
    };
    /**
     * Watson AssistantのセッションIDを取得して初期メッセージを表示する
     */
    createWatsonAssistantSession(){
      fetch(process.env.REACT_APP_API_SERVER + "/create-session", {
        mode: 'cors'
      })
      .then((response) => {
        return response.json().then(res=>{
          this.setState({
            session_id : res.session_id
          })
          this.fetchAssistant();
        });
      })
      .then((error)=>{
        return error;
      });
    }
    fetchAssistant(){
      fetch(process.env.REACT_APP_API_SERVER + "/conversation", {
        mode  : 'cors',
        method: 'POST',
        body : JSON.stringify({ session_id : this.state.session_id, inputText : this.state.textValue }),
        headers : new Headers({ "Content-type" : "application/json" })
      })
      .then((response) => {
        let conversation = [];
        const question = {kind: "question",text: this.state.textValue};
        return response.json().then(res=>{
          let answer;
          if(res.output.generic[0]){
            answer   = {kind: "answer",text: res.output.generic[0].text};
          }else{
            answer   = {kind: "answer",text: "答えがありません"};
          }
          conversation.push(question,answer);
          this.setState({
            messages : this.state.messages.concat(conversation)
          });
          this.setState({
            isActive:false
          });

        });
      })
      .then((error)=>{
        return error;
      });  
    }
    handleClick(){
      this.setState({
        isActive:true
      });
      this.fetchAssistant();
    };
    changeText(e){
      this.setState({
        textValue: e.target.value
      });
    };
    render(){
      return (
        <div className="container">
            <LoadingOverlay active={this.state.isActive} spinner={<ScaleLoader /> } text='Watsonに問合せ中…'>
            <div className="row">
              <h3 className="offset-md-3 text-danger"><FaRobot /> Reactボット</h3>
            </div>
            <div className="row">
              <div className="offset-md-3 col-md-6">
                <input type="text" className="form-control" placeholder="入力してみてね" value={this.state.textValue}
                  onChange={this.changeText}></input>
              </div>
              <div className="col-md-2">
                <button className="btn btn-primary" onClick={this.handleClick}>Talk To Watson</button>
              </div>
            </div>
          </LoadingOverlay>
          <OutputLabel messages={this.state.messages} />
        </div>
      );
    }
  }
  /**
   * 会話の内容をラベルとして表示する
   */
  class OutputLabel extends React.Component{
    /**
     * 発信なのかbotからの応答なのかを判定してrenderする文字列を返す
     * @param {*} message 
     * @param {*} index 
     */
    determineClass(message,index){
      if(message.text===""){
        return;
      }
      let   bg    = (message.kind === "question")?"offset-md-3 col-md-5 bg-warning":"offset-md-4 col-md-5 bg-success";
      const actor = (message.kind === "question")?"あなた":"bot"
      return(
        <div className="row" key={index} >
          <div className={bg}>
            <label className="control-label" >
              {actor}{message.text}
            </label>
          </div>
        </div>);
    };
    render(){
      const labels = this.props.messages.map((message, index) => {
        return this.determineClass(message,index);
      });
      return(
        <div>
         {labels}
       </div>
      );
    }
  }
  return ReactDOM.render(<Input />, document.getElementById('root'));
}

export default App;

fetch部分をaxiosに変えるともう少し記述はシンプルになります。

    /**
     * Watson AssistantのセッションIDを取得して初期メッセージを表示する
     */
    createWatsonAssistantSession(){
      axios.get(process.env.REACT_APP_API_SERVER + "/create-session")
      .then((response) => {
        this.setState({
          session_id : response.data.session_id
        })
        this.fetchAssistant();
        return;
      })
      .then((error)=>{
        return error;
      });
    }
    fetchAssistant(){
      let params = new URLSearchParams();
      params.append('session_id',this.state.session_id);
      params.append('inputText',this.state.textValue);
      axios.post(process.env.REACT_APP_API_SERVER + "/conversation",params)
      .then((response) => {
        let conversation = [];
        const question = {kind: "question",text: this.state.textValue};
        let answer;
        if(response.data.output.generic[0]){
          answer   = {kind: "answer",text: response.data.output.generic[0].text};
        }else{
          answer   = {kind: "answer",text: "答えがありません"};
        }
        conversation.push(question,answer);
        this.setState({
          messages : this.state.messages.concat(conversation)
        });
        this.setState({
          isActive:false
        });  
      })
      .then((error)=>{
        return error;
      });  
    }

環境変数(.env)

.env.example

# API Server for Watson Call
REACT_APP_API_SERVER=http://localhost:3010/watson/assistant

Express側

package.json(一部抜粋)

dotenvibm-watsonを追加

  "dependencies": {
    "body-parser": "~1.18.2",
    "cookie-parser": "~1.4.3",
    "cors": "^2.8.5",
    "debug": "~2.6.9",
    "dotenv": "^8.0.0",
    "ejs": "~2.5.7",
    "express": "~4.15.5",
    "ibm-watson": "^4.2.1",
    "morgan": "~1.9.0",
    "serve-favicon": "~2.4.5",
    "watson-developer-cloud": "^4.0.1"
  }

app.js

cors対策とwatson用のroutesを追記

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var cors = require('cors');

var index = require('./routes/index');
var users = require('./routes/users');
var watson = require('./routes/watson');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);
//ここを追記
app.use(cors())
app.use('/watson', watson);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

routes

今回は手抜きで直接routeの中に処理を書きましたw
公開されているAPIリファレンスのまんま、
新規セッション作成と会話用の受け口を作成

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/assistant/create-session', function(req, res, next) {
  const AssistantV2 = require('ibm-watson/assistant/v2');

  const service = new AssistantV2({
    iam_apikey: process.env.REACT_APP_ASSISTANT_IAM_APIKEY,
    version: '2019-06-14',
    url: process.env.REACT_APP_ASSISTANT_URL
  });

  service.createSession({
    assistant_id: process.env.REACT_APP_ASSISTANT_ID
  })
  .then(watson_response => {
    console.log(JSON.stringify(watson_response, null, 2));
    res.send(JSON.stringify(watson_response, null, 2));
  })
  .catch(err => {
    console.log(err);
  });
});

router.post('/assistant/conversation', function(req, res, next) {
  const AssistantV2 = require('ibm-watson/assistant/v2');

  const service = new AssistantV2({
    iam_apikey: process.env.REACT_APP_ASSISTANT_IAM_APIKEY,
    version: '2019-02-28',
    url: process.env.REACT_APP_ASSISTANT_URL
  });

  service.message({
    assistant_id: process.env.REACT_APP_ASSISTANT_ID,
    session_id: req.body.session_id,
    input: {
      'message_type': 'text',
      'text': req.body.inputText
      }
    })
    .then(watson_response => {
      res.send(JSON.stringify(watson_response, null, 2));
    })
    .catch(err => {
      console.log(err);
    });
});

module.exports = router;

環境変数(.env)

.env.example

# Environment variables
PORT=3010
REACT_APP_ASSISTANT_ID="XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
REACT_APP_ASSISTANT_IAM_APIKEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
REACT_APP_ASSISTANT_URL=https://gateway.watsonplatform.net/assistant/api

ハマったポイント

  1. CORS対策 React側から通信をすると、CORSでエラーとなってしまいました。対策としてExpress側のサーバでCORS対策として、corsをインストール
  2. React側で環境変数が読めない! よくあるケースの様で、環境変数の先頭にREACT_APP_をつける必要があるようです。Express側では不要ですが、今回は平仄を合わせる意味でこちらの環境変数名にもREACT_APP_をつけています。

Reactを触った感想

今回はチュートリアルをザっと眺めてからトライしてみましたが、思ったよりも簡単に実装をする事が出来ました。
特に各コンポーネントを部品として扱うので、再利用性や他のReactのパッケージ組み込みが簡単で、今までよりもリッチな画面を作りやすいかな…という感じです。(この機能の実装にかかったのは4h程度)
一方で設計をしっかりしないと、いつどこでstateに値が設定されているか分かり辛くなったり、コンポーネントを重複して開発しそうだなぁ。。。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む