تقديم ملف HTML: فصل من كتاب ReactPHP للمبتدئين من Skyeng


يواصل سيرجي جوك مطور تطبيقات الجوال في Skyeng كتابة كتب جيدة. هذه المرة أصدر كتابًا باللغة الروسية لجمهور يتقن PHP. طلبت من Sergey مشاركة فصل مفيد من الاكتفاء الذاتي من كتابه ، وإعطاء قراء Habra رمز خصم. يوجد أدناه كلاهما.


أولاً ، دعنا نخبرك بما توقفنا في الفصول السابقة.

لقد كتبنا خادم HTTP البسيط في PHP. لدينا ملف index.php الرئيسي - البرنامج النصي الذي يبدأ الخادم. إليك رمز المستوى الأعلى: نقوم بإنشاء حلقة حدث ، وتكوين سلوك خادم HTTP وبدء الحلقة:


 use React\Http\Server; use Psr\Http\Message\ServerRequestInterface; $loop = React\EventLoop\Factory::create(); $router = new Router(); $router->load('routes.php'); $server = new Server( function (ServerRequestInterface $request) use ($router) { return $router($request); } ); $socket = new React\Socket\Server(8080, $loop); $server->listen($socket); $loop->run(); 

لتوجيه الطلبات ، يستخدم الخادم جهاز توجيه:


 // src/Router.php use Psr\Http\Message\ServerRequestInterface; use React\Http\Response; class Router { private $routes = []; public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request); } public function load($filename) { $routes = require $filename; foreach ($routes as $path => $handler) { $this->add($path, $handler); } } public function add($path, callable $handler) { $this->routes[$path] = $handler; } private function notFound($path) { return function () use ($path) { return new Response( 404, ['Content-Type' => 'text/html; charset=UTF-8'], "No request handler found for $path" ); }; } } 

يتم تحميل المسارات من ملف routes.php إلى routes.php . تم الآن الإعلان عن مسارين فقط هنا:


 use React\Http\Response; use Psr\Http\Message\ServerRequestInterface; return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Main page' ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

حتى الآن ، كل شيء بسيط ، ويتوافق تطبيقنا غير المتزامن مع عدة ملفات.


ننتقل إلى أشياء أكثر "مفيدة". إجابات بضع كلمات من نص عادي تعلمنا استخلاصه في الفصول السابقة لا تبدو جذابة للغاية. نحتاج إلى إعادة شيء حقيقي ، مثل صفحة HTML.


لذا ، أين نضع هذا HTML؟ بالطبع ، يمكنك ترميز محتويات صفحة الويب مباشرة داخل ملف المسارات مباشرة:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { $html = <<<HTML <!DOCTYPE html> <html lang=”en”> <head> <meta charset=”UTF-8”> <title>ReactPHP App</title> </head> <body> Hello, world </body> </html> HTML; return new Response( 200, ['Content-Type' => 'text/html'], $html ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

لكن لا تفعل ذلك! لا يمكنك مزج منطق الأعمال (التوجيه) مع العرض التقديمي (صفحة HTML). لماذا؟ تخيل أنك بحاجة إلى تغيير شيء ما في رمز HTML ، على سبيل المثال ، لون الزر. وأي ملف يجب تغييره؟ الملف مع router.php ؟ تبدو غريبة ، أليس كذلك؟ قم بإجراء تغييرات على التوجيه لتغيير لون الزر ...


لذلك ، سنترك المسارات وحدها ، وبالنسبة لصفحات HTML ، سننشئ دليلًا منفصلاً. في جذر المشروع ، أضف دليلًا جديدًا يسمى الصفحات. ثم ننشئ داخله ملف index.html . ستكون هذه صفحتنا الرئيسية. هنا محتوياته:


 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>ReactPHP App</title> <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" > </head> <body> <div class="container"> <div class="row"> <form action="/upload" method="POST" class="justify-content-center"> <div class="form-group"> <label for="text">Text</label> <textarea name="text" id="text" class="form-control"> </div> <button type="submit" class="btn btn-primary">Submit</button> </form> </div> </div> </body> </html> 

الصفحة بسيطة للغاية ، وتحتوي على عنصر واحد فقط - النموذج. يحتوي النموذج بداخله مربع نص وزر للإرسال. أضفت أيضًا أنماط Bootstrap لجعل صفحتنا تبدو أجمل.


قراءة الملفات. كيف لا تفعل


إن الطريقة الأكثر وضوحًا هي قراءة محتويات الملف داخل معالج الطلبات وإرجاع هذا المحتوى كنص استجابة. شيء من هذا القبيل:


 // routes.php return [ '/' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

وبالمناسبة ، ستعمل. يمكنك تجربتها بنفسك: أعد تشغيل الخادم وأعد تحميل الصفحة http://127.0.0.1:8080/ في متصفحك.



إذن ما هو الخطأ هنا؟ ولماذا لا تفعل ذلك؟ باختصار ، لأنه ستكون هناك مشاكل إذا بدأ نظام الملفات في التباطؤ.


حظر المكالمات وعدم حظرها


دعني أوضح لك ما أعنيه من خلال "حظر" المكالمات ، وما الذي يمكن أن يحدث عندما يحتوي أحد معالجات الطلب على رمز الحظر. قبل إعادة كائن الاستجابة ، أضف مكالمة إلى وظيفة sleep() :


 // routes.php return [ '/' => function (ServerRequestInterface $request) { sleep(10); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

سيؤدي ذلك إلى تجميد معالج الطلب لمدة 10 ثوانٍ قبل أن يتمكن من إرجاع استجابة بمحتويات صفحة HTML. يرجى ملاحظة أننا لم نلمس معالج العنوان /upload . من خلال استدعاء وظيفة sleep(10) ، أقوم بمحاكاة تنفيذ نوع من عملية الحظر.


إذن ماذا لدينا؟ عندما يطلب المستعرض الصفحة / ، ينتظر المعالج 10 ثوانٍ ثم يعيد صفحة HTML. عندما نفتح /upload العنوان ، يجب أن يقوم معالجه بإرجاع استجابة على الفور مع السلسلة "صفحة التحميل".


الآن دعونا نرى ما يحدث في الواقع. كما هو الحال دائمًا ، نعيد تشغيل الخادم. الآن ، يرجى فتح نافذة أخرى في متصفحك. في شريط العنوان ، أدخل http://127.0.0.1:8080/upload ، ولكن لا تفتح هذه الصفحة على الفور. فقط اترك هذا العنوان في شريط العناوين في الوقت الحالي. ثم انتقل إلى نافذة المتصفح الأولى وافتح الصفحة http://127.0.0.1:8080/ فيه. أثناء تحميل هذه الصفحة (تذكر أن الأمر سيستغرق 10 ثوانٍ للقيام بذلك) ، انتقل بسرعة إلى النافذة الثانية واضغط على "Enter" لتحميل العنوان الذي تم تركه في شريط العناوين ( http://127.0.0.1:8080/upload ) .


على ماذا حصلنا؟ نعم ، يستغرق العنوان / كما هو متوقع 10 ثوانٍ للتحميل. ولكن ، من المدهش ، أن الصفحة الثانية استغرقت نفس الوقت للتحميل ، على الرغم من أننا لم نضيف أي مكالمات sleep() إليها. أي فكرة لماذا حدث هذا؟


ReactPHP يعمل في خيط واحد. قد يبدو أنه في تطبيق غير متزامن ، يتم تنفيذ المهام بالتوازي ، ولكن هذا ليس كذلك في الواقع. يتم إنشاء وهم التوازي من خلال دورة من الأحداث التي تنتقل باستمرار بين المهام المختلفة وتؤديها. ولكن في وقت معين ، يتم دائمًا تنفيذ مهمة واحدة فقط. هذا يعني أنه إذا استغرقت إحدى هذه المهام وقتًا طويلاً ، فستحجب حلقة الأحداث ، التي لن تتمكن من تسجيل الأحداث الجديدة واستدعاء معالجاتها. وهذا يؤدي في النهاية إلى "تجميد" التطبيق بأكمله ، سيفقد ببساطة عدم التزامن.


حسنًا ، ولكن ما علاقة ذلك بالاتصال file_get_contents('pages/index.h') ؟ المشكلة هنا هي أننا ندخل إلى نظام الملفات مباشرة. بالمقارنة مع العمليات الأخرى ، مثل العمل مع الذاكرة أو الحوسبة ، يمكن أن يكون العمل مع نظام الملفات بطيئًا للغاية. على سبيل المثال ، إذا تبين أن الملف كبير جدًا ، أو إذا كان القرص بطيئًا ، فقد تستغرق قراءة الملف بعض الوقت ، ونتيجة لذلك ، حظر حلقة الحدث.


في النموذج المتزامن القياسي ، لا تعد استجابة الطلب مشكلة. إذا طلب العميل ملفًا ثقيلًا للغاية ، فسوف ينتظر حتى يتم تنزيل هذا الملف. لا يؤثر مثل هذا الطلب الثقيل على العملاء الآخرين. لكن في حالتنا ، نحن نتعامل مع نموذج غير متزامن موجه نحو الحدث. لقد أطلقنا خادم HTTP الذي يجب أن يعالج الطلبات الواردة باستمرار. إذا استغرق طلب واحد الكثير من الوقت لإكماله ، فسيؤثر ذلك على جميع عملاء الخادم الآخرين.


كقاعدة ، تذكر:


  • لا يمكنك أبدا منع حلقة الحدث.

لذا ، كيف نقرأ الملف بعد ذلك بشكل غير متزامن؟ وهنا نصل إلى القاعدة الثانية:


  • عندما لا يمكن تجنب عملية الحظر ، يجب أن تكون متشعبة في العملية الفرعية وتستمر في التنفيذ غير المتزامن في الخيط الرئيسي.

لذا ، بعد أن تعلمنا كيفية عدم القيام بذلك ، دعنا نناقش الحل الصحيح غير المحظور.


عملية الطفل


يجب إجراء جميع الاتصالات بنظام الملفات في تطبيق غير متزامن في العمليات الفرعية. لإدارة العمليات الفرعية في تطبيق ReactPHP ، نحتاج إلى تثبيت مكون آخر "عملية تابعة " . يتيح لك هذا المكون الوصول إلى وظائف نظام التشغيل لتشغيل أي أمر نظام داخل العملية التابعة. لتثبيت هذا المكون ، افتح محطة طرفية في جذر المشروع وقم بتشغيل الأمر التالي:


composer require react/child-process


التوافق مع Windows


في نظام تشغيل Windows ، يتم حظر سلاسل عمليات STDIN و STDOUT و STDERR ، مما يعني أن مكون العملية الفرعية لن يكون قادرًا على العمل بشكل صحيح. لذلك ، تم تصميم هذا المكون بشكل أساسي للعمل فقط على أنظمة nix. إذا حاولت إنشاء كائن من فئة العملية على نظام Windows ، فسيتم طرح استثناء. لكن المكون يمكن أن يعمل تحت نظام Windows الفرعي لنظام Linux (WSL) . إذا كنت تنوي استخدام هذا المكون ضمن Windows ، فستحتاج إلى تثبيت WSL.


الآن يمكننا تنفيذ أي أمر shell داخل العملية التابعة. افتح ملف routes.php ، ثم دعنا نغير معالج المسار. أنشئ كائنًا من فئة React\ChildProcess\Process ، React\ChildProcess\Process ، مرر ls إليه للحصول على محتويات الدليل الحالي:


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request) { $childProcess = new Process('ls'); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, // ... ]; 

ثم نحتاج إلى بدء العملية عن طريق استدعاء طريقة start() . الصيد هو أن الأسلوب start() يحتاج إلى كائن حلقة حدث. ولكن في ملف routes.php ليس لدينا هذا الكائن. كيف نمرر حلقة الحدث من index.php إلى المسارات مباشرة إلى معالج الطلب؟ الحل لهذه المشكلة هو "حقن التبعية".


حقن التبعية


لذا ، يحتاج أحد مساراتنا إلى حلقة حدث للعمل. في تطبيقنا ، يعرف مكون واحد فقط عن وجود مسارات - فئة Router . اتضح أنه من مسؤوليته توفير حلقة حدث للطرق. بمعنى آخر ، يحتاج جهاز التوجيه إلى حلقة حدث ، أو يعتمد على حلقة الحدث. كيف نعبر بوضوح عن هذا التبعية في الكود؟ كيف تجعل من المستحيل حتى إنشاء موجه بدون تمرير حلقة حدث إليه؟ بالطبع ، من خلال مُنشئ فئة Router . افتح Router.php وأضف المُنشئ إلى فئة Router :


 use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\Http\Response; class Router { private $routes = []; /** * @var LoopInterface */ private $loop; public function __construct(LoopInterface $loop) { $this->loop = $loop; } // ... } 

داخل المُنشئ ، احفظ حلقة الحدث التي تم تمريرها في الخاصية $loop الخاصة. هذا هو حقن التبعية عندما نوفر للفصل الأشياء التي يحتاجها للعمل في الخارج.


الآن بعد أن أصبح لدينا هذا المنشئ الجديد ، نحتاج إلى تحديث إنشاء جهاز التوجيه. افتح ملف index.php وقم بتصحيح الخط الذي ننشئ فيه كائن فئة Router :


 // index.php $loop = React\EventLoop\Factory::create(); $router = new Router($loop); $router->load('routes.php'); 

تم. ارجع إلى routes.php . كما توقعت بالفعل ، يمكننا هنا استخدام نفس الفكرة مع حقن التبعية وإضافة حلقة حدث كمعلمة ثانية لمعالجات الاستعلام الخاصة بنا. قم بتغيير رد الاتصال الأول وإضافة الوسيطة الثانية: كائن يقوم بتطبيق LoopInterface :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('ls'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], file_get_contents('pages/index.html') ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

بعد ذلك ، نحتاج إلى تمرير حلقة الحدث إلى طريقة start() للعملية الفرعية. وأين يحصل المعالج على حلقة الحدث؟ وهي مخزنة بالفعل داخل جهاز التوجيه في خاصية $loop الخاصة. نحتاج فقط إلى تمريره عندما يتم استدعاء المعالج.


__invoke() نفتح فئة Router __invoke() طريقة __invoke() ، بإضافة الوسيطة الثانية إلى استدعاء معالج الطلب:


 public function __invoke(ServerRequestInterface $request) { $path = $request->getUri()->getPath(); echo "Request for: $path\n"; $handler = $this->routes[$path] ?? $this->notFound($path); return $handler($request, $this->loop); } 

هذا كل شيء! من المحتمل أن يكون هذا حقنة تبعية كافية. حدث رحلة كبيرة إلى حد ما لدورة الأحداث ، أليس كذلك؟ من ملف index.php إلى فئة Router ، ثم من فئة Router إلى ملف routes.php مباشرة داخل عمليات الاسترجاعات.


لذا ، لتأكيد أن العملية الفرعية ستقوم بسحرها غير المحظور ، دعنا نستبدل ls البسيط بـ ping 8.8.8.8 الأثقل ping 8.8.8.8 . أعد تشغيل الخادم وحاول مرة أخرى لفتح صفحتين في نافذتين مختلفتين. أولاً ، http://127.0.0.1:8080/ ، ثم /upload . يتم فتح كلا الصفحتين بسرعة ، دون أي تأخير ، على الرغم من تنفيذ الأمر ping في المعالج الأول في الخلفية. هذا ، بالمناسبة ، يعني أنه يمكننا تفرع أي عملية مكلفة (على سبيل المثال ، معالجة الملفات الكبيرة) ، دون حظر التطبيق الرئيسي.


ربط العملية والاستجابة الفرعية باستخدام الخيوط


دعنا نعود إلى تطبيقنا. لذلك ، أنشأنا عملية فرعية ، بدأناها ، ولكن متصفحنا لا يعرض نتائج عملية متشعبة بأي شكل من الأشكال. دعنا نصلحه.


كيف يمكننا التواصل مع عملية الطفل؟ في حالتنا ، لدينا أمر تشغيل ls يعرض محتويات الدليل الحالي. كيف نحصل على هذا الاستنتاج ، ثم نرسله إلى نص الجواب؟ الجواب المختصر: خيوط.


لنتحدث قليلاً عن العمليات. يحتوي أي أمر shell تقوم بتنفيذه على ثلاثة تدفقات للبيانات: STDIN و STDOUT و STDERR. دفق إلى الإخراج والإدخال القياسي ، بالإضافة إلى دفق الأخطاء. على سبيل المثال ، عندما نقوم بتنفيذ ls ، يتم إرسال نتيجة هذا الأمر مباشرة إلى STDOUT (على الشاشة الطرفية). لذا ، إذا كنا بحاجة إلى الحصول على مخرجات العملية ، فإن الوصول إلى دفق الإخراج مطلوب. الأمر بهذه البساطة. في إنشاء عنصر الاستجابة ، $childProcess->stdout استدعاء file_get_contents() بـ $childProcess->stdout :


 return new Response( 200, ['Content-Type' => 'text/plain'], $childProcess->stdout ); 

جميع العمليات الفرعية لها ثلاث خصائص تتعلق stdio : stdout و stdin و stderr . في حالتنا ، نريد عرض ناتج العملية على صفحة ويب. بدلاً من سلسلة في مُنشئ فئة Response ، نقوم بتمرير دفق كوسيطة ثالثة. فئة Response ذكية بما يكفي لإدراك أنها تلقت الدفق ومعالجته وفقًا لذلك.


لذا ، كالعادة ، نعيد تشغيل الخادم ونرى ما قمنا به. دعونا نفتح الصفحة http://127.0.0.1:8080/ في المتصفح: يجب أن تشاهد قائمة ملفات المجلد الجذر للمشروع.



الخطوة الأخيرة هي استبدال ls بشيء أكثر فائدة. بدأنا هذا الفصل عن طريق تقديم ملف pages/index.html باستخدام وظيفة file_get_contents() . الآن ، يمكننا قراءة هذا الملف بشكل غير متزامن تمامًا ، دون القلق من أنه سيحظر تطبيقنا. استبدل ls cat pages/index.html .


إذا لم تكن على دراية cat ، يتم استخدامه لربط الملفات وإخراجها. في معظم الأحيان ، يتم استخدام هذا الأمر لقراءة ملف وإخراج محتوياته إلى الإخراج القياسي. يقرأ الأمر cat pages/index.html ملف cat pages/index.html ويطبع محتوياته إلى STDOUT. ونحن بالفعل نرسل stdout كهيئة استجابة. فيما يلي النسخة النهائية من ملف routes.php :


 // routes.php use Psr\Http\Message\ServerRequestInterface; use React\EventLoop\LoopInterface; use React\ChildProcess\Process; use React\Http\Response; return [ '/' => function (ServerRequestInterface $request, LoopInterface $loop) { $childProcess = new Process('cat pages/index.html'); $childProcess->start($loop); return new Response( 200, ['Content-Type' => 'text/html'], $childProcess->stdout ); }, '/upload' => function (ServerRequestInterface $request) { return new Response( 200, ['Content-Type' => 'text/plain'], 'Upload page' ); }, ]; 

نتيجة لذلك ، كان كل هذا الرمز مطلوبًا فقط لاستبدال مكالمة واحدة بدالة file_get_contents() . حقن التبعية ، تمرير كائن حلقة حدث ، إضافة العمليات التابعة والعمل مع مؤشرات الترابط. كل هذا لمجرد استبدال استدعاء دالة واحدة. هل كان يستحق ذلك؟ الجواب: نعم ، كان الأمر يستحق ذلك. عندما يمكن لشيء ما أن يحجب حلقة الحدث ، ويمكن لنظام الملفات بالتأكيد ، تأكد من أنه سيتم حظره في نهاية المطاف ، وفي أكثر الأوقات غير المناسبة.


قد يبدو إنشاء عملية فرعية في كل مرة نحتاج فيها للوصول إلى نظام الملفات بمثابة حمل إضافي سيؤثر على سرعة وأداء تطبيقنا. لسوء الحظ ، في PHP لا توجد طريقة أخرى للعمل مع نظام الملفات بشكل غير متزامن. تستخدم جميع مكتبات PHP غير المتزامنة العمليات الفرعية (أو الامتدادات التي تجردها).


يمكن لقراء Habra شراء الكتاب بالكامل بخصم على هذا الرابط .


ونذكرك أننا نبحث دائمًا عن مطورين رائعين ! تعال ، نحن نستمتع!

Source: https://habr.com/ru/post/ar416003/


All Articles