この記事で書いてあること
- html/templateでhtmlのページを表示する
- echoを利用して自作のFunctionを叩く
- Firestoreへのデータ登録、データ取得、データ削除を行う
- とりあえず全部がっちゃんこしてみる方法
ハマったこと、同じレベルの初学者へのポイント
- 簡単なHundleFuncを自作してhtml/templateを利用するのは難しくない
- echo+html/templateを利用するとなると、ちょっと難しくなる
- そこにFirestoreのFunctionも絡ませると、もうちょっと難しくなる
- この記事から応用すれば、Firestoreを利用した簡単なWebアプリ作成の基礎を作成できる(はず)
必要なもの
- 諦めない気持ち
- 利用できるFirestore(今回はFirestoreEmulatorを利用しました)
FirestoreEmulatorの用意
FirebaseEmulatorの用意を行います。
gcloud init
した際の下記さえ越えれば行けたと記憶してます。
Would you like to create one? (Y/n)? n // ここをnでnoしておけばGCPでプロジェクトも作らなくていい
何度も長いコマンドを打つのはしんどいので、Makefile
を利用します(Makefile
を知らない人は調べてみてください!めちゃ便利です!)
run-firestore:
gcloud beta emulators firestore start --host-port=localhost:8888
そして
make run-firestore
gcloud beta emulators firestore start --host-port=localhost:8888
Executing: /Users/Rererr/google-cloud-sdk/platform/cloud-firestore-emulator/cloud_firestore_emulator start --host=localhost --port=8888
[firestore] API endpoint: http://localhost:8888
[firestore] If you are using a library that supports the FIRESTORE_EMULATOR_HOST environment variable, run:
[firestore]
[firestore] export FIRESTORE_EMULATOR_HOST=localhost:8888
[firestore]
[firestore] Dev App Server is now running.
[firestore]
となれば、もうデータを投入する先のFirestoreは用意できました!
echoでHello,World
正直、echoでHello,Worldは公式Tutorialで十分なので、割愛します!
ただし、Firestoreで利用するGETやPOSTrouter.go
で分けたいので、下記のようにします。
func main() {
e := echo.New()
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Renderer = server.NewRenderer() // html/templateの初期化
server.Router(e) // router.goのRouter関数
e.Logger.Fatal(e.Start(":8085"))
}
echo用のtemplateの作成
echoでhtmlをRenderするために、templateの作成を行います。
main.go
の
e.Renderer = server.NewRenderer() // html/templateの初期化
の部分です。
その中身は、server
ディレクトリのserver
パッケージにtemplate.go
として、このようになっています。
type Template struct {
templates *template.Template
}
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
func NewRenderer() echo.Renderer {
t := &Template{
templates: template.Must(template.ParseGlob("views/*.html")), // htmlファイルのあるディレクトリ+/*.html
}
return t
}
Routerの作成
Firestore用のFunctionへGET/POSTするRouterを作成します。
func Router(e *echo.Echo) {
e.GET("/", Root)
e.GET("/fetch", FetchMember)
e.POST("/put", PutMember)
e.GET("/delete_all", DeleteAllMember)
e.POST("/delete", DeleteMember)
}
このrouter.go
をserver
ディレクトリのserver
パッケージに設定してあるので、
server.Router(e) // router.goのRouter関数
で呼び出すことができます。
Routerで扱っているFunctionたちの作成
Firestore用のFunctionは、2ファイルに分けています。
下記のfirestore/member.go
は、firestore
パッケージとして作成し、
次に作成するserver/member.go
でインポートして利用します。
type Member struct {
Id string `firestore:"-" form:"id"` // FirestoreのドキュメントID / FirestoreのSet時の構造体のMember.Idは入らないのでignore
Name string `firestore:"name" form:"name"`
Age int32 `firestore:"age" form:"age"`
}
const collectionMember = "Member"
// FetchAllMembers Memberコレクションに存在する全てのドキュメントを取得し、Member構造体のPスライスに入れて返す
func FetchAllMembers(ctx context.Context, client *firestore.Client) ([]*Member, error) {
docRefs := client.Collection(collectionMember).Documents(ctx)
docs, err := docRefs.GetAll()
if err != nil {
return nil, err
}
members := make([]*Member, len(docs))
for i, doc := range docs {
var member *Member
if err := doc.DataTo(&member); err != nil {
return nil, err
}
member.Id = doc.Ref.ID
members[i] = member
}
return members, nil
}
// PutMember 渡されたMember構造体の中身を、ドキュメントとして保存する
func PutMember(ctx context.Context, client *firestore.Client, member *Member) {
dr, _, err := client.Collection(collectionMember).Add(ctx, member)
if err != nil {
log.Fatal(err.Error())
}
member.Id = dr.ID
}
// DeleteAllMember Memberコレクションに存在する全てのドキュメントを削除する
func DeleteAllMember(ctx context.Context, client *firestore.Client) {
docRefs := client.Collection(collectionMember).Documents(ctx)
docs, err := docRefs.GetAll()
if err != nil {
log.Fatal(err)
}
for _, doc := range docs {
doc.Ref.Delete(ctx)
}
}
// DeleteMember ドキュメントIDを受け取って、単体のドキュメントを削除する
func DeleteMember(ctx context.Context, client *firestore.Client, id string) {
docRef := client.Collection(collectionMember).Doc(id)
if _, err := docRef.Delete(ctx); err != nil {
log.Fatal(err)
}
}
server/member.go
では、上記のFunctionを利用してRenderにデータを渡す仕組みを実装しています。
html/template
では、map[string]interface{}
を渡すことでhtml
にデータを渡せます。
const projectID = "advent"
type Response struct {
OK bool `json:"ok,omitempty"`
Members []*repo.Member `json:"members,omitempty"`
}
func MustClient(ctx context.Context) *firestore.Client {
client, err := firestore.NewClient(ctx, projectID)
if err != nil {
log.Fatal(err)
}
return client
}
func FetchMember(c echo.Context) error {
ctx := c.Request().Context()
client := MustClient(ctx)
members, err := repo.FetchAllMembers(ctx, client)
if err != nil {
return err
}
res := &Response{
Members: members,
}
return c.Render(http.StatusOK, "index", res)
}
func PutMember(c echo.Context) error {
ctx := c.Request().Context()
client := MustClient(ctx)
mem := new(repo.Member)
if err := c.Bind(mem); err != nil {
return err
}
repo.PutMember(ctx, client, mem)
res := &Response{
OK: true,
}
return c.Render(http.StatusOK, "index", res)
}
func DeleteAllMember(c echo.Context) error {
ctx := c.Request().Context()
client := MustClient(ctx)
repo.DeleteAllMember(ctx, client)
return c.Render(http.StatusOK, "index", nil)
}
func DeleteMember(c echo.Context) error {
ctx := c.Request().Context()
client := MustClient(ctx)
mem := new(repo.Member)
if err := c.Bind(mem); err != nil {
return err
}
repo.DeleteMember(ctx, client, mem.Id)
return c.Render(http.StatusOK, "index", nil)
}
HTMLファイルの作成
懺悔すると、ここに力を入れる時間がなかったので、最低限の機能だけ実装しています。
{{ define "index" }}
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/put" method="post"> <!-- actionがrouter.goに対応している -->
<label>Name</label>
<input type="text" name="name" required>
<label>Age</label>
<input type="number" min="0" max="999" name="age" required>
<button type="submit">submit</button>
</form>
<form action="/fetch" method="get"> <!-- actionがrouter.goに対応している -->
<button type="submit">Fetch</button>
</form>
<form action="/delete_all" method="get"> <!-- actionがrouter.goに対応している -->
<button type="submit">delete all</button>
</form>
<div>
{{ range .Members }} <!-- rangeを利用して、配列の中身を展開 -->
<form action="/delete" method="post"> <!-- actionがrouter.goに対応している -->
<div>
<input type="text" name="id" value="{{ .Id }}" hidden>
<!-- FirestoreのドキュメントIDは非表示で'value'として受け渡ししている -->
Name: {{ .Name }}
Age: {{ .Age }}
<button type="submit">delete</button>
</div>
</form>
{{ end }}
</div>
</body>
</html>
{{ end }}
実行
main.go
の存在するディレクトリで、下記を実行するとechoのhttpサーバーを立ち上げられます。
go run main.go
その後localhost:8085
にアクセスすると入力フォームやボタンがあるページが表示されるはずです。
まとめ
本当はFirestoreEmulatorをDockerで動かして接続するとかやりたかった...
投稿の遅刻までして、n番煎じって感じですが、
意外とhtml/template
+echo
+Firestore
のジャストな組み合わせの記事は見なかったので書いてみました。
後付けビビりですが、Goを知って1年半、仕事で扱わせてもらうようになって3ヶ月が経ちアウトプットの場として楽しませてもらいました。
至らないコードや、非効率な書き方などあると思いますが、ぜひ編集リクエストやコメントでご指導ご鞭撻よろしくお願い致します!!🙇♂️