تشريح معاملة HTTP
الغرض من هذا الدليل هو نقل فهم راسخ لعملية معالجة HTTP في Node.js. سنفترض أنك تعرف، بمعنى عام، كيف تعمل طلبات HTTP، بغض النظر عن اللغة أو بيئة البرمجة. سنفترض أيضًا بعض الإلمام بـ Node.js EventEmitters و Streams. إذا لم تكن على دراية تامة بها، فمن الجدير بالاطلاع السريع على وثائق API لكل منهما.
إنشاء الخادم
أي تطبيق خادم ويب node سيتعين عليه في مرحلة ما إنشاء كائن خادم ويب. يتم ذلك باستخدام createServer
.
const http = require('node:http');
const server = http.createServer((request, response) => {
// السحر يحدث هنا!
});
يتم استدعاء الدالة التي يتم تمريرها إلى createServer
مرة واحدة لكل طلب HTTP يتم إجراؤه ضد هذا الخادم، لذلك يطلق عليها معالج الطلبات. في الواقع، كائن Server الذي يتم إرجاعه بواسطة createServer
هو EventEmitter، وما لدينا هنا هو مجرد اختصار لإنشاء كائن خادم ثم إضافة المستمع لاحقًا.
const server = http.createServer();
server.on('request', (request, response) => {
// يحدث نفس النوع من السحر هنا!
});
عندما يضرب طلب HTTP الخادم، تستدعي node دالة معالج الطلبات مع بعض الكائنات المفيدة للتعامل مع المعاملة والطلب والاستجابة. سنتناول هذه الأمور قريبًا. من أجل تقديم الطلبات فعليًا، يجب استدعاء طريقة listen
على كائن الخادم. في معظم الحالات، كل ما ستحتاج إلى تمريره إلى listen
هو رقم المنفذ الذي تريد أن يستمع إليه الخادم. هناك بعض الخيارات الأخرى أيضًا، لذا راجع مرجع API.
الطريقة وعنوان URL والرؤوس
عند التعامل مع طلب، فإن أول شيء قد ترغب في القيام به هو إلقاء نظرة على الطريقة وعنوان URL، بحيث يمكن اتخاذ الإجراءات المناسبة. تجعل Node.js هذا الأمر غير مؤلم نسبيًا عن طريق وضع خصائص مفيدة على كائن الطلب.
const { method, url } = request;
كائن الطلب هو نسخة من IncomingMessage
. ستكون الطريقة هنا دائمًا طريقة/فعل HTTP عاديًا. عنوان URL هو عنوان URL الكامل بدون الخادم أو البروتوكول أو المنفذ. بالنسبة لعنوان URL النموذجي، هذا يعني كل شيء بعد الشرطة المائلة الأمامية الثالثة بما في ذلك.
الرؤوس ليست بعيدة أيضًا. إنها في كائنها الخاص على الطلب يسمى headers
.
const { headers } = request;
const userAgent = headers['user-agent'];
من المهم ملاحظة هنا أن جميع الرؤوس ممثلة بأحرف صغيرة فقط، بغض النظر عن كيفية إرسالها العميل فعليًا. هذا يبسط مهمة تحليل الرؤوس لأي غرض كان.
إذا تم تكرار بعض الرؤوس، فسيتم استبدال قيمها أو ضمها معًا كسلاسل مفصولة بفواصل، اعتمادًا على الرأس. في بعض الحالات، قد يكون هذا إشكاليًا، لذلك يتوفر أيضًا rawHeaders
.
جسم الطلب
عند استقبال طلب POST أو PUT، قد يكون جسم الطلب مهمًا لتطبيقك. الوصول إلى بيانات الجسم أكثر تعقيدًا قليلاً من الوصول إلى رؤوس الطلب. يقوم كائن الطلب الذي يتم تمريره إلى المعالج بتطبيق واجهة ReadableStream
. يمكن الاستماع إلى هذا التدفق أو توجيهه إلى مكان آخر تمامًا مثل أي تدفق آخر. يمكننا الحصول على البيانات مباشرة من التدفق من خلال الاستماع إلى أحداث 'data'
و 'end'
الخاصة بالتدفق.
الكتلة المنبعثة في كل حدث 'data'
هي Buffer
. إذا كنت تعلم أنها ستكون بيانات سلسلة، فإن أفضل ما يمكنك فعله هو جمع البيانات في مصفوفة، ثم في 'end'
، قم بدمجها وتحويلها إلى سلسلة.
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// في هذه المرحلة، يحتوي 'body' على جسم الطلب بأكمله المخزن فيه كسلسلة
});
NOTE
قد يبدو هذا مملًا بعض الشيء، وفي كثير من الحالات يكون كذلك. لحسن الحظ، توجد وحدات مثل concat-stream
و body
على npm يمكن أن تساعد في إخفاء بعض هذا المنطق. من المهم أن يكون لديك فهم جيد لما يحدث قبل سلوك هذا الطريق، وهذا هو سبب وجودك هنا!
شيء سريع عن الأخطاء
نظرًا لأن كائن الطلب هو ReadableStream
، فهو أيضًا EventEmitter
ويتصرف مثله عندما يحدث خطأ.
يظهر خطأ في تدفق الطلب عن طريق إصدار حدث 'error'
على التدفق. إذا لم يكن لديك مستمع لهذا الحدث، فسيتم طرح الخطأ، مما قد يؤدي إلى تعطل برنامج Node.js الخاص بك. لذلك يجب عليك إضافة مستمع 'error'
على تدفقات الطلب الخاصة بك، حتى إذا قمت بتسجيله ومتابعة طريقك. (على الرغم من أنه ربما يكون من الأفضل إرسال نوع من استجابة خطأ HTTP. المزيد عن ذلك لاحقًا.)
request.on('error', err => {
// يطبع هذا رسالة الخطأ وتتبع المكدس إلى stderr.
console.error(err.stack);
});
هناك طرق أخرى للتعامل مع هذه الأخطاء مثل التجريدات والأدوات الأخرى، ولكن كن دائمًا على دراية بأن الأخطاء يمكن أن تحدث وتحدث بالفعل، وعليك التعامل معها.
ما توصلنا إليه حتى الآن
حتى هذه اللحظة، قمنا بتغطية إنشاء خادم، واستخراج الطريقة وعنوان URL والرؤوس والجسم من الطلبات. عندما نجمع كل ذلك معًا، قد يبدو الأمر كما يلي:
const http = require('node:http');
http.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request.on('error', err => console.error(err));
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
// في هذه المرحلة، لدينا الرؤوس والطريقة وعنوان URL والجسم، ويمكننا الآن
// القيام بكل ما نحتاج إليه من أجل الاستجابة لهذا الطلب.
});
});
.listen(8080); // يقوم بتنشيط هذا الخادم، والاستماع على المنفذ 8080.
إذا قمنا بتشغيل هذا المثال، فسنكون قادرين على استقبال الطلبات، ولكن ليس الاستجابة لها. في الواقع، إذا قمت بزيارة هذا المثال في متصفح الويب، فسوف تنتهي مهلة طلبك، حيث لا يتم إرسال أي شيء إلى العميل.
حتى الآن لم نتطرق إلى كائن الاستجابة على الإطلاق، وهو مثيل لـ ServerResponse
، وهو عبارة عن WritableStream
. يحتوي على العديد من الطرق المفيدة لإرسال البيانات مرة أخرى إلى العميل. سنغطي ذلك بعد ذلك.
رمز حالة HTTP
إذا لم تكلف نفسك عناء تعيينه، فسيكون رمز حالة HTTP في الاستجابة دائمًا 200. بالطبع، لا يضمن كل رد HTTP ذلك، وفي مرحلة ما سترغب بالتأكيد في إرسال رمز حالة مختلف. للقيام بذلك، يمكنك تعيين الخاصية statusCode
.
response.statusCode = 404; // أخبر العميل أنه لم يتم العثور على المورد.
هناك بعض الاختصارات الأخرى لذلك، كما سنرى قريبًا.
تعيين رؤوس الاستجابة
يتم تعيين الرؤوس من خلال طريقة ملائمة تسمى setHeader
.
response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');
عند تعيين الرؤوس في استجابة، يكون حالة الأحرف غير حساسة لأسماءها. إذا قمت بتعيين رأس بشكل متكرر، فإن القيمة الأخيرة التي قمت بتعيينها هي القيمة التي يتم إرسالها.
إرسال بيانات الرأس بشكل صريح
تفترض طرق تعيين الرؤوس ورمز الحالة التي ناقشناها بالفعل أنك تستخدم "رؤوسًا ضمنية". هذا يعني أنك تعتمد على Node لإرسال الرؤوس نيابة عنك في الوقت الصحيح قبل أن تبدأ في إرسال بيانات النص الأساسي.
إذا كنت تريد، يمكنك كتابة الرؤوس بشكل صريح إلى دفق الاستجابة. للقيام بذلك، هناك طريقة تسمى writeHead
، والتي تكتب رمز الحالة والرؤوس إلى الدفق.
إرسال بيانات الرأس بشكل صريح
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
});
بمجرد تعيين الرؤوس (سواء ضمنيًا أو صريحًا)، فأنت على استعداد لبدء إرسال بيانات الاستجابة.
إرسال نص الاستجابة
نظرًا لأن كائن الاستجابة هو WritableStream
، فإن كتابة نص استجابة إلى العميل هي مجرد مسألة استخدام طرق الدفق المعتادة.
response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();
يمكن لوظيفة end
الموجودة على التدفقات أيضًا أن تأخذ بعض البيانات الاختيارية لإرسالها كآخر جزء من البيانات الموجودة على الدفق، لذلك يمكننا تبسيط المثال أعلاه على النحو التالي.
response.end('<html><body><h1>hello,world!</h1></body></html>');
NOTE
من المهم تعيين الحالة والرؤوس قبل البدء في كتابة أجزاء من البيانات إلى النص الأساسي. هذا منطقي، لأن الرؤوس تأتي قبل النص الأساسي في استجابات HTTP.
شيء سريع آخر حول الأخطاء
يمكن لدفق الاستجابة أيضًا إصدار أحداث "خطأ"، وفي مرحلة ما سيتعين عليك التعامل مع ذلك أيضًا. تظل جميع النصائح المتعلقة بأخطاء دفق الطلب سارية هنا.
ضع كل شيء معًا
الآن بعد أن تعلمنا كيفية إنشاء استجابات HTTP، دعنا نضع كل شيء معًا. بالاعتماد على المثال السابق، سنقوم بإنشاء خادم يرسل مرة أخرى جميع البيانات التي أرسلها إلينا المستخدم. سنقوم بتنسيق هذه البيانات بتنسيق JSON باستخدام JSON.stringify
.
const http = require('node:http');
http
.createServer((request, response) => {
const { headers, method, url } = request;
let body = [];
request
.on('error', err => {
console.error(err);
})
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
// BEGINNING OF NEW STUFF
response.on('error', err => {
console.error(err);
});
response.statusCode = 200;
response.setHeader('Content-Type', 'application/json');
// Note: the 2 lines above could be replaced with this next one:
// response.writeHead(200, {'Content-Type': 'application/json'})
const responseBody = { headers, method, url, body };
response.write(JSON.stringify(responseBody));
response.end();
// Note: the 2 lines above could be replaced with this next one:
// response.end(JSON.stringify(responseBody))
// END OF NEW STUFF
});
})
.listen(8080);
مثال خادم الصدى
دعنا نبسط المثال السابق لإنشاء خادم صدى بسيط، والذي يرسل ببساطة أي بيانات يتم تلقيها في الطلب مرة أخرى في الاستجابة. كل ما نحتاج إلى القيام به هو الحصول على البيانات من دفق الطلب وكتابة هذه البيانات إلى دفق الاستجابة، على غرار ما فعلناه سابقًا.
const http = require('node:http');
http.createServer((request, response) => {
let body = [];
request.on('data', chunk => {
body.push(chunk);
});
request.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
});
.listen(8080);
الآن دعنا نعدل هذا. نريد فقط إرسال صدى في ظل الشروط التالية:
- طريقة الطلب هي POST.
- عنوان URL هو /echo.
في أي حالة أخرى، نريد ببساطة الاستجابة بـ 404.
const http = require('node:http');
http
.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
let body = [];
request
.on('data', chunk => {
body.push(chunk);
})
.on('end', () => {
body = Buffer.concat(body).toString();
response.end(body);
});
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
ملاحظة
من خلال التحقق من عنوان URL بهذه الطريقة، فإننا نقوم بنوع من "التوجيه". يمكن أن تكون أشكال التوجيه الأخرى بسيطة مثل عبارات switch
أو معقدة مثل أطر عمل كاملة مثل express
. إذا كنت تبحث عن شيء يقوم بالتوجيه ولا شيء آخر، فحاول استخدام router
.
عظيم! الآن دعونا نحاول تبسيط هذا. تذكر أن كائن الطلب هو ReadableStream
وكائن الاستجابة هو WritableStream
. هذا يعني أنه يمكننا استخدام pipe
لتوجيه البيانات من أحدهما إلى الآخر. هذا بالضبط ما نريده لخادم صدى!
const http = require('node:http');
http.createServer((request, response) => {
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
ياي تيارات!
لم ننته بعد تمامًا. كما ذكر عدة مرات في هذا الدليل، يمكن أن تحدث الأخطاء وتحدث بالفعل، ونحن بحاجة إلى التعامل معها.
للتعامل مع الأخطاء في دفق الطلب، سنقوم بتسجيل الخطأ في stderr
وإرسال رمز الحالة 400 للإشارة إلى طلب غير صالح
. في تطبيق واقعي، على الرغم من ذلك، سنرغب في فحص الخطأ لمعرفة رمز الحالة والرسالة الصحيحين. كالمعتاد مع الأخطاء، يجب عليك الرجوع إلى وثائق الأخطاء.
في الاستجابة، سنقوم فقط بتسجيل الخطأ في stderr
.
const http = require('node:http');
http.createServer((request, response) => {
request.on('error', err => {
console.error(err);
response.statusCode = 400;
response.end();
});
response.on('error', err => {
console.error(err);
});
if (request.method === 'POST' && request.url === '/echo') {
request.pipe(response);
} else {
response.statusCode = 404;
response.end();
}
})
.listen(8080);
لقد قمنا الآن بتغطية معظم أساسيات التعامل مع طلبات HTTP. في هذه المرحلة، يجب أن تكون قادرًا على:
- إنشاء خادم HTTP باستخدام وظيفة معالج
request
، وجعله يستمع على منفذ. - الحصول على الرؤوس وعنوان URL والطريقة وبيانات النص الأساسي من كائنات
request
. - اتخاذ قرارات التوجيه بناءً على عنوان URL و/أو بيانات أخرى في كائنات
request
. - إرسال الرؤوس ورموز حالة HTTP وبيانات النص الأساسي عبر كائنات
response
. - توجيه البيانات من كائنات
request
وإلى كائنات الاستجابة. - التعامل مع أخطاء الدفق في كل من دفق
request
وresponse
.
من هذه الأساسيات، يمكن بناء خوادم Node.js HTTP للعديد من حالات الاستخدام النموذجية. هناك الكثير من الأشياء الأخرى التي توفرها واجهات برمجة التطبيقات هذه، لذا تأكد من قراءة وثائق واجهة برمجة التطبيقات لـ EventEmitters
وStreams
و HTTP
.