صفحات SPA با تکنیکهای Ajax و Polling
مقدمه:
وقتی که درخواستی از سمت مرورگر به سمت سرور ارسال میشود، مرورگر رفرش میشود. یعنی برای هر درخواست ارسالی به سمت سرور، مرورگر رفرش میشود تا پاسخ ارسالی از جانب سرور را دریافت و پردازش کند. اما بهتر نیست که پروسه ارسال درخواست و دریافت پاسخ بدون رفرش شدن صفحه اتفاق بیافتد؟ این خواسته را میتوان با کمک تکنیکهای Ajax ی جامعهی عمل پوشاند. به درخواستها و پاسخهایی از این نوع که بدون رفرش شدن صفحه ارسال و دریافت میشوند، اصطلاحاً polling گفته میشود. این مطلب در قالب یک نمایش ارائه خواهد شد. یک طرف این نمایش رها خواهد بود که توسعه دهندهای باتجربه یک شرکت نرمافزاری است و در طرف دیگر نیما خواهد بود که به تازگی به شرکت مذکور پیوسته است. رها قصد دارد شیوهی ارسال درخواست و دریافت پاسخ بدون رفرش شدن صفحه را به نیما آموزش دهد.
پیشنیازها
برای ادامه پیشنیازهایی ضروری است. انتظار میرود که با پایتون راحت باشید. همچنین درکی ابتدایی از JavaScript داشته باشید. البته آشنایی با فریمورکهای پایتونی مانند Flask و کتابخانهی Tornado شاید در ادامهی کار ضروری به نظر برسد. تمام کدها بر روی سیستم عامل گنو/لینوکس توزیع کوبونتو اجرا شدهاند.
چه چیزی خواهیم ساخت؟
صفحهای خواهیم ساخت که فقط یک دکمه دارد و با کلیک بر روی دکمه درخواستی به سرور ارسال شده و پاسخ دریافت میشود و پاسخ حاوی لینک یک عکس است. سپس آدرس در یک تگ img قرار داده خواهد شد تا تصویر نمایش داده شود. تمام مراحل بدون رفرش شده صفحه انجام خواهد شد.
ایجاد یک API
رها: سلام نیما
نیما: سلام
رها: آمادهای که شروع کنیم؟
نیما: بله
رها: قدم اول ساخت یک API خواهد بود.
نیما: این API چه استفادهای خواهد داشت؟
رها: هر بار که درخواستی ارسال میشود، درنهایت به این API خواهد رسید و پاسخی جیسانی به صورت
{"status": "success", "message": imageAddress.png}
خواهیم داشت.
نیما: به نظرم این کاری بیهوده است!
رها: چرا؟
نیما: خب از API های آماده مانند این استفاده کنید.
رها: بله شما درست میفرمایید ولی احتمالاً در چند روز آینده اینترنت قطع خواهد شد و ما نمیخواهیم که کارمان لنگ بماند. از آن گذشته این API از جانب دیگر توسعهدهندگان شرکت نیز استفاده خواهد شد.
نیما: خیلی خب. برای ساخت این API از چه فریمورکی استفاده خواهیم کرد؟
رها: مهم نیست. ولی اکثر توسعه دهندگان شرکت ما با پایتون راحت هستند. به نظرم یکی از فریمورکهای پایتونی گزینه مناسبی باشد.
نیما: من فلسک را ترجیح میدهم.
رها: خب بیاید شروع کنیم. virtualenv و virtualenvwrapper را نصب شده دارید؟
نیما: نه
رها: تا من یک قهوه مینوشم، این دو ابزار را نصب کنید!
نیما: نصب شد.
رها: حالا یک محیط ایزوله برای API ایجاد میکنیم.
mkvirtualenv api
نظر من: با استفاده از mkproject یک پروژه ایجاد کنید. بدین ترتیب یک پوشه به صورت خودکار به یک محیط ایزوله بایند خواهد شد.
رها: بعد از ایجاد محیط ایزوله، باید فریمورک فلسک را نصب کنیم. دستور نصب را میدانید؟
نیما: بله
pip install flask
رها: خیلی خب. یک پوشه به نام polling ایجاد میکنیم(شما به هر نامی که دوست داشتید، ایجاد کنید). بعد پوشه دیگری به نام api درون پوشه قبلی ایجاد میکنیم(شما به هر نامی که دوست داشتید، ایجاد کنید). در نهایت یک فایل به نام app.py ایجاد میکنیم و البته یک پوشه دیگر به نام static در کنار این فایل ایجاد میکنیم.
نیما: همهی اینها را از قبل میدانستم!
رها: هممممم
رها: خب، اولین کاری که باید انجام دهیم ایجاد یک پوشه دیگر در داخل پوشه static به نام img است. عکسها در این پوشه قرار میگیرند. ۲۰ یا ۳۰ عکس دانلود کنید و در این پوشه قرار دهید.
نیما: با چه فرمتی؟
رها: مهم نیست. ولی همه یک فرمت داشته باشند مثلاً png .
نیما: عکسها را دانلود کردم و به این صورت نام گذاری کردم.
010.png 014.png 018.png 021.png 025.png 029.png 032.png 036.png 03.png 043.png 047.png 050.png 07.png
011.png 015.png 019.png 022.png 026.png 02.png 033.png 037.png 040.png 044.png 048.png 051.png 08.png
012.png 016.png 01.png 023.png 027.png 030.png 034.png 038.png 041.png 045.png 049.png 05.png 09.png
013.png 017.png 020.png 024.png 028.png 031.png 035.png 039.png 042.png 046.png 04.png 06.png
رها: خب. بیا شروع به نوشتن کنیم. محتوای فایل app.py این گونه خواهد بود:
import random
from flask import Flask, url_for
app = Flask(__name__)
@app.route("/polling/api/v1.0/rndimg")
def api():
numbers = [str(number) for number in range(1, 52)]
leading_zero_numbers = []
for number in numbers:
leading_zero_numbers.append(number.zfill(len(number)+1))
random_number = random.choice(leading_zero_numbers)
image = f"img/{random_number}.png"
return {
"status": "success",
"message": url_for(
"static",
filename=image,
_external=True
)}
رها: در خط اول پکیج استاندارد random را ایمپورت کردیم. در خط بعد Flask و url_for را از فلسک ایمپورت کردیم. در خط بعد نیز یک شی app ساختیم. و در نهایت یک function view به نام api ساختیم.
نیما:شاید route ی که برای این view نوشته شده است، خیلی طولانی باشد؟
رها: بله! ولی چون بعداً توسعه دهندگان دیگری از شرکت از این api استفاده خواهند کرد، بهتر است که آدرس به صورت استاندارد نوشته شود. بخش اول نام api است. بخش دوم کلمهی api را ذکر میکنیم. بخش بعد مربوط است به ورژن api. و درنهایت کاری که از api درخواست میشود در انتها قرار میگیرد. با این روش، مدیریت endpoint ها راحت خواهد بود و هر آدرس در یک فضای نام است و تغییرات بعدی راحتتر انجام خواهد شد.
نیما: چرا یک لیست ۵۲ عنصره از اعداد ساختیم و بعد به رشته تبدیل کردیم؟
رها: چون شما ۵۲ عکس را دانلود کردهاید و آنها را به صورت leading zero نامگذاری نمودهاید. برای کار با این دستگل شما این لیست از اعداد را ساختیم. متوجه هستید که این اعداد ساخته شدهاند و به رشته تبدیل شدهاند و هنوز leading zero نشدهاند. برای این کار یک لیست خالی به نام leading_zero_numbers ساختهایم و بعد با هر بار پیمایش عناصر لیست numbers و با استفاده از متد zfill اعداد(رشتههای عددی) را leading zero میکنیم. در نهایت عنصری رندم از leading_zero_numbers انتخاب میکنیم و آدرس عکس را در متغیر image قرار میدهیم. در نهایت خروجی json را خواهیم داشت.
نیما: میدانم که url_for آدرس عکسها را به دست میدهد اما کاربرد آرگومان _external چیست؟
رها: url_for آدرس نسبی را برگشت میدهد. با آرگومان مذکور آدرس کامل به دست میآید. در ضمن آدرس نسبی را که خودمان هم داشتیم. آدرس نسبی آدرسی است که در متغیر image ذخیره شده است. به آدرس عکسها در جایی دیگر نیاز خواهیم داشت که خارج از api است پس به آدرس کامل عکسها نیاز خواهیم داشت.
نیما: چرا از Error Handling استفاده نکردیم؟
رها: گاماس گاماس.
به همین دلیل api را ورژن بندی کردیم! چگونگی اجرای این api را میدانید؟
نیما: بله
flask run
رها: یک نکته را فراموش کردهاید؟!! چون در محیط development هستیم، باید متغیرهایی محیطی را تنظیم کنیم تا reloader و debugger فعال شوند. البته در مورد api به debugger نیازی نداریم. علاوه بر آن باید app.py را به فریمورک معرفی کنیم. پس باید دو متغیر محیطی را تنظیم کنیم.
export FLASK_APP=app.py
export FLASK_ENV=development
در زمان اجرا نیز با دستور
--no-debugger
عدم نیاز به دیباگر را به فریمورک اعلام میکنیم. حالا سرور را اجرا میکنیم.
flask run --no-debugger
سرور بر روی پورت ۵۰۰۰ بالا میآید. البته میتوانستیم پورت را تغییر دهیم.
نیما: خب. api آماده شده است. چطور میتوانیم آن را تست کنیم؟
رها: چه ایدهای به ذهنت میرسد؟
نیما: اگر من باشم، برای تست این api از مرورگر استفاده نمیکنم!
رها: آفرین. در شرکت ما برای تست api ها از دو ابزار استفاده میشود:
curl
و
postman
.
اولی خط فرمانیاست و دومی گرافیکی. اما curl نیاز ما را برای تست این api برطرف میکند. curl را نصب شده دارید؟
نیما: همممم نه!
رها: تا من به این تماس پاسخ میدهم، curl را نصب کنید.
نیما: نصب شد.
رها: خیلی خب. با دستور زیر، api را تست میکنیم
curl http://127.0.0.1:5000/polling/api/v1.0/rndimg
خروجی جیسان به صورت زیر است:
{
"message": "http://127.0.0.1:5000/static/img/023.png",
"status": "success"
}
پس api به درستی کار میکند.
نیما: برای تست این api، نصب curl زیادهروی نیست؟
رها: curl همچون حلقهی ننیاست. اگر بر آن مسلط شوی، قدرتش را به تو نشان خواهد داد.
خب نوشتن api تمام شد.
ایجاد سرور درخواست دهنده
کار بعدی نوشتن سروری برای فرستادن درخواست به api و دریافت پاسخ از آن است. چه فریمروکی را برای این کار مناسب میدانید؟
نیما: شاید Django گزینه خوبی باشد؟
رها: بنابراین ما از Tornado استفاده خواهیم کرد!
pip install tornado
در همان پوشه polling یک پوشه دیگر به نام polling_server ایجاد میکنیم و یک فایل به نام server.py و دو پوشه به نامهای templates و static درون آن ایجاد میکنیم. فایل server.py به صورت زیر خواهد بود
from urllib.request import urlopen
from tornado.web import RequestHandler, Application
from tornado.ioloop import IOLoop
class IndexHandler(RequestHandler):
def get(self):
self.render("foo.html")
def post(self):
data = ""
with urlopen("http://127.0.0.1:5000/polling/api/v1.0/rndimg") as response:
for line in response:
data += line.decode("utf-8")
self.write(data)
if __name__ == "__main__":
app = Application(
handlers=[
(r"/", IndexHandler),
],
template_path="templates",
static_path="static",
debug=True,
)
app.listen(8000)
instance = IOLoop.instance()
instance.start()
نیما(ملتمسانه): آیا امکان استفاده از جنگو وجود نداشت؟
رها: در خط اول برای ارسال درخواست و دریافت پاسخ urlopen را از urllib.request ایمپورت کردهایم.
نیما: به خدا پکیجی راحتتر به نام requests برای این کار وجود دارد. چرا از آن استفاده نکنیم؟!
رها: فرض کن یک هفته اینترنت قطع باشد و نتوانی پکیجی نصب کنی و تنها پکیجهای استاندارد در دسترس باشند؟
سه خط بعدی چند کلاس و تابع را از تورنادو ایمپورت کردهایم.
RequestHandler
شبیه یک فریمورک وب عمل میکند و درخواستها را دریافت، پردازش میکند و به درخواستها پاسخ میدهد.
Application
کلاسی است که نمونهای از آن را ایجاد میکنیم و در عمل با آن شی سرور
را اجرا میکنیم. البته میتوانیم کلاسی بسازیم که از Application ارثبری
کندو … که فعلاً توضیح نمیدهم.
IOLoop
همانگونه که از نام آن برمیآید. حلقهای را بر روی یک نخ برای گرفتن درخواستهایی که
io bound
هستند، ایجاد میکند. پس لابد متوجه شدهای که تورنادو برای کارهایی که با cpu سروکار دارند یا اصطلاحاً
cpu bound
هستند، چندان مناسب نیست. با
Parallelism
و
Asynchronous
آشنایی دارید؟
نیما: نه زیاد
رها: شاید این
لینک
و این
لینک
بتواند کمکی کند.
بعد یک کلاس به نام
IndexHandler
ایجاد کردهایم که ازRequestHandler ارث بری میکند و دو متد
get
و
post
را برای مدیریت درخواستها نوشتهایم. متد get یک فایل html را رندر
میکند. در متد post یک متغیر به نام data ایجاد کردهایم. بعد با
context manager
(
لینک
) urlopen یک درخواست به api ایجاد میکنیم. پاسخی که از api برگشت داده
میشود به صورت encode شده است. بنابراین با پیمایش خط به خط پاسخ و
decode کردن هر خط و درنهایت پیوست هر خط به متغیر data، پاسخ درخواست را
در متغیر data به صورت رشتهای خواهیم داشت. در نهایت با استفاده از متد
write
متغیر data به صورت جیسان به کلاینت فرستاده میشود. خطوط آخر هم مربوط
به نمونه سازی از کلاس Application و نگاشت route به handler و نیز تعیین
پوشه فایلهای تمپلیت و فایلهای استاتیک برای تورنادو است. در نهایت سرور
بر روی پورت ۸۰۰۰ به درخواستها پاسخ خواهد داد.
ایجاد صفحه Html
نیما: قدم بعدی چیست؟
رها: باید در پوشهی template یک فایل html بسازیم. به همان نامی که در متد get برای رندر قرار دادیم. محتوای این فایل به صورت زیر است.
<!doctype html>
<html>
<head>
<title>Foo</title>
<script src="{{ static_url('js/script.js') }}"></script>
</head>
</body>
<button type="button" onclick="manage()" />Get</button>
<img id="demo" style="display: none;" width=400 height=400 />
</body>
</html>
همانگونه که متوجه شدهای در قسمت head اسکریپتی را به صفحه اضافه کردهایم. در ادامه یک دکمه خواهیم داشت که با کلیک شدن یک تابع جاوااسکریپتی اجرا خواهد شد و در نهایت یک تگ img با آیدی demo و استایلی برای مخفی شدن عکس.
نیما: عملکرد static_url به چه صورت است؟
رها: مشابه url_for فلسک است.
ایجاد اسکریپت JS
نیما: قدم بعدی ساخت فایل script.js در پوشه static است.
رها: آفرین. میتوانیم اسکریپت را در پوشه
static/js/
قرار دهیم که کدهایمان تمیزتر باشند.
محتوای فایل script.js به صورت زیر خواهد بود
function manage() {
let xhrObject = false;
let self = this;
self.xhrObject = new XMLHttpRequest();
self.xhrObject.open("POST", "/", true);
self.xhrObject.onreadystatechange = function() {
if (self.xhrObject.readyState == 4 && self.xhrObject.status == 200)
updatePage(self.xhrObject.response);
}
self.xhrObject.send();
}
function updatePage(response) {
response = JSON.parse(response);
element = document.getElementById("demo");
element.src = response.message;
element.style.display = "block";
}
معلوم است که تابعی به نام manage ساختهایم. این همان تابعی است که با کلیک بر روی دکمه اجرا خواهد شد. بعد یک متغیر به نام xhrObject میسازیم و به آن مقدار اولیه false میدهیم. البته من برنامه نویس جاوااسکریپت نیستم.
نیما: خب خدا رو شکر!
رها: چییی؟؟؟
نیما: هااااا. خب عملکرد خطوط بعدی به چه صورت است؟
رها: چون به استفاده از self عادت دارم، this را که به شی جاری اشاره میکند به متغیر self نسبت میدهم.
برای ارسال درخواست و دریافت پاسخ بدون رفرش صفحه از تکنیکهای Ajax ی استفاده میشود که یکی از این تکنیکها استفاده از شی XMLHttpRequest است.
یک نمونه از XMLHttpRequest می سازیم و آن را xhrObject نامگذاری میکنیم. در خط بعد متد
open
از شی را فراخوانی میکنیم. این متد اتصال به سرور را ایجاد میکند(اما
هنوز درخواست فرستاده نشده است) این متد سه آرگومان دارد. اولی نوع
HTTP Method
است که من post قرار دادهام. دومی آدرسی است که درخواست به آن ارسال
میشود و سومی نوع ارسال درخواست از نظر همزمانی و ناهمزمانی است که برای
بهرهمندی از خواص ناهمزمانی باید همواره true باشد(اگر true نباشد، ارسال
درخواست در زمینه عملاً بلاموضوع است)
خط بعد یک شنودگر را تنظیم میکند. هر تغییری در خاصیت
readyState
شی xhrObject باعث اجرای یک تابع خواهد شد. البته خاصیت readyState پنج
مقدار متفاوت دارد که نمایانگر وضعیت درخواست است. مقادیر readyState به
صورت زیر هستند:
UNSENT -> 0
OPEND -> 1
LOADING -> 2
HEADERS_RECEIVED -> 3
DONE -> 4
اگر مقدار readyState برابر ۴ باشد و کد درخواست ۲۰۰ باشد، تابع callback اجرا خواهد شد. تابع کالبک به نوبه خود تابع updatePage را اجرا خواهد کرد و پاسخ را به عنوان آرگومان به آن پاس خواهد داد. این تابع نیز پاسخ را serialize خواهد کرد و src را برای تگ img مقدار دهی کرده و عکس را قابل مشاهده خواهد کرد.
نیما: ضرورت استفاده از تورنادو را متوجه نشدم؟
رها: تورنادو همچون شمشیر آندوریل است که با آن میتوان هر کاری را انجام داد.
نتیجه
با استفاده از فلسک یک api ساختیم که در هر بار اجرا، آدرس یک عکس را به صورت json به ما میداد. با تورنادو یک سرور ایجاد کردیم تا درخواستها را به api ارسال کند و در نهایت با تکنیکهای ajaxی درخواستها و پاسخها را بدون رفرش شدن صفحه فرستادیم و دریافت کردیم.