使用 Firestore 處理工作階段

本教學課程說明如何在 Cloud Run 上處理工作階段。

許多應用程式需要處理工作階段以進行驗證和使用者偏好設定。 Gorilla Web Toolkit sessions 套件隨附以檔案系統為基礎的實作,可執行這項函式。但是,此實作並不適合可從多個執行個體提供的應用程式,因為記錄在一個執行個體中的工作階段可能與其他執行個體不同。gorilla/sessions 套件也隨附以 Cookie 為基礎的實作。但這種做法需要加密 Cookie,並將整個工作階段儲存在用戶端,而不只是工作階段 ID,這對某些應用程式來說可能過於龐大。

目標

  • 編寫應用程式。
  • 在本機執行應用程式。
  • 在 Cloud Run 上部署應用程式。

費用

在本文件中,您會使用下列 Google Cloud的計費元件:

如要根據預測用量估算費用,請使用 Pricing Calculator

初次使用 Google Cloud 的使用者可能符合免費試用資格。

完成本文所述工作後,您可以刪除已建立的資源,避免繼續計費。詳情請參閱清除所用資源一節。

事前準備

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  6. Verify that billing is enabled for your Google Cloud project.

  7. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  8. 在 Google Cloud 控制台中,於 Cloud Shell 開啟應用程式。

    前往 Cloud Shell

    Cloud Shell 可讓您直接在瀏覽器中使用指令列工具存取雲端資源。在瀏覽器中開啟 Cloud Shell,然後按一下「繼續」,將程式碼範例和變更下載到應用程式目錄中。

  9. 在 Cloud Shell 中,將 gcloud CLI 設為使用新的 Google Cloud 專案:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID
    

設定專案

  1. 在終端機視窗中,將範例應用程式存放區複製到本機電腦:

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. 變更為包含範例程式碼的目錄:

    cd golang-samples/getting-started/sessions

瞭解網頁應用程式

這個應用程式為每個使用者顯示不同語言的問候語。系統一律會用與上次相同的語言問候回訪者。

多個應用程式視窗會顯示不同語言的問候語。

您必須先將目前使用者的相關資訊儲存在工作階段中,應用程式才能儲存使用者的偏好設定。這個範例應用程式使用 Firestore 儲存工作階段資料。

  1. 應用程式會先匯入依附元件,定義 app 型別來保留 sessions.Store 和 HTML 範本,並定義問候語清單。

    import (
    	"context"
    	"html/template"
    	"log"
    	"math/rand"
    	"net/http"
    	"os"
    
    	"cloud.google.com/go/firestore"
    )
    
    // app stores a sessions.Store. Create a new app with newApp.
    type app struct {
    	tmpl         *template.Template
    	collectionID string
    	projectID    string
    }
    
    // session stores the client's session information.
    // This type is also used for executing the template.
    type session struct {
    	Greetings string `json:"greeting"`
    	Views     int    `json:"views"`
    }
    
    // greetings are the random greetings that will be assigned to sessions.
    var greetings = []string{
    	"Hello World",
    	"Hallo Welt",
    	"Ciao Mondo",
    	"Salut le Monde",
    	"Hola Mundo",
    }
    
  2. 接著,應用程式會定義 main 函式,建立新的 app 例項、註冊索引處理常式,並啟動 HTTP 伺服器。newApp 函式會設定 projectIDcollectionID 值,並剖析 HTML 範本,藉此建立 app 執行個體。

    func main() {
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    
    	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    	if projectID == "" {
    		log.Fatal("GOOGLE_CLOUD_PROJECT must be set")
    	}
    
    	// collectionID is a non-empty identifier for this app, it is used as the Firestore
    	// collection name that stores the sessions.
    	//
    	// Set it to something more descriptive for your app.
    	collectionID := "hello-views"
    
    	a, err := newApp(projectID, collectionID)
    	if err != nil {
    		log.Fatalf("newApp: %v", err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app.
    func newApp(projectID, collectionID string) (app, error) {
    	tmpl, err := template.New("Index").Parse(`<body>{{.Views}} {{if eq .Views 1}}view{{else}}views{{end}} for "{{.Greetings}}"</body>`)
    	if err != nil {
    		log.Fatalf("template.New: %v", err)
    	}
    
    	return app{
    		tmpl:         tmpl,
    		collectionID: collectionID,
    		projectID:    projectID,
    	}, nil
    }
    
  3. 索引處理常式會取得使用者工作階段,並視需要建立工作階段。系統會為新工作階段隨機指派語言,並將觀看次數設為 0。接著,瀏覽次數會增加 1,系統會儲存工作階段,而 HTML 範本會寫入回應。

    
    // index uses sessions to assign users a random greeting and keep track of
    // views.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	// Allows requests only for the root path ("/") to prevent duplicate calls.
    	if r.RequestURI != "/" {
    		http.NotFound(w, r)
    		return
    	}
    
    	var session session
    	var doc *firestore.DocumentRef
    
    	isNewSession := false
    
    	ctx := context.Background()
    
    	client, err := firestore.NewClient(ctx, a.projectID)
    	if err != nil {
    		log.Fatalf("firestore.NewClient: %v", err)
    	}
    	defer client.Close()
    
    	// cookieName is a non-empty identifier for this app, it is used as the key name
    	// that contains the session's id value.
    	//
    	// Set it to something more descriptive for your app.
    	cookieName := "session_id"
    
    	// If err is different to nil, it means the cookie has not been set, so it will be created.
    	cookie, err := r.Cookie(cookieName)
    	if err != nil {
    		// isNewSession flag is set to true
    		isNewSession = true
    	}
    
    	// If isNewSession flag is true, the session will be created
    	if isNewSession {
    		// Get unique id for new document
    		doc = client.Collection(a.collectionID).NewDoc()
    
    		session.Greetings = greetings[rand.Intn(len(greetings))]
    		session.Views = 1
    
    		// Cookie is set
    		cookie = &http.Cookie{
    			Name:  cookieName,
    			Value: doc.ID,
    		}
    		http.SetCookie(w, cookie)
    	} else {
    		// The session exists
    
    		// Retrieve document from collection by ID
    		docSnapshot, err := client.Collection(a.collectionID).Doc(cookie.Value).Get(ctx)
    		if err != nil {
    			log.Printf("doc.Get error: %v", err)
    			http.Error(w, "Error getting session", http.StatusInternalServerError)
    			return
    		}
    
    		// Unmarshal documents's content to local type
    		err = docSnapshot.DataTo(&session)
    		if err != nil {
    			log.Printf("doc.DataTo error: %v", err)
    			http.Error(w, "Error parsing session", http.StatusInternalServerError)
    			return
    		}
    
    		doc = docSnapshot.Ref
    
    		// Add 1 to current views value
    		session.Views++
    	}
    
    	// The document is created/updated
    	_, err = doc.Set(ctx, session)
    	if err != nil {
    		log.Printf("doc.Set error: %v", err)
    		http.Error(w, "Error creating session", http.StatusInternalServerError)
    		return
    	}
    
    	if err := a.tmpl.Execute(w, session); err != nil {
    		log.Printf("Execute: %v", err)
    	}
    }
    

    下圖說明 Firestore 如何處理 Cloud Run 應用程式的工作階段。

    架構圖:使用者、Cloud Run、Firestore。

刪除工作階段

您可以刪除工作階段資料,或實作自動化的刪除策略。如要刪除資料,請前往Google Cloud 控制台。如果您使用 Memcache 或 Redis 這類工作階段儲存解決方案,則會自動刪除過期的工作階段。

在本機環境中執行

  1. 在終端機視窗中,建構 sessions 二進位檔:

    go build
    
  2. 啟動 HTTP 伺服器:

    ./sessions
    
  3. 在網路瀏覽器中查看應用程式:

    Cloud Shell

    在 Cloud Shell 工具列中,按一下 [Web preview] (網頁預覽) 網頁預覽,然後選取 [Preview on port 8080] (透過以下通訊埠預覽:8080)

    本機電腦

    透過瀏覽器前往以下網址:http://localhost:8080

    您會看到以下五個問候語其中之一:「Hello World」、「Hallo Welt」、「Hola mundo」、「Salut le Monde」或「Ciao Mondo」。如果您在不同的瀏覽器或在無痕模式中開啟頁面,則語言會變更。 您可以在Google Cloud 控制台中查看和編輯工作階段資料。

     Google Cloud 控制台中的 Firestore 工作階段。

  4. 如要停止 HTTP 伺服器,請在您的終端機視窗中,按下 Control+C

在 Cloud Run 上部署及執行

您可以使用 Cloud Run 建構及部署應用程式,即使是處在高負載和大量資料的情況下,應用程式仍會穩定可靠地執行。

  1. 在 Cloud Run 上部署應用程式:
        gcloud run deploy firestore-tutorial-go 
    --source . --allow-unauthenticated --port=8080
    --set-env-vars=GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID
  2. 前往這個指令傳回的網址,即可查看網頁載入期間的工作階段資料保留情形。

問候語目前是由 Cloud Run 執行個體上執行的網路伺服器提供。

應用程式偵錯

如果無法連線至 Cloud Run 應用程式,請檢查下列事項:

  1. 檢查 gcloud 部署指令是否順利完成,且未輸出任何錯誤。如果發生錯誤 (例如 message=Build failed),請修正錯誤,並再次嘗試部署 Cloud Run 應用程式
  2. 前往 Google Cloud 控制台的「Logs Explorer」頁面。

    前往「Logs Explorer」(記錄檔探索工具) 頁面

    1. 在「Recently selected resources」(最近選取的資源) 下拉式清單中,按一下「Cloud Run Application」(Cloud Run 應用程式),然後按一下「All module_id」(所有 module_id)。系統隨即會顯示您造訪應用程式時的要求清單。如果您沒看見要求清單,請確認您已從下拉式清單中選取 [All module_id] (所有 module_id)。如果在 Google Cloud 控制台中看到錯誤訊息,請檢查應用程式的程式碼是否與「編寫網頁應用程式」一節中的程式碼相符。

    2. 確認已啟用 Firestore API。

清除所用資源

刪除專案

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

刪除 Cloud Run 執行個體

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. 如要刪除應用程式版本,請按一下 「刪除」

後續步驟