20200322のReactに関する記事は17件です。

サーバーレス(React+Firebase)で新型コロナウイルスの感染者数マップを作った話

Firebaseを使ってサーバーレスで新型コロナウイルス(COVID-19)の感染者数の推移を地域別・日別で見られるWebサイトを作成しました。
https://covid19-visualization.web.app/
map_screen.png

以下のような技術を使って制作しています。

システム・ツール 役割
React フロント側の画面の構築
Google Maps Platform - Maps JavaScript API 地図の表示
Firebase Hosting ホスティング
Cloud Functions for Firebase APIサーバー・感染者数データのスクレイピング
Cloud Firestore データベース
Google Cloud Scheduler スクレイピングを定期実行するためのトリガー

個人開発のプロセスとして、企画編・開発編・リリース運用編としてそれぞれ書いていますので、よかったら参考にしていただければと思います。

具体的なコードはほとんど載せていないのですが、作成をするときに参考にしたサイトや公式サイトのリファレンス等のリンクは載せていますので、そちらを参考にしていただければと思います。

企画編

作ろうと思ったきっかけ

ニュースを見ていても、その日だけの新規の感染者数や累計での感染者数の報道が多く、今現在どの程度感染が拡大しているのかが分かりづらいと思い、視覚的に感染者数の推移を確認できるものがほしいと思って作りました。

※ 他にも感染者数の推移を確認できるサイトはいろいろとあります。以下のようなサイトもよければご覧ください。

主な機能

主な機能としては、地図で感染者数の推移を確認できる機能とグラフで確認できる機能があります。

地図表示機能

map_screen.png
都道府県別に感染者数を円で表示しています。感染者数の数値は厚生労働省の報道発表資料から取得しています。本当は市区町村単位での表示にしたかったのですが、公開されているデータが必ずしも市区町村別になっていないため、都道府県単位で集計をして各都道府県の県庁所在地に円をプロットしています。

感染が拡大しているのか縮小しているのかが視覚的にわかるように、日別にデータを表示することができます。また、画面右側にある「日付を自動で切替え」のボタンを押すことで自動で日付が切り替わるようになっています。

実際のサイトでもご覧いただければと思いますが、これを見ると、例えば、北海道は2月終わりから3月始めごろは感染者数が拡大しているものの、3月中旬ごろからは感染が収まってきていることがわかります。

グラフ表示機能

graphe_screen.png

日ごとの全国の感染者数の累計をグラフで表示しています。

都道府県別でも見られるようにしたかったのですが、まだ間に合っておらず作成できていません。今後作成予定です。

こちらも実際のサイトでご覧いただければと思いますが、このグラフを見ると3月に入ってからも大きな拡大はなく、ほぼ横ばいで感染者数の増加を抑えられていることがわかります。

開発編

ここからがQiita的には本編の記事になります。

開発編として設計と実装に分けて説明をしていきます。

設計

個人での開発とはいえ、今後も機能拡張はしていきたいので、メンテナンスや機能追加のしやすいものを作りたいと思います。そのためにある程度の設計はしてから進めます。

画面設計と機能の洗い出し

まずは画面設計です。

画面設計を最初に行うのはユーザビリティとして問題なさそうかを確認するという目的ももちろんありますが、

  • 必要な機能の洗い出し
  • 必要なデータの洗い出し

という目的もあります。

ワイヤーフレームを設計するためのツールとして、今回はJUSTINMINDというものを使いました。JUSTINMINDはデスクトップアプリが提供されています。他のツールにはオンラインで使えるものもいろいろありますが、オフラインでも作業ができた方がストレスなくできるため、今回はオフラインでも使えるこのツールにしました。

作成したワイヤーフレームは以下です。
app-screen-design.png

作成をした画面を見ながら、機能に漏れがないかの確認やどのようなデータをどういった形式で保存するのかを検討していきました。

また、この段階でコンポーネントをどの単位で分割をするのかもある程度決めておきます。以下のようにしました。(コンポーネント名もこの段階で決めてしまいます。適当な名前で進めてしまうと、気に入らないとなったときにあとから変更をするのが面倒ですので。)
app-screen-design-component.png

システム構成

次に作成をした画面と機能一覧を見ながらシステム構成を検討します。

バックエンド側には以下のような機能が必要になります。

  • APIサーバー: フロントエンド側からの呼び出しにより、感染者数のデータを返却します。

  • スクレイピングサーバー: 厚生労働省のホームページからスクレイピングにより感染者数のデータを取得します

  • データベース:スクレイピングにより取得したデータを保存します。

これを踏まえ作成したシステム構成は以下になります。
system-architecture.png

クライアント側はReactで作り、サーバー側はFirebaseで作っています。

管理画面は初期の検討段階では無かったのですが、後述する実現性の検証をした際に必要そうなことがわかったため、システム構成に入れました。

実現性検証

実装に入る前に、難易度が高そうなところや実現性がわからない機能についてはあらかじめ実現性の検証をしていきます。

今回のシステムで言えば、スクレイピングでのデータ取得ができるのかというのができるかがわからないところがありましたので、この部分について検証をしました。

感染者数のデータは厚生労働省ホームページのこちらの報道発表一覧のページから取得できます。この一覧から、例えばこちらの詳細ページへのリンクが貼られています。一覧ページと詳細ページをスクレイピングしていけばデータは取得できそうです。

もう少し検証を進めて、スクレイピングによりすべてのデータが正しく取得できるかを検証したところ、以下のような課題がありました。

  1. 感染者数の数値がすべて1つのdivタグに囲まれており(下記に貼った画面キャプチャ参照)、CSSセレクタを指定してのデータ取得ができない
  2. 都道府県単位でのデータになっていたり市単位でのデータになっている
  3. 「東 京 都:患者7例」というように、「東」と「京」の間にスペースが入っている場合があったり、「患者○例」となっていることがほとんどなのに、たまに「患者○名」となっている
  4. 最近(3月ごろ)のページのフォーマットと初期(1月から2月始めごろ)のページのフォーマットが異なる

1についてはスクレイピングで全文を取得したあとに、正規表現でテキストを抽出することにしました。

2〜4については正規表現やデータフォーマットの変換で対応できるものについてはできるだけ対応をしつつ、スクレイピングした結果を目視で確認することとしました。(データ補正用の管理画面が必要となったのはこのためです。)

データ取得元のページ
scrapingpage.png

実装

ここからは実際の実装作業について、順を追って説明をしていきます。

環境構築

最初にフロントエンド側、バックエンド側ともに環境を構築します。

ここまでできたら一旦デプロイをして、環境が正しく構築できているかを確認します。無事に環境が構築できて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の構成に従っています。発案者が書いた記事がありますので、詳しくはこちらを参考にしてください。

スクレイピングの実装

ここまで来て、ようやく具体的な機能の実装を始めます。

最初にスクレイピング部分の実装です。
scraping.png

スクレイピング

スクレイピングは大まかには以下のようなロジックです。

  1. 厚生労働省の報道発表一覧のページからデータが記述された詳細ページのURLを取得する
  2. 詳細ページ(例えばこちらのページ)から感染者数のデータを取得する
  3. 取得したデータをFirestoreに保存する

Firestoreへの保存については、公式サイトのチュートリアルを参考にして実装しまいた。

実装をして、ローカル環境では問題なかったのですがデプロイをしたところ以下のようなエラーとなりました。

Error: memory limit exceeded. Function invocation was interrupted.

どうやらメモリ不足のようです。

デフォルトで割り当てられているメモリや256MBになっているようですので、Firebase公式サイトのこちらのページを参考にして、メモリを1GBに増やしました。

スクレイピングの呼び出し

スクレイピングの呼び出しは、Firebaseの関数のスケジュール設定のページを参考にして実装します。

使用したツール・システム

APIの実装

次にバックエンド側のAPIを実装します。
apiserver.png

Cloud FunctionsをHTTP経由で呼び出せるようにします。公式サイトのチュートリアルを参考にして実装します。Express アプリをそのままHTTP関数に渡すことができますので、今回はこの方法を採用しました。

使用したツール・システム

画面の作成

ここからはフロントエンド側の実装に入ります。

最初にまずは画面を作ります。
screen.png

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というライブラリを使用しています。

使用したツール・システム

管理画面の作成

以下の管理画面部分を作成します。
adminscreen.png

管理画面は以下のような感じです。
admin.png

ユーザーが使用する画面と同様にMaterial-UIを使用して実装しています。

またテーブル表示には、Material-UIを拡張して作られたmaterial-tableというライブラリを使用しています。

使用したツール・システム

リリース・運用編

デプロイ

実装が終わり、無事にテストも終わったところでデプロイをします。

Firebase HostingのデプロイもCloud Functionsのデプロイも以下のコマンドを打つだけで簡単にデプロイできます。

xxx$ firebase deploy

予算上限・アラートの設定

そんなに利用者数も多くないため、Firebaseの無料枠の範囲内で収まるとは思いますが、念のため予算の設定をしておきます。

Firebaseには1ヶ月間の使用料金が超過したときにアラートを知らせてくれる機能があります。Firebaseの使用量と制限のページを参考にして設定しました。

また、1日あたりの料金もアラート設定の説明ページと同じページにある1 日あたりの費用制限を設定するに書いてある方法で設定できるようです。

(これも設定をしたかったのですが、このページに書いてあるとおりに進めても設定をするページが現れず、設定できませんでした。。。Googleのサポートに電話をかけても1時間待ってもつながらず、結局設定できていません。。。どなたかやり方をご存知でしたらコメント欄などで教えていただけると嬉しいです。)

ということで、完成したのがこちらのページになります。ご質問、ご要望などございましたらコメント欄などでお気軽にお知らせください!

今後の予定

当初は以下のような機能も実装をしようと思っていたのですが、時間が足りずに残念ながらまだ実装できていません。今後実装をする予定です。

  • 地域別に感染者数を表示する機能

  • 表示期間を設定する機能

  • 感染予防のためのお役立ち情報集

(他にもこんな機能があったらいいというのがあれば、ぜひコメントください!)

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

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.js
import 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.js
import 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.js
   onSubmit(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のデータを引き継ぎできないという課題が残るため、引き続き調べていきます。

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

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ブロックに分かれています。

  1. Reactアプリケーションのビルド
  2. Javaアプリケーションのビルド
  3. 実行するイメージの作成

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 package

Reactアプリケーションのビルド成果物を/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=local

metaタグや.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

範囲を選択_019.png

次に、thymeleaf_testtestという値を入れて、コンテナ起動してみます。

docker run --rm -p 80:8080 test:dev

範囲を選択_020.png

反映が確認できました。

おわりに

そもそもこのような構成で、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

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

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' }

で、上書きしてあげましょう

送信内容

スクリーンショット 2020-03-22 16.48.47.png

こんな感じです。

確認ポイント

  1. RequestHeaderscontent-typemultipart/form-dataになっている
  2. FormDataのvalueが(binary)になっている

まとめ

これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです

初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。

  • Fileのまま送信(multipart/form-data)
  • Base64にエンコードして送信(application/json)

エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね

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

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' }

で、上書きしてあげましょう

送信内容

スクリーンショット 2020-03-22 16.48.47.png

こんな感じです。

確認ポイント

  1. RequestHeaderscontent-typemultipart/form-dataになっている
  2. FormDataのvalueが(binary)になっている

まとめ

これで正常にmultipart/form-dataでファイルをPOSTできるようになりました!
WEBサービスを作成する際、画像のアップロードはよく出でくるシチュエーションだとおもいますので、参考になれば幸いです

初めてこの機能を実装する際には迷う部分がたくさんあって、ファイルのPOSTする形も何種かあると思っていましたが、2種類のみのようです。

  • Fileのまま送信(multipart/form-data)
  • Base64にエンコードして送信(application/json)

エンコードするとjsonのまま送れるメリットがあるのですが、ファイルサイズが20~30%あがるそうです。
やればやるほど奥が深く、正解がなくて迷いますが、楽しいですね

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

ReactとFlaskがGCPへ

GCP使ってみたいドリブンでYelp API使ったテストサイトを作る

GCPでAPIテストサーバー作りたかった
言語: React, Flask
Screen Shot 2020-03-22 at 1.12.36.png

これにどこからかのapi付けて描画すればいいかなっと思ったけど味気ない (Learn Udonのリンク先は、うどんについて語るwiki)ので
image.pngYelp 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_screen_shot.png

結果を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-gcr

こんな感じ
Screen Shot 2020-03-22 at 14.08.08.png

GCE(Google Compute Engine)

インスタンスをgcpコンソール画面から作成

マシンスペックはテストなので最弱の最弱にしよう!
image.png

docker使ってコンテナからすぐデプロイしたい人は下記のチェックとコンテナイメージを忘れずに
image.png

GCE VMインスタンス画面の外部IPを http:[IP] にすれば同じ結果が出るようになった!
SSL証明書がない場合は外部IPを https じゃなくて http にしないとダメだよ
image.png

この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がグルグル回ってるのがうっとうしい、良いかは賛否ある

参考

ContainerRegistry(GCR)に登録されたDockerイメージをGCE上で動かす

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

ReactとFlaskがGCPへ。

GCP使ってみたいドリブンでYelp API使ったテストサイトを作る

GCPでAPIテストサーバー作りたかった
言語: React, Flask
Screen Shot 2020-03-22 at 1.12.36.png

これにどこからかのapi付けて描画すればいいかなっと思ったけど味気ない (Learn Udonのリンク先は、うどんについて語るwiki)ので
image.pngYelp 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_screen_shot.png

結果を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-gcr

こんな感じ
Screen Shot 2020-03-22 at 14.08.08.png

GCE(Google Compute Engine)

インスタンスをgcpコンソール画面から作成

マシンスペックはテストなので最弱の最弱にしよう!
image.png

docker使ってコンテナからすぐデプロイしたい人は下記のチェックとコンテナイメージを忘れずに
image.png

GCE VMインスタンス画面の外部IPを http:[IP] にすれば同じ結果が出るようになった!
SSL証明書がない場合は外部IPを https じゃなくて http にしないとダメだよ
image.png

この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のリンク先にとんで詳細が見れる
image.png

うどんのSVGがグルグル回ってるのがうっとうしい、良いかは賛否ある

参考

ContainerRegistry(GCR)に登録されたDockerイメージをGCE上で動かす

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

【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でレポジトリの作成

スクリーンショット 2020-03-22 12.37.05.png
Githubにログインして、マイページからリポジトリに移動して、新規で作成するのでNewボタンをクリックし、説明に沿って好きなリポジトリ名で作成します。
スクリーンショット 2020-03-22 12.49.47.png

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にて無料でサイトを公開することができるようになるはずです^^

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

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.js
const 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 パッケージの利用で解決できます。

また忘れてハマりそうなのでメモしておいた。

参考

Next.js 公式ドキュメント日本語翻訳プロジェクト - カスタムサーバーとルーティング

Nextで特定のAPI リクエストをproxyする方法 - Qiita

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

背景の見えるポップアップスクリーンの作り方

背景が見えるモーダル画面って個人的には好きです。
今回はその作成方法を紹介します。
output.gif

使用ライブラリ

  • @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は、モーダル部分が下に詰めすぎないように制御しています。

SafeAreaView あり/なし
Simulator Screen Shot - iPhone 11 Pro Max - 2020-03-22 at 08.08.15.pngSimulator Screen Shot - iPhone 11 Pro Max - 2020-03-22 at 09.21.43.png

画面コンポーネントの作成

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 を用いると、より「アプリらしい」挙動をさせることができます。
組み合わせ次第で、いろんなモーダル画面が作れるので、ぜひ試してみてください。

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

Reactコーディングを爆速でぶち上げる方法

React_emmet.gif

前置き

Reactコーディングをする時に、便利だなと感じる機能、ツールをまとめました。
コーディングスピードを爆速でぶち上げたい方向けです。(言いたいだけ)

仕様ツール

  • Visula Stadio Code
  • Simple React Snippets(拡張機能)

①VS Codeのショートカット

Reactのファイルを開いたら...

  • commandK を押してから M

これめちゃ便利です。Reactのファイルは、デフォルトだとJavaScriptのファイルと認識されてしまいますので、Reactで使いたいSnippetsやEmmetが使えなかったりします。

②Simple React Snippets(拡張機能)

スクリーンショット 2020-03-22 9.43.10.png

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のコーディングでも使えますよー

まとめ

新しいコンポーネントを作った時とかに、是非使ってみてください!
おすすめの拡張機能、コーディング方法あればコメントいただけると大変喜びます。

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

Docker で zeit nowのデプロイ環境とNext.jsの開発環境を作ったメモ

概要

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 deploy

next

2020 年初頭における Next.js をベースとしたフロントエンドの環境構築

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

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の初期画面が表示されます。

スクリーンショット 2020-03-22 06.44.54.png

2.MDCを導入する

MDC-Reactを導入します。

ターミナル
npm install @material/react-button

で、コンポーネントをダウンロードします。

3.Sassを導入する

ターミナルで以下を実行し、環境変数を追加します。

ターミナル
// macの場合
export SASS_PATH=./node_modules

// windowsの場合
SET SASS_PATH=.\node_modules

Sassモジュールをダウンロードします。

ターミナル
npm install node-sass

src/App.cssファイルを、App.scssにリネームします。

ターミナル
mv ./src/App.css ./src/App.scss

4.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.tsx
import 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.json
14    "forceConsistentCasingInFileNames": true,
15    "noImplicitAny": false, // ここに挿入
16    "module": "esnext",

5.reactの型定義を更新

最後に、reactの型定義を最新に更新します。

ターミナル
yarn upgrade @types/react@latest

ここまで来れば、再度ビルドが通るようになるはずです。

ターミナル
npm run build

当たり前ですが、実行もできます。

ターミナル
npm start

スクリーンショット 2020-03-22 07.30.43.png

MDC-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

で解決します。

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

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の初期画面が表示されます。

スクリーンショット 2020-03-22 06.44.54.png

2.MDCを導入する

MDC-Reactを導入します。

ターミナル
npm install @material/react-button

で、コンポーネントをダウンロードします。

3.Sassを導入する

ターミナルで以下を実行し、環境変数を追加します。

ターミナル
// macの場合
export SASS_PATH=./node_modules

// windowsの場合
SET SASS_PATH=.\node_modules

Sassモジュールをダウンロードします。

ターミナル
npm install node-sass

src/App.cssファイルを、App.scssにリネームします。

ターミナル
mv ./src/App.css ./src/App.scss

4.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.tsx
import 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.json
14    "forceConsistentCasingInFileNames": true,
15    "noImplicitAny": false, // ここに挿入
16    "module": "esnext",

5.reactの型定義を更新

最後に、reactの型定義を最新に更新します。

ターミナル
yarn upgrade @types/react@latest

ここまで来れば、再度ビルドが通るようになるはずです。

ターミナル
npm run build

当たり前ですが、実行もできます。

ターミナル
npm start

スクリーンショット 2020-03-22 07.30.43.png

MDC-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

で解決します。

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

【LINE】LIFFアプリを試してみる~ユーザ情報の取得とトークへの送信~

概要

  • LIFFを使ってLINEのユーザ情報の取得とLINEへのメッセージ投稿を試してみます
  • 前回書いたセットアップが完了している前提で進めます
  • 公式のスターターは特にフレームワークを使っていませんが今回は私が使い慣れたReactで試してみます

Reactアプリの雛形生成

  • create-react-appでさくっと作成します
npx create-react-app liff-sample

LIFF 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.js
export const liff = window.liff;
  • これでimport { liff } from './lib/liff'のような形でimportできるようになりました

LIFF APIへのアクセス処理を追加

実装

  • まずはliffのinitialize処理を書きます
  • 使いやすいようにCustomHooks化してしまいます
  • src/hooks/useLiff.jsを作成します
    • initLiffメソッドがちょっと長いですが例外処理を書いているだけでtryの中に注目してください
src/hooks/useLiff.js
import { 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.js
import 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-abcedfgh1234567890-abcedfghの部分
    • 環境変数で埋め込んでもよいですが説明の簡略化のためここでは直接書いてしまいます

動作確認

  • いったんここまでで動きを確認してみます
  • 確認するためにはhttpsの環境にデプロイする必要があるのでお好きなホスティングサービスにデプロイしてください
    • NowNetlifyであれば無料であっという間にデプロイできます
  • デプロイできたらLINE DeveloperのWebコンソールでURLを設定します
    • 前回の記事で暫定でQiitaのURLを設定してたやつを差し替えます
  • LIFFの設定までたどって「エンドポイントURL」を変更します

スクリーンショット 2020-03-22 3.52.48.png

  • 設定を反映できたらLINEからアクセスしてみましょう
  • うまくいけば動画のようにinitがsuccessするはずです

init.gif

ユーザ情報の取得

実装

  • ログインまで確認できたので次はユーザ情報を取得してみます
  • src/hooks/useLiff.jsに処理を追加します
src/hooks/useLiff.js
import { 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.js
import 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;

動作確認

  • ここまでできたらデプロイして取得できることを確認します
  • うまくいけば取得した情報が表示されるはずです

profile.gif

メッセージの送信

実装

  • 最後にLINEのトークへのメッセージの送信を実装します
  • src/hooks/useLiff.jsに関数を追加します
src/hooks/useLiff.js
import { 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;

動作確認

  • 再度デプロイして試してみますよう
  • うまくいけばメッセージが送信されているはずです!

send.gif

まとめ

  • 今回はLIFFアプリを作ってユーザ情報の取得とトークへのメッセージ送信を実装しました
  • この2つができればLIFFの恩恵は受けられそうですね
  • どんなユースケースで活用できそうか考えてみようと思います

コード

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

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の重い処理を移したのでそれについて書きます.
(デザインも前から少し変わりました)
screen.png

WebWorker

WebWorkerに重い処理を逃がすことでUIがカクついたり固まったりするのを防ぎます.
Before
before.gif
After
after.gif
ボタンを押した後,すぐにローディングになりました.

今回は,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からuseDispatchuseSelectorを読み込んで使います.
今回は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}
}.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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ディレクトリを作ります。
ここをプロジェクトのトップにしようと思います。
image.png

2. VSCodeでその場所を開きます。
image.png
3. ターミナルをそこで開き
image.png
以下を実行

npx create-react-app my-app

色々なものが取得されました。
image.png
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の内容が表示されています。
image.png
コンパイルされたjsはどこにあるんや・・・。
F12で開発者ツール立ち上げて見てみたけど
image.png
謎です。

レッスンに戻ります。
ブラウザを表示しようのところです。

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を表示すると以下になりました!
image.png

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.Componentconstructorを呼び出している。
 お決まりとして覚える。

propsはコンストラクタの引数。
 何が渡ってくるんだろうw

thisAppインスタンス自身を表す。

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;

んー。onClickのところが煩雑すぎてヤバイです。
ダウンロード.gif

あれ。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;

ブラウザで表示
ダウンロード.gif
onClickの所どうにかならないかなぁ・・・。

クリアしました
image.png

感想

・独特な記法がありすぎて戸惑いました汗 TypeScriptよりも独特な感じ・・・。
 jsから派生した言語は、JavaScriptの記法を知っていればある程度いけると思っていましたが、そう簡単ではないですね。
 プログラミング初心者の時にこれから勉強し初めていたら必ず挫折していたと思いますw

・onClickの所が煩雑になるのが懸念でした。
・propsを使って子コンポーネントと親コンポーネントのやり取りとかもできるみたいなので、いつか習得したいです。

・いままでのレッスンの中では一番難しかったですが、一番面白かったです。
・公式のチュートリアルに三目並べゲームがあるので、近いうちそれをやってみたいと思います。

次回はNode.jsをやっていこうと思います。
→ 次回

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