1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Go 3Advent Calendar 2020

Day 5

html/template + echo + Firestoreのつかいかた

Last updated at Posted at 2020-12-05

この記事で書いてあること

  • html/templateでhtmlのページを表示する
  • echoを利用して自作のFunctionを叩く
  • Firestoreへのデータ登録、データ取得、データ削除を行う
  • とりあえず全部がっちゃんこしてみる方法

ハマったこと、同じレベルの初学者へのポイント

  • 簡単なHundleFuncを自作してhtml/templateを利用するのは難しくない
  • echo+html/templateを利用するとなると、ちょっと難しくなる
  • そこにFirestoreのFunctionも絡ませると、もうちょっと難しくなる
  • この記事から応用すれば、Firestoreを利用した簡単なWebアプリ作成の基礎を作成できる(はず)

必要なもの

  • 諦めない気持ち
  • 利用できるFirestore(今回はFirestoreEmulatorを利用しました)

FirestoreEmulatorの用意

FirebaseEmulatorの用意を行います。
gcloud initした際の下記さえ越えれば行けたと記憶してます。

terminal
Would you like to create one? (Y/n)? n // ここをnでnoしておけばGCPでプロジェクトも作らなくていい

何度も長いコマンドを打つのはしんどいので、Makefileを利用します(Makefileを知らない人は調べてみてください!めちゃ便利です!)

Makefile
run-firestore:
gcloud beta emulators firestore start --host-port=localhost:8888  

そして

terminal
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で分けたいので、下記のようにします。

main.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として、このようになっています。

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を作成します。

router.go
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.goserverディレクトリのserverパッケージに設定してあるので、

        server.Router(e)           // router.goのRouter関数

で呼び出すことができます。

Routerで扱っているFunctionたちの作成

Firestore用のFunctionは、2ファイルに分けています。
下記のfirestore/member.goは、firestoreパッケージとして作成し、
次に作成するserver/member.goでインポートして利用します。

firestore/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にデータを渡せます。

server/member.go
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ファイルの作成

懺悔すると、ここに力を入れる時間がなかったので、最低限の機能だけ実装しています。

index.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サーバーを立ち上げられます。

terminal
go run main.go

その後localhost:8085にアクセスすると入力フォームやボタンがあるページが表示されるはずです。

まとめ

本当はFirestoreEmulatorをDockerで動かして接続するとかやりたかった...
投稿の遅刻までして、n番煎じって感じですが、
意外とhtml/template+echo+Firestoreのジャストな組み合わせの記事は見なかったので書いてみました。
後付けビビりですが、Goを知って1年半、仕事で扱わせてもらうようになって3ヶ月が経ちアウトプットの場として楽しませてもらいました。
至らないコードや、非効率な書き方などあると思いますが、ぜひ編集リクエストやコメントでご指導ご鞭撻よろしくお願い致します!!🙇‍♂️

1
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?