5 سال از زمانی که فیسبوک بهصورت عمومی GraphQL را عرضه کرد میگذرد. GraphQL دیگر یکچیز خیلی جدید نیست. GraphQL اکنون کاملاً توسعه پیداکرده و بالغ شده است. یعنی وقتی شما در حال انتخاب یکی از انواع مختلف API های (Application Programming Interface) طراحی هستید باید به آن نیز توجه کنید.
اگر با GraphQL آشنا نیستید؛ این مقاله به شما کمک خواهد کرد تا نحوهی ارتباط بین کلاینت و سرور را متوجه شوید. همچنین تفاوتهای کلیدی بین GraphQL و دیگر API مرسوم یعنی RESTful API را بیان خواهیم کرد.
ما در این مقاله نحوهی ارسال یک درخواست از کلاینت به سرور را به شما نشان خواهیم داد. و آنچه در این فرآیند اتفاق میافتد را باهم بررسی میکنیم. بهترِ زودتر بریم سراغ اصل مطلب!
الگو و نوع دادهها (Data Type)
تصور کنید که یک فضانورد هستید. شما میخواهید یک سفینه (Spaceship) خریداری کنید تا به همراه دوستانتان در فضا سفر کنید. بهعنوان یک فضانورد، شما در مورد ویژگیهای سفینه اطلاعات دارید. بنابراین میتوانید بهراحتی یک Type برای آن بهصورت زیر تعریف کنید:
type Spaceship { model: String! weight: Float speed: Int turboEnabled: Boolean }
برای تعریف شیء از نوعِ Spaceship ما از چیزی به نامِ زبان تعریف الگوی GraphQL یا “GraphQL Schema Definition Language” و یا بهصورت مخفف GraphQL SDL استفاده میکنیم.
همهی فیلدهای Spaceship بهصورت درونی یا Built-in از نوع اسکالِر هستند. GraphQL دارای 5 نوع دادهای اسکالِر Built-in است: Int, Float, String, Boolean, ID. ما فقط به نوع دادهای اسکالِر محدود نیستیم. یک نوع دیگر از شیءها، نوع Enum است.
توجه داشته باشید که چطور بعد از نوشتن نوع داده از علامت تعجب ! استفاده کردیم – String!. با استفاده از علامت تعجب، ما از سرور انتظار داریم تا یک مقدار غیر خالی یا non-null برای آن فیلد برگرداند. در این مورد، سرور یک مقدار null برای آن فیلد برمیگرداند. یک خطای زمان اجرا رخ میدهد.
تعریف یک شیء
حالا که ما میدانیم چگونه از GraphQL SDL استفاده کنیم؛ بیایید یک شیء از نوع فروشگاه یا Shop تعریف کنیم. میخواهیم Spaceship موردنظرمان را از این Shop خریداری کنیم.
type Shop { name: String! address: String! spaceships: [Spaceship] }
هر Shop، انواع مختلفی از Spaceship را عرضه میکند. بنابراین ما یک نوع فیلد [Spaceship] داریم که لیستی از Spaceship ها را نشان میدهد. قبل از اینکه جلوتر برویم؛ باید بدانیم چطور میتوانیم روی دادههای خودمان کوئری (Query) بزنیم. برای این منظور، باید از نوع شیء خاص Query استفاده کنیم.
type Query { spaceships: [Spaceship] shop(name: String!): Shop }
ما میتوانیم به فیلدهای Query بهعنوان Route ها در REST نگاه کنیم. یعنی درواقع آنها یک نقطهی ورود برای API هستند. با بررسی نوع Query میتوانیم بفهمیم که چه نوعی از داده را از سرور میتوانیم دریافت کنیم. در این مورد میتوانیم یک لیست از Spaceship ها یا دریافت کنیم. یا هم میتوانیم یک Shop را بر اساس نامش دریافت کنیم.
درنهایت، الگوی GraphQL ما به شکل زیر خواهد بود:
type Spaceship { model: String! weight: Float speed: Int! turboEnabled: Boolean } type Shop { name: String! address: String! spaceships: [Spaceship] } type Query { spaceships: [Spaceship] shop(name: String!): Shop }
تعریف الگو فقط نباید یکی از وظایف توسعهدهندهی سمت سرور یا Backend باشد. بلکه توسعهدهندههای سمت کلاینت یا Frontend نیز باید در آن نقش داشته باشند. زیرا در انتها آنها هستند که میخواهند دادهها را از سرور دریافت کنند و باید از الگوی کد برای این کار استفاده کنند.
ساخت Query
اینجا قسمتی است که کلاینت ایفای نقش میکند. ما الگوی مشخصی را بیان کردیم تا با استفاده از آن بتوانیم با انجام Query دادههای موردنظرمان را از سرور دریافت کنیم. نوشتن کوئری ساده است. اساساً کوئری فیلدهایی که نیاز داریم را برای ما انتخاب میکند. مثلاً؛ شما یک لیست از Spaceship ها میخواهید. اما فقط مشخصات مدل و سرعت هرکدام را میخواهید و نه چیز دیگری. بنابراین کوئری موردنظر را باید اینطور بنویسید:
{ spaceships { model speed } }
بعدازآن باید یک درخواست به سرور GraphQL بدهید. در این درخواست، کوئری گفتهشده بهعنوان پارامتر Query درون درخواست قرار میگیرد.
fetch('/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({query: "{ spaceships { model speed } }"}) })
اگر همهچیز درست پیش رفت؛ یک پاسخ مثل پاسخ زیر دریافت خواهید کرد:
{ "data": { "spaceships": [ { "model": "Mercury Conqueror", "speed": 2000 }, ... ] } }
بهاضافه، اگر میخواهید که نام یک Shop را همراه با لیستی از Spaceship ها دریافت کنید؛ نیازی نیست تا درخواست دیگری را با یک کوئری مجزا ارسال کنید. بلکه شما میتوانید کوئری قبلی را تغییر دهید و فیلدهای موردنظرتان را اضافه کنید. از این روش ما هر چیزی که بخواهیم دریافت کنیم را فقط با یک درخواست به دست میآوریم.
این کارها در دنیای REST API کمی متفاوت است. اگر شما میخواهید:
- یک لیست از Spaceship ها را دریافت کنید؛ احتمالاً باید یک درخواست GET به /spaceship ارسال کنید.
- یک Shop را بر اساس نامش دریافت کنید؛ باید یک درخواست GET به /shop/:shopname ارسال کنید.
احتمالاً متوجه شدهاید که برای دریافت اطلاعات در REST باید درخواستهای بیشتری انجام دهیم. نهتنها درخواستهای بیشتری باید انجام دهیم؛ بلکه دادههایی هم دریافت میکنیم که الزاماً به همهی آنها نیاز نداریم. یعنی ما بیشازحد نیاز، داده دریافت میکنیم. چون دریافت دادهها بهصورت نوع ثابتی از ساختار داده انجام میشود. با استفاده از GraphQL دیگر اینطور مشکلی مطرح نیست. چون ما خودمان چیزی که نیاز داریم را مشخص میکنیم و فقط همان را دریافت خواهیم کرد.
جداسازی، اعتبار سنجی و اجرا
اکنون ما در سمت سرور هستیم. رسیدگی به درخواستها در REST بسیار سرراست است. هر Route یا Endpoint به یک تابع یا کنترلکننده مرتبط است. وقتی سرور یک درخواست دریافت میکند؛ آن درخواست توسط تابع اجرا میشود و نتیجه برای کاربر ارسال میشود. در بیشتر موارد، قبل از رسیدن درخواست به تابع کنترلکننده، باید درخواست جداسازی انجام شود. سپس اعتبار سنجی شده و دادههایی که از کلاینت به سرور آمده است؛ طبق استاندارد بررسی شوند.
در سمت دیگر، GraphQL کوئری را از درخواست ما دریافت میکند و آن را بهصورت AST یا Abstract Syntax Tree جداسازی میکند. بعد از جداسازی، GraphQL الگوی ما را گرفته و کوئری دریافت شده را با آن اعتبارسنجی میکند. اگر کلاینت دادههای موردنیاز را ارسال نکرده باشد؛ یا بهجای نوع دادهی عددی یک String یا متن ارسال کرده باشد؛ یا از فیلدهایی که وجود ندارند کوئری زده باشد؛ جای نگرانی نیست. GraphQL در این موارد مدیریت لازم را انجام میدهد و اگر لازم باشد به کاربر یک پیام خطا نمایش میدهد. اگر همهچیز درست باشد؛ میتوانیم وارد فاز اجرایی شویم.
فاز اجرایی
باید برای GraphQL مشخص کنیم که برای کوئری دادهشده؛ هر فیلد چطور Resolve یا پاسخ داده میشود. برای یادآوری، نوع شیءِ Query ما میتوانست دارای دو کوئری باشد. spaceship و shop(name: String!) .
type Query { spaceships: [Spaceship] shop(name: String!): Shop }
برای اینکه به GraphQL بفهمانیم که چطور هر فیلد را Resolve کند یا پاسخ دهد؛ یک تابع Resolver یا پاسخدهنده برای هر فیلدِ Query مینویسیم. این تابع به دیتابیس دسترسی دارد و کارهایی که برای به دست آوردن دادهها لازم است را انجام میدهد.
const resolvers = { Query: { spaceships(obj, args, context, info) { return db.findAllSpaceships() }, shop(obj, args, context, info) { return db.findShopByName(args.name) } } }
نکته: GraphQL به زبان خاصی وابسته نیست و توسط زبانهای مختلفی پشتیبانی میشود. در اینجا ما از جاوا اسکریپت استفاده کردیم. در این لینک جزئیات بیشتری دربارهی پارامترهای Resolver بیانشده است.
ما میتوانیم Resolver را برای Spaceship و Shop هم بنویسیم. برای مثال، میتوانیم فیلد Speed را Resolve کنیم و درصورتیکه مقدار turboEnabled برابر مقدار True بود یک مقدار دیگر را برگردانیم:
const resolvers = { Query: {...}, Spaceship: { speed(obj, args, context, info) { return obj.turboEnabled ? obj.speed * 2 : obj.speed } } }
بهطور پیشفرض، اگر ما Resolver را حذف کنیم؛ GraphQL یک فیلد از مشخصات را بانام مشابه Resolve میکند. مقادیری که Resolve میشوند؛ یک نقشهی کلید-مقدار یا key-value تولید میکنند که کوئری اصلی را نمایش میدهند. این نتیجه به کلاینتی که آن را درخواست کرده است ارسال میگردد.
موارد استفاده از GraphQL
نکتهی مهم دربارهی GraphQL این است که میتوانید آن را روی یک API موجود قرار دهید. پس نیازی نیست همهچیز را از صفر با GraphQL انجام دهید.
یک استفادهی معمول از GraphQL وقتی است که یک کلاینت، دادههایی از چندین منبع نیاز دارد. با استفاده از GraphQL شما میتوانید دادهها را تجمیع کرده و به کلاینت اجازه بدهید که از آن به یک روش استاندارد و از یکجا دسترسی یابد.
یک مورداستفادهی دیگر از GraphQL وقتی است که چندین کلاینت مختلف از دادههای مختلف استفاده میکند. بهاحتمال زیاد، در حالت عادی این کلاینتها باید چندین درخواست مختلف ارسال کنند تا دادههای موردنظرشان را دریافت کنند. و در این شرایط احتمالاً مواردی که موردنیاز کلاینت نیست هم دریافت خواهد شد. با استفاده از GraphQL هر کلاینت میتواند انتخاب کند که چه دادهای را دریافت کند.