Let's Go قالب‌های HTML پویا › نمایش داده‌های پویا
قبلی · فهرست · بعدی
فصل 5.1.

نمایش داده‌های پویا

در حال حاضر تابع handler snippetView ما یک شیء models.Snippet را از پایگاه داده واکشی می‌کند و سپس محتوا را در یک پاسخ HTTP متنی ساده چاپ می‌کند.

در این فصل، این را بهبود می‌دهیم تا داده‌ها در یک صفحه وب HTML مناسب نمایش داده شوند که کمی شبیه این به نظر می‌رسد:

05.01-01.png

بیایید در handler snippetView شروع کنیم و کدی برای رندر یک فایل قالب جدید view.tmpl اضافه کنیم (که در یک لحظه ایجاد خواهیم کرد). امیدوارم این باید از قبل در کتاب برای شما آشنا به نظر برسد.

File: cmd/web/handlers.go
package main

import (
    "errors"
    "fmt"
    "html/template" // Uncomment import
    "net/http"
    "strconv"

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

...

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
    }

    // Initialize a slice containing the paths to the view.tmpl file,
    // plus the base layout and navigation partial that we made earlier.
    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/view.tmpl",
    }

    // Parse the template files...
    ts, err := template.ParseFiles(files...)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // And then execute them. Notice how we are passing in the snippet
    // data (a models.Snippet struct) as the final parameter?
    err = ts.ExecuteTemplate(w, "base", snippet)
    if err != nil {
        app.serverError(w, r, err)
    }
}

...

بعدی، باید فایل view.tmpl حاوی نشانه‌گذاری HTML برای صفحه را ایجاد کنیم. اما قبل از انجام این کار، یک تئوری کوچک وجود دارد که باید توضیح دهم…

هر داده‌ای که به عنوان پارامتر نهایی به ts.ExecuteTemplate() ارسال می‌کنید، در قالب‌های HTML شما با کاراکتر . (که به آن dot گفته می‌شود) نمایش داده می‌شود.

در این مورد خاص، نوع زیرین dot یک ساختار models.Snippet خواهد بود. وقتی نوع زیرین dot یک ساختار است، می‌توانید مقدار هر فیلد صادر شده را در قالب‌های خود رندر (یا yield) کنید با پسوند دادن dot با نام فیلد. بنابراین، چون ساختار models.Snippet ما یک فیلد Title دارد، می‌توانیم عنوان snippet را با نوشتن {{.Title}} در قالب‌های خود yield کنیم.

من نشان می‌دهم. یک فایل جدید در ui/html/pages/view.tmpl ایجاد کنید و نشانه‌گذاری زیر را اضافه کنید:

$ touch ui/html/pages/view.tmpl
File: ui/html/pages/view.tmpl
{{define "title"}}Snippet #{{.ID}}{{end}}

{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Title}}</strong>
            <span>#{{.ID}}</span>
        </div>
        <pre><code>{{.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Created}}</time>
            <time>Expires: {{.Expires}}</time>
        </div>
    </div>
{{end}}

If you restart the application and visit http://localhost:4000/snippet/view/1 in your browser, you should find that the relevant snippet is fetched from the database, passed to the template, and the content is rendered correctly.

05.01-02.png

Rendering multiple pieces of data

An important thing to explain is that Go’s html/template package allows you to pass in one — and only one — item of dynamic data when rendering a template. But in a real-world application there are often multiple pieces of dynamic data that you want to display in the same page.

A lightweight and type-safe way to achieve this is to wrap your dynamic data in a struct which acts like a single ‘holding structure’ for your data.

Let’s create a new cmd/web/templates.go file, containing a templateData struct to do exactly that.

$ touch cmd/web/templates.go
File: cmd/web/templates.go
package main

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

// Define a templateData type to act as the holding structure for
// any dynamic data that we want to pass to our HTML templates.
// At the moment it only contains one field, but we'll add more
// to it as the build progresses.
type templateData struct {
    Snippet models.Snippet
}

و سپس بیایید 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
    }

    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/view.tmpl",
    }

    ts, err := template.ParseFiles(files...)
    if err != nil {
        app.serverError(w, r, err)
        return
    }

    // Create an instance of a templateData struct holding the snippet data.
    data := templateData{
        Snippet: snippet,
    }

    // Pass in the templateData struct when executing the template.
    err = ts.ExecuteTemplate(w, "base", data)
    if err != nil {
        app.serverError(w, r, err)
    }
}

...

پس اکنون، داده snippet ما در یک ساختار models.Snippet درون یک ساختار templateData قرار دارد. برای yield کردن داده، باید نام‌های فیلد مناسب را به هم زنجیره کنیم، مانند این:

File: ui/html/pages/view.tmpl
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}

{{define "main"}}
    <div class='snippet'>
        <div class='metadata'>
            <strong>{{.Snippet.Title}}</strong>
            <span>#{{.Snippet.ID}}</span>
        </div>
        <pre><code>{{.Snippet.Content}}</code></pre>
        <div class='metadata'>
            <time>Created: {{.Snippet.Created}}</time>
            <time>Expires: {{.Snippet.Expires}}</time>
        </div>
    </div>
{{end}}

می‌توانید برنامه را مجدداً راه‌اندازی کنید و دوباره به http://localhost:4000/snippet/view/1 بروید. باید همان صفحه رندر شده در مرورگر خود را مانند قبل ببینید.


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

فرار محتوای پویا

پکیج html/template به طور خودکار هر داده‌ای که بین تگ‌های {{ }} yield می‌شود را escape می‌کند. این رفتار در جلوگیری از حملات اسکریپت‌نویسی بین‌سایتی (XSS) بسیار مفید است، و دلیل این است که باید از پکیج html/template به جای پکیج عمومی‌تر text/template که Go نیز ارائه می‌دهد استفاده کنید.

به عنوان مثالی از escaping، اگر داده پویا که می‌خواستید yield کنید این بود:

<span>{{"<script>alert('xss attack')</script>"}}</span>

به طور بی‌ضرر به این صورت رندر می‌شد:

<span>&lt;script&gt;alert(&#39;xss attack&#39;)&lt;/script&gt;</span>

پکیج html/template همچنین به اندازه کافی هوشمند است که escaping را وابسته به بافت کند. بسته به اینکه داده در بخشی از صفحه که شامل HTML، CSS، Javascript یا یک URI است رندر می‌شود یا نه، از دنباله‌های escape مناسب استفاده می‌کند.

قالب‌های تو در تو

بسیار مهم است که توجه داشته باشید که وقتی یک قالب را از قالب دیگری فراخوانی می‌کنید، dot باید به صراحت ارسال یا pipelined شود به قالب فراخوانی شده. این کار را با شامل کردن آن در انتهای هر عمل {{template}} یا {{block}} انجام می‌دهید، مانند این:

{{template "main" .}}
{{block "sidebar" .}}{{end}}

به عنوان یک قاعده کلی، توصیه من این است که به عادت همیشه pipelining کردن dot هر زمان که یک قالب را با اعمال {{template}} یا {{block}} فراخوانی می‌کنید عادت کنید، مگر اینکه دلیل خوبی برای انجام ندادن آن داشته باشید.

فراخوانی متدها

اگر نوعی که بین تگ‌های {{ }} yield می‌کنید متدهایی روی آن تعریف شده باشد، می‌توانید این متدها را فراخوانی کنید (تا زمانی که صادر شده باشند و فقط یک مقدار — یا یک مقدار و یک خطا — برگردانند).

به عنوان مثال، فیلد ساختار .Snippet.Created ما نوع زیرین time.Time دارد، به این معنی که می‌توانید نام روز هفته را با فراخوانی متد Weekday() آن مانند این رندر کنید:

<span>{{.Snippet.Created.Weekday}}</span>

همچنین می‌توانید پارامترها را به متدها ارسال کنید. به عنوان مثال، می‌توانید از متد AddDate() برای افزودن شش ماه به یک زمان مانند این استفاده کنید:

<span>{{.Snippet.Created.AddDate 0 6 0}}</span>

توجه کنید که این نحو متفاوت از فراخوانی توابع در Go است — پارامترها در پرانتز قرار نمی‌گیرند و با یک کاراکتر فاصله (space) جدا می‌شوند، نه کاما.

نظرات HTML

در نهایت، پکیج html/template همیشه هر نظرات HTML که در قالب‌های خود شامل می‌کنید را حذف می‌کند، از جمله هر نظرات شرطی.

دلیل این کار کمک به جلوگیری از حملات XSS هنگام رندر محتوای پویا است. اجازه دادن به نظرات شرطی به معنای این است که Go همیشه نمی‌تواند پیش‌بینی کند که یک مرورگر چگونه نشانه‌گذاری در یک صفحه را تفسیر می‌کند، و بنابراین لزوماً نمی‌تواند همه چیز را به طور مناسب escape کند. برای حل این، Go به سادگی همه نظرات HTML را حذف می‌کند.