20200325のReactに関する記事は8件です。

超初心者のためのReactチュートリアル

超初心者のためのReactチュートリアル

この記事はReactを学び始めたReact超初心者の私が今からReact学習を始める超初心者の方のために書いたものです。
学習していく中で躓いた所を挙げ、その解決方法を書いていきます。

この記事の対象となる方

1.Reactを学び始めた方
2.TypeScriptを使ってReactを学びたい方
3.Reactでコンポーネント間のデータの受け渡しを学びたい方
4.Reactで簡単なアプリを作りたい方
5.Reactでコンポーネントの表示をボタンなどで切り替えたい方

TypeScriptとReactを使った簡単なアプリを作りながら、私がReactで躓いてしまった所を紹介し、その問題を解決していきます!
※最低限Reactチュートリアルに目を通しておくと理解しやすいと思います。

では、はじめましょう!

事前準備

まずは開発環境を整えます。
以下の項目を実施していきましょう。

1.npmのインストール
Node.jsをインストールするとnpmが使えるようになるので以下からインストールしてください。Reactの環境構築に必須なので必ずインストールして下さい。
Node.jsインストール

2.React&TypeScriptの開発環境を構築
ReactはHTMLやCSSなどとは違い、ブラウザとエディターがあれば開発できる訳ではありません。いろいろなファイルを用意しておかないと使うことができません。TypeScriptも同様です。ブラウザはReactやTypeSctriptを直接認識することができません。なので、ブラウザが認識してくれるように書いたコードを変換する必要があります。このように、あるプログラミング言語から違うプログラミング言語に変換することをトランスパイルというらしいです。

このトランスパイルを行いブラウザでReactで作ったアプリを表示するための環境を作ります。
ここからnpmを多用するので必ずインストールしておいて下さい。
では、環境構築をはじめましょう!

まず、環境構築したいディレクトリ内にpackage.jsonというファイルを作成して下さい。そして、以下の内容をファイル内に書いて下さい。

package.json
{
  "scripts": {
    "start": "webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback",
    "build": "webpack",
    "watch": "webpack -w",
    "gulp": "gulp sass:watch"
  },
  "devDependencies": {
    "@types/react-router-dom": "^5.1.3",
    "gulp": "^4.0.2",
    "gulp-sass": "^4.0.2",
    "gulp-sass-glob": "^1.1.0",
    "ts-loader": "^5.4.3",
    "typescript": "^3.4.4",
    "webpack": "^4.30.0",
    "webpack-cli": "^3.3.1",
    "webpack-dev-server": "^3.10.3"
  },
  "dependencies": {
    "@types/react": "^16.8.14",
    "@types/react-dom": "^16.8.4",
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2"
  },
  "private": true
}

その後ターミナルを開き、このpackage.jsonがあるディレクトリに移動し、

npm install

を実行して下さい。これでディレクトリ内に必要なモジュールがインストールされます。インストール後に

run `npm audit fix` to fix them, or `npm audit` for details

のように表示された場合、使用しているモジュールに脆弱性のあるものが含まれているということなので、

npm audit fix

を実行して脆弱性のあるモジュールをバージョンアップして下さい。

次に同じディレクトリ内にtsconfig.jsとwebpack.config.jsを作成して下さい。

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    // TSはECMAScript 5に変換
    "target": "es5",
    // TSのモジュールはES Modulesとして出力
    "module": "es2015",
    // JSXの書式を有効に設定
    "jsx": "react",
    "moduleResolution": "node",
    "lib": [
      "es2020",
      "dom"
    ]
  }
}
webpack.config.js
module.exports = {
    // モード値を production に設定すると最適化された状態で、
    // development に設定するとソースマップ有効でJSファイルが出力される
    mode: "production",

    // メインとなるJavaScriptファイル(エントリーポイント)
    entry: "./src/main.tsx",
    // ファイルの出力設定
    output: {
      //  出力ファイルのディレクトリ名
      publicPath: "/js/",
      path: `${__dirname}/dist/js`,
      // 出力ファイル名
      filename: "main.js",
    },
    module: {
      rules: [
        {
          // 拡張子 .ts もしくは .tsx の場合
          test: /\.tsx?$/,
          // TypeScript をコンパイルする
          use: "ts-loader"
        }
      ]
    },
    // import 文で .ts や .tsx ファイルを解決するため
    resolve: {
      extensions: [".ts", ".tsx", ".js", ".json"]
    },
    performance: {
      maxEntrypointSize: 500000,
      maxAssetSize: 500000,
    },
  };

最後にdistディレクトリとsrcディレクトリを作成して下さい。
また、dist内にはjsディレクトリを作成して下さい。
distはhtml/css/jsファイルを格納する場所です。
srcはTypeScriptのファイルを格納する場所です。
現在のディレクトリの構成が以下のようになっていれば環境構築は完了です。

root
├── dist
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
├── tsconfig.json
└── webpack.config.js

この構成の詳しい内容は割愛しますが、これでReactとTypeScriptを使った最低限の開発環境が完成しました。
この構築は以下のサイトの構築を参考にし、少しだけ手を加えています。
引用元:最新版TypeScript+webpack 4の環境構築まとめ(React, Vue.js, Three.jsのサンプル付き)

Reactが動くことを確認しよう!

環境構築が完了したので、簡単なコードを書いてReactがブラウザで動くことを確認しましょう。

まず、以下のindex.htmlをdistディレクトリ内に作成します。
main.jsはTypeScriptをトランスパイルしたファイルです。
今はまだTypeScriptファイルがないため、main.jsはありません。

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React Sample App</title>
    <script defer src="js/main.js"></script>
</head>
<body>
    <div id="app"></div>
</body>
</html>

はい!、ここで私の躓きポイントです!!!

<script defer src="js/main.js"></script>

ここでdeferが指定されていると思います。本環境でReactをブラウザで表示させようとするとdeferなしではmain.jsが読み込まれずReactのコンポーネントがレンダリングされませんでした。ちゃんと確認できている訳ではありませんがdeferなしの場合、レンタリングするための領域<divid="app></div>"が読み込まれるより前にmain.jsが実行されたため、レンダリングされなかったのだと思います。deferはHTMLが全て読み込まれた後にjsを実行するため、この問題を解決できたのだと思います。
この後、TypeScriptファイルを作成してReactが動くことを確認するので、その時にdeferを外してみて下さい。恐らく、ブラウザに何も表示されないと思います。

ここからはTypeScriptでReactを使っていきます!
main.tsxとtest.tsxを作成して動作確認します。
main.tsxはscrディレクトリに保存して下さい。
test.tsxはscrディレクトリ内にscreensというディレクトリを作成してそこに保存して下さい。

ここまででディレクトリは以下の構成になっているはずです。

root
├── dist
│   ├── index.html
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── main.tsx
│   └── screens
│       └── Test.tsx
├── tsconfig.json
└── webpack.config.js

保存した後に、ターミナルで以下のコードを入力し実行して下さい。

npm start

実行後、ブラウザにテストと表示されると成功です!

main.tsx
//reactとreact-domの機能を使うためにインポートします 
import * as React from 'react';
import * as ReactDOM from 'react-dom';
//Testコンポーネントをインポートしてmain.tsx内で使用できるようにしています。
import Test from './screens/Test';

//React.Componentは上でReactをインポートしたことによって使用できます。
//これはReactのコンポーネントをAppという名前で作成するためのものです。
//class 任意の名前 extends React.Componentで作成します。
class App extends React.Component {

//Reactで画面に表示させたいときは以下のようにrender,returnを使います。
render() {
    return (
      <div className="container">
        <Test />//JSX構文
      </div>
    );
  }
}
//ReactDomも上でReact.Domをインポートしたことによって使用できます。
//このコードはindex.html内のid="app"をもつ要素内にAppコンポーネントをレンダリングするという意味です。
ReactDOM.render(<App/>, document.querySelector('#app'));
Test.tsx
import * as React from 'react';

class Test extends React.Component {
    render () {
        return (
            <h1>テスト</h1>
        );
    }
}
//このコードを書くことにより他のファイルからTestコンポーネントを使用できるようになります。
export default Test; 

tsxについて

拡張子.tsxはTypeScriptとJSXという構文を同時に使う時に用いる拡張子です。
JSXを使うときは拡張子を.tsxにして下さい。
JSXについてはReactチュートリアルで確認して下さい。

ウサギとカメゲームを作ろう!

テストで動作確認できたので実際にアプリを作っていきます。
今回はウサギとカメが闘うゲームを作ります。

構成は
ゲームスタート画面
バトルフィールド画面
結果画面 
の3つの画面を作っていきます。

完成したアプリはこのようになっています。
ウサギとカメ
このアプリを作りながらReactを学んでいきましょう。

reactではブラウザをリロードせずに簡単に画面の切り替えを行えます。
その機能を使って3つの画面を作っていきます。
今回はHTML、CSSの解説はしないのでGitHubからCSSファイルをクローンもしくはコピーしておいて下さい。
CSSファイル
また、画像ファイルもダウンロードしておいて下さい。
画像ファイル

状態を持たないコンポーネントもあり、そのときは関数コンポーネントを使用した方がいいと思うのですが、
今回はClassコンポーネントで統一します。

スタート画面

最初にスタート画面を作っていきます。
機能としてはタイトルを表示してGameStartボタンを押すとバトルフィールド画面に遷移するという簡単なものです。

早速実装していきましょう!
まず、動作確認で作成したmain.tsxを以下のように書き換えます。
先に実装するコンポーネントをmain.tsxに追加して、今使わないものはコメントアウトしておきます。
実装した際にコメントアウトを解除していって下さい。

main.tsx
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import GameStart from './screens/GameStart';
//import Field from './screens/Field';
//import TurtleWin from "./screens/Turtle_win";
//import RabbitWin from "./screens/Rabbit_win";

class App extends React.Component {
  render() {
    return (
      <BrowserRouter>
        <Switch>
           <Route exact path="/" component={GameStart} />
           {/*<Route exact path="/field/" component={Field} />*/}
           {/*<Route exact path="/Turtle_win/" component={TurtleWin} />*/}
           {/*<Route exact path="/Rabbit_win/" component={RabbitWin} />*/}
        </Switch>
      </BrowserRouter>
    );
  }
}


ReactDOM.render(<App/>, document.querySelector('#app'));

<BrowserRouter>,<Switch>,<Route>が新しく出てきたと思います。
これはReactで画面の切り替えを実装する際に使います。

      <BrowserRouter>
        <Switch>
          <Route exact path="/" component={GameStart} />
        </Switch>
      </BrowserRouter>

この部分はURLが"/"となったときcomponent GameStartを表示するよという意味です。
この環境ではlocalhost:8080で動作確認しているので、
localhost:8080/のときGameStartを表示します。

react-router-domで画面を切り替える際に注意点が一つあります。
react-router-domの画面切り替えではデフォルトの状態だと直接URLを指定するとページが取得できません。
例えばlocalhost:8080/field/
のように直接fieldに遷移しようとした時などです。
しかし、この環境ではローカル環境のサーバー設定で
この問題を回避しています。

package.json
{
  "scripts": {
    "start": "webpack-dev-server --hot --inline --watch-content-base --content-base ./dist --open --history-api-fallback",

package.json内の--history-api-fallbackの部分です。
サーバー側でフォールバックの設定をしてあげることで
localhost:8080/field/のように直接URLを指定してもそのページ移動できます。
これはローカルでの設定なので、もし本番の環境に移行した場合はそのサーバーでフォールバックを設定して下さい。
今回はすでに設定してあるのでこのような仕様があるといことを覚えておいて下さい。

次にGameStartコンポーネントを作成していきます。
GameStart.tsxはscreensディレクトリに保存して下さい。

GameStart.tsx
import * as React from "react";
import { Link } from 'react-router-dom';


class GameStart extends React.Component {
    render() {
        return (
            //classの代わりにclassNameでクラス名を指定
            <div className="l-start">
                <h1 className="p-start__title -view">ウサギとカメ</h1>
                <button className="p-start__button -view"><Link to="/field" className="p-start__link">Game Start</Link></button>
            </div>

            );
    }
}

export default GameStart;

import { Link } from 'react-router-dom';でimportしているLinkは画面を遷移させたい時に使います。
ブラウザ上ではaタグとして表示されます。
使い方は、要素をLinkでかこい、to="遷移先"で遷移するURLを指定します。
今回はボタンを押すことでバトルフィールドに遷移したいので、to="/field"を指定しています。
次にclassNameについて説明します。
要素にクラス名をつける場合、class="クラス名"で指定すると思うのですが、Reactでclassは別のところで使用しているため、
クラス名のためにclassが使用できません。ですから、Reactでクラス名を指定する際はclassNameを使います。

ファイルを保存したら以下のコマンドを実行して下さい。
スタート画面が表示されると思います。

npm start

バトルフィールド画面

バトルフィールドはGameStartコンポーネントからから遷移します。
移動後ウサギとカメのバトルが始まりましすが、この画面は3つのコンポーネントでできています。
1.Fiiedコンポーネント
2.HPコンポーネント
3.Commentコンポーネント

この3つの関係は
Fieldコンポーネントが親コンポーネントでHP、Commentコンポーネントがその子コンポーネントになっています。
Fieldコンポーネントで計算した値などを子コンポーネントに渡して画面の表示を切り替えていきます。

では、まずFieldコンポーネントとHPコンポーネントを作成します。
以下のコードからTypeScriptならではの書き方が出てきますが一旦スルーして下さい。
すぐに説明します。

Field.tsx
import * as React from "react"; 
import HP from "./HP";

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
} 

class Field extends React.Component<{},FieldState> {
    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
        }
    }

    render () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view">たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
            </div>
        );
    }
}

export default Field;
HP.tsx
import * as React from "react"

interface HPProps {
    CharacterHP: {},
}

class HP extends React.Component<HPProps,{}> {
    render (){
        return(
            <div className="l-hp">
                <p>HP:</p>
                <div className="p-hp__box">
                    <div className="p-hp__bar -view" style = {this.props.CharacterHP}></div>
                </div>
            </div>
        );
    }
}

export default HP;

ここからは細かく解説していきます。
FieldコンポーネントはカメとウサギのHPを保存しています。
このように何か値を保存しておきたい場合はstateを使います。

stateを使う場合はクラスコンポーネントの中にコンストラクタを作成します。

    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
        }
    }

コンストラクタを使用するときはsuper(props)から始めるのがお約束となっています。
その後にthis.state=...とつなげていくことでstateを使用できます。
ここではカメとウサギのHPを5としてstateに保存しています。
このHPの値を更新していきキャラクターがダメージ受けるたびにHPバーが減っていく機能を実装します。
書き方は

    constructor(props:{}) {
        super(props);
        this.state = {
            任意の名前: 任意の値(文字列や数値など),
        }
    }

という風に書きます。
ここでTypeScriptの型について軽く説明します。
TypeScriptは静的型付け言語の一種で変数などに型を宣言する必要があります。
宣言の仕方は

const name: string = "yamabaku";
const age: number = 24;

のように宣言します。

コンストラクタの部分ではpropsの後にprops:{}という風に型を宣言しています。
propsは親コンポーネントからデータを受け取る際に使用するのですが、Fieldコンポーネントは親からデータを受け取る必要がないので空のオブジェクトを型として宣言しています。

次にstateの型宣言について説明します。
stateの型を宣言する場合はinterfaceを使います。

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
} 

class Field extends React.Component<{},FieldState> {
    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
        }
    }

クラスを作成する前にinterfaceで型を宣言し、
class Field extends React.Component<{},FieldState>
のように書くことでクラス内で宣言した型を使用できます。
{}の部分はpropsのinterfaceで使用しますが今回はpropsがないため
空のオブジェクトを入れています。
型については以下の記事を参考にしました。
参考:TypeScriptの型入門

次にrender部分を解説していきます。

    render () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 

ここのコードはHPコンポーネントに送るためのオブジェクトを作成しています。
Fieldコンポーネントのstateを使用するときはthis.state.TurtleaHP
というように使います。
カメとウサギがダメージを受けたときここのHPが変化し、その変化したHPをHPコンポーネントに送ることで
HPバーの表示を変化させます。

次にreturn部分です。

        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view">たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
            </div>
        );

まずimagタグのpathのですが、Reactを本番環境(実際のサーバー上で動作させる)ときに普通にpathを指定するだけでは読み込んでくれません。
しかし、以下のように指定してあげると読み込んでくれます。
src={${window.location.origin}/images/turtle.png}
ここはローカル環境でのみ動作させる場合は関係ないのですが、もし本番環境でimgタグがうまく動作していないと思ったらここを思い出して下さい。
他にも画像をimportを使ってコンポーネントに読み込んでからimgタグのsrcを指定する方法もありますので調べてみて下さい。

次にReactのとても便利な機能である、コンポーネント間のデータの受け渡しを解説します。
このコードでデータの受け渡しを行っているのは以下の部分です。
<HP CharacterHP = {TurtleHP} />
ここはHPコンポーネントをレンダリングする時にHPコンポーネントにTurtleHPというデータを渡してレンダリングしてねという意味です。
このCharacterHPがHPコンポーネントのpropsになります。propsは先ほども説明したように親から子に渡されたデータです。ここではFieldが親でHPが子ですね。

ここでも私が躓いたポイントがあります。

<HP CharacterHP = {TurtleHP} />
という風に記述し、子コンポーネントにデータを渡す際は、その子コンポーネントで以下のようにinterfaceを使ってpropsの型を宣言しておく必要があります。

HP.tsx
interface HPProps {
    CharacterHP: {},
}

こうしておかないとField.tsxをコンパイルする際にデータを送り先がないよということでエラーが出てしまいます。

例えば上のinterfaceを削除してコンパイルすると

 TS2769: No overload matches this call.
  Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
  Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.

ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx
./src/screens/Field.tsx
[tsl] ERROR in /Users/username/Desktop/Turtle-VS-Rabbit/src/screens/Field.tsx(38,33)
      TS2769: No overload matches this call.
  Overload 1 of 2, '(props: Readonly<{}>): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
  Overload 2 of 2, '(props: {}, context?: any): HP', gave the following error.
    Type '{ CharacterHP: CharacterHP; }' is not assignable to type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.
      Property 'CharacterHP' does not exist on type 'IntrinsicAttributes & IntrinsicClassAttributes<HP> & Readonly<{}> & Readonly<{ children?: ReactNode; }>'.

のようなエラーが出ます。
このようなエラーが出た場合はデータを渡す子コンポーネントでpropsの型を宣言しているか確認してみて下さい。

ここで一旦HPコンポーネントの解説に移りましょう。
HPコンポーネントは先に説明したようにFieldコンポーネントからCharacterHPを受け取ります。
その値は以下のstyleの値として使用します。
propsを使う際もstateと同様にthis.props.CharacterHPというように指定することでそのデータを扱うことができます。

class HP extends React.Component<HPProps,{}> {
    render (){
        return(
            <div className="l-hp">
                <p>HP:</p>
                <div className="p-hp__box">
                    <div className="p-hp__bar -view" style = {this.props.CharacterHP}></div>
                </div>
            </div>
        );
    }
}

HPバーのstyle属性をpropsで変動させることによりHPバーが減っていく動きを作っています。
Reactではstateやpropsが変化したことを感知して自動でレンダリングし直してくれます。
ここではその機能を使っています。
ReactのコンポーネントのCSSをどのように当てるかは以下の記事が大変参考になりました。
参考:Reactのコンポーネントのスタイリングをどうやるか

それではFieldコンポーネントに戻ってHPの値を変化させる機能を実装しましょう。
以下のコードではキャラクターの残りHPと攻撃したキャラクターの名前を更新し、残りHPが0になったらResult画面へ遷移する関数、攻撃したキャラクターとそのダメージを表示するCommentコンポーネントを追加しています。
以下で細かく解説していきます。

Field.tsx
import * as React from "react"; 
import HP from "./HP";
import Comment from "./Comment";

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
    //追加
    Damage: number;
    name: string;
    ShowFlag: boolean;
    ClickFlag: boolean;
} 

class Field extends React.Component<{},FieldState> {
    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
            //追加
            Damage: null,
            name: '',
            ShowFlag: false,
            ClickFlag: true,
        }
        this.RabbitAttack = this.RabbitAttack.bind(this)
    }
    //カメが与えるダメージを決めウサギの残りHPを計算する関数
    TurtleAttack ():void {
        if(this.state.ClickFlag) {
            this.setState({ClickFlag: false});
            const Damage:number[] = [10,15,20,25];
            let TurtleAttack = Damage[Math.floor(Math.random()* Damage.length)];
            this.setState({Damage: TurtleAttack});
            this.setState({name: 'カメイ・ウェザー'})
            let RestHP = this.state.RabbitHP - TurtleAttack / 20; 
            if (RestHP > 0){
                this.setState({RabbitHP: RestHP}); 
            } else {
                this.setState({RabbitHP: 0});
                location.href ="/Turtle_win"

            }
            setTimeout(this.RabbitAttack ,800);
        }else {
            this.setState({ClickFlag:false});
        }
    }
    //ウサギが与えるダメージを決めカメの残りHPを計算する関数
    RabbitAttack ():void{
        const Damage:number[] = [15,15,15,15,15,20,20,20,20,20,100000000];
        let RabbitAttack = Damage[Math.floor(Math.random()* Damage.length)];
        this.setState({Damage: RabbitAttack});
        this.setState({name: 'バニー・パッキャオ'})
        let RestHP = this.state.TurtleHP - RabbitAttack / 20; 
        if (RestHP > 0){
            this.setState({TurtleHP: RestHP}); 
        } else {
            this.setState({TurtleHP: 0});
            setTimeout(()=>{location.href ="/Rabbit_win"},500);
        }
        this.setState({ClickFlag: true});
    }

    render () {
        interface CharacterHP {
            width:string;
        }
        let TurtleHP:CharacterHP = {
            width:`${this.state.TurtleHP}rem`,
        }
        let  RabbitHP:CharacterHP = {
            width:`${this.state.RabbitHP}rem`,
        } 
        //追加
        let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            {/* onClick追加 */}
                            <button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
                {/* 追加 */}
                {ShowDamage}
            </div>
        );
    }
}

export default Field;

まず、interfaceで宣言するstateが増えているのがわかると思います。
以下の目的で追加しています。

interface FieldState {
    TurtleHP: number;
    RabbitHP: number;
    Damage: number;//相手に与えるダメージを保存
    name: string;//攻撃したキャラクターの名前を保存
    ShowFlag: boolean;//Commentコンポーネントの表示、非表示を制御
    ClickFlag: boolean;//ボタンクリックの連打を防ぐためのものです
} 

次に追加した関数、TurtleAttack (),RabbitAttack ()について解説します。
この関数はrender内のbuttonがクリックされた時に呼び出されます。

<button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>

このボタンはクッリクした時にTurtleAttack()を呼び出し、this.setState({ShowFlag:true}により
ShowFlagを更新します。ShowFlagはCommentコンポーネントで解説します。

まずボタンを押したときの流れを確認します
ボタンクリック→TurtleAttack()→ダメージ、残りHP計算→RabbitAttack()→ダメージ、残りHP計算...→結果画面に遷移という処理になっています。
結果画面は最後に実装します。

まず、関数にコメントを追加し解説していきます。

その前に1つ説明しておきます。
stateの値を更新するためには必ず
this.setState({stateの名前: 更新する値})
というようにstateを更新します。
これを使って以下の関数では値を更新していきます。
では関数をみていきましょう。

 TurtleAttack ():void {
        //ClickFlagを設けこのフラグがtrueの間のみクリックを受け付けます。
        //これによりボタン連打による誤動作を防ぎます。
        if(this.state.ClickFlag) {
            //ここでClickFlagをfalseとすることでこの関数の実行中はボタンのクリックを受け付けません
            this.setState({ClickFlag: false});

            //カメが与えるダメージの配列を作成します。
            const Damage:number[] = [10,15,20,25];

            //上の配列からランダムに値を取り出し、カメがウサギに与えるダメージを決めます。
            let TurtleAttack = Damage[Math.floor(Math.random()* Damage.length)];

            //そのダメージをと攻撃する側のキャラクターの名前を保存します。
            //この2つの値はCommentコンポーネントに送り、攻撃時のコメントとして表示します。
            this.setState({Damage: TurtleAttack});
            this.setState({name: 'カメイ・ウェザー'})

            //ランダムで取り出したダメージを現在のHPから引いて残りのHPを計算しています。
            //20で割っているのはHPを100と設定しているからです。HPバーの初期値は5rem(50px)です
            //widthを10remとすると大きすぎるので、20で割って5remにしています。
            //ダメージの値は一桁より二桁の方が見栄えがいいと思ったのでこのように計算しています。
            let RestHP = this.state.RabbitHP - TurtleAttack / 20; 
            if (RestHP > 0){
                //残りHPが0以上の場合は上で計算した値をそのまま現在のHPとして保存します。
                this.setState({RabbitHP: RestHP}); 
            } else {
                //残りHPが0以下になった場合はHPを0とし結果画面に遷移させます。
                this.setState({RabbitHP: 0});
                location.href ="/Turtle_win"

            }
            //カメの与えたダメージを0.8秒間表示してウサギの攻撃に移ります。
            //ウサギの攻撃もカメと同様に計算し、最後にClickFlagをtrueにしてボタン操作を再受付けします
            //ウサギの攻撃はClickFlagをtrueにする以外ほぼ同じなので説明は省きます。
            setTimeout(this.RabbitAttack ,800);
        }else {
            this.setState({ClickFlag:false});
        }
    }

TurtleAttack ()は上記のようになっています。
ここでまた私が躓いたポイントです。
TurtleAttack ()のsetTimeoutのなかでthis.RabbitAttack()を呼び出しています。
ここはそのままだとうまく動作しません。thisの意味が変わってしまっているからです。
ですから、constractor内で以下のようにthisを固定してあげる必要があります。

    constructor(props:{}) {
        super(props);
        this.state = {
            TurtleHP: 5,
            RabbitHP: 5,
            Damage: null,
            name: '',
            ShowFlag: false,
            ClickFlag: true,
        }
        this.RabbitAttack = this.RabbitAttack.bind(this)
    }

これでネストが深いところでもthisで関数が使えます。
ここは以下のサイトがとても参考になりました。
React のクラスコンポーネントの bind は何を解決しているのか

これで関数の実装は完了です。
次にCommentコンポーネントについてみていきます。
説明済みの部分は省略しています。

    render () {
        //省略
        let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';
        return (
            <div className="l-field">
                <div className="p-field">
                    <div className="p-field__wrapper">
                        <div className="p-field__character-box -turtle">
                            <img src={`${window.location.origin}/images/turtle.png`} alt="キャラクターの画像" className="p-field__character -turtle"/>
                            <HP CharacterHP = {TurtleHP} />
                            <button className="p-field__button -view" onClick = { () => {this.TurtleAttack(); this.setState({ShowFlag:true});}}>たたかう</button>
                        </div>
                        <div className="p-field__character-box -rabbit">
                            <HP CharacterHP = {RabbitHP} />
                            <img src={`${window.location.origin}/images/rabbit.png`} alt="キャラクターの画像" className="p-field__character -rabbit"/>
                        </div>
                    </div>
                </div>
                {ShowDamage}
            </div>
        );
    }

まず、ShowDamageについて解説します。

let ShowDamage = this.state.ShowFlag ? <Comment Damage = {this.state.Damage} name = {this.state.name} /> : '';

ここはShowFlagがtrueのときCommentコンポーネントを表示し、falseのときは何も表示しないという意味です。
これはReactチュートリアルにも出て来るので確認してみて下さい。

buttonをクリックするとShowFlagをfalseからtrueに切り替えます。
この{ShowDamage}をrender内に入れておくことでボタンを押すと表示されるコメント欄を実装できます。

以上でバトルフィールドの実装は終了です。

結果画面の実装

最後に結果画面を実装します。

以下のTurtle_win.tsx,Rabbit_win.tsxをscreens内に作成して下さい。

Turtle_win.tsx
class TurtleWin extends React.Component {
    render() {
        return(
            <div className="l-winner">
                <div className="p-winner">
                    <p className="p-winner__text -view">カメイ・ウェザーの勝ち!!</p>
                </div>
            </div>
        );
    }
}

export default TurtleWin;
Rabbit_win.tsx
import * as React from "react";


class RabbitWin extends React.Component {
    render() {
        return(
            <div className="l-winner">
                <div className="p-winner">
                    <p className="p-winner__text -view">バニー・パッキャオ<br/>の勝ち!!</p>
                </div>
            </div>
        );
    }
}

export default RabbitWin;

この画面への遷移はFieldに実装してあります。

関数、TurtleAttack (),RabbitAttack ()内の

 location.href ="/Turtle_win"
 location.href ="/Rabbit_win"

でキャラクターのHPが0になった時、結果画面に遷移します。

以上で完成です!
お疲れ様でした。

ここまでのディレクトリ構成を確認しておきましょう。

root
├── dist
│   ├── css
│   │   └── style.css
│   ├── images
│   │   ├── rabbit.png
│   │   └── turtle.png
│   ├── index.html
│   └── js
├── node_modules
├── package-lock.json
├── package.json
├── src
│   ├── main.tsx
│   └── screens
│       ├── Comment.tsx
│       ├── Field.tsx
│       ├── GameStart.tsx
│       ├── HP.tsx
│       ├── Rabbit_win.tsx
│       ├── Test.tsx
│       └── Turtle_win.tsx
├── tsconfig.json
└── webpack.config.js

確認できたらターミナルで以下のコマンドを実行して下さい。

npm start

これで正常に動けば完了です!

最後に

この記事は私のReact学習の復習もかねて作成しました。
今回作ったものは本当に基礎的なものかと思いますが、Reactを学びはじめた私にとってはよくわからないエラーがでたりして大変でした。
Reactを学び始めた人は高い確率で同じエラーが出たりするのではないかと思います。
そのような人たちにこの記事を参考にしていただければ幸いです。
もしこの記事でわからないことがあれば質問して下さい。
出来る限り答えたいと思います。

以上です。
最後までみて頂きありがとうございます。

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

create-react-appを卒業して自分でReact + TypeScriptの開発環境を作れるようになるということ

これまでReactの環境構築をする時はcreate-react-appに頼りっきりでしたが、いい加減自分で作れないとまずいなと思い忘備録も兼ねて残しておきます。
また、せっかくTypeScriptも使うので webpack.config.js もTypeScriptで書けるようにしたいと思います。


最終的なディレクトリ構成は次のようになります。

.
┃━━ public
┃  ┗━ index.html
┃━━ src
┃  ┃━ index.tsx
┃  ┗━ App.tsx
┃━━ package.json
┃━━ package-lock.json
┃━━ tsconfig.json
┗━━ webpack.config.ts

それではやっていきましょう。

1. ディレクトリの初期化

適当なディレクトリを作って npm init するだけです。

mkdir hogehoge
cd hogehoge
npm init -y

-y オプションはお好みで(つけると全部初期値で自動的に初期化されます)。
初期化が終了すると package.json ができているはずです。

2. モジュールのインストール

React + TypeScriptの環境構築なので、とりあえず必要そうなモジュールをインストールしていきます。
バンドラにはWebpackを利用します。

npm i -d react react-dom
npm i -D typescript ts-loader webpack webpacl-cli webpack-dev-server @types/node @types/react @types/react-dom

@types/node の中に webpack.Configuration が定義されており、これがWebpackの設定情報の型です。
これを使って webpack.config.ts を書いていきます。

webpack.config.ts
import { Configuration } from 'webpack'
import path from 'path'

const config: Configuration = {
  mode: 'development',
  entry: './src/index.tsx',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: [
      '.tsx',
      '.ts',
      '.js',
    ],
  },
  devtool: 'inline-source-map',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}

export default config

感のいい人はお気づきかもしれませんが、現段階だと開発用サーバーの設定である devServer プロパティがありません(書いてみるとエラーになるはずです)。
そこで @types/webpack-dev-server をインストールします。

npm i -D @types/webpack-dev-server

これで webpack-dev-server の設定ができるようになりました。 webpack.config.ts に設定を追記します。

webpack.config.ts
const config: Configuration = {
  // ...
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
  },
  // ...
}

これでエラーが消えたはずです。

HtmlWebpackPlugin の設定

詳しくは割愛しますが、このプラグインを使うとテンプレートのHTMLのbodyタグの末尾にscriptタグを勝手に挿入してくれるので便利です。
というわけでインストールします。

npm i -D html-webpack-plugin @types/html-webpack-plugin
webpack.config.ts
import HtmlWebpackPlugin from 'html-webpack-plugin'

const config: Configuration = {
  // ...
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
  // ...
}

テンプレートとなるHTMLファイルを public/index.html に作成します。

public/index.html
<html lang="ja">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>your app title</title>
</head>

<body>
  <div id="root"></div>
</body>

</html>

ここではテンプレートHTMLのパスしか設定しませんが、 title など他にもいくつかオプションがあるので気になる方は調べてみてください。

3. TypeScriptの設定

tsconfig.json を作成・編集します。

tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "noImplicitAny": true,
    "module": "es6",
    "target": "es5",
    "jsx": "react",
    "allowJs": true,
    "allowSyntheticDefaultImports": true
  }
}

src ディレクトリ内に、エントリポイントとなる index.tsx を作っていきます。とりあえず簡単なものだけ。

src/App.tsx
import React, { FC } from 'react'

export const App: FC = () => <div>Hello World!</div>
src/index.tsx
import React from 'react'
import ReactDOM from 'react-dom'
import { App } from './App'

ReactDOM.render(<App />, document.getElementById("root"))

4. npm scriptsを設定する

package.json を編集します。
開発用サーバーを立ち上げるコマンドと、ビルド用のコマンドを用意します。

package.json
{
  // ...
  "scripts": {
    "dev": "webpack-dev-server --open",
    "build": "webpack"
  },
  // ...
  }
}

ここまででほぼ完成みたいなものですが、このままだとコンパイルできません。

5. 設定ファイルを読み込めるようにする

今のままだと設定ファイルが .ts ファイルなので読み込めません。そこで ts-node というモジュールを使用します。
モジュール名の通りですが、Node.jsがTypeScriptを直接読めるようにするものです。

npm i -D ts-node

これでコンパイルが通る・・・ようにはなりません。もう1ステップ必要です。
作成した webpack.config.ts ですが、import/export文を使用しているためこのままだと使えません。
これが使えるように tsconfig.json を編集します。

tsconfig.json
{
  "compilerOptions": {
    // ...
    "esModuleInterop": true,
    "module": "commonjs",
    // ...
  }
}

詳細な説明は省きますが、「CommonJSモジュールだよ~」と教えてあげることで使えるようになります。

とりあえずはこれにて終了です。npm run dev なり npm run build して開発に励みましょう。

6. おわりに

ところどころ端折りましたが、これでReact + TypeScriptの開発環境は作れたはずです。
似たような記事は結構あるのですが、同じReact + TypeScriptでもWebpackの設定までTypeScriptで行っているものは少なかったので記事にしました。型定義があるぶん補完も効くので書きやすいです。

こんな記事でも誰かのお役に立てば幸いです。ご覧頂きありがとうございました。

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

様々な Web API を使用して感情付きのチャットシステムを作った話

はじめに

先月、「2019 年度 enPiT ビジネスシステムデザイン分野ワークショップ」に参加してきました。(私は「感情表現を付加した Web チャットシステム」のプロジェクトメンバーとして参加)
そこでプロトタイプを制作したので機能や実装方法も含めここに書き出していきたい思います。
https://enpit.coins.tsukuba.ac.jp/bizsysdws2019/

注意点

  • PBL 終了後( 1 月終わり)に助っ人として参加
  • 実装期間が 1 週間しかなかった
  • いろいろゴリ押して実装している所多々あり

気になること多々あると思いますが、多めに見てください... 😇
書き方やその他のアドバイスなどあればお気軽にどうぞ!

できたもの

せっかくなのでデモ動画をどうぞ 📺
IMAGE ALT TEXT HERE
https://youtu.be/wIenptbd71I

機能としては

1. ユーザー名を指定してログイン
2. 普通のテキストチャットのように文章を打ち込む
3. 右上の音声化ボタンをクリックすることによって感情が付与された音声が再生される(アイコン付き)

となっています

使用技術

クライアントサイド

サーバーサイド

チャット部分

React で UI 部分を作成し、テキストデータを Firebase Realtime Database に送るように実装しました。
こんな感じでゴリ押してます😅

function MessageInput(props) {
    const [value, setValue] = useState('');

    function setMessageForFirebase () {
        if (value !== '') {
            const sendData = {
                user: props.user,
                text: value,
                createdAt: new Date()
            };

            const newPostKey = props.database.ref().push().key;
            let updates = {};
            updates['/messages/' + newPostKey] = sendData;
            props.database.ref().update(updates);

            setValue('');
        }

        else {
            alert('文章を入力してください!');
        }
    }

    return (Object.keys(props.database).length)
        ? (
            <div className="MessageInput">
                <input className="MessageInput-input" type="text" value={value} onChange={(e) => { setValue(e.target.value) }} />
                <div className="MessageInput-button" onClick={() => setMessageForFirebase()}>
                    <FontAwesomeIcon icon={['fas', 'paper-plane']} />
                </div>
            </div>
        )
        : (<div />);
}

テキストの感情付与

チャットの吹き出し右上にある音声ボタンをクリックすると音声が再生されるように実装しました。
ボタンが押されると NestJS で実装した自作 API にテキストデータを送信します。

テキストデータは複数行にも対応できるようにし、「。」「?」「!」「.」で行が区切られると仮定しました。
「、」はまだ行が続いていることが多いと判断したため、区切らないようにしています。

// ローディング画面を表示
props.setIsLoading(true);

// 複数行や「。」に対応する
const splitTextArray = (props.message.text).split(/[。?!.]/);
if (splitTextArray[splitTextArray.length - 1] === "") {
    splitTextArray.pop();
}

const url = `http://localhost:3000/api`;
const res = await fetch(url, {
    method: 'POST',
    mode: 'cors',
    body: JSON.stringify(({ texts: splitTextArray })),
    headers: {
        'Content-Type': 'application/json'
    }
});
const data = await res.json();

感情の分類
次にメタデータ株式会社さんの「感情分析 API」と NTT コミュニケーション株式会社さんの「COTOHA API」を使用してテキストがどの感情を指しているかを決めます。
今回は音声を再生するときに使用する「VoiceTextWeb API」の仕様上、感情を表現できる

  • 怒り
  • 悲しみ
  • 喜び

を感情の値として分類しました。

public async analysisForMetadataApi (text: string): Promise<any> {
    const res = await fetch(`http://ap.mextractr.net/ma9/emotion_analyzer?out=json&apikey=${this.metadataApikey}&text=${encodeURI(text)}`);
    const data = await res.json();

    // 感情値が取れなかった場合
    if (data.likedislike === 0 && data.joysad === 0 && data.angerfear === 0) {
        return 'none';
    }

    // まずは 3 軸のどれにするかを決める (VoiceText Api がこの 3 種類の感情から声を生成するため)
    // TODO: ゴリ押し
    let targetEmotion = 'happiness';
    let targetEmotionScore = data.likedislike;

    if (Math.abs(data.joysad) < Math.abs(data.angerfear)) {
        targetEmotion = 'anger';
        targetEmotionScore = data.angerfear;
    }

    if (Math.abs(data.likedislike) < Math.abs(data.joysad)) {
        targetEmotion = 'sadness';
        targetEmotionScore = data.joysad;
    }

    // 感情の値が負か正かを判別
    if (Math.sign(targetEmotionScore) === 1) {
        if (targetEmotion === 'sadness') {
            targetEmotion = 'happiness';
        }
    }

    else if (Math.sign(targetEmotionScore) === -1) {
        if (targetEmotion === 'happiness') {
            targetEmotion = 'sadness';
        }

        else if (targetEmotion === 'anger') {
            targetEmotion = 'sadness';
        }
    }

    return targetEmotion;
}

public async analysisForCotohaApi (text: string): Promise<any> {
    // アクセストークンを何度も取得する必要はない
    if (this.cotohaAccessToken === '') {
        const tokenUrl = `https://api.ce-cotoha.com/v1/oauth/accesstokens`;
        const tokenBody = {
            'grantType': 'client_credentials',
            'clientId': this.cotohaClientId,
            'clientSecret': this.cotohaClientSecret
        };

        const tokenRes = await fetch(tokenUrl, {
            method: 'POST',
            headers: { 'Content-Type': 'application/json;charset=UTF-8' },
            body: JSON.stringify(tokenBody)
        });

        const tokenJson = await tokenRes.json();
        this.cotohaAccessToken = await tokenJson.access_token;
    }

    const url = `https://api.ce-cotoha.com/api/dev/nlp/v1/sentiment`;
    const body = { 'sentence': text };

    const res = await fetch(url, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json;charset=UTF-8',  'Authorization': `Bearer ${this.cotohaAccessToken}` },
        body: JSON.stringify(body)
    });

    const data = await res.json();

    if (data.status === 0) {
        const emotionData = data.result;

        // 感情分類値は 3 つ存在している
        switch (emotionData.sentiment) {
            case 'Positive':
                return 'happiness';

            case 'Negative':
                // 怒りか悲しみか判断する
                // とりあえず、「怒る」と「悲しい」の判別要素の個数で多い方を優先する
                const angryPhraseArray = emotionData.emotional_phrase.filter((e) => (e.emotion === '怒る'));
                const sadPhraseArray = emotionData.emotional_phrase.filter((e) => (e.emotion === '悲しい'));

                if (angryPhraseArray.length < sadPhraseArray.length) {
                    return 'sadness';
                }

                // 感情値の個数が同値だった場合は怒りを優先する
                else {
                    return 'anger';
                }

            case 'Neutral':
                return 'neutral';
        }
    }
}

メタデータ株式会社さんの API では感情の値が likedislikejoysadangerfear の3つで値が返ってきます。それぞれ -3 から 3 の間で値が返ってくるので、今回は一番値が大きい感情値を使うことにしました。Math.abs() を使用することで絶対値が取れるので一番大きな値がわかります。

COHOTA API では感情の値が PositiveNegative で返ってきます。 Negative の場合は「怒り」と「悲しみ」を一発で判断できないので判別する処理を入れています。

ちなみに COTOHA API ではもう少し詳細に感情値が返ってくるのですが今回は割愛します…
たくさん機能があるので試しに使ってみるのはどうでしょうか。

感情の起伏
その後、感情の起伏がどのくらいあるのかを判別します。
今回は GCP の「Natural Language API」に判別をかけ、ポジネガの値の大きさで感情の起伏を判別しました。
公式から Node.js で Natural Language API を使用できるライブラリが出ていますのでそれを使いました。
Natural Language: Node.js Client
起伏の閾値は適当に決めています。

public async analysisForGoogleNaturalLanguageApi (text: string): Promise<any> {
    const client = new LanguageServiceClient;
    const document: any = {
        content: text,
        type: 'PLAIN_TEXT'
    };

    if (sentiment.score <= -0.75 || sentiment.score >= 0.75) {
        return '4';
    }

    else if (sentiment.score <= -0.5 || sentiment.score >= 0.5) {
        return '3';
    }

    else if (sentiment.score <= -0.25 || sentiment.score >= 0.25) {
        return '2';
    }

    else {
        return '1';
    }
}

テキストの音声化

VoiceTextWeb API を使用して音声化を行います。
パラメータに音声化したい文章と感情値と感情の起伏を入れて送信すると音声データが返ってきます。
最終的に音声データを base64 で使いたいので変換しておきます。

public async convertToVoice (data: any): Promise<any> {
    const url = `https://api.voicetext.jp/v1/tts`;
    const params = new URLSearchParams();
    params.append('text', data.text);
    params.append('speaker', 'hikari');
    params.append('emotion', (data.emotion !== 'neutral') ? data.emotion : '');
    params.append('emotion_level', data.emotionLevel);

    const res = await fetch(url, {
        method: 'POST',
        headers: { Authorization: "Basic " + new Buffer(`${this.voiceTextWebApiUserID}:${this.voiceTextWebApiPassword}`).toString("base64") },
        body: params
    });

    const buffer = await res.buffer();
    const base64 = buffer.toString('base64');

    return {
        emotion: data.emotion,
        base64: base64
    };
}

クライアントサイドに結果を返す

最終的にデータが集まったら感情値と音声データが入っている配列データとしてクライアントサイドに返してあげます。

public async mixTextToVoice(params: any): Promise<any> {
    const texts = params.texts;
    const analysisArray = [];
    const voiceDataArray = [];

    // 感情分析をかける (Google Natural Language API と メタデータ感情分析 API)
    for (const text of texts) {
        // 今回はメタデータさんの感情分析から一番感情のゆらぎが大きいものをその文章の感情として使用する
        let emotion = await this.analysisForMetadataApi(text);
        let emotionLevel = '';

        // メタデータさんの結果が微妙な場合
        if (emotion === 'none') {
            // COTOHA API
            emotion = await this.analysisForCotohaApi(text);
        }

        // COTOHA API でも結果が微妙な場合
        if (emotion === 'neutral') {
            emotion = '';
            emotionLevel = '';
        }

        // 感情の値が取れている場合は Google Natural Language API を使用する
        else {
            emotionLevel = await this.analysisForGoogleNaturalLanguageApi(text);
        }

        analysisArray.push({
            text: text,
            emotion: emotion,
            emotionLevel: emotionLevel
        });
    }

    // 分析したデータから音声化を行う
    for (const anaData of analysisArray) {
        const bufferVoiceData = await this.convertToVoice(anaData);
        voiceDataArray.push(bufferVoiceData);
    }

    return voiceDataArray;
}

結果を表示する

アイコンを表示する
感情値を表示してくれるコンポーネントを作成して表示します。

親コンポーネントから子コンポーネントにデータを受け渡して…

// 感情をアイコンで表示する
let emotionArray = [];
data.map((item) => emotionArray.push(item.emotion));
emotionArray = emotionArray.filter(function(x, i, self) {
    return self.indexOf(x) === i;
});
setEmotion(emotionArray);

子コンポーネントで表示します。

function MessageEmotion(props) {
    return (props.emotion.length) ?
        (
            <div className="MessageEmotion">
                {
                    props.emotion.map((v, i) => <EmotionComponent key={i} emotion={v} />)
                }
            </div>
        )
        : (<div />);
}

function EmotionComponent(props) {
    const componentArray = {
        'happiness': <FontAwesomeIcon className="MessageEmotion-child smile" icon={['fas', 'smile']} />,
        'anger': <FontAwesomeIcon className="MessageEmotion-child angry" icon={['fas', 'angry']} />,
        'sadness': <FontAwesomeIcon className="MessageEmotion-child sad" icon={['fas', 'sad-cry']} />,
        'neutral': <FontAwesomeIcon className="MessageEmotion-child meh" icon={['fas', 'meh']} />
    };

    return componentArray[props.emotion];
}

音声の再生
音声の再生は、文章の順に音声データが返ってくるため順に再生できるように実装しました。
Audio オブジェクトを作成して再生 します。音声再生終了後に呼ばれるイベントリスナーがあったので Promise オブジェクトを返すようにする ことで 音声が同時に再生してしまう問題を解消 しました。

個人的にこの辺の理解が甘くてハマってしまった…

// 音声を再生する
let audioPlayStateQueue = [];
for (const d of data) {
    audioPlayStateQueue.push(d.base64);
    const targetSoundBase64 = audioPlayStateQueue.pop();

    await new Promise((resolve) => {
        const sound = new Audio("data:audio/wav;base64," + targetSoundBase64);
        sound.play();
        sound.addEventListener('ended',async () => {
            await sleep(100);
            resolve();
        }, {once: true});
    });
}

感想

1 週間で制作したので結構ガタガタなコードになってしまいました…
NestJS の勉強も含めて制作したので思ったより時間がかかってしまったなあという印象です。
はやく express.js しかサーバーサイドのJSフレームワーク触れない人間やめたい
あと TypeScript、React もちゃんと勉強しないなあと思いました。
(TypeScript に関してはほとんど触ったことなかったので…)

個人的には結構面白いプロトタイプを作れたんじゃないかなあと思っているの機会があったらもう少し改良していきたいなあと考えてます。

まとめ

早く JavaScript つよつよ人間になりたいなあ 🥺

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

Windows10にてReact公式チュートリアル用のローカル開発環境構築時、node最新化、npx実行で突っかかった備忘録

はじめに

React公式チュートリアルをやってみたところ
ローカル開発環境構築中にnode最新化、npx実行で突っかかったので、備忘録。

実施したチュートリアル
Reactチュートリアル(日本語)

なお、自分はReactは触ったことが無い。

環境

  • windows10 64bit
  • node 6.9.4 -> 13.11.0
  • npm 6.13.7

ローカル開発環境の構築

チュートリアルを進める方法には、2つのオプションがある。

オプション 1: ブラウザでコードを書く
始めるのに一番手っ取り早い方法です!
...
オプション 2: ローカル開発環境
これは完全にオプションであり、このチュートリアルを進めるのに必須ではありません!

今回はオプション2を実施した。

node最新化

既存nodeバージョン確認

  1. 最新の Node.js がインストールされていることを確かめる。

とあるので、nodeを最新化する。
現行入っているバージョンを確認。

$ node -v
v6.9.4

nodistでnodeインストール

入れ替え方法は下記を参考にした。
windowsでNode.jsをバージョン管理する

$ nodist dist
...
  13.9.0
  13.10.0
  13.10.1
  13.11.0

$ nodist 13.11.0
13.11.0
Installing 13.11.0
 13.11.0 [===============] 53396/53396 KiB 100% 0.0s
Installation successful.

$ nodist + 13.11.0
13.11.0

$ node -v
v6.9.4

 
何故かnodeのバージョンが変わっていない。

既存nodeアンインストール

うーん。わからないので、「プログラムと機能」から「Node.js」をアンインストール。

再度nodeバージョン確認

その後、nodeのバージョンを確認。

$ node -v
v13.11.0

npm最新化

nodeが最新化された。npmも最新化しておく。

$ nodist npm match
npm matc

$npm -v
6.13.7

新しいプロジェクト作成

npxでcreate-react-app実行

  1. Create React App のインストールガイドに従って新しいプロジェクトを作成する

チュートリアルに従いnpxを実行する。

$ npx create-react-app my-app
'npx' は、内部コマンドまたは外部コマンド、
操作可能なプログラムまたはバッチ ファイルとして認識されていません。

npxが認識されなかった。
nodistでnodeインストール時にnpxもインストールされるはずだが……。

npxを手動インストール

https://paradox-tm.hateblo.jp/entry/2018/04/25/115243
上記記事で同様の現象が出ていたので、記事を参考に強制的にnpxをインストール。

$ npm install -g npx
...
added 493 packages from 654 contributors in XX.XXXs

$ npx create-react-app my-app
...
Happy hacking!

プロジェクトの動作確認

以降、チュートリアルに従いソースファイル作成し、最後にnpm startでサーバ起動。
http://localhost:3000 を開くと、空の三目並べの盤面が表示されることを確認できた。

以上です。

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

AWS Codepipelineを使ってReactアプリのCI環境をゼロから作る

はじめに

Macローカルで開発しているReactアプリを、git pushすれば自動的にビルドし公開環境へデプロイするCI環境をゼロから作ります。AWS Codepipelineを使い、GitHubと連携させてCodebuildでビルドしS3へデプロイします。

別記事にて紹介するAWS Amplifyを使えば(AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作る)、より簡単にCICD環境を作ることが可能です。ただしAmplifyにはいくつか制約もあるため、手動でCI環境を作る以下の手順も残します。

作成するもの

次のような環境をAWS上に構築します。

Untitled Diagram.png

事前準備

手順

GitHub環境の準備

GitHubリポジトリの準備

GitHubアカウントを開設し、公開鍵を登録し、リポジトリを作成します。アカウント開設と公開鍵登録までは「GitHubのアカウント登録からリポジトリ操作まで」が参考になります。リンク先はWindows環境ですが、Macだと最初からgitコマンドやssh-keygenコマンドが入っていますのでそれをそのまま使えます。gitを最新バージョンにしたい場合は「最新の Git を Mac にインストールする手順」が参考になります。
新規リポジトリの作成は、リンク先と異なり、Initialize this repository with a READMEのチェックボックスは外して空のリポジトリを作成します。ローカルのMacでcreate-react-appを使って作ったReactソースコードをリポジトリにアップロードするためです。リポジトリ名はここでは「cityrank」として説明を進めます。

Reactコードのリポジトリへのpush

ローカルでcreate-react-appを使って作ったReactアプリをGitHubのリポジトリに反映します。
create-react-appで作ると、自動的にgit initやgit commitは行われているため、下の手順でリポジトリの登録とpushを行います。
Reactプロジェクトのホームディレクトリに移動して下コマンドを実行します。

$ git remote add origin https://github.com/[アカウント名]/cityrank.git
$ git push -u origin master

originという名前でGitHub上のリポジトリをリモート登録し、masterという名前のブランチにpushしています。
SSH鍵のパスフレーズの入力を求められるので、SSH鍵を作るときに設定したパスフレーズを入力します。

Codepiplineの準備

buildspec.ymlの準備

Reactプロジェクトのホームディレクトリ直下に、以下の内容でbuildspec.ymlファイルを新規作成します。この定義に従って、このあと構成するCodebuildがビルドします。ここでは、Reactivesearchを使ったReactアプリを前提にbuildspec.ymlを作っています(install部分)。
ファイル作成後、git pushします。

buildspec.yml
version: 0.2
phases:
    install:
        commands:
            - npm install @appbaseio/reactivesearch
    pre_build:
        commands:
            - npm ci
    build:
        commands:
            - npm run build
artifacts:
    base-directory: build
    files:
        - '**/*'
cache:
    paths:
        - node_modules/**/*

buildspec.ymlをgit pushします。

$ git add buildspec.yml
$ git commit -m "buildspec hogehoge"
$ git push -u origin master

Codepipelineの構成

  1. AWS管理コンソールにログインし、Codepipelineの画面に移動
  2. 「パイプラインを作成する」ボタンを押す
  3. 「パイプライン名」に任意の名前を入れ、それ以外はデフォルトのまま「次に」ボタンを押す
  4. 「ソースプロバイダー」としてGitHubをリストから選び、「GitHubに接続する」ボタンを押して接続後、リポジトリとブランチを選択(接続するとGitHub上にあるものから選べるようになります)、検出オプションはGitHub ウェブフックのまま「次に」ボタンを押す
  5. 「プロバイダーを構築する」ではCodebuildを選び、リージョンはアジアパシフィック(東京)のまま、「プロジェクトを作成する」ボタンを押す
  6. 別ウィンドウが立ち上がるので、プロジェクト名に任意の名前を入れ、オペレーティングシステムにAmazon Linux 2、ランタイムにStandard、イメージにaws/codebuild/amazonlinux2-x86_64-standard:3.0を選択肢、それ以外はデフォルトのまま(ビルド仕様はbuildspecファイルを使用する、CloudWatch Logsはチェックをいれたまま)、「Codepipelineに進む」ボタンを押す
  7. 少し待つと、元のCodepipelineの画面に戻り「正常に作成されました」メッセージが表示されるので、「次に」ボタンを押す
  8. 「デプロイプロバイダー」にAmazon S3を選択し、「バケット」に公開用バケットを選び、「デプロイする前にファイルを抽出する」にチェックを入れて「次に」ボタンを押す
  9. 内容を確認し、「パイプラインを作成する」ボタンを押す

パイプライン作成後、自動で初回のパイプラインが走ります。エラーがないか確認し、Deployに成功していれば、公開環境へアクセスしてアプリが動作するか確認します。

リンク

GitHubのアカウント登録からリポジトリ操作まで

Reactで作ったWebアプリをGitHubで管理してS3に自動デプロイする

buildspec.ymlについては、AWS Codebuild ユーザーガイド

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

【React初心者】create-react-appからReactプロジェクトの環境構築

プロジェクトとしてReactのアプリケーションを作成し、実際に動かしてみます。

Node.jsの用意

以下URLからNode.js(LTS版)をダウンロードします。既にインストール済の方は不要です。
https://nodejs.org/ja/

インストールが完了したら、Node.jsが動くかどうか確認します。
コマンドまたはターミナルを起動し、以下を実行してください。

$ node --version

「v12.16.1」のようなバージョン番号が表示されればOKです。

Creare React Appでプロジェクト作成

Reactのアプリケーションを作成するには、「Create React App」というプログラムを使用します。
インストールは不要で、事前にインストールしたNode.jsに組み込まれている「npx」というプログラムを使用します。
コマンドまたはターミナルを起動し、cdコマンドを使ってプロジェクトを作成する場所に移動してください。
そして、以下コマンドを実行します。

$ npx create-react-app react_app

実行すると、その場に「react_app」というフォルダが作成されます。これがReactアプリケーションです。

プロジェクトを実行

作成されたプロジェクトを実際に動かしてみます。
コマンドまたはターミナルから、「cd react_app」を実行してreact_appフォルダの中に移動します。
そして、以下コマンドを実行します。

$ npm start

npm startは、開発用のWEBサーバープログラムを起動し、そこでアプリケーションを公開し、アクセスできる状態にします。
http://localhost:3000/」
に自動でアクセスされます。これがアプリケーションのアドレスになります。
Reactのロゴがゆっくりと回転する画面が表示されていたら成功です。
これで、プロジェクトを実行してWEBブラウザでアプリの画面を表示する操作ができました。
動作を確認できたら、コマンドまたはターミナルに戻り、Ctrlキーを押したまま「C」キーを押してスクリプトの実行を中断しましょう。

プロジェクトの中身をチェック

作成したプロジェクトがどのようになっているのか、「react_app」フォルダの中身を見てみましょう。
画面表示に関するファイルのみピックアップしています。

「public」フォルダ内
公開フォルダです。HTMLやCSSなど公開されるファイル類が保管されます。

      説明
index.html アクセスされるときに表示されるファイル。この中に、画面表示に関する基本的な要素がまとめてある。

「src」フォルダ内
Reactで作成したファイルなどがまとめられます。

説明
index.js アプリケーションのベースとなるスクリプト
index.css index.jsで使用するスタイルシート
App.js indexに組み込まれる、実際に画面を表示しているコンポーネント
App.css App.jsで使用するスタイルシート

アプリケーションにアクセスすると、以下の流れで画面表示されます。

  • index.htmlが読み込まれる。
  • index.htmlを読み込む際、index.jsが読み込まれ実行される。
  • index.jsの中でAppコンポーネントが読み込まれ表示される。

終わりに

今回の記事で、プロジェクトの作成と実行、その中身ができました。
次は自分でコンポーネントを作成しそれを表示される操作についてやっていきます。

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

【useState/ReactRedux】Reactにおける状態管理

概要

  • Reactにおける状態管理のしかたをReact標準で備えるuseStateを使ったものとReactにおけるReduxライブラリであるReactReduxを使ったものを紹介します

サンプルアプリの準備

  • この後の説明をするためのサンプルを作っておきます
  • 必要なライブラリを追加
yarn add react react-dom react-router-dom parcel-bundler

index.htmlを作成
index.html
<div id="root"></div>
<script src="index.js"></script>


index.jsを作成
index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './src/App';

ReactDOM.render(<App />, document.getElementById('root'));


src/App.jsの作成
src/App.js
import React from 'react';
import Router from './routes/router';

function App() {
  return <Router />;
}

export default App;


src/routes/router.jsを作成
src/routes/router.js
import React from 'react';
import { BrowserRouter, Route } from 'react-router-dom';

import Home from '../components/Home';
import Counter from '../components/Counter';
import TodoList from '../components/TodoList';

function Router() {
  return (
    <BrowserRouter>
      <div>
        <Route path="/" exact>
          <Home />
        </Route>
        <Route path="/counter" exact>
          <Counter />
        </Route>
        <Route path="/todo-list" exact>
          <TodoList />
        </Route>
      </div>
    </BrowserRouter>
  );
}

export default Router;


src/components/Home.jsを作成
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';

function Home() {
  return (
    <div>
      <h1>Home</h1>
      <p>
        <Link to="/counter">Counterへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Home;


src/components/Couner.jsを作成
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';

function Counter() {
  return (
    <div>
      <h1>Counter</h1>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;


src/components/TodoList.jsを作成
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';

function TodoList() {
  return (
    <div>
      <h1>TodoList</h1>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;

npx parcel index.html
  • ここまでだとこんな感じです

init.gif

useStateを使った状態管理

Counterアプリ

counter.gif

  • を押すとインクリメントされてを押すとデクリメントされるサンプルです
    • 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
  • src/components/Counter.jsを以下の内容に変更します
    • 1行目でuseStateのimportが追加されているようにReactが標準で提供するuseStateを使います
src/components/Counter.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';

function Counter() {
  // countは現在のカウントの値、setCountはcountを更新する関数、useStateの引数は初期値
  const [count, setCount] = useState(0);
  // countを+1する関数
  const increment = () => setCount(count + 1);
  // countを-1する関数
  const decrement = () => setCount(count - 1);
  return (
    <div>
      <h1>Counter</h1>
      <p>{count}</p>
      <button onClick={increment}></button>
      <button onClick={decrement}></button>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;
  • ボタンを押すとそれぞれincrement/decrement関数を呼び出しています
    • increment/decrementの中ではsetCountを実行しています
    • useStateで生成したsetXxxを実行し値が更新されると、最新の値で自動的に再描画が走るというのが特徴です
  • useStateの動きをなんとなくつかんでもらえたのではないでしょうか?

TodoListアプリ

todolist.gif

  • TodoListアプリを作ってみます
  • 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます
src/components/TodoList.js
import React, { useState } from 'react';
import { Link } from 'react-router-dom';

function TodoList() {
  // todoListを管理するstateを宣言
  const [todoList, setTodoList] = useState([]);
  // 入力内容を管理するstateを宣言
  const [inputText, setInputText] = useState('');

  // 入力内容が変化をstateにsetする関数
  const onChangeText = event => setInputText(event.target.value);
  // 入力内容をTodoListに追加して入力域を空にする関数
  const onClickAdd = () => {
    setTodoList([...todoList, { text: inputText }]);
    setInputText('');
  };
  return (
    <div>
      <h1>TodoList</h1>
      <input onChange={onChangeText} value={inputText} />
      <button onClick={onClickAdd}>追加</button>
      {todoList.map((todo, i) => (
        <p key={i}>{todo.text}</p>
      ))}
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;
  • Stateを複数管理したい場合はこのようにuseStateを必要なだけ宣言すればよいということがわかるかと思います
  • useStateの使い方に慣れてきましたか?

useStateの特徴

  • useStateはコンポーネント単位で宣言しています
  • なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
  • サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います

reset-state.gif

ReactReduxを使った状態管理

  • useStateはコンポーネント単位で状態を管理していましたが、ReactReduxはアプリ全体で状態の管理をします
    • single source of truthと言われるように状態はアプリ全体で一箇所で管理するという考え方です
  • ReactReduxを使うと、状態の管理や状態を更新するための処理がコンポーネントから切り離され役割が分離されるのが特徴です

ReactReduxのセットアップ

  • 必要なライブラリを追加します
    • 今回はredux-toolkitも使います
    • Reduxチームが作っているライブラリでこれを使うと実装量が大きく減少します
yarn add react-redux @reduxjs/toolkit
  • ReactReduxの設定周りのファイルを作成します
  • src/store/index.jsを作成します
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';

export const store = configureStore({
  reducer: combineReducers({
    // この中は後で作る
  }),
});
  • src/App.jsに適用します
src/App.js
import React from 'react';
import { Provider } from 'react-redux';
import Router from './routes/router';
import { store } from './store';

function App() {
  return (
    <Provider store={store}>
      <Router />
    </Provider>
  );
}

export default App;
  • これで準備OKです

Counterアプリ

  • useStateを使ったCounterアプリをReactRedux化していきます
  • まずはCounter周りの状態管理と状態更新を定義するファイルを作成します
  • src/store/counterSlice.jsを作成します
src/store/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';

export const counterSlice = createSlice({
  name: 'counter',
  // Stateの初期値を設定
  initialState: {
    count: 0,
  },
  // 状態を更新する関数を定義する場所
  reducers: {
    // countを+1する処理
    increment(state) {
      return { count: state.count + 1 };
    },
    // countを-1する処理
    decrement(state) {
      return { count: state.count - 1 };
    },
  },
});

// reducersに定義した処理を呼び出すActionをexportする
export const { increment, decrement } = counterSlice.actions;

// 現在のcountの値を取得するためのSelectorをexportする
export const selectCount = ({ counter }) => counter.count;

// お作法としてdefault exportでreducerをexport
export default counterSlice.reducer;
  • useStateのパターンでCounter.jsに定義されていた状態管理と状態更新の処理がこちらに移動してきました
    • 書き方は独特ですがひとつひとつの処理は見れば分かると思います
  • 次に今作ったファイルをsrc/store/index.jsに反映させます
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
// importを追加
import counterReducer from './counterSlice';

export const store = configureStore({
  reducer: combineReducers({
    // 定義を追加
    counter: counterReducer,
  }),
});
  • 次にcounterSliceに定義したものをコンポーネントが使いやすいようにワンクッション挟むファイルを作ります
    • このファイルでやってることを直接コンポーネントで書いても動きます
  • src/hooks/useCounter.jsを作成します
src/hooks/useCounter.js
import { useSelector, useDispatch } from 'react-redux';
// 現在のcountを取得するためのselectCount、countを更新するためのincrementとdecrementをimport
import { selectCount, increment, decrement } from '../store/counterSlice';

function useCounter() {
  const dispatch = useDispatch();
  // count,increment, decrementをすぐに使える状態で返す
  return {
    count: useSelector(selectCount),
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement()),
  };
}

export default useCounter;
  • counterSliceでexportしたものはSelect系はuseSelectorを、Action系はuseDispatchを通さないと使うことができません
    • コンポーネント側にReactReduxを意識したコードを増やしたくないのでそれを隠蔽するためにuseCounter.jsを作りました
  • 最後にsrc/components/Counter.jsuseCounterを使うように修正します
src/components/Counter.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';

function Counter() {
  // 以下の3行が不要になった
  // const [count, setCount] = useState(0);
  // const increment = () => setCount(count + 1);
  // const decrement = () => setCount(count - 1);

  // useCounterから値を取得
  const { count, increment, decrement } = useCounter();

  // 以下修正なし
  return (
    <div>
      <h1>Counter</h1>
      <p>{count}</p>
      <button onClick={increment}></button>
      <button onClick={decrement}></button>
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Counter;
  • 状態管理や状態更新をしていたコードが全て不要になりuseCounterから取得するものに置き換わりました
  • これでuseStateからの置き換え完了です
  • ページ遷移をしても状態が維持されることを確認しましょう

counter-redux.gif

TodoListアプリ

  • 同じ要領でTodoListもReactRedux化していきましょう
  • まずはsrc/store/todoSlice.jsを作成します
src/store/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';

// Slice
export const todoSlice = createSlice({
  name: 'todo',
  // Stateの初期値を設定
  initialState: {
    inputText: '',
    todoList: [],
  },
  // 状態を更新する関数を定義する場所
  reducers: {
    changeText(state, action) {
      return {
        // returnしたものがStateにセットされるため変更がないものはそのままになるように`...state`をセットしている
        ...state,
        // 引数で渡された値はaction.payloadで取得できる
        inputText: action.payload.inputText,
      };
    },
    add(state, action) {
      return {
        ...state,
        todoList: [...state.todoList, { text: action.payload.text }],
      };
    },
  },
});

// reducersに定義した処理を呼び出すActionをexportする
export const { changeText, add } = todoSlice.actions;

// 現在のcountの値を取得するためのSelectorをexportする
export const selectInputText = ({ todo }) => todo.inputText;
export const selectTodoList = ({ todo }) => todo.todoList;

// お作法としてdefault exportでreducerをexport
export default todoSlice.reducer;
  • Counterの時との違いは管理する状態が複数あることと、Actionの実行時に引数を受け取ることです
    • reducersの中でreturnする時に変更しない値も返す必要があるので...stateを一番上に入れておく
    • reducersの中の関数で第2引数で受け取るactionからaction.payloadでAction実行時に渡された引数を取得できる
  • src/store/index.jsに反映させます
src/store/index.js
import { configureStore, combineReducers } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';
// importを追加
import todoReducer from './todoSlice';

export const store = configureStore({
  reducer: combineReducers({
    counter: counterReducer,
    // 定義を追加
    todo: todoReducer,
  }),
});
  • 続いてコンポーネントが使いやすいようにワンクッション挟むuseTodoを作成します
  • src/hooks/useTodo.jsを作成します
src/hooks/useTodo.js
import { useSelector, useDispatch } from 'react-redux';
import { selectTodoList, selectInputText, changeText, add } from '../store/todoSlice';

function useTodo() {
  const dispatch = useDispatch();
  return {
    inputText: useSelector(selectInputText),
    todoList: useSelector(selectTodoList),
    changeText: (inputText) => dispatch(changeText({ inputText })),
    add: (text) => dispatch(add({ text })),
  };
}

export default useTodo;
  • 最後にuseTodoをコンポーネントに適用します
  • src/components/TodoList.jsを修正します
src/components/TodoList.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useTodo from '../hooks/useTodo';

function TodoList() {
  // useTodoから値を取得
  const { inputText, todoList, changeText, add } = useTodo();

  // 呼び出す関数をchangeTextに変更
  const onChangeText = event => changeText(event.target.value);

  const onClickAdd = () => {
    // 呼び出す関数をaddに変更
    add(inputText);
    // 呼び出す関数をchangeTextに変更
    changeText('');
  };

  // 以下修正なし
  return (
    <div>
      <h1>TodoList</h1>
      <input onChange={onChangeText} value={inputText} />
      <button onClick={onClickAdd}>追加</button>
      {todoList.map((todo, i) => (
        <p key={i}>{todo.text}</p>
      ))}
      <p>
        <Link to="/">Homeへ</Link>
        <Link to="/counter">Counterへ</Link>
      </p>
    </div>
  );
}

export default TodoList;
  • これで完成です
  • 動作確認してみましょう

todolist-redux.gif

ReactReduxの特徴

  • useStateの時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました
  • ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
    • それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
  • 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
    • 試しにsrc/components/Home.jsを以下のようにしてみましょう
src/components/Home.js
import React from 'react';
import { Link } from 'react-router-dom';
// importを追加
import useCounter from '../hooks/useCounter';
import useTodo from '../hooks/useTodo';

function Home() {
  // 値を取得
  const { count } = useCounter();
  const { todoList } = useTodo();
  return (
    <div>
      <h1>Home</h1>
      <p>現在のCounterの値: {count}</p>
      <p>現在のTodoListの件数: {todoList.length}</p>
      <p>
        <Link to="/counter">Counterへ</Link>
        <Link to="/todo-list">TodoListへ</Link>
      </p>
    </div>
  );
}

export default Home;
  • Homeコンポーネントからでも管理している状態にアクセスできることが確認できます

home-redux.gif

  • どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
    • 永続化したい場合はサーバに送信して保存しておく必要があります

まとめ

  • useStateを使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました
  • ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう

蛇足

  • 今回の例では問題は起きないがuseTodoの中でuseSelector(selectInputText)useSelector(selectTodoList)を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまう
  • useSelectorの存在意義からしてもコンポーネントで必要なものだけuseSelectorする方がいいのかもしれない
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成

はじめに

Reactのようなフロントエンドアプリの配置先としては、AWS S3のようなフルマネージドなWebホスティング環境が最適です。ただし、S3のWebホスティング機能は単体ではSSL(TSL)通信が不可のため、クローズドなS3の前にCloudFrontを置いてCloudFront経由でSSL(TSL)通信を実現します。その際、S3への直接アクセスやCloudFrontへの非暗号HTTP通信は拒否し、必ずSSL(TSL)で暗号化されたHTTPS通信となるようにします。
ポイントは、S3はパブリックアクセスを禁止して構成し、CloudFrontからのOAI(Origin Access Identity)を使ったアクセスのみ許可します。OAIの構成はCloudFrontの構成の中で自動で設定するようにできます。

より簡単に環境を作るには、別記事にて紹介するAWS Amplifyが使えます(AWS Amplify Consoleを使ってReactアプリのCICD環境を10分で作る)。以下の手順をすべてAmplifyが自動で行ってくれて同じような環境を自動で作ってくれるうえに、別記事で紹介するCICD環境も合わせてAmplifyが自動で用意してくれます。ただしAmplifyにはいくつか制約もあります。そちらも参考にしてください。

作成するもの

次のような環境をAWS上で構築します。
React-Deploy-Environment.png

事前準備

  • 公開するURL(ここでは、cityrank.test.comとします)を決める
  • お名前.comなどでドメイン名を登録しておく(AWSのRoute53からも取得できます)
  • AWS CLIを導入しておく(参照:AWS CLI v2をMacにインストールする

手順

初めにRoute53でDNSの準備と、公開したいURL用のサーバ証明書をACMで準備します。その後、Reactアプリの置き場としてS3バケットを作成し、公開用のCloudFrontを構成していきます。

DNSとサーバ証明書の準備

Route53登録

  1. AWS管理コンソールにログインし、Route53の画面に移動
  2. 「ホストゾーンの作成」ボタンを押す
  3. 「ドメイン名」に公開するFQDN(cityrank.test.com)のドメイン名部分(test.com)を入れ、タイプはパブリックホストゾーンのまま「作成」ボタンを押す
  4. 作成されたホストゾーンのNSレコード(タイプがNSとなっている行)の値にある4つのネームサーバ(ns-xx.awsdns-xx.com.といった値)を、レジストラ(ここではお名前.com)側の管理画面を開き、対象ドメイン名のネームサーバの箇所に4つすべて転記する

ACM(AWS Certificate Manager)でサーバ証明書作成

  1. AWS管理コンソールにログインし、Certificate Managerの画面に移動
  2. 右上のリージョンを東京からバージニア北部(us-east-1)に変更する(*1)
  3. 「証明書のプロビジョニング」の「今すぐ始める」ボタンを押す(もしくは証明書管理画面が出る場合は「証明書のリクエスト」ボタンを押す)
  4. 「パブリック証明書のリクエスト」のまま「証明書のリクエスト」ボタンを押す
  5. 「ドメイン名の追加」で「ドメイン名」にドメイン共通で使えるようにするためアスタリスクを使ったドメイン名を記入し(例:*.test.com)、「次へ」ボタンを押す
  6. 「DNSの検証」にチェックを入れたたま「次へ」ボタンを押す
  7. 「タグを追加」では、タグ名にName、値に識別する名称を入れて(ここではcityrank)「確認」ボタンを押す(タグの追加は省略してもOK)
  8. 「確定とリクエスト」ボタンを押す
  9. 「検証状態」が「検証保留中」となる
  10. そのまま「続行」ボタンを押す
  11. 次のページで、対象の証明書の左の三角(▶)をクリックして開き、そのなかの「ドメイン名」の左の三角(▶)を再度クリックして開く
  12. 「Route53でのレコードの作成」ボタンを押す(Route53側の設定が自動でアップデートされる)(*2)
  13. 「状況」が「発行済み」となるまで待つ(数分から30分以上)

*1 このあとでCloudFrontから証明書を使えるようにする際に、証明書がバージニア北部リージョンにないとCloudFrontから見えません。CloudFrontがリージョンに依存しないグローバル・サービスのためです。必ず東京ではなくバージニア北部で証明書を作成します。

*2 手動でのRoute53設定は以下の通り(「Route53でのレコードの作成」ボタンは同じことをしています)

Route53設定手順

1. 「ドメイン名」の左の三角(▶)をクリックして開き、そこにあるCNAMEレコードをRoute53に登録していく
2. Route53の管理画面を開き、test.comホストゾーンを開く
3. 「レコードセットの作成」ボタンを押し、「名前」にACMのCNAMEレコードの名前のランダムな文字列の部分だけを入れる
4. 「タイプ」をCNAMEに変更する
5. 「値」にACMのCNAMEレコードの値を入力する([ランダムな文字列].acm-validations.aws.)
6. 「作成」ボタンを押す

アプリ公開環境(S3+CloudFront)の構築

Reactアプリ配置先S3バケットの作成

今回はCloudFront経由で公開するために、S3の静的ウェブサイトホスティング機能は使いません。そのため、バケット名もFQDN気にせず自由に付けます。後のCloudFrontの設定でS3バケットポリシーも設定されるため、ここでは「ブロックパブリックアクセスのバケット設定」もオンのままで、パブリックアクセスできないメッセージが出ても問題ありません。

  1. AWS管理コンソールにログインし、S3の画面に移動
  2. 「バケットを作成」ボタンを押す
  3. 「一般的な設定」で、「バケット名」に任意の名前を付け、「リージョン」は東京(ap-northeast-1)を選ぶ
  4. 「ブロックパブリックアクセスのバケット設定」では「パブリックアクセスをすべてブロック」にチェックを入れたまま(デフォルト)
  5. 「バケットを作成」ボタンを押す
  6. 作成されたバケットの名前のリンクをクリックしてバケットを開き、「アップロード」ボタンでテスト用のindex.htmlをアップロード

CloudFrontログ保管先S3バケットの作成

上手順と同様にバケットを作成し、ログ保管先とするフォルダ(ここではcityrank)を作成します。

CloudFrontの構成

CloudFrontからのHTTPS通信のみ許可します。そのため、OAI(Origin Access Identity)を使い、OAIをもつCloudFrontからのみS3へのアクセスを受け付けるようS3バケットポリシーを構成します。そのバケットポリシーの更新は「Yes, Update Bucket Policy」にチェックを入れることでCloudFrontの構成の中で自動的に行われます。

  1. AWS管理コンソールにログインし、CloudFrontの画面に移動
  2. 「Create Distribution」ボタンを押す
  3. 「Web」の「Get Started」ボタンを押す
  4. 「Origin Settings」で「Origin Domain Name」でリストされる中から、上で作ったバケットを選択する
  5. 「Restrict Bucket Access」をYesにチェックを入れ、S3バケットへは必ずCloudFront経由のアクセスとする
  6. 「Origin Access Identity」で「Create a New Identity」にチェックを入れる
  7. 「Grant Read Permissions on Bucket」で「Yes, Update Bucket Policy」にチェックを入れ、S3バケットポリシーを自動でOrigin Access Identity対応に更新する設定にする
  8. 「Default Cache Behavior Settings」で「Viewer Protocol Policy」は「Redirect HTTP to HTTPS」にチェックを入れる
  9. 「Allowed HTTP Methods」は「GET, HEAD」のまま
  10. 「Distribution Settings」で「Alternate Domain Names(CNAMEs)」に公開用のURL(cityrank.test.com)を入れる
  11. 「SSL Certificate」で「Custom SSL Certificate (example.com)」にチェックを入れ、上のACMで作った証明書が入力ボックスに表示されるのを確認する(表示されていなければリストから対象証明書を選ぶ)
  12. 「Default Root Object」にindex.htmlを入れる(ルートパスにアクセスしてもReactコンテンツ=index.htmlを表示させるため)
  13. LoggingをOnにチェックボックスを入れ、ログ保管先のS3バケットとPrefix(フォルダ名=cityrank)を指定する
  14. それ以外はデフォルトのまま「Create Distribution」ボタンを押す
  15. 左ペインのDistributionをクリックし、しばらく待つとStatusがIn ProgressからDeployedに変わる
  16. 対象DistributionのIDのリンクをクリックして開き、「Domain Name」のところに記述されているURLにアクセスできるかを確認する(xxxxx.cloudfront.net)。

Route53へ独自ドメイン名の設定

独自ドメイン名を使用するために、Route53でCloudFrontへのエイリアスの設定を行います。

  1. AWS管理コンソールのRoute53の画面に移動
  2. ホストゾーンの対象ドメイン名を開く
  3. 「レコードセットの作成」ボタンを押し、名前に公開したいURLのサブドメイン名(cityrank)を入れ、タイプはAのまま、「エイリアス」をはいにチェックを入れ、「エイリアス先」のリストから上で設定したCloudFrontを選ぶ
  4. 「作成」ボタンを押す
  5. 公開したいURLでアクセスできるかを確認する(cityrank.test.com)。

Reactアプリのアップロード

AWS CLIを使ったアップロード

  1. Reactプロジェクトのフォルダに移動し、ビルドする $ npm run build
  2. buildフォルダに移動し、buildフォルダ配下の内容を、公開用S3バケットへ、AWS CLIでアップロードする $ aws s3 cp ./* s3://[バケット名]/
  3. 公開したいURLでアクセスし、Reactアプリが表示されるか確認する(cityrank.test.com)。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む