در این فصل ما در مورد مباحث زیر بحث خواهیم کرد:
- RESTful API
- طراحی API
- فریمورک رست جنگو
- الگوهای API
تا کنون اپلیکیشنهای جنگویی که ما طراحی میکردیم مورد استفاده انسانها قرار میگرفت اما حالا خیلی از آنها به وسیله اپلیکیشنهای دیگر مورد استفاده قرار میگیرند که استفاده ماشین با ماشین (machine to machine) است. طراحی خوب API باعث میشود که برنامهنویسها برای نوشتن کد استفاده از آن، راحتتر باشند.
در این فصل، هر موقع از لفظ API استفاده کردیم منظورمان Representational state transfer (REST) web APIها است، همانطوری که عموماً هم اشاره میشود. APIها معنای محبوبی دارند نه نه فقط بخاطر دسترسی عملکرد وب اپلیکشنها است بلکه همچنین بخاطر ترکیب کردن و ساختن اپلیکیشن به طور کامل نیز است.
اکثر اپلیکشنها و وب سایتهای محبوب این روزها یک رابط برنامهنویسی کاربردی(API) REST یا REST API ارائه میدهند. آمازون، نتفلیکس، تویلیو و هزاران کمپانی دیگر دارای رابط کاربری عمومی هستند که بخش قابل توجهی از رشد تجارت آنها شده است.
یک RESTful API یک وب سرویس API است که به ویژگیهای معماری REST پایبند است. ما به طور خلاصه در فصل 4، ویوها و urlها اشاره کردیم که تز روی فیلدینگ(Roy Fielding) بود که سبک معماری Rest را معرفی کرد. با توجه به سادگی و انعطاف پذیری آن در موارد استفاده مختلف از جمله اپلیکیشنهای موبایل به یک استاندارد واقعی در صنعت برای رابطهای برنامهنویسی تبدیل شده است.
شش محدودیت معماری برای سیستم خالص RESTful است که آنها در زیر آمدهاند:
- کلاینت - سرور(Client-server): الزام اینکه کلاینت و سرور باید به صورت جدا باشند و بتوانند به صورت مستقل تکامل یابند.
- بی تابعیت(Stateless): لازمه REST که به آن بیتابعیت هم میگویند این است که context کلاینت فقط در کلاینت ذخیره شود ولی در سرور خیر.
- قابل کش شدن(Cacheable): پاسخها(Response) باید مشخص باشند که قابل کش شدن هستند یا خیر، که میتواند مقیاس پذیری و بازدهی را بهبود دهد.
- سیستم لایهای(Layered system): به صورت سلسله مراتبی به مدیریت پیچیدگی کمک کرده و مقیاس پذیری را بهبود میدهد.
- کد بر اساس تقاضا(Code on demand): به کدها یا اپلتها(applet) اجازه میدهد که توسط سرور به کلاینت فرستاده شوند.
- رابط یک شکل(Uniform Interface): مجموعهای از محدودیتهای اساسی است که معماری را جدا میکند مثل منابع و پیامهای خود توصیف کننده.
هرچند اکثر APIها به صورت خالص RESTful نیستند چون آنها ممکن است یک یا چندتا از این محدودیتها را بشکنند(معمولاً هم رابط یک شکل). هرچند هنوز هم ممکن است که آنها را REST API بنامند.
در عمل اکثر آنها به چند مفهوم معماری پایبندند، مانند زیر:
- منابع(Resources): هر شئ، داده یا سرویسها به وسیله Uniform Resource Identifiers(URI) قابل دسترس هستند. این میتواند یک شئ تنها(میتوانیم بگوییم کاربر) یا یک مجموعه(میتوانیم بگوییم کاربران) باشد. معمولاً آنها به یک اسم اشاره میکنند تا یک فعل.
- عملیاتهای درخواست(Request operations): عملیات روی منابع عموماً از عملیاتهای استاندارد HTTP مانند GET, PUT, POST, OPTION و DELETE استفاده میشود. آنها از همان قوانین به خوبی پیروی میکنند مثلا GET پوچ است(بدون عوارض جانبی است) و PUT/DELETE ناتواناند(مهم نیست چندبار آنها را اجرا شوند همیشه همان نتیجه را میگیرید).
- کدهای خطا(Error codes): REST API از کدهای خطای HTTP استاندارد مانند 200(موفق)، 300(ریدایرکت) و 400(خطای کاربر) استفاده میکند.
- هایپرمدیا(Hypermedia): جوابها معمولاً حاوی هایپرلینک یا URIها به دیگر اقدامات(Action) و منابع برای انعطاف پذیری و قابل کشف شدن است. به طور مثال از هایپرلینکها برای صفحهبندی(Pagination) یا دیتاهای با ساختار تو در تو استفاده میشود.
پیشنهاد من به شما این خواهد بود که استفاده از APIتان را هر چه میتوانید آسان کنید تا به صورت سختگیرانه از محدودیتهای REST خالص پیروی کنید. خیلی از APIهای محبوب و شناخته شده آنها را نقص میکنند. اگر REST طراحی کردن API از آن روش راحتتر است، از آن روش استفاده کنید.
ما یک استاندارد تنها برای REST API نداریم. هرچند در طول زمان تعداد زیادی APIهای خوب طراحی شده توسط کمپانیهایی مثل استرایپ، گیتهاب و ترلو به استانداردهایی تبدیل شدهاند که اکنون APIهای وب بر اساس آن طراحی میشوند. در اینجا ما چندین بهترین شیوه(best practice) را علاوه بر اصول طراحی که در بالا ذکر کردیم پوشش میدهیم.
یک API مثل قراردادی بین سرور و کلاینت است. اگر تغییراتی در رابط(Interface) یا معمولا در سرور صورت بگیرد قرارداد فسخ شده است. هرچند API نیاز دارد که تکامل یابد و ویژگیهای جدیدی به آن اضافه شود و ویژگیهای قدیمی آن منسوخ شوند.
از این رو ورژن بندی کردن API تصمیم کلیدی طراحی، در چرخه زندگی آن است که باید زود گرفته شود. چندین پیادهسازی محبوب ورژن بندی API وجود دارد:
- ورژن بندی کردن URI(URI versioning): پیشوندی کردن URI با شماره ورژن مانند http://example.com/v3/superheroes/3. این روش محبوبی است اما اصول را نقض میکند که هر منبع دارای یک URI منحصر به فرد در سراسر ورژنها است.
- ورژن بندی پرس و جوی رشتهای(Query String versioning): اضافه کردن یک پرس و جوی رشتهای مخصوص هر ورژن به URI مانند http://example.com/superheroes/3?version=3 . به طور فنی، URI در سراسر ورژنها یکی است اما چنین پاسخهایی در پروکسیهای وب قدیمیتر کش نخواهند شد در نتیجه عملکردی تحقیرآمیز دارد.
- ورژن بندی هدر سفارشی(Custom Header versioning): شامل یک هدر سفارشی در درخواست شما. به عنوان مثال نمونه زیر را در نظر بگیرید
GET /superheroes/3 HTTP/1.1
Host: example.com
Accept: application/json
api-version: 3
در حالی که این مورد ممکنه به اصول REST نزدیکتر باشه و تمیزتر ولی این میتواند در تست بعضی از کلاینتهای وب مثل مرورگر سختتر باشد. هدرهای سفارشی دارای مشخصات بیرونی(outside spec) هستند و ممکن است مشکلات پنهانی را ایجاد کنند و این سبب میشود که دیباگ کردن آنها سختتر باشد.
- ورژن بندی نوع رسانهای(Media Type versioning): استفاده از هدر Accept برای مشخص کردن نوع مدیا سفارشی شده که صریحاً در این ورژن ذکر شده است. مثال زیر را در نظر بگیرید:
GET /superheroes/3 HTTP/1.1
Host: example.com
Accept: application/vnd.superhero-api.v3+json
این در حالی است که ممکن است مشکلات تست را نیز داشته باشد مانند هدر سفارشی. این افتخاری است که این روش استاندارد است. ممکن است این خالصترین مدل ورژن بندی REST باشد.
تصمیمات طراحی دیگری نیز وجود دارد، مثل اینکه کدام طرحهای ورژن بندی را باید دنبال کنید؟ باید یک افزایش عددی ساده باشد(مانند مثالهای قبلی)، ورژن بندی مفهومی(مثل فیسبوک)، یا تاریخ انتشار آن(تویلیو)؟ این کاملاً شبیه تمرین ورژن بندی محصول است.
سازگاری رو به عقب نیز تصمیم مهمی در چرخه زندگی API است. اینکه چند ورژن قدیمی را نگه دارید؟ اینکه چه چیزی تغییر جزئی یا کلی ورژن را تعیین میکند؟ اینکه چگونه ورژنهای قدیمیتر را منسوخ کنیم؟
بهترین کار این که یک سیاست ارتباطی شفاف داشته باشید و همواره از آن پیروی کنید.
ساختن API وب سایتتان ممکن است با استفاده از الگوی سرویسها که در فصل 3، مدلها یاد گرفتیم بیاهمیت به نظر برسد. هرچند APIهای دنیای واقعی نیازمند عملکردهای خیلی خیلی بیشتر است، از جمله اسناد مروری وب(web browsable document)، احراز هویت(authentications)، سریالایز کردن(serialization) و throttling؛ بهتره که از ابزاری مثل فریمورک رست جنگو(Django Rest framework(DRF)) استفاده کنید.
فریمورک رست جنگو(DRF) محبوبترین ابزار API برای جنگو است. آن خیلی با معماری جنگو مناسب است و از چندین مفهوم(concept) آشنا استفاده مجدد میکند از جمله ویوهای عمومی و مدل فرمها. خارج از بحث، API قابل استفاده و قابل دسترس با یک مرورگر وب معمولی است که تست کردن و پیدا کردن مستندات را برای توسعه دهندهها آسانتر میکند.
مثال الگوی سرویسها را بخاطر دارید، جایی که یک سرویس ساختیم که کل آخرین پستهای عمومی بازیابی میکرد؟ حالا ما میخواهیم با ویژگیهایی که ارائه شده بود، به وسیله DRF آن را مجدداً بسازیم.
در وهله اول، DRF را نصب میکنیم و آن را به INSTALLED_APPS
اضافه میکنیم. بعد مدل مجوزها و سطح دسترسیها را در setting.py
ذکر میکنیم.
# Django Rest Framework settings
REST_FRAMEWORK = {
# Allow unauthenticated access to public content
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
]
}
حتی اگر چه ما اجازه دسترسی نامحدود(AllowAny
) را دادهایم. قویاً توصیه میشود که محدودترین سیاست دسترسی را برای امنیت APIتان انتخاب کنید.
فریمورک رست جنگو(DRF) به ما اجازه انتخاب تنوع وسیعی از سیاستهای دسترسی API را میدهد از جمله اجازه فقط برای کاربران احراز هویت شده(IsAuthenticated
) یا اجازه دسترسی فقط خواندن کاربران احراز هویت نشده را میدهد(DjangoModelPermissionsOrAnonReadOnly
) و بیشتر. سطح دسترسیهای خیلی جرئیتری نیز میتوانند تعریف شوند.
از آنجایی که ما قبلاً مدل Post
و مدیر مدل(model manager) را برای پستهای عمومی تعریف کردهایم، میتوانیم سریالایزر پست(Post Serializer) را بسازیم. سریالایزرها برای تبدیل اشیاء ساختار یافته استفاده میکنند از جمله نمونههای مدل یا مجموعههای پرس و جو(Queryset) به فرمتهای مثل JSON یا XML که بتوانند بر روی سیم اینترنت ارسال شوند. آنها همچنین فرآیند معکوس این تبدیل شدن یعنی دیسریالایرز کردن هم انجام میدهند، اینکه فرمت JSON یا XML را به اشیاء ساختار یافته برگردانند.
فایل جدیدی به نام viewschapter/serializers.py
با محتوای زیر بسازید:
from rest_framework import serializers
from posts import models
class PostSerializer(serializers.ModelSerializer):
class Meta:
model = models.Post
fields = ("posted_by_id", "message")
ما به صورت اعلامی کلاس serializers
را با آدرس دادن به کلاس model
و فیلدهای آن تعریف کردهایم که نیاز به سریالایز و دیسریالایز شدن دارد. توجه کنید که چگونه به تعریف کردن ModelForm
شباهت دارد.
این از قصد است مانند وب سایتهایی که بر پایه HTML هستند و نیاز دارند داده ورودی کاربر به فرمها را اعتبارسنجی کنند. APIهای وب نیز به یک دیسریالایزر نیاز دارد تا دادههایی که به API ارسال میشود را اعتبارسنجی کند. دقیقاً همانطور که فرمها به مدلها نگاشته میشوند، ModelForms
نامیده میشوند، سریالایزرها(Serializers
) نیز که به مدلها نگاشته میشوند مدل سریالایزر(ModelSerializers
) نامیده میشوند.
بعداً ما ویو APIمان را در فایل جدایی به نام viewschapter/apiviews.py
تعریف میکنیم:
from rest_framework.views import APIView
from rest_framework.response import Response
from posts import models
from .serializers import PostSerializer
class PublicPostList(APIView):
"""
Return the most recent public posts by all users
"""
def get(self, request):
msgs = models.Post.objects.public_posts()[:5]
data = PostSerializer(msgs, many=True).data
return Response(data)
متودهای کلاس APIView
پارامترهای متفاوتی استفاده میکنند و نوع دادهای متفاوتی در مقایسه با View
جنگو برمیگردانند. رست فریمورک نمونههای Request
را میگیرد و جنگو به جای آن نمونههای HttpRequest
. همچنین رست فریمورک نمونههای Response
را برمیگرداند در مقابل جنگو نمونههای HttpResponse
.
در نهایت، ما این را در اپمان سیمکشی میکنیم viewschapter/urls.py
:
path('api/public/',
apiviews.PublicPostList.as_view(), name="api_public"),
حالا اگر آدرس http://127.0.0.1:8000/api/public/ یعنی آخرین نقطه API را در مرورگر وبتان ببینید، این صفحه معرکه را میبینید:
این را مقایسه کنید با فصل ویو که یک رشته JSON خالی را به شما نشان میداد. ما میتوانیم اسم این اندپوینت API و توضیحات آن را ببینیم(از رشته مستندات(docstring) کلاس APIView
)، همچنین هدر درخواست و همچنین محموله JSON(با سینتکس مشخص شده).
همه چیز API عالی به نظر میرسد به جز ریسک امنیتی افشا شدن کلید اصلی مدل کاربر به صورت عمومی. خوشبختانه سریالایزرها(serializers
) میتوانند فیلدهای موجود را با اضافه کردن فیلدهایی که در مدل موجود نیستند، تغییر دهند مانند کد زیر:
class PostSerializer(serializers.ModelSerializer):
posted_by = serializers.SerializerMethodField()
def get_posted_by(self, obj):
return obj.posted_by.username
class Meta:
model = models.Post
fields = ("posted_by", "message",)
فیلد SerializerMethodField
، فیلدی فقط خواندنی است که مقدار(value) را از متود کلاس(class method) میگیرد. به صورت پیش فرض این متود به صورت <get_<field_name نامگذاری میشود.
حالا API به جای نمایش دادن کلید اصلی کاربران، پستها را با نام کاربری برمیگرداند. همانطور که در تصویر زیر نیز مشخص است:
اگر شما یک خالص نویس REST هستید ممکن است اشاره کنید که به جای نام کاربری میتوانیم منابع User
را هایپرلینک کنیم. شما ممکن است بخواهید این را پیادهسازی کنید اگر کاربرهایتان با اینکه جزئیات حسابشان با API عمومی به اشتراک گذاشته شود راحت باشند.
در این قسمت ما چندین مشکل طراحی آشنا را که هنگام کار با APIها به آن بر میخوریم پوشش میدهیم.
مسئله: دیدن API در مرورگر تجربه دلخراشی است که به پذیرشی ضعیف منتهی میشود.
راه حل: از فرصتها برای بهبود بخشیدن رابط قابل مرور انسانی APIتان استفاده کنید.
اگر چه APIها به وسیله کدها طراحی میشوند که مصرف شوند ولی اولین کسانی که با آن تعامل میکنند انسانها هستند. هنگام پیاده سازی کار در صورتی که پارامترهای صحیحی به آن فرستاده باشید، API ممکن است که پاسخ صحیحی به شما دهد ولی بدون مستندات مناسب API میتواند قابل استفاده نباشد.
بدون مستند بودن APIها میتواند همکاری تیمهای مختلف با اپلیکیشن شما را کاهش دهد. اغلب منابع لازم مثل مرورهای مفهومی(conceptual overviews) و راهنماهای شروع پیدا نمیشود و این منجر به یک تجربه توسعه دهندگی خسته کننده میشود.
در نهایت از آنجایی که اکثر APIهای وب قابل دسترس و قابل استفاده با مرورگرهای وب هستند، توانایی اینکه در داخل مستندات بتوانند با API تعامل و کار کنند بسیار مفید و کمک کننده است. حتی اگر رفتاری که در داکیومنت است متفاوت با کد باشد، توانایی تلاش و تایید رفتار در مرورگر به فرآیند تست کردن کمک میکند.
فریمورک رست جنگو به صورت پیش ساخته از ساخت رابط قابل مرور انسانی(human browsable interface) پشتیبانی میکند که مشکلات ذکر شده در این الگو را برطرف میکند. مشاهده اندپوینتهای API با مرورگر، مستنداتی از آن اندپوینت به همراه پشتیبانی کردن عملیاتهای HTTP و توانایی تعامل و کار با آن را میسازد.
مستندات API شما میتواند خیلی جامعتر و تعاملیتر باشد وقتی که از Swagger یا ابزار coreapi
خود DRF استفاده میکنید. Swagger توانایی این را دارد که بدون دسترسی به سورس کد شما، تمام اندپوینتهای API اپلیکیشن شما را پیدا کند. همچنین توانایی این را دارد که با ارسال درخواست و دریافت پاسخ به اندپوینتها در فرآیند تست کردن استفاده شود.
از سوی دیگر شما میتوانید به راحتی از coreapi
با اضافه کردن یک خط در urls.py
استفاده کنید که مثال آن را در زیر میبینید:
from rest_framework.documentation import include_docs_urls
urlpatterns = [
path('api-docs/', include_docs_urls(title='Superbook API')),
]
اگر به آدرس قبلی در مرورگرتان مراجعه کنید شما مستندات API را که آماده استفاده کردن هستند را میبینید:
به خاطر داشته باشید که مستندات API شامل مثال کد به زبان پایتون است(و دیگر زبانها)
بعضی از بهترین روشها برای زمان ساخت مستندات API در زیر لیست شدهاند:
- سوار شدن راحت و سریع(Easy and quick onboarding): آسان ساختن فرآیند استفاده برای توسعه دهندهها با مثالها و آموزشهای آماده اجرا و آماده استفاده. به طور ایده آل فهمیدن API شما و نحوه استفاده از آن نباید بیشتر از پنج دقیقه وقت از توسعه دهنده بگیرد.
- جعبه شنی تعاملی(Interactive sandbox): نمایش اعتباری دموی مستندات برای کاربر که حاوی دادههای نمونه باشد و هنگام کار با آن بتواند با آن تعامل کند خیلی بهتر از این است که آن را خالی نگه داشته باشید.
- فراتر از اندپوینتها رفتن(Go beyond endpoints): اطمینان حاصل کنید که تمام موضوعات ضروری را پوشش داده باشید به طور مثال چگونه بدست آوردن توکنهای احراز هویت یا قیمتها و همچنین مفاهیم سطح بالاتر.
مستندات خوب یک API برای پذیرفته شدن خیلی مهم است و حتی توانایی این را دارد که بر یک API ضعیف غلبه کند پس بنابراین ارزش صرف وقت و تلاش را دارد.
مسئله: محدود شدن محتوای ویوهای صفحه بندی شده برای کاربران مصرف کننده
راه حل: کاربران را با استفاده از صفحات با اسکرول نامحدود به صورت طولانی مدت درگیر کنید
ملاقات کنندگان گاه به گاه شما، اشتیاق شدیدی به مصرف محتوای زیادی مثل فید اخبار اجتماعی یا ترندهای لباس دارند. هرچند آنها متوجه این هستند که کلیک کردن روی یک لینک برای رفتن به صفحه بعدی چقدر آزاردهنده است. کاربران موبایل حتی ممکن است این تجربه رو منزجرکنندهتر ببینند چون آنها پیمایش کردن در لیست بزرگتر را شهودی میبینند.
به طور سنتی یک صفحه حاوی حاوی دادههای زیادی است که صفحه بندی شده تا سرعت بارگذاری آن کاهش بیابد و در نتیجه تجربه کاربری بهتری را خلق کند. پس تکنولوژی Asynchronous JavaScript And XML(AJAX) توانایی بارگذاری محتوا را به صورت ناهمزمان به مرورگرها میدهد.
بدین ترتیب الگوی طراحی پیمایش نامحدود متولد شد که زمانی که کاربر به انتهای صفحه رسید محتوای جدید در ادامه به آن صفحه اضافه شود. این شیوه مرسومی در وب سایتهای شبکههای اجتماعی از جمله فیسبوک یا توئیتر است که باعث افزایش درگیر کردن کاربر با کمترین تعامل میشود.
هرچند همۀ کاربران صفحات با پیمایش بی نهایت را یک بهبود نمیدانند. آنها وقتی که دنبال یک محتوای به خصوص در یک صفحه طولانی میگردند میتوانند سرگردان شوند. پیاده سازی ضعیف میتواند عملکرد دکمه برگشتن به عقب مرورگر را که باید شما را به همانجا در صفحه قبلی برگرداند مختل کند.
توصیههای ما در زیر آمده است:
- توجه کردن به جاوا اسکریپت برای رویداد
پیمایش
تا زمانی که به آن علامت قطعی رسیده باشد. - زمانی که به علامت قطعی و معین شده رسید، لینک صفحه بعدی درخواست ناهمزمان(AJAX) را بدهد.
- لینک باید توسط سرویس جنگو یا REST API مدیریت شود؛ و صفحه مناسب و لینک صفحه بعدی را برگرداند.
- محتوای جدید به انتهای صفحه اضافه گردد.
- به صورت اختیاری میتوانید از
pushState
API مرورگر برای بروز کردن URL به آخرین صفحه بارگذاری شده استفاده کنید.
اساسا ما به بک-اند AJAX که توسط جنگو ارائه شده نیاز داریم که صفحه محتوای مناسب را تدارک ببیند. ListView
به همراه پارامتر paginate_by
که تعداد شئ در هر صفحه را مشخص میکند ممکن است ویو عمومی مناسبی برای این مورد باشد.
پیشمایش نامحدود نکته چشمگیری است که اگر خوب اجرایش کنید حس خیلی خوبی به کاربران میدهد. هرچند نیازمند این است که آزمایش کاربر دقیقی صورت گیرد که آیا این محتوا مناسب محتوای نمایش داده شده به کاربر است یا خیر. به طور مثال گوگل از پیشمایش نامحدود برای بخش جستجوی تصاویر گوگل استفاده میکند ولی برای جستجوهای عادی از صفحه بندی، پس ممکن است این تکنیک برای همه سناریوها مناسب نباشد.
در این فصل متوجه مفاهیم زیربنایی RESTful API شدیم و اینکه چرا مجبور نیستیم به شدت به همه آن پایند باشیم. همچنین به فریمورک رست جنگو نگاهی انداختیم و مثال خیلی سادهای از اندپوینت API ساخته و از آن استفاده کردیم.
در فصل بعد، نگاهی به رویکرد سیستماتیک کار کردن با کدهای پایهای جنگو میاندازیم و اینکه چگونه میتوانیم نیازهای کلاینتها را بهبود ببخشیم.