Let's Go پایه‌ها › قالب‌بندی HTML و وراثت (HTML Templating and Inheritance)
قبلی · فهرست · بعدی
فصل 2.8.

قالب‌بندی HTML و وراثت (HTML Templating and Inheritance)

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

02.08-01.png

بیایید با ایجاد یک فایل قالب (Template File) در ui/html/pages/home.tmpl شروع کنیم تا محتوای HTML برای صفحه اصلی را در خود جای دهد. به این صورت:

$ mkdir ui/html/pages
$ touch ui/html/pages/home.tmpl

و HTML زیر را اضافه کنید:

File: ui/html/pages/home.tmpl
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Home - Snippetbox</title>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <main>
            <h2>Latest Snippets</h2>
            <p>There's nothing to see here yet!</p>
        </main>
        <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
</html>

حالا که یک فایل قالب حاوی نشانه‌گذاری HTML برای صفحه اصلی ایجاد کرده‌ایم، سوال بعدی این است که چگونه می‌توانیم handler home خود را برای رندر کردن آن استفاده کنیم؟

برای این کار باید از بسته html/template Go استفاده کنیم، که مجموعه‌ای از توابع برای تجزیه (Parse) و رندر (Render) کردن ایمن قالب‌های HTML فراهم می‌کند. می‌توانیم از توابع این بسته برای تجزیه فایل قالب و سپس اجرا قالب استفاده کنیم.

من نشان خواهم داد. فایل cmd/web/handlers.go را باز کنید و کد زیر را اضافه کنید:

File: cmd/web/handlers.go
package main

import (
    "fmt"
    "html/template" // New import
    "log"           // New import
    "net/http"
    "strconv"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")

    // Use the template.ParseFiles() function to read the template file into a
    // template set. If there's an error, we log the detailed error message, use
    // the http.Error() function to send an Internal Server Error response to the
    // user, and then return from the handler so no subsequent code is executed.
    ts, err := template.ParseFiles("./ui/html/pages/home.tmpl")
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    // Then we use the Execute() method on the template set to write the
    // template content as the response body. The last parameter to Execute()
    // represents any dynamic data that we want to pass in, which for now we'll
    // leave as nil.
    err = ts.Execute(w, nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

...

چند نکته مهم در مورد این کد وجود دارد:

بنابراین، با این گفته، مطمئن شوید که در ریشه دایرکتوری پروژه خود هستید و برنامه را مجدداً راه‌اندازی کنید:

$ cd $HOME/code/snippetbox
$ go run ./cmd/web
2024/03/18 11:29:23 starting server on :4000

سپس http://localhost:4000 را در مرورگر وب خود باز کنید. باید ببینید که صفحه اصلی HTML به خوبی شکل گرفته است.

02.08-02.png

ترکیب قالب (Template Composition)

همانطور که صفحات بیشتری به برنامه وب خود اضافه می‌کنیم، برخی از نشانه‌گذاری‌های HTML مشترک و تکراری وجود خواهد داشت که می‌خواهیم در هر صفحه قرار دهیم — مانند هدر، ناوبری و متادیتا درون عنصر HTML <head>.

برای جلوگیری از تکرار و صرفه‌جویی در تایپ، ایده خوبی است که یک قالب پایه (Base) (یا اصلی (Master)) ایجاد کنیم که این محتوای مشترک را در خود جای دهد، که سپس می‌توانیم با نشانه‌گذاری خاص صفحه برای صفحات جداگانه ترکیب (Compose) کنیم.

یک فایل جدید ui/html/base.tmpl ایجاد کنید…

$ touch ui/html/base.tmpl

و نشانه‌گذاری زیر را اضافه کنید (که می‌خواهیم در هر صفحه ظاهر شود):

File: ui/html/base.tmpl
{{define "base"}}
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
    </head>
    <body>
        <header>
            <h1><a href='/'>Snippetbox</a></h1>
        </header>
        <main>
            {{template "main" .}}
        </main>
        <footer> Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
</html>
{{end}}

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

ما از عملیات {{define "base"}}...{{end}} به عنوان یک پوشش برای تعریف یک قالب نام‌گذاری شده (Named Template) به نام base استفاده می‌کنیم، که حاوی محتوایی است که می‌خواهیم در هر صفحه ظاهر شود.

در داخل این، از عملیات {{template "title" .}} و {{template "main" .}} برای نشان دادن اینکه می‌خواهیم قالب‌های نام‌گذاری شده دیگر (به نام title و main) را در یک مکان خاص در HTML فراخوانی کنیم، استفاده می‌کنیم.

حالا به فایل ui/html/pages/home.tmpl برگردید و آن را به‌روزرسانی کنید تا قالب‌های نام‌گذاری شده title و main را که حاوی محتوای خاص صفحه اصلی هستند، تعریف کنید.

File: ui/html/pages/home.tmpl
{{define "title"}}Home{{end}}

{{define "main"}}
    <h2>Latest Snippets</h2>
    <p>There's nothing to see here yet!</p>
{{end}}

پس از انجام این کار، مرحله بعدی این است که کد در handler home خود را به‌روزرسانی کنید تا هر دو فایل قالب را تجزیه کند، به این صورت:

File: cmd/web/handlers.go
package main
    
    ...
    
    func home(w http.ResponseWriter, r *http.Request) {
        w.Header().Add("Server", "Go")
    
        // Initialize a slice containing the paths to the two files. It's important
        // to note that the file containing our base template must be the *first*
        // file in the slice.
        files := []string{
            "./ui/html/base.tmpl",
            "./ui/html/pages/home.tmpl",
        }
    
        // Use the template.ParseFiles() function to read the files and store the
        // templates in a template set. Notice that we use ... to pass the contents 
        // of the files slice as variadic arguments.
        ts, err := template.ParseFiles(files...)
        if err != nil {
            log.Print(err.Error())
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
            return
        }
    
        // Use the ExecuteTemplate() method to write the content of the "base" 
        // template as the response body.
        err = ts.ExecuteTemplate(w, "base", nil)
        if err != nil {
            log.Print(err.Error())
            http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        }
    }
    
    ...

بنابراین اکنون، به جای اینکه مستقیماً HTML داشته باشیم، مجموعه قالب ما حاوی 3 قالب نام‌گذاری شده — base، title و main است. ما از متد ExecuteTemplate() استفاده می‌کنیم تا به Go بگوییم که به طور خاص می‌خواهیم با استفاده از محتوای قالب base پاسخ دهیم (که به نوبه خود قالب‌های title و main ما را فراخوانی می‌کند).

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

ترکیب قالب (Template Composition)

همانطور که صفحات بیشتری به برنامه وب خود اضافه می‌کنیم، برخی از نشانه‌گذاری‌های HTML مشترک و تکراری وجود خواهد داشت که می‌خواهیم در هر صفحه قرار دهیم — مانند هدر، ناوبری و متادیتا درون عنصر HTML <head>.

برای جلوگیری از تکرار و صرفه‌جویی در تایپ، ایده خوبی است که یک قالب پایه (Base) (یا اصلی (Master)) ایجاد کنیم که این محتوای مشترک را در خود جای دهد، که سپس می‌توانیم با نشانه‌گذاری خاص صفحه برای صفحات جداگانه ترکیب (Compose) کنیم.

ما از عملیات {{define "base"}}...{{end}} به عنوان یک پوشش برای تعریف یک قالب نام‌گذاری شده (Named Template) به نام base استفاده می‌کنیم، که حاوی محتوایی است که می‌خواهیم در هر صفحه ظاهر شود.

در داخل این، از عملیات {{template "title" .}} و {{template "main" .}} برای نشان دادن اینکه می‌خواهیم قالب‌های نام‌گذاری شده دیگر (به نام title و main) را در یک مکان خاص در HTML فراخوانی کنیم، استفاده می‌کنیم.

حالا به فایل ui/html/pages/home.tmpl برگردید و آن را به‌روزرسانی کنید تا قالب‌های نام‌گذاری شده title و main را که حاوی محتوای خاص صفحه اصلی هستند، تعریف کنید.

File: ui/html/pages/home.tmpl
{{define "title"}}Home{{end}}

{{define "main"}}
    <h2>Latest Snippets</h2>
    <p>There's nothing to see here yet!</p>
{{end}}

پس از انجام این کار، مرحله بعدی این است که کد در handler home خود را به‌روزرسانی کنید تا هر دو فایل قالب را تجزیه کند، به این صورت:

File: cmd/web/handlers.go
package main

import (
    "fmt"
    "html/template" // New import
    "log"           // New import
    "net/http"
    "strconv"
)

func home(w http.ResponseWriter, r *http.Request) {
    w.Header().Add("Server", "Go")

    // Include the navigation partial in the template files.
    files := []string{
        "./ui/html/base.tmpl",
        "./ui/html/partials/nav.tmpl",
        "./ui/html/pages/home.tmpl",
    }

    ts, err := template.ParseFiles(files...)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    }
}

...

پس از راه‌اندازی مجدد سرور، قالب base باید اکنون قالب nav را فراخوانی کند و صفحه اصلی شما باید به این شکل باشد:

02.08-03.png

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

عملیات بلوک

در کد بالا ما از عملیات {{template}} برای فراخوانی یک قالب از قالب دیگر استفاده کرده‌ایم. اما Go همچنین یک عملیات {{block}}...{{end}} فراهم می‌کند که می‌توانید به جای آن استفاده کنید. این مانند عملیات {{template}} عمل می‌کند، به جز اینکه به شما اجازه می‌دهد محتوای پیش‌فرضی را مشخص کنید اگر قالب فراخوانی شده در مجموعه قالب فعلی وجود نداشته باشد.

در زمینه یک برنامه وب، این زمانی مفید است که می‌خواهید محتوای پیش‌فرضی (مانند یک نوار کناری) ارائه دهید که صفحات جداگانه می‌توانند در صورت نیاز به صورت موردی آن را نادیده بگیرند.

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

{{define "base"}}
    <h1>An example template</h1>
    {{block "sidebar" .}}
        <p>My default sidebar content</p>
    {{end}}
{{end}}

اما — اگر بخواهید — نیازی نیست که هیچ محتوای پیش‌فرضی بین عملیات {{block}} و {{end}} قرار دهید. در این صورت، قالب فراخوانی شده به صورت ‘اختیاری’ عمل می‌کند. اگر قالب در مجموعه قالب وجود داشته باشد، رندر خواهد شد. اما اگر وجود نداشته باشد، هیچ چیزی نمایش داده نخواهد شد.

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

اصطلاح فارسی معادل انگلیسی توضیح
قالب‌بندی HTML HTML Templating فرآیند ایجاد قالب‌های HTML که می‌توانند با داده‌های پویا پر شوند
وراثت Inheritance مکانیزمی که در آن یک قالب می‌تواند از قالب دیگر محتوا را به ارث ببرد
فایل قالب Template File فایلی که حاوی ساختار HTML و دستورالعمل‌های قالب‌بندی است
تجزیه Parse فرآیند خواندن و تفسیر محتوای یک فایل قالب
رندر Render فرآیند تبدیل یک قالب به خروجی HTML نهایی
قالب پایه Base Template قالبی که ساختار اصلی و مشترک صفحات را تعریف می‌کند
قالب اصلی Master Template مترادف با قالب پایه، قالبی که سایر قالب‌ها از آن ارث می‌برند
ترکیب Compose فرآیند ترکیب چندین قالب برای ایجاد یک صفحه کامل
قالب نام‌گذاری شده Named Template قالبی که با یک نام مشخص تعریف شده و می‌تواند در جاهای دیگر فراخوانی شود
جزئیات Partials بخش‌های کوچک و قابل استفاده مجدد از کد HTML که می‌توانند در چندین قالب استفاده شوند