- 投稿日:2021-08-09T22:58:28+09:00
TypeScriptで名前(文字)が入った丸いアイコン画像を作成する
前書き https://murasuke.github.io/js-name-icon/ チャットアプリのデフォルトアイコンとして、名前からアイコンを作成しようと考えました。 Canvasに名前を描画して、丸くくり抜いたものを画像化しています。 利用サンプル画面イメージ アイコンを作る関数を利用した画面のイメージです 名前のアイコンを作成してダウンロードします スペースで区切っていない場合は、先頭2文字で作成します ダウンロード画像 ダウンロード画像 ダウンロード画像 概要 名前からアイコン画像を作成します(.png)。オプションで色やフォントを指定可能 スペースを含む場合、splitして最初の2文字を結合する '山田 太郎' -> '山太'。 スペースを含まない場合、先頭2文字にする '山田太郎' -> '山田' 名前の文字列から、画像(オブジェクトURL)を生成して返します。imgタグのhrefにセットすると画像を表示できます。 // アイコンを作成する const imageUrl = await iconMaker('山本 太郎'); 関数定義 オプションは指定しなけれれば直径60pxの円でアイコンを作成します。 // Icon作成オプション export type IconOption = { size?: number, // iconのサイズ foreColor?: string, // フォントの色 backColor?: string, // 背景色 fontScale?: number, // フォントのサイズ(iconのサイズに対する比率(0.7程度が適当)) fontFamily?: string,// フォントの種類 }; // 関数定義 const iconMaker = async(name: string, option?: IconOption): Promise<string> => { }; export default iconMaker; 利用方法 作成した画像(png)をダウンロードするには下記のようにします。 imgタグの.hrefにセットすれば表示も可能です。 const downloadCanvasImage = async() => { const imageUrl = await iconMaker(txtName); // 画像ダウンロード const dlLink = document.createElement("a"); dlLink.href = imageUrl; dlLink.download = 'nameicon.png'; dlLink.click(); dlLink.remove(); }; ソース全体 // Icon作成オプション export type IconOption = { size?: number, // iconのサイズ foreColor?: string, // フォントの色 backColor?: string, // 背景色 fontScale?: number, // フォントのサイズ(iconのサイズに対する比率(0.7程度が適当)) fontFamily?: string,// フォントの種類 }; // Icon作成デフォルト値 const defaultValue: IconOption = { size: 60, foreColor: '#3c665f', backColor: 'aliceblue', fontScale: 0.7, fontFamily: 'sans-serif' }; const iconMaker = async(name: string, option?: IconOption): Promise<string> => { // デフォルト値をoptionのプロパティーで(あれば)上書き const opt = {...defaultValue, ...option}; const [width, height] = [opt.size, opt.size]; // 描画用のCanvasを用意する const canvas = new OffscreenCanvas(width, height); const context = canvas.getContext('2d'); if (!context) throw new Error('could not get context.'); // スペースを含む場合、splitして最初の2文字を結合する '山田 太郎' -> '山太'。 // スペースを含まない場合、先頭2文字にする '山田太郎' -> '山田' const splitName = name.split(' '); const abbrev = (splitName.length >= 2 ? splitName[0].substring(0,1) + splitName[1].substring(0,1) : name.substring(0, 2)); // canvasを円形にくり抜く(clip) context.beginPath(); context.ellipse(width / 2, height / 2, width / 2, height / 2, 0, 0, Math.PI * 2); context.closePath(); context.clip(); // 背景を塗りつぶす context.fillStyle = opt.backColor; context.fillRect(0, 0, width * 2, height * 2); // 名前を描画 context.fillStyle = opt.foreColor; context.font = `bold ${height * opt.fontScale}px ${opt.fontFamily}`; // 文字の中心を合わせる const mesure = context.measureText(abbrev); const centerX = width - mesure.width > 0 ? (width - mesure.width) / 2 : 0; const centerY = (height + mesure.actualBoundingBoxAscent + mesure.actualBoundingBoxDescent) / 2; context.fillText(abbrev, centerX, centerY, width); // Canvasの画像をオブジェクトURLへ変換(imgタグのhrefにセットすると画像を表示できる) const blob = await canvas.convertToBlob(); const imageUrl = URL.createObjectURL(blob); return imageUrl; }; export default iconMaker; ```
- 投稿日:2021-08-09T22:36:34+09:00
【Mac】ReactとPython flaskを使ったWebアプリ
Reactの基本的な勉強を済ませ、簡易アプリのソースを読んで知見を蓄え中の身です。本記事は、下記の記事に触発され、愛用しているMacbookのローカル環境で実行した時のメモです。 どんなアプリ? mecabを使った分かち書きスクリプトを使って、フロントで受け取った入力テキストをサーバー側で分かち書きをし、その結果をフロントで表示するという非常にシンプルなアプリです。 どんな構成? フロント側はReact、サーバー側はpython flaskで実装しています。 実装環境 macOS Big Sur 11.2.3 Python: 3.9.6 flask==1.0.2 npm: 7.20.3 1. Mecabのインストール 日本語の形態素解析エンジン"MeCab"をMacにインストールしてPython3から利用します。 $ brew install mecab $ brew install mecab-ipadic $ brew install git curl xz yarn $ git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git もしくは $ git clone --depth 1 git@github.com:neologd/mecab-ipadic-neologd.git $ cd mecab-ipadic-neologd $ ./bin/install-mecab-ipadic-neologd -n $ brew install swig これでmecab-python3をpip installできるようになりました。 2. ソースをgit clone mecab-ipadic-neologdフォルダ内にいる場合は、別フォルダに移動した方が良いでしょう。ここでは親フォルダに移動します。その後、ソースコードをgit cloneします。 $ cd .. $ git clone https://github.com/Pu-of-Parari/jparser_app.git $ cd jparser_app $ ls README.md backend frontend ref 3. サーバー側の準備 まだ動きません。python3の仮想環境内で必要なモジュールをインポートします。 いま、jparser_appフォルダ内にいます。 $ cd backend venvという名前で仮想環境を作成します。 $ python3 -m venv venv $ source venv/bin/activate 一応、pipをアップグレードします。 $ python3 -m pip install --upgrade pip requirements.txtを用いて、必要なモジュールをインストールします。 $ pip install -r requirements.txt 手順1を飛ばし、homebrewでmecabをあらかじめインストールしておかないと、ここでエラーが出るでしょう。 4. バックエンドのサーバを立てる いま、backendフォルダ内にいます。 $ python3 server.py 問題が起きなければ、これでバックエンドサーバの準備は完了です。 5. フロントエンドのサーバを立てる まず、フロントエンド用に先ほどとは別のターミナルを立ち上げてください。立ち上げたら、jparser_app/frontend/appフォルダに移動します。 $ cd jparser_app/frontend/app axiosがインストールされていない場合は、axiosをインストールします。 $ npm install axios node_modulesフォルダを作成します。 git cloneした場合、node_modulesフォルダは基本的にありません。 local環境に依存するため、通常は.gitignoreで無視します。 $ npm install フロントエンドサーバを立てます。 $ yarn start これでフロントエンドサーバの準備は完了です。 そして、全ての準備が完了です。お疲れさまでした。 localhost:3000にアクセス ブラウザからlocalhost:3000にアクセスします(yarn startで自動で開きます)。 app screenshots
- 投稿日:2021-08-09T19:01:56+09:00
iMac2021でReact開発環境構築
iMac2021でreactインストールする方法について。 まずはterminalでnpmとnodeを使うためにNode.jsをインストール こちらクリックしてダウンロード→https://nodejs.org/ja/ ダウンロードしたパッケージを開いてインストール terminalで以下の順でコマンド実行 npm install -g create-react-app npx create-react-app my-app cd my-app npm start [参考になる記事] https://saisai-weblink.com/2021/08/08/imac-2021reactでweb開発する方法/
- 投稿日:2021-08-09T17:04:57+09:00
GastbyとNetlify CMSで個人ブログ作ってみよう!#0 なぜGatsbyとNetlify CMS?
前書き 初めまして、ウェブをやってるけん(外人)です。生物専攻にも関わらずIT業界に就職したいということで、ESの代わりにGatsbyで個人サイト作ってみた結果、思ったより開発がスムーズで、すぐサイトが出来上がりました。その快適さをぜひみんなに体験してみたいということでこのシリーズを作ることになりました。ウェブ開発者ではない方にもこのテンプレートを使えるように、Gitレポそのままフォークすれば自分のブログにすることもできますので、ぜひお使いください。(まだ作ってますので#1からリンクをつけます) Gatsbyって何?キムタク? Javascriptには数多くフレームワークが存在しているため、フロントエンド開発者でもGatsbyのことよくわからない方いると思いますので、Gatsbyの紹介させていただきます。 GatsbyはReactをもとに開発された静的サイトジェネレータフレームワークとなります。 「React使えばいいじゃん。」と思う方もいらしゃるので、Gatsbyによる解決されたReactの一つの問題点を挙げます。 (Stack Overflowから) Reactプロジェクトのindex.jsをご覧になったことがある方ならわかると思います。Reactのページには何もありません。なぜかというと、Reactアプリのコンテンツは全部Javascriptでレンダーされてます。 このアプローチの問題点はいくつかあります Javascriptバンドルサイズが大きいやネットワークが遅い場合、ページ表示速度が遅くなります。 ネットワークに問題があると、そもそもJavascriptバンドルが届かない可能性がある。 デバイスの処理速度が遅いと、ページ表示速度が遅くなります。 GoogleなどのクローラーはJavascriptを実行してないので、SEO対応が難しい。(最近JS対応してるクローラーもあるらしいですが) ここでGatsbyなどの静的サイトジェネレータを使うと流れはこのようになります。 このようにReactとJavascriptの機能を保ちづつ、ページ表示速度が速くなり、SEOも向上します。 しかし静的サイトジェネレータは新しい情報をある度に一からビルドしないといけないので、アップデート頻度が多いサイトにはあんま向いてませんが、ブログなどには最適と言っても過言ではありません。 じゃNetlify CMSはなに? CMSはコンテンツ管理システムの略で、WordPressなどが有名ですが、Netlify CMSはその実装しやすさと使いやすさで近年多く使われるようになりました。configファイル一つで設定ができ、すぐ使えますし、オーバーキルにはなりませんし、これしか使ってことないのでNetlify CMSを使います。 予告 今回作るブログサイトは自分の個人サイトをもとに必要最低限の機能(ブログ)以外を取り除いたものとなります。 それにGithub Actionを使ってFirebase HostingにデプロイするCI/CDパイプラインも作ります。 リンク 今週内何回か分けてアップデートする予定ですので、ぜひご覧ください。 (この記事に間違っていることがある場合指摘していただけると幸いです。)
- 投稿日:2021-08-09T16:57:54+09:00
初心者による初心者のためのGatsbyJS覚書4(パーツのコンポーネント化)
この記事について 会社の業務でGatsbyJSを少しだけさわった経験がある新卒2年目が作成しています。 参考資料として書籍を使用していますが、筆者がビギナークラスのため読んでいて「?」となる部分や間違っている箇所もあるかと思います。 参考までに、そして間違えている箇所がありましたらご連絡いただけると嬉しいです。 Chapter3:パーツのコンポーネント化備忘録メモ 今回のチャプター文では特につまずきや迷いはありませんでした。 なので簡単にまとめて行きます。 ページを増やす準備 src/の配下にcomponentsディレクトリを用意してその下にheader.jsとfooter.jsを用意する。 中身はこのような感じで準備。 import React from "react" export default () => ( //要素を追加していく ) が、gatsby developをするとこのような警告が出るのでコンポーネントの形を変えたほうが良さそう、、、 6:1 warning Anonymous arrow functions cause Fast Refresh to not preserve local component state. Please add a name to your function, for example: Before: export default () => {} After: const Named = () => {} export default Named; no-anonymous-exports-page-templates 6:1 warning Assign arrow function to a variable before exporting as module default ということでindex.jsやコンポーネントファイル`も下記のような記法に変更。 import React from "react" const ComponentName = () => ( <div> //要素を追加 </div> ) export default ComponentName index.jsの<header></header>や<footer></footer>部分をコピーして 準備したコンポーネントファイルに引っ越しをする。 どちらの要素も<header></header>のようにラップされている状態なのでdivなどで囲む必要はなし。 各コンポーネントファイルに引っ越しが完了したら、components/にlayout.jsを作成する。 これは作ったコンポーネントパーツをひとまとめに取り込むことのできる「まとめセット」ファイルとなる。 childrenはReactのプロパティでページごとの子要素を取り込める様になっている。 import React from "react" import Header from "./header" import Footer from "./footer" import "./layout.css" const Layout = ({ children }) => ( <div> <Header /> {children} <Footer /> </div> ) export default Layout 今まで使用していたsrc/style/style.cssを名前を変えてsrc/components/layout.cssとなるように移動させる。 また、今までgatsby-browser.jsでsrc/style/style.cssを取り込んでいたが、 グローバルCSSを利用しているので推奨の「サイトの基本となるコンポーネントから適用する方法」を 使用するためgatsby-browser.jsはファイルごと削除しておく。 ※gatsby-browser.jsでスタイルシートを取り込む方法は Gatsbyにおけるスタイルの適用方法として挙げられる「CSS in JS」の方法に影響があるため。 あとは index.jsでlayout.jsを取り込み、 引っ越しを済ませた<header>や<footer>要素は削除し、 残りの要素を<Layout></Layout>で挟み込めば完成。 import Layout from "../components/layout" //追加 const IndexPage = () => ( <Layout> //残りの要素 </Layout> ) export default IndexPage 今回はここまで。 次回はこれを使用しながらページを増やしていく予定です。
- 投稿日:2021-08-09T16:52:12+09:00
Điều hòa, máy giặt, bình nóng lạnh, tủ lạnh chính hãng giá rẻ
- 投稿日:2021-08-09T15:31:08+09:00
メモ > Reactで使えそうなフォームライブラリ
調べてる途中のメモです Schema data-driven-forms すべての機能を備えたフォームを構築するための宣言型の方法 react-jsonschema-form JSONSchemaからWebフォームを構築する react-jsonschema-formのをもっとかんたんに使う便利ツール - Qiita react-jsonschema-form playground HTMLフォーム入力からJSONを生成できるWebサービスで遊んでみよう ( react-jsonschema-form ) - Qiita JSON Schemaとreact-jsonschema-form - Qiita 人気? react-hook-form Reactのフックを使って、手間をかけずにフォーム検証を行うことができます。 formik フォームを構築するための宣言型の方法で、Reactのフォームを制御するシンプルで簡単な方法です。 Form formcat React ContextAPIを使用してReactのフォームを制御するシンプルで簡単な方法です。 formy-react React JS 用のフォーム入力ビルダとバリデータです。 react-final-form サブスクリプションベースのフォーム状態管理。 react-formawesome 素晴らしいフォームを作るための複雑なライブラリ。 surveyjs 進化したアンケート・フォームライブラリ。 更新されてない prexus-form 6 years ago JSON-Schemaを利用したReact用の動的なフォームコンポーネントです。 react-validation-mixin React用のシンプルな検証mixin(HoC)です。 6 years ago This repository has been archived by the owner. It is now read-only.
- 投稿日:2021-08-09T15:18:04+09:00
初心者による初心者のためのGatsbyJS覚書3(画像の最適化編)
この記事について 会社の業務でGatsbyJSを少しだけさわった経験がある新卒2年目が作成しています。 参考資料として書籍を使用していますが、筆者がビギナークラスのため読んでいて「?」となる部分や間違っている箇所もあるかと思います。 参考までに、そして間違えている箇所がありましたらご連絡いただけると嬉しいです。 Chapter2:画像の最適化の備忘録メモ 注意 参考資料である「Webサイト高速化のための静的サイトジェネレーター活用入門」では まだGatsby v2の手法が記載されているが、今回環境構築をした際、最新版のv3で作っているので 参考書通りとはならなかったが、付属されているv3用のダウンロード資料をもとに取り組んでみた。 「Gatsby v2」と「Gatsby v3」の違いについて この画像の最適化編で大きく影響してくるのはズバリ 「インストールするプラグインとそのルールの違い」である。 v2までは「gatsby-image」というプラグインをインストールしていたが、 v3からは新しく「gatsby-plugin-image」というプラグインが導入されている。 ざっくり説明するとgatsby-imageで必要だった画像最適化までの手順が gatsby-plugin-imageだと大幅に削減されるよって話らしい。 gatsby-plugin-imageではStaticImageとGatsbyImageの2つが使用でき、 StaticImageはGraphQLを使わずに最適化した画像を引っ張ってこれるため使い勝手が◎ ひとまず必要なプラグインたちをインストールしておく。 npm install gatsby-plugin-image gatsby-plugin-sharp gatsby-transformer-sharp gatsby-source-filesystem gatsby-transformer-sharpは後にダイナミックイメージ(GatsbyImage)を使用する場合に必要 gatsby-source-filesystemはStaticのImageを使っている場合必要 あとは使いたいファイルにインポートして使うのみ。 import { StaticImage, GatsbyImage } from "gatsby-plugin-image" バージョンの違いで解説を使いつつも手間取った部分 gatsby-imageを使用している場合、src/配下のimageを取得しようとするとクエリを作成する必要がある。 なので参考資料の解説も「クエリを使用していること」を前提に進んでいくわけだが、 やはり諸々使い勝手が違いすぎで初心者大混乱。笑 GraphQLの構造も自分が見つけたいデータがなかなか見つからず大冒険の果てにデータを探し当て 達成感とともにハッと気がつく。「あれ、そういえばStaticImage使えば早いのでは、」と。 そして解説のPDFにもすこし進むとそのことが記載されている。。。(早く言ってよ心の中で叫ぶ。) 結局クエリを使用しない方法を取ったので日の目をみることがはなかったが 頭を悩ませた記念にデータ構造だけ写真に収めておくことにする。 悩んだのはFULL WIDTHなどをもつlayoutのチェックボックスがなかなか見つからなかったため。 childImageSharpが同階層に2つあるのに気がつけなかったのが運の尽き、、、 GraphQLでなんか欲しいデータ取れないな、、、と思ったら 「その階層ではないのでは?と広い視野でパトロールをしましょう。」が今回の教訓。 そのlayoutでの選択肢ってどういう意味?となったのでそれについての参考はこちら それをクエリでこんな感じで各画像ごとにエイリアスをつけて取得。 export const query = graphql` query { hero: file(relativePath: { eq: "hero.jpg" }) { childImageSharp { gatsbyImageData(layout: FULL_WIDTH) } } fruit: file(relativePath: { eq: "fruit.jpg" }) { childImageSharp { gatsbyImageData(width: 320, layout: CONSTRAINED) } } } ` 画像を出力する部分にはこのように記述。 <GatsbyImage image={data.hero.childImageSharp.gatsbyImageData} alt="" style={{ height: "100%" }} /> <GatsbyImage image={data.grain.childImageSharp.gatsbyImageData} alt="" /> StaticImageで手っ取り早く画像最適化 StaticImageならクエリいらずでGatsbyImageと同等の画像の最適化ができるらしい。最高。 GatsbyImageを使用したときに指定していたlayoutの設定もこのように書けば指定できる。 StaticImageしか使用しないのであればgraphqlやGatsbyImageのインポートは不要。 //例1 <StaticImage src="../images/hero.jpg" alt="" layout="fullWidth" /> //例2 <StaticImage src="../images/fruit.jpg" alt="" layout="constrained" style={{ width :"320px"}} /> プラグインを使用して最適化した画像のスタイルを整える StaticImageやGatsbyImageを使用して画像を最適化してみると さっきまで整っていた見た目に変な余白、ズレ、見えなくなるなどの事象が発生。 「なんだこれ?」と思っていたがどうやらプラグインで最適化すると今まで効いていたCSSが無効になるとか。 解説によると「画像の読み込み中もスペースを確保するためにgatsby-image-wrapperが画像の縦横比にあわせたボックスを構成してくれっちゃってる」らしい。 解決方法はこんなかんじ。 <StaticImage src="../images/hero.jpg" alt="" layout="fullWidth" style={{ height :"100%"}} //ラッパーに高さ100%を指定 /> cssファイルでgatsby-image-wrapperの親要素にあたるクラスfigureにmax-height:100%;を与える。 高さは子要素のimgに合わせて設定。 メディアサイズごとのサイズ指定も追加しておく。 .hero figure img { width: 100%; height: 450px; object-fit: cover; } //追加 .hero figure { max-height: 100%; height: 450px; } @media (min-width: 768px) { //追加 .hero figure { height: 750px; } .hero figure img { height: 750px; } } 背景画像を最適化する 背景画像として画像が使用されている場合はまず、CSSで背景画像を指定している部分をnoneへ変更。 JSXへ新しく背景画像を表示するためのマークアップを追加。 <footer> <div className="container"> //もとからある要素.... //追加 <div className="back"> <StaticImage src="../images/pattern.jpg" alt="" layout="fullWidth" style={{ height :"100%"}} /> </div> </div> </footer> CSSで大きさを調整して、positionを指定。z-indexで重なりを調整する。 /*フッター背景画像*/ .footer{ position: relative; } .footer .container{ position: relative; z-index: 10; } .footer .back{ position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } 画質が荒いとき gatsby-config.jsでqualityの調整をする。 plugins: [ { resolve: `gatsby-plugin-sharp`, options: { defaults: { quality: 90, //デフォルトは50 }, }, }, `gatsby-transformer-sharp`, `gatsby-plugin-image`, ], 今回は画像の最適化について書いてみました。 次回はコンポーネント化について書いて行きます。
- 投稿日:2021-08-09T10:06:36+09:00
React ✖️ Typescript Todoアプリ
概要 今回はreact × typescriptで簡単なtodoアプリを作成します 必要なインストール @material-ui/core @material-ui/icons firebase react-router-dom 1. firebaseとreactを連携させる firebaseの初期設定を行い, firebase.tsファイルでfirebaseのそれぞれのキーを読み取り、initializeします ❇︎今回この内容は省略します 2. databaseの内容を表示させる 今回、databaseにあらかじめtasks -> {id: , title: } というデータを作成 その内容を取得し、表示させることが目的 1 . useStateでtaskの状態を管理する。 初期のidとtitleは空 const App: React.FC = () => { const [tasks, setTasks] = useState([{ id: "", title: "" }]); return <div className="App"> </div>; }; Appの型はReact.FC 2 . useEffectで一度だけデータベースの情報を取得する const App: React.FC = () => { const [tasks, setTasks] = useState([{ id: "", title: "" }]); useEffect(() => { const unSub = db.collection("tasks").onSnapshot((snapshot) => { setTasks( snapshot.docs.map((doc) => ({ id: doc.id, title: doc.data().title })) ); }); return () => unSub(); }, []); return <div className="App"> </div>; }; db.collection('tasks'): データベースのtasksを取得 .onSnapshot: コレクションの追加・削除・変更をクライアントサイドにリアルタイムに反映実行時にすべてのドキュメントを取得する snapshot.docsにidやtitleの情報が入っている それをsetTasksで状態を更新する 3 .最後にmapで出力する return <div className="App"> {tasks.map((task) => ( <h3 key={task.id}>{task.title}</h3> ))} </div>; 3. タスク作成機能 1 .初めに入力するフィールドと送信ボタンを作成する // 一部省略 return ( <FormControl> <TextField InputLabelProps={{ shrink: true, }} label="New task" value={input} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setInput(e.target.value) } /> </FormControl> <button disabled={!input} onClick={newTask}> <AddToPhotosIcon /> </button> ) material-uiの<FormControl>タグで囲った中に<TextField>を作成 InputLabelPropsのshrinkをtrueにすると、labelの文字が左上に寄る onChange属性でinputの内容を更新する eの型はReact.ChangeEvent タグのdisabled={!input}で文字が入力されたら送信できるようにする 2 .newTask関数でinputされた内容をデータベースに更新する const [input, setInput] = useState(""); const newTask = () => { db.collection("tasks").add({ title: input }); setInput(""); }; db.collection("tasks").add({ title: input })でtitleにinputの情報を加える その後inputの内容を初期化 4. コンポーネントを分割してCURD(クラッド)機能を追加する App.tsx {tasks.map((task) => ( <TaskItem key={task.id} id={task.id} title={task.title} /> ))} 新たにTaskItem.tsxを作成し、key, id, titleをpropsとして渡す TaskItem.tsx const TaskItem: React.FC<PROPS> = (props) => { const { id, title } = props; const [titleState, setTitleState] = useState(title); const editTask = () => { db.collection("tasks").doc(id).set({ title: titleState }, { merge: true }); }; const deleteTask = () => { db.collection("tasks").doc(id).delete(); }; return ( <ListItem> <h2>{title}</h2> <Grid container justify="flex-end"> <TextField InputLabelProps={{ shrink: true, }} label="Edit task" value={titleState} onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTitleState(e.target.value) } /> </Grid> <button className={styles.taskitem__icon} onClick={editTask}> <EditOutlinedIcon /> </button> <button className={styles.taskitem__icon} onClick={deleteTask}> <DeleteOutLineOutLinedIcon /> </button> </ListItem> ); }; <h2>{title}</h2>でタスクを表示する <Grid container justify="flex-end">で右寄せに合わせる editTaskでfirebaseの情報を更新する deleteTaskで項目を削除する 5. Login機能を追加する ①index.tsxを編集してルーティングを設定する index.tsx ReactDOM.render( <React.StrictMode> <BrowserRouter> <> <Route exact path="/" component={App} /> <Route exact path="/login" component={Login} /> </> </BrowserRouter> </React.StrictMode>, document.getElementById("root") ); /の時にAppコンポーネントを /loginの時にLoginコンポーネントを表示する ②Login.tsxを編集する Login.tsx export const Login: React.FC = (props: any) => { const [isLogin, setIsLogin] = useState(true); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); useEffect(() => { const unSub = auth.onAuthStateChanged((user) => { user && props.history.push("/"); }); return () => unSub(); }, [props.history]); return ( <div className={styles.login__root}> <h1>{isLogin ? "Login" : "Register"}</h1> <br /> <FormControl> <TextField InputLabelProps={{ shrink: true, }} name="email" label="E-mail" value={email} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setEmail(e.target.value); }} /> </FormControl> <br /> <FormControl> <TextField InputLabelProps={{ shrink: true, }} name="password" label="Password" type="password" value={password} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { setPassword(e.target.value); }} /> </FormControl> <br /> <Button variant="contained" color="primary" size="small" onClick={ isLogin ? async () => { try { await auth.signInWithEmailAndPassword(email, password); props.history.push("/"); } catch (error) { alert(error.message); } } : async () => { try { await auth.createUserWithEmailAndPassword(email, password); props.history.push("/"); } catch (error) { alert(error.message); } } } > {isLogin ? "Login" : "Register"} </Button> <br/> <Typography align="center" > <span onClick={() => setIsLogin(!isLogin)}> {isLogin ? "Create new Account ?" : "Back to Login"} </span> </Typography> </div> ); }; emailとpasswordをuseStateで管理する サインインの設定 isLoginがfalseの場合(新規にアカウントを作成する場合) async () => { try { await auth.createUserWithEmailAndPassword(email, password); props.history.push("/"); } catch (error) { alert(error.message); } awaitで結果が返されるまで待機する createUserWithEmailAndPasswordで正しくemailとpasswordが設定されれば"/"に移動 errorが出ればアラート出力 isLoginがtrueの場合(既存のアカウントにログインする場合) try { await auth.signInWithEmailAndPassword(email, password); props.history.push("/"); } catch (error) { alert(error.message); } 今回の場合はsignInWithEmailAndPasswordを使用
- 投稿日:2021-08-09T08:33:24+09:00
keycloak or React or NestJS
keycloak or React or NestJS この記事は「いのべこ夏休みアドベントカレンダー 2021-8-12 の記事となります。 あくまでホビーレベルの個人的趣味、技術探求なのでご参考までに・・・ はじめに 記事タイトルが「keycloak or React or NestJS」となっており「or」で何れかを書こうかという感じでしたが、 全部絡めて書こうと思います。理由は実際の業務では、巷には技術情報があふれていて最新技術やベンダロック技術をやらない限り、 今の時代のエンジニアは組合せをいかに早くモノにするスキルが結構必要なんじゃないかなと思ってます。 この辺りの重要性やマインドは社内の発表会でも訴えた部分ではあります 目的 上で述べた通りピンポイントでXXXやってみましたという感じではなく(それも重要ですが) 、各要素技術を組み合わせてより実践的(業務で作るレベル)な成果物の完成目指します。 訴えたい部分としては、自宅でホビーでここまで作るか!?という感じでなるべく妥協しないようにします。 お題は巷で話題!?の「接種予約システム」の一部を想定して作成します。 社内の若手が研修で予約者側の作成をしているので、私は自治体側の「接種予約一覧」を作成します。 成果物 作成過程の説明がなが~いので成果物を先にご紹介します。実際の動作キャプチャです。 システムへログインして、接種予約一覧を表示(するだけ)です。あとログアウトできます keycloakを用いてフロントエンド、バックエンドの認証/認可を分けています。 認証が必要な場面でkeycloakへリダイレクトして、リソースアクセスに必要なトークンを発行してもらってます。 適用技術について 狭義、広義で色々な技術を組み合わせて実現させてます。 実際の業務でもこれを使ってサクッと作れるレベルが求められます。(新技術でも習得時間は短く即戦力が求められます) 狭義の技術スキル よくXXXができる人と兵隊を集める際に言われる部分 従来はもっとざっくりでJava,C#ができる人とかだったきがする) フロントエンド技術:React、デザイン?(デザインはMaterialUIにお任せですが・・・) バックエンド技術:Node.js、NestJS(Express)、TypeORM、DB設計、実装(幅広く) 認証/認可技術:OAuth、OpenID Connect、keycloak 言語:サーバーサイドjavascript、TypeScript ここから下は、この成果物でサービス構築する際に必要なスキル コンテナ技術: Docker(compose)、k8s アーキテクチャ:色々ありますよね、正解はないですし一長一短なのであえて述べませんが 基盤構築技術:クラウド、CI/CD基盤 ベース技術があれば応用が利くので何れかの経験でOKだと思います。 運用/保守技術:概要を理解し実際に必要なツール導入(zabbix、fluentd)これもツールありきではないです。 広義の技術スキル(可視化できないスキル この辺は人に依存したポテンシャル部分) 様々な引き出しをバックエンドとした、柔軟な設計能力(経験ともいう) 既存の技術スキルを超えていくスキル(努力ともいう) 何事にもあきらめず前向きな姿?(チャレンジ精神ともいう) 仕事を楽しめるスキル(仕事とは何かを理解し没頭しすぎない) ほぼ精神論ですね。この辺のスキルも非常に重要だとは思いますが・・・ 今のご時世「多能工」が活躍できる機会も多いし求められている(DX、アジャイル、スクラムなどのキーワードで) 大規模なウォーターフォール的PJではピンポイント技術で仕事になりますが、 小規模な場合、「なんでも屋さん」が重宝されますよね そして人月ビジネスの場合、「余計なことやるな」「いわれたこと(仕様)を実装すればいい」なんて感じです。 良かれと思って客先で色々やると、「余計な工数」として会社の利益(原価)悪化を招くということになります。 しかもバグを作りこんでいたり、行間を読めとかもうデスマーチ状態です。 あくまで決められた仕様を高品質かつ高生産性で作ることを追求されます。結構ストレスだったりします。 ふわっとしたビジネスイメージから、サクッと動くものを作ってスプリントを回して作りこむみたいなイメージで仕事できる人でしょうか? お客様と一体になって上手く目標(ビジネス実現)に向かっていくのが本来の目的ですから。 自社の利益も重要ですが経験上結果は後からついてくると思います。(時間はかかります) ただしスコープを決めるうえでもやりたいことに対するビジネスセンス(工数見積もり)は必要ですよね。 私がこうしてホビーで守秘義務に抵触しない範囲で開発をするのは、知識の定着に「手を動かすこと」が必要だからです。 よく課題や壁にぶち当たりピンポイントでホビーで開発したりしてます(もう仕事の境界がない・・・) オラクルの旧Gold取得時なんて自宅のDB何回も壊してコマンドで復旧でニンマリなんて 作成するもの 以下の図の「予約確認」部分です。他にも業務は沢山ありますが・・・ ざっくりストーリー 自治体職員はPCブラウザでシステムのWebページを開く ログイン画面から自身のアカウント情報を使いシステムへログインする 接種予約一覧から予約、接種状況を確認する システムからログアウトする 軽く設計 業務設計 上記のストーリーから必要な機能は以下の通り ログイン、ログアウト機能 接種予約一覧表示機能 接種予約一覧表示機能のインプット 接種券のサンプル情報から推測します。 恐らく発送時に住基の情報と紐づけてはいると思いますね。細かいことは分からないですが・・・ 予約機能側で券番号、氏名、予約日時、接種会場を確定することにします。 制約としては1回目の接種が完了したら、2回目予約済になるということでしょうか? 画面設計 トップ画面 ログイン画面(keycloak) 予約一覧 API設計 上記の画面「予約一覧」で必要なAPIを抽出します。 あまり細かく分ける必要もないかなと思いますので、予約情報を取得するAPIとして設計します。 本来であればAPI設計としては接種会場、接種対象者は分けたほうがいい!!となると思います。 時間の関係上1APIで全部のリソースを取得してしまいます。(この辺はパフォーマンスとのトレードオフかもしれないですね) GET inoculation/reservations/:id 予約取得API GET inoculation/persons/:id 接種対象者取得API GET inoculation/venues/:id 接種会場取得API [概要] 〇〇市の接種予約済一覧を取得する [エンドポイント] GET /api/v1/inoculation/reservations [パラメータ] なし [レスポンス] { id: number; ticketNumber: string; fullName: string; ... } [ステータス] 200 OK 403 Forbidden 404 Notfound データベース設計 ここまでできればデータベース設計は問題ないと思います。 本当は1エンティティでもいいのですが、属性の違うものは分離しました。(人、場所) ちょっと図が変な感じもしますが、以下3エンティティになります。 接種予約 : 接種予約対象者 (1:1) 接種予約 : 接種会場 (n:1) ※厳密には0~を考慮する必要がありますが、今回はデータありきなので割愛します。 この辺はちゃんと定義しないと、結合時にnull考慮不足でエラーになったりするので重要ですが・・・ 本当はCRUD等でディスカッションしたりしてインターフェースやエンティティを決めていくのがいいですね。 エンティティ名 説明 inoculation_reservation 接種予約 inoculation_target_person 接種予約対象者 inoculation_venue 接種会場 ここを深堀すると中々大変ですよね。特に予約側。とてもアドベントカレンダーレベルではなくなる。 接種会場のキャパや時間枠、予約対象者の属性・・・考えること一杯ですが全部割愛します! この辺を妄想して色々考えるスキルを「業務機能設計スキル」といったりします。 やりたいことに対し制約など抽出し機能設計に落とし込むことです。 このシステムで言えば、会場の枠以上に予約できたらダメですし色々制約かけないと機能しないですよね。 環境整備 実装を始める前に環境を整備します。以下環境を前提に作成を進めます。 コードエディタ:VSCode ...最近はこれだけで生活できそうな気がするぐらい使っている レポジトリ管理:モノレポlerna バージョン戦略は「Fixed」 React:create-react-appでひな形作成 NestJS:NestJSのCLIを使ってひな形作成 keycloak:docker-composeで起動 DB:postgres 上記のcompose内で公式イメージからPullして起動 いよいよ実装 作るものは分かっているのでどこからはじめてもいいのですが、教科書的にDBに近い部分から作ります。 バックエンド(API)の実装 TypeORM NestJSではTypeORMというORマッパをサポートしているので、公式サイトに従い準備します。 今回はPostgresでいきます。(実はsqliteを試して少し挫折したので・・・) 実際にはDBの接続先は環境毎に変わるのでnode-configや dotenvやNestJSのお作法などを利用して環境依存は排除すべきです。今回は.envを使ってますなくても||でデフォルト適用となります データベース接続部分 app.module.ts ... @Module({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: process.env.DB_HOST || 'localhost', port: Number(process.env.DB_PORT) || 5432, username: process.env.DB_USER || 'postgres', password: process.env.DB_PASS || 'password', database: process.env.DB_DATA || 'postgres', synchronize: true, logging: false, entities: getMetadataArgsStorage().tables.map((tbl) => tbl.target), migrations: [__dirname + '**/entities/migrations/*.{js,ts}'], cli: { migrationsDir: 'src/entities/migrations', }, }), TypeOrmModule.forFeature([ InoculationReservation, InoculationTargetPerson, InoculationVenue, ]), ... 上記のようにモジュール定義しておけばアプリケーション起動時にコネクションを張ってくれます。 接続できないとアプリケーションが起動しません(リトライもデフォルトでやってくれます) DBアクセス実装 Repository patternをサポートしており、公式サイトに倣ってサービスクラスへ実装します。 業務ではDDDで実装していてもう少しレイヤを分けていますがこれはまたの機会ということで inoculation.reservation.entity.ts @Entity() export class InoculationReservation { @PrimaryGeneratedColumn() id: number; @Column({ type: 'varchar', length: 10, unique: true }) ticketNumber: string; @OneToOne(() => InoculationTargetPerson) @JoinColumn() inoculationTargetPerson: InoculationTargetPerson; @ManyToOne(() => InoculationVenue) @JoinColumn() inoculationVenue: InoculationVenue; @Column() firstTimeDate: Date; @Column() firstTimeStatus: InoculationReservationStatus; 全部載せませんが、上記のように3エンティティ定義します。 定義できてしまえばDDL不要です。TypeORMが良しなにテーブルをPostgresへ作成してくれます。 (データ検索もSQL開発不要です。上記のデコレータ(@OneToOne,@ManyToOne)等でリレーション定義します。 サービスクラス ほとんど実装ありません。データを検索して少しコンバートする程度です。 app.service.ts import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import * as moment from 'moment-timezone'; import { Repository } from 'typeorm'; import { Reservation } from './app.interface'; import { InoculationReservation } from './entities/inoculation.reservation.entity'; @Injectable() export class AppService { constructor( @InjectRepository(InoculationReservation) private inoculationReservationRepository: Repository<InoculationReservation>, ) {} async getInoculationReservations(): Promise<Reservation[]> { const result = await this.inoculationReservationRepository.find({ relations: ['inoculationTargetPerson', 'inoculationVenue'], }); const reservations = result.map((item) => { const reservation: Reservation = { id: item.id, ticketNumber: item.ticketNumber, firstTimeDate: item.firstTimeDate ? moment(item.firstTimeDate) .tz('Asia/Tokyo') .format('YYYY/MM/DD HH:mm') : '', firstTimeStatus: item.firstTimeStatus, secondTimeDate: item.secondTimeDate ? moment(item.secondTimeDate) .tz('Asia/Tokyo') .format('YYYY/MM/DD HH:mm') : '', secondTimeStatus: item.secondTimeStatus || '', note: item.note || '', fullName: item.inoculationTargetPerson.firstName + ' ' + item.inoculationTargetPerson.lastName, venue: item.inoculationVenue.name, }; return reservation; }); return reservations; } } @InjectRepositoryデコレータで接種予約リポジトリをDIします。 そのリポジトリを使ってfind()ですべてのレコードを抽出しています。 引数にrelationsを指定すればデフォルトではleft outer joinしてくれます。(左外部結合) データはあるので外部結合でなくてもいいのですが・・・ 予約日時はDB上UTCで保持しているので(JSTは+9)momentを使ってタイムゾーンを指定してフォーマットかけてます。 あとは氏名は(普通)姓名等分けて保持するので結合して返します。 コントローラークラス app.controler.ts import { Controller, Get } from '@nestjs/common'; import { AuthenticatedUser, Resource, Scopes } from 'nest-keycloak-connect'; import { Reservation } from './app.interface'; import { AppService } from './app.service'; @Controller('inoculation') @Resource('inoculation-reservation-app-api') export class AppController { constructor(private readonly appService: AppService) {} @Get('reservations') @Scopes('') async getInoculationReservations( @AuthenticatedUser() user: any, ): Promise<Reservation[]> { if (user) { console.log(`access from=>${user.preferred_username}`); } return await this.appService.getInoculationReservations(); } } 色々デコレータで修飾されていますが、NestJS自体色々デコレータで修飾して振舞い決めるフレームワークなので・・・ 少し解説します。 @Controller('inoculation') コントローラーを表すデコレータ。エンドポイントURLの一部を渡します。 @Resource('inoculation-reservation-app-api') これはkeycloakのリソース(クライアント)を指定します。 keycloak設定については後程解説します。 @Get('reservations') GETメソッドであることを表します。エンドポイントURLの一部を渡します。 @Scopes('')使ってませんが指定しないと何故かエラーになるので指定します。これもkeycloak関連です。 @AuthenticatedUser() これはリクエストヘッダのbearerトークンに含まれているJWT内のユーザ情報を取得できます。 使ってませんが、これで誰がAPIアクセスしてきたかわかります。 このデコレータで認証が必要なAPIとなり、認証がされていないアクセスでは403が返されます。 認証が不要なパブリックなAPIはデコレータ「@Public()」をつけます。 ロールなどの制御やメソッド(GET、POST、DELETEなど)によってもデコレータで細かい制御が可能。 publicなAPIの例 @Get() @Public() // Can also use `@Unprotected` async findAll() { return await this.service.findAll(); } これだけでAPI側の実装は終わりです。 テストコード等も作成していますが、割愛します。 最後にkeycloak設定です。 今回はNestJSのモジュールライブラリnest-keycloak-connectを利用します。簡単に言ってしまうとnode.js用のkeycloakアダブタのラッパーになります。 上述のコントローラークラスをデコレータでセキュアなAPIにできるのもこのラッパーのおかげです。 仕組みとしてはNestJSのGuardsの仕組みを使っています。 ベースの部分はExpressのミドルウェアを使ってkeycloakを入れ込んでいますね。 app.module.ts @Module({ imports: [ ... KeycloakConnectModule.register({ realm: 'inoculation-reservation-app', realmPublicKey: 'レルム公開鍵', authServerUrl: 'http://localhost:8080/auth', resource: 'inoculation-reservation-app-api', 'public-client': true, secret: 'クライアントシークレット', cookieKey: 'KEYCLOAK_JWT', logLevels: ['log', 'error', 'warn', 'debug', 'verbose'], useNestLogger: true, bearerOnly: true, policyEnforcement: PolicyEnforcementMode.ENFORCING, tokenValidation: TokenValidation.OFFLINE, }), ], providers: [ { provide: APP_GUARD, useClass: AuthGuard, }, ... こんな感じで先ほどDB接続設定を入れたモジュール定義にkeycloak設定も追加します。 キーなどははkeycloakのadmin cosoleを設定した上で設定する必要があります。 階層はこんな感じです。 フロントエンド(React)の実装 実際にはバックエンド単体でユニットテスト、e2eテストを実施してAPIが仕様通りに正しく動作するか確認したうえでOKとしますが、 (実装していますが・・・)その辺のテクニックや技術は後日記事を別に書きます。 あとは実際にサーバー上でビルド&デプロイする仕組みも必要ですし、テストを自動化、可視化する必要もあります。 外部公開するのであればセキュリティー対策や第三者の監査なども必要!! この辺が実際の業務で培った技術がたっぷり詰まっていて他SEと差別化できる部分でもあります。 (色々Quita見ても俯瞰して書いてある記事を見たことがないです 皆さん出し惜しみしますよね) これくらいにして画面を作りましょう! 私ははっきりいって画面側(特にデザイン)は苦手です。センスがないのです あまり凝ったことができないので、出来合いのモノ(フレームワーク)を利用します。 今回はMATERIAL-UIを利用します。 様々なコンポーネントが利用でき、Google提唱のマテリアルデザインが実現できます。 テーマ変更やカスタマイズも簡単で、今回は使っていませんがReact Hook Form等も対応しているのでBootStrap等と並んで人気のあるコンポーネントです。 フロント側のkeycloakについてはreact-keycloak/webを利用します。 解説し忘れましたが、フロント、バックエンド共に認証をkeycloakでやって、SSOを実現する感じです。 ログイン周り index.tsx import { AuthClientError, AuthClientEvent } from "@react-keycloak/core"; import { ReactKeycloakProvider } from "@react-keycloak/web"; import { KeycloakProfile } from "keycloak-js"; import * as React from "react"; import ReactDOM from "react-dom"; import App from "./App"; import "./index.css"; import keycloak from "./keycloak"; import reportWebVitals from "./reportWebVitals"; export interface UserProfile extends KeycloakProfile {} class UserProfileImpl implements UserProfile { constructor(profile: KeycloakProfile) { Object.assign(this, profile); } } export const UserProfileContext = React.createContext<UserProfile | undefined>( undefined ); const Root = () => { const [userProfile, setUserProfile] = React.useState< UserProfile | undefined >(); const eventLogger = async ( event: AuthClientEvent, error: AuthClientError | undefined ) => { console.log("onKeycloakEvent", event, error); if (event === "onAuthSuccess" && !userProfile) { const profile = await keycloak.loadUserProfile(); setUserProfile(new UserProfileImpl(profile)); } }; const tokenLogger = (tokens: unknown) => { console.log("onKeycloakTokens", tokens); }; return ( <ReactKeycloakProvider authClient={keycloak} onEvent={eventLogger} onTokens={tokenLogger} > <UserProfileContext.Provider value={userProfile}> <App /> </UserProfileContext.Provider> </ReactKeycloakProvider> ); }; ReactDOM.render( <React.StrictMode> <Root /> </React.StrictMode>, document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); あまり分割していないので見づらいですが、ReactKeycloakProviderやUserProfileContextで括って、 子コンポーネントで利用できるようにしてます。 App.tsx一部 ... const App = () => { const { initialized } = useKeycloak(); if (!initialized) { return ( <Grid container spacing={0} direction="column" alignItems="center" justifyContent="center" style={{ minHeight: "100vh" }} > <Grid item xs={3}> <Grid> <CircularProgress size={80} thickness={4} /> </Grid> <Grid>Loading...</Grid> </Grid> </Grid> ); } return ( <ThemeProvider theme={theme}> <BrowserRouter> <MenuAppBar /> <Switch> <PrivateRoute path="/list" component={InoculationReservationList} /> <Route path="/login" component={Login} /> <Redirect from="/" to="/list" /> </Switch> </BrowserRouter> </ThemeProvider> ); }; export default App; PrivateRouteで保護するコンポーネントを定義します。 今回は接種一覧コンポーネントを保護したいのでパスを/listとして定義します。 パスが/loginの場合はログインコンポーネントを表示します。 keycloakはフックで提供されているので、useKeycloak()で呼び出します。 初期化に時間がかかるので、スピナ表示で初期化完了を待ちます。 Login.tsx import { faSyringe } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Button, Grid, makeStyles } from "@material-ui/core"; import LockOpenIcon from "@material-ui/icons/LockOpen"; import { useKeycloak } from "@react-keycloak/web"; import * as React from "react"; import { useCallback } from "react"; import { Redirect, useLocation } from "react-router-dom"; const useStyles = makeStyles((theme) => ({ button: { margin: theme.spacing(1), }, })); const Login = () => { const classes = useStyles(); const { keycloak } = useKeycloak(); const location = useLocation<{ [key: string]: unknown }>(); const currentLocationState = location.state || { from: { pathname: "/list" }, }; const login = useCallback(() => { keycloak?.login(); }, [keycloak]); // if (keycloak?.authenticated && keycloak?.hasRealmRole("web-access")) { if (keycloak?.authenticated) { return <Redirect to={currentLocationState?.from as string} />; } return ( <Grid container direction="column" justifyContent="center" alignItems="center" spacing={5} > <Grid item></Grid> <Grid item> <h2> {" "} <FontAwesomeIcon icon={faSyringe} size="2x" /> 接種予約 advent-calendar-2021 ログイン </h2> </Grid> <Grid item> <Button onClick={login} variant="contained" color="primary" size="large" className={classes.button} startIcon={<LockOpenIcon />} > Login </Button> </Grid> <Grid item></Grid> </Grid> ); }; export default Login; ログインコンポーネントではボタン押下でkeycloak?.login();を呼び出し、keycloakの認証画面へリダイレクトさせます。 認証後リダイレクトされ認証されている場合は/listへリダイレクトします。 あとmaterial-uiでは注射器のアイコンがなかったのでFontAwesomeIcon を使ってます。 こんな感じでトップ画面と認証回りは終わりです。 ナビゲーション部分は認証している/していないによって表示を変えています。 認証情報なし 認証情報あり ユーザアイコン+ユーザ名を表示しサブメニュー(ログアウトなど)を表示 ユーザ名等はkeycloak側から得られるトークンに含まれている情報を表示しています。 一覧表示 特に凝っていないのでTableコンポーネントを使ってaxiosで取得したデータを表示しています。 App.tsx一部 const InoculationReservationList: React.FC = () => { const classes = useStyles(); const [reservationList, setReservationList] = React.useState<Reservation[]>( [] ); const axiosInstance = useAxios(""); useEffect(() => { const fetchData = async () => { const response = await axiosInstance?.current?.get( "/api/v1/inoculation/reservations" ); setReservationList(response?.data); }; fetchData(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( <TableContainer component={Paper}> <Table className={classes.table} aria-label="customized table"> <TableHead> <TableRow> <StyledTableCell>ID</StyledTableCell> <StyledTableCell align="right">接種券番号</StyledTableCell> <StyledTableCell align="right">氏名</StyledTableCell> <StyledTableCell align="right">会場</StyledTableCell> <StyledTableCell align="right">予約日時(1回目)</StyledTableCell> <StyledTableCell align="right">状況</StyledTableCell> <StyledTableCell align="right">予約日時(2回目)</StyledTableCell> <StyledTableCell align="right">状況</StyledTableCell> <StyledTableCell align="right">備考</StyledTableCell> </TableRow> </TableHead> <TableBody> {reservationList.map((row) => ( <StyledTableRow key={row.id}> <StyledTableCell component="th" scope="row"> {row.id} </StyledTableCell> <StyledTableCell component="th" scope="row"> {row.ticketNumber} </StyledTableCell> <StyledTableCell component="th" scope="row"> {row.fullName} </StyledTableCell> <StyledTableCell align="right">{row.venue}</StyledTableCell> <StyledTableCell align="right"> {row.firstTimeDate} </StyledTableCell> <StyledTableCell align="right"> {row.firstTimeStatus === "reserved" ? ( <Chip label="予約済" color="primary" /> ) : row.firstTimeStatus === "inoculated" ? ( <Chip label="接種済" color="secondary" disabled deleteIcon={<DoneIcon />} /> ) : ( <span> </span> )} </StyledTableCell> <StyledTableCell align="right"> {row.secondTimeDate} </StyledTableCell> <StyledTableCell align="right"> {row.secondTimeStatus === "reserved" ? ( <Chip label="予約済" color="primary" /> ) : row.secondTimeStatus === "inoculated" ? ( <Chip label="接種済" color="secondary" disabled deleteIcon={<DoneIcon />} /> ) : ( <span> </span> )} </StyledTableCell> <StyledTableCell align="right">{row.note}</StyledTableCell> </StyledTableRow> ))} </TableBody> </Table> </TableContainer> ); }; hooks.ts import { useKeycloak } from "@react-keycloak/web"; import axios, { AxiosInstance } from "axios"; import { useEffect, useRef } from "react"; export const useAxios = (baseURL: string) => { const axiosInstance = useRef<AxiosInstance>(); const { keycloak, initialized } = useKeycloak(); const kcToken = keycloak?.token ?? ""; useEffect(() => { axiosInstance.current = axios.create({ baseURL, headers: { Authorization: initialized ? `Bearer ${kcToken}` : undefined, }, }); return () => { axiosInstance.current = undefined; }; }, [baseURL, initialized, kcToken]); return axiosInstance; }; 特筆するところはないですが、axiosに関してはカスタムフックを作成し、 APIアクセスに必要なヘッダ情報(Bearerトークン)を設定しています。(共通部品とか言ったりします?) 本当はもっと例外ハンドリングやモック等axiosも奥が深いので色々できます。今回はaxiosインスタンス生成部分だけ切り出しました。 Allow-Originはあくまで開発用です。実際には適切なものを設定したりします。~ 注)ここでの記述は不要でした。開発用にproxyを設定するため、axiosで設定する必要はありません。 接種予約状態についてはChipを使って見やすく表示しています。接種済の色がセンスないですが・・・ keycloak設定 ざっくりですが実装を説明したので、keycloak設定します。 docker-composeで起動させて、リバプロ(nginx)のアドレスへアクセスし、管理者でログインします。 余談ですが、実際の業務では、VM上やクラウド上に構築することになりネットワーク的な知識も多少必要で各サーバー間の疎通が重要ですが、 スタンドアロンで開発用に起動させます。途中にAPFWがいたり、名前解決できない等keycloakアダプタがトークン取れないとか 色々トラブルあります。 レルム作成 クライアント作成 今回は細かい制御してませんが、クライアントはwebとapiそれぞれ作成しました。 ユーザ作成 色々細かい設定ができるのですが、とりあえず一番簡単な設定で作成しました。 例えばパスワードテンポラリやメールアドレス検証、有効期間など 普通にアカウント管理する上で必要十分な機能を備えていますし、認証に関わる部分なので、独自実装だとセキュリティー的に不安ですよね あとはAD連携できたり、アトリビュートを追加してJWTに含めたり この辺でもカスタマイズを紹介してますね。時間があればチャレンジしてみたいです これで使えるようになりました。完成です! ホビーレベルではやらない(必要ない)細かいテクニックを少しだけご紹介します。 開発用初期データ投入 本番ではやりませんが開発時は初期データを自由に操りたいですよね?毎回データ投入するのはめんどくさい そこでtypeormのマイグレーション機能を使って初期データを起動時に投入します。 main.ts const execMigration = async () => { await getConnection().query('delete from migrations'); const result = await getConnection().runMigrations(); console.log('run migrations is done'); console.table(result); }; 1622117028739-InitData.ts import { MigrationInterface, QueryRunner } from 'typeorm'; import { InoculationReservation } from '../inoculation.reservation.entity'; import { InoculationTargetPerson } from '../inoculation.target.person.entity'; import { InoculationVenue } from '../inoculation.venue.entity'; export class InitData1622117028739 implements MigrationInterface { name = 'InitData1622117028739'; async up(queryRunner: QueryRunner): Promise<void> { await this.down(queryRunner); const venues = await queryRunner.manager.save([ new InoculationVenue('〇〇体育館', 'test'), new InoculationVenue('△△市民センター', 'test'), new InoculationVenue('◇◇公民館', 'test'), ]); const persons = await queryRunner.manager.save([ new InoculationTargetPerson( '接種', '太郎', 'taro.sessyu@test.com', 'test', ), new InoculationTargetPerson( '接種', '花子', 'hanako.sessyu@test.com', 'test', ), new InoculationTargetPerson( '接種', '次郎', 'jiro.sessyu@test.com', 'test', ), ]); const getVenue = (name: string): InoculationVenue | undefined => { return venues.find((venue) => venue.name === name); }; const getPerson = (email: string): InoculationTargetPerson | undefined => { return persons.find((person) => person.email === email); }; await queryRunner.manager.save([ new InoculationReservation( '0000000001', getPerson('taro.sessyu@test.com'), getVenue('〇〇体育館'), new Date('2021/07/01 15:00'), 'inoculated', new Date('2021/07/14 15:00'), 'reserved', '1回目済、2回目未', 'test', ), new InoculationReservation( '0000000002', getPerson('hanako.sessyu@test.com'), getVenue('△△市民センター'), new Date('2021/07/02 14:00'), 'reserved', null, null, '1回目未、2回目未', 'test', ), ]); } async down(queryRunner: QueryRunner): Promise<void> { await queryRunner.manager .createQueryBuilder() .delete() .from(InoculationReservation) .execute(); await queryRunner.manager .createQueryBuilder() .delete() .from(InoculationVenue) .execute(); await queryRunner.manager .createQueryBuilder() .delete() .from(InoculationTargetPerson) .execute(); } } 一旦マイグレーションテーブルのレコードを削除しrunMigration()でマイグレーション実行。 マイグレーションファイルに初期データを記述すれば起動時に投入できます。 またawait this.down(queryRunner);で 一旦反対処理(データ削除)を行っていてこれはpostgresのデータを永続化しているために毎回クリアしています。 シーケンスが進んでしまうのでシーケンス戻すなども必要かもしれないですし、データが多いと起動に時間がかかるので、 DBダンプから投入するなどソリューションは色々あります。この辺も実業務では工夫して提案し構築する部分です。 nodeプロセスkill ほんと細かいことですが、VSCode(gitbash)から起動停止を繰り返すとnodeのプロセスが残ってしまっていて、 ポートが開放されず起動できないケースがあります。 強引ですが、以下スクリプトでnodeのプロセスをクリーンアップしてしまいます。 プロキシ設定 スタンドアロンで開発する場合、React側(localhost:3001)、API側(localhost:3000)の場合、同一オリジン制限に引っかかり、 ブラウザからAPIへアクセスができません。 http-proxy-middlewareを利用すると便利です。 setupProxy.ts import { createProxyMiddleware } from "http-proxy-middleware"; module.exports = function (app: any) { app.use( "/auth/*", createProxyMiddleware({ target: "http://localhost:8080", changeOrigin: true, }) ); app.use( "/api/v1/*", createProxyMiddleware({ target: "http://localhost:3000", changeOrigin: true, }) ); }; こんな感じで作成しておけばReactからのAPIやkeycloakへのアクセスをプロキシしてくれます。 changeOrigin:trueで良しなにやってくれていると思います。 これで問題なくアクセスできました。 最後に 簡単でしょう?というつもりは全くございません。すんごく行間ありますし、 ご紹介していない技術を色々盛り込んでますしこれではまだまだ実業務では使えないです・・・ アドベントカレンダーとしては中々ヘビーな量だと思います。(正直1日で実装できなかった・・・) 記事書きながら賞味2~3日で一応完成。でも実際の業務ではこれよりもっと大変ですし、 プレッシャーの中でモノを作らないといけませんし、もっと色々考えて考えて試して試して・・・作ります!作りこみます!! 経験があるからアドベントカレンダーネタとして比較的簡単そうに実装できます。 未経験、未踏技術なら大変ですしおそらく途中挫折します。 若手の予約機能は2週間掛かったとも聞いています。(0.5人月)それでも未完 でも向上心、探求心があればきっと糧にはなっているはず!頑張ってほしいです!! (私も若手に負けず日々探求して年齢を経験でカバーしています) 今の時代ある程度型にハマればノン〇〇でもできるとは思いますが・・・ 我々はプログラミングできますが、工作機械もプログラミングで動きます(CNC) でも我々では作れませんよね?なぜか??加工技術を持っていないからです。 いくらプログラムで工具を制御できても、どれくらいの速度とか素材に応じた最適解などは経験から学ぶもの。 何が言いたいかというと、どんなに機械化(ノン〇〇)が進んでも、 こういった「経験や人の感性でやる仕事」は人がやることだと思います。 特に何もないところから形にしていく過程や沢山のナレッジから様々なライブラリを利用してサクッと作り、サクッと動かす そしてひたすら妥協しないで作りこむ。ダメな部分は後からでも直す。 私も一昔前はこんなサクッと開発できるとは思ってもいませんでした。 今求められているものはここではなく、この先なんですね 気が向いたらGitにでもPushしておきます。 あとどこかのクラウドへデプロイもしたい! もう少し機能実装もしたいけど仕事じゃないし・・・ ~おしまい~