- 投稿日:2020-03-22T21:33:06+09:00
サーバーレス(React+Firebase)で新型コロナウイルスの感染者数マップを作った話
Firebaseを使ってサーバーレスで新型コロナウイルス(COVID-19)の感染者数の推移を地域別・日別で見られるWebサイトを作成しました。
https://covid19-visualization.web.app/
以下のような技術を使って制作しています。
システム・ツール 役割 React フロント側の画面の構築 Google Maps Platform - Maps JavaScript API 地図の表示 Firebase Hosting ホスティング Cloud Functions for Firebase APIサーバー・感染者数データのスクレイピング Cloud Firestore データベース Google Cloud Scheduler スクレイピングを定期実行するためのトリガー 個人開発のプロセスとして、企画編・開発編・リリース運用編としてそれぞれ書いていますので、よかったら参考にしていただければと思います。
具体的なコードはほとんど載せていないのですが、作成をするときに参考にしたサイトや公式サイトのリファレンス等のリンクは載せていますので、そちらを参考にしていただければと思います。
企画編
作ろうと思ったきっかけ
ニュースを見ていても、その日だけの新規の感染者数や累計での感染者数の報道が多く、今現在どの程度感染が拡大しているのかが分かりづらいと思い、視覚的に感染者数の推移を確認できるものがほしいと思って作りました。
※ 他にも感染者数の推移を確認できるサイトはいろいろとあります。以下のようなサイトもよければご覧ください。
- 厚生労働省 - 新型コロナウイルス感染症 国内事例
- 朝日新聞 - 新型コロナウイルス感染者数の推移
- 東洋経済ONLINE - 新型コロナウイルス国内感染の状況
- 都道府県別新型コロナウイルス感染者数マップ
主な機能
主な機能としては、地図で感染者数の推移を確認できる機能とグラフで確認できる機能があります。
地図表示機能
都道府県別に感染者数を円で表示しています。感染者数の数値は厚生労働省の報道発表資料から取得しています。本当は市区町村単位での表示にしたかったのですが、公開されているデータが必ずしも市区町村別になっていないため、都道府県単位で集計をして各都道府県の県庁所在地に円をプロットしています。感染が拡大しているのか縮小しているのかが視覚的にわかるように、日別にデータを表示することができます。また、画面右側にある「日付を自動で切替え」のボタンを押すことで自動で日付が切り替わるようになっています。
実際のサイトでもご覧いただければと思いますが、これを見ると、例えば、北海道は2月終わりから3月始めごろは感染者数が拡大しているものの、3月中旬ごろからは感染が収まってきていることがわかります。
グラフ表示機能
日ごとの全国の感染者数の累計をグラフで表示しています。
都道府県別でも見られるようにしたかったのですが、まだ間に合っておらず作成できていません。今後作成予定です。
こちらも実際のサイトでご覧いただければと思いますが、このグラフを見ると3月に入ってからも大きな拡大はなく、ほぼ横ばいで感染者数の増加を抑えられていることがわかります。
開発編
ここからがQiita的には本編の記事になります。
開発編として設計と実装に分けて説明をしていきます。
設計
個人での開発とはいえ、今後も機能拡張はしていきたいので、メンテナンスや機能追加のしやすいものを作りたいと思います。そのためにある程度の設計はしてから進めます。
画面設計と機能の洗い出し
まずは画面設計です。
画面設計を最初に行うのはユーザビリティとして問題なさそうかを確認するという目的ももちろんありますが、
- 必要な機能の洗い出し
- 必要なデータの洗い出し
という目的もあります。
ワイヤーフレームを設計するためのツールとして、今回はJUSTINMINDというものを使いました。JUSTINMINDはデスクトップアプリが提供されています。他のツールにはオンラインで使えるものもいろいろありますが、オフラインでも作業ができた方がストレスなくできるため、今回はオフラインでも使えるこのツールにしました。
作成をした画面を見ながら、機能に漏れがないかの確認やどのようなデータをどういった形式で保存するのかを検討していきました。
また、この段階でコンポーネントをどの単位で分割をするのかもある程度決めておきます。以下のようにしました。(コンポーネント名もこの段階で決めてしまいます。適当な名前で進めてしまうと、気に入らないとなったときにあとから変更をするのが面倒ですので。)
システム構成
次に作成をした画面と機能一覧を見ながらシステム構成を検討します。
バックエンド側には以下のような機能が必要になります。
APIサーバー: フロントエンド側からの呼び出しにより、感染者数のデータを返却します。
スクレイピングサーバー: 厚生労働省のホームページからスクレイピングにより感染者数のデータを取得します
データベース:スクレイピングにより取得したデータを保存します。
クライアント側はReactで作り、サーバー側はFirebaseで作っています。
管理画面は初期の検討段階では無かったのですが、後述する実現性の検証をした際に必要そうなことがわかったため、システム構成に入れました。
実現性検証
実装に入る前に、難易度が高そうなところや実現性がわからない機能についてはあらかじめ実現性の検証をしていきます。
今回のシステムで言えば、スクレイピングでのデータ取得ができるのかというのができるかがわからないところがありましたので、この部分について検証をしました。
感染者数のデータは厚生労働省ホームページのこちらの報道発表一覧のページから取得できます。この一覧から、例えばこちらの詳細ページへのリンクが貼られています。一覧ページと詳細ページをスクレイピングしていけばデータは取得できそうです。
もう少し検証を進めて、スクレイピングによりすべてのデータが正しく取得できるかを検証したところ、以下のような課題がありました。
- 感染者数の数値がすべて1つのdivタグに囲まれており(下記に貼った画面キャプチャ参照)、CSSセレクタを指定してのデータ取得ができない
- 都道府県単位でのデータになっていたり市単位でのデータになっている
- 「東 京 都:患者7例」というように、「東」と「京」の間にスペースが入っている場合があったり、「患者○例」となっていることがほとんどなのに、たまに「患者○名」となっている
- 最近(3月ごろ)のページのフォーマットと初期(1月から2月始めごろ)のページのフォーマットが異なる
1についてはスクレイピングで全文を取得したあとに、正規表現でテキストを抽出することにしました。
2〜4については正規表現やデータフォーマットの変換で対応できるものについてはできるだけ対応をしつつ、スクレイピングした結果を目視で確認することとしました。(データ補正用の管理画面が必要となったのはこのためです。)
実装
ここからは実際の実装作業について、順を追って説明をしていきます。
環境構築
最初にフロントエンド側、バックエンド側ともに環境を構築します。
- フロントエンドはReactで開発をします。Create React Appでプロジェクトを作成します。
- ホスティングの設定もします。Firebase Hostingのチュートリアルを参考にしてプロジェクトを初期化します。
- バックエンドはFirebaseのCloud Functionsで開発をします。Cloud Functionsのチュートリアルを参考にしてプロジェクトを初期化します。
ここまでできたら一旦デプロイをして、環境が正しく構築できているかを確認します。無事に環境が構築できてCreate React Appで作成した初期ページが表示されることが確認できました。
使用したツール・システム
ディレクトリと空のファイルの作成
次に大まかなディレクトリ構成を決めて、空のファイルだけを作成しておきます。
開発をする中で変更をしたり足したりしたものもありますが、最終的には以下のような構成となっています。
covid19-visualization ├── backend │ ├── firebase.json │ ├── firestore.rules │ ├── functions │ │ ├── constants/ # 定数(都道府県と都道府県コードの対応表など) │ │ ├── database/ # Firestore接続部分の機能 │ │ │ └── Firebase.js │ │ ├── index.js │ │ ├── models/ # モデル層 │ │ ├── package.json │ │ ├── routes/ # ルーティング層 │ │ └── utils/ # 汎用機能(主にスクレイピング関連) │ └── package.json └── frontend ├── build ├── firebase.json ├── package.json ├── public └── src ├── constants # 定数(都道府県ごとの座標値など) ├── index.js ├── state │ ├── modules │ │ ├── app-state # アプリの状態を扱う機能 │ │ │ ├── actions.js │ │ │ ├── index.js │ │ │ ├── operations.js │ │ │ ├── reducers.js │ │ │ ├── selectors.js │ │ │ └── types.js │ │ ├── covid19data/ # 画面に表示するデータを扱う機能 │ │ ├── covid19data-edit/ # 編集用のデータを扱う機能 │ │ └── index.js │ ├── store.js │ └── utils/ └── views ├── App.js ├── admin # 管理画面に表示するコンポーネント │ ├── Admin │ ├── Login │ └── Table ├── map # 地図関連のコンポーネント │ ├── GraphViewer # といいつつ、グラフも入っている。命名が良くなかった。 │ ├── Map │ ├── MapController │ ├── MapLayout │ ├── MapViewer │ └── Title ├── settings # 設定関連のコンポーネント │ ├── Settings │ ├── SettingsFooter │ ├── SettingsItem │ ├── SettingsItemAutoPlay │ ├── SettingsItemDisplayMode │ └── SettingsItemSelectedRegion └── utils # 汎用機能(日付のフォーマット変換等)フロントエンド側はRe-ducksの構成に従っています。発案者が書いた記事がありますので、詳しくはこちらを参考にしてください。
スクレイピングの実装
ここまで来て、ようやく具体的な機能の実装を始めます。
スクレイピング
スクレイピングは大まかには以下のようなロジックです。
- 厚生労働省の報道発表一覧のページからデータが記述された詳細ページのURLを取得する
- 詳細ページ(例えばこちらのページ)から感染者数のデータを取得する
- 取得したデータをFirestoreに保存する
Firestoreへの保存については、公式サイトのチュートリアルを参考にして実装しまいた。
実装をして、ローカル環境では問題なかったのですがデプロイをしたところ以下のようなエラーとなりました。
Error: memory limit exceeded. Function invocation was interrupted.どうやらメモリ不足のようです。
デフォルトで割り当てられているメモリや256MBになっているようですので、Firebase公式サイトのこちらのページを参考にして、メモリを1GBに増やしました。
スクレイピングの呼び出し
スクレイピングの呼び出しは、Firebaseの関数のスケジュール設定のページを参考にして実装します。
使用したツール・システム
- Puppeteer (Node.jsのスクレイピング用ライブラリ)
- Cloud Functions for Firebase (Google Cloud Schedulerを使用した定期実行)
- Cloud Firestore
APIの実装
Cloud FunctionsをHTTP経由で呼び出せるようにします。公式サイトのチュートリアルを参考にして実装します。Express アプリをそのままHTTP関数に渡すことができますので、今回はこの方法を採用しました。
使用したツール・システム
画面の作成
ここからはフロントエンド側の実装に入ります。
UIをゼロから作るのも時間がかかりそうだったので今回はUIライブラリを使いました。今回使用をしたのはMaterial-UIというライブラリです。Reactで簡単にマテリアルデザインのUIを作ることができます。
設計時にコンポーネントの分割方法は決めていますので、この内容に沿って実装をしていきました。
使用したツール・システム
データ取得機能の実装
次にAPI経由でデータを取得する部分を実装します。
特別なこともなく、淡々と実装をしていきます。
使用したツール・システム
地図表示の実装
ここまで来てようやく地図表示部分の実装になります。
地図表示機能は主に以下の2つになります。
- Google Mapを表示する機能
- Google Map上に感染者数の大きさを表す円をプロットする機能
Google Mapを表示する機能については、公式サイトの解説を参考にして進めていきます。Google Map APIをES6で使う方法についてはこちらの【JavaScript・ES6】Google Mapの埋め込みスニペットのページも参考にさせていただきました。
円をプロットする機能についてはこちらも公式サイトのVisualizing Dataにあるチュートリアルを参考にして進めました。
地図上に情報を表示するためのフォーマットとして、GeoJsonというものがあります。Google Map上に円を表示する際にも、このGeoJsonというフォーマットに変換をしています。
使用したツール・システム
グラフ表示の実装
データをグラフで表示する機能の実装です。Rechartsというライブラリを使用しています。
使用したツール・システム
管理画面の作成
ユーザーが使用する画面と同様にMaterial-UIを使用して実装しています。
またテーブル表示には、Material-UIを拡張して作られたmaterial-tableというライブラリを使用しています。
使用したツール・システム
リリース・運用編
デプロイ
実装が終わり、無事にテストも終わったところでデプロイをします。
Firebase HostingのデプロイもCloud Functionsのデプロイも以下のコマンドを打つだけで簡単にデプロイできます。
xxx$ firebase deploy
予算上限・アラートの設定
そんなに利用者数も多くないため、Firebaseの無料枠の範囲内で収まるとは思いますが、念のため予算の設定をしておきます。
Firebaseには1ヶ月間の使用料金が超過したときにアラートを知らせてくれる機能があります。Firebaseの使用量と制限のページを参考にして設定しました。
また、1日あたりの料金もアラート設定の説明ページと同じページにある1 日あたりの費用制限を設定するに書いてある方法で設定できるようです。
(これも設定をしたかったのですが、このページに書いてあるとおりに進めても設定をするページが現れず、設定できませんでした。。。Googleのサポートに電話をかけても1時間待ってもつながらず、結局設定できていません。。。どなたかやり方をご存知でしたらコメント欄などで教えていただけると嬉しいです。)
ということで、完成したのがこちらのページになります。ご質問、ご要望などございましたらコメント欄などでお気軽にお知らせください!
今後の予定
当初は以下のような機能も実装をしようと思っていたのですが、時間が足りずに残念ながらまだ実装できていません。今後実装をする予定です。
地域別に感染者数を表示する機能
表示期間を設定する機能
感染予防のためのお役立ち情報集
(他にもこんな機能があったらいいというのがあれば、ぜひコメントください!)
- 投稿日:2020-03-22T19:18:55+09:00
historyオブジェクトについて
課題
- historyオブジェクトを利用して、ページをリダイレクトしたいがreduxを利用すると上手く動かない。
- console.logを仕込んでみると、どうやらhistoryオブジェクト自体がpropsとして渡されていないようだった。
環境
- node v12.7.0
- express 4.16.1
- react 16.12.0
- npm 6.10.2
- macOS Mojave 10.14.4
解決方法
reduxとreact-router-domのみを利用するとhistoryが利用できないので、reduxとreact-routerの統合が必要となる。
上記を実現するためにConnected React Routerというライブラリをインストールする。
(react-router-reduxは現在、非推奨)
引用元/@nacam403さんの記事実装の流れ
- まず、npmで上記のライブラリを全てインストールします。
npm install --save redux npm install --save react-router-dom npm install --save connected-react-router npm install --save redux-thunk
- index.jsを実装します。
index.jsimport React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware } from 'redux' import { createBrowserHistory } from "history" import { routerMiddleware, ConnectedRouter } from 'connected-react-router' import thunk from "redux-thunk" import './index.css'; import App from './App'; import * as serviceWorker from './serviceWorker'; import rootReducers from './reducers' const history = createBrowserHistory() const store = createStore( rootReducers(history), applyMiddleware( thunk, routerMiddleware(history), ) ) ReactDOM.render(<App store={store} history={history} />, document.getElementById('root')); serviceWorker.unregister();
- App.jsを実装します。
AddFormでデータを登録して、CharacterListで一覧を表示する、というシンプルな構成です。
App.jsimport React, {Component} from 'react' import {Switch, Route} from 'react-router-dom' import { Provider } from "react-redux" import { ConnectedRouter } from 'connected-react-router' import AddForm from './crud_components/AddForm' import CharacterList from './crud_components/CharacterList' class App extends Component { render() { return ( <Provider store={this.props.store}> <ConnectedRouter history={this.props.history}> <Switch> <React.Fragment> <div className="App"> <div className="container"> <Route exact path="/addform" render={() => <AddForm store={this.props.store} history={this.props.history}/>}/> <Route exact path="/characterlist" render={() => <CharacterList store={this.props.store}/>}/> </div> </div> </React.Fragment> </Switch> </ConnectedRouter> </Provider> ); } } export default App;
- AddForm.jsを実装します。
データ登録時のメソッドのみ抜粋します。ここでリダイレクトしたいpathをhistoryにpushします。
AddForm.jsonSubmit(e){ e.preventDefault() // フォームsubmit時のデフォルトの動作を抑制 const newCharacter = { name: this.state.name, age: this.state.age } const store = this.props.store; character(newCharacter).then(res => { if (res) { this.props.history.push('/characterlist') store.dispatch(initializeForm()) // submit後はフォームを初期化 } }) }まとめ
これでhistoryが利用できるようになりました。
しかし、historyでリダイレクトする際にstoreのデータを引き継ぎできないという課題が残るため、引き続き調べていきます。
- 投稿日:2020-03-22T17:08:36+09:00
Dockerコンテナ起動時に、フロント側コンテンツを制御する一事例
概要
1つのDockerイメージとして提供する、Java+React構成のアプリケーションについて、コンテナ起動時にビルド済みのReactコードを制御する方法を、具体例をもとに考えていきます。
はじめに
Twelve-Factor Appの「III. 設定」で述べられているように、アプリケーションの設定は環境変数に格納し、コードからは分離することが望ましいとされています。
つまり、Dockerコンテナとして動作するアプリケーションであれば、develop用、staging用、production用と各環境ごとで別のDockerイメージを用意するべきではなく、イメージ自体は1つにして、各環境ごとの差分はコンテナ実行時に与える環境変数で吸収するべきです。しかし、コンテナ起動するアプリケーションに、環境変数を反映させることが容易でない場合も多々あります。
例えば、ビルド実施済みのフロント側コンテンツに対して、環境変数での制御を行う方法は、グーグルで検索を行ってもほとんどヒットしません。そこで、本記事では、どうすればビルド済みのフロント側コンテンツに対して環境変数を反映させることができるかについて、Java+React構成のアプリケーションを一事例として説明していきます。
環境変数を反映させる戦略
コンテナ起動時にフロント側コンテンツに環境変数を反映させるために、Javaの起動時に読まれる環境変数を、HTMLのmetaタグ経由で、フロント資材に渡す、という戦略をとりました。
また、HTMLがレンダリングされるタイミングで、metaタグの内容が環境変数の値に書き換わるようにするために、Thymeleafを利用しました。構成
ここから説明で使用するコードは、以下のような
thymeleaf/my-app
配下でフロント側のコードを管理し、thymeleaf/thymeleaf
配下でバック側のコードを管理する構成です。└── thymeleaf ├── Dockerfile ├── my-app │ ├── README.md │ ├── package.json │ ├── public │ │ └── .... │ ├── src │ │ └── .... │ └── yarn.lock └── thymeleaf ├── pom.xml └── src ├── main │ ├── java │ │ └── .... │ └── resources │ └── .... └── test └── java └── ....コードの全量は以下に配置しています。
https://github.com/nannany/thymeleafここからどのようにDockerイメージを作成していくか示すために、まずDockerfileから説明していきます。
Dockerfile
Dockerfileは全体は以下のようです。
FROM node:13 AS front-build WORKDIR /work1 ADD my-app . RUN npx yarn && npx yarn build FROM maven:3.6 AS back-build WORKDIR /work2 ADD thymeleaf . RUN mkdir -p /work2/src/main/resources/static COPY --from=front-build /work1/build/ /work2/src/main/resources/static/ RUN mkdir -p /work2/src/main/resources/templates && \ sed -e "s!<meta name=\"from-environment\" content=\"\"/>!<meta name=\"from-environment\" th:content=\${@environment.getProperty('thymeleaf.test')}>!" \ /work2/src/main/resources/static/index.html > /work2/src/main/resources/templates/index.html && \ rm /work2/src/main/resources/static/index.html && \ mvn package FROM adoptopenjdk/openjdk11:jdk-11.0.6_10-alpine COPY --from=back-build /work2/target/thymeleaf-0.0.1-SNAPSHOT.jar . ENTRYPOINT ["java", "-jar", "thymeleaf-0.0.1-SNAPSHOT.jar"]このDockerfileは、以下の3ブロックに分かれています。
- Reactアプリケーションのビルド
- Javaアプリケーションのビルド
- 実行するイメージの作成
Reactアプリケーションのビルド
最初のブロックでは、Reactアプリケーションのビルドを行っています。
FROM node:13 AS front-build WORKDIR /work1 ADD my-app . RUN npx yarn && npx yarn build特記することはありません。
Javaアプリケーションのビルド
次のブロックでは、Javaアプリケーションのビルドを行っています。
FROM maven:3.6 AS back-build WORKDIR /work2 ADD thymeleaf . RUN mkdir -p /work2/src/main/resources/static COPY --from=front-build /work1/build/ /work2/src/main/resources/static/ RUN mkdir -p /work2/src/main/resources/templates && \ sed -e "s!<meta name=\"from-environment\" content=\"\"/>!<meta name=\"from-environment\" th:content=\${@environment.getProperty('thymeleaf.test')}>!" \ /work2/src/main/resources/static/index.html > /work2/src/main/resources/templates/index.html && \ rm /work2/src/main/resources/static/index.html && \ mvn packageReactアプリケーションのビルド成果物を
/work2/src/main/resources/static/
に配置して、/work2/src/main/resources/static/index.html
の<meta name="from-environment" content=""/>という記述を、
<meta name="from-environment" th:content=${@environment.getProperty('thymeleaf.test')}>に書き換えて、Thymeleafにレンダリングさせるため
/work2/src/main/resources/templates/index.html
に配置しています。なぜ元のindex.htmlに置換後のように書いていないかというと、
yarn build
時に、Thymeleaf記法で書いている部分のパースに失敗するからです。(おそらくwebpackが吐いているエラー)そのあとで
mvn package
をたたいてjarファイルを生成しています。実行イメージの作成
最後のブロックで、コンテナ起動時に
java -jar 作ったjarファイル
のプロセスを立ち上げるイメージを作成しています。FROM adoptopenjdk/openjdk11:jdk-11.0.6_10-alpine COPY --from=back-build /work2/target/thymeleaf-0.0.1-SNAPSHOT.jar . ENTRYPOINT ["java", "-jar", "thymeleaf-0.0.1-SNAPSHOT.jar"]特記することはありません。
React
index.html
のhead内に、Dockerfile内で置換していたmetaタグを記述します。<head> .... <meta name="from-environment" content=""/> .... </head>Dockerイメージに固める前の開発段階では、
.env
(もしくは.env.local
など)を使って環境変数を指定します。
.env
の記述は以下のようです。REACT_APP_LOCAL_SUBSTITUTE=localmetaタグや
.env
から値を取得する部分は、以下のようです。getMetaData() { const fromEnvironment = document.getElementsByName( "from-environment" )[0].content; return fromEnvironment === '' ? process.env.REACT_APP_LOCAL_SUBSTITUTE : fromEnvironment; }
from-environment
というnameのmetaタグのcontentが空文字であれば、.env
内の値を使い、それ以外の場合はmetaタグのcontentを使います。
このようにした理由としては、npm run start
で、Reactの動作のみ確かめたい場合にも、コードを書き換えたりせずに対応できるようにしたかったためです。Java
バックエンド側に関して特記することはないです。
記述も少ないです。
https://github.com/nannany/thymeleaf/tree/master/thymeleaf動かしてみる
上記のDockerfileをもとに作成したイメージについて、環境変数を与えてコンテナ起動してみます。(
test:dev
という名前でイメージを作りました)
thymeleaf_test
という環境変数に応じて、フロント側コンテンツが変更されるようになっています。まず、何も環境変数を与えないで動かしてみると、以下のように表示されます。
docker run --rm -p 80:8080 test:dev
次に、
thymeleaf_test
にtest
という値を入れて、コンテナ起動してみます。
docker run --rm -p 80:8080 test:dev
反映が確認できました。
おわりに
そもそもこのような構成で、1つのDockerイメージにしてデプロイしようと考えるのが間違っているという説はあります。
フロントとバックは別のイメージにして、フロント側にNext.jsなどを導入すれば容易に環境変数で制御できるといった記事を見たので、きっとそういう構成にするのがいいのでしょう。参考
https://itnext.io/frontend-dockerized-build-artifacts-with-nextjs-9463f3da3362
https://qiita.com/shibukawa/items/6a3b4d4b0cbd13041e53
https://12factor.net/ja/config
- 投稿日:2020-03-22T17:01:35+09:00
React FCでFileを『Content-type: multipart/form-data』でPOSTする方法(axios)
案件でPOSTする際、
『Content-type: form-data』で送信する機会があったので、まとめます。
ボタン部分はマテリアルUIを使っています、初見の方は細かく気にしなくても大丈夫です環境
react 16.12.0
typescript 3.7.3
material-ui/core 4.8.0(Buttonに使用)したいこと
inputで選択したファイルをstateにセット、セットしたファイルを POSTする
全体
const IconUpload: FC = () => { const [userIconFormData, setUserIconFormData] = useState<File>() const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) } const handleSubmitProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) } return ( <form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form> ) } export default IconUpload切り分けて解説
form周り、ボタン部分
<form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form>input
- type:fileにすることによってアップロードが可能になる
- accept:アップロード画面で、ここに指定された拡張子のファイルのみ選択可能になる
- onChange:通常のinputと異なり、valueにはFileを入れることはできない、なので別の場所に取得したFileを保持するfunctionを呼び出す
Button
- onClick:onChangeでセットしたstateをaxiosでPOSTするfunctionを呼び出す
ファイルを取得するfunction
const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) }
if (!e.target.files) return
TypeScriptでは、undefindになる可能性のある値に関してはエラーがでるので先に無い場合はreturnすることを明示的にしている
const iconFile:File = e.target.files[0]
これがファイル本体。
filesは複数選択可能の場合を備え、配列になっていて、[0]としてあげないと取得できない。
型はFile。
setUserIconFormData(iconFile)
stateにセットちなみに、
e.target.files[0]
をURLにしてimgに入れたいとなるとconst blobUrl = URL.createObjectURL(iconFile)
blob:http://パス
に変換され、URLをしてとして扱うことができます。ファイルをaxiosでPOSTするfunction
const createProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) }ようやくAPI送信部分
content-type: multipart/form-data
で送信する際、気をつけること2つ1.
FormData
という形式で送ってあげなければいけない。
const iconPram = new FormData()
FormDataオブジェクトを作成
型はそのまんまで、FormData
iconPram.append('user[icon]', userIconFormData)
作成したFormDataオブジェクトにパラメーターを追加
append(キー, 送信したいファイル)2. headersに
'content-type': 'multipart/form-data'
を指定基本API通信する時はjsonが多いかと思いますが、jsonでは正しく送れず、空になるので注意
_axios.create
を使って、Classでまとめている場合にも、className.defaults.headers = { 'Content-Type': 'multipart/form-data' }で、上書きしてあげましょう
送信内容
こんな感じです。
確認ポイント
- RequestHeadersの
content-type
がmultipart/form-data
になっている- FormDataのvalueが
(binary)
になっているまとめ
これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。
- Fileのまま送信(multipart/form-data)
- Base64にエンコードして送信(application/json)
エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね
- 投稿日:2020-03-22T17:01:35+09:00
React FCで画像ファイルを『Content-type: multipart/form-data』でPOSTする方法(axios)
案件でPOSTする際、
『Content-type: form-data』で送信する機会があったので、まとめます。
ボタン部分はマテリアルUIを使っています、初見の方は細かく気にしなくても大丈夫です環境
react 16.12.0
typescript 3.7.3
material-ui/core 4.8.0(Buttonに使用)したいこと
inputで選択したファイルをstateにセット、セットしたファイルを POSTする
全体
const IconUpload: FC = () => { const [userIconFormData, setUserIconFormData] = useState<File>() const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) } const handleSubmitProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) } return ( <form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form> ) } export default IconUpload切り分けて解説
form周り、ボタン部分
<form> <p>アイコンアップロード</p> <input type="file" accept="image/*,.png,.jpg,.jpeg,.gif" onChange={(e: ChangeEvent<HTMLInputElement>) => handleSetImage(e)} /> <Button text="変更する" variant="contained" color="primary" type="button" onClick={handleSubmitProfileIcon} disabled={userIconPreview === undefined} /> </form>input
- type:fileにすることによってアップロードが可能になる
- accept:アップロード画面で、ここに指定された拡張子のファイルのみ選択可能になる
- onChange:通常のinputと異なり、valueにはFileを入れることはできない、なので別の場所に取得したFileを保持するfunctionを呼び出す
Button
- onClick:onChangeでセットしたstateをaxiosでPOSTするfunctionを呼び出す
ファイルを取得するfunction
const handleSetImage = (e: ChangeEvent<HTMLInputElement>) => { if (!e.target.files) return const iconFile:File = e.target.files[0] setUserIconFormData(iconFile) }
if (!e.target.files) return
TypeScriptでは、undefindになる可能性のある値に関してはエラーがでるので先に無い場合はreturnすることを明示的にしている
const iconFile:File = e.target.files[0]
これがファイル本体。
filesは複数選択可能の場合を備え、配列になっていて、[0]としてあげないと取得できない。
型はFile。
setUserIconFormData(iconFile)
stateにセットちなみに、
e.target.files[0]
をURLにしてimgに入れたいとなるとconst blobUrl = URL.createObjectURL(iconFile)
blob:http://パス
に変換され、URLをしてとして扱うことができます。ファイルをaxiosでPOSTするfunction
const createProfileIcon = () => { const iconPram = new FormData() if (!userIconFormData) return iconPram.append('user[icon]', userIconFormData) axios .post( 'https://api/update', iconPram, { headers: { 'content-type': 'multipart/form-data', }, } ) }ようやくAPI送信部分
content-type: multipart/form-data
で送信する際、気をつけること2つ1.
FormData
という形式で送ってあげなければいけない。
const iconPram = new FormData()
FormDataオブジェクトを作成
型はそのまんまで、FormData
iconPram.append('user[icon]', userIconFormData)
作成したFormDataオブジェクトにパラメーターを追加
append(キー, 送信したいファイル)2. headersに
'content-type': 'multipart/form-data'
を指定基本API通信する時はjsonが多いかと思いますが、jsonでは正しく送れず、空になるので注意
_axios.create
を使って、Classでまとめている場合にも、className.defaults.headers = { 'Content-Type': 'multipart/form-data' }で、上書きしてあげましょう
送信内容
こんな感じです。
確認ポイント
- RequestHeadersの
content-type
がmultipart/form-data
になっている- FormDataのvalueが
(binary)
になっているまとめ
これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。
- Fileのまま送信(multipart/form-data)
- Base64にエンコードして送信(application/json)
エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね
- 投稿日:2020-03-22T15:30:56+09:00
ReactとFlaskがGCPへ
GCP使ってみたいドリブンでYelp API使ったテストサイトを作る
GCPでAPIテストサーバー作りたかった
言語: React, Flask
これにどこからかのapi付けて描画すればいいかなっと思ったけど味気ない (Learn Udonのリンク先は、うどんについて語るwiki)ので
Yelp API使ってこんな感じにしたYelp api
日本の食べ物系サイトはみんな知ってるし、海外のをちょっと見てるのもよいかなとyelp apiを使った
api keyの取得は簡単。yelp developersに入ってgoogleかfacebookのアカウントで登録すればすぐ発行してくれる
Flask
ディレクトリ構成
app ├── config.py └── run.py# run.py from flask import Flask, request from flask_cors import CORS import requests import config app = Flask(__name__) CORS(app) URL = "https://api.yelp.com/v3/businesses/search" headers = {'Authorization': f"Bearer {config.API_KEY}"} @app.route('/') def ramen_yelp(): payload = { "term": request.args.get("term", "ramen"), "location": request.args.get("location", "ny") } response = requests.request("GET", URL, headers=headers, params=payload) return response.json() if __name__ == "__main__": app.run(host="0.0.0.0", port=config.PORT, debug=config.DEBUG_MODE)root('/')の場合はデフォルトでニューヨークのラーメンランキングにしている
configにはflask, gunicornの設定
大事なことはenv変数に入れておく# config.py from os import environ import multiprocessing PORT = int(environ.get("PORT", 8080)) DEBUG_MODE = int(environ.get("DEBUG_MODE", 1)) API_KEY = environ.get("API_KEY") bind = ":" + str(PORT) workers = multiprocessing.cpu_count() * 2 + 1 threads = multiprocessing.cpu_count() * 2ブラウザ(firefox or chrome)から0.0.0.0:8080に入ると
結果をjsonで吐き出すようになった
GCR (Google Container Registry)に登録
flaskをdocker化してGCRにup
Dockerfileはこんな感じでサクッと
# pull official base image FROM python:3.8.1-alpine # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # install dependencies RUN pip install --upgrade pip COPY requirements.txt requirements.txt RUN pip install -r requirements.txt # set work directory WORKDIR /usr/src/app # copy project COPY ./app . ENV PORT 80 ENV API_KEY YOUR_API_KEY CMD ["gunicorn", "run:app", "--config=config.py"]# requirements.txt Flask==1.1.1 gunicorn==20.0.4 requests==2.23.0 Flask-Cors==3.0.8*本番はAPI_KEYをdotenv使ってconfig.pyにまとめる
- gcloudをローカルにインストール(doc)
- GCRの課金を有効にする
次に下記のようにコマンド打てば簡単に登録できる
#!/bin/sh docker build -t flask-test-gcr docker tag flask-test-gcr [HOSTNAME]/[PROJECT-ID]/flask-test-gcr docker push [HOSTNAME]/[PROJECT-ID]/flask-test-gcr gcloud container images list-tags [HOSTNAME]/[PROJECT-ID]/flask-test-gcrGCE(Google Compute Engine)
インスタンスをgcpコンソール画面から作成
docker使ってコンテナからすぐデプロイしたい人は下記のチェックとコンテナイメージを忘れずに
GCE VMインスタンス画面の外部IPを http:[IP] にすれば同じ結果が出るようになった!
SSL証明書がない場合は外部IPを https じゃなくて http にしないとダメだよ
このIPアドレスをReactから呼び出すのでメモしておく
FrontはReact
create-react-appというステキなボイラープレートで簡単に
# app-nameは好きな名前で npx create-react-app app-name
App.jsはほとんどテンプレで、画像やリンク、Yelp.jsのComponentを呼んでいる違いぐらい
// App.js import React from 'react'; import logo from './logo.svg'; import './App.css'; import Yelp from './Yelp' //←これ function App() { return ( <div className="main"> <Yelp /> <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="Udon logo" /> <a className="App-link" href="https://ja.wikipedia.org/wiki/%E3%81%86%E3%81%A9%E3%82%93" target="_blank" rel="noopener noreferrer" > Learn Udon </a> </header> </div> </div> ); } export default App;// Yelp.js import React, { useEffect, useState } from 'react' import './Yelp.css' const Yelp = () => { const [error, setError] = useState(null) const [yelpData, setYelpData] = useState({}) useEffect(() => { const API_URL = 'http://さっきメモした外部IP/' fetch(API_URL) .then(res => res.json()) .then( result => { setYelpData(result) }) .catch(e => { setError(e) }) }, []) if (error) { return ( <div className="Yelp">error</div> ) } else if (yelpData.businesses) { const images = yelpData.businesses.map(item => { return ( < a key={item.id} href={item.url} target="_blank" rel="noopener noreferrer" > < img height="150" width="150" crop="fill" radius="20" src={item.image_url} alt="ramen" /> </a > ) }) return ( <div className="Yelp"> {images} </div> ) } else { return ( <div className="Yelp"></div> ) } } export default Yelpあとはローカルで起動すればラーメン画像が出てくる
# 起動 yarn start
好きな画像をクリックするとYelpのリンク先にとんで詳細が見れる
- ニューヨークのラーメン屋さんはメニューが豊富。ラーメン一筋でやるのは文化的に合わないのかな
- うどんのSVGがグルグル回ってるのがうっとうしい、良いかは賛否ある
参考
- 投稿日:2020-03-22T15:30:56+09:00
ReactとFlaskがGCPへ。
GCP使ってみたいドリブンでYelp API使ったテストサイトを作る
GCPでAPIテストサーバー作りたかった
言語: React, Flask
これにどこからかのapi付けて描画すればいいかなっと思ったけど味気ない (Learn Udonのリンク先は、うどんについて語るwiki)ので
Yelp API使ってこんな感じにしたYelp api
日本の食べ物系サイトはみんな知ってるし、海外のをちょっと見てるのもよいかなとyelp apiを使った
api keyの取得は簡単。yelp developersに入ってgoogleかfacebookのアカウントで登録すればすぐ発行してくれる
Flask
ディレクトリ構成
app ├── config.py └── run.py# run.py from flask import Flask, request from flask_cors import CORS import requests import config app = Flask(__name__) CORS(app) URL = "https://api.yelp.com/v3/businesses/search" headers = {'Authorization': f"Bearer {config.API_KEY}"} @app.route('/') def ramen_yelp(): payload = { "term": request.args.get("term", "ramen"), "location": request.args.get("location", "ny") } response = requests.request("GET", URL, headers=headers, params=payload) return response.json() if __name__ == "__main__": app.run(host="0.0.0.0", port=config.PORT, debug=config.DEBUG_MODE)root('/')の場合はデフォルトでニューヨークのラーメンランキングにしている
configにはflask, gunicornの設定
大事なことはenv変数に入れておく# config.py from os import environ import multiprocessing PORT = int(environ.get("PORT", 8080)) DEBUG_MODE = int(environ.get("DEBUG_MODE", 1)) API_KEY = environ.get("API_KEY") bind = ":" + str(PORT) workers = multiprocessing.cpu_count() * 2 + 1 threads = multiprocessing.cpu_count() * 2ブラウザ(firefox or chrome)から0.0.0.0:8080に入ると
結果をjsonで吐き出すようになった
GCR (Google Container Registry)に登録
flaskをdocker化してGCRにup
Dockerfileはこんな感じでサクッと
# pull official base image FROM python:3.8.1-alpine # set environment variables ENV PYTHONDONTWRITEBYTECODE 1 ENV PYTHONUNBUFFERED 1 # install dependencies RUN pip install --upgrade pip COPY requirements.txt requirements.txt RUN pip install -r requirements.txt # set work directory WORKDIR /usr/src/app # copy project COPY ./app . ENV PORT 80 ENV API_KEY YOUR_API_KEY CMD ["gunicorn", "run:app", "--config=config.py"]# requirements.txt Flask==1.1.1 gunicorn==20.0.4 requests==2.23.0 Flask-Cors==3.0.8*本番はAPI_KEYをdotenv使ってconfig.pyにまとめる
- gcloudをローカルにインストール(doc)
- GCRの課金を有効にする
次に下記のようにコマンド打てば簡単に登録できる
#!/bin/sh docker build -t flask-test-gcr docker tag flask-test-gcr [HOSTNAME]/[PROJECT-ID]/flask-test-gcr docker push [HOSTNAME]/[PROJECT-ID]/flask-test-gcr gcloud container images list-tags [HOSTNAME]/[PROJECT-ID]/flask-test-gcrGCE(Google Compute Engine)
インスタンスをgcpコンソール画面から作成
docker使ってコンテナからすぐデプロイしたい人は下記のチェックとコンテナイメージを忘れずに
GCE VMインスタンス画面の外部IPを http:[IP] にすれば同じ結果が出るようになった!
SSL証明書がない場合は外部IPを https じゃなくて http にしないとダメだよ
このIPアドレスをReactから呼び出すのでメモしておく
FrontはReact
create-react-appというステキなボイラープレートで簡単に
# app-nameは好きな名前で npx create-react-app app-name
App.jsはほとんどテンプレで、画像やリンク、Yelp.jsのComponentを呼んでいる違いぐらい
// App.js import React from 'react'; import logo from './logo.svg'; import './App.css'; import Yelp from './Yelp' //←これ function App() { return ( <div className="main"> <Yelp /> <div className="App"> <header className="App-header"> <img src={logo} className="App-logo" alt="Udon logo" /> <a className="App-link" href="https://ja.wikipedia.org/wiki/%E3%81%86%E3%81%A9%E3%82%93" target="_blank" rel="noopener noreferrer" > Learn Udon </a> </header> </div> </div> ); } export default App;// Yelp.js import React, { useEffect, useState } from 'react' import './Yelp.css' const Yelp = () => { const [error, setError] = useState(null) const [yelpData, setYelpData] = useState({}) useEffect(() => { const API_URL = 'http://さっきメモした外部IP/' fetch(API_URL) .then(res => res.json()) .then( result => { setYelpData(result) }) .catch(e => { setError(e) }) }, []) if (error) { return ( <div className="Yelp">error</div> ) } else if (yelpData.businesses) { const images = yelpData.businesses.map(item => { return ( < a key={item.id} href={item.url} target="_blank" rel="noopener noreferrer" > < img height="150" width="150" crop="fill" radius="20" src={item.image_url} alt="ramen" /> </a > ) }) return ( <div className="Yelp"> {images} </div> ) } else { return ( <div className="Yelp"></div> ) } } export default Yelpあとはローカルで起動すればラーメン画像が出てくる
# 起動 yarn start
好きな画像をクリックするとYelpのリンク先にとんで詳細が見れる
うどんのSVGがグルグル回ってるのがうっとうしい、良いかは賛否ある
参考
- 投稿日:2020-03-22T13:14:35+09:00
【React】GithubでSPAサイトを公開するまで
経緯
JavaScriptの学習を進めていく中で、React Routerを使用したSPAサイトの作成を学び、そのアウトプットとして自身のポートフォリオがてら、サーバー代も痛いのでGithubで無料で公開可能とのことなので、取り組んでみました。
今回はSPAサイトをGithubで公開するにあたっての経験を元に記して行ければと思います。
参考サイト
・https://create-react-app.dev/docs/deployment/#github-pages-https-pagesgithubcom条件
既にnode.js,React.jsの環境構築とGithubの登録ができていることを前提に進めていきます。
node.js公式サイト
React.js環境構築記事
ディレクトリ内構造(node_modules, public,src, README.md, package.json, package-lock.json)手順
1.Githubでリポジトリの作成
2.package.jsonの編集
3.Github Pagesへの公開1.Githubでレポジトリの作成
Githubにログインして、マイページからリポジトリに移動して、新規で作成するのでNewボタンをクリックし、説明に沿って好きなリポジトリ名で作成します。
2.package.jsonの編集
"nameプロパティ"の下に"homepageプロパティ"を作成してURLを記入する。
package.json{ "name": "repository", "homepage": "https://komosyu4649.github.io/repository", "version": "0.1.0", "private": true, "dependencies": { "gh-pages": "^2.1.1", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "^5.1.2", "react-scripts": "3.4.1" }, "scripts": { "predeploy": "npm run build", "deploy": "gh-pages -d build", "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" },・"homepageプロパティ"のURLを以下の要領で記入します。
homepageプロパティ"homepage": "https://自分のGithubアカウント名.github.io/リポジトリ名",・"gh-pagesプロパティ"をインストールします。
ーターミナル内で"npm install gh-pages"のコマンドを入力すると、"gh-pageプロパティ"がインストールされます。gh-pageプロパティ"gh-pages": "^2.1.1",・scriptに"predeployプロパティ"、"deployプロパティ"の順で追加します。
scripts"predeploy": "npm run build", "deploy": "gh-pages -d build",3.Github Pagesへの公開
ここまでで準備が整いました。
最後に、ターミナル内に"npm run deploy"のコマンドを入力し、GithubのURLが出力されれば成功です。
終わったら、Githubに戻りリポジトリを確認し、"gh-pages"ブランチができたことを確認しましょう。
無事表示されていれば問題ありません。
Githubにて無料でサイトを公開することができるようになるはずです^^
- 投稿日:2020-03-22T13:08:13+09:00
Next.js(React)でのCORSエラー解決(Next.js + Golang)
Next.jsでのCORSエラー解決(Next.js + Golang)
React + GoでSPAを作りたく、ReactフレームワークのNext.jsを使ってみた。
SPAの最初で躓きがちなCORSエラーについて、案の定ハマったのでメモ。(Angularでの開発の際もだいぶハマった思い出)CORSエラーが出る
今回のようにフロントエンド、バックエンドでサーバを分けて開発する際に、そのままだとCORS(Cross-Origin Resource Sharing)についてのエラーが発生する。
これはブラウザの機能として、クロスドメイン(ドメインの違うサーバへの)アクセスが制限されているため起きます。
例えば、フロントhttp://localhost:3000
, バックhttp://localhost:5000
とした時、フロントからバックのapiを叩こうとするとcorsエラーとなります。(ドメイン違うので)正常にapi叩くにはこれを解決する必要があります。
Next.jsではカスタムサーバを利用し、プロキシする
Next.jsでカスタムサーバを使用するため、 プロジェクトルートに
server.js
を作成します。また、
http-proxy-middleware
というパッケージを使い、任意のリクエストをプロキシします。server.jsconst express = require('express'); const next = require('next'); const { createProxyMiddleware } = require('http-proxy-middleware'); const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV !== 'production' const API_URL = process.env.API_URL || 'http://localhost:5010' const app = next({ dev }) const handle = app.getRequestHandler() app.prepare().then(() => { const server = express(); server.use( '/api', createProxyMiddleware({ target: API_URL, pathRewrite: { "^/api": "" }, changeOrigin: true }) ); server.all('*', (req, res) => { return handle(req, res) }); server.listen(port, err => { if (err) throw err console.log(`> Ready on http://localhost:${port}`) }); });カスタムサーバを利用してサーバ起動するので
package.json
を修正します。package.json... "scripts": { "dev": "node server.js", "build": "next build", "start": "NODE_ENV=production node server.js" }, ...これで
yarn dev
でカスタムサーバで立ち上がります。リクエストがプロキシされる
上記により、
/api
へのリクエストはhttp://localhsot:5010
にプロキシされます。
例えば/api/auth
へリクエストした場合、http://localhost:5010/auth
にプロキシされます。
pathRewrite
の記述を消せばhttp://localhost:5010/api/auth
にアクセスします。まとめ
Next.jsのcorsエラーの解決は、カスタムサーバと
http-proxy-middleware
パッケージの利用で解決できます。また忘れてハマりそうなのでメモしておいた。
参考
- 投稿日:2020-03-22T10:38:17+09:00
背景の見えるポップアップスクリーンの作り方
背景が見えるモーダル画面って個人的には好きです。
今回はその作成方法を紹介します。
使用ライブラリ
@react-navigation/stack
@react-navigation/native
react-native-safe-area-context
react-native-linear-gradient
styled-components
透過するモーダルページ(外枠)の作成
const TransparentModalPage: React.FunctionComponent = ({ children }) => { const navigation = useNavigation(); return ( <LinearGradient colors={['rgba(0,0,0,0)', 'rgba(0,0,0,0.9)']} style={{ flex: 1 }} > <TouchableWithoutFeedback style={{ flex: 1, alignSelf: 'stretch' }} onPress={() => navigation.goBack()} > <View style={{ flex: 1 }} /> </TouchableWithoutFeedback> <View style={{ justifyContent: 'center' }}>{children}</View> </LinearGradient> ); };
ToucableWithoutFeedback
の部分で、子要素のモーダルコンテンツ(children
)以外をタップした場合に、ページを閉じるように設定しています。モーダルのコンテンツを作成
const RoundView = styled(View)` border-top-left-radius: 10px; border-top-right-radius: 10px; background-color: 'rgba(255,255,255,1)'; `; export const ModalCard: React.FunctionComponent = ({ children }) => { return ( <RoundView> <View style={{ paddingVertical: 16 }}> <View style={{ alignSelf: 'center', width: 50, borderBottomWidth: 3, borderBottomColor: 'rgba(0,0,0,1)', borderRadius: 8, }} /> </View> <View style={{ paddingHorizontal: 32, paddingBottom: 32 }}> {children} </View> {/* To avoid a typescript error. */} <SafeAreaView> <></> </SafeAreaView> </RoundView> ); };
SafeAreaView
は、モーダル部分が下に詰めすぎないように制御しています。画面コンポーネントの作成
const ModalScreen = () => ( <TransparentModalPage> <ModalCard> <View style={{ height: 50, alignItems: 'center', justifyContent: 'center' }} > <Text>Modal Area</Text> </View> </ModalCard> </TransparentModalPage> );作成した
TransparentModalPage
,ModalCard
に子要素を設定するだけ完了です。ナビゲーター・スクリーンの作成
const Stack = createStackNavigator(); const StackNavigator = () => { return ( <Stack.Navigator mode="modal" headerMode="screen"> ... <Stack.Screen name="ModalScreen" component={ModalScreen} options={{ headerShown: false, cardStyle: { backgroundColor: 'transparent', opacity: 1 }, }} /> </Stack.Navigator> ); };
ModalScreen
の画面オプションは、cardStyle
で背景を透明設定にします。
この設定がないと、モーダル画面にグラデーションはかかりますが、背景が表示されません。以上で設定は完了です。
おわりに
ここでは紹介しませんでしたが、 Animation や GestureHandler を用いると、より「アプリらしい」挙動をさせることができます。
組み合わせ次第で、いろんなモーダル画面が作れるので、ぜひ試してみてください。
- 投稿日:2020-03-22T10:02:09+09:00
Reactコーディングを爆速でぶち上げる方法
前置き
Reactコーディングをする時に、便利だなと感じる機能、ツールをまとめました。
コーディングスピードを爆速でぶち上げたい方向けです。(言いたいだけ)仕様ツール
- Visula Stadio Code
- Simple React Snippets(拡張機能)
①VS Codeのショートカット
Reactのファイルを開いたら...
command
+K
を押してからM
これめちゃ便利です。Reactのファイルは、デフォルトだとJavaScriptのファイルと認識されてしまいますので、Reactで使いたいSnippetsやEmmetが使えなかったりします。
②Simple React Snippets(拡張機能)
Snippet Renders imr Import React cc Class Component ccc Class Component with Constructor クラスコンポーネントなんて、打つと長過ぎるので100億秒くらい節約できます。
タブストップもいい感じに設定されているので、上手に使いこなしましょう。③VS CodeのEmmet
JSXを記述する時
.container
→<div className="container"></div>
classNameって長すぎやねん...
HTMLのコーディングでも使えますよーまとめ
新しいコンポーネントを作った時とかに、是非使ってみてください!
おすすめの拡張機能、コーディング方法あればコメントいただけると大変喜びます。
- 投稿日:2020-03-22T08:58:02+09:00
Docker で zeit nowのデプロイ環境とNext.jsの開発環境を作ったメモ
概要
- zeit nowのデフォルトのnext.jsを修正
- 2020 年初頭における Next.js をベースとしたフロントエンドの環境構築 を参考にTypeScriptの環境構築
- dockerを利用して環境を作っている
- storybookはできてない
now
zeit now へのデプロイを確認
- 事前に、zeit へ github 連携を行っている。
- コンテナに bin/bash.sh でログイン。以下のコマンドを実行してデプロイしてみる。
now login # メールを確認する。メールは https://zeit.co/account の画面から確認可能 now init # nextjsのデフォルトプロジェクトを作成 cd nextjs # initで作ったディレクトリに移動する now # デプロイを行う。とりあえず、初期の状態でデプロイできることを確認
- このとき、/root/.local/share/now/ディレクトリに、config.json と auth.json が出来ているので、コンテナ外に保存しておく。認証用の情報
- nextjs フォルダに最初のファイルが出来ているので、コンテナ外に保存しておく。
- コンテナ作成時に、/root/.local/share/now/ディレクトリに先ほど保存したファイルを取り込むように Dockerfile を修正。
- 毎回、now login しなくてもよいようになる。
フォルダ構造変更
- src ファイル整理
now dev
でローカルで確認できるように docker-compose.yml を修正react ts
環境構築
2020 年初頭における Next.js をベースとしたフロントエンドの環境構築 を参考に環境を作っていく。
参考
now
dist now
設定なしでデプロイできる Zeit Now
Now でクラウドの複雑さから解放されよう、今すぐに
example
now deploynext
- 投稿日:2020-03-22T07:37:24+09:00
React + Typescript + MDC Web の環境を新規で作る
React + Typescript + Material-Design-Component
プログラミングは、たいてい環境構築が最も大変です。
React単体の環境構築はできても、TypescriptやSass、Material-Design-Component(MDC)を組み込んでゆくと、得体の知れないエラーと格闘する羽目になったりします。
本記事では、なるべく困らずに開発をスタートできるよう、手順を書いておきます。
本記事は、2020年3月22日時点で動作確認しています。参考記事:MDC-webのチュートリアル
MDC-webの公式チュートリアルがあります(廃止されるようですが)。
これを参考に環境を構築します。1.React + Typescriptを導入する
最初に、create-react-appを使用して、React+Typescriptを一気に導入します。
my-appの部分はプロジェクト名なので、好きに変えて構いません。ターミナルnpx create-react-app my-app --template typescriptプロジェクトができたら、その中に移動してビルドします。
ターミナルcd my-app npm run build最初のビルドは通ります。
ターミナルnpm startを実行すると、Reactの初期画面が表示されます。
2.MDCを導入する
MDC-Reactを導入します。
ターミナルnpm install @material/react-buttonで、コンポーネントをダウンロードします。
3.Sassを導入する
ターミナルで以下を実行し、環境変数を追加します。
ターミナル// macの場合 export SASS_PATH=./node_modules // windowsの場合 SET SASS_PATH=.\node_modulesSassモジュールをダウンロードします。
ターミナルnpm install node-sasssrc/App.cssファイルを、App.scssにリネームします。
ターミナルmv ./src/App.css ./src/App.scss4.App.scssを編集する
App.scssをテキストエディタで編集し、2行の@import文を冒頭に挿入します。
/src/App.scss@import "@material/react-button/index.scss"; @import "./react-button-overrides"; ...srcフォルダ内にreact-button-overrides.scssファイルを新規に作成し、以下の文をコピペして保存します。
/src/react-button-overrides.scss@import "@material/button/mixins"; .button-alternate { @include mdc-button-container-fill-color(lightblue); }5.App.tsxを編集する
src/App.tsxファイルをテキストエディタで開き、以下の文をコピペして保存します。元のコードは全部上書きで消します。
src/App.tsximport React, {Component} from 'react'; import Button from '@material/react-button'; import './App.scss'; // add the appropriate line(s) in Step 3a if you are using compiled CSS instead. class App extends Component { render() { return ( <div> <Button raised className='button-alternate' onClick={() => console.log('clicked!')} > Click Me! </Button> </div> ); } } export default App;このままでは、Typescriptの規約でコンパイルエラーが出るので、規則を変えます。
tsconfig.json に、下記の1行を挿入します。
tsconfig.json
"noImplicitAny": false,
気分的には15行目あたりが収まりがいい気がしますね。
tsconfig.json14 "forceConsistentCasingInFileNames": true, 15 "noImplicitAny": false, // ここに挿入 16 "module": "esnext",5.reactの型定義を更新
最後に、reactの型定義を最新に更新します。
ターミナルyarn upgrade @types/react@latestここまで来れば、再度ビルドが通るようになるはずです。
ターミナルnpm run build当たり前ですが、実行もできます。
ターミナルnpm startMDC-webのボタンのデモアプリが実行できました。
おまけ:エラーとその原因
SassError: File to import not found or unreadable: @material/elevation/mixins.
MDCのコンポーネントが、Sassモジュールから読めていません。
ターミナル// macの場合 export SASS_PATH=./node_modules // windowsの場合 SET SASS_PATH=.\node_modulesを実行しましょう。
Could not find a declaration file for module 'react'.
reactの定義ファイルがない・・と言ってますが、そもそもありません。
これは、Typescriptの設定が厳しいために発生するエラーです。
tsconfig.jsonを編集して、設定を緩めましょう。以下の文を挿入します。tsconfig.json"noImplicitAny": false,JSX element type 'App' is not a constructor function for JSX elements.
reactの型定義が古いことで発生するエラーで、Appクラスが読めなくなります。
ターミナルyarn upgrade @types/react@latestで解決します。
- 投稿日:2020-03-22T07:37:24+09:00
React + Typescript + Sass + MDC Web の環境を新規で作る
React + Typescript + Sass + MDC Web の環境を新規で作る
プログラミングは、たいてい環境構築が最も大変です。
React単体の環境構築はできても、TypescriptやSass、Material-Design-Component(MDC)を組み込んでゆくと、得体の知れないエラーと格闘する羽目になったりします。
本記事では、なるべく困らずに開発をスタートできるよう、手順を記します。
本記事は、2020年3月22日時点で動作確認しています。参考記事:MDC-webのチュートリアル
MDC-webの公式チュートリアルがあります(廃止されるようですが)。
これを参考に環境を構築します。1.React + Typescriptを導入する
最初に、create-react-appを使用して、React+Typescriptを一気に導入します。
my-appの部分はプロジェクト名なので、好きに変えて構いません。ターミナルnpx create-react-app my-app --template typescriptプロジェクトができたら、その中に移動してビルドします。
ターミナルcd my-app npm run build最初のビルドは通ります。
ターミナルnpm startを実行すると、Reactの初期画面が表示されます。
2.MDCを導入する
MDC-Reactを導入します。
ターミナルnpm install @material/react-buttonで、コンポーネントをダウンロードします。
3.Sassを導入する
ターミナルで以下を実行し、環境変数を追加します。
ターミナル// macの場合 export SASS_PATH=./node_modules // windowsの場合 SET SASS_PATH=.\node_modulesSassモジュールをダウンロードします。
ターミナルnpm install node-sasssrc/App.cssファイルを、App.scssにリネームします。
ターミナルmv ./src/App.css ./src/App.scss4.App.scssを編集する
App.scssをテキストエディタで編集し、2行の@import文を冒頭に挿入します。
/src/App.scss@import "@material/react-button/index.scss"; @import "./react-button-overrides"; ...srcフォルダ内にreact-button-overrides.scssファイルを新規に作成し、以下の文をコピペして保存します。
/src/react-button-overrides.scss@import "@material/button/mixins"; .button-alternate { @include mdc-button-container-fill-color(lightblue); }5.App.tsxを編集する
src/App.tsxファイルをテキストエディタで開き、以下の文をコピペして保存します。元のコードは全部上書きで消します。
src/App.tsximport React, {Component} from 'react'; import Button from '@material/react-button'; import './App.scss'; // add the appropriate line(s) in Step 3a if you are using compiled CSS instead. class App extends Component { render() { return ( <div> <Button raised className='button-alternate' onClick={() => console.log('clicked!')} > Click Me! </Button> </div> ); } } export default App;このままでは、Typescriptの規約でコンパイルエラーが出るので、規則を変えます。
tsconfig.json に、下記の1行を挿入します。
tsconfig.json
"noImplicitAny": false,
気分的には15行目あたりが収まりがいい気がしますね。
tsconfig.json14 "forceConsistentCasingInFileNames": true, 15 "noImplicitAny": false, // ここに挿入 16 "module": "esnext",5.reactの型定義を更新
最後に、reactの型定義を最新に更新します。
ターミナルyarn upgrade @types/react@latestここまで来れば、再度ビルドが通るようになるはずです。
ターミナルnpm run build当たり前ですが、実行もできます。
ターミナルnpm startMDC-webのボタンのデモアプリが実行できました。
おまけ:エラーとその原因
SassError: File to import not found or unreadable: @material/elevation/mixins.
MDCのコンポーネントが、Sassモジュールから読めていません。
ターミナル// macの場合 export SASS_PATH=./node_modules // windowsの場合 SET SASS_PATH=.\node_modulesを実行しましょう。
Could not find a declaration file for module 'react'.
reactの定義ファイルがない・・と言ってますが、そもそもありません。
これは、Typescriptの設定が厳しいために発生するエラーです。
tsconfig.jsonを編集して、設定を緩めましょう。以下の文を挿入します。tsconfig.json"noImplicitAny": false,JSX element type 'App' is not a constructor function for JSX elements.
reactの型定義が古いことで発生するエラーで、Appクラスが読めなくなります。
ターミナルyarn upgrade @types/react@latestで解決します。
- 投稿日:2020-03-22T04:40:26+09:00
【LINE】LIFFアプリを試してみる~ユーザ情報の取得とトークへの送信~
概要
- LIFFを使ってLINEのユーザ情報の取得とLINEへのメッセージ投稿を試してみます
- 前回書いたセットアップが完了している前提で進めます
- 公式のスターターは特にフレームワークを使っていませんが今回は私が使い慣れたReactで試してみます
Reactアプリの雛形生成
- create-react-appでさくっと作成します
npx create-react-app liff-sampleLIFF SDKのセットアップ
- LIFF SDKはnpmからインストールする形式では提供されていないので使いやすいように一手間入れておきます
public/index.html
にLIFF SDKの読み込み処理を追加
- しれっとそれ以外も手を加えてますがそのままでも影響ありません
public/index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>LIFF App</title> </head> <body> <div id="root"></div> <!-- LIFF SDKの読み込みを追加 --> <script src="https://static.line-scdn.net/liff/edge/2.1/sdk.js"></script> </body> </html>
- グローバルに定義された
liff
をexportするファイルを追加
- そのままグローバルな
liff
を使ってもいいですがimportして使えるようにしますsrc/lib/liff.js
を作成src/lib/liff.jsexport const liff = window.liff;
- これで
import { liff } from './lib/liff'
のような形でimportできるようになりましたLIFF APIへのアクセス処理を追加
実装
- まずはliffのinitialize処理を書きます
- 使いやすいようにCustomHooks化してしまいます
src/hooks/useLiff.js
を作成します
initLiff
メソッドがちょっと長いですが例外処理を書いているだけでtryの中に注目してくださいsrc/hooks/useLiff.jsimport { useState, useEffect } from 'react'; import { liff } from '../lib/liff'; function useLiff({ liffId }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const initLiff = async ({ liffId }) => { setLoading(true); try { // LIFF APIのinitを呼び出して初期化 await liff.init({ liffId }); alert('success liff init'); } catch (error) { alert({ error }); setError(error); } finally { setLoading(false); } }; // useLiffが呼ばれたらinitialize処理を実行する useEffect(() => { initLiff({ liffId }); }, [liffId]); return { loading, error }; } export default useLiff;
useLiff
をReactアプリのエントリーポイントであるsrc/App.js
に適用しますsrc/App.jsimport React from 'react'; import useLiff from './hooks/useLiff'; // LIFF IDを設定(後述) const liffId = '1234567890-abcedfgh'; function App() { const { loading, error } = useLiff({ liffId }); if (loading) return <p>...loading</p>; if (error) return <p>{error.message}</p>; return ( <div> <h1>Hello LIFF</h1> </div> ); } export default App;
- LIFF IDはURLスキームの値です
line://app/1234567890-abcedfgh
の1234567890-abcedfgh
の部分- 環境変数で埋め込んでもよいですが説明の簡略化のためここでは直接書いてしまいます
動作確認
- いったんここまでで動きを確認してみます
- 確認するためにはhttpsの環境にデプロイする必要があるのでお好きなホスティングサービスにデプロイしてください
- デプロイできたらLINE DeveloperのWebコンソールでURLを設定します
- 前回の記事で暫定でQiitaのURLを設定してたやつを差し替えます
- LIFFの設定までたどって「エンドポイントURL」を変更します
- 設定を反映できたらLINEからアクセスしてみましょう
- うまくいけば動画のようにinitがsuccessするはずです
ユーザ情報の取得
実装
- ログインまで確認できたので次はユーザ情報を取得してみます
src/hooks/useLiff.js
に処理を追加しますsrc/hooks/useLiff.jsimport { useState, useEffect } from 'react'; import { liff } from '../lib/liff'; function useLiff({ liffId }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); // 追加 const [profile, setProfile] = useState(null); const initLiff = async ({ liffId }) => { // 省略 }; // 追加 const fetchProfile = async () => { setLoading(true); try { // LIFF APIのgetProfileを実行し結果をセット setProfile(await liff.getProfile()); } catch (error) { console.log({ error }); setError(error); } finally { setLoading(false); } }; useEffect(() => { initLiff({ liffId }); }, [liffId]); // 修正 return { loading, error, fetchProfile, profile }; } export default useLiff;
src/App.js
に適用します
- ボタンを押すと通信処理が走り情報を取得します
- 取得できる情報は以下の4つです
- ユーザID
- 表示名
- プロフィール画像のURL
- ステータスメッセージ(設定されてなければ何も返されない)
- 今回は画面に出しておしまいですがユーザIDをサーバに送ってキー情報として使うイメージですね
src/App.jsimport React from 'react'; import useLiff from './hooks/useLiff'; // 自身のLIFF IDを設定 const liffId = '1234567890-abcedfgh'; function App() { // 修正 const { loading, error, profile, fetchProfile } = useLiff({ liffId }); if (loading) return <p>...loading</p>; if (error) return <p>{error.message}</p>; return ( <div> <h1>Hello LIFF</h1> {/* 追加 */} <section> {/* ボタンをクリックしたらfetchProfileを実行 */} <button onClick={() => fetchProfile()}>Get Profile</button> {/* 取得したProfileを表示 */} {profile && ( <div> <p>UserID: {profile.userId}</p> <p>DisplayName: {profile.displayName}</p> <p> Picture: <br /> <img src={profile.pictureUrl} height="300" width="300" /> </p> {profile.statusMessage && <p>StatusMessage: {profile.statusMessage}</p>} </div> )} </section> </div> ); } export default App;動作確認
- ここまでできたらデプロイして取得できることを確認します
- うまくいけば取得した情報が表示されるはずです
メッセージの送信
実装
- 最後にLINEのトークへのメッセージの送信を実装します
src/hooks/useLiff.js
に関数を追加しますsrc/hooks/useLiff.jsimport { useState, useEffect } from 'react'; import { liff } from '../lib/liff'; function useLiff({ liffId }) { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [profile, setProfile] = useState(null); const initLiff = async ({ liffId }) => { // 省略 }; const fetchProfile = async () => { // 省略 }; // 送信する内容を引数で受け取る const sendMessage = async ({ text }) => { setLoading(true); try { // LIFF APIのsendMessagesを実行 liff.sendMessages([{ type: 'text', text }]); console.log(`success send message: ${text}`); } catch (error) { console.log({ error }); setError(error); } finally { setLoading(false); } }; useEffect(() => { initLiff({ liffId }); }, [liffId]); // 修正 return { loading, error, fetchProfile, profile, sendMessage }; } export default useLiff;
src/App.js
に適用します- 送信内容入力するフォームも合わせて設置しておきます
src/App.js// useStateを追加 import React, { useState } from 'react'; import useLiff from './hooks/useLiff'; // 自身のLIFF IDを設定 const liffId = '1234567890-abcedfgh'; function App() { // 修正 const { loading, error, profile, fetchProfile, sendMessage } = useLiff({ liffId }); // メッセージのstateを追加 const [text, setText] = useState(''); if (loading) return <p>...loading</p>; if (error) return <p>{error.message}</p>; return ( <div> <h1>Hello LIFF</h1> <section> <button onClick={() => fetchProfile()}>Get Profile</button> {profile && ( <div> <p>UserID: {profile.userId}</p> <p>DisplayName: {profile.displayName}</p> <p> Picture: <br /> <img src={profile.pictureUrl} height="300" width="300" /> </p> {profile.statusMessage && ( <p>StatusMessage: {profile.statusMessage}</p> )} </div> )} </section> {/* メッセージの入力域と送信ボタンを追加 */} <section> <input onChange={e => setText(e.target.value)} /> <button onClick={() => sendMessage({ text })}>Send</button> </section> </div> ); } export default App;動作確認
- 再度デプロイして試してみますよう
- うまくいけばメッセージが送信されているはずです!
まとめ
- 今回はLIFFアプリを作ってユーザ情報の取得とトークへのメッセージ送信を実装しました
- この2つができればLIFFの恩恵は受けられそうですね
- どんなユースケースで活用できそうか考えてみようと思います
コード
- この記事の完成系のコードはこちらにも置いてあります
- https://github.com/ozaki25/liff-sample/tree/send-message
- 投稿日:2020-03-22T03:00:22+09:00
React + TensorFlow.jsでAdversarial Example (DeepFool, NewtonFool)をやった WebWorker編
はじめに
この記事はReact + TensorFlow.jsでAdversarial Example (FGSM)をやったの続編です.
あらすじ:React+TensorFlow.jsで画像分類モデルの訓練からAdversarial Example (AE)の生成まで行う静的サイトを作成した
https://github.com/Catminusminus/adv-examples-fun今回はAEの作成アルゴリズムを増やしたのと,WebWorkerにTensorFlow.jsの重い処理を移したのでそれについて書きます.
(デザインも前から少し変わりました)
WebWorker
WebWorkerに重い処理を逃がすことでUIがカクついたり固まったりするのを防ぎます.
Before
After
ボタンを押した後,すぐにローディングになりました.今回は,redux-sagaでTensorFlow.jsのモデルの訓練や予測を動かしていて,それごとWebWorkerに逃がす必要がありました.さらに,最も難しかったのは,モデルの訓練時に,バッチ毎にaccuracyとlossを記録したい(dispatchしたい),という点でした.
まず,ReduxをWebWorkerに逃がすために,react-redux-workerを使いました.使い方は,
store
を普通に作った後,worker.tsでimport { expose, createProxyStore } from 'react-redux-worker' const proxyStore = createProxyStore(store) expose(proxyStore, self)として,通常のReduxの
Provider
の代わりにimport { getProvider } from 'react-redux-worker' // worker.tsのパスを書く const worker = new Worker('./worker/worker.ts') const ProxyProvider = getProvider(worker) // 中略 const App = () => { const classes = useStyles() return ( <ProxyProvider> <Hoge /> </ProxyProvider> ) }という感じで,
getProvider
の返り値を使えばよいです.
そしてコンポーネント側でimport { useDispatch, useSelector } from 'react-redux-worker'と,react-redux-workerから
useDispatch
とuseSelector
を読み込んで使います.
今回はredux-sagaを併用したので,worker.tsはimport { createStore, applyMiddleware } from 'redux' import { expose, createProxyStore } from 'react-redux-worker' import createSagaMiddleware from 'redux-saga' import rootSaga from '../sagas' import { reducer } from '../modules' const sagaMiddleware = createSagaMiddleware() const store = createStore(reducer, applyMiddleware(sagaMiddleware)) sagaMiddleware.run(rootSaga) const proxyStore = createProxyStore(store) expose(proxyStore, self)となりました.これでReduxをWebWorkerで動かせます.
さて,これで一件落着,とはいきませんでした.モデルの訓練時にaccとlossをdispatchする手法が思いつかなかったのです.そんなのredux-sagaの
put
を使えばよい,と思われるかもしれませんが,モデルの訓練はawait this.model.fit(trainData.xs, trainData.labels, { batchSize, validationSplit, epochs: trainEpochs, callbacks: { onBatchEnd: async (batch: any, logs: any) => { emitter.emit('logs', logs) // ここでlogs.accとlogs.lossをdispatch/putしたい! // eslint-disable-next-line no-console console.log(`batchend loss:${logs.loss} acc:${logs.acc}`) }, onEpochEnd: async () => { // eslint-disable-next-line no-console console.log('epochend') }, }, })というように行うのでそのままでは
put
できません.実はWebWorkerを使う前は,dispatch
関数をコンポーネントからdispatchして引き回してきて,それでdispatchしていたのですが,今回WebWorkerを使ったことで,それができなくなりました(transferableでないためです).この問題は,redux-sagaのeventChannelを使うことで解決しました.
まずfunction* setLossAndAccChannel(lossAndAcc: any) { const chan = eventChannel(emitter => { lossAndAcc.on('logs', (logs: any) => emitter(logs)) // eslint-disable-next-line @typescript-eslint/no-empty-function return () => {} }) while (true) { const { loss, acc } = yield take(chan) // ここでlossとaccをセットするアクションをputする yield all([put(setLoss(loss)), put(setAcc(acc))]) } }として、rootSagaで
fork
します。yield fork(setLossAndAccChannel, emitter)後はモデルの訓練のときに
await this.model.fit(trainData.xs, trainData.labels, { batchSize, validationSplit, epochs: trainEpochs, callbacks: { onBatchEnd: async (batch: any, logs: any) => { emitter.emit('logs', logs) // eslint-disable-next-line no-console console.log(`batchend loss:${logs.loss} acc:${logs.acc}`) }, onEpochEnd: async () => { // eslint-disable-next-line no-console console.log('epochend') }, }, })(ただし
emitter
はconst emitter = new EventEmitter()です)
という感じで、EventEmitterを使ってput
できます(any
ばかりですみません。近いうちにちゃんと型をつけます)。
これでWebWorkerでモデルが訓練され、かつaccとlossがput
されて、UI threadで値を表示できます。DeepFool
DeepFoolはこちらの論文で提案されたAEの手法です.FGSMに比べて小さい摂動で分類器を騙すことができます.
本サイトでの実装は以下の部分です.論文の疑似コードをそのまま実装に落とした感じです.特にnumerical stabilityは考慮できてません.これによるのか,ほかのアルゴリズムに比べ,摂動があまり上手く計算できてないようなケースがあります(摂動がかなり大きくなってしまう).これは今後公式実装やFoolBoxの実装をよく読んで調べていきたいです.// 変数名は論文の疑似コードにできるだけ寄せています const x0 = image const xArr = [x0] const rArr = [] const kHatX0 = (model.predict(xArr[0]) as tf.Tensor<tf.Rank>) .argMax(axis) .dataSync()[0] // イテレーションは10回までに制限 // あまり大きくするとブラウザが固まってしまった(ただしwebworker対応する前) for (let i = 0; i < 10; i++) { const kHatXI = (model.predict(xArr[i]) as tf.Tensor<tf.Rank>) .argMax(axis) .dataSync()[0] // 攻撃が成功したらbreak if (kHatX0 !== kHatXI) { break } const wKArr: any[] = [] const fKArr: any[] = [] for (let k = 0; k < 10; k++) { if (k === kHatX0) { continue } const fK = (x: any) => (model.predict(x) as tf.Tensor<tf.Rank>) .flatten() .gather(tf.tensor1d([k], 'int32')) const fK0 = (x: any) => (model.predict(x) as tf.Tensor<tf.Rank>) .flatten() .gather(tf.tensor1d([kHatX0], 'int32')) const wKP = tf .grad(fK)(xArr[i]) .sub(tf.grad(fK0)(xArr[i])) wKArr.push(wKP) const fKP = (model.predict(xArr[i]) as tf.Tensor<tf.Rank>) .flatten() .gather(tf.tensor1d([k], 'int32')) .sub( (model.predict(xArr[i]) as tf.Tensor<tf.Rank>) .flatten() .gather(tf.tensor1d([kHatX0], 'int32')), ) fKArr.push(fKP) } const coefArr: any[] = wKArr.map( (v, i) => fKArr[i] .abs() // 現在の調査では,この分母が小さくなり,結果として摂動が大きくなってしまうことが分かっている // せめてもの抵抗として0.01を足している .div(v.norm().add(tf.scalar(0.01))) .dataSync()[0], ) const coef = tf.tensor1d(coefArr).argMin() const rI = tf .tensor1d(coefArr) .min() .mul(wKArr[coef.dataSync()[0]].div(wKArr[coef.dataSync()[0]].norm())) rArr.push(rI) xArr.push(xArr[i].add(rI)) } const reducer_ = ( accumulator: tf.Tensor<tf.Rank>, currentValue: tf.Tensor<tf.Rank>, ) => accumulator.add(currentValue) const perturbation = rArr.reduce(reducer_)NewtonFool
NewtonFoolはこちらの論文で提案されたAEの手法です.上の方にあるサイトの画像でNewtonFoolが使われていますが,見た感じだとかなり摂動が小さいです.
本サイトでの実装は以下の部分です.論文のbasic versionの疑似コードをそのまま実装に落とした感じです.const l = (model.predict(image) as tf.Tensor<tf.Rank>) .argMax(axis) .dataSync()[0] const dArr = [] const xArr = [image] const f = (x: any) => (model.predict(x) as tf.Tensor<tf.Rank>) .flatten() .gather(tf.tensor1d([l], 'int32')) for (let i = 0; i < 50; i++) { if ( (model.predict(xArr[i]) as tf.Tensor<tf.Rank>) .argMax(axis) .dataSync()[0] !== l ) { break } const delta = tf.minimum( tf // 論文で使われた値と同じ値 .scalar(0.01) .mul(image.norm()) .mul( tf .grad(f)(xArr[i]) .norm(), ), // 10クラスなので10分の1=0.1 (model.predict(xArr[i]) as tf.Tensor<tf.Rank>).max().sub(tf.scalar(0.1)), ) const dI = delta .mul(tf.grad(f)(xArr[i])) .div( tf .grad(f)(xArr[i]) .norm() .pow(tf.scalar(2).toInt()), ) .mul(tf.scalar(-1)) xArr.push(xArr[i].add(dI)) dArr.push(dI) } const reducer_ = ( accumulator: tf.Tensor<tf.Rank>, currentValue: tf.Tensor<tf.Rank>, ) => accumulator.add(currentValue) const perturbation = dArr.reduce(reducer_)おわりに
WebWorkerは前に記事を書いた時からずっと対応しようとしていて,それに伴う問題を解決するのにeventChannelにたどりつくまで相当時間がかかりました.何とかやりたいことが出来て良かったです.
またFGSMをTensorFlow.jsでやるのは先駆者がいましたが,他の攻撃方法は私が初めてだと思います.
そのうちPWA対応とかします.それでは.References
DeepFool is described by
@misc{moosavidezfooli2015deepfool, title={DeepFool: a simple and accurate method to fool deep neural networks}, author={Seyed-Mohsen Moosavi-Dezfooli and Alhussein Fawzi and Pascal Frossard}, year={2015}, eprint={1511.04599}, archivePrefix={arXiv}, primaryClass={cs.LG} }.NewtonFool is described by
@inproceedings{10.1145/3134600.3134635, author = {Jang, Uyeong and Wu, Xi and Jha, Somesh}, title = {Objective Metrics and Gradient Descent Algorithms for Adversarial Examples in Machine Learning}, year = {2017}, isbn = {9781450353458}, publisher = {Association for Computing Machinery}, address = {New York, NY, USA}, url = {https://doi.org/10.1145/3134600.3134635}, doi = {10.1145/3134600.3134635}, booktitle = {Proceedings of the 33rd Annual Computer Security Applications Conference}, pages = {262–277}, numpages = {16}, keywords = {Adversarial Examples, Machine Learning}, location = {Orlando, FL, USA}, series = {ACSAC 2017} }.
- 投稿日:2020-03-22T02:08:27+09:00
Progate無料版をやってみる【React】
前回に引き続きProgate無料レッスンをやっていこうと思います。
今回はReactです。
ProgateにはJavaScriptライブラリと書かれているけど、フレームワークではないのでしょうか?
→ AngularJSと勘違いしてましたw
AngularJSはフレームワークで、Vue.js等とよく比較に挙げられます。業務ではフレームワークだとVue.jsやKnockout.jsをちょろちょろとやったことがありました。
ライブラリだと、jQuery、Prototype.jsの経験があります。Reactは初めてです。
jQueryみたいにセレクタの嵐になるのだろうか・・・。
ならなかった!React
Reactを学ぼう
・Reactは流行りなんでしょうか・・・。
・jQueryではなく、Reactである理由ってなんなんでしょう・・・。ブラウザに表示しよう
・JSX!?とは!?
App.jsにJavaScriptでは書けないような記法がある・・・。import React from 'react'; class App extends React.Component { render() { return ( <h1>Hello React</h1> ); } } export default App;え、拡張子
js
でいいのだろうか・・・。ちょっとレッスンは止めて、まず環境構築と理解から・・・。
Reactとは
Reactは、旧来のjsとHTMLを分けて記述する手法ではなく、一緒に記述していく手法らしいです。
JSXを使う理由その方が直感的に書けそうですね。
Rubyの<% %>
や、PHPとかの埋め込み処理<?php ?>
のイメージなんでしょうか。
でもReactはクライアントサイドで動作するからちょっと違うか。React:素のJavaScriptに変換されて、クライアント側(主にブラウザ)で動作する。
クライアントでレンダリングされる → HTMLをjs内に直書きしたところはJavaScriptのdocument.createElementで行われる?Ruby、PHP、ASP.NET、JSP、Thymeleaf等のサーバーサイド処理や変数を埋め込み:
サーバーサイドで、「埋め込まれた処理や変数」を解析して、HTMLをレンダリング(整形)し、出来上がったHTMLをクライアントに返す。
ブラウザはレンダリングが少なくて済む。と理解しました。
※でもReactにも一応サーバーサイドレンダリングあるみたいです。
Next.js環境構築
公式の日本語説明のHello World的なものがあったので、こちらを参考にしていきます。
Create React AppどうやらNode.jsが必要みたいです。
Node.js前のなにかのレッスンで既にインストール済みでした。
1. 適当な場所に
React
ディレクトリを作ります。
ここをプロジェクトのトップにしようと思います。
2. VSCodeでその場所を開きます。
3. ターミナルをそこで開き
以下を実行npx create-react-app my-app色々なものが取得されました。
my-app
ディレクトリがTOPになるから、上で作ったReactディレクトリは必要なかったですね。あと、ところどころで出てくるBabelってなんすかw
以下を参考にさせていただきました。
→【5分でなんとなく理解!】Babel入門最新のECMAScriptで書いた処理を古いECMAScriptに変換してくれるツールでした!(トランスパイラ)
ReactのJSXをJSに変換するっていうところで使用しているんだろうな多分・・・。
4. 作成した
my-app
に移動して、npm start
するそうです。cd my-app npm startなんか立ち上がった!index.htmlの内容が表示されています。
コンパイルされたjsはどこにあるんや・・・。
F12で開発者ツール立ち上げて見てみたけど
謎です。レッスンに戻ります。
ブラウザを表示しようのところです。Progateの演習を真似するため、
App.js
をまるっと書き換えます。import React from 'react'; import logo from './logo.svg'; import './App.css'; function App() { return ( <h1>Hello React</h1> ); } export default App;再度
http://localhost:3000
を表示すると以下になりました!
JSX
・JSXはreturn内に複数の要素はNGらしい。複数ある場合は大枠を1つ用意して内包すること。
JSXの注意点
・コメントは{/* */}}
で囲む。独特すぎる・・・。imgタグ
・なぜかJSXは
img
タグの閉じタグ/>
が必要。App.jsの構成
・
import React from 'react';
やexport default App;
はお決まりらしい。
個人的にはretun ( );
の記述がしっくりこない・・・。JSXとJS
・JSXとJSには書ける範囲があるらしい。
JSXはrender()内のreturnの中だけ。
・JSX内にJavaScriptを書き込むときは{ }
で囲う。表示切り替えの準備・onClickイベント
・onClick属性等のイベントに記述する際は以下
<button onClick={() => {console.log('ひつじ仙人');}}>ひつじ仙人</button>んーちょっと煩雑・・・。
アロー関数() => {}
はfucntion(){}
の簡易的な書き方(※thisが異なる点は注意)state、stateの表示
・ユーザーの動きに合わせて変わる値。
・constructor
はコンストラクタ。
Appクラスのインスタンス生成時に呼ばれる処理。
例えば:const app = new App(); const app2 = App('test'); // new無しも行けたはずの時に呼ばれる。
インスタンス生成は多分
index.js
の以下でされる。ReactDOM.render(<App />, document.getElementById('root'));最初に呼ばれるんだなーくらいのイメージで・・・。
・
super
は継承元のReact.Component
のconstructor
を呼び出している。
お決まりとして覚える。・
props
はコンストラクタの引数。
何が渡ってくるんだろうw・
this
はApp
インスタンス自身を表す。class App extends React.Component { constructor(props) { super(props); this.state = {name: 'にんじゃわんこ'}; } }の
this
はconst app = new App(); // の「app」を表す const app2 = new App(); // の「app2」を表すインスタンスがそれぞれ作られたなら、
this
もそれぞれ異なる。stateの変更
・
state
を変更するときは直接代入はNGらしい。this.state = {name: 'test'}; // × this.state.name = 'test'; // ×
this.setState
を呼ぶらしい。this.setState({name: 'test'});Appクラス上にはsetStateメソッドはないが、継承元の
React.Component
におそらくある。演習
import React from 'react'; import logo from './logo.svg'; import './App.css'; class App extends React.Component { constructor(props) { super(props); this.state = {name: 'にんじゃわんこ'}; } render() { return ( <div> <h1>こんにちは、{this.state.name}さん!</h1> {/* onClickの処理に、stateを変更する処理を加えてください */} <button onClick={() => {this.setState({name: 'ひつじ仙人'}); }}>ひつじ仙人</button> {/* onClickの処理に、stateを変更する処理を加えてください */} <button onClick={() => {this.setState({name: 'にんじゃわんこ'}); }}>にんじゃわんこ</button> </div> ); } } export default App;あれ。
state
にしか保持できないのだろうか。→
this.state2
とか他の名称にしたら、setStateできなかった・・・。ということは、途中で値を変えるものはすべて
state
に代入するオブジェクトに詰め込んでおかないとだめなのかな?wメソッドの作成
・メソッドを作って、そこから
this.setState
する。import React from 'react'; import logo from './logo.svg'; import './App.css'; class App extends React.Component { constructor(props) { super(props); this.state = {name: 'にんじゃわんこ'}; } // handleClickメソッドを定義してください handleClick (name) { this.setState({name: name}); } render() { return ( <div> <h1>こんにちは、{this.state.name}さん!</h1> {/* onClickイベント内の処理を、handleClickメソッドを呼び出す処理に書き換えてください*/} <button onClick={() => {this.handleClick('ひつじ仙人');}}>ひつじ仙人</button> {/* onClickイベント内の処理を、handleClickメソッドを呼び出す処理に書き換えてください*/} <button onClick={() => {this.handleClick('にんじゃわんこ');}}>にんじゃわんこ</button> </div> ); } } export default App;
{name: name}
のところ、{"name": name}
にしなくてもエラーにならないんですね。最初のname
はちゃんとプロパティ名と判断してくれるみたい。カウントアップ機能を作ろう(1)、(2)
演習
import React from 'react'; import logo from './logo.svg'; import './App.css'; class App extends React.Component { constructor(props) { super(props); this.state = {count: 0}; } // handleClickメソッドを定義してください handleClick () { this.setState({count: this.state.count + 1}); } render() { return ( <div> <h1> {this.state.count} </h1> {/* <button>タグ内でonClickイベントを追加してください */} <button onClick={() => {this.handleClick();}}>+</button> </div> ); } } export default App;ブラウザで表示
onClick
の所どうにかならないかなぁ・・・。感想
・独特な記法がありすぎて戸惑いました汗 TypeScriptよりも独特な感じ・・・。
jsから派生した言語は、JavaScriptの記法を知っていればある程度いけると思っていましたが、そう簡単ではないですね。
プログラミング初心者の時にこれから勉強し初めていたら必ず挫折していたと思いますw・onClickの所が煩雑になるのが懸念でした。
・propsを使って子コンポーネントと親コンポーネントのやり取りとかもできるみたいなので、いつか習得したいです。・いままでのレッスンの中では一番難しかったですが、一番面白かったです。
・公式のチュートリアルに三目並べゲームがあるので、近いうちそれをやってみたいと思います。