- 投稿日:2020-12-04T19:50:16+09:00
StripeでApple PayとGoogle Payをウェブアプリに対応させる方法【Stripe, Next.js, React.js, Typescript, Javascript】
PostCoffeeでウェブエンジニアをしています、@imryoheiです!
概要
普段は〜Pay(Apple Pay, Google Pay, Line Payなど)を利用するだけでしたがつい最近StripeとNext.jsを使ってApple PayとGoogle Payをシステムに実装しました。弊サービスのECサイトではチェックアウト画面で顧客情報の入力を求めていましたが、Apple PayとGoogle Payの導入によりログイン情報だけを求める決済UIUXを実現することができました。ユーザーの入力時間を省くことでスマートな決済体験を生み出すことが可能になりました。
本記事では、実際に開発中につまづいたポイントなども含めながら、StripeのPayment Request Buttonの実装方法を解説していきます。※Stripeの決済処理などは割愛しています。
それではApple Pay & Google Payの実装を始めましょう。
実装方法
- StripeのPayment Request Buttonを実装
- Stripeがブラウザに合わせて自動的にApple PayまたはGoogle Payを表示
- 購入ボタンを押してペイメントシートを表示
- 決済に必要な情報を編集・選択
- Stripeの決済処理を実行(サブスクまたは単品購入)
- 完了or失敗
前提条件
- ブラウザに支払い方法を追加済みであること。Chromeにカードを保存、またはSafariで使うにはWalletにカードを保存。
- HTTPS通信。開発/本番環境で必要です。Ngrokと言うサービスを使うとデプロイなしでHttps通信が可能になるので便利です!(ローカル環境を共有できるんですね...知らなかった。笑 まじで便利ですね)
- Apple Payを使用するためには、ドメインを登録する。開発/本番環境で設定が必須です。
- https://stripe.com/docs/stripe-js/elements/payment-request-button からdomain association fileを取得
設定
- domain association fileを次のようにホストする。
https://your doman here.com/.well-known/apple-developer-merchantid-domain-association
- Stripe経由でAppleにドメインを登録(Stripeのダッシュボードで登録しました)
- 1と2が問題なく完了するとApple Payのテストができるようになります。
テスト方法
Google Payをテストするための環境設定は特にありませんが、Google Payのテストカードを取得するにはテストカード スイートから「ユーザーグループに参加」を見つけて、グループに参加してください。参加するだけでテスト用のGoogle Payを使えるようになります。Goole Payのテスト決済のやり方がわからずStripeのテストカードでカード登録を行ったりしていたらなんと二日くらい無駄にしました。。。
Apple Payはセキュリティー上fakeカードを登録することができないらしく、リアルカードを登録して決済テストを行います。ドキュメントによると、
Apple won’t let you add a fake credit card to Passport and the iPhone. However, if you’re using Apple Pay and your test Stripe API keys, we’ll recognize the situation when the Apple Pay request is made and return a successful test card token. To test a payment in live mode, you’ll need to use a working credit card.
Stripe側がtest API keyかどうかを判定するらしいので請求されません。
実装
Stripeのドキュメントを参考に解説していきます。
import React, {useState, useEffect} from 'react'; import {PaymentRequestButtonElement, useStripe} from '@stripe/react-stripe-js'; const CheckoutForm = () => { const stripe = useStripe(); const [paymentRequest, setPaymentRequest] = useState(null); useEffect(() => { if (stripe) { const pr = stripe.paymentRequest({ country: 'JP', currency: 'jpy', total: { label: 'Demo total', amount: 1099, }, requestPayerName: true, requestPayerEmail: true, }); // Check the availability of the Payment Request API. pr.canMakePayment().then(result => { if (result) { setPaymentRequest(pr); } }); } }, [stripe]); if (paymentRequest) { return <PaymentRequestButtonElement options={{paymentRequest}} /> } // Use a traditional checkout form. return 'Insert your form or button component here.'; }StripeがPayment Request Buttonを使えるかどうかを判定し、使える場合はユーザーのブラウザに合わせてボタンが表示されます。SafariはApple Pay、ChromeはGoogle Payになります。Apple Pay利用可能の場合はresultが
{applepay: true}
になります。
PaymentRequest
にはshippingOptions
(配送方法)を追加することもできます。複数登録することができるため配列になっています。const pr = stripe.paymentRequest({ country: 'US', currency: 'usd', total: { label: 'Demo total', amount: 1099, }, requestPayerName: true, requestShipping: true, requestPayerEmail: false, requestPayerPhone: true, shippingOptions: [{ id: 'payment id', label: 'ネコポス (クロネコヤマト)', amount: totalAmount, }], });ユーザーから取得できる情報は、name(フルネーム), shipping address(配送先), email(メール), phone(電話番号)ですがemailが不要であればfalseまたは指定しないと非表示になります。これら全て必須キーではないためオブジェクトから省いても大丈夫です。必須情報は
country
、currency
、total
です。
https://stripe.com/docs/js/payment_request/createユーザーの住所変更を検知する場合は
paymentRequest
に用意されているイベントリスナーを利用します。以下のコードはドキュメントのサンプルコードです。event
から変更後の住所を取得し、サーバー側で配送料を計算させていますね。paymentRequest.on('shippingaddresschange', function(ev) { if (ev.shippingAddress.country !== 'JP') { ev.updateWith({status: 'invalid_shipping_address'}); } else { // Perform server-side request to fetch shipping options fetch('/calculateShippingPrice', { data: JSON.stringify({ shippingAddress: ev.shippingAddress }) }).then(function(response) { return response.json(); }).then(function(result) { ev.updateWith({ status: 'success', shippingOptions: result.supportedShippingOptions, }); }); } });配送先によって配送料金が変わるという前提のはずなので、
shippingOptions
、厳密にはamount
表示の更新をするためにupdateWith()
関数を呼び出しています。requestShipping
をtrueにすると、配送先入力が必須になるのでshippingOptions
を更新が必要みたい?です。(配送先なし =requestShipping = false
で検証してないので憶測ですが多分あってると思います。)const shippingOptions = [{ id: 'payment id', label: 'ネコポス (クロネコヤマト)', amount: totalAmount, }],以下のイベントはStripeがWalletまたはChromeに保存されたカードを処理し
paymentMemthod
のidを生成してくれます。event
からpaymentMethod.idを取得できるようになっています。決済処理はここに含めると良いと思います。token
イベントもあるので、プロジェクトに合わせてどのイベントを利用するか選ぶと良いと思います。paymentRequest.on('paymentmethod', async (event) => { // 決済ロジック with event.paymentMethod.id // if(payCompleted) { // event.complete('success') // 必須 // } else { // event.complete('fail') // 必須 // } });また、決済開始から30秒以内にeventのcompleteを呼び出す必要があります。決済開始から30秒を超えるとペイメントシートで支払い失敗のエラーが発生します。サーバー側で決済完了するのにペイメントシートでエラーが発生する場合は、決済処理を30秒以内に最適化すると良いと思います。
イベントの詳細はここで確認できます!
詳しくはこちらから: https://stripe.com/docs/js/payment_request/events/on_paymentmethodボタンのデザイン
オプションにstyleを含めることでデザインを変更することが可能です。
const options = { paymentRequest, style: { paymentRequestButton: { type: 'default', // One of 'default', 'book', 'buy', or 'donate' // Defaults to 'default' theme: 'dark', // One of 'dark', 'light', or 'light-outline' // Defaults to 'dark' height: '64px', // Defaults to '40px'. The width is always '100%'. }, } } <PaymentRequestButtonElement options={options} />Google Payは以下のボタンが表示されます。Google PayのデザインにならないのでGoogle Payを本番環境で利用するためにはGoogleのレビューが必要になると思いましたが、審査は不要でした。これはStripeの仕様で、開発環境も本番環境もこのボタンのデザインになります。
詳しくはこちらから:
https://stackoverflow.com/questions/58027517/displaying-stripe-payment-request-button-as-a-google-pay-button所感
ECサイトにApple Payのチャリーンって音が出るようになった時テンション上がりました。また新機能を開発したら記事をあげます!最後まで読んでいただきありがとうございました!
?☕️
- 投稿日:2020-12-04T16:59:43+09:00
【TypeScript】useContextとuseStateを組み合わせて、子孫コンポーネントから直接先祖コンポーネントのstateを編集する
実務でReactをいろいろ経験させてもらってから、ようやくReact Hooksの便利さに気づき始めたラスカルです。こんにちは。
今回は、タイトルの通りではあるのですが、useContextとuseStateを組み合わせることで、子孫コンポーネントから、propsのバケツリレーをせずに先祖コンポーネントのstateを更新する実装をTypeScriptで実装したいと思います。
(強調したのは、jsでの実装はあるもののtsでの実装がすぐに見つからなかったからです?)
前提条件
import React, { useState } from 'react' //親コンポーネント const Parent: React.FC = () => { const [count, setCount] = useState(0) return ( <> <Child /> </> ) } //子コンポーネント const Child: React.FC = () => { return ( <> <GrandChild /> </> ) } //孫コンポーネント const GrandChild: React.FC = () => { return ( <> <button></button> <button></button> </> ) }親コンポーネントはuseStateを使って、状態管理をしています。
このcountの値を更新するためには、setCountを使ってあげる必要がありますね。
これを孫コンポーネントでボタンをおすイベントで、行いたいというわけです。結論
useContextを使い、propsのバケツリレーをしなくても、孫要素から直接親要素のstateを更新できるようにします。
手法としてはuseContextで直接count, setCountを孫コンポーネントに送ってしまいます。
import React, { useState, useContext } from 'react' //親コンポーネント //useContextの初期値を設定。 const CountContext = React.createContext({} as { count: number setCount: React.Dispatch<React.SetStateAction<number>> }) const Parent: React.FC = () => { const [count, setCount] = useState(0) return ( <> //孫コンポーネントを含む子コンポーネントをuseContextで定めた変数で囲む。 //valueでcountとsetCountをオブジェクトで渡している点に注意 <CountContext.Provider value={{ count, setCount }}> <Child /> </CountContext.Provider > </> ) } //子コンポーネント //特に変更なし const Child: React.FC = () => { return ( <> <GrandChild /> </> ) } //孫コンポーネント const GrandChild: React.FC = () => { // 親要素で指定した変数を受け取る const {count, setCount} = useContext(CountContext) return ( <> //親要素のuseStateがそのまま使える! <button onClick={() => setCount(count + 1)}>+</button> <button onClick={() => setCount(count - 1)}>-</button> </> ) }ここで大事なのは親コンポーネントの
const CountContext = React.createContext({} as { count: number setCount: React.Dispatch<React.SetStateAction<number>> })の部分です。
jsでは
const CountContext = React.createContext()と、初期値を特に設定しなくてもいいようですが、(いい意味で)型に厳しいTypeScriptでは初期値を必ず設定してあげる必要があるようです。
補足
もし親コンポーネントと孫コンポーネントが別ファイル(というか、その状況の方が多い気もするけど)の場合、
- 親要素のuseContextで設定した変数にexportをつけてあげる
- 孫要素でその変数をインポートする
必要があります。
【親コンポーネント】
Patrent.tsximport React, { useState } from 'react' //useContextの初期値を設定。 export const CountContext = React.createContext({} as { count: number setCount: React.Dispatch<React.SetStateAction<number>> }) const Parent: React.FC = () => { const [count, setCount] = useState(0) return ( <> //孫コンポーネントを含む子コンポーネントをuseContextで定めた変数で囲む。 //valueでcountとsetCountをオブジェクトで渡している点に注意 <CountContext.Provider value={{ count, setCount }}> <Child /> </CountContext.Provider > </> ) }【子コンポーネント】
Child.tsx//特に変更なし const Child: React.FC = () => { return ( <> <GrandChild /> </> ) }【孫コンポーネント】
GrandChild.tsximport { CountContext } from './Parent.tsx' const GrandChild: React.FC = () => { // 親要素で指定した変数を受け取る const {count, setCount} = useContext(CountContext) return ( <> //親要素のuseStateがそのまま使える! <button onClick={() => setCount(count + 1)}>+</button> <button onClick={() => setCount(count - 1)}>-</button> </> ) }最後に
propsのバケツリレーがなくなったので、改修がかなりしやすくなりましたね。
ただuseContextは今回で言う親コンポーネントと孫コンポーネントの依存性をバリバリに強くしてしまうので、再利用性がやや失われてしまいます。ご利用は計画的に?
- 投稿日:2020-12-04T16:55:57+09:00
axios をモックする方法 (React + TypeScript)
はじめに
HTTPクライアントでお馴染みの axios のモック化を試したかったので、
それに関しての導入記事となります。モックに関しては、 axios-mock-server を利用します。
フロントは、 React + TypeScript でサンプルを記述します。
環境
- macOS Catalina 10.15.6
- node: v15.3.0
- yarn: 1.22.10
- TypeScript: 4.1.2
- React: 17.0.0
- axios-mock-server: 0.19.0
1. パッケージの導入
$ yarn add axios $ yarn add --dev axios-mock-servernpm じゃなくて yarn を利用しています。
2. axios を利用した HTTPクライアント の作成
axios を利用した形の HTTPクライアント部分を実装します。
とりあえず最低限の get/post のみです。src/data/rest.tsimport axios from "axios"; type Data = { [key: string]: string | number | boolean | object } const rest = (() => { const client = axios.create({ baseURL: '', // 今回は未指定 (通常は env などから参照させる) timeout: 15000, }); return { client, get: <T = any, R = AxiosResponse<T>>(url: string): Promise<R> => { return client.get(url); }, post: <T = any, R = AxiosResponse<T>>(url: string, data: Data): Promise<R> => { return client.post(url, data); } })(); export { rest };この時点ではまだモックを考慮していません。
また、本サンプルでは POST は未使用ですが一応定義しておきます。3. API のインターフェースを定義
今回仮に
User一覧
を返すAPIを想定します。
一旦クエリパラメータは無しで。
Method URL GET http://localhost:3000/user/list
レスポンスの型定義
src/data/type/index.tstype User = { id: number; name: string; } export type { User };APIの実装部分
src/data/api/getUsers.tsimport { rest } from '../rest'; import { User } from '../type'; const getUsers = async (): Promise<User[]> => { const url = "/user/list"; try { const { data } = await rest.get<User[]>(url) return data; } catch (error) { throw new Error(error); } } export { getUsers };コンポーネント側から呼び出す場合はこんな感じです。
src/component/Users.tsximport React, { useEffect } from 'react'; import { getUsers } from '../data'; const Users: React.FC = () => { useEffect(() => { getUsers().then((users) => { console.log('# users', users); }); }, []); return <span>Users</span>; }; export { Users };4. mockserverrc の作成
プロジェクトのルート階層(package.jsonがある場所)に .mockserverrc を作成します。
今回 モックAPI のファイルとして、
/src/data/mock
に格納したいので、そこのパスを指定しています。.mockserverrc{ "input": "./src/data/mock" }5. モックAPI の作成
モックAPIを格納するディレクトリは以下の通りです。
(見ての通りディレクトリ構造に沿って、自動的にルーティングが設定されます)├── data │ └── mock │ ├── $mock.ts ... ※自動生成されるファイル (後述で説明) │ └── user │ └── list.tsUser一覧のモックAPIの実装です。
src/data/mock/user/list.tsimport { MockMethods, MockResponse } from 'axios-mock-server'; import { User } from '../../type'; const list: MockMethods = { get: async (): Promise<MockResponse> => { const data: User[] = [ { id: 1, name: 'A' }, { id: 2, name: 'B' } ]; return [200, data]; // 200 はステータスコード } }; export default list; // ここは `default export` にしないと動かない6. モックAPIをビルドするスクリプトを定義
package.json の
scripts
にモックAPIのビルド設定を追記します。
-c
で 設定ファイルを指定しています。package.json... "scripts": { ... "mock:build": "axios-mock-server -c .mockserverrc" },7. モックAPIのビルド
以下のコマンドを実行します。
$ yarn mock:build
$mock.ts
が自動生成されていればOKです。
生成されたコードは以下の通りです。
自動生成されたファイルは、適宜.gitignore
に追加してください。data/mock/$mock.ts/* eslint-disable */ import { AxiosInstance } from 'axios' import mockServer from 'axios-mock-server' import mock0 from './user/list' export default (client?: AxiosInstance) => mockServer([ { path: '/user/list', methods: mock0 } ], client, '')8. axios とモックの紐付け
手順
2.
で生成した HTTPクライアント に修正を加えます。
自動生成した mock モジュールをimport
し、
関数呼び出しの引数に axios の client を渡しております。また、お好みで ログ出力 や 遅延設定 は行なえます。
src/data/rest.tsimport axios, { AxiosResponse } from "axios"; + import { mock } from './mock'; ... 省略 ... + const useMock = true; + if (useMock) { + mock(rest.client) + .enableLog() + .setDelayTime(500); + } export { rest };9. 起動コマンドの追加 及び 動作確認
このままだとモックAPIを改修する度に、
mock:build
コマンドを実行しないといけないので、
独自の起動コマンドを追加します。package.json の
scripts
に以下を追加します。package.json... "scripts": { ... "mock:build": "axios-mock-server -c .mockserverrc" + "mock:start": "axios-mock-server -c .mockserverrc && react-scripts start" },
react-scripts start
はyarn start
で実行するコマンド内容です。ローカルでの実行は以下のコマンドとなります。
$ yarn mock:start
enableLog()
を有効にしていると、API呼び出し時にログが出力されます。
まとめ
以上、axios をモックする方法となります。
サンプルコードは以下にアップしています。
https://github.com/unpii/react-axios-mock-example
- 投稿日:2020-12-04T15:55:35+09:00
TypeScript+React+Cloud Firestore+Stateモナドで、ブラウザで動くボードゲームの対戦ツールを作った (3/3 UI・通信編)
前回はボードゲーム、ナショナルエコノミーオンライン対戦ツールにおけるゲームメカニクス部のStateモナドを用いた実装手法について解説しました。
今回はCloud Firestoreとの通信、およびReactを用いたUIの実装を行います。はじめに
通信とUIの実装はメカニクス部と異なり実際に動かしながら動作を確認する必要があります。webpack-dev-serverを用いるなり、create-react-appを用いるなりしてローカルに開発環境を構築し、ブラウザからアクセスして見た目を確認しつつ行うのが手っ取り早いでしょう。
身内にのみ限定して共有する予定であったことから、本対戦ツールでは認証などの仕組みを実装しませんでした。したがって本記事で解説する実装は、あまり「安全」な構成にはなっていないことをご留意ください。Cloud Firestoreとの通信
接続
Firebase SDKの
initializeApp
関数を用いてCloud Firestoreとの接続を確立します。import firebase from "firebase/app"; const db = firebase.initializeApp({ apiKey: "*******-*******************************", authDomain: "****.firebaseapp.com", databaseURL: "https://****.firebaseio.com", projectId: "****", storageBucket: "****.appspot.com", messagingSenderId: "************", appId: "*******" }).firestore();スクリプトの冒頭で作成したこの
firebase.Firestore
型を全体で用いることで送受信を行います。Cloud Firestoreのデータ構造は、最小単位を「ドキュメント」として、それを保有する「コレクション」によって成り立っています。ドキュメントはKVS型のレコードであり、実質的にはJSONと同じです。したがって、メンバすべてが数値、文字列、あるいはその配列やオブジェクトであるような
Game
型をそのままJSONとして送受信することができます。
実際には、ゲーム内各種操作のログも保持・共有できるように、合わせた型として次のGameAndLog
を定義して、それを送受信します。game-and-log.tstype GameAndLog = { game: Game, log: string[]; }受信
ゲーム状態の取得はいちどきりではなく、書き込みが行われるたびにリアルタイムに更新される必要があります。これはコールバックの登録によっても購読できますが、今回はrxfireを用いて
Observable
の形でゲーム状態を購読することにします。fetch.tsimport * as firebase from "firebase"; import { Observable } from "rxjs"; import GameAndLog from "entity/gameandlog"; import { docData } from "rxfire/firestore"; export function fetchGame(db: firebase.firestore.Firestore, id: string): Observable<GameAndLog> { return docData(db.collection("games").doc(id)); }事前準備としてCloud Firestoreのコンソール上で、「games」コレクションを作成しておきました。「games」コレクション内の適当な「ゲームID」ドキュメントを
GameAndLog
型として購読します。
あとはこれを必要な個所でsubscribe
すれば、リアルタイムに更新されるGameAndLog
が得られます。送信
ゲーム状態の送信は、適切な操作がなされた場合にそれに対応するStateモナドと旧
GameAndLog
から新たなGameAndLog
が作成された際に行われます。fetch.ts(続き)export function updateGame(db: firebase.firestore.Firestore, id: string, game: GameAndLog) { db.collection("games").doc(id).update(game); }新たなゲームの開始
送受信メソッドに含まれる「ゲームID」は、Cloud Firestoreに対し新たなドキュメントを作成させる形で生成することができます。適当な手段で決定したリーダークライアントがこのメソッドを呼ぶことにより新規ゲームを開始します。
function createGame(db: firebase.firestore.Firestore, id: string, game: Game) { db.collection("games").add({game: game, log: []}) .then(doc => db.collection("rooms").doc(`room${id}`).update({game_id: doc.id})); }ゲーム開始前のロビーの状態を保持するコレクションとして「rooms」コレクションを定義しており、これに
add
メソッドで生成されたゲームIDを書き込むことで「部屋が進行中のゲームで埋まっている」ことを表現しています。UI
特別変わったことをするわけではなく、
Game
型で定義される現在のゲーム状態を用いて各種コンポーネントを順に作成していきます。ゲーム状態の購読
ゲーム状態が
Observable
で得られるため、次のような購読用カスタムフックを作成するとよいでしょう。use-observable.tsimport * as React from "react"; import { Subscription, Observable } from "rxjs"; export default function useObservable<T>(observable: Observable<T>): [T | null, any | null, () => void] { const [subscription, setSubscription] = React.useState<Subscription | null>(null); const [value, setValue] = React.useState<T | null>(null); const [error, setError] = React.useState<any | null>(null); const ref = React.useRef(subscription); React.useEffect(() => { setSubscription(observable.subscribe(v => { setValue(v) }, setError)); return () => ref.current?.unsubscribe(); }, []); return [value, error, () => subscription?.unsubscribe()]; }Reactコンポーネントの作成
ナショナルエコノミーはすべてのコンポーネントがカードで構成されたゲームであり、「手札」や「所有不動産」など、カードを並べる似た形式のコンポーネントを多く用いることになります。
コンポーネントの作成手段についてはいくらでも記述が見つかると思われるので、ここでは一例としてcards
コンポーネントだけを取り上げます。cards.tsximport * as React from "react"; import Card from "./atoms/card"; import * as style from "./cards.styl"; import { CardName } from "model/protocol/game/card"; import { Building } from "model/protocol/game/building"; import Workers from "./atoms/workers"; type Props = { title: string, tooltip?: string, cards?: CardName[], buildings?: Building[] }; const hand: React.FC<Props> = props => { const cards = props.cards ? props.cards.map((c, i) => <Card card={c} key={`${i}-${c}`} />) : null; const buildings = props.buildings ? props.buildings.map((b, i) => ( <Card card={b.card} key={`${i}-${b.card}`}> <Workers owners={b.workersOwner} /> </Card> )) : null; return ( <section className={style.cards}> <h2 title={props.tooltip}>{props.title}</h2> {cards ? <ul>{cards}</ul> : null} {buildings ? <ul>{buildings}</ul> : buildings} </section> ); }; export default hand;まとめ
今回の記事でCloud Firestoreとの通信、およびUIの構成について説明したため、これでゲーム作成に必要な要素を構築できるようになりました。
Firebase Authenticationを利用して、ゲームプレイにユーザ登録を要求するなど、触れていない部分も多くありますが、とりあえずStateモナドとCloud Firestoreの組み合わせ方については解説できたのではないかと思われます。参考文献
- 投稿日:2020-12-04T13:29:56+09:00
react-konvaでお絵描き
canvasを組み込んだwebサービスを作る機会があり、その際に利用したreact-konvaが使いやすかったので紹介です。
今回はreact-konvaとhookを利用して簡単なお絵描き機能を作っていきます。
以下のような感じです。
https://codesandbox.io/s/elastic-agnesi-pn76b
react-konvaとは
canvasのjsフレームワークkonva.jsを名前の通りreactで利用できるものです。
konva.jsのオブジェクトクラスがコンポーネントとして提供されています。konva.js
https://konvajs.org/docs/overview.html
react-konvaはStageというコンポーネントを土台に、その上にいろいろな要素を載せて実装を進めていきます。
例えば、LayerコンポーネントをStage上に複数載せることで、絵を描くときにあると嬉しいレイヤー機能を簡単に実装することができます。
イメージとして以下のようなコンポーネント階層を作っていきます(公式より抜粋)
hookを使った実装ではstateにオブジェクト要素を詰め込んでいき、その内容をcanvas上に描画していきます。
canvas上に絵を描くという目的を達成するために必要な処理は大まかに以下です。
①mouseDownなどのeventに反応してstateに線の描画位置や色設定などを詰め込んでいく。
②Layerコンポーネント内でstateの値を走査し、オブジェクトを描画していく。canvasの描画内容をstateで管理できるため、すごく楽に実装を進められます。
実装イメージ(いろいろ省いて抜粋)
sample.jsconst App = () => { const [lines, setLines] = React.useState([]); const handleMouseDown = (e) => { const pos = e.target.getStage().getPointerPosition(); // mouseDownなどのeventに反応してstateに値(線の描画位置や色設定など)を詰め込んでいきます。 setLines([...lines,{points: [pos.x, pos.y], color, size} ]); }; return ( <> <Stage onMouseDown={handleMouseDown} > <Layer> {/* stateを走査して詰め込んだ値を描画していきます。 */} {lines.map((line, i) => ( <Line key={i} points={line.points} stroke={line.color} strokeWidth={line.size} tension={0.5} lineCap="round" /> ))} </Layer> </Stage> </> ); }; export default App;最後に
業務はサーバーAPI構築がメインのためReactの勉強がてらと思いreact-konvaを選んだのですが、webは技術進歩が目覚ましく、すぐに置いていかれてしまいますね、、
(記事の内容と直接関係ないのですが)特に今では当たり前のように使われているオンラインエディター(今回はCodeSandbox)が使いやすく驚きました。
- 投稿日:2020-12-04T12:56:33+09:00
C4DとThree.jsで製品の3D表示ページを開発する
完成品の表示ページ:
https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj
C4D
1. C4Dモデリング
C4Dを使って製品のモデルを作成する。
ご注意:
Discの使用を避けること、ブラウザで解析できないため。
各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。
2.
*.obj
ファイルをエクスポートするfile -> Export -> Wavefront OBJ(*.obj)ソース実装
開発環境:
Node.js
/Webpack4
/React16
/Three.js
ソース:https://github.com/capricorncd/blog/tree/master/demos/three
1. Install
# "three": "^0.120.1" npm i -S three # or yarn add three2. ソース
src/components/ClockObj/core.js
import { AmbientLight, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three' import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' let scene, renderer /** * load resource * @returns {Promise<unknown>} */ function loadResource() { return new Promise((resolve, reject) => { const objLoader = new OBJLoader() const mtlLoader = new MTLLoader() // テクスチャファイルをロードする mtlLoader.load('static/clock.mtl', mtl => { // オブジェクトをロードする前に、まずテクスチャデータを設定します objLoader.setMaterials(mtl) // オブジェクトをロードする objLoader.load('static/clock.obj', res => { resolve(res) }, undefined, reject) }, undefined, reject) }) } /** * 初期化 */ function _init(el, obj) { // コンテナサイズを取得する // windowの場合、window.innerWidthとwindow.innerHeightで取得する const width = el.offsetWidth const height = el.offsetHeight // シーンを作成する scene = new Scene() // オブジェクトをシーンに追加する scene.add(obj) // 周囲光を作成する const ambientLight = new AmbientLight(0x666666) ambientLight.position.set(100, -100, -200) scene.add(ambientLight) // 指向性ライトを作成する const light = new DirectionalLight(0xcccccc, 1) light.position.set(2000, 1000, 1000) scene.add(light) // カメラを作成する const camera = new PerspectiveCamera(45, width / height, 1, 80000) camera.position.set(-150, -50, 300) // レンダラーを作成する renderer = new WebGLRenderer({ antialias: true }) // レンダリング領域のサイズを設定する renderer.setSize(width, height) // 背景色を設定する renderer.setClearColor(0x000000, 1) el.appendChild(renderer.domElement) const orbitControls = new OrbitControls(camera, el) orbitControls.addEventListener('change', render) function render() { renderer.render(scene, camera) } render() } /** * init */ export function init(el) { loadResource().then(res => { _init(el, res) }).catch(console.error) } /** * destroy */ export function destroy() { if (!scene || !renderer) return scene.remove() renderer.dispose() scene = null renderer = null }src/components/ClockObj/index.jsx
import React, { useEffect, useRef } from 'react' import { destroy, init } from './core' function ClockObjDemo() { const elRef = useRef() useEffect(() => { init(elRef.current) return () => { destroy() } }, []) return <main className="font-size-zero" ref={elRef} /> } export default ClockObjDemo完成品URL
https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj
- 投稿日:2020-12-04T12:56:33+09:00
C4DとThree.jsで製品の3D表示ページの開発
完成品の表示ページ:
https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj
C4D
1. C4Dモデリング
C4Dを使って製品のモデルを作成する。
ご注意:
Discの使用を避けること、ブラウザで解析できないため。
各ジオメトリをマップする必要があります。グループマップは使用しないほうがいい。
2.
*.obj
ファイルをエクスポートするfile -> Export -> Wavefront OBJ(*.obj)ソース実装
開発環境:
Node.js
/Webpack4
/React16
/Three.js
ソース:https://github.com/capricorncd/blog/tree/master/demos/three
1. Install
# "three": "^0.120.1" npm i -S three # or yarn add three2. ソース
src/components/ClockObj/core.js
import { AmbientLight, DirectionalLight, PerspectiveCamera, Scene, WebGLRenderer } from 'three' import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader' import { MTLLoader } from 'three/examples/jsm/loaders/MTLLoader' import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls' let scene, renderer /** * load resource * @returns {Promise<unknown>} */ function loadResource() { return new Promise((resolve, reject) => { const objLoader = new OBJLoader() const mtlLoader = new MTLLoader() // テクスチャファイルをロードする mtlLoader.load('static/clock.mtl', mtl => { // オブジェクトをロードする前に、まずテクスチャデータを設定します objLoader.setMaterials(mtl) // オブジェクトをロードする objLoader.load('static/clock.obj', res => { resolve(res) }, undefined, reject) }, undefined, reject) }) } /** * 初期化 */ function _init(el, obj) { // コンテナサイズを取得する // windowの場合、window.innerWidthとwindow.innerHeightで取得する const width = el.offsetWidth const height = el.offsetHeight // シーンを作成する scene = new Scene() // オブジェクトをシーンに追加する scene.add(obj) // 周囲光を作成する const ambientLight = new AmbientLight(0x666666) ambientLight.position.set(100, -100, -200) scene.add(ambientLight) // 指向性ライトを作成する const light = new DirectionalLight(0xcccccc, 1) light.position.set(2000, 1000, 1000) scene.add(light) // カメラを作成する const camera = new PerspectiveCamera(45, width / height, 1, 80000) camera.position.set(-150, -50, 300) // レンダラーを作成する renderer = new WebGLRenderer({ antialias: true }) // レンダリング領域のサイズを設定する renderer.setSize(width, height) // 背景色を設定する renderer.setClearColor(0x000000, 1) el.appendChild(renderer.domElement) const orbitControls = new OrbitControls(camera, el) orbitControls.addEventListener('change', render) function render() { renderer.render(scene, camera) } render() } /** * init */ export function init(el) { loadResource().then(res => { _init(el, res) }).catch(console.error) } /** * destroy */ export function destroy() { if (!scene || !renderer) return scene.remove() renderer.dispose() scene = null renderer = null }src/components/ClockObj/index.jsx
import React, { useEffect, useRef } from 'react' import { destroy, init } from './core' function ClockObjDemo() { const elRef = useRef() useEffect(() => { init(elRef.current) return () => { destroy() } }, []) return <main className="font-size-zero" ref={elRef} /> } export default ClockObjDemo完成品URL
https://capricorncd.github.io/blog/dist/three/index.html#/ClockObj
- 投稿日:2020-12-04T10:58:47+09:00
【入力値反映されない】Reactでクラスを使ったときにフォームでハマった話【Cannot read property 'state' of undefined】
この記事は「株式会社オープンストリーム "小ネタ" Advent Calendar 2020」の 3 日目の記事です。
React でフォームを実装したときにいきなりハマった点を共有したいと思います。
環境
$ node --version v14.15.0package.json{ ... "dependencies": { "next": "10.0.3", "react": "17.0.1", "react-dom": "17.0.1" }, "devDependencies": { "@types/node": "^14.14.10", "@types/react": "^17.0.0", "typescript": "^4.1.2" } }ちなみに私はVue(Nuxt)を使っていてReactに入門しています…!
state に紐づけたテキストボックスに入力しても反映されない
これはReactのドキュメントに書いてあるので、初めに読めば防げる問題ですが…
React で管理したいテキストボックスには
handleChange
関数とonChange
イベントを入れないと「入力しても反映されない」現象が起きます。src/App.tsximport React, { Component } from 'react'; import './App.css'; type Props = {} type State = { todo: TodoItem[], currentText: string } type TodoItem = { title: String } class App extends Component<Props, State> { constructor(props: Props) { super(props) this.state = { todo: [ { title: 'JavaScript覚える' }, { title: 'jQuery覚える' }, { title: 'ES2015覚える' }, { title: 'React覚える' } ], currentText: '' } this.handleChange = this.handleChange.bind(this) } handleChange(event: React.ChangeEvent<HTMLInputElement>) { this.setState({ currentText: event.target.value }) } render() { return ( ... <input type="text" value={this.state.currentText} onChange={this.handleChange}/> <input type="button" value="追加" onClick={this.addTodo} /> </div> ); }さらに、TypeScript で書くときはテキストボックスに入力したときの Event の型も指定します。
any にしなくても次の Event 型から使うことができます。any 型で諦めない React.EventCallback - Qiita
https://qiita.com/Takepepe/items/f1ba99a7ca7e66290f24クラス内の関数でstateを操作するとCannot read property 'state' of undefinedと出る
ボタンをクリックしたときにフォームで入力した値を使いたいですよね?ところがボタンをクリックしたときに次のメッセージが表示されます。
TypeError: Cannot read property 'state' of undefined
クラスの「中で」関数を書く場合、
state
などthis
から使うものは 関数をアロー関数にしないと関数が呼び出されたときにthis
がバインドされていない状態になってエラーになります。アロー関数で書いてみると次の通りになります。
src/App.tsxclass App extends Component<Props, State> { ... addTodo =()=> { this.state.todo.push({ title: this.state.currentText }) this.setState({ todo: this.state.todo, currentText: '' }) } deleteTodo = (i: number)=> { this.state.todo.splice(i, 1) this.setState({ todo: this.state.todo }) } render() { return ( <div> <h1>TODOアプリ</h1> <nav> <ul>{this.state.todo.map( (todo, i) => { return <li key={i}><input type="button" value="☓" onClick={() => this.deleteTodo(i)}/> {todo.title}</li> })}</ul> <input type="text" value={this.state.currentText} onChange={this.handleChange}/> <input type="button" value="追加" onClick={this.addTodo} /> </div> ); }参考
(コードの元ネタ)
React で作る TODO アプリ前編 – React 入門 - to-R Media
https://www.to-r.net/media/react-tutorial13/React.js で Form を扱う - Qiita
https://qiita.com/koba04/items/40cc217ab925ef651113any 型で諦めない React.EventCallback - Qiita
https://qiita.com/Takepepe/items/f1ba99a7ca7e66290f24「TypeError: Cannot read property ‘state’ of undefined」に出会ったら Javascript の”this”について真剣に考えよう
https://applingo.tokyo/article/1422
- 投稿日:2020-12-04T05:34:49+09:00
FNS歌謡祭のHPに実装されているアニメーションを自分も作成したい!!! ハンズオン!!
はじめに
お疲れ様です。
DMM WEBCAMP Advent Calendar 2020の4日目を担当させていただきます。
メンターの@koseiinfratopです。みなさん先日OAされたFNS歌謡祭はみましたか?
バンタンや3代目JSBなど豪華アーティストが出演されていて本当におもしろい番組でしたね。なかでもユーミンと嵐のコラボは感動的でした。
OA当日僕はふと思いました。嵐はいつごろ出番なのだろうかと。そこでFNS歌謡祭の公式HPを訪れたことで今回のAdvent Calendarで何を書くがが決定しました。
一度公式HPを訪れていただけるとわかるのですが、アーティストの画像にカーソルを乗せてみてください。カーソルを乗せると薄黒いボックスとアーティスト名が出てきます。僕は疑問に思いました。これはどうやって実装しているのかと。。。
ということで今回は疑問に思ったことを解消するためにFNSのHPのようにカーソルを画像に載せるとアニメーションが発火するような機能を実装しようと思います。
- 開発効率を上げたかったため今回はホットリロードが可能なReact.jsでコーディングしました。
- しれっとiTunesAPIも使用しています。
1.html(js)を記述
music.js...*iTunesAPI関係の処理は割愛 return( <div className="searchresult"> {artistData.map(artistdata => ( <ArtistData key={artistdata.CollectionId.toString()} id={artistdata.CollectionId} name={artistdata.ArtistName} album={artistdata.AlbumName} albumUrl={artistdata.AlbumUrl} genre={artistdata.AlbumGenre} release={artistdata.AlbumRelease} /> ) )} </div> );artistdata.jsimport React from 'react'; const ArtistData = (props) => { return( <div className={props.id ? 'album': 'noalbum'}> <div className="flex"> <img src={props.albumUrl} alt={props.album} className="albumImage"/> <div> <p>ジャンル: <b>{props.genre}</b></p> </div> </div> <div className="mask"> <div className="caption">{props.album}</div> </div> <div> <p>アーティスト: <b>{props.name}</b></p> <p>アルバム名: <b>{props.album}</b></p> <p>リリース日: <b>{props.release}</b></p> </div> </div> ); } export default ArtistData2.CSSの記述
*ポイントだけコメントアウトを用いて解説します。
music.css.album { width: 300px; height: 230px; /* overflow・・・アルバムクラスのdivタグ範囲内に内容が収まらない場合(今回で言うとmaskクラスのdivタグがはみ出る)の処理 overflow: hidden; /* 表示させないようにしている */ margin: 10px 8px 10px 16px; position: relative; border: ridge 10px #87CEFA; } .mask { width: 100%; height: 100%; position: absolute; top: -100%; /* 枠の上に配置し非表示にする。 */ opacity: 0; /* マスクスクラス内を透明化(0)にすることで非表示にする。*/ background-color: rgba(0,0,0,0.4); transition: all 0.6s ease; } .caption { font-size: 130%; text-align: center; color: #fff; } img.alabumImage { width: 80%; height: 80%; position: absolute; } .album:hover .mask { アルバムクラス内をhover(カーソルを乗せる)時に発火する。 opacity: 1; /* マスクを完全に不透明表示にする */ padding-top: 80px; /* ホバーで下にずらす */ top: 0; /* 先ほどのtop: -100% から top: 0;にすることにより下から降りてくるように見せることができる */ }
完成!!!
デモンストレーション
![]()
詳細な動き
form内にアーティスト(曲名も可)を入力し検索ボタンをクリックすることでitunesapiからアーティストが発売したアルバムを取得し、表示しています。
今回のmusic.js, music.cssの全コード
(※artistdata.js
は上記のコードが全コードです。)music.jsimport React, {useState } from 'react'; import axios from 'axios'; import ArtistData from './artistdata.js'; import '../styles/music.css' const Music = () => { const [artist, setArtist] = useState(''); const [artistData, setArtistData] = useState([{ CollectionId: '', ArtistName: '', AlbumName: '', AlbumUrl: '', AlbumGenre: '', AlbumRelease: '', }]); async function itunesGet(params){ try{ const prm = params.trim(); const response = await axios.get(`https://itunes.apple.com/search?term=${prm}&entity=album`) const responsedata = response.data.results const responseAPI = responsedata.map(value => { return { CollectionId: value.collectionId, ArtistName: value.artistName, AlbumName: value.collectionName, AlbumUrl: value.artworkUrl100, AlbumGenre: value.primaryGenreName, AlbumRelease: value.releaseDate, }; } ); setArtistData(responseAPI); console.log(artistData); }catch(error) { const { status, statusText } = error.response; console.log(`Error! HTTP Status: ${status} ${statusText}`) } }; return ( <div> <form onSubmit = {e => { e.preventDefault(); const artistnameElement = e.target.elements["artist"]; console.log(artistnameElement.value); itunesGet(artistnameElement.value); setArtist(artistnameElement.value); artistnameElement.value = ''; }} > <input type="text" id="artist" placeholder="アーティスト名または曲名を入力してください" /> <button type="submit">検索する</button> </form> <p className="result">検索結果: <b>{artist}</b></p> <div className="searchresult"> {artistData.map(artistdata => ( <ArtistData key={artistdata.CollectionId.toString()} id={artistdata.CollectionId} name={artistdata.ArtistName} album={artistdata.AlbumName} albumUrl={artistdata.AlbumUrl} genre={artistdata.AlbumGenre} release={artistdata.AlbumRelease} /> ) )} </div> </div> ) } export default Musicmusic.css* { margin: 0 auto; padding: 0; box-sizing: border-box; } form > :first-child { outline: none; border: 1px solid #aaa; transition: all .3s; border-radius: 2px; } form > :first-child { width: 400px; font-size: 18px; height: 24px; padding: 2px 8px; } form > :nth-child(1):focus { box-shadow: 0 0 7px #1abc9c; border: 1px solid #1abc9c; } form > :last-child { margin-top: 4px; margin-left: 7px; font-size: 16px; height: 40px; padding: 2px 8px; } form button { border: 1px solid #ccc; background-color: #FFFFFF; border-radius: 2px; cursor: pointer; box-shadow: 0px 2px 2px 0px rgba(0,0,0,.1); } form button:hover { box-shadow: 0px 2px 2px 2px rgba(0, 0, 0, .1); } .result { text-align: center; } .searchresult { display: flex; flex-wrap: wrap; justify-content: space-around; } .noalbum{ display: none; } .flex { padding: 3px 1px; display: flex; flex-direction: row; } .flex > :nth-child(2) { padding-top: 1.7em; padding-bottom: 0.5em; } .flex > :nth-child(2):nth-child(2){ font-family: 'Courier New', Courier, monospace; } .album { width: 300px; height: 230px; overflow: hidden; margin: 10px 8px 10px 16px; position: relative; border: ridge 10px #87CEFA; } .mask { width: 100%; height: 100%; position: absolute; top: -100%; opacity: 0; /* マスクにする */ background-color: rgba(0,0,0,0.4); transition: all 0.6s ease; } .caption { font-size: 130%; text-align: center; color: #fff; } img.alabumImage { width: 80%; height: 80%; position: absolute; } .album:hover .mask { opacity: 1; /* マスクを完全に不透明表示する */ padding-top: 80px; /* ホバーで下にずらす */ top: 0; }まとめ
- 「このサイト、アプリはどのように実装されているのだろうか?」と疑問を持ち実際に自分で実装することで自分の技術力UPに繋がるということを改めて知ることができたそんな師走上旬でした。。。
裏話
FNS公式HPのデベロッパーツールのSourceパネルと睨めっこしながら実装したのは内緒。。。
参考資料