20200101のReactに関する記事は6件です。

ReactのRedux

はじめに

ReactのReduxに関するメモ。
ReactのクラスコンポーネントとかJSXとかコンテキストとか調べてみたの続き

参考

React.js&Next.js超入門

Redux

  • 複数のコンポーネント、アプリケーション全体で使うような値をまとめるもの
    • this.stateはコンポーネント単位で存在する
  • 値や処理をアプリ内で統合し管理するための仕組みのライブラリ
  • 値の保管は場所は一つだけ
    • アプリケーションごとに一つだけ
  • 値は読み取り専用、書き換え不可
  • 変更は単純な関数で用意

インストール

npm install --save redux
npm isntall --save react-redux
npm isntall --save redux-devtools

仕組み

  • ストア
    • 値を保管するステートと、値の操作処理であるレデューサーを内部に持っている
  • プロバイダー
    • ストアを他のコンポーネントに受け渡すための仕組み
  • レデューサー
    • ストアに保管されているステートを変更するための仕組み

Reduxの値の管理は、上記3つでできている

ストアの作成

変数= createStore(レデューサー);

作成されたストアを変数に収めておき、それを画面表示のJSXでプロバイダーに渡して利用

コンポーネントにストアを接続

変数=connect(ステート)(コンポーネント);

これでthis.props.ストアの値とthis.props.dispatchが使えるようになる

変数=connect()(コンポーネント);

これはステートを使わずthis.props.dispathを使えるようにする

providerでストアを受け渡す

<Provider store={store}>
  <APP/>
</Provider>

redux全体

index.jsでステート、レデューサーを定義して、renderでAppコンポーネントを利用。
Appコンポーネントの中で各コンポーネントを利用、ストアを使いたいコンポーネントだけプロバイダーでくくりその中でコンポーネントを利用する
Appコンポーネントで利用するコンポーネントの定義とコンポーネントとストアを結びつけることで、各コンポーネントから
dispatchからレデューサーを利用する

データの永続化 Redux Persist

Reduxのストアのデータをブラウザのローカルストレージに保存する

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

ReactNative(expo)でアプリとWeb(View)で値をやり取りする

ReactNativeのWebView(Web)とアプリを連携する必要があったのでメモ。

忙しい人向け

  • アプリからWeb => WebViewのinjectedJavaScriptにJSを渡して値をインジェクトしてWeb側でよしなにする
  • Webからアプリ => window.ReactNativeWebView.postMessage("message")で送りonMessage()で受け取る

やりたいこと

例えば決済機能等の実装において、決済画面だけは決済サービス会社が提供するものを利用したいが、値はアプリ側で計算したものをデフォルト値として渡したいケースなど。

以下のような仕様。

スクリーンショット 2020-01-01 15.37.16.png

最近の決済APIはカード情報非保持・非通過とするためWeb画面でカード番号を入れさせるものが多い。さらに言えばexpoでEjectしないで利用できるコンポーネントもない。

Web側

Web側は普通のHTMLだとまだ簡単なのですがReactを使うことが多いのでReactを使ってみます。

準備

場所作って必要なモジュールをインストール。

create-react-app web-app
cd web-app

npm install --save bootstrap reactstrap formik yup

実装

続いて実装。

index.js

bootstrap cssの読み込みとServiceWorkerを削除している(キャッシュが効いちゃうので)。

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';

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

App.js

App.jsに一旦すべて実装。

  • priceというidのinputを用意
  • cardというidのinputを用意
  • priceにinjectedJavaScriptから値を設定

ということを想定。私いつもFormikつかうので使ってます。

App.js
import React from 'react';
import { Form, FormGroup, Label, Input, Button, FormFeedback } from 'reactstrap';
import { Formik } from 'formik';
import * as Yup from 'yup';

class App extends React.Component {

    handlePayment = async (values) => {

        //1秒休む
        await this.sleep(1000);

        //終了したらアプリ側にメッセージを送る
        window.ReactNativeWebView.postMessage(values.price + "円の決済が完了しました。");
    }

    //おやすみ補助関数
    sleep = (msec) => {
        return new Promise((resolve) => {
            setTimeout(() => {
                return resolve();
            }, msec)
        })
    }

    render() {
        return (
            <div className="container">
                <h3 className="my-4 text-center">Payment(ここはWeb</h3>
                <div className="col-10 mx-auto">
                    <Formik
                        initialValues={{ price: 0, card: '1111-2222-3333-4444' }}
                        onSubmit={(values) => this.handlePayment(values)}
                        validationSchema={Yup.object().shape({
                            price: Yup.number().min(1).max(1000),
                            card: Yup.string().required(),
                        })}
                    >
                        {
                            ({ handleSubmit, handleChange, handleBlur, values, errors, touched, setFieldValue }) => (
                                <Form>
                                    <FormGroup>
                                        <Label>金額</Label>
                                        <Input
                                            type="text"
                                            name="price"
                                            id="price" //idで強引に値をセット
                                            value={values.price}
                                            onChange={handleChange}
                                            onBlur={handleBlur}
                                            invalid={Boolean(touched.price && errors.price)}
                                            disabled
                                        />
                                        <FormFeedback>
                                            {errors.price}
                                        </FormFeedback>
                                    </FormGroup>
                                    <FormGroup>
                                        <Label>カード番号</Label>
                                        <Input
                                            type="text"
                                            name="card"
                                            id="card"
                                            value={values.card}
                                            onChange={handleChange}
                                            onBlur={handleBlur}
                                            invalid={Boolean(touched.card && errors.card)}
                                        />
                                        <FormFeedback>
                                            {errors.card}
                                        </FormFeedback>
                                    </FormGroup>
                                    <Button type="button" onClick={async () => {
                                        const price = document.getElementById("price");
                                        //Formik使ってるので値を明示的にセットしてやる(完了するうちにValidationが走らないようawait)
                                        await setFieldValue("price", price.value);
                                        handleSubmit();
                                    }}>購入</Button>
                                </Form>
                            )
                        }
                    </Formik>
                </div>
            </div>
        );
    }
}

export default App;

とりあえず完成。window.ReactNativeWebView.postMessage()なんていう関数は標準のブラウザにはないのでchrome等でデバッグするとエラー出ますが無視します。

アプリ側

次にアプリ側。
場所の準備と必要コンポーネントをインストール。WebViewは普通にインストールするとexpoに怒られるのでexpo installコマンドで適切なバージョンのものをインストール。

expo init app-web-integration
cd app-web-integration

expo install react-navigation react-native-gesture-handler react-native-reanimated react-native-screens
expo install react-navigation-stack react-navigation-tabs react-navigation-drawer
expo install react-native-webview

まずApp.js。基本的にStackNavigatorを設定しているだけ。
Home.jsとPayment.jsを利用しています。

App.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';

import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';

import Home from './Home';
import Payment from './Payment';

//stack navigator
const HomeStack = createStackNavigator(
    {
        Home: {
            screen: Home,
        },
        Payment: {
            screen: Payment,
        }
    }
);

const AppContainer = createAppContainer(HomeStack);

class App extends React.Component {
    render() {
        return (
            <AppContainer />
        );
    }
}

export default App;

Home.js

ボタンを配置してPayment.jsに移動します。またその時金額をパラメーターとして渡しています。

Home.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';

class Home extends React.Component {
    render() {
        return (
            <View style={{ flex: 1, alignItems: 'center', marginTop: 50 }}>
                <Text style={{ fontSize: 24 }}>Home(ここはアプリ)</Text>
                <Button
                    title="100円コースを買う"
                    style={{ width: '80%', marginTop: 20 }}
                    onPress={() => this.props.navigation.navigate("Payment", { price: 100 })}
                />
                <Button
                    title="200円コースを買う"
                    style={{ width: '80%', marginTop: 20 }}
                    onPress={() => this.props.navigation.navigate("Payment", { price: 200 })}
                />
            </View>
        );
    }
}

export default Home;

Payment.js

このコンポーネントはWebViewになります。WebViewの、

  • injectedJavaScriptにWeb側に渡す値を設定
  • onMessageに戻りの処理を書く
Payment.js
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Card, Input, Button } from 'react-native-elements';
import { WebView } from 'react-native-webview';

class Payment extends React.Component {

    state = {
        js: '',
    }

    componentDidMount = async () => {
        //前のページからパラメータを受け取る(なければ0)
        const price = await this.props.navigation.state.params.price ? this.props.navigation.state.params.price : 0;
        //priceを設定するスクリプトを動的に生成
        const js = `
            const price = document.getElementById("price");
            price.value = ${price}
        `;
        //stateを通じて渡す
        this.setState({ js: js });
    }

    //Web側からのpostMessageに対応
    onMessage = (event) => {
        const message = event.nativeEvent.data;
        this.props.navigation.navigate("Home");
        alert(message);
    }

    render() {

        //js内の変数が処理されないうちにWebViewがレンダリングするのを防ぐ
        if (this.state.js === '') {
            return <Text>Loading...</Text>
        }

        //WebViewをレンダリング
        return (
            <WebView
                source={{ uri: 'http://localhost:3000/' }}
                injectedJavaScript={this.state.js}
                onMessage={this.onMessage}
            />
        )
    }
}

export default Payment;

かなり端折ってるけるけどとりあえず。

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

(2020年元旦時点で最新の)Stripeの決済をReactで使う

どこの決済サービスを利用するかは悩ましいところですが、業界標準のStripeはいずれにしてもおさえておきたい・・・ということで調査。意外と苦労したのでメモ。

前提知識

ネットに多くの情報がありますが、仕様が変化していて最新の情報を見つけるのに苦労しました。
事前に知っていればもっと楽だったことをまとめてみます。

Stripeのサービス

Stripeが提供するサービスはいろいろある。

  • PAYMENT(ま、普通の決済)
  • BILLING(月額課金)
  • CONNECT(プラットフォーマー用)

ここの記事では PAYMENT を扱います。

他にも色々ありますが、日本では使えないものもあるので注意(Issuingとか)。

PAYMENTの中でもいろいろ

1つのサービスの中でも自サイトへの埋め込み方法やAPIの種類など複数あります。

埋め込み方

  • Checkoutを利用する(Stripeが用意した決済画面を利用する(自分のサイトに埋め込む))
  • Stripe.js/Elementを利用する(パーツとして用意されたUIとJSを利用する)

スクリーンショット 2020-01-01 10.57.47.png

決済(API群)の種類

2019年の9月にSCA Readyである必要が発生し、その対応のためにpaymentIntentが登場したもよう。
日本で言うカード情報の「非通過」、「非保持」のためのPCI-DSS対応のようなものなやつ。

これが古い記事が参考にならない原因のようです。

  • charge(古い => 事前にtokenを作るタイプのやつ(カード情報の処理が先))
  • payementIntent(新しい => 事前にpaymentIntentを作る(カード情報の処理は後))

比較表が本家サイトにあります。

client側とserver側を実装する必要がある

プログラムはクライアントとサーバ側両方での実装が必要になります(めんどい)。
技術的には1つでもいい感じがしますが、paymentIntent作成リクエストに秘密鍵が必要なので、それを隠蔽するためかなという印象。

  • server側プログラムが必要なのは 秘密鍵 を隠蔽するため(技術的にはなくても決済自体はできる)

React

これは私の用途限定。

  • Reactに特化したelementとしてreact-stripe-elementsというパッケージがある
  • 本家サイトで紹介されているのはcharge方式。ただ、paymentIntetにも対応している
  • 本記事ではreact-stripe-elementsでpaymentIntentを利用する方法を紹介

ReactNativeだと現時点でtipsi-stripeとかを利用しないと行けないみたい(ExpoをEjectせずに利用できるライブラリは無いみたいです。。。)

paymentIntent方式のフロー

では、現時点で主流のpaymentIntetを利用する決済フローを見てみます。間違ってたらご指摘を。
フローでの処理は大きく2つ。

  1. 金額を投げてpaymentIntentを作成する(紐付いたclient_secret(tokenではない)が戻る)
  2. client_securetを利用してconfirmCardPayment()を実行すると、裏でカード情報が一緒にStripeサーバに送られる

という感じ。

まず、カード情報をStripeサーバに投げて、戻ってきたtokenを利用して金額等を投げる仕様とは逆なので注意。

図式化したイメージ。

スクリーンショット 2020-01-01 10.31.08.png

実装

では上記を踏まえて実装してみます。

準備

Stripeのアカウントとかなければ作って下さい。あとはテスト用の公開キーと秘密キーがあればいいです。

  • Stripeのアカウントを作る(なければ)
  • ダッシュボードで左メニュー下段の「テストデータの表示」をOnした状態で「公開可能キー」と「シークレットキー」をメモしておく。
    • テストだとpk_test_xxxx, sk_test_xxxxという形式。本番だとtestの部分がliveになる。
  • 処理した結佐は左メニューの「支払い」から確認できる

完成図

完成予定は下記のような感じ。決済OKならアラート出します。

スクリーンショット 2020-01-01 10.39.19.png

1つのクリックで上記2つの通信をします(ので分かりづらい)。

クライアント側

ではクライアント側から。流れはこの記事と同じですが、決済方式がchargeではなくpaymentIntetntになります。雛形作成にはcreate-react-appを利用します。

必要なモジュールのインストール。

create-react-app stripe-client
cd stripe-client
npm install --save react-stripe-elements bootstrap reactstrap formik yup

実装。App.jsと同じ階層にCheckoutForm.jsを作成して下記のようにします。

CheckoutForm.js
import React from 'react';
import { CardElement, injectStripe, CardNumberElement, CardExpiryElement, CardCVCElement, Elements } from 'react-stripe-elements';
import { Button, Form, FormGroup, Label, Input, FormFeedback } from 'reactstrap';
import { Formik } from 'formik'
import * as Yup from 'yup';

class CheckoutForm extends React.Component {

    handlePayment = async (values) => {

        // alert(JSON.stringify(values));

        const headers = new Headers();
        headers.set('Content-type', 'application/json');
        // headers.set('Access-Control-Allow-Origin', '*');

        //paymentIntentの作成を(ローカルサーバ経由で)リクエスト
        const createRes = await fetch('http://localhost:9000/createPaymentIntent', {
            method: 'POST',
            headers: headers,
            body: JSON.stringify({ amount: values.amount, username: values.username })
        })

        //レスポンスからclient_secretを取得
        const responseJson = await createRes.json();
        const client_secret = responseJson.client_secret;

        //client_secretを利用して(確認情報をStripeに投げて)決済を完了させる
        const confirmRes = await this.props.stripe.confirmCardPayment(client_secret, {
            payment_method: {
                // card: this.props.elements.getElement('card'),
                card: this.props.elements.getElement('cardNumber'),
                billing_details: {
                    name: values.username,
                }
            }
        });

        if (confirmRes.paymentIntent.status === "succeeded") {
            alert("決済完了");
        }
    }

    render() {
        console.log(this.props.stripe);
        return (
            <div className="col-8">
                <p>決済情報の入力</p>
                <Formik
                    initialValues={{ amount: 100, username: 'TARO YAMADA' }}
                    onSubmit={(values) => this.handlePayment(values)}
                    validationSchema={Yup.object().shape({
                        amount: Yup.number().min(1).max(1000),
                    })}
                >
                    {
                        ({ handleChange, handleSubmit, handleBlur, values, errors, touched }) => (
                            <Form onSubmit={handleSubmit}>
                                <FormGroup>
                                    <Label>金額</Label>
                                    <Input
                                        type="text"
                                        name="amount"
                                        value={values.amount}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.amount && errors.amount)}
                                    />
                                    <FormFeedback>
                                        {errors.amount}
                                    </FormFeedback>
                                </FormGroup>
                                <FormGroup>
                                    <Label>利用者名</Label>
                                    <Input
                                        type="text"
                                        name="username"
                                        value={values.username}
                                        onChange={handleChange}
                                        onBlur={handleBlur}
                                        invalid={Boolean(touched.username && errors.username)}
                                    />
                                    <FormFeedback>
                                        {errors.username}
                                    </FormFeedback>
                                </FormGroup>
                                {/* <CardElement
                                    className="bg-light p-3"
                                    hidePostalCode={true}
                                /> */}
                                <legend className="col-form-label">カード番号</legend>
                                <CardNumberElement
                                    ref={this.cardNumberRef}
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">有効期限</legend>
                                <CardExpiryElement
                                    className="p-2 bg-light"
                                />
                                <legend className="col-form-label">セキュリティーコード</legend>
                                <CardCVCElement
                                    className="p-2 bg-light"
                                />

                                <Button
                                    onClick={this.submit}
                                    className="my-3"
                                    color="primary"
                                >
                                    購入
                                </Button>
                            </Form>
                        )
                    }
                </Formik>

            </div>
        );
    }
}

export default injectStripe(CheckoutForm);

App.jsでCheckoutForm.jsを読み込みます。また、鍵の設定等も行います。

App.js
import React from 'react';
import { Elements, StripeProvider } from 'react-stripe-elements';
import CheckoutForm from './CheckoutForm';

function App() {
  return (
    <StripeProvider apiKey="pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx">
      <div className="container">
        <h3 className="my-4">React Stripe Element Sample</h3>
        <Elements>
          <CheckoutForm />
        </Elements>
      </div>
    </StripeProvider>
  );
}

export default App;

これでクライアント側は一旦完了。ボタンを押すと404エラーが出るはずです。

サーバ側

続いてサーバ側。
まず、必要なモジュールをインストールします。

mkdir stripe-server
cd stripe-server
npm init -f
npm install express body-parser stripe

メイン実装。

index.js
const app = require("express")();
const stripe = require("stripe")("sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
const cors = require('cors');
const bodyParser = require('body-parser');

app.use(require("body-parser").text());
app.use(cors());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());

app.post('/createPaymentIntent', async (req, res) => {

    const result = await stripe.paymentIntents.create({
        amount: req.body.amount,
        currency: 'jpy',
        description: '●●商店決済', //option
        metadata: { username: req.body.username, tranId: '11111' } //option
    });

    console.log(result);
    res.json(result);

});

app.listen(9000, () => console.log("Listening on port 9000"));

stripe.paymentIntetns.create()が裏でStripeサーバと通信をしてIntentを作成しています。
作成が完了したらクライアント側でに結果を戻します。

動作確認

クライアント側

npm start

サーバ側

node index.js

Stripeダッシュボード

スクリーンショット 2020-01-01 11.17.56.png

その他

サーバ側をFirebase Functionsに展開してみましたが、問題なく動きました。
あと、Functionsは1回以上実行される可能性もあるので冪等性を確保するためのkeyを付与したほうがいいという話があります。

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

DockerでReactの環境を作成してみた

はじめに

この記事ではJavaScriptのライブラリであるReactの環境をDockerを使用して構築したいと思います。

目次

  1. Dockerfileの用意
  2. Dockerfileをビルド
  3. コンテナの起動
  4. Reactアプリケーションの作成
  5. Reactアプリケーションの起動
  6. ブラウザで確認
  7. まとめ

1. Dockerfileの用意

今回はcreate-react-appを使用してReactの環境を構築していきます。
ディレクトリ構成は以下です。

ディレクトリ構成
.
├── app         # Reactアプリケーションのフォルダ
└── Dockerfile  # React環境のDockerfile
Dockerfile
# nodeのverを指定してDockerのイメージをpull
FROM node:13.5.0

# Reactアプリケーション作成時に最低限の環境を提供してくれるライブラリをインストール
RUN yarn global add create-react-app

# コンテナ接続時のディレクトリを指定
WORKDIR /home

# アプリケーションの起動時にコンテナで開放するポートを指定
EXPOSE 3000

2. Dockerfileをビルド

DockerfileからDockerイメージを作成します。
イメージ名はreact-tutorialにします。
以下のコマンドをDockerfileが存在するディレクトリで実行してください。

Dockerイメージのビルド
$ docker build --rm -f "react-tutorial/Dockerfile" -t react-tutorial:latest "react-tutorial"

3. コンテナの起動

起動すると以下のようになると思います。
これで、Reactアプリケーションが作成できる環境のコンテナに接続できたことになります。

Dockerコンテナの起動
$ docker run --rm -it -v ${PWD}/app:/home/react-tutorial  -p 3000:3000/tcp react-tutorial:latest /bin/bash
root@03887209ce2d:/home# 

4. Reactアプリケーションの作成

それでは、Reactアプリケーションの作成をしてきます。
作成にはcreate-react-appを使用します。

Reactアプリケーションの作成
root@03887209ce2d:/home# create-react-app react-tutorial

コマンド実行中の表示(長いので畳んでおきます)
Creating a new React app in /home/react-tutorial.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
warning sha.js@2.4.11: Invalid bin entry for "sha.js" (in "sha.js").
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 35 new dependencies.
info Direct dependencies
├─ cra-template@1.0.0
├─ react-dom@16.12.0
├─ react-scripts@3.3.0
└─ react@16.12.0
info All dependencies
├─ @babel/plugin-proposal-class-properties@7.7.4
├─ @babel/plugin-proposal-decorators@7.7.4
├─ @babel/plugin-proposal-nullish-coalescing-operator@7.7.4
├─ @babel/plugin-proposal-numeric-separator@7.7.4
├─ @babel/plugin-proposal-optional-chaining@7.7.4
├─ @babel/plugin-syntax-decorators@7.7.4
├─ @babel/plugin-syntax-flow@7.7.4
├─ @babel/plugin-syntax-nullish-coalescing-operator@7.7.4
├─ @babel/plugin-syntax-numeric-separator@7.7.4
├─ @babel/plugin-syntax-optional-chaining@7.7.4
├─ @babel/plugin-transform-flow-strip-types@7.7.4
├─ @babel/plugin-transform-runtime@7.7.4
├─ @babel/plugin-transform-typescript@7.7.4
├─ @babel/preset-typescript@7.7.4
├─ @types/parse-json@4.0.0
├─ babel-plugin-macros@2.7.1
├─ babel-plugin-named-asset-import@0.3.5
├─ babel-preset-react-app@9.1.0
├─ core-js@3.6.1
├─ cra-template@1.0.0
├─ eslint-config-react-app@5.1.0
├─ fork-ts-checker-webpack-plugin@3.1.0
├─ lines-and-columns@1.1.6
├─ open@7.0.0
├─ promise@8.0.3
├─ raf@3.4.1
├─ react-app-polyfill@1.0.5
├─ react-dev-utils@10.0.0
├─ react-dom@16.12.0
├─ react-error-overlay@6.0.4
├─ react-scripts@3.3.0
├─ react@16.12.0
├─ scheduler@0.18.0
├─ whatwg-fetch@3.0.0
└─ yaml@1.7.2
Done in 36.57s.

Installing template dependencies using yarnpkg...
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
warning " > @testing-library/user-event@7.2.1" has unmet peer dependency "@testing-library/dom@>=5".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 18 new dependencies.
info Direct dependencies
├─ @testing-library/jest-dom@4.2.4
├─ @testing-library/react@9.4.0
├─ @testing-library/user-event@7.2.1
├─ react-dom@16.12.0
└─ react@16.12.0
info All dependencies
├─ @sheerun/mutationobserver-shim@0.3.2
├─ @testing-library/dom@6.11.0
├─ @testing-library/jest-dom@4.2.4
├─ @testing-library/react@9.4.0
├─ @testing-library/user-event@7.2.1
├─ @types/prop-types@15.7.3
├─ @types/react-dom@16.9.4
├─ @types/react@16.9.17
├─ @types/testing-library__dom@6.11.0
├─ @types/testing-library__react@9.1.2
├─ css.escape@1.5.1
├─ csstype@2.6.8
├─ min-indent@1.0.0
├─ react-dom@16.12.0
├─ react@16.12.0
├─ redent@3.0.0
├─ strip-indent@3.0.0
└─ wait-for-expect@3.0.1
Done in 9.58s.
Removing template package using yarnpkg...

yarn remove v1.21.1
[1/2] Removing module cra-template...
[2/2] Regenerating lockfile and installing missing dependencies...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
warning " > @testing-library/user-event@7.2.1" has unmet peer dependency "@testing-library/dom@>=5".
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
success Uninstalled packages.
Done in 7.62s.

Success! Created react-tutorial at /home/react-tutorial
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd react-tutorial
  yarn start

Happy hacking!

5. Reactアプリケーションの起動

Reactアプリケーションが作成されたので、実際に起動します。

Reactアプリケーションの起動
# ディレクトリ確認
root@03887209ce2d:/home# ls
node  react-tutorial
root@03887209ce2d:/home# cd react-tutorial/
# Reactアプリケーションの起動
root@03887209ce2d:/home/react-tutorial# yarn start

6. ブラウザで確認

yarn startを実行すると以下のように出力されます。

Reactアプリケーションの起動後
Compiled successfully!

You can now view react-tutorial in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://172.17.0.3:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

あとは、ブラウザからhttp://localhost:3000/にアクセスしてみましょう。
以下のような表示がされれば完了です。
スクリーンショット 2020-01-01 3.36.21.png

7. まとめ

Dockerを使用してReactの環境をお手軽に作成できました。
ホストマシンの環境をいじらずにお試しで環境を構築できるのはめっちゃ便利ですよね!!
あとは、appフォルダ配下のファイルを編集してアプリケーションを作成していくのみです!!
指摘や質問があれば大歓迎なので、是非よろしくお願いします。
以上です。ありがとうございました!

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

DockerでReactの環境を作成してみた。

はじめに

この記事ではJavaScriptのライブラリであるReactの環境をDockerを使用して構築したいと思います。

目次

  1. Dockerfileの用意
  2. Dockerfileをビルド
  3. コンテナの起動
  4. Reactアプリケーションの作成
  5. Reactアプリケーションの起動
  6. ブラウザで確認
  7. まとめ

1. Dockerfileの用意

今回はcreate-react-appを使用してReactの環境を構築していきます。

# nodeのverを指定してDockerのイメージをpull
FROM node:13.5.0

# Reactアプリケーション作成時に最低限の環境を提供してくれるライブラリをインストール
RUN yarn global add create-react-app

# コンテナ接続時のディレクトリを指定
WORKDIR /home

# アプリケーションの起動時にコンテナで開放するポートを指定
EXPOSE 3000

2. Dockerfileをビルド

DockerfileからDockerイメージを作成します。
イメージ名はreact-tutorialにします。
以下のコマンドをDockerfileが存在するディレクトリで実行してください。

$ docker build --rm -f "react-tutorial/Dockerfile" -t react-tutorial:latest "react-tutorial"

3. コンテナの起動

$ docker run --rm -it -p 3000:3000/tcp react-tutorial:latest /bin/bash

起動すると以下のようになると思います。
これで、Reactアプリケーションが作成できる環境のコンテナに接続できたことになります。

$ docker run --rm -it -v ${PWD}/src:/home/react-tutorial  -p 3000:3000/tcp react-tutorial:latest /bin/bash
root@03887209ce2d:/home# 

4. Reactアプリケーションの作成

それでは、Reactアプリケーションの作成をしてきます。

root@03887209ce2d:/home# create-react-app react-tutorial

コマンド実行中の表示(長いので畳んでおきます)
Creating a new React app in /home/react-tutorial.

Installing packages. This might take a couple of minutes.
Installing react, react-dom, and react-scripts with cra-template...

yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
warning sha.js@2.4.11: Invalid bin entry for "sha.js" (in "sha.js").
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 35 new dependencies.
info Direct dependencies
├─ cra-template@1.0.0
├─ react-dom@16.12.0
├─ react-scripts@3.3.0
└─ react@16.12.0
info All dependencies
├─ @babel/plugin-proposal-class-properties@7.7.4
├─ @babel/plugin-proposal-decorators@7.7.4
├─ @babel/plugin-proposal-nullish-coalescing-operator@7.7.4
├─ @babel/plugin-proposal-numeric-separator@7.7.4
├─ @babel/plugin-proposal-optional-chaining@7.7.4
├─ @babel/plugin-syntax-decorators@7.7.4
├─ @babel/plugin-syntax-flow@7.7.4
├─ @babel/plugin-syntax-nullish-coalescing-operator@7.7.4
├─ @babel/plugin-syntax-numeric-separator@7.7.4
├─ @babel/plugin-syntax-optional-chaining@7.7.4
├─ @babel/plugin-transform-flow-strip-types@7.7.4
├─ @babel/plugin-transform-runtime@7.7.4
├─ @babel/plugin-transform-typescript@7.7.4
├─ @babel/preset-typescript@7.7.4
├─ @types/parse-json@4.0.0
├─ babel-plugin-macros@2.7.1
├─ babel-plugin-named-asset-import@0.3.5
├─ babel-preset-react-app@9.1.0
├─ core-js@3.6.1
├─ cra-template@1.0.0
├─ eslint-config-react-app@5.1.0
├─ fork-ts-checker-webpack-plugin@3.1.0
├─ lines-and-columns@1.1.6
├─ open@7.0.0
├─ promise@8.0.3
├─ raf@3.4.1
├─ react-app-polyfill@1.0.5
├─ react-dev-utils@10.0.0
├─ react-dom@16.12.0
├─ react-error-overlay@6.0.4
├─ react-scripts@3.3.0
├─ react@16.12.0
├─ scheduler@0.18.0
├─ whatwg-fetch@3.0.0
└─ yaml@1.7.2
Done in 36.57s.

Installing template dependencies using yarnpkg...
yarn add v1.21.1
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
warning " > @testing-library/user-event@7.2.1" has unmet peer dependency "@testing-library/dom@>=5".
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 18 new dependencies.
info Direct dependencies
├─ @testing-library/jest-dom@4.2.4
├─ @testing-library/react@9.4.0
├─ @testing-library/user-event@7.2.1
├─ react-dom@16.12.0
└─ react@16.12.0
info All dependencies
├─ @sheerun/mutationobserver-shim@0.3.2
├─ @testing-library/dom@6.11.0
├─ @testing-library/jest-dom@4.2.4
├─ @testing-library/react@9.4.0
├─ @testing-library/user-event@7.2.1
├─ @types/prop-types@15.7.3
├─ @types/react-dom@16.9.4
├─ @types/react@16.9.17
├─ @types/testing-library__dom@6.11.0
├─ @types/testing-library__react@9.1.2
├─ css.escape@1.5.1
├─ csstype@2.6.8
├─ min-indent@1.0.0
├─ react-dom@16.12.0
├─ react@16.12.0
├─ redent@3.0.0
├─ strip-indent@3.0.0
└─ wait-for-expect@3.0.1
Done in 9.58s.
Removing template package using yarnpkg...

yarn remove v1.21.1
[1/2] Removing module cra-template...
[2/2] Regenerating lockfile and installing missing dependencies...
info fsevents@2.1.2: The platform "linux" is incompatible with this module.
info "fsevents@2.1.2" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@1.2.9: The platform "linux" is incompatible with this module.
info "fsevents@1.2.9" is an optional dependency and failed compatibility check. Excluding it from installation.
warning " > @testing-library/user-event@7.2.1" has unmet peer dependency "@testing-library/dom@>=5".
warning "react-scripts > @typescript-eslint/eslint-plugin > tsutils@3.17.1" has unmet peer dependency "typescript@>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta".
success Uninstalled packages.
Done in 7.62s.

Success! Created react-tutorial at /home/react-tutorial
Inside that directory, you can run several commands:

  yarn start
    Starts the development server.

  yarn build
    Bundles the app into static files for production.

  yarn test
    Starts the test runner.

  yarn eject
    Removes this tool and copies build dependencies, configuration files
    and scripts into the app directory. If you do this, you can’t go back!

We suggest that you begin by typing:

  cd react-tutorial
  yarn start

Happy hacking!

5. Reactアプリケーションの起動

Reactアプリケーションが作成されたので、実際に起動します。

# ディレクトリ確認
root@03887209ce2d:/home# ls
node  react-tutorial
root@03887209ce2d:/home# cd react-tutorial/
# Reactアプリケーションの起動
root@03887209ce2d:/home/react-tutorial# yarn start

6. ブラウザで確認

yarn startを実行すると以下のように出力されます。

Compiled successfully!

You can now view react-tutorial in the browser.

  Local:            http://localhost:3000/
  On Your Network:  http://172.17.0.3:3000/

Note that the development build is not optimized.
To create a production build, use yarn build.

あとは、ブラウザからhttp://localhost:3000/にアクセスしてみましょう。
以下のような表示がされれば完了です。
スクリーンショット 2020-01-01 3.36.21.png

7. まとめ

Dockerを使用してReactの環境をお手軽に作成できました。
ホストマシンの環境をいじらずにお試しで環境を構築できるのはめっちゃ便利ですよね!!
あとは、srcフォルダ配下のファイルを編集してアプリケーションを作成していくのみです!!
指摘や質問があれば大歓迎なので、是非よろしくお願いします。
以上です。ありがとうございました!

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

React + TensorFlow.jsでAdversarial Example (FGSM)をやった

はじめに

もうすぐ2019年が終わりますね!
あけました。はい。
そういうわけでTensorFlow.jsでAdversarial Exampleを体験できるデモを公開しました。
以下で遊べます。(特に学習の際は)重いので注意してください。真っ白なページになる場合は何度かリロードすると画面がちゃんと表示されることがあります。
https://adv-examples-fun.netlify.com/

リポジトリ:https://github.com/Catminusminus/adv-examples-fun

最初の画面
Screenshot from 2019-12-31 23-14-50.png

順次ボタンを押していき、データのロード〜Adversarial Exampleの生成まで終わった後
Screenshot from 2019-12-31 23-17-40.png

以下、詳しい説明です。

Adversarial Example

Adversarial Exampleについて、画像分類の場合の説明をします。ここに人間の目にも、機械学習モデルにも1に見える手書き数字の画像があるとします。これに微小な摂動を加えることで、人間の目には1に見えるままで、機械学習モデルは別の数字、例えば9と誤分類させることができます。実際、これが上のスクリーンショットで起こっていることです(もとの画像は1と分類しているが、生成された摂動を0.3倍して足すと、9と誤分類している)。
今は手書きの数字で説明しましたが、それ以外でももちろんできます。よくパンダをテナガザルに誤分類させる例が挙げられます。
今回使用しているAdversarial Exampleの手法はFast Gradient Sign Method(FGSM)というものです。これは攻撃の対象となるモデルのlossを使うため、white-box attackであり、また特定のラベルへ誤分類を誘導できるのではなく、とにかく正解のラベルと違うラベルに分類させるという、untargeted attackです。
詳細は論文を参照してください。

TensorFlow.js

TensorFlow.jsはJavaScriptの機械学習ライブラリです。なのでブラウザで動きます。今回はこれを用いて機械学習モデルの構築とFGSM Attackを行いました。
モデルの構築は、https://github.com/tensorflow/tfjs-examples/tree/master/mnist をTypeScriptで動くようにしただけです。ほとんどanyでサボっていますが…
で、FGSMは、TensorFlowによる実装が https://www.tensorflow.org/tutorials/generative/adversarial_fgsm で公開されています。これをTensorFlow.jsで書けば終わりです。
余談ですが、私が見たときにはFGSMの実装が間違っていて、修正するPRを出し、(修正後)マージされました。しばらく反映を待っていましたが、今見たら反映されていました。なので皆さんも上のチュートリアルでFGSMをやっていきましょう。

攻撃部分だけ簡単に解説します。コードは以下です。

const testExamples = 100
const examples = data.getTestData(testExamples)
tf.tidy(() => {
  const output = model.predict(examples.xs)
  const axis = 1
  const labels = Array.from(examples.labels.argMax(axis).dataSync())
  const predictions = Array.from(output.argMax(axis).dataSync())
  const accIndices = selectAccurateExample(labels, predictions)
  const index = accIndices[Math.floor(Math.random() * accIndices.length)]
  const image = examples.xs.slice([index, 0], [1, examples.xs.shape[1]])
  const loss = (input: any) =>
    tf.metrics.categoricalCrossentropy(
      examples.labels.slice([index, 0], [1, examples.labels.shape[1]]),
      model.predict(input),
    )
  const grad = tf.grad(loss)
  const signedGrad = tf.sign(grad(image))
  const scalar = tf.scalar(0.3, 'float32')
  const outputAdv = model.predict(signedGrad.mul(scalar).add(image))
  const predictionsAdv = Array.from(outputAdv.argMax(axis).dataSync())
})

まず100件テストデータを取ってきます。

const testExamples = 100
const examples = data.getTestData(testExamples)

次に、メモリリークを防ぐため、tf.tidyで囲っています。
そうしたら、100件のデータのうち、モデルの予測が正解ラベルと同じデータを取ってきます。そもそも予測があってないと攻撃も何もないからです。

  const output = model.predict(examples.xs)
  const axis = 1
  const labels = Array.from(examples.labels.argMax(axis).dataSync())
  const predictions = Array.from(output.argMax(axis).dataSync())
  const accIndices = selectAccurateExample(labels, predictions)
  const index = accIndices[Math.floor(Math.random() * accIndices.length)]
  const image = examples.xs.slice([index, 0], [1, examples.xs.shape[1]])

次が、チュートリアルでいうところの

loss_object = tf.keras.losses.CategoricalCrossentropy()

def create_adversarial_pattern(input_image, input_label):
  with tf.GradientTape() as tape:
    tape.watch(input_image)
    prediction = pretrained_model(input_image)
    loss = loss_object(input_label, prediction)

  # Get the gradients of the loss w.r.t to the input image.
  gradient = tape.gradient(loss, input_image)
  # Get the sign of the gradients to create the perturbation
  signed_grad = tf.sign(gradient)
  return signed_grad

に当たる部分です。それが以下です。

  const loss = (input: any) =>
    tf.metrics.categoricalCrossentropy(
      examples.labels.slice([index, 0], [1, examples.labels.shape[1]]),
      model.predict(input),
    )
  const grad = tf.grad(loss)
  const signedGrad = tf.sign(grad(image))

あとは摂動を元画像に加え、それをモデルに食わせるだけです。

  const scalar = tf.scalar(0.3, 'float32')
  const outputAdv = model.predict(signedGrad.mul(scalar).add(image))
  const predictionsAdv = Array.from(outputAdv.argMax(axis).dataSync())

Reactで表示

上記で作った画像を表示するため、React Konvaを使いました。つまりcanvasを使っています。当初プラスの記号等を@material-uiのアイコンとしてLayerの間に突っ込めないかやっていましたが、無理っぽいです(無理という情報源も当時見つけていた気がするのですが、覚えていません)。諦めてLineで線を引いています。方法があれば教えていただけると喜びます。

おわりに

記事を書き終わったら2019年も終わっていました。
ところで、TensorFlow.jsでAdversarial Exampleやってる人は他にいないと思ってたらなんと先駆者がいました。遥かにこちらの方がすごいので見ることをオススメします。
今後はデザイン周りをもう少しどうにかするとか他の攻撃手法への対応とかoff-the-main-thread対応とか無駄にPWA対応とかしていきたいです。
それではよい2020年を。

Citation

FGSM is described by

@misc{goodfellow2014explaining,
    title={Explaining and Harnessing Adversarial Examples},
    author={Ian J. Goodfellow and Jonathon Shlens and Christian Szegedy},
    year={2014},
    eprint={1412.6572},
    archivePrefix={arXiv},
    primaryClass={stat.ML}
}.
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む