راهنمای استفاده از تگ های جنگو برای طراحی قالب
چطور با تگ های جنگو قالب های حرفهای، امن و قابل نگهداری بسازیم؟
تگ های جنگو (Django Template Tags) ستون فقرات هر قالبی هستند که در این فریمورک ساخته میشود؛ اگر تا حالا با قالب های جنگو کار کرده باشید، حتماً چیزهایی مثل {% if %} و {% for %} را دیدهاید، اما اینها فقط نوک کوه یخ هستند. در پروژههای واقعی بارها دیدهام که یک توسعهدهنده کل منطق نمایش را داخل ویو مینویسد و قالب را به یک لایه بیخاصیت تبدیل میکند؛ در حالی که تگ های جنگو دقیقاً برای این طراحی شدهاند که منطق نمایش را در جای درست خودش نگه دارند. در این راهنما از ساختار پایه تگ ها شروع میکنیم، تا تگ های کنترلی، حلقه، وراثت قالب، تگ های سفارشی و در نهایت نکات کارایی و امنیت پیش میرویم. هدف این است که بعد از خواندن این متن، بتوانید هر قالبی را با اعتماد به نفس طراحی کنید و بدانید کدام تگ کجا معنا پیدا میکند.
تگ قالب چیست و چرا اهمیت دارد؟
در جنگو (Django) سیستم قالب بهصورت عمدی محدود طراحی شده است تا نتوانید منطق تجاری برنامه را داخل قالب بنویسید. این محدودیت، یک تصمیم معماری آگاهانه است: ویو (View) داده را آماده میکند و قالب فقط آن را نمایش میدهد. تگ های جنگو در واقع همان ابزارهایی هستند که این لایه نمایش را از یک فایل HTML ساده به یک موتور پویا تبدیل میکنند، ولی هرگز اجازه نمیدهند کد پایتون دلخواه داخل قالب اجرا شود.
اگر با توسعه قالب وردپرس آشنا باشید، احتمالاً میدانید که در PHP میتوانید هر کدی را داخل فایل قالب بنویسید. این آزادی در پروژههای کوچک جذاب است، ولی در پروژههای بزرگ به کابوس نگهداری تبدیل میشود. جنگو از ابتدا این مسیر را نبست، بلکه آن را به شکل کنترلشدهای باز کرد: شما فقط از طریق تگ ها و فیلترها میتوانید روی دادهها عملیات انجام دهید. برای درک عمیقتر این تفاوت، پیشنهاد میکنم راهنمای ویو و URL در جنگو را هم ببینید که پیشنیاز ذهنی خوبی برای این بحث است.
قالب خوب، قالبی است که وقتی شش ماه بعد به آن نگاه میکنید، بدون خواندن ویو بفهمید چه چیزی و کجا نمایش داده میشود. تگ های جنگو دقیقاً همین خوانایی را ممکن میکنند.
تگ های جنگو در برابر فیلترها
یک نکته که خیلی از تازهکارها با هم قاطی میکنند، تفاوت «تگ» و «فیلتر» است. تگ ها با {% %} نوشته میشوند و کارهای کنترلی یا ساختاری انجام میدهند، مثل حلقه زدن یا تعریف بلوک. فیلترها با | به یک متغیر اعمال میشوند و مقدار را تغییر میدهند، مثل {{ name|upper }}. اگر تفاوت این دو را دقیق نمیدانید، پیشنهاد میکنم ابتدا راهنمای فیلترهای قالب در جنگو را بخوانید و بعد به این مقاله برگردید، چون در ادامه فرض میکنیم این تفاوت برایتان روشن است.
برای اینکه درک بهتری داشته باشید، به جدول زیر نگاه کنید. این جدول کوتاه، خودش تقریباً تمام چیزی است که در بخش های بعدی با جزئیات باز میکنیم.
| ویژگی | تگ | فیلتر |
|---|---|---|
| نحو نوشتاری | {% %} | {{ value|filter }} |
| نقش | کنترل جریان، وراثت، ساختار | تغییر یا فرمت مقدار |
| مثال | {% if user.is_staff %} | {{ user.email|lower }} |
| امکان تعریف سفارشی | بله | بله |
ساختار پایه: تفاوت {{ }} و {% %}
هر قالب جنگو از سه نوع نشانهگذاری تشکیل شده است. اولین مورد {{ variable }} است که یک متغیر را نمایش میدهد. دومی {% tag %} است که یک عمل کنترلی انجام میدهد. سومی {# comment #} است که کامنت یکخطی میسازد. این سه، پایهایترین چیزی است که باید بلد باشید.
یک نکته مهم که در تنظیمات حرفهای جنگو هم روی آن تأکید میشود این است که اگر متغیری در context وجود نداشته باشد، جنگو بهطور پیشفرض آن را خالی نمایش میدهد، نه خطا. این رفتار در توسعه راحت است، ولی در محیط تولید میتواند باگ های خاموش بسازد. برای همین توصیه میکنم در قالب های حساس از {% if variable %} استفاده کنید تا مطمئن شوید داده واقعاً وجود دارد.
<p>سلام {{ user.first_name }}</p>
{% if user.is_authenticated %}
<a href="{% url 'logout' %}">خروج</a>
{% endif %}
{# این یک کامنت است و در خروجی نهایی دیده نمیشود #}
هر نشانهگذاری در قالب جنگو یک معنی مشخص دارد. اگر
{{ }}و{% %}را با هم اشتباه بگیرید، خروجی یا خالی است یا خطای نحوی میگیرید.
تگ های کنترلی: if, elif, else, ifchanged
تگ {% if %} سادهترین و پرکاربردترین تگ کنترلی است. ولی چیزی که خیلی از توسعهدهندهها نمیدانند این است که جنگو در داخل if مجموعهای از عملگرها را پشتیبانی میکند که در پایتون هم وجود دارند: and, or, not, in, ==, !=, >, <, >=, <=. ترکیب اینها میتواند منطق نمایش را بدون نوشتن حتی یک خط پایتون، دقیق کند.
مثال زیر یک الگوی رایج را نشان میدهد: نمایش دکمه بر اساس سطح دسترسی کاربر.
{% if user.is_authenticated and user.is_staff %}
<a class="btn" href="{% url 'admin:index' %}">پنل مدیریت</a>
{% elif user.is_authenticated %}
<a class="btn" href="{% url 'profile' %}">پروفایل من</a>
{% else %}
<a class="btn" href="{% url 'login' %}">ورود</a>
{% endif %}
تگ {% ifchanged %} کمتر شناخته شده است ولی در گزارشها معجزه میکند. فرض کنید لیستی از سفارشها دارید که همه در یک تاریخ ثبت شدهاند. اگر بخواهید تاریخ را فقط یک بار در بالای هر گروه نمایش دهید، ifchanged دقیقاً همین کار را میکند. برای اینکه با ساختار دادهای که قرار است روی آن حلقه بزنید راحتتر باشید، نگاهی هم به راهنمای مدل و ORM جنگو بیندازید.
تگ های حلقه: for, empty و متغیر forloop
تگ {% for %} پرکاربردترین تگ بعد از if است. ولی چیزی که خیلی از توسعهدهندهها بهدرستی استفاده نمیکنند، متغیر خودکار forloop است که داخل هر حلقه در دسترس قرار میگیرد و اطلاعات مفیدی مثل شماره تکرار، اولین/آخرین بودن، و حلقه والد را میدهد.
| متغیر | معنی |
|---|---|
forloop.counter | شماره تکرار جاری از ۱ |
forloop.counter0 | شماره تکرار جاری از ۰ |
forloop.revcounter | شمارش معکوس تا آخر |
forloop.first | در اولین تکرار True |
forloop.last | در آخرین تکرار True |
forloop.parentloop | ارجاع به حلقه بیرونی در حلقههای تودرتو |
استفاده از {% empty %} هم نکتهای است که خیلیها فراموش میکنند. اگر لیست خالی باشد و empty نداشته باشید، کاربر یک صفحه کاملاً خالی میبیند. با empty میتوانید پیام مناسب نشان دهید.
<ul>
{% for post in posts %}
<li class="{% if forloop.first %}first{% endif %}">
{{ forloop.counter }}. <a href="{% url 'post_detail' post.slug %}">{{ post.title }}</a>
</li>
{% empty %}
<li>هنوز پستی منتشر نشده است.</li>
{% endfor %}
</ul>
نکتهای که در پروژههای واقعی زیاد دیدهام: فراموش کردن forloop.last وقتی باید کاما بین آیتمها بگذارید. یک بار دیگر به جدول بالا نگاه کنید و مطمئن شوید این متغیرها را میشناسید.
وراثت قالب: extends, block, include
بدون وراثت قالب، هر صفحه باید کل ساختار HTML را تکرار کند و این یعنی فاجعه نگهداری. جنگو با {% extends %} و {% block %} یک سیستم وراثت تمیز ارائه میدهد. قاعده طلایی این است که {% extends %} باید اولین تگ غیرکامنت قالب باشد؛ اگر قبل از آن حتی یک فضای خالی اضافه بنویسید، جنگو خطا میدهد.
{# templates/base.html #}
<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<title>{% block title %}سایت من{% endblock %}</title>
</head>
<body>
<header>{% block header %}{% endblock %}</header>
<main>{% block content %}{% endblock %}</main>
<footer>{% block footer %}{% endblock %}</footer>
</body>
</html>
و در قالب فرزند:
{% extends "base.html" %}
{% block title %}صفحه تماس{% endblock %}
{% block content %}
<h1>تماس با ما</h1>
<p>از طریق فرم زیر پیام بگذارید.</p>
{% endblock %}
تفاوت {% include %} با {% extends %} در این است که include یک قالب را در جای دیگری «کپی» میکند، در حالی که extends یک رابطه ارثبری میسازد. برای قطعات کوچک مثل نوار کناری یا فرم جستجو از include استفاده کنید. برای فرم ها که معمولاً به ویو و URL نیاز دارند، راهنمای فرم ها در جنگو نکات تکمیلی خوبی دارد.
تگ های کاربردی: url, static, csrf_token, with
تگ {% url %} یکی از مهمترین تگ های جنگو است که هرگز نباید URL را دستی در قالب بنویسید. اگر مسیر یک ویو تغییر کند، همه قالبهایی که از url استفاده کردهاند بهطور خودکار بهروز میشوند. این تگ با نام URL کار میکند و آرگومان میپذیرد:
<a href="{% url 'article_detail' slug=post.slug %}">{{ post.title }}</a>
تگ {% csrf_token %} در هر فرم POST اجباری است. اگر آن را فراموش کنید، جنگو درخواست را با خطای ۴۰۳ رد میکند. این تگ یک فیلد مخفی در فرم قرار میدهد که توکن CSRF را نگه میدارد. در کنار این، {% static %} برای لینک دادن به فایلهای استاتیک (CSS، JS، تصاویر) استفاده میشود و اجازه میدهد مسیرها را در راهنمای فایل های استاتیک یکجا مدیریت کنید.
{% load static %}
<link rel="stylesheet" href="{% static 'css/app.css' %}">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">ارسال</button>
</form>
تگ {% with %} برای تعریف یک نام کوتاه جهت استفاده مکرر از یک عبارت طولانی یا سنگین است. این تگ هم خوانایی را بالا میبرد و هم در مواردی که عبارت شامل کوئری است، از اجرای مکرر جلوگیری میکند:
{% with total=cart.items.count %}
<p>{{ total }} کالا در سبد خرید شما</p>
{% endwith %}
تگ های پیشرفته: cycle, regroup, widthratio, templatetag
تگ {% cycle %} در جدولهای راه راه یا رنگبندی متناوب کاربرد دارد. بهجای نوشتن منطق پایتونی برای تعیین رنگ ردیفها، کافی است دو مقدار را به cycle بدهید و بگذارید خودش بچرخاند:
{% for item in items %}
<tr class="{% cycle 'odd' 'even' %}">
<td>{{ item.name }}</td>
</tr>
{% endfor %}
تگ {% regroup %} یک لیست تخت را بر اساس یک ویژگی گروهبندی میکند. فرض کنید لیست محصولات را دارید و میخواهید بر اساس دسته نمایش دهید. بهجای اینکه در ویو این کار را بکنید، میتوانید در قالب از regroup استفاده کنید. توجه داشته باشید که لیست ورودی باید بر اساس همان ویژگی مرتب شده باشد، وگرنه نتیجه درست نخواهد بود.
{% regroup products by category as grouped %}
{% for group in grouped %}
<h3>{{ group.grouper }}</h3>
<ul>
{% for p in group.list %}
<li>{{ p.name }}</li>
{% endfor %}
</ul>
{% endfor %}
تگ {% widthratio %} نسبت را محاسبه میکند و برای نمایش درصد در نمودارهای ساده کاربرد دارد. مثلاً برای نمایش درصد پیشرفت یک پروژه:
{% widthratio done total 100 %}%
در نهایت، تگ {% templatetag %} برای چاپ خود نشانههای قالب استفاده میشود. اگر بخواهید متن آموزشی درباره جنگو بنویسید و نیاز دارید {% را در خروجی نشان دهید، از {% templatetag openblock %} استفاده کنید. و اگر کل بلوکی را میخواهید خام نگه دارید (مثلاً کد جاوااسکریپت با {{ }} که نباید تفسیر شود)، از {% verbatim %} استفاده کنید. دانستن این نکات وقتی قالب جنگو را با فریمورکهای سمت کلاینت مثل Vue یا Alpine ترکیب میکنید، حیاتی است. برای درک بهتر کار با مبانی HTML و CSS پیشنهاد میکنم اول آن را مرور کنید.
اگر مجبورید منطق را در قالب بنویسید، اول از خودتان بپرسید چرا این منطق در ویو نیست. در نود و نه درصد موارد، جواب این است که میتواند باشد.
تگ های سفارشی: simple_tag و inclusion_tag
جنگو امکان تعریف تگ های سفارشی را فراهم میکند، ولی این کار باید آخرین گزینه باشد. تگ سفارشی زمانی معنا دارد که واقعاً چیزی را میخواهید در چند قالب تکرار کنید و منطقش ساده است. برای ساختاردهی درست این کار، راهنمای تگ های سفارشی در جنگو را جدا نوشتهام، ولی در اینجا خلاصهای میآورم.
ساختار پوشه این است:
myapp/
templatetags/
__init__.py
my_tags.py
و داخل my_tags.py:
from django import template
from django.utils.safestring import mark_safe
register = template.Library()
@register.simple_tag
def current_year():
from django.utils import timezone
return timezone.now().year
@register.inclusion_tag("partials/user_card.html")
def user_card(user):
return {"user": user}
برای استفاده از این تگ ها، ابتدا باید در قالب با {% load my_tags %} آنها را بارگذاری کنید. simple_tag یک مقدار برمیگرداند و inclusion_tag یک قالب کوچک را رندر میکند. اگر تگ شما HTML برمیگرداند، دقت کنید که بهطور پیشفرض escape میشود. برای برگرداندن HTML خام باید از mark_safe استفاده کنید، ولی این کار خطرناک است و فقط با دادههای تحت کنترل خودتان باید انجام شود. در راهنمای امنیت جنگو درباره XSS و escape بهتفصیل توضیح دادهام.
یک نکتهای که در راهنمای context processor هم گفتم: اگر تگ شما نیاز به دسترسی به request دارد، از takes_context=True استفاده کنید. این در تگ هایی که باید زبان یا کاربر را ببینند، ضروری است.
امنیت در تگ ها: autoescape, safe, escape
جنگو بهطور پیشفرض همه متغیرها را escape میکند. یعنی اگر کاربری در فیلد نامش <script>alert(1)</script> بگذارد، شما آن را بهصورت متن میبینید، نه کد اجراشده. این رفتار پیشفرض دقیقاً همان چیزی است که جلوی XSS (Cross-Site Scripting) را میگیرد.
ولی گاهی لازم است HTML را خام نمایش دهید. مثلاً محتوایی که از یک ادیتور WYSIWYG آمده. در این حالت {{ content|safe }} را استفاده میکنید. توجه کنید که safe یک شمشیر دولبه است: اگر محتوا از کاربر ناشناس آمده باشد، استفاده از safe یک درباز برای XSS است. برای همین در پروژههای واقعی حتماً قبل از ذخیره، محتوا را با کتابخانهای مثل bleach پاکسازی کنید.
تگ {% autoescape off %} هم escape را برای کل بلوک خاموش میکند. از این تگ فقط در جاهایی استفاده کنید که کاملاً به منبع داده اعتماد دارید. بهطور کلی قاعدهای که در پروژهها رعایت میکنم این است: هرگز autoescape off یا safe را روی دادهای که از کاربر میآید اعمال نکنید.
درباره این موضوع، اطلاعات دقیقتری در ویکیپدیا درباره XSS موجود است که پیشنهاد میکنم مطالعه کنید.
کارایی و بارگذاری درست کتابخانهها
هر {% load %} یک کتابخانه تگ را وارد قالب میکند و اگر زیاد باشد، میتواند زمان پارس را بالا ببرد. قاعدهای که در پروژههای بزرگ رعایت میکنم این است که فقط کتابخانههایی را بارگذاری کنم که واقعاً لازم است و در سطح قالب والد بارگذاری میکنم، نه در هر قطعه.
نکته دوم درباره کوئریهای پنهان در قالب است. اگر در قالب چیزی مثل {{ post.author.profile.bio }} مینویسید و این روابط select_related یا prefetch_related نشده باشند، هر دسترسی یک کوئری جدا میزند. مشکل N+1 در قالب دقیقاً از همینجا میآید. برای درک عمیق این موضوع، پیشنهاد میکنم راهنمای ORM را مطالعه کنید. همچنین اگر پروژهتان در حال رشد است، مبانی پایتون را جدی بگیرید؛ خیلی از باگ های کارایی ریشه در درک نادرست ساختارهای داده دارد.
سوم، از تگ های سنگین مثل {% regroup %} روی لیستهای بزرگ پرهیز کنید. اگر لیست شما هزاران آیتم دارد، این کار را در ویو با ابزارهای بهینهتر انجام دهید.
اشتباهات رایجی که در پروژهها دیدهام
اشتباه اول، گذاشتن منطق تجاری در قالب است. اگر در قالب شما بیش از دو سطح شرط تودرتو دارید، احتمالاً باید آن منطق را به ویو یا به یک متد روی مدل منتقل کنید.
اشتباه دوم، استفاده از {{ variable }} بدون چک کردن وجود آن است. اگر متغیر ممکن است تعریف نشده باشد، از {% if variable %} استفاده کنید یا مقدار پیشفرض بدهید.
اشتباه سوم، فراموش کردن {% csrf_token %} در فرم POST است که باعث میشود فرم بهطور مرموزی کار نکند.
اشتباه چهارم، استفاده از |safe بدون بررسی منبع داده است که میتواند به XSS منجر شود.
اشتباه پنجم، بارگذاری مکرر کتابخانهها در قالب های جزئی است که بار پارس را زیاد میکند.
اشتباه ششم، فراموش کردن {% empty %} در حلقههاست که تجربه کاربری بدی میسازد.
اشتباه هفتم، استفاده از {% include %} در حلقهای که هزاران بار اجرا میشود. include در هر تکرار یک بار پارس و رندر میشود و برای مقادیر بزرگ بسیار کند است. در این حالت بهجای include از یک تگ سفارشی یا از انتقال منطق به ویو استفاده کنید.
پرسش های پرتکرار درباره تگ های جنگو
آیا میتوانم پایتون دلخواه داخل قالب اجرا کنم؟
خیر. این یک تصمیم طراحی جنگو است. اگر نیاز به منطق پیچیده دارید، باید در ویو، مدل، یا بهصورت تگ سفارشی آن را پیادهسازی کنید. تلاش برای دور زدن این محدودیت معمولاً به کد غیرقابل نگهداری منجر میشود.
تفاوت {% include %} و {% extends %} در چیست؟
extends وراثت قالب است و برای ساختار کلی صفحه استفاده میشود. include برای درج یک قطعه قالب در جای دیگر است و رابطه وراثتی ندارد.
چرا {% extends %} خطا میدهد؟
چون اولین تگ غیرکامنت قالب نیست. اگر فاصله یا متن اضافه قبل از آن باشد، جنگو نمیپذیرد. آن را اولین خط قالب قرار دهید.
چطور از بارگذاری مکرر کتابخانهها جلوگیری کنم؟
کتابخانهها را در قالب والد یک بار بارگذاری کنید. در جنگو ۳.۱ به بعد، {% load %} در قالب والد به فرزندان هم منتقل میشود و نیازی به تکرار نیست.
آیا استفاده از تگ سفارشی همیشه توصیه میشود؟
خیر. اگر تگ شما منطق تجاری دارد، بهتر است در ویو باشد. تگ سفارشی برای قالببندی، نمایش مقادیر محاسبهشده سبک، یا چیزهایی مثل نمایش تاریخ خاص مناسب است.
چه زمانی از {% with %} استفاده کنم؟
وقتی یک عبارت را چند بار استفاده میکنید یا وقتی عبارت شامل کوئری است و نمیخواهید چند بار اجرا شود. توجه کنید که with روی کوئریهای تنبل بهتنهایی کمک نمیکند، ولی حداقل خوانایی را بالا میبرد.
آیا تگ ها روی کارایی سایت تأثیر دارند؟
بله، ولی معمولاً نه در حدی که نگرانکننده باشد. بیشترین تأثیر روی کارایی از کوئریهای پنهان و include های مکرر میآید، نه از خود تگ ها.
چطور یک تگ برای نمایش تاریخ شمسی بسازم؟
با استفاده از simple_tag و یک کتابخانه مثل jdatetime یا django-jalali. تگ شما مقدار تاریخ میلادی را میگیرد و معادل شمسی برمیگرداند. چون این کار در چندین قالب تکرار میشود، تگ سفارشی انتخاب درستی است.
یک نگاه پایانی به انتخاب درست تگ
تگ های جنگو ابزارهایی هستند که اگر با نیت درست استفاده شوند، قالب شما را از یک فایل ثابت به یک لایه نمایش پویا و قابل نگهداری تبدیل میکنند. اصلیترین قاعدهای که در طول این سالها به آن رسیدهام این است: هر چیزی که «چیستی داده» را تعیین میکند، باید در ویو باشد و هر چیزی که «چطور نمایش داده میشود» را میتوان به قالب سپرد. تگ های جنگو دقیقاً در همین مرز کار میکنند. اگر این مرز را رعایت کنید، قالب های شما نه فقط تمیزتر، بلکه سریعتر و امنتر خواهند بود.
پیشنهاد میکنم در پروژه بعدی، قبل از افزودن هر تگ سفارشی، از خودتان بپرسید: آیا این منطق میتواند در ویو باشد؟ اگر جواب بله است، همانجا بماند. اگر نه، تگ سفارشی انتخاب درستی است. و اگر جایی از این راهنما برایتان مبهم ماند یا در پروژهای با مورد عجیبی روبرو شدید که تگ های جنگو رفتار غیرمنتظره داشتند، تجربهتان را بنویسید. برای من جالب است بدانم کدام تگ بیشترین وقت شما را گرفت و چطور آن را حل کردید؛ مخصوصاً اگر راهحل جایگزینی پیدا کردهاید که میتواند برای خواننده بعدی مفید باشد.