Let's Go HTTP با حالت › کار با داده‌های نشست
قبلی · فهرست · بعدی
فصل ۸.۳.

کار با داده‌های نشست

در این فصل بیایید عملکرد نشست را به کار ببریم و از آن برای ماندگار کردن پیام فلش تأیید بین درخواست‌های HTTP که قبلاً بحث کردیم استفاده کنیم.

در فایل cmd/web/handlers.go خود شروع می‌کنیم و متد snippetCreatePost خود را به‌روزرسانی می‌کنیم تا یک پیام فلش به داده‌های نشست کاربر اضافه شود اگر — و فقط اگر — اسنیپت با موفقیت ایجاد شده باشد. به این صورت:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
    var form snippetCreateForm

    err := app.decodePostForm(r, &form)
    if err != nil {
        app.clientError(w, http.StatusBadRequest)
        return
    }

    form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
    form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
    form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
    form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")

    if !form.Valid() {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, r, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

    id, err := app.snippets.Insert(form.Title, form.Content, form.Expires)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // Use the Put() method to add a string value ("Snippet successfully 
    // created!") and the corresponding key ("flash") to the session data.
    app.sessionManager.Put(r.Context(), "flash", "Snippet successfully created!")

    http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}

این خوب و ساده است، اما چند نکته برای اشاره وجود دارد:

بعد می‌خواهیم handler snippetView ما پیام فلش را بازیابی کند (اگر یکی در نشست برای کاربر فعلی وجود داشته باشد) و آن را به قالب HTML برای نمایش بعدی بدهد.

چون می‌خواهیم پیام فلش را فقط یک بار نمایش دهیم، در واقع می‌خواهیم پیام را از داده‌های نشست بازیابی و حذف کنیم. می‌توانیم هر دو این عملیات را همزمان با استفاده از متد PopString() انجام دهیم.

نشان می‌دهم:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    // Use the PopString() method to retrieve the value for the "flash" key.
    // PopString() also deletes the key and value from the session data, so it
    // acts like a one-time fetch. If there is no matching key in the session
    // data this will return the empty string.
    flash := app.sessionManager.PopString(r.Context(), "flash")

    data := app.newTemplateData(r)
    data.Snippet = snippet

    // Pass the flash message to the template.
    data.Flash = flash 

    app.render(w, r, http.StatusOK, "view.tmpl", data)
}

...

اگر الان سعی کنید برنامه را اجرا کنید، کامپایلر (درست) شکایت می‌کند که فیلد Flash در ساختار templateData ما تعریف نشده است. پیش بروید و آن را به این صورت اضافه کنید:

File: cmd/web/templates.go
package main

import (
    "html/template"
    "path/filepath"
    "time"

    "snippetbox.alexedwards.net/internal/models"
)

type templateData struct {
    CurrentYear int
    Snippet     models.Snippet
    Snippets    []models.Snippet
    Form        any
    Flash       string // Add a Flash field to the templateData struct.
}

...

و حالا، می‌توانیم فایل base.tmpl خود را برای نمایش پیام فلش، در صورت وجود، به‌روزرسانی کنیم.

File: ui/html/base.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
        <link rel='stylesheet' href='/static/css/main.css'>
        <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
        <link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        {{template "nav" .}}
        <main>
            <!-- Display the flash message if one exists -->
            {{with .Flash}}
                <div class='flash'>{{.}}</div>
            {{end}}
            {{template "main" .}}
        </main>
        <footer>
            Powered by <a href='https://golang.org/'>Go</a> in {{.CurrentYear}}
        </footer>
        <script src='/static/js/main.js' type='text/javascript'></script>
    </body>
</html>
{{end}}

به یاد داشته باشید، بلوک {{with .Flash}} فقط اجرا می‌شود اگر مقدار .Flash رشته خالی نباشد (not the empty string). بنابراین، اگر کلید "flash" در نشست کاربر فعلی وجود نداشته باشد، نتیجه این است که تکه نشانه‌گذاری جدید (chunk of new markup) به سادگی نمایش داده نخواهد شد.

وقتی این کار انجام شد، همه فایل‌های خود را ذخیره کنید و برنامه را مجدداً راه‌اندازی کنید. سعی کنید یک اسنیپت جدید اضافه کنید، به این صورت…

08.03-01.png

و پس از هدایت باید پیام فلش را که حالا نمایش داده می‌شود ببینید:

08.03-02.png

اگر سعی کنید صفحه را رفرش کنید، می‌توانید تأیید کنید که پیام فلش دیگر نمایش داده نمی‌شود — این یک پیام یکباره برای کاربر فعلی بلافاصله پس از ایجاد اسنیپت بود.

08.03-03.png

نمایش خودکار پیام‌های فلش

بهبود کوچکی که می‌توانیم انجام دهیم (که بعداً در پروژه کار ما را صرفه‌جویی می‌کند) خودکارسازی نمایش پیام‌های فلش است، به طوری که هر پیامی به طور خودکار دفعه بعدی که هر صفحه رندر می‌شود گنجانده شود.

می‌توانیم این کار را با افزودن هر پیام فلش به داده قالب از طریق متد کمک‌کننده newTemplateData() که قبلاً ساختیم انجام دهیم، به این صورت:

File: cmd/web/helpers.go
package main

...

func (app *application) newTemplateData(r *http.Request) templateData {
    return templateData{
        CurrentYear: time.Now().Year(),
        // Add the flash message to the template data, if one exists.
        Flash:       app.sessionManager.PopString(r.Context(), "flash"),
    }
}

...

انجام این تغییر به این معنی است که دیگر نیازی به بررسی پیام فلش در داخل handler snippetView نداریم، و کد می‌تواند به این صورت برگردانده شود:

File: cmd/web/handlers.go
package main

...

func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil || id < 1 {
        http.NotFound(w, r)
        return
    }

    snippet, err := app.snippets.Get(id)
    if err != nil {
        if errors.Is(err, models.ErrNoRecord) {
            http.NotFound(w, r)
        } else {
            app.serverError(w, r, err)
        }
        return
    }

    data := app.newTemplateData(r)
    data.Snippet = snippet

    app.render(w, r, http.StatusOK, "view.tmpl", data)
}

...

آزادانه سعی کنید برنامه را دوباره اجرا کنید و یک اسنیپت دیگر ایجاد کنید. باید ببینید که عملکرد پیام فلش هنوز همانطور که انتظار می‌رود کار می‌کند.


اطلاعات اضافی

پشت صحنه مدیریت نشست

می‌خواهم لحظه‌ای وقت بگذارم تا برخی از 'جادو' پشت مدیریت نشست را باز کنم و توضیح دهم که چگونه در پشت صحنه کار می‌کند.

اگر می‌خواهید، ابزارهای توسعه‌دهنده را در مرورگر وب خود باز کنید و به داده‌های کوکی برای یکی از صفحات نگاه کنید. باید یک کوکی به نام session در داده‌های درخواست ببینید، مشابه این:

08.03-04.png

این کوکی نشست است و با هر درخواستی که مرورگر شما می‌کند به برنامه Snippetbox ارسال می‌شود.

کوکی نشست شامل توکن نشست است — که گاهی شناسه نشست (session ID) نیز نامیده می‌شود. توکن نشست یک رشته تصادفی با آنتروپی بالا است که در مورد من مقدار y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4 است (مال شما متفاوت خواهد بود).

مهم است که تأکید کنیم که توکن نشست فقط یک رشته تصادفی است. به خودی خود، هیچ دادهٔ نشست را حمل یا منتقل نمی‌کند (مانند پیام فلش که در این فصل تنظیم کردیم).

بعد، ممکن است بخواهید یک ترمینال به MySQL باز کنید و یک پرس‌وجوی SELECT روی جدول sessions اجرا کنید تا توکن نشست که در مرورگر خود می‌بینید را جستجو کنید. به این صورت:

mysql> SELECT * FROM sessions WHERE token = 'y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4';
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
| token                                       | data                                                                                                                                                                                                                                             | expiry                     |
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
| y9y1-mXyQUoAM6V5s9lXNjbZ_vXSGkO7jy-KL-di7A4 | 0x26FF81030102FF820001020108446561646C696E6501FF8400010656616C75657301FF8600000010FF830501010454696D6501FF8400000027FF85040101176D61705B737472696E675D696E74657266616365207B7D01FF8600010C0110000016FF82010F010000000ED9F4496109B650EBFFFF010000 | 2024-03-18 11:29:23.179505 |
+---------------------------------------------+--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+----------------------------+
1 row in set (0.00 sec)

این باید یک رکورد برگرداند. مقدار data در اینجا چیزی است که در واقع شامل داده‌های نشست کاربر است. به طور خاص، آنچه که به آن نگاه می‌کنیم یک BLOB MySQL (شیء بزرگ باینری) است که شامل یک نمایش کدگذاری شده gob از داده‌های نشست است.

هر بار که تغییری در داده‌های نشست خود ایجاد می‌کنیم، این مقدار data برای منعکس کردن تغییرات به‌روزرسانی می‌شود.

در نهایت، ستون نهایی در پایگاه داده زمان expiry است، پس از آن نشست دیگر معتبر در نظر گرفته نخواهد شد.

بنابراین، آنچه در برنامه ما اتفاق می‌افتد این است که میدلور LoadAndSave() هر درخواست ورودی را برای یک کوکی نشست بررسی می‌کند. اگر یک کوکی نشست وجود داشته باشد، توکن نشست را از کوکی می‌خواند و داده‌های نشست مربوطه را از پایگاه داده بازیابی می‌کند (در حالی که همچنین بررسی می‌کند که نشست منقضی نشده است). سپس داده‌های نشست را به context درخواست اضافه می‌کند تا بتواند در handlerهای شما استفاده شود.

هر تغییری که در داده‌های نشست در handlerهای خود ایجاد می‌کنید در context درخواست به‌روزرسانی می‌شود، و سپس میدلور LoadAndSave() پایگاه داده را با هر تغییری در داده‌های نشست قبل از بازگشت به‌روزرسانی می‌کند.