Next.jsとFirebaseでLIFFモバイルオーダーアプリを作る
ベース
Firebaseってどんなの?
Realtime Database
データベース
Google Cloud Functions for Firebase
ファンクション
この二つさえあればよさそう
ベースのバックエンドはAWSを使っていてdockerでデプロイしているがFirebaseではどうやってやるのか?
FirebaseはAll in one ??
Firebaseを使うと認証が楽になるかも
全部トークンだけで済む
参考↓
Firebaseのベンダーロックインは問題かも
→でもfirebase alternative のSupabaseがあるぜ!!
意外と簡単に行けるんちゃう?(純東京人)
dockerいらないし
スキーマレスだから小さいDB立てるにはもってこい
行くぜ
ga
できた
realtime database vs Cloud Firestore
そんなに規模が大きくなくて、変更がないなら後発のCloud Firestore
がいいっぽい
firestoreの優位性
- 参照するデータの範囲をコントロールしやすい
- 複雑なクエリを使ったデータの検索がかけられる
DB TableOrderItemList
とPaymentOrderInfo
の二つだけだしCloud Firestore
でいいかな~
asia-east2
ロケーション選択があるけど、具体的に地名が書いてないからどこが近いかわからん 多分
はずれ~
asia-northeast1(東京)
asia-northeast2(大阪)が近かった
Tier 1 の料金設定って何ぞや
Tier 1 が高いと思ったらTier 1 が安いんじゃ
東京にするぜ!
作れたけどコレクションとかいう初見の単語出てきた、スキーマのことかな?
開発環境作るために、とりあえずベースのリポジトリクローンします
git clone https://github.com/line/line-api-use-case-table-order.git
cd line-api-use-case-table-order
backendディレクトリはいらないかも
sdkインストール
yarn add firebase
余ってline-use-caseってvue.jsじゃんvue.js
ビルド遅くてtemplateトjs別で書かなきゃいけないからなんだよね(vueファンの方ごめんなさい)
なので最初から作っちゃいます!
delete
yarn create next-app --typescript
みんな大好きnextjs
とtypescript
yarn add tailwind sass postcss autoprefixer firebase
tailwind
とsass
とfirebase
追加!
tsconfigもあるし、eslintもあるしNext.js
神
とりあえずページとコンポーネントだけ作ってrfce
スぺニットで型だけ作った
アプリ追加してなかった
firebase初期化
import { initializeApp } from "firebase/app";
import { getAnalytics } from "firebase/analytics";
// TODO: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
apiKey: "API_KEY",
authDomain: "PROJECT_ID.firebaseapp.com",
// The value of `databaseURL` depends on the location of the database
databaseURL: "https://DATABASE_NAME.firebaseio.com",
projectId: "PROJECT_ID",
storageBucket: "PROJECT_ID.appspot.com",
messagingSenderId: "SENDER_ID",
appId: "APP_ID",
// For Firebase JavaScript SDK v7.20.0 and later, `measurementId` is an optional field
measurementId: "G-MEASUREMENT_ID",
};
const app = initializeApp(firebaseConfig);
DATABASE_NAMEがよくわからんが取り合えずなくても大丈夫そう
8/10 23時とりあえず今日は眠いからここまで
8/11 9:51スタート
寝坊しました。
アプリで Firebase にアクセスする
import { initializeApp } from "firebase/app";
import { getFirestore, collection, getDocs } from "firebase/firestore/lite";
// TODO: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
apiKey: process.env.FIREBASE_API,
authDomain: `${process.env.PROJECT_ID}.firebaseapp.com`,
databaseURL: `DATABASE_NAME{process.env.REGION}.firebaseio.com`,
projectId: process.env.PROJECT_ID,
storageBucket: `${process.env.PROJECT_ID}.appspot.com`,
messagingSenderId: process.env.MESSAGEING_SENDER_ID,
appId: process.env.APP_ID,
measurementId: process.env.G_MEASUREMENT_ID,
};
const app = initializeApp(firebaseConfig);
const db = getFirestore(app)
コレクションを定義しようとおもったけど、データ追加するだけでいいっぽい
ベースの商品情報を配列にしてjsファイルとして作成
export const tableOerderItems = [
{
categoryId: 0,
categoryName: "オススメ",
items: [
{
categoryId: 0,
categoryName: "オススメ",
discountRate: 20,
discountWay: 1,
imageUrl:
"https://media.istockphoto.com/photos/french-fries-on-white-background-picture-id604378894?s=2048x2048",
itemDespription: "国産じゃがいも使用。ケチャップでどうぞ。",
itemId: 1,
itemName: "ポテトフライ",
orderNo: 1,
price: 190,
stockoutFlg: false,
},
{
categoryId: 0,
categoryName: "オススメ",
discountRate: 10,
discountWay: 2,
imageUrl:
"https://media.istockphoto.com/photos/japanese-fried-chicken-picture-id1150109573?s=2048x2048",
itemDespription: "サクサク、ジューシーな味をお楽しみください。",
itemId: 2,
itemName: "特製唐揚げ",
orderNo: 1,
price: 500,
stockoutFlg: false,
},
{
categoryId: 0,
categoryName: "オススメ",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/green-salad-with-fresh-vegetables-picture-id953810510?s=2048x2048",
itemDespription: "人気のサラダです。",
itemId: 3,
itemName: "グリーンサラダ",
orderNo: 1,
price: 390,
stockoutFlg: false,
},
],
orderNo: 1,
},
{
categoryId: 1,
categoryName: "ドリンク",
items: [
{
categoryId: 1,
categoryName: "ドリンク",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/glass-of-beer-isolated-on-white-background-picture-id862774556?s=2048x2048",
itemDespription: "まずはビールから!",
itemId: 1001,
itemName: "生ビール",
orderNo: 1,
price: 300,
stockoutFlg: false,
},
{
categoryId: 1,
categoryName: "ドリンク",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/glass-of-gin-tonic-picture-id803905662?s=2048x2048",
itemDespription: "フルーティーで優しい口当たり。",
itemId: 1002,
itemName: "特製カクテル",
orderNo: 2,
price: 350,
stockoutFlg: false,
},
{
categoryId: 1,
categoryName: "ドリンク",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/cola-with-crushed-ice-and-straw-in-tall-glass-picture-id681018122?s=2048x2048",
itemDespription: "",
itemId: 1003,
itemName: "コーラ",
orderNo: 3,
price: 200,
stockoutFlg: false,
},
],
orderNo: 2,
},
{
categoryId: 2,
categoryName: "スピードメニュー",
items: [
{
categoryId: 2,
categoryName: "スピードメニュー",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/edamame-japanese-foodboiled-green-soybeans-picture-id1066735192?s=2048x2048",
itemDespription: "甘み引き立つうす塩味に仕上げました。",
itemId: 2005,
itemName: "枝豆",
orderNo: 6,
price: 190,
stockoutFlg: false,
},
{
categoryId: 2,
categoryName: "スピードメニュー",
discountRate: 20,
discountWay: 1,
imageUrl:
"https://media.istockphoto.com/photos/japanese-food-japanese-soft-cold-tofu-picture-id1170639696?s=2048x2048",
itemDespription: "",
itemId: 2006,
itemName: "冷ややっこ",
orderNo: 6,
price: 200,
stockoutFlg: false,
},
],
orderNo: 3,
},
{
categoryId: 3,
categoryName: "料理",
items: [
{
categoryId: 3,
categoryName: "料理",
discountRate: 10,
discountWay: 2,
imageUrl:
"https://media.istockphoto.com/photos/japanese-fried-chicken-picture-id1150109573?s=2048x2048",
itemDespription: "サクサク、ジューシーな味をお楽しみください。",
itemId: 3001,
itemName: "特製唐揚げ",
orderNo: 1,
price: 500,
stockoutFlg: false,
},
{
categoryId: 3,
categoryName: "料理",
discountRate: 20,
discountWay: 1,
imageUrl:
"https://media.istockphoto.com/photos/french-fries-on-white-background-picture-id604378894?s=2048x2048",
itemDespription: "国産じゃがいも使用。ケチャップでどうぞ。",
itemId: 3002,
itemName: "ポテトフライ",
orderNo: 1,
price: 190,
stockoutFlg: false,
},
],
orderNo: 4,
},
{
categoryId: 5,
categoryName: "本日限定品",
items: [
{
categoryId: 5,
categoryName: "本日限定品",
discountRate: 0,
discountWay: 0,
imageUrl:
"https://media.istockphoto.com/photos/isolated-top-view-of-sukiyaki-hot-pot-with-boiling-vegetables-picture-id1090355486?s=2048x2048",
itemDespription: "",
itemId: 5001,
itemName: "寄せ鍋",
orderNo: 1,
price: 1200,
stockoutFlg: false,
},
],
orderNo: 5,
},
];
utilsファイルからfirebaseファイルに移動
utils\firebase-client.ts
→ firebase\firebase-client.ts
forEachでぶん回す
import { tableOerderItems } from "order-item/table_order_items";
import { collection, addDoc } from "firebase/firestore";
import { db } from "./firebase-client";
tableOerderItems.forEach(async (item: any) => {
try {
const docRef = await addDoc(collection(db, "TableOrderItemList"), item);
console.log("Document written with ID: ", docRef.id);
} catch (e) {
console.error("Error adding document: ", e);
}
});
ちょっと頭が混乱してきた
商品情報のデータベース構築はどこで実行すればいのか?
実行されるごとに上書きされるみたい
getStaticPropsで実行すればビルド時に毎回商品情報を更新できるのでは?
商品情報使うページでgetStaticPropsだ!
とりあえずフロントページにおいておく
export async function getStaticProps() {
const items = await InitTableOerderItems();
return{
props: {
items
}
}
}
これで商品情報のデータベースはできた
いやまてインデックス作ってない
コンソールでも追加できるが
firebase CLIを使った方が再現性が高そう
firebase CLI install
npm install -g firebase-tools
ログイン
firebase login
プロジェクト確認
firebase projects:list
┌──────────────────────┬─────────────────┬────────────────┬──────────────────────┐
│ Project Display Name │ Project ID │ Project Number │ Resource Location ID │
├──────────────────────┼─────────────────┼────────────────┼──────────────────────┤
│ LineMobileOrder │ linemobileorder │ ------------ │ asia-northeast1 │
└──────────────────────┴─────────────────┴────────────────┴──────────────────────┘
初期化
firebase init
いや待て、自動インデックスっていう機能あるから商品情報のインデックスの設定の必要ない
無駄骨だったが、注文情報で使うからいいか
注文情報データベースはベースの設定だとAWSのグローバルセカンダリインデックスを使ってる
firebaseだと複合インデックスト、単一インデックスがあって複合インデックスは2つの条件でクエリを実行するときに必要
citiesRef.where("country", "==", "USA").orderBy("population", "asc")
citiesRef.where("country", "==", "USA").where("population", "<", 3800000)
citiesRef.where("country", "==", "USA").where("population", ">", 690000)
// in and == clauses use the same index
citiesRef.where("country", "in", ["USA", "Japan", "China"])
.where("population", ">", 690000)
でも複数条件でも、値を指定してクエリする場合は単一インデックスでも行ける
citiesRef.where('country', 'in', ["USA", "Japan", "China"])
// Compound equality queries
citiesRef.where("state", "==", "CO").where("name", "==", "Denver")
citiesRef.where("country", "==", "USA")
.where("capital", "==", false)
.where("state", "==", "CA")
.where("population", "==", 860000)
単一インデックスにする
あれ自動インデックスってすべてのフィールドについているっぽい
いらねーじゃん(笑)
てかfirebase AWSより圧倒的に使いやすいんだが
ちょっと行き詰ってきたけど注文情報はフロント作るときに一緒に作っちゃお
とりあえずfunctionsを作る
ベースがpythonなのでpythonでデプロイしようと思ったら
javascriptとtypescriptだけっぽい、一応GCPのfunctionsを使えばpythonでも行けるみたいだけど
デプロイの仕方がいまいちわからない
1つのファイルに関数をまとめないといけないのか?
pythonでやるのにもGCPを使って、ファイルに関数入れなきゃいけないみたいだから
ベースを参考にtypescriptでfunctions作ります。
getItems
export default functions.https.onRequest(async function ItemListGet(req, res) {
const getItemList = async (params: unknown) => {
if (typeof params === "string") params = parseInt(params, 10);
const items = await admin
.firestore()
.collection("TableOrderItemList")
.where("categoryId", "==", params);
functions.logger.log("items %s", items);
return items;
};
functions.logger.log(req.query);
const items = await getItemList(req.query);
if (items) {
functions.logger.log("Occur Exception: %s", items);
}
return res.json(items);
});
ベースのレイヤディレクトリの部分を作るのが予想以上にしんどい気がする
やはりcloud functions にgcp CLIでデプロイするのがよきかも
いや、そもそもdockerでコンテナ化してないので、pythonでやっても書き直しが結構多いことに気づいた
firebaseじゃなくてAWSをシンプルに使った方がよかったのか…
でも、AWSの使いにくさは嫌いなんだよね
GCPは無料期間が短いのでAzureがいいのかもしれない
validationでdateフォーマットが必要ライブラリに何を使おうか
date-fns
に決定
めちゃ簡単
import { format } from "date-fns";
export const check_year_month = (columns, column_name) => {
const columnsReplaced = columns.replace("-", "").replace("/", "");
try {
format(new Date(columnsReplaced), "yyyyMM");
} catch {
return `年月形式エラー : ${column_name}(${columns})`;
}
};
export const check_year_month_day = (columns, column_name) => {
const columnsReplaced = columns.replace("-", "").replace("/", "");
try {
format(new Date(columnsReplaced), "yyyyMMdd");
} catch {
return `年月日形式エラー : ${column_name}(${columns})`;
}
};
export const check_time_format = (columns, column_name, time_format) => {
const columnsReplaced = columns.replace("-", "").replace("/", "");
try {
format(new Date(columnsReplaced), time_format.toString());
} catch {
return `年月日形式エラー : ${column_name}(${columns})`;
}
};
pythonの__init__って何なの?
コンストラクタっていうらしい(前にやったなー)
機能的にはクラスを初期化するらしい
class TableOrderParamCheck(ParamCheck):
def __init__(self, params):
self.category_id = params['categoryId'] if 'categoryId' in params else None # noqa:E501
self.table_id = params['tableId'] if 'tableId' in params else None
self.payment_id = params['paymentId'] if 'paymentId' in params else None # noqa:E501
self.transaction_id = params['transactionId'] if 'transactionId' in params else None # noqa:E501
self.item = params['item'] if 'item' in params else None # noqa:E501
self.error_msg = []
def check_api_order_put(self):
self.check_table_id()
self.check_item()
return self.error_msg
普通にjavascript
ならinit()
みたいにしてファイルの上の方に書いておけばOKかな?
functions ディレクトリ内でファイル分割して関数を定義して、exportしていたら
Parsing error: Cannot read file '.../tsconfig.json'.eslint
(...の部分は自分のプロジェクトフォルダまでのパス)というエラーが表示された。
{
"eslint.workingDirectories": ["./front", "./functions"]
}
結果的にこれで解決
lineの関数と日付け調整の関数が完成
/* eslint-disable linebreak-style */
import * as functions from "firebase-functions";
import line from "@line/bot-sdk";
import axios from "axios";
export const sendPushMessage = (
channelAccessToken: any,
flexObj: any,
userId: string
) => {
const client = new line.Client({
channelAccessToken,
channelSecret: process.env.CHANNEL_SECRET || "",
});
let response;
try {
response = client.pushMessage(userId, {
type: "flex",
altText: "This is a Flex Message",
content: flexObj,
});
} catch (e) {
// functions.logger.error(
// "Got exception from LINE Messaging API: %s\n" % e.message
// );
functions.logger.error("Occur Exception: %s", e);
}
return response;
};
export const getProfile = (idToken: any, channelId: any) => {
const headers = { "Content-Type": "application/x-www-form-urlencoded" };
const body = {
id_token: idToken,
client_id: channelId,
};
const response = axios.post(process.env.API_USER_ID_URL, {
headers,
data: body,
});
const resBody = JSON.parse(response.text);
return resBody;
};
もう遅いから寝る
明日は用事あるから休み