قالببندی HTML و وراثت (HTML Templating and Inheritance)
بیایید کمی زندگی به پروژه بدهیم و یک صفحه اصلی مناسب برای برنامه وب Snippetbox خود توسعه دهیم. در طول چند فصل بعدی، به سمت ایجاد صفحهای که به این شکل است، کار خواهیم کرد:

بیایید با ایجاد یک فایل قالب (Template File) در ui/html/pages/home.tmpl شروع کنیم تا محتوای HTML برای صفحه اصلی را در خود جای دهد. به این صورت:
$ mkdir ui/html/pages $ touch ui/html/pages/home.tmpl
و HTML زیر را اضافه کنید:
<!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 را باز کنید و کد زیر را اضافه کنید:
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) } } ...
چند نکته مهم در مورد این کد وجود دارد:
مسیر فایلی که به تابع
template.ParseFiles()میدهید باید یا نسبت به دایرکتوری کاری فعلی شما باشد یا یک مسیر مطلق. در کد بالا، من مسیر را نسبت به ریشه دایرکتوری پروژه قرار دادهام.اگر هر یک از توابع
template.ParseFiles()یاts.Execute()خطایی برگردانند، ما پیام خطای دقیق را ثبت میکنیم و سپس از تابعhttp.Error()برای ارسال پاسخ به کاربر استفاده میکنیم.http.Error()یک تابع کمکی سبک است که یک پیام خطای متنی ساده و یک کد وضعیت HTTP خاص را به کاربر ارسال میکند (در کد ما پیام"خطای داخلی سرور"و کد وضعیت500، که توسط ثابتhttp.StatusInternalServerErrorنشان داده میشود، ارسال میکنیم). به طور مؤثر، این بدان معناست که اگر خطایی وجود داشته باشد، کاربر پیامخطای داخلی سروررا در مرورگر خود خواهد دید، اما پیام خطای دقیق در پیامهای لاگ برنامه ثبت خواهد شد.
بنابراین، با این گفته، مطمئن شوید که در ریشه دایرکتوری پروژه خود هستید و برنامه را مجدداً راهاندازی کنید:
$ cd $HOME/code/snippetbox $ go run ./cmd/web 2024/03/18 11:29:23 starting server on :4000
سپس http://localhost:4000 را در مرورگر وب خود باز کنید. باید ببینید که صفحه اصلی HTML به خوبی شکل گرفته است.
ترکیب قالب (Template Composition)
همانطور که صفحات بیشتری به برنامه وب خود اضافه میکنیم، برخی از نشانهگذاریهای HTML مشترک و تکراری وجود خواهد داشت که میخواهیم در هر صفحه قرار دهیم — مانند هدر، ناوبری و متادیتا درون عنصر HTML <head>.
برای جلوگیری از تکرار و صرفهجویی در تایپ، ایده خوبی است که یک قالب پایه (Base) (یا اصلی (Master)) ایجاد کنیم که این محتوای مشترک را در خود جای دهد، که سپس میتوانیم با نشانهگذاری خاص صفحه برای صفحات جداگانه ترکیب (Compose) کنیم.
یک فایل جدید ui/html/base.tmpl ایجاد کنید…
$ touch 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 را که حاوی محتوای خاص صفحه اصلی هستند، تعریف کنید.
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
پس از انجام این کار، مرحله بعدی این است که کد در handler home خود را بهروزرسانی کنید تا هر دو فایل قالب را تجزیه کند، به این صورت:
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 را که حاوی محتوای خاص صفحه اصلی هستند، تعریف کنید.
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
پس از انجام این کار، مرحله بعدی این است که کد در handler home خود را بهروزرسانی کنید تا هر دو فایل قالب را تجزیه کند، به این صورت:
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 را فراخوانی کند و صفحه اصلی شما باید به این شکل باشد:
اطلاعات اضافی
عملیات بلوک
در کد بالا ما از عملیات {{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 که میتوانند در چندین قالب استفاده شوند |