eslintとprettier
概要
- eslintとprettierをGitコミット時に実行する
環境
- node:14.14.0
- eslint:7.23.0
- eslint-config-prettier:8.1.0
- husky:6.0.0
- lint-staged:10.5.4
- prettier:2.2.1
設定ファイル
prettierrc.json
{ "singleQuote": true }
eslintrc.json
{ "env": { "es6": true }, "globals": { "alert": false, "document": false, "$": false, "console": false }, "extends": ["eslint:recommended", "prettier"] }
.husky/pre-commit
#!/bin/sh #!/bin/sh . "$(dirname "$0")/_/husky.sh" npm run lint-staged
package.json
"scripts": { "lint-staged": "lint-staged" }, "lint-staged": { "*.{js,ts,jsx,tsx}": [ "npx eslint . --fix", "npx prettier --write ." ] },
Nuxt.jsでPAY.JPの初期化エラー(既にインスタンス化されています)対策
概要
- Nuxt.jsベースでJamstackなECサイトのサンプル実装しているときに、PAY.JPの初期化でエラーが出ることがあったのでメモ
- PAY.JPを実装するにあたり、payjp(2.0.5)のnode moduleを利用
- 画面側では
payjp
のCardElement
を生成し、トークンを作成して決済用のAPI呼び出す流れで実装
エラーとなる実装
- PAY.JPを利用する画面の
mounted
でpayjp
のインスタンス生成していた
<template> <div id="payjp-form"></div> </template> <script> mounted() { const payjp = window.Payjp(process.env.PAY_JP_PK) const elements = this.payjp.elements() const cardElement = elements.create('card') cardElement.mount('#payjp-form') cardElement.on('change', async (event) => { if (event.complete) { const res = await this.$payjp.createToken(cardElement) this.payjpToken = res.id this.isValidCard = true } }) }, </script>
- 画面初期表示時は問題なくカードレイアウトが表示されていたが、画面遷移後、再び画面を開くと「Error: 既にインスタンス化されています」というエラーが発生。
複数回の初期化処理はNGとのこと。
対応策(Pluginを作成し初期化を1度に制御する)
- plugins/payjp.jsを新規作成
import Vue from 'vue'; export default ({store, isHMR}) => { if (isHMR) return if (process.client) { Vue.prototype.$payjp = window.Payjp(process.env.PAY_JP_PK) } }
- nuxt.config.jsにPlugin定義追加
plugins: [ '~/plugins/payjp.js', ],
- エラーとなっていた画面の修正
<template> <div id="payjp-form"></div> </template> <script> mounted() { const elements = this.$payjp.elements() const cardElement = elements.create('card') cardElement.mount('#payjp-form') cardElement.on('change', async (event) => { if (event.complete) { const res = await this.$payjp.createToken(cardElement) this.payjpToken = res.id this.isValidCard = true } }) }, </script>
Firestoreを使用するFirebase Cloud Functionsをファイル分割した時のエラー対応
概要
- Firebase Cloud functionsで、Firestoreを利用している
- 当初は、index.jsに関数定義していた
- 複数関数定義する必要があったので、関数ごとにファイル分割した
- 1回目の関数実行時は特に問題ないが、2回目以降の呼び出し時に以下のエラーが発生 (初期化は1回にしなさいとのこと)
The default Firebase app already exists. This means you called initializeApp() more than once without providing an app name as the second argument. In most cases you only need to call initializeApp() once. But if you do want to initialize multiple apps, pass a second argument to initializeApp() to give each app a unique name.
エラー発生時のコード
- index.js
const foo = require('./foo') const bar = require('./bar') exports.foo = foo.hoge exports.bar = bar.fuga
- foo.js
const functions = require('firebase-functions'); const admin = require('firebase-admin') exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => { admin.initializeApp() const fireStore = admin.firestore() // 以下、firestoreを使った処理 })
- bar.js
const functions = require('firebase-functions'); const admin = require('firebase-admin') exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => { admin.initializeApp() const fireStore = admin.firestore() // 以下、firestoreを使った処理 })
エラー解決用に修正したコード
- index.js(変更なし)
const foo = require('./foo') const bar = require('./bar') exports.foo = foo.hoge exports.bar = bar.fuga
- firestore.js(新規追加)
const admin = require('firebase-admin'); admin.initializeApp(); const fireStore = admin.firestore(); module.exports = { fireStore, };
- foo.js(変更)
const functions = require('firebase-functions'); const { fireStore } = require('./firestore') exports.hoge = functions.region('asia-northeast1').https.onCall(async (data, context) => { // 以下、firestoreを使った処理 })
- bar.js(変更)
const functions = require('firebase-functions'); const { fireStore } = require('./firestore') exports.fuga = functions.region('asia-northeast1').https.onCall(async (data, context) => { // 以下、firestoreを使った処理 })
JamstackなECサイトの考察
概要
- Jamstack Advent Calendar 2020 - Qiita の22日目の記事です。
- 株式会社トライビート| TRIBEAT CO., LTD. という会社でBtoCや業務システムのWebアプリケーション構築や運用・保守をしています。
- これまでJamstackという言葉が出てくる前から、社内独自のSSGの仕組みで試行錯誤して対応することがありました。
- Jamstack関係の環境やサービスが充実してくる中で、ECサイト構築が現実的に良い選択になりそうかを実際に試してみたので、考察やハマったポイントを備忘録としてまとめる。(まだ検証中)
アーキテクチャの構成
- microCMS
- Firebase hosting
- Firestore
- Firebase functions
- Firebase Authentication
- Nuxt.js
- Algolia
- Stripe
- GitHub Actions
※Next.jsとVercelへの移行も試したいが、データ層の設計がまとまったタイミングで検討する。
画面構成
ECサイト
- 商品一覧
- サイトのtopページ
- algoliaから商品一覧を取得して表示
- 検索を可能にするため、商品一覧情報を組み込んだ静的ページ化はせずSPAとする
- 商品をクリックすることで、商品詳細画面へ遷移
- 商品詳細画面
- SSGで静的ファイル化したページ
- 数量変更とカート追加が可能
- カート追加ボタンをクリックすることで、カート画面へ遷移
- カート画面
- アカウント画面(未実装)
- いつでも作成できるので、後回しにして進めていた画面
- 取り急ぎは、Firebase Authentication上でユーザー作成し、簡易画面からログイン処理とログアウト処理を実装
- アカウント情報の作成や更新できるようにする
- ログイン、ログアウトの組み込み
- 認証はあくまでも便利機能として考え、未認証でも購入できる想定
管理者サイト
- プレビュー画面
- 在庫管理画面
- 商品データに在庫数を付与したリスト画面
- 商品データの一覧はSSGビルドで静的ファイルとして作成
- 在庫数のみ、描画時にFirestroeから取得して当て込む
- 在庫数の変更が可能(Firestoreを更新する)
- 販売管理画面
- 販売データ(日付、合計金額)とステータスを表示したリスト画面
- FireStoreから動的にデータを取得するので、静的化せずSPAとする
- 詳細ボタンをクリックすると、購入者情報や購入商品一覧をオーバーレイで表示
Functions(API)
- 決済用関数
- 在庫チェックと金額改ざんチェック後、StripeのAPI呼び出し
- 在庫数変更(Firestore)
- 販売情報の登録(Firestore)
現状の簡易フロー図
実装時の考察
ECサイトの機能について
- 当初、商品一覧画面はページング付きですべてSSGしたもので進めていたが、やはり検索したいということからAlgoliaを利用したSPAとした
- ログイン情報の状態管理はVuexとlocalstorageを同期させた管理方法で実装(詳細は下に別途書く)
- ブラウザから直接Firestoreへwriteすることはやめ、Cloud Functionsを間に挟むように実装(ReadはOK)
- カートの状態管理も認証と同様にVuexで管理
- カート画面から購入時の在庫チェックは、その後のCloud Functions内でもするが一応フロント側でも見るようにした(要検討)
管理者サイトの機能について
- リアルタイム性が必要になるデータを扱うことが多く利用ユーザも限定されるため、SSGに意固地になりすぎずSPAでの設計も積極的に検討する
- Firestoreへのwrite系処理もCloud Functionsが間に挟めなくて良しとする(要検討)
- プレビュー画面ではmicroCMSのAPI_KEYがソースから見えるが良しとする(要検討)
データ層について
- ここが一番迷っていて検討要素が多い(Stripeでどこまでデータ管理すべきか見えていない)
- 在庫データ、商品データ、販売データをFirestoreで管理して作成を進めた(要検討)
- 在庫データと販売データはStripe側で持たせるほうがいいのか?
- 商品データはStripeに連携してそちらでも管理する方がいいのか?
- 商品データはmicroCMSからAPIで取得もでき2重管理になるがFirestoreにも入れた(要検討)
- 決済時のCloud Functionsで金額チェックをしたい 具体的には、商品マスタ情報の金額と画面から渡された数量が、画面から渡された合計金額に一致するかチェックしたい
- 上記チェック時の金額判断するためには、microCMSから更新された商品データの履歴が必要になるため、履歴管理をどこかで持たせたい
- 商品データもStripeでもたせる方がいいのか?
- 顧客データ(要検討)
- 認証周りは簡易実装しかしていないので、現状はFirebase Authenticationに固定ユーザー作って試している ユーザー情報が登録されたときに、StripeとFirestoreどちらで管理すべきか?
SSG(ビルド)について
- 各商品ページをSSG
- 上記で、microCMSのAPIから商品データ一式取得するので、AlgoliaとFirestoreへ必要なデータ更新するように実装
- Firestoreへの更新をトリガーにしたCloud Functionsを用意してAlgoliaのデータと同期するようにした方がいいか迷っている。
- microCMSから商品データ更新(追加・削除)された場合に、Firestoreのデータを全て変更する必要はないので、microCMSで設定するカスタムのWebhookでやってもいいものか迷っている(詳細は下に別途書く)
ハマったポイントと解決案
1. GitHub Actionsでデプロイ時にfirebaseコマンドがないと言われた
背景
GitHub ActionsのFirebaseデプロイでは、よく以下を利用したサンプルを見かけるが、まずは理解のためにも自分でfirebaseコマンドを叩くように書いてみた
デプロイ用のymlでは以下のように、stripeのシークレットKEYを設定し、デプロイコマンドを叩いていた
- name: deploy to Firebase Hosting run: | firebase functions:config:set stripe.secretkey=${{secrets.STRIPE_SECRET_KEY}} firebase deploy env: FIREBASE_TOKEN: ${{secrets.FIREBASE_TOKEN}}
- GitHub Actions内のデプロイ処理で、firebase command not foundエラーが発生していた
解決案
- デプロイの前処理でビルド処理が実行されるので、そこの npm install 時にfirebase-toolsを、-gでインストール
npm install -g firebase-tools
2. ある日突然、firebaseのデプロイがコケるようになった
背景
- 12/16になったらデプロイがコケるようになった。
- GitHub Actionsのエラー内容は以下
Error: An unexpected error has occurred. Error: Process completed with exit code 2.
解決案
- firebase-toolsのバージョンチェック
- プロジェクト生成時は8系で進めていたが、12/16に9系に上がっていた!
- とりあえずバージョンを8系にするように変更して正常動作することを確認できた
npm install -g firebase-tools@^8.0.0
- 9系になったことで設定ファイル系のフォーマットなど変わったのかも、まだ詳細調査はできていない。後日する予定。
3. Firebase Auth使った認証状態の保持
背景
- 認証自体はFirebase Authenticationを利用して簡易実装していたが、どう状態保持するか悩んだ
- 最初、middlewareにauth処理を実装してvuexで状態保持を検討した
- npm run devで確認しているとうまく動くように見えるが、npm run generate & npm run startだとうまくいかない
- middlewareはF5(page refresh)時には呼ばれないため、Jamstack構成では使えないと判断
- 次に、共通のヘッダコンポーネントを用意していたので、そこのbeforeCreateでfirebaseの認証チェックするようにした
- 商品ページの切り替え時に、その都度認証確認リクエストが飛ぶことにモヤモヤ
- 今後ページが増えていく中で、ヘッダが全て共通になるのかモヤモヤ
- カートの状態も何かしらの方法で保持しなくてはいけないことに気づき、使えないと判断
解決案
- 最終的には、以下のpluginを導入しlocalstorage管理することで対応
4. GitHub Actionsのパフォーマンス検討
背景
- GitHub Actionsの処理全体で約4〜5分掛かっている
- 内訳としては、ビルド(npm install含む)とfirebaseのデプロイでそれぞれ2分くらい
- ビルド処理は常に1からインストールしているので、対策ないか調査
解決案
- 探すとすぐに以下が見つかった。
- ただ、使ってみたけど特に大きくパフォーマンスが改善されることはなく、ここについては後日調査
5. nuxt run generate 時にFirestoreへデータ入れようとして発生したワーニング
背景
- build処理時、nuxt.config.jsのgenerateで以下のような感じでFirestoreにデータ更新していた
generate: { async routes() { const { data } = await axios.get( `https://xxx.microcms.io/api/v1/items?limit=100`, { headers: { 'X-API-KEY': API_KEY } } ) const fsStocks = firestore.collection('items') const batch = firestore.batch() const pages = [] data.contents.forEach((content) => { pages.push({ route: `/${content.id}`, payload: content }) const nycRef = fsStocks.doc(content.id) batch.set(nycRef, content) }) await batch.commit() return pages } },
- npm run generate すると一応SSGは正常完了するものの、以下のようなワーニングが表示される
⚠ Nuxt Warning The command 'nuxt generate' finished but did not exit after 5s This is most likely not caused by a bug in Nuxt Make sure to cleanup all timers and listeners you or your plugins/modules start. Nuxt will now force exit DeprecationWarning: Starting with Nuxt version 3 this will be a fatal error
解決案
- generate完了時に明示的にFirestoreを終了させてやる必要がある
- nuxt.config.jsのhooksに以下を追記
hooks: { generate: { done(builder) { firestore.terminate().then() } } },
6. Firestoreへデータ登録かmicroCMSのWebhookで迷った
背景
- ビルド時にmicroCMSから全データ取得しているので、その流れでFirestoreも一律全データ更新するようにしていた
- microCMSで商品データ更新(追加、削除)された場合に、全データ更新ではなく、更新データのみFirestoreに反映した方が効率よいと思った
- microCMSではWebhookにカスタム通知が設定できるので、それを利用しCloud Functionsを呼び出せば、1件ごとのFirestore更新ができるのではないかと思った
解決案
- 以下のようなCloud Functionsを用意し、カスタム通知にURL設定することでWebhook連携の接続までは簡単にできることを確認
exports.updateCms = functions.region('asia-northeast1').https.onCall(async (data, context) => { functions.logger.log("called updateCms.") }) exports.updateCms2 = functions.region('asia-northeast1').https.onRequest((request, response) => { functions.logger.log("called updateCms2.") })
- 関数内でリクエストパラメータを取得しFirestore更新まではいけるものの、セキュリティが心配に。
- microCMSのカスタム通知で渡せるパラメータは決まっているため、認証用トークンなどは含めれないはず?
- カスタム通知に設定するURLにクエリストリングを付与して、それをトークンとして扱うことも検討したが、一旦はそのままビルド時に全データ更新で落ち着いた。
結論
PHPのログライブラリ(monolog)のPull Requestがマージされた
2019年に上記ブログで書いてたとおり、monologを使った日付のログローテート処理でうまくいかないことがありPRを投げていた。
PR作成してから2週間程は気にして見てたけど、特に音沙汰なかったため、自分の拙い英語ではやはり相手にされないのかと勝手に納得していた。自分の関わっているプロジェクトでは必要な修正だったためライブラリのクラスをoverrideして運用していた。
月日は経ち、プロジェクトで使っているフレームワークのバージョンを上げるタイミングがやってきた。それに伴いライブラリ等のバージョンも上げることになった。
バージョンを上げるための調査は僕とは別の人が進めいて、今朝、その人から僕のPRがマージされてると教えてもらった。寝耳に水で、最初何のことかもわからないくらいだった。
確認してみるとPRから3ヶ月後くらいにマージされていて、当たり前だがメール通知もちゃんと届いていた。
PRは普段から業務でPrivateなリポジトリに対して作成しているが、一般的によく使われているオープンソースに対しては初めてだったのでなんだかとても嬉しかった。
今後も続けていきたい。
いつも昼はカップラーメンや柿ピーで済ませてしまうけど、今日は自分へのご褒美にチキンカツのサンドイッチを食べた。
Google Cloud Platform(Cloud Functions, Cloud PubSub, Cloud Storage, Speech-to-Text, Secret Manager)を使ってPodcast音声データの文字起こし
概要
- 2020年のコロナ時期から社内コミュニケーションを活性化させたく、社内環境でPodcast配信を開始
- Podcastの収録はzoomで録音し、編集などはせずに社内のGoogleサイトへアップする流れ(継続させることに重き置いて、極力手間を掛けないことを意識している)
- 収録はいつも1〜1.5hほどになっている
- 社内から聞く前にある程度話の内容などがわかると嬉しいという意見をもらったので、Google Cloud APIのSpeech-to-Textを利用して文字起こしを試してみた
ゴール
- 当初は、自分のローカルマシンでやっていたが色々運用がめんどくさくなってきたのと、GCPのサービス連携を試してみたかったので、Storageに音声ファイル(wav)をアップロードしたら自動で文字起こしをしてくれるものをゴールにした。
- 今回、Secret ManagerやPubSubは利用しなくても構築可能だったが、色々な連携を試してみたかったので利用することに決めた。(サービスの最適化よりかは、色々な素振りをするために利用サービスを検討した。)
- 言語はNode.jsで書く
GCPのサービス関連図と処理内容
- wavファイルをローカルPCからCloud Storageへアップロード(zoom収録時はm4a形式になっているので、それをwavに変換するところまではローカルで実施)
- アップロード用のCloud Storageにファイル配置(上書き)されたことを契機にCloud Functions実行。
- 関数内ではシークレットマネージャーからCloud Storageアクセス情報(Bucket名)を取得(特に不要そうではあるが、試しに使ってみたかった)
- Speech-to-Text APIを利用して、音声(wav)データから話している内容を取得(結構、処理に時間が掛かる)
- アップロードとは別のCloud Storageへjsonファイルを書き出し
- Cloud PubSubにjsonファイル名をメッセージとしてパブリッシュ
- メッセージのパブリッシュを契機にCloud Functions実行
- 関数内ではシークレットマネージャーからCloud Storageアクセス情報(Bucket名)を取得(特に不要そうではあるが、試しに使ってみたかった)
- Cloud Storageからjsonデータを取得
- 取得したjsonデータを解析し、時間ごとの会話内容として整形したファイルを別なCloud Storageへ書き出し
ハマったポイント
- Cloud FunctionsからStorageやシークレットマネージャーにアクセスしたときに権限エラーが発生。
単純にStorageやシークレットマネージャー側で権限を絞っていたので、適宜権限を変更することで解決した - Speech-to-Text APIの利用でタイムアウトエラー
Cloud Functionsのタイムアウト最大値が540秒なため、タイムアウトや使用メモリサイズにMAX値を設定していたが540秒以内に完了せずタイムアウトエラーとなっていた。
当初、Storageへのファイル配置をトリガーとして呼ばれる関数には、asyncをつけて同期的な処理実行をするようにしていたが、それをやめ非同期処理させるようにして解決。
今後の課題
- デプロイ方法
gcloudコマンドを利用してローカルからデプロイしているが、GitHubと連携させたい - エラーハンドリング
音声ファイルのテキスト化部分は非同期処理としただけなので、エラー時の処理を検討したい - GoogleのSpeech-to-Text APIの精度
雑な日本語で話しているせいかもしれないが、結構話している内容とテキストで差が生じる。
マイクやレコーディング後の編集などで音質を上げることで、精度向上につながるものなのか時間があれば試してみたい。
GitHub
※helloWorldのソースは、Cloud Functionsの関数内でsetTimeoutした場合に、大元の関数処理自体は完了しても非同期処理される関数はその後実行されることを確認するためのもの。