ラベル JavaScript の投稿を表示しています。 すべての投稿を表示
ラベル JavaScript の投稿を表示しています。 すべての投稿を表示

2021年7月20日

TailwindCSS + Alpine.js でモーダルダイアログを作ろう

最近知ったのですが、 TailwindCSSAlpine.js の組み合わせがなかなか使いやすかったので紹介します。



作った手順は次の通りです。


1. TailwindCSS をCDNから読み込む。


headタグ内に次のlinkタグを追加します。

<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet" />





2. Alpine.js をCDNから読み込む。


headタグ内に次のstyleタグを追加します。

<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>



3. ページのレイアウトを作る。


bodyタグ直下のdivタグの中に、header, main, footer の3つの要素を作成します。

親のdivのスタイルで display: grid とし、それぞれのエリアの高さが header 6rem, main 1fr, footer 4rem  になるように指定します。また w-screen, h-screen で親のdivが画面一杯に広がるようにしています。

<div
class="m-0 p-0 w-screen h-screen grid"
style="grid-template-rows: 6rem 1fr 4rem;"
>


詳細はソースコードを参照してください。


下の画像のような表示になればOKです。



mainに overflow-y-auto クラスを指定しているので、コンテンツが画面に収まらない場合はフッターやヘッダーを固定にしたままmain部分だけを縦スクロールさせることが出来ます。

<main class="p-6 overflow-y-auto">




画面の中央にある「開く」ボタンのクラス指定は次のようになっています。(一部省略)

<button
class="py-2 px-4 max-h-12 bg-blue-600
hover:bg-blue-400 text-white border rounded-md shadow"
>


TailwindCSS のおかげでそれなりに見栄えの良いボタンになります。





4. モーダルダイアログのHTMLを追加。


親divの一番最後に下のHTMLを追加します。

<!-- モーダルダイアログのラッパー -->
<div>
<!-- 背景を暗くするための半透明部分 -->
<div></div>

<!-- 実際のモーダルダイアログ部分 -->
<div>
<header>
<h1>確認してください</h1>
</header>

<main>
<p>本当にこの操作を実行しますか?</p>
</main>

<footer>
<button>キャンセル</button>
<button>実行!</button>
</footer>
</div>
</div>


5. モーダルダイアログのスタイルを設定。


ラッパー用divに次のクラスを指定して、画面一杯をカバーするようにします。

<div class="absolute top-0 left-0 w-screen h-screen">


次に背景を暗くするためのdivのスタイルを設定します。

<div class="absolute w-full h-full bg-black opacity-80"></div>


ダイアログ本体部分のスタイル指定は下のようになります。

<div
class="relative w-5/6 max-w-xl h-1/2 m-auto grid bg-gray-300 border rounded-md shadow"
style="top: 20vh; grid-template-rows: 4rem 1fr 6rem;"
>


TailwindCSSで定義されていない値についてはstyle属性で独自に指定しています。

最終的にはこのようなダイアログになります。






6.  初期状態でモーダルダイアログを非表示にする。


ここから Alpine.js が活躍します。

まず、ページの一番親(body直下)のdivに x-data 属性を追加します。

<div
x-data="{ open : false }"
class="m-0 p-0 w-screen h-screen grid"
style="grid-template-rows: 6rem 1fr 4rem;"
>

次にモーダルダイアログのラッパーのdivに x-show 属性を追加します。

<!-- モーダルダイアログ -->
<div x-show="open" class="absolute top-0 left-0 w-screen h-screen">


これでモーダルダイアログが初期状態では表示されなくなります。




7. 「開く」ボタンでモーダルダイアログを表示する。


ページ中央の「開く」ボタンに @click="open = true" を追加します。

<button
@click="open = true"
class="py-2 px-4 max-h-12 bg-blue-600 ...
>開く</button>


これで「開く」ボタンをクリックするとモーダルダイアログが表示されるようになりました!


8. 「キャンセル」ボタンでモーダルダイアログを閉じる。


モーダルダイアログ内の「キャンセル」ボタンでは「開く」ボタンと反対に "open = false" とすることでダイアログを非表示にします。

<button
@click="open = false"
class="py-2 px-4 text-white bg-gray-600 ..."
>キャンセル</button>


Alpine.js シンプルで良いですね!


ちなみにx-showでの表示・非表示切替時にトランジションアニメーションを付けるには、x-transition 属性を付加するだけで大丈夫です。


x-transition ではデフォルトで250msの scale + opacityのアニメーションが実行されます。これをopacityのみに変更したい場合は、x-transitionx-transition.opacity とすればOKです。もちろん実行時間の指定も可能です。





9.  外側をクリックされたらモーダルダイアログを閉じる。


@click.outside という表記を使えば、その要素の外側がクリックされた時の処理を書くことが出来ます。

<!-- モーダルダイアログ -->
<div x-show="open" class="absolute top-0 left-0 ...">
<div class="absolute w-full h-full bg-black ..."></div>
<div
@click.outside="open = false"
class="relative w-5/6 max-w-xl h-1/2 m-auto ..."
style="top: 20vh; grid-template-rows: 4rem 1fr 6rem;"
>



10.  「実行!」ボタンが押されたらモーダルダイアログを閉じてカスタムイベントを発行する。


「実行!」ボタンの処理もキャンセルと同じように @click="..."の中に全て書いてしまっても良いのですが、ここでは Alpine.js のカスタムイベント発行の機能を使ってみます。 

<button
@click="open = false; $dispatch('dialog-ok')"
class="py-2 px-4 text-white bg-red-600 ..."
>実行!</button>





11.  カスタムイベントを受け取ったら何らかの処理を実行する。


$dispatch() で発行したカスタムイベントはDOMツリーの上位にある全ての要素で受け取ることが可能です。

ここでは body直下のdivにイベント処理を追加しました。

<div
x-data="{ open : false }"
@dialog-ok="setTimeout(() => alert('Hi'), 100)"
class="m-0 p-0 w-screen h-screen grid"
>


setTimeout()を使っているのは、これが無いとモーダルダイアログが閉じる前にアラートが表示されてしまっていたためです。



まとめ


TailwindCSSもAlpine.jsも、覚えないといけないことが少なくてすんなりと習得出来そうです。

TailwindCSSに慣れるとスタイルの設定にかかる時間が格段に短縮されます。Alpine.jsも必要最小限の使い方さえ覚えれば簡単な動作であればさくさくと実装出来て、とても便利です。


興味を持った方はぜひ試してみてください!








 🍻

2020年7月30日

Parcelでasync/awaitを使うと「regeneratorRuntime is not defined」エラーが出る場合の対処法

Parcel を使ってJavaScriptアプリケーションを書いているときに、async/awaitを使おうとすると下のエラーが出ました。

Uncaught ReferenceError: regeneratorRuntime is not defined

調べたところ、次のサイトに解決策が載っていました。





対処法1


ブラウザを比較的新しいものだけに限定して構わないのであれば、次の方法が簡単です。

package.jsonに以下を追記します。

  "browserslist": [
    "since 2017-06"
  ]
これでasync/awaitを使ってもエラーが出なくなります。



対処法2


もう一つの対処法としては、「regenerator-runtime」パッケージをインストールするというものがあります。

> npm install regenerator-runtime

でインストールしておき、index.jsなどから

import 'regenerator-runtime/runtime';

として読み込みます。

バンドルサイズが25KBほど増えてしまうようですが、こちらの方法でもエラーが出なくなります。古いブラウザもサポートする必要がある場合はこちらを使うことになると思います。






 .


2020年6月16日

データベース不要! ReactとGoogle Drive APIで任意のデータを保存する

前回はReactアプリにGoogle認証を組み込む方法を試しました。

今回はそのコードを使いながら、さらにGoogle Drive APIを使って任意のデータをログインしているユーザーのGoogle Driveに保存することに挑戦してみました。


完全にブラウザのみで動作するReactアプリケーションなので、バックエンドサーバは不要です。Netlifyなど静的サイトのホスティングサービスを使ってデプロイすることが可能です。


Screen shot on a PC

Screen shot on a phone


  • Google Drive内の所定のフォルダからファイル一覧を表示。
  • ファイル名がクリックされるとその内容(テキストデータ)を表示。
  • テキストを入力・編集して保存ボタンを押し、ファイル名を入力するとGoogle Driveにテキストデータを保存。

ということを行っています。



2つのContextプロバイダー


認証状態とアプリケーション固有の状態を分けて2つのContextプロバイダーで別々に共有するようにします。

import React from 'react';
import { FileList } from './FileList';
import { FileContent } from './FileContent';
import { AuthProvider } from './auth-state';
import { AppStateProvider } from './app-state';
import { Header } from './Header';
import './App.css';

function App() {
return (
<AuthProvider>
<AppStateProvider>
<div className="App">
<Header />
<FileList />
<FileContent />
</div>
</AppStateProvider>
</AuthProvider>
);
}
export default App;



Google Drive APIの有効化とAPI Keyの作成


下のドキュメントにしたがってGoogle Drive APIを有効化し、API Keyを作成しておいてください。

https://developers.google.com/drive/api/v3/quickstart/js



Google DriveへのCRUD処理


次にGoogle Driveにアクセスする処理を「google-api.js」に記述します。

google-api.js
---
---

前回作成した認証関係の関数に加えて、

 getFiles ファイル一覧取得  
 getFileContent     ファイル内容取得
 uploadFile ファイル保存
 deleteFile ファイル削除

などの関数をエクスポートしています。



アップロード処理がなかなか動かず苦労しましたが、最終的には下のサイトの情報にしたがってMultipart形式でPOST(更新のときはPATCH)リクエストを送ることで解決しました。


またファイルの内容を取得する方法も最初は分からず試行錯誤しました。こちらは下のURLにGETリクエストを送ることでダウンロード出来ました。

https://www.googleapis.com/drive/v3/files/${fileId}?alt=media&source=downloadUrl

もちろんリクエストヘッダーにはGoogle認証で得られたアクセストークンを付加する必要があります。


ファイルの保存先フォルダは現状では APP_FOLDER という定数で定義していますが、これはユーザーが任意のフォルダ名を指定出来るようにした方が便利ですね。


以上、今回のサンプルを作成してみて、バックエンドサーバ無しでもブラウザ上のJavaScriptだけでかなり柔軟にGoogle Driveへのデータ保存が出来るということが分かりました。


単純なテキストではなく例えばJSONを保存すれば、ある意味簡易的なデータベースとしても使えるかも知れませんね。何よりアプリの開発者側で保存先のデータベースやストレージを用意する必要が無いのが魅力的です。



今回作成したアプリケーションのソースコードはこちらにあります。






 

2020年6月14日

ReactのSPAアプリケーションでContextとHooksを使ってGoogle認証を実装する

ReactのSPAアプリケーションにおいて、ContextとHooksを使うことでGoogle認証の処理をラップして利用するというサンプルを作成しました。


Google Cloud Consoleでアプリケーションを登録する




OAuth consent screenを設定する  







OAuth client IDを作成する








Create React AppでReactアプリケーションを作成する


npx create-react-app project-name
cd project-name


public/index.html を編集する


WebアプリケーションでGoogle認証を利用するための最もシンプルなサンプルコードがこちらにあります。

Google Sign-In for Websites  |  Google Developers 

<html lang="en">
  <head>
    <meta name="google-signin-scope" content="profile email">
    <meta name="google-signin-client_id" content="YOUR_CLIENT_ID.apps.googleusercontent.com">
    <script src="https://apis.google.com/js/platform.js" async defer></script>
  </head>
  <body>
    <div class="g-signin2" data-onsuccess="onSignIn" data-theme="dark"></div>
    <script>
      function onSignIn(googleUser) {
        // Useful data for your client-side scripts:
        var profile = googleUser.getBasicProfile();
        console.log("ID: " + profile.getId()); // Don't send this directly to your server!
        console.log('Full Name: ' + profile.getName());
        console.log('Given Name: ' + profile.getGivenName());
        console.log('Family Name: ' + profile.getFamilyName());
        console.log("Image URL: " + profile.getImageUrl());
        console.log("Email: " + profile.getEmail());
        // The ID token you need to pass to your backend:
        var id_token = googleUser.getAuthResponse().id_token;
        console.log("ID Token: " + id_token);
      }
    </script>
  </body>
</html>

これを参考に public/index.htmlを編集します。

上のサンプルコードの中でReactアプリで必要になるのはGoogleのスクリプトを読み込んでいる行だけなので、その部分をコピーしてheadタグ内に挿入します。

    <script src="https://apis.google.com/js/platform.js"></script>

Googleのサンプルコードでは「async defer」という属性が付いていますが、Create React Appの場合これがあるとスクリプトが実行されるタイミングの関係で上手く動かないので削除しています。



.env.localファイルを作成する


次にプロジェクトのルートフォルダに「.env.local」 ファイルを作成して値を設定しておきましょう。

REACT_APP_CLIENTID=41702000-xxxxxxxxxxxxx.apps.googleusercontent.com

この環境変数の名前は、Create React Appの規約上「REACT_APP_」から始まる必要があるので注意してください。

ClientIdを環境変数に持たせておくことによって、本番用、ステージング用など、ビルド環境に応じて柔軟に変更することが出来るようになります。


src/google-auth.jsファイルを作成する


Google APIを実際に呼び出す際に使う関数をこのファイルにまとめておきます。

init(), signIn(), signOut()の3つの関数をエクスポートしています。
------


src/auth-state.jsファイルを作成する


上のgoogle-auth.jsに定義した関数をReactアプリ内で使うためのContextプロバイダーとフックを提供するファイルになります。

内部的にはuseReducerを使って認証状態を管理していますが、それはファイル内に隠蔽して外部へはAuthProviderコンポーネントとuseAuthStateフックをエクスポートしています。

useAuthStateフックからの戻り値は、認証状態を表す state オブジェクトとサインイン、サインアウト処理を呼び出すための関数を提供する actions オブジェクトの2要素配列となっています。

---
---


ここまで準備が出来れば、あとは

1. AuthProviderコンポーネントでアプリをラップする。
2. 任意のコンポーネント内でuseAuthStateフックを使って認証機能にアクセスする。

という形になります。


App.js
function App() {
  return (
    <AuthProvider>
      <div className="App">
        <header className="App-header">
          <h1>Google Auth React Examle</h1>
          <SignInOutButton />
        </header>
        <div className="App-main">
          <UserInfo />
        </div>
      </div>
    </AuthProvider>
  );
}


SignInOutButton.js
 
import React from 'react'; import { useAuthState } from './auth-state'; 
export function SignInOutButton() {
  const [state, actions] = useAuthState();
  const { isSignedIn } = state;
  const { signIn, signOut } = actions;
  if (isSignedIn === undefined) {
    return null;
  }
  return (
    <div>
      {isSignedIn ? (
        <button onClick={signOut}>Sign Out</button>
      ) : (
        <button onClick={signIn}>Sign In</button>
      )}
    </div>
  );
}




今回作成したアプリケーションのソースコードはこちらにあります。





以上、ReactのSPAアプリケーションにおいて、ContextとHooksを使ってGoogle認証の処理をラップするサンプルでした。



次回は、認証した後さらにGoogle Drive APIを使ってアプリケーションのデータを(バックエンドサーバー無しで)ブラウザから直接Google Driveに保存するということに挑戦したいと思います。





 

2020年5月7日

Next.jsで静的サイトを出力してFirebase Hostingでホスティングする

まずは公式のドキュメントにしたがってNext.jsのアプリケーションを作成しましょう。

Create a Next.js App | Learn Next.js 


package.jsonファイルのscriptsセクションに下記を追加しておきます。

  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "export": "next export"
  },


npm run build && npm run export を実行して outフォルダに結果が出力されるのを確認します。



次にFirebase CLIを初期化します。


firebase login

firebase init



カーソルキーで「Hosting」を選択してスペースキーを押し、Enterで確定です。

Firebaseの管理画面でまだ「プロジェクト」を作成していない場合は、プロジェクトIDと名称を入力して新規作成します。既に使われているIDを入れてしまうとエラーになるので、ユニークなIDを入力しましょう。

アップロード対象のフォルダを聞かれるので、デフォルトの「public」ではなく「out」を指定します。


firebase deploy を実行して、「Deploy complete!」と表示されれば完了です!


慣れれば3分もかからずに出来てしまいますね。


バックエンド処理にCloud Functionsを使ったりするような場合も、下のドキュメントの通り設定すれば決して難しくはありません。

本当に便利な世の中になったものです。^^)


あ、特にFirebase Hostingでなくても良いのであれば、もちろんNext.jsの開発元であるVercel(旧Zeit)社のサービスを使うのが一番簡単でオススメです。



こちらであれば、静的サイトでなくSSRありのサイトであっても、またバックエンドのAPIが必要な場合でも、何も考えずにデプロイしてしまえばあとはサービス側で上手くやってくれるみたいなので、こちらも素晴らしいと思います。







 

2020年5月4日

ReactでSVGファイルをコンポーネントとしてレンダリングする

egghead.ioのレッスンを観ていて良さそうなものがあったので、メモしておきます。


Add SVGs as React Components with Create React App 2.0
https://egghead.io/lessons/react-add-svgs-as-react-components-with-create-react-app-2-0

Create React App のドキュメントにも記述があります。
https://create-react-app.dev/docs/adding-images-fonts-and-files


Create React Appで作ったReactアプリケーションでは画像ファイルを下のようにインポートしてコンポーネント内で参照することが出来ます。

import logo from './logo.png';
function Header() {
  return <img alt="Logo" src="{logo}" />;
}

もちろん画像の形式がSVGであっても同様です。

import logo from './logo.svg';
function Header() {
  return <img alt="Logo" src="{logo}" />;
}

ただ、SVG形式の場合はインポートの仕方を下のように変えるとReactコンポーネントとしても使えるようになるそうです。

import { ReactComponent as Logo } from './logo.svg';
function App() {
  return (
    <div className="App">
<header className="App-header">
<Logo className="App-logo" />
</header>
</div>);
}


このようにすると、HTMLとしてレンダリングされる際のタグが imgタグではなく、svgタグになります。

svgタグになってうれしい点は、SVG内の個々の要素(図形)をCSSでコントロール出来るようになることです。


例えば、線の色や太さを変えたり、アニメーションを付け加えたりすることが可能になります。


下の例では、Reactロゴのpathエレメントの部分のみ点線にして、さらに点線のオフセットをアニメーションで動かしています。

.App-logo path {
  stroke: palegoldenrod;
  stroke-width: 10px;
  fill: none;
  stroke-dasharray: 35px 15px;
  animation: orbit 1s infinite linear;
} @keyframes orbit {
  to {
    stroke-dashoffset: 50px;
  }
}

実行するとこんな感じになります。









2020年4月20日

AWS AmplifyとDynamoDBでサーバーレスなREST APIを構築する

前回はユーザー認証機能を付けましたが、まだデータの永続化が出来ていないのでTodoアプリとしては未完成です。



  1) Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

  2) AWS AmplifyでReactアプリにユーザー認証機能を追加する

  3) AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する




今回はサーバーレスなREST APIを追加してデータをデータベースに保存出来るようにします。

amplify status で現状を確認すると、下のようになっています。



下のコマンドでAPIカテゴリを追加します。

amplify add api 

まず GraphQL か REST かを選ぶように言われます。

Web上で見つかる情報にはGraphQLを使ったものが圧倒的に多いように思いますが、ここではあえてRESTを選びます。



DynamoDB上に作成するテーブル名まで入力すると、次に作成するカラム情報の入力になります。

ドキュメントをざっと読んだところ、DynamoDBでは基本的に1アプリケーションで使うテーブルは出来るだけ少ない方が望ましいらしく、「1テーブルで済ませられればそれが最も良い」との事です。



長年リレーショナルデータベースに馴染んだ身としてはなかなか目から鱗な考え方です。

どうやらDynamoDBでのスキーマ設計の勘所は、「パーティションキー」と「ソートキー」および「グローバル・セカンダリインデックス」「ローカル・セカンダリインデックス」を上手く使うことにあるようです。

DynamoDBのテーブル設計をするとき、自分に問いかけていること – 或る阿呆の記 ( https://hack-le.com/dynamodb-query/ )

この辺り、非常に奥が深そうで面白いのですがとりあえず今はシンプルなTodoリストアプリを作りたいだけなので、極力簡単な方法で行きたいと思います。



パーティションキーを「pk」、ソートキーを「sk」とし、3つ目のカラムはmap型で「data」としておきました。map型にはJSONを格納できるので、こうしておけば後からの仕様変更にもある程度柔軟に対応することが可能になるかと思います。

パーティションキーとソートキーを何にするかというのは、アプリケーションの要件によって大きく変わる部分で、開発効率にも大きく関わってきます。


今回は、pkカラムには認証されたユーザーのidに "user:" というプリフィックスを付加して格納し、skカラムにはTodoアイテムのid(ランダムに生成されたもの)の先頭に "todo:"というプリフィックスを付加して格納することにしました。

こうすることでユーザーのidが分かればそのユーザーに属するタスクの一覧を簡単に取得することが出来るようになります。

実際のクエリーとしては、
- pkが "user:" + ユーザーid に一致する
- skが "todo:" で始まる
という条件で検索することになります。

DynamoDBではパーティションキーは完全一致でしか検索出来ませんが、ソートキーは部分一致や範囲検索が可能なので、このようなクエリーが可能になります。

また、ユーザーに属するデータでTodo項目以外のデータを保存したい場合には、skに付けるプリフィックスを "todo:" 以外のものにすれば対応出来ます。

例えば、ユーザーごとのアプリ設定を保存したい場合は、pkは同じでskを "pref:" として保存すれば、簡単にそのレコードを一意に指定して読み出す事が出来ます。

1アプリケーションで一つのテーブルしか使わない、というのはこのように2つのキー(およびセカンダリインデックス)を上手く使って複数の種類のデータを保存する、ということになるのかなと思います。


カラム設定の入力が終わると、テーブルへのアクセス権の設定をするか聞かれるので、認証していないユーザーはAPIへのアクセスが出来ない様に設定しておきます。




次に amplify push を実行して、追加した設定をAWS側に反映します。


完了するとAWS側では下記のリソースが作成されています。

- API Gateway
- Lambda
- DynamoDB


これらのうち、Lambda関数は自動生成されたコードだと上手く動かず、何をしているのかを理解した上でアプリケーションの要件に合わせてそこそこ手を加える必要がありました。


/amplify/backend/function/(Function名)/src/app.js (一部のみ抜粋)
-----

-----


アプリケーション側で扱うModelとしては

    {      
        id: 'todo:12312-312132',      
        text: 'ミルクを買う',      
        done: false 
    }

のような形になっているのですが、DynamoDBに保存されるのは、

    {
        pk: 'user:aaaaaaaaaaa',
        sk: 'todo:12312-312132',
        data: {
            text: 'ミルクを買う',
            done: false
        }
    }

という形になっています。

このため app.js の内部でAPI Gatewayから受け取ったオブジェクトをDynamoDBのスキーマに合った形式に適宜変換してから保存しています。



さて、次はReactアプリの方を変更して行きます。


この辺りのドキュメントを見ながらAPIへのアクセス処理を実装します。

Amplify Docs –– Fetching Data 



基本的には、

import { API } from 'aws-amplify';

でAPIクラスをインポートした後、

await API.post(apiName, path, options);  // CREATE
await API.get(apiName, path, options);  // READ
await API.put(apiName, path, options);  // UPDATE
await API.del(apiName, path, options);  // DELETE

でLamdaで作成してあるAPIにアクセスすることが出来るので、各アクションからこれらを適宜呼び出しています。


React側の実装としては、Hooks(useEffect, useReducer)を使って作っています。


/services/todo-service.js
-----

-----



ここまででなんとかシンプルなTodoリストアプリが完成しました。



  • Amplify Consoleによるスタティックなサイトのホスティングは簡単・超便利!

  • Amplify CLIでのユーザー認証機能の追加はパラメータの設定が最初ちょっと大変だけど、何回か試行錯誤して分かってしまえばこちらも簡単で素晴らしい!

  • Amplify CLIでのREST APIの作成は、DynamoDBの特性を理解して設定する事が超重要!(*



* もちろんDynamoDB以外の任意のデータベースを使うことも可能。


現在のソースコードは下記から確認可能です!

https://github.com/mikehibm/amplify-test01




  1) Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

  2) AWS AmplifyでReactアプリにユーザー認証機能を追加する

  3) AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する






 

2020年4月17日

AWS AmplifyでReactアプリにユーザー認証機能を追加する

前回はNext.jsでエクスポートしたWebアプリをAmplify Consoleでホスティングするところまで行いました。

今回はこのアプリにAWS Cognitoによるユーザー認証を追加したいと思います。



まず amplify status で現在の状態を確認しておきます。



次に、 amplify auth add を実行。




最初に「Do you want to use the default authentication and security configuration?」と聞かれます。

ここでデフォルト設定を使って試したところ、サインアップの際にメールアドレスの他に電話番号の入力も必須になってしまったので、それを回避するために今度は「Manual configuration」を選択して次に進みました。


次に、amplify push を実行して追加した設定をAWS側に反映します。


コマンドが完了すると、AWS側では必要なリソース(Cognito User Pool、Lambdaファンクションなど)が作成されているのが分かります。


次にReactアプリケーションの方でCognitoに接続するために、必要なライブラリを追加します。

npm i aws-amplify @aws-amplify/ui-react


ログイン画面の表示には「Amplify UI Components」を使います。

/pagesフォルダ内にlogin.jsを作成し下のコードを入力しました。

----------

----------

これだけで、http://localhost:3000/login を開くと下のようなログイン画面が表示されるようになります。



もちろん最初はユーザー登録が必要なので、「Create account」のリンクをクリックします。(この辺りの文言はカスタマイズ可能です。)


ここで、電話番号の入力欄が必須となってしまっています。
これを非表示にするためのカスタマイズ方法がドキュメントには載っているのですが、その通りにやってみても上手く行きませんでした。

検索したところ、UI ComponentのバグとしてGitHubのIssueに上がっているようです。ちょうどこの記事を書いている数時間前にFixがマージされているみたいなので、近日中には解決しそうです。今はとりあえずダミーの電話番号を入力して次へ進みます。


登録フォームをSubmitすると、確認コードの入力画面に移ります。




入力したメールアドレスに確認コードが送られているので、そのコードを入力して「Confirm」をクリックすれば登録完了です。



この時点でAWSコンソールでCognitoのUser Poolを見てみると、確かに1件のユーザー情報が登録されているのが分かります。


バックエンド側を何も作り込んでいないのにここまで自動的に出来上がってしまうのはすごいですね!


最後に上で作成したlogin.jsと同様に認証用のUIをindex.jsページにも追加します。

-----

-----

ここではサインアウトボタンは要らないので削除し、代わりに右上のヘッダー内に「Sign Out」リンクを追加ししました。

サインアウトの処理を実装するには、

import { Auth } from 'aws-amplify';

でAuthクラスをインポートし、リンクがクリックされたら

Auth.signOut();

を実行すればOKです。



次回はバックエンド側のREST APIをAmplifyのサーバーレスな機能を使って追加します!

AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する 




ここまでのソースコードはこちらで確認出来ます!






 

2020年4月16日

Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

今回は、Next.jsから(SSR無しの)スタティックなSPAとしてエクスポートしたサイトを、Amplify Consoleでホスティングしてみます。


サンプルReactアプリの作成


npm init -y
npm i -S react react-dom next
 


package.jsonはこんな感じになっています。




npm run dev を実行すると開発サーバーが立ち上がるのでブラウザで http://localhost:3000 を開きます。


npm run export を実行すると out フォルダにindex.htmlや404.html などのファイルが出力されます。今回はこれらの静的なHTMLファイルを Amplify Consoleでホスティングすることまで出来れば目的達成です。


amplify-cliのインストール


npm i -g amplify-cli

Amplifyの初期化


amplify configure








amplify init



Amplify Consoleでのホスティング設定を追加


amplify hosting add


上のコマンドを実行して「Continuous deployment...」を選ぶと、ブラウザが開いて接続するGitリポジトリを聞かれます。




amplify.yml という設定ファイルを手動で変更する必要があるので、ダウンロードしてプロジェクトのルートフォルダに保存します。


ここではNext.jsでスタティック・エクスポートした成果物をデプロイしたいので、下のように一部を変更します。(commandsとbaseDirectory)




変更したamplify.ymlファイルをGitリポジトリにコミット・プッシュします。

プッシュした時点で自動的にAmplify Console側でビルド処理が走ります。




ProvisionからVerifyまで全てグリーンになってから画面に表示されているURLをクリックすると、アプリケーションがデプロイされているのが確認出来ます。


今は「Hello!」と表示するだけで何もしないアプリケーションですが、後は通常のReactアプリケーションとして「npm run dev」で実行しながらローカルで開発します。




ひとまずブラウザ上だけで動くTodoアプリを作りました。





このままではブラウザを閉じるとデータが消えてしまうので意味が無いですが、次回以降のエントリではここに「ユーザー認証」と「REST API経由でのDynamoDBへのデータ永続化機能」を追加して行こうと思います。


AWS AmplifyでReactアプリにユーザー認証機能を追加する

AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する




現在のソースコードはこちらで参照出来ます!

https://github.com/mikehibm/amplify-test01/tree/blog-2020-04-15






 

2020年4月14日

AWS AmplifyでReactアプリ開発を試してみた

時間があったので前から気になっていた AWS Amplify を試してみることにしました。

とりあえず作ったのは、Todoリスト。React (Next.js) によるWebアプリです。



username / password でログインすると、DynamoDBに保存された自分のTodoリストが表示されます。



使った技術は、

フロントエンド
 - Next.js
 - Amplify Console (Hosting)

バックエンド
 - Lambda
 - API Gateway
 - DynamoDB
 - Cognito

という構成になりました。


至ってシンプルなTodoリストですが、Cognitoによるユーザー認証やDynamoDBなど慣れないモノについて使い方を調べながら作ったので結構時間がかかりました。

Amplify ConsoleによるフロントエンドのHosting機能は非常に便利で使いやすいと思いました。

ただ、今のところ全体的な感想としては 「Firebase の方が使いやすいかなあ」という感じです。特にドキュメントが(内容的には非常に多岐に渡って網羅されているのですが)今ひとつ分かりにくい気がします。


これからせっかくなのでもう少し機能追加をしながら使い込んで見たいと思います。


  1) Next.jsでスタティック・エクスポートしたサイトをAWS Amplify Consoleでホスティングする

  2) AWS AmplifyでReactアプリにユーザー認証機能を追加する

  3) AWS AmplifyとDynamoDBでサーバーレスなREST APIを構成する






 

2019年5月7日

初めてElmで何か作ってみた。- エニグマ(風)暗号機

最近子どもが学校の図書館かどこかで暗号に関する本を読んだらしく、

 「お友達のAちゃんと秘密のメッセージを交換したいの。エニグマみたいなので。」

とせがまれました。

いきなりエニグマ暗号機の名前が出てきたのでびっくりしましたが、

 「よし、エニグマ(風)暗号機を作ってあげよう!」

ということになりました。




この際なので、以前から試してみたかった、Elmを使うことにしました。

出来たモノはこちらに公開してあるのでもし良ければ試してみて下さい。

https://mysecretmsg.mikehi.now.sh/




伝えたいメッセージを打ち込んでEnterキーを押すと、「暗号化キー」の入力画面になります。



ランダムな4桁の数字を入力してもう一度Enterキーを押すと、暗号化されたメッセージが表示されます。



これを相手に伝えます。

暗号化されたメッセージを受け取った相手は、同じアプリを開いて暗号文を打ち込み、さっきと同じ4桁の「暗号化キー」を入力すると復号化されたメッセージを見ることが出来ます。


暗号化されたメッセージを同じキーで再度暗号化すると元の平文に戻る(暗号化と復号化のプロセスが同じ)」、というのが「エニグマっぽい挙動」ということになるかと思います。


これをどうやって実現しようかと考えて少し悩みました。

単純に文字コードのXORを取れば出来そうなのですが、それだと結果が文字として表示出来ない値になってしまう事があるので、別の方法を考えました。

その結果、

- 入力可能な文字を2つのグループに分けて、2文字単位で「置換ペア」を予め決めておく。
- このような置換ペアのテーブルを10種類作っておく。
- 「暗号化キー」の各桁(0〜9の数字)を置換ペアのテーブルに対応させて、順に置換する。

という簡単な方法で実装してみました。


このアプリを使って秘密のメッセージを送る際には、メッセージとキーは別々の方法で伝える必要があります。(キーだけは事前に合意しておく、メッセージはメール、キーは口頭で伝える、など)

メッセージ本文と暗号化キーの両方を知っている人は誰でも復号化して元のメッセージを見ることが出来るので、キーを秘密にしておくことはとても重要です。(当たり前ですが。)


今回のソースコードはこちらにあります。

https://github.com/mikehibm/mysecretmsg




さて、完成したアプリを子どもに使わせて見たところ、予想外の感想が返って来ました。


「やっぱり...本物のEnigmaが欲しい!」



追記:

Elmを初めて使った感想を何も書いていなかったので思い出しながら書きます。

- 最初慣れるまでは結構大変。アプリケーションのひな形も Sandbox, Element, Document, Application とあってどれを使ったら良いのか迷った。

- 特にキー入力をどうやって処理するのか、ドキュメントを見てもよく分からず。Githubのサンプルとにらめっこして何とか動かせた。Decoderとか今でも完全には理解してません。笑

- 関数の集まりとしてアプリケーションが宣言的に定義出来てしまうというのは、やはり目からウロコ的な体験で面白かった。

- 上位レベルの関数から順に追っていけばプログラム全体が見通せるので、誰が書いてもそこそこ読みやすくなりそうな気がする。

- parcel + Elmの組み合わせでホットリローディングが出来るので、これはオススメ。








.

2019年3月22日

React/TypeScriptでリバーシゲームを作る (5) - アニメーション

前回でようやくそれなりに強い思考ルーチンを実装することが出来たので、今回は仕上げとして石を裏返すときのアニメーションと、画面遷移時のアニメーションを実装します。


こちらを開くと実際に遊べます。
https://reversi-d1kqojbar.now.sh/


現時点のソースコードはこちらにあります。



1. 石をひっくり返す時のアニメーション


まず、石を置いた時には StoreクラスのsetStone()メソッドが呼ばれるようになっているので、その中で20msの間隔を空けて20回の「EV_BOARD_FLIPPING」イベントを発生するようにしました。

このイベントをBoardコンポーネントで受け取って、各セルを描画する際に、「もしひっくり返しアニメーションの実行中で、かつひっくり返し対象のセルであれば、Cellコンポーネントにflippingプロパティを通してその旨を伝える」ということを行っています。

Cellコンポーネントでは、flippingプロパティがnullでない場合は、

 flipping.count / flipping.total 

で現在のアニメーションの進行率が分かるので、それに応じて円を描画するときの横幅を変化させています。

さらに、アニメーションの進行率が 50% を越えた時点で石の色を反転させるようになっています。

これで石をひっくり返すアニメーションは上手く行きました。


石をひっくり返すアニメーション

---



ちなみに、CSSトランジションを使えば自分で石の横幅を変化させなくても、開始状態と終了状態を指定するだけでブラウザ側でアニメーションさせることが出来ます。その方法を使った方がパフォーマンス的には良かったかも知れません。




2. 画面遷移時のアニメーション


今回のアプリケーションでは、

 タイトル画面 → 設定画面 → ゲーム画面 

という流れで画面が切り替わるようになっています。この切り替えのタイミングで、次に進む場合は右から左へ、前の画面に戻る場合は左から右へとアニメーションする処理を入れました。

タイトル画面 → 設定画面 → ゲーム画面の遷移アニメーション

---

この部分は、CSSトランジションを使っています。

Reactでは 「React Transition Group」というライブラリを使うのが半ば公式に推奨されているみたいです。

https://github.com/reactjs/react-transition-group

これを使うのも悪くないのですが、今回は勉強のために全て自前で実装してみました。その結果、かなり試行錯誤して時間がかかりましたが、分かってみればそんなに難しくはないので、これぐらいの画面遷移であれば自前でやってしまうのもありだと思います。

基本的には、CSSで表示状態のスタイルと非表示状態のスタイルを定義しておいて、各画面のコンポーネントでCSSクラスを切り替えるだけです。

非表示状態の画面は透明(opacity:0)でかつ表示位置が画面の範囲外になるようにしていますが、それだけだとドキュメント全体のサイズが、表示されていない部分まで含んで認識されてしまって、余計な横スクロールが出来てしまう状態になったので、そこは一工夫が必要でした。

これに対処するために、一段上のAppコンポーネントのスタイルで、

.App {
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}

とすることで、画面外の部分はドキュメント全体のサイズに影響をおよぼさないようになり、余計な横スクロールを抑止することが出来ました。



3. スマホ対応


AndroidやiOSの実機で動作確認していると、色々と気になる点が出てきました。

スマホ向けにPWAなどでゲーム的なアプリケーションを作る場合には常に出てくる問題だと思いますが、以下の3つの問題があります。

1. ダブルタップ問題
2. ピンチズーム問題
3. Pull to Refresh問題


これらになんとか対処するために色々と調べて、以下の変更を行いました。どれがどの問題への対策だったかよく覚えていないのですが、とりあえず下記の対策をしておけば良いのではないかと思います。


htmlタグに属性を追加

style="overflow-y: hidden;"


viewportメタタグの設定

meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no,shrink-to-fit=no"

モバイルアプリ用メタタグの追加

meta name="apple-mobile-web-app-capable" content="yes"
meta name="mobile-web-app-capable" content="yes"



これで少なくとも手元のAndroid端末(Android 9.0)ではほぼ完璧になりました。

Chromeブラウザでページを開いてから、「ホーム画面に追加」を選んでホーム画面から起動すると、ブラウザのアドレスバーも無くなり、ダブルタップしようが上下にスワイプしようがびくともしません。ほぼネイティブアプリの感覚ですね。

手元のiOS端末(iPhone 5s / iOS 12.1.3)では、Pull to Refresh問題は解消しましたが、ダブルタップやピンチズームは反応しなかったり何かのタイミングで急に出来てしまったり、よくわからない感じです。(笑)


4. 終わりに


さて、ここまでで今回の「React/TypeScriptでリバーシゲームを作る」シリーズは無事終了です! 10月から3月まで、ほぼ半年もかかってしまいました。その間にGoogle発のクロスプラットフォーム開発環境であるFlutterが正式リリースされて、最近はかなり人気も高まってきているようです。


今度はFlutterでまたリバーシゲームを作ってみようかなあと思っているところです。




React/TypeScriptでリバーシゲームを作る

(1) - ボードの描画と石の配置
https://blog.makotoishida.com/2018/10/reacttypescript.html

(2) - ゲームロジック
https://blog.makotoishida.com/2018/11/reacttypescript-2.html

(3) - 思考ルーチンその1
https://blog.makotoishida.com/2019/01/reacttypescript-3-1.html

(4) - 思考ルーチンその2
https://blog.makotoishida.com/2019/03/reacttypescript-3-2.html

(5) - アニメーション
https://blog.makotoishida.com/2019/03/reacttypescript-5.html






 

2019年3月7日

React/TypeScriptでリバーシゲームを作る (4) - 思考ルーチンその2

前回でコンピュータの思考ルーチンの枠組みは出来ましたが、まだ単純なルールで動いているだけなので全く強くありませんでした。

今回は、そこそこ強い思考ルーチンを作ることに挑戦してみました。

出来たソースコードはこちらにあります。
https://github.com/mikehibm/reversi-react/tree/blog-4


動いているものはこんな感じになりました。
---

---

下のURLで実際に動かせますのでぜひ試して見て下さい。

https://reversi-d1kqojbar.now.sh/


CPUのレベルは1から3まであります。レベル1は1手先、レベル2は2手先、レベル3は3手先まで打てる可能性のある場所を全て調べて、最も有利になりそうな場所に打つようになっています。

思考ルーチンの内容は長くなるので省略しますが、とても面白いテーマです。興味のある方には、次の本を強くおすすめしておきます。これからリバーシを自分で作ってみようと思う人には本当に役に立つ情報が詰まっています。
---

---

今回は、上の本では「用いないほうが良い」と書かれている「得点テーブル」による評価を使ってしまいましたが、それに加えて「確定石」の数による評価を組み合わせて実装したところ、意外とまあまあ強い思考ルーチンになったような気がしています。


さて、このアプリケーションではコンピュータの思考ルーチンを Web Worker を使って別スレッドで実行するようになっています。と言うと簡単に聞こえますが、実際にはかなり試行錯誤と苦労の連続でした。


なぜ苦労したかと言うと、それなりに複雑な処理をWebWorkerで実行しようとするとやはり TypeScript を使いたいし、複数のWorker間で共通に使える関数はモジュール化して import 出来るようにしたかったからです。


① WebWorkerもTypeScriptで書く、かつimport文を使えるようにする

②  Create React App で作成されたプロジェクトをejectせずに(Webpackの設定を変えずに)これを実現する


この①と②の目標を達成するためにいろいろと試した結果、前回の記事で使った「Workerの関数をtoString()で文字列化した上でBlobとして読み込んでからWorkerスレッドを生成する」という方法ではなく、シンプルな

  const worker = new Worker('my-worker.js');

という形式で単にpublicフォルダに置いたJSファイルを指定して読み込む方法を使うことにしました。

その上で、WebWorker関連のTypeScriptファイルだけをアプリケーション本体とは切り離して独自にトランスパイルする方法を考えました。


ただ、共通部分をモジュール化して import/export を使うというのは、結局WebWorkerとの組み合わせではいい感じで正しく動かすための設定方法を見つけることが出来ませんでした。(tscでトランスパイルするのではなくwebpack/babelとworker-loaderプラグインなどを使えばなんとかなるのかも知れません。詳細は末尾の参考URL参照)


その代わりに、

  importScripts('インポートされるJSファイル名') 

という記法はWebWorkerの中で問題なく使えたので、これを利用することにしました。


プロジェクトのルートに tsconfig.json がありますが、それとは別に「tsconfig.worker.json」ファイルを作成しました。

---

---


  "module": "none",

とすることでモジュールシステムを使わないようにしている点と、

  "outDir": "public"

でトランスパイル後のJSファイルを直接publicフォルダに保存している点に注目です。


これで、

  tsc -p tsconfig.worker.json

を実行すると
  public/reversi.worker.js
  public/players/cpu1.worker.js
  public/players/cpu2.worker.js
  public/players/cpu3.worker.js

という4つのJSファイルが出来るようになります。


cpu1〜3.worker.jsの先頭では、

  importScripts('../reversi.worker.js');

とすることで共通部分である reversi.worker.js を読み込んでいます。


あとは、アプリ内でWorkerを生成する必要があるときに

  new Worker('players/cpu1.worker.js');

のような感じで読み込めば良いということになります。




参考URL:
Workerを駆使するためのプロジェクト構成 with webpack - Qiita
https://qiita.com/_likr/items/d382dc120a942ba4c6fe
4パターンのWebWorker生成方法とインラインワーカーの技法 - Qiita
https://qiita.com/mohayonao/items/872166cf364e007cf83d
Two example projects which use WebWorker in TypeScript + Webpack environment.
https://github.com/Qwaz/webworker-with-typescript 





React/TypeScriptでリバーシゲームを作る


(1) - ボードの描画と石の配置
https://blog.makotoishida.com/2018/10/reacttypescript.html

(2) - ゲームロジック
https://blog.makotoishida.com/2018/11/reacttypescript-2.html

(3) - 思考ルーチンその1
https://blog.makotoishida.com/2019/01/reacttypescript-3-1.html

(4) - 思考ルーチンその2
https://blog.makotoishida.com/2019/03/reacttypescript-3-2.html

(5) - アニメーション
https://blog.makotoishida.com/2019/03/reacttypescript-5.html






.