GraphQL لغة استعلام مفتوحة المصدر لواجهات برمجة التطبيقات API وبيئة تشغيل لتنفيذ الاستعلامات، وأُنشِئَت في الأساس لمعالجة مشكلات REST التقليدية وقد عرضنا بعضًا منها، وسنطّلع في هذا المقال على آلية تفاعل مكونات GraphQL مع بعضها، عبر بناء خادم GraphQL API في بيئة Node.js، وسيُستخدم خادم Express API بدلًا من Apollo GraphQL رغم أنه الأشهر بين تطبيقات GraphQL.
ستتعلم خلال متابعتك لخطوات العمل إنشاء مخطط GraphQL متوافق مع نظام الأنواع بما فيه من عمليات، مثل الاستعلامات والطفرات ومحللات الاستجابة، وستتآلف مع بيئة التطوير المتكاملة GraphiQL، إذ سنستعملها لاستعراض المخطط وتصحيح أخطائه، ولطلب استعلامات العميل من واجهة GraphQL.
متطلبات العمل
جهّز المتطلبات التالية لتتابع معنا سير العمل:
- بيئة Node.js جاهزة على خادمك المحلي، ويمكنك الاسترشاد بأحد المقالين تثبيت Node.js على نظام أبونتو 18.04 أو كيفية تثبيت Node.js على Debian 8 لإعدادها.
- فهم أساسيات GraphQL.
- الإلمام ببروتوكول HTTP، ننصحك بقراءة مدخل إلى HTTP.
- معرفة أساسية بكل من HTML و JavaScript، تفيدك هذه المقالات أساسيات إنشاء موقع ويب باستخدام تعليمات HTML، وأساسيات لغة جافاسكربت.
إعداد خادم Express HTTP
يبدأ إعداد خادم Express بتثبيت الحزمتين express
و cors
، باستخدام الأمر npm install
ضمن مشروع جديد وفق التالي:
npm install express cors
Express هو إطار لتطبيقات الويب المبنية على Node.js، وهو مصمم خصيصًا لبناء واجهات برمجة التطبيقات، وسيكون إطار العمل الخاص بخادمنا، أما الحزمة CORS وهي اختصار لمصطلح البرمجيات الوسيطة لمشاركة الموارد ذات الأصل المختلط Cross-Origin Resource Sharing، إذ إن الغاية منها هي السماح لمتصفحك بالوصول إلى موارد الخادم.
ثبّت أيضًا الاعتمادية Nodemon مع الراية D بصفتها اعتمادية تطوير، لتستخدمها في مرحلة التطوير فقط:
npm install -D nodemon
Nodemon هي أداة مساعدة في تطوير تطبيقات Node، تعيد تشغيل التطبيق في كل مرة يطرأ فيها تغيير على ملفات المشروع الموجودة في مجلد الجذر لتأخذ التعديلات مفعولها.
سينشأ بتثبيت هذه الحزم المجلد node_modules
والملف package.json
مع اعتماديتي إنتاج واعتمادية تطوير.
إذا فتحت الملف package.json بمحرر نصوص مثل نانو nano ستجده يحتوي المعلومات التالية:
{ "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" } }
عدّل عليه ليصبح وفق التالي:
{ "main": "server.js", "scripts": { "dev": "nodemon server.js" }, "dependencies": { "cors": "^2.8.5", "express": "^4.17.3" }, "devDependencies": { "nodemon": "^2.0.15" }, "type": "module" }
لنشرح التعديلات السابقة، إذ وضعنا ملف الخادم "server.js" -الذي سننشئه بعد قليل- في الحقل main
، وبهذا تتأكد من تشغيل npm start
للخادم؛ أما الملف النصي المسمى "dev"، فيُسهل التطوير إذ يتضمن أمر تشغيل nodemon server.js
.
أضفنا في السطر الأخير القيمة module
لحقل النوع type
لنُفعّل إمكانية استخدام تعليمة الاستيراد import
عوضًا عن require
-تعليمة CommonJS الافتراضية- وفي هذا أيضًا تسهيلٌ للعمل.
احفظ التغييرات، وأغلق الملف، لننتقل لإنشاء ملف الخادم.
سنبني الآن خادم Express بسيط عبر إنشاء ملف نصي باسم "server.js"، يتضمن التعليمات المبينة أدناه، يعمل هذا الخادم على المنفذ 4000
، وتظهر في صفحته الأولى عبارة الترحيب !Hello, GraphQL
:
import express from 'express' import cors from 'cors' const app = express() const port = 4000 app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) app.get('/', (request, response) => { response.send('Hello, GraphQL!') }) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) })
لاحظ أن الملف يبدأ بتعليمة استيراد الدالة express
، ثم بعض الإعدادات الخاصة بكل من CORS و JSON، وبعدها استعملنا ('/')app.get
لتحديد ما ينبغي إرساله إلى الجذر /
ضمن طلبات GET
، وفي نهاية الملف استخدمنا ()app.listen
لتحديد منفذ خادم API.
احفظ الآن التغيرات، وأغلق الملف، ثم نفذ الأمر التالي لتشغيل خادم Node:
npm run dev
تأكد -قبل الانتقال للخطوات التالية- من سلامة تشغيل الخادم بظهور صفحة الترحيب !Hello, GraphQL
عند فتح الرابط "http://localhost:4000" من المتصفح أو تشغيل الأمر:
curl http://localhost:4000
إعداد البرمجية الوسيطة لخادم GraphQL HTTP
يوجد ثلاث خطوات أساسية لتحقيق التكامل بين مخطط GraphQL مع خادم إكسبريس، وتتلخص في إعداد الاتصال مع مخزن البيانات، وتعريف المخطط، وتعريف محللات الاستجابة.
ستحتاج في البداية لتنفيذ الأمر التالي:
npm install graphql@14 express-graphql @graphql-tools/schema
الذي يثبت الحزم الضرورية للعمل مع GraphQL، وهي:
-
graphql
: الحزمة المرجعية التي تستخدمها لغة جافا سكربت لجعل التعامل مع GraphQL متاحًا في اللغة. -
express-graphql
: البرمجية الوسيطة لخادم HTTP للتعامل مع GraphQL. -
graphql-tools/schema@
: مجموعة أدوات تُسرّع تطوير GraphQL.
ضِف تعليمات استيراد هذه الحزم إلى الملف "server.js"، لتصبح بدايته على النحو التالي:
import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' ...
لننجز الآن إعدادات الاتصال مع مخزن البيانات قبل إنشاء مخطط GraphQL القابل للتنفيذ.
أنشئ الكائن data
بإضافة التعليمات الخاصة به إلى الملف "server.js" مباشرةً بعد تعليمات الاستيراد كما هو مبين أدناه، إذ يمثل هذا الكائن مخزنًا للبيانات في الذاكرة in-memory store، ويحتوي مجموعة بيانات بسيطة لنستعلم عنها من خادم GraphQL. نخفف بهذه الطريقة عبء إنشاء قاعدة بيانات حقيقية والاتصال معها كوننا نجهز بيئة تعليمة بسيطة.
import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } …
تمثّل هيكلة البيانات جدولًا بسيطًا يدعى warriors
، الذي يحتوي سطرين 'Jaime'
و 'Jorah'
لبيانات اثنين من محاربي لعبة افتراضية سنستخدمها في مثالنا.
ملاحظة: لا يغطي هذا المقال حالة اتصال خادم GraphQL بقاعدة بيانات حقيقية، وعادةً ما يستخدم خادم GraphQL دوال الاختزال reducers للوصول إلى قواعد البيانات والتعديل عليها، ويعتمد تقنياتٍ مثل Prisma، وهي أداة ربط العلاقات بالكائنات ORM، أما إعدادات الاتصال مع قاعدة البيانات فيحملها المعامل context
أحد معاملات محللات الاستجابة غير المتزامنة. سيمثل المتغير data
إعدادات اتصالنا مع مخزن البيانات حتى نهاية هذا المقال.
الآن، بعد تثبيت الحزم المطلوبة، وتجهيز بعض البيانات للاستعلام، يمكننا إنشاء المخطط الذي يُعرّف واجهة API، ويصف ماهية البيانات الممكن الاستعلام عنها.
مخطط GraphQL
سننشئ مخططًا أوليًّا لواجهة API يتضمن الحد الأدنى من التعليمات البرمجية التي تسمح لنا بالاتصال مع نقطة الوصول GraphQL Endpoint. سنفترض في هذا المثال أننا نستعلم عن بيانات لعبة مغامرات فيها أدوار لشخصيات مختلفة، مثل المحاربين والسحرة والمعالجين وغيرهم، لذا سنحرص عند إنشاء المخطط أن يكون مرنًا وقابلًا لإعادة الاستخدام، لتستطيع تكراره مع أي نوع من شخصيات اللعبة وتوسعته ليشمل متغيرات أخرى تخصهم مثل الأسلحة والتعويذات.
تستند مخططات GraphQL إلى نظام الأنواع، ويمكنك دائمًا إنشاء أنواعك الخاصة إذا لم تلبِ الأنواع الافتراضية المُضمّنة في النظام حاجتك، وهذا ما سنفعله هنا إذ سننشئ نوعًا type
يدعى المحارب warrior
له حقلين id
و name
.
type Warrior { id: ID! name: String! }
الحقل id
نوعه ID
رقم تعريفي، والحقل name
نوعه سلسلة نصية String
، وكلاهما من الأنواع الأولية المُضمّنة في نظام الأنواع، والأنواع الأولية تستخدم لإنشاء أنواع أعقد مثل الكائنات، أما إشارة التعجب !
الواردة في التعليمات أعلاه فتشير لكون الحقل بجانبها لا يقبل قيمة فارغة "Null"، فلا يجب أن يكون فارغًا بأية حال.
الآن بعد إنشاء نوع المحارب، سننشئ نوع الاستعلام Query
، وهو نقطة الدخول entry point إلى استعلام GraphQL. ألقِ نظرةً على التعليمات المبينة أدناه، إذ يُعرّف هذا النوع المحاربين warriors
على أنهم مصفوفة من البيانات التابعة لنوع المحارب Warrior
.
type Query { warriors: [Warrior] }
أصبح مخططنا جاهزًا للاستخدام مع البرمجية الوسيطة GraphQL HTTP، لذا سنمرره للدالة makeExecutableSchema
، إحدى أدوات graphql-tools
، بصفته typeDefs
ليصبح قابلًا للتنفيذ. للدالة makeExecutableSchema
معاملان أحدهما للمخطط والآخر لمحللات الاستجابة، وينبغي تعريفها قبل تنفيذ الدالة:
-
typeDefs
: لغة مخطط GraphQL، وهي سلسلة نصية. -
resolvers
: محللات الاستجابة، وهي الدوال التي تُستدعى لتنفيذ حقل معين وإرجاع قيمة.
اكتب تعليمات تعريف typeDefs
ضمن الملف "server.js" تمامًا بعد تعليمات استيراد الاعتماديات، وأسند له قيمة نصية هي مخطط GraphQL، وفق التالي:
... const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` ...
إذًا، فقد جهزّنا إلى الآن البيانات التي سنستعلم عنها data
، وعرّفنا المخطط ضمن typeDefs
، وسنعرّف في الخطوة التالية محللات الاستجابة، التي ستحدد طريقة استجابة خادمنا للطلبات الواردة.
محللات الاستجابة
محللات الاستجابة Resolvers هي مجموعة من الدوال البرمجية التي توّلد استجابة خادم GraphQL، ولكل محلل استجابة أربعة معاملات:
-
obj
: الكائن الأب، غير ضروري في حالتنا لأن الكائن المستخدم لدينا وحيد وهو الأب أو الجذر. -
args
: تمرر هنا أي متغيرات وسيطة يحتاجها GraphQL. -
context
: حقل مشترك بين جميع محللات الاستجابة، ويُخصص غالبًا لمعلومات الاتصال مع قاعدة البيانات. -
info
: معلومات إضافية.
سننشئ الآن محلل استجابة لاستعلام الجذر Query
، يُرجع لطالب الاستعلام معلوماتٍ عن المحاربين warriors
، وسنمرّر له مخزن البيانات في الذاكرة الذي عرّفناه سابقًا.
ضِف ما يلي إلى الملف "server.js":
... const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } ...
أنشأنا بهذه التعليمات دالة محلل استجابة تدعى warriors
، ترتبط بالاستعلام Query
وهو نقطة دخولنا إلى خادم GraphQL، ومهمة هذه الدالة هي إرجاع بيانات المحاربين من جدول المحاربين warriors
الموجود في قاعدة البيانات التي يشير إليها المعامل context
، وهي في حالتنا مخزن البيانات البسيط data
.
إذًا، فلكل دالة محلل استجابة أربعة معاملات: obj
و args
و info
، إضافةً إلى context
، الذي كان أكثرها ارتباطًا بمخططنا، وهذا المعامل مشترك بين جميع محللات الاستجابة، ويشير غالبًا إلى إعدادات الاتصال بين خادم GraphQL وقاعدة البيانات.
الآن، بعد أن عرّفنا typeDefs
و resolvers
معاملات الدالة makeExecutableSchema
، يمكننا تحويل المخطط إلى مخطط قابل للتنفيذ بكتابة الأوامر التالية في الملف "server.js":
... const resolvers = { Query: { warriors: (obj, args, context, info) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) ...
تنشئ الدالة makeExecutableSchema مخططًا كاملًا يمكنك تمريره إلى نقطة وصل GraphQL.
لنستبدل نقطة الوصل الحالية أي عبارة الترحيب !Hello, GraphQL
بنقطة الوصول graphql/
عبر كتابة التعليمات التالية:
... const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) ...
يتطلب استخدام البرمجية الوسيطة graphqlHTTP
مخططًا أنشأناه وسياقًا يمثّل معلومات الاتصال مع مخزن البيانات، وينص الاتفاق على استخدام خادم GraphQL نقطة الوصول graphql/
.
إليك المحتوى النهائي للملف "server.js" بعد جميع التعديلات:
import express from 'express' import cors from 'cors' import { graphqlHTTP } from 'express-graphql' import { makeExecutableSchema } from '@graphql-tools/schema' const app = express() const port = 4000 // مخزن البيانات في الذاكرة const data = { warriors: [ { id: '001', name: 'Jaime' }, { id: '002', name: 'Jorah' }, ], } // المخطط const typeDefs = ` type Warrior { id: ID! name: String! } type Query { warriors: [Warrior] } ` // محلل الاستجابة للمحاربين const resolvers = { Query: { warriors: (obj, args, context) => context.warriors, }, } const executableSchema = makeExecutableSchema({ typeDefs, resolvers, }) app.use(cors()) app.use(express.json()) app.use(express.urlencoded({ extended: true })) // نقطة الدخول app.use( '/graphql', graphqlHTTP({ schema: executableSchema, context: data, graphiql: true, }) ) app.listen(port, () => { console.log(`Running a server at http://localhost:${port}`) })
احفظ التغييرات على الملف وأغلقه.
تكتمل بذلك واجهة برمجة التطبيقات، يمكنك الآن فتح الرابط "http://localhost:4000/graphql"، واستعراض مخطط GraphQL وتصحيح أخطائه باستخدام بيئة التطوير المتكاملة GraphiQL IDE، وهو موضوع القسم التالي من المقال.
استخدام GraphiQL IDE
الخطوة الأولى لاستخدام بيئة التطوير GraphiQL IDE هي تفعيلها بإسناد القيمة true
للخيار graphiql
في كتلة التعليمات الخاصة بالبرمجية الوسيطة ضمن ملف الخادم، ولو رجعت للتعليمات السابقة ستجدها مُفعلة.
تعمل GraphiQL ضمن المتصفح، وتفيدك في كتابة استعلامات GraphQL، والتحقق من صحتها، واختبارها لضمان خرج صحيح من الخادم.
تنقسم شاشة GraphiQL إلى قسمين، القسم الأيسر لكتابة التعليمات والأيمن لعرض النتائج. سنجري الآن استعلامًا عن المحاربين warriors
، لذلك اطلب المُعرّف id
والاسم name
لكل محارب بكتابة التعليمات التالية في القسم الأيسر من الشاشة:
{ warriors { id name } }
اضغط بعدها على الزر التشغيل أعلى يسار الشاشة، وستظهر نتيجة استعلامك في القسم الأيمن بصيغة JSON، وفق التالي:
Output{ "data": { "warriors": [ { "id": "001", "name": "Jaime" }, { "id": "002", "name": "Jorah" } ] } }
حاول تعديل الاستعلام، استعلم مثلًا عن الاسم name
فقط:
{ warriors { name } }
ولاحظ تغير النتيجة تبعًا لاستعلامك، فهي تشمل الآن أسماء المحاربين دون أرقامهم:
Output{ "data": { "warriors": [{ "name": "Jaime" }, { "name": "Jorah" }] } }
تعكس هذه التجربة واحدةً من أقوى خصائص GraphQL، وهي أنها موجهة بطلبات العميل [Client-driven]()، إذ تسمح له بالاستعلام عن الحقول التي يحتاجها فقط، وتعرضها هي بالذات دون زيادة أو نقصان.
تفيدك GraphiQL أيضًا باستعراض تفاصيل المخطط، فكل ما تحتاجه هو الضغط على زر التوثيق Docs في يمين الشاشة، وسيظهر لك شريط جانبي خاص بالتوثيقات.
لننتقل إلى القسم الأخير من مقالنا، وفيه محاكاة لتقديم طلبات استعلام فعلية من العميل إلى واجهة GraphQL API.
إرسال طلب استعلام لواجهة GraphQL API من طرف العميل
يمكنك إرسال طلبات الاستعلام إلى واجهة GraphQL عبر بروتوكول HTTP بالطريقة نفسها المتبعة مع واجهات REST، وتستطيع أيضًا استعمال واجهات APIs المُضمّنة في المتصفحات مع GraphQL، ومن أمثلتها fetch
الخاصة بجلب البيانات.
افتح الملف "index.html"، وأنشئ بداخله هيكل HTML مع الوسم <pre> كما يلي:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL Client</title> </head> <pre><!-- البيانات ستعرض هنا --></pre> <body> <script> // Add query here </script> </body> </html>
ثم أنشئ دالة غير متزامنة تحت الوسم script
، ترسل طلب POST
لواجهة GraphQL API:
... <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() // إضافة البيانات إلى الوسم const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // JSON بصيغة } queryGraphQLServer() </script> </body> ...
أسند القيمة application/json
للحقل Content-Type
في ترويسة المقطع script
، واكتب الاستعلام الذي تطلبه بصيغة سلسلة نصية ضمن المتن، فيكون تسلسل الإجراءات كما يلي: يستدعي script
الدالة غير المتزامنة، فترسل طلبًا للخادم، وعند ورود الاستجابة توضع تحت الوسم pre
.
إليك المحتوى النهائي للملف "index.html":
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>GraphQL</title> </head> <pre></pre> <body> <script> async function queryGraphQLServer() { const response = await fetch('http://localhost:4000/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query: '{ warriors { name } }', }), }) const data = await response.json() const pre = document.querySelector('pre') pre.textContent = JSON.stringify(data, null, 2) // Pretty-print the JSON } queryGraphQLServer() </script> </body> </html>
احفظ التغييرات، وأغلق الملف "index.html"، ثم استعرضه بواسطة المتصفح، وافتح نافذة الشبكة Network ضمن أدوات المطور لترى الطلب المرسل عبر الشبكة من المتصفح إلى نقطة وصول GraphQL ذات العنوان "http://localhost:4000/graphql"، وافحص بعدها استجابة الخادم؛ فإذا تضمنت البيانات التي استعلمت عنها مع رمز الاستجابة 200
، فاستعلامك صحيح وقد نجحت بإنشاء خادم GraphQL API.
الخاتمة
تناول هذا المقال إعداد خادم GraphQL API في بيئة Node.js باستخدام إطار العمل Express بطريقة مبسطة تعينك على فهم أساسيات GraphQL، وأبرزها أن الخادم يتكون من نقطة وصول وحيدة graphql/
تستقبل جميع طلبات الاستعلام، وواجهة برمجة التطبيقات تحتوي دومًا مخطط ومحلل استجابة. يتضمن المخطط نوعين، نوعًا أساسيًا للاستعلام Query
، ونوعًا مخصصًا تُعرّفه حسب حالتك، وفي مثالنا كان نوع المحارب Warrior
، ومهمة محلل الاستجابة جلب البيانات المناسبة لهذه الأنواع من مخزن البيانات.
نأمل أننا ساعدناك في خطوتك الأولى مع GraphQL، وننصحك بتوسيع بحثك عنها لتتعرف على خياراتها المتقدمة ومكملاتها، مثل أدوات المصادقة والأمان والتخبئة caching، لتزيد فعاليتها في تطبيقاتك.
ترجمة -وبتصرف- للمقال How To Set Up a GraphQL API Server in Node.js لصاحبته Tania Rascia.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.