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

React + TypeScriptでMarkdownをMathjax + Highlight付きで表示する(メモ)

こういう時、あるよね?(たぶん)

React + TypeScriptで、サーバー上に置いたMarkdownを綺麗に表示させたい!(数式、コードも)

で、結構詰まったのでメモ。

ファイルの読み込み

XMLHttpRequestを使って、"source"にMarkdownを読み込みます。

src/Md.tsx
import React, { useEffect } from 'react';
import './Md.css'

export default function Md() {
    const [source, setSource] = React.useState<string>("");
    useEffect(() => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "./Problems/p1.md");
        xhr.send();
        xhr.onload = function () {
            if (xhr.status !== 200) {
                alert("Error");
            } else {
                setSource(xhr.responseText);
            }
        }
    }, [])
    return (
        <div className="main">
            \(ax^2+bx+c=0\)
        </div>
    );
}

Mathjaxを入れる

index.htmlに、これを入れるだけです。簡単!

<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax
/2.7.7/MathJax.js?config=TeX-AMS_CHTML"></script>

さっきの

\(ax^2+bx+c=0\)

が数式っぽく表示されるようになりました。

MarkdownをHTMLに変換して表示する

これはなんでかわからないんですが、

npm i --save @types/marked marked

でJSとTS両方入れておきます。どっちか欠けてるとできませんでした。なんでだろ

tsxファイルは以下のように修正します。

src/Md.tsx
import React, { useEffect } from 'react';
import marked from 'marked';
import './Md.css'

export default function Md() {
    const [source, setSource] = React.useState<string>("");
    useEffect(() => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "./Problems/p1.md");
        xhr.send();
        xhr.onload = function () {
            if (xhr.status !== 200) {
                alert("Error");
            } else {
                setSource(marked(xhr.responseText));
            }
        }
    }, [])
    return (
        <div className="main">
            <div dangerouslySetInnerHTML={{ __html: source }} />
        </div>
    );
}

これで数式が表示されるはず...と思ったらされなくて、数式のバックスラッシュがmarkedで消えてしまうっぽいのでバックスラッシュを2本書くようにします。

Syntax Highlightを付ける

コードを表示する時、Syntax Highlight欲しいですよね?私は欲しいです。

そこで、highlight.jsを使います。

npm i --save hightlight.js @types/hightlight.js

後は、これでOKです。

ハイライトのスタイルはいっぱいあるので、色々試してみて下さい。

src/Md.tsx
import React, { useEffect } from 'react';
import marked from 'marked';
import highlightjs from 'highlight.js';
import 'highlight.js/styles/atelier-lakeside-dark.css'
import './Md.css'

marked.setOptions({
    highlight: function (code, lang) {
        return highlightjs.highlightAuto(code, [lang]).value;
    },
});

export default function Md() {
    const [source, setSource] = React.useState<string>("");
    useEffect(() => {
        const xhr = new XMLHttpRequest();
        xhr.open("GET", "./Problems/p1.md");
        xhr.send();
        xhr.onload = function () {
            if (xhr.status !== 200) {
                alert("Error");
            } else {
                setSource(marked(xhr.responseText));
            }
        }
    }, [])
    return (
        <div className="main">
            <div dangerouslySetInnerHTML={{ __html: source }} />
            \(ax^2+bc+c=0\)
        </div>
    );
}

これで、無事にMarkdownを表示できました。やったー!

コードのフォントはUbuntu Monoを使いました。

image.png

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

Bot Framework Composer と Web Chat で 3D モデルを WebXR (AR/VR) 出力する

Bot Framework Composer で簡単に作成できる Web Chat ボットに、WebXR を使って 3D モデルを表示する機能を追加する方法を紹介します。

Bot Framework Web Chat の標準機能では、まだ 3D モデルを扱えないため、ボットから Attachment として glTF/GLB フォーマットのデータを返却し、Web Chat 側で React Component を追加してハンドルします。3D モデルの表示と AR/VR 出力部分は <model-viewer>Babylon.js を使います。

よかったらデモ動画をご覧ください。また、https://edgewatcher.azurewebsites.net/webxrbot.htm で実際に試せます。

前準備

以下を利用できるように準備します。Azure のアカウントをお持ちでない方は、Azure 無料アカウント作成から試用できます。

Azure Bot Service については、@chomado さんの完全無料でボットを作る記事、Composer の導入や使い方については、@kenakamu さんのボット開発の記事がわかりやすいです。また、私が作成した Composer の解説動画もあるので、よかったらこちらもどうぞ。

ボットのダイアログ

Composer でボットのプロジェクトを作成したら、ボットの応答 (Bot Responses) に以下のテンプレートを追加します。

# WebXRCard(mode, url, alturl, description)
[Activity
    Attachments = ${ActivityAttachment(json('{
      "mode": "' + mode + '",
      "url": "' + url + '",
      "alturl": "' + alturl + '",
      "description": "' + description + '"
    }'), 'model/gltf-binary')}
]

続いて、ボットの応答で 3D モデルを返却したい部分で、以下のように記述します。第 1 引数で 'ar' もしくは 'vr' を指定します。第 2 引数で glTF/GLB フォーマットのファイルを指定します。iOS での AR 出力に対応したい場合は、第 3 引数に USDZ フォーマットのファイルも用意して指定します。

- ${WebXRCard('ar', 'https://edgewatcher.azurewebsites.net/balustervase.glb', 'https://edgewatcher.azurewebsites.net/balustervase.usdz', 'スミソニアーンの伝説の壺')}

ダイアログのデザイン

ボットの実行と Direct Line チャネルの登録

作成したボットをローカルで実行して Bot Framework Emulator でテストすると、3D モデルの応答部分で以下のように表示されるはずですが、問題ありません。エミュレーターでは扱えないフォーマットであるためです。

エミュレーターでテスト

このボットを Web Chat (Direct Line) で接続できるようにするために、Azure Bot Service に公開します。Composer から直接 Azure にデプロイしてもよいのですが、Azure にホストせず、ローカルで実行しているボットを ngrok を使用して一時的に Bot Channels Registration で公開することも可能です。いずれかの方法でボットを公開し、Direct Line チャネルを登録してシークレットをメモしておきます。

Direct Line

Web Chat ページの作成

Bot Framework Web Chat コンポーネントは、高度なカスタマイズが可能な、Web ベースの Bot クライアントです。React ベースで開発されており、自分でビルドすることもできますが、CDN でビルド済みの JavaScript を公開しているので、ビルド環境を用意しなくても使えます。

3D モデルの Attachment をハンドルする部分を React で拡張するため、このサンプルをベースにカスタマイズします。初めに、3D モデルの表示に必要な <model-viewer> と Babylon.js を <head> タグ内でインポートしておきます。

index.html
    <!-- model-viewer -->
    <script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.1.3/webcomponents-loader.js"></script>
    <script src="https://unpkg.com/intersection-observer@0.5.1/intersection-observer.js"></script>
    <script src="https://unpkg.com/resize-observer-polyfill@1.5.0/dist/ResizeObserver.js"></script>
    <script src="https://unpkg.com/fullscreen-polyfill@1.0.2/dist/fullscreen.polyfill.js"></script>
    <script src="https://unpkg.com/@magicleap/prismatic/prismatic.min.js"></script>
    <script src="https://unpkg.com/focus-visible@5.0.2/dist/focus-visible.js" defer></script>

    <script type="module" src="https://unpkg.com/@google/model-viewer/dist/model-viewer.js"></script>
    <script nomodule src="https://unpkg.com/@google/model-viewer/dist/model-viewer-legacy.js"></script>

    <!-- Babylon.js -->
    <script src="https://code.jquery.com/pep/0.4.2/pep.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.6.2/dat.gui.min.js"></script>
    <script src="https://cdn.babylonjs.com/ammo.js"></script>
    <script src="https://cdn.babylonjs.com/cannon.js"></script>
    <script src="https://cdn.babylonjs.com/Oimo.js"></script>
    <script src="https://cdn.babylonjs.com/libktx.js"></script>
    <script src="https://cdn.babylonjs.com/earcut.min.js"></script>
    <script src="https://cdn.babylonjs.com/babylon.js"></script>
    <script src="https://cdn.babylonjs.com/inspector/babylon.inspector.bundle.js"></script>
    <script src="https://cdn.babylonjs.com/materialsLibrary/babylonjs.materials.min.js"></script>
    <script src="https://cdn.babylonjs.com/proceduralTexturesLibrary/babylonjs.proceduralTextures.min.js"></script>
    <script src="https://cdn.babylonjs.com/postProcessesLibrary/babylonjs.postProcess.min.js"></script>
    <script src="https://cdn.babylonjs.com/loaders/babylonjs.loaders.js"></script>
    <script src="https://cdn.babylonjs.com/serializers/babylonjs.serializers.min.js"></script>
    <script src="https://cdn.babylonjs.com/gui/babylon.gui.min.js"></script>

これから作成する React Component 部分は、別ファイルに分けた方がわかりやすいため、同じく <head> タグ内で以下のように定義します。

index.html
    <script type="text/babel" src="webxrcard.js"></script>

続いて、ボットの応答をインターセプトし、'model/gltf-binary' の Attachment として返却されたデータをハンドルします。サンプルの async function() の部分に以下のコードを追加します。<WebXRCard> の部分を React Component として webxrcard.js に実装します。

index.html
    <script type="text/babel" data-presets="es2015, react, stage-3">
      (async function() {

        const attachmentMiddleware = () => next => card => {
          switch (card.attachment.contentType) {
            case 'model/gltf-binary':
              return (
                <WebXRCard mode={card.attachment.content.mode} src={card.attachment.content.url} ios-src={card.attachment.content.alturl} alt={card.attachment.content.description} />
              );

            default:
              return next(card);
          }
        };

<ReactWebChat> コンポーネントに、追加した attachmentMiddleware を指定します。また、公式のサンプルでは、Direct Line のトークンを Web サービスから取得する実装になっていますが、テスト目的であれば、先ほどチャネル登録時にメモした Direct Line のシークレットを直接指定できます。直接シークレットを指定する場合は、以下のように変更します。'YOUR_DIRECT_LINE_TOKEN' の部分をメモした文字列に置き換えます。

index.html
        window.ReactDOM.render(
          <ReactWebChat
            attachmentMiddleware={attachmentMiddleware}
            directLine={window.WebChat.createDirectLine({ token: 'YOUR_DIRECT_LINE_TOKEN' })}
          />,
          document.getElementById('webchat')
        );

また、トークンを取得する部分の処理は削除しておきます。

index.html
/*
        const res = await fetch('https://webchat-mockbot.azurewebsites.net/directline/token', { method: 'POST' });
        const { token } = await res.json();
*/

WebXRCard コンポーネントの実装

別ファイルに分けた webxrcard.js に、コンポーネント部分を実装します。今回は AR の場合は <model-viewer> を、VR の場合は Babylon.js を使用しています。3D モデルを表示するだけであれば、チュートリアルで解説されているレベルのシンプルなコードで実現できます。コード全体を掲載します。

webxrcard.js
function getUniqueStr(myStrong) {
    var strong = 1000;
    if (myStrong) strong = myStrong;
    return new Date().getTime().toString(16) + Math.floor(strong * Math.random()).toString(16);
}

function prepareBabylonCanvas(id, src) {
    if ((id == null) || (src == null)) { return; }

    var canvas = document.getElementById(id);

    var engine = null;
    var scene = null;
    var sceneToRender = null;
    var createDefaultEngine = function () { return new BABYLON.Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true }); };

    var delayCreateScene = function () {
        // Create a scene
        var scene = new BABYLON.Scene(engine);
        scene.createDefaultCameraOrLight();

        // Append glTF model to scene
        Promise.all([
            BABYLON.SceneLoader.AppendAsync(src)
        ]).then(function () {
            scene.createDefaultCamera(true, true, true);

            // The default camera looks at the back of the asset
            // Rotate the camera by 180 degrees to the front of the asset
            scene.activeCamera.alpha += Math.PI;

            const env = scene.createDefaultEnvironment();

            // Here we add XR support
            const xr = scene.createDefaultXRExperienceAsync({
                floorMeshes: [env.ground]
            });
        });

        return scene;
    };

    var engine;

    try {
        engine = createDefaultEngine();
    } catch (e) {
        console.log("the available createEngine function failed. Creating the default engine instead");
        engine = createDefaultEngine();
    }

    if (!engine) throw 'engine should not be null.';

    scene = delayCreateScene();
    sceneToRender = scene;

    engine.runRenderLoop(function () {
        if (sceneToRender) {
            sceneToRender.render();
        }
    });

    // Resize
    window.addEventListener("resize", function () {
        engine.resize();
    });
}

function WebXRCard(props) {
    if (props.mode == "ar") {
        return <model-viewer {...props} auto-rotate autoplay camera-controls ar magic-leap></model-viewer>;
    } else if (props.mode == "vr") {
        const id = "renderCanvas-" + getUniqueStr();
        setTimeout(() => prepareBabylonCanvas(id, props.src), 1000);
        return <canvas id={id} className="renderCanvas" title={props.alt}></canvas>;
    }

    return <div aria-hidden="true" className="markdown css-1b7yvbl"><p>Invalid mode!</p></div>;
}

作成した Web Chat ページのテスト

以上で完成です。作成した index.html と webxrcard.js を適当な Web サーバーに配置して、ブラウザーからアクセスします。ローカル ファイルのままでは、React による動的なスクリプトの参照や、3D モデルのデータ取得ができないため、Web サーバーに配置して試す必要があります。

Web Chat ページのテスト

Astronaut by Poly, licensed under CC-BY.

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

【Vite】 Vite をReact x TSXで入門してみた!

Vite とは

Vite は、Vue.js フレームワークを開発したEvan You 氏が作ったフレームワークです。

Vue やReact, Preact でも開発を行うことができるだけでなく、公式の CLI でアプリを制作するよりもホットリロードやビルドにかかる時間が少なく、アプリをより効率的に開発することができます。さらに、Vue Composition API やReact Hooks も公式にサポートしているため、初期設定することなく利用することができます。

公式ドキュメント: https://github.com/vitejs/vite

この記事では、Vite をReact でTODO アプリを構築してみます。

環境構築してみる

まず、セットアップを行います。

npm init vite-app --template react <プロジェクト名> # 今回は vite-react としました。
cd <プロジェクト名>
npm install 

これにより、生成されたディレクトリは次のようになります。

$tree
.
├─node_modules
├─src 
│  ├─App.css
│  ├─App.jsx
│  ├─index.css
│  ├─logo.svg
│  └─main.jsx
│
├─.gitignore
├─index.html
├─package.json
├─package.json.lock
└─vite.config.js

ここで、開発サーバーを起動します。

npm run dev 

スクリーンショット (86).png

http://localhost:3000 で上のような画面が描写されれば成功です。

TypeScript で開発を行う

続いて、TypeScript での開発を行います。

まず、親ディレクトリにて、tsconfig.jsonを次のように実装します。

Vite では、ESBuild によって型情報なしにコンパイルされるため、const enum などの一部の型に対応していません。そのため、tsconfig.json では、compilerOptions で "isolatedModules": true とする
必要があります。

tsconfig.json
{
    "compilerOptions": {
        "target": "es5",
        "isolatedModules": true,
        "module": "commonjs",
        "outDir": "./dist",
        "strict": true,
        "esModuleInterop": true // ここを変更
    },
    "include": [
        "./src/**/*."
    ]
}

また、すべての.jsx ファイルを.tsx に変更します。

このとき、index.html ファイルで読み込むファイル名を/src/main.jsx から/src/main.tsx に変更しないと画面が表示されないので気をつけてください。

index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vite App</title>
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.tsx"></script> // 読み込むファイル名を "main.tsx" に変更する必要あり
</body>
</html>

注意:

Vite では、.ts ファイルをコンパイルすることしか対応していないので、型のチェックを行ってはくれません。 そのため、IDE を使って開発する時ややビルド実行時には、型のチェックが行われるように設定してください。

まとめ

以上で、React で TypeScript が実行できるように設定してみました。
今後、React Router や Redux を使うための設定はどうすればいいかといったことについて書いていきたいと思います。

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

Gatsby.js で作ったサイトから Tableau を読み込む方法

Gatsby.js で作ったサイトでに Tableau を埋め込む

総務省がPDFで公開しているマイナンバーカードの交付状況というファイルから CSV ファイルを作り、Tableau で取り込み表示しています。

Gatsby を使って生成したファイルを、Github Pages でホストしています。(リポジトリ)

char.js などを使ってグラフを自前で作成しても良かったのですが、データを色々いじりながら試行錯誤したかったので、Tableau をそのまま取り込むことにしてみました。

2020年7月12日現在、以下のように表示されています。

マイナンバーカード普及状況ダッシュボード
image.png

tableau-react コンポーネントを使う

React から Tableau のグラフを表示する、tableau-react というコンポーネントがあります。

こちらを使うことで、Gatsby でも Tableau のグラフをサイトに埋め込むことができます。

yarn add tableau-react

これを導入することにより、Tableau のシートをサイトに埋め込むことができるようになります。Tableau Public にシート公開(例えばこちらのグラフ)し、シェアボタンからシェア用のLinkを取得し、以下のようなコンポーネントを作ります。

PrefectureReport.tsx
//@ts-ignore  (TypeScript の定義が無いのでエラー回避)
import TableauReport from 'tableau-react'

const SimpleReport = () => (
  <TableauReport url="https://public.tableau.com/views/26693/sheet0?:language=en&:display_count=y&publish=yes&:origin=viz_share_link" />
)

export default SimpleReport

このコンポーネントを使うことで、Tableau をEmbedできます。今はグラフが一つなので固定にしていますが、URLはパラメータで渡せるようにしても良いかもしれませんね。

Code Splitting でyarn build のエラー対応をする

この状態で、gatsby develop ではサイトは表示できても、yarn build でファイルを生成しようとすると、以下のようなエラーがでます。

error Building static HTML failed

See our docs page on debugging HTML builds for help https://gatsby.dev/debug-html

  521 | tab.WindowHelper._cancelAnimationFrameFunc = null;
  522 | (function () {
> 523 |     if (('innerWidth' in window)) {
      | ^
  524 |         tab.WindowHelper._innerWidthFunc = function(w) {
  525 |             return w.innerWidth;
  526 |         };


  WebpackError: ReferenceError: window is not defined

  - tableau-2.0.0.js:523 
    [lib]/[tableau-api]/tableau-2.0.0.js:523:1

tableau.js は事前コンパイルする必要はないので、loadable-component を導入し Code Splitting します。

yarn add @loadable/component @types/loadable__component
index.tsx
import loadable from '@loadable/component'

import Page from '../components/Page'
import Container from '../components/Container'
import IndexLayout from '../layouts'
const PrefectureReport = loadable(() => import('../components/PrefectureReport'))

const IndexPage = () => (
  <IndexLayout>
    <Page>
      <Container>
        (省略)
        <PrefectureReport></PrefectureReport>
      </Container>
    </Page>
  </IndexLayout>
)

export default IndexPage

以上で、無事に build を行うことができました。

このプロジェクトでは、ご協力いただける仲間を募集中です。

GitHub:https://github.com/codeforjapan/mynumbercard_statistics/issues

もしご協力いただける方がいたら、ぜひ Issue にコメントいただければと思います。

Code for Japan の Slack に入り、Hal Seki(@hal_sk) にお声がけいただいても大丈です。

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

Recoilで動的に初期stateを与える方法

やりたいこと

みんな大好きstate管理の雄、Recoil
ユニットテストをするときや初期状態をどこかのオラクルから与えたい場合の書き方が公式docを見てもわかりづらかったのでメモ

こうする

RecoilRootinitializeStateとして、初期値をsetしてあげる。

state.js
import { atom, selector } from "recoil"

export const hogeState = atom({
  key: "hogeState",
  default: true,
})
App.js
import { RecoilRoot } from "recoil"
import { hogeState } from "./state"

const initializeState = ({ set }) => {
  // 必要に応じて対象のステート及び値を設定するロジックを追加
  set(hogeState, false)
}

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <RecoilRoot initializeState={initializeState}>
      <Component {...pageProps} />
    </RecoilRoot>
  )
}

export default MyApp


かんたんですね。

Recoil、とてもdeveloperフレンドリーなライブラリで好きです。

参考

https://recoiljs.org/docs/guides/persistence/

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

日本一わかりやすいReact-Redux講座 実践編 #4 学習備忘録

はじめに

この記事は、Youtubeチャンネル『トラハックのエンジニア学習ゼミ【とらゼミ】』の『日本一わかりやすいReact-Redux講座 実践編』の学習備忘録です。

ここから、いよいよ本格的にアプリを開発していきます。

前回記事はこちら

要点

  • 商品登録ページを作成(CRUDのC)
  • 商品登録用関数saveProduct()を定義
  • Cloud Firestoreへのデータ保存では、doc/set/addメソッドを使用する。

#4 Firestoreにデータをadd/setしよう

おさらいですが、今回の開発アプリのデータ設計は以下の通りでした。

データ設計
1. categoriesコレクション
2. productsコレクション
3. usersコレクション
  ├── 3-1. cartサブコレクション
  └── 3-2. ordersサブコレクション

これらのうち、2. productsコレクションについては、以下の通りのCRUD機能を実装します。

  • Create: 商品情報の追加
  • Read: 商品情報の読み込み
  • Update: 商品情報の更新
  • Delete: 商品情報の削除

本講座では、Create: 商品情報の追加に対応するページを作っていきます。

商品登録ページのテンプレートを作成

まず、Viewから作成します。

先に実装画面を見せると、こんな感じになります。

商品登録ページ
image.png

店舗商品として登録する商品の情報をフォームに入力し、「商品情報を保存」をクリックすることで、DBに商品情報をが登録(CRUDのC)される流れです。

入力フォームの内、「商品名」「商品説明」「価格」に関しては、すでに定義済みのUIコンポーネントであるTextInput.jsxで実装できそうです。

対して、「カテゴリー」「性別」については、いくつかの選択肢から入力値を決めるセレクトボックス形式の入力フォームとして実装すべきです。

こちらに関してはTextInput.jsxでは実装できなさそうなので、新たにSelectBox.jsxというコンポーネントを定義することにします。

新規作成・編集ファイル
1. src/components/UIkit/SelectBox.jsx
2. src/components/UIkit/index.js
src/components/UIkit/SelectBox.jsx
import React from "react";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import Select from "@material-ui/core/Select";
import {makeStyles} from "@material-ui/styles"

const useStyles = makeStyles({
  formControl:{
    marginBottom: 16,
    minWidth: 128,
    width: "100%"
  }
})

const SelectBox = (props) => {
  const classes = useStyles();

  return (
    <FormControl className={classes.formControl}>
      <InputLabel>{props.label}</InputLabel>
      <Select
        required={props.required} value={props.value}
        onChange={(event) => props.select(event.target.value)}
      >
        {props.options.map((option) => (
          <MenuItem key={option.id} value={option.id}>{option.name}</MenuItem>
        ))}
      </Select>
    </FormControl>
  )
}

export default SelectBox
  • Material UI の FormControl,InputLabel,MenuItem, Selectを組み合わせて実装。
  • props.optionsとして親コンポーンネントから渡されたものをmapメソッドで展開することで、リストを実現している。

※動作イメージ
SelectBox.gif

src/components/UIkit/index.js
export {default as PrimaryButton} from "./PrimaryButton"
export {default as SelectBox} from "./SelectBox" //追記
export {default as TextInput} from "./TextInput"

これでSelectBoxコンポーネントが完成しました。

これを踏まえて、商品登録ページのViewを作成します。

新規作成・編集ファイル
1. src/templates/ProductEdit.jsx //テンプレートファイル
2. src/components/src/templates/index.js //エントリーポイント
3. src/components/src/Router.jsx //ProductEditテンプレートのルーティングを定義
src/templates/ProductEdit.jsx
import React,{useState,useCallback} from "react";
import {PrimaryButton,SelectBox,TextInput} from "../components/UIkit";

const ProductEdit = () => {

  const [name, setName] = useState(""),
        [description, setDescription] = useState(""),
        [category, setCategory] = useState(""),
        [gender, setGender] = useState(""),
        [price, setPrice] = useState("");

  const inputName = useCallback((event) => {
    setName(event.target.value)
  },[setName])

  const inputDescription = useCallback((event) => {
    setDescription(event.target.value)
  },[setDescription])

  const inputPrice = useCallback((event) => {
    setPrice(event.target.value)
  },[setPrice])

  const categories = [
    {id:"tops", name:"トップス"},
    {id:"shirt", name:"シャツ"},
    {id:"pants", name:"パンツ"}

  ]
  const genders = [
    {id:"all", name:"すべて"},
    {id:"male", name:"メンズ"},
    {id:"female", name:"レディース"}
  ]

  return (
    <section>
      <h2 className="u-text__headline u-text-center">商品の登録・編集</h2>
      <div className="c-section-container">
        <TextInput
          fullWidth={true} label={"商品名"} multiline={false} required={true}
          onChange={inputName} rows={1} value={name} type={"text"}
          />
        <TextInput
          fullWidth={true} label={"商品説明"} multiline={true} required={true}
          onChange={inputDescription} rows={5} value={description} type={"text"}
          />
        <SelectBox
          label={"カテゴリー"} required={true} options={categories} select={setCategory}
        />
        <SelectBox
          label={"性別"} required={true} options={genders} select={setGender}
        />
        <TextInput
          fullWidth={true} label={"価格"} multiline={false} required={true}
          onChange={inputPrice} rows={1} value={price} type={"number"}
          />
      </div>
      <div className="module-spacer--medium" />
      <div className="center">
        <PrimaryButton
          label={"商品情報を保存"}
          onClick={() => {console.log('Clicked!')}}
        />
      </div>
    </section>
  )
}

export default ProductEdit
  • name, description, price に関してはこれまで通り、<TextInput>を利用して実装
  • category, gender については、<SelectBox>で実装。選択肢は定数として定義。
  • 「商品情報を登録」ボタンの onClick イベントには、ダミーとしてconsole.log('Clicked!')を設置
src/components/src/templates/index.js
export {default as Home} from './Home'
export {default as ProductEdit} from './ProductEdit' //追記
export {default as Reset} from './Reset'
export {default as SignIn} from './SignIn'
export {default as SignUp} from './SignUp'

src/components/src/Router.jsx
import React from 'react';
import {Route, Switch} from "react-router";
import {Home,ProductEdit,Reset,SignIn,SignUp} from "./templates";
import Auth from "./Auth"

const Router = () => {
  return (
    <Switch>
      <Route exact path={"/signup"} component={SignUp} />
      <Route exact path={"/signin"} component={SignIn} />
      <Route exact path={"/signin/reset"} component={Reset} />

      <Auth>
        <Route exact path={"(/)?"} component={Home} />
        <Route exact path={"/product/edit"} component={ProductEdit} /> //追記
      </Auth>
    </Switch>
  );
};

export default Router
  • (/product/edit)をルーティング。
  • 認証ユーザー(特に店舗側のユーザー)しか利用できないようにしたいので、<Auth>でラッピングする。

以上でViewの実装は完了です。動作確認します。

http://localhost:3000/product/edit

image.png

image.png

いい感じにできています!また、「商品情報を保存」をクリックすると、デベロッパーツールにコンソールにClicked!が表示されます。

saveProduct関数の実装

products state の追加

「商品情報を保存」ボタンの中身を作っていきます。

入力したフォームを受け取り、DBに保存する関数を定義すればよさそうです。

reducksパターンに則って考えると、この関数はproductsカテゴリーに関するものになるため、src/reducks/products/operations.jsに記述するべき、と分かります。

しかし、現時点のreducksパターンにおいて、productsという state を管理するファイルは未定義です。

というわけで、 products という新しい state を管理するためのReduxファイルを、reducksパターンに則って作りましょう。

必要な初期設定は以下の通りです。

1. src/reducks 直下に products ディレクトリを作成
2. src/reducks/products 直下に actions.js, operations.js, reducers.js, selectors.js, (type.js) を作成
3. src/reducks/store/initialState.js で、products の初期値を定義
4. src/reducks/products/reducers.js で ProductsReducers を定義
5. src/reducks/store/store.js で ProductsReducers を読み込む
src/reducks/store/initialState.js
const initialState = {
  products: {
    list: []
  },
.
.
.
};

export default initialState
src/reducks/products/reducers.js
import initialState from '../store/initialState'

export const ProductsReducer = (state = initialState.products, action) => {
  switch (action.type) {
    default:
      return state
  }
}
src/reducks/store/store.js
.
.
.
import {ProductsReducer} from "../products/reducers"; //追記
.
.
.

export default function createStore(history) {
  return reduxCreateStore(
    combineReducers({
      router: connectRouter(history),
      users: UsersReducer,
      products:ProductsReducer //追記
    }),
.
.
.
  )
}

これで、 products state が Redux の Store と接続されました。

actions.js, operations.js, selectors.jsは、ひとまず空白ファイルとして保存しておきます。

saveProduct()関数の実装

reducksパターンによる関数実装をおさらいすると、

編集ファイル
1. コンポーネントファイル
2. operations.js
3. actions.js
4. reducers.js

でした。

ただし、今回は「入力テキストを state として受け取って画面描画」などはしませんので、3. actions.js, 4. reducers.jsはいったんノータッチです。

src/templates/ProductEdit.jsx
.
.
.
      <div className="center">
        <PrimaryButton
          label={"商品情報を保存"}
          // onClick={() => {console.log('Clicked!')}}
          onClick={() => dispatch(saveProduct(name, description, category, gender, price))}
        />
      </div>
.
.
.

PrimaryButtonのonClickイベントに、saveProduct関数を設定します。

saveProductはoperationsで定義するものなので、dispatchを噛ませる必要があります。

src/reducks/products/operations.js
import {db, FirebaseTimestamp} from "../../firebase";
import { push } from "connected-react-router";

const productsRef = db.collection("products")

export const saveProduct = (name,description,category,gender,price) => {
  return async (dispatch) => {
    const timestamp = FirebaseTimestamp.now()

    const data = {
      category: category,
      description: description,
      gender: gender,
      name: name,
      price: parseInt(price, 10),
      updated_at: timestamp
    }

    const ref = productsRef.doc();
    const id = ref.id;
    data.id = id
    data.created_at = timestamp

    return productsRef.doc(id).set(data)
      .then(() => {
        dispatch(push('/'))
      }).catch((error) => {
        throw new Error(error)
      })
  }
}

saveProduct関数を定義します。

  • 外部DBと通信を行う処理のため、return async (dispatch) => {...
  • 引数は文字列として渡ってくるため、parseInt(price, 10),で十進数のint型数値へ変換。
  • set()メソッドを使うことで、Cloud firestore にデータを保存。

※ add/set/docメソッドの役割

DB(Cloud firestore) にデータを保存するとき、

(1) DB内で、データを保存するための場所(=ID)を自動で採番する
(2) そのIDの場所にデータを保存する

という流れで処理が行われる。

このとき、

  • (1)を行う: doc()メソッド
  • (2)を行う: set()メソッド
  • (1)(2)を一気に行う: add()メソッド

という役割のメソッドが存在する。doc()+set()=add()と考えると分かりやすい。そのため、

doc&setメソッド
const data = {...}
const ref = db.collection("products").doc();
const id = ref.id;

return db.collection("products").doc(id).set(data)

これと、

addメソッド
const data = {...}

return db.collection("products").add(data)

これは最終的な結果は同じになる。

単純にデータを保存したいだけであればadd()の方で問題ないが、今回のアプリでは「自動採番されたIDをフィールド値として保存したい」ため、doc()+**set()で処理を行っている。

(add()を使用すると、IDの自動採番と同時にデータの保存処理が行われるので、IDをデータ内部に取り込む処理が行えない)

先のoperations.jsのうち、

src/reducks/products/operations.js
.
.
.
    const ref = productsRef.doc();
    const id = ref.id;
    data.id = id
.
.
.

の箇所で、自動採番されたIDをdataの中に追加している。

動作確認

saveProduct()の動作確認をしてみます。

http://localhost:3000/product/edit
image.png

入力フォームにテキストを入れて、「商品情報を保存」をクリックします。

http://localhost:3000/
image.png

saveProduct()が正常に動作すれば、ルートへリダイレクトします。

実際にデータが保存されたのか、Firebaseコンソールから確認します。

image.png

先ほどの入力テキストが保存されています!

入力値に加え、Cloud Firestore上で自動採番されたID(zUTHkCmsEm1ov0ahrUTo)が、idカラムとして保存されていることも確認できます!

まとめ

本講座の要点をまとめると、
- 商品登録ページを作成(CRUDのC)
- 商品登録用関数saveProduct()を定義
- Cloud Firestoreへのデータ保存では、doc/set/addメソッドを使用する。

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

Reactを使って料理レシピの検索アプリを作ろう!

イントロダクション

Qiita夏祭り2020「〇〇(言語)のみを使って、今○○(アプリ)を作るとしたら」のテーマに合わせて開発をしていきます.
コロナウイルスの影響でリモートワークを強いられる世の中になっている気がしたので家で料理でもしてみてはいかがでしょうか!!!!!!そこで今回はReactを使ったレシピ検索アプリを作っていきましょう.今回はReact中級者向けの内容となっておりますので下記の内容については説明しませんので,もしまだ学習していない方は先にそちらを学習することをお勧めします. また,ここでは全てのコードを乗っけることはしないので手元で試したい方はこちらよりクローン可能です.

  • React hooks
  • styled-components
  • TypeScriptの説明
  • PostMan等の使い方

今回作ったアプリはこちらから御覧いただけます

最終的な目標としてこのアプリを土台にみなさんがいろいろな技術を追加して最終的に完成品となることを望んでおります.
そのため,実装としては最低限の部分のみをやっていくので是非記事を読み終わったあとは自分なりに改良してみてください?

プロジェクトを作る

Reactの導入までをなるべく簡単にするために今回はcreate-react-appを使って簡単にアプリを作成していきましょう!!!
また,今回はTypeScriptを使って開発を行っていくのでオプションを付けていきます.
Jsで記述したい人はオプションは必要ありません.

$ npx create-react-app cooking --typescript
$ cd cooking
$ yarn start

下記のような画面が出てくれば成功です.

スクリーンショット 2020-07-08 22.54.59.png

デプロイ

いきなりデプロイしていきます.これはあとで使うAPIでサイトURLが必要になるため,最初にデプロイする必要があります.
今回はFirebase Hostingを使っていきます.それではやっていこう!!!
上記のサイトにアクセスしてコンソールに移動しまず,プロジェクトの作成をしていきます.

  1. プロジェクトの作成をクリック
  2. プロジェクト名を決める(なんでもいい)
  3. プロジェクトが作成されるまでお茶?で飲む
  4. 無事作成できると次のようなページが出てきます. スクリーンショット 2020-07-08 23.16.40.png

コンソールでアプリの追加を行っていきます!

$  yarn global add firebase-tools
$  firebase login
$  firebase init
? Which Firebase CLI features do you want to set up for this folder? Press Space to select features, then Enter to con
firm your choices.
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
 ◯ Functions: Configure and deploy Cloud Functions
❯◉ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
 ◯ Emulators: Set up local emulators for Firebase features

どのプロジェクトを使うか聞かれるのでさっき作ったプロジェクトを選択しましょう.

$ yarn build
$ firebase deploy

これでコンソールに戻り左のメニューからHOSTRINGを選択するとURLが表示されるのでそれをクリックして見てみるとさっきのReactのロゴがグルグル回っているはずです.もし,うまく行かない人がいる場合は

  • firebase.jsonのpublicがあっているか
  • yarn buildし忘れていないか この辺をチェックしてみてみださい.

これでいつでもデプロイを簡単にできるようになりました.

楽天レシピカテゴリ別ランキングAPIを使う

今回は楽天の素晴らしいAPIがあったのでそちらを使っていきます.
楽天レシピカテゴリ別ランキングAPI

アプリケーションの登録を行い,アプリIDを取得してください(環境変数をenvで管理).これがAPIをコールするためのkeyとなります.実際にAPIがちゃんと叩けるか確認してみましょう.POSTMANとかinsomnia等で確認できまし,ブラウザからも確認できます.

https://app.rakuten.co.jp/services/api/Recipe/CategoryRanking/20170426?
    applicationId=[アプリID]&
    categoryId=10

いい感じですね!!!

スクリーンショット 2020-07-09 8.36.13.png

それではこれで最低限な準備はできました.この後も適宜必要なところでパッケージを追加していきます.
他にもESlintを入れたり,huskyを入れてコミット前に整形したりすることなど色々できます.

?実際に書いていく?

下準備

  • tsconfig.jsonにbaseURLを追加
  • ファイル・フォルダの整理
  • reset.cssのいれる

ファイル・フォルダの整理等は人それぞれになると思いますので,自分のは参考程度に思ってください.

$ mkdir hooks components pages types service
  • hooks ロジックの管理
  • pages URLに対応
  • components UIを構築してくコンポーネント
  • types 型宣言していくところ
  • service APIロジック等の管理
  • assets cssファイルやimgを管理

ブラウザのデフォルトのスタイルをなくすためにreset.cssを入れていきます.ネットに転がっているものを持ってきてcssフォルダの中に入れました.

$ touch reset.css

UIの構築

ここは説明はかなりさらっとやっていきます.人によってはBootStorapMaterial-ui等をいれると簡単にUIを作成できます.今回はstyled-componentsを使って,簡単なUIを作っていきます.コピペすれば同じように自分と同じようなUIができると思いますので,特にUIを気にしない人はコピペしちゃってください.

$ yarn add styled-components @types/styled-components

スクリーンショット 2020-07-09 15.14.16.png
スクリーンショット 2020-07-09 15.14.45.png

デザインセンスが全くないのでpinterestを眺めながら,いいなと思うところを摘んできてます.
UIの構築にあたって検索画面と結果画面の2つが必要になりますのでルート制御をしていきます.
下記のパッケージを入れていきます.-DオプションをつけることでdevDependenciesの方に追加されます.

$ yarn -D add react-router react-router-dom @types/react-router-dom

これでreact-routerとreact-router-domが使えるようになったので,実際にルーティングの設定をしていきます.index.tsxとApp.tsxにそれぞれ記述をしていきます.

index.tsx
import React from 'react';
import ReactDOM from 'react-dom';
import './assets/css/reset.css';
import './assets/css/index.css';
import App from './pages/App';
import { BrowserRouter as Router } from "react-router-dom";

ReactDOM.render(
  <Router>
    <App />
  </Router>,
  document.getElementById('root')
);
index.tsx
import React from 'react';
import { Header } from "components/Header";
import styled from "styled-components";
import { Menu } from "./Menu";
import { Result } from "./Result";

import { Switch, Route } from "react-router-dom";

const App = () => {
  return (
    <Content>
      <Header />
      <Switch>
        <Route path="/result">
          <Result />
        </Route>
        <Route path="*">
          <Menu />
        </Route>
      </Switch>
    </Content>
  );
};

const Content = styled.div`
  max-width: 1200px;
  margin: auto;
  margin-bottom: 100px;
`;

export default App;

App.tsxに注目するとSwitchをロートを分けていることがわかると思います.余談ですが,Next.js等を使うとPagesの中にファイル追加するだけで自動的にルーティングできます.今回は404のページを作成しないので/result以外のURLを全て検索画面の受け付けています.

またロジック部分で取得したデータ実際にもらうのでPropsを定義してあげることで,コンポーネントに渡すデータをスニペット等ですぐに把握することができます.

component
import React, { FC } from "react";

type Props = {
  name: string;
  id: string;
  handleSelect: (e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

export const MenuItem: FC<Props> = ({ name, id, handleSelect }) => (
 ....
);

このように書いてあげることで受け取るデータを制限できます.
それではロジック部分を組み込んでいきましょう.

ロジックの作成

まず,今回必要なロジックはこちら
- APIから選択肢をGET
- ユーザのレシピの選択機能
- 選択による絞り込み
- 検索結果
可能な限りロジックはhooksというフォルダの中で管理して,それらの恩恵をAPP.tsxより受け取り各コンポーネントに渡していくという形で書いていきます.

APIから選択肢をGET

https://app.rakuten.co.jp/services/api/Recipe/CategoryList/20170426?applicationId=[applicationId]
上記のURLに対してGETリクエストをすると全ての選択肢を取得できます.useReducerとuseEffectを使い初回レンダリングにデータを取得していきましょう.

useCategory.tsx
export const useCategory = () => {
  const [state, dispatch] = useReducer(reducer, initialState);

  useEffect(() => {
    handleGet()
  }, [])

  const handleGet = async () => {
    const ALlcategory = await API.category();
    const result = ALlcategory.result
    dispatch(get(result));
  };
API.tsx
const Fetch = async (type: string, params = "") => {

  const url =
    type === "CATEGORY"
      ? process.env.REACT_APP_API_CATEGORY_URL
      : process.env.REACT_APP_API_RANKING_URL;

  const options: RequestInit = {
    method: "GET",
    mode: "cors",
  };

  const response = await fetch(
    `${url}?applicationId=${process.env.REACT_APP_API_KEY}${params}`,
    options
  );
  const json = await response.json();
  return json;
};

export const category = async () => {
  return Fetch("CATEGORY");
}

通信を行うところをAPI.tsに分けてしまいます.こうすることで余計なFetchやaxiosを減らすことができます.またtoken等が必要になったときもここに追加すればいいのであとで拡張がしやすくなります.
typeを引数にとることで,環境変数の読み込みの切り替えを行っています.あとは,GETしてきたjsonを返してあげて読み出し元でdispatchすることでstateを更新しています.

ユーザのレシピの選択機能

選択肢の内容を確認してみると大 > 中 > 小のように中と小がそれぞれ一つ上のIDを持っていることがわかると思います.そのため,この親IDを使って絞り込みを行っていきます.

useResult
 export const useResult = () => {
  const [select, setSelect] = useState<string[]>([]);

  const selectFilter = (array: Child[]): Child[] => {
    const filter = array.filter(value => select.includes(value.parentCategoryId))
    return filter
  };

  const handleSelect = (e: React.MouseEvent<HTMLElement, MouseEvent>) => {
    const id_value = (<HTMLInputElement>e.target).value;
    setSelect([...select, id_value]);  
  };

  return { result, select, handleSearch, handleSelect, selectFilter };
};

selectFilterで親IDにより絞り込みを行っていることがわかると思います.
useStateを使ってユーザの選択肢したIDを管理することで,あとでAPIに結果を求める時に利用することが可能です.スプレッド構文を使うことでstateを更新していきいます.stateの値を直接書き換えるのはご法度なのでやめましょう

あとここで重要な部分がReact.MouseEventのところでこれを書かないと勝手にany型になってしまいます.
Tsを書いてる時input関係の型を定義する時にanyだと楽なのですが, ある記事を読んでからちゃんと指定するようにしたので是非読んでみてください.
any型で諦めない React.EventCallback

選択による絞り込み

こいつをどのように実装するか実は悩みました.理由としてカテゴリーを取得するものとユーザの選択したIDを持っている場所が違うため,そこの切り替えのロジック部分をAPP.tsx内に書かなければいけなかったのです(いい方法がある方は教えてください)
そのためAPP.tsxの中に下記を追加していきます.

App.tsx
  const array = select.length === 0 ? state.large : select.length === 1 ? selectFilter(state.medium)
    : select.length === 2 ? selectFilter(state.small) : []

これユーザが現在の数によって,選択肢の切り替えをしています.
大 > 中 > 小になっているので0 or 1 or 2で切り替えることができます.そのため,選択肢が2よりも大きくなるタイミングでAPI通信をすることで最後の選択肢を選んだタイミングでGETリクエストを出せます.

検索結果

先述したように2より大きくなったタイミングでAPI通信を行います.useEffectとの引数を指定してあげることでその値が変更した時のみ関数を実行できるように制御できます. また,検索画面への移動も行っています

useResult
export const useResult = () => {
  const [result, dispatch] = useReducer(reducer, initialState);
  const [select, setSelect] = useState<string[]>([]);
  let history = useHistory();

 useEffect(() => {
   if (select.length > 2) handleSearch();
  }, [select])

  const handleSearch = async () => {
    history.push("/result");
    const recip =  await API.search(select);
    dispatch(get(recip.result));
  };

   ......
};

API通信のところでParams関数を用意してあげることで,リクエストパラメータを生成しています.このように書いてあげることでわざわざプレーンな引数を渡す必要がなくなります.

Api.ts
const Fetch = async (type: string, params = "") => {

  .......

}

const Params = (params: string[]): string => {
  return "&categoryId=" + params.join("-");
}

export const search = (select: string[]) => {
  const params = Params(select); 
  return Fetch("SEARCH", params);
}
useResult
import { useReducer, useState, useEffect } from "react";
import { MenuResult, Child } from "types/index";
import * as API from "service/API";
import { useHistory } from "react-router-dom";

const initialState: MenuResult[] = [];

enum ResultActionType {
  GET = "GET",
}

type ResultAction = {
  type: ResultActionType;
  res: MenuResult[];
};

const get = (res: MenuResult[]) => ({
  type: ResultActionType.GET,
  res,
});

const reducer = (state: MenuResult[], action: ResultAction): MenuResult[] => {
  switch (action.type) {
    case ResultActionType.GET:
      return [...state, ...action.res];
    default:
      return state;
  }
};

export const useResult = () => {
  const [result, dispatch] = useReducer(reducer, initialState);
 ....
}

最後にReducerの部分を説明を軽くします.Typesフォルダを最初に作成したので,そこから使用する型をもらってきます.ここでちゃんと型を指定してあげることで,あとでUI部分に変数を渡す時にスニペットが効いて開発スピードをあげることができます.特に開発者はResultActionTypeをみることで,リクエストタイプを確認することができます.

これでロジック部分と核となる部分の説明は終わりです.

最後に

今回のアプリはまだまだ拡張可能だと思います.例えば,
- 検索履歴
- 選択肢の戻るボタンの実装
- たまにAPIがエラーを吐くのでエラーハンドリング
- SSRにしちゃう
- useReducerじゃなくてReduxに書き換えてみる.
- メモ化
- Atomic Designにしてみる
- 材料の表示
など他にもたくさん拡張できる部分があると思いますのでぜひ試してみてください.
最後まで読んでいただきありがとうございました.

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

[AWS]S3でReact.jsを超爆速簡易型Webホスティングする

Reactを勉強中、それとなくおもしろおかしいWebページをスクラッチで作成することができたので、メンバーに自慢公開した。

今までメンバーに自慢公開する際には、Heroku、GitHub ioなどを使用していたが、S3で爆速にできることが判明したので備忘録として残す。

はじめに

ドメインとの紐付けやSSL対応などは実施しておりません。
なので公開する際には十分に気をつけてください。

ReactPjの準備

公式ドキュメント参照。

$ npx create-react-app my-app
$ cd my-app/
$ yarn build

S3で公開するデプロイようのバケットを作成

S3のコンソールから「バケットを作成する」を押下。
スクリーンショット 2020-07-12 0.04.24.png

バケット名は任意の値を入力。
※ Route53の独自ドメインを割り当てたい場合は、ドメイン名と同じ値を入力すること。
 ドメイン名はアカウント関係なく、プライマリーなので。。。

リージョンは思いがなければ「アジアパシフィック(東京)」を選択。
スクリーンショット 2020-07-12 0.06.50.png

入力したら「次へ」を押下。
オプションの設定は特に行わないので、さらに「次へ」を押下。
スクリーンショット 2020-07-12 0.08.58.png

「パブリックアクセスをすべてブロック」のチェックボックスを外す。
スクリーンショット 2020-07-12 0.10.40.png

「次へ」を押下し、問題がなければ「バケットを作成」を押下
スクリーンショット 2020-07-12 0.12.42.png

バケットの作成は完了。

S3の設定を変更

先ほど作成したバケットの「プロパティ」を選択。
スクリーンショット 2020-07-12 0.18.03.png

Static website hostingを選択し、「このバケットを使用してウェブサイトをホストする」を押下。
インデックスドキュメントに「index.html」と入力し「保存」を押下。
スクリーンショット 2020-07-12 0.17.12.png

次に、「アクセス権限」を選択し、バケットポリシーを押下。
バケットポリシーには下記文字列を入力する。
[BucketName]には今回作成したバケット名を入力すること。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "PublicReadGetObject",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::[BucketName]/*"
        }
    ]
}

スクリーンショット 2020-07-12 0.21.49.png
「保存」を押下したら、S3バケットの設定は完了。

ソースコードをアップロード

AWS CLIでやった方がかっこいいけど、手動で。
先ほど作成したReactPJの配下に「build」というディレクトリができているので、開く。

そして、配下のファイルを一括選択して、S3の「概要」めがけてドラッグ&ドロップ。
スクリーンショット 2020-07-12 0.29.57.png
そしてアップロード。
スクリーンショット 2020-07-12 0.30.58.png

これで、完了。

確認

StaticWebsiteHostingに記載のあるエンドポイントのURLにアクセス。
http://[BucketName].s3-website-ap-northeast-1.amazonaws.com/
でも可。
スクリーンショット 2020-07-12 0.34.30.png
無事ブラウザで表示。

まとめ

身内で公開してキャッキャうふふするのであればこれでOKかな?と思う。
次は独自ドメイン化してみようかな。

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