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

کار با داده‌های جلسه (Working with Session Data)

در این بخش، نحوه کار با داده‌های جلسه (Working with Session Data) را بررسی می‌کنیم. این شامل ذخیره‌سازی (Storage)، بازیابی (Retrieval) و حذف داده‌ها (Data Removal) از جلسه می‌شود.

در این فصل، بیایید قابلیت جلسه را به کار بگیریم و از آن برای حفظ پیام تأیید بین درخواست‌های 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", "این فیلد نمی‌تواند خالی باشد")
    form.CheckField(validator.MaxChars(form.Title, 100), "title", "این فیلد نمی‌تواند بیش از 100 کاراکتر باشد")
    form.CheckField(validator.NotBlank(form.Content), "content", "این فیلد نمی‌تواند خالی باشد")
    form.CheckField(validator.PermittedValue(form.Expires, 1, 7, 365), "expires", "این فیلد باید برابر با 1، 7 یا 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", "قطعه با موفقیت ایجاد شد!")

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

اولین پارامتری که به app.sessionManager.Put() می‌دهیم، زمینه درخواست فعلی است. ما بعداً در کتاب به‌طور کامل درباره زمینه درخواست و نحوه استفاده از آن صحبت خواهیم کرد، اما فعلاً می‌توانید آن را به‌عنوان جایی که مدیر جلسه به‌طور موقت اطلاعات را ذخیره می‌کند در حالی که هندلرهای شما با درخواست سروکار دارند، در نظر بگیرید.

پارامتر دوم (در مورد ما رشته "flash") کلید برای پیام خاصی است که به داده‌های جلسه اضافه می‌کنیم. ما بعداً پیام را از داده‌های جلسه بازیابی خواهیم کرد.

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

در مرحله بعد، می‌خواهیم هندلر 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='fa'>
    <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 رشته خالی نباشد. بنابراین، اگر کلید "flash" در جلسه کاربر فعلی وجود نداشته باشد، نتیجه این است که قطعه جدیدی از نشانه‌گذاری به‌سادگی نمایش داده نمی‌شود.

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

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"),
    }
}

...

ایجاد این تغییر به این معنی است که دیگر نیازی به بررسی پیام فلش در هندلر 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 می‌فرستد، ارسال خواهد شد. کوکی جلسه حاوی توکن جلسه — که گاهی اوقات به‌عنوان شناسه جلسه نیز شناخته می‌شود. توکن جلسه یک رشته تصادفی با انتروپی بالا است، که در مورد من مقدار 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() هر درخواست ورودی را برای یک کوکی جلسه بررسی می‌کند. اگر یک کوکی جلسه وجود داشته باشد، توکن جلسه را از کوکی می‌خواند و داده‌های جلسه مربوطه را از پایگاه داده بازیابی می‌کند (در حالی که بررسی می‌کند که جلسه منقضی نشده باشد). سپس داده‌های جلسه را به زمینه درخواست اضافه می‌کند تا بتواند در هندلرهای شما استفاده شود.

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

واژه‌نامه اصطلاحات فنی

اصطلاح فارسی معادل انگلیسی توضیح
کار با داده‌های جلسه Working with Session Data مدیریت و دستکاری داده‌های جلسه
ذخیره‌سازی Storage نگهداری داده‌ها در جلسه
بازیابی Retrieval خواندن داده‌ها از جلسه
حذف داده‌ها Data Removal پاک کردن داده‌ها از جلسه
پیام فلش Flash Message پیام موقتی برای نمایش به کاربر
جلسه کاربر User Session داده‌های مربوط به جلسه یک کاربر
مدیریت خطا Error Handling مدیریت خطاهای جلسه
نوع داده Data Type نوع داده‌های ذخیره شده در جلسه
زمان انقضا Expiry Time مدت زمان اعتبار داده‌های جلسه
امنیت داده‌ها Data Security حفاظت از داده‌های جلسه