- 投稿日:2020-03-25T20:01:58+09:00
超初心者のための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.jsmodule.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.tsximport * 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.tsximport * 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.tsximport * 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.tsximport * 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.tsximport * 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.tsxinterface 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.tsximport * 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.tsxclass 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.tsximport * 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を学び始めた人は高い確率で同じエラーが出たりするのではないかと思います。
そのような人たちにこの記事を参考にしていただければ幸いです。
もしこの記事でわからないことがあれば質問して下さい。
出来る限り答えたいと思います。以上です。
最後までみて頂きありがとうございます。
- 投稿日:2020-03-25T15:02:32+09:00
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.tsimport { 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.tsconst config: Configuration = { // ... devServer: { contentBase: path.resolve(__dirname, 'dist'), }, // ... }これでエラーが消えたはずです。
HtmlWebpackPlugin
の設定詳しくは割愛しますが、このプラグインを使うとテンプレートのHTMLのbodyタグの末尾にscriptタグを勝手に挿入してくれるので便利です。
というわけでインストールします。npm i -D html-webpack-plugin @types/html-webpack-pluginwebpack.config.tsimport 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.tsximport React, { FC } from 'react' export const App: FC = () => <div>Hello World!</div>src/index.tsximport 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で行っているものは少なかったので記事にしました。型定義があるぶん補完も効くので書きやすいです。こんな記事でも誰かのお役に立てば幸いです。ご覧頂きありがとうございました。
- 投稿日:2020-03-25T14:38:38+09:00
様々な Web API を使用して感情付きのチャットシステムを作った話
はじめに
先月、「2019 年度 enPiT ビジネスシステムデザイン分野ワークショップ」に参加してきました。(私は「感情表現を付加した Web チャットシステム」のプロジェクトメンバーとして参加)
そこでプロトタイプを制作したので機能や実装方法も含めここに書き出していきたい思います。
https://enpit.coins.tsukuba.ac.jp/bizsysdws2019/注意点
- PBL 終了後( 1 月終わり)に助っ人として参加
- 実装期間が 1 週間しかなかった
- いろいろゴリ押して実装している所多々あり
気になること多々あると思いますが、多めに見てください... ?
書き方やその他のアドバイスなどあればお気軽にどうぞ!できたもの
せっかくなのでデモ動画をどうぞ ?
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 では感情の値が
likedislike
、joysad
、angerfear
の3つで値が返ってきます。それぞれ-3 から 3
の間で値が返ってくるので、今回は一番値が大きい感情値を使うことにしました。Math.abs()
を使用することで絶対値が取れるので一番大きな値がわかります。COHOTA API では感情の値が
Positive
とNegative
で返ってきます。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 つよつよ人間になりたいなあ ?
- 投稿日:2020-03-25T12:42:52+09:00
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バージョン確認
- 最新の Node.js がインストールされていることを確かめる。
とあるので、nodeを最新化する。
現行入っているバージョンを確認。$ node -v v6.9.4nodistで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.0npm最新化
nodeが最新化された。npmも最新化しておく。
$ nodist npm match npm matc $npm -v 6.13.7新しいプロジェクト作成
npxでcreate-react-app実行
- 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 を開くと、空の三目並べの盤面が表示されることを確認できた。以上です。
- 投稿日:2020-03-25T12:41:42+09:00
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上に構築します。
事前準備
- 「React版Reactivesearch v3を使ってゼロから最速でElasticsearchフロントアプリを作る」で作ったReactアプリ、もしくはcreate-react-appで作ったReactアプリ
- 「AWS S3 + CloudFrontでReactアプリをHTTPS公開するための正しい構成」で作った環境、もしくはアプリを公開するS3環境
手順
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 masteroriginという名前でGitHub上のリポジトリをリモート登録し、masterという名前のブランチにpushしています。
SSH鍵のパスフレーズの入力を求められるので、SSH鍵を作るときに設定したパスフレーズを入力します。Codepiplineの準備
buildspec.ymlの準備
Reactプロジェクトのホームディレクトリ直下に、以下の内容でbuildspec.ymlファイルを新規作成します。この定義に従って、このあと構成するCodebuildがビルドします。ここでは、Reactivesearchを使ったReactアプリを前提にbuildspec.ymlを作っています(install部分)。
ファイル作成後、git pushします。buildspec.ymlversion: 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 masterCodepipelineの構成
- AWS管理コンソールにログインし、Codepipelineの画面に移動
- 「パイプラインを作成する」ボタンを押す
- 「パイプライン名」に任意の名前を入れ、それ以外はデフォルトのまま「次に」ボタンを押す
- 「ソースプロバイダー」としてGitHubをリストから選び、「GitHubに接続する」ボタンを押して接続後、リポジトリとブランチを選択(接続するとGitHub上にあるものから選べるようになります)、検出オプションはGitHub ウェブフックのまま「次に」ボタンを押す
- 「プロバイダーを構築する」ではCodebuildを選び、リージョンはアジアパシフィック(東京)のまま、「プロジェクトを作成する」ボタンを押す
- 別ウィンドウが立ち上がるので、プロジェクト名に任意の名前を入れ、オペレーティングシステムにAmazon Linux 2、ランタイムにStandard、イメージにaws/codebuild/amazonlinux2-x86_64-standard:3.0を選択肢、それ以外はデフォルトのまま(ビルド仕様はbuildspecファイルを使用する、CloudWatch Logsはチェックをいれたまま)、「Codepipelineに進む」ボタンを押す
- 少し待つと、元のCodepipelineの画面に戻り「正常に作成されました」メッセージが表示されるので、「次に」ボタンを押す
- 「デプロイプロバイダー」にAmazon S3を選択し、「バケット」に公開用バケットを選び、「デプロイする前にファイルを抽出する」にチェックを入れて「次に」ボタンを押す
- 内容を確認し、「パイプラインを作成する」ボタンを押す
パイプライン作成後、自動で初回のパイプラインが走ります。エラーがないか確認し、Deployに成功していれば、公開環境へアクセスしてアプリが動作するか確認します。
リンク
Reactで作ったWebアプリをGitHubで管理してS3に自動デプロイする
buildspec.ymlについては、AWS Codebuild ユーザーガイド
- 投稿日:2020-03-25T12:04:23+09:00
【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 startnpm 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コンポーネントが読み込まれ表示される。
終わりに
今回の記事で、プロジェクトの作成と実行、その中身ができました。
次は自分でコンポーネントを作成しそれを表示される操作についてやっていきます。
- 投稿日:2020-03-25T03:33:07+09:00
【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.jsimport React from 'react'; import ReactDOM from 'react-dom'; import App from './src/App'; ReactDOM.render(<App />, document.getElementById('root'));
src/App.js
の作成src/App.jsimport React from 'react'; import Router from './routes/router'; function App() { return <Router />; } export default App;
src/routes/router.js
を作成src/routes/router.jsimport 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.jsimport 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.jsimport 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.jsimport 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;
- 以下のコマンドで起動すると http://localhost:1234 でアクセスできます
npx parcel index.html
- ここまでだとこんな感じです
useStateを使った状態管理
Counterアプリ
+
を押すとインクリメントされてー
を押すとデクリメントされるサンプルです
- 現在の値をメモリ上で保持しておく必要があるためそこで状態管理が発生するわけですね
src/components/Counter.js
を以下の内容に変更します
- 1行目で
useState
のimportが追加されているようにReactが標準で提供するuseState
を使いますsrc/components/Counter.jsimport 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アプリを作ってみます
- 現在のTodoのリストや現在の入力された値をuseStateを使って管理してみます
src/components/TodoList.jsimport 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
はコンポーネント単位で宣言しています- なのでコンポーネントが破棄されると管理していた状態もすべて破棄されてしまいます
- サンプルアプリでページ遷移して戻ってくると値がリセットされていることからも分かるかと思います
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.jsimport { configureStore, combineReducers } from '@reduxjs/toolkit'; export const store = configureStore({ reducer: combineReducers({ // この中は後で作る }), });
src/App.js
に適用しますsrc/App.jsimport 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.jsimport { 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.jsimport { 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.jsimport { 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.js
でuseCounter
を使うように修正しますsrc/components/Counter.jsimport 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
からの置き換え完了です- ページ遷移をしても状態が維持されることを確認しましょう
TodoListアプリ
- 同じ要領でTodoListもReactRedux化していきましょう
- まずは
src/store/todoSlice.js
を作成しますsrc/store/todoSlice.jsimport { 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.jsimport { 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.jsimport { 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.jsimport 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;
- これで完成です
- 動作確認してみましょう
ReactReduxの特徴
useState
の時は状態管理はコンポーネントに閉じていたためコンポーネントが破棄されると状態も破棄されていました- ReactReduxを使うとコンポーネントの外で状態を管理するためコンポーネントが破棄されても状態を維持することができます
- それにともなってソースコードも状態管理とコンポーネントと分離されてGoodですね
- 状態がコンポーネントの外で管理されているためどのコンポーネントからでも状態にアクセス可能にもなります
- 試しに
src/components/Home.js
を以下のようにしてみましょうsrc/components/Home.jsimport 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コンポーネントからでも管理している状態にアクセスできることが確認できます
- どのコンポーネントからでもアクセスはできますが、あくまでメモリ上で保持しているだけなのでリロードすると値は消えてしまいます
- 永続化したい場合はサーバに送信して保存しておく必要があります
まとめ
useState
を使ったコンポーネント内での状態管理と、ReactReduxを使ったアプリ全体での状態管理を紹介しました- ReactReduxを使うと登場人物が増えて複雑になってくるところもあるので規模や要件に応じて使い分けるとよいでしょう
蛇足
- 今回の例では問題は起きないが
useTodo
の中でuseSelector(selectInputText)
とuseSelector(selectTodoList)
を呼んでいるため、どちらか片方しか使わないコンポーネントがもう一方の値が更新された時にも再レンダリングされてしまうuseSelector
の存在意義からしてもコンポーネントで必要なものだけuseSelector
する方がいいのかもしれない
- 投稿日:2020-03-25T02:16:15+09:00
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にはいくつか制約もあります。そちらも参考にしてください。
作成するもの
事前準備
- 公開するURL(ここでは、cityrank.test.comとします)を決める
- お名前.comなどでドメイン名を登録しておく(AWSのRoute53からも取得できます)
- AWS CLIを導入しておく(参照:AWS CLI v2をMacにインストールする)
手順
初めにRoute53でDNSの準備と、公開したいURL用のサーバ証明書をACMで準備します。その後、Reactアプリの置き場としてS3バケットを作成し、公開用のCloudFrontを構成していきます。
DNSとサーバ証明書の準備
Route53登録
- AWS管理コンソールにログインし、Route53の画面に移動
- 「ホストゾーンの作成」ボタンを押す
- 「ドメイン名」に公開するFQDN(cityrank.test.com)のドメイン名部分(test.com)を入れ、タイプはパブリックホストゾーンのまま「作成」ボタンを押す
- 作成されたホストゾーンのNSレコード(タイプがNSとなっている行)の値にある4つのネームサーバ(ns-xx.awsdns-xx.com.といった値)を、レジストラ(ここではお名前.com)側の管理画面を開き、対象ドメイン名のネームサーバの箇所に4つすべて転記する
ACM(AWS Certificate Manager)でサーバ証明書作成
- AWS管理コンソールにログインし、Certificate Managerの画面に移動
- 右上のリージョンを東京からバージニア北部(us-east-1)に変更する(*1)
- 「証明書のプロビジョニング」の「今すぐ始める」ボタンを押す(もしくは証明書管理画面が出る場合は「証明書のリクエスト」ボタンを押す)
- 「パブリック証明書のリクエスト」のまま「証明書のリクエスト」ボタンを押す
- 「ドメイン名の追加」で「ドメイン名」にドメイン共通で使えるようにするためアスタリスクを使ったドメイン名を記入し(例:*.test.com)、「次へ」ボタンを押す
- 「DNSの検証」にチェックを入れたたま「次へ」ボタンを押す
- 「タグを追加」では、タグ名にName、値に識別する名称を入れて(ここではcityrank)「確認」ボタンを押す(タグの追加は省略してもOK)
- 「確定とリクエスト」ボタンを押す
- 「検証状態」が「検証保留中」となる
- そのまま「続行」ボタンを押す
- 次のページで、対象の証明書の左の三角(▶)をクリックして開き、そのなかの「ドメイン名」の左の三角(▶)を再度クリックして開く
- 「Route53でのレコードの作成」ボタンを押す(Route53側の設定が自動でアップデートされる)(*2)
- 「状況」が「発行済み」となるまで待つ(数分から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バケットポリシーも設定されるため、ここでは「ブロックパブリックアクセスのバケット設定」もオンのままで、パブリックアクセスできないメッセージが出ても問題ありません。
- AWS管理コンソールにログインし、S3の画面に移動
- 「バケットを作成」ボタンを押す
- 「一般的な設定」で、「バケット名」に任意の名前を付け、「リージョン」は東京(ap-northeast-1)を選ぶ
- 「ブロックパブリックアクセスのバケット設定」では「パブリックアクセスをすべてブロック」にチェックを入れたまま(デフォルト)
- 「バケットを作成」ボタンを押す
- 作成されたバケットの名前のリンクをクリックしてバケットを開き、「アップロード」ボタンでテスト用のindex.htmlをアップロード
CloudFrontログ保管先S3バケットの作成
上手順と同様にバケットを作成し、ログ保管先とするフォルダ(ここではcityrank)を作成します。
CloudFrontの構成
CloudFrontからのHTTPS通信のみ許可します。そのため、OAI(Origin Access Identity)を使い、OAIをもつCloudFrontからのみS3へのアクセスを受け付けるようS3バケットポリシーを構成します。そのバケットポリシーの更新は「Yes, Update Bucket Policy」にチェックを入れることでCloudFrontの構成の中で自動的に行われます。
- AWS管理コンソールにログインし、CloudFrontの画面に移動
- 「Create Distribution」ボタンを押す
- 「Web」の「Get Started」ボタンを押す
- 「Origin Settings」で「Origin Domain Name」でリストされる中から、上で作ったバケットを選択する
- 「Restrict Bucket Access」をYesにチェックを入れ、S3バケットへは必ずCloudFront経由のアクセスとする
- 「Origin Access Identity」で「Create a New Identity」にチェックを入れる
- 「Grant Read Permissions on Bucket」で「Yes, Update Bucket Policy」にチェックを入れ、S3バケットポリシーを自動でOrigin Access Identity対応に更新する設定にする
- 「Default Cache Behavior Settings」で「Viewer Protocol Policy」は「Redirect HTTP to HTTPS」にチェックを入れる
- 「Allowed HTTP Methods」は「GET, HEAD」のまま
- 「Distribution Settings」で「Alternate Domain Names(CNAMEs)」に公開用のURL(cityrank.test.com)を入れる
- 「SSL Certificate」で「Custom SSL Certificate (example.com)」にチェックを入れ、上のACMで作った証明書が入力ボックスに表示されるのを確認する(表示されていなければリストから対象証明書を選ぶ)
- 「Default Root Object」にindex.htmlを入れる(ルートパスにアクセスしてもReactコンテンツ=index.htmlを表示させるため)
- LoggingをOnにチェックボックスを入れ、ログ保管先のS3バケットとPrefix(フォルダ名=cityrank)を指定する
- それ以外はデフォルトのまま「Create Distribution」ボタンを押す
- 左ペインのDistributionをクリックし、しばらく待つとStatusがIn ProgressからDeployedに変わる
- 対象DistributionのIDのリンクをクリックして開き、「Domain Name」のところに記述されているURLにアクセスできるかを確認する(xxxxx.cloudfront.net)。
Route53へ独自ドメイン名の設定
独自ドメイン名を使用するために、Route53でCloudFrontへのエイリアスの設定を行います。
- AWS管理コンソールのRoute53の画面に移動
- ホストゾーンの対象ドメイン名を開く
- 「レコードセットの作成」ボタンを押し、名前に公開したいURLのサブドメイン名(cityrank)を入れ、タイプはAのまま、「エイリアス」をはいにチェックを入れ、「エイリアス先」のリストから上で設定したCloudFrontを選ぶ
- 「作成」ボタンを押す
- 公開したいURLでアクセスできるかを確認する(cityrank.test.com)。
Reactアプリのアップロード
AWS CLIを使ったアップロード
- Reactプロジェクトのフォルダに移動し、ビルドする
$ npm run build
- buildフォルダに移動し、buildフォルダ配下の内容を、公開用S3バケットへ、AWS CLIでアップロードする
$ aws s3 cp ./* s3://[バケット名]/
- 公開したいURLでアクセスし、Reactアプリが表示されるか確認する(cityrank.test.com)。