更新
2020/05/05 更新
- ありがたいことに、思っていたよりもLGTMしていただけたため、流石に色々省きすぎだなと思い少し追記しました。
- 変更点
- webpackについて追記
- Firestoreへの投入部分の追記
はじめに
大学の友人数名と京都のテイクアウト情報が集まるサービスを作りました!
https://kyoto-take-out.web.app/
京都に住んでいる人はぜひ使ってみてください👘
ツイッターも拡散していただけると幸いです🙇♂️
https://twitter.com/LN_shono/status/1253324958601637890
このサービスで使用したものはざっくりとこんな感じです。
- Google スプレッドシート
- Google フォーム
- GAS
- React
- TypeScript
- Firestore
今回私は、GASを使ったお店情報の収集・編集・firestoreへの投入などの開発を担当したので、やったことを簡単にまとめます。
併せてスプレッドシートとGitHubもご覧ください。
スプレッドシート
GitHub
サービス構成
このサービスではお店の情報をGoogleフォームを用いて収集しています。
そしてフォームで集まった回答を、GASで集計・firestoreに投入し、
フロントエンドはfirestoreの情報を表示しています。
つまり、情報の流れは以下のようになっています。
- お店がフォームに回答
- スプレッドシートに回答が反映
- GASで回答をfirestoreに投入する形に整形
- 緯度経度を取得等もここ
- GASでfirestoreを更新
- フロントエンドはfirestoreのデータを表示
スプレッドシート & GAS
概要
スプレッドシート(GAS)は主に以下のような役割を担っています。
- ログの保存
- 住所からの緯度経度の取得
- 編集機能
- firestoreへのデータ投入
各シートの説明
フォームの回答 1
これは名前のままで、フォームの回答が反映されるシートです。
上のスプレッドシートではフォームを作成していないのでただのシートですが、
実際に使っているものはフォームとリンクしており、
フォームが回答されるとリアルタイムで反映されます。
また、フォームの設定では回答の編集を許可しており、フォームの回答が編集されると該当する部分の値が変更されます。
入力データログ
このシートはフォームへの入力をログとして保存しています。
基本的にはフォームの回答 1
と同じ内容にはなるのですが、編集された場合に前のデータが消えてしまうことを防ぐ目的で作成しています。
トリガーを設定しており、フォームから新しい回答が来たり編集されたりするとこのシートが更新されます(後述)。
firestore投入用シート
このシートは最終的にfirestoreに保存されるデータを表示しています。
そして同時に、スプレッドシートを編集することで、firestoreに入力するデータを編集することができるようにしています。
このようにしている理由としては、フォームの入力が誤っていたり、少し見た目を整形したい場合などに、開発側でデータを確認・編集したいことがあるからです。
スプレッドシートを見ていただけるとわかるのですが、一部のカラムに同じ名前+(編集済み)
や同じ名前+(過去の編集)
といったカラムを用意することで編集を可能にしています。
詳しくは後述します。
シートの更新
入力データログの更新
index.ts
のcreateInputLogSheet
に対応します。
やっていることは簡単で、フォームの回答 1
と入力データログ
の内容をそれぞれ取得しIDを比較して、
-
入力データログ
になくフォームの回答 1
にあるデータ(つまり新しい回答) -
入力データログ
にもあるがフォームの回答 1
の方が新しいデータ(つまり編集された回答)
を入力データログ
に挿入します。
フォームの回答が編集されるとタイムスタンプも更新されることを利用しています。
firestore投入用シートの更新
index.ts
のcreateStoredSheet
に対応します。
こちらも上と基本的にやっていることは変わりませんが、
- 挿入ではなく、シートを毎回作り直している(正確には全てのデータを消した後に挿入している)
- 最新版のみ表示したい & firestoreに入っているデータと同期させていたいのでこのような形にしています。
- 緯度経度取得の処理を挟んでいる
- 編集用の処理を挟んでいる
などの違いがあります。
編集用の処理について補足します。
基本的に、(編集済み)
とついた列に入力があればそちらを優先してfirestoreに投入するようにしています。
そして、(編集済み)
の列はフォームに更新があった際もそのまま残るようにしています。
しかし、このままですとテイクアウトメニューや営業日時に変更があった場合にデータが更新されないことになってしまいます。そこで、(過去の編集)
といった列も用意し、フォームに更新があった際には(編集済み)
の値を(過去の編集)
に移動させることで、店舗による新しい入力を保存する & 編集履歴として確認できるようにする、といったことを可能にしています。
画像URLや緯度経度などは店舗側が更新することがほとんどないだろうと思い(編集済み)
のみになっています。
また、TwitterURLなどは(編集済み)
の列はないのですが、編集したデータを更新時も優先するようになっています。
これも更新されることがまずないだろうという考えでそうしています。
Firestoreへのデータ投入(2020/05/05 追記)
index.ts
のsheetToFirestore
に対応しています。
以下のことをしています。
-
firestore投入用シート
のデータを取得 - Firestore用のデータへと整形
-
isNew
,needUpdating
の値によって追加 or 更新-
isNew
がtrueの時は追加、isNew
がfalse かつneedUpdating
がtrueの時は更新、どちらもfalseであれば何もしない
-
-
isNew
とneedUpdating
を全てfalseに
コードの話は後述します。
コードの補足
スプレッドシートからのデータの取得
スプレッドシートからのデータの取得はJSONなどのように綺麗にはできないので、どうすれば一番綺麗で楽かなぁ、と悩みました。
最終的にはこのような形が一番楽かなと思いました。
スプレッドシートに列を追加する際は
- スプレッドシートを変更する
- モデルに要素を追加する
-
(1)
でエラーが出るので修正 -
(2)
を修正
とすることで、いい感じに修正できます。
ただこれでも、列名を変えてしまうと動きません。
また、処理時間の制限が厳しいときには使えないです。
(スプレッドシートからの取得に時間がかかる)
コード
export class SpreadsheetService {
spreadsheetID: string;
constructor() {
this.spreadsheetID = OriginalUtilities.getProperty("SPREADSHEET_ID");
}
// (2)
getIndexesOfFormAnswer = (firstRow: string[]) => {
return {
timestamp: firstRow.indexOf("タイムスタンプ"),
email: firstRow.indexOf("メールアドレス"),
storeName: firstRow.indexOf("店舗名"),
...
};
};
...
getDataFromFormAnswer = (): SpreadsheetModel.FormAnswer[] => {
const sheetName = "フォームの回答 1";
const sheet = this.getSheetByName(sheetName);
const firstRow = this.getFirstRowFromSheet(sheet);
const data = sheet
.getRange(2, 1, sheet.getLastRow() - 1, firstRow.length)
.getValues();
const indexOf = this.getIndexesOfFormAnswer(firstRow);
for (const key in indexOf) {
if (indexOf[key] < 0) {
console.log(`[${sheetName}]に[${key}]が存在しません`);
}
}
const arrayOfAnswer: SpreadsheetModel.FormAnswer[] = [];
// (1)
data.forEach((row, index) => {
arrayOfAnswer.push({
ID: index + 1,
timestamp: row[indexOf.timestamp],
email: row[indexOf.email],
storeName: row[indexOf.storeName],
...
});
});
return arrayOfAnswer;
};
...
getFirstRowFromSheet = (sheet: GoogleAppsScript.Spreadsheet.Sheet) => {
const indexOfLastColumn = sheet
.getRange(1, 1)
.getNextDataCell(SpreadsheetApp.Direction.NEXT)
.getColumn();
const firstRow = sheet.getRange(1, 1, 1, indexOfLastColumn).getValues()[0];
return firstRow;
};
}
export interface FormAnswer {
ID: number;
timestamp: string;
email: string;
storeName: string;
address: string;
telephoneNumber: string;
takeoutMenu: string;
twitterURL: string;
instagramURL: string;
tabelogURL: string;
onlineSalesURL: string;
imageURL: string;
businessHour: string;
facebookURL: string;
homepageURL: string;
}
.dev-clasp
普段GASの開発ではclaspを使っています。
普通に使うと一つのフォルダが一つのGASプロジェクトに対応するのですが、テスト環境で使ってから本番に反映したい、といったときもありますよね。
そこで、.dev-clasp.json
とpushDev.sh
というスクリプトを用いて、package.json
のscripts
でdev用と本番用でスクリプトを分けました。
#!/bin/sh
cd `dirname $0`
mv .clasp.json .clasp-renamed.json
mv .dev-clasp.json .clasp.json
clasp push
mv .clasp.json .dev-clasp.json
mv .clasp-renamed.json .clasp.json
webpack(2020/05/05 追記)
参考
claspはwebpackでbuildしなくてもTypescriptを変換してpushしてくれるのですが、node_modules
を自動的に含めるといったことはしてくれません。(そもそもimportという概念がGASになく全てグローバルといった感じ(のはず))
そこで、moment
などのパッケージを使うときはwebpackを使ってbuildするようにするとうまく出来ます。
GASでもライブラリが使える(moment
もあり同じように使える)のですが、ローカルで開発する際は補完等出来なかったり、エラーが表示されたりしてしまいます。
ただ「絶対webpackを使用すべき」という訳ではないと思っていて、それぞれ一長一短あるので使い分けかなと思っています。
基本的にはclasp単体で事足りると思います。
(むしろ事足りないのであれば、GASでやらない方が良さそう)
今回は実際やってみたらどんな感じなのか知りたくて使ってみました。
clasp単体 | webpack併用 | |
---|---|---|
難易度 | 簡単 | 少し面倒 |
node_module | 使えない(ただしGASのライブラリがある) | 使える ただしGASで使えない機能を使っているものは使えない |
ライブラリの補完 | 基本的に使えない 後述する型定義ファイルを用意すれば使える |
使える |
pushにかかる時間 | 短い | buildを挟むので少し長い |
GAS上での見た目 | 基本的にそのままのコードなので、GASエディタ上でも編集できる | bundleされたファイルなので、GASエディタ上では見にくい ファイルサイズも大きくなる |
Firestoreへのデータ投入(2020/05/05 追記)
sheetToFirestore
について、もう少し具体的に解説します。
上でも少し触れているのですが、GASには便利なライブラリが数多く存在しています。
実際に、sheetToFirestore
でもFirestoreApp
(FirestoreGoogleAppsScript)というライブラリを使用しています。
使用方法は簡単で、
- GASエディタにアクセス
- リソース>ライブラリを選択
-
1VUSl4b1r1eoNcRWotZM3e87ygkxvXltOgyDZhixqncz9lQ3MjfT1iKFw
を入力し追加
をするだけで、使えるようになります。
ただ、これだけですとローカル環境ではこのライブラリの存在を知らないので、補完等も出来ずエラーが出てしまいます。
そこで、型定義ファイルを作成することでライブラリを使用しつつTypescriptの利点を活かした開発を可能にします。
と言ってもやることは簡単で、src
フォルダにtypes
フォルダを作成し(typesフォルダは必須ではない)、以下のFirestoreApp.d.ts
を作成すると、補完が出てくるようになり、GASでもちゃんと動きます。
他のGASライブラリを使用する際も同じように、GASライブラリの名前と同一(正確にはGASエディタ上のライブラリに設定した名前?)の型定義を作成することで、開発がやりやすくなると思います。
declare class FirestoreApp {
static getFirestore(email: string, key: string, projectId: string): Firestore;
}
declare class Firestore {
getDocuments(path: string): number;
createDocument(path: string, fields: object): any;
updateDocument(path: string, fields: object, mask: boolean): any;
}
おわりに
今回はスプレッドシート、フォーム、GASを用いてデータ収集・編集・投入をするものを作成しました。
このまま使うことはないでしょうが、色々と応用はできるのではないかなと思います。
何かお役に立てれば幸いです。
ありがとうございました。