tag:crieit.net,2005:https://crieit.net/tags/Nextjs/feed 「Nextjs」の記事 - Crieit Crieitでタグ「Nextjs」に投稿された最近の記事 2023-08-24T22:27:40+09:00 https://crieit.net/tags/Nextjs/feed tag:crieit.net,2005:PublicArticle/18557 2023-08-24T22:27:40+09:00 2023-08-24T22:27:40+09:00 https://crieit.net/posts/Next-js-Cloud-Run-The-request-failed-because-either-the-HTTP-response-was-malformed-or-connection-to-the-instance-had-an-error Next.js+Cloud RunでThe request failed because either the HTTP response was malformed or connection to the instance had an errorがでる <p>Next.jsを使って構築したサービスをCloud Runで動かしていると、ある時から503エラーが出るようになった。エラーメッセージは <code>The request failed because either the HTTP response was malformed or connection to the instance had an error</code></p> <p>利用者さんによるととにかくエラーが頻発するらしい。</p> <p>OpenAIのAPIを使っていたのでもしかしたらそれかも…と思ったが、どうもそうでない場合もエラーが出たり、色んなところが重くなったりと、変な感じだった。</p> <p>一応Cloud Runのエラーログにドキュメントへの誘導があるので見てみたが分からなかった。</p> <p>どうしてもわからないのでとにかく解決したい一心でひたすらログを追ってみることにした。するとなんとなく原因がわかってきた。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/18412 2023-03-22T10:07:36+09:00 2023-03-22T10:07:36+09:00 https://crieit.net/posts/fdf85d1f9aeb47981c9bf9a43303c3c8 エンジニアのアウトプット力を高めるメモ投稿サービスをリリースしました【個人開発】 <p>この記事は <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/shwld/articles/ca8a7972cd1f0c">Zennに投稿したもの</a>と同じ内容です</p> <p>こんにちは<a target="_blank" rel="nofollow noopener" href="https://www.mof-mof.co.jp/">mofmof</a>でエンジニアをしている<a target="_blank" rel="nofollow noopener" href="https://twitter.com/shwld">shwld</a>です。</p> <p>mofmofではエンジニアの成長にフォーカスして色々施策を思索して試作したりしていたりもします。<br /> 今回は、課題の一つである、「<strong>アウトプットを習慣づけたい</strong>」について、ソリューションとなるサービスを作ってみました。</p> <p>この記事では</p> <ul> <li>なぜサービスを作るに至ったのか</li> <li>どんなサービスなのか</li> </ul> <p>あたりを書いていきます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://revelup.dev/ja">https://revelup.dev/ja</a></p> <h1 id="なぜアウトプットを習慣づけたいのか"><a href="#%E3%81%AA%E3%81%9C%E3%82%A2%E3%82%A6%E3%83%88%E3%83%97%E3%83%83%E3%83%88%E3%82%92%E7%BF%92%E6%85%A3%E3%81%A5%E3%81%91%E3%81%9F%E3%81%84%E3%81%AE%E3%81%8B">なぜアウトプットを習慣づけたいのか</a></h1> <p>アウトプットには様々なメリットがあります。</p> <ul> <li>インプット内容を自分のものにする <ul> <li>技術情報をインプットしただけではいざ仕入れた情報を使おうとした時に使える状態になっていないことがほとんどです。実際に手や頭を動かし、相手に教えられる状態になっていると、学習・定着効果が高まります。</li> </ul></li> <li>プレゼンスを高める <ul> <li>エンジニアの技術力を客観的に評価するにはアウトプットや経歴が重要です。極論ですが、アウトプットしていないと何もしていないと同義になってしまうのです。</li> </ul></li> </ul> <p>と、メリットはわかるのですが、実際自分がたくさんのアウトプットをできているわけではありません。自分が継続的アウトプットを改善した方法を元に今回サービスにしました。</p> <h1 id="解決したい課題"><a href="#%E8%A7%A3%E6%B1%BA%E3%81%97%E3%81%9F%E3%81%84%E8%AA%B2%E9%A1%8C">解決したい課題</a></h1> <p>アウトプットの際に以下のような課題があります。</p> <ul> <li>ひたすら開発してしまい、つまづいたところ、工夫したところを残してない <ul> <li>いざ記事を書こうとしても、記事を書くための調べ直しに多くの時間がかかる</li> </ul></li> <li>記事を公開するのにハードルがある。誤った情報を書いていないか、読めるものになっているか気になってしまう。</li> </ul> <p>ZennのScrap機能はこの課題への良い解決策で、Scrapの思想が好きです。<br /> ただ、今回自分はScrapよりもさらにハードルを下げたサービスが欲しいと思いました。</p> <h1 id="どんなサービスなのか"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AA%E3%81%AE%E3%81%8B">どんなサービスなのか</a></h1> <p>REVELUP.devはTwitterのような気軽さで技術アウトプットメモを投稿できるサービスです。<br /> フロー情報を前提としているため、正確性などを気にせず気軽に投稿しながら、本番アウトプットの練習ができる場を目指します。</p> <p>以下のような機能が備わっています。</p> <h2 id="気づいた時に即残せるデスクトップ用アプリケーション"><a href="#%E6%B0%97%E3%81%A5%E3%81%84%E3%81%9F%E6%99%82%E3%81%AB%E5%8D%B3%E6%AE%8B%E3%81%9B%E3%82%8B%E3%83%87%E3%82%B9%E3%82%AF%E3%83%88%E3%83%83%E3%83%97%E7%94%A8%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">気づいた時に即残せるデスクトップ用アプリケーション</a></h2> <p>開発の途中、何か工夫したり、つまづいたその瞬間に記録を残します。<br /> 任意のショートカットを登録することで、10秒で投稿できる体験を提供します<br /> <a href="https://crieit.now.sh/upload_images/238daf5d99d86c5f7aba8121fd533a99641a54920b8be.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/238daf5d99d86c5f7aba8121fd533a99641a54920b8be.gif?mw=700" alt="すぐ投稿" /></a></p> <h3 id="階層化可能なタグ付けによるノート整理を採用し、投稿速度と内容の整理を両立します"><a href="#%E9%9A%8E%E5%B1%A4%E5%8C%96%E5%8F%AF%E8%83%BD%E3%81%AA%E3%82%BF%E3%82%B0%E4%BB%98%E3%81%91%E3%81%AB%E3%82%88%E3%82%8B%E3%83%8E%E3%83%BC%E3%83%88%E6%95%B4%E7%90%86%E3%82%92%E6%8E%A1%E7%94%A8%E3%81%97%E3%80%81%E6%8A%95%E7%A8%BF%E9%80%9F%E5%BA%A6%E3%81%A8%E5%86%85%E5%AE%B9%E3%81%AE%E6%95%B4%E7%90%86%E3%82%92%E4%B8%A1%E7%AB%8B%E3%81%97%E3%81%BE%E3%81%99">階層化可能なタグ付けによるノート整理を採用し、投稿速度と内容の整理を両立します</a></h3> <p><a href="https://crieit.now.sh/upload_images/198bda9998ea53c0276f1b1902c752d8641a54ac5e3c5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/198bda9998ea53c0276f1b1902c752d8641a54ac5e3c5.png?mw=700" alt="階層化可能なタグ" /></a><br /> 階層化可能なタグ↑</p> <h2 id="心理的安全性を担保しつつ、公開練習できる"><a href="#%E5%BF%83%E7%90%86%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7%E3%82%92%E6%8B%85%E4%BF%9D%E3%81%97%E3%81%A4%E3%81%A4%E3%80%81%E5%85%AC%E9%96%8B%E7%B7%B4%E7%BF%92%E3%81%A7%E3%81%8D%E3%82%8B">心理的安全性を担保しつつ、公開練習できる</a></h2> <p>REVELUP.devへ投稿する内容は、承認した友達だけがみることができます。<br /> Twitterというよりは、技術投稿版facebookといった感じです。<br /> <a href="https://crieit.now.sh/upload_images/0e5b6ae2572719b6daf3806758f28bef641a54c60f023.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0e5b6ae2572719b6daf3806758f28bef641a54c60f023.png?mw=700" alt="友達にだけ見せられる" /></a></p> <p>気づいた時に投稿する性質上、業務中や驚くような時間であることもあると思いますが、友達からは投稿した時間はバレないようになっています。</p> <h2 id="実際にアウトプットする材料とする"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E3%82%A2%E3%82%A6%E3%83%88%E3%83%97%E3%83%83%E3%83%88%E3%81%99%E3%82%8B%E6%9D%90%E6%96%99%E3%81%A8%E3%81%99%E3%82%8B">実際にアウトプットする材料とする</a></h2> <p>投稿した内容は、GraphQL APIで取得することができます。<br /> 投稿内容を好きに加工、バックアップしたり他のサービスに連携など、ご活用ください。</p> <p>ストックした情報を使って、AIで記事を生成するということができると、アウトプットのハードルが下がりそうだなーとかを考えています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://revelup.dev/ja">https://revelup.dev/ja</a></p> <p>このプロダクトは、株式会社mofmofの「水曜日の個人開発」にサポートされています。<br /> <a target="_blank" rel="nofollow noopener" href="https://indie-dev.mof-mof.co.jp">https://indie-dev.mof-mof.co.jp</a></p> shwld tag:crieit.net,2005:PublicArticle/18263 2022-07-27T20:14:00+09:00 2022-07-27T20:14:00+09:00 https://crieit.net/posts/Next-js-Google Next.jsでGoogle広告のコンバージョンが測定できないとき <p>Next.jsで作ったサービスでなぜか全然コンバージョンが測定できなかった。トリガーはURL。</p> <p>やり方としてはタグマネージャを使って測定を行っている。タグマネージャのプレビューでは正常にトリガーが働いている。しかしGoogle広告側のコンバージョンテストを行ってみると正常に反映されない。</p> <p>結局、router.pushでページ遷移するのをやめ、location.hrefに変更するとうまく取れた。よくわからないがタグマネージャのプレビューでも全部のタイミングで発火しているわけではなかったのでこんな感じにしないと発火するタイミングにならないのかもしれない。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/18166 2022-04-11T23:43:19+09:00 2022-04-15T16:49:25+09:00 https://crieit.net/posts/Flutter-62543e877f391 Flutter用問い合わせフォームウィジェット&サービスを作った <p>Flutter用の問い合わせフォームのウィジェットを簡単に設置できるパッケージ及び連携サービス Contact Nite を作りました。</p> <p><a href="https://crieit.now.sh/upload_images/8c23e18a7a344fca6bd592674e642b446252e63476ec0.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8c23e18a7a344fca6bd592674e642b446252e63476ec0.png?mw=700" alt="card.png" /></a></p> <p>上記画像のように簡単なコードを設置するだけで、サービス上で設定した項目通りの問い合わせフォームウィジェットを表示することができます。また、<br /> 送信された問い合わせはサービス上で確認することができるようになっています。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a6252ee397f453.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a6252ee397f453.png?mw=700" alt="image.png" /></a></p> <p>サービス<br /> <a target="_blank" rel="nofollow noopener" href="https://contact-nite.com/ja">https://contact-nite.com/ja</a><br /> パッケージ<br /> <a target="_blank" rel="nofollow noopener" href="https://pub.dev/packages/contact_form">https://pub.dev/packages/contact_form</a></p> <p>無料プランもありますので気になる方は是非試してみてください。僕も自分のアプリに入れて試してみています。</p> <p>問い合わせが来るとメール及びSlack通知を行うことができます。そのうちサービス内で直接問い合わせ下ユーザーとやり取りができるようにしてみようと思っています。</p> <h2 id="技術的な話"><a href="#%E6%8A%80%E8%A1%93%E7%9A%84%E3%81%AA%E8%A9%B1">技術的な話</a></h2> <p>パッケージ自体は純粋なFlutterパッケージです。サービス側は今回 Next.js, PlanetScale, Cloud Run を利用しました。</p> <h3 id="PlanetScale"><a href="#PlanetScale">PlanetScale</a></h3> <p>丁度開発している途中で知った、MySQLサービスです。最近記事も書いたので良ければ見てみてください。</p> <p><a href="https://crieit.net/posts/MySQL-PlanetScale-Next-js-Prisma">サーバーレスMySQLのPlanetScaleをNext.js+Prismaで使ってみた</a><br /> <a href="https://crieit.net/posts/Prisma-PlanetScale">PrismaでPlanetScaleを使う時のエラーあれこれ</a></p> <p>ここ最近はずっとFirestoreを使ってサービスを作っていました。安くて容量が大きいとなるとこれくらいしかなかったためです。ところがPlanetScaleを知りそちらに乗り換えてみることにしました。とにかく容量が大きいというのが決め手です。本当はMySQLの方が好きなので僕にとっては嬉しいサービスです。</p> <p>まだリリースしたサービスで利用した経験が無いのでどうなるかわかりませんが、これから見ていこうと思っています。問題なければこれからの僕の定番になりそうです。</p> <h3 id="Next.js"><a href="#Next.js">Next.js</a></h3> <p>Next.jsで作っています。サーバー側もNext.jsのAPI Routesです。もうとにかく楽ちんですね、サーバーサイドとフロント側の連携とか、何も考えなくて良いというのは。仕事だと色々問題が出てくるのかもしれませんがとにかく個人で開発するものだと今はこれが楽すぎて他を考えられません。</p> <p>特に日本専用サービスとする必要もないためInternationalized Routingを使って日本語と英語の対応を行っています。</p> <h3 id="デプロイ"><a href="#%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">デプロイ</a></h3> <p>Cloud Runを利用しています。最近こればかり使っているので元々持っている資産的に楽になってきたため他を考えられません。といいつつPlanetScaleを使っているのでDBまわりで悩まなくてもいいしHerokuとかで良かったかもしれません。</p> <p>push時に連動もできるのですが、自動テストはGitHub Actionsでやったほうが超簡単なため、push→GitHub Actionsでテスト→Cloud Buildでビルド&Runにデプロイという流れをとっています。DBのマイグレーションもCloud Buildでビルドしたイメージを利用して自動化しています。</p> <p>テストはJestによるシンプルなAPIのテストと、Cypressを使ったE2Eテストを行っています。Cypressはあまり使ってないですがCypress Dashboardと連携して動画も見れたりするの面白いですね。</p> <h3 id="メール"><a href="#%E3%83%A1%E3%83%BC%E3%83%AB">メール</a></h3> <p>SendGridです。Dynamic Templatesむっちゃ楽ちんですね。SendGrid上でメールの本文を調整して簡単に送信できます。ごちゃごちゃプログラムやDB上にテンプレートを定義しなくていいので良いです。</p> <p>あとはメール受信のhookを利用して、メールも見ずにサービス上だけでやり取りできるようにもしたいなと思っています。なんかできるっぽいので。</p> <h2 id="Flutter側"><a href="#Flutter%E5%81%B4">Flutter側</a></h2> <h3 id="多言語化"><a href="#%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%8C%96">多言語化</a></h3> <p>ハマりどころとして、多言語化が結構複雑でした。パッケージを作成する場合一緒に作成されたサンプルプロジェクトと連携して動作させるのですが、そのプロジェクト内だとうまくいくのに、別途他のアプリに組み込んでみたらちゃんと言語が反映されないという問題が発生したりして手こずりました。</p> <p>ちなみにFlutterはVS Code拡張Flutter Intlを使うことで簡単にローカライズできるのですが、それも使えたようです。</p> <h3 id="Freezed"><a href="#Freezed">Freezed</a></h3> <p>Freezed普通に使えたので使っています。</p> <h3 id="テスト"><a href="#%E3%83%86%E3%82%B9%E3%83%88">テスト</a></h3> <p>パッケージは公開ということもありGitHub Actionsでのテストが無料で放題なので、せっかくなので自動テストをいれてあります。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>小ネタですがサービスサイトの問い合わせフォームもWebではありますがContact Niteに送信して実現しています。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17962 2022-02-02T08:58:21+09:00 2022-02-02T08:58:21+09:00 https://crieit.net/posts/React-async-await Reactでasync/awaitだけで確認ダイアログを出せるようにする <p>今回はNext.jsですが、Reactで確認ダイアログを出す時、confirm関数的なものをawaitで呼べたら楽ちんだよなあと思うのでその実装方法を書きます。そのページに別途ダイアログ用のテンプレートは記述しない方法です。</p> <p>例えば下記のような感じです。</p> <pre><code class="typescript">const onClick = useCallback(async () => { const isConfirmed = await confirm('タイトル', 'OKですか?') if (!isConfirmed) { return } doSomething() }, []) </code></pre> <h2 id="ダイアログの状態管理を作成"><a href="#%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E3%81%AE%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%E3%82%92%E4%BD%9C%E6%88%90">ダイアログの状態管理を作成</a></h2> <p>まずはダイアログの状態管理を行います。今回は状態管理としてRecoilを使っています。下記のような <code>useConfirmDialog.ts</code> を作成します。</p> <pre><code class="typescript">import { useCallback } from 'react' import { atom, useRecoilState } from 'recoil' type OpenArgs = { title?: string body: React.ReactNode } type DialogProps = OpenArgs & { resolve: (value: boolean) => void } const dialogPropsState = atom<DialogProps>({ key: 'confirmDialog/dialogProps', default: undefined, }) export function useConfirmDialog() { const [dialogProps, setDialogProps] = useRecoilState(dialogPropsState) const close = useCallback( (result: boolean) => { console.log({ dialogProps }) if (dialogProps.resolve) { dialogProps.resolve(result) } setDialogProps(undefined) }, [dialogProps] ) function open({ title, body }: OpenArgs): Promise<boolean> { return new Promise((resolve) => { setDialogProps({ title, body, resolve, }) }) } return { isOpen: !!dialogProps, open, close, dialogProps } } </code></pre> <p>ダイアログが開いているかの <code>isOpen</code>、開くための関数 <code>open</code>、閉じるための <code>close</code>、ダイアログの設定である <code>dialogProps</code> を出力します。</p> <p>ここで重要なのがPromiseのresolveをdialogPropsとして保存しているところです。これを保存しておき、ダイアログが終わるタイミングでそのresolveを呼び出すことで呼び出し元のawaitを終了させることができます。</p> <p>ちなみにこのdialogPropsという形ではなく、タイトルや内容、resolveをそれぞれのstateにすることも考えられますが、これはだめでした。なぜかresolveが勝手にundefinedになってしまい即awaitが終了してしまいます。何か勝手に解放されてしまうのでしょうか。ということでここが一番の肝でした。</p> <h2 id="ダイアログの表示を行う"><a href="#%E3%83%80%E3%82%A4%E3%82%A2%E3%83%AD%E3%82%B0%E3%81%AE%E8%A1%A8%E7%A4%BA%E3%82%92%E8%A1%8C%E3%81%86">ダイアログの表示を行う</a></h2> <p>ダイアログ自体は ConfirmDialogProvider というコンポーネントを作り、アプリケーション全体を囲むレイアウトとして設置しておきます。</p> <p>Next.jsであれば <code>pages/_app.tsx</code> に下記のような感じです。</p> <pre><code class="jsx">function MyApp({ Component, pageProps }) { const colors = useColors() return ( <RecoilRoot> <ConfirmDialogProvider> <Component {...pageProps} /> </ConfirmDialogProvider> </RecoilRoot> ) } </code></pre> <p>ConfirmDialogProviderは下記のような感じです。Chakra UIのダイアログを使った例です。</p> <pre><code class="jsx">import { useConfirmDialog } from '@/hooks/dialog/useConfirmDialog' import { useLocale } from '@/hooks/useLocale' import { AlertDialog, AlertDialogOverlay, AlertDialogContent, AlertDialogHeader, AlertDialogBody, AlertDialogFooter, Button, } from '@chakra-ui/react' import { useRef } from 'react' type Props = { children: React.ReactNode } export default function ConfirmDialogProvider(props: Props) { const { isOpen, close, dialogProps } = useConfirmDialog() const { title, body } = dialogProps ?? {} const cancelRef = useRef() return ( <> {props.children} <AlertDialog isOpen={isOpen} leastDestructiveRef={cancelRef} onClose={() => close(false)} > <AlertDialogOverlay> <AlertDialogContent> {title && ( <AlertDialogHeader fontSize="lg" fontWeight="bold"> {title} </AlertDialogHeader> )} <AlertDialogBody mt={title ? undefined : 4}>{body}</AlertDialogBody> <AlertDialogFooter> <Button ref={cancelRef} onClick={() => close(false)}> Cancel </Button> <Button colorScheme="teal" onClick={() => close(true)} ml={3}> OK </Button> </AlertDialogFooter> </AlertDialogContent> </AlertDialogOverlay> </AlertDialog> </> ) } </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>これで確認が楽ちんになりました。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17949 2022-01-23T20:41:17+09:00 2022-02-06T21:57:21+09:00 https://crieit.net/posts/MySQL-PlanetScale-Next-js-Prisma サーバーレスMySQLのPlanetScaleをNext.js+Prismaで使ってみた <p>PlanetScaleというのはMySQL互換のサーバーレスデータベース。つまりどこからでもMySQL接続してデータベースとして利用できるサービス。</p> <p>接続方法は普通によくあるようなパスワードを使ったデータベースURLで接続可能。そのためだいたいどんなフレームワークでも利用できる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://planetscale.com/">https://planetscale.com/</a></p> <h2 id="無料枠がでかい"><a href="#%E7%84%A1%E6%96%99%E6%9E%A0%E3%81%8C%E3%81%A7%E3%81%8B%E3%81%84">無料枠がでかい</a></h2> <p>すごく気に入った理由の一つとして、無料枠がかなり大きいことがあげられる。2022/1時点で容量10GB、書き込み回数は月100万回、読み込み回数も月1000万回と小さいアプリケーションであれば気にするレベルでないほど十分にある。ちょっとしたアプリをたくさん作っているという人にとってはとても嬉しい無料枠。</p> <p>というのも同じく無料で使えるHerokuのJawsDBも容量5MB、最大接続数10と、むちゃくちゃ少なく、集客下手だから…と思いつつもちょっと心配になってしまう制限のため、なんかこう、微妙だなぁ、とずっと感じていた。</p> <p>しかもPlanetScaleは東京リージョンまであるのでびっくり。</p> <h2 id="コンソールもある"><a href="#%E3%82%B3%E3%83%B3%E3%82%BD%E3%83%BC%E3%83%AB%E3%82%82%E3%81%82%E3%82%8B">コンソールもある</a></h2> <p>サービス内にコンソールもあるためちょっとした調査とか、個人サービスのデータ調整とかはこれで簡単にできそう。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a61ed30770571d.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a61ed30770571d.png?mw=700" alt="image.png" /></a></p> <p>あとは現在のスキーマも見れる。</p> <h2 id="実際に試してみた"><a href="#%E5%AE%9F%E9%9A%9B%E3%81%AB%E8%A9%A6%E3%81%97%E3%81%A6%E3%81%BF%E3%81%9F">実際に試してみた</a></h2> <p>ということで実際に使ってみた。Next.jsからPrismaで利用。かんたんな書き込み、取得処理だけで試してみた。</p> <h3 id="接続方法"><a href="#%E6%8E%A5%E7%B6%9A%E6%96%B9%E6%B3%95">接続方法</a></h3> <p>PlanetScaleのConnect設定のところでパスワードを設定できる。それにより普通に接続が可能。ここはいろんなライブラリでの接続方法も表示してくれる。Prismaの設定もそのままコピペでできるように教えてくれる。</p> <h3 id="開発"><a href="#%E9%96%8B%E7%99%BA">開発</a></h3> <p>Prismaを使う場合、開発は普通にローカルのデータベースを使ってやると良さそう。というのもPrismaはShadow databaseというものを用いている。開発時はなんかそれで色々いい感じにしているらしい。ということでそのためにデータベースのCreate, Drop権限が必要。その関係でマイグレーションの作成を行うタイミングでPlanetScaleとの接続ではエラーになってしまう。そのため開発時はローカルだけで完結させておくとスムーズ。</p> <p>本番に反映させたいときだけPlanetScaleへの接続に変え、 <code>npx prisma db push</code> を実行することでマイグレーションを反映できる。</p> <p>PlanetScaleはドキュメントも結構しっかり書かれているようで、このあたりの解説もちゃんと書かれている。<br /> <a target="_blank" rel="nofollow noopener" href="https://docs.planetscale.com/tutorials/prisma-quickstart">https://docs.planetscale.com/tutorials/prisma-quickstart</a></p> <h3 id="速さ"><a href="#%E9%80%9F%E3%81%95">速さ</a></h3> <p>クラウドということで速さがちょっと気になったていたが、一つ作ったデータ取得用のAPIでだいたいTTFBが300msくらいだったので十分そうだった。</p> <p>気をつける点として、Vercelの場合は無料枠だとリージョンがUSAのEASTと決まっているのでPlanetScale側もそれに合わせておく必要がある。一旦東京で作ってみていた時は1.5sかかっていた。遅くて使えないな~と思った場合はこのあたりの設定が間違っている可能性があるかもしれないので気をつけよう。</p> <h2 id="追記"><a href="#%E8%BF%BD%E8%A8%98">追記</a></h2> <p>実際に別途使ってみるとエラーが色々出たので対処法</p> <p><a href="https://crieit.net/posts/Prisma-PlanetScale">PrismaでPlanetScaleを使う時のエラーあれこれ</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>実際に作ってみたテストサイトはこちら。<br /> <a target="_blank" rel="nofollow noopener" href="https://simple-planetscale-test.vercel.app/">https://simple-planetscale-test.vercel.app/</a></p> <p>試したソースコードはこちら<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/dala00/simple-planetscale-test">https://github.com/dala00/simple-planetscale-test</a></p> <p>まだ実運用したことがないのでどうなるかはわからないが、今のところとても気に入っているのでFirestoreで作っているちょっと大きめのサービスを移行してみようか悩み中…。今後もちょっとしたちいちゃいネタサービスを作るときにも使ってみたい。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17836 2021-12-08T08:54:05+09:00 2021-12-08T08:55:11+09:00 https://crieit.net/posts/Cloud-Run-Next-js-NEXT-PUBLIC Cloud RunでNext.jsのNEXT_PUBLIC_の環境変数が有効にならない場合の対処 <p>Cloud RunはGitHub連携もできるため、Next.jsのアプリケーションもDockerfileを準備すれば簡単にデプロイを自動化できる。しかし、連携設定どおりにちゃちゃっと設定していっても、NEXT_PUBLIC_で始まるクライアント用の環境変数がうまく設定できない。Cloud Runの設定を行う際に環境変数を指定することができるのだが、それを設定していてもNEXT_PUBLIC_がついている環境変数だけが動かない。しかもAPIなどで使っている環境変数は問題なく動いている。</p> <h2 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h2> <p>そもそもNEXT_PUBLIC_系の環境変数というのは、実行したタイミングで利用されるものではない。サーバー側で直接利用される場合はそのまま動くが、クライアント側はビルドした時に埋め込まれる仕様となっている。そのためCloud Runで実行した場合ではなく、Cloud Buildでビルドされるタイミングで環境変数が利用されるようにしておかなければならない。つまり、Cloud Build側に環境変数の設定が必要ということ。</p> <h2 id="Cloud Buildでの設定方法"><a href="#Cloud+Build%E3%81%A7%E3%81%AE%E8%A8%AD%E5%AE%9A%E6%96%B9%E6%B3%95">Cloud Buildでの設定方法</a></h2> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17765 2021-11-17T09:36:55+09:00 2021-11-17T09:36:55+09:00 https://crieit.net/posts/102949946b0e6197f103d453a781a1fc 数日で有料サービスをリリースしてみた話 <p>有料サービスを2,3日位で作ってリリースしてみました。多分計10時間ほど? 急にぱっと思いついて次の日の夜くらいにはだいたい完成していました。作ったのは下記のHand Refactorerというプログラムのコードを手動的にリファクタリングしてくれるサービスです。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">130円でプログラムのコードを手動的にリファクタリングしてくれるサービスをリリースしました。プログラミングを始めたばかりの方にはもしかしたら役立つ場合もあるかもです。よろしければお試しください!!<a target="_blank" rel="nofollow noopener" href="https://t.co/bHlNXJFbTh">https://t.co/bHlNXJFbTh</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/VhPI80a6Cl">pic.twitter.com/VhPI80a6Cl</a></p>— だら@Flutterもやってる (@dala00) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/dala00/status/1459816338050859008?ref_src=twsrc%5Etfw">November 14, 2021</a></blockquote> <p>ちなみにこれ自体は単に自動的じゃなくて手動かよというツッコミがほしいだけのために作ったネタクソアプリです。</p> <p>色々やるべきことを削ったりなどで考えたりしたので書いておきます。</p> <h2 id="サービスの流れ"><a href="#%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E6%B5%81%E3%82%8C">サービスの流れ</a></h2> <p>そもそもサービスの流れですが、まずユーザーがリファクタリングしてほしいコードを入力して登録します。</p> <p>それを僕が人力でリファクタリングし、回答として登録するとユーザーがそれを確認できる、という形です。</p> <h2 id="課金関連の開発を極限まで削る"><a href="#%E8%AA%B2%E9%87%91%E9%96%A2%E9%80%A3%E3%81%AE%E9%96%8B%E7%99%BA%E3%82%92%E6%A5%B5%E9%99%90%E3%81%BE%E3%81%A7%E5%89%8A%E3%82%8B">課金関連の開発を極限まで削る</a></h2> <p>課金となると、結構色々考えなければなりません。ユーザー登録し、そのユーザーに対して課金ログを保存し、購入履歴などを用意する必要があります。ただそこまでするとどうしても工数がかかってしまいます。そのため下記のようにして開発事項を削りました。</p> <h3 id="オーソリを使う"><a href="#%E3%82%AA%E3%83%BC%E3%82%BD%E3%83%AA%E3%82%92%E4%BD%BF%E3%81%86">オーソリを使う</a></h3> <p>オーソリというのは、仮売上です。即課金確定するのではなく、注文が確定した時にキャプチャという処理を行って決済を確定させる方法です。ユーザーが注文してきた時に仮売上とし、僕が処理を行って納品でき、ユーザーがそれを意図的に閲覧した場合に納品確定としキャプチャを行うようにしました。</p> <p>この方法には色々メリットがありました。</p> <h4 id="ユーザーの心理的不安を取り除ける"><a href="#%E3%83%A6%E3%83%BC%E3%82%B6%E3%83%BC%E3%81%AE%E5%BF%83%E7%90%86%E7%9A%84%E4%B8%8D%E5%AE%89%E3%82%92%E5%8F%96%E3%82%8A%E9%99%A4%E3%81%91%E3%82%8B">ユーザーの心理的不安を取り除ける</a></h4> <p>個人サービスですので、そこに課金するのにはちょっと不安があります。ただ、ちゃんと納品が行われないと課金が確定しないということが事前にわかっていればその不安をある程度取り除くことができます。</p> <p>確定する場合には、ユーザーに回答画面のURLが貼られたメールが送られます。ここでも実際に見て課金を確定するかどうかをユーザーが意図的に選ぶ事ができます。興味がなければ見ずに終わらせればいいですし、お金を払ってでも見たいと思っていれば閲覧して確定できます。</p> <h4 id="運営側の不安も取り除ける"><a href="#%E9%81%8B%E5%96%B6%E5%81%B4%E3%81%AE%E4%B8%8D%E5%AE%89%E3%82%82%E5%8F%96%E3%82%8A%E9%99%A4%E3%81%91%E3%82%8B">運営側の不安も取り除ける</a></h4> <p>個人サービスで課金機能を提供するのは、運営者としてもなかなか気持ち的に不安になることが多いです。うまくサービスを提供できなかったらどうしよう、大きなミスに気づかず迷惑をかけてしまったらどうしよう、など。</p> <p>しかしオーソリを利用してユーザーがお金を払って結果を見たいと確信して行動する時に初めて課金が行われるようにすれば、なかなか間違えたり問題が起こる可能性は低くなります。そうなると運営側としても問題が発生しにくく安心です。</p> <p>また、最悪僕が怠けたりなにか急なトラブルで全然手を付けられず放置したとしても一切課金されることはありません。また、そんなにリファクタリングすることがないコードであれば確定しないことでキャンセル扱いにすることもできます。</p> <p>このようにしてお互いの不安をなるべく少なくすることでユーザーは購入を、運営はリリースをしやすい気持ちにできるようにしました。これができなければ色々と複雑なテストをしたり入念にいろいろな機能をつくったりしてなかなかリリースはできなかったでしょう。</p> <h3 id="チェックアウトを使わない"><a href="#%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%82%A2%E3%82%A6%E3%83%88%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84">チェックアウトを使わない</a></h3> <p>Stripeにはチェックアウトというプログラムを書かなくても商品を売れる機能があります。すごく簡単なのですが今回はそれは使いませんでした。というのも売上の結果を知るためにWebhookでチェックする必要があり、ちょっとそれが面倒でした。プログラムで決済してしまえば登録時に一緒にやってしまえばすべてが終わるためそちらの方が処理的にもわかりやすくテストもしやすかったためです。</p> <h2 id="メールアドレスを使う"><a href="#%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%A2%E3%83%89%E3%83%AC%E3%82%B9%E3%82%92%E4%BD%BF%E3%81%86">メールアドレスを使う</a></h2> <p>メールアドレスを使うことでログイン機能を削りました。ユーザー登録をすると、やはり離脱率は上がります。ちょっと投稿して利用するだけのサービスでユーザー登録は面倒くさいです。</p> <p>また、ログイン機能を作るとそれはそれで結構面倒です。Next.js と Firebase を使っているのですが、それで認証を入れるとFirestoreのセキュリティルールも設定しなければなりませんし、その状態だとNext.jsのサーバーサイドレンダリング時にエラーが発生することもあり、結構確認事項が増えます。ログイン機能を作らずメールアドレスのみの照合とすることでそれら全ての開発を削りました。</p> <p>メールアドレスだけでは勝手に決済されて危険なのでは? と思われるかもですが、クレジットカード自体は操作している人自身のものを入れなければならないためなりすましは難しいですし、できたとしても結局オーソリまでしか進めないため、利用者が最後まで進まないとお金は一切動きません。</p> <p>またこれにより結果のURLはメールで本人しか知ることができないため漏洩の心配などもありません。</p> <p>最初はユーザーもメールアドレス登録するのはいやかな…とも思ったのですが、そもそも大量の依頼がくるとそれはそれで僕のスケジュールがパンクしてしまうのである意味抑制になっていいかなと思っています。</p> <h2 id="極限までハードルを下げる"><a href="#%E6%A5%B5%E9%99%90%E3%81%BE%E3%81%A7%E3%83%8F%E3%83%BC%E3%83%89%E3%83%AB%E3%82%92%E4%B8%8B%E3%81%92%E3%82%8B">極限までハードルを下げる</a></h2> <p>このハードルというのはユーザー、運営者両方のことです。</p> <p>まず費用は130円という自販機でジュースを買うのと同じ(?)ような料金にしました。また、説明分にはレジャー感覚で使ってほしいということ、意図した結果が帰ってくるとは限らないということを書いておき、運営者的にもちゃんとした結果をださなければ、という不安をなくし、利用者としてもさほど結果に期待しない状態を作っておきました。</p> <p>ハイクオリティになったり、ハードルの認識の齟齬があると開発もそれに対処するためにちょっとやることや考えることが増えたりなど、すぐにリリースはできません。とにかくすぐに作るレベルのサービスに連動したクオリティの商品をそれに見合った低価格で提供することによってリリース時の負担を下げました。</p> <h2 id="管理機能を公開しない"><a href="#%E7%AE%A1%E7%90%86%E6%A9%9F%E8%83%BD%E3%82%92%E5%85%AC%E9%96%8B%E3%81%97%E3%81%AA%E3%81%84">管理機能を公開しない</a></h2> <p>リファクタリング結果の送信ですが、Firestoreのダッシュボードだけではできません。そのためそこは返答用の画面を作ってあげる必要がありました。</p> <p>しかし前述の通り認証もありませんし、URLを作ってしまうと万が一アクセスされてしまった時に情報が漏洩してしまいます。</p> <p>そのため管理画面は公開せず、別プロジェクトとして作り、ローカルのデバッグ実行だけでできるようにしました。絶対に他の人にアクセスされることもありませんので安全です。また、作っていたプログラムをコピーするところから始めたのでベース部分はまるまる流用でき、特に大変でもありませんでした。</p> <h2 id="Firebaseクライアントを使わず全部APIのみにする"><a href="#Firebase%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%82%92%E4%BD%BF%E3%82%8F%E3%81%9A%E5%85%A8%E9%83%A8API%E3%81%AE%E3%81%BF%E3%81%AB%E3%81%99%E3%82%8B">Firebaseクライアントを使わず全部APIのみにする</a></h2> <p>前述の通り、セキュリティルールを設定するのが面倒だったためFirebaseのクライアントはアクセス解析にしか使っていません。セキュリティルールは全部のデータへのアクセス不許可にしています。</p> <p>普通に単なるAPIとしてfirebase-adminを利用してサーバーサイドのDBのようにして作っています。なにもかんがえることもなく非常に楽です。勝手に変なデータにアクセスされてしまうこともありません。</p> <p>また、Firestoreを使うことによってデプロイする場所を選ばなくてもよいようにもなっています。</p> <h2 id="Cloud Runにデプロイ"><a href="#Cloud+Run%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">Cloud Runにデプロイ</a></h2> <p>今回はCloud Runにデプロイしました。前述の通りDBはFirestoreなのでどこにでもデプロイできます。本来はVercelが一番楽なのですが、この無料プランは個人的な利用のみに限られますので、今回の場合は利用できません。ということでCloud Runを使いました。GitHubにpushしたのに連動して自動的にデプロイもできますし。Herokuでも良いと思いますが頻繁に利用されるサービスではありませんし、あまりアクセスして遅いのもあれかなと思いCloud Runにしました。</p> <p>せっかくなのでDockerfileを使わないでデプロイできるBuildpackというものを試してみたかったというのがあったのですが、どうもビルド無しで実行されてしまうようでエラーになったので今回は諦めてDockerfileを使うことにしました。だいたいDockerfileならどっかーにあると思いますので。</p> <p>今回は下記を使いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kazumax4395/articles/427cc791f6145b">【Node.js/Next.js】Cloud Runで動作する軽量なDockerを構築してみた</a></p> <p>これの真ん中のやつでだいたいデプロイに8分ほどかかります。それほどパフォーマンスが必要ではないので一番最初の簡易的なやつでも良かったのかもしれません。なんにしろVercelよりはやはりどうしても遅くなってしまいますね。</p> <p>あと日本リージョンにしたのでちょっとネットワーク料金がかかります(アクセスがちょっと多ければ月何十円とか100円とか?)</p> <h2 id="CSSも使わない"><a href="#CSS%E3%82%82%E4%BD%BF%E3%82%8F%E3%81%AA%E3%81%84">CSSも使わない</a></h2> <p>今回はChakra UIを使いました。とにかくデフォルトのデザインパーツで、カスタマイズは全部Chakraコンポーネントのプロパティのみで、ぶわっと作りました。非常に楽です。</p> <h2 id="DIFFの表示"><a href="#DIFF%E3%81%AE%E8%A1%A8%E7%A4%BA">DIFFの表示</a></h2> <p>リファクタリングということで、DIFFを表示したほうがわかりやすいかなと思いました。</p> <p>React Diff Viewerというのを使いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/praneshr/react-diff-viewer">https://github.com/praneshr/react-diff-viewer</a></p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>最初に書いたとおりクソアプリですので、ものすごい依頼が増えない限りは特にアップデートする予定はありません(依頼がきたらちゃんと対応します。リファクタリングは結構好きなのでわりと楽しんでやっています)。</p> <p>ただ数日で作れたので、これを機に低価格で同じように自分で提供できることを売るのも面白いかなとは思います。どうしても手がかかるので規模が大きくなるとスケールもしないし限界がありますので、あくまでも今回のようにジョーク交じりかつすごく負担の少ない作業だけになるとは思いますが。</p> <p>逆に言うと、低価格でちょっとなにか提供できる、というひとはたくさんいると思いますので、みんながこういうサービスを各々作れたら面白いのにな、とも作ってる途中に思いました。例えば小説の手直し、絵の手直し、もしくはちょっとした絵を書いてあげたり、サービスの感想を伝えてあげたり、なにか写真をとってあげたり、ロゴをつくってあげたり。皆自分の得意なことを提供して少しでもお金になれば楽しいのでは、と思いました。</p> <p>もちろんそういうモール的なサービスはいくつかあると思いますが、あくまでも自分の個人商店的な感じで運用するのも面白いと思います。</p> <p>自分でみんながそういうことを提供できるサービスを作ればよいのでは、とも一瞬思ったのですが、やはり価格的に運営者に入るお金はかなり薄利になり、儲けを出すにはかなり大量に集客しないといけませんし個人だと逆に大変なことが多そうなので、無理かなと思いました。Skebのもっと安くて気軽バージョン的な感じかもです。</p> <p>なので可能な方は是非同じ様なサービスを作ってなにか提供してみると面白いのではないかと思います。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16704 2021-03-01T01:31:55+09:00 2021-03-01T01:31:55+09:00 https://crieit.net/posts/Next-js-Vercel-Recoil-Material-Table-AWS Next.jsとVercelとRecoilとMaterial Tableを使ってAWSのステータスダッシュボードを作ってみた話 <h2 id="AWSのステータス確認難しいよね"><a href="#AWS%E3%81%AE%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E7%A2%BA%E8%AA%8D%E9%9B%A3%E3%81%97%E3%81%84%E3%82%88%E3%81%AD">AWSのステータス確認難しいよね</a></h2> <p>AWSを使ったことのある人ならばわかると思いますが、公式がAWSの障害情報を掲載する<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/">AWS Service Health Dashboard</a>があまり使いやすくないです。</p> <p><img src="https://i.imgur.com/XghDulZ.png" alt="img" /></p> <p>それぞれのリージョンの障害がRSSで配信される形式になっているのですが、わざわざRSSを登録するのもめんどくさいし、Slackとかの連携に乗っけるのもそれはそれで便利なのですが、そもそもSlackを見ていないほかの人でも障害情報を共有したいです。</p> <p>実は、AWS Service Health Dashboardの情報はJSONで取得することができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/data.json">https://status.aws.amazon.com/data.json</a></p> <p>こちらのJSONを活用して勉強がてら使いやすいダッシュボードを作っていきます。</p> <h2 id="クビになるぞ!"><a href="#%E3%82%AF%E3%83%93%E3%81%AB%E3%81%AA%E3%82%8B%E3%81%9E%EF%BC%81">クビになるぞ!</a></h2> <p>最近、これといった新しい技術に触れておらず、このままだとクビになりそうなので、そろそろ重い腰を上げてNext.jsを勉強することにしました。</p> <p>また、Next.jsを使う場合はVercelが便利だよーとのことですので、こちらも使っていきます。</p> <h2 id="Next.js"><a href="#Next.js">Next.js</a></h2> <p>Next.jsではpages/api配下に格納したコードについては、サーバーサイドとして振る舞います。</p> <p>クライアントから直接status情報がかかれたJSONを読みとってもよかったのですが、HTMLの面倒なサニタイジング処理やら、値の補完など面倒なことはサーバーサイドに持ってこようということで、<br /> statusJSONを取得して、フロントに返却するサーバーコードを書いていきます。</p> <p>次のようなコードになりました。</p> <pre><code class="typescript">import { NextApiRequest, NextApiResponse } from 'next' import axios from 'axios' export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } const handler = (req: NextApiRequest, res: NextApiResponse) => { axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { const handlerResp = resp.data.archive.map((x) => ({ // eslint-disable-next-line @typescript-eslint/camelcase service_name: x.service_name, summary: x.summary, region: x.service.includes('management-console') ? 'global' : x.service.split('-').slice(1).join('-') === '' ? 'global' : x.service.split('-').slice(1).join('-'), date: x.date, status: x.status, details: x.details, service: x.service.includes('management-console') ? 'management-console' : x.service.split('-')[0], description: x.description .replace(/<("[^"]*"|'[^']*'|[^'">])*>/g, '') .replace(/&nbsp;/g, '\n'), })) res.statusCode = 200 // eslint-disable-next-line no-console console.log(handlerResp) res.json(handlerResp) }) .catch((error) => { console.error(error.response) res.statusCode = error.response.status || 500 res.statusMessage = error.response.statusText || 'InternalServerError' res.json({ error: error.response.statusText || 'InternalServerError' }) }) } export default handler </code></pre> <p>注意点として、必ずハンドラーはexport defaultを指定してあげないこと以外はいたって直感的なコードとなっております。</p> <p>Vercelに載っけるとわかるのですが、こちらのコード、Lambdaにデプロイされることになります。たしかに見覚えある感じですね。</p> <p>また、Next.jsと関係ないのですが、axiosのレスポンスに型がつけられるって知ってましたか?</p> <pre><code class="typescript">export interface AwsStatusResp { archive: AwsStatusArchive[] } export interface AwsStatusArchive { service_name: string summary: string date: string status: string details: string description: string service: string } axios .get<AwsStatusResp>('https://status.aws.amazon.com/data.json') .then((resp) => { .......... </code></pre> <h2 id="Material Table"><a href="#Material+Table">Material Table</a></h2> <p>Material UI準拠のテーブルとして、Material Tableなるものがありましたので今回採用することにしました。</p> <pre><code class="typescript">import MaterialTable from 'material-table' import tableIcons from '../components/tableIcons' <MaterialTable icons={tableIcons} columns={[ { title: 'Service Name', field: 'service_name' }, { title: 'Service', field: 'service', width: 10 }, { title: 'Region', field: 'region', lookup: regionNameMapping }, { title: 'Summary', field: 'summary' }, { title: 'Date (' + dayjs.tz.guess() + ')', field: 'date', render: (rowData) => ( <div> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ssZ[Z]')} </div> ), defaultSort: 'desc', type: 'string', }, { title: 'Status', field: 'status', lookup: statusMapping, }, ]} data={aws} detailPanel={[ { tooltip: 'Details', render: (rowData) => { return ( <> <div className="title">{rowData.summary}</div> <div className="description"> {dayjs .unix(Number(rowData.date)) .format('YYYY-MM-DDTHH:mm:ss')}{' '} {rowData.service_name} </div> <div className="code">{rowData.description}</div> </> ) }, }, ]} options=<span>{</span><span>{</span> filtering: true, grouping: true, exportButton: true, exportFileName: 'exported', headerStyle: { backgroundColor: '#e77f2f', color: '#FFF', }, <span>}</span><span>}</span> isLoading={loading} actions={[ { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.BarChartIcon, tooltip: 'Show Bar Chart', isFreeAction: true, disabled: loading, onClick: async () => { setShowGraph(!showG) }, }, { // Issue: https://github.com/mbrn/material-table/issues/51 //@ts-ignore icon: tableIcons.Refresh, tooltip: 'Refresh Data', isFreeAction: true, disabled: loading, onClick: async () => { setLoading(true) await getAwsStatus() }, }, ]} title={ <div className="header"> <img src="/awslogo.png" /> <a href="https://aws-health-dashboard.vercel.app/"> AWS Health Dashboard </a> </div> } /> </code></pre> <p>使い方もシンプルかつ比較的高機能でいい感じです。</p> <p>いい感じですが後述するRecoilとの相性問題とDatetimeの扱いが微妙なのがツラミでした。</p> <p>本当はDate型を渡してあげるとSearchableの際、カレンダーでの絞り込みができるのかなと思ったのですが、こちらがうまくいきませんでした。</p> <p>あと、微妙に型もおかしく例えば、actionsはactionを複数指定することができるはずですが、型チェックで怒られるので、仕方なくts-ignoreしてます。</p> <p>あなたが直せばいいじゃんアゼルバイジャンって言われそうですが、めんどくさくなってしまいIssueだけあげてしまいました。申し訳ねぇ...。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/mbrn/material-table/issues/2762">https://github.com/mbrn/material-table/issues/2762</a></p> <h2 id="Recoil"><a href="#Recoil">Recoil</a></h2> <p>RecoilとはReactの新しい状態管理ライブラリで、いわゆるReact HooksでGlobal Storeを作ろうというものです。</p> <p>基本的な使い方はまず、storeとしてatomという共有ステートを作成します。</p> <p>atomのkeyはプロジェクトで一意にする必要がありますが、今回はそこまで大規模なプロジェクトではないのでawsとかいうクソ名をつけてます。</p> <p>storeなので、store/aws.ts として格納します。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { // eslint-disable-next-line @typescript-eslint/camelcase service_name: 'Auto Scaling (N. Virginia)', summary: '[RESOLVED] Example Error', date: '1542849575', status: '1', details: '', description: 'The issue has been resolved and the service is operating normally.', service: 'autoscaling', region: 'us-east-1', }, ], dangerouslyAllowMutability: true, }) export default awsState </code></pre> <p>次にステートを共有したいコンポーネントのルートにRecoilRootを設置します。</p> <p>Next.jsの場合、_app.tsxが全ページのルートにあたるのでここに置けばいいですね。</p> <pre><code class="typescript">import { AppProps } from 'next/app' import Head from 'next/head' import { RecoilRoot } from 'recoil' import React from 'react' const App = ({ Component, pageProps }: AppProps) => ( <> <RecoilRoot> <Head> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <title>AWS Health Dashboard</title> </Head> <Component {...pageProps} /> </RecoilRoot> </> ) export default App </code></pre> <p>そして、利用するときはuseRecoilStateをReact Hooksのように利用するだけです。簡単ですね。</p> <pre><code class="typescript">import React, { useState, useEffect } from 'react' import { useRecoilState } from 'recoil' import awsState from '../store/aws' import showGraph from '../store/showGraph' import axios from 'axios' import dayjs from 'dayjs' dayjs.extend(utc) dayjs.extend(timezone) const Alert = (props: AlertProps) => { return <MuiAlert elevation={6} variant="filled" {...props} /> } export const Table = (): JSX.Element => { // 20200112: dangerouslyAllowMutabilityでできた const [aws, setAws] = useRecoilState(awsState) const [showG, setShowGraph] = useRecoilState(showGraph) const [loading, setLoading] = useState(true) const [slackBarOpen, setSlackBarOpen] = React.useState(false) const [apiErrorMsg, setApiErrorMsg] = React.useState('') useEffect(() => { getAwsStatus() setLoading(false) }, []) const getAwsStatus = () => { axios .get('/api/aws') .then((resp) => { setAws(resp.data) setLoading(false) }) .catch((error) => { console.error(error.response) setSlackBarOpen(true) setApiErrorMsg(error.response.statusText || 'Error') setAws([]) setLoading(false) }) } </code></pre> <p>stateの読み込みはgetterから、書き込みはsetterから行います。</p> <p>React Hooksに慣れていれば簡単ですね。</p> <h2 id="思わぬ落とし穴 Material TablesでRecoilが使えない"><a href="#%E6%80%9D%E3%82%8F%E3%81%AC%E8%90%BD%E3%81%A8%E3%81%97%E7%A9%B4+Material+Tables%E3%81%A7Recoil%E3%81%8C%E4%BD%BF%E3%81%88%E3%81%AA%E3%81%84">思わぬ落とし穴 Material TablesでRecoilが使えない</a></h2> <p>Recoilのatomは基本値の書き換えはset stateを使うことが求められます。ですが、material tablesはテーブルを作るときにdataにIDの書き込みが発生するようでそのままだと怒られてしまいます。</p> <pre><code>Cannot add property tableData, object is not extensible </code></pre> <p>これの解決策はRecoilにstateへの直接的な書き換えを許可することです。こちらはatomのoptionでdangerouslyAllowMutabilityを有効にすることで解決できます。</p> <pre><code class="typescript">import { atom } from 'recoil' const awsState = atom({ key: 'aws', default: [ { }, ], dangerouslyAllowMutability: true, }) </code></pre> <p>これがわかるのに半日くらい使っちまいました。</p> <h2 id="Chart.js"><a href="#Chart.js">Chart.js</a></h2> <p>さて、無事にRecoilでstateの共有ができたのでせっかくなので別コンポーネントも作ります。</p> <p>意味があるかどうか不明ですが、AWSの障害発生状況を可視化してみようと思います。</p> <p>ということで、採用したのがChart.js。</p> <p>次のようにデータを渡すだけできれいめなグラフを書いてくれます。</p> <pre><code class="typescript">import { useRecoilValue } from 'recoil' import awsState from '../store/aws' import React from 'react' import { regionNameMapping, } from './const' import BarGraph from './barGraph' export const AlertPerRegion = (): JSX.Element => { const aws = useRecoilValue(awsState) const labels = Array.from( new Set(aws.map((data) => regionNameMapping[data.region])) ) const data = [] for (const r of labels) { data.push( aws .map((data) => regionNameMapping[data.region]) .reduce((total, x) => { return x === r ? total + 1 : total }, 0) ) } return ( <div className="container"> <BarGraph labels={labels} data={data} title="Alert per region" /> </div> ) } </code></pre> <p>どうでもいい実装ですが、各グラフを一覧で見れる画面を用意し、実際のグラフは遷移先で表示するようにしてます。</p> <p><img src="https://i.imgur.com/tfnpq4w.png" alt="img" /></p> <p><img src="https://i.imgur.com/hpJ70fR.png" alt="img" /></p> <h2 id="Vercelにデプロイ"><a href="#Vercel%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4">Vercelにデプロイ</a></h2> <p>さて、実装ができたので後はVercelにあげるだけです。</p> <p>もうここはほとんど書くことがないのですが、Next.jsで作ったアプリケーションはVercelでレポジトリと使っているフレームワークを設定するだけで簡単にデプロイ出来てしまいます。</p> <p>これはすごい。</p> <h2 id="完成"><a href="#%E5%AE%8C%E6%88%90">完成</a></h2> <p>ということで、AWS Health Dashboardが完成しました。</p> <p>アクセスすると、Next.jsのapiをコールし、AWSのstatusを取得加工したものを返却します。</p> <p>フロントでは受け取ったデータをRecoilのatomに格納しつつ、material tableで表として描画します。</p> <p>また右上のグラフボタンを押すことで色々な切り口の可視化を行うことができます。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aws-health-dashboard.vercel.app/">https://aws-health-dashboard.vercel.app/</a></p> <p>できれば使う場面にならないことを祈りつつ、ご活用いただければとおもいます。</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>食わず嫌いでやらなかったNext.js+Recoilをやってみましたが、楽しく実装ができました。</p> <h2 id="2021/02/20追記"><a href="#2021%2F02%2F20%E8%BF%BD%E8%A8%98">2021/02/20追記</a></h2> <p>2021/02/19~20にかけて起きた<a target="_blank" rel="nofollow noopener" href="https://status.aws.amazon.com/rss/ec2-ap-northeast-1.rss">AWS EC2障害</a>ですが、本ダッシュボードでは更新がされませんでした。</p> <p>どうやら、data.jsonはRSSとは違い、同期的に更新されないようです。</p> <p>大変ご迷惑をおかけしました。改めて、改修しRSS更新にも対応できるように頑張ります。</p> tubone24 tag:crieit.net,2005:PublicArticle/16448 2020-12-30T20:51:19+09:00 2020-12-30T20:52:17+09:00 https://crieit.net/posts/f05ede37f52343628fe7705f6a531ed7 キャップ野球専用スコアブック入力アプリをリリースした <h1 id="蓋野球スコア入力アプリ「CAP-SCOREBOOK」"><a href="#%E8%93%8B%E9%87%8E%E7%90%83%E3%82%B9%E3%82%B3%E3%82%A2%E5%85%A5%E5%8A%9B%E3%82%A2%E3%83%97%E3%83%AA%E3%80%8CCAP-SCOREBOOK%E3%80%8D">蓋野球スコア入力アプリ「CAP-SCOREBOOK」</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://cap-scoresheet.netlify.app">https://cap-scoresheet.netlify.app</a></p> <h2 id="操作マニュアル"><a href="#%E6%93%8D%E4%BD%9C%E3%83%9E%E3%83%8B%E3%83%A5%E3%82%A2%E3%83%AB">操作マニュアル</a></h2> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/XtF5HmEmwWU" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h2 id="機能概要"><a href="#%E6%A9%9F%E8%83%BD%E6%A6%82%E8%A6%81">機能概要</a></h2> <ul> <li>非ログイン時(閲覧モード) <ul> <li>試合結果閲覧</li> <li>試合内容閲覧</li> <li>個人成績閲覧</li> <li>チーム成績閲覧</li> <li>リーグ成績閲覧</li> <li>ログイン時(入力モード)</li> <li>チーム新規作成</li> <li>選手新規作成</li> <li>試合新規作成</li> <li>スターティングメンバー設定</li> <li>結果入力 <ul> <li>1球ごと入力</li> <li>1打席ごと入力</li> </ul></li> <li>自責点・勝敗S入力</li> </ul></li> </ul> <h2 id="開発環境"><a href="#%E9%96%8B%E7%99%BA%E7%92%B0%E5%A2%83">開発環境</a></h2> <ul> <li>NextJS(static export)</li> <li>Netlify</li> <li>検証機: Pixel4a(Android)/12mini(iOS)/PC</li> <li>サーバ側: NodeJS</li> <li>DB: MySQL</li> <li>Docker-compose</li> <li>ReactNative For Web</li> <li>ユーザ認証: firebase</li> </ul> <h2 id="開発の経緯"><a href="#%E9%96%8B%E7%99%BA%E3%81%AE%E7%B5%8C%E7%B7%AF">開発の経緯</a></h2> <p>10/21 蓋世・エスト監督からスコアを共有できるアプリはないか聞かれる<br /> 10/25 1打席ごとの入力ができるWEBアプリの仮実装が終わる<br /> 10/30 スコアの活用方法思い付かずに実装中断<br /> 11/26<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/Yokoppe_cap/status/1331653356872298496">https://twitter.com/Yokoppe_cap/status/1331653356872298496</a><br /> よこっぺさんの一言で中断してたスコアアプリの開発のやる気が復活する。<br /> 11/28 リーグ戦の帰りにkamiさんが「投球分析したいなぁ」と言う話をする。<br /> 11/29〜<br /> ある程度実装が終わっていたアプリに1球ごとのコース分析機能を拡張したアプリに着手。<br /> 当初はReactNativeを使ったandroid/iOSアプリを想定。<br /> 12/1 ネイティブアプリから得意なWEBアプリへの舵の切り直しを宣言。<br /> 12/4 ReactNative For Webの採用を決定。<br /> 12/7 Hirooookiさんから投手視点への切り替えが欲しいとの要望を受けるhttps://twitter.com/gstiltonhs/status/1335606102675783680<br /> 12/11 配球分析機能の実装が完了<br /> 12/14 いらすとやからのデザイン変更<br /> 12/21 テストコードの実装に着手<br /> 12/30 α版リリース</p> <h2 id="今回新たに使った技術"><a href="#%E4%BB%8A%E5%9B%9E%E6%96%B0%E3%81%9F%E3%81%AB%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">今回新たに使った技術</a></h2> <ul> <li>ReactNative For Web</li> </ul> <p>CSSが書きにくいと思って敬遠していたのですが、いざ使ってみるとモバイルに最適化されていて、きちんとスタイルを書けば複雑なUIも実現できたので、食わず嫌いはよくないなと。</p> <h2 id="開発する上で役に立ったこと"><a href="#%E9%96%8B%E7%99%BA%E3%81%99%E3%82%8B%E4%B8%8A%E3%81%A7%E5%BD%B9%E3%81%AB%E7%AB%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">開発する上で役に立ったこと</a></h2> <p>やっぱり<a target="_blank" rel="nofollow noopener" href="https://jcbl-score.com">みんなのSCORE</a>の開発経験ですかね。<br /> DB設計的にはCAP-SCOREBOOKの方が1球ずつ記録する分複雑なのですが、<br /> CAP-SCOREBOOKにほとんどDB設計を応用できました。</p> <h1 id="今後の展開(β版へ向けて)"><a href="#%E4%BB%8A%E5%BE%8C%E3%81%AE%E5%B1%95%E9%96%8B%28%CE%B2%E7%89%88%E3%81%B8%E5%90%91%E3%81%91%E3%81%A6%29">今後の展開(β版へ向けて)</a></h1> <ul> <li>共同編集機能</li> <li>スコアシート出力機能(需要があれば)</li> </ul> ckoshien tag:crieit.net,2005:PublicArticle/16242 2020-11-27T23:04:34+09:00 2020-11-27T23:04:34+09:00 https://crieit.net/posts/Next-js-Bootstrap-Sticky-Footer Next.jsでBootstrapのSticky Footerを利用する <p>BootstrapにはSticky Footerのデモがあります。</p> <p><a target="_blank" rel="nofollow noopener" href="https://v5.getbootstrap.jp/docs/5.0/examples/sticky-footer/">https://v5.getbootstrap.jp/docs/5.0/examples/sticky-footer/</a></p> <p>昔はCSSを使っていましたが、現在はBootstrapのユーティリティを利用してクラス指定だけで実現しています。そのため適切にクラスを指定していけばよいだけなのですが、Next.jsのようなフレームワークの場合は各HTMLタグがデフォルトでは編集できなかったり、配置が決まっていたりするためなかなか思うように行きません。</p> <p>実際に対応した方法を書いておきます。</p> <h2 id="_document.jsを作成する"><a href="#_document.js%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">_document.jsを作成する</a></h2> <p>まずは全体のHTMLを編集するための _document.js というファイルを作ります。TypeScriptの場合は _document.tsx です。_app.js と同様、pagesの直下に配置します。</p> <p>中身は公式で解説しているとおりです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/advanced-features/custom-document">Advanced Features: Custom <code>Document</code> | Next.js</a></p> <h2 id="Sticky Footer用のクラスを指定していく"><a href="#Sticky+Footer%E7%94%A8%E3%81%AE%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%92%E6%8C%87%E5%AE%9A%E3%81%97%E3%81%A6%E3%81%84%E3%81%8F">Sticky Footer用のクラスを指定していく</a></h2> <p>次にどんどんクラスを指定していきます。まずはhtmlとbodyタグに高さ100%のクラス <code>h-100</code> を指定します。 _document.js 内です。</p> <pre><code class="jsx"> return ( <Html lang="ja" className="h-100"> <Head /> <body className="h-100"> <Main /> <NextScript /> </body> </Html> ) </code></pre> <p>次にフッター以外のメインコンテンツを下記のタグで囲みます。ここからは _document.js ではなく各ページもしくは共通レイアウトに設定します。</p> <pre><code class="html"> <main className="flex-shrink-0"> メインコンテンツ </main> </code></pre> <p>そしてその下にSticky Footerを配置します。</p> <pre><code class="html"><footer className="footer mt-auto py-3 bg-light"> <div className="container"> <span className="text-muted">フッターのコンテンツをここに置きます。</span> </div> </footer> </code></pre> <h2 id="Next.js用の対応"><a href="#Next.js%E7%94%A8%E3%81%AE%E5%AF%BE%E5%BF%9C">Next.js用の対応</a></h2> <p>これで一通りBootstrapのデモと同じ設定は完了です。しかし、Next.jsの場合は下記のように <code>__next</code> というidのdivが全体を囲んでしまっています。そのためデモと同様にbodyに <code>d-flex flex-column</code> を指定しても全てそのdivに反映されてしまい、メインコンテンツとフッターが正常に動作しません。そのためbodyは <code>h-100</code> だけにしていました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5fc106aea8470.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5fc106aea8470.png?mw=700" alt="" /></a></p> <p>そのため、ここだけはCSSで指定してあげます。</p> <pre><code class="css">#__next { display: flex; flex-direction: column; height: 100%; } </code></pre> <p>自動挿入されている要素ですのでidなど、仕様が変わったら適宜調整が必要です。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16169 2020-10-24T16:23:32+09:00 2020-10-24T16:23:32+09:00 https://crieit.net/posts/Next-js-Dgraph-5-Dgraph Next.jsとDgraphで作るクイズ・アプリ (5) Dgraphの基本 <h1 id="Dgraph操作の基本"><a href="#Dgraph%E6%93%8D%E4%BD%9C%E3%81%AE%E5%9F%BA%E6%9C%AC">Dgraph操作の基本</a></h1> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-4">前回</a>を参照</p> <h2 id="用語"><a href="#%E7%94%A8%E8%AA%9E">用語</a></h2> <p>グラフデータベースなので, ノードで構成されます.</p> <div class="table-responsive"><table> <thead> <tr> <th>用語</th> <th>意味</th> </tr> </thead> <tbody> <tr> <td>ノード</td> <td>データを構成する基本要素. レコードのようなもの</td> </tr> <tr> <td>プレディケート</td> <td>ノードの属性やエッジ(ノード間の関係性)を表す</td> </tr> </tbody> </table></div> <h2 id="スキーマ"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E">スキーマ</a></h2> <p>とりあえず各ノードは以下のようなスキーマ持つとします.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>title</td> <td>string</td> </tr> <tr> <td>user</td> <td>string</td> </tr> <tr> <td>version</td> <td>string</td> </tr> <tr> <td>date</td> <td>datetime</td> </tr> <tr> <td>question</td> <td>[uid]</td> </tr> <tr> <td>answer</td> <td>[uid]</td> </tr> <tr> <td>tags</td> <td>[uid]</td> </tr> </tbody> </table></div> <p>[uid]というのは別のノードを参照しているという意味で1対多の関係になります.</p> <blockquote> <p>UID arrays represent a collection of UIDs. This is used to represent one to many relationships.</p> </blockquote> <p><a target="_blank" rel="nofollow noopener" href="https://dgraph.io/docs/tutorial-3/#data-types-for-predicates">Data types for predicates</a></p> <h3 id="question and answer nodes"><a href="#question+and+answer+nodes">question and answer nodes</a></h3> <p>クイズの本体とも言える問とその答えです. 同じデータ構造で表現します.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>text</td> <td>string</td> </tr> <tr> <td>content</td> <td>string</td> </tr> </tbody> </table></div> <h3 id="tags"><a href="#tags">tags</a></h3> <p>クイズを分類するタグです. ノードで表現することで一意性をもたせることができます.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Data Type</th> </tr> </thead> <tbody> <tr> <td>tag_name</td> <td>string</td> </tr> </tbody> </table></div> <h2 id="ミューテーション"><a href="#%E3%83%9F%E3%83%A5%E3%83%BC%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">ミューテーション</a></h2> <p>データに変更を加えるにはsetとdeleteという2つのキーワードを使います.</p> <h2 id="CRUD"><a href="#CRUD">CRUD</a></h2> <h3 id="create"><a href="#create">create</a></h3> <p>setはノードの作成とプレディケートの更新を行います.</p> <pre><code class="json">{ "set": [ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "question": [ { "type": "text", "content": "MonQとは何でしょうか?" }, { "type": "text", "content": "またどのようなものを目指しているでしょうか?" } ], "answer": [ { "type": "text", "content": "クイズベースの学習システムです." }, { "type": "text", "content": "クイズ同士の寒冷性や依存性を定義することでクイズをモジュール化することを目指します." } ], "tags": [ { "uid": "_:monq", "tag_name": "monq" } ] } ] } </code></pre> <p>作成されたノードには自動でuidが与えられます.</p> <h3 id="read"><a href="#read">read</a></h3> <p>クエリを用いて取得します. <a target="_blank" rel="nofollow noopener" href="https://dgraph.io/docs/query-language/functions/#has">has関数</a>を使うと指定したプレディケートを持っているノードを取得できます. func引数にhas関数を渡してやります.</p> <pre><code>{ quizzes(func: has(title)) { uid title user date version question { type content } answer { type content } tags { tag_name } } } </code></pre> <p>ここでquizzesは関数名というよりはrootノードです. レスポンスは以下のようになります. quizzesという配列に取得したノードが収められています. 指定したルートノードから辿れるようにな構造になるわけです.</p> <pre><code class="json">{ "data": { "quizzes": [ ... ] } ... } </code></pre> <p>以下のようなグラフが取得できます. ピンクがquとあるのでquestionで緑がanswer, そして紫がtagsです.中心の青はtitleを含むノードです. つまりクイズそのものを指します.</p> <p><a href="https://crieit.now.sh/upload_images/c999c6bf95bbd99d341a53a9c5795a8a5f8e70a39720a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c999c6bf95bbd99d341a53a9c5795a8a5f8e70a39720a.png?mw=700" alt="image" /></a></p> <h4 id="ブランク・ノード"><a href="#%E3%83%96%E3%83%A9%E3%83%B3%E3%82%AF%E3%83%BB%E3%83%8E%E3%83%BC%E3%83%89">ブランク・ノード</a></h4> <p>ノードを指定する時にuidは自動で作られます. 作成後のノードには全てuidが付番されているので参照することは簡単です. 問題はノードを定義する時に共通のuidを指定したい場合です.</p> <p>例えば共通のtagを持つクイズを同時に作りたい場合です. tagはノードとして管理されるのでuidで識別できます. 同じ文字列を指定しても同じノードを参照しているという関係は表せません. この場合_:identifierという表記を使うと同じuidであることを表せます.</p> <p>以下は英単語の意味を問うよくあるクイズです. これらはどちらもenglishというタグ・ノードを参照するのが妥当でしょう.</p> <pre><code class="json">{ "set": [ { "title": "put on holdとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "put on holdとは?" } ], "question": [ { "type": "text", "content": "保留する" } ], "tags": [ { "uid": "_:english", "tag_name": "english" } ] }, { "title": "hotshotとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "hotshotとは?" } ], "question": [ { "type": "text", "content": "有能な人、やり手, 凄腕" } ], "tags": [ { "uid": "_:english", "tag_name": "english" } ] } ] } </code></pre> <p>先程のクエリを実行すると以下のようなグラフが得られます.</p> <p><a href="https://crieit.now.sh/upload_images/a28995f869f3975eb5f4a6989755647e5f8e7341911b6.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a28995f869f3975eb5f4a6989755647e5f8e7341911b6.png?mw=700" alt="image" /></a></p> <p>englishというtagsノードを通じてつながっていることが分かります.</p> <h3 id="delete"><a href="#delete">delete</a></h3> <h4 id="ノードの削除"><a href="#%E3%83%8E%E3%83%BC%E3%83%89%E3%81%AE%E5%89%8A%E9%99%A4">ノードの削除</a></h4> <p>よく分からん. とにかく消えないです.</p> <h4 id="プレディケーとの削除"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%81%A8%E3%81%AE%E5%89%8A%E9%99%A4">プレディケーとの削除</a></h4> <p>更新処理の前に削除をしてみましょう. 各ノードにはプレディケートというフィールドに該当するものが存在する. ノードをuidで指定してプレディケートをnullに指定するとそのプレディケートは削除されたことになります. 全てのプレディケートがなくなるとそのノードは削除されたとみなされます(とされていますが, コンソールで試すと消えません).</p> <p>ノードの場合は単純にuidを指定すれば良いようです.</p> <p>さて先程の英単語のクイズを削除してみましょう. 以下のようなミューテーションを実行します.</p> <pre><code class="json">{ "delete": [ { "uid": "0x3a982", "title": null, "answer": null, "question": null, "tags": null } ] } </code></pre> <p>この場合tagsは他方のクイズからも消えてしまうでしょうか? このノードからは辿れなくなりますが, ノード自体は残ります.</p> <h4 id="プレディケートの全削除"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88%E3%81%AE%E5%85%A8%E5%89%8A%E9%99%A4">プレディケートの全削除</a></h4> <p>以下のような記法を使います.</p> <pre><code>{ delete { <0x3a985> * * . } } </code></pre> <p>プレディケートのフィールド名を使うこともできます.</p> <pre><code>{ delete { <0x3a985> <title> * . } } </code></pre> <p>こえでtitleプレディケートは消せます.</p> <h3 id="update"><a href="#update">update</a></h3> <h4 id="プロパティ・プレディケート"><a href="#%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%83%BB%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88">プロパティ・プレディケート</a></h4> <p>単純に値を上書きすれば良いようです. userの名前を変更してみましょう.</p> <pre><code class="json">{ "set": [ { "uid": "0x3a985", "user": "no-brainer" } ] } </code></pre> <p>ではuser名を取得してみましょう.</p> <pre><code>{ quizzes(func: uid(0x3a985)) { user } } </code></pre> <p>user名がbrainvaderからno-brainerに変わっているのが分かります.</p> <p><a href="https://crieit.now.sh/upload_images/ce43595fe29e31547cdeb53c66b173d75f8e815697351.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ce43595fe29e31547cdeb53c66b173d75f8e815697351.png?mw=700" alt="image" /></a></p> <h4 id="エッジ・プレディケート"><a href="#%E3%82%A8%E3%83%83%E3%82%B8%E3%83%BB%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88">エッジ・プレディケート</a></h4> <p>おなじみのクイズを作ってみましょう.</p> <pre><code class="json">{ "set": [ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "question": [ { "type": "text", "content": "MonQとは何でしょうか?" }, { "type": "text", "content": "またどのようなものを目指しているでしょうか?" } ], "answer": [ { "type": "text", "content": "クイズベースの学習システムです." }, { "type": "text", "content": "クイズ同士の寒冷性や依存性を定義することでクイズをモジュール化することを目指します." } ], "tags": [ { "tag_name": "monq" } ] } ] } </code></pre> <p>tagを更新して見ます.</p> <pre><code class="json">{ "set": [ { "uid": "0x3a992", "tags" : { "tag_name": "tutorial" } } ] } </code></pre> <p>新しくtutorialというtagが追加されたことが分かります.</p> <pre><code>{ quizzes(func: has(tag_name)) { tag_name } } </code></pre> <h2 id="Q&amp;A"><a href="#Q%26amp%3BA">Q&A</a></h2> <h3 id="プレディケートの前に付く~(ティルダ)の意味とは?"><a href="#%E3%83%97%E3%83%AC%E3%83%87%E3%82%A3%E3%82%B1%E3%83%BC%E3%83%88%E3%81%AE%E5%89%8D%E3%81%AB%E4%BB%98%E3%81%8F%7E%28%E3%83%86%E3%82%A3%E3%83%AB%E3%83%80%29%E3%81%AE%E6%84%8F%E5%91%B3%E3%81%A8%E3%81%AF%3F">プレディケートの前に付く~(ティルダ)の意味とは?</a></h3> <p>エッジの方向を反対に探索することを意味する.</p> <h3 id="anyofterms(predicate, &quot;query word&quot;)"><a href="#anyofterms%28predicate%2C+%26quot%3Bquery+word%26quot%3B%29">anyofterms(predicate, "query word")</a></h3> <p>プレディケーとがqueryかwordにマッチする.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <ul> <li>uid(value)</li> <li>has(predicate)</li> <li>gt(predicate, value)</li> <li>@recurse(depth: value)</li> <li>@filter(lt(dislikes, 10))</li> <li>eq(tag_name,"devrel")</li> </ul> <h2 id="クエリ集"><a href="#%E3%82%AF%E3%82%A8%E3%83%AA%E9%9B%86">クエリ集</a></h2> <h3 id="タグの取得"><a href="#%E3%82%BF%E3%82%B0%E3%81%AE%E5%8F%96%E5%BE%97">タグの取得</a></h3> <pre><code>{ all_tags(func: has(tag_name)) { tag_name } } </code></pre> <h3 id="特定のタグの削除"><a href="#%E7%89%B9%E5%AE%9A%E3%81%AE%E3%82%BF%E3%82%B0%E3%81%AE%E5%89%8A%E9%99%A4">特定のタグの削除</a></h3> <p>プロパティ・プレディケートがなくなると削除されるので唯一のプレディケートであるtag_nameをnullに設定すれば良い.</p> <p>```json<br /> {<br /> "delete": [<br /> {<br /> "uid": "0x3a992",<br /> "tags" : [{<br /> "uid" : "0x3a994",<br /> "tag_name": null<br /> }<br /> ]<br /> }<br /> ]<br /> }</p> ブレイン tag:crieit.net,2005:PublicArticle/16125 2020-10-11T19:07:14+09:00 2020-10-20T10:56:23+09:00 https://crieit.net/posts/Next-js-Dgraph-4 Next.jsとDgraphで作るクイズ・アプリ (4) データ形式とデータ通信 <h1 id="JSONを用いたデータのやり取り"><a href="#JSON%E3%82%92%E7%94%A8%E3%81%84%E3%81%9F%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE%E3%82%84%E3%82%8A%E5%8F%96%E3%82%8A">JSONを用いたデータのやり取り</a></h1> <p>今回はクライアントとAPIエンドポイントの間でのデータのやり取りの仕方をまとめておきます.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect">前回</a>を参照</p> <h2 id="APIエンドポイント一覧"><a href="#API%E3%82%A8%E3%83%B3%E3%83%89%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88%E4%B8%80%E8%A6%A7">APIエンドポイント一覧</a></h2> <p>以下のようなエンドポイントが必要になります.</p> <div class="table-responsive"><table> <thead> <tr> <th>API</th> <th>リソースの内容</th> </tr> </thead> <tbody> <tr> <td>GET base_url/api/quizzes</td> <td>クイズを取得する</td> </tr> <tr> <td>POST base_url/api/quizzes</td> <td>新規のクイズを作成する</td> </tr> <tr> <td>GET base_url/api/quizzes/[uid]</td> <td>指定したクイズの取得</td> </tr> <tr> <td>DELETE base_url/api/quizzes/[uid]</td> <td>指定したクイズの削除</td> </tr> <tr> <td>PUT base_url/api/quizzes/[uid]</td> <td>指定したクイズの更新</td> </tr> </tbody> </table></div> <h2 id="どんなデータをやり取りするか?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E3%82%84%E3%82%8A%E5%8F%96%E3%82%8A%E3%81%99%E3%82%8B%E3%81%8B%3F">どんなデータをやり取りするか?</a></h2> <h3 id="スキーマ"><a href="#%E3%82%B9%E3%82%AD%E3%83%BC%E3%83%9E">スキーマ</a></h3> <p>Dgraphではスキーマと呼ばれます.</p> <div class="table-responsive"><table> <thead> <tr> <th>Predicate</th> <th>Type</th> </tr> </thead> <tbody> <tr> <td>title</td> <td>string</td> </tr> <tr> <td>user</td> <td>string</td> </tr> <tr> <td>date</td> <td>datetime</td> </tr> <tr> <td>question</td> <td>[uid]</td> </tr> <tr> <td>answer</td> <td>[uid]</td> </tr> <tr> <td>tags</td> <td>[string]</td> </tr> </tbody> </table></div> <p>questionとanswerはtypeとcontentというstring型のプレディケートを持ちます.</p> <p>---追記---</p> <h3 id="例"><a href="#%E4%BE%8B">例</a></h3> <p>JSONの方が分かりやすと思います. 以下のようなJSONを登録すると上のようなスキーマが設定される感じです. なおanswerとquestionはDgraphでは別のノードとして管理されます. そのため[uid]のような型になるようです.</p> <pre><code class="json">[ { "title": "MonQとは何でしょうか?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:00", "answer": [ { "type": "text", "content": "MonQとは何でしょうか?" } ], "question": [ { "type": "text", "content": "クイズベースの学習システムです." } ], "tags": [ "monq" ] }, { "title": "put on holdとは?", "user": "brainvader", "version": "0.0.1", "date": "2020-10-08T18:01:01", "answer": [ { "type": "text", "content": "put on holdとは?" } ], "question": [ { "type": "text", "content": "保留する" } ], "tags": [ "english" ] } ] </code></pre> <p>こんな感じのデータがAPIを介してエディタとDBの間でやり取りされるわけです.</p> <h2 id="Next.jsにおけるデータ通信"><a href="#Next.js%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%87%E3%83%BC%E3%82%BF%E9%80%9A%E4%BF%A1">Next.jsにおけるデータ通信</a></h2> <p>サーバー側に送るデータはbodyに渡されます.</p> <pre><code class="javascript">const createQuizHandler = async () => { const body = { data: 'test' } const res = await fetch('/api/quizzes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const data = await res.json() console.log(`create quiz with uid: ${data.uid}`) } </code></pre> <p>サーバー側ではreq.bodyとして参照できます. 値を返すときはjsonメソッドにJSオブジェクトを渡します. これがクライアント側ではres.jsonとして取得できます.</p> <pre><code class="javascript">const addQuiz = async (req, res) => { const data = req.body.data console.log(`I've gottern ${data}`) res.statusCode = 200 res.setHeader('Content-Type', 'application/json') res.json({ uid: '101' }) } </code></pre> <p>データのやり取りは非常に簡単ですね.</p> <h3 id="パス・パラメータ"><a href="#%E3%83%91%E3%82%B9%E3%83%BB%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF">パス・パラメータ</a></h3> <p>クイズを作成しuidだけもらってエディタ上で開く場合pages/quizzes/[uid].jsというようにパス・パラメータを渡す必要があります. uidはクイズの生成時に動的に決まるのでルートも動的に指定する必要があるからです.</p> <p>useRouterを使うと命令的にルーティングを実行することができ, この時にuidを指定することができるようになります. まずは読み込みます.</p> <pre><code class="javascript">import { useRouter } from 'next/router' const router = useRouter </code></pre> <p>routerのpushメソッドに以下のようにパス・パラメータを指定します.</p> <pre><code class="javascript">router.push({ pathname: '/quizzes/[uid]', query: { uid: uid }, }) </code></pre> <p>これでuidの部分にuidとして渡した値が使われることになります.</p> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-reference/next/router#with-url-object">With URL object</a></p> <p>apiの場合はfetchなどの際に文字列テンプレートを使って挿入すればいいです.</p> <pre><code class="javascript">const url = `api/quizzes${uid}` </code></pre> <p>サーバー側では以下のようにqueryオブジェクトから取得できます.</p> <pre><code class="javascript">export default function handler(req, res) { const { query: { pid }, } = req res.end(`Post: ${pid}`) } </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>あまり考えないでもNext.js側でやり方を強制してくるので, 使うのは難しくないと感じました.</p> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p>簡単にDgraphのクライアントの使い方をまとめます.</p> <p>次回</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-reference/next/router#routerpush">router.push</a><br /> <a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/routing/dynamic-routes">Dynamic Routes</a><br /> <a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/dynamic-api-routes">Dynamic API Routes</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16124 2020-10-11T18:11:02+09:00 2020-10-11T19:07:40+09:00 https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect Next.jsとDgraphで作るクイズ・アプリ (3) APIルートの設定とnext-connect <h1 id="APIの設定"><a href="#API%E3%81%AE%E8%A8%AD%E5%AE%9A">APIの設定</a></h1> <p>今回はAPIルートを使って簡単なバックエンドを作ります.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-2">前回</a>を参照</p> <h2 id="APIルートの設定"><a href="#API%E3%83%AB%E3%83%BC%E3%83%88%E3%81%AE%E8%A8%AD%E5%AE%9A">APIルートの設定</a></h2> <h3 id="APIルートとは?"><a href="#API%E3%83%AB%E3%83%BC%E3%83%88%E3%81%A8%E3%81%AF%3F">APIルートとは?</a></h3> <p>Next.jsが提供する機能でAPIエンドポイントが作れます. まずpagesフォルダ以下にapiというフォルダを作ります. この中にハンドラを定義します. フォルダのパスがそのままAPIのパスとして使われます.</p> <p>例えばapi/quizzes.jsなら</p> <p><a target="_blank" rel="nofollow noopener" href="http://localhost:3000/api/quizzes">http://localhost:3000/api/quizzes</a></p> <p>としてアクセスできます.</p> <h3 id="ハンドラのシグニチャ"><a href="#%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E3%82%B7%E3%82%B0%E3%83%8B%E3%83%81%E3%83%A3">ハンドラのシグニチャ</a></h3> <p>ハンドラはリクエストを受け取ってレスポンスを返すような非同期関数です.</p> <pre><code class="javascript">const handler = async (req, res) => { // do something with local file system, database, or external apis } </code></pre> <p>これをファイルの末尾でエクスポートしておくだけです.</p> <pre><code>export default handler </code></pre> <h3 id="HTTPメソッド"><a href="#HTTP%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89">HTTPメソッド</a></h3> <p>api/quizzesエンドポイントではクイズの取得, クイズの作成ができます. これらはHTTPメソッドの違いで区別されるわけです. メソッドはreq.methodで参照できます.</p> <pre><code class="javascript">export default function handler(req, res) { if (req.method === 'POST') { // Process a POST request } else { // Handle any other HTTP method } } </code></pre> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/docs/api-routes/introduction">API Routes</a></p> <p>これは少しめんどくさいです. next-connectを使うともう少し整理した書き方が出来るようになります.</p> <h2 id="dgraph-js-http"><a href="#dgraph-js-http">dgraph-js-http</a></h2> <p>DgraphへのアクセスはJavaScriptクライアントの<a target="_blank" rel="nofollow noopener" href="https://github.com/dgraph-io/dgraph-js-http">dgraph-js-http</a>を使います.</p> <h3 id="インストール"><a href="#%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB">インストール</a></h3> <pre><code>npm i dgraph-js-http </code></pre> <h3 id="読み込み"><a href="#%E8%AA%AD%E3%81%BF%E8%BE%BC%E3%81%BF">読み込み</a></h3> <pre><code>import * as dgraph from 'dgraph-js-http' </code></pre> <h3 id="クライアントの作成"><a href="#%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%AE%E4%BD%9C%E6%88%90">クライアントの作成</a></h3> <p>その前にDgraph ZeroのURLを.env.localに保存しておきましょう.</p> <pre><code>DGRAPH_URL='http://localhost:8080/' </code></pre> <p>これを使いクライアントを作成します. DgraphClientStubは第一引数にアドレスを取ります.</p> <pre><code>function DgraphClientStub(addr, stubConfig, options) { ... } </code></pre> <p>このインスタンスをDgraphClientに渡します.</p> <pre><code>const clientStub = new dgraph.DgraphClientStub(process.env.DGRAPH_URL) const dgraphClient = new dgraph.DgraphClient(clientStub) </code></pre> <h2 id="next-connect"><a href="#next-connect">next-connect</a></h2> <p>実際にデータをやり取りするにはHTTPメソッドの指定が必要です. またその度にクライアントを作成するのも面倒です. next-connectを使うとHTTPメソッドごとのハンドラのヒモ付やDBクライアント取得処理をミドルウェア化できます. 内部的にはtrouterという機能を使っているようです.</p> <h3 id="導入"><a href="#%E5%B0%8E%E5%85%A5">導入</a></h3> <pre><code>npm i next-connect </code></pre> <h3 id="ミドルウェア"><a href="#%E3%83%9F%E3%83%89%E3%83%AB%E3%82%A6%E3%82%A7%E3%82%A2">ミドルウェア</a></h3> <p>middlware/database.js</p> <pre><code class="javascript">import * as dgraph from 'dgraph-js-http' import nextConnect from 'next-connect'; const clientStub = new dgraph.DgraphClientStub(process.env.DGRAPH_URL) const dgraphClient = new dgraph.DgraphClient(clientStub) async function database(req, res, next) { req.dbClient = dgraphClient; return next(); } const middleware = nextConnect(); middleware.use(database); export default middleware; </code></pre> <p>dbClientとして参照出来るようになります. ハンドラ側ではミドルウェアを利用するようにします.</p> <pre><code class="javascript">import nextConnect from 'next-connect'; import middleware from '../../../middleware/database'; const handler = nextConnect(); handler.use(middleware); export default handler; </code></pre> <h3 id="ハンドラの登録"><a href="#%E3%83%8F%E3%83%B3%E3%83%89%E3%83%A9%E3%81%AE%E7%99%BB%E9%8C%B2">ハンドラの登録</a></h3> <p>各メソッド毎にハンドラを登録できるようになりました.</p> <pre><code class="javascript">const getHandler = async (req, res) => { /*...*/ } handler.get(getHandler) </code></pre> <p>これで</p> <pre><code>GET http://localhost:8080/api/quizzes POST http://localhost:8080/api/quizzes </code></pre> <p>を使い分けることができるようになりました.</p> <h3 id="Dgraphクライアントへのアクセス"><a href="#Dgraph%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9">Dgraphクライアントへのアクセス</a></h3> <p>reqから参照できます.</p> <pre><code class="javascript">const getQuizzes = async (req, res) => { const client = req.dbClient } </code></pre> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>next-connectを使うことでHTTPメソッドとハンドラの関係性が記述しやすくなったかなと思います.</p> <h2 id="次回"><a href="#%E6%AC%A1%E5%9B%9E">次回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-4">次回</a>を参照</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://vercel.com/guides/deploying-next-and-mysql-with-vercel">Create a Next.js App with a MySQL Database That Builds and Deploys with Vercel</a><br /> <a target="_blank" rel="nofollow noopener" href="https://developer.mongodb.com/how-to/nextjs-building-modern-applications">Building Modern Applications with Next.js and MongoDB</a><br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/insight3110/items/5514f41404ce2036bd87">Next.jsのAPI Routerを使ってMongoDBからデータを引っ張ってくるAPIを作る(next-connectを利用したミドルウェアも)</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16123 2020-10-11T17:22:58+09:00 2020-10-11T18:11:37+09:00 https://crieit.net/posts/Next-js-Dgraph-2 Next.jsとDgraphで作るクイズ・アプリ (2) プロジェクトの環境構築 <h1 id="プロジェクトの構築"><a href="#%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%81%AE%E6%A7%8B%E7%AF%89">プロジェクトの構築</a></h1> <p>今回からはプロジェクトを構築していく.</p> <h2 id="前回"><a href="#%E5%89%8D%E5%9B%9E">前回</a></h2> <p><a href="https://crieit.net/posts/Next-js-Dgraph-1">前回</a>を参照</p> <h2 id="前提条件"><a href="#%E5%89%8D%E6%8F%90%E6%9D%A1%E4%BB%B6">前提条件</a></h2> <ul> <li>Node.js入ってる?</li> <li>Gitが入ってる?</li> <li>Docker入ってる?</li> </ul> <h2 id="Next.js"><a href="#Next.js">Next.js</a></h2> <p>Next.jsの環境構築は簡単です. create-react-appのようにcreate-next-appを使うとプロジェクト・テンプレートが生成されます.</p> <pre><code>npm i -g create-next-app </code></pre> <p>必要ならログイン・シェルを再起動します.</p> <pre><code>exec $SHELL -l </code></pre> <p>後は適当にプロジェクト名を決めて実行するだけです.</p> <pre><code>create-next-app minq-editor --use-npm </code></pre> <p>ターミナルなどで以下を実行してアプリケーションを起動しましょう.</p> <pre><code class="shell">npm run dev </code></pre> <h2 id="Dgraphクラスタの起動"><a href="#Dgraph%E3%82%AF%E3%83%A9%E3%82%B9%E3%82%BF%E3%81%AE%E8%B5%B7%E5%8B%95">Dgraphクラスタの起動</a></h2> <p><a href="https://crieit.net/posts/Dgraph">無心でDgraph入門</a>を参照</p> <pre><code>docker run --name myq -v minq:/dgraph --rm -it -p 8080:8080 -p 9080:9080 -p 8000:8000 dgraph/standalone:v20.03.0 </code></pre> <p>コンテナの名前, ボリュームの名前, ポート番号などは適当に変えましょう.</p> <h2 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h2> <p>環境の構築は非常に簡単ですね.</p> <p><a href="https://crieit.net/posts/Next-js-Dgraph-3-API-next-connect">次回</a></p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://shinkufencer.hateblo.jp/entry/2018/11/22/233000">exec $SHELL -l でシェルが再読込されるしくみ</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16122 2020-10-11T16:01:46+09:00 2020-10-11T16:01:46+09:00 https://crieit.net/posts/Next-js-Dgraph-1 Next.jsとDgraphで作るクイズ・アプリ (1) アプリの構想 <h1 id="アプリの構想"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E6%A7%8B%E6%83%B3">アプリの構想</a></h1> <h2 id="どんなアプリか?"><a href="#%E3%81%A9%E3%82%93%E3%81%AA%E3%82%A2%E3%83%97%E3%83%AA%E3%81%8B%3F">どんなアプリか?</a></h2> <p>クイズ・アプリなのでクイズができるアプリになる予定ですが, どちらかというとクイズベースの学習システムと考えています. クイズという名前も通りが良いから使ってるだけで実際は問です. ここで問とは答えがあるものを指します.</p> <p>基本的な機能としては,</p> <ul> <li>クイズ・データベース</li> <li>テスト</li> <li>成績管理</li> <li>スキル・マップ</li> <li>レコメンド</li> <li>サーティフィケート</li> </ul> <p>ができたら良いなぁと思っています. レコメンドやらサーティフィケートとかは現時点では構想というよりは誇大妄想的な感じもします(金力もないですしね). 能力の可視化ができたらとりあえずは完成でしょうか.</p> <h2 id="何故クイズ?"><a href="#%E4%BD%95%E6%95%85%E3%82%AF%E3%82%A4%E3%82%BA%3F">何故クイズ?</a></h2> <p>なんかの本でクイズ(練習問題)が効率的な学習法だと読んだからです. 本は忘れましたが多分以下のどれかの本です.</p> <p><a target="_blank" rel="nofollow noopener" href="http://www.eijipress.co.jp/book/book.php?epcode=2258">『Learn Better』</a><br /> <a target="_blank" rel="nofollow noopener" href="https://www.diamond.co.jp/book/9784478021835.html">『脳が認める勉強法』</a><br /> <a target="_blank" rel="nofollow noopener" href="https://d21.co.jp/book/detail/978-4-7993-1685-6">『「学力」の経済学 』</a></p> <h2 id="技術スタック"><a href="#%E6%8A%80%E8%A1%93%E3%82%B9%E3%82%BF%E3%83%83%E3%82%AF">技術スタック</a></h2> <p>上記のことをクリアするには, いろんな技術が必要になります. 当面はクイズ風のブログという感じで公開して行く予定ですが以下のような技術(というかライブラリやらフレームワークやらSaaS)を使う予定です.</p> <ul> <li>Next.js</li> <li>Netlify</li> <li>Semantic UI React</li> <li>Dgraph</li> </ul> <p>他にも以下を検討中です.</p> <ul> <li>モバイル (Android)</li> <li>SaaS</li> </ul> <h3 id="選定理由・用途"><a href="#%E9%81%B8%E5%AE%9A%E7%90%86%E7%94%B1%E3%83%BB%E7%94%A8%E9%80%94">選定理由・用途</a></h3> <h4 id="Next.js &amp; Jamstack"><a href="#Next.js+%26amp%3B+Jamstack">Next.js & Jamstack</a></h4> <p>Next.jsはクイズ用のエディタの作成やクイズの公開に使いたいと思います.</p> <p>Next.jsはSPA(Single Page Application)からSSR(Server Side Rendering)そしてJamstackまでカバーしたWebアプリケーション・フレームワークです. Jamstackというのはwebサイトやwebアプリケーション開発のアーキテクチャの一つです.</p> <blockquote> <p>Jamstack is an architecture designed to make the web faster, more secure, and easier to scale.</p> </blockquote> <p>もともとはJavaScript, APIs, Markupから作られた造語です. フロントは静的サイト・ジェネレータ(Markup)で静的なファイル(HTML)として作ってCDNで配布, 動的な部分はJavaScriptとAPIsを使って更新するというような考え方です. ある意味Ajaxへの回帰と言えるでしょうか.</p> <p><img src="https://d33wubrfki0l68.cloudfront.net/b7d16f7f3654fb8572360301e60d76df254a323e/385ec/img/svg/architecture.svg" alt="image" /></p> <p>Next.jsのポイントはページをReactアプリケーションとして構築できるところです. React自体はUIライブラリという位置づけだったのでWebアプリケーション開発となると面倒なことも多かったです. Reactやって, クライアントサイド・ルーティング勉強して, 別個にAPIサーバーを立ててなどと考える必要はなく, 一つのフレームワークで完結します.</p> <p>Next.jsならファイル・パスとルートが対応したり, API(バックエンド)も作れたりと考えることが少なくて良いです. 過去この点で何度も失敗しているのでNext.jsを触ったときは感動しました.</p> <p>またNext.jsを作っているVercelのホスティング・サービスを使えばサイトのデプロイや配信なども簡単にできるので技術ブログなんかにはちょうど良いのかもしれません.</p> <p>個人的には簡易なサーバーからフロントエンドまで完結する点を評価してエディタの開発に採用しました(これだけ流行っているフレームワークなので小賢しい後付に近いですが).</p> <h4 id="Netlify"><a href="#Netlify">Netlify</a></h4> <p>Netlifyの何かが良かったのですが忘れてしまいました.</p> <h4 id="Semantic UI React"><a href="#Semantic+UI+React">Semantic UI React</a></h4> <p>手頃のUIコンポーネントが欲しかったので適当に選びました.</p> <h4 id="Dgraph"><a href="#Dgraph">Dgraph</a></h4> <p>クイズ間の関係性/依存性が記述できるようなデータベースが良いと思って探していてちょうど見つけたのがDgraphでした. とりあえず静的ファイルのデータソースとして使う文には問題なさそうです. クラウド・サービスもあるようなのでちょっと気になるところです.</p> <h4 id="モバイル (Android)"><a href="#%E3%83%A2%E3%83%90%E3%82%A4%E3%83%AB+%28Android%29">モバイル (Android)</a></h4> <p>当然スマフォで空き時間に勉強できたら便利です. モバイルならとりあえずは成績の管理とかはスマフォ側のストレージでできそうです. Google Driveとかに保存しても良いかもしれません.</p> <p>モバイル開発ではトレンド的にはFlutterなどが注目を集めています. ReactやってるならReact-nativeとかでもいいし, 用途的にはUnityとかでも良いのかもしれません. Android版に限定する予定なので, ネイティブとしても良いのかもしれません. 作ることだけが決めていますがよく分かりません.</p> <h4 id="GS2"><a href="#GS2">GS2</a></h4> <p>ゲーム用のSaaSです. Playfabとかもあるようです. GS2は日本製で条件付きで売上1000万まで無料で使えるので安心して使えるかなとGS2に傾きつつある昨今です.</p> <h2 id="Reference"><a href="#Reference">Reference</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/">Next.js</a><br /> <a target="_blank" rel="nofollow noopener" href="https://dgraph.io/">Dgraph</a><br /> <a target="_blank" rel="nofollow noopener" href="https://jamstack.org/">Jamstack</a><br /> <a target="_blank" rel="nofollow noopener" href="https://reactnative.dev/">React Native</a><br /> <a target="_blank" rel="nofollow noopener" href="https://flutter.dev/?gclid=Cj0KCQjwt4X8BRCPARIsABmcnOry5Uy1LKLGR43-ZnRuX60vTy5JMG6NFhkF7Z0wPhXeS-5tpmO48wUaAjssEALw_wcB&gclsrc=aw.ds">Flutter</a><br /> <a target="_blank" rel="nofollow noopener" href="https://unity.com/">Unity</a></p> ブレイン tag:crieit.net,2005:PublicArticle/16121 2020-10-10T23:46:11+09:00 2020-10-11T07:07:33+09:00 https://crieit.net/posts/Vercel-API-Routes-OGP-2020 VercelにてNext.jsのAPI RoutesでOGPを作成する 2020年版 <p>VercelにてNext.jsのAPI Routesでファイルが読み込めるようになっていました! 元々、API Routesでは別のファイルの読み込みができませんでした。そのため下記の記事のように別途node builderを利用していましたが、もうそれも必要なくなり、開発もデプロイもシンプルなAPI Routesとして実行できるようになっているようで、非常に簡単になりました。</p> <p><a href="https://crieit.net/posts/Vercel-Zeit-Now-Next-js-node-canvas-OGP">Vercel(元ZeitのNow)にてNext.jsでnode-canvasを使ってOGP</a></p> <h2 id="どういうことか"><a href="#%E3%81%A9%E3%81%86%E3%81%84%E3%81%86%E3%81%93%E3%81%A8%E3%81%8B">どういうことか</a></h2> <p>このissueで話が進んでおり、このコメントが解決方法になっています。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/vercel/next.js/issues/8251#issuecomment-657770901">Next.js API routes (and pages) should support reading files · Issue #8251 · vercel/next.js</a></p> <p>つまり、</p> <blockquote> <p>path.resolve('./public/ts-data.csv')</p> </blockquote> <p>というパス指定でファイルが読み込めるようになっています! めでたい。</p> <h2 id="具体例"><a href="#%E5%85%B7%E4%BD%93%E4%BE%8B">具体例</a></h2> <p>具体的にはこういうコードでVercelでも正常に動作しました。フォントを読み込んでのテキスト描画も、画像の描画も、正常にできています。</p> <pre><code class="typescript">import { NextApiRequest, NextApiResponse } from 'next' import * as path from 'path' const { createCanvas, registerFont, loadImage } = require('canvas') export default async (req: NextApiRequest, res: NextApiResponse) => { registerFont(path.resolve('./fonts/ipagp.ttf'), { family: 'ipagp', }) const width = 600 const height = 315 const canvas = createCanvas(width, height) const context = canvas.getContext('2d') context.fillStyle = '#fafafa' context.fillRect(0, 0, width, height) context.font = '15px ipagp' context.fillStyle = '#424242' context.textAlign = 'center' context.textBaseline = 'middle' context.fillText('あいうえお', 100, 50) const test = await loadImage(path.resolve('./images/test.png')) context.drawImage(test, 300, 0, 70, 70) const buffer = canvas.toBuffer() res.writeHead(200, { 'Cache-Control': 'public, max-age=315360000, s_maxage=315360000', Expires: new Date(Date.now() + 315360000000).toUTCString(), 'Content-Type': 'image/png', 'Content-Length': buffer.length, }) res.end(buffer, 'binary') } </code></pre> <p>下記がVercel上で描画した画像です。</p> <p><a href="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5f81c82e431f5.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/597a7f2e06da1377ad7cbc486bd0f4cf5f81c82e431f5.png?mw=700" alt="" /></a></p> <p>ちなみにnode-canvasでのOGPの作成はハマるのでそれは別途下記参照です。</p> <p><a href="https://crieit.net/posts/Vercel-Zeit-Now-Next-js-API-Routes-node-canvas">Vercel(元ZeitのNow)にてNext.jsのAPI Routesでnode-canvasを使う</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16063 2020-09-20T00:57:20+09:00 2020-09-20T11:34:07+09:00 https://crieit.net/posts/9607755c4800bec007d1a912ce8f42dc 草野球のライブ配信を補助するツールを作った話 <h1 id="草野球放送局"><a href="#%E8%8D%89%E9%87%8E%E7%90%83%E6%94%BE%E9%80%81%E5%B1%80">草野球放送局</a></h1> <p><a target="_blank" rel="nofollow noopener" href="https://baseball-broadcast.vercel.app/">https://baseball-broadcast.vercel.app/</a></p> <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f66295a77955.jpg?mw=700" alt="" /></a></p> <h1 id="仕組み"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF">仕組み</a></h1> <p>「ツイキャスゲームズ」などのスクリーン配信に対応したサービスで、<br /> スマホの画面を直接ストリーミング配信できることを利用して、<br /> スマホ側でカメラを起動して画面を合成したらいいじゃないかという発想に。<br /> react-webcamというライブラリ(canvasで描画している)とNextJSを組み合わせました。</p> <h1 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h1> <div class="iframe-wrapper"><iframe width="560" height="315" src="https://www.youtube.com/embed/EjPa1nqVwAk" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe></div> <h1 id="使った技術"><a href="#%E4%BD%BF%E3%81%A3%E3%81%9F%E6%8A%80%E8%A1%93">使った技術</a></h1> <h2 id="NextJS"><a href="#NextJS">NextJS</a></h2> <p>最低でもQA画面と設定画面、合成画面が必要になると思っていたので、<br /> NextJSを選択しました。</p> <h2 id="Vercel"><a href="#Vercel">Vercel</a></h2> <p>デプロイ用のプラットフォーム</p> <h2 id="react-webcam"><a href="#react-webcam">react-webcam</a></h2> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/mozmorris/react-webcam">react-webcam</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/ozota/items/0fc56f600955c6380dde">react(nativeでない)でカメラを使ってみる</a></li> </ul> <h1 id="苦労したこと"><a href="#%E8%8B%A6%E5%8A%B4%E3%81%97%E3%81%9F%E3%81%93%E3%81%A8">苦労したこと</a></h1> <h2 id="カメラのデバッグ"><a href="#%E3%82%AB%E3%83%A1%E3%83%A9%E3%81%AE%E3%83%87%E3%83%90%E3%83%83%E3%82%B0">カメラのデバッグ</a></h2> <p>セキュリティ的にカメラの起動条件がlocalhostか、https通信下だったので、最初はvercelに上げながら動作確認していましたが、途中からcodesandboxを使っていました。</p> <p><strong>iPhone/Chromeではカメラが起動しない</strong>とか。</p> <h2 id="スマホの向きを変えたときの描画"><a href="#%E3%82%B9%E3%83%9E%E3%83%9B%E3%81%AE%E5%90%91%E3%81%8D%E3%82%92%E5%A4%89%E3%81%88%E3%81%9F%E3%81%A8%E3%81%8D%E3%81%AE%E6%8F%8F%E7%94%BB">スマホの向きを変えたときの描画</a></h2> <p>縦横の向きを変えたときにリサイズする処理を色々試したのですが、</p> <ul> <li>addEventListener('resize')</li> <li>再描画回避のためにuseRefを使う</li> <li>縦横のサイズを入れ替える</li> </ul> <p>どれも結局上手くいかず、stackoverflowなどを見ながら最終的にたどり着いたのはCSSでの表示制御でした。えぇぇぇぇぇ.....。</p> <h2 id="UI詰め過ぎ問題"><a href="#UI%E8%A9%B0%E3%82%81%E9%81%8E%E3%81%8E%E5%95%8F%E9%A1%8C">UI詰め過ぎ問題</a></h2> <p>スコアボードのUIを詰め過ぎてタップしづらいという問題が発生。<br /> <code>transform: scale(1.3)</code>でコンポーネントごと1.3倍にして逃げました。<br /> <a href="https://crieit.net/boards/web1week-202005/WEB-UI#パーツ%28コンポーネント%29の組み合わせ">秘伝のたれを思いついた瞬間</a></p> <pre><code class="javascript">import Webcam from "react-webcam"; import NHKBoard from "./NHKBoard"; const Home = () => { const videoConstraints = { facingMode: { exact: "environment" }, aspectRatio: 1.78 }; return ( <div style=<span>{</span><span>{</span> width:'95vw', height:'95vh', position:'relative' <span>}</span><span>}</span> > <Webcam style=<span>{</span><span>{</span> position:'absolute', top:0, left:0, width:'95vw', height:'95vh', <span>}</span><span>}</span> videoConstraints={videoConstraints} /> <div style=<span>{</span><span>{</span> transform:'scale(1.3)', position:'absolute', bottom:0, right:0 <span>}</span><span>}</span> > <NHKBoard/> </div> </div> ) } export default Home; </code></pre> <h1 id="IKEN"><a href="#IKEN">IKEN</a></h1> <p>スコアボードの種類を選べるようにするとか、選手紹介を表示するとか機能拡張は考えていますが、一旦最小構成でリリースしますのでフィードバックいただけるとうれしいです!</p> <p><a target="_blank" rel="nofollow noopener" href="https://ikens.net/ckoshien_tech/baseball-broadcast">https://ikens.net/ckoshien_tech/baseball-broadcast</a></p> ckoshien tag:crieit.net,2005:PublicArticle/16045 2020-09-01T23:08:37+09:00 2020-09-01T23:08:37+09:00 https://crieit.net/posts/A-Frame-VR-5f4e55e505ae0 A-FrameでVR選手ロッカーを作ってみた話 <p><a href="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f4e4eb3ed4c2.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/c757f121bd19245f1a30d36c3424b0625f4e4eb3ed4c2.jpg?mw=700" alt="" /></a></p> <h2 id="①NextJSでaframe-reactを動かす"><a href="#%E2%91%A0NextJS%E3%81%A7aframe-react%E3%82%92%E5%8B%95%E3%81%8B%E3%81%99">①NextJSでaframe-reactを動かす</a></h2> <h3 id="aframe-react"><a href="#aframe-react">aframe-react</a></h3> <p>ReactJSでAFrameを動かすときはこれ。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/supermedium/aframe-react">aframe-react</a></p> <p>NextJSは一部SSR(サーバサイドレンダリング)が走るので、クライアントで動作する状態になってからロードする必要がある。<br /> 参考: <a target="_blank" rel="nofollow noopener" href="https://github.com/michaltakac/aframe-next-static/blob/master/pages/index.js">aframe-next-static</a></p> <p>windowというグローバル変数が使えるようになればクライアント側で動作する状態なので、レンダリング完了フラグで管理する。</p> <pre><code class="javascript">if (typeof window !== "undefined") { require("aframe"); setRendered(true); } </code></pre> <h2 id="②aframe-reactで書いてみる"><a href="#%E2%91%A1aframe-react%E3%81%A7%E6%9B%B8%E3%81%84%E3%81%A6%E3%81%BF%E3%82%8B">②aframe-reactで書いてみる</a></h2> <p>aframeは配置するものが多いと意外とソースが長くなるので、<br /> 1個のLockerを構成する単位でコンポーネント化しました。<br /> 選手の情報をAPIから取得して、奇数と偶数の場合で振り分けています。</p> <pre><code class="html">if (!rendered || !teamMembers) { return <></>; } return ( <Scene //device-orientation-permission-ui="enabled: true" > {teamMembers.map((member,idx)=>( <> {idx%2 === 0 ?<RightLocker member={member} x={3 - idx*0.35} y={0} z={1.7+idx*0.35} rot_x={0} rot_y={0} rot_z={0}/> :<LeftLocker member={member} x={-idx*0.35} y={0} z={idx*0.35} rot_x={0} rot_y={0} rot_z={0}/> } </> ))} <Entity primitive="a-sky" material="color: #555" /> <Entity camera look-controls wasd-controls position="3 1 2"/> </Scene> ); </code></pre> <p><code>a-sky</code>は背景、<code>camera</code>はカメラの初期位置を定義しています。</p> <p>これがRightLockerのソースです。<br /> プログラム的に難しいことはないのですが、<br /> 座標や光源の調整などが結構面倒でした。<br /> (..planeって光透過すんの?)</p> <pre><code class="javascript">import { Entity } from "aframe-react"; import { Player } from "../../model/typedef"; type diff={ x:number, y:number, z:number, rot_x:number, rot_y:number, rot_z:number, member:Player } const RightLocker:React.FC<diff> = ({x,y,z,rot_x,rot_y,rot_z,member}) => { return ( <> <Entity geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg",alphaTest:0.1 <span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-4.3<span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: -135+rot_y, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="2" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg", transparent:false<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="2" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: "/images/wood.jpg"<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x, y: y, z: z-4.3 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity geometry=<span>{</span><span>{</span> primitive: "box", width:0.5, height:0.3<span>}</span><span>}</span> material=<span>{</span><span>{</span> color:'gray' <span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.5, y: y-1, z: z-4.5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: rot_y-45, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-image" height="1.5" width="0.8" //geometry=<span>{</span><span>{</span> primitive: "plane", width:1, height:2<span>}</span><span>}</span> material=<span>{</span><span>{</span> src: member.ogp_image<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.7, y: y, z: z-4.3<span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x, y: -135+rot_y, z: rot_z <span>}</span><span>}</span> /> <Entity primitive="a-light" light=<span>{</span><span>{</span>type:"spot"<span>}</span><span>}</span> material=<span>{</span><span>{</span>color:'yellow'<span>}</span><span>}</span> position=<span>{</span><span>{</span> x: x+0.3, y: y+1.3, z: z-4.5 <span>}</span><span>}</span> rotation=<span>{</span><span>{</span> x: rot_x-90, y: rot_y-10, z: rot_z-10 <span>}</span><span>}</span> /> </> ); }; export default RightLocker; </code></pre> <h2 id="DEMO"><a href="#DEMO">DEMO</a></h2> <p>まだ本サイトにはデプロイしていませんが、<code>team/[チームID]/vr_locker</code>で各チームのVRロッカーを見ることができます。<br /> - <a target="_blank" rel="nofollow noopener" href="https://deploy-preview-19--cap-baseball-info.netlify.app/team/67/vr_locker">キャップ野球情報局・VRロッカー</a></p> ckoshien tag:crieit.net,2005:PublicArticle/16040 2020-08-30T22:58:17+09:00 2020-08-30T22:58:17+09:00 https://crieit.net/posts/GitHub-OAuth-App GitHub OAuth Appのトークンを取得失敗した <h1 id="access tokenはクライアント側から取得できない"><a href="#access+token%E3%81%AF%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E5%81%B4%E3%81%8B%E3%82%89%E5%8F%96%E5%BE%97%E3%81%A7%E3%81%8D%E3%81%AA%E3%81%84">access tokenはクライアント側から取得できない</a></h1> <h2 id="やりたかったこと"><a href="#%E3%82%84%E3%82%8A%E3%81%9F%E3%81%8B%E3%81%A3%E3%81%9F%E3%81%93%E3%81%A8">やりたかったこと</a></h2> <p>GitHubをJSONデータベース代わりに使いたかった. GitHub APIを使うとGitHubをREST APIから操作できるので, これにフロント付けたら簡易なデータベースを作れるのではと思ったわけです. これは結局Cross-Origin Request Blockedでできないことが分かりました.</p> <p>忘れちゃうのもあれなので概要をメモしておきます.</p> <h2 id="環境"><a href="#%E7%92%B0%E5%A2%83">環境</a></h2> <p>以下のパケージを利用しました.</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://nextjs.org/">Next.js</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://marmelab.com/react-admin/">react-admin</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/octokit/auth-oauth-app.js">octokit/auth-oauth-app.js</a></li> </ul> <p>またreact-adminで認可するロジックは以下を参考にしました.</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/marmelab/ra-example-oauth">ra-example-oauth</a></li> </ul> <h2 id="OAuth Applicationの作成"><a href="#OAuth+Application%E3%81%AE%E4%BD%9C%E6%88%90">OAuth Applicationの作成</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://docs.github.com/en/developers/apps/creating-an-oauth-app">Creating an OAuth App</a>を参考にしましょう.</p> <p>作成されたGitHub OAuth Appには固有の値であるclient idとclient secretが付与されるます.</p> <h2 id="Auth Provider"><a href="#Auth+Provider">Auth Provider</a></h2> <p>react-adminで認証や認可を行うにはauth providerにロジックを書きます.</p> <pre><code class="javascript">const authProvider = { login: params => Promise.resolve(), logout: params => Promise.resolve(), checkAuth: params => Promise.resolve(), checkError: error => Promise.resolve(), getPermissions: params => Promise.resolve(), }; </code></pre> <p>取得したトークンはローカルストレージに保存します. checkAuthではこのトークンがすでに設定されているかを確認し, 無ければログイン画面が表示されます.</p> <pre><code class="javascript">checkAuth: () => { const user_name = localStorage.getItem('username'); console.log(`checkAuth: ${user_name}`) return localStorage.getItem('username') ? Promise.resolve() : Promise.reject(); }, </code></pre> <p>ログイン画面では2つの処理を行います. まず認可ページで認可を行います. 認可後はAuthorization callback URLに指定したページに戻ってくるのですが, この際codeというURLパラメータが付与されます. これとclient id, client secretを使ってトークンを取得します. stateは任意ですが一応付けてます.</p> <pre><code class="javascript">import { createOAuthAppAuth } from '@octokit/auth-oauth-app'; const authorizeURL = new URL('oauth/authorize', process.env.login_url); authorizeURL.searchParams.append('client_id', process.env.client_id); authorizeURL.searchParams.append('scope', 'public_repo'); authorizeURL.searchParams.append('state', 'QAbgG6f9Rit2C5'); const auth = createOAuthAppAuth({ clientId: process.env.client_id, clientSecret: process.env.client_secret, }); login: async (params) => { const { searchParams } = new URL(window.location.href); if (!searchParams.has('code') || !searchParams.has('state')) { redirectToURL(authorizeURL) return Promise.resolve(); } const code = searchParams.get('code'); const state = searchParams.get('state'); const tokenAuthentication = await auth({ type: 'token', code: code, state: state, }); const { token } = tokenAuthentication; localStorage.setItem('username', token); return Promise.resolve(); }, </code></pre> <p>ところがauthでtokenを取得しようとすると失敗します. リバース・プロキシを設置してやるといけるようですが, 試してません.</p> <p><a target="_blank" rel="nofollow noopener" href="https://andreybleme.com/2018-02-24/oauth-github-web-flow-cors-problem/">OAuth Github web flow doesn't support CORS</a></p> <h2 id="補足"><a href="#%E8%A3%9C%E8%B6%B3">補足</a></h2> <p>Next.jsとreact-adminを使うとDocument is not definedというエラーが出ます. hisotryというパッケージが内部でDOM APIを利用しているのですがNext.jsは最初ローカル側(サーバー側)で動くのでこの時点ではDOMが存在しません. クライアント側かどうか調べる必要があります. getStaticPropsはロカールで実行され, ファイルなどを取得してpropsとしてページ・コンポーネントに渡すことができます. この処理が終了するとブラウザ側の処理に移ることを利用するとコンポーネントを切り替えることができるようです.</p> <p><a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/58987174/how-to-prevent-parent-component-from-re-rendering-with-react-next-js-ssr-two-p">How to prevent parent component from re-rendering with React (next.js) SSR two-pass rendering?</a></p> ブレイン