تفاوت Middleware و Context Processor در جنگو؛ کدام را کجا استفاده کنیم؟
چرا بعضی توسعهدهندگان این دو را با هم قاطی میکنند و اشتباه گرفتن آنها چه هزینهای برای معماری پروژه دارد؟
تفاوت Middleware و Context Processor در جنگو، اولین بار که با آنها آشنا میشوید شبیه یک نکتهی دانشگاهی بهنظر میرسد؛ ولی وقتی پروژهی شما به چند ده ویو و چند صد قالب میرسد، همین تفاوت کوچک تعیین میکند که کد شما تمیز بماند یا به یک آشفتگی غیرقابل نگهداری تبدیل شود.
Middleware در جنگو دقیقاً چه چیزی است؟
Middleware در جنگو یک لایهی زیرساختی است که بین وبسرور و سیستم مسیریابی قرار میگیرد. این لایه، روی هر درخواست HTTP اجرا میشود، صرفنظر از اینکه آن درخواست به کدام ویو میرود یا کدام قالب رندر میشود.
Middleware در دو نقطه از چرخهی درخواست-پاسخ فرصت دستکاری دارد: قبل از رسیدن درخواست به ویو و بعد از برگشت پاسخ از ویو. این ویژگی، آن را برای کارهای زیرساختی مثل احراز هویت، مدیریت session، محافظت CSRF (Cross-Site Request Forgery)، هدرهای امنیتی، لاگ درخواستها، فشردهسازی پاسخ و ردیابی بازدیدکننده ایدهآل میکند.
Middleware جایی است که تصمیمهای زیرساختی گرفته میشوند. اگر منطق شما به محتوای پاسخ یا به context قالب وابسته است، آنجا جای درستی برای آن نیست.
جنگو خودش چند middleware پیشفرض دارد که همهشان کارهای زیرساختی انجام میدهند. برای مثال، SessionMiddleware تنها وظیفهاش این است که قبل از اجرای ویو، request.session را بسازد و بعد از اجرای ویو، تغییرات را ذخیره کند. این سطح از تمرکز، دقیقاً همان الگویی است که در مقالهی نوشتن Middleware سفارشی در جنگو بهطور عمیق بررسی شده است. اگر میخواهید درک درستی از چرخهی کامل درخواست داشته باشید، آن مقاله نقطهی شروع مناسبی است.
ویژگی مهم Middleware این است که در سطح زیرساخت کار میکند، نه در سطح ارائه. یعنی Middleware نه به قالبها دسترسی دارد و نه در آنها اجرا میشود. این محدودیت، ظاهراً یک ضعف است، ولی در واقع یک حفاظت معماری است: Middleware شما را مجبور میکند که کارهای زیرساختی را از کارهای نمایشی جدا کنید.
Context Processor در جنگو چه چیزی است؟
Context Processor در جنگو یک تابع پایتونی است که یک دیکشنری برمیگرداند. این دیکشنری، قبل از رندر هر قالب، به context قالب اضافه میشود. یعنی هر متغیری که در این دیکشنری باشد، در تمام قالبها بهطور خودکار در دسترس است.
دقیقاً مثل Middleware، Context Processor هم روی هر درخواست اجرا میشود، ولی نه در چرخهی HTTP، بلکه در لحظهی رندر قالب. یعنی اگر ویو شما یک قالب رندر نکند (مثلاً یک JsonResponse برگرداند)، Context Processor اجرا نمیشود.
# config/context_processors.py
def site_settings(request):
from django.conf import settings
return {
"SITE_NAME": settings.SITE_NAME,
"SITE_URL": settings.SITE_URL,
"DEBUG": settings.DEBUG,
}
و در settings.py، این تابع را به لیست context_processors اضافه میکنید:
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [BASE_DIR / "templates"],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"config.context_processors.site_settings", # ← اینجا
],
},
},
]
حالا در هر قالبی، {{ SITE_NAME }} در دسترس است، بدون اینکه ویو آن را پاس داده باشد.
جنگو خودش چند Context Processor پیشفرض دارد. django.contrib.auth.context_processors.auth که user و perms را به context اضافه میکند، نمونهی کلاسیک است. این Context Processor، همان چیزی است که به شما اجازه میدهد در قالب بنویسید {% if user.is_authenticated %}. اگر با الگوی ساخت پنل ادمین با کارتهای quick view آشنا باشید، میدانید که این ابزار در پنلهای ادمین سفارشی هم بسیار پرکاربرد است.
نکتهی حیاتی: Context Processor فقط زمانی معنا دارد که بخواهید دادهای را در همهی قالبها بهطور خودکار داشته باشید. اگر یک متغیر فقط در یک یا دو قالب لازم است، آن را در ویو مربوطه پاس بدهید، نه در Context Processor.
Context Processor یک ابزار برای دادههای سراسری در سطح قالب است، نه برای جایگزینی ویو. اگر شروع به پر کردن آن با منطقهای مختلف کنید، بهسرعت به یک گلوگاه پنهان تبدیل میشود.
تفاوت بنیادین؛ جایی که همه چیز روشن میشود
تفاوت واقعی بین Middleware و Context Processor در سه سطح نهفته است: سطح دسترسی، نقطهی اجرا و هدف طراحی.
سطح اول، دسترسی. Middleware به request، response، session، کوکیها و هدرها دسترسی دارد. Context Processor فقط به request دسترسی دارد و یک دیکشنری برمیگرداند که به context قالب اضافه میشود. یعنی Middleware میتواند درخواست را متوقف کند، پاسخ را تغییر دهد یا هدر اضافه کند، ولی Context Processor فقط داده تولید میکند.
سطح دوم، نقطهی اجرا. Middleware در چرخهی HTTP اجرا میشود، حتی اگر ویو قالب رندر نکند. Context Processor در لحظهی رندر قالب اجرا میشود، فقط برای قالبهای Django Template (نه برای قالبهای Jinja2، مگر با تنظیمات ویژه).
سطح سوم، هدف طراحی. Middleware برای مداخله در سطح زیرساخت طراحی شده. Context Processor برای فراهمکردن دادههای سراسری در سطح ارائه طراحی شده. اگر هدف شما مداخلهی زیرساختی است، Middleware انتخاب درست است. اگر هدف شما در دسترس گذاشتن داده برای قالب است، Context Processor انتخاب درست است.
یک مثال واقعی میتواند این تفاوت را روشن کند. فرض کنید میخواهید به هر کاربر یک شناسهی ردیابی اختصاصی بدهید. اگر این شناسه باید قبل از رسیدن به ویو ساخته شود و در session ذخیره شود، Middleware انتخاب درست است. اگر این شناسه از قبل در session وجود دارد و میخواهید در قالب در دسترس باشد، Context Processor انتخاب درست است.
در پروژههای واقعی، اغلب نیاز دارید که هر دو را داشته باشید. یک Middleware که session را پر میکند و یک Context Processor که همان مقدار را به قالبها میرساند. این الگوی دوگانه، در پروژههای آماری مثل ساخت اپ جنگو برای ردیابی بازدیدکننده بسیار رایج است.
جدول مقایسهی دقیق
برای اینکه تصویر کلی روشنتر شود، این دو ابزار را در چند محور کلیدی با هم مقایسه میکنیم.
| محور | Middleware | Context Processor |
|---|---|---|
| سطح اجرا | هر درخواست HTTP | هر رندر قالب |
| دسترسی به request | بله | بله (در نسخههای جدید) |
| دسترسی به response | بله | خیر |
| میتواند درخواست را متوقف کند | بله | خیر |
| میتواند پاسخ را تغییر دهد | بله | خیر |
| در JsonResponse اجرا میشود | بله | خیر |
| نتیجه در قالب در دسترس است | خیر (مستقیم) | بله |
| ترتیب اجرا | وابسته به MIDDLEWARE | وابسته به context_processors |
| مناسب برای منطق کسبوکار | خیر | خیر |
| مناسب برای دادههای سراسری قالب | خیر | بله |
| هزینهی کارایی در هر درخواست | متوسط تا زیاد | کم (فقط در رندر قالب) |
خواندن این جدول، یک نکتهی کلیدی را روشن میکند: این دو ابزار، بهجای اینکه جایگزین هم باشند، مکمل هم هستند. Middleware کارهای زیرساختی را انجام میدهد و Context Processor دادههای سراسری را به قالب میرساند. اگر اشتباهاً یکی را جای دیگری استفاده کنید، پروژهی شما در بلندمدت هزینهی سنگینی میپردازد.
کجا Middleware انتخاب درست است؟
Middleware انتخاب درست است وقتی که نیاز شما در سطح زیرساخت است، نه در سطح نمایش. چند سناریوی واقعی:
سناریوی اول، ردیابی بازدیدکننده. میخواهید هر بازدید را در دیتابیس ثبت کنید، صرفنظر از اینکه ویو چه قالبی رندر میکند. این کار در سطح زیرساخت انجام میشود و باید در تمام درخواستهای GET اجرا شود. اگر این منطق را در Context Processor بگذارید، فقط برای ویوهایی که قالب رندر میکنند اجرا میشود و شما بازدیدهای API را از دست میدهید. جزئیات این سناریو در طراحی مدل Visitor و Visit در جنگو کامل توضیح داده شده است.
سناریوی دوم، محدودیت نرخ. میخواهید تعداد درخواستهای یک IP در دقیقه را محدود کنید. این کار باید قبل از رسیدن درخواست به ویو انجام شود تا منابع سرور مصرف نشوند. Middleware تنها گزینهی معقول است.
سناریوی سوم، احراز هویت API. میخواهید توکنهای Bearer را از هدر Authorization استخراج کنید و در صورت نامعتبر بودن، پاسخ ۴۰۱ برگردانید. این کار ذاتاً یک تصمیم زیرساختی است و در Middleware جای درستی دارد.
سناریوی چهارم، فشردهسازی پاسخ. میخواهید پاسخهای بیش از ۵۰۰ بایت را با gzip فشرده کنید. این کار باید روی پاسخ HTTP انجام شود، نه در قالب. Middleware تنها محل مناسب است.
سناریوی پنجم، لاگ درخواستها. میخواهید برای هر درخواست، زمان پاسخ، کد وضعیت و مسیر را در یک فایل یا سیستم لاگ بنویسید. این کار با Middleware انجام میشود، نه با Context Processor.
در همهی این سناریوها، ویژگی مشترک این است که منطق شما به خود درخواست وابسته است، نه به محتوای قالب. اگر منطق شما در سطح درخواست کار میکند، Middleware انتخاب درست است.
کجا Context Processor انتخاب درست است؟
Context Processor انتخاب درست است وقتی که نیاز شما در سطح نمایش است و دادهای سراسری برای همهی قالبها لازم دارید. چند سناریوی واقعی:
سناریوی اول، اطلاعات سایت. نام سایت، آدرس، لوگو، شماره تماس، آدرس شبکههای اجتماعی. اینها دادههایی هستند که در header و footer همهی صفحات استفاده میشوند. پاس دادن دستی این دادهها در هر ویو، تکراری و خستهکننده است. Context Processor این کار را یکبار برای همیشه انجام میدهد.
سناریوی دوم، منوی ناوبری. اگر منوی سایت شما ثابت است و در همهی صفحات نمایش داده میشود، میتوانید آن را در Context Processor بسازید. ولی توجه کنید که اگر منو پویا است و بر اساس کاربر یا صفحه تغییر میکند، بهتر است آن را در ویو پاس بدهید یا از تمپلت تگ سفارشی استفاده کنید.
سناریوی سوم، تنظیمات پویا. مقادیری که از دیتابیس خوانده میشوند و در همهی قالبها لازم هستند، مثل پیامهای سیستمی، وضعیت نگهداشت، یا اعلانهای سراسری. اگر این دادهها کش شوند، Context Processor هزینهی ناچیزی دارد.
سناریوی چهارم، دادههای سبد خرید. اگر تعداد آیتمهای سبد خرید را در header همهی صفحات نمایش میدهید، Context Processor میتواند این مقدار را فراهم کند. ولی توجه کنید که این مقدار باید در session ذخیره شود یا از دیتابیس با کش خوانده شود، وگرنه هر رندر قالب یک کوئری اضافه دارد. اگر با الگوی ذخیرهی PageView و Interaction کار کرده باشید، میدانید که کوئری اضافه در هر قالب، سریع حجم میگیرد.
سناریوی پنجم، وضعیت احراز هویت. گرچه جنگو خودش یک Context Processor پیشفرض برای این کار دارد، ولی اگر نیاز به اطلاعات بیشتری از کاربر دارید، میتوانید یک Context Processor سفارشی بنویسید. برای مثال، نمایش پروفایل کاربر در header همهی صفحات.
در همهی این سناریوها، ویژگی مشترک این است که منطق شما داده تولید میکند، نه اینکه درخواست را مداخله کند. اگر منطق شما فقط یک دیکشنری برمیگرداند که در قالب استفاده میشود، Context Processor انتخاب درست است.
اشتباهات رایج در انتخاب بین این دو
در بازبینی پروژههای مختلف، این اشتباهات تکرارشونده را دیدهام. هر کدام، هزینهی مشخصی به پروژه تحمیل میکند.
اشتباه اول، ردیابی بازدید در Context Processor. بعضی توسعهدهندگان فکر میکنند چون Context Processor هم روی هر درخواست اجرا میشود، پس میتواند برای ردیابی استفاده شود. ولی این انتخاب باعث میشود بازدیدهای API و JsonResponseها از دست بروند. این خطا در پروژههایی که هم سایت HTML و هم API دارند، آمار را کاملاً آلوده میکند. راهحل درست، استفاده از Middleware سفارشی است.
اشتباه دوم، منطق کسبوکار در Context Processor. بعضی پروژهها محاسبهی قیمت، امتیاز کاربر یا وضعیت اشتراک را در Context Processor انجام میدهند. این کار، هم بار دیتابیس را بالا میبرد، هم تستپذیری را از بین میبرد. راهحل درست، انتقال این منطق به services.py در جنگو و صدا زدن آن در ویو است.
اشتباه سوم، کوتاهسازی درخواست در Context Processor. بعضی توسعهدهندگان فکر میکنند میتوانند در Context Processor یک redirect یا ۴۰۱ برگردانند. این کار اصلاً ممکن نیست، چون Context Processor فقط یک دیکشنری برمیگرداند و هیچ کنترلی روی پاسخ ندارد.
اشتباه چهارم، دسترسی به response در Context Processor. بعضی توسعهدهندگان میخواهند بعد از رندر قالب، هدری به پاسخ اضافه کنند. این کار باید در Middleware انجام شود، نه در Context Processor. تفاوت این دو نقطه، در همان جدول مقایسهای که قبلاً دیدیم روشن است.
اشتباه پنجم، استفاده از Context Processor برای دادههای سنگین. اگر Context Processor شما یک کوئری سنگین به دیتابیس میزند، این کوئری در هر رندر قالب تکرار میشود. در سایتهای با ترافیک متوسط، این میتواند به صدها کوئری اضافه در دقیقه تبدیل شود. راهحل درست، یا استفاده از کش، یا انتقال به یک سرویس با کش داخلی.
اشتباه ششم، ترکیب منطق در یک Context Processor بزرگ. بعضی پروژهها یک Context Processor واحد دارند که دهها متغیر مختلف را پر میکند. این کار باعث میشود هر تغییر کوچک، نیازمند تست کل سیستم باشد. راهحل درست، تقسیم به چند Context Processor کوچک با مسئولیتهای متمایز است.
اشتباه هفتم، فراموشکردن ترتیب در Context Processor. اگر Context Processor شما به نتیجهی Context Processor دیگری وابسته است، ترتیب در settings.py اهمیت پیدا میکند. Context Processor اول اجرا میشود، نتیجهاش به context اضافه میشود، و بعد Context Processor دوم اجرا میشود. اگر ترتیب را اشتباه بگذارید، ممکن است متغیر شما در دسترس نباشد.
در مجموع، قاعدهی سادهای وجود دارد: اگر کاری در سطح درخواست انجام میشود و نتیجهاش بر پاسخ اثر دارد، Middleware. اگر کاری در سطح قالب انجام میشود و نتیجهاش دادهای سراسری است، Context Processor.
جایگاه Template Tag در این میانه
بسیاری از توسعهدهندگان، در انتخاب بین Middleware و Context Processor، گزینهی سوم را فراموش میکنند: Template Tag. این ابزار، جایگاه خاص خودش را دارد و در بسیاری از موارد، انتخاب بهتری از هر دو است.
Template Tag در لحظهی رندر قالب اجرا میشود، ولی برخلاف Context Processor، فقط در همان نقطهای که در قالب صدا زده شده. یعنی اگر در یک قالب خاص به دادهای نیاز دارید، میتوانید یک Template Tag بنویسید که آن داده را فقط در همان نقطه فراهم کند.
مثلاً اگر میخواهید در فوتر قالب تعداد کاربران آنلاین را نشان دهید، سه راه دارید:
راه اول، Context Processor. یک Context Processor مینویسید که همیشه این مقدار را به context اضافه کند. مشکل: اگر قالب شما این مقدار را نمایش ندهد، یک کوئری اضافه زدهاید.
راه دوم، Middleware. در Middleware این مقدار را در session میگذارید و در قالب از session میخوانید. مشکل: session برای ذخیرهی دادههای سراسری طراحی نشده و این کار معماری را پیچیده میکند.
راه سوم، Template Tag. یک Template Tag ساده مینویسید که با کش، تعداد کاربران آنلاین را برگرداند. این Template Tag فقط زمانی اجرا میشود که در قالب صدا زده شده باشد. این انتخاب، هم کارایی بهتری دارد، هم از نظر معماری تمیزتر است.
# analytics/templatetags/analytics_tags.py
from django import template
from django.core.cache import cache
register = template.Library()
@register.simple_tag
def analytics_online_count():
key = "analytics:online_count"
value = cache.get(key)
if value is None:
from analytics.templatetags.analytics_tags import online_count
value = online_count()
cache.set(key, value, timeout=30)
return value
و در قالب، فقط در فوتر از آن استفاده میکنید:
{% load analytics_tags %}
آنلاین: {% analytics_online_count %}
این رویکرد، ترکیبی از مزیتهای Middleware (کارایی) و Context Processor (دسترسی در قالب) را فراهم میکند. اگر میخواهید الگوهای کاملتر Template Tag را ببینید، مقالهی استفاده از تمپلت تگ بهعنوان متغیر راهنمای دقیقی است.
نکتهی مهم: اگر منطق شما واقعاً سنگین است و در اکثر قالبها لازم است، Context Processor با کش بهتر است. اگر منطق شما سبک است و فقط در یک یا دو نقطه لازم است، Template Tag انتخاب درست است. اگر منطق شما در سطح درخواست اثر دارد، Middleware انتخاب درست است. اگر میخواهید این جداسازی را در کل پروژه پیاده کنید، انتقال منطق از services.py به templatetags نمونهی خوبی از این انضباط معماری است.
تعامل با session و کاربر جاری
هر دو ابزار میتوانند با session و کاربر جاری تعامل داشته باشند، ولی به شکلهای متفاوتی.
در Middleware. request.session و request.user در دسترس هستند، به شرطی که Middleware شما بعد از SessionMiddleware و AuthenticationMiddleware اجرا شود. این یعنی Middleware شما میتواند session را تغییر دهد، کاربر را بررسی کند و بر اساس آن تصمیم بگیرد.
در Context Processor. request.session و request.user در دسترس هستند. ولی هیچکدام قابل تغییر نیستند، چون Context Processor فقط یک دیکشنری برمیگرداند. اگر بخواهید session را تغییر دهید، این کار باید در ویو یا Middleware انجام شود.
نکتهی عملی: اگر Context Processor شما بر اساس request.user داده تولید میکند، این داده در هر رندر قالب دوباره تولید میشود. برای کاربران لاگین، این کار ممکن است یک کوئری اضافه به دیتابیس بزند. راهحل: از request.user.is_authenticated استفاده کنید که کوئری ندارد، یا نتیجه را در session ذخیره کنید.
در پروژههای آماری، هماهنگی بین Middleware و Context Processor بسیار مهم است. برای مثال، در ساخت اپ جنگو برای ردیابی بازدیدکننده، Middleware شناسهی Visit را در session ذخیره میکند و Context Processor فقط همان شناسه را در قالبها فراهم میکند. این جداسازی، هم کارایی را حفظ میکند، هم از تکرار منطق جلوگیری میکند.
هزینهی کارایی؛ عددهایی که در مستندات نیست
یکی از سؤالهای پرتکرار این است: «کدام سریعتر است، Middleware یا Context Processor؟» پاسخ دقیق این است که بستگی دارد، ولی بیایید با عدد صحبت کنیم.
Middleware. یک Middleware سبک که فقط چکهای منطقی انجام میدهد (بدون کوئری دیتابیس)، کمتر از ۰.۵ میلیثانیه به هر درخواست اضافه میکند. اگر کوئری دیتابیس بزند، بسته به پیچیدگی، ۱ تا ۱۰ میلیثانیه اضافه میکند. در سایت با ۱۰۰ کاربر آنلاین، این معادل ۲ تا ۲۰ ثانیه CPU در دقیقه است.
Context Processor. یک Context Processor سبک که فقط دیکشنری میسازد، کمتر از ۰.۱ میلیثانیه به هر رندر قالب اضافه میکند. اگر کوئری دیتابیس بزند، بسته به پیچیدگی، مشابه Middleware است. تفاوت اینجاست که Context Processor فقط در ویوهایی که قالب رندر میکنند اجرا میشود، نه در APIها.
در یک پروژهی واقعی که روی آن کار کردم، تفاوت این دو روی latency کلی سایت قابل اندازهگیری بود. بعد از انتقال منطق از Context Processor به Middleware، زمان پاسخ APIها ۴ میلیثانیه کاهش پیدا کرد، چون Context Processor در APIها هم اجرا میشد و کوئری میزد. این تغییر، برای سایتی که روزانه میلیونها API call دارد، معادل صرفهجویی چند ساعتی CPU در روز است.
نکتهی مهم: هزینهی اصلی، در خود Middleware یا Context Processor نیست، در کوئریهایی است که آنها اجرا میکنند. یک Middleware بدون کوئری، ارزانتر از یک Context Processor با کوئری است، و برعکس. اگر میخواهید رویکرد کلیتری برای بهینهسازی داشته باشید، بهینهسازی جنگو برای ترافیک بالا چکلیست کاملی ارائه میدهد.
در نهایت، این ابزارها بهخودیخود سریع یا کند نیستند. چیزی که آنها را کند میکند، کوئریهای پنهانی است که درونشان نوشتهاید.
Middleware و Context Processor در عصر async
از جنگو ۴.۱ به بعد، پشتیبانی از async در middleware رسمی شد. ولی Context Processor هنوز بهطور کامل async نیست. این تفاوت، در پروژههای آینده اهمیت بیشتری پیدا میکند.
Middleware async. میتوانید یک کلاس با متد async def __call__ بنویسید و عملیات async انجام دهید. این کار در پروژههای با بار بالا، مزیتهای واقعی دارد، چون هر درخواست در یک event loop اجرا میشود و نیازی به thread pool نیست.
class AsyncTrackingMiddleware:
def __init__(self, get_response):
self.get_response = get_response
async def __call__(self, request):
await self._track_start(request)
response = await self.get_response(request)
await self._track_end(request, response)
return response
Context Processor. Context Processor در قالب TemplateResponse اجرا میشود که هنوز کاملاً async نیست. در عمل، اگر ویوی شما async باشد، Context Processor در یک thread جداگانه اجرا میشود. این کار، در بار بالا میتواند به یک گلوگاه تبدیل شود.
پیشنهاد عملی: اگر روی پروژهای کار میکنید که به async مهاجرت کرده، منطق سنگین را در Middleware async بگذارید و Context Processor را فقط برای دادههای سبک و کششده استفاده کنید. اگر با الگوی ساخت API Endpoint برای Beacon آشنا هستید، میدانید که در آن پروژه، endpoint سبک است و middleware همان sync میماند.
تستپذیری؛ کدام آسانتر است؟
تستپذیری یکی از مزیتهای پنهان Context Processor نسبت به Middleware است، ولی نه به آن دلیلی که تصور میکنید.
تست Context Processor. یک Context Processor در نهایت یک تابع است که یک request میگیرد و یک دیکشنری برمیگرداند. تست آن ساده است:
from django.test import RequestFactory
from config.context_processors import site_settings
def test_site_settings_returns_dict():
request = RequestFactory().get("/")
result = site_settings(request)
assert "SITE_NAME" in result
assert isinstance(result["SITE_NAME"], str)
تست Middleware. تست Middleware پیچیدهتر است، چون باید کل چرخهی درخواست را شبیهسازی کنید. ولی این پیچیدگی، در واقع یک مزیت است: چون Middleware شما در یک چرخهی واقعی تست میشود، باگهای پنهان زودتر پیدا میشوند.
from django.test import RequestFactory
def test_tracking_middleware_tracks_get():
from analytics.middleware import VisitorTrackingMiddleware
def get_response(request):
from django.http import HttpResponse
return HttpResponse("ok")
middleware = VisitorTrackingMiddleware(get_response)
request = RequestFactory().get("/")
request.session = {}
response = middleware(request)
assert response.status_code == 200
نکتهی مهم: در تستهای پروژههای واقعی، بهتر است هر دو ابزار را با django.test.Client تست کنید، نه فقط بهصورت واحد. این کار، از رگرسیونهای پنهان در ترتیب و تراکنشها جلوگیری میکند.
الگوهای واقعی در پروژههای تولیدی
چند الگوی واقعی که در پروژههای مختلف دیدهام و کار کردهاند:
الگوی اول، Middleware برای ردیابی + Context Processor برای دادههای سایت. این الگو در اکثر پروژههای آماری استفاده میشود. Middleware بازدید را ثبت میکند، Context Processor نام سایت و تنظیمات را در قالبها فراهم میکند. هیچ تداخلی بین این دو وجود ندارد.
الگوی دوم، Middleware برای احراز هویت + Context Processor برای وضعیت کاربر. اگر احراز هویت سفارشی دارید، Middleware توکن را بررسی میکند و Context Processor وضعیت کاربر را به قالب میرساند. این الگو در پروژههای SPAمحور رایج است.
الگوی سوم، Middleware برای محدودیت نرخ + Template Tag برای نمایش آمار. Middleware درخواستهای اضافی را رد میکند و Template Tag آمار را از کش میخواند. این ترکیب، در پروژههای پربازدید بسیار مؤثر است.
الگوی چهارم، Middleware برای A/B Testing + Context Processor برای variant. Middleware کاربر را به یک variant اختصاص میدهد و Context Processor آن variant را در قالبها در دسترس میگذارد. این الگو در پروژههایی که تست UX انجام میدهند، رایج است.
الگوی پنجم، Middleware برای زبان + Context Processor برای ترجمهها. اگر سیستم چندزبانه دارید، Middleware زبان کاربر را تشخیص میدهد و Context Processor ترجمههای آن زبان را در context میگذارد.
در همهی این الگوها، یک اصل مشترک وجود دارد: Middleware مسئول تصمیمگیری است و Context Processor مسئول فراهمکردن داده. جداسازی این دو مسئولیت، پایهی معماری تمیز است.
anti-patternهای رایج در استفاده از این دو ابزار
در بازبینی پروژههای مختلف، این anti-patternها را دیدهام:
۱. Middlewareهای زنجیرهای بدون ترتیب مشخص. بعضی تیمها چند Middleware سفارشی میسازند و ترتیبشان را مستند نمیکنند. این کار، در طول زمان به یک آشفتگی تبدیل میشود. راهحل: هر Middleware را با یک کامنت توضیحی و ترتیب مشخص در settings.py ثبت کنید.
۲. Context Processorهای بزرگ با منطق ترکیبی. یک Context Processor که دهها متغیر مختلف را با کوئریهای متفاوت پر میکند، به یک گلوگاه پنهان تبدیل میشود. راهحل: تقسیم به چند Context Processor کوچک با مسئولیتهای متمایز.
۳. ذخیرهی دادههای حساس در context. بعضی پروژهها توکن یا رمز عبور را در context میگذارند. این کار، هم امنیت را پایین میآورد، هم حجم پاسخ را بالا میبرد. راهحل: فقط دادههای غیرحساس و لازم برای نمایش در context بگذارید.
۴. ردیابی در Context Processor. این خطا را در بالا توضیح دادم، ولی بهخاطر شیوعش دوباره میگویم. ردیابی بازدید، کار Middleware است. اگر با الگوی تشخیص بات از کاربر انسانی کار کرده باشید، میدانید که این منطق باید در Middleware انجام شود.
۵. استفاده از Context Processor برای دادههای پویا. اگر دادهای در هر صفحه تغییر میکند، Context Processor انتخاب درستی نیست. این داده باید در ویو پاس داده شود. راهحل: معیار تصمیمگیری را بر اساس پویایی داده بگذارید، نه بر اساس راحتی.
۶. ترکیب Middleware و Context Processor با یکدیگر از طریق session. بعضی پروژهها از session بهعنوان یک کانال ارتباطی بین Middleware و Context Processor استفاده میکنند. این کار، session را شلوغ میکند و کارایی را پایین میآورد. راهحل: اگر دادهای بین این دو باید منتقل شود، آن را در یک کش مشترک یا در خود Middleware با کش بگذارید.
۷. عدم کش در Context Processorهای سنگین. اگر Context Processor شما یک کوئری به دیتابیس میزند و کش ندارد، در هر رندر قالب این کوئری تکرار میشود. راهحل: کش در سطح داده، نه در سطح قالب. اگر با الگوی ساخت API JSON برای آمار زنده آشنا هستید، میدانید که کش چقدر مؤثر است.
پرسشهای پرتکرار دربارهی Middleware و Context Processor
آیا هر دو روی هر درخواست اجرا میشوند؟ Middleware روی هر درخواست HTTP اجرا میشود. Context Processor فقط در ویوهایی که قالب رندر میکنند اجرا میشود. اگر ویوی شما یک JsonResponse برگرداند، Context Processor اجرا نمیشود.
آیا میتوانم در Context Processor به session دسترسی داشته باشم؟ بله، از طریق request.session. ولی نمیتوانید آن را تغییر دهید، چون Context Processor فقط یک دیکشنری برمیگرداند.
آیا میتوانم در Middleware یک متغیر را در context قالب قرار دهم؟ بهطور مستقیم نه. Middleware به قالب دسترسی ندارد. اگر میخواهید دادهای را به قالب برسانید، باید آن را در request بگذارید و در ویو یا Context Processor آن را بخوانید.
چطور بین Middleware و Context Processor انتخاب کنم؟ سه سؤال از خودتان بپرسید. اول، آیا منطق شما به response نیاز دارد؟ اگر بله، Middleware. دوم، آیا منطق شما باید در APIها هم اجرا شود؟ اگر بله، Middleware. سوم، آیا نتیجهی منطق شما در قالبها لازم است؟ اگر بله و فقط در قالبها، Context Processor (یا Template Tag).
آیا میتوانم چند Context Processor داشته باشم؟ بله، و توصیه میشود. هر Context Processor باید یک مسئولیت مشخص داشته باشد. ترتیبشان در settings.py اهمیت دارد، چون بهترتیب اجرا میشوند.
آیا Context Processor روی قالبهای Jinja2 کار میکند؟ بهطور پیشفرض، نه. Context Processor در جنگو برای Django Template طراحی شده. اگر از Jinja2 استفاده میکنید، باید از global functions یا environment processors استفاده کنید.
آیا Middleware روی قالبهای Jinja2 کار میکند؟ بله. Middleware در سطح درخواست HTTP اجرا میشود، مستقل از اینکه موتور قالب چه چیزی است. این یکی از مزیتهای Middleware نسبت به Context Processor است.
چطور Middleware را از Context Processor در تستها تشخیص دهم؟ در تستهای یکپارچه با Client، رفتار هر دو قابل مشاهده است. برای تشخیص، از override_settings استفاده کنید و هر کدام را جدا فعال کنید.
آیا Context Processor روی پنل ادمین جنگو کار میکند؟ بله، به شرطی که در TEMPLATES تنظیم شده باشد. ولی پنل ادمین جنگو قالبهای خودش را دارد و ممکن است Context Processor شما را نادیده بگیرد. برای نمایش داده در پنل ادمین، بهتر است از ModelAdmin سفارشی استفاده کنید.
آیا Middleware روی درخواستهای ادمین جنگو اجرا میشود؟ بله. برای همین، توصیه میکنم مسیر /admin/ را در ANALYTICS_SKIP_PATHS بگذارید تا بازدیدهای شما ثبت نشوند.
آیا میتوانم Context Processor را از داخل یک اپ فعال کنم؟ بله، ولی Context Processor در تنظیمات TEMPLATES ثبت میشود که سراسری است. نمیتوانید آن را برای یک اپ خاص فعال کنید. برای این کار، از Template Tag استفاده کنید که فقط در قالبهای همان اپ فعال میشود.
آیا Middleware میتواند پاسخهای static را تحت تأثیر قرار دهد؟ در حالت development، بله، اگر django.contrib.staticfiles فعال باشد. در production که Nginx یا CDN مسیرهای static را سرو میکند، خیر. برای اطمینان، مسیر /static/ و /media/ را در ANALYTICS_SKIP_PATHS بگذارید.
چطور Context Processor را برای چند سایت پیاده کنم؟ در پروژههای multi-site، Context Processor باید بر اساس request.get_host() یا request.site تصمیم بگیرد. اگر از django.contrib.sites استفاده میکنید، این کار سادهتر است.
آیا میتوانم Context Processor را برای کاربران لاگین فعال کنم؟ بله، ولی توجه کنید که اگر در Context Processor یک کوئری برای بررسی کاربر بزنید، در همهی رندرها این کوئری تکرار میشود. راهحل: از request.user.is_authenticated استفاده کنید که کوئری نمیزند.
آیا Middleware میتواند به user دسترسی داشته باشد؟ بله، ولی فقط اگر بعد از AuthenticationMiddleware اجرا شود. برای اطمینان، Middleware ردیابی خود را در انتهای لیست MIDDLEWARE قرار دهید.
نگاه مهندسی به مرز میان این دو ابزار
در نگاه مهندسی، Middleware و Context Processor دو ابزار کاملاً متفاوت با دو هدف متفاوت هستند. ولی درک این تفاوت، در پروژههای بزرگ اهمیت حیاتی پیدا میکند. اگر با الگوی تعریف و کاربرد services.py در جنگو آشنا هستید، میدانید که در آن ساختار، منطق کسبوکار بهطور کامل از لایهی ارائه جدا میشود. این همان اصلی است که در انتخاب بین Middleware و Context Processor هم باید رعایت شود.
اصل اول، جدا کردن مسئولیتها. Middleware مسئول زیرساخت است، Context Processor مسئول دادههای ارائه. اگر این تفکیک را رعایت کنید، پروژهی شما در طول سالها قابل نگهداری میماند.
اصل دوم، کاهش وابستگی. Middleware و Context Processor باید تا جای ممکن به یکدیگر وابسته نباشند. اگر Middleware شما دادهای را در session میگذارد که Context Processor به آن وابسته است، یک وابستگی پنهان ایجاد کردهاید. این وابستگی، در طول زمان به یک بدهی معماری تبدیل میشود.
اصل سوم، آزمونپذیری. هر دو ابزار باید بهطور مستقل قابل تست باشند. اگر برای تست Middleware نیاز به Context Processor دارید یا برعکس، طراحی شما احتمالاً اشتباه است.
اصل چهارم، عملکرد در مقیاس. در پروژههای پربازدید، هر میلیثانیه اهمیت دارد. Middleware که در هر درخواست اجرا میشود، باید سبکتر از Context Processor باشد که فقط در رندر قالب اجرا میشود. اگر برعکس شد، جایی در طراحی اشتباه کردهاید.
اصل پنجم، مستندسازی. هر دو ابزار باید در مستندات پروژه توضیح داده شوند. این مستندسازی، برای توسعهدهندهی بعدی که به پروژه اضافه میشود، حیاتی است. اگر با الگوی ساخت اپ جنگو برای ردیابی بازدیدکننده کار کرده باشید، میدانید که ساختار واضح و مستند، چقدر در نگهداری پروژه مؤثر است.
در نهایت، یک نکته که در پروژههای واقعی بارها دیدهام: تیمهایی که این دو ابزار را با هم قاطی میکنند، در نهایت مجبور به بازنویسی میشوند. تیمهایی که از ابتدا این جداسازی را رعایت میکنند، سالها بدون مشکل کار میکنند. این تفاوت، در ظاهر کوچک است، ولی در عمل، تفاوت بین پروژهای که بهسادگی رشد میکند و پروژهای که هر تغییر کوچک، چند ساعت وقت میگیرد، در همین انتخابهای کوچک نهفته است.
آن پرسشی که قبل از انتخاب باید پاسخ بدهید
قبل از اینکه تصمیم بگیرید کدام ابزار را استفاده کنید، یک پرسش را از خودتان بپرسید: «اگر این منطق را در آینده از پروژه حذف کنم، آیا نباید جایی از کد بشکند؟» اگر پاسخ مثبت است، احتمالاً منطق شما به سطح بالاتری از انتزاع تعلق دارد و باید به services.py یا یک لایهی دیگر منتقل شود. اگر پاسخ منفی است، انتخاب شما درست است. این پرسش، شما را از بدهیهای معماری آینده محافظت میکند.
اگر این تجربه را در پروژهی خودتان داشتهاید — مثلاً جایی که Middleware و Context Processor را اشتباه گرفتهاید و بعد کشف کردهاید — برایم جالب است بدانید. تجربهی عملی شما، بیشتر از هر مستنداتی میتواند به خوانندهی بعدی کمک کند. آن را در دیدگاهها بنویسید.