اذهب إلى المحتوى

ابراهيم الخضور

الأعضاء
  • المساهمات

    164
  • تاريخ الانضمام

  • تاريخ آخر زيارة

كل منشورات العضو ابراهيم الخضور

  1. تمتلك الرسوم الشعاعية vector graphics أهميةً واضحةً في حالات عديدة، إذ تتميز بحجم منخفض وقدرة كبيرة على تغيير أبعادها دون أن تتشوه عند تصغيرها أو تكبيرها، ونستعرض في مقالنا كيفية إضافة رسوميات مثل هذه إلى صفحات الويب وخاصةً صور SVG. لا بدّ قبل متابعة القراءة أن تكون على دراية بأساسيات HTML وبكيفية إدراج الصور في صفحاتك. ملاحظة: لا نهدف في هذا المقال إلى تعليمك إنشاء رسوميات SVG، وإنما فقط كيفية إدراجها في صفحات الويب. ما هي الرسوميات الشعاعية؟ ستتعامل خلال مسيرتك في عالم الويب مع نوعين من الصور: الصور النقطية Raster images: تعرَّف على صورة شبكة من البكسلات، إذ يحتوي ملف الصورة النقطية على معلومات تعرض موقع كل بكسل في الصورة واللون الدقيق لكل بكسل، ومن أكثر تنسيقات الصور النقطية انتشارًا على الويب هي بِت-ماب bmp. و png. و jpg. و gif.. الصور الشعاعية vector images: تُعرَّف باستخدام خوارزمية، إذ يحتوي ملف الصورة الشعاعية على تعريفات للشكل والمسار اللذين يستخدِمهما الحاسوب في عرض الصورة بالطريقة الصحيحة عندما يصيرها على الشاشة، ومن أمثلتها صور SVG التي تساعدك في إنشاء رسوم شعاعية مناسبة جدًا للاستخدام في الويب. حتى تدرك الفرق بين النوعين سنعرض عليك مثالًا يمكنك استعراضه مباشرة ضمن مستودع جيت-هاب المخصص، إذ يعرض الملف صورتان متطابقتان تمامًا لنجمتين حمراوين يحيط بهما ظل أسود إلى جوار بعضهما، وتكون الصورة اليمينية بتنسيق SVG واليسارية بتنسيق PNG، كما سيبدو الفرق واضحًا عندما تكبر الصورة في الصفحة، إذ تتشوه صورة PNG لأنها تحتوي على معلومات تحدِّد موقع كل بكسل بدقة كما تحدد لونه، فعندما تكبر هذه الصورة سيزداد حجم كل بكسل ليملأ حجمًا أكبر على الشاشة وستبدو الصورة مشوشةً، في حين لا يتغير شكل صورة SVG، لأن تغير أبعاد الصورة لن يؤثر في مظهرها نظرًا لاستخدام هذه الصور خوارزمية لعرض الشكل حتى بعد تضاعف حجمه. ملاحظة: إن كلتا الصورتين السابقتين في الواقع من نوع PNG (طريقة عرضهما في المقال)، إذ تمثِّل الصورة اليسارية صورةً نقطيةً، في حين تمثِّل الصورة اليمينية صورةً شعاعيةً. يُعَدّ حجم ملفات الصور الشعاعية أقل من مقابلاتها النقطية، لأنها ستحمل فقط عدة خوارزميات بدلًا من معلومات عن كل بكسل بمفرده. ما هي صور SVG؟ تُعَدّ SVG لغةً وصفيةً مبنيةً على أساس اللغة XML، وتشابه من حيث المبدأ لغة HTML، ما عدا أنها تضم عناصر مختلفة لتعريف الأشكال التي تريد عرضها في صفحتك والتأثيرات التي تريد تطبيقها على هذه الأشكال، فهي إذًا لغة لتوصيف الرسوم وليس المحتوى، وستجد عناصر إنشاء الأشكال البسيطة مثل الدائرة <circle> والمستطيل <rect> على أساس مثال عن أبسط العناصر، كما ستجد عناصر تعطي ميزات أكثر تقدمًا مثل <feColorMatrix> لتحويل الألوان باستخدام مصفوفة التحويل و <animate> لتحريك أجزاء من الرسم الشعاعي و <mask> لتطبيق قناع فوق صورتك. ستعطيك الشيفرة التالية على سبيل المثال صورةً لدائرة ومستطيل: <svg version="1.1" baseProfile="full" width="300" height="200" xmlns="http://www.w3.org/2000/svg"> <rect width="100%" height="100%" fill="black" /> <circle cx="150" cy="100" r="90" fill="blue" /> </svg> وستكون النتيجة كما يلي: قد يراودك شعور أنّ كتابة شيفرات الصور الشعاعية هين كما في المثال السابق، وبالطبع يمكنك كتابة شيفرات أمثلة بسيطة مثل تلك، ولكن سرعان ما ستجد صعوبةً بالغةً عند تصميم الصور الأعقد، إذ يستخدِم معظم المصممين برامج تحرير خاصة لإنشاء صور مثل هذه مثل "إنكسكيب Inkscape" و "إليستريتور illustrator"، كما تساعدك هذه البرمجيات في إنشاء رسومات متنوعة باستخدام أدوات متنوعة للرسم وإنشاء أشكال تقريبية للصور مثل ميزة "تتبع الخريطة النقطية Trace Bitmap" التي يتيحها برنامج "إنك سكيب". تمتلك صور SVG ميزات متقدمةً إضافةً إلى ما ذكرناه سابقًا: يمكن الوصول إلى النصوص ضمن صور SVG والتعامل معها، ولهذا الأمر فوائده فيما يتعلق بالسيو SEO. يمكن أن تنقل جزءًا من صورة إلى أخرى لأنها مكوّنات مستقلة على هيئة عناصر، وبإمكانك أيضًا تنسيقها باستخدام CSS وكتابة شيفرة للتحكم بها باستخدام جافاسكربت. لماذا سيفضِّل المطورون إذًا استخدام الصور النقطية على صور SVG؟ السبب هو بعض المساوئ: يمكن أن تتعقد شيفرتها بسرعة، مما يعني أنّ حجمها قد يزداد، وقد يتطلب تصييرها زمنًا أطول من قِبَل المتصفح. قد يكون إنشاء هذه الصورة أصعب من الصور النقطية تبعًا لطبيعة الصور التي تنوي إنشاءها. لا تدعم المتصفحات القديمة هذه الصور، وبالتالي لن تكون مناسبةً إذا قررت دعم تلك المتصفحات مثل النسخ القديمة لإنترنت إكسبلورر فهي مدعومة ابتداءً من النسخة IE9. يرى البعض أنّ الرسوميات النقطية أفضل عند إنشاء صور معقدة عالية الدقة مثل الصور الفوتوغرافية، وذلك للأسباب التي شرحناها آنفًا. إضافة صور SVG إلى صفحات الويب سنعرض في هذا القسم الطرق المختلفة التي يمكن اتباعها لإضافة صور SVG الشعاعية إلى صفحة الويب. الطريقة السريعة: استخدام العنصر يُنفَّذ الأمر بالإشارة إلى ملف الصورة الشعاعية من خلال السمة src، كما تحتاج إلى ضبط قيمتي الطول والعرض من خلال السمتَين width و height وخاصةً إذا لم ترث صورة SVG نسبة البُعدين aspect ratio، لذا راجع مقال إضافة الصور في صفحات HTML إذا لم تفعل ذلك من قبل. <img src="equilateral.svg" alt="triangle with all three sides equal" height="87" width="100" /> الحسنات: طريقة سريعة، صياغة قواعدية مألوفة، ويمكن تزويدها ببديل نصي من خلال السمة alt. يمكن تحويلها إلى رابط بسهولة عن طريق وضعها ضمن العنصر <a>. يمكن أن يخزِّن المتصفح ملف SVG تخزينًا مؤقتًا، مما يزيد من سرعة تحميل الصفحة إذا أردت تحميلها مجددًا في المستقبل. السيئات: لا يمكن التلاعب بالصورة عبر جافاسكربت. ينبغي استخدام قواعد CSS سطرية inline CSS ضمن شيفرة ملف SVG لتتمكن من التحكم بمحتوى الصورة، إذ لن تؤثر ملفات التنسيق الخارجية التي يستدعيها ملف SVG. لا يمكن إعادة تنسيق الصورة باستخدام أصناف CSS المجردة مثل :focus. استكشاف الأخطاء ودعم المتصفحات عند استخدامك متصفحات لا تدعم SVG مثل IE8 ومتصفحات نظام أندرويد بنسخة تحت 2.3، فيمكنك الإشارة إلى صورة PNG أو JPG عبر السمة src ومن ثم استخدم السمة srcset التي تميزها المتصفحات الحديثة فقط للإشارة إلى صورة SVG، وفي هذه الحالة لن تشغل صورة SVG سوى المتصفحات الحديثة التي تدعمها وستشغل المتصفحات الأقدم الصورة النقطية: <img src="equilateral.png" alt="triangle with equal sides" srcset="equilateral.svg"> يمكنك استخدام صورة SVG على أساس خلفية من خلال تنسيقات CSS، إذ ستجد في الشيفرة التالية أنّ المتصفحات القديمة ستتعامل فقط مع الصورة النقطية التي تفهمها، في حين ستميز الحديثة صورة SVG: background: url("fallback.png") no-repeat center; background-image: url("image.svg"); background-size: contain; لا يمكن كذلك التلاعب بالخلفية SVG التي ندرجها من خلال تنسيقات CSS باستخدام جافاسكربت، وتخضع للقيود نفسها التي يخضع لها استخدام قواعد CSS مع صور SVG التي يدرجها العنصر <img>. كيفية إدراج صور SVG ضمن صفحات HTML يمكنك فتح ملف SVG باستخدام محرر نصي، كما يمكن نسخ شيفرته ولصقها في ملف HTML، إذ تُدعى هذه العملية بالإدراج المباشر SVG inline أو inlining SVG، بحيث تُضاف هذه الشيفرة إلى HTML بين وسمَي البداية والنهاية للعنصر <svg>، وإليك مثالًا بسيطًا كما يلي: <svg width="300" height="200"> <rect width="100%" height="100%" fill="green" /> </svg> الحسنات: تقلل هذه العملية من طلبات HTTP، وبالتالي قد تقلل من زمن تحميل الصفحة. يمكنك استخدام السمة class والسمة id مع العنصر <svg>، كما يمكن تنسيقه باستخدام CSS ضمن العنصر ذاته أو في أيّ مكان تضع فيه قواعد تنسيق CSS الخاصة بملف HTML، وعمومًا يمكنك استخدام أي سمة وصفية للعنصر SVG على أساس خاصية من خواص CSS. يُعَدّ الإدراج المباشر لشيفرة SVG الطريقة الوحيدة التي تتيح لك استخدام قواعد CSS التفاعلية مثل الأصناف المجردة مثل :focus والرسوم المتحركة ضمن صور SVG، حتى ضمن صفحات التنسيق النمطية. يمكنك توصيف شيفرة SVG على أساس رابط بتغليفها داخل العنصر <a>. السيئات: تُعَدّ هذه الطريقة مناسبةً فقط إذا استخدمت SVG في مكان واحد، فالتكرار قد يسبب استخدامًا كثيرًا للموارد. سيزيد استخدام شيفرة SVG زائدة من حجم ملف HTML. لا يمكن للمتصفحات تخزين شيفرة SVG المباشرة كما تخزن شيفرة SVG المدرَجة ضمن الصور النمطية، وبالتالي لن تزيد سرعة تحميل الصفحات التي حُمِّلت سابقًا في هذه الحالة. بإمكانك تزويد صورة SVG بمحتوى بديل باستخدام العنصر <foriegnObject>، لكن ستنزل المتصفحات التي تدعم SVG أيضًا الصور البديلة أو الاحتياطية، ولهذا عليك التفكير مليًا في ضرورة دعم المتصفحات القديمة أو لا. إدراج صور SVG باستخدام العنصر <iframe> يمكنك أيضًا فتح صور SVG في متصفحك، وبالتالي يمكنك إدراج هذه الصورة في صفحتك باستخدام <iframe> على نحو مماثل تمامًا لما فعلناه في مقال آليات إدراج المحتوى والوسائط المتعددة في صفحة HTML، وإليك مراجعة سريعة: <iframe src="triangle.svg" width="500" height="500" sandbox> <img src="triangle.png" alt="Triangle with three unequal sides" /> </iframe> وبالطبع فهي ليست الطريقة الأفضل لإدراج SVG. السيئات: يملك لعنصر <iframe> آليةً لعرض محتوى بديل في حال حدثت مشكلة في عرضه، لكن ستعرض المتصفحات هذا المحتوى فقط إذا لم تدعم العنصر <iframe> بالكامل. إذا لم يكن لصفحتك وملف SVG أصلًا مشتركًا، فلن تتمكن من استخدام جافاسكربت في صفحتك الرئيسية للتلاعب بهذا الملف. تطبيق: العمل مع SVG سنحاول في هذا التطبيق العمل على بعض رسوميات SVG على سبيل التسلية، فلقد وضعنا في قسم المدخلات Input ضمن محرر الشيفرة في الأسفل بعض الأمثلة لتبدأ، كما يمكنك الاطلاع على عناصر SVG أخرى لتعمل عليها بالعودة إلى مرجع MDN إلى عناصر SVG، إذًا فهذا القسم لشحذ مهاراتك في البحث مع قليل من التسلية. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset. خلاصة قدمنا لك في هذا المقال جولةً سريعةً على الرسوم الشعاعية وملفات SVG، كما بيّنا فائدتها وكيفية إدراج صور SVG في صفحات الويب، فلم يكن القصد من هذا المقال تزويدك بدليل لتعلم SVG، وإنما لتعريفك بهذه التقنية إذا صادفتها لاحقًا خلال رحلتك في تطوير الويب، فلا تقلق إذًا إذا لم تجد نفسك خبيرًا بعد. سنناقش في آخر مقالات هذه السلسلة مفهوم الصور المتجاوبة responsive images بشيء من التفصيل، وسنلقي نظرةً على أدوات HTML التي تتيح لك إدراج صور تُعرَض جيدًا على مختلف الأجهزة. ترجمة -وبتصرُّف- للمقال Adding vector graphics to the web. اقرأ أيضًا نصائح لإنشاء وتصدير رسومات SVG للويب إضافة الصور في صفحة HTML كيفية إنشاء أشكال بسيطة باستخدام SVG
  2. لا بد وأنك قد امتلكت فكرةً واضحةً عن مفهوم إدراج المحتوى في صفحات الويب بما في ذلك إدراج الصور والفيديو والصوتيات، إذ سنتخذ في هذا المقال خطوات جانبيةً لنلقي نظرةً على العناصر <object> و <embed> و <iframe> التي تسمح بإدراج محتويات متنوعة في صفحات الويب، إذ يُستخدَم <iframe> في إدراج صفحات الويب أخرى؛ أما العنصران الباقيان فيسمحان لك بإدراج ملفات PDF و SVG وحتى ملفات فلاش التي تُعَدّ في نهاياتها ومع ذلك قد تراها. لا بد قبل البدء بقراءة هذا المقال أن تكون على دراية بأساسيات عمل الحاسوب وطريقة تثبيت البرمجيات الأساسية ودراية بطريقة عمل الملفات، ولا بد أيضًا من الدراية بأساسيات اللغة HTML كما عرضناها في مقال تعرَّف على HTML، وطريقة إدراج الصور في صفحة HTML وطريقة إدراج المحتوى البصري والسمعي في صفحة HTML. لمحة موجزة عن تاريخ إدراج المحتوى زادت فيما مضى شعبية استخدام الإطارات frames لإنشاء مواقع الويب، وهي أجزاء صغيرة من موقع ويب مخزنة ضمن صفحات HTML مستقلة، وقد أدرجت هذه الأجزاء في ملف رئيسي دُعي مجمِّع الإطارات frameset يعطيك إمكانية التحكُّم بالمكان الذي يشغله كل إطار بصورة مشابهة نوعًا ما لتحديد أبعاد الصفوف والأعمدة في جدول، وقد عُدّ هذا الأسلوب الأكثر جمالًا في بداية تسعينات القرن الماضي، كما لوحظ أن تقسيم الصفحة إلى أجزاء أصغر سيزيد من سرعة تحميلها وخاصةً مع سرعات الاتصال المنخفضة في تلك الفترة، ولكن مع ذلك، فقد عانى هذه الأسلوب من مشاكل عدة ألغت معها إيجابية السرعة، فلم تَعُد تراها في أيامنا. ازدادت شعبية تقنية الإضافات plug-ins بعد فترة -أي أواخر التسعينات وبداية الألفية الجديدة- مثل جافا أبليت Java Applets وفلاش، مما يسمح لمطورِي الويب من إدراج محتوى غني في صفحات الويب مثل مقاطع الفيديو والرسوم المتحركة التي لم تكن متاحةً باستخدام HTML وحدها، وقد أدرجت هذه التقنيات باستخدام عناصر مثل <object> والعنصر الأقل استخدامًا <embed>، ولقد كانا شديدي الفائدة حينها، لكن سرعان ما خبى تألقهما لأسباب عديدة يتعلق بعضها بسهولة الوصول والأمان وحجم الملفات، وقد توقفت معظم المتصفحات الحديثة في أيامنا عن دعم هذه الإضافات مثل تقنية فلاش. ظهر أخيرًا العنصر <iframe> مع طرق أخرى لإدراج المحتوى مثل <canvas> و <video> الذي يتيح إمكانية إدراج صفحة ويب بأكملها ضمن أخرى كما لو كانت صورةً أو أيّ عنصر آخر، وتستخدَم كثيرًا حاليًا. لننتقل بعد هذا الدرس التاريخي لنرى كيف نستخدِم بعض هذه العناصر. تطبيق: الاستخدامات التقليدية لإدراج العناصر سنقفز مباشرةً في هذا المقال إلى التطبيق لكي نعطيك فكرةً واقعيةً عن فائدة تقنيات إدراج المحتوى في صفحات ويب، إذ يشتهر موقع اليوتيوب كثيرًا في عالم الويب، لكن لا يعلم الكثيرون عن إمكانيات المشاركة التي يتيحها، فلنلق نظرةً إذًا على الطريقة التي يسمح فيها اليوتيوب بإدراج فيديو في أيّ صفحة باستخدام العنصر <iframe>. انتقل إلى موقع اليوتيوب واختر الفيديو الذي تريد. ستجد تحت الفيديو زر مشاركة Share، انقر عليه لتعرض خيارات المشاركة. اختر زر إدراج Embed وستشاهد شيفرة تضم العنصر <iframe>. انسخ هذه الشيفرة والصقها في الصندوق Input لمحرر الشيفرة الذي تراه في الأسفل وراقب ما يحدث في منطقة الخرج output. حاول إدراج خريطة من خرائط جوجل لتكسب نقاطًا إضافيةً: انتقل إلى جوجل ماب وانتق خريطةً ما. انقر على أيقونة القائمة الرئيسية -أي أيقونة الخطوط المتلاحقة- في الزاوية العليا اليسارية من واجهة المستخدِم. اختر مشاركة أو إدراج خريطة Share Embed map. اختر إدراج خريطة Embed map والذي سيزوّدك بشيفرة تضم العنصر <iframe>، انسخها. الصقها في الصندوق Input لمحرر الشيفرة الذي تراه في الأسفل وراقب ما يحدث في منطقة الخرج output. إذ ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. العنصر iframe بالتفصيل لقد كان الأمر مسليًا وسهلًا حتى الآن، وقد صُمم العنصر <iframe> ليسمح بإدراج صفحات ويب خارجية ضمن صفحة الويب التي تعمل عليها، كما تقدِّم هذه التقنية فائدةً عظيمةً لإضافة محتوى مصدره طرف ثالث في الصفحة لا يمكنك التحكّم به مباشرةً ولا تريد في الوقت نفسه إدراج نسخة خاصة بك من هذا المحتوى مثل الفيديوهات التي تقدمها مزودات الفيديو على الإنترنت مثل يوتيوب أو أنظمة إدراج التعليقات مثل Disque أو الخرائط أو لوحات الإعلانات وغيرها، وما سنعمل عليه في مقالنا هو استخدام العنصر <iframe> في إدراج محتوى مثل هذا في صفحات ويب. لكن، هنالك بعض الهواجس الأمنية فيما يتعلق باستخدام <iframe> كما سنناقش لاحقًا، لكن هذا لايعني ألا تستخدِمه في مواقعك، وإنما لدفعك إلى القليل من التفكير المتأني، ولنتفحّص الشيفرة التالية بشيء من التفصيل، فلنقل أنك تريد إدراج "قائمة مصطلحات شبكة مطوري موزيللا MDN glossary"، إذ يمكنك محاولة العمل على الشيفرة التالية: <head> <style> iframe { border: none } </style> </head> <body> <iframe src="https://developer.mozilla.org/en-US/docs/Glossary" width="100%" height="500" allowfullscreen sandbox> <p> <a href="/en-US/docs/Glossary"> Fallback link for browsers that don't support iframes </a> </p> </iframe> </body> يضم المثال السابق الأساسيات الضرورية لاستخدام <iframe>: border: none: إذا استخدمت هذه السمة، فسيُعرَض العنصر <iframe> دون إطار محيط به، وإلا فسيعرض المتصفح افتراضيًا إطارًا حوله. allowfullscreen: إذا استخدمت هذه السمة، فسيُعرَض العنصر <iframe> داخل شاشة كاملة، وذلك باستخدام الواجهة البرمجية التي تدير العرض على شاشة كاملة Fullscreen API. src: تشابه نظيرتها في العنصر <video> وتضم مسارًا يدل على عنوان URL الخاص بالمحتوى المُدرج. height و width: وهما سمتان تحددان طول وعرض المنطقة التي تعرض محتوى العنصر <iframe>. المحتوى الاحتياطي Fallback content: كما هو الحال مع العنصر <video>، إذ يمكنك إدراج محتوى احتياطي بين وسمَي البداية والنهاية للعنصر <iframe> سيظهر في حال لم يدعم المتصفح هذا العنصر، وقد زودنا المستخدِم في حالات مثل هذه برابط مباشر إلى الصفحة التي لم يتمكن المتصفح من إدراجها، لكن من غير المحتمل أن تصادف متصفحًا حديثًا لا يدعم <iframe>. sandbox: تعمل هذه السمة في متصفحات أكثر حداثةً مثل إنترنت إكسبلورر 10 موازنةً بغيرها من ميزات <iframe>، كما تتطلب إعدادات أمان متشددة أكثر، وسنتحدث عن ذلك في القسم التالي. ملاحظة: لتزيد من سرعة تحميل الصفحة يمكن إسناد قيمة السمة src باستخدام شيفرة جافاسكربت عند انتهاء تحميل المحتوى الرئيسي للصفحة، وبهذه الطريقة يمكن استخدام صفحتك مباشرةً ويقل زمن تحميلها الرسمي وهو معيار سيو SEO هام. اعتبارات أمنية لا نتوقع منك أن تفهم جيدًا جميع هذه الاعتبارات الأمنية من المرة الأولى التي تتعرف عليها، وإنما نريدك الانتباه إليها وأن يكون لديك مرجع لتعود إليه عندما تزداد خبراتك وتقرر استخدام العنصر <iframe> في أعمالك، ولا حاجة للخوف طبعًا من استخدام هذا العنصر وإنما لمزيد من الانتباه. لقد تعلم صانعِي المتصفحات ومطورِي الويب من تجاربهم المريرة أنّ النوافذ الضمنية المتمثلة بالعنصر <iframe> هي هدف شائع -أو ضحية هجوم Attack vectom بالتسمية الرسمية- لأشخاص سيئون على الويب والذين يطلَق عليهم اسم مختطفين hackers أو مخترقين crackers، وذلك كي يهاجموك من خلالها عندما يحاولون تعديل صفحتك بطريقة غير سليمة أو أن يخدعوا آخرين لفعل أمور لا يريدون فعلها مثل إفشاء معلومات حساسة مثل اسم المستخدِم أو كلمة السر، ولهذا السبب يحاول المهندسون المختصون ومطورو المتصفحات ابتكار آليات حماية متنوعة لجعل استخدام العنصر <iframe> أكثر أمانًا وإيجاد طرق عملية مثلى يجدر اتباعها وسنغطي بعضًا منها في هذا القسم. ملاحظة: يعد الاختطاف بالنقر هجومًا شائعًا يستهدف النوافذ الضمنية، إذ يُدرِج المخترق نافذةً غير مرئية في صفحتك أو يدرج صفحتك ضمن موقعه المشبوه، ثم يستخدِم ذلك في لفت انتباه المستخدِمين، وهي طريقة شائعة في تضليلهم أو لسرقة بياناتهم الحساسة. سنقدم لك مثالًا سريعًا: حاول تحميل المثال الذي عرضناه في الأعلى في متصفحك، إذ يمكنك إيجاده ضمن المستودع المخصص له على جيت -هاب، كما يمكنك أيضًا الاطلاع على الشيفرة المصدرية، ما سيحدث أنه بدلًا من أن يعرض لك الصفحة المطلوبة وهي "قائمة مصطلحات MDN"‘ فقد تشاهد رسالة من قبيل "لا يمكن فتح هذه الصفحة I can't open this page"، فإذا انتقلت إلى "الطرفية Console" ضمن أدوات مطورِي الويب المدمجة مع المتصفح، فستجد رسالةً تفسِّر لك سبب ما حدث، إذ سيكون نص هذه الرسالة في فايرفوكس مشابهًا للتالي: (إنّ فتح المورد "https://developer.mozilla.org/en-US/docs/Glossary" في إطار مرفوض لأن التوجيه "X-Frame-Options" قد ضُبط على القيمة DENY)، ويعود السبب إلى أنّ المطورين الذين بنوا "شبكة مطور موزيللا MDN" قد أضافوا إعدادًا إلى الخادم الذي يستضيف MDN يمنع إدراج هذه الصفحات ضمن نوافذ ضمنية <iframe>، ويُعَدّ الأمر منطقيًا، فمن غير الوارد إدراج صفحة كاملة من MDN ضمن صفحات أخرى ما لم تحاول وضعها في موقعك الخاص ثم تدعي ملكيتك لها، أو أن تحاول سرقة بيانات حساسة من خلال هجوم الاختطاف بالنقر، وكلاهما أمر سيء، وبالإضافة إلى ذلك، سيكلِّف هذا الفعل موزيللا مبالغ طائلة عندما يتخطى استهلاك الموقع عرض حزمة التراسل المعتمدة. لا تدرج ما ليس ضروريا قد يبدو معقولًا في بعض الحالات إدراج محتوى مصدره طرف آخر مثل فيديوهات يوتيوب أو خرائط جوجل، لكن وفِّر على نفسك الكثير من المشاكل ولا تدرج أيّ محتوى مصدره طرف ثالث ما لم يكن ذلك ضروريًا جدًا، وتذكَّر دائمًا قاعدة أمان أساسية في ويب تقول: "لا شيء يُدعى زيادةً في الحرص، تحقق مجددًا حتى لو فعلت ذلك، وإذا تحقق أحدهم، فافترض أنك في خطر حتى يثبت العكس". إضافة إلى عامل الأمان، لا بد من الانتباه إلى مواضيع الخصائص، إذ يتمتع المحتوى بمعظمه بحقوق نشر على الإنترنت أو خارجها حتى غير المتوقع منها مثل معظم صور ويكيميديا، لذلك لا تعرض محتوًى على صفحتك ما لم تمتلكه أو حصلت على إذن مكتوب وجلي، فعقوبات مخالفة حقوق النشر شديدة، ونقولها مجددًا: "لا شيء يُدعى زيادةً في الحرص"، فإذا كان المحتوى مرخصًا، فالتزم بشروط الرخصة، فمحتوى أكاديمية حسوب مثلًا مرخَّص بموجب الرخصة CC-BY-NC-SA، ويعني ذلك أنه عليك الإشارة إلى أكاديمية حسوب بصورة مناسبة عند اقتباس أيّ محتوى منها حتى لو أجريت تعديلات مهمة عليه واستعمال المحتوى بشكل غير تجاري. استخدم بروتوكول HTTPS يُعَدّ نسخةً مشفرةً من بروتوكول HTTP، وينبغي استخدامه ما أمكن في موقعك: يقلل استخدام HTTPS فرصة التلاعب بالمحتوى عن بعد أثناء نقله. يمنع HTTPS المحتوى الذي تُدرجه من الوصول إلى محتوى الصفحة الأساسية وبالعكس. يمكِّن بروتوكول HTTPS موقعك من طلب تثبيت شهادات أمان خاصة، إذ تقدِم الكثير من مزودات الاستضافة استخدام HTTPS دون الحاجة إلى أية إعدادات من طرفك لتثبيت شهادة أمان في مكانها، وإذا أردت أيضًا إعداد خدمات HTTPS بنفسك على موقعك، فستجد أدوات وإرشادات تقدِّمها منظمة Let's Encrypt لإنشاء وتثبيت الشهادات المطلوبة تلقائيًا مع دعم مدمج لمعظم خوادم الويب الأكثر استخدامًا مثل Apache و Nginx وغيرها، وقد صممت تلك الأدوات لتجعل العملية سهلة قدر الإمكان، فلا سبب يمنع استخدامها أو غيرها من وسائل دعم HTTPS على موقعك. ملاحظة: تدعم خدمة GitHub pages بروتوكول HTTPS افتراضيًا، فهي منتسبة إذًا لاستضافة المحتوى، فإذا كنت تستخدِم مزود استضافة آخر ولست واثقًا من دعمه للبروتوكول، فاسأله عن هذه التفاصيل. استخدم السمة sandbox دائما عليك منح المحتوى الذي تُدرجه في موقعك أقل مستوى من السماحيات التي تمّكنه من إنجاز عمله دون إعطاء فرصة للمخترقين بإحداث ضرر في موقعك، وينطبق الأمر أيضًا على المحتوى الخاص بك، كما تُدعى أي حاوية للشيفرات يمكن أن تُستخدَم فيها تلك الشيفرات بصورة ملائمة أو لأغراض اختبارية دون أن تسبب أذىً لبقية الشيفرة مصادفةً أو عمدًا بصندوق معزول أو محمي sandbox. يمكن لأيّ محتوى لا يقيده صندوق عزل فعل الكثير على صفحاتك مثل تنفيذ شيفرات جافاسكربت أو إرسال استمارات أو عرض نوافذ منبثقة، لذلك لا بد من استخدام كل التقييدات المتاحة باستخدام السمة sandbox دون أية معاملات -أي قيم- كما عرضناها في مثالنا السابق. لكن إذا كان الأمر ضروريًا، فتستطيع إضافة الأذونات واحدًا تلو الآخر داخل قيمة السمة ""=sandbox وسنتحدث عن هذه الأذونات تاليًا، كما تجدر الإشارة إلى عدم استخدام الخيارين allow-scripts و allow-same-origin معًا، فقد يلتف المحتوى الذي تُدرجه على الخيار allow-same-origin الذي يمنع المواقع من تنفيذ الشيفرة، ثم تُستخدم جافاسكربت في إلغاء عمل السمة sandbox كلها. ملاحظة: لا يمكن لصندوق عزل تأمين الحماية من المخترقين إذا خدعوا المستخدِمين ودفعوهم لزيارة مواقع مشبوهة مباشرةً خارج العنصر <iframe>، فإذا كان هناك احتمال أن يكون المحتوى الذي تريد إدراجه مشبوهًا مثل المحتوى الذي ينتج عن تفاعل مستخدِمين، فحاول تقديمه لمتابعيك من نطاق domain مختلف عن نطاق موقعك. تهيئة توجيهات سياسة أمن المحتوى CSP تؤمن سياسة أمن المحتوى مجموعةً من ترويسات بروتوكول HTTP وهي بيانات وصفية تُرسل مع صفحات ويب عندما يعيدها الخادم مصممة لتحسين مستوى أمان صفحات HTML؛ أما فيما يتعلق باستخدام العنصر <iframe>، فبإمكانك تهيئة الخادم ليعيد ترويسة تضم خيار ملائم للتوجيه X-Frame-Options، إذ سيمنع ذلك مواقع الويب الأخرى من إدراج المحتوى الخاص بك في صفحاتها والذي قد يمكِّن هجمات الاختطاف بالنقر وغيرها من الهجمات، وهذا ما فعله تمامًا مطورو MDN كما رأينا سابقًا. العنصران object و embed لهذين العنصرين وظيفتين مختلفتين عن العنصر <iframe>، فهما أداتان عامتان لإدراج محتوى خارجي مثل ملفات PDF، ومع ذلك لن تستخدِم هذين العنصرين كثيرًا، إذ يستحسن إذا أردت مثلًا عرض ملف PDF أن تربطه بصفحتك بدلًا من إدراجه. استُخدِم هذان العنصران تاريخيًا لإدراج محتوى تتعامل معه إضافات المتصفح plug-ins مثل أدوبي فلاش، لكنها تقنية في طريقها إلى الزوال حاليًا ولا تدعمها المتصفحات الحديثة، لكن إذا رأيت نفسك مضطرًا إلى إدراج إضافة، فإليك الحد الأدنى من المعلومات اللازمة لإنجاز الأمر: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } <embed> <object> عنوان URL للمحتوى الذي ستدرجه src data النوع الدقيق لملف الوسائط الذي تدرجه type type طول وعرض بكسلات CSS الصندوق الذي تتحكم به الإضافة height ، width height ، width الأسماء والقيم التي تستخدمها الإضافة على أساس معامِلات استخدم عند الحاجة سمات بهذه الأسماء أو القيم استخدم عنصر <parm> مفرد ضمن العنصر <object> محتوى HTML مستقل على أساس خطة بديلة عند عدم توافر المورد غير مدعوم (العنصر <noembed> ملغي) يمكن إضافتها داخل العنصر <object> بعد <parm> لنلق نظرةً على مثال يستخدِم العنصر <object> لإدراج ملف PDF في صفحة ويب، كما يمكنك أيضًا تجريب المثال مباشرة والاطلاع على الشيفرة المصدرية: <object data="mypdf.pdf" type="application/pdf" width="800" height="1200"> <p>You don't have a PDF plugin, but you can <a href="mypdf.pdf">download the PDF file. </a> </p> </object> تُعَدّ ملفات PDF خطوةً انتقاليةً هامةً بين الورقيات والعالم الرقمي، لكنها تعرض الكثير من التحديات المتعلقة بسهولة الوصول، وقد يعاني البعض من قراءتها ضمن شاشات صغيرة، ومازالت هذه الملفات شعبيةً ضمن بعض حلقات المستخدِمين، مع ذلك، يفضَّل ربطها بصفحاتك ليتمكن هؤلاء من تنزيلها أو قراءتها في صفحة مستقلة بدلًا من إدراجها في صفحتك. خلاصة حاولنا في مقالنا هذا تقديم مفهوم إدراج محتوى في صفحاتك بطريقة مبسطة، لأنّ موضوعًا مثل هذا قد يتعقّد بسرعة إذا خضنا فيه بطريقة غير ملائمة، ولهذا اعتمدنا أسلوبًا مألوفًا يبدو متعلقًا مباشرةً بمضمون سلسلة المقالات، ومع ذلك يحاول شرح بعض الميزات المتقدمة المتعلقة بالتقنيات المستخدَمة، كما ننصحك في البداية بعدم إدراج محتوى خارج نطاق المحتوى الذي يقدمه طرف ثالث مثل الفيديوهات والخرائط، وسرعان ما ستبدأ البحث عن استخدامات أخرى لعناصر الإدراج عندما تصبح أكثر تمرّسًا. ستجد الكثير من التقنيات المتعلقة بإدراج محتوى خارجي بالإضافة إلى ما ناقشناه، فقد رأينا بعضها في المقالات السابقة مثل العناصر <video> و <audio> و <img>، كما ستجد غيرها الكثير مثل <canvas> الذي يُستخدَم مع جافاسكربت لتوليد رسوميات ثنائية وثلاثية الأبعاد والعنصر <SVG> الذي يستخدَم في إدراج الرسوميات الشعاعية، وهذا هو موضوعنا في المقال القادم. لمزيد من التفاصيل حول عناصر HTML، ارجع إلى توثيق لغة HTML العربي في موسوعة حسوب. ترجمة -وبتصرُّف- للمقال From object to iframe - other embedding technologies. اقرأ أيضًا مكونات الويب: عناصر HTML المخصصة وقوالبها إضافة محتوى سمعي ومرئي في صفحة HTML إضافة الصور في صفحة HTML
  3. سنتحدث في هذا المقال عن مفهوم جهة انسياب المحارف Directionality عند الكتابة، وهو أمر على درجة كبيرة من الأهمية لمؤلفي المقالات والكتب التي تتطلب استخدام لغتين أو إدخال شيفرات برمجية أو عبارات رياضية مخصصة؛ لكننا سنوجّه هذا المقال لمستخدمي تطبيق مستندات جوجل لأنه لا يقدم دعمًا واضحًا لهذه النقاط على غرار بقية المحررات النصية المتقدمة مثل مايكروسوفت وورد الذي لن يلاحظ مستخدموه الكثير من المشاكل التي نستعرضها في مقالنا. قد تبدو المفاهيم مربكةً للوهلة الأولى، لكن ما إن تألفها حتى تسهّل عليك الأمور وتوفر وقتك وجهدك في تصويب أخطاء قد لا تدرك وجودها إلّا بعد فوات الآوان. مفهوم جهة انسياب المحارف يشير مصطلح "جهة انسياب المحارف" إلى الاتجاه الذي تتحرك وفقه محارف النص من حرف إلى آخر. ولأن معظم قراء هذه المقالة من المؤلفين الذين يكتبون باللغة العربية مع لغة لاتينية أو أكثر، فسنلقي الضوء على ثلاث اتجاهات لانسياب الأحرف هي: يمين-إلى-يسار Right-to-Left واختصارًا RTL: من أبرز الأمثلة عليها اللغة العربية. يسار-إلى-يمين Left-to-Right واختصارًا LTR: ومن أبرز الأمثلة عليها اللغات اللاتينية مثل الإنجليزية. محايدة Neutral: لا تتعلق بجهة محددة بل تتبع سياق الأحرف مثل علامات الترقيم. جهة انسياب المحارف للمحارف جهة انسياب أصيلة لا يمكن أن تتغير تلقائيًا، فمحارف اللغة العربية مثلًا هي محارف من اليمين إلى اليسار حصرًا، ويأتي كل حرف على يسار الآخر، بينما تتجه محارف اللغات اللاتينية دائمًا من اليسار إلى اليمين، في حين يأتي كل حرف على يمين الآخر؛ أما المحارف المحايدة، فتأخذ اتجاه المحارف المحيطة بها، فإن كانت محاطةً بمحرفين لهما نفس جهة الانسياب، ستأخذ جهة انسيابهما، بينما إن كانا من جهتي انسياب مختلفتين، فستأخذ جهة انسياب الفقرة. جهة انسياب الفقرات الفقرة هي كتلة متواصلة من المحارف تنتقل وفق اتجاه محدد وتنتهي بمحرف السطر الجديد. تدعم محررات النصوص ثنائية الاتجاه، مثل مستندات جوجل الكتابة من اليمين إلى اليسار وبالعكس، لاحظ لقطة الشاشة التالية: لا تمتلك الفقرات جهة انسياب مخصصة لها، بل يفرض عليها محرر النصوص اتجاهًا واحدًا، فإما أن تكون من اليمين إلى اليسار RTL، أو من اليسار إلى اليمين LTR، ولا يمكن أن تكون محايدة الاتجاه. فلو فرضنا وجود العبارة التالية "العربية English فارسى"، فإنها ستُعرض بنفس الترتيب (ابتداءً من الهامش نحو الداخل)، سواءً كان الاتجاه RTL أو LTR، وذلك لأن كلمتي "العربية" و"فارسى" لهما محارف ذات اتجاه أصلي RTL، أما "English" فاتجاهها الأصلي هو LTR، وطالما لا وجود لاتجاه موحد لكلمات الفقرة، فستخضع العبارة إلى تنسيق الفقرة. حل مشاكل انسيابية الأحرف في مستندات جوجل أدخلت إلى محارف Unicode مجموعة محارف لا تُطبع على الشاشة وتستخدم للتحكم بانسيابية الأحرف، حيث تُستخدم هذه المحارف بفعالية لحل مشكلة التغير الخاطئ لاتجاه الأحرف عند تبديل لغة الكتابة من يمينية إلى يسارية والعكس. سنتعرف بداية على هذه المحارف، وننتقل بعد ذلك إلى كيفية استخدامها في تطبيق مستندات جوجل لحل بعض مشاكل انسيابية الأحرف. محارف التحكم باتجاه انسياب المحارف المحرف LRM: يقرأ بالشكل (علامة الانسياب نحو اليمين)، ويستخدم لضبط اتجاه نص ينساب نحو اليمين ضمن فقرة تنساب إلى اليسار. تظهر المشكلة عندما تنتهي الجملة التي تنساب إلى اليمين بمحرف محايد كإشارة التعجب. في هذه الحالة إن لم يأتي بعده محرف LTR أي محرف يعزز الاتجاه نحو اليمين (حرف لاتيني مثلًا)، فسيأخذ المحرف المحايد تنسيق الفقرة ويكون RTL، وينكسر الانسياب نحو اليمين ويعود للانسياب نحو اليسار. تُعطى شيفرة Unicode للمحرف بالشكل "U+200E". المحرف RLM: وله عمل معاكس للمحرف السابق، إذ يعطي علامةً لمجرى الكتابة بالتحول نحو اليسار، وتُعطى شيفرة Unicode للمحرف بالشكل"U+200F". المحرفين RLE و LRE: يقرآن بالشكل (إدراج كتلة تنساب إلى اليمين LRE أو اليسار RLE)، وتستخدمان كما يُستخدم المحرفان السابقان، إلا أنهما يستعملان عادةً في المحررات التي لا يوجد فيها ضبط لاتجاه الفقرات، وينتهي تأثير هذا المحرف عند انتهاء الفقرة أو ظهور المحرف المعاكس أو المحرف PDF (اختصار للمصطلح Pop Directional formatting أنهِ تنسيق الانسياب) الذي ينهي عمل أي من المحرفين السابقين. استخدام محارف التحكم باتجاه الكتابة في تطبيق مستندات جوجل لإدراج محارف التحكم في مستندات جوجل، نفذ الخطوات التالية: من "إدراج" انتقل إلى "الرموز الخاصة" لتظهر لك النافذة التالية: في مربع البحث اكتب العبارة "right-to-left" أو "left-to-right" لإيجاد هذه المحارف وستكون نتيجة البحث كالتالي: انقر على الرمز الذي تشاء استخدامه وسيدرج تمامًا حيث يكون مؤشر الكتابة. تمرين تطبيقي (1): حوّل العبارة المكتوبة بالشكل "مرحبًا Hello!" إلى شكلها الصحيح "مرحبًا !Hello". الحل: أدرج الرمز LRM بعد كتابة إشارة التعجب لتخبر مجرى الكتابة أن يعامل المحرف المحايد "!" وكأنه محرف LTR. تمرين تطبيقي (2): حول العبارة ( means careful "!انتبه" The Arabic word) إلى شكلها الصحيح ("means careful "انتبه!" The Arabic word). الحل: أدرج الرمز RLM بعد كتابة إشارة التعجب لتخبر مجرى الكتابة أن يعامل المحرف المحايد "!" وكأنه محرف RTL. تمرين تطبيقي (3): يريد مبرمج إدراج تعليق ضمن شيفرته يشرح باللغة العربية نوع المتغيّر الذي سيعرّفه كالتالي: // التصريح عن المتغيّر Counter على أنه من النوع int int Counter =0; طبعًا كتابة هذا الشرح غير كافٍ، لذا فهلّا ساعدته في حل المشكلة من خلال محارف توجيه الانسياب؟ الحل: انتقل بكل بساطة إلى اليمين واختر اللغة العربية، وسيكون اتجاه الفقرة عندها من اليمين إلى اليسار. اكتب الجملة السابقة تمامًا كما هي وأنهها بالمحرفين (//) كالتالي: "التصريح عن المتغير counter على أنه من النوع int //". افتح بعذ ذلك لوحة المحارف الخاصة وأدرج المحرف RLE في بداية الجملة. لن تلاحظ في البداية أي شيء، لكن بمجرد تغيير اتجاه الفقرة لتصبح من اليسار إلى اليمين ستجد أنك حللت المشكلة! تمرين تطبيقي (4): واجهت شخصيًا عند ترجمة أحد الكتب التي تتحدث عن استخدام التعابير النمطية في جافاسكربت (وهي سلسلة من الرموز تساعدك في البحث عما تريده في نص أو صفحة) أوقاتًا عصيبةً جدًا في كتابة هذه السلاسل ضمن جمل اللغة العربية، علمًا أنني استخدمت تطبيق مستندات جوجل. كان ذلك قبل أن أتعرف على محارف التحكم بالانسياب، لهذا أطلب مساعدتكم في كتابة الجملة الواردة في الصورة التالية: الحل: اكتب الجملة العربية أولًا بالطريقة الاعتيادية، ثم ضع النقطتين المتعامدتين وأدرج بعدها المحرف LRM وستلاحظ أنك ستكتب سلسلة الرموز بعد ذلك بالشكل الصحيح! تهيئة اختصارات سريعة لمحارف جهة الإنسياب في تطبيق مستندات جوجل ستساعدك تلك المحارف كثيرًا، وأشجعك على استخدامها كي لا يضيع وقتك سدى في إيجاد طريقة لترتيب المحارف المختلطة، والتي قد لا تجدها أبدًا! لكن من الواضح أنّ عملية الانتقال إلى نافذة "المحارف الخاصة" ثم البحث عن محارف التحكم باتجاه انسياب المحارف ثم إدراجها في كل مرة أمر أشد إزعاجًا كما يرى البعض. فما الحل إذًا؟ يقدِّم تطبيق مستند جوجل طريقةً لاستبدال سلسلة من المحارف عندما ترد تباعًا بمحرف آخر، إذ يستبدل التطبيق مجموعة المحارف (c) مثلًا بالرمز ©. تُسهّل هذه الميزة حل مشكلتنا كثيرًا، إذ سنخص لكل محرف من محارف الانسياب السابقة سلسلةً من المحارف التي يستبدلها تطبيق مستندات جوجل عند ورودها بمحرف التحكم المطلوب، ونكون قد أنجزنا الأمر. المشكلة أنه لا توجد في تطبيق مستندات جوجل طريقة لنسخ هذه المفردات لذلك سنحتال على الأمر قليلًا! إليك قائمةٌ برموز Unicode الخاصة بكل محرف من محارف التحكم السابقة: table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المحرف رمز Unocode RLM U+200F LRM U+200E RLE U+202B LRE U+202A اتبع الآن الخطوات التالية: انتقل إلى موقع unicode-explorer الذي يمكنك من نسخ تلك المحارف إن كنت تعرف رمزها. كل ما عليك فعله أن تكتب العنوان التالي "/https://unicode-explorer.com/c" وتضيف رمز المحرف في نهايته (بلا +U)، أي عندما تنتقل إلى العنوان "https://unicode-explorer.com/c/202f" ستصل إلى صفحة المحرف RLM، وعندها انقر على الزر "Copy" لنسخ محرف التحكم. بإمكانك أيضًا استخدام الجدول المعروض في لقطة الشاشة هذه لنسخ محارف التحكم، وكل ما عليك فعله هو أن تنقر الزر "Select"، ثم الضغط على الاختصار Ctrl+C لنسخه: عد إلى تطبيق مستندات جوجل وانتقل إلى "أدوات"، ثم "إعدادات مفضلة" لتظهر لك نافذة الإعدادات. انقر بعد ذلك على النافذة الفرعية "بدائل"  .   سأستبدل المحرفين << عند كتابتهما تلقائيًا بمحرف التحكم RLM، لذلك سأكتب في نافذة "البدائل" الرمز << في المربع "استبدال"، ثم ألصق رمز المحرف RLM الذي نسخته سابقًا في المربع "بـ". عند نجاح الأمر، سيظهر سطر جديد في نافذة "البدائل" يضم الاختصار الذي أنشأته. كرر العملية باختيار الرموز التي تناسبك لكي تستبدل بقية محارف التحكم بانسياب الأحرف. وأقترح أن تستخدم >> للمحرف LRM و]] للمحرف RLE و[[ للمحرف LRE، كما يمكن استخدام الرمز LRM مثلًا للمحرف RLM وهكذا. بهذه الشكل ستكون قادرًا على إدراج محرف التحكم المطلوب بمجرد كتابة سلسلة الرموز التي اخترتها، إذ سيستبدل التطبيق هذه السلسلة بمحرف التحكم تلقائيًا، مما يوفر عليك الوقت والجهد. خلاصة تحدثنا في هذا المقال عن مفهوم جهة انسياب المحارف ورأينا ما يلي: بعض محررات النصوص مثل مستندات جوجل قادر على التعامل إلى حد ما مع الاتجاهات الثنائية يمين إلى يسار والعكس. لبعض المحارف اتجاه أصلي وبعضها الآخر محايد يأخذ اتجاهه من اتجاه المحارف التي تحيط به، مثل علامات الترقيم. تُظهر العناصر المحايدة بعض المشاكل إن لم تتمكن من تحديد اتجاه انسياب النص وتتبع اتجاه انسياب الفقرة. تُستخدم عناصر التحكم باتجاه انسياب المحارف RLM و LRM و LRE و RLE في توجيه النصوص مختلفة الاتجاهات إن كتبت في نفس الفقرة، لكن بطريقة غير صحيحة، وهي محارف غير مطبوعة ولا تظهر على الشاشة. يمكن إدراج هذه العناصر في مستندات جوجل من نافذة "العناصر الخاصة" أو يمكن تخصيص سلسلة محارف خاصة تُستبدل عند ورودها تباعًا بمحرف التحكم من خلال نافذة "بدائل" ضمن نافذة "الإعدادات المفضلة". اقرأ أيضًا مقدمة إلى تطبيق مستندات جوجل Google Docs قائمة الأدوات في مستندات جوجل المشاركة وتوزيع المهام على مستندات جوجل تنسيق وتوثيق الأوراق الأكاديمية باستخدام مستندات جوجل
  4. سنناقش في هذا المقال الموجه خصيصًا للطلاب والباحثين، كيفية استخدام تطبيق مستندات جوجل لكتابة أوراق بحثية وأكاديمية خطوةً خطوة. ربما تمتلك فكرةً عن معظم الأفكار والأدوات التي سنستخدمها في مقالنا، لكننا سنعتمد معايير التنسيق المستخدمة عالميًا في كتابة الأوراق، مثل تنسيق "جمعية اللغات المعاصرة Modern Language Association" واختصارًا MLA وتنسيق "جمعية علم النفس الأمريكية American Psychological Assossiation" واختصارًا APA. هنالك الكثير من التنسيقات الأخرى لكتابة المقالات والأوراق البحثية، لكننا سنكتفي بهذين التنسيقين لأنهما الأكثر شيوعًا من جهة، ولأنهما مدعومان من قِبل مستندات جوجل من جهة أخرى. إنشاء مستند جديد سنفترض في هذه الفقرة أنك تمتلك حسابًا على جوجل، وتألف طريقة الوصول إلى تطبيق مستندات جوجل من خلال جوجل درايف Google Drive أو موقع مستندات جوجل مباشرةً. من خلال موقع مستندات غوغل: النقر على "أيقونة (+)" يمين أسفل الصفحة. بكتابة العبارة "docs.new" في شريط عنوان المتصفح الذي تستخدمه، ثم الضغط على المفتاح Enter. من خلال جوجل درايف: انقر على قائمة "جديد" على يمين الشاشة، ثم النقر على خيار "مستندات Google". التنسيق العام تُطبق مجموعة من التنسيقات العامة المشتركة بين MLA و APA، وهي كالتالي: خط الكتابة وحجمه تفضل جميع التنسيقات السابقة أن يكون الخط مقروءًا دون مبالغة، وأن تكون عائلة الخط المستخدم مألوفةً للقارئ ومريحةً دون وجود ديكورات خاصة، ولهذا وجدتُ أن جميع تلك التنسيقات تدعم الخط "Times New Roman" أو "Arial" على أن يكون حجمه 12. ولتنفيذ الأمر: انقر على القائمة المنسدلة "الخط" في شريط الأدوات أعلى الصفحة، ثم تنقّل بين الخطوط حتى تصل إلى "Times New Roman". انقر على القائمة المنسدلة "حجم الخط" إلى جوار القائمة السابقة، واختر حجم الخط "12". تحديد هوامش الصفحة وقياسها يحدّد التنسيقان السابقان عرض جميع الهوامش (أعلى وأسفل ويمين ويسار) لتكون 1 إنش (2.54 ستنيمتر). عادةً ما يضبط تطبيق مستندات جوجل عرض الهوامش على 1 إنش، لكن لا بد من التأكد من ذلك: انقر على القائمة "ملف"، ثم انتقل إلى إعداد الصفحة. تحقق في نافذة إعداد الصفحة أن جميع الهوامش هي 2.54 سم أو 1 إنش. تفضّل التنسيقات السابقة أن يكون قياس الصفحة من النوع "رسالة 11x8.5 إنش"، حيث يطابق هذا القياس في مستندات جوجل الخيار "21.6x27.9 Lettre سم" ضمن القائمة المنسدلة "مقاس الورق" في نفس النافذة. يمكنك بالطبع اختيار القياس الذي يريده المشرف على الورقة البحثية أو الجهة المسؤولة عن طباعتها. تحديد التباعد بين الأسطر وإزالة الفراغات في بداية الفقرات ينبغي ضبط التباعد بين الأسطر ليكون بمقدار "2" أو ما يسمى بالفراغ المضاعف double-spaced. بين جميع الأسطر والعناوين الرئيسية والفرعية والفقرات والترويسة وقائمة المراجع، أي باختصار، في كل مكان ضمن الورقة البحثية. وقد تجد إغراءً في ترك المزيد من الأسطر الفارغة بين العناوين الرئيسية وباقي الفقرات، لكن هذا الأمر مرفوض في جميع التنسيقات. لضبط التباعد في مستندات جوجل تحتاج إلى فعل الآتي: انقر على قائمة "تنسيق" في شريط الأدوات. انتقل إلى القائمة الفرعية "تباعد الأسطر والفقرات"، ثم اختر "مزدوج". تأكد من إزالة أية فراغات قد تكون قبل الفقرة، وذلك من خلال الخيار "إضافة مسافة قبل الفقرة" الذي سيكون بالصيغة "إزالة مسافة قبل الفقرة" إن كانت هناك مسافة فارغة. إزاحة الفقرات يتطلب التنسيقان السابقان إزاحة أولى كلمات الفقرة النصية بمقدار نصف إنش أو 1.27 سنتيمتر عن الهامش الأيمن أو الأيسر، وذلك وفقًا لجهة كتابة الورقة البحثية (يمين إلى يسار) أو (يسار إلى يمين). ولتطبيق الإزاحة في مستندات جوجل نحتاج إلى الآتي: إن لم يكن شريط قياس المسافات أعلى الصفحة موجودً، فأظهره عن طريق النقر على القائمة المنسدلة "عرض"، ثم الانتقال إلى خيار "عرض أداة قياس المسافات". حرك مزلاج أداة القياس نحو اليسار أو اليمين (حسب اتجاه الكتابة) مقدار 1.27 سنتيمتر. إن لم تتمكن من ضبط هذه القيمة عن طريق المزلاج، يكفي أن تضع مؤشر الفأرة في بداية السطر الأول للفقرة، ثم تضغط المفتاح Tab. تنسيق MLA إليك الآن النقاط الخاصة بتنسيق MLA الخاص بكتابة الأبحاث. عنوان الورقة وصفحة العنوان يضم عنوان الورقة وفق تنسيق MLA الرسمي المعلومات التالية: اسم المؤلف: ويكتب بداية اسم الأستاذ المشرف. اسم ورقم المادة أو الصف. تاريخ وجوب التسليم. لتنفيذ ذلك في مستندات جوجل: انقر على أول سطر في الصفحة المنسقة تنسيقًا عامًا كما أشرنا سابقًا، واحرص أن يكون مؤشر الكتابة عند الهامش الأيمن أو الأيسر وفقًا للغة الكتابة. اكتب اسم المؤلف (اسمك كاملًا إن كان البحث لك، ويفضل كتابة الاسم الأول يليه فراغ، ثم أول حرف من الاسم الأوسط، ثم نقطة، ثم الكنية). انتقل إلى السطر الثاني، ثم اكتب اسم المشرف العلمي. انتقل إلى السطر الثالث واكتب اسم المادة أو الصف ورقمه إن كان متاحًا. انتقل إلى السطر الرابع واكتب تاريخ وجوب التسليم على شكل "يوم (رقم)، ثم فراغ، ثم الشهر (كتابة)، ثم العام (رقم)". احرص أن يكون التباعد مزدوجًا بين الأسطر السابقة وسطر العنوان انتقل إلى السطر التالي لكتابة عنوان الورقة وانقر على زر "محاذاة" واختر "محاذاة للوسط"، أو اضغط المفاتيح Ctrl+Shift+E. اكتب العنوان بالخط العادي قياس 12 دون تسميك أو إمالة أو علامات اقتباس إلا إن احتوى عنوانك على عنوان آخر، واحرص أن يكون الحرف الأول من الكلمة مكتوبًا بشكله الكبير ما عدا أحرف الجر وأحرف الوصل، وذلك إن كانت الأطروحة باللغة الإنجليزية. الترويسة المكررة وأرقام الصفحات ينبغي أن تضم الترويسة وفق هذا التنسيق الاسم الأخير للمؤلف، يليه رقم الصفحة عند الهامش الأيسر تمامًا في الأبحاث المكتوبة بالعربية، وعند الهامش الأيمن في الأبحاث المكتوبة بالإنكليزية. واحرص أن يكون حجم الخط من النوع "Times New Roman" أو "Arial". لتنفيذ ذلك باستخدام تطبيق مستندات جوجل: انقر على القائمة المنسدلة "إدراج"، ثم أرقام الصفحات، ثم إدراج خيار الترقيم المتزايد ابتداءً من صفحة العنوان ذات الرقم "1". بمجرد النقر على الخيار، ستظهر لك ترويسة الصفحة وقد أدرج رقم الصفحة تلقائيًا بجوار الهامش. انقر خلف رقم الصفحة ثم ضع فراغًا، وانقر الزر "تعيين اتجاه النص من اليسار إلى اليمين" في شريط الأدوات لتتمكن من كتابة اسمك قبل رقم الصفحة. انقر في أي مكان آخر من الصفحة لإغلاق الترويسة. ملخص البحث والفقرات يلي عنوان البحث مباشرةً: انتقل إلى السطر التالي بعد العنوان. اختر "محاذاة للوسط" واكتب عبارة "ملخص" على أن تكون بخط سميك. اضبط المحاذاة إلى اليمين مجددًا، ثم اكتب الملخص وفق معايير كتابة الفقرات، مع الحرص على إزاحة الجملة الأولى. افصل الملخص وصفحة العنوان عن بقية البحث بإضافة فاصل صفحات بالنقر على القائمة "إدراج" واختيار "فاصل"، ثم اختيار "فاصل صفحة". ويمكنك أيضًا الضغط على المفاتيح Ctrl+E. يوضح الشكل التالي طريقة تنسيق ملخص البحث، ثم الطريقتين الأكثر اتباعًا في تحديد مستويات العناوين في MLA، لذلك لا ننصحك باستخدام تنسيقات مستندات جوجل نفسها. الصور والجداول لإدراج جدول وفق توجيهات MLA: اكتب عنوان الجدول قبل إدراجه على الشكل "الجدول.1" لأول جدول في الورقة ثم تتابع تصاعديًا في الأرقام حتى ترقم جميع الجداول، ولا تنسى التباعد المزدوج مع السطر التالي. اكتب عنوان الجدول أو ما يتحدث عنه الجدول باختصار إن لم تشر إلى ذلك في جسم المقال، ثم أدرج الجدول. لإدراج جدول: اختر "إدراج" ثم "جدول"، بعدها حدد أبعاد الجدول من الشكل التفاعلي الظاهر. اجعل الجدول بالتنسيق الذي تريد. حافظ على التباعد المزدوج بين الجدول والسطر الذي يليه. حدد مصدر الجدول وفق تنسيق MLA إن كان مأخوذًا من مصدر معين بعد الجدول مباشرةً. إن أردت إدراج أية ملاحظة عن معلومة معينة وردت في وصف الجدول، فانتقل إلى النقطة التي تريد التنويه إلى الملاحظة فيها، ثم أشر إلى وجود الملاحظة بوضع حروف بدلًا من الأرقام على شكل نص مرتفع، كما في لقطة الشاشة التالية: أما لإدراج صورة، فهنالك أسلوبان وفق MLA: الأسلوب الأول: التقديم عبر النص. التقديم للصورة ثم إنهائه بعبارة (شكل.1) أو ((fig.1) في الإنكليزية) وزيادة هذا الرقم تصاعديًا ابتداءً من أول صورة تدرجها. إدراج الصورة في أقرب مناسبة للتقديم السابق، كالتالي باختيار القائمة "إدراج" ثم "صورة" ثم اختر موقع وجود الصورة. أضف مصدر الصورة وفق MLA مباشرةً بعد الصورة، مسبوقًا بالعبارة "شكل.1." ثم المصدر مع المحافظة على التباعد المزدوج بين الصورة والمصدر. الأسلوب الثاني: الشرح مع المصدر. أدرج الصورة. اكتب بعدها مباشرةً عبارة "شكل.1."، يليه وصف الصورة بين علامتي اقتباس مزدوجتين، يليه نقطة، ثم مصدر الصورة. انظر إلى لقطة الشاشة التالية: إدراج الاقتباسات الموثقة وهي العملية الأكثر أهميةً في إعداد الأوراق البحثية، وتشير إلى الأبحاث الأخرى والمراجع التي اقتُبست منها بعض المعلومات حرفيًا أو أشير إليها. يقدم تطبيق مستندات جوجل طريقةً سريعةً وفعّالةً في إدخال معلومات التوثيق citations سواءً يدويًا، أو بالبحث عنها عبر الإنترنت وإيجاد التفاصيل المطلوبة عن مصدر التوثيق، ثم تنسيقه بإحدى طرق التنسيق. لإدراج اقتباس موثّق وفق MLA الإصدار 8 عبر مستندات جوجل، اتبع الخطوات التالية: من القائمة "أدوات" اختر "الاقتباسات الموثَّقة" لتظهر لك النافذة الجانبية "الاقتباسات الموثَّقة" إلى يسار الصفحة. اختر من القائمة المنسدلة أعلى النافذة نوع التنسيق "MLA (الإصدار رقم 8)". انقر على رابط "إضافة مصدر التوثيق" لتظهر لك صفحة جديدة في هذه النافذة. اختر نوع المصدر من القائمة المنسدلة التي تحمي الاسم "نوع المصدر"، سواءً كتاب أم مقال في مجلة علمية أو أي مصدر تريد إدراجه، وتختلف الخطوات التالية وفقًا للمصدر الذي اخترته: في حال كتاب: بإمكانك التوثيق يدويًا من خلال النقر على رابط التوثيق يديويًا في الأسفل إن كنت تعلم جميع تفاصيل ما تريد توثيقه، أو يمكنك لتوفير الوقت البحث عن معلومات الكتاب من خلال رقم ISBN الخاص به. وسواءٌ اخترت التوثيق اليدوي أو الآلي عبر ISBN، ستتوسع النافذة لتعرض تفاصيل الكتاب التي عليك ملؤها، كما قد تلاحظ أن حقولًا كثيرة ملئت مسبقًا إن استخدمت البحث التلقائي. تذكر أنه عليك نقر زر "متابعة" أسفل النافذة عند انتهاء البحث الآلي أو "التوثيق يدويًا" إن لم تكن المعلومات كافية، كما ينبغي أن نلفت انتباهك إلى بعض الحقول التي لا يزال عليك ملؤها حتى لو استخدمت البحث الآلي مثل أرقام الصفحات ورقم العدد ورقم المجلد. في حال مقال علمي أو صحفي: عليك هنا ملء كل الحقول بنفسك، خاصةً تلك التي تحمل علامة (*) التي يتوجب عليك ملؤها. في حال موقع إلكتروني: يمكنك اختيار "التوثيق يدويًا" أو كتابة عنوان الموقع لكي يبحث لك التطبيق عن المعلومات المتوفرة عنه. عند الإنتهاء من إضافة جميع المعلومات، انقر على زر "إضافة مصدر الاقتباس". يمثل الشكل التالي مراحل إعداد اقتباس موثق باستخدام مستندات جوجل. لإضافة التوثيق الذي أعددته وفق تنسيق MLA إلى مستندك اتبع الخطوات التالية: انقر في نهاية الجملة التي تريد توثيقها. اختر من نافذة "الاقتباسات الموثّقة" المصدر الصحيح لهذه الجملة، ثم اختر الأمر "اقتباس" الذي يظهر بمجرد تمرير مؤشر الفأرة فوق المصدر. تظهر علامة الاقتباس وفق تنسيق MLA إلى جوار الجملة المطلوبة، وكل ما عليك الآن هو استبدال المحرف (#) في علامة الاقتباس برقم الصفحة التي أخذ منها الاقتباس أو إهمالها إن لم تكن ضرورية. يمكنك أيضًا تعديل أو حذف الاقتباس بالنقر على زر النقاط الثلاث المتعامدة بجوار "اقتباس" واختيار الأمر المطلوب. عليك أخيرًا إضافة قائمة كاملة بجميع المراجع التي وثّقتها في عملك في آخر الورقة العلمية ووفق تنسيق MLA، لذلك عندما تفرغ من كتابة نص الورقة ويحين وقت كتابة المراجع نفّذ العملية كما يلي: إدراج فاصل صفحات بالنقر على "إدراج" ثم "فاصل" ثم "فاصل صفحة" انقر في السطر الأول للصفحة الجديدة ثم انتقل إلى نافذة "الاقتباسات الموثَّقة" وانقر على زر "إدراج الأعمال الموثّقة" لتحصل على المطلوب. تنسيق APA إليك الآن النقاط الخاصة بتنسيق APA الخاص بكتابة الأبحاث. عنوان الورقة وصفحة العنوان تضم صفحة العنوان وفق تنسيق APA الرسمي المعلومات التالية وفق نفس الترتيب: عنوان المقال: في وسط الصفحة مصاغًا بأبسط وأوضح طريقة ممكنة دون أي حشو أو اختصارات على بعد 3 إلى 4 أسطر عن قمة الصفحة. يكتب بخط سميك وبنفس حجم خط باقي المقاطع مع الانتباه إلى كتابة أحرف أوائل الكلمات بأحرف كبيرة (للغة الإنجليزية) أسماء المؤلفين: في وسط الصفحة تحت العنوان مباشرة وتخضع لقاعدة التباعد المزدوج، تُكتب بالخط العادي دون تثخين وعلى التتالي إن كان هناك أكثر من مؤلف. الجامعة والقسم: كما هو حال تنسيق أسماء المؤلفين. المشرف العلمي: كما هو حال تنسيق المؤلفين. تاريخ وجوب التسليم: كما هو حال تنسيق المؤلفين ويكتب وفق أسلوب (شهر يوم سنة). راجع تنسيق MLA لمعرفة طريقة تطبيق التنسيقات السابقة باستخدام تطبيق مستندات جوجل. الترويسة المكررة وأرقام الصفحات ينبغي أن تضم الترويسة وفق هذا التنسيق عنوان البحث على أن لا يزيد عن 50 محرفًا، وإن كان أطول، فلا بد من اقتراح نسخة قصيرة منه. يأتي رقم الصفحة عند الهامش الأيسر تمامًا في الأبحاث المكتوبة بالعربية، وعند الهامش الأيمن في الأبحاث المكتوبة بالإنجليزية؛ بينما يأتي العنوان ملاصقًا للهامش المقابل. احرص هنا على أن يكون العنوان بحجم الخط من النوع "Times New Roman" أو "Arial" وأن يكون مكتوبًا بأحرف كبيرة. ولتنفيذ ذلك باستخدام تطبيق مستندات جوجل، راجع ما فعلنا سابقًا عند تنسيق ترويسة MLA. ملخص البحث والفقرات يأتي الملخص في تنسيق APA في صفحة مستقلة تلي صفحة العنوان، وتبدأ بالكلمة "ملخص" بخط سميك وبنفس نوع وحجم خط بقية الفقرات. يأتي بعد ذلك نص الملخص دون إزاحة لأول سطر أو لأي سطر آخر، وينبغي المحافظة على قاعدة التباعد المزدوج. في نهاية الملخص وفي نفس الصفحة، انتقل إلى سطر جديد وطبّق إزاحةً مقدارها 1.27 سم، ثم اكتب عبارة "كلمات مفتاحية" بخط مائل، تليها نقطتان متعامدتان ثم فراغ، واسرد بعدها الكلمات المفتاحية في مقالك تفصل بينها الفاصلة ",". ولتنفيذ ذلك باستخدام تطبيق مستندات جوجل، راجع ما فعلنا سابقًا في تنسيق MLA. أما فيما يخص مستوى العناوين، يوضح الشكل التالي طريقة تنسيقها وفق APA باستخدام مستندات جوجل: الصور والجداول إليك أهم قواعد APA في تنسيق الجداول: ابدأ من الهامش الأيمن واكتب كلمة "الجدول"، يليها فراغ، ثم رقم الجدول الذي يتزايد تصاعديًا بخط سميك. اكتب في السطر التالي عنوان الجدول بخط عادي مائل. أدرج الجدول وأظهر الخطوط الأفقية فقط. ولتنفّذ ذلك في مستندات جوجل: علّم الجدول بأكمله وانقر على المثلث الصغير الذي يظهر في الزاوية العليا اليسارية من الجدول، فتظهر لك قائمة من ثلاثة أسطر. انقر على أول عناصر السطر الثالث، ثم انقر على أيقونة "عرض الحدود" التي قد تكون مختفية ضمن الأيقونة "مزيد". اختر القيمة 0 ليختفي العمود الأيمن. كرر نفس الخطوات، لكن اختر هذه المرة ثاني عناصر السطر الثالث وتابع. كرر نفس الخطوات واختر أخيرًا العنصر الثالث من العمود الثالث وتابع فتحصل على المطلوب. أدرج كلمة "ملاحظة" قبل أي ملاحظة ستكتبها تحت الجدول مباشرةً، واكتبها بخط مائل. بالنسبة للصور اتبع خطوات تنسيق الجدول تمامًا، لكن ينبغي أن تكون الصور في المقالات العلمية باللونين الأبيض والأسود فقط. إدراج الاقتباسات الموثقة اتبع نفس الخطوات التي ذكرناها في تنسيق الاقتباسات الموثَّقة وفق MLA، لكن اختر التنسيق "APA (الإصدار رقم 7)" خلاصة تعلمنا في هذا المقال طريقة كتابة ورقة أكاديمية علمية بطريقة احترافية مستخدمين تطبيق مستندات جوجل وذلك وفق التنسيقان APA و MLA وعرضنا الكثير من لقطات الشاشة التي توضح بالأرقام خطوات تنفيذ بعض العمليات. إليك أهم النقاط التي عملنا عليها: تنسيق صفحة العنوان. تنسيق الملخص. تنسيق الفقرات وترويسات الفقرات. التعامل مع مستويات العناوين. إدراج الصور والجداول. إدراج الاقتباسات الموثَّقة. اقرأ أيضًا مقدمة إلى تطبيق مستندات جوجل Google Docs قائمة الأدوات في مستندات جوجل المشاركة وتوزيع المهام على مستندات جوجل
  5. حان الوقت لإدراج مشغلات الفيديو والصوتيات ضمن صفحات HTML بعد أن امتلكنا بعض الخبرة في إدراج الصور ضمن صفحات الويب، إذ سيشرح مقالنا هذا الأمر باستخدام العنصرين <video> و <audio> ثم سيلقي الضوء في النهاية على طريقة إضافة الشروحات أو الكلام الوارد في هذه المقاطع إلى صفحات ويب. لا بدّ قبل البدء بهذا المقال أن تكون على دراية بأساسيات عمل الحاسوب وطريقة تثبيت البرمجيات الأساسية ودراية بطريقة عمل الملفات، ولا بد أيضًا من الدراية بأساسيات اللغة HTML كما عرضناها في مقال تعرَّف على HTML، وطريقة إضافة الصور في صفحة HTML. مقاطع الصوت والفيديو في الويب أراد مطورو الويب استخدام المقاطع الصوتية والفيديوهات منذ بداية هذا القرن -القرن الحادي والعشرين- مع ازدياد سرعة الاتصال بالإنترنت وزيادة عرض حزم التراسل بشكل كاف لدعم أنواع من مقاطع الفيديو (ملفات الفيديو أكبر حجمًا من النصوص والصور)، ولم تتمكن التقنيات الأساسية للويب مثل HTML في البدايات من إدراج الفيديوهات ومقاطع الصوت في صفحات الويب، لذلك ظهرت تقنيات مبنية على الإضافات plug-in مثل تقنية الفلاش Flash وبعدها سيلفرلايت Silverlight وكلاهما قد خرج من الخدمة الآن وقد تصدَّرتا الواجهة في عرض هذا النوع من المحتوى، كما عملت هذه التقنيات جيدًا لكن المشاكل سرعان ما واجتها مثل عدم توافق عملها مع ميزات HTML و CSS إضافة إلى مشاكل أمنية ومشاكل في سهولة الوصول. كان استخدام الحلول الأصلية هو السبيل الأساسي إذا جرى ذلك بالصورة الصحيحة، ولم يتأخر الأمر كثيرًا مع ظهور مواصفات HTML5 التي أضافت ميزات إدراج الفيديو والصوت من خلال العنصرين <video> و <audio> وبعض الواجهات البرمجية بلغة جافاسكربت للتحكم بهما، ولن نتحدث عن جافاسكربت في المقال وإنما فقط العمليات الأساسية التي يمكن إنجازها باستخدام HTML، كما لن نتعلم في مقالنا كيفية إنتاج ملفات الفيديو والصوت فالأمر يتطلب مجموعة مختلفة من المهارات، ولهذا سنستخدِم مجموعةً من ملفات الوسائط المتعددة وشيفرة تدريبية لتجرب العمل عليها إذا لم تستطع التعامل مع ما لديك. ملاحظة: عليك أن تعرف قبل البدء هنا بوجود العديد من مزودِي خدمة الفيديو على الإنترنت online video providers -أو OVP اختصارًا- مثل اليوتيوب وديلي موشن وفيميو، بالإضافة إلى مزودِي خدمات الصوت مثل ساوند كلاود، إذ تقدِّم لك هذه الشركات طريقةً سهلةً لاستضافة وتشغيل الفيديوهات دون أن تلق بالًا لاستنزاف حجم حزمة التراسل، كما تزوّدك هذه الشركات بشيفرة جاهزة لإدراج مقاطع الفيديو أو الصوت في صفحاتك، فإذا قررت استخدام هذا المسار، فيمكنك الابتعاد عن الكثير من النقاط الصعبة التي نناقشها في هذا المقال، إذ سنناقش هذا النوع من الخدمات لاحقًا. العنصر <video> يتيح لك هذا العنصر إدراج الفيديوهات بكل سهولة، وإليك مثالًا بسيطًا: <video src="rabbit320.webm" controls> <p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.webm">link to the video</a> instead.</p> </video> تجدر ملاحظة الميزات التالية: src: سمة مشابهة تمامًا لتلك الموجودة في العنصر <img>، وتضم المسار إلى مقطع الفيديو الذي تريد إدراجه. controls: ينبغي أن يكون المستخدِم قادرًا على التحكم بتشغيل وإيقاف الفيديو، لذلك قد تستخدِم هذه السمة لإضافة الواجهة الخاصة بالمتصفح للتحكم بالفيديو، أو أن تبني الواجهة الخاصة بك باستخدام واجهة جافاسكربت برمجية مناسبة، ولا بدّ من ضم الواجهة آلية لبدء وإيقاف تشغيل المقطع على الأقل، وكذلك ضبط مستوى الصوت. الفقرة النصية داخل العنصر <video>: وُتدعى الخطة البديلة fallback، إذ يُعرَض محتواها إذا لم يدعم المتصفح المستخدَم العنصر <video>، مما يسمح بوجود بديل للمتصفحات القديمة، ولا يوجد أسلوب محدَّد لتنظيم الخطة البديلة، فقد زودنا المستخدِم في حالتنا برابط إلى ملف الفيديو لكي يتمكن من الوصول إليه بصرف النظر عن المتصفح الذي يستخدِمه. سيبدو مقطع الفيديو بعد إدراجه مشابهًا للصورة التالية: بإمكانك أيضًا تجريب الصفحة مباشرةً، وألق نظرةً على الشيفرة المصدرية. استخدام تنسيقات مصدرية مختلفة لتحسين التوافقية ربما قد لاحظت وجود مشكلة في المثال السابق إذا حاولت الدخول إلى الرابط المباشر للفيديو من متصفح قديم مثل إنترنت إكسبلورر أو نسخة قديمة من سفاري، إذ لن يعمل الفيديو لأن المتصفحات المختلفة تدعم مقاطع فيديو ومقاطع صوتية بتنسيقات مختلفة، ومن الممكن لحسن الحظ الالتفاف على المشكلة بطرق عدة. محتوى ملف الوسائط لنلق نظرةً سريعةً أولًا على بعض المصطلحات، إذ تُدعى التنسيقات مثل MP3 و MP4 و WebM بالتنسيقات الحاوية أو المُغلِّفة container formats، كما تعرِّف هذه الحاويات هيكليةً لتخزين مسارات الفيديو أو الصوت بالإضافة إلى البيانات الوصفية التي تشرح هذه الوسائط والمرمزات التي تستخدَم في ترميز الأقنية وما شابه. يضم الملف WebM فيديو مكوَّن من مسار فيديو رئيسي وآخر بزاوية مختلفة بالإضافة إلى مسارات صوتية باللغتين الإنجليزية والإسبانية بالإضافة إلى مسار صوتي تعليقي يمكن تصوّره من خلال المخطط التالي، كما يحتوي الملف أيضًا على مسارات كتابية تضم شروحات عن الفيلم وترجمة باللغة الإسبانية وشروحات باللغة الإنجليزية لمسار التعليقات. تحمل مسارات الفيديو والمسارات الصوتية داخل الحاوية بيانات بالتنسيق الذي يوافق المرمّز المستخدَم في ترميز هذه الوسائط، إذ تستخدَم عدة تنسيقات لتخزين مسارات الصوت لتتماشى مع مسارات الفيديو، كما يرمّز كل مسار صوتي باستخدام مرمِّز صوتي وترمّز مسارات الفيديو باستخدام مرمِّز فيديو، كما تدعم المتصفحات المختلفة تنسيقات فيديو مختلفة وتنسيق حاويات مختلف مثل MP3 و MP4 و WebM والتي تحتوي بدورها على تنسيقات مختلفة للفيديو والصوتيات، فعلى سبيل المثال: تحزِم حاوية WebM تقليديًا مسارات الصوت بتنسيق Vorbis أو Opus مع مسارات فيديو بتنسيق VP8 و VP9، كما تدعم كل المتصفحات الحديثة هذه التنسيقات، لكنها قد لا تعمل مع المتصفحات القديمة. تحزِم حاوية MP4 عادةً مسارات الصوت بتنسيق MP3 و AAC مع مسار فيديو بتنسيق H.246، كما تدعم كل المتصفحات الحديثة هذه التنسيقات كما يدعمها إنترنت إكسبلورر. تميل حاويات Ogg إلى حزم مسارات الصوت بتنسيق Vorbis مع مسار فيديو بتنسيق Theora، كما تدعم هذه الحاوية متصفحات فايرفوكس وكروم، لكن تُستبدل بحاويات WebM ذات الجودة الأعلى. تظهر بعض الحالات الخاصة التي تُخزّن فيها بيانات المرمز الصوتي لبعض المسارات الصوتية دون حاوية أو ضمن حاوية بسيطة. مثل المرمزات FLAC التي تُخزّن البيانات عادةً في ملفات بامتداد FLAC. وهي مجرد مسارات خام لبيانات هذا المرمز. توجد حالة أخرى وهي ملفات MP3 دائمة الشعبية وهي المسار أو الطبقة الصوتية الثالثة لتنسيق MPEG-1 مخزّنةً ضمن حاوية MPEG أو MPEG-2، وتظهر أهمية الأمر عندما نعرف أن معظم المتصفحات لا تدعم ملفات الوسائط بتنسيق MPEG من خلال العنصرين <video> و <audio>، لكنها قد تستمر بدعم MP3 نظرًا لشعبيته. تميل مشغلات الصوت إلى تشغيل المسارات الصوتية مثل MP3 و Ogg مباشرةً دون الحاجة إلى الحاويات في هذه الحالة. دعم ملفات الوسائط المتعددة في المتصفحات ملاحظة: ما الذي خلق مشكلة التوافق؟ الجواب هو وجود تنسيقات شعبية مثل MP3 و MP4/H.246 التي تتمتع بأداء ممتاز لكنها خاضعة لبراءات الاختراع التي تغطي بعضًا من التكنولوجيا التي تستند إليها أو كلها، فقد غطت براءة الاختراع MP3 حتى عام 2017 وستغطِّي H.246 حتى 2027 على الأقل، ونظرًا لوجود براءات الاختراع، فلا بد من دفع المتصفحات ضرائب كبيرة إذا أرادت إضافة دعم لهذه المرمزات، بالإضافة إلى ذلك، يفضِّل الكثيرون تحاشي القيود على البرمجيات واستخدام التنسيقات مفتوحة المصدر، ولهذه الأسباب قد يضطر المطورون إلى دعم تنسيقات مختلفة للوصول إلى جمهور أوسع. وجدت المرمزات التي أشرنا إليها سابقًا لضغط الفيديو والصوتيات ضمن ملفات قابلة للإدارة، نظرًا لكون بيانات الفيديو والبيانات الصوتية الخام كبيرة الحجم، في حين يدعم كل متصفح صنفًا من المرمّزات مثل Vorbis أو H.264 التي تستخدَم في تحويل الفيديوهات والصوتيات المضغوطة إلى بيانات ثنائية وبالعكس، كما يملك كل مرمز إيجابياته وسلبياته وتملك كل حاوية إيجابياتها وسلبياتها أيضًا، مما سيؤثِّر على قرارك في الاختيار. ستغدو الأمور أكثر تعقيدًا نظرًا لدعم المتصفحات لتنسيقات مختلفة من ملفات الحاويات، إضافةً إلى اختلافها في دعم المرمزات، ولكي تزيد من فرصة تشغيل معظم المتصفحات لتطبيقك أو موقعك، فمن الأفضل تقديم ملفات الوسائط المتعددة بتنسيقات عدة، إذ لن يعرض المتصفح ملف الوسائط المتعددة إذا لم يدعم تنسيقه، ولكي تتأكد من أنّ ملفات الوسائط التي تستخدِمها ستعمل عبر أيّ مجموعة من المتصفحات أو المنصات أو الأجهزة التي ترغب في الوصول إليها، فاختر أفضل مجموعة من المرمزات والحاويات، ولكنها مَهمة معقدة بالفعل. ملاحظة: تعرَّف على طريقة اختيار حاوية الوسائط المناسبة بالاطلاع على المقال تنسيقات حاويات الوسائط المتعددة، وطريقة اختيار المرمز الصوتي المناسب في مقال دليلك إلى مرمزات الملفات الصوتية في الويب، وطريقة اختيار مرمز الفيديو المناسب في مقال دليلك إلى مرمزات الفيديو في الويب. لا بد أن تأخذ في حسبانك أنّ متصفحات الأجهزة المحمولة قد تدعم تنسيقات إضافية غير مدعومة من المتصفحات نفسها على الأجهزة المكتبية والعكس صحيح، فقد تصمَّم بعض متصفحات الأجهزة المحمولة والمكتبية لإيقاف تحميل الوسائط المتعددة (لكل الوسائط أو لبعض الأنواع التي لا يمكنها التعامل معها داخليًا)، أي أنّ البرمجيات التي ثبتها قد تؤثر جزئيًا على دعم الوسائط المتعددة. إذًا كيف سننجز الأمر؟ لنلق نظرةً على المثال التالي، ويمكنك تجريبه مباشرةً أيضًا: <video controls> <source src="rabbit320.mp4" type="video/mp4"> <source src="rabbit320.webm" type="video/webm"> <p>Your browser doesn't support HTML5 video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p> </video> لقد أخرجنا في الشيفرة السابقة السمة src من العنصر <video> ووضعناها داخل عنصر مستقل هو <source> الذي يشير إلى موارد خاصة به، إذ سينتقل المتصفح في هذه الحالة إلى مجموعة العناصر <source> ويشغِّل المورد في أول عنصر يمتلك مرمزه، وبوجود الحاويتين WebM و MP4 ستضمن تشغيل ملفك على معظم المنصات والمتصفحات الموجودة حاليًا. يمتلك كل عنصر <source> السمة type وهي اختيارية لكننا ننصح باستخدامها، كما تتضمن هذه السمة نوع الوسائط المتعددة MIME للملف الذي يحدده العنصر <source>، إذ يستخدِمها المتصفح لتجاوز الفيديوهات التي لا يفهمها، فإذا لم تُحدد قيمة لهذه السمة، فسيحمِّل المتصفح كل ملف ويحاول تشغيله حتى يجد الملف الذي يعمل، وهذا ما يستغرق وقتًا ويسبب هدرًا في استخدام الموارد. ملاحظة: عُد إلى مقال دليلك إلى أنواع ملفات الوسائط وتنسيقاتها لاختيار أنسب مرمز وأنسب حاوية لاستخداماتك ولتتعرف أيضًا على أنواع الوسائط المتعددة MIME التي تعرِّف كل حاوية. ميزات أخرى هنالك عدد من الميزات التي يمكنك إضافتها عند عرض فيديوهات HTML، وإليك مثالًا كما يلي: <video controls width="400" height="400" autoplay loop muted preload="auto" poster="poster.png"> <source src="rabbit320.mp4" type="video/mp4"> <source src="rabbit320.webm" type="video/webm"> <p>Your browser doesn't support HTML video. Here is a <a href="rabbit320.mp4">link to the video</a> instead.</p> </video> ستبدو نتيجة تنفيذ هذه الشيفرة للمستخدِم كما يلي: لنلق نظرةً على الميزات الجديدة: width و height: يمكنك التحكم بأبعاد الفيديو باستخدام هاتين السمتَين أو من خلال تنسيقات CSS، إذ يحافظ الفيديو في كلتا الحالتين على النسبة الأصلية للعرض إلى الطول أو كما تسمى النسبة الباعية أو نسبة البُعدَين aspect ratio، فإذا لم تتحقق هذه النسبة عندما تضبط بنفسك الطول والعرض، فسيغطي الفيديو الصفحة أفقيًا وتُملأ المنطقة غير المشغولة بالفيديو بخلفية ملونة مستمرة. autoplay: تسبب تشغيل الفيديو أو الصوت مباشرةً، في حين لا تزال بقية المحتوى قيد التحميل، كما لا ننصحك باستخدام هذه السمة للفيديو أو ملفات الصوت ضمن موقعك لأنه أمر مزعج لكثير من المستخدِمين. loop: تسبب تشغيل الفيديو او الصوت عند انتهائه، وقد يكون الأمر مزعجًا، لذا لا تستخدِمه إلا إذا كان ذلك ضروريًا. muted: تشغّل مقطع الفيديو افتراضيًا بدون صوت. poster: تضم عنوان URL للصورة التي تُعرَض قبل تشغيل الفيديو، وهي معدَّة للاستخدام مع الشاشات التمهيدية splash screen أو الشاشات الدعائية. preload: تستخدَم للتخزين المؤقت للملفات الضخمة، كما تأخذ أحد القيم الثلاث التالية: "none" وتعني لا تخزين مؤقت، أو "auto" وتعني ضرورة التخزين المؤقت للملف، أو "metadata" لتخزين البيانات الوصفية فقط للملف. يمكنك تجريب المثال السابق مباشرةً على جيت-هاب، كما يمكنك الاطلاع على الشيفرة المصدرية أيضًا، ولاحظ عدم استخدام السمة autoplay في النسخة المباشرة، إذ لن ترى صورة البداية إذا بدأ الفيديو قبل انتهاء تحميل الصفحة. العنصر <audio> يشابه العنصر <[video](https://wiki.hsoub.com/HTML/video)> مع بعض الاختلافات كما سنوضح لاحقًا، وإليك مثالًا عن استخدام العنصر: <audio controls> <source src="viper.mp3" type="audio/mp3"> <source src="viper.ogg" type="audio/ogg"> <p>Your browser doesn't support HTML5 audio. Here is a <a href="viper.mp3">link to the audio</a> instead.</p> </audio> تمثل الصورة التالية نتيجة تنفيذ الشيفرة: ملاحظة: يمكنك تشغيل المثال السابق مباشرةً عبر جيت-هاب، كما يمكنك الاطلاع على الشيفرة المصدرية لمشغل الصوت. يحتل العنصر مساحةً أقل من المساحة التي يحتلها مشغل الفيديو نظرًا لعدم وجود مكوّن مرئي، فكل ما تحتاجه هو عرض مجموعة التحكم لتشغيل المقطع الصوتي، وتتلخص بقية الاختلافات عن عنصر الفيديو بما يلي: لا يدعم العنصر <audio> السمتين width و height لعدم وجود مكوّن مرئي، وبالتالي لا حاجة إلى تحديد طول أو عرض. لا يدعم أيضًا السمة poster. ما عدا ذلك، يدعم العنصر <audio> جميع الميزات التي يدعمها العنصر <video>. عرض المسارات النصية لفيديو سنتحدث الآن عن مفهوم متقدِّم قليلًا ومن المهم معرفته، إذ لا يرغب الكثير من المستخدِمين بسماع محتوى الفيديو أو المقطع الصوتي على الأقل في أوقات معينة، فمثلًا: قد يعاني البعض من مشاكل سمعية مثل الطرش أو صعوبة السمع، وبالتالي لن يسمع الصوت بوضوح وقد لا يسمعه إطلاقًا. قد لا يسمع آخرون الصوت لكونهم في بيئات كثيرة الضجيج مثل مقهى مزدحم عند عرض مباراة رياضية. في البيئات التي يُمنع فيها إصدار الأصوات كما في المكتبات أو عندما يحاول أحدهم النوم إلى جوارك، إذ سيكون وجود الشروحات أو كلمات المقطع أمرًا مفيدًا. قد يرغب الناس الذين لا يتحدثون لغة الفيديو بوجود كلمات المقطع مكتوبةً أو مترجمةً لفهم ما يدور فيه. من الجيد في حالات مثل هذه وجود نص يعرض ما يُقال في الفيديو أو المقطع الصوتي، وهذا ما يتيحه العنصر <video> في HTML، ولإنجاز الأمر، سنستخدِم ملفًا بتنسيق WebVTT والعنصر <track>. يُعَدّ WebVTT هو تنسيق لكتابة ملفات نصية تضم إضافةً إلى المقاطع النصية بيانات وصفية مثل توقيت عرض كل عبارة نصية ومعلومات محدودة عن موقع وتنسيق تلك العبارات النصية. تُدعى العبارات النصية بالأنساق Cues وتُصنَّف وفق أنواع مختلفة تخدم غايات مختلفة: الترجمات Subtitles: وذلك للمقاطع التي تتحدث بلغات مختلفة لا يستطيع فهمها من لا يجيد اللغة. شروحات (تعليقات) captions: نصوص تتزامن مع حوار أو وصف لأصوات ملفتة لكي تساعد من لديه صعوبة في السمع على فهم ما يجري. شروحات متزامنة timed descriptions: نصوص يلفظها مشغل الوسائط المتعددة لتصف مشاهد لذوي المشاكل البصرية أو فاقدي البصر. سيبدو ملف WebVTT نمطيًا كما يلي: WEBVTT 1 00:00:22.230 --> 00:00:24.606 This is the first subtitle. 2 00:00:30.739 --> 00:00:34.074 This is the second. ... لكي تعرض محتويات هذا الملف مع مشغِّلات الوسائط التي تقدمها HTML سنحتاج إلى: حفظ الملف بالامتداد vtt.. ربط الصفحة بملف vtt. مستخدِمًا العنصر <track> الذي ينبغي وضعه ضمن العنصر <video> أو العنصر <audio> لكن بعد جميع العناصر <source>. استخدام السمة kind للعنصر <track> لتحديد نوع الأنساق إذا كانت "subtitles" أو "captions" أو "descriptions". استخدام السمة srclang لإخبار المتصفح عن اللغة التي يجب أن يعرض بها الأنساق، واستخدام السمة label لمساعدة المستخدِم في تحديد اللغة التي يبحث عنها. إليك مثالًا: <video controls> <source src="example.mp4" type="video/mp4"> <source src="example.webm" type="video/webm"> <track kind="subtitles" src="subtitles_es.vtt" srclang="es" label="Spanish"> </video> ستكون النتيجة مقطع فيديو مع الترجمة كما يلي: ملاحظة: تساعد المسارات النصية في تحسين محركات البحث كونها متعطشة للعبارات النصية، إذ تسمح المسارات النصية لمحركات البحث بربط النتيجة بلحظة معينة في الفيديو. تطبيق: إدراج مقطع الفيديو أو الصوت الخاص بك نشجعك في هذا التمرين على الخروج وتسجيل مقطع فيديو أو مقطع صوتي لما يجري في محيطك، إذ يمكنك ببساطة استخدام هاتفك المحمول لسهولة استخدامه في التصوير وفي نقل المقاطع المسجلة إلى الحاسوب، وقد تضطر إلى تحويل الملفات المسجلة لتصبح حاويات MP4 أو WebM في حال سجلت مقطع فيديو، و MP3 أو Ogg في حال تسجيلك لمقطع صوتي، كما ستجد الكثير من البرمجيات التي تساعدك في إنجاز الأمر دون عناء كبير مثل Miro Video Converter و Audacity. إذا لم تشأ أن تسجِّل بنفسك، فلك كامل الحرية في اختيار عينات الفيديو والصوت التي جهزناها لهذا التمرين، كما يمكنك استخدام شيفرتنا المصدرية على أساس مرجع. نطلب منك: حفظ ملفات الفيديو والصوت في مجلد جديد على حاسوبك إنشاء ملف HTML جديد في المجلد نفسه باسم index.html. أضف العنصرين <video> و <audio> إلى الصفحة واجعلهما يعرضان أدوات التحكم الافتراضية للمتصفح. أضف السمة source إلى كل منهما لكي تجد المتصفحات التنسيق الصوتي الذي تدعمه، ولا بد في هذه الحالة من استخدام السمة type أيضًا. أضف صورة بداية poster إلى العنصر <video> كي تعرض قبل تشغيل المقطع. لكي تضع لنفسك نقاطًا إضافيةً، حاول الاطلاع أكثر على المسارات النصية وإضافة بعض الشروحات إلى الفيديو الخاص بك. خلاصة نكون هكذا قد وصلنا إلى نهاية المقال وقد اطلعنا على الأسلوب الدلالي في إدراج مقاطع الفيديو والمقاطع الصوتية في صفحتك، وسنتابع في المقال التالي موضوع إدراج محتوى من ويب ضمن صفحاتك باستخدام تقنيات مثل العنصر <iframe>. ترجمة -وبتصرُّف- للمقال Video and audio content. اقرأ أيضًا إعداد صور متجاوبة إضافة الصور في صفحات HTML إضافة مقاطع الفيديو عبر العنصر <video> في HTML5
  6. كانت الويب في بداياتها مجرد نصوص عدَّها الكثيرون مملةً، ولحسن الحظ لم يتأخر استخدام الصور -وغيرها من المحتوى الأكثر إمتاعًا- كثيرًا ضمن صفحات الويب، كما هناك الكثير من أنواع الوسائط المتعددة التي يمكن التفكير بها، لكن من المنطقي أن نبدأ مع العنصر المتواضع <img> الذي يُستخدَم في إدراج صور بسيطة في الصفحة، إذ سنطلع في هذا المقال على طريقة استخدام العنصر بتفاصيل أكثر بما في ذلك الأساسيات وطريقة إدراج شرح للصورة باستخدام العنصر <figure> وكيفية ربطه بصور الخلفية في CSS. لا بدّ قبل البدء بقراءة هذا المقال أن تكون على دراية بأساسيات عمل الحاسوب وطريقة تثبيت البرمجيات الأساسية ودراية بطريقة عمل الملفات، كما لا بدّ أيضًا من الدراية بأساسيات اللغة HTML كما عرضناها في مقال تعرَّف على HTML. كيف تضع صورة في صفحة ويب نستخدِم العنصر <img> لإدراج صورة في موقع ويب، إذ يُعَدّ هذا العنصر فارغًا، أي لا يضم أيّ محتوى نصي أو وسم نهاية، كما يتطلب على الأقل سمةً واحدةً لكي يعمل بصورة صحيحة وهي src وتُقرأ source، كما تحتوي قيمة هذه السمة على المسار الذي يدل على موقع الصورة التي تريد إدراجها في الصفحة والذي قد يكون عنوان URL نسبيًا relative أو مطلقًا absolute ومشابهةً تمامًا لقيمة السمة href الخاصة بالعنصر <a>. ملاحظة: عليك الاطلاع على مقال أساسيات عنوان URL وأنواعه وخاصة فقرة "عناوين URL المطلقة والنسبية" لتتذكَّر مفهومَي العنوان المطلق والعنوان النسبي قبل المتابعة. إذا كان اسم صورتك dinosaur.jpg مثلًا وتتواجد في المجلد نفسه الذي يوجد فيه ملف HTML، فستدرِج صورتك كما يلي: <img src="dinosaur.jpg"> أما إذا كانت الصورة في مجلد فرعي يُدعى images يتواجد في المجلد نفسه الذي يوجد فيه ملف HTML، فستدرِج صورتك كما يلي: <img src="images/dinosaur.jpg"> وهكذا. ملاحظة: تقرأ محركات البحث أسماء ملفات الصور ويؤثر ذلك من ناحية تحسين محركات البحث سيو SEO، لهذا السبب لا بدّ من تسمية الصور بأسماء وصفية معبرة، فالاسم dinosaur.jpg أفضل بالتأكيد من image835.png. بإمكانك إدراج الصورة أيضًا باستخدام عنوان URL مطلق، وإليك مثالًا كما يلي: <img src="https://www.example.com/images/dinosaur.jpg"> لكن هذا الأمر عديم الجدوى في حالتنا وسيدفع المتصفح إلى مزيد من العمل للبحث عن عنوان آي بي IP لخادم دي إن إس DNS من جديد، فما تفعله على الدوام تقريبًا هو وضع الصور المستخدَمة في الموقع على الخادم نفسه الذي يستضيف صفحات HTML. تحذير: تمتلك معظم الصور حقوق نشر، فلا تعرض صورةً على موقع ما لم: تكن إحدى صورك. تحصل على تصريح خطِّي من مالكها. تكن على يقين تام بأنها صورة تندرِج في نطاق الصور العامة. يُعَدّ انتهاك حقوق النشر أمرًا غير قانوني وغير أخلاقي، وعليك ألا تضع قيمة لسمة src تشير إلى صورة على موقع شخص آخر لم تحصل منه على إذن رسمي، إذ تُدعى هذه العملية بالربط الساخن hotlinking، ولا بدّ من الإشارة إلى أنّ العملية ستسبب سرقة حزمة تبادل البيانات المخصصة لموقع هذا الشخص وهذا أيضًا غير قانوني، كما تبطئ هذه الروابط صفحتك وتحرمك من التحكم بما سيعرض عليها، فلن تعرف إذا أُزيلت هذه الصورة أو عُدّلت بصورة قد تسبب لك حرجًا. ستعطي الشيفرة السابقة النتيجة التالية: ملاحظة: يُشار أحيانًا إلى العنصرين <img> و <video> بأنهما عنصران قابلان للاستبدال Replaced elements، لأن ما يُعرِّف محتواهما وحجمهما هي موارد خارجية مثل ملف صورة أو ملف فيديو، وليس من قِبَل محتواهما مباشرةً. ملاحظة: يمكنك الاطلاع على المثال بصيغته النهائية ضمن المستودع المخصص له على جيت-هاب، كما يمكنك الاطلاع على الشيفرة المصدرية أيضًا. أنشئ موقع احترافي لأعمالك وعزّز حضورك الرقمي صمم موقع إلكتروني فريد وجذاب لعملائك في دقائق دون خبرة برمجية باستخدام سنديان منشئ المواقع العربي أطلق موقعك الآن النص البديل ما سنتحدث عنه الآن هو السمة alt، ومن المفترض أن تكون قيمة هذه السمة نصًا يُظهر وصفًا للصورة وذلك لاستخدامه في الحالات التي لا تُعرض فيها، أو الحالات التي تستغرق فيها الصورة وقتًا طويلًا حتى يصيّرها المتصفح نظرًا لبطء الاتصال بالإنترنت، كما يمكن مثلًا تعديل الشيفرة التي عرضناها سابقًا لتصبح كما يلي: <img src="images/dinosaur.jpg" alt="The head and torso of a dinosaur skeleton; it has a large head with long sharp teeth"> أسهل الطرق التي يمكن من خلالها اختبار ظهور النص البديل هو كتابة اسم ملف الصورة بصورة خاطئة، فإذا كتبنا اسم الصورة في الشيفرة السابقة ليكون dinosooooor.jpg، فلن يعرضها المتصفح وإنما سيعرض النص البديل كما يلي: لماذا إذًا ستحتاج إلى النص البديل؟ إليك بعض الأسباب: إذا كان المستخدِم ذو إعاقة بصرية ويستخدم قارئات الشاشة لقراءة محتوى الصفحات، فسيصف النص البديل الصورة لهذا المستخدِم وبالتالي سيكون أكثر فائدةً منها. ارتكاب خطأ في كتابة اسم الملف أو في مساره. قد لا يدعم المتصفح نوع الصورة، فلا تزال المتصفحات النصية مستعملة فقط مثل Lynx والذي سيعرِض في هذه الحالة وصف الصورة. قد ترغب ربما بتزويد محركات البحث ببعض المعلومات عن صورتك، إذ يمكن لمحركات البحث استخدام النص البديل في مطابقة معيار البحث. قد يوقف المستخدِم عرض الصور على متصفحه عمدًا لتقليل استهلاك البيانات وخاصةً عند استخدام بيانات الهواتف المحمولة أو في الدول التي يكون فيها نطاق التراسل محدودًا أو مكلفًا. ما الذي ينبغي كتابته ضمن السمة alt؟ يعتمد الأمر على سبب استخدامك للصورة، أو بمعنى آخر ما الذي ستخسره إذا لم تُعرض الصورة؟: ديكور للصفحة: عليك استعمال صور الخلفية في CSS لأغراض الديكور، لكن إذا كان عليك استخدام HTML، فأضف السمة فارغةً " "=alt، فإذا لم تكن الصورة جزءًا من المحتوى، فلن تهدر قارئات الشاشة الوقت في نطقها. محتوى ضمن الصفحة: إذا قدَّمت الصورة معلومات هامةً، فعليك إضافة المعلومات نفسها لكن بإيجاز إلى السمة alt أو إلى النص الرئيسي للصفحة كي يراه الجميع وهذا هو الحل الأفضل، ولا تكتب نصًا بديلًا مكررًا عن المحتوى الرئيسي فالأمر مزعج بالنسبة للمستخدِم صحيح البصر، فإذا كانت الصورة مشروحةً شرحًا وافيًا ضمن النص الرئيسي، فأضف سمة النص البديل فارغةً " "=alt. رابط إلى محتوى آخر: إذا وضعت صورةً ضمن العنصر <a> لكي تحوِّل الصورة إلى رابط، فلا بدّ أن تزوِّد الرابط بنص واضح، وفي هذه الحالة يمكنك كتابته ضمن العنصر <a> أو على أساس قيمة للسمة alt العائدة للعنصر <img> أيًا ما يخدم حالتك. محتوى نصي: لا يجب وضع نص ضمن صورة، فإذا احتجت لأن تعرض ظلًا لعنوانك الرئيسي مثلًا، فاستخدم تنسيقات CSS بدلًا من وضع النص ضمن الصورة؛ أما إذا كنت مضطرًا كثيرًا، فضع النص داخل السمة alt. إنّ الغاية الأساسية هي تقديم تجربة يمكن الاستفادة منها، حتى إذا لم تكن الصور ظاهرةً، وبهذا تضمن وصول المحتوى الذي تقدمه إلى جميع المستخدِمين، وحاول منع متصفحك من عرض الصور وانظر كيف ستبدو الصفحة؟ إذ ستدرك عندها أهمية النص البديل في حالات مثل هذه. أبعاد الصورة يمكن استخدام السمتَين width و height لتحديد بُعدَي الصورة أي العرض والارتفاع، كما بإمكانك معرفة هذين البعدين بطرق عديدة، إذ يمكنك مثلًا الضغط على المفتاحين I+Cmd في ماك لعرض المعلومات عن ملف الصورة، وبالعودة إلى مثالنا السابق يمكن كتابة التالي: <img src="images/dinosaur.jpg" alt="The head and torso of a dinosaur skeleton; it has a large head with long sharp teeth" width="400" height="341"> لن يغير ذلك كثيرًا في طريقة عرض الصورة ضمن الظروف الاعتيادية، لكن إذا لم تظهر الصورة عندما ينتقل المستخدِم إلى الصفحة التي تعرضها ولم تحمل هذه الصورة بعد على سبيل المثال، فستجد أنّ المتصفح سيترك فراغًا له أبعاد محددة لكي تُعرَض الصورة ضمنه: سيفيدك هذا الأمر في تحميل الصفحة بسرعة وأكثر سلاسةً. لكن عليك تفادي تغيير أبعاد الصورة مستخدمًا سمات HTML، فإذا كبّرت حجم الصورة كثيرًا، فسينتهي الأمر بإظهار الصورة خشنة وضبابية، وإذا صغّرتها كثيرًا، فستهدِر حجم تراسل البيانات في تنزيل صورة لا تلبي حاجات المستخدِم، كما قد تبدو الصورة مشوهةً إذا لم تحافظ على تناسق الأبعاد، لذلك عليك استخدام محرر صور لضبط الأبعاد ضبطًا صحيحًا قبل إدراجها في صفحة الويب. ملاحظة: إذا كنت مضطرًا لتغيير أبعاد الصورة، فعليك استخدام تنسيق CSS كبديل عن تغيير الأبعاد من خلال العنصر <img> مباشر. عناوين الصور بإمكانك إضافة عناوين إلى صورك باستخدام السمة title لإضافة معلومات دعم إضافية عند الحاجة، وبالعودة إلى مثالنا السابق يمكننا كتابة ما يلي: <img src="images/dinosaur.jpg" alt="The head and torso of a dinosaur skeleton; it has a large head with long sharp teeth" width="400" height="341" title="A T-Rex on display in the Manchester University Museum"> سيعرِض ذلك نص العنوان الذي اخترته عندما تمرر مؤشر الفأرة فوق الصورة. لا يُعَدّ استخدام السمة title مع ذلك محبّذًا، فله مشاكل عدة تتعلق بالوصول السهل وتتمركز حول حقيقة أنّ دعم قارئات الشاشة لها غير متوقع، ولن تعرضها معظم المتصفحات ما لم تمرر مؤشر الفأرة فوقها، لذا من الأفضل إدراج معلومات داعمة مثل هذه ضمن نص المقالة الرئيسية في الصفحة بدلًا من ربطها بالصورة. تطبيق: إدراج صورة حان دورك في تجربة الأمر، إذ سيحاول هذا التمرين تحفيزك على تجربة إدراج صورة، لهذا سيزودك بالعنصر <img> وعليك إدراج الصورة الموجودة على العنوان: https://raw.githubusercontent.com/mdn/learning-area/master/html/multimedia-and-embedding/images-in-html/dinosaur_small.jpg تذكَّر أننا أشرنا سابقًا إلى عدم قانونية إدراج الصور عبر روابط ساخنة، لكننا سنتغاضى هذه المرة عن الأمر كونه تمرين تدريبي. حاول إذا استطعت أيضًا: إضافة نص بديل، ثم التحقق من أنه يعمل بكتابة عنوان URL للصورة بشكل خاطئ. ضبط الارتفاع height والعرض width الصحيحَين للصورة (تلميح: أبعاد الصورة 200 بكسل عرضًا و 171 ارتفاعًا)، ثم اختبار قيم مختلفة للطول والعرض لرؤية التأثيرات. وضْع عنوان title للصورة. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. استخدام عنصري الشكل <figure> والشرح <figcaption> مع الصور هنالك طرق عدة لإضافة شروحات إلى الصورة، وإليك مثالًا كما يلي: <div class="figure"> <img src="images/dinosaur.jpg" alt="The head and torso of a dinosaur skeleton; it has a large head with long sharp teeth" width="400" height="341"> <p>A T-Rex on display in the Manchester University Museum.</p> </div> تُعَدّ هذه الطريقة مقبولةً باستخدام العنصر <div>، فهي تعرض المحتوى الذي تحتاجه بطريقة يمكن تنسيقها باستخدام CSS، لكن المشكلة هنا هي عدم وجود ربط دلالي semantical بين الصورة وشرحها مما سيسبب المشاكل عند استخدام قارئات الشاشة، فعند وجود 50 صورة مثلًا مع شروحها، فلا يمكن تمييز أيّ شرح لأيّ صورة. الطريقة الأفضل هي استخدام العنصرين <figure> و <figcaption>، وهما عنصران خاصَّان بالنسخة 5 من HTML، فقد ظهرت هذه العناصر خصيصًا لهذا الغرض، أي لإيجاد حاضنة دلالية للأشكال تربط جليًا بين الصورة وشرحها، ويمكن كتابة المثال السابق على سبيل المثال كما يلي: <figure> <img src="images/dinosaur.jpg" alt="The head and torso of a dinosaur skeleton; it has a large head with long sharp teeth" width="400" height="341"> <figcaption>A T-Rex on display in the Manchester University Museum.</figcaption> </figure> يخبر العنصر <figcaption> المتصفح والتقنيات المساعدة أنّ الشرح الذي يضمَّه سيكون مخصصًا للصورة التي يضمها العنصر <figure>. ملاحظة: لكل من النص البديل alt والشروحات captions دورًا مختلفًا من وجهة نظر الوصول السهل. إذ يستفيد جميع المستخدِمين من الشروحات بمن فيهم من يراها، لكن تقتصر رؤية محتوى السمة alt التي تؤدي المهمة ذاتها على الحالة التي لا تُعرض فيها الصورة، لهذا يجب ألا يتشابه محتوى الشرح ومحتوى النص البديل لأنّ كلاهما سيظهر عند غياب الصورة، لذا حاول أن تمنع متصفحك من عرض الصور وراقب ما سيحدث. لا حاجة ليكون الشكل -أي العنصر <figure>- صورةً حصرًا، فهو حاوية مستقلة يمكن أن: تعبِّر عما تعنيه بطريقة مختصرة وسهلة الفهم. تتواجد في أماكن عدة ضمن تسلسل العناصر السطرية في الصفحات. تزوّد الصفحة بمعلومات أساسية تدعم النص الرئيسي لها. قد يضم الشكل عدة صور أو شذرات (مقاطع) من الشيفرة أو ملفات فيديو أو ملفات صوتية أو جداول أو غير ذلك. تطبيق: إنشاء شكل سنأخذ في هذا التمرين الشيفرة الجاهزة للتمرين التطبيقي السابق ونحولها إلى شكل: غلِّف الشيفرة داخل العنصر <figure>. انسخ النص الموجود داخل السمة title وضَعه داخل العنصر <figcaption> تحت الصورة واحذف بعد ذلك السمة title. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. إدراج صور للخلفية باستخدام CSS يمكنك أيضًا استخدام CSS في إدراج الصور ضمن صفحات الويب وشيفرة جافاسكربت أيضًا، لكنه موضوع مختلف، إذ تُستخدّم الخاصية background-image وغيرها من خواص الخلفية background-image التي تبدأ بالكلمة *-background للتحكم في موضع خلفية الصفحة، فإذا أردت مثلًا وضع الخلفية لكل فقرة نصية، فكل ما عليك فعله هو كتابة ما يلي: p { background-image: url("images/dinosaur.jpg"); } قد يكون ضبط هذه الصورة التي استخدِمت على أساس خلفية أسهل من ضبط الصور التي تُدرجها HTML، فلماذا نزعج أنفسنا بها إذًا؟ لأن هذه الصور لأغراض الديكور والتزيين فقط كما أشرنا سابقًا، فإذا أردت استخدامها لتحسِّن مظهر صفحتك، فلا بأس بذلك، لكن تذكَّر أنها لا تملك أية دلالات ولا يمكن أن تحمل نصًا بديلًا ولا تراها قارئات الشاشة، وهنا تظهر أهمية صور HTML. باختصار: إذا كانت للصورة دلالةً تتعلق بالمحتوى الذي تقدِّمه، فعليك استخدام صور HTML، في حين عليك استخدام صور CSS إذا أردتها لتحسين مظهر الصفحة فقط. خلاصة هذا كل ما لدينا الآن، فقد غطينا موضوع الصورة وشروحاتها بالتفصيل، وسنتقدم خطوةً أخرى في مقالنا التالي من جزئية HTML من هذه السلسلة لنتعلم كيف نستخدِم HTML في إدراج مقاطع الفيديو والمقاطع الصوتية في صفحات الويب. ترجمة -وبتصرُّف- للمقال Images in HTML. اقرأ أيضًا المقال السابق: تنقيح شيفرة HTML إعداد صور متجاوبة إضافة مقاطع الفيديو عبر العنصر <video> في HTML5
  7. تُعَدّ كتابة شيفرة HTML أمرًا جليًا، لكن ما الحل عندما تواجهك بعض المشاكل ولم تستطع اكتشاف موطن الخطأ فيها؟ لذا سنقدِّم لك في هذا المقال شرحًا عن بعض الأدوات التي يمكنها مساعدتك لإيجاد الخطأ وإصلاحه. عليك قبل البدء في قراءة هذا المقال الاطلاع على أساسيات HTML التي أوردناها في مقال تعرَّف على HTML وكذلك هيكلة النصوص في HTML وطريقة إنشاء الروابط التشعبية في HTML. تنقيح الشيفرة ليس أمرا صعبا تبقى الأمور عند كتابة شيفرة واضحةً وسلسةً حتى يقع خطأ ما، فقد ارتكبت خطأً ما فجاةً، ولم تَعُد تعمل الشيفرة إطلاقًا أو لم تَعُد تعمل بالطريقة المطلوبة، ويُظهِر الشكل التالي مثالًا عن خطأ يظهر عند ترجمة برنامج بسيط مكتوب بلغة رست Rust. يسهل فهم الخطأ الذي يظهر في الصورة وهو نص بين مزدوجتين غير مغلقتين unterminated double quote string، فإذا نظرت إلى قائمة الأخطاء، فسترى الشيفرة التالية: println!(hello, world!"); يفتقد فيها النص !hello, world إلى إشارة تنصيص البداية، لكن ستتعقد هذه الرسائل وسيصعب فهمها عندما يكبر البرنامج، إذ ستبدو الحالات البسيطة مخيفةً لشخص لا يعرف شيئًا عن لغة Rust. مع ذلك، فإنّ تنقيح البرامج ليس أمرًا صعبًا، والمفتاح الأساسي لإتقان الأمر في أيّ لغة برمجة هو استيعاب اللغة وأدوات التنقيح المساعدة. تنقيح اللغة HTML ليست عملية تنقيح HTML بتعقيد تنقيح Rust، فلن تُترجم إلى صورة أخرى قبل أن يفسرها المتصفح ويعرض النتائج، فهي لغة مفسَّرة interpreted وليست مصرَّفة compiled، كما تُعَدّ عناصر HTML أوضح من ناحية الصيغة والكتابة بصورة أو بأخرى موازنةً بلغة برمجة حقيقية مثل Rust أو جافاسكربت أو بايثون، كما أنّ أسلوب التفسير المتساهل الذي تستخدِمه المتصفحات موازنةً ببقية لغات البرمجة له ميزات إيجابية وأخرى سلبية. شيفرة متساهلة ماذا نعني بالمتساهل permissive إذًا؟ ما يحصل عادةً عند ارتكاب أخطاء في الشيفرة هو ظهور نوعَين من رسائل الخطأ: أخطاء الصياغة syntax error: تنتج عن أخطاء في كتابة التعليمات أو علامات الوصول في الشيفرة مما يتسبب في إيقاف تنفيذ البرنامج كما في الخطأ الذي عرضناه في برنامج Rust السابق (نسيان إشارة تنصيص)، إذ تُعَدّ هذه الأخطاء سهلة الحل إذا كنت ملمًا بقواعد اللغة المستخدَمة وما تعنيه رسائل الخطأ. الأخطاء المنطقية logical errors: لا تقع عند حدوث أخطاء صياغة، لكن عندما لا تعطيك الشيفرة النتيجة المطلوبة، بمعنى ان البرنامج يعمل بطريقة غير صحيحة، فهذه الأخطاء صعبة الحل نظرًا لعدم وجود رسالة خطأ توجهك إلى مصدره. لن تجد في HTML أخطاءً في الصياغة لأن المتصفح يتساهل في تفسيرها، أي ستعرض الصفحة نتيجة تفسير الشيفرة حتى بوجود هذه الأخطاء، إذ تُبنى المتصفحات على قواعد تتيح لها أن تقرِّر طريقة تفسير الشيفرة غير الصحيحة لكي تعرضها حتى بصورة تخالف التوقعات، ويُعَدّ هذا الأمر مشكلةً أيضًا. ملاحظة: يعود تفسير HTML بصورة متساهلة إلى بدايات الويب، إذ اتخذ قرار عندها بأنّ إيصال المحتوى إلى المستخدِم أكثر أهميةً من صحة الصياغة، وربما لم تغدو الويب بهذه الشعبية الآن إذا كانت أكثر تشددًا في بداياتها. تطبيق: دراسة شيفرة متساهلة حان الوقت لدراسة الطبيعة المتساهلة التي تُفسَّر بها شيفرة HTML. نزّل أولًا ملفات مثال التنقيح التجريبي واحفظها على جهازك، إذ كُتِب المثال ليضم قصدًا أخطاءً لكي نستكشفها، ونقول عندها بأنّ شيفرة HTML سيئة التوصيف badly-formed على نقيض أن تكون حسنة التوصيف well-formed. افتح الملف بواسطة المتصفح، إذ ستبدو لك النتيجة كما يلي: ستلاحظ مباشرةً أنّ المشهد ليس جيدًا، فلنلق نظرةً إذًا على الشيفرة لنرى إذا كان بإمكاننا فعل شيء ما. <h1>HTML debugging examples</h1> <p>What causes errors in HTML? <ul> <li>Unclosed elements: If an element is <strong>not closed properly, then its effect can spread to areas you didn't intend <li>Badly nested elements: Nesting elements properly is also very important for code behaving correctly. <strong>strong <em>strong emphasized?</strong> what is this?</em> <li>Unclosed attributes: Another common source of HTML problems. Let's look at an example: <a href="https://www.mozilla.org/>link to Mozilla homepage</a> </ul> لنبحث عن المشاكل: لا توجد وسوم نهاية لعنصر الفقرة <p> ولعناصر القائمة <li>، لكن بالنظر إلى النتيجة، سنجد أنها لم تؤثر سلبًا على تصيير الشيفرة، وذلك لأنّ المتصفح سيستدِل بسهولة على نهاية كل عنصر وبداية الآخر. لا يوجد وسم نهاية لأول عنصر <strong>، وهذا قد يسبب مشكلةً لأنه من الصعب أن يقرِّر المتصفح متى سينتهي تأثير هذا العنصر، ولاحظ أنّ بقية النص قد كُتب بصورة بارزة. لاحظ التوصيف السيئ للقسم التالي للشيفرة، إذ لا يمكن أبدًا توقُّع كيف سيعرض المتصفح هذه الشيفرة نظرًا لمشكلة التداخل السابقة. <strong>strong <em>strong emphasized?</strong> what is this?</em> تفتقد قيمة الصفة href للرابط في نهايتها إلى إشارة تنصيص مزدوجة، وهذا ما يبدو أنه المسبب الأكبر للمشكلة فلم يُصيّر الرابط أبدًا. لنلق نظرةً الآن على الشيفرة التي صيّرها المتصفح مقابل الشيفرة المصدرية، إذ سنستخدم أدوات مطورِي ويب المدمجة في المتصفح. سترى الشيفرة التي صيّرها المتصفح ضمن فاحص DOM: دعونا نتفحَّص الشيفرة الموجودة في نافذة فاحص DOM لنرى كيفية تعامل المتصفح مع الأخطاء، وقد راجعنا الشيفرة باستخدام فايرفوكس، وينبغي أن يكون الأمر مماثلًا في جميع المتصفحات الحديثة: أضاف المتصفح وسوم نهاية للفقرات ولعناصر القائمة. من غير الواضح أين سينتهي أول عنصر <strong>، لذلك غلَّف المتصفح كل كتلة نصية منفصلة ضمن عنصر <strong> منفصل حتى نهاية المستند. حلّ المتصفح مشكلة التداخل الخاطئ للعناصر كما يلي: <strong>strong <em>strong emphasized?</em> </strong> <em> what is this?</em> أما الرابط الذي تنقصه إشارة تنصيص مزدوجة، فقد حذفه المتصفح بالكامل، وأصبح آخر عنصر في قائمة العناصر كما يلي: <li> <strong>Unclosed attributes: Another common source of HTML problems. Let's look at an example: </strong> </li> التحقق من شيفرة HTML لا بد أنك أدركت الآن أهمية التوصيف الحسن لملف HTML، فكيف ستفعل ذلك؟ من السهل في مثال بسيط مثل الذي أوردناه في الأعلى أن تبحث ضمن سطور الشيفرة لتقصي الأخطاء، لكن توقع كيف سيكون الأمر في ملف ضخم؟ إنّ أفضل استراتيجية قد تبدأ بها هي استخدام خدمة التحقق من شيفرة HTML المعروفة باسم Markup Validation Service التي ابتكرتها وتصونها منظمة W3C المسؤولة عن تعريف مواصفات HTML و CSS وغيرها من تقنيات الويب، إذ تُعَدّ هذه الخدمة تطبيق ويب يقبل ملف HTML في الدخل ثم يتفحص محتواه ثم يعطيك تقريرًا بما هو خاطئ: يمكنك استخدام عنوان ملف HTML الذي تريد التحقق منه أو رفعه أو تنسخ وتلصق الشيفرة مباشرةً. تطبيق: التحقق من ملف HTML لنحاول استخدام الملف البسيط الذي ناقشناه سابقًا: حمِّل خدمة التحقق من التوصيف في المتصفح إذا لم تكن قد فعلت ذلك. انتقل إلى نافذة التحقق من دخل مباشر Validate by Direct Input. انسخ شيفرة الملف بالكامل ثم الصقها في مربع النص الكبير الظاهر في النافذة. انقر الزر "تحقق Check". من المفترض أن تظهر قائمة من الأخطاء بالإضافة إلى معلومات أخرى. تفسير رسائل الخطأ عادةً ما تكون رسائل الخطأ مهمةً، لكنها ليست كذلك في بعض الأحيان، وبالطبع ستتمكن مع الممارسة من فهم وتفسير الأخطاء لإصلاح الشيفرة، فلنلق نظرةً إذًا على رسائل الخطأ التي ظهرت لنفهم معناها، ولاحظ أنّ كل رسالة تحمل رقم السطر والعمود الذي ارتكب الخطأ فيه لمساعدتك في تحديد موقعه. Consider adding a `lang` attribute to the `html` start tag to declare the language of this document أي فكّر في استخدام الصفة lang ضمن وسم بداية العنصر html لتحديد لغة المستند: هذه ليست رسالة خطأ وإنما تنبيه، إذ تشير التوصيات إلى ضرورة تحديد لغة الصفحة دائمًا وهذا ما ترشدك إليه هذه الرسالة. End tag `li` implied, but there were open elements أي وسم النهاية للعنصر li موجود لكن هناك وسوم بداية لعناصر (وردت مرتين): تشير الرسالة إلى عنصر مفتوح ورد وسم بداية له وينبغي إغلاقه، إذ وجدت الخدمة وسم النهاية لكنه ليس في المكان الصحيح، ويشير رقمَي السطر والعمود إلى السطر الأول الذي يلي المكان الذي ينبغي وجود وسم النهاية فيه، وهذا دليل واضح لفهم الخطأ الحاصل. Unclosed element `strong` أي عنصر strong غير مغلق: الرسالة واضحة، ويشير رقمَي السطر والعمود مباشرةً إلى مكان الخطأ. End tag `strong` violates nesting rules أي يخالف وسم نهاية العنصر strong قواعد التداخل: تشير الرسالة إلى تداخل خاطئ للعناصر، ويشير رقمَي السطر والعمود مباشرةً إلى موقع المشكلة. End of file reached when inside an attribute value. Ignoring tag أي الوصول إلى نهاية الملف ولانزال ضمن قيمة سمة. إهمال الوسم: هذه الرسالة مشفّرة نوعًا ما، وتشير إلى وجود سمة في مكان ما لم تكتب قيمتها بصورة صحيحة وربما قرب نهاية الملف لأن نهايته ظهرت داخل قيمة السمة، وطالما أنّ المتصفح لم يصيّر الرابط، فسيدلنا ذلك على مصدر الخطأ. End of file seen and there were open elements أي الوصول إلى نهاية الملف مع وجود عناصر مفتوحة: هذه الرسالة غامضة، لكنها تشير مبدئيًا إلى وجود عناصر مفتوحة لابد من إغلاقها بطريقة صحيحة، إذ يشير رقم السطر إلى السطور الأخيرة في الملف، كما تأتي الرسالة مع سطر من الشيفرة يشير إلى مثال عن عنصر مفتوح: example: <a href="https://www.mozilla.org/>link to Mozilla homepage</a> ↩ </ul>↩ </body>↩</html> ملاحظة: قد ينتج عن سمة لم تغلق بإشارة تنصيص عنصر مفتوح لأنّ بقية المستند ستفسر على أنها محتوى لقيمة السمة. Unclosed element `ul` أي عنصر ul غير مغلق: لن تقدِّم لك هذه الرسالة فائدةً كبيرةً لأن العنصر مغلق بصورة صحيحة، لكن سبب رسالة الخطأ هو إشارة التنصيص المفقودة. لا تقلق إذا لم تتمكن من فهم الأخطاء التي تشير إليها الرسائل، وحاول إصلاح بعض الأخطاء في كل مرة ثم أعد تقييم الشيفرة لتقف على الأخطاء التي بقيت، فقد يؤدي إصلاح خطأ واحد ظهر في البداية إلى التخلص من بقية الأخطاء، وستجد أنّ عدة أخطاء قد تنتج أحيانًا عن مشكلة واحدة. ستعرف أنك أصلحت جميع الأخطاء عندما تشاهد الشريط التالي في خرج الخدمة: خلاصة قدمنا في المقال لمحةً عن تنقيح شيفرة HTML والتي ستمنحك بعض الخبرة التي تساعدك عندما تبدأ تعلُّم تنقيح شيفرة CSS وجافاسكربت وغيرها من الشيفرات التي ستقابلها خلال مسيرتك المهنية، وبهذا المقال نصل إلى نهاية سلسلة المقالات التي تُمهِّد للغة HTML. ترجمة -وبتصرُّف- للمقال Debugging HTML. اقرأ أيضًا تنقيح أخطاء شيفرة جافاسكربت في Chrome اختبار الوحدات وأدوات تنقيح الشيفرات وتصحيح الأخطاء في Cpp
  8. تعرّفنا في المقال السابق على طريقة إنشاء مستند جوجل، وأنشأنا مستندًا جديدًا أسميناه "نصائح عن تبادل الأفكار في مستندات جوجل"؛ كما تعرفنا على طريقة مشاركة المستند مع مساهمين آخرين وكيفية تحديد الصفة التي يساهم بها كل منهم والتي قد تكون "محررًِّا" أو "معلقًا" أو "عارضًا". وشاركنا في نهاية المطاف المستند مع ثلاثة مساهمين افتراضيين الأول بصفة محرر، والثاني بصفة معلّق، والثالث بصفة عارض. سنتابع الآن شرح الأفكار المتعلقة بمشاركة المستندات من خلال نافذة المستند التي تظهر عند كل مساهم على حدة وطرق التواصل بين المساهمين وتنظيم العمل. إن كنت مستعدًا سنبدأ. قبل البدء ربما من الأفضل في مثالنا أن تنتحل أنت شخصية المساهمين الثلاث كما فعلت أنا، أي أن تكون لك عدة حسابات على غوغل أو غيرها ثم تستخدم الحساب الأول لإنشاء المستند وستكون المالك، بعدها شارك المستند مع حسابك الثاني بصفة محرر ومع الثالث بصفة معلّق. يُنصح في هذه الحالة بتثبيت متصفحين مختلفين جوجل كروم وفايرفوكس مثلًا، ثم سجّل دخولك من خلال المتصفحين إلى حساب مختلف، الأول كمالك والثاني كمساهم آخر (محرر مثلًا)، وهكذا. وبهذه الطريقة ستتفهم صلاحياتك وطريقة العمل المشترك من وجهتي نظر مختلفتين. العمل المتزامن لمحرر (أو مالك) مع محرر آخر في مستندات جوجل نقصد بالعمل المتزامن وجود أكثر من مساهم في نفس الوقت للعمل على المستند، وهذه ميزة في غاية الأهمية لمناقشة تفاصيل نهائية أو توضيح نقاط أساسية قبل البدء بالعمل تتطلب وجود المساهمين معًا. نفترض في هذا المثال أن المساهم "address@website.com" هو المحرر الرئيسي، والمساهم "address@mysite.com" هو المحرر المساعد، أي كلاهما يساهمان في مستندنا بصفة محرر. إليك لقطة الشاشة التي تظهر عند دخول المحرر المساعد إلى المستند: لربما لاحظت التغيرات التالية في المستند سواءً في منطقة التحرير أو في منطقة أشرطة التحكم أبرزها كما تظهر في لقطة الشاشة السابقة هي: المنطقة 1: يظهر فيها مؤشر على رأسه اسم المساهم الذي يضع مؤشر الفأرة في هذا الموضع من نسخته من المستند (على جهازه)، وهكذا يمكنك متابعة ما يجريه من تغييرات على نسخته مباشرة على نسختك. المنطقة 2: تعرض لك أيقونات المساهمين الموجودين حاليًا إضافة إلى أيقونة المحادثة المباشرة إلى جوارها، وهذا ما سنتحدث عنه لاحقًا. المنطقة 3: تُعرض عند فتح المستند قائمة بالعناوين الرئيسية والفرعية التي يتضمنها، وذلك لأغراض التنقل السريع نحو النقاط المستهدفة إن كانت محددةً مسبقًا من قِبل المالك أو المحرر الرئيسي. يمكن إخفاء الملخص بالنقر على السهم المجاور لكلمة "ملخص". ستظهر في نافذة متصفح المحرر الرئيسي نفس التغيرات السابقة تمامًا، أي ظهور موقع مؤشر فأرة المحرر الآخر وأيقونته في شريط التحكم. التلميح المباشر في المكان ضمن مستندات جوجل لنفترض أن المحرر الرئيسي يريد توجيه المحرر المساعد إلى التوسع في كتابة بعض الفقرات، ولكي يدل المحرر المساعد الذي يُفترض به إنجاز الأمر على المكان المحدد بدقة، سينقر إلى جوار الفقرة المطلوبة ويكتب مثلًا "ما رأيك في التوسع هنا". في نفس اللحظة سيرى المحرر المساعد ما هو مطلوب وأين سينفذه وهذا ما يختصر الكثير من الكلام والنقاش. يمكن استخدام هذا الأسلوب أيضًا في حال أراد أن يعلم أحد المساهمين المساهم الآخر طريقة تطبيق تنسيق معين أو إدراج أي شيء في المستند، ولهذا تُعَد هذه الميزة ممتازةً في هذا المضمار. استخدام المحادثة المباشرة في مستندات جوجل تُعَد هذه الميزة أكثر أناقةً في التواصل بين المساهمين الذين يعملون في نفس الوقت على المستند، إذ تُناقش الآراء ويجري تبادل الأفكار في إطار منفصل عن نص المستند. لبدء المحادثة المباشرة، انقر على زر "إظهار المحادثة" الملاصق لأيقونات المساهمين المتواجدين حاليًا في شريط الأدوات أعلى المستند. لتظهر لك النافذة الجانبية التالية: تعمل هذه المحادثة الكتابية كأي واجهة محادثة تقليدية، إذ يوجد أسفل النافذة مربع يحمل النص الافتراضي "اكتب المحادثة هنا"، ويمكنك كتابة الرسالة التي تريد ثم اضغط المفتاح Enter لإرسالها. استخدام جوجل ميت لعقد اجتماع حول مستند جوجل يُتاح هذا الخيار للمساهمين إن كان لا بد من مناقشة محتوى المستند على أصعدة مختلفة تتطلب خيارات أخرى، مثل مشاركة شاشات الحواسب التي يعملون عليها لتوضيح أمور تتعلق مثلًا باستخدام التطبيق نفسه ولا تتعلق بالمحتوى. في هذه الحالة يمكن جدولة اجتماع ليكون في يوم محدد وتدعو إليه من تريد من المساهمين وتحدد إن كان الاجتماع سيستخدم جوجل ميت Google Meet أم لا. سنحاول تنفيذ الأمر خطوةً بخطوة: انقر على أيقونة التقويم التي تقع أعلى القائمة الجانبية اليسارية لتطبيق مستندات جوجل، ثم انقر أيقونة القائمة (النقط الثلاث المتعامدة) واختر "جدول المواعيد"، بعدها انقر على زر "إنشاء حدث". تظهر لك نافذة إنشاء الحدث التي يتصدرها عنوان المستند كعنوان للحدث بإمكانك تغييره طبعًا. انقر على تحديد موعد لتستخدم تقويم غوغل الذي يُعرض لتحديد الشهر واليوم والساعة، ثم انقر على أيقونة توسيع في اللوحة التي تقع أسفل التقويم لاستئناف عملية التخطيط للاجتماع. انقر على "إضافة مدعوّين" لتختارهم من القائمة المنسدلة أو بكتابة بريدهم الإلكتروني، ثم حدد أذونات المدعويين. تشير اللوحة التي تلي أن الاجتماع سيكون باستخدام جوجل ميت. يمكن بعد ذلك إضافة وصف الاجتماع وسببه وستلاحظ وجود المستند مرفقًا مع الوصف. اختر اللون الذي يميزك كمؤسس للاجتماع (إن أردت)، ثم حدد ما إن كنت متواجدًا فيه. اختر طبيعة عرض هذا الحدث إن كان خاص أو علني (خاص لن يعرف به سوى من دُعي إليه). اختر فترة إرسال إشعار أو تنبيه باقتراب موعد الاجتماع. انقر الزر "حفظ"، ثم حدد إن كنت تريد إبلاغ المدعويين عن طريق البريد الإلكتروني أم لا. يُبلَّغ المدعوون إلى الاجتماع من خلال البريد الإلكتروني وعبر نسختهم من المستند، وذلك عند النقر على أيقونة جوجل مِيت في شريط ادوات المستند، كما يجري تذكيرهم بالاجتماع قبل الموعد المحدد بفترة زمنية هي نفسها التي حددها مؤسس الاجتماع عندما أنشأه؛ أما بالنسبة لتفاصيل عملية الاجتماع ومشاركة الشاشات وعقد مؤتمر فيديو، فهي مماثلة تمامًا لإنشاء مكالمة فيديو من خلال جوجل مييت، ولسنا بصدد شرحها هنا. العمل غير المباشر على مستندات جوجل باستخدام التعليقات والاقتراحات قد لا يكون وجود مساهمين في نفس الوقت لمناقشة موضوع معين خاص بمحتوى المستند أمرًا ضروريًا في معظم الأحيان، كما قد يكتفي كل طرف بإنجاز ما يوكل إليه من مهام مع الإشارة إلى ما أنجزه، أو الاستفسار عن نقطة معينة، أو لفت الانتباه إلى وجود مشكلة ما من خلال ما يُعرف بالتعليق أو اقتراح استبدال محتوى بآخر، وهذا ما يُدعى اقتراحًا. التعليقات في مستندات جوجل وهي طريقة يشير فيها أحد المساهمين إلى مشكلة أو موضوع معين في مكان معين من المستند ويسند أمر متابعته إلى مساهم معين. بالعودة إلى مثالنا التطبيقي "نصائح عن تبادل الأفكار في مستندات جوجل" كما في لقطة الشاشة السابقة، سيلاحظ المحرر المساعد مثلًا تكرار الفقرة الأولى والثانية ويرغب في الاستفهام عن الموضوع قبل أن يعدّل أي شيء. بإمكانه في هذه الحالة تعليم عنوان الفقرة الأولى ثم النقر على أيقونة التعليق ضمن قائمة خيارات الأوضاع الجانبية لتظهر النافذة التالية: اكتب نص التعليق ثم انقر بعد ذلك المفتاح Enter لتنتقل إلى سطر جديد. ابدأ السطر الجديد بالمحرف @ يليه البريد الإلكتروني للمحرر الرئيسي، وعند كتابة عنوان بريد صحيح أو اسم موجود في قائمة الاتصال، سيظهر في نافذة التعليق مربع تحقق من أنك تريد فعلًا إسناد المهمة إلى المساهم الذي ذكرته في التعليق. فعّل مربع التحقق ثم انقر على الزر "تعيين" أسفل النافذة. سيرى المساهم الرئيسي التعليق في موضعه الذي حدده المحرر المساعد عندما يفتح المستند ويلاحظ أن مهمة الإجابة عن الإستفسار منوطة به وحده، كما سيتلقى في نفس الوقت بريدًا إلكترونيًا يلفت انتباهه إلى ما جرى مؤخرًا ضمن المستند كما توضح لقطتي الشاشة المتجاورتين التاليتين: يمكن للمحرر الرئيسي أن يحل الأمر مباشرةً وينهي المناقشة عن طريق النقر على الأيقونة التي تحمل إشارة "صحيح" أعلى نافذة التعليق (ستظهر العبارة "وضع علامة كتم وإخفاء المناقشة" عند وصول مؤشر الفأرة إليها)، وقد يعيد الأمر إلى صاحب التعليق الأساسي أو أي مساهم آخر لتوجيهه إلى طريقة حل الموضوع، وذلك بالنقر على نافذة التعليق، ثم كتابة المطلوب والإشارة بالمحرف @ إلى المساهم المطلوب كما في الشكل التالي: الاقتراحات والوجوه التعبيرية في مستندات جوجل قد يرى أحد المحررين طريقةً أفضل في صياغة محتوى معين أو إدراج محتوى آخر أكثر ملاءمةً لموضوع المستند، لكنه يرغب في استشارة فريق العمل، ولهذا يتيح تطبيق مستندات جوجل وضعًا يُسمى وضع الاقتراح يمكن تفعيله بأن تنقر على قائمة الأوضاع الجانبية التي تظهر على يمين المستند أو من خلال أشرطة الأدوات أعلى المستند: لنفترض أن أحد المحررين وجد أنّ استبدال عبارة "بشكل أسرع" بالكلمة "بسرعة" أمر أفضل من ناحية صياغة النص وأراد أن يقترح الأمر على محرر محدد. عليه في هذه الحالة اختيار وضع الاقتراح، ثم تحديد المكان الذي وردت فيه العبارة وكتابة الكلمة "بسرعة" ليلاحظ أنها تزيح العبارة السابقة التي تظهر مشطوبة دون أن تزيلها ويحيط إطار أخضر بمكانها. تظهر في نفس الوقت نافذة الاقتراح على يسار المستند توضح ما يُقترح استبداله (أو على اليمين إذا كان بالنسخة العربية)، يمكنك عندها ترك النافذة كما هي لتظهر عند كل المساهمين أو الإشارة إلى مساهم محدد من خلال بريده الإلكتروني بعد المحرف @: يمكن عندها للمساهم الآخر قبول الاقتراح بالنقر على أيقونة "موافق" أعلى نافذة الاقتراح أو رفضه بالنقر على أيقونة "مرفوض". لكن إن أردت أن تلفت انتباه المساهمين إلى عبارة أو صورة أو أي محتوى دون أن تكتب شيئًا، فقط أن تشير إلى هذا الموضوع بأسلوب ظريف ومريح لا يحمل معاني ضغط العمل، بإمكانك النقر على أيقونة الوجوه التعبيرية ضمن قائمة الأوضاع الجانبية واختيار الوجه التعبير الذي تريد وسيراه الجميع أينما وضعته. العمل على مستند جوجل بصفة معلق أو عارض في الواقع، لن تتغيّر الأمور كثيرًا من ناحية التفاصيل التي تحدثنا عنها سابقًا، إلا أن المعلّق لن يكون قادرًا على تغيير أي عبارة بل أن يقترح تغييرها فقط، ويجري التغيير أو الاستبدال في حال وافق محرر على هذا الاقتراح، كما يُتاح للمعلق بطبيعة الحال ترك تعليقات وحضور اجتماعات أو تحضيرها واستخدام المحادثة المباشرة. ستجرد المساهم عند دخوله إلى المستند بصفة "عارض" من كل إمكانيات التحرير وإضافة التعليقات أو الاقتراحات ويبدو شكل نافذة التطبيق كالتالي: لكن يمكن أن يطلب العارض من مالك المستند حصرًا إذنًا بالتعديل بالنقر على زر "طلب الإذن بالتعديل" أو مشاركة آخرين بالنقر على زر "مشاركة". خلاصة تعرفنا في هذا المقال على الطرق التي يمكن بها للمساهمين التواصل لإكمال العمل على مستند جوجل. ورأينا أن هنالك حالات يُضطر فيها المساهمون إلى العمل المتزامن (في الوقت ذاته)، لذلك تحدثنا عن التلميح المباشر في المكان بمجرد تعليم العبارة النصية أو استخدام المحادثة الكتابية المباشرة، كما تحدثنا عن أهمية تحديد اجتماع عام من خلال جوجل مييت لمناقشة مواضيع شتى تتعلق بالمستند أو بمستندات جوجل أو غيرها من النقاط التي تتطلب اجتماعًا موسّعًا. أشرنا أيضًا إلى إمكانيات مستندات جوجل في دعم التعاون غير المباشر من خلال التعليقات والاقتراحات وطريقة إسناد المهام إلى المساهمين المختلفين من خلال لقطات شاشة متنوعة وشرح موسع لمعظم الأفكار المتعلقة بهذا الشكل من التعاون. اقرأ أيضًا المشاركة وتوزيع المهام على مستندات جوجل الجداول في تطبيق مستندات جوجل - إنشاء سيرة ذاتية. إعداد الصفحة وتنسيق الفقرات في مستندات جوجل إدراج المعادلات والكائنات الرسوميَّة في تطبيق مستندات جوجل
  9. تقدِّم HTML إضافةً إلى العناصر التي تحدد أجزاءً مستقلةً في الصفحة -مثل الفقرات النصية والصور- عددًا من العناصر الكتلية Block elements لتحدد مناطق كاملة ضمن صفحة ويب مثل ترويسة الصفحة Header أو قوائم التنقل Navigation menu أو أعمدة المحتوى الأساسي main content columns، إذ سيبحث هذا المقال في كيفية التخطيط لوضع هيكلية بسيطة لموقع ويب وكتابة شيفرة HTML المناسبة. عليك قبل البدء في قراءة هذا المقال الاطلاع على أساسيات HTML التي أوردناها في مقال تعرَّف على HTML وكذلك هيكلة النصوص في HTML ومقال طريقة إنشاء الروابط التشعبية. الأقسام الأساسية لصفحة ويب تبدو صفحات ويب مختلفةً عن بعضها لكنها تميل لأن تتشارك مكونات أو أقسامًا معياريةً، ما لم تعرض فيديو على كامل الشاشة أو لعبة ما أو أن تكون جزءًا من مشروع فني أو أن تكون سيئة الهيكلة: الترويسة Header: يظهر عادة على أساس شريط واسع أعلى الصفحة يضم عنوانًا عريضًا وشعارًا، كما يبقى هذا القسم نفسه في كل صفحات الموقع على الأغلب. شريط التنقل Navigation bar: يرتبط بالأقسام الرئيسية للموقع ويُعرض على هيئة أزرار أو روابط أو نوافذ ضمنية tabs، كما يبقى عادةً محتوى هذا الشريط كما هو في جميع صفحات الموقع تمامًا مثل محتوى الترويسة، فعدم وجود هذا الشريط في صفحة ما سيربك المستخدِم ويدفعه إلى ترك الموقع، كما يعتقد الكثير من المصممين أنّ أشرطة القوائم لا بد وأن تكون جزءًا من الترويسة، لكنها تبقى مجرد رؤية غير ملزمة، لأن البعض يجادل أيضًا في أفضلية وجودهما مستقلين من منظور سهولة الوصول أو الشمولية accessibility، إذ يمكن لقارئات الشاشة أن تقرأهما بصورة أفضل في حال كانا منفصلين. المحتوى الرئيسي main content: يُعَدّ منطقةَ واسعةَ منتصف الصفحة تتضمن المحتوى الخاص بصفحة الويب، مثل الفيديو الذي تريد مشاهدته أو القصة التي تريد قراءتها أو الخرائط التي تريد رؤيتها أو عناوين الأخبار أو غير ذلك، وهذا القسم بالتحديد سيختلف بكل تأكيد من صفحة لأخرى. الشريط الجانبي sidebar: يضم بعض المعلومات على أطراف الصفحة مثل الروابط أو الاقتباسات أو الإعلانات وغيرها، يتعلق محتوى هذا القسم بما يرد في المحتوى الرئيسي عادةً، فقد تجد في الشريط الجانبي لمحةً عن صاحب الصفحة أو روابط إلى مواضيع مشابهة، كما قد تجد أحيانًا بعض العناصر الاحتياطية مثل قوائم تنقل إضافية. تذييل الصفحة footer: شريط يمتد أسفل الصفحة ويضم عادة رسومات دقيقة وملاحظات عن حقوق النشر أو معلومات التواصل، ويُستخدَم هذا القسم لإدراج معلومات عامة كما هو حال الترويسة، لكن هذه المعلومات ليست حيويةً أو مكملة لما يعرضه الموقع، كما يُستخدَم التذييل أحيانًا لأغراض تحسين محركات البحث Search Engine Optimization، إذ يعرض أحيانًا روابط سريعة للوصول إلى محتوى ذو شعبية كبيرة على الويب. قد يُبنى هيكل موقع ويب نموذجي مشابه لما تعرضه الصورة التالية: استخدام HTML في هيكلة المحتوى لا يُعَدّ النموذج في الصورة السابقة جميلًا لكنه جيد في توضيح تخطيط موقع ويب نموذجي، إذ تحتوي بعض المواقع على أعمدة أكثر وبعضها يبدو أعقد، لكن الفكرة واضحة، كما تستطيع من خلال تنسيقات CSS استخدام أية عناصر تريد لتغلف بها أيّ قسم ليبدو بالصورة الذي تريدها، لكن وكما تحدثنا سابقًا لا بد من احترام دلالات عناصر HTML واستخدام العنصر الصحيح للغاية الصحيحة. لا يخبرك ما تراه على الصفحة كل شيء، إذ تُستخدَم الخطوط والألوان للفت انتباه المستخدِم إلى الأقسام الأكثر فائدةً في الصفحة مثل قوائم التنقل والروابط المتعلقة بها، لكن ماذا لو أردنا الانتباه إلى الأشخاص ذوي المشاكل البصرية؟ فلن يهتم هؤلاء باللون الزهري أو بالخط الكبير. ملاحظة: يُقدَّر عدد المصابين بعمى الألوان من الذكور بحدود 8% ومن النساء بحدود 0.5%، أي رجل من بين 12 وامرأة من بين 200، في حين ويمثِّل المصابون بالعمى وأصحاب المشاكل البصرية الجادة نسبة 4-5% من تعداد البشر، وقد كان عددهم قرابة 285 مليون عالميًا عام 2012 عندما كان تعداد سكان الأرض سبعة مليارات. تستطيع من خلال HTML توصيف أقسام المحتوى وفقًا لوظيفتها، فبإمكانك استخدام عناصر تعكس وظيفة هذا القسم من المحتوى بشكل واضح لا لبس فيه، وبالتالي ستتمكن التكنولوجيا المساعدِة مثل قارئات الشاشة من تمييز هذه العناصر والمساعدة في إنجاز مهام مختلفة مثل إيجاد العناوين أو قوائم التنقل أو المحتوى الرئيسي، وكما أشرنا في مقالات سابقة، هنالك عواقب عديدة لعدم استخدام هيكلية صحيحة للعناصر أو إهمال المعنى الدلالي لها. تقدِّم لك HTML مجموعةَ من العناصر لإنجاز هيكلية دلالية تعكس وظيفة كل قسم منها: الترويسة header: يمثلها العنصر <header>. شريط التنقل navigation bar: يمثله العنصر <nav>. المحتوى الرئيسي main content: يمثله العنصر <main>، بالإضافة إلى العديد من العناصر التي تمثل أقسامًا فرعيةً للمحتوى الرئيسي مثل <section> و <div>. الشريط الجانبي sidebar: يمثله العنصر <aside>. التذييل footer: يمثله العنصر <footer>. تطبيق: فهم شيفرة الصفحة النموذجية السابقة تمثِّل الشيفرة التالية شيفرة الموقع النموذجي الذي عرضنا صورته سابقًا، ويمكنك تنزيلها أيضًا من المستودع المخصص للمثال على جيت-هب GitHub، كما نطلب منك إلقاء نظرة على الشيفرة ومن ثم العودة إلى القائمة في الأسفل لتحديد أجزاء الشيفرة وما تمثله من أقسام. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My page title</title> <link href="https://fonts.googleapis.com/css?family=Open+Sans+Condensed:300|Sonsie+One" rel="stylesheet" type="text/css"> <link rel="stylesheet" href="style.css"> <!--الدلالية في النسخ HTML5 الأسطر الثلاث التالية هي طريقة لتعمل عناصر الأقدم من متصفح إنترنت إكسبلورر--> <!--[if lt IE 9]> <script src="https://cdnjs.cloudflare.com/ajax/libs/html5shiv/3.7.3/html5shiv.js"></script> <![endif]--> </head> <body> <!-- الترويسة الرئيسية التي ستستخدم في جميع صفحات الموقع --> <header> <h1>Header</h1> </header> <nav> <ul> <li><a href="#">Home</a></li> <li><a href="#">Our team</a></li> <li><a href="#">Projects</a></li> <li><a href="#">Contact</a></li> </ul> <!-- استمارة بحث وهو أسلوب غير مباشر للتنقل عبر الموقع --> <form> <input type="search" name="q" placeholder="Search query"> <input type="submit" value="Go!"> </form> </nav> <!-- المحتوى الرئيسي للموقع --> <main> <!-- يتضمن مقالة--> <article> <h2>Article heading</h2> <p>Lorem ipsum dolor sit amet, consectetur adipisicing elit. Donec a diam lectus. Set sit amet ipsum mauris. Maecenas congue ligula as quam viverra nec consectetur ant hendrerit. Donec et mollis dolor. Praesent et diam eget libero egestas mattis sit amet vitae augue. Nam tincidunt congue enim, ut porta lorem lacinia consectetur.</p> <h3>Subsection</h3> <p>Donec ut librero sed accu vehicula ultricies a non tortor. Lorem ipsum dolor sit amet, consectetur adipisicing elit. Aenean ut gravida lorem. Ut turpis felis, pulvinar a semper sed, adipiscing id dolor.</p> <p>Pelientesque auctor nisi id magna consequat sagittis. Curabitur dapibus, enim sit amet elit pharetra tincidunt feugiat nist imperdiet. Ut convallis libero in urna ultrices accumsan. Donec sed odio eros.</p> <h3>Another subsection</h3> <p>Donec viverra mi quis quam pulvinar at malesuada arcu rhoncus. Cum soclis natoque penatibus et manis dis parturient montes, nascetur ridiculus mus. In rutrum accumsan ultricies. Mauris vitae nisi at sem facilisis semper ac in est.</p> <p>Vivamus fermentum semper porta. Nunc diam velit, adipscing ut tristique vitae sagittis vel odio. Maecenas convallis ullamcorper ultricied. Curabitur ornare, ligula semper consectetur sagittis, nisi diam iaculis velit, is fringille sem nunc vet mi.</p> </article> <!-- يمكن للمحتوى الجانبي أن يتداخل مع المحتوى الرئيسي --> <aside> <h2>Related</h2> <ul> <li><a href="#">Oh I do like to be beside the seaside</a></li> <li><a href="#">Oh I do like to be beside the sea</a></li> <li><a href="#">Although in the North of England</a></li> <li><a href="#">It never stops raining</a></li> <li><a href="#">Oh well...</a></li> </ul> </aside> </main> <!-- التذييل الرئيسي الذي سيستخدم في جميع صفحات الموقع --> <footer> <p>©Copyright 2050 by nobody. All rights reversed.</p> </footer> </body> </html> خذ وقتك في الاطلاع على الشيفرة وفهمها، كما ستساعدك التعليقات التي أدرجناها ضمن الشيفرة في ذلك، ولا نطلب منك فعل أيّ شيء آخر، لأنّ المفتاح الأساسي لفهم مخطط صفحة أو ملف هو كتابة هيكل HTML ومن ثم إبرازه عبر تنسيق CSS. تفاصيل عناصر تخطيط HTML من الجيد فهم المعنى العام لجميع العناصر التي تعرّف الأقسام المختلفة في صفحة HTML، وهذا ما ستعمل عليه لاحقًا عندما تكتسب خبرةَ أكبر في تطوير الويب، لكن ما عليك استيعابه الآن هي التعريفات التالية: العنصر <main>: يستخدَم لإدراج المحتوى الفريد للصفحة، وينبغي استخدام هذا العنصر مرةً واحدةً في الصفحة، كما يوضَع مباشرةً ضمن العنصر <body>، ولا يجب أن يتداخل هذا العنصر مع عناصر أخرى. العنصر <article>: يغلِّف كتلًا من المحتوى المترابط القائم بذاته والذي لا يتعلق بمحتوى بقية الأقسام مثل المنشورات المستقلة. العنصر <section>: يشابه العنصر <article> لكنه يستخدَم لتحضير جزء من الصفحة ليقدّم وظيفةً بمفرده مثل خريطة صغيرة أو مجموعة من العناوين أو الملخصات لمقالات أو ليقدم موضوعًا محددًا، كما يُعَدّ استخدام عناصر العناوين في بداية كل عنصر <section> ممارسةً جيدةً، وتجدر الملاحظة أنه بالإمكان تجزئة محتوى العنصر <article> إلى أقسام مختلفة وتجزئة القسم <section> إلى مقالات مختلفة وفقًا للسياق. العنصر <aside>: يضم محتوًى لا يتعلق مباشرةً بالمحتوى الرئيسي للصفحة لكنه يزوِّد الصفحة بمعلومات إضافية مثل مدخلات مختصرة أو سيرة ذاتية للمؤلف أو روابط متعلقة بالمحتوى وغيرها. العنصر <header>: يضم محتوًى تعريفيًا للصفحة، فإذا كان هذا العنصر ابنًا للعنصر <body>، فسيمثل الترويسة العامة للصفحة، في حيم إذا كان ابنًا للعنصر <section>، فسيمثل ترويسةً خاصةً بهذا القسم، وحاول عدم الخلط بين الترويسة وعنوان صفحة والعناوين الأخرى. العنصر <nav>: يؤمن وظيفة التنقل الأساسية في الصفحة، ولا يضم روابط ثانوية وما شابهها. العنصر <footer>: يضم محتوًى مناسبًا لتذييل الصفحة. يمكنك الاطلاع على كل عنصر من العناصر السابقة بالعودة إلى توثيق HTML العربي في موسوعة حسوب. عناصر غير دلالية قد لا تجد في بعض الأحيان عنصرًا دلاليًا مناسبًا لتغليف محتوى معيّن، وقد ترغب في أحيان أخرى ضم مجموعة من العناصر مع بعضها لكي تتعامل معها على أساس كيان واحد عند استخدام CSS أو جافاسكربت، إذ تقدِّم HTML في حالات مثل هذه العنصر <div> الذي يُفضَّل استخدامه مع السمة class لكي تعطيه تسميةً محددةً فيسهل استهدافه، كما يُعَدّ العنصر <span> عنصرًا سطريًا غير دلالي ينبغي استخدامه فقط إذا لم تجد عنصرًا دلاليًا مناسبًا لتغليف المحتوى المطلوب أو لم ترد إعطاء معنى محدد لهذا المحتوى، وإليك مثالًا كما يلي: <p>، دخل الملك إلى غرفته مترنحًا في الواحدة ليلًا لم يساعده الدواء كثيرًا وجر نفسه عبر الباب <span class="editor-note">[ملاحظة للمحرر: في هذه اللحظة ينبغي تخفيض الأضواء]</span>.</p> يُفترَض في هذه الحالة أن تقدِّم ملاحظة المحرر الموسومة بالصنف "editor-note" توجيهات إضافيةً للقارئ بما أنها لا تحمل أي دلالة إضافية، ويمكن حل هذا الموضوع ببساطة باستخدام CSS أيضًا، وذلك بإزاحة الملاحظة قليلًا عن النص الرئيسي. يُعَدّ العنصر <div> عنصرًا كتليًا غير دلالي ينبغي استخدامه فقط إذا لم تجد عنصرًا دلاليًا كتليًا مناسبًا لتغليف المحتوى المطلوب أو لم ترد إعطاء معنى محدد لهذا المحتوى، فتخيل على سبيل المثال قائمة تسوّق يمكنك طلبها في أيّ وقت من موقع تجاري: <div class="shopping-cart"> <h2>Shopping cart</h2> <ul> <li> <p><a href=""><strong>Silver earrings</strong></a>: $99.95.</p> <img src="../products/3333-0985/thumb.png" alt="Silver earrings"> </li> <li> ... </li> </ul> <p>Total cost: $237.89</p> </div> لا تُعَدّ هذه القائمة محتوىً جانبيًا <aside> في الواقع، فقد لا تتعلق بالضرورة بالمحتوى الرئيسي للصفحة وتريدها أن تظهر في أيّ مكان ضمن الموقع، كما ليس من المجدي استخدام العنصر <section> لكونها لا تمثل جزءًا من المحتوى الرئيسي، وبالتالي يكون استخدام العنصر <div> في هذه الحالة مناسبًا، كما استخدمنا معه عنوانًا لمساعدة قارئات الشاشة على إيجاده. تنبيه: يُعَدّ استخدام العنصر <div> مريحًا وسهلًا للغاية، فهو لا يحمل أيّ معنى دلالي، وإنما يُجمّع شيفرة HTML ضمن كيان واحد، ولكن انتبه إلى استخدامها فقط عندما لا تجد عنصرًا دلاليًا مناسبًا، وحاول التقليل من استخدامه إلى الحد الأدنى كي لا تعاني في صيانة الموقع لاحقًا. بادئات الأسطر الجديدة والخطوط الأفقية يوجد عنصران ستستخدمهما في مناسبات عدة ولا بد من الاطلاع عليهما: عنصر الانتقال إلى سطر جديد <br>: يدرِج هذا العنصر سطرًا جديدًا ضمن فقرة نصية، ويُعَدّ هذا العنصر الأسلوب الوحيد لتقسيم كتلة نصية أو بنية محددة إلى سلسلة من الأسطر القصيرة مثل حالة عنوان بريدي أو قصيدة شعرية، وإليك مثالًا كما يلي: <p>There once was a man named O'Dell<br> Who loved to write HTML<br> But his structure was bad, his semantics were sad<br> and his markup didn't read very well.</p> سيُصيَّر أو يُعرَض النص في الفقرات على صورة خط طويل دون استخدام العنصر <br>، أما بوجوده، فسيصيِّر المتصفح الفقرة النصية على الصورة التالية: عنصر إدراج سطر أفقي <hr>: يُنشئ العنصر خطًا أفقيًا في الصفحة لكي يعرض تغيرًا في سمة النص مثل تغير الموضوع أو المشهد، وإليك مثالًا كما يلي: <p>Ron was backed into a corner by the marauding netherbeasts. Scared, but determined to protect his friends, he raised his wand and prepared to do battle, hoping that his distress call had made it through.</p> <hr> <p>Meanwhile, Harry was sitting at home, staring at his royalty statement and pondering when the next spin off series would come out, when an enchanted distress letter flew through his window and landed in his lap. He read it hazily and sighed; "better get back to work then", he mused.</p> إذ سيصيَّر كما يلي: التخطيط لموقع ويب بسيط ستكون الخطوة المنطقية التالية حالما تنتهي من وضع خطط هيكلة موقعك البسيط هي العمل على تحديد المحتوى الذي يضمه الموقع وما الصفحات التي تحتاجها وكيف سترتبها وتربطها ببعضها لكي تقدِّم لمستخدِميك أفضل تجربة، وتُدعى هذه الخطوة بمعمارية المعلومات، وقد يحتاج الأمر في المواقع الضخمة والمعقدة إلى الكثير من التخطيط، لكن لموقع بسيط مثل الذي نشير إليه في هذا المقال، فالعملية أسهل وأكثر متعة. تذكّر أنه لديك مجموعة من العناصر التي ستتكرر في معظم -إن لم يكن في كل- الصفحات مثل قوائم التنقل والمحتوى الموجود في تذييل الصفحة، فمن الجيد مثلًا -إذا كان موقعك لإدارة عمل ما- وضع معلومات للتواصل في تذييل جميع الصفحات، ولهذا ينبغي عليك تدوين ما الذي تريده أن يكون مشتركًا في صفحاتك. ارسم لوحة بسيطة لما تتوقع أن يكون عليه هيكل الصفحة (فقد تريدها أن تبدو مماثلة للموقع النموذجي الذي عرضناه في الأعلى). دوّن ما سيكون عليه كل جزء. فكِّر الآن بما تريده من المحتوى غير المشترك بين الصفحات لتضعه في موقعك، ودوِّن ذلك في قائمة. حاول تصنيف عناصر المحتوى الذي تختاره في مجموعات لكي تتشكل لديك أفكار عن المجموعات التي يمكن أن تقع في الصفحات نفسها. حاول بعد ذلك وضع خريطة للموقع على هيئة فقاعات لكل صفحة ثم صل بخطوط بينها لتوضيح طبيعة التواصل بينها، إذ ستكون الصفحة الرئيسية في المنتصف وسترتبط بمعظم الصفحات، وينبغي أن ترتبط كل الصفحات في المواقع البسيطة بالصفحة الرئيسية مع وجود بعض الاستثناءات، كما عليك أيضًا تدوين بعض الملاحظات عن كيفية عرض الأشياء، وهذا أمر مشابه لتقنية تُدعى تصنيف البطاقات. تطبيق: أنشئ خريطة موقع خاصة بك حاول إنشاء خريطة لموقعك، ما الذي تريده من موقعك؟ ملاحظة: احفظ عملك في مكان مناسب، فقد تحتاج إليه لاحقًا. خلاصة من المفترض أنك كوَّنت صورةً واضحةً عن هيكلة صفحة أو موقع ويب، وسنناقش في المقال التالي كيف ننقح الأخطاء في HTML. ترجمة -وبتصرُّف- للمقال Document and website structure. اقرأ أيضا تعلم لغة HTML هيكلة وتوزيع محتوى صفحات الويب تصميم هيكل صفحة الويب والعناصر الأساسية في مجال تجربة المستخدم الفرق بين صفحة الويب وموقع الويب وخادم الويب ومحرك البحث
  10. نلقي نظرةً في هذا المقال على الطرق المتبعة في نقل التغييرات التي تجري أثناء جلسة العمل إلى قاعدة البيانات العلاقية وتخزينها، إضافةً إلى ربط الجداول ببعضها من خلال المفاتيح الخارجية foreign keys. ملفات التهجير Migrations لنتابع في توسعة تطبيق الملاحظات الذي عملنا عليه في كل من مقال العمل مع قواعد بيانات علاقية باستخدام Sequelize والمقال السابق، إذ سننجز آليةً تمكّن بعض المستخدمين الذي يمتلكون ميزةً إدارية admin status من إيقاف نشاط مستخدمين آخرين ومنعهم من تسجيل الدخول أو إنشاء ملاحظات جديدة. نحتاج لإنجاز الأمر إلى حقل يأخذ قيمًا منطقيةً في جدول قاعدة البيانات يشير إلى المستخدمين ذوي الامتيازات الإدارية وأخرى تدل على حالة المستخدم إن كان نشاطه معلقًا أم لا. يمكننا العمل وفق الآلية السابقة بتعديل النموذج الذي يعرّف الجدول والاعتماد على مكتبة Sequelize في مزامنة التغييرات مع قاعدة البيانات من خلال أسطر الشيفرة التالية في الملف "models/index.js": const Note = require('./note') const User = require('./user') Note.belongsTo(User) User.hasMany(Note) Note.sync({ alter: true }) User.sync({ alter: true }) module.exports = { Note, User } ولكن يُعد هذا الأسلوب (تغيير النموذج كلما أردنا توسعة التطبيق) غير منطقي على المدى الطويل، لهذا سنزيل الأسطر التي تدعم مزامنة التغييرات ونستخدم أسلوبًا أكثر قوةً وهو ملفات التهجير migrations التي تقدمها Sequelize وغيرها من المكتبات. ملف التهجير migration عمليًا هو ملف جافاسكربت JavaScript وحيد يصف التعديلات التي تطرأ على قاعدة البيانات، وينشأ عن تغير وحيد أو مجموعة تغيرات معًا. تحتفظ Sequelize بسجلات عن التهجيرات التي أجريت، أي التغيرات التي طرأت وجرى نقلها ومزامنتها مع قاعدة البيانات، إذ تبقى Sequelize مطلّعةً على التغيرات التي طرأت على مخطط قاعدة البيانات ولم تُطبّق بعد عند إنشاء تهجير جديد، ويمكن بهذه الطريقة التحكم بطريقة تطبيق التغييرات من خلال شيفرة البرنامج المخزنة في منظومة التحكم بالإصدار version control. لننشئ أولًا ملف تهجير يهيئ قاعدة البيانات. إليك شيفرة هذا الملف: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('notes', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE }, }) await queryInterface.createTable('users', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, }) await queryInterface.addColumn('notes', 'user_id', { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('notes') await queryInterface.dropTable('users') }, } يعرِّف الملف الدالتين up و down؛ إذ تحدّد الأولى كيفية تعديل قاعدة البيانات عندما يجري تهجير؛ وتحدد الثانية آلية التراجع إن كان هناك مبررٌ لذلك. يتضمن ملف التهجير ثلاثة خيارات؛ إذ يُنشئ الخيار الأول الجدول notes؛ بينما ينشئ الثاني الجدول users؛ ويضيف الخيار الثالث مفتاحًا خارجيًا إلى الجدول notes يشير إلى مُنشئ الملاحظة، وتُحدَّد التغيرات في المخطط باستدعاء توابع الكائن queryInterface. و على نقيض النماذج، من الضروري أن تتذكر دائمًا كتابة أسماء الأعمدة والجداول بأسلوب الأفعى snake case عند تعريف ملف تهجير: await queryInterface.addColumn('notes', 'user_id', { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }) أي ستكتب أسماء الجداول والأعمدة كما تظهر تمامًا في قاعدة البيانات، بينما تُكتب في النماذج بالأسلوب التقليدي للمكتبة Sequelize وذلك عبر استخدام أسلوب سنام الجمل camel Case. خزّن شيفرة ملف التهجير في ملف يحمل الاسم "migrations/20211209_00_initialize_notes_and_users.js". لا بُد أن تُسمى ملفات التهجير أبجديًا عند إنشائها كي تكون التغيرات القديمة قبل الجديدة، وقد تبدأ اسم الملف بتاريخ وتسلسل هذا الملف وهذه طريقةٌ جيدة. يمكننا تشغيل ملف التهجير انطلاقًا من سطر الأوامر باستخدام أداة سطر الأوامر الخاصة بالمكتبة Sequelize، لكننا قررنا تنفيذ التهجيرات يديويًا انطلاقًا من شيفرة البرنامج باستخدام المكتبة Umzug، لهذا سنثبت هذه المكتبة: npm install umzug لنعدّل الملف "util/db.js" الذي يتكفل بمعالجة الاتصال مع قاعدة البيانات: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const { Umzug, SequelizeStorage } = require('umzug') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const runMigrations = async () => { const migrator = new Umzug({ migrations: { glob: 'migrations/*.js', }, storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }), context: sequelize.getQueryInterface(), logger: console, }) const migrations = await migrator.up() console.log('Migrations up to date', { files: migrations.map((mig) => mig.name), }) } const connectToDatabase = async () => { try { await sequelize.authenticate() // highlight-start await runMigrations() // highlight-end console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') console.log(err) return process.exit(1) } return null } module.exports = { connectToDatabase, sequelize } تُنفّذ الدالة runMigrations ملف التهجير في كل مرة يؤسِّس فيها التطبيق اتصالًا مع قاعدة بيانات عند تشغيله، كما تراقب مكتبة Sequelize التهجيرات المُكتملة، فإذا لم يكن هناك تهجيرات جديدة، فلن ينفع تشغيل الدالة runMigrations في أي شيء. لنبدأ كل شيء من جديد، ونزيل كل الجداول الموجودة من التطبيق: username => drop table notes; username => drop table users; username => \d Did not find any relations. عند تشغيل التطبيق، تظهر رسالةً على الطرفية تحيطك علمًا بوضع التهجيرات: INSERT INTO "migrations" ("name") VALUES ($1) RETURNING "name"; Migrations up to date { files: [ '20211209_00_initialize_notes_and_users.js' ] } database connected إذا أعدنا تشغيل التطبيق، سيُظهر السجل أن التهجير لم يتكرر، وسيبدو مخطط قاعدة بيانات التطبيق على النحو التالي: username=> \d List of relations Schema | Name | Type | Owner --------+--------------+----------+---------------- public | migrations | table | username public | notes | table | username public | notes_id_seq | sequence | username public | users | table | username public | users_id_seq | sequence | username وهكذا نرى أن Sequelize قد أنشأت جدولًا للتهجيرات يسمح بتتبع ما نُفِّذ منها، ويبدو هذا الجدول على النحو التالي: username=> select * from migrations; name ------------------------------------------- 20211209_00_initialize_notes_and_users.js دعونا نضيف الآن بعض المستخدمين إلى قاعدة البيانات، إضافةً إلى مجموعة من الملاحظات، وبعدها نكون جاهزين لتوسعة التطبيق. يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-6. مستخدم بصلاحيات مدير وتعطيل مستخدم آخر علينا بدايةً إضافة حقلين يقبلان قيمًا منطقية إلى الجدول users، هما: admin: ويحدد فيما لو كان المستخدم مديرًا أم لا. disabled: ويحدد فيما لو أُوقف نشاط هذا المستخدم أم لا. لننشئ أيضًا ملف تهجير يُعدّل قاعدة بيانات في الملف "migrations/20211209_01_admin_and_disabled_to_users.js": const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.addColumn('users', 'admin', { type: DataTypes.BOOLEAN, default: false }) await queryInterface.addColumn('users', 'disabled', { type: DataTypes.BOOLEAN, default: false }) }, down: async ({ context: queryInterface }) => { await queryInterface.removeColumn('users', 'admin') await queryInterface.removeColumn('users', 'disabled') }, } عدّل النموذج بما يتوافق مع الجدول "users": User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, admin: { type: DataTypes.BOOLEAN, defaultValue: false }, disabled: { type: DataTypes.BOOLEAN, defaultValue: false }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user' }) يتغير المخطط كما نريد، عندما تُنفَّذ شيفرة ملف التهجير عند إعادة تشغيل التطبيق: username-> \d users Table "public.users" Column | Type | Collation | Nullable | Default ----------+------------------------+-----------+----------+----------------------------------- id | integer | | not null | nextval('users_id_seq'::regclass) username | character varying(255) | | not null | name | character varying(255) | | not null | admin | boolean | | | disabled | boolean | | | Indexes: "users_pkey" PRIMARY KEY, btree (id) "users_username_key" UNIQUE CONSTRAINT, btree (username) Referenced by: TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) لنوسّع الآن المتحكمات على النحو التالي، بحيث نمنع المستخدم من تسجيل الدخول في حال كانت قيمة الحقل disabled هي true: loginRouter.post('/', async (request, response) => { const body = request.body const user = await User.findOne({ where: { username: body.username } }) const passwordCorrect = body.password === 'secret' if (!(user && passwordCorrect)) { return response.status(401).json({ error: 'invalid username or password' }) } if (user.disabled) { return response.status(401).json({ error: 'account disabled, please contact admin' }) } const userForToken = { username: user.username, id: user.id, } const token = jwt.sign(userForToken, process.env.SECRET) response .status(200) .send({ token, username: user.username, name: user.name }) }) دعونا نوقف نشاط المستخدم "jakousa" بالاستعانة بمعرّفه الفريد ID: username => update users set disabled=true where id=3; UPDATE 1 username => update users set admin=true where id=1; UPDATE 1 username => select * from users; id | username | name | admin | disabled ----+----------+------------------+-------+---------- 2 | lynx | Kalle Ilves | | 3 | jakousa | Jami Kousa | f | t 1 | mluukkai | Matti Luukkainen | t | تأكد من نجاح الأمر بفشل تسجيل دخوله: لننشئ تاليًا وجهةً تسمح للمدير بتغيير حالة حساب مستخدم آخر: const isAdmin = async (req, res, next) => { const user = await User.findByPk(req.decodedToken.id) if (!user.admin) { return res.status(401).json({ error: 'operation not allowed' }) } next() } router.put('/:username', tokenExtractor, isAdmin, async (req, res) => { const user = await User.findOne({ where: { username: req.params.username } }) if (user) { user.disabled = req.body.disabled await user.save() res.json(user) } else { res.status(404).end() } }) تُستخدم في هذه الشيفرة أداتان وسطيتان، تُدعى الأولى tokenExtractor وتماثل الأداة التي استخدمناها لإنشاء الوجهة route الخاصة بإنشاء ملاحظة، إذ تضع مفتاح الاستيثاق الذي فُكّ تشفيره في الحقل decodedToken من كائن الطلب؛ بينما تتحقق الأداة الثانية isAdmin فيما لوكان المستخدم ذا صلاحيات إدارية، فإن لم يكن كذلك يُضبط رمز حالة الطلب على القيمة 401 وتُعاد رسالة خطأ مناسبة. لاحظ كيف رُبطت الأداتين إلى الوجهة، إذ تُنفَّذ كلٌ منهما قبل تنفيذ معالج الوجهة الفعلي، ويمكنك ربط العدد الذي تريده من الأدوات الوسيطة إلى طلبٍ ما. تُنقل الأداة الوسطية tokenExtractor إلى الملف "util/middleware.js" كونها تُستخدم في عدّة مواضع: const jwt = require('jsonwebtoken') const { SECRET } = require('./config.js') const tokenExtractor = (req, res, next) => { const authorization = req.get('authorization') if (authorization && authorization.toLowerCase().startsWith('bearer ')) { try { req.decodedToken = jwt.verify(authorization.substring(7), SECRET) } catch{ return res.status(401).json({ error: 'token invalid' }) } } else { return res.status(401).json({ error: 'token missing' }) } next() } module.exports = { tokenExtractor } يمكن للمدير الآن السماح للمستخدم "jakousa" بمعاودة نشاطه عن طريق إرسال طلب من النوع PUT إلى الوجهة "api/users/jakousa/" إذ يأتي الطلب مع البيانات التالية: { "disabled": false } وكما رأينا في نهاية القسم 4، تحمل هذه الطريقة في إيقاف نشاط المستخدمين بعض المشاكل، إذ يجري التحقق من وضع المستخدم عند تسجيل الدخول، وإن امتلك المستخدم مفتاح استيثاق صحيح في لحظة إيقاف نشاطه، فقد يتمكن من متابعة استخدام مفتاحه لعدم وجود فترة زمنية لصلاحيته، ولن يجري التحقق من حالته عند إضافة ملاحظة جديدة. قبل أن نتابع، سنكتب سكربت npm يسمح لنا بالتراجع عن آخر تهجير، فقد لا ينجح كل شيء في المرة الأولى بالضرورة عند تطوير ملفات التهجير. لنعدّل الملف "util/db.js" على النحو التالي: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const { Umzug, SequelizeStorage } = require('umzug') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const connectToDatabase = async () => { try { await sequelize.authenticate() await runMigrations() console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') return process.exit(1) } return null } const migrationConf = { migrations: { glob: 'migrations/*.js', }, storage: new SequelizeStorage({ sequelize, tableName: 'migrations' }), context: sequelize.getQueryInterface(), logger: console, } const runMigrations = async () => { const migrator = new Umzug(migrationConf) const migrations = await migrator.up() console.log('Migrations up to date', { files: migrations.map((mig) => mig.name), }) } const rollbackMigration = async () => { await sequelize.authenticate() const migrator = new Umzug(migrationConf) await migrator.down() } module.exports = { connectToDatabase, sequelize, rollbackMigration } // highlight-line لننشئ الملف "util/rollback.js" الذي يتيح لسكربت npm تنفيذ دالة التراجع عن التهجير: const { rollbackMigration } = require('./db') rollbackMigration() وسيكون السكربت على النحو التالي: { "scripts": { "dev": "nodemon index.js", "migration:down": "node util/rollback.js" }, } وهكذا سنتمكن من التراجع عن آخر تهجير بتنفيذ الأمر npm run migration:down من خلال سطر الأوامر. تُنفّذ ملفات التهجير تلقائيًا عندما يُشغَّل البرنامج، لكن قد يكون من الأنسب في مرحلة التطوير تعطيل التنفيذ التلقائي للتهجير وتنفيذه يدويًا من خلال سطر الأوامر. يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-7. التمرينان 13.7 و 13.8 حاول إنجاز التمارين التالية: التمرين 13.7 احذف كل الجداول من قاعدة بيانات تطبيقك ثم أنشئ ملف تهجير ليهيئ قاعدة البيانات. أضف البصمتان الزمنيتان "created_at" و "updated_at" إلى كلا الجدولين، وتذكر أن عليك إضافتها في ملف التهجير بنفسك. ملاحظة: تأكد من إزالة التعليمتين ()User.sync و ()Blog.sync اللتين تزامنان مخطط النموذج من شيفرتك وإلا سيخفق التهجير. ملاحظة: إذا كان عليك حذف الجداول باستخدام سطر الأوامر، أي إذا لم تكن تنوي التراجع عن الحذف بالتراجع عن آخر عملية تهجير، فلا بُد حينها من حذف محتوى جدول التهجير migrations إذا أردت من برنامجك تنفيذ التهجير مجددًا. التمرين 13.18 وسّع تطبيقك (مستخدمًا ملف تهجير) لكي يكون للمدونة سمةً تدل على سنة كتابتها، أي حقلٌ بالاسم يأخذ قيمًا صحيحة تبدأ من 1991 ولا يزيد عن العام الحالي. تأكد من ظهور رسالة خطأ مناسبة إن أُدخلت قيمة غير صحيحة للسنة. علاقات متعدد-إلى-متعدد many-to-many بين الجداول سنتابع توسعة التطبيق كي يُضاف كل مستخدم إلى فريقٍ أو أكثر، وطالما يمكن لأي عدد من المستخدمين الانضمام إلى فريق وكذلك يمكن لأي مستخدم الانضمام إلى أي عدد من الفرقاء، فإننا أمام علاقة متعدد-إلى-متعدد many-to-many والتي تنُفّذ تقليديًا في قواعد البيانات من خلال جدول الاتصال connection table. لنكتب الآن الشيفرة التي يحتاجها الجدول teams إضافةً إلى جدول الاتصال. سيبدو ملف التهجير أولًا على النحو التالي: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('teams', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }) await queryInterface.createTable('memberships', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('teams') await queryInterface.dropTable('memberships') }, } تتضمن النماذج نفس شيفرة ملفات التهجير تقريبًا، وإليك شيفرة نموذج الجدول team في الملف "models/team.js": const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Team extends Model {} Team.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }, { sequelize, underscored: true, timestamps: false, modelName: 'team' }) module.exports = Team وهذه هي شيفرة نموذج جدول الاتصال الموجودة في الملف "models/membership.js": const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Membership extends Model {} Membership.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, }, { sequelize, underscored: true, timestamps: false, modelName: 'membership' }) module.exports = Membership منحنا جدول الاتصال اسمًا يصف طبيعة محتوياته membership، لكن قد لا تجد بالضرورة اسمًا يصف هذا الجدول، ولهذا يمكنك تسميته بأسماء الجداول التي يربطها تفصل بينها شرطة سفلية، مثل "user_teams" الذي يصلح أيضًا لحالتنا هذه. أجرينا إضافةً صغيرةً على الملف "models/index.js" لربط الفرقاء والمستخدمين على مستوى الشيفرة باستخدام التابع belongsToMany: const Note = require('./note') const User = require('./user') const Team = require('./team') const Membership = require('./membership') Note.belongsTo(User) User.hasMany(Note) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) module.exports = { Note, User, Team, Membership } لاحظ الفَرق بين ملف التهجير لجدول الاتصال ونموذجه عند تعريف مفتاح خارجي، إذ تُعرَّف الحقول باستخدام أسلوب الأفعى في ملف التهجير: await queryInterface.createTable('memberships', { // ... user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, team_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, } }) بينما تُعرَّف نفس الحقول في النموذج باستخدام أسلوب سنام الجمل: Membership.init({ // ... userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, teamId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'teams', key: 'id' }, }, // ... }) لننشئ الآن بعض الفُرقاء وبعض الأعضاء باستخدام الطرفية: insert into teams (name) values ('toska'); insert into teams (name) values ('mosa climbers'); insert into memberships (user_id, team_id) values (1, 1); insert into memberships (user_id, team_id) values (1, 2); insert into memberships (user_id, team_id) values (2, 1); insert into memberships (user_id, team_id) values (3, 2); تُضاف معلومات أعضاء الفرقاء بعد ذلك إلى الوجهة التي تعيد كل المستخدمين: router.get('/', async (req, res) => { const users = await User.findAll({ include: [ { model: Note, attributes: { exclude: ['userId'] } }, { model: Team, attributes: ['name', 'id'], } ] }) res.json(users) }) لاحظ أن الاستعلام الذي طُبع على شاشة الطرفية يجمع ثلاثة جداول. هذا الحل جيدٌ فعلًا، لكن ما تزال هنالك ثغرة، إذ تأتي نتيجة الاستعلام مع جميع سمات الصف المطلوب من جدول الاتصال، علمًا أننا لا نحتاجها كلها. وبقراءة توثيق Sequelize جيدًا ستجد الحل: router.get('/', async (req, res) => { const users = await User.findAll({ include: [ { model: Note, attributes: { exclude: ['userId'] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } } ] }) res.json(users) }) يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-8. فكرة عن خاصيات كائن نموذج Sequelize تظهر خاصيات نموذجنا من خلال الأسطر التالية: User.hasMany(Note) Note.belongsTo(User) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) يمكّن ذلك Sequelize من إنشاء استعلامات تستخلص مثلًا كل ملاحظات المستخدمين أو كل أعضاء فريق، وبفضل تلك التعريفات نستطيع أيضًا الوصول مباشرةً إلى ملاحظات مستخدم مثلًا من خلال الشيفرة؛ ففي الشيفرة التالية مثلًا نحاول البحث عن مستخدم معرّفه المميز id=1، ثم نطبع الملاحظات المرتبطة به: const user = await User.findByPk(1, { include: { model: Note } }) user.notes.forEach(note => { console.log(note.content) }) يربط التعريف: User.hasMany(Note) الخاصية notes إلى الكائن user الذي يمنح وصولًا إلى الملاحظات التي أنشأها المستخدم، ويربط التعريف: User.belongsToMany(Team, { through: Membership }) الخاصية teams إلى الكائن user، الذي يمكن استخدامه في الشيفرة: const user = await User.findByPk(1, { include: { model: team } }) user.teams.forEach(team => { console.log(team.name) }) لنفترض أننا نريد إعادة كائن JSON من وجهة route تقود إلى مستخدم واحد يتضمن اسم المستخدم وعدد الملاحظات التي أنشأها. يمكننا تجربة الطريقة التالية: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { include: { model: Note } } ) if (user) { user.note_count = user.notes.length delete user.notes res.json(user) } else { res.status(404).end() } }) حاولنا إضافة الحقل noteCount في الكائن الذي أعادته Sequelize وإزالة الحقل notes منه، لكن ما يحدث أن هذه الطريقة لن تنفع لأن الكائن الذي تعيده Sequelize ليس كائنًا عاديًا تعمل فيه الحقول الإضافية كما نريد؛ ولذلك فإن الحل الأفضل لحالتنا هو إنشاء كائن جديد كليًا بناءً على البيانات المستخلصة من قاعدة البيانات: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { include: { model: Note } } ) if (user) { res.json({ username: user.username, name: user.name, note_count: user.notes.length }) } else { res.status(404).end() } }) نظرة ثانية إلى العلاقات متعدد-إلى-متعدد many-to-many سنختبر علاقة متعدد-إلى-متعدد أخرى في التطبيق؛ إذ ترتبط كل ملاحظة بالمستخدم الذي أنشأها من خلال مفتاح خارجي، ونريد الآن أن يدعم التطبيق ربط الملاحظة بمستخدمين آخرين، وربط المستخدم بعدد غير محدد من الملاحظات التي أنشأها آخرون، والفكرة هنا أن هذه الملاحظات هي تلك التي أشار المستخدم على أنها تهمّه. لننشئ جدول الاتصال user_notes في هذه الحالة، وسيكون ملف التهجير بسيطًا: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('user_notes', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, user_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, note_id: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'notes', key: 'id' }, }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('user_notes') }, } كما لن تجد أفكارًا جديدةً أيضًا في شيفرة النموذج: const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class UserNotes extends Model {} UserNotes.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, }, noteId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'notes', key: 'id' }, }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user_notes' }) module.exports = UserNotes يضم الملف "models/index.js" بعض التغيرات ليصبح على النحو التالي: const Note = require('./note') const User = require('./user') const Team = require('./team') const Membership = require('./membership') const UserNotes = require('./user_notes') Note.belongsTo(User) User.hasMany(Note) User.belongsToMany(Team, { through: Membership }) Team.belongsToMany(User, { through: Membership }) User.belongsToMany(Note, { through: UserNotes, as: 'marked_notes' }) Note.belongsToMany(User, { through: UserNotes, as: 'users_marked' }) module.exports = { Note, User, Team, Membership, UserNotes } يُستخدم التعريف belongsToMany مجددًا، إذ يربط الآن المستخدمين إلى الملاحظات عن طريق النموذج UserNotes المتعلق بجدول الاتصال، لكننا سنستخدم هذه المرة اسمًا بديلًا alias للسمات المُنشأة باستخدام الكلمة المحجوزة as، إذ سيتداخل overlap الاسم الافتراضي ("notes" المستخدم) مع معناه السابق وهو الملاحظات المُضافة من قِبل المستخدم. سنوسِّع الوجهة التي تقود إلى مستخدم وحيد لتعيد الفُرقاء التي ينتمي إليها المستخدم، وملاحظاتهم، والملاحظات الأخرى التي حددها المستخدم: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: Note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } }, ] }) if (user) { res.json(user) } else { res.status(404).end() } }) لا بُد من استخدام الاسم البديل الذي عرّفناه من خلال السمة as خلال السياق. سننشئ بعض البيانات الاختبارية في قاعدة البيانات لاختبار الميزة: insert into user_notes (user_id, note_id) values (1, 4); insert into user_notes (user_id, note_id) values (1, 5); ستكون النتيجة النهائية على النحو التالي: لكن ماذا لو أردنا أن نضمّن معلومات تتعلق بمؤلف الملاحظة إلى الملاحظات التي يحددها مستخدم؟ يُنفَّذ الأمر بإضافة التعليمة include إلى الملاحظات المحدّدة من قبل المستخدم: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: Note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] }, include: { model: User, attributes: ['name'] } }, { model: Team, attributes: ['name', 'id'], through: { attributes: [] } }, ] }) if (user) { res.json(user) } else { res.status(404).end() } }) ها هي النتيجة النهائية كما نتوقع: يمكنك الحصول على شيفرة التطبيق بوضعه الحالي من المستودع المخصص له على GitHub ضمن الفرع part13-9. التمرينات 13.19 - 13.23 حاول إنجاز التمرينات التالية. التمرين 13.19 اِمنح المستخدمين القدرة على إضافة مدوّنات إلى قائمة قراءة reading list. وعند إضافتها إلى القائمة يجب أن تكون حالتها "غير مقروءة unreaded"، ويمكن لاحقًا تعليم المدوّنة على أنها "مقروءة". نفّذ فكرة قائمة القراءة مستخدمًا جدول اتصال، وعدّل قاعدة البيانات من خلال ملف تهجير. لا يهم في هذا التمرين إضافة مدونات إلى القائمة وعرضها بنجاح أكثر من استخدام فكرة الولوج المباشر إلى قاعدة البيانات. التمرين 13.20 أضف الآن طريقةً كي يدعم التطبيق قوائم القراءة. تُضاف المدوّنة إلى قائمة القراءة من خلال الطلب HTTP POST إلى الوجهة "api/readinglists/"، ويُرفق مع الطلب المدوّنة ومعرّف المستخدم: { "blogId": 10, "userId": 3 } عدّل الوجهة "GET /api/users/:id" لإعادة قائمة المدوّنات إضافة إلى معلومات المستخدم بالتنسيق التالي: { name: "Matti Luukkainen", username: "mluukkai@iki.fi", readings: [ { id: 3, url: "https://google.com", title: "Clean React", author: "Dan Abramov", likes: 34, year: null, }, { id: 4, url: "https://google.com", title: "Clean Code", author: "Bob Martin", likes: 5, year: null, } ] } حتى هذه اللحظة، لا حاجة لإظهار إن كانت المدوّنة مقروءةً أم لا. التمرين 13.21 عدّل الوجهة التي تصل إلى مستخدم وحيد لكي تعرض فيما إذا كانت كل مدوّنة في قائمة القراءة مقروءةً أم لا، إضافةً إلى المُعرَّف المميز "id" للصف المقابل في جدول الاتصال. يمكن عرض المعلومات وفق التنسيق الآتي مثلًا: { name: "Matti Luukkainen", username: "mluukkai@iki.fi", readings: [ { id: 3, url: "https://google.com", title: "Clean React", author: "Dan Abramov", likes: 34, year: null, readinglists: [ { read: false, id: 2 } ] }, { id: 4, url: "https://google.com", title: "Clean Code", author: "Bob Martin", likes: 5, year: null, readinglists: [ { read: false, id: 2 } ] } ] } التمرين 13.22 قدّم طريقةً يستطيع من خلالها التطبيق تعليم مدوّنة ضمن قائمة القراءة على أنها مقروءة، إذ يُنفَّذ الأمر بإجراء طلب PUT إلى الوجهة "api/readinglists/:id/" وإرساله مع القيمة: { "read": true } يمكن للمستخدم أن يعلّم مدوّنة على أنها مقروءةً إذا كانت فقط ضمن قائمة القراءة الخاصة به. يستوثق من المستخدم عادةً من خلال مفتاح الاستيثاق الذي يُرفق مع الطلب. التمرين 13.23 عدّل الوجهة التي تعيد معلومات مستخدم وحيد لكي يتحكم الطلب بالمدوّنة التي ينبغي إحضارها من قائمة القراءة: "GET /api/users/:id": يعيد كامل قائمة القراءة. "GET /api/users/:id?read=true": يعيد المدوّنات المقروءة. "GET /api/users/:id?read=false": يعيد المدوّنات غير المقروءة. ملاحظات عامة يمكننا عدّ حالة تطبيقنا الآن مقبولة، لكن لا بُدّ من إلقاء نظرةٍ على بعض الأفكار قبل أن نختم هذا القسم. إحضار البيانات الكسول lazy والمتلهف eager عندما ننشئ استعلامًا مستخدمين السمة include: User.findOne({ include: { model: note } }) يحدُث ما يُسمى الإحضار المُتلهِّف eager fetch للبيانات، إذ تُجلب جميع الصفوف في كل الجداول المرتبطة بالمستخدم بواسطة الاستعلام join بنفس الوقت في مثال الملاحظات التي يُنشئها مستخدم. هذا السلوك هو ما نحتاجه عادةً، لكن ستجد في المقابل حالات تحتاج فيها إلى ما يُدعى بالإحضار الكسول أو المحدود lazy fetch مثل البحث عن فُرقاء مرتبطةٍ بمستخدم إذا لزم الأمر. لنعدّل وجهة إحضار مستخدم واحد كي تُحضر الفُرقاء التي ينتمي إليها مستخدم إذا احتوى الاستعلام على المعامل teams: router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id, { attributes: { exclude: [''] } , include:[{ model: note, attributes: { exclude: ['userId'] } }, { model: Note, as: 'marked_notes', attributes: { exclude: ['userId']}, through: { attributes: [] }, include: { model: user, attributes: ['name'] } }, ] }) if (!user) { return res.status(404).end() } let teams = undefined if (req.query.teams) { teams = await user.getTeams({ attributes: ['name'], joinTableAttributes: [] }) } res.json({ ...user.toJSON(), teams }) }) وهكذا لن يحضر الاستعلام User.findByPk الفُرقاء، لكنها ستُجلب عند الحاجة باستخدام التابع user.getTeams الذي تولِّده Sequelize تلقائيًا لكائن النموذج. تولَّد Sequelize تلقائيًا توابع -get مماثلة وتوابع أخرى مفيدة عندما تٌعرّف ارتباطات associations بين الجداول على مستوى قاعدة بيانات. ميزات النموذج قد تصادفنا حالات لا نريد فيها معالجة كل أسطر جدول محدد افتراضيًا، إذ من الممكن مثلًا ألا نرغب في عرض المستخدمين الذين أوقفت نشاطاتهم في التطبيق، ويمكننا في هذه الحالة تعريف مجالات الرؤية الافتراضية للنموذج على النحو التالي: class User extends Model {} User.init({ // field definition }, { sequelize, underscored: true, timestamps: false, modelName: 'user', defaultScope: { where: { disabled: false } }, scopes: { admin: { where: { admin: true } }, disabled: { where: { disabled: true } } } }) module.exports = User سيضم الاستعلام الناتج عن التابع ()User.findAll عبارة WHERE التالية: WHERE "user". "disabled" = false; يمكن أن نعرّف أيضًا مجالات رؤية أخرى للنماذج: User.init({ // field definition }, { sequelize, underscored: true, timestamps: false, modelName: 'user', defaultScope: { where: { disabled: false } }, scopes: { admin: { where: { admin: true } }, disabled: { where: { disabled: true } }, name(value) { return { where: { name: { [Op.iLike]: value } } } }, } }) تُستخدم مجالات الرؤية على النحو التالي: // جميع المدراء const adminUsers = await User.scope('admin').findAll() // جميع المستخدمين غير النشطين const disabledUsers = await User.scope('disabled').findAll() // في أسمائهم jami المستخدمون الذين لديهم سلسلة نصية const jamiUsers = User.scope({ method: ['name', '%jami%'] }).findAll() كما يمكن سلسلة مجالات الرؤية (ربطها ببعضها): // في أسمائهم jami المدراء الذين لديهم سلسلة نصية const jamiUsers = User.scope('admin', { method: ['name', '%jami%'] }).findAll() وطالما أن نماذج هي أصناف جافا سكربت JavaScript، من الممكن إضافة توابع جديدة إليها، وإليك مثالين عن ذلك: const { Model, DataTypes, Op } = require('sequelize') const Note = require('./note') const { sequelize } = require('../util/db') class User extends Model { async number_of_notes() { return (await this.getNotes()).length } static async with_notes(limit){ return await User.findAll({ attributes: { include: [[ sequelize.fn("COUNT", sequelize.col("notes.id")), "note_count" ]] }, include: [ { model: Note, attributes: [] }, ], group: ['user.id'], having: sequelize.literal(`COUNT(notes.id) > ${limit}`) }) } } User.init({ // ... }) module.exports = User التابع الأول numberOfNotes هو تابع نسخة instance method، أي أن استدعاءه ممكن من نسخٍ instances عن النموذج: const jami = await User.findOne({ name: 'Jami Kousa'}) const cnt = await jami.number_of_notes() console.log(`Jami has created ${cnt} notes`) تشير الكلمة this في تابع النسخة إلى نسخة النموذج نفسها: async number_of_notes() { return (await this.getNotes()).length } أما التابع الثاني للنموذج، فيعيد هؤلاء المستخدمين الذين يملكون على الأقل "X" وهي القيمة التي يحملها المعامل وتدل على كمية الملاحظات في الصنف، أي التي تُستدعى مباشرةً عن طريق النموذج: const users = await User.with_notes(2) console.log(JSON.stringify(users, null, 2)) users.forEach(u => { console.log(u.name) }) قابلية التكرار في النماذج وملفات التهجير لقد رأينا أن الشيفرة في النموذج أو ملف التهجير تتكرر كثيرًا، فلو أخذنا نموذج الفُرقاء teams: class Team extends Model {} Team.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }, { sequelize, underscored: true, timestamps: false, modelName: 'team' }) module.exports = Team وملف التهجير فإنهما يضمان كمًّا كبيرًا من نفس الشيفرة: const { DataTypes } = require('sequelize') module.exports = { up: async ({ context: queryInterface }) => { await queryInterface.createTable('teams', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, name: { type: DataTypes.TEXT, allowNull: false, unique: true }, }) }, down: async ({ context: queryInterface }) => { await queryInterface.dropTable('teams') }, } هل يمكن تحسين الشيفرة كي يُصدّر النموذج مثلًا الأجزاء المشتركة التي يحتاجها ملف التهجير؟ تكمن المشكلة في احتمال تغيُّر تعريف النموذج مع الوقت، فقد يتغير مثلًا الحقل أو يتغير نوع البيانات المُخزّنة فيه، وعلى ملف التهجير أن يُنفَّذ بنجاح في أي وقت من البداية إلى النهاية. إذا اعتمَدت ملفات التهجير على النموذج في الحصول على محتوى معين، فقد لا يكون هذا متاحًا خلال شهر أو سنة؛ لهذا ورغم وجود الكثير من الشيفرة للنسخ واللصق، لكن يُعد فصل ملف التهجير عن النموذج كاملًا أمرًا ضروريًا. قد يكون أحد الحلول هو استخدام أداة سطر أوامر Sequelize الذي يولّد كلًا من النموذج وملف التهجير بناءً على الأوامر التي تُنفِّذها، إذ سيُنفِّذ الأمر التالي النموذج User الذي يمتلك السمات name و username و admin، إضافةً إلى ملف التهجير الذي يدير شؤون إنشاء جدول قاعدة البيانات: npx sequelize-cli model:generate --name User --attributes name:string,username:string,admin:boolean يمكننا أيضًا تنفيذ أمر التراجع عن التهجيرات انطلاقًا من سطر الأوامر. لم يكتمل بعد لسوء الحظ توثيق سطر الأوامر هذا، لهذا قررنا إنشاء النماذج وملفات التهجير يدويًا في المنهاج، وقد يكون أو لايكون ما أنجزناه من حلول جيدًا. التمرين 13.24 نهاية عظيمة: أشرنا في نهاية القسم 4 إلى مشكلة جدّية في مفتاح الاستيثاق، فلو قررنا إيقاف نشاط مستخدم بعد أن دخل إلى المنظومة، سيبقى هذا المستخدم قادرًا على استخدام مفتاح الاستيثاق الذي يمتلكه وبالتالي استخدام المنظومة. الحل الإعتيادي للمشكلة هو تخزين سجلات بكل مفتاح استيثاق مُنح إلى عميل في قاعدة بيانات الواجهة الخلفية، ثم التحقق من صلاحية كل طلب، ويمكن في هذه الحالة إزالة صلاحية هذا المفتاح مباشرةً عند الحاجة. يُشار إلى هذا الأسلوب عادةً بجلسة عمل على الخادم server-side session. لنوسّع الآن المنظومة كي يُمنع المستخدم الذي فقد قدرته على الوصول إليها من إجراء أي تفاعل يتطلب تسجيل دخول. قد تحتاج إلى ما يلي لإنجاز الأمر: عمودٌ يضم قيمًا منطقية في جدول المستخدمين يشير إلى كون المستخدم نشطًا أم لا. يكفي في تمريننا أن توقف نشاط مستخدم أو تعيده من خلال قاعدة البيانات مباشرةً. جدولٌ يُخزّن جلسات العمل الجارية تُخزَّن الجلسة عند تسجيل الدخول (عند تنفيذ الطلب "POST /api/login"). يجري التحقق من وجود جلسة أو صلاحيتها عندما يُنفِّذ المستخدم عمليةً تتطلب تسجيل دخول. وجهةٌ تسمح للمستخدم بتسجيل خروجه من المنظومة لإزالة الجلسة من قاعدة البيانات، وقد يكون للوجهة المسار التالي "DELETE /api/logout". تذكر أنه لا يسمح بنجاح أي عملية تتطلب تسجيل دخول إذا كان مفتاح الاستيثاق منتهي الصلاحية، مثل الحالة التي يسجل فيها المستخدم خروجه. قد ترغب أيضًا في استخدام مكتبة npm مخصصة للتعامل مع الجلسات، ولا تنسى استخدام ملف التهجير لتنفيذ التغييرات اللازمة على قاعدة البيانات. ترجمة -وبتصرف- للفصل migrations, many-to-many relationships من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: ضم الجداول والاستعلامات المشتركة في قواعد البيانات باستخدام Sequelize المفاهيم الأساسية في قواعد البيانات وتصميمها تهجير قواعد البيانات في Laravel 5 تجريد إعداد قواعد البيانات في لارافيل باستعمال عملية التهجير Migration والبذر Seeder
  11. تجد في HTML الكثير من العناصر المستخدَمة في تنسيق النصوص ولم نأت على ذكرها في مقال هيكلة النصوص باستخدام لغة HTML، فليست جميع العناصر التي سنذكرها في هذا المقال معروفةً جيدًا، لكن من الجيد الاطلاع عليها (مع ذلك لن تكتمل قائمة العناصر كلها!)، إذ سنتعلم في مقالنا التعامل مع الاقتباسات وقوائم الوصف وطريقة عرض الشيفرات البرمجية والنصوص المتعلقة بها بالإضافة إلى كتابة نصوص مرتفعة عن نسق الكتابة الرئيسي superscript أو منخفضة عنه subscript، وكذلك عرض معلومات التواصل وغيرها. لا بد قبل الشروع في قراءة المقال الاطلاع على أساسيات HTML كما تحدثنا عنها في مقال تعرَّف على لغة HTML وكذلك هيكلة النصوص باستخدام لغة HTML. قوائم الوصف Description lists تعلمنا في مقالات سابقة طريقة إنشاء قوائم HTML بسيطة، لكننا لم نذكر النوع الثالث الذي قد نصادفه أحيانًا وهي قوائم الوصف description lists، إذ يُعَدّ الغرض الرئيسي لهذه القوائم هو تعداد مجموعة من العناصر وإدراج وصف لها أيضًا مثل المصطلحات وتعريفاتها أو الأسئلة وأجوبتها، وإليك مثالًا عن مجموعة من المصطلحات وتعريفها: soliloquy In drama, where a character speaks to themselves, representing their inner thoughts or feelings and in the process relaying them to the audience (but not to other characters.) monologue In drama, where a character speaks their thoughts out loud to share them with the audience and any other characters present. aside In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought or piece of additional background information تُغلَّف عناصر هذه القائمة داخل عنصر قوائم مختلف وهو <dl>، كما يُغلَّف كل من عناصرها بالعنصر <dt>، في حين يوضَع وصف أو تعريف كل عنصر ضمن العنصر <dd> بعده مباشرةً. مثال عن قائمة وصف سنستخدِم قائمة الوصف لتنسيق النص السابق: <dl> <dt>soliloquy</dt> <dd>In drama, where a character speaks to themselves, representing their inner thoughts or feelings and in the process relaying them to the audience (but not to other characters.)</dd> <dt>monologue</dt> <dd>In drama, where a character speaks their thoughts out loud to share them with the audience and any other characters present.</dd> <dt>aside</dt> <dd>In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought, or piece of additional background information.</dd> </dl> يعرض المتصفح قائمة الوصف افتراضيًا بحيث ينزاح فيه وصف أو تعريف العنصر قليلًا عن مستوى المصطلح. تعريفات متعددة لمصطلح واحد يُسمح في قوائم الوصف وجود أكثر من وصف أو تعريف لمصطلح، وإليك مثالًا: <dl> <dt>aside</dt> <dd>In drama, where a character shares a comment only with the audience for humorous or dramatic effect. This is usually a feeling, thought, or piece of additional background information.</dd> <dd>In writing, a section of content that is related to the current topic, but doesn't fit directly into the main flow of content so is presented nearby (often in a box off to the side.)</dd> </dl> تطبيق: توصيف مجموعة من التعريفات حان الوقت لتجرّب بنفسك قوائم الوصف، لذلك أضف العناصر المناسبة إلى النص الموجود في حقل مدخلات محرر الشيفرة في الأسفل لكي يظهر على صورة قائمة وصف ضمن حقل المخرجات output field، كما يمكنك تجريب مصطلحات وتعريفات خاصة بك أيضًا. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. الاقتباسات Quotations تتيح لغة HTML عناصر للدلالة على الاقتباسات quotations، ويختلف العنصر المُستخدَم لإنشاء اقتباس إذا كنت توصِّف اقتباسًا مأخوذًا من عنصر كتلي Block أو سطري Inline. الاقتباسات الكتلية إذا اقتُبس جزء من محتوى عنصر كتلي مثل فقرة نصية أو عدة فقرات أو قائمة من مكان ما، فلا بد من تضمينه داخل العنصر <blockquote> لإظهاره على أساس فقرة وإضافة عنوان URL إلى مصدر الاقتباس بواسطة السمة cite، ويعرض المثال التالي محتوًى مأخوذًا من صفحة العنصر <blockquote> على شبكة مطورِي موزيللا MDN: <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> علينا كتابة ما يلي لتحويل هذا المحتوى إلى اقتباس عن عنصر كتلي: <p>Here below is a blockquote...</p> <blockquote cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"> <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> </blockquote> سيصيّر المتصفح هذا الاقتباس افتراضيًا على هيئة فقرة نصية تنزاح قليلًا عن بداية السطر ليدل على أنها اقتباس، وقد وضعنا الفقرة النصية في الأعلى لتوضيح هذه الفكرة. الاقتباس السطري يعمل بطريقة مماثلة للاقتباس عن عنصر كتلي إلا أنه يستخدِم العنصر <q>، إذ يعرض المثال التالي محتوًى مأخوذًا من صفحة العنصر <q> على شبكة مطورِي موزيللا وحوّلناه إلى اقتباس سطري: <p>The quote element — <code><q></code> — is <q cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q">intended for short quotations that don't require paragraph breaks.</q></p> سيعرض المتصفح هذه الاقتباس على صورة نص عادي محاط بإشارتَي تنصيص مزدوجتين على السطر نفسه: الاقتباسات المرجعية Citations على الرغم من أنّ محتوى السمة cite يبدو مفيدًا إلّا أنّ المتصفحات وقارئات الشاشة لا يعيرانه اهتمامًا، إذ لا توجد طريقة مباشرة تطلب بها من المتصفح إظهار محتوى هذه السمة سوى بابتكار حلول خاصة بك من خلال استخدام جافاسكربت أو CSS، فإذا أردت إتاحة مصدر الاقتباس للعرض أو الاستخدام، فعليك إظهاره ضمن نص أو رابط أو أية طريقة ملائمة أخرى، كما يوجد أيضًا العنصر <cite>، إلا أنّ الغاية منه هو احتواء عنوان المصدر الذي أُخذ منه الاقتباس مثل اسم كتاب، لكن لا يوجد أبدًا ما يمنع ربط النص الموجود داخل هذا العنصر بمصدر الاقتباس بطريقة أو بأخرى: <p>According to the <a href="/en-US/docs/Web/HTML/Element/blockquote"> <cite>MDN blockquote page</cite></a>: </p> <blockquote cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/blockquote"> <p>The <strong>HTML <code><blockquote></code> Element</strong> (or <em>HTML Block Quotation Element</em>) indicates that the enclosed text is an extended quotation.</p> </blockquote> <p>The quote element — <code><q></code> — is <q cite="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/q">intended for short quotations that don't require paragraph breaks.</q> -- <a href="/en-US/docs/Web/HTML/Element/q"> <cite>MDN q page</cite></a>.</p> تُعرَض الاقتباسات المرجعية افتراضيًا بخط مائل. تطبيق: من صاحب القول؟ حان الوقت لتمرين آخر، انظر إلى النص الموجود في حقل مدخلات محرر الشيفرة في الأسفل وحاول: تحويل المقطع في المنتصف إلى اقتباس كتلي يتضمن السمة cite. تحويل الجملة "The Need To Eliminate Negative Self Talk" في المقطع الثالث إلى اقتباس سطري يتضمن السمة cite. تغليف عنوان كل مصدر داخل العنصر <cite> وتحويل العنوان إلى رابط، واستخدم المرجعين التاليين للربط: للاقتباس عن كونفوشيوس: http://www.brainyquote.com/quotes/authors/c/confucius.html. للجملة في المقطع الثالث: http://example.com/affirmationsforpositivethinking. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة الضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر أظهر الحل Show solution لترى الحل الصحيح. الاختصارات تُعَدّ الاختصارات Abbreviations عناصر شائعة الاستخدام نوعًا ما في ويب وتميزها بالوسم <abbr>، إذ تستخدَم للدلالة على اختصارات لمصطلحات أو نحوت من عدة كلمات، فالنحت هو الأخذ بأوائل الحروف في كلمات جملة، كما تعطي الاختصارات وصفًا للمصطلح عند تمرير مؤشر الفأرة فوقه عند استخدام السمة title. مثال عن الاختصارات لنلق نظرةً على المثال التالي: <p>We use <abbr title="Hypertext Markup Language">HTML</abbr> to structure our web documents.</p> <p>I think <abbr title="Reverend">Rev.</abbr> Green did it in the kitchen with the chainsaw.</p> ستكون النتيجة مشابهةً للتالي: ملاحظة: دعمَت النسخ الأولى من HTML العنصر <acronym>، لكنه حُذِف ليحل محله <abbr> الذي يُستخدَم للدلالة على الاختصارات والنحوت معًا. تطبيق: الدلالة على اختصار نطلب منك في هذا التمرين البسيط إنشاء اختصار، واستخدم العينة الموجودة في الأسفل أو استبدلها بما تشاء. الدلالة على معلومات التواصل يُستخدَم العنصر <address> في لغة HTML لإدراج معلومات التواصل مع مسؤول الصفحة أو الموقع، إليك مثالًا كما يلي: <address> Chris Mills, Manchester, The Grim North, UK </address> يمكن احتواء العنصر على عناصر أخرى أو نموذج للتواصل لإدراج معلومات أكثر، وإليك مثالًا كما يلي: <address> <p> Chris Mills<br> Manchester<br> The Grim North<br> UK </p> <ul> <li>Tel: 01234 567 890</li> <li>Email: me@grim-north.co.uk</li> </ul> </address> يمكنك تنظيم المعلومات ضمن العنصر <address> بالصورة التالية أيضًا: <address> Page written by <a href="../authors/chris-mills/">Chris Mills</a>. </address> ملاحظة: يجب ألا يستخدَم العنصر <address> سوى لإدراج معلومات التواصل في الصفحة أسفل أقرب عنصر مقال <article> أو أسفل جسم الصفحة <body>، ومن الصحيح أيضًا وضعه في تذييل الصفحة لتُعرض معلومات التواصل في جميع صفحات الموقع، أو داخل عنصر المقال لعرض معلومات التواصل مع مؤلفه، لكن لا تستخدِمه لإدراج قائمة من العناوين التي لا تتعلق بمحتوى الصفحة. إزاحة الكتابة أعلى النسق أو أسفله نحتاج أحيانًا إلى رفع بعض الاحرف إلى أعلى نسق الكتابة أو أسفلها عندما نتعامل مع تواريخ مثلًا أو صيغ المواد الكيميائية أو المعادلات الرياضية لكي تشير إلى المعنى الحقيقي، إذ يُستخدم العنصران <sup> و <sub> لإنجاز الأمر، وإليك مثالًا كما يلي: <p>My birthday is on the 25<sup>th</sup> of May 2001.</p> <p>Caffeine's chemical formula is C<sub>8</sub>H<sub>10</sub>N<sub>4</sub>O<sub>2</sub>.</p> <p>If x<sup>2</sup> is 9, x must equal 3 or -3.</p> سيعرِض المتصفح النتيجة على الصورة التالية: إدراج شيفرة برمجية تقدِّم HTML مجموعةً من العناصر لعرض الشيفرات البرمجية بطريقة مميزة: العنصر <code>: للدلالة على أن المحتوى هو مقتطفات من شيفرة برمجية. العنصر <pre>: الذي يُبقي على المسافات الفارغة الزائدة في المحتوى والتي قد تكون إزاحةً للشيفرة، إذ يتجاهل المتصفح المسافات الفارغة الزائدة في النصوص العادية، لكن عند تغليف النص داخل هذا العنصر، فسيعرضه المتصفح كما هو تمامًا دون إهمال أيّ شيء. العنصر <var>: ويستخدَم للدلالة على المتغيرات على وجه الخصوص. العنصر <kbd>: ويستخدَم للدلالة على مدخلات لوحة المفاتيح أو غيرها من المدخلات إلى الحاسوب. العنصر <samp>: ويستخدَم للدلالة على خرج برنامج حاسوبي. لنلق نظرةً على بعض الأمثلة، كما عليك التجريب بنفسك، ولهذا ننصح بتنزيل نسخة من الملف other-semantics.html. <pre><code>var para = document.querySelector('p'); para.onclick = function() { alert('Owww, stop poking me!'); }</code></pre> <p>You shouldn't use presentational elements like <code><font></code> and <code><center></code>.</p> <p>In the above JavaScript example, <var>para</var> represents a paragraph element.</p> <p>Select all the text with <kbd>Ctrl</kbd>/<kbd>Cmd</kbd> + <kbd>A</kbd>.</p> <pre>$ <kbd>ping mozilla.org</kbd> <samp>PING mozilla.org (63.245.215.20): 56 data bytes 64 bytes from 63.245.215.20: icmp_seq=0 ttl=40 time=158.233 ms</samp></pre> ستُعرض الشيفرة السابقة في المتصفح كما يلي: الدلالة على الوقت والتاريخ تقدِّم لغة HTML العنصر <time> لتوصيف الوقت والتاريخ بطريقة يمكن للآلة قراءتها، وإليك مثالًا كما يلي: <time datetime="2016-01-20">20 January 2016</time> ما الفائدة من هذا العنصر؟ يستخدِم البشر طرقًا مختلفةً لكتابة التاريخ، فقد يُكتب التاريخ في المثال السابق على الصورة: 20 January 2016 20th January 2016 Jan 20 2016 20/01/16 01/20/16 The 20th of next month 20e Janvier 2016 20 كانون الثاني 2016 وغير ذلك. لكن لا تستطيع الحواسب التمييز بسهولة بين طرق الكتابة تلك، فماذا إذا أردت الحصول تلقائيًا على تواريخ جميع المناسبات في صفحة ويب وإضافتها إلى التقويم الخاص بك؟ لهذا السبب، يُستخدَم العنصر <time> لإضافة تنسيق للوقت والتاريخ، بحيث يكون مفهومًا بالنسبة إلى الحواسيب وغيرها من الآلات، كما يعرض لك المثال السابق تاريخًا بسيطًا يمكن للآلة فهمه، لكن هناك خيارات عدة أخرى، وإليك بعض الأمثلة: <!-- تاريخ بسيط بصيغة معيارية --> <time datetime="2016-01-20">20 January 2016</time> <!--فقط السنة و الشهر--> <time datetime="2016-01">January 2016</time> <!-- فقط الشهر و اليوم --> <time datetime="01-20">20 January</time> <!-- الوقت فقط، ساعات ودقائق --> <time datetime="19:30">19:30</time> <!-- يمكنك إضافة الثواني وأجزاء الثواني --> <time datetime="19:30:01.856">19:30:01.856</time> <!-- الوقت والتاريخ --> <time datetime="2016-01-20T19:30">7.30pm, 20 January 2016</time> <!-- الوقت والتاريخ مع إزاحة زمنية حسب خطوط الزمن--> <time datetime="2016-01-20T19:30+01:00">7.30pm, 20 January 2016 is 8.30pm in France</time> <!-- استدعاء أسبوع محدَّد من السنة وفق رقمه --> <time datetime="2016-W04">The fourth week of 2016</time> خلاصة نكون بهذا قد أنهينا دراسة دلالة العناصر التي تنسِّق النصوص في HTML، وتذكَّر تمامًا أنّ العناصر التي تعرَّفت عليها في سلسلة المقالات لا تمثِّل القائمة كلها، فقد حاولنا تغطية الأساسية منها أو بعض العناصر التي يشيع استخدامها أو التي رأينا بأنك ستجدها مهمةً، ويمكنك العودة إلى توثيق لغة HTML باللغة العربية أو شبكة مطوري موزيللا للاطلاع على بقية العناصر، كما سنتحدث في المقال التالي عن عناصر لغة HTML التي نستخدِمها في هيكلة الأجزاء المختلفة من صفحة ويب. ترجمة -وبتصرف- للمقال Advanced text formatting. اقرأ أيضًا HTML و CSS للمبتدئين: مقدمة إلى تنسيقات CSS تنسيق نصوص صفحات الويب باستخدام CSS تنسيق النصوص وتحليلها في PHP كيفية تنسيق النصوص في بايثون 3
  12. يدعم محرر النصوص "مستندات جوجل Google Docs" ميزة مشاركة العمل على أكثر من جهة أو مساهم، مما يسمح بتوزيع حمل العمل على فريق بأكمله، بحيث يختص كل فرد فيه بناحية معينة مثل التحرير والتنقيح وإضافة المصادر والتنسيق والمراجعات العامة بأسلوب عصري مريح يعزز تكامل العمل للحصول على نتائج أفضل. كل ذلك ضمن بيئة عمل متطورة مدعومة بتقنيات سحابية تؤمن سهول الوصول والمشاركة مع مستوى عال من الأمان. سنتحدث في هذا المقال عن ميزة مشاركة مستندات جوجل بتفاصيلٍ وافيةٍ لتكون قادرًا على توزيع أية أعمال تتعلق بإعداد الملفات النصية بفعالية كبيرة وجهد أقل. ولتوضيح النقاط التي ذكرناها، سنعمل عبر مثال تطبيقي بسيط يتضمن إنشاء مستند جوجل دوكس، ثم العمل على مشاركته مع فريقك وتوضيح خيارات المشاركة وآليات العمل الجماعي عليه. إنشاء مستند جديد في مستندات جوجل ينبغي أن يكون لديك بدايةً حساب على منصة جوجل، ويُعَد هذا الحساب وثيقة دخولك إلى جميع الخدمات التي تقدمها المنصة. يرتبط حسابك على منصة جوجل ببريد إلكتروني تُنشئه تلقائيًا عند تسجيل حساب جديد، وعليك تذكره دائمًا مع كلمة السر التي اخترتها لتتمكن من الولوج إلى أي خدمة من خدمات جوجل (علمًا أن بعضها لا يتطلب تسجيل دخول). إن كنت تملك حسابًا، فهذا أمر جيد وإن لم يكن لديك حساب فيجب عليك التسجيل على حساب جديد حتى تتمكن من متابعة العمل معنا في هذا المقال. يمكن الدخول إلى جوجل دوكس من خلال موقع ويب جوجل دوكس مباشرةً، أو من خلال منصة جوجل درايف Google Drive. حاول إنشاء ملف جديد بالنقر على "أيقونة (+)" يمين أسفل الصفحة إن كنت تستخدم موقع مستندات جوجل، كما يمكنك النقر على قائمة "جديد" على يمين الشاشة، ثم النقر على خيار "مستندات Google" إن اخترت أن تعمل على درايف. ولمزيد من التفاصيل عن كيفية إنشاء مستند جوجل، عُد إلى المقال "مقدمة إلى تطبيق مستندات جوجل Google Docs". لنلق نظرةً الآن على واجهة تطبيق مستندات جوجل (واجهة الحاسوب)، التي تتيح لك إمكانية تحرير المستند الجديد الذي أنشأته: تُحفظ مستندات جوجل تلقائيًا بالاسم الافتراضي "بلا عنوان"، لذلك انقر على الخيار "ملف" من شريط القوائم أعلى الصفحة، وانتقل بعدها إلى الخيار "إعادة تسمية" وانقر عليه وستلاحظ وجود عنوان جديد مقترح قد حل مكان العنوان السابق. بإمكانك اعتماد الاسم المقترح أو كتابة العنوان الذي تريد، كما يمكنك النقر مباشرةً على العنوان الافتراضي "بلا عنوان" وكتابة العنوان الجديد للمستند. وبالنسبة لمثالنا التطبيقي، سنختار العنوان "نصائح عن تبادل الأفكار في مستندات جوجل"، وعندها سيُحفظ الملف تلقائيًا في المجلد "ملفاتي". مشاركة مستند جوجل لمشاركة المستند، انقر ببساطة على زر "مشاركة" على يسار الصفحة، وستظهر عندها النافذة التالية: انقر على مربع النص "إضافة أشخاص ومجموعات" أعلى النافذة لكتابة البريد الإلكتروني أو اسم الشخص الذي ترغب بمشاركته هذا الملف (إن كان ضمن قائمة معارفك)، ثم اختر الصفة التي تريده أن يشارك بها من خلال الصندوق الذي يظهر إلى جوار البريد الإلكتروني للشخص المدعو. بإمكانك دعوة أي شخص إلى المساهمة في العمل وفق صفات ثلاث: محرِّر: وتمنحه بذلك وصولًا كاملًا إلى الملف لقراءته وتعديله وترك التعليقات وإضافة مساهمين جدد وتغيير أذونات الوصول (افتراضيًا) وحفظ التغييرات التي أحدثها معلِّق: بإمكان الشخص المدعو الاطلاع على محتوى الملف وترك التعليقات التي يرغب بها دون القدرة على تحرير المحتوى أو تغييره، ويبقى قادرًا أيضًا على استخدام خيارات النسخ والطباعة (افتراضيًا). عارض: لكي تسمح للشخص المدعو أن يقرأ فقط محتوى المستند دون أي قدرة على التفاعل. إن أردت أن تبلغ الشخص بمشاركتك له في العمل على هذا المستند، فانقر مربع التحقق "إشعار الأشخاص"، ثم صُغ رسالة لتوضيح المطلوب منه، بعدها أنقر على الزر "إرسال" أسفل النافذة؛ أما إن لم ترغب بإشعاره بذلك (كأن تبلغه بطريقة أخرى)، فلا تفعّل الخيار "إشعار الأشخاص" وانقر على زر "مشاركة" أسفل النافذة. دعوة شخص لا يملك حساب جوجل: بإمكانك أن تكتب عنوان بريد إلكتروني لشخص لا يمتلك حسابًا على جوجل. وهنا سيبلغك التطبيق في هذه الحالة أن هذا البريد الإلكتروني لا يرتبط بحساب جوجل وأنك تمنح إذنًا لأي شخص يصله رابط المشاركة من خلال هذا البريد، بحيث يترك لك خيار إكمال عملية المشاركة أو إلغائها، إذ يُعَد ذلك ثغرةً أمنيةً بطريقة أو بأخرى. عند إكمال المشاركة تظهر نافذة منبثقة صغيرة أعلى التطبيق تبلغك أن إمكانية الوصول قد تغيّرت، وبإمكانك معرفة عدد الأشخاص الذي تشاركهم العمل من خلال تمرير مؤشر الفأرة فوق زر "مشاركة". انقر على زر المشاركة من جديد لتضيف مساهمًا جديدًا إلى الملف أو لتستعرض المساهمين الحاليين أو لإجراء أية تغييرات على إمكانية الوصول، وستبدو نافذة التحكم بالمشاركة الآن كالتالي: تُظهر النافذة وجود مالك للمستند، وهو بالطبع أنت، إذ يظهر بريدك الإلكتروني وإلى جواره الصفة "مالك"، يليه مساهمين اثنين حتى الآن، حيث يمتلك المساهم الأول البريد الإلكتروني "address@mysite.com" بصفة "معلِّق"، أما الآخر فيمتلك البريد الإلكتروني "address@website.com" بصفة "محرر". تمرين تطبيقي تأكد من قدرتك على مشاركة مستند "نصائح عن تبادل الأفكار في مستندات جوجل" مع ثلاث مساهمين: الأول محرر والثاني معلّق والثالث عارض، إذ ستحتاج ذلك لاحقًا. دعوة شخص تجهل بريده الإلكتروني للمساهمة في مستند جوجل ربما تجمعك علاقة عمل طيبة من زميل، لكنك تجهل في الواقع عنوان البريد الإلكتروني الذي يستخدمه. فإن أردت دعوته للمساهمة في العمل على هذا المستند، يمكنك بكل بساطة اتباع الخطوات التالية: انقر على زر "مشاركة". انقر على زر "نسخ الرابط" أسفل يسار نافذة المشاركة، وسيُخزّن رابط الوصول إلى الملف في الحافظة. أرسل الرابط إلى زميلك بنسخه في رسالة نصية أو عبر إحدى وسائل التواصل الاجتماعي أو بالطريقة التي تشاء. عندما يصل الرابط إلى الشخص المحدد ويحاول الدخول إلى المستند سيتعذر عليه ذلك وتظهر له صفحة تحثّه على طلب إذن منك للمساهمة في الملف. بمجرد أن يطلب إذنًا للوصول إلى مستندك، ستصلك رسالة بريد إلكتروني توضح رغبة هذا الشخص بالمساهمة في العمل على مستندك، وعندما تفتح مستندك ستظهر لك نافذة المشاركة وفيها عنوان البريد الإلكتروني للشخص الذي دعوته للمساهمة كي تحدد الصفة التي تريده أن يساهم بها، وهكذا تكتمل العملية. تعديل إمكانية الوصول إلى مستند جوجل تتيح لك نافذة المشاركة مجموعة من الخيارات التي تساعدك في التحكم بصفات المساهمين ومنح أو إزالة بعض المزايا عن المساهمين عمومًا. السماح بالوصول العام إلى مستند جوجل قد ترغب في نشر مستندك للقراءة أو التعليق أو حتى التعديل من قِبل أكبر شريحة من المهتمين (قد يبدو الأمر محفوفًا بالمخاطر لكنه مفيد في حالات معينة). في هذه الحالة ما عليك سوى نسخ الرابط ونشره عبر الإنترنت، لكن عليك قبل ذلك تغيير حالة المشاركة في قسم "الوصول العام" من "حصري" إلى "أي مستخدم لديه رابط"، ثم اختيار الصفة التي يمكن للمساهمين العامين الدخول بها. تُمنح في هذه الحالة نفس الصفة لجميع المساهمين العامين سواءً اخترتهم أن يكونوا محررين أو معلقين أو عارضين. تعديل صفة الوصول إلى مستند جوجل وإلغائها بإمكانك أن تغير صفة الوصول التي منحتها كمالك أو كمحرر لأحد المساهمين، كأن تمنحه صفة محرر في حال كان "معلّقًا" فقط أو "عارضًا"، والعكس بالعكس؛ بالإضافة إلى منع المساهم من الوصول مجددًا إلى الملف (إزالة الوصول). ولإنجاز الأمر عليك بالخطوات التالية: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على القائمة المنسدلة إلى جوار اسم المساهم واختر الصفة الجديدة له أو اختر "إزالة الوصول". انقر على الزر "حفظ" أسفل النافذة. نقل ملكية مستند جوجل يُعًد من أنشأ الملف أو رفعه مالكًا لهذا الملف ويُخزّن ضمن المساحة المخصصة للمالك على جوجل درايف، وعندما يُشارك هذا الملف، ستُرسل نسخة منه إلى المساهم وفق الصفة أو الإذن الممنوح له؛ لكن إن أردت لسبب ما نقل ملكية مستند أنشأته إلى أحد المساهمين، فهذا ممكن وإليك الطريقة: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على القائمة المنسدلة إلى جوار اسم المساهم واختر "نقل الملكية". تظهر لك رسالة مفادها أنك ترغب في إرسال دعوة إلى المساهم الذي اخترته ليكون مالكًا لهذا المستند، انقر عند ظهورها على الزر "إرسال الدعوة". بعد إرسال الدعوة ستبقى مالكًا للمستند حتى يصل الإشعار إلى المساهم المحدد ويقبل أن يكون المالك الجديد، عندها تفقد صفة المالك للمستند وتبقى محررًا حتى يغير المالك الجديد صفتك أو يمنعك من الوصول إلى المستند. التراجع عن نقل الملكية: طالما أن المساهم الذي نقلت ملكية الملف إليه لم يستجب لدعوة النقل إيجابًا أو سلبًا، تستطيع التراجع عن نقل الملكية بالنقر على اسم المساهم، ثم اختيار "إلغاء نقل الملكية". الوصول المحدود إلى مستندات جوجل المشاركة يتيح لك تطبيق مستندات جوجل بصفتك مالكًا للمستند إمكانية منع بعض الميزات الناتجة عن مشاركة الملف، وإليك بعضها: منع المساهمين من تنزيل أو نسخ أو طباعة مستند جوجل يمكن للمساهمين الذي يدخلون بصفة "محرر" أو "معلّق" أن ينزّلوا نسخةً من المستند على أجهزتهم أو يطبعوه أو ينسخوا أجزاء منه لأغراض شخصية، لكن بإمكانك منعهم من ذلك كما يلي: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على أيقونة الإعدادات في أعلى يسار النافذة لتظهر لك النافذة الجديدة التالية: الغِ تفعيل الخيار "تظهر للمشاركين والمعلقين خيارات التنزيل والطباعة والنسخ". انقر على زر "حفظ". هل هذا كافٍ؟ هذا ما يقدمه تطبيق مستندات جوجل رسميًا، علمًا أن المساهم قد يجد طرقًا كثيرةً للاستفادة من المحتوى وبأساليب مختلفة، لهذا اختر مساهميك بعناية إن كان الأمر مهمًا. منع المساهمين من مشاركة مستندات جوجل وتغيير الأذونات يمكن للمساهم الذي شاركته مستندك بصفة "محرر" أن يعيد مشاركة الملف مع آخرين أو يغيّر أذونات الوصول إليه. ولكي تحتفظ بهذه الميزة لك فقط، اتبع الخطوات التالية: انقر على زر "مشاركة" لتعرض نافذة التحكم بمشاركة الملف. انقر على أيقونة الإعدادات في أعلى يسار النافذة لتظهر لك نافذة الإعدادات السابقة. الغِ تفعيل الخيار "يمكن للمحرِّرين تغيير الأذونات ومشاركة العناصر". انقر على زر "حفظ". حذف مستندات جوجل المشاركة بصفتك مالكًا للمستند يمكنك حذف هذا الملف. وطالما أن الملف محذوف مؤقتًا (موجود في سلة المهملات)، يمكن للمساهمين الذين يمتلكون الأذونات اللازمة العمل على نسخهم من هذا المستند، لكن بمجرد أن تفرغ سلة المهملات سينتهي كل شيء؛ أما إن حذفت ملفًا شاركك به أحد الأشخاص (أي أنك لا تملكه)، فستُحذف نسختك فقط، ولن يؤثر ذلك على عمل بقية المساهمين. بإمكانك استعادة هذا الملف إن حذفته عن طريق الخطأ، وذلك بالوصول إلى الملف من خلال رابطه، ثم اختيار "ملف" يلي ذلك "إضافة إلى ملفاتي". خلاصة غطينا في هذا المقال النقاط التالية: الطرق المختلفة لإنشاء مستند جوجل جديد. طرق مشاركة مستندات جوجل وإضافة أذونات الوصول لمساهمين وإزالتها. تغيير أذونات وصول المساهمين. نقل ملكية مستندات جوجل. الوصول المحدود إلى مستندات جوجل المُشاركة وحذفها. اقرأ أيضًا كيفية المشاركة في كتابة وتعديل مستند باستعمال مستندات جوجل قائمة الأدوات في مستندات جوجل تاريخ النُسخ واستعمال الإضافات في مستندات جوجل
  13. تمتلك الروابط التشعبية أهميةً كبيرةً، فهي العناصر التي تجعل من الويب شبكة حقيقية، إذ نستعرض في هذا المقال الصيغة القواعدية اللازمة لإنشاء رابط، ونناقش أفضل الممارسات المتبعة في إنشائها، كما لا بد قبل المتابعة في قراءة هذا المقال الاطلاع على أساسيات HTML التي تحدثنا عنها في مقال تعرَّف على لغة HTML، وطريقة تنسيق صفحة HTML وهيكلة محتواها كما ورد في مقال هيكلة النصوص باستخدام لغة HTML. ما هو الرابط التشعبي؟ تُعَدّ الروابط التشعبية واحدةً من أكثر الابتكارات أهميةً في عالم الويب، فهي من أولى ميزات الويب منذ انطلاقها وهي بالفعل ما يجعل الويب شبكةً حقيقيةً، إذ تسمح لنا الروابط التشعبية بربط الصفحات بصفحات أخرى أو بأجزاء محددة منها أو بغيرها من الموارد أو الوصول إلى تطبيقات موجودة على عنوان ويب محدَّد، كما يمكن لأيّ محتوى التحول إلى رابط ينقل المستخدِم عند النقر عليه إلى عنوان ويب آخر URL. ملاحظة: يمكن لعنوان URL الإشارة إلى ملف HTML أو ملف نصي أو صور أو ملفات صوت ومقاطع فيديو أو أي شيء آخر على الويب، فإذا لم يتمكن المتصفح من التعامل مع نوع ما من الملفات، فسيسألك إذا كنت تريد فتحه (عندها ينقل مهمة فتح الملف إلى أحد التطبيقات المحلية على جهازك)، أو يُنزّل هذا الملف (لتتعامل معه لاحقًا). تشير الروابط في الصفحة الرئيسية لشبكة BBC مثلًا إلى قصص إخبارية متنوعة إضافية مختلفة، كما تشير إلى مناطق محتلفة من الموقع نفسه (إذ يقدم الموقع آليات للتنقل) وإلى صفحات تسجيل الدخول والاشتراك -أي أدوات المستخدِم- وغير ذلك. تشريح الرابط التشعبي ننشئ الرابط بأبسط أشكاله بتغليف نص أو محتوى معيَّن داخل العنصر <a>، ثم نستخدِم السمة href التي تُعرَف أيضًا باسم مرجع النص التشعبي أو الهدف، والتي تحتوي على العنوان الوجهة. <p>سينقلك هذا الرابط إلى <a href="https://academy.hsoub.com">أكاديمية حسوب</a>. </p> ستكون نتيجة الشيفرة السابقة ما يلي: سينقلك هذا الرابط إلى أكاديمية حسوب. إضافة معلومات داعمة من خلال السمة title قد ترغب أيضًا في إضافة السمة title إلى رابطك، إذ توضع ضمن هذه السمة معلومات إضافيةً عن الرابط مثل نوع المعلومات التي تقدمها الصفحة الهدف، أو الأشياء التي ينبغي الانتباه لها على الموقع. كما تظهر هذه المعلومات على صورة تلميح عند تمرير مؤشر الفأرة فوق الرابط. <p>سينقلك هذا الرابط إلى <a href="https://www.mozilla.org/en-US/" title="أكاديمية حسوب هو موقع تعليمي عربي يهدف إلى توجيه المهتمين العرب بمجال البرمجة و التقنية إلى مادة علمية صحيحة و باللغة العربية" >أكاديمية حسوب</a>. </p> ملاحظة: لا يظهر عنوان الرابط سوى عند تمرير مؤشر الفأرة فوق الرابط، ويعني هذا صعوبة الحصول على معلومات العنوان لمن يستخدِم لوحة المفاتيح أو شاشات اللمس للتنقل عبر صفحات الويب، فإذا كانت معلومات العنوان مهمةً بالفعل لاستخدام الصفحة الهدف، فلا بد من تقديمها بطريقة تمكِّن الجميع من الوصول إليها، كأن تضعها ضمن نص تقليدي. تطبيق: إنشاء رابط خاص بك أنشئ ملف HTML باستخدام القالب الذي نزّلته في مقال تعرف على لغة HTML ومحرِّر الشيفرة على حاسوبك، ثم نفّذ التعديلات التالية: أضف فقرةً نصيةً <p> أو أكثر داخل العنصر <body>، أو غير ذلك من العناصر التي تعلمتها. حوّل أجزاءً من المحتوى إلى روابط. أضف عناوين إلى هذه الروابط باستخدام السمة title. تحويل العناصر البنائية إلى روابط يمكن تحويل كما ذكرنا سابقًا أيّة عناصر إلى روابط حتى العناصر البنائية Block elements، فإذا أردت مثلًا تحويل صورة إلى رابط، فاستخدم العنصر <a> ثم العنصر <img> للدلالة على الصورة. <a href="https://www.mozilla.org/en-US/"> <img src="mozilla-image.png" alt="mozilla logo that links to the mozilla homepage"> </a> جولة سريعة على عناوين URL والمسارات ستحتاج إلى استيعاب عناوين URL ومسارات الملفات لكي تفهم تمامًا وجهة الروابط التشعبية، وهذا ما سنفعله في هذه الفقرة. يُعَدّ محدد موقع المورد Uniform Resource Locator أو URL اختصارًا- سلسلةً نصيةً تُحدِّد مكان تواجد غرض ما على ويب، فالصفحة الرئيسية لموقع حسوب موجودة على العنوان https://www.hsoub.com، إذ تستخدِم عناوين URL المسارات لإيجاد الملفات، في حين تحدد المسارات الموقع الذي يوجد فيه الملف ضمن نظام الملفات، ولنلق نظرةً على هيكلية مجلد: يُدعى المجلد الجذري creating-hyperlinks. سيقع الموقع بأكمله ضمن مجلد واحد عندما نعمل محليًا على موقع ويب، إذ ستجد ضمن المجلد الجذري الملفَين index.html و contacts.html، بحيث سيمثل الملف index.html في موقع الويب الحقيقي الصفحة الرئيسية أو صفحة الهبوط، أي الصفحة التي تُعَدّ نقطة البداية لموقع ويب أو لقسم محدد منه. لاحظ أيضًا وجود مجلدَين ضمن المجلد الجذري هما pdfs و projects، إذ يحتوي كل منهما على ملف وحيد، ولاحظ أنه من الممكن وجود ملفَين باسم index.html في المشروع طالما أنهما في موقعين مختلفين ضمن منظومة الملفات، وقد يكون الملف index.html الثاني الصفحة الرئيسية لمعلومات تتعلق بالمشروع. ملفات في المجلد نفسه: إذا أردت وضع رابط ضمن index.html الأول -أي الأعلى مستوى- يشير إلى الملف contacts.html، فضع اسم هذا الملف فقط لأن كلاهما يعود إلى المستوى نفسه ضمن نظام ترتيب الملفات، وسيكون عنوان URL المستخدم هو contacts.html: <p>Want to contact a specific staff member? Find details on our <a href="contacts.html">contacts page</a>.</p> ملفات في مجلدات فرعية: إذا أردت وضع رابط ضمن index.html الأول -أي الأعلى مستوى- يشير إلى الملف projects/index.html، فعليك الانتقال أولًا إلى المجلد projects ثم الإشارة إلى الملف الذي تريد إنشاء رابط إليه، ويكون ذلك بتحديد اسم المجلد يليه المحرف / ثم اسم الملف، وسيكون عنوان URL الصحيح في هذه الحالة هو projects/index.html: <p>Visit my <a href="projects/index.html">project homepage</a>.</p> ملفات في المجلدات الآباء: إذا أردت وضع رابط ضمن الملف projects/index.html يشير إلى الملف pdfs/project-brief.pdf، فعليك الانتقال إلى المستوى الأعلى مباشرةً ثم الانتقال ثانيةً إلى داخل المجلد pdfs، واستخدم نقطتين متتاليتين .. للانتقال إلى مستوى واحد أعلى، وسيكون العنوان الصحيح هو pdfs/project-brief.pdf/..: <p>A link to my <a href="../pdfs/project-brief.pdf">project brief</a>.</p> ملاحظة: يمكنك الدمج بين الطرق السابقة لكتابة عناوين URL مركّبة عند الحاجة مثل: ../../../complex/path/to/my/file.html الانتقال إلى قسم من صفحة HTML يمكن أن تستهدف من خلال الرابط التشعبي جزءًا محددًا من صفحة HTML -أو ما يعرف بتجزئة الصفحة document fragment- بدلًا من الانتقال إلى أعلى الصفحة، ولإنجاز الأمر لا بد من استخدام السمة id في العنصر الذي ستنتقل إليه في الصفحة المستهدَفة، ومن المنطقي أن تنشئ رابطًا إلى عنصر عنوان مثلًا، إذ سيبدو ذلك قريبًا من التالي: <h2 id="Mailing_address">Mailing address</h2> نضع بعد ذلك قيمة id للعنصر الذي سننتقل إليه في نهاية عنوان URL للصفحة المستهدَفة مسبوقًا بالمحرف #: <p>Want to write us a letter? Use our <a href="contacts.html#Mailing_address">mailing address</a>.</p> يمكن أيضًا استخدام الأسلوب ذاته في الانتقال إلى جزء مختلف من الصفحة نفسها: <p>The <a href="#Mailing_address">company mailing address</a> can be found at the bottom of this page.</p> عناوين URL المطلقة والنسبية ستصادف أثناء تجوالك في ويب مصطلحَي عنوان URL مطلق absolute URL وعنوان URL نسبي relative URL. عنوان URL مطلق يشير إلى موقع محَّدد عن طريق مساره المطلق أو الكامل بما في ذلك الجزء الذي يحدد البروتوكول واسم النطاق. فلو كان الملف index.html ضمن المجلد projects الموجود ضمن المجلد الجذري لموقع ويب التالي: https://www.example.com فسيكون العنوان المطلق إلى هذا الملف هو: https://www.example.com/projects/index.html أو يمكن أن يُكتب فقط على الصورة التالية: https://www.example.com/projects/ إذ معظم الخوادم ستبحث تلقائيًا عن صفحة بداية اسمها index.html، كما يشير العنوان المطلق دائمًا إلى الموقع نفسه أينما استخدِم. عنوان URL نسبي يشير العنوان النسبي إلى موقع ملف بالنسبة إلى الملف الذي تنوي ربطه به، فإذا أردت الربط بين الملف الذي رابطه: https://www.example.com/projects/index.html والملف project-brief.pdf الذي يقع في المجلد نفسه، فسيكون العنوان النسبي هو اسم الملف الهدف كما هو، في حين إذا كان هذا الملف في مجلد فرعي يُدعى pdfs ضمن المجلد نفسه project، فسيكون العنوان النسبي له pdfs/project-brief.pdf، ولاحظ أن المسار المطلق لنفس الملف هو: https://www.example.com/projects/pdfs/project-brief.pdf كما تشير العناوين النسبية إلى مواقع مختلفة تبعًا لموقع الملف الذي سيرتبط بها، فإذا نقلت الملف index.html في مثالنا السابق إلى المجلد الجذري، فسيشير عنوان URL النسبي pdfs/project-brief.pdf إلى الملف الموجود على العنوان التالي: https://www.example.com/pdfs/project-brief.pdf وليس إلى الملف الموجود على العنوان التالي: https://www.example.com/projects/pdfs/project-brief.pdf لم يتغير بالطبع موقع الملف project-brief.pdf ولا موقع المجلد pdfs عندما نقلنا الملف index.html، لكن كل ما هنالك أنّ العنوان النسبي سيشير في هذه الحالة إلى المكان الخاطئ ولن يعمل الرابط عند النقر عليه، فعليك الحذر إذًا. أفضل الممارسات لإنشاء الروابط التشعبية توجد بعض الممارسات الممتازة التي يجدر بك تعلمها لكتابة روابط تشعبية جيدة، لنلق نظرةً عليها. استخدم كلمات واضحة تدل على الرابط من السهل إلقاء الروابط هنا وهناك في صفحتك، وهذا الأمر بالطبع ليس كافيًا، إذ لا بد أن يستطيع جميع القراء الوصول إلى الوجهة الصحيحة أيًا كان وضعهم وأيًا كانت الأدوات التي يستخدمونها، وإليك بعض الأمثلة: يُفضِّل مستخدمِي قارئات الشاشة التنقل من رابط إلى آخر في الصفحة وقراءة هذه الروابط بمعزل عن بقية المحتوى. تستخدِم محركات البحث نص الرابط لفهرسة الملف الذي يستهدفه، فمن الجيد إذًا إضافة كلمات مفتاحية إلى نص الرابط لكي يصف ما يرتبط به بفعالية أكبر. يجول قارئو الصفحات بأنظارهم عبر الصفحة دون قراءة كل كلمة فيها، ويلفت انتباههم ما يبرز بوضوح فيها مثل الروابط، وبالتالي سيشعر المتابع بأهمية الرابط إذا كان النص الوصفي له مفيدًا. الرابط التالي جيد مثلًا: <p><a href="https://firefox.com/"> Download Firefox </a></p> أما هذا، فسيّئ: <p><a href="https://firefox.com/"> Click here </a> to download Firefox</p> تلميحات أخرى: لا تضع عنوان URL لرابط ضمن نص الرابط، إذ سيبدو سيئًا ويصبح أسوأ عندما ينطقه قارئ الشاشة حرفًا حرفًا. لا تكتب "رابط" أو "رابط إلى" ضمن نص الرابط لأنه أمر عبثي، إذ تخبر قارئات الشاشة المستخدِم بأنه رابط، كما يعلم بكل بساطة من يقرأ النص بأنه رابط لأنه سيظهر عمومًا بتنسيق مختلف وبسطر تحته. حاول أن يكون نص الرابط أقصر ما يمكن، فهذا مفيد لمستخدمِي قارئات الشاشة لأنها ستقرأ النص بأكمله. قلل الحالات التي تستخدِم فيها نص الرابط نفسه للدلالة على أماكن مختلفة، فقد يسبب ذلك مشاكل لمستخدمِي قارئات الشاشات إذا ظهرت قائمة من الروابط المتلاحقة التي تقول "انقر هنا". الارتباط بمورد مختلف عن صفحات HTML عند الارتباط بمورد قابل للتنزيل مثل ملف PDF أو وورد، أو بمورد يُتابع مباشرةً مثل ملفات الفيديو والصوت، أو قد يُظهر تأثيرات غير متوقعة مثل فتح نافذة منبثقة أو تحميل مقطع فلاش، فلا بد في هذه الحالات من اختيار كلمات مناسبة للرابط منعًا لأيّ التباس، وإليك بعض الأمثلة: قد يكون حجم باقة التراسل bandwidth connection لديك صغيرًا ثم تنقر فجأةً على رابط لتتفاجأ بتنزيل عدة ميغابايتات لم تتوقعها. قد لا يكون مشغل مقاطع فلاش مثبّتًا على جهازك ثم تنقر رابطًا يأخذك إلى صفحة تتطلب مشغل فلاش. لنلق نظرةً على أمثلة لنصوص يمكن استخدامها في هذه الحالات: <p><a href="https://www.example.com/large-report.pdf"> (PDF ، 10MB) حمل تقرير المبيعات </a></p> <p><a href="https://www.example.com/video-stream/" target="_blank"> (HD سيُعرض في نافذة منفصل وبدقة)‎ شاهد الفيديو </a></p> <p><a href="https://www.example.com/car-game"> (يتطلب مشغل فلاش‎) ‎‎شغِّل لعبة السيارة‎ </a></p> استخدم السمة download عندما ترتبط بمورد لتنزيله يمكن استخدام السمة download عندما تحاول إنشاء رابط إلى مورد لكي يُنزَّل بدلًا من أن يُفتَح في المتصفح، وذلك لكي تزوّد المستخدِم باسم لحفظ الملف، وإليك مثالًا عن رابط لتحميل آخر إصدارات فايرفوكس لنظام التشغيل ويندوز: <a href="https://download.mozilla.org/?product=firefox-latest-ssl&os=win64&lang=en-US" download="firefox-latest-64bit-installer.exe"> Download Latest Firefox for Windows (64-bit) (English, US) </a> تطبيق: إنشاء قائمة للتنقل يُطلَب منك في هذا التمرين ربط بعض الصفحات إلى قائمة تنقّل لإنشاء موقع ويب متعدد الصفحات، وهذه طريقة شائعة لإنشاء المواقع، إذ تُستخدَم الهيكلية نفسها لجميع الصفحات بما في ذلك قائمة التنقّل، وبالتالي عند النقر على أحد الروابط سيعطي ذلك انطباعًا بأنك لازلت في المكان ذاته لكن بمحتوى مختلف. عليك تخزين نسخ من الملفات التالية على حاسوبك وفي المجلد نفسه، كما يمكنك الحصول على جميع هذه الملفات من مستودع جيت-هاب المخصص للتمرين: index.html. projects.html. pictures.html. social.html. عليك: إضافة قائمة غير مرتبة في المكان المطلوب في الصفحة التي تضم أسماء بقية الصفحات التي سترتبط بها، فقائمة التنقل هي عادةً قائمة من الروابط، وبالتالي هذا الأسلوب صحيح دلاليًا. تحويل اسم كل صفحة إلى رابط إلى تلك الصفحة. نسخ قائمة التنقل إلى جميع الصفحات. إزالة الرابط من كل صفحة والذي يتعلق بها، فوجوده أمر غير ضروري ومربك، كما يعطي عدم ظهوره تذكيرًا بصريًا بالصفحة التي نتواجد فيها. سيبدو الحل عندما تنتهي منه قريبًا من الصفحة التالية: ملاحظة: إذا لم تستطع المتابعة أو لم تكن واثقًا من عملك، فيمكن التحقق من الحل في المستودع المخصص للتمرين على جيت-هاب. روابط البريد الإلكتروني من الممكن إنشاء روابط أو أزرار تَفتح عند النقر عليها نموذجًا أو تطبيقًا لكتابة رسالة بريد إلكتروني بدلًا من الانتقال إلى مورد معين. يُستخدم العنصر <a> لهذه الغاية بالإضافة إلى بروتوكول mailto:‎، إذ يشير البروتوكول :mailto في أكثر الحالات بساطةً وشيوعًا إلى عنوان البريد الإلكتروني للمستقبِل: <a href="mailto:nowhere@mozilla.org">Send email to nowhere</a> يُعَدّ عنوان البريد الإلكتروني اختياريًا في واقع الأمر، فإذا أزلت العنوان وأبقيت على البروتوكول :mailto فقط، فستظهر لك نافذة عميل البريد الإلكتروني المثبت على جهازك لإنشاء بريد إلكتروني جديد دون تحديد المستقبِل، ولهذه الميزة فوائدها في روابط المشاركة Share التي يمكن للمستخدِمين النقر عليها لإرسال بريد إلكتروني إلى عناوين من اختيارهم. تحديد بعض التفاصيل يمكن إضافة معلومات أخرى إلى عنوان البريد الإلكتروني، إذ يمكن إضافة أية حقول مستخدَمة في ترويسة البريد الإلكتروني إلى :mailto، وأكثر هذه المعلومات شيوعًا هي الموضوع Subject ونسخة إلى cc وجسم الرسالة body الذي لا يمثل حقلًا فعليًا لنص الرسالة، لكن بالإمكان استخدامه لكتابة محتوى قصير للرسالة، كما يُحدَّد كل حقل مع قيمته بأسلوب الاستعلام، وإليك مثالًا كما يلي: <a href="mailto:nowhere@mozilla.org?cc=name2@rapidtables.com&bcc=name3@rapidtables.com&subject=The%20subject%20of%20the%20email&body=The%20body%20of%20the%20email"> Send mail with cc, bcc, subject and body </a> ملاحظة: ينبغي أن تكون القيم في كل حقل مكتوبةً وفق ترميز عنوان URL، أي دون محارف لا تطبع شيئًا مثل محارف السطر الجديد والجدولة ودون فراغات، ولاحظ أيضًا استخدام إشارة الاستفهام ? في الفصل بين عنوان URL الرئيسي عن قيم الحقول والمحرف & للفصل بين الحقول، إذ تُعَدّ هذه الرموز رموزًا معياريةً لاستعلام URL، وإليك بعض الأمثلة عن عناوين :mailto بسيطة أخرى: mailto: mailto:nowhere@mozilla.org mailto:nowhere@mozilla.org,nobody@mozilla.org mailto:nowhere@mozilla.org?cc=nobody@mozilla.org mailto:nowhere@mozilla.org?cc=nobody@mozilla.org&subject=This%20is%20the%20subject خلاصة هذا كل ما تحتاجه عن الروابط، وسنعود إلى الروابط لاحقًا في سلسلة مقالاتنا عندما نتحدث عن أساليب التنسيق، كما سنتحدث في المقال التالي عن الدلالات التي تقدمها عناصر HTML، وسنطلع على بعض الميزات المتقدمة أو غير المألوفة والتي قد تعُدّها مفيدة، لذا سيكون التنسيق المتقدم للنصوص في لغة HTML هو خطوتنا التالية. احصل على موقع إلكتروني مخصص لأعمالك أبهر زوارك بموقع احترافي ومميز بالاستعانة بأفضل خدمات تطوير وتحسين المواقع على خمسات أنشئ موقعك الآن ترجمة -وبتصرف- للمقال Creating hyperlinks. اقرأ أيضًا مفهوم الروابط التشعبية في مواقع الويب كيفية إنشاء الارتباطات التشعبية (Hyperlinks) والإجراءات (Actions) في Microsoft PowerPoint ترويسة الصفحة والبيانات الوصفية في HTML
  14. نستعرض في هذا المقال طريقة هيكلة التطبيق الذي عملنا عليه في المقال السابق، والاستعلام عن معلومات متنوعة تضمها قاعدة البيانات العلاقية. هيكلية التطبيق لقد كتبنا حتى اللحظة كامل الشيفرة في نفس الملف، لهذا سنحاول الآن إعطاء التطبيق هيكلًا أوضح. لننشئ إذًا المجلدات والملفات وفق الهيكلية التالية: index.js util config.js db.js models index.js note.js controllers notes.js أما محتوى الملفات فستكون على النحو التالي: الملف "util/config.js": يهتم بالتعامل مع متغيرات البيئة environment variables: require('dotenv').config() module.exports = { DATABASE_URL: process.env.DATABASE_URL, PORT: process.env.PORT || 3001, } الملف "index.js": ويهتم بتهيئة وتشغيل التطبيق: const express = require('express') const app = express() const { PORT } = require('./util/config') const { connectToDatabase } = require('./util/db') const notesRouter = require('./controllers/notes') app.use(express.json()) app.use('/api/notes', notesRouter) const start = async () => { await connectToDatabase() app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) } start() يختلف تشغيل التطبيق قليلًا عما رأيناه سابقًا، لأننا نريد التأكُّد من نجاح الاتصال بقاعدة البيانات قبل أن يبدأ التطبيق العمل الفعلي. الملف "util/d b.js": ويضم الشيفرة التي تُهيئ قاعدة البيانات: const Sequelize = require('sequelize') const { DATABASE_URL } = require('./config') const sequelize = new Sequelize(DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const connectToDatabase = async () => { try { await sequelize.authenticate() console.log('connected to the database') } catch (err) { console.log('failed to connect to the database') return process.exit(1) } return null } module.exports = { connectToDatabase, sequelize } الملف "models/note.js": وتُخزّن فيه الملاحظات في النموذج المقابل للجدول الذي سيُحفظ. const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class Note extends Model {} Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note الملف "models/index.js": لا يُستخدم حاليًا تقريبًا بسبب وجود نموذج واحد فقط في التطبيق، لكنه سيصبح أكثر فائدةً عندما نبدأ إضافة نماذج جديدة، إذ سيلغي الحاجة إلى إدراج ملفات منفصلة تُعرِّف بقية النماذج: const Note = require('./note') Note.sync() module.exports = { Note } الملف "controllers/notes.js": ويضم الوجهات المرتبطة بالملاحظات، أي مسار التوجيه إلى ملاحظة: const router = require('express').Router() const { Note } = require('../models') router.get('/', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) router.post('/', async (req, res) => { try { const note = await Note.create(req.body) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { res.json(note) } else { res.status(404).end() } }) router.delete('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { await note.destroy() } res.status(204).end() }) router.put('/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { note.important = req.body.important await note.save() res.json(note) } else { res.status(404).end() } }) module.exports = router هكذا يبدو هيكل التطبيق جيدًا الآن، لكننا نلاحظ أنّ معالجات التوجيه route handlers الذي يتعامل مع ملاحظة واحدة يضم قليلًا من الشيفرات المكررة، فجميعها تبدأ بالسطر الذي يبحث عن الملاحظة التي يتعامل معها: const note = await Note.findByPk(req.params.id) لنعيد كتابة الشيفرة على شكل أداة وسطية middleware خاصةٍ بنا، ونطبّقها في معالجات التوجيه: const noteFinder = async (req, res, next) => { req.note = await Note.findByPk(req.params.id) next() } router.get('/:id', noteFinder, async (req, res) => { if (req.note) { res.json(req.note) } else { res.status(404).end() } }) router.delete('/:id', noteFinder, async (req, res) => { if (req.note) { await req.note.destroy() } res.status(204).end() }) router.put('/:id', noteFinder, async (req, res) => { if (req.note) { req.note.important = req.body.important await req.note.save() res.json(req.note) } else { res.status(404).end() } }) تستقبل معالجات الوجهة ثلاثة معاملات: الأول نصي يُعرّف الوجهة، والثاني الأداة الوسطية noteFinder المُعرفة مُسبقًا والتي تستخلص الملاحظة من قاعدة البيانات وتضعها في الخاصية note للكائن req. بإمكانك إيجاد الشيفرة الحالية للتطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع part13-2. التمرينات 13.5 إلى 13.7 حاول إنجاز التمارين التالية التمرين 13.5 غيّر هيكل تطبيقك ليشابه المثال السابق أو اتبع هيكليةً أخرى واضحة وملائمة. التمرين 13.6 قدِّم طريقةً تدعم تغيير عدد الإعجابات بمدوّنة في تطبيقك مستخدمًا العملية "PUT /api/blogs/:id"، إذ ينبغي أن يصل العدد الجديد للإعجابات مع الطلب: { likes: 3 } التمرين 13.7 استخدم أداةً وسطيةً للتحكم المركزي بمعالجة الأخطاء كما فعلنا في القسم 3 كما يمكنك استخدام الأداة الوسطية express-async-errors كما فعلنا في القسم 4. لا تهتم للبيانات المُعادة في سياق رسالة الخطأ. حتى اللحظة لا تتطلب سوى حالتين في التطبيق معالجةً للأخطاء، هما: إضافة مدوّنة جديدة وتغيير عدد الإعجابات، لذلك تأكد من قدرة معالج الأخطاء على التعامل مع هاتين الحالتين بما يلائمهما. إدارة المستخدمين سنضيف تاليًا جدول قاعدة بيانات يُدعى "users" تُخزّن في فيه بيانات مستخدمي التطبيق، كما سنضيف أيضًا وظيفةً لإضافة مستخدمين جدد وآلية تسجيل دخول مبنية على مفاتيح الاستيثاق كما فعلنا في القسم 4، ولكي نبسط العمل، سنُعدِّل ما أنجزناه سابقًا كي يكون لجميع المستخدمين كلمة المرور ذاتها وهي "secret". محتوى الملف "models/user.js" الذي يُعرّف المستخدمين واضحٌ تمامًا: const { Model, DataTypes } = require('sequelize') const { sequelize } = require('../util/db') class User extends Model {} User.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, username: { type: DataTypes.STRING, unique: true, allowNull: false }, name: { type: DataTypes.STRING, allowNull: false }, }, { sequelize, underscored: true, timestamps: false, modelName: 'user' }) module.exports = User سيكون حقل اسم المستخدم "username" ذا قيم فريدة، لكن وعلى الرغم من إمكانية جعله المفتاح الرئيسي للجدول، إلا أننا قررنا أن ننشئ حقلًا field منفصلًا "id" ذا قيم صحيحة ليكون المفتاح الرئيسي. سيتوسع الملف "models/index.js" قليلًا: const Note = require('./note') const User = require('./user') Note.sync() User.sync() module.exports = { Note, User } لا يضم الملف "controllers/users.js" الذي يحتوي معالجات الوجهة التي تهتم بإنشاء مستخدمين جدد أي شيء مهم حاليًا سوى عرض كل المستخدمين: const router = require('express').Router() const { User } = require('../models') router.get('/', async (req, res) => { const users = await User.findAll() res.json(users) }) router.post('/', async (req, res) => { try { const user = await User.create(req.body) res.json(user) } catch(error) { return res.status(400).json({ error }) } }) router.get('/:id', async (req, res) => { const user = await User.findByPk(req.params.id) if (user) { res.json(user) } else { res.status(404).end() } }) module.exports = router أما معالج الوجهة الذي يتحكم بتسجيل الدخول (الملف "controllers/login.js") فسيكون على النحو التالي: const jwt = require('jsonwebtoken') const router = require('express').Router() const { SECRET } = require('../util/config') const User = require('../models/user') router.post('/', async (request, response) => { const body = request.body const user = await User.findOne({ where: { username: body.username } }) const passwordCorrect = body.password === 'secret' if (!(user && passwordCorrect)) { return response.status(401).json({ error: 'invalid username or password' }) } const userForToken = { username: user.username, id: user.id, } const token = jwt.sign(userForToken, SECRET) response .status(200) .send({ token, username: user.username, name: user.name }) }) module.exports = router سيُرفق اسم المستخدم وكلمة المرور مع طلب POST، ويُستخلص الكائن المتعلق باسم المستخدم أولًا من قاعدة البيانات باستخدام التابع findOne العائد للنموذج User: const user = await User.findOne({ where: { username: body.username } }) يمكنك أن تلاحظ من خلال الطرفية أن تعليمة SQL المتعلقة باستدعاء التابع السابق هي: SELECT "id", "username", "name" FROM "users" AS "User" WHERE "User". "username" = 'mluukkai'; إن وُجد المستخدم وكانت كلمة المرور صحيحة (وهي "secret" لجميع المستخدمين)، يُعاد مفتاح الاستيثاق "jsonwebtoken" متضمنًا معلومات المستخدم مع الاستجابة. لهذا سنُثبّت الاعتمادية "jsonwebtoken": npm install jsonwebtoken سيتوسع الملف "index.js" قليلًا: const notesRouter = require('./controllers/notes') const usersRouter = require('./controllers/users') const loginRouter = require('./controllers/login') app.use(express.json()) app.use('/api/notes', notesRouter) app.use('/api/users', usersRouter) app.use('/api/login', loginRouter) يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-3. الاتصال بين الجداول يمكن الآن إضافة مستخدمين إلى التطبيق، كما يمكن للمستخدمين تسجيل الدخول، لكنها ليست ميزات مفيدة جدًا في وضعها الحالي، لذلك سنضيف ميزات لا يمكن للمستخدم الاستفادة منها ما لم يُسجل دخوله مثل إضافة ملاحظة جديدة، إذ ترتبط هذه الملاحظة بالمستخدم الذي أنشأها، ولهذا لا بُدّ من إضافة مفتاح خارجي foreign key إلى جدول "notes". يمكن تعريف المفتاح الخارجي عند استخدام مكتبة Sequelize بتعديل الملف "models/index.js" على النحو التالي: const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) Note.sync({ alter: true }) User.sync({ alter: true }) module.exports = { Note, User } وهكذا نكون قد عرّفنا علاقة واحد-إلى-متعدد one-to-many تصل بين جدولي المستخدمين "users" والملاحظات "notes"، كما عدّلنا الخيارات في استدعاءات sync لتتطابق جداول قاعدة البيانات مع التغييرات التي حدثت على تعريفات النموذج. سيبدو مخطط قاعدة البيانات على شاشة الطرفية على النحو التالي: username=> \d users Table "public.users" Column | Type | Collation | Nullable | Default ----------+------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('users_id_seq'::regclass) username | character varying(255) | | not null | name | character varying(255) | | not null | Indexes: "users_pkey" PRIMARY KEY, btree (id) Referenced by: TABLE "notes" CONSTRAINT "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL username=> \d notes Table "public.notes" Column | Type | Collation | Nullable | Default -----------+--------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('notes_id_seq'::regclass) content | text | | not null | important | boolean | | | | date | timestamp with time zone | | | | user_id | integer | | | | Indexes: "notes_pkey" PRIMARY KEY, btree (id) Foreign-key constraints: "notes_user_id_fkey" FOREIGN KEY (user_id) REFERENCES users(id) ON UPDATE CASCADE ON DELETE SET NULL يشير المفتاح الخارجي user_id الذي أُنشئ في الجدول notes إلى أسطر في الجدول users. لنربط الآن كل ملاحظة جديدة بالمستخدم الذي أنشأها، ولكن قبل تقديم الأمر (ربط الملاحظة بمفتاح استيثاق المستخدم الذي استخدمه لتسجيل دخوله)، لا بُد من كتابة الملاحظة التي ترتبط بالمستخدم الأول الذي يُعثر عليه يدويًا في الشيفرة: router.post('/', async (req, res) => { try { const user = await User.findOne() const note = await Note.create({...req.body, userId: user.id}) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) انتبه إلى وجود العمود user_id في جدول الملاحظات "notes" على مستوى قاعدة البيانات، ويُشار إلى اسم كل صف في قاعدة البيانات بالطريقة التقليدية للمكتبة Sequelize، وذلك بكتابته على نقيض أسلوب سنام الجمل "userId"، أي كما تُكتب تمامًا في الشيفرة (حروف صغيرة). من السهل تنفيذ استعلامات مشتركة في Sequelize، لهذا سنغيّر الوجهة التي تعيد كل المستخدمين لتعرض كل ملاحظات المستخدم: router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note } }) res.json(users) }) يُنفَّذ الاستعلام المشترك باستخدام الخيار include مثل معامل استعلام، أما تعليمة SQL المولّدة من الاستعلام، فستُطبع على شاشة الطرفية على النحو التالي: SELECT "User". "id", "User". "username", "User". "name", "Notes". "id" AS "Notes.id", "Notes". "content" AS "Notes.content", "Notes". "important" AS "Notes.important", "Notes". "date" AS "Notes.date", "Notes". "user_id" AS "Notes.UserId" FROM "users" AS "User" LEFT OUTER JOIN "notes" AS "Notes" ON "User". "id" = "Notes". "user_id"; ستبدو النتيجة النهائية كما تتوقع: الإضافة الملائمة للملاحظات لنحاول تغيير طريقة إضافة الملاحظات لتعمل بالطريقة التي عملت بها من قبل، بحيث ينجح إنشاء الملاحظات فقط إذا حمل طلب إنشائها مفتاح استيثاق صحيح عند تسجيل الدخول. تُخزّن بعدها الملاحظة ضمن قائمة الملاحظات التي أنشأها المستخدم المُعرّف بواسطة مفتاح الاستيثاق: const tokenExtractor = (req, res, next) => { const authorization = req.get('authorization') if (authorization && authorization.toLowerCase().startsWith('bearer ')) { try { req.decodedToken = jwt.verify(authorization.substring(7), SECRET) } catch{ res.status(401).json({ error: 'token invalid' }) } } else { res.status(401).json({ error: 'token missing' }) } next() } router.post('/', tokenExtractor, async (req, res) => { try { const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({...req.body, userId: user.id, date: new Date()}) res.json(note) } catch(error) { return res.status(400).json({ error }) } }) يُستخلص مفتاح الاستيثاق من ترويسة الطلب ويفُك تشفيره ويوضع ضمن الكائن req بواسطة الأداة الوسطية tokenExtractor. يُضاف زمن الإنشاء إلى الحقل date أيضًا عند إنشاء الملاحظة. ضبط الواجهة الخلفية تعمل الواجهة الخلفية حتى اللحظة بنفس الطريقة التي تعمل بها في نسخة القسم 4 من التطبيق ماعدا فكرة معالجة الأخطاء. لنغيّر الآن وجهات إحضار جميع الملاحظات وجميع المستخدمين قليلًا قبل توسيع هذه الواجهة. سنضيف إلى كل ملاحظة بعض المعلومات المتعلقة بالمستخدم الذي أنشأها: router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: User, attributes: ['name'] } }) res.json(notes) }) كنا قد قيّدنا سابقًا القيم التي يأخذها الحقل المطلوب، إذ كنا نعيد جميع حقول البيانات الخاصة بكل ملاحظة بما في ذلك اسم المستخدم "name" الذي يرتبط بالملاحظة لكن باستثناء حقل المعرّف الفريد للمستخدم "userId". لنجرِ التغيير ذاته على الوجهة route التي تحضر جميع المستخدمين والملاحظات وذلك بإزالة الحقل userId غير الضروري من الملاحظات المرتبطة بمستخدم معين على النحو التالي: router.get('/', async (req, res) => { const users = await User.findAll({ include: { model: Note, attributes: { exclude: ['userId'] } } }) res.json(users) }) يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-4. قليل من الانتباه إلى تعريفات النماذج رغم وجود العمود ‍‍‌‍‍user_id، إلا أننا لم نغيّر النموذج الذي يعرّف الملاحظات، ولكن يمكننا مع ذلك إضافة مستخدم إلى كائن الملاحظة: const user = await User.findByPk(req.decodedToken.id) const note = await Note.create({ ...req.body, userId: user.id, date: new Date() }) يعود السبب وراء ذلك إلى أننا لم نحدّد وجود علاقة واحد-إلى-متعدد في الاتصال بين جدولي المستخدمين "users" والملاحظات "notes" ضمن الملف "models/index.js": const Note = require('./note') const User = require('./user') User.hasMany(Note) Note.belongsTo(User) // ... تُنشئ المكتبة Sequelize تلقائيًا سمةً تُدعى userId في النموذج Note تمنح وصولًا إلى العمود user_id عندما يُشار إليه. وتذكّر أنه يمكنك إنشاء ملاحظة باستخدام التابع build: const user = await User.findByPk(req.decodedToken.id) // إنشاء ملاحظة دون تخزينها بعد const note = Note.build({ ...req.body, date: new Date() }) // للملاحظة المُنشأة userId وضع المعرف الفريد للمستخدم في خاصية note.userId = user.id // تخزين كائن الملاحظة في قاعدة البيانات await note.save() هكذا نرى صراحة أن userId هي سمة attribute لكائن الملاحظات، وقد كان بالإمكان تعريف النموذج على النحو التالي للحصول على النتيجة ذاتها: Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE }, userId: { type: DataTypes.INTEGER, allowNull: false, references: { model: 'users', key: 'id' }, } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) module.exports = Note لا يوجد داعٍ للتعريف على مستوى الصنف في النموذج كما فعلنا سابقًا: User.hasMany(Note) Note.belongsTo(User) وبدلًا من ذلك يمكننا تحقيق الأمر بهذه الطريقة، ولا بُد من استخدام إحدى هاتين الطريقتين وإلا لن تُدرك مكتبة Sequelize كيف يتصل الجدولين ببعضهما على مستوى الشيفرة. التمرينات 13.8 إلى 13.11 حاول إنجاز التمارين الآتية: التمرين 13.8 زِد دعم التطبيق لمستخدميه، إذ لا بُد أن يضم جدول المستخدمين الحقول التالية إضافةً إلى الحقل "ID": name: ذو قيمة نصية (لا يمكن أن يكون فارغًا). username: ذو قيمة نصية (لا يمكن أن يكون فارغًا). وعلى خلاف ما أوردنا في الشروحات النظرية، لا تمنع مكتبة Sequelize حاليًا إنشاء البصمتين الزمنيتين create_dat و update_dat لجدول المستخدمين. يمكن إعطاء كلمة المرور نفسها لجميع المستخدمين، وكذلك اختيار طريقة التحقق من كلمة المرور كما في القسم 4، وعليك أيضًا إنجاز الوجهات التالية: "POST api/users": لإضافة مستخدم جديد. "GET api/users": عرض جميع المستخدمين. "PUT api/users/:username": لتغيير اسم المستخدم وليس المعرّف "id". تأكد من إدراج البصمات الزمنية تلقائيًا من قِبل مكتبة Sequelize وعلى النحو الصحيح عند إضافة مستخدم أو تغيير اسمه. التمرين 13.9 تزودنا مكتبة Sequelize بمجموعة قواعد تحقق معرّفةٌ مسبقًا للتأكد من قيم حقول النموذج تُنفّذها قبل تخزين الكائنات في قاعدة البيانات. ولأننا نريد تغيير سياسة إنشاء مستخدم جديد تتطلب أن يكون البريد الإلكتروني المُدخل صحيحًا كما هو حال اسم المستخدم، أنجِز طريقةً للتحقق من ذلك أثناء إنشاء المستخدم. عدّل أيضًا بالأداة الوسطية المستخدمة في معالجة الأخطاء لتعرض وصفًا أكثر وضوحًا لرسالة الخطأ مثل استخدام رسائل خطأ Sequelize على سبيل المثال. { "error": [ "Validation isEmail on username failed" ] } التمرين 13.10 وسّع التطبيق لكي تربط كل مستخدم سجّل دخوله بنجاح عبر مفتاح الاستيثاق بكل مدونة يضيفها. لا بُد من تقديم وصلة endpoint تسجيل دخول "POST /api/login" تُعيد مفتاح الاستيثاق. التمرين 13.11 تأكد أن المستخدم قادرٌ فقط على حذف المدونات التي يضيفها. التمرين 13.12 عدّل الوجهات كي تكون قادرًا على استخلاص جميع المدوّنات والمستخدمين كي تعرض كل مدوّنة المستخدم الذي أضافها وأن يعرض كل مستخدم المدوّنات التي أضافها. استعلامات أوسع لا يزال تطبيقنا حتى اللحظة بسيطًا من ناحية الاستعلامات التي يجريها، إذ اقتصر البحث على صف واحد بناءً على مفتاح رئيسي من خلال التابع "findByPk"، أو البحث عن كل الصفوف باستخدام التابع "findAll". كانت هذه الاستعلامات كافيةً بالنسبة للواجهة الأمامية في نسخة القسم 5 من التطبيق، لكننا سنوّسع الواجهة الخلفية لنتمكن من تنفيذ استعلامات أعقد قليلًا. لننجز أولًا طريقة للحصول على ملاحظات مصنّفة على أنها مهمة أو غير مهمة فقط، باستخدام معامل الاستعلام important: router.get('/', async (req, res) => { const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important: req.query.important === "true" } }) res.json(notes) }) وهكذا ستتمكن الواجهة الخلفية من استخلاص الملاحظات المهمة عند وصول الطلب: http://localhost:3001/api/notes?important=true والملاحظات غير المهمة عند وصول الطلب: http://localhost:3001/api/notes?important=false يتضمن استعلام SQL الذي ولّدته Sequelize العبارة WHERE، التي تُرشّح الصفوف المُعادة في الحالة الطبيعية: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true; وبالطبع لن ينفع إنجاز الأمر بهذه الطريقة ما لم يهتم الاستعلام بأهمية الملاحظة، كأن يكون على النحو التالي: http://localhost:3001/api/notes يمكن تدارك الموضوع بطرق عدة، أولها -وقد لا تكون الطريقة الأفضل- على النحو التالي: const { Op } = require('sequelize') router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important } }) res.json(notes) }) يُخزّن الكائن important الآن شرط الاستعلام، وسيكون الشكل الافتراضي لهذا الاستعلام على النحو التالي: where: { important: { [Op.in]: [true, false] } } أي أن العمود قد يحمل إحدى القيمتين true أو false باستخدام العامل Op.in، وهو أحد عوامل Sequelize العديدة؛ فإذا حددنا قيمة للمعامل req.query.important، سيتغير الاستعلام ليصبح بأحد الشكلين التاليين: where: { important: true } أو where: { important: false } بناءً على قيمة معامل الاستعلام. يمكن زيادة القدرة الوظيفية للتطبيق بالسماح للمستخدم بتخصيص كلمة مفتاحية لإحضار البيانات، إذ يعيد الطلب التالي: http://localhost:3001/api/notes?search=database الملاحظات التي تشير إلى الكلمة "database". كما يعيد الطلب التالي: http://localhost:3001/api/notes?search=javascript&important=true جميع الملاحظات التي حُددت أنها مهمة "important" وتشير إلى الكلمة "javascript". سيكون تنفيذ الأمر على النحو التالي: router.get('/', async (req, res) => { let important = { [Op.in]: [true, false] } if ( req.query.important ) { important = req.query.important === "true" } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where: { important, content: { [Op.substring]: req.query.search ? req.query.search : '' } } }) res.json(notes) }) يوّلد التابع Op.substring الاستعلام الذي نريده باستخدام كلمة "LIKE"، فإذا أنشأت الاستعلام التالي مثلًا: http://localhost:3001/api/notes?search=database&important=true فسيولّد استعلام SQL ما نتوقعه تمامًا: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%'; لكن لا تزال هناك ثغرة مزعجة تتمثل في أن الطلب التالي: http://localhost:3001/api/notes والذي يعني أننا نريد الحصول على جميع الملاحظات، سيتسبب في وجود عبارة WHERE غير الضرورية في الاستعلام، والتي قد تؤثر في فعاليته وفقًا لآلية عمل محرك قاعدة البيانات: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" IN (true, false) AND "note". "content" LIKE '%%'; لنحسّن الشيفرة لكي تُستخدم عبارة WHERE عند الحاجة فقط: router.get('/', async (req, res) => { const where = {} if (req.query.important) { where.important = req.query.important === "true" } if (req.query.search) { where.content = { [Op.substring]: req.query.search } } const notes = await Note.findAll({ attributes: { exclude: ['userId'] }, include: { model: user, attributes: ['name'] }, where }) res.json(notes) }) وإذا احتوى الطلب على شرط للبحث مثل: http://localhost:3001/api/notes?search=database&important=true سيتولد استعلام يضم عبارة WHERE: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id" WHERE "note". "important" = true AND "note". "content" LIKE '%database%'; وإذا لم يحتوي شرطًا، فلن يكون لوجود WHERE حاجة: SELECT "note". "id", "note". "content", "note". "important", "note". "date", "user". "id" AS "user.id", "user". "name" AS "user.name" FROM "notes" AS "note" LEFT OUTER JOIN "users" AS "user" ON "note". "user_id" = "user". "id"; يمكنك الحصول على شيفرة التطبيق بوضعها الحالي من المستودع المخصص على GitHub ضمن الفرع part13-5. التمرينات 13.13 إلى 13.16 حاول إنجاز التمرينات التالية: التمرين 13.13 قدِّم آليةً لترشيح filtering النتائج من خلال كلمة مفتاحية ضمن الوجهة التي تعيد كل المدوّنات، وينبغي أن تعمل الآلية على النحو التالي: "GET /api/blogs?search=react": تعيد كل المدونات التي تضم الكلمة "react" في الحقل علمًا أن البحث حساس لحالة الأحرف. "GET /api/blogs": يعيد جميع المدوّنات. يمكنك الاستفادة من عوامل Sequelize لتنفيذ التمرين. التمرين 13.14 وسّع آلية الترشيح للبحث عن كلمة مفتاحية في أحد الحقلين "field" أو "author"، إذ سيعيد الاستعلام: GET /api/blogs?search=jami كل المدوّنات التي تضم الكلمة في أحد الحقلين "field" أو "author". التمرين 13.15 عدّل وجهات المدوّنات لتعيد المدوّنات بناءً على عدد الإعجابات بترتيب تنازلي. اطلع من خلال توثيق Sequelize على تعليمات ترتيب نتائج الاستعلام. التمرين 13.16 أنشئ الوجهة "api/authors/" كي تعيد عدد المدونات التي أضافها المؤلف والعدد الكلي للإعجابات. قدّم العملية على مستوى قاعدة البيانات. قد تحتاج إلى وظيفة التجميع group by ودالة التجميع sequelize.fn. قد تبدو المعلومات المُعادة بتنسيق JSON على غرار المثال الآتي: [ { author: "Jami Kousa", articles: "3", likes: "10" }, { author: "Kalle Ilves", articles: "1", likes: "2" }, { author: "Dan Abramov", articles: "1", likes: "4" } ] مهمة لعلامة إضافية: رتِّب البيانات المُعادة بناءً على عدد الإعجابات، على أن تُنفّذ عملية الترتيب من خلال استعلام قاعدة البيانات. ترجمة -وبتصرف- للفصل Join tables and queries من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: العمل مع قواعد بيانات علاقية باستخدام Sequelize النماذج واﻻستعلامات والتقارير في برنامج قواعد بيانات مايكروسوفت أكسس Microsoft Access التعامل مع قواعد البيانات تصميم الجداول ومعلومات المخطط وترتيب تنفيذ الاستعلامات في SQL
  15. قد يحصل المخترق على مفتاح التشفير، لكن لن يستطيع استخدامه لأسباب كثيرة.
  16. أحد المهام الرئيسية للغة HTML هي هيكلة النص لكي يتمكن المتصفح من عرض صفحات ويب بالطريقة التي يريدها المطوّر، إذ يصف هذا المقال طريقة استخدام لغة HTML لهيكلة صفحة نصية بإضافة عناوين وفقرات وإبراز بعض العبارات والكلمات وإنشاء قوائم وغير ذلك. لا بدّ قبل الشروع في القراءة من الاطلاع على أساسيات لغة HTML التي يغطيها المقال تعرَّف على لغة HTML التعامل مع الأساسيات: العناوين الرئيسية والفقرات تتكون معظم النصوص المهيكلة من عناوين وفقرات سواء أكان النص قصة أو صحيفة أو كتاب جامعي أو مجلة. تسهل النصوص المهيكلة تجربة القراءة وتزيد المتعة، وتُنظَّم النصوص ضمن فقرات في لغة HTML بتغليف النص داخل عنصر الفقرة النصية <p> كما يلي: <p>أكتب حاليًا فقرة نصية</p> بينما لا بد من استخدام عنصر عنوان مثل العنوان الرئيسي <h1> لإبراز النص على أساس عنوان للمحتوى الذي تعرضه الصفحة: <h1>هذا عنوان المقال</h1> تقدّم لغة HTML ستة مستويات للعناوين ابتداءً من الأعلى مستوى <h1> وانتهاءً بالأدنى<h6>، إذ يمثِّل العنصر <h1> العنوان الرئيسي ويمثِّل <h2> العناوين الفرعية للعنوان الرئيسي و<h3> العناوين الفرعية للعناوين الفرعية وهكذا، انظر توثيقها -العناصر h1-h6- في موسوعة حسوب. إنجاز الهيكلية المتدرجة لمحتوى صفحة HTML سنستخدِم في القصة التي نراها تاليًا العنصر <h1> ليمثل عنوان القصة و<h2> ليمثل عنوان كل فصل، بينما سيمثل العنصر <h3> الأقسام الفرعية لكل فصل. <h1>The Crushing Bore</h1> <p>By Chris Mills</p> <h2>Chapter 1: The dark night</h2> <p>It was a dark night. Somewhere, an owl hooted. The rain lashed down on the ...</p> <h2>Chapter 2: The eternal silence</h2> <p>Our protagonist could not so much as a whisper out of the shadowy figure ...</p> <h3>The specter speaks</h3> <p>Several more hours had passed, when all of a sudden the specter sat bolt upright and exclaimed, "Please have mercy on my soul!"</p> يعود الأمر إليك في طبيعة الحال لاختيار عناصر العناوين ودلالاتها طالما أنّ التسلسل الذي تتبعه في هيكلة المحتوى منطقي، لذلك لا بد أن تتذكر دائمًا بعض الممارسات الجيدة عند هيكلة العناوين: يفضَّل استخدام عنوان رئيسي <h1> واحد في كل صفحة، فهو العنوان ذو المستوى الأعلى وتتدرج تحته بقية العناصر. تأكد من استخدام عناصر العناوين بالترتيب الصحيح في الهيكلية المتدرجة للصفحة، فلا تستخدِم مثلًا <h3> ليشير إلى عنوان فرعي ثم تستخدم بعده <h2> ليمثل عنوانًا فرعيًا لعنوان فرعي، فهذا أمر غير منطقي وسيقود إلى نتائج غريبة. حاول عدم استخدام أكثر من ثلاثة مستويات في كل صفحة إلا إذا كان الأمر ضروريًا، فقد قَلَّ انتشار المستندات التي تعتمد هيكلية ذات مستويات عميقة نظرًا لصعوبة التنقل بين مستوياتها، ويفضل في حالات مثل هذه توزيع المحتوى ضمن صفحات عدة. ما فائدة إعطاء الصفحة هيكلية معينة؟ للإجابة على هذا السؤال، دعنا نلقي نظرةً على الملف text-start.html الذي سيكون نقطة انطلاق في تطبيقات هذا المقال (وصفة حمُّص جيدة)، لذلك انسخ هذا الملف وضعه على حاسوبك لأنك ستحتاجه لاحقًا، كما يحتوي جسم المستند محتوًى مكوّنًا من أجزاء مختلفة لم تُنظَّم بعد في هيكلية محددة، لكنها مفصولة عن بعضها بمحارف الأسطر الجديدة، فعندما تفتح الملف عبر المتصفح، فستجد أنّ النص قطعة واحدة كبيرة. السبب في ذلك هو عدم وجود أيّة عناصر لهيكلة هذا النص، فلن يميّز المتصفح بين العناوين أو الفقرات، بالإضافة إلى ذلك: يستعرض الزائر النص بسرعة بحثًا عن المحتوى الأساسي ويكتفي عادةً بقراءة العناوين ليختار نقطة البداية، إذ يقضي المستخدِم عادةً فترةً قصيرةً جدًا ضمن صفحة الويب، فإذا لم يستطع الزائر إيجاد أيّ شيء واضح أو مفيد خلال ثوان معدودة، فعلى الغالب سيتوقف عن القراءة ويغادر الصفحة. تفهرِس محركات البحث الصفحات آخذةً بالحسبان محتوى العناوين على أنها كلمات مفتاحية مهمة تؤثِّر على ترتيب نتائج البحث، فدون العناوين إذًا لن تُعَدّ صفحتك جيدةً وفق معايير السيو SEO أو تحسين محركات البحث Search Engine Optimization. لا يقرأ الزوار ذوو المشاكل البصرية الحادة صفحات ويب بأنفسهم، وإنما يستمعون إلى محتواها عن طريق برمجيات تُدعى قارئات الشاشة، إذ تتيح هذه البرمجيات الوصول السريع إلى محتوى نصي محدد، وتجري هذه العملية بطرق عدة منها تحديد خطوط عريضة للمحتوى بقراءة العناوين، مما يسمح للمستخدِم بإيجاد المعلومات التي يريدها بسرعة، فإذا لم تكن العناوين موجودةً، فسيضطر المستخدِم إلى الاستماع إلى النص بأكمله. إذا أردت تنسيق الصفحة باستخدام CSS أو جعلها تفاعليةً أكثر عن طريق جافاسكربت، فلا بد من تغليف المحتوى المطلوب بواسطة عناصر HTML لكي تستهدفه شيفرات CSS أو جافاسكربت بفعالية أكبر. لهذه الأسباب وغيرها سنحتاج إلى هيكلة محتوى الصفحة. تطبيق: هيكلة محتوى لننتقل مباشرةً إلى التطبيق العملي، إذ سنضيف عناصر إلى النص غير المنسق الموجود في حقل المدخلات Input field والذي يُعرض في حقل المخرجات Output field، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة ضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر"أظهر الحل Show solution" لترى الحل الصحيح. لماذا نحتاج إلى الدلالات؟ نعتمد على الدلالات في كل مكان حولنا، فنحن نعتمد على تجاربنا السابقة لتدلنا على وظيفة كل الأغراض التي حولنا، فعندما نرى هذا الغرض نعرف تمامًا عمله، فنحن نتوقع مثلًا أن تعني إشارة المرور الحمراء التوقف والخضراء الحركة، لكن سنرتبك بالفعل إذا تغيّرت الدلالات أو طبقناها بصورة خاطئة. لا بد في السياق عينه أن نتأكد من استخدام العناصر الصحيحة لكي نعطي المحتوى المعنى والوظيفة والمظهر الصحيح، فوفقًا لهذه المقاربة، فإن العنصر <h1> هو عنصر دلالي يعطي المحتوى الذي يغلفه صفة عنوان رئيسي لصفحتك بأعلى مستوى. <h1>This is a top level heading</h1> سيعطي المتصفح محتوى هذا العنوان حجمًا كبيرًا افتراضيًا لكي يظهر على أساس عنوان رئيسي (على الرغم من أنك قادر على تنسيقه بالطريقة التي تريد). والأهم من ذلك أن لدلالته استخدامات عدة في محركات البحث مثلًا أو في قارئات الشاشة، كما يمكنك من ناحية أخرى عرض أيّ عنصر وكأنه عنوان رئيسي. لاحظ الشيفرة التالية: <span style="font-size: 32px; margin: 21px 0; display: block;">Is this a top level heading?</span> ليس للعنصر <span> أية دلالات، ويستخدَم لتطبيق تنسيق لغة CSS على محتوى محدد أو جزء منه أو استهدافه بشيفرة جافاسكربت دون أن يدل ذلك على معنى جديد، وقد طبقنا في الشيفرة السابقة بعض التنسيقات على محتوى ليبدو تمامًا على أنه عنوان رئيسي، لكنك لن تجني أيّ فوائد أخرى من هذه العملية لأنّ هذا العنصر غير دلالي ولا يحمل معنًى محددًا، لذلك من الأفضل دائمًا استخدام عناصر HTML المخصصة لأداء وظيفة معينة. القوائم لنركِّز الآن على القوائم، فهي موجودة في كل مكان يحيط بنا ابتداءً من قائمة التسوق إلى قائمة الاتجاهات التي تسلكها يوميًا للوصول إلى المنزل، وستجد كذلك أنّ القوائم موجودة في كل مكان على الويب ويهمك منها ثلاثة أنواع سنناقشها بشيء من التفصيل. القوائم غير المرتبة تستخدَم القوائم غير المرتبة Unordered list لوصف مجموعة عناصر لا أولوية لترتيبها ضمن القائمة، ومن الأمثلة عليها قائمة التسوق: milk eggs bread hummus تبدأ القائمة غير المرتبة بالعنصر <ul> الذي يغلف جميع العناصر: <ul> milk eggs bread hummus </ul> وينبغي أن نغلف عناصر القائمة بالعنصر <li>: <ul> <li>milk</li> <li>eggs</li> <li>bread</li> <li>hummus</li> </ul> تطبيق: إنشاء قائمة غير مرتبة حاول استخدام محرر الشيفرة في الأسفل لإنشاء قائمة غير مرتبة خاصة بك: القوائم المرتبة تستخدَم القوائم المرتبة ordered list لوصف مجموعة عناصر ينبغي ترتيبها ضمن القائمة، ومن الأمثلة عليها الاتجاهات التي يجب أن تسلكها لتصل إلى مكان معيَّن: Drive to the end of the road Turn right Go straight across the first two roundabouts Turn left at the third roundabout The school is on your right, 300 meters up the road توصَّف القائمة المرتبة بأسلوب القوائم غير المرتبة نفسه باستثناء العنصر المستخدَم في تغليف العناصر الذي سيكون في هذه الحالة <ol> بدلًا من <ul>: <ol> <li>Drive to the end of the road</li> <li>Turn right</li> <li>Go straight across the first two roundabouts</li> <li>Turn left at the third roundabout</li> <li>The school is on your right, 300 meters up the road</li> </ol> تطبيق: إنشاء قائمة مرتبة حاول استخدام محرر الشيفرة في الأسفل لإنشاء قائمة مرتبة خاصة بك: تطبيق: هيكلة وصفة الطعام في مثالنا السابق أصبحت في هذه المرحلة قادرًا على هيكلة الصفحة التي تتحدث عن وصفة الحمُّص في مثالنا السابق، إذ يمكنك تخزين نسخة عن الملف text-start.html على جهازك ثم تجري التعديلات عليه، أو تنفيذ الأمر مستخدمًا محرر الشيفرة في الأسفل، وقد يكون تنفيذ التمرين على حاسوبك أفضل لأنك ستتمكن من حفظ عملك، في حين لن تتمكن من ذلك إذا استخدمت محرر الشيفرة في المقال لأن التغييرات التي أجريتها ستختفي عند إغلاق الصفحة. إذا وجدت نفسك تائهًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح، أو تحقق من الملف text-complete.html الذي يحتوي على العمل بأكمله. القوائم المتداخلة من الممكن جدًا أن تتداخل القوائم، فقد تحتاج إلى قائمة فرعية تحت أحد عناصر قائمة أخرى، ولنأخذ القائمة الثانية من مثال وصفة الطعام: <ol> <li>Remove the skin from the garlic, and chop coarsely.</li> <li>Remove all the seeds and stalk from the pepper, and chop coarsely.</li> <li>Add all the ingredients into a food processor.</li> <li>Process all the ingredients into a paste.</li> <li>If you want a coarse "chunky" hummus, process it for a short time.</li> <li>If you want a smooth hummus, process it for a longer time.</li> </ol> بما أن آخر عنصرين يتعلقان بالموضوع ذاته، فقد يبدوان مثل إرشادات فرعية تحت العنصر الذي يسبقهما، ومن المنطقي في هذه الحالة وضعهما ضمن قائمة غير مرتبة فرعية تحت العنصر الذي يسبقهما: <ol> <li>Remove the skin from the garlic, and chop coarsely.</li> <li>Remove all the seeds and stalk from the pepper, and chop coarsely.</li> <li>Add all the ingredients into a food processor.</li> <li>Process all the ingredients into a paste. <ul> <li>If you want a coarse "chunky" hummus, process it for a short time.</li> <li>If you want a smooth hummus, process it for a longer time.</li> </ul> </li> </ol> عُد إلى التطبيق السابق وحاول تعديل القائمة الثانية كما فعلنا هنا. إبراز النص والإشارة إلى أهميته نحاول أحيانًا إبراز بعض الكلمات عندما نتحدث للإيحاء بأهميتها أو لنعني أمرًا آخر، وقد نحاول إعطاء أهمية لكلمات أخرى أوتمييزها بطريقة أو بأخرى، وكذلك الأمر في HTML، إذ تضم اللغة عناصر دلالية لتمييز المحتوى النصي بأسلوب مماثل، وسنناقش تاليًا أكثر هذه العناصر شيوعًا. إبراز محتوى عندما نريد إبراز أو تفخيم عبارة في الكلام المحكي نشدِّد على كلمات محدَّدة لكي نغيّر عمدًا دلالة ما نقول، وهذا ما نفعله أيضًا أثناء الكتابة، إذ نميل إلى التشديد على الكلمات بكتابتها على نحو مائل، وتحمل الجملتين التاليتين على سبيل المثال دلالتين مختلفتين: سعيد لأنك لم تتأخر سعيد لأنك لم تتاخر تشير الجملة الأولى إلى راحة الشخص بأن الآخر لم يتأخر بالفعل، بينما تحمل الثانية نوعًا من الاستهزاء أو هجوم مبطن على الآخر للتعبير عن الانزعاج من تأخره. نستخدِم في HTML العنصر <em> لتوصيف حالات مثل هذه وزيادة المتعة أثناء قراءة النص، كما تُميِّز قارئات الشاشة هذه العناصر وتنطقها بنبرة صوتية مختلفة، وتعرض المتصفحات محتوى هذا العنصر بخط مائل افتراضيًا، لكن انتبه إلى استخدام العنصر لمجرد عرض النص مائلًا، وعليك عندها استخدام العنصر <i> أو أن تطبق تنسيق CSS مع عنصر بصنف محدد. <p>I am <em>glad</em> you weren't <em>late</em>.</p> إظهار أهمية محتوى لإبراز أهمية بعض الكلمات، فإننا نشدِّد عليها عند لفظها أو نكتبها بخط ثخين Bold مثل هذا السائل عالي السميّة. نستخدِم في HTML العنصر <strong> لإظهار أهمية الكلمات، إذ يعرض المتصفح محتوى هذا العنصر بخط ثخين افتراضيًا، كما تميزه قارئات الشاشة وتنطقها بنبرة صوتية مختلفة، ولا تحاول أيضًا استخدام العنصر لمجرد عرضه بخط ثخين، وعليك عندها استخدام العنصر <b> أو تطبيق تنسيق CSS مع عنصر بصنف محدد. <p>This liquid is <strong>highly toxic</strong>.</p> <p>I am counting on you. <strong>Do not</strong> be late!</p> يمكنك أيضًا المزج بين العنصرين <strong> و <em>. <p>This liquid is <strong>highly toxic</strong> — if you drink it, <strong>you may <em>die</em></strong>.</p> تطبيق: لنغير أهمية محتوى يوجد نص قابل للتعديل في محرِّر الشيفرة التالي، وحاول أن تبرز بعض الكلمات وتعطي أهمية لبعضها الآخر، كما ترى لكي تتمرن قليلًا: خط مائل أو ثخين أو تحته سطر تتميز العناصر التي تحدثنا عنها سابقًا بدلالات معنوية محددة، لكن حالة العناصر <b> و <i> و <u> أعقد قليلًا، فقد وضعت هذه العناصر كي يكتب الأشخاص كلمات محددة بخط ثخين أو مائل أو ليظهر تحتها سطر، وذلك في حقبة لم تكن تنسيقات CSS مدعومة أو كانت محدودة الدعم، إذ تؤثر هذه العناصر على طريقة العرض وليس على الدلالة، وتُعرف حاليًا بعناصر العرض presentational elements، كما لا ينبغي استخدام هذه العناصر حاليًا لأنها غير دلالية، وقد رأينا أهمية الدلالات على أصعدة مختلفة مثل سهولة الوصول accessibility وتحسين محركات البحث أو سيو SEO. أعادت HTML5 تعريف هذه العناصر مانحةً إياها دلالات قد تبدو مربكةً نوعًا ما، وإليك قاعدة مهمة جدًا: قد يكون استخدام العناصر <b> أو <i> أو <u> ملائمًا لإيصال معنى محدد يمكن إيصاله تقليديًا بإمالة الخط أو تثخينه أو وضع سطر تحته إذا لم توجد عناصر أخرى مناسبة، لكن عليك أن تبقي في حساباتك مفهوم سهولة الوصول أو الشمولية accessibility، فليس استخدام الخط المائل في غير سياقه مفيدًا لمستخدمِي قارئات الشاشة أو للأشخاص الذين يستخدِمون أبجديات مختلفة عن اللاتينية. <i>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط مائل مثل كلمات أجنبية، مرادفات، تصميم، مصطلحات تقنية، أفكار… <b>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط ثخين مثل كلمات مفتاحية، أسماء منتجات، جملة افتتاحية.. <u>: يُستخدَم لإيصال معنى يمكن إيصاله تقليديًا بخط تحته سطر مثل الأخطاء الإملائية والنحوية. ملاحظة: يربط الناس جدًا بين الكلمات التي تحتها سطر والروابط التشعبية، لذلك يفضَّل في صفحات الويب أن لا تضع سطرًا تحت محتوى ما لم يكن رابطًا، واستخدم <u> عندما تجد أنّ دلالته صحيحة، وفكّر دائمًا باستخدام CSS لتغيير المحتوى الذي تحته سطر إلى شكل أكثر ملاءمةً لويب، كما يشرح المثال التالي ما يمكن فعله: <!-- مصطلحات علمية --> <p> الطنان ياقوتي الحنجري (<i>Archilochus colubris</i>) هو الطنان الأكثر انتشارًا في أمريكا الشمالية. </p> <!-- كلمات أجنبية --> <p> كانت القائمة بحرًا من الكلمات الغريبة <i lang="uk-latn">vatrushka</i>, <i lang="id">nasi goreng</i> و <i lang="fr">soupe à l'oignon</i>. </p> <!-- أخطاء إملائية غير معروفة --> <p> Someday I'll learn how to <u style="text-decoration-line: underline; text-decoration-style: wavy;">spel</u> better. </p> <!-- تظليل كلمات عند عرض إرشادات --> <ol> <li> <b>شرِّح</b> قطعتين من الخبز </li> <li> <b>أدخل</b> شريحة طماطم وورقة خس بين شريحتي الخبز </li> </ol> خلاصة هذا كل شيء حتى الآن، ومن المفترض أن يكسبك هذا المقال فكرةً جيدةً عن توصيف النصوص وهيكلتها في HTML وأن يعرّفك على أهم العناصر المستخدمة لهذه الأغراض، كما هنالك الكثير من العناصر التي تحمل دلالات مختلفة وسنطَّلع على الكثير منها عند التطرق إلى التنسيق المتقدم للنصوص لاحقًا، وسنستعرض في المقال التالي تفاصيل إنشاء الروابط التشعبية في HTML والذي يُعَدّ العنصر الأهم تقريبًا في ويب. ترجمة -وبتصرف- للمقال HTML text fundamentals. اقرأ أيضًا هيكلة وتوزيع محتوى صفحات الويب HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك ترويسة الصفحة والبيانات الوصفية في HTML
  17. تُعَدّ الترويسة الجزء الذي لا تعرضه متصفحات ويب من الصفحة عند تحميلها، وتضم معلومات مثل عنوان الصفحة وروابط إلى ملفات تنسيق CSS إذا أردت تنسيق محتوى صفحتك، بالإضافة إلى روابط إلى أيقوناتك المفضلة وغيرها من البيانات الوصفية مثل المؤلف والكلمات المفتاحية الهامة التي تصف الصفحة، كما تستخدِم المتصفحات البيانات الموجودة في الترويسة لتصيير صفحة HTML بصورة صحيحة، وسنناقش في هذا المقال كل النقاط السابقة لتقف على ركيزة قوية عندما تعمل مع HTML. لا بد قبل متابعة القراءة أن تطلع على أساسيات HTML التي ذكرناها في مقال تعرّف على لغة HTML. ما هي ترويسة صفحة HTML لنراجع صفحة HTML البسيطة التي عرضناها في المقال السابق: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <p>This is my page</p> </body> </html> الترويسة هي محتوى العنصر <head>، ولا تُعرض محتويات الترويسة على خلاف محتوى العنصر <body> الذي يعرضه المتصفح، لأن وظيفتها هي احتواء معلومات وصفية عن الصفحة، كما إنّ محتوى الترويسة في المثال السابق قليل كما نرى: <head> <meta charset="utf-8"> <title>My test page</title> </head> لكن في الصفحات الكبيرة قد يغدو محتوى الترويسة أكبر، وحاول فتح بعض الصفحات الكبيرة التي تفضلها ثم استخدام أدوات مطوري ويب لتطلع على محتويات الترويسة، كما أننا لا نهدف في هذا المقال إلى استعراض كل ما يمكن وضعه ضمن الترويسة، وإنما لتتعلم استخدام العناصر الرئيسية التي ستجد أن إدراجها ضمن الترويسة ضروري ولتعتاد على ذلك. لنبدأ إذًا. إضافة عنوان للصفحة رأينا سابقًا عمل العنصر <title> والذي يُستخدَم لإضافة عنوان إلى مستند HTML، لكنك قد تخلط بينه وبين العنوان الرئيسي <h1> الذي يُستخدَم في إضافة عنوان رئيسي لمحتوى الصفحة المرئي ضمن العنصر <body>، كما يُدعى العنصر <h1> أحيانًا باسم عنوان الصفحة، وتتلخص الاختلافات بما يلي: يظهر العنصر <h1> على صفحة الويب عند تحميلها ضمن المتصفح، ومن المفترض أن يُستخدَم مرةً واحدةً في الإشارة إلى العنوان الرئيسي لمحتوى الصفحة، أي عنوان قصة أو عنوان رئيسي لنشرة إخبارية وغير ذلك. يُعَدّ العنصر <title> عنصرًا وصفيًا يمثل عنوان مستند HTML برمته وليس عنوان المحتوى المرئي للصفحة. تطبيق: تفحص مثال بسيط لتبدأ العمل عليك تنزيل نسخة عن الملف "title-example.html" من المستودع المخصص على جيت-هاب بإحدى الطريقتين التاليتين: انسخ محتوى الملف ثم الصقه ضمن ملف نصي جديد باستخدام محرِّر النصوص المتوفر لديك، ثم احفظه في مكان مناسب. اضغط الزر "Raw" في صفحة جيت-هاب لتظهر الشيفرة (ربما في نافذة جديدة للمتصفح)، ثم اختر ملف File ثم حفظ الصفحة باسم Save Page As، واختر بعد ذلك مكانًا مناسبًا لتخزين الملف. افتح بعد ذلك الملف باستخدام المتصفح وسيبدو بالصورة التالية: من الواضح الآن أين سيظهر عنوان الصفحة <h1> وعنوان المستند <title>. جرب بعدها أيضًا فتح الشيفرة ضمن المحرِّر النصي وتعديل محتوى هذين العنصرين، ثم حدّث المحتوى المعروض ضمن المتصفح. يُستخدَم محتوى العنصر <title> بطرق أخرى، فإذا أردت مثلًا إضافة علامةً مرجعيةً إلى صفحة، فستجد أنّ هذا المحتوى سيكون الاسم المقترح لهذه العلامة، كما يُستخدَم في نتائج البحث كما سنرى لاحقًا. عنصر البيانات الوصفية تعتمد HTML طريقةً رسميةً في إضافة البيانات الوصفية metadata إلى المستند من خلال العنصر <meta>، كما يمكن أن تُعدَّ بقية الأمور التي نتحدث عنها في هذا المقال بيانات وصفية أيضًا، فهنالك أنواع عديدة للبيانات الوصفية تُضاف إلى جانب <meta> داخل ترويسة الصفحة <head>، لكننا لن نشرحها في هذه المرحلة لأنها ستربكك كثيرًا، وبدلًا من ذلك سنوضِّح عدة أمور قد تشاهدها كثيرًا وذلك لتأخذ فكرةً عنها. تحديد مجموعة المحارف المستخدمة في صفحتك ستجد في مثالنا السابق سطرًا يبدو كما يلي: <meta charset="utf-8"> يحدد هذا العنصر مجموعة المحارف المستخدَمة في ترميز الصفحة، وتُعَدّ المجموعة مجموعةً عالميةً من المحارف وتضم تقريبًا أيّ محرف يُستخدَم في اللغات البشرية المكتوبة، أي ستعرض صفحتك المحتوى المكتوب أيًا كانت لغته، فلهذا من الجيد دائمًا تحديد هذه المجموعة من المحارف في كل صفحة تبنيها، فقد تتعامل صفحتك مع اليابانية والإنجليزية في الوقت نفسه دون أية مشاكل تذكر: قد لا تُعرض الصفحة بالصورة الصحيحة إذا استخدمت مجموعة المحارف ISO-8859-1 مثل مجموعة المحارف الخاصة بالأبجدية اللاتينية: ملاحظة: تصحِّح بعض المتصفحات -مثل كروم- هذه الأخطاء تلقائيًا، لذلك قد لا ترى هذه المشكلة في بعض المتصفحات، ومع ذلك يُفضّل استخدام مجموعة المحارف utf-8 لتلافي أيّ مشاكل محتمَلة في المتصفحات المختلفة. تطبيق: اختبار مجموعات المحارف عُد إلى قالب HTML الذي عملنا عليه قبل قليل في فقرة "إضافة عنوان للصفحة"، وحاول تغيير قيمة السمة charset للعنصر <meta> إلى ISO-8859-1 وستدخِل المحارف اليابانية إلى صفحتك: <p>Japanese example: ご飯が熱い。</p> إضافة اسم المؤلف ووصف للصفحة قد يحتوي العنصر <meta> على سمات مثل name و content: name: يحدِّد نوع البيانات التي يعرضها العنصر <meta>. content: يحدِّد المحتوى الفعلي للعنصر. تساعدك الصفتين السابقتين على إضافة مؤلف الصفحة وتزودك بوصف مناسب لمحتواها، وإليك مثالًا كما يلي: <meta name="author" content="Chris Mills"> <meta name="description" content="The MDN Web Docs Learning Area aims to provide complete beginners to the Web with all they need to know to get started with developing web sites and applications."> يساعدك تحديد اسم المؤلف في نواح عدة منها الحصول على أجوبة عن نقاط في المحتوى أو التواصل معه، كما تتيح بعض أنظمة إدارة المحتوى إمكانية الاستخراج التلقائي لمعلومات مؤلف الصفحة لكي تستخِدمها لاحقًا للاستفسار عن المحتوى أو التواصل، ومن الجيد أيضًا أن تقدِّم وصفًا لصفحتك يضم كلمات مفتاحيةً تتعلق بطبيعة المحتوى الذي تقدمه، فقد يزيد ذلك من فرصة تصدُّر صفحتك لنتائج محركات البحث، إذ يُدعى هذا الأمر تحسين محركات البحث Search Engine Optimization أو SEO اختصارًا. تطبيق: استخدام وصف الصفحة في محركات البحث يُستخدَم وصف الصفحة أيضًا في نتائج محركات البحث، وسنوضِّح ذلك من خلال المثال التالي: انتقل إلى الصفحة الرئيسية لموقع حسوب. اعرض الشيفرة المصدرية للصفحة بالنقر عليها بالزر اليميني للفأرة ثم اختر "عرض الشيفرة المصدرية للصفحة View Page Source" من القائمة. ابحث عن البيانات الوصفية التي تحدِّد وصفًا للصفحة، وستبدو لك كما يلي: <meta name="description" content="في مهمة لتطوير العالم العربي. نعمل لنمكّن الشباب ونفتح مزيدًا من الفرص أمامهم. نحن حسوب."> ابحث الآن عن "حسوب" باستخدام محرك البحث جوجل وستلاحظ كيف استخدم عنوان المستند <title> والبيانات الوصفية <meta> في إظهار نتيجة البحث. ملاحظة: قد تلاحظ في جوجل صفحات فرعيةً مرتبطةً بنتيجة البحث مرتبةً في قائمة أسفل رابط الصفحة الرئيسية، إذ تُدعى هذه الصفحات باسم الروابط الداخلية siteLink ويمكن إدارتها من خلال الأداة Google's webmaster tools لتبدو نتائج البحث أفضل. ملاحظة: لم تعُد بعض البيانات الوصفية مستخدَمةً حاليًا مثل الكلمات المفتاحية "keywords": <meta name="keywords" content="fill, in, your, keywords, here"> التي يُفترض استخدامها من قبل محركات البحث للحكم على مطابقة صفحة ما لمعايير البحث، وتتجاهل محركات البحث حاليًا هذه الكلمات، إذ يضيف بعض المخترقين مئات من هذه الكلمات للحصول على نتائج منحازة إلى الصفحات التي يريدونها. أنواع أخرى من البيانات الوصفية قد تصادف في رحلتك ضمن عالم ويب أنواع أخرى من البيانات الوصفية، والكثير من الميزات التي ستراها في مواقع الويب هي ملكية إبداعية خاصة صُمِّمت لتزويد بعض المواقع بمعلومات محددة مثل مواقع التواصل الاجتماعي، فقد طوّرت الفيسبوك مثلًا بروتوكول بيانات وصفية يُدعى Open Graph Data لكي يقدّم بيانات وصفيةً غنيةً عن موقع ما، فإذا اطلعت على الشيفرة المصدرية لموقع "MDN Web Docs"، فستجد التالي: <meta property="og:image" content="https://developer.mozilla.org/static/img/opengraph-logo.png"> <meta property="og:description" content="The Mozilla Developer Network (MDN) provides information about Open Web technologies including HTML, CSS, and APIs for both Web sites and HTML5 Apps. It also documents Mozilla products, like Firefox OS."> <meta property="og:title" content="Mozilla Developer Network"> سيظهر الرابط مزوّدًا بصورة ووصف للموقع عندما تضع رابطًا إلى موقع "MDN Web Docs" على فيسبوك، مما يزيد غنى تجربة المستخدِم: يستخدِم تويتر بروتوكولًا وصفيًا مماثلًا يُدعى Twitter Cards يقدِّم محتوًى غنيًا عن عنوان URL محدد عندما يُعرض على twitter.com، وإليك مثالًا كما يلي: <meta name="twitter:title" content="Mozilla Developer Network"> إضافة أيقونات مخصصة إلى صفحتك تستطيع الإشارة إلى أيقونات مخصصة تُعرض في مناسبات محددة من خلال البيانات الوصفية لكي تُغني تصميم صفحتك، وأكثر هذه الأيقونات استخدامًا هي الأيقونة المفضلة favicon والتي تُستخدم لتدل على صفحتك عندما تنشئ علامةً مرجعيةً إليها في متصفحك أو عندما تضيفها إلى المفضلة، ولا يزال استخدام هذه الأيقونة شائعًا منذ سنوات عديدة، فهي عبارة عن أيقونة مربعة بعرض 16 بكسل تشاهدها في أماكن عدة مثل عناوين نوافذ المتصفح وإلى جانب العلامات المرجعية للصفحات، كما يمكنك إضافة الأيقونة المفضلة على صفحتك كما يلي: احفظ الأيقونة المطلوبة باللاحقة ico. في المجلد نفسه الذي يحتوي على الصفحة index.html، وقد تدعم بعض المتصفحات لواحق أخرى مثل gif. أو png.، لكن يضمن لك استخدام اللاحقة السابقة العمل على أيّ متصفح رجوعًا إلى إنترنت أكسبلورر 6. أضف السطر التالي إلى ترويسة ملف HTML: <link rel="icon" href="favicon.ico" type="image/x-icon"> إليك مثالًا عن أيقونة مفضلة إلى جانب علامة مرجعية: ستجد أيضًا الكثير من أنواع الأيقونات حاليًا، فإذا فتحت الشيفرة المصدرية لموقع "MDN Web Docs"، فستجد التالي: <!-- third-generation iPad with high-resolution Retina display: --> <link rel="apple-touch-icon-precomposed" sizes="144x144" href="https://developer.mozilla.org/static/img/favicon144.png"> <!-- iPhone with high-resolution Retina display: --> <link rel="apple-touch-icon-precomposed" sizes="114x114" href="https://developer.mozilla.org/static/img/favicon114.png"> <!-- first- and second-generation iPad: --> <link rel="apple-touch-icon-precomposed" sizes="72x72" href="https://developer.mozilla.org/static/img/favicon72.png"> <!-- non-Retina iPhone, iPod Touch, and Android 2.1+ devices: --> <link rel="apple-touch-icon-precomposed" href="https://developer.mozilla.org/static/img/favicon57.png"> <!-- basic favicon --> <link rel="icon" href="https://developer.mozilla.org/static/img/favicon32.png"> تستخدَم الأيقونات السابقة لإظهار أيقونات عالية الدقة عندما يُحفظ موقع ويب على الشاشة الرئيسية لجهاز آيباد. لا تقلق حيال هذا الكم الكبير من الأيقونات وضرورة إدراجها الآن، فهي ميزة متقدِّمة نوعًا ما ولا حاجة لأن تعرف عنها الكثير حتى تكمل معنا المسيرة خلال هذه المقالات، فالغاية الحقيقية من هذا العرض هو إطلاعك على أمور مثل هذه قد تصادفها أثناء تصفحك للشيفرة المصدرية للمواقع. ملاحظة: في حال استخدمت سياسة خصوصية المحتوى Content Security Policy أو CSP اختصارًا في موقعك، فستُطبق هذه السياسة على أيقونة المفضلة favico، فإذا لم يُحمّل المتصفح هذه الأيقونة، فتأكد من أنّ التوجيه img-src العائد للترويسة Content-Security-Policy لا يعيق الوصول إلى الأيقونة. تطبيق تنسيقات CSS وشيفرة جافاسكربت على لغة HTML تستخدِم معظم المواقع في أيامنا هذه لغة CSS لتحسين مظهر الصفحة وجافاسكربت لإكساب الصفحة قدرات تفاعلية مع المستخدِم مثل مشغلات الفيديو والخرائط والألعاب وغيرها، إذ تُطبَّق هذه الأمور على الصفحات باستخدام العنصر <link> والعنصر <script>: <link>: ينبغي وضع هذا العنصر ضمن ترويسة الملف <head> ويمتلك صفتين هما "rel="stylesheet والتي تشير إلى أنّ الرابط سيستخدَم لتنسيق الصفحة، والسمة href التي تحدد مسارًا إلى ملف التنسيق: <link rel="stylesheet" href="my-css-file.css"> <script>: ينبغي أن يكون أيضًا ضمن الترويسة، كما ينبغي أن يمتلك السمة src التي تحتوي على مسار ملف جافاسكربت الذي تريد استخدامه، بالإضافة إلى السمة defer التي تدفع بالمتصفح إلى تحميل ملف الشيفرة بعد الانتهاء من تفسير شيفرة لغة HTML، وللأمر أهميته في ضمان تحميل كل عناصر لغة HTML قبل تشغيل شيفرة جافاسكربت، وبالتالي لن تقع أخطاء نتيجة محاولة جافاسكربت الوصول إلى عناصر لغة HTML غير موجودة بعد في الصفحة، وهناك طرق عدة للتحكم بتحميل شيفرة جافاسكربت ضمن الصفحات، لكنه الأسلوب الأكثر فعاليةً واستخدامًا في المتصفحات الحديثة: <script src="my-js-file.js" defer></script> ملاحظة: قد يبدو لك العنصر <script> فارغًا لكنه ليس كذلك، كما ينبغي استخدام وسم النهاية، إذ نستطيع أيضًا كتابة الشيفرة داخل هذا العنصر بدلًا من الإشارة إلى ملف خارجي. تطبيق: استخدام لغة CSS ولغة جافاسكربت في صفحتنا لبدء العمل على هذا التطبيق، أحضر نسخًا عن الملفات meta-example.html و script.js و style.css وخزِّنها على حاسوبك في المجلد نفسه، ثم تأكد من حفظها تمامًا بالأسماء واللواحق -أي الامتدادات- السابقة. افتح ملف HTML في المتصفح وفي محرِّر النصوص معًا. أضف العنصر <link> إلى شيفرة HTML لكي تشير إلى ملفَي CSS وجافاسكربت كلًا على حدة كما فعلنا سابقًا. إذا نجحت في تنفيذ الأمر، فسترى أنّ الأمور قد تغيرت ليعرض المتصفح بعد تحديثه ما يلي: أضافت شيفرة جافاسكربت قائمةً فارغةً إلى الصفحة، فإذا نقرت الآن في أيّ مكان من الصفحة، فستظهر لك رسالةً منبثقةً تسألك إضافة بعض الكلمات لتكوين عنصر قائمة جديد، وعند النقر على الزر "موافق OK"، سيُضاف عنصر جديد إلى القائمة يحمل النص الذي أدخلته؛ أما عندما تنقر على عنصر قائمة موجود، فستظهر لك رسالة منبثقة لتغيير محتوى هذا العنصر. غيّرت CSS الخلفية إلى اللون الأخضر وكبّرت حجم النص، كما غيّرت في تنسيق بعض العناصر التي أضافتها جافاسكربت، أي الشريط الأحمر والإطار الأسود للقائمة التي ولّدتها جافاسكربت. ملاحظة: لو وجدت نفسك تائهًا في هذا التمرين ولم تتمكن من إحضار ملفات CSS وجافاسكربت، فحاول التحقق من الملف css-and-js.html. ضبط اللغة الرئيسية للصفحة لابد أخيرًا من الإشارة إلى إمكانية وضرورة تحديد لغة الصفحة، فيمكن إنجاز الأمر من خلال السمة lang للعنصر الجذري <html>: <html lang="en-US"> للأمر فوائد عدة منها فهرسة موقعك بفعالية من قِبَل محركات البحث، إذ يسمح ذلك بظهور الموقع في النتائج المرتبطة بهذه اللغة مثلًا، كما يساعد ذلك كثيرًا الأشخاص ذوي الإعاقات البصرية الذين يستخدِمون قارئات الشاشة، فالكلمة "six" مثلًا موجودة في الفرنسية والإنكليزية لكنها تلفظ بصورة مختلفة، كما يمكنك أيضًا تحديد أجزاء من صفحتك لتعرض بلغة مختلفة، إذ يمكنك مثلًا اختيار اللغة لقسم فقط من الصفحة كما يلي: <p>Japanese example: <span lang="ja">ご飯が熱い。</span>.</p> ملاحظة: تُحدَّد رموز اللغات بواسطة المعيار ISO 639-1. خلاصة بهذا نكون قد وصلنا إلى نهاية هذا العرض التمهيدي لترويسة HTML، وهنالك الكثير مما يمكن إنجازه أيضًا لكن ستكون الإطالة في الأمر مزعجةً ومربكةً في هذه المرحلة، فكل ما نريده هو إيصال الأفكار الأكثر شيوعًا حول الموضوع، وسنلقي نظرةً في المقال القادم على أساسيات نصوص HTML. ترجمة -وبتصرف- للمقال What’s in the head? Metadata in HTML. اقرأ أيضًا HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك HTML و CSS للمبتدئين: مقدمة إلى تنسيقات CSS خمسة أشياء عليك معرفتها عن HTML5 مدخل إلى البيانات الوصفية (microdata) في HTML5
  18. نستكشف في هذا المقال تطبيقات نود Node التي تستخدم قواعد بيانات علاقية، وسنبني خلال تقدمنا في المقال واجهةً خلفيةً تستخدم قاعدة بيانات علاقية ليتعامل معها تطبيق الملاحظات الذي عملنا عليه سابقًا. ولإكمال هذا المقال، لا بُدّ من تعميق معرفتك بقواعد البيانات العلاقية ولغة SQL. يمكنك الاطلاع على سلسلة مقالات المرجع المتقدم إلى SQL في أكاديمية حسوب، وكذلك الاطلاع على توثيق لغة SQL في موسوعة حسوب. ستجد 24 تمرينًا في هذا المقال وعليك أن تنجزها جميعًا لتكمل الدورة التعليمية، تُسلّم الحلول إلى منظومة تسليم الحلول كما هو الحال في الأقسام السابقة ما عدا الأقسام من 0 إلى 7 والتي تُسلّم حلول التمارين فيها إلى مكان آخر. إيجابيات وسلبيات قواعد بيانات المستندات استخدمنا قاعدة البيانات MongoDB في جميع المقال السابقة، وهي قاعدة بيانات مستندات، ومن أهم ميزاتها أنها لا تملك أي مخططات، أي أنّ لها معرفةٌ محدودةٌ بطبيعة البيانات المخزنة في كل مجموعة. يتواجد مخطط قاعدة البيانات schema في شيفرة البرنامج فقط، إذ يفسِّر البيانات بطريقة معينة، كأن يحدد أن بعض الحقول هي مراجعٌ لكائنات في مجموعةٍ أخرى. رأينا في المثال التطبيقي الذي يضم قاعدة بيانات تخزن الملاحظات والمستخدمين (في القسمين 3 و 4)، إذ تُخزِّن الملاحظات "notes" على النحو التالي: [ { "_id": "600c0e410d10256466898a6c", "content": "HTML is easy" "date": 2021-01-23T11:53:37.292+00:00, "important": false "__v": 0 }, { "_id": "600c0edde86c7264ace9bb78", "content": "CSS is hard" "date": 2021-01-23T11:56:13.912+00:00, "important": true "__v": 0 }, ] وتُخزّن أيضًا بيانات المستخدمين في المجموعة "users" على النحو التالي: [ { "_id": "600c0e410d10256466883a6a", "username": "mluukkai", "name": "Matti Luukkainen", "passwordHash" : "$2b$10$Df1yYJRiQuu3Sr4tUrk.SerVz1JKtBHlBOARfY0PBn/Uo7qr8Ocou", "__v": 9, notes: [ "600c0edde86c7264ace9bb78", "600c0e410d10256466898a6c" ] }, ] تعرِف MongoDB نوع البيانات في الحقول التي تُخزِّن كيانات البيانات، لكنها لا تعرف إلى أي مجموعة من الكيانات يشير المعرِّف المميز الذي سجّله المستخدم، ولا تهتم أيضًا بالحقول التي تمتلكها الكيانات المُخزنة في مجموعات البيانات؛ لهذا تترك MongoDB أمر التحقق من صحة المعلومات المخزّنة في قاعدة البيانات إلى المبرمج. هناك طبعًا إيجابيات وسلبيات لعدم وجود مخطط لقاعدة البيانات، وإحدى الإيجابيات هي المرونة التي تضيفها الميزة اللا إدارية في مخطط البيانات، إذ سيسرّع انتفاء الحاجة إلى تعريف مخطط على مستوى قاعدة البيانات من عمل المطوّر في حالات عدة، كما أنه أسهل من تعريف وتعديل المخطط في حالات أخرى؛ في حين تتعلق المشاكل الناجمة عن عدم وجود المخطط غالبًا بسهولة الوقوع في الأخطاء، فكل شيء ملقىً على عاتق المبرمج، وليس لقاعدة البيانات في هذه الحالة طريقةً للتحقق من نزاهتها honest، أي إذا احتوت كل الحقول الإجبارية على القيم الصحيحة، أو إذا أشارت الحقول ذات النوع المرجعي إلى كيانات من النوع الصحيح عمومًا وهكذا. تعتمد قاعدة البيانات العلاقية التي ستكون محور حديثنا في هذا المقال بشدة على وجود مخطط، وتكاد تكون الإيجابيات والسلبيات في استخدامها على نقيض تلك المتعلقة بقاعدة بيانات المستندات. يعود السبب الرئيسي في استخدام قاعدة البيانات MongoDB تحديدًا في الأقسام السابقة إلى طبيعتها التي لا تحتاج مخططات، الأمر الذي يسهّل استخدامها بالنسبة لذوي المعرفة المحدودة بقواعد البيانات العلاقية، لكن نفترض بالنسبة لكل الحالات التي عرضناها في منهاجنا أن القواعد العلاقية هي الأنسب. قاعدة بيانات التطبيق نحتاج في تطبيقنا إلى قاعدة بيانات علاقية، وهناك خيارات عديدة، لكننا سنستخدم حاليًا الحل الأكثر شعبيةً والمفتوح المصدر PostgreSQL؛ ويمكنك تثبيت Postgres (هكذا تُدعى قاعدة البيانات هذه غالبًا) على جهازك إذا أردت؛ والأسهل من هذا هو استخدامها بمثابة خدمة سحابية، مثل ElephantSQL. بإمكانك أيضًا الاستفادة مما جاء في جزئية سابقة من السلسلة لاستخدام Postgres محليًا من خلال دوكر Docker. اخترنا في هذا المقال الاستفادة من إمكانية إنشاء قاعدة بيانات Postgres للتطبيق على خدمة "Heroku" السحابية التي ألفنا العمل معها في المقالين 3 و4. سنبني في الجزء النظري من هذا المقال نسخةً مرتبطة بقاعدة البيانات Postgres عن الواجهة الخلفية لتطبيق تخزين الملاحظات الذي بنيناه في المقالين 3 و4. لننشئ أولًا مجلدًا مناسبًا ضمن تطبيق Heroku ونضيف إليه قاعدة بيانات، ثم نستخدم الأمر heroku config للحصول على "السلسلة النصية connect string" اللازمة للاتصال مع القاعدة: heroku create # heroku يعيد اسم التطبيق للتطبيق الذي أنشأته في heroku addons:create heroku-postgresql:hobby-dev -a <app-name> heroku config -a <app-name> === cryptic-everglades-76708 Config Vars DATABASE_URL: postgres://<username>:<password>@<host-of-postgres-addon>:5432/<db-name> يُعد الوصول إلى قاعدة البيانات مباشرةً من الأمور الأساسية التي ينبغي الانتباه إليها تحديدًا في قواعد البيانات العلاقية، نظرًا لوجود أساليب عدة لتنفيذ الأمر، ووجود عدة واجهات مستخدم رسومية، مثل pgAdmin، لكن سنستخدم أداة سطر الأوامر psql الخاصة بالقاعدة Postgres. يمكن الوصول إلى قاعدة البيانات من خلال تنفيذ أمر psql على خادم Heroku على النحو التالي (لاحظ كيف تعتمد معاملات الأمر على عنوان url للاتصال بقاعدة بيانات Heroku): heroku run psql -h <host-of-postgres-addon> -p 5432 -U <username> <dbname> -a <app-name> بعد إدخال كلمة المرور، سننفذ أمر psql الأساسي وهو d\، الذي يخبرك بمحتوى قاعدة البيانات: Password for user <username>: psql (13.4 (Ubuntu 13.4-1.pgdg20.04+1)) SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, bits: 256, compression: off) Type "help" for help. username=> \d Did not find any relations. وكما ترى، لا يوجد شيء حاليًا في قاعدة البيانات. لننشئ إذًا جدولًا للملاحظات: CREATE TABLE notes ( id SERIAL PRIMARY KEY, content text NOT NULL, important boolean, date time ); يمكننا ملاحظة بعض النقاط: يُعرِّف العمود id مفتاحًا أساسيًا primary key، ويعني ذلك أن قيم هذا العمود لا بُدّ أن تكون فريدةً لكل سطر في الجدول ولا ينبغي أن تكون قيمتها فارغة. يُعرّف نوع العمود id على أنه تسلسلي SERIAL، وهو ليس نوع حقيقي بل تمثيل لعمود ذي قيم صحيحة تعيّن Postgres قيمه الفريدة تلقائيًا وتزيد هذه القيمة بمقدار "واحد" عند إنشاء سطر جديد. يكون العمود المُسمى content من النوع النصي ويُعُرِّف كي تعيّن له قيمةٌ في كل سطر. لنلق نظرةً على الوضع الآن من خلال الطرفية، ولننفّذ أولًا الأمر d\ الذي يعرض جداول قاعدة البيانات: username=> \d List of relations Schema | Name | Type | Owner --------+--------------+----------+---------------- public | notes | table | username public | notes_id_seq | sequence | username (2 rows) إذ تُنشئ Postgres إضافةً إلى الجدول notes جدولًا فرعيًا يُدعى notes_id_seq يتتبع القيم المُسندة إلى العمود id عند إنشاء ملاحظة جديدة. وبتنفيذ الأمر d notes\ يمكننا رؤية طريقة تعريف الجدول notes: username=> \d notes; Table "public.notes" Column | Type | Collation | Nullable | Default -----------+------------------------+-----------+----------+----------------------------------- id | integer | not null | nextval('notes_id_seq'::regclass) content | text | | not null | important | boolean | | | | date | time without time zone | | | | Indexes: "notes_pkey" PRIMARY KEY, btree (id) إذًا، للعمود id قيم افتراضية تُستخرج من تنفيذ الدالة الداخلية nextval في Postgres. لنُضِف بعض المحتوى إلى الجدول: insert into notes (content, important) values ('Relational databases rule the world', true); insert into notes (content, important) values ('MongoDB is webscale', false); لنرى الآن كيف يبدو المحتوى الذي أضفناه: username=> select * from notes; id | content | important | date ----+-------------------------------------+-----------+------ 1 | relational databases rule the world | t | 2 | MongoDB is webscale | f | (2 rows) إذا حاولنا تخزين البيانات في قاعدة البيانات دون العودة إلى مخطط، فلن ينجح الأمر، لأنّ قيم الأعمدة الإجبارية لا بُد أن تكون موجودةً: username=> insert into notes (important) values (true); ERROR: null value in column "content" of relation "notes" violates not-null constraint DETAIL: Failing row contains (9, null, t, null). ولا يمكن أن تكون قيمة العمود من النوع الخاطئ: username=> insert into notes (content, important) values ('only valid data can be saved', 1); ERROR: column "important" is of type boolean but expression is of type integer LINE 1: ...tent, important) values ('only valid data can be saved', 1); ^ ولا يمكن القبول بأعمدة غير موجودة في المخطط: username=> insert into notes (content, important, value) values ('only valid data can be saved', true, 10); ERROR: column "value" of relation "notes" does not exist LINE 1: insert into notes (content, important, value) values ('only ... سننتقل تاليًا إلى طريقة الدخول إلى قاعدة البيانات من التطبيق. تطبيق Node يستخدم قاعدة بيانات علاقية لنشغّل التطبيق كما جرت العادة من خلال التعليمة npm init ونثبّت "nodemon" على أنه اعتمادية تطوير development dependency وكذلك اعتماديات زمن التشغيل التالية: npm install express dotenv pg sequelize نجد من بين هذه الاعتماديات sequelize، وهي المكتبة التي يمكننا من خلالها استخدام Postgres؛ وتنتمي هذه المكتبة إلى مكتبات الربط العلاقي للكائنات Object relational mapping -أو اختصارًا ORM-، التي تسمح بتخزين كائنات جافا سكربت JavaScript في قاعدة بيانات علاقية دون استخدام لغة SQL بحد ذاتها وبصورةٍ مشابهة للمكتبة "Mongoose" التي استخدمناها مع قاعدة البيانات MongoDB. لنختبر الآن قدرتنا على الاتصال الناجح بقاعدة البيانات، لهذا أنشئ الملف index.js وأضف إليه الشيفرة التالية: require('dotenv').config() const { Sequelize } = require('sequelize') const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }) const main = async () => { try { await sequelize.authenticate() console.log('Connection has been established successfully.') sequelize.close() } catch (error) { console.error('Unable to connect to the database:', error) } } main() ينبغي تخزين سلسلة الاتصال النصية connect string بقاعدة البيانات التي أظهرها الأمر heroku config في ملفٍ له الامتداد env.، وينبغي أن تكون مشابهةً لما يلي: $ cat .env DATABASE_URL=postgres://<username>:<password>@ec2-54-83-137-206.compute-1.amazonaws.com:5432/<databasename> لنتحقق من نجاح الاتصال: $ node index.js Executing (default): SELECT 1+1 AS result Connection has been established successfully. إذا عمل الاتصال، يمكننا حينها تشغيل الاستعلام الأول. لنعدّل البرنامج على النحو التالي: require('dotenv').config() const { Sequelize, QueryTypes } = require('sequelize') const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); const main = async () => { try { await sequelize.authenticate() const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT }) console.log(notes) sequelize.close() } catch (error) { console.error('Unable to connect to the database:', error) } } main() ينبغي أن يطبع تنفيذ البرنامج ما يلي: Executing (default): SELECT * FROM notes [ { id: 1, content: 'Relational databases rule the world', important: true, date: null }, { id: 2, content: 'MongoDB is webscale', important: false, date: null } ] وعلى الرغم من كون Sequelize مكتبة ربط علاقي للكائنات ORM، أي أنها تحتاج إلى قليلٍ فقط من شيفرة SQL التي تكتبها بنفسك، فقد استخدمنا شيفرة SQL مباشرةً مع تابع Sequelize الذي يُدعى query. طالما أنّ كل شيء يعمل على ما يرام، لنحوّل التطبيق إلى تطبيق ويب. require('dotenv').config() const { Sequelize, QueryTypes } = require('sequelize') const express = require('express') const app = express() const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); app.get('/api/notes', async (req, res) => { const notes = await sequelize.query("SELECT * FROM notes", { type: QueryTypes.SELECT }) res.json(notes) }) const PORT = process.env.PORT || 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) يبدو أن التطبيق يعمل جيدًا. لنتحول الآن إلى استخدام Sequelize بدلًا من SQL. النماذج في Sequelize يُمثَّل كل جدول من جداول قاعدة البيانات عند استخدام Sequelize بنموذج model، والذي يُعدُّ صنف JavaScript الخاص بالجدول. لنعرِّف الآن النموذج Note المتعلق بالجدول notes من التطبيق وذلك بتغيير الشيفرة على النحو التالي: require('dotenv').config() const { Sequelize, Model, DataTypes } = require('sequelize') const express = require('express') const app = express() const sequelize = new Sequelize(process.env.DATABASE_URL, { dialectOptions: { ssl: { require: true, rejectUnauthorized: false } }, }); class Note extends Model {} Note.init({ id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, content: { type: DataTypes.TEXT, allowNull: false }, important: { type: DataTypes.BOOLEAN }, date: { type: DataTypes.DATE } }, { sequelize, underscored: true, timestamps: false, modelName: 'note' }) app.get('/api/notes', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) const PORT = process.env.PORT || 3001 app.listen(PORT, () => { console.log(`Server running on port ${PORT}`) }) لا يوجد شيء جديد في تعريف النموذج، فكل عمودٍ له نوع معرَّف، إضافةً إلى خاصيات أخرى عند الضرورة كأن يكون العمود مفتاحًا أساسيًا للجدول. يضم المعامل الثاني في تعريف النموذج السمة sequelize إضافةً إلى غيرها من معلومات التهيئة. كما عرّفنا أن الجدول لا ينبغي أن يضم أعمدة لها بصمات زمنية timestamps، مثل created_atأنشئ عند و updated_at حُدِّث عند. عرّفنا أيضًا القيمة true إلى underscored، والتي تعني أن أسماء الجدول مشتقةٌ من أسماء النموذج لكن بصيغة الجمع وباستخدام تنسيق الأفعى snake_case، الذي تُوضع فيه الشرطة السفلية بدلًا من الفراغات بين الكلمات وتُكتب بأحرف صغيرة؛ ويعني هذا عمليًا أنه إذا كان اسم النموذج "Note"، فسيكون اسم الجدول المقابل "notes". ولو كان اسم النموذج مكونًا من جزئين مثل "StudyGroup"، فسيكون اسم الجدول المقابل "study_groups". وبدلًا من الاستدلال على أسماء الجداول تلقائيًا، تسمح لك المكتبة Sequelize بتعريف أسماء الجداول صراحةً أيضًا. تُطبق سياسة التسمية هذه على أسماء الأعمدة أيضًا. فلو عرّفنا أن ملاحظةً ما سترتبط بعام الإنشاء creationYear أي بالمعلومات المتعلقة بعام إنشاء الملاحظة، سنعرّفها في النموذج على النحو التالي: Note.init({ // ... creationYear: { type: DataTypes.INTEGER, }, }) سيكون اسم العمود المقابل في قاعدة البيانات creation_year، لكن الإشارة إلى العمود في الشيفرة تكون دائمًا باستخدام تنسيق النموذج نفسه أي طريقة سنام الجمل camel case. وعرّفنا كذلك السمة modelName التي أُسندت إليها القيمة note، وستكون القيمة الافتراضية لاسم النموذج بحرف بداية كبير "Note"، وسترى أنّ هذا أكثر ملاءمةً لاحقًا. يسهل التعامل مع قاعدة البيانات من خلال واجهة الاستعلام التي يؤمنها النموذج، إذ يعمل التابع works تمامًا كما يوحي اسمه: app.get('/api/notes', async (req, res) => { const notes = await Note.findAll() res.json(notes) }) تخبرك الطرفية أن التابع ()Note.findAll يُنفِّذ الاستعلام التالي: Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note"; سننجز تاليًا وصلة endpoint بغرض إنشاء ملاحظات جديدة: app.use(express.json()) // ... app.post('/api/notes', async (req, res) => { console.log(req.body) const note = await Note.create(req.body) res.json(note) }) تُضاف الملاحظة الجديدة باستدعاء التابع create الذي يوفّره النموذج "Note" بعد تمرير كائن يعرِّف قيم الأعمدة لسطر الملاحظة الجديدة على أنه وسيطٌ لهذا التابع. وبإمكانك أيضًا حفظ قاعدة البيانات بتنفيذ التابع build أولًا لإنشاء كائن نموذج من البيانات المطلوبة، ثم استدعاء التابع save وذلك بدلًا من التابع create: const note = Note.build(req.body) await note.save() لا يؤدي استدعاء التابع build إلى حفظ الكائن في قاعدة البيانات، وبالتالي من الممكن تعديله قبل تنفيذ أمر الحفظ الفعلي: const note = Note.build(req.body) note.important = true await note.save() يُعد استخدام التابع create في المثال السابق أكثر ملاءمةً لما نريده من التطبيق، لذلك سنلتزم باستخدامه من الآن فصاعدًا. إذا لم يكن الكائن المُنشأ صالحًا، ستظهر رسالة خطأ، لذلك إذا حاولت إضافة ملاحظة جديدة دون محتوى، ستخفق العملية وتكشف لك الطرفية عن السبب بأن "المحتوى لا يمكن أن يكون فارغًا": (node:39109) UnhandledPromiseRejectionWarning: SequelizeValidationError: notNull Violation: Note.content cannot be null at InstanceValidator._validate (/Users/mluukkai/opetus/fs-psql/node_modules/sequelize/lib/instance-validator.js:78:13) at processTicksAndRejections (internal/process/task_queues.js:93:5) لنضف آليةً بسيطةً لاصطياد الأخطاء عند إضافة ملاحظةٍ جديدة: app.post('/api/notes', async (req, res) => { try { const note = await Note.create(req.body) return res.json(note) } catch(error) { return res.status(400).json({ error }) } }) التمرينات 13.1 إلى 13.3 سنبني في هذه التمرينات واجهة خلفية لتطبيق مدوّنات يشابه ما فعلنا في القسم 4، وينبغي أن يتوافق مع الواجهة الأمامية التي أنشأناها في القسم 5 باستثناء معالجة الأخطاء. سنضيف أيضًا ميزات مختلفة إلى الواجهة الخلفية لا يمكن للواجهة الأمامية في القسم 5 التعامل معها. التمرين13.1 أنشئ مستودع غيت هاب GitHub مخصص للتطبيق، ثم أنشئ تطبيق Heroku خاص به إضافةً إلى قاعدة بيانات Postgres، وتأكد من قدرتك على تأسيس اتصال بين التطبيق وقاعدة البيانات. التمرين 13.2 أنشئ باستخدام سطر الأوامر الجدول "blogs" الذي يضم الأعمدة التالية: id: يمثل قيمةً فريدةً تزداد باستمرار. author: قيمة نصية. url: قيمة نصية لا يمكن أن تكون فارغة. title: قيمة نصية لا يمكن أن تكون فارغة. likes: قيمة صحيحة تبدأ من الصفر افتراضيًا. أضف مدونتين على الأقل إلى قاعدة البيانات. احفظ بعد ذلك الأوامر التي استخدمتها في ملف يُدعى "commands.sql" في جذر التطبيق. التمرين 13.3 أضف إلى تطبيقك وظيفة طباعة المدوّنات الموجودة في قاعدة البيانات باستخدام سطر الأوامر كما في المثال التالي: $ node cli.js Executing (default): SELECT * FROM blogs Dan Abramov: 'On let vs const', 0 likes Laurenz Albe: 'Gaps in sequences in PostgreSQL', 0 likes إنشاء جداول قاعدة البيانات تلقائيا يوجد في تطبيقنا الحالي جانب غير مرغوب فهو يفترض وجود قاعدة بيانات مع المخطط المعين، أي أنّ الجدول "notes" قد أُنشأ بتنفيذ الأمر create table. تُخزّن شيفرة البرنامج على غيت هاب GitHub، لذلك من المنطقي تخزين الأوامر التي أنشأت بها قاعدة البيانات ضمن سياق الشيفرة لكي يبقى مخطط قاعدة البيانات نفسه كما تتوقعه شيفرة البرنامج. يُمكن للمكتبة أن تولّد تلقائيًا مخططًا انطلاقًا من تعريف النموذج من خلال التابع sync. لندمر الآن قاعدة البيانات الموجودة من خلال أوامر الطرفية على النحو التالي: drop table notes; تأكد من تدمير القاعدة بتنفيذ الأمر d\: username=> \d Did not find any relations. لن يعمل التطبيق الآن، لهذا سننفِّذ الأمر التالي مباشرةً بعد تعريف النموذج "Note": Note.sync() عندما يعمل التطبيق سيظهر ما يلي على شاشة الطرفية: Executing (default): CREATE TABLE IF NOT EXISTS "notes" ("id" SERIAL , "content" TEXT NOT NULL, "important" BOOLEAN, "date" TIMESTAMP WITH TIME ZONE, PRIMARY KEY ("id")); وهكذا، عندما يعمل التطبيق، تُنفَّذ التعليمة: CREATE TABLE IF NOT EXISTS "notes"... التي تُنشئ الجدول "notes" إن لم يكن موجودًا. خيارات أخرى لنكمل تطبيقنا بإضافة عدة خيارات أخرى. بإمكاننا البحث عن ملاحظة محددة باستخدام التابع findByPk لأنه يبحث ضمن قيم id التي تمثّل المفتاح الرئيسي لقاعدة البيانات: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { res.json(note) } else { res.status(404).end() } }) يؤدي البحث عن ملاحظة محددة إلى تنفيذ أمر SQL التالي: Executing (default): SELECT "id", "content", "important", "date" FROM "notes" AS "note" WHERE "note". "id" = '1'; في حال عدم وجود أية ملاحظات، سيعيد البحث القيمة null (لا شيء)، وسيعطي رمز الحالة المناسب. تُعدَّل الملاحظة على النحو التالي، ولا يمكن تعديل سوى الحقل important لأن الواجهة الأمامية لا تحتاج أي شيء آخر: app.put('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { note.important = req.body.important await note.save() res.json(note) } else { res.status(404).end() } }) يُستخرج الكائن المتعلق بسطر قاعدة البيانات باستخدام التابع findByPk، ويُعدّل بعدها الكائن وتُخزَّن النتيجة باستدعاء التابع save. بإمكانك إيجاد شيفرة التطبيق كاملةً في المستودع المخصص على GitHub ضمن الفرع "part13-1". طباعة الكائن الذي تعيده Sequelize على الطرفية تُعد الأداة console.log أفضل أدوات مبرمجي JavaScript إذ يمكّنهم استعمالها المتكرر من التقاط أسوأ الثغرات. لهذا سنطبع الملاحظات على الطرفية: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { console.log(note) res.json(note) } else { res.status(404).end() } }) لاحظ أنّ النتيجة النهائية ليست ما نتوقعه تمامًا: note { dataValues: { id: 1, content: 'Notes are attached to a user', important: true, date: 2021-10-03T15:00:24.582Z, }, _previousDataValues: { id: 1, content: 'Notes are attached to a user', important: true, date: 2021-10-03T15:00:24.582Z, }, _changed: Set(0) {}, _options: { isNewRecord: false, _schema: null, _schemaDelimiter: '', raw: true, attributes: [ 'id', 'content', 'important', 'date' ] }, isNewRecord: false } فكل الأشياء التي يحتويها الكائن إضافةً إلى البيانات قد طُبعت على الطرفية، لهذا يمكن الوصول إلى النتيجة المرجوة باستدعاء التابع toJSON العائد إلى كائن النموذج: app.get('/api/notes/:id', async (req, res) => { const note = await Note.findByPk(req.params.id) if (note) { console.log(note.toJSON()) res.json(note) } else { res.status(404).end() } }) ها هي النتيجة الآن كما هو متوقع. { id: 1, content: 'MongoDB is webscale', important: false, date: 2021-10-09T13:52:58.693Z } في الحالة التي نريد فيها طباعة مجموعة من الكائنات، لن يعمل التابع toJSON مباشرةً، بل يجب أن يُستدعى بصورةٍ مستقلة لكل كائن في المجموعة: router.get('/', async (req, res) => { const notes = await Note.findAll() console.log(notes.map(n=>n.toJSON())) res.json(notes) }) وتبدو النتيجة على النحو التالي: [ { id: 1, content: 'MongoDB is webscale', important: false, date: 2021-10-09T13:52:58.693Z }, { id: 2, content: 'Relational databases rule the world', important: true, date: 2021-10-09T13:53:10.710Z } ] وربما من الأفضل أن تحوّل المجموعة إلى تنسيق JSON لطباعتها باستخدام التابع JSON.stringify: router.get('/', async (req, res) => { const notes = await Note.findAll() console.log(JSON.stringify(notes)) res.json(notes) }) تُعد هذه الطريقة أكثر ملاءمةً خاصةً إذا احتوت الكائنات على كائنات أخرى، كما أنها مفيدةٌ لتنسيق الكائنات على الشاشة بطريقة تُسهِّل القراءة، ويُنفَّذ ذلك من خلال الأمر التالي: console.log(JSON.stringify(notes, null, 2)) وتبدو النتيجة على النحو التالي: [ { "id": 1, "content": "MongoDB is webscale", "important": false, "date": "2021-10-09T13:52:58.693Z" }, { "id": 2, "content": "Relational databases rule the world", "important": true, "date": "2021-10-09T13:53:10.710Z" } ] التمرين 13.4 حوّل تطبيقك إلى تطبيق ويب يدعم العمليات التالية: الحصول على كل المدونات: GET api/blogs. إضافة مدوّنة جديدة: POST api/blogs. حذف مدوّنة: DELETE api/blogs/:id. ترجمة -وبتصرف- للفصل Using relational databases with Sequelize من سلسلة Deep Dive Into Modern Web Development. اقرأ أيضًا المقال السابق: أساسيات تنسيق الحاويات مقدمة عن قواعد البيانات التعامل مع قواعد البيانات مفاهيم نموذج البيانات العلائقية RDM الأساسية المهمة في تصميم قواعد البيانات مقارنة بين أنظمة إدارة قواعد البيانات العلاقية: SQLite مع MySQL مع PostgreSQL
  19. يغطِّي هذا المقال المبادئ الأكثر بساطة لتنطلق، إذ يعرِّف مفهوم العناصر elements والسمات attributes وغيرها من المفاهيم المهمة ويعرض كيفية استخدامها، ثم ينتقل إلى عرض هيكلية صفحة مكتوبة بلغة HTML وكيف تُنظَّم العناصر داخلها، بالإضافة إلى شرح ميزات أساسية مهمة أخرى، كما ستجد خلال قراءتك لهذا المقال بعض الأمثلة التطبيقية التي تعطيك فرصة لتجريب ما تتعلمه، ولا بد أن تمتلك خلفيةً بسيطةً عن الحواسب قبل أن تبدأ قراءة المقال، كما يتطلب الأمر درايةً بالبرمجيات التي ينبغي تثبيتها لبدء العمل، ودرايةً ولو بسيطة بالتعامل مع الملفات. ما هي لغة HTML؟ لا تمثِّل لغة توصيف النص التشعبي Hypertext Markup Language أو لغة HTML اختصارًا لغة برمجة، وإنما لغةً وصفيةً تساعد المتصفحات على هيكلة صفحات الويب التي تزورها، وقد تكون الصفحات بسيطةً جدًا أو معقدةً جدًا وفقًا لرؤية مطور ويب. تتألف لغة HTML من سلسلة من العناصر التي تستخدِمها في إحاطة أو تغليف أو توصيف الأجزاء المختلفة للمحتوى الذي تريد عرضه ليظهر أو يسلك سلوكًا محددًا، فقد تُستخدَم الوسوم tags المحيطة بالمحتوى لتوصيف رابط تشعبي إلى صفحة أخرى أو لإظهار نص بحروف مائلة أو غير ذلك، ولنتأمل على سبيل المثال المحتوى النصي التالي: My cat is very grumpy إذا أردت أن يكون المحتوى فقرةً نصيةً مستقلةً بذاتها، فيمكن إحاطتها بوسمَي بداية ونهاية عنصر الفقرة النصية <p>: <p>My cat is very grumpy</p> ملاحظة: لا تُعَدّ وسوم لغة HTML حساسةً لحالة الأحرف، أي من الممكن كتابتها بحروف كبيرة أو صغيرة، إذ يمكن كتابة العنصر <title> مثلًا بالصورة <TITLE> أو <Title> أو <TiTlE> وسيعمل، لكن كتابة الوسوم بأحرف صغيرة هي ممارسة تطبيقية جيدة تسهِّل قراءة الشيفرة وتُظهر استمرارية في أسلوب الكتابة. تشريح عنصر HTML لنلق نظرةً أعمق على عنصر الفقرة <P>: يتكوَّن العنصر من: وسم البداية Opening tag: يتكون من اسم العنصر -أي p في حالتنا- محاطًا بقوسَي زاوية، كما يشير هذا الوسم إلى النقطة التي يبدأ عندها العنصر أو التي يبدأ تأثيره عندها (بداية الفقرة النصية في حالتنا). وسم النهاية Closing tag: يشابه وسم البداية لكنه يبدأ بشرطة أمامية / قبل اسم العنصر، ويشير هذا الوسم إلى نهاية العنصر -أي نهاية الفقرة في حالتنا-، ويُعَدّ إغفال وسم النهاية من أكثر الأخطاء التي يرتكبها المبتدئون وقد تفضي إلى نتائج غريبة بالفعل. المحتوى Content: يشير إلى المحتوى الفعلي للعنصر وهو في حالتنا نص فقط. العنصر Element: يتكون من وسمَي البداية والنهاية وبينهما المحتوى. تطبيق: إنشاء أول عنصر في لغة HTML استخدم منطقة تحرير الشيفرة Editable code في المحرِّر المضمّن التالي لتغليف السطر المكتوب في تلك المنطقة وذلك بإضافة الوسم <em> قبل السطر والوسم <em/> بعده، إذ سيؤدي ذلك إلى ظهور السطر مكتوبًا بأحرف مائلة، وسترى نتيجة عملك في منطقة الخرج المباشر Live Output"، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر إعادة ضبط Reset، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على زر "أظهر الحل Show solution" لترى الحل الصحيح. عناصر متداخلة يمكن وضع عناصر داخل عناصر أخرى أيضًا وهذا ما يُعرَف بالتداخل nesting، فإذا أردنا إظهار الكلمة "very" في الفقرة "My cat is very grumpy" بخط سميك، فيمكن تغليف هذه الكلمة داخل العنصر <strong> كما يلي: <p>My cat is <strong>very</strong> grumpy.</p> لكن عليك أن تتأكد دومًا من تداخل العناصر بصورة صحيحة، فقد فتحنا في المثال السابق العنصر <p> أولًا ثم <strong>، وبالتالي توجَّب علينا إغلاق العنصر الثاني على الصورة <strong/> ثم إغلاق الأول <p/>، وبالتالي يكون التداخل التالي غير صحيح: <p>My cat is <strong>very grumpy.</p></strong> لا بدّ من فتح وإغلاق العناصر بالصورة الصحيحة لكي تظهر بوضوح داخل أو خارج عنصر آخر، فإذا تداخلت بالصورة التي عرضناها في الشيفرة السابقة، فسيحاول المتصفح أن يخمّن بأفضل صورة ما تحاول قوله، مما قد يسبب بظهور نتائج غير متوقعة، فلا تفعل ذلك. دورة تطوير واجهات المستخدم ابدأ عملك الحر بتطوير واجهات المواقع والمتاجر الإلكترونية فور انتهائك من الدورة اشترك الآن العناصر الكتلية والعناصر السطرية تصنَّف عناصر لغة HTML ضمن مجموعتين مهمتين هما مجموعة العناصر الكتلية Block elements والسطرية inline elements: العناصر الكتلية: تشكل كتلةً مرئيةً ضمن الصفحة، كما تظهر في سطر جديد بعد العنصر الذي يسبقها، أي لا يمكن أن يقع على سطر آخر مع عنصر آخر، وسيظهر العنصر الذي يليه في سطر جديد أيضًا، كما تُعَدّ العناصر الكتلية في الصفحة عناصر هيكلة وتنظيم، فقد تمثِّل فقرةً نصيةً أو عناوين أو قوائم تعداد أو قوائم تنقل أو حواشي سفلية، وتجدر الإشارة إلى أنّ العناصر الكتلية لا توضَع ضمن عناصر سطرية لكنها قد تتداخل مع بعضها. العناصر السطرية: تقع داخل العناصر الكتلية وتحيط بأجزاء صغيرة من المحتوى، أي جزء من فقرة نصية أو مجموعة من بنود قائمة، ولا تسبب هذه العناصر ظهور سطر جديد في المستند، أي لا تشغل بنفسها السطر بالكامل، كما تُستخدَم هذه العناصر مع النصوص غالبًا، إذ ينشئ العنصر <a> مثلًا رابطًا تشعبيًا، في حين يميِّز العنصران <em> و <strong> أجزاءً من النص. انظر إلى المثال التالي: <em>first</em><em>second</em><em>third</em> <p>fourth</p><p>fifth</p><p>sixth</p> لاحظ أنّ <em> هو عنصر سطري، وكما ترى في محرر الشيفرة، ستقع العناصر الثلاث الأولى على السطر ذاته دون أية فراغات بينها، لكن من ناحية أخرى سيظهر العنصر الكتلي <p> في سطر جديد، بحيث يسبقه فراغ ويليه فراغ، وهذه الفراغات هي نتيجة لتطبيق تنسيق CSS الافتراضي لعنصر الفقرة النصية <p>. ملاحظات: أعادت لغة HTML بنسختها الخامسة -أي HTML5- تعريف فئات العناصر، وعلى الرغم من دقة التعاريف الجديدة وتفاديها للكثير من الغموض الذي يلف بعض العناصر، إلا أنها أكثر تعقيدًا موازنةً بمفهومَي العنصر الكتلي والسطري، لذلك سنبقى في مقالنا مع هذين المفهومين. لا يجب الخلط بين مفهومَي العنصر الكتلي والسطري مع أنواع صناديق CSS التي تحمل الأسماء نفسها، وعلى الرغم من الترابط الافتراضي للأسماء في الحالتين، فإنّ تغيير قيمة الخاصية display عند تنسيق العنصر لن يغير من الفئة التي ينتمي إليها العنصر أصلًا، أي لن يؤثّر على محتواه من العناصر الأخرى، لهذا تخلّت HTML5 عن مفهومَي العنصر الكتلي والسطري منعًا لهذا الالتباس الشائع. العناصر الفارغة لا تتقيد جميع العناصر بالنمط المؤلف من وسم البداية والمحتوى ثم وسم النهاية، إذ لا تحتوي بعض العناصر سوى على وسم بداية وتستخدَم عادةً في إدراج شيء ما في الصفحة، كما يُدرَج العنصر <img> على سبيل المثال على أساس صورة في الصفحة: <img src="https://raw.githubusercontent.com/mdn/beginner-html-site/gh-pages/images/firefox-icon.png"> ستكون نتيجة تنفيذ الشيفرة كما يلي: ملاحظة: تُدعى العناصر الفارغة أحيانًا بالعناصر الخالية void elements ولاحاجةً إلى وضع المحرف / قبل إغلاق الوسم، أي بالشكل <‎img ="cat.jpg" alt="cat" /‎> إلا في حالات خاصة مثل التوافق مع صيغة XML. السمات في لغة HTML يمكن أن تحمل عناصر لغة HTML سمات، وتبدو السمات كما يلي: تحتوي السمات على معلومات إضافية عن العنصر، ولا تظهر مع المحتوى الذي يعرضه، إذ تُشير السمة class في الصورة السابقة مثلًا إلى اسم يُستخدَم لتطبيق تنسيقات محددة على العنصر. ينبغي أن تُكتب السمة بحيث تراعي ما يلي: توجد مسافة فارغة بينها وبين اسم العنصر، كما ينبغي أن تفصل مسافة فارغة بينها وبين السمة التي تليها إذا وجدت. تلي اسم السمة إشارة مساواة =. توضع قيمة السمة بين إشارتي تنصيص مزدوجتين " ". تطبيق عملي: إضافة سمات إلى عنصر HTML يُدعى العنصر <a> بعنصر المربط anchor الذي يحوّل النص الموجود ضمنه إلى رابط تشعبي، كما يمكن أن تحمل المرابط سمات عدة منها: href: إن قيمة هذه السمة هي عنوان ويب للرابط التشعبي مثل "href="https://www.hsoub.com. title: تستخدَم لإدراج معلومات إضافية عن الرابط مثل وصف الصفحة التي سينقلنا إليها، أي مثل title="The Mozilla homepage"، كما تظهر هذه المعلومات على أساس تلميح فوق الرابط عندما تمرِّر مؤشر الفارة فوقه. target: تستخدَم لتحديد طريقة عرض الصفحة التي ينقلنا إليها الرابط مثل "target="_blank، إذ سيعرض لك المتصفح الآن الصفحة في نافذة جديدة، في حين سيعرض المتصفح هذه الصفحة في النافذة نفسها ما لم تستخدم هذه السمة. سنحاول تحويل السطر الموجود في منطقة تحرير الشيفرة إلى مربط كما يلي: أضف الوسم <a>. أضف الصفتين href و title. حدد طريقة عرض صفحة الرابط من خلال السمة target. سترى نتيجة عملك في منطقة عرض النتيجة، ومن المفترض أن ترى رابطًا يعرض قيمة السمة titleعندما تمرِّر المؤشر فوقه، وينقلك عند النقر عليه إلى الصفحة التي حددتها في السمة href، وتذكَّر أن تضع مسافة فارغة بين اسم العنصر والسمات، فإذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة الضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح. السمات المنطقية قد تلاحظ أحيانًا سمات دون قيم، وتُدعى هذه السمات بالسمات المنطقية Boolean، إذ تمتلك هذه السمات قيمةً واحدةً وعادةً ما تكون هذه القيمة مطابقةً لاسم السمة، ونضرب مثلًا على ذلك السمة disabled التي تضاف إلى عنصر الإدخال النصي، بحيث تمنع المستخدِم من الكتابة ضمن مربع النص الذي يبدو رماديًا: <input type="text" disabled="disabled"> يمكن أن نكتب السمة المنطقية بصيغة مختصرة كما يلي: <!-- لمنع المستخدم من الكتابة ضمن المربع النصي disabled استخدام السمة --> <input type="text" disabled> <!-- لا يضم عنصر الإدخال النصي هذه السمة ويمكن للمستخدم الكتابة ضمنه --> <input type="text"> تعطيك الشيفرة في فقرة "السمات المنطقية" النتيجة التالية عند تنفيذها: حذف إشارتي التنصيص المحيطتين بقيمة السمة ستلاحظ خلال متابعتك لشيفرات العديد من المواقع وجود بعض الأساليب الغريبة في كتابة الشيفرة، منها وجود سمات لا توُضع قيمها بين إشارتَي تنصيص، إذ يُسمح بهذا الأمر في حالات محددة وقد يُضِر شيفرتك في أخرى، كما يمكن مثلًا كتابة السمة href في مثال المربط السابق كما يلي: <a href=https://www.mozilla.org/>favorite website</a> لكن ستظهر المشاكل عندما نطبِّق الأمر نفسه على السمة title: <a href=https://www.mozilla.org/ title=The Mozilla homepage>favorite website</a> سيخطئ المتصفح في تفسير المطلوب وسيفهم السمة title على أنها ثلاث سمات الأولى هي title وقيمتها "The" وسِمتان منطقيّتان هما Mozilla و homepage، إذ يسبب هذا الأمر أخطاءً غير متوقعة أو سلوكًا غير متوقع كما سترى في نتيجة التنفيذ التالية: لهذا السبب، يجب أن تضع قيمة السمات ضمن إشارتَي تنصيص دومًا لتجنب مشاكل مثل هذه ولتسهِّل قراءة الشيفرة. استخدام إشارتي تنصيص مزدوجتين أو مفردتين ستلاحظ في هذه المقال أننا وضعنا قيم السمات ضمن إشارتَي تنصيص مزدوجتين " "، لكنك قد ترى في مواقع أخرى قيمًا محاطةً بإشارتَي تنصيص مفردتين ' '، والأمر برمته مسألة ذوق، إذ يمكنك اختيار الأسلوب الذي تريد، أي تعطي كلا الكتابتين في المثال التالي النتيجة نفسها: <a href="https://www.example.com">A link to my example.</a> <a href='https://www.example.com'>A link to my example.</a> انتبه إلى عدم المزج بين إشارتَي التنصيص، إذ سيقود المزج الذي سنعرضه في مثالنا التالي إلى أخطاء: <a href="https://www.example.com'>A link to my example.</a> إذا أردت وضع إشارة تنصيص محددة داخل قيمة السمة، فعليك إحاطة القيمة كلها بإشارتَي التنصيص الأخرى كما يلي: <--! "" ضع قيمة السمة بين مزدوجتين title لتضع الإشارة ' ضمن قيمة السمة --> <a href="https://www.example.com" title="Isn't this fun?">A link to my example.</a> استخدم كيانات HTML لتضع إشارة تنصيص ضمن إشارتَي تنصيص من النوع نفسه -أي مفردة ضمن مفردتين مثلًا-، إذ تُعَدّ الكتابة التالية خاطئةً: <a href='https://www.example.com' title='Isn't this fun?'>A link to my example.</a> عليك استخدام كيان HTML الذي يعطي المحرف ' وهو مجموعة المحارف ;apos&: <a href='https://www.example.com' title='Isn&apos;t this fun?'>A link to my example.</a> تشريح مستند HTML لا فائدة كبيرة من عناصر لغة HTML بمفردها، وإنما عليك تعلّم كيفية تنظيمها لتبني صفحة ويب بأكملها: <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My test page</title> </head> <body> <p>This is my page</p> </body> </html> إليك الطريقة: أولًا: استخدم العنصر: <!DOCTYPE html> لقد كانت الغاية من هذا العنصر في الأيام الأولى (1991/1992) أن يعمل على أساس رابط إلى مجموعة من القواعد التي ينبغي أن تحققها صفحة مكتوبة بلغة HTML لكي تُعَدّ صفحةً جيدةً، إذ يبدو هذا العنصر كما يلي: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> يُستخدَم حاليًا هذا العنصر في بداية المستند حتى يعمل كل شيء بالطريقة الصحيحة، إذ تمثل الشيفرة <!DOCTYPE html> أقل مجموعةً من المحارف كي يكون هذا العنصر صالحًا. ثانيًا: استخدم العنصر <html></html> الذي يضم محتوى الصفحة بأكمله، ويعرف أحيانًا بالعنصر الجذري. ثالثًا: استخدم العنصر <head></head> الذي يعمل على أساس حاوية لكل ما تريد وضعه في صفحة HTML دون أن يكون جزءًا من المحتوى المعروض للزائر، ويتضمن ذلك الكلمات المفتاحية ووصف الصفحة الذي يظهر في نتائج محركات البحث وملفات تنسيق CSS ومجموعة المحارف الكتابية المستخدَمة وغيرها. رابعًا: استخدم العنصر <"meta charset="utf-8> الذي يضبط مجموعة المحارف التي تستخدِمها في الصفحة، وهنا اخترنا المجموعة UTF-8 التي تضم محارف الأغلبية الساحقة من اللغات المكتوبة، إذ تستطيع هذا المحارف أن تعرض الآن أيّ محتوى نصي بأي لغة قد تضعه في صفحتك، ولا مبرر لعدم ضبط مجموعة المحارف المستخدَمة، كما ستساعدك على تحاشي الكثير من الأخطاء لاحقًا. خامسًا: استخدم العنصر <title></title> الذي يضبط عنوان صفحتك أعلى المتصفح عند تحميل الصفحة، كما يُستخدَم لوصف الصفحة عندما تضيفها إلى قائمة الصفحات المفضلة. سادسًا: استخدم العنصر <body></body> الذي يضم المحتوى الذي تريد عرضه على زائرِي صفحتك بأكمله، سواءً كان نصًا أو صورًا أو فيديو أو ألعاب أو أيّ شيء آخر. تطبيق عملي: إضافة بعض الميزات إلى مستند HTML إذا أردت تجريب كتابة شيفرة HTML على جهازك، فيمكنك تنفيذ النقاط التالية: نسخ المثال السابق عن هيكلية صفحة HTML. إنشاء ملف نصي جديد باستخدام محرر النصوص لديك. لصق الشيفرة في الملف. حفظ الملف باسم index.html. ملاحظة: ستجد ايضًا قالب HTML الأساسي ضمن المستودع المخصص فريق مطوري موزيللا على جيت-هاب. افتح الملف باستخدام متصفحك لترى نتيجة تصيير الشيفرة، وعدّل الشيفرة كما تشاء وحدِّث ما يعرضه المتصفح لإظهار أيّة تغييرات، إذ ستبدو الصفحة بشكلها الأصلي كما يلي: يمكنك التعديل على الشيفرة في هذا التمرين على حاسوبك كما شرحنا سابقًا أو من خلال المحرر المدمج مع المقال والذي نعرضه بين الفينة والأخرى، كما يمكنك تعزيز مهاراتك بإنجاز المهام التالية: أضف عنوانًا رئيسيًا للصفحة تحت العنصر <body> مباشرةً، إذ يجب أن تضع العنوان بين وسم البداية <h1> ووسم النهاية <h1/>. عدِّل محتوى الفقرة النصية لتتضمن نصًا من اختيارك. أبرز الكلمات الهامة في نصك بتغليفها ضمن العنصر <strong>. أضف رابطًا ضمن الفقرة النصية بالطريقة التي شرحناها سابقًا. أضف صورةً إلى صفحتك تحت الفقرة النصية، إذ ستحصل على نقاط إضافية إذا تمكنت من إنشاء رابط إلى صورة أخرى على حاسوبك الشخصي أو على ويب. إذا ارتكبت خطأً، فيمكنك مسح ما كتبته بالنقر على زر "إعادة الضبط Reset"، في حين إذا وجدت نفسك تائهًا كليًا، فانقر على الزر "أظهر الحل Show solution" لترى الحل الصحيح. المسافات الفارغة في HTML لاحظ استخدامنا للكثير من المسافات الفارغة في شيفرة الصفحة، وهذا أسلوب كتابة قد تعتمده، إذ سيعطي أسلوبَي كتابة الشيفرة التاليين النتيجة ذاتها: <p>Dogs are silly.</p> <p>Dogs are silly.</p> مهما أضفت من مسافات فارغة ضمن محتوى العنصر الذي قد يتضمن مسافات فارغة عدة وربما محرف الانتقال إلى سطر جديد، فسيختزِل محلل HTML كل سلسلة متلاحقة من المسافات الفارغة إلى مسافة فارغة واحدة، فلماذا إذًا نضيف فراغات أكثر؟ القضية تتعلق بسهولة القراءة. من السهل فهم ما فعلته أثناء كتابتك لشيفرة الصفحة إذا رتبتها جيدًا، فما نفعله عادةً أثناء كتابة الشيفرة هو إزاحة العناصر بمقدار مسافتين فارغتين عن العنصر الذي تقع داخله، ويعود الأمر إليك دائمًا في اختيار التنسيق الذي تراه مناسبًا لتنظيم الشيفرة، ومن الأفضل دائمًا تنسيقها. كيانات لغة HTML: إضافة محارف خاصة تُعَدّ المحارف < و > و " و ' و & في HTML محارف خاصة لكونها جزءًا من الصياغة القواعدية للغة، فكيف سنتمكن إذًا من إضافة هذه المحارف إلى محتوى العناصر؟ وكيف سنضع مثلًا إشارة "أكبر من" دون أن تؤثر على تفسير الشيفرة؟ يمكن ذلك من خلال مراجع إلى تلك المحارف، والمراجع هي رموز خاصة تمثِّل محرفًا محددًا يمكن استخدامه في هذه الحالة، كما يبدأ كل مرجع بالمحرف & وينتهي بالمحرف ;. table { width: 100%; } thead { vertical-align: middle; text-align: center; } td, th { border: 1px solid #dddddd; text-align: right; padding: 8px; text-align: inherit; } tr:nth-child(even) { background-color: #dddddd; } المحرف الخاص سلسلة الحروف المرجعية < ;lt& > ;gt& " ;quot& ' ;apos& & ;amp& يمكن تذكر المرجع إلى المحرف الخاص بسهولة لأن النص الذي يشير إلى إشارة أصغر مثلًا ;lt& يمكن أن يُفهم من المصطلح الذي أخذ منه أي Less Than، والأمر مشابه لبقية المراجع. ستجد في المثال التالي فقرتين نصيتين: <p>In HTML, you define a paragraph using the <p> element.</p> <p>In HTML, you define a paragraph using the <p> element.</p> ستُعرض الفقرة الأولى بطريقة غير صحيحة عند التنفيذ، إذ يفسر المتصفح الوسم <p> المكتوب ضمن محتوى الفقرة على أنه بداية فقرة نصية جديدة، في حين ستبدو الفقرة النصية الثانية صحيحةً عند استخدام مراجع إلى المحارف. ملاحظة: لا حاجة إلى استخدام مراجع إلى كيانات الرموز الأخرى، لأن المتصفحات الحديثة ستتعامل مع جميع الرموز جيدًا إذا كانت UTF-8 هي مجموعة محارف المستخدَمة في ترميز HTML. التعليقات في لغة HTML زوِّدت لغة HTML بآلية لإدراج تعليقات ضمن الشيفرة يتجاهلها المتصفح عند تصيير الصفحة ولا تُعرض للزائر، والغاية من التعليقات هي إضافة ملاحظات إلى الشيفرة بغية شرحها أو توضيح منطق العمل، كما تظهر فائدة التعليقات عندما تضطر إلى العمل على شيفرة أنجزتها قبل فترة من الزمن ولم تَعُد تتذكر تفاصيل العمل بأكمله، كما تظهر أهميتها عندما يعمل على تغيير الشيفرة وتحديثها مجموعة من الأشخاص، ولإدراج تعليق، ضعه ضمن الوسمين <-- و --!> كما في المثال التالي: <p>I'm not inside a comment</p> <!-- <p>I am!</p> --> لاحظ أنّ ما سيعرضه المتصفح هو الفقرة الأولى فقط. خلاصة لقد وصلت إلى نهاية المقال، ونتمنى أن تكون قد استمتعت بما جاء فيه، إذ يفترض الآن أنك فهمت ماهية لغة HTML وكيف تعمل بأبسط صورها، كما من المفترض أيضًا أنك أصبحت قادرًا على كتابة بعض العناصر والسمات، كما سنتعمق في المقالات اللاحقة في بعض المفاهيم التي طرحناها وسنضيف مفاهيم جديدة. وطالما أنك شرعت في تعلم لغة HTML، فننصحك بتعلم مبادئ تنسيق الصفحات باستخدام CSS، فهي تعمل جيدًا مع لغة HTML كما سترى لاحقًا. ترجمة -وبتصرف- للمقال Getting started with HTML. اقرأ أيضًا نحو فهم أعمق لتقنيات HTML5 مكونات الويب: عناصر HTML المخصصة وقوالبها HTML و CSS للمبتدئين: كيف تصمم أول صفحة ويب لك
  20. نتعرف في هذا المقال على وضع تطبيق ويب بأكمله مع جميع خدماته ضمن حاويات دوكر Docker وتنسيقها وضبط إعداداتها. استخدام الحاويات مع React سنحاول أن ننشئ تطبيق React ونضعه ضمن حاوية تاليًا، لهذا سنختار npm مديرَا للحزم علمًا أن yarn هو المدير الافتراضي لبرنامج create-react-app: $ npx create-react-app hello-front --use-npm ... Happy hacking! يُثبت create-react-app كل الاعتماديات اللازمة، فلا حاجة لتنفيذ الأمر npm install. ستكون الخطوة الثانية تحويل شيفرة JavaScript و CSS إلى ملفات ساكنة جاهزة لمرحلة الإنتاج. أمّا create-react-app فلديه build ليكون سكربت npm، لهذا سنستفيد من هذه الناحية: $ npm run build ... Creating an optimized production build... ... The build folder is ready to be deployed. ... أما الخطوة الأخيرة هي التفكير بطريقة للعمل مع خادم لتقديم الملفات الساكنة. يمكننا الاستفادة من express.static مع خادم Express لهذا الغرض، لهذا سأترك الموضوع تمرينًا لك، وسننتقل بدلًا من ذلك إلى كتابة ملف Dockerfile: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build يبدو ما كتبناه صحيحًا تقريبًا، لهذا سنبنيه ونقيّم مسارنا، والهدف أن نبني التمرين دون أخطاء. سنحاول بعد ذلك أن نتحقق من وجود الملفات داخل الحاوية من خلال أوامر "bash". $ docker build . -t hello-front [+] Building 172.4s (10/10) FINISHED $ docker run -it hello-front bash root@98fa9483ee85:/usr/src/app# ls Dockerfile README.md build node_modules package-lock.json package.json public src root@98fa9483ee85:/usr/src/app# ls build/ asset-manifest.json favicon.ico index.html logo192.png logo512.png manifest.json robots.txt static الخيار الصحيح لتخديم الملفات الساكنة ضمن الحاوية هو serve نظرًا لوجود Node ضمن الحاوية. لنحاول تثبيت serve لتخديم الملفات الساكنة ونحن داخل الحاوية: root@98fa9483ee85:/usr/src/app# npm install -g serve added 88 packages, and audited 89 packages in 6s root@98fa9483ee85:/usr/src/app# serve build ┌───────────────────────────────────┐ │ │ │ Serving! │ │ │ │ Local: http://localhost:5000 │ │ │ └───────────────────────────────────┘ أغلق الحاوية باستخدام الاختصار "Ctrl+C" لإضافة بعض التوجيهات إلى ملف Dockerfile. يتحوّل تثبيت serve إلى عملية تشغيل RUN في ملف Dockerfile، وهكذا ستُثبّت الاعتمادية أثناء عملية البناء، وسيكون أمر تخديم مجلد البناء هو نفسه أمر تشغيل الحاوية: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build RUN npm install -g serve CMD ["serve", "build"] يتضمن الأمر ‍‍CMD الآن قوسين مربعين ونكون بذلك قد استخدمنا ما يُعرف بالشكل التنفيذي exec form من الأمر ‍‍CMD، علمًا أنه يُكتب بثلاثة أشكال لكن الشكل السابق هو المُفضّل. عندما نبني الصورة الآن باستخدام الأمر: docker build . -t hello-front ثم تشغيلها باستخدام الأمر: docker run -p 5000:3000 hello-front سيكون التطبيق متاحًا على العنوان "http://localhost:5000". استخدام المراحل المتعددة على الرغم من أن serve خيار صحيح لكن بالإمكان إيجاد بديل أفضل، إذ أن الغاية هنا هي إنشاء صورة لا تحتوي أي شيء غير مطلوب وبأقل عدد من الاعتماديات، وبالتالي ينخفض احتمال توقف الصورة عن العمل أو أن تصبح عرضةً للهجمات مع الوقت. صُمّمت عملية البناء متعددة المراحل Multi-stage builds لتفصل عملية البناء إلى مراحل مختلفة، ومن الممكن حينها تحديد ملفات الصورة التي يُسمح لها بالانتقال من مرحلة إلى أخرى، كما يتيح ذلك أيضًا إمكانية التحكم بحجم الصورة، فلن نحتاج إلى جميع الملفات الجانبية التي تنتج عن عملية البناء ضمن الصورة الناتجة. إنّ الصورة الأصغر أسرع في التحميل والتنزيل وتساعد في تخفيض عدد نقاط الضعف في برنامجك. عند استخدام البناء متعدد المراحل، بالإمكان الاعتماد على حلول تتبع نهج المحاولة والخطأ مثل استخدام الخادم Nginx في إدارة الملفات الساكنة دون عناء شديد. تدلنا صفحة استخدام Nginx مع Docker Hub على المعلومات الضرورية لفتح المنافذ واستضافة محتوى ساكن. لنستخدم الآن ملف Dockerfile السابق بعد تغيير ما يلي التعليمة FROM لإضافة اسم المرحلة: # called build-stage الأولى الآن إلى مرحلة تُدعى FROM تشير تعليمة FROM node:16 AS build-stage WORKDIR /usr/src/app COPY . . RUN npm ci RUN npm run build # هذه مرحلة جديدة الآن، وسيختفي كل شيء قبلها ما عدا الملفات التي نريد نسخها FROM nginx:1.20-alpine # /usr/share/nginx/html إلى build-stage نسخ مجلد البناء من # docker hub page وجد الموقع الوجهة من خلال صفحة شروحات COPY --from=build-stage /usr/src/app/build /usr/share/nginx/html لقد صرحنا أيضًا عن مرحلة أخرى تُنقل إليها فقط الملفات الضرورية من المرحلة الأولى (المجلد "build" الذي يضم المحتوى الساكن). بعد بنائها ثانيةً، ستكون الصورة جاهزة لتخديم المحتوى الساكن. إن المنفذ الافتراضي لخادم Nginx هو 80 وبالتالي سينفع تنفيذ الأمر p 8000:80-، لهذا لا بد من تغيير معاملات أمر التشغيل قليلًا. تنطوي عملية البناء متعددة المراحل على تحسينات داخلية قد تؤثر على عملية البناء، إذ تتجاوز عملية البناء متعددة المراحل مثلًا المراحل التي لم تُستخدم، وبالتالي علينا تمرير بعض البيانات إلى المرحلة القادمة إن أردنا استخدام مرحلة ما لتبديل جزء من خط البناء مثل الاختبارات أو التنبيهات. لهذا الأمر تبريراته في بعض الحالات كأن تَنسخ الشيفرة من مرحلة الاختبار إلى مرحلة البناء كي تضمن بناء الشيفرة المُختبرة. التمرينان 12.13 و 12.14 حاول أن تحل التمرينين التاليين: التمرين 12.13: الواجهة الامامية لتطبيق المهام لقد وصلنا أخيرًا إلى الواجهة الأمامية، لهذا عليك أن تلقي نظرة على محتويات المجلد "todo-app/todo-frontend" وتقرأ ملف "اقرأني README". ابدأ بتشغيل الواجهة الأمامية خارج الحاوية، وتأكد من تناغم عملها مع الواجهة الخلفية. ضع التطبيق ضمن الحاوية بعد ذلك أنشئ الملف "todo-app/todo-frontend/Dockerfile" ثم استخدم التعليمة ENV لتمرير متغير البيئة REACT_APP_BACKEND_URL إلى التطبيق وشغّله مع الواجهة الخلفية. ينبغي أن تعمل الواجهة الخلفية حاليًا خارج الحاوية. وانتبه إلى ضرورة ضبط المتغير REACT_APP_BACKEND_URL قبل بناء الواجهة الأمامية وإلا لن يُعرَّف ضمن الشيفرة. التمرين 12.14: إجراء الاختبارات أثناء عملية البناء من الميزات الهامة للبناء متعدد المراحل، استخدام مرحلة البناء لإجراء الاختبارات على التطبيق testing. فإن أخفقت مرحلة الاختبار ستُخفق عملية البناء بأكملها. لكن تنفيذ الاختبارات جميعها خلال عملية بناء الصورة ليست فكرة جيدة؛ لهذا قد تكون الاختبارات المتعلقة بالحاوية هي الأنسب. استخلص مكوّن Todo يمثل مهمة واحدة من الشيفرة ، ثم اكتب اختبارًا للمكوّن الجديد وأضف تنفيذ الاختبارات إلى عملية البناء. نفّذ الاختبار من خلال الأمر CI=true npm test وإلا سيبدأ برنامج create-react-app بمراقبة التغييرات مسببًا توقف خط العمل pipeline. بإمكانك إضافة مرحلة بناء جديدة لإجراء الاختبار إن أردت ذلك. لكن تذكر في هذه الحالة أن تقرأ آخر فقرة قبل التمرين 12.13 مجددًا. التطوير ضمن الحاويات لننقل الآن تطبيق المهام بأكمله إلى حاوية. وإليك بعض الأسباب التي قد تدفعنا إلى ذلك: للإبقاء على تماثل بيئة العمل بين التطوير والإنتاج تفاديًا للثغرات التي تظهر فقط في بيئة التشغيل. لتفادي الاختلافات بين المطورين وبيئات عملهم الخاصة والتي تقود إلى صعوبات أثناء تطوير التطبيق. لمساعدة أعضاء الفريق الجدد على البدء بتثبيت بيئة تشغيل الحاوية دون الحاجة إلى أي شيء آخر. في المقابل، قد نواجه سلوكًا غير معهود عندما لا نشغّل التطبيق كما اعتدنا، ولهذا لا بد من فعل ما يلي على الأقل لنقل التطبيق إلى الحاوية: تشغيل التطبيق في وضع التطوير. الوصول إلى الشيفرة من خلال برنامج VSCode. لنبدأ بالواجهة الأمامية، وطالما أن ملف Dockerfile لمرحلة التطوير سيختلف تمامًا عن ملف Dockerfile لنسخة الإنتاج، سننشئ ملف جديد اسمه "dev.Dockerfile". لنشغّل create-react-app في وضع التطوير ومن المفترض أن يكون الأمر بسيطًا على النحو التالي: FROM node:16 WORKDIR /usr/src/app COPY . . # طالما سنعمل في وضع التطوير npm install إلى npm ci غيّر RUN npm install # npm start إن أمر التشغيل في وضع التطوير هو CMD ["npm", "start"] تُستخدم الراية f- أثناء البناء لتحديد الملف الذي يُستخدم، وإلا سيقع الاختيار على الملف Dockerfile، لهذا يكون أمر بناء الصورة على النحو التالي: docker build -f ./dev.Dockerfile -t hello-front-dev . سيُخدَّم create-react-app على المنفذ 3000، لهذا يمكنك اختباره بتشغيل الحاوية على هذا المنفذ. تقتضي المهمة الثانية الوصول إلى الملفات باستخدام VSCode، وهناك على الأقل طريقتان لإنجاز الأمر: باستخدام الموسِّع Visual Studio Code Remote - Containers. باستخدام الأقراص volumes، وبنفس طريقة تخزين البيانات في قاعدة البيانات. لنتجاوز المهمة الثانية كوننا سنضطر فيها إلى التعامل مع محررات أخرى، ولنجرب تشغيل الحاوية مع الراية v-، فإذا جرى كل شيء على ما يرام ننقل الإعدادات إلى ملف docker-compose. لاستخدام تلك الراية علينا تزويدها بالمجلد الحالي من خلال تنفيذ الأمر pwd الذي يعطي المسار إلى المجلد الحالي. حاول أن تنفِّذ ذلك من خلال الأمر (echo $(pwd في واجهة سطر الأوامر لديك وبالترتيب التالي: $ docker run -p 3000:3000 -v "$(pwd):/usr/src/app/" hello-front-dev Compiled successfully! You can now view hello-front in the browser. بإمكاننا الآن تعديل الملف "src/App.js" وستُعرض التغييرات مباشرةً على المتصفح. سننقل تاليًا الإعدادات إلى الملف "docker-compose.yml" الذي ينبغي أن يكون موجودًا في جذر المشروع: services: app: image: hello-front-dev build: context: . # يختار السياق هذا المجلد ليكون سياق البناء dockerfile: dev.Dockerfile # الذي سيُستخدم Dockerfile لاختيار ملف volumes: - ./:/usr/src/app # يمكن أن يكون المسار نسبي, so ./ is enough to say # ./ لهذا يكفي استخدام #docker-compose.yml للقول أنه نفس مكان وجود الملف ports: - 3000:3000 container_name: hello-front-dev # hello-front-dev لتسمية الحاوية بالاسم يمكننا بهذه الإعدادات الآن تشغيل التطبيق في وضع التطوير من خلال الأمر docker-compose up، ولن تحتاج حتى إلى تثبيت Node. يسبب تثبيت اعتماديات جديدة عدة مشاكل في إعداد بيئة تطوير كهذه، لهذا ستجد أن تثبيت الاعتمادية الجديدة ضمن الحاوية هو أحد الخيارات الجيدة. فبلدلًا من تنفيذ الأمر التالي مثلًا npm install axios ، ثبّت هذه الاعتمادية ضمن الحاوية التي تعمل من خلال الأمر docker exec hello-front-dev npm install axios أو أضفها إلى الملف ثم نفِّذ الأمر docker build من جديد. التمرين 12.15: إعداد بيئة تطوير الواجهة الأمامية أنشئ الملف واستخدم الأقراص لتمكين تطوير الواجهة الأمامية لتطبيق المهام عندما يعمل ضمن الحاوية. التواصل بين الحاويات في شبكة Docker تهيئ الأداة Docker شبكةً بين الحاويات وتضيف خادمًا لأسماء النطاقات DNS لربط أي حاويتين بسهولة. دعونا إذًا نضيف خدمة جديدة إلى وسنرى كيف تعمل الشبكة وخادم DNS. سنستخدم الحزمة التنفيذية Busybox التي تضم مجموعةً من الأدوات التي قد تحتاجها وتُعرف هذه الحزمة باسم "سكين الجيش السويسري الخاصة بنظام Linux المدمج". لهذا يمكننا بالتأكيد الاستفادة منها. تساعدنا Busybox في تنقيح إعداداتنا، لهذا إن لم تتمكن من حل التمرين السابق، عليك استخدام Busybox لمعرفة ما يعمل من إعداداتك وما لا يعمل. لنختبر ما قلناه الآن. تتواجد تلك الحاويات ضمن شبكة ويمكنك الربط بينها بسهولة، ويمكن إضافة Busybox إلى الخلطة بتغيير الملف "docker-compose.yml" إلى: services: app: image: hello-front-dev build: context: . dockerfile: dev.Dockerfile volumes: - ./:/usr/src/app ports: - 3000:3000 container_name: hello-front-dev Debug-helper: image: busybox لن تتضمن حاوية Busybox على أية عمليات تجري ضمنها لذلك يمكننا تنفيذ الأمر exec. عندها سيبدو الخرج الناتج عن تنفيذ التعليمة docker-compose up على النحو التالي: $ docker-compose up Pulling debug-helper (busybox:)... latest: Pulling from library/busybox 8ec32b265e94: Pull complete Digest: sha256:b37dd066f59a4961024cf4bed74cae5e68ac26b48807292bd12198afa3ecb778 Status: Downloaded newer image for busybox:latest Starting hello-front-dev ... done Creating react-app_debug-helper_1 ... done Attaching to react-app_debug-helper_1, hello-front-dev react-app_debug-helper_1 exited with code 0 hello-front-dev | hello-front-dev | > react-app@0.1.0 start hello-front-dev | > react-scripts start هذا الخرج متوقع كون الحزمة هي مجموعة أدوات مثل غيرها. لنستخدم الحزمة في إرسال طلب إلى الحاوية "hello-front-dev" لنرى كيف يعمل خادم DNS. بإمكاننا تنفيذ الطلب wget أثناء عمل الحاوية فهو أداةٌ موجودةٌ ضمن Busybox مهمتها إرسال طلب إلى الحاوية hello-front-dev من مساعد التنقيح debug-helper. يمكننا استخدام الأمر docker-compose run SERVICE COMMAND لتنفيذ خدمة مع أمر محدد، ويتطلب استخدام الأمر wget السابق الراية O- تليها - لنقل الاستجابة إلى مجرى الخرج: $ docker-compose run debug-helper wget -O - http://app:3000 Creating react-app_debug-helper_run ... done Connecting to hello-front-dev:3000 (172.26.0.2:3000) writing to stdout <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> ... يُعد عنوان URL الجزء المهم هنا، فهو يشير إلى أننا اتصلنا بالخدمة "hello-front-dev" والمنفذ 3000. لقد منحنا الحاوية اسمها "hello-front-dev" باستخدام التعليمة container_name في ملف "docker-compose"، أما المنفذ فهو المنفذ المتاح للوصول إلى التطبيق ضمن الحاوية. ولا حاجة لنشر المنفذ كي تتصل به بقية الخدمات الموجودة على نفس الشبكة، فالمنافذ المحددة في الملف "docker-compose" هي للوصول الداخلي وحسب. دعونا نغيّر رقم المنفذ في الملف "docker-compose.yml" لتوضيح الأمر: services: app: image: hello-front-dev build: context: . dockerfile: dev.Dockerfile volumes: - ./:/usr/src/app ports: - 3210:3000 container_name: hello-front-dev debug-helper: image: busybox سيُتاح التطبيق على الحاسوب المضيف وعلى العنوان http://localhost:3210 عند تنفيذ الأمر docker-compose up، لكن التطبيق لا يزال يعمل وفقًا للأمر السابق docker-compose run debug-helper wget -O - http://app:3000 طالما أن المنفذ هو 3000 أيضًا ضمن شبكة دوكر. يطلب الأمر docker-compose run -كما تشرح الصورة السابقة-من مساعد التنقيح أن يرسل طلبًا ضمن شبكة دوكر docker بينما يرسل المتصفح في الجهاز المضيف الطلب من خارج الشبكة. أما الآن وقد علمت سهولة إيجاد الخدمات في الملف "docker-compose.yml" وليس لدينا أي شيء للتنقيح، سنزيل مساعد التنقيح و نُعيد المنافذ إلى 3000:3000 في الملف "docker-compose.yml". التمرين 12.16: تشغيل الواجهة الخلفية لتطبيق المهام ضمن حاوية التطوير استخدم الأقراص والمكتبة Nodemon لتمكين عملية تطوير الواجهة الخلفية لتطبيق المهام وهي تعمل ضمن الحاوية. أنشئ الملف "todo-backend/dev.Dockerfile" وعدّل الملف "todo-backend/docker-compose.dev.yml". عليك إعادة التفكير أيضًا في الاتصالات بين الواجهة الخلفية و قاعدة البيانات MongoDB، أو Redis. ولحسن الحظ يدعم docker-compose استخدام متحولات بيئة يمكن تمريرها إلى التطبيق: services: server: image: ... volumes: - ... ports: - ... environment: - REDIS_URL=... - MONGO_URL=... وُضعت عناوين URL للخادم المحلي بطريقة خاطئة عمدًا وعليك وضع القيم الصحيحة. وتذكر أن تراقب دائمًا ما يحدث على شاشة الطرفية، فقد ستلمّح رسائل الخطأ إلى مكان المشكلة إن حدث خلل ما. إليك هذه الصورة التوضيحة التي قد تنفع في توضيح الاتصالات ضمن شبكة docker: التواصل بين الحاويات في بيئة أكثر حيوية سنضيف تاليًا خادم وكيل معكوس reverse proxy إلى الملف " docker-compose.yml". واستنادًا إلى ويكيبيديا: سيكون الخادم الوكيل المعكوس في حالتنا نقطة دخول مفردة إلى تطبيقنا، أم الهدف النهائي فهو إعداد واجهة React الأمامية و واجهة Express الخلفية معًا خلف الخادم والوكيل المعكوس. لديك عدة خيارات تساعدك في إنجاز الخادم الوكيل، مثل Traefik و Caddy و Nginx و Apache وقد رُتبت من الأحدث إلى الأقدم ظهورًا، لكن خيارنا سيكون Nginx. لنضع الآن الواجهة الأمامية المتمثلة بالحاوية "hello-frontend" خلف الخادم الوكيل المعكوس. لهذا عليك إنشاء الملف "nginx.conf" في جذر المشروع واتّبع القالب التالي بمثابة نقطة انطلاق. لا بُد من إنجاز بعض التعديلات الثانوية لتشغيل التطبيق: # الأحداث مطلوبة لكن لا بأس بالاعتماد على الأحداث الافتراضية events { } # 80 يستمع إلى المنفذ http خادم http { server { listen 80; # (/) تُعالج الطلبات التي تبدأ بالمحرف location / { # نحتاج الأسطر الثلاثة التالية للتحميل المباشر للتغييرات proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; # http://localhost:3000 تُوجَّه الطلبات مباشرةً إلى العنوان proxy_pass http://localhost:3000; } } } أنشئ تاليًا خدمة Nginx ضمن الملف "docker-compose.yml"، ثم أضف قرصًا كما ورد في إرشادات الصفحة الرسمية للأداة Docker Hub حيث يكون الجانب الأيمن على الشكل هو: etc/nginx/nginx.conf:ro/:،إذ يدل التصريح ro على أن القرص للقراءة فقط: services: app: # ... nginx: image: nginx:1.20.1 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - 8080:80 container_name: reverse-proxy depends_on: - app # انتظر حاوية الواجهة الخلفية حتى تُقلع يمكنك الآن تنفيذ الأمر ومراقبة نتيجة العمل: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a02ae58f3e8d nginx:1.20.1 "/docker-entrypoint.…" 4 minutes ago Up 4 minutes 0.0.0.0:8080->80/tcp, :::8080->80/tcp reverse-proxy 5ee0284566b4 hello-front-dev "docker-entrypoint.s…" 4 minutes ago Up 4 minutes 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp hello-front-dev عند الانتقال إلى العنوان http://localhost:8080 ستظهر صفحة الحالة 502 المألوفة، وهذا لأن توجيه الطلبات إلى العنوان http://localhost:3000 لن يقود إلى أي مكان، لأن حاوية الخادم Nginx لا تحتوي أي تطبيق يعمل على المنفذ 3000. إن مصطلح "خادم محلي" يُستخدم عادة للدلالة على الحاسوب الحالي الذي نلج إليه، بينما يكون الخادم المحلي فريدًا في عالم الحاويات لكل حاوية ويقود إلى الحاوية نفسها. لنختبر ذلك بالدخول إلى الحاوية Nginx واستخدام الأداة curl لإرسال طلب إلى التطبيق نفسه، وتشابه هذه الأداة من حيث الطريقة التي نستخدمها الأداة wget لكنها لا تحتاج أية رايات: $ docker exec -it reverse-proxy bash root@374f9e62bfa8:/# curl http://localhost:80 <html> <head><title>502 Bad Gateway</title></head> ... يمكننا الاستفادة من الشبكة التي يهيئها docker-compose عند تنفيذ الأمر docker-compose up، فهي تضيف كل الحاويات الموجودة في الملف "docker-compose.yml" إلى الشبكة. يتأكد خادم DNS من إمكانية إيجاد بقية الحاويات، وتُمنح كل منها اسمان الأول اسم الخدمة والآخر اسم الحاوية. طالما أننا ضمن الحاوية، سنختبر خادم DNS، لنستخدم إذًا curl لطلب الخدمة التي تُدعى (app) على المنفذ 3000 root@374f9e62bfa8:/# curl http://app:3000 <!DOCTYPE html> <html lang="en"> <head> ... <meta name="description" content="Web site created using create-react-app" /> ... هذا كل ما في الأمر. لنبدل الآن عنوان proxy_pass في الملف "nginx.conf" بهذا العنوان (http://app:3000). إن ظهرت الصفحة 502 مجددًا ، تأكد من بناء create-react-app أولًا، واقرأ الخرج الناتج عن تنفيذ الأمر docker-compose up. أمر آخر: لقد أضفنا الخيار depends_on إلى الإعدادات لنتأكد أن حاوية "nginx" لن تعمل قبل حاوية الواجهة الأمامية "app": services: app: # ... nginx: image: nginx:1.20.1 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro ports: - 8080:80 container_name: reverse-proxy Depends_on: - app إن لم نفرض تسلسل الإقلاع هذا باستخدام الخيار depends_on فقد نقع في خطر فشل إقلاع لأنه يحاول تحليل أسماء نطاقات DNS التي يُشار إليها في ملف الإعدادات: http { server { listen 80; location / { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_pass http://app:3000; } } } تجدر الملاحظة أن الخيار depends_on لا يضمن أن تكون الخدمة في الحاوية جاهزةً للعمل، بل يضمن أن الحاوية قد بدأت العمل وأضيف المُدخل الخاص بها إلى خادم DNS، لكن إذا أردت من خدمة أن تنتظر أخرى حتى تجهز، فعليك الاطلاع على حلول أخرى. التمرينات 12.17 - 12.19 حاول إنجاز التمرينات التالية: التمرين 12.17: إعداد خادم وكيل معكوس Nginx أمام الواجهة الأمامية لتطبيق المهام سنحاول في هذا التمرين وضع خادم Nginx أمام الواجهتين الأمامية والخلفية لتطبيق المهام todo-app. سنبدأ بإنشاء ملف docker-compose جديد "todo-app/docker-compose.dev.yml" وملف تهيئة Nginx يحمل الاسم "todo-app/nginx.conf". todo-app ├── todo-frontend ├── todo-backend ├── nginx.conf └── docker-compose.dev.yml أضف الخدمتين nginx و todo-app/todo-frontend/dev.Dockerfile إلى الملف "todo-app/docker-compose.dev.yml". التمرين 12.18: إعداد خادم ليكون أمام الواجهة الخلفية لتطبيق المهام أضف الخدمة todo-backend إلى الملف "*todo-app/docker-compose.dev.yml" في وضع التطوير، ثم أضف مكانًا جديدًا للملف كي تُخدَّم الطلبات إلى العنوان "api/" عبر الخادم الوكيل إلى الواجهة الخلفية. قد يكون القالب التالي مناسبًا لإنجاز الأمر: server { listen 80; # (/) تُعالج الطلبات التي تبدأ بالمحرف location / { # نحتاج الأسطر الثلاثة التالية للتحميل المباشر للتغييرات proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; # http://localhost:3000 تُوجَّه الطلبات مباشرةً إلى العنوان proxy_pass http://localhost:3000; } # (/api/) تُعالج الطلبات التي تبدأ بالمسار location /api/ { ... } } للتوجيه proxy_pass ميزةٌ مهمة عندما تُضاف إليه الشرطة المائلة الزائدة "/"، وطالما أننا نستخدم المسار "api/" لتحديد المكان علمًا أن تطبيق الواجهة الخلفية سيجيب فقط على العنوان "/" أو "todos/" فلا بد من إزالة "api/" من الطلب. وبكلمات أخرى، حتى لو أرسل المتصفح الطلب GET إلى العنوان "api/todos/1/" نريد من الخادم Nginx أن ينقل الطلب بالوكالة إلى العنوان "todos/1/". لتنفيذ الأمر لا بد من إضافة شرطة مائلة "/" زائدة إلى العنوان في نهاية التوجيه proxy_pass. انتبه للموضوع جيدًا فقد تقضي ساعات في البحث عن حل لمشاكل سببها إهمال الشرطة الزائدة. وهذه إحدى المشاكل الشائعة: إليك هذه الصورة التوضيحة التي قد تنفع في توضيح الأمر عندما تواجهك المشاكل: التمرين 12.19: ربط الخدمتين todo-frontend و todo-backend سلّم في هذا التمرين بيئة التطوير بأكملها بما في ذلك تطبيقي Express و React وملفات Dockerfiles والملف "docker-compose.yml". تأكد بدايةً من عمل الواجهة الأمامية مع الواجهة الخلفية، وسيتطلب ذلك تغييرات في متغير البيئة REACT_APP_BACKEND_URL. وإذا كانت الواجهة جاهزةً للعمل خلال التمرين السابق يمكنك تجاوز هذه الخطوة. تأكد من أن بيئة التطوير تعمل الآن بكامل طاقتها، أي: أن تعمل جميع ميزات تطبيق المهام. عندما تغيّر الشيفرة المصدرية لا بُد أن تظهر نتيجة التغيرات مباشرةً في حال كان التغيير في الواجهة الأمامية وبإعادة تحميل التطبيق إن كان التغيير في الواجهة الخلفية. أدوات لمرحلة الإنتاج التعامل مع الحاويات ممتعٌ في مرحلة التطوير، لكن أفضل حالات الاستخدام ستكون في مرحلة الإنتاج، إذ توجد أدوات أكثر قدرة من docker-compose في تشغيل الحاويات في مرحلة الإنتاج. تسمح لنا أداة تنسيق الحاويات Kubernetes مثلًا بإدارة الحاويات ضمن مستوى جديد بالكامل، وتخفي تلك الأدوات عمومًا التجهيزات الفيزيائية المستخدمة مما يجعل المطورين أقل انشغالًا بأمور البنية التحتية. إن كنت ترغب في الاطلاع أكثر على الحاويات باستخدام Docker فعليك بالمنهاج DevOps with Docker، وكذلك المنهاج DevOps with Kubernetes لتتعلم تنسيق الحاويات باستخدام Kubernetes. التمرينات 12.20-12.22 حاول إنجاز التمرينات التالية: التمرين 12.20 أنشئ نسخة إنتاج من الملف "todo-app/docker-compose.yml" تضم كل الخدمات: Nginx و todo-backend و todo-frontend و MongoDB و Redis. استخدم الملف "Dockerfiles" بدلًا من "dev.Dockerfiles" وتأكد من تشغيل التطبيق في وضع الإنتاج. استخدم الهيكلية التالية في هذا التمرين: todo-app ├── todo-frontend ├── todo-backend ├── nginx.conf ├── docker-compose.dev.yml └── docker-compose.yml التمرين 12.21 أنشئ بيئة تطوير تعتمد على الحاويات مشابهة لما فعلنا، وذلك لأحد التطبيقات التي مرّت معك خلال منهاجنا أو التي أنشاتها في أوقات فراغك. اجعل هيكلية تطبيقك ضمن مستودع التسليم على النحو التالي: └── my-app ├── frontend | └── dev.Dockerfile ├── backend | └── dev.Dockerfile └── docker-compose.dev.yml التمرين 12.22 أنهِ القسم بإعداد نسخة إنتاج تعتمد على الحاويات خاصة بالتطبيق الذي اخترته. جعل هيكلية تطبيقك ضمن مستودع التسليم على النحو التالي: └── my-app ├── frontend | ├── dev.Dockerfile | └── Dockerfile ├── backend | └── dev.Dockerfile | └── Dockerfile ├── docker-compose.dev.yml └── docker-compose.yml ترجمة -وبتصرف- للفصل basics of orchestration من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: بناء الصور وتهيئة بيئة العمل للتطبيقات المبنية ضمن الحاويات مدخل إلى الحاويات أبرز المفاهيم التي يجب عليك الإلمام بها عن الحاويات
  21. وصلنا في سلسلة مقالتنا إلى موضوع أكثر عمقًا، بعد أن خدشنا السطح الخارجي لأوبونتو، الذي هو أحد التوزيعات الشهيرة لنظام لينكس مفتوح المصدر. تأتي إنتاجية نظام التشغيل من كم ونوع التطبيقات التي يشغلها ويدعمها، وسهولة تثبيت هذه البرمجيات وإزالتها بأبسط طريقة ممكنة، دون أية مضاعفات تؤثر على استقرار النظام. وليس من السهل على القادمين الجدد إلى لينكس إدارة عملية تثبيت وإزالة التطبيقات، فليست جميعها بسهولة برمجيات ويندوز أو ماك التي يكفي في معظم اﻷحيان النقر المزدوج على ملف التثبيت ليقودك معالج التثبيت خطوةً خطوة خلال العملية، عن طريق واجهات رسومية يمكن استيعابها. مع ذلك، وكمستخدم مبتدئ، ومن منطلق أنك تستخدم حاسوبك لأغراض عامة؛ سنقدم لك مجموعةً مفيدةً من اﻷفكار والنصائح التي تساعدك في تثبيت معظم البرمجيات التي تحتاجها، دون أن تغوص في متاهات أوامر الطرفية Terminal، التي تتيح لك إمكانات واسعة في العمل مع النظام وفهمه في آن معًا، وإن كنت في مرحلة ما خلال مسيرتك، فستضطر إلى التعامل معها. تحديث النظام قبل كل شيء أوبونتو وكغيره من توزيعات لينكس، يُحدَّث باستمرار من جميع النواحي انطلاقًا من عملياته البنيوية وحتى البرمجيات التي ثُبِّتت عليه. ولهذا السبب وقبل أن تفكر في تثبيت أي تطبيق جديد، احرص أن يكوُن نظام التشغيل محدّثًا ومدعومًا بآخر إصدارات مكتباته واعتمادياته. في واقع اﻷمر، يُحدّث أوبونتو نفسه بنفسه، فهو مُعدٌّ افتراضيًا للتحديث التلقائي؛ إذ يتصل النظام بخوادم مخصصة وموثوقة للحصول على آخر إصدارات الحزم Packages التي تضم معلومات التحديث أو الموارد الجديدة، ثم يفك هذه الحزم ويثبت محتواها. مع ذلك، لابد من الاطلاع على بعض التفاصيل المهمة التي يمكن الوصول إليها من خلال تطبيق "البرمجيات والتحديثات". تطبيق "البرمجيات والتحديثات" لننتقل إذًا إلى قائمة التطبيقات ونفتح هذا التطبيق. يعرض التطبيق مجموعةً من النوافذ الفرعية، وسنلقي نظرةً على ما نحتاجه منها حاليًا: برمجيات Ubuntu: يحدد هذا الخيار ما يمكن تثبيته من برمجيات خاصة لدعم نظام أوبونتو من خلال اﻹنترنت، إذ يعطيك خيار تثبيت برمجيات حرة ومفتوحة المصدر مصدرها الرئيسي هو شركة كانونيكال (الشركة المطورة لنظام التشغيل أوبونتو) وتدعى البرمجيات الرئيسية Main؛ كما يعطيك خيار تثبيت برمجيات حرة ومفتوحة المصدر من شركاء تدعمها كانونيكال (تُدعى برمجيات عامة Universe)، وخيار البحث عن تعريفات للعتاد الصلب مملوكة من قبل مطوريها، وخيار تثبيت برمجيات مقيدة ومملوكة لأصحابها (تُدعى برمجيات مختلفة المصادر Multiverse). قد تختار بعض هذه اﻷنواع من البرمجيات أو جميعها بما يلائمك وننصحك بترك هذه اﻹعداد كما هو. برمجيات أخرى: لتنزيل برمجيات وتعريفات تطوّرها كانونيكال لحساب شُركائها مع شيفرتها المصدرية. التحديثات: يتيح لك التطبيق مجموعةً من الخيارات للتحقق من وجود تحديثات وهي: التسجيل في قائمة التحديثات subscribe to: تتيح لك اﻹشتراك بكل التحديثات all updates أو بالتحديثات اﻷمنية والتحديثات المزكّاة security and recommended updates، أو التحديثات اﻷمنية فقط. ويُنصح المبتدئ هنا بترك الخيار اﻹفتراضي. فحص التحديثات تلقائيًا: قد تختار فحص التحديثات يوميًا أو أسبوعًا أو شهريًا، وقد لا تختار الفحص التلقائي أبدًا. لذا يُنصح هنا باختيار الفحص التلقائي اﻷسبوعي، لأنها فترة زمنية معقولة لوجود تحديثات أمنية، وهذا أمر حيوي جدًا. عند وجود تحديثات أمنية: يُنصح هنا بخيار "اعرضها فورًا"، فهذا الخيار أكثر مرونةً، ويبقيك على اطلاع بما سيجري تثبيته؛ إذ يعرض لك النظام من خلال تطبيق آخر يُدعى "مُحدث البرمجيات" نوع التحديثات المتوفرة، ويترك لك الخيار بتثبيتها أو لا. قد يجد الكثيرون أن خيار "نزّلها وثبتها تلقائيًا" هو اﻷنسب كمبتدئ -وهذا صحيح للوهلة اﻷولى-، لكنك قد تشعر بالصدمة إن كنت تحاول في وقت ما تثبيت برنامج ضروري ولا تلاحظ أي تقدم في عملية تثبيته! عند توفر تحديثات أخرى: ننصحك أيضًا بخيار "اعرضها فورًا" للأسباب السابقة نفسها. لن نتحدث عن باقي النوافذ الفرعية للتطبيق كي لا ندخلك في دوّامات لست مستعدًا لها حاليًا، وسنكتفي بهذا القدر من الشرح، فهذا ما تحتاجه حاليًا. تطبيق "محدث البرمجيات" يبحث هذا التطبيق عن آخر التحديثات التي تتعلق بالبرمجيات وتحديثات النظام والتحديثات اﻷمنية، وذلك وفقًا لخياراتك التي اعتمدها في تطبيق "البرمجيات والتحديثات"، كما يعرض لك كل جديد تلقائيًا، أو يمكنك فتحه إن أردت من قائمة التطبيقات. يصنف هذا التطبيق التحديثات المتوفرة كما يلي: قاعدة Ubuntu: وتضم التحديثات الجديدة على نواة النظام. تحديثات أمنية. تحديثات برمجية. يمكن أن تختار ما تشاء منها لتثبيته حسب اﻷولوية التي تراها، ووفقًا لوقتك وانشغالك؛ لكن يُنصح هنا بتثبيتها تباعًا إن لم يكن فورًا. انقر على زر "ثبت اﻵن" لبدأ عملية تثبيت التحديثات، أو "تذكير لاحقًا" للتثبيت في وقت لاحق. تثبيت تطبيقات "سناب" من خلال تطبيق "برمجيات" من جنوم سناب هو اﻹسم الذي يُطلق على حزمة برمجية تضم كل ما يتطلبه تطبيق معين من ملفات وموارد واعتماديات، ليعمل باستقلالية وبشكل منفصل عن ملفات النظام واعتمادياته. لقد صُممت حزم سناب من قِبل شركة كانونيكال نفسها التي صممت أوبونتو، وكان الهدف منها أن تعمل التطبيقات على كل توزيعات لينكس بسلاسة، ودون أن تُضطر إلى مشاركة بيانات أو ملفات مع تطبيقات أخرى مخصصة للعمل على توزيعات مختلفة. يُحمِّل المطوّرون وأصحاب الشأن (وفق آلية محددة) تطبيقات سناب إلى ما يُعرف بمخزن سناب أو Snap-Store بما يشابه ما يجري على متجر غوغل بلاي أو ماك ستور. ولكي تستعرض هذه التطبيقات وتثبتها، ستجد نفسك أمام خيارين: اﻷول هو تعليمات سطر اﻷوامر (الطرفية)، أو الثاني هو خيار الواجهات الرسومية التي يفضّلها المستخدمون العاديون أو حديثو العهد. وفي كلتا الحالتين يعمل تطبيق سناب-دي أو سناب دايمون snapd على ضمان تنزيل وتثبيت وتحديث تطبيقات سناب تلقائيًا. لا أنوي إطلاقًا الخوض في تفاصيل الطرفية وتعليماتها في هذا المقال، لذا سأتوجه مباشرةً إلى الواجهات الرسومية التي يتوفر منها الكثير، والتي تختلف اﻷراء واﻷهواء في استحسانها واستهجانها. مع ذلك، وطالما أننا نعمل على سطح مكتب جنوم سنستخدم الواجهة الرسومية الخاصة به وتُدعى "برمجيات" في النسخة العربية. نظرة على "برمجيات" يرتبط هذا البرنامج تلقائيًا بمخزن سناب (وبغيره من المصادر الموثوقة أيَضًا)، ويعرض لك من خلال واجهة رسومية واضحة وسهلة الاستخدامات. أغلب التطبيقات المتوفرة مصنفة بطريقة يسهل معها البحث، كما تساعدك الواجهة أيضًا على تثبيت وإزالة التطبيقات واستعراضها؛ مع اﻹشارة إلى التطبيقات التي تتوفر لها تحديثات (مع أن تطبيقات سناب تُحدّث تلقائيًا). انقر على زر "أظهر التطبيقات" في شريط التطبيقات، ثم انقر على تطبيق "برمجيات". إن طُبَّقت أية تحديثات مؤخرًا على نظامك، فستظهر لك الشاشة التالية: تخبرك هذه الشاشة بأن التطبيق يتواصل مع مخازن التطبيقات لتنزيل قوائم بأسماء التطبيقات المتوفرة حاليًا ومعلومات عنها. وعند اﻹنتهاء من تنزيل هذه المعلومات، ستظهر لك نافذة التطبيق كما في اللقطة التالية: لنبدأ أولًا باستكشاف نافذة التطبيق قبل أن ننتقل إلى موضوع تثبيت وإزالة التطبيقات. يحتوي شريط المهام على ثلاثة أزرار تَعرض ثلاثة نوافذ فرعية: استكشاف Explore: وهي النافذة التي تُعرض افتراضيًا. تُصنِّف النافذة التطبيقات الموجودة في المخازن إلى فئات مختلفة مثل "الصوتيات والمرئيات" و"اﻷلعاب" و"أدوات التطوير" وغيرها، وبالنقر على أية فئة، ستعرض لك النافذة مجموعة التطبيقات التي تنتمي إلى هذه الفئة، كما يمكنك ترتيبها وفقًا للتقييم أو وفقًا للاسم؛ كما تعرض النافذة بعض التطبيقات التي يزكّيها المحررون، إذ يرونها مناسبة ومفيدة للمستخدم. المنصَّبة: تعرض هذه النافذة جميع التطبيقات المثبتة على حاسوبك سواءٌ التطبيقات التي ثبتها بنفسك، أو التي ثُبتت افتراضيًا مع النظام؛ باﻹضافة إلى تطبيقات النظام. ستجد إلى جوار كل تطبيق الزر "أزل" ﻹزالة التطبيق، كما ستجد أسفل الزر حجم هذا التطبيق. التحديثات: تعرض هذه النافذة التطبيقات التي تتوفر لها تحديثات جديدة إن لم تكن هنالك تطبيقات تحتاج إلى تحديث تبلغك النافذة أن برامجك محدّثة. تطبيق عملي: تثبيت تطبيق معالجة الصور "جيمب" من جنوم كمثال عن تثبيت البرمجيات افتح تطبيق "برمجيات"، ثم اختر فئة "الرسوميات والتصوير"؛ وابحث بين التطبيقات المتوفرة عن تطبيق"جنو لمعالجة الصور"، أو انقر على زر البحث في أقصى يمين شريط مهام النافذة، واكتب "gimp" ليبحث عنه التطبيق؛ عندها ستظهر النتائج التي تتطابق مع معايير البحث، ومن بينها تطبيق "جيمب". انقر على أيقونة التطبيق لتنتقل إلى نافذة التثبيت التي تقدم معلومات عن عمل التطبيق ورخصة استخدامه ومصمميه وبعض المراجعات والتقييمات التي وضعها المستخدمون. لتثبيت البرنامج، انقر على زر "نصّب". قد تستغرق العملية وقتًا طويلًا أو قصيرًا وفقًا لحجم التطبيق ولسرعة اتصالك باﻹنترنت، والضغط على خادم التثبيت ومشغولية نظامك (فقد يكون مشغولًا في تنصيب أو تحديث بعض البرمجيات). على أية حال، اترك لتطبيق "برمجيات" الوقت الكافي ﻹنجاز الأمر. يخبرك "برمجيات" عند انتهاء التنزيل والتثبيت بأن البرنامج قد ثُبِّت بنجاح على حاسوبك، وستظهر أيقونته ضمن قائمة التطبيقات. تثبيت التطبيقات المجمّعة في حزم ديبيان Dep. لا بد من اﻹشارة إلى وجود الكثير من التطبيقات التي ينتجها مطوّرون مستقلون أو شركات معروفة مجمّعة أو محزّمةً بأرشيفات تدعى حزم ديبيان (نسبةً لنظام التشغيل لينكس ديبيان الذي يتفرع عنه أوبونتو)؛ إذ تقدم هذه الشركات تطبيقاتها على مواقعها الخاصة ويمكنك عندها تنزيل هذه الحزمة وتثبيت التطبيق. تُعدُّ حزم ديبيان أقل حجمًا من مثيلاتها في سناب، إلا أنها أقل أمانًا، وقد ترتبط بتطبيقات أخرى أو بموارد واعتماديات للنظام. تظهر حزم ديبيان على شكل مجلد مضغوط وردي اللون ينتهي اسم الحزمة باللاحقة dep.. ولتثبيت هذه الحزمة، انقر نقرًا مزدوجًا عليه، وهذا كل ما في اﻷمر. يهتم تطبيق "البرمجيات" بأمر تثبيت هذا التطبيق بالطريقة الملائمة، فعلى الرغم من ارتباط هذا التطبيق بمخازن سناب لتنزيل وتثبيت هذا النوع من التطبيقات، إلا أنه قادر أيضًا على الارتباط بتطبيق "تنصيب البرمجيات" لإدارة حزم ديبان وتنصيبها بطريقة مشابهة لتطبيقات سناب. تطبيق عملي: تثبيت برنامج "فيجوال ستديو كود" من حزمة ديبيان ابحث بكل بساطة مستخدمًا أي محرك بحث عن "visual studio code" وهو تطبيق من شركة مايكروسوفت لتحرير كم هائل من ملفات الشيفرة للكثير من اللغات ويُعد محررًا ممتازًا لك كمبتدئ أيضًا إن أردت البدء بتعلم البرمجة. تعرض لقطة الشاشة التالية لمتصفح فايرفوكس صفحة تنزيل البرنامج: لاحظ أن التطبيق متوفر على الموقع بصيغة dep. لتوزيعات ديبيان وأوبونتو. انقر على الرابط، وسيبدأ المتصفح بتنزيل حزمة التطبيق. عند اكتمال التطبيق، انقر نقرًا مزدوجًا على اﻷرشيف، وستظهر لك النافذة التالية المرتبطة بتطبيق "البرمجيات": انقر زر "نصّب" وانتظر اكتمال عملية التثبيت. حزم أخرى هل سأصادف حزمًا أخرى يفهمها أوبونتو على أنها تطبيقات ويثبتها من خلال تطبيق "البرمجيات"؟ في الواقع نعم، وسنتكلم بإيجاز عن أحدها، وهي حزم "فلات-باك"، اختصارًا للامتداد flatpak. أو اﻹصدار اﻷحدث الذي يضم تعليمات وصفية أكثر عن التطبيق flatpakref. تمتاز هذه الحزم بأنها محتواة في حاوية خاصة بها وتثبّت في معظم توزيعات لينكس دون الحاجة إلى شيئ خارج إطار الحاوية، فهي مستقلة بذاتها مثل تطبيقات سناب. ويمكنك الحصول على هذه الحزم من مخزن فلات-هب flathub، حيث يرفع المطوّرون تطبيقاتهم إليه، كما يمكن أن تجدها على سناب-ستور أيضًا وقد تكون العديد من التطبيقات التي تثبتها من خلال تطبيق "البرمجيات" هي حزم "فلات-باك" في الواقع. على أية حال إن حصلت على التطبيق الذي تريد تثبيته مجمّعًا في أرشيف فلات-باك، فاﻷمر يسير. انقر نقرًا مزدوجًا على الحزمة، وسيتكفل تطبيق "البرمجيات" بكل شيء كما في حزم "ديبيان". خلاصة يساعدك تطبيق "برمجيات" على البحث عن العديد من التطبيقات الموجودة في مخازن مختلفة وموثوقة، إذ يعرض لك وصفًا عن عملها ورخصة استخدامها والجهة التي طوّرتها وتقييم المستخدمين لها. إن لم تجد التطبيق الذي تبحث عنه في قوائم البرمجيات التي يقدمها تطبيق "البرمجيات"، فعليك البحث عنه ضمن مواقع اﻹنترنت، وبالطبع قد تكون العملية محفوفةً بالمخاطر. بمجرد أن تجد الموقع المناسب لتنزيل التطبيق المطلوب وتتأكد من سلامة الموقع والحزمة، حاول أن تنزّل النسخة الخاصة بنظام أوبونتو. وإن تعّذر اﻷمر أو رأيته غامضًا، فابحث عن حزم بإحدى الامتدادات أو اللواحق التالية: dep. flatpak. flatpakref. نزّل هذه الحزمة ثم انقر نقرًا مزدوجًا عليها لتبدأ عملية التثبيت. وهكذا نكون قد قدمنا في مجموعة المقالات هذه الموجهة إلى القادمين الجدد إلى عالم لينكس، شرحًا مكثفًا ومبسطًا لمعظم النواحي التي يحتاجها مستثمر نظام أوبونتو حتى يألف بيئة العمل ويزيد من إنتاجيته. اقرأ أيضًا المقال السابق: إعدادات أوبونتو 20.04: التجهيزات والإعدادات الإقليمية والشمولية إعدادات أوبونتو 20.04: التطبيقات وإدارة المستخدمين تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  22. يصف هذا المقال ما يجري عند استعراض صفحة ويب على حاسوبك أو هاتفك المحمول بصورة مبسّطة، فقد لا تجد هذا الأمر بدايةً أمرًا أساسيًا لكي تكون قادرًا على كتابة شيفرة ويب، لكنك ستجني بسرعة فائدة معرفتك خفايا العملية. الخوادم والعملاء تُدعى الحواسب التي تتواصل على ويب بخوادم servers وعملاء clients، وإليك توضيحًا مبسّطًا عن طريقة تفاعلها: العميل: يُعرَّف تقليديًا بأنه جهاز متصل بويب مثل حاسوب عبر شبكة لاسلكية أو هاتف محمول عبر شبكة خلوية، بالإضافة إلى برنامجيات على هذه الأجهزة قادرة على الوصول إلى ويب مثل متصفحات ويب مثل كروم أو فايرفوكس عادةً. الخادم: هو حاسوب يخزِّن صفحات ومواقع وتطبيقات ويب. حيث يزوّد الخادم الجهاز العميل بنسخة عن صفحة ويب عندما يريد الوصول إليها ليتمكن من عرضها على متصفح المستخدِم. تفاصيل أكثر لن تكتمل القصة بمفهومَي الخادم والوكيل التي عرضناها قبل قليل، فهنالك الكثير من الأجزاء الأخرى التي سنناقشها تاليًا. دعونا نتخيل الآن أنّ الويب هي طريق يقع العميل في طرفه الأول وليكن بيتك مثلًا ويقع الخادم في طرفه الآخر وليكن متجرًا تشتري منه حاجياتك على سبيل المثال. عليك أن تتعرف على ما يلي بالإضافة إلى الخادم والعميل: الاتصال بالإنترنت: الذي يسمح لك بإرسال واستقبال البيانات من وإلى ويب، فهو تقريبًا مثل الشارع الذي يربط بيتك بالمتجر. بروتوكول TCP/IP: يُعَدّ بروتوكول التحكم بالنقل Transmission Control Protocol وبروتوكول الإنترنت Internet Protocol بروتوكلَي اتصال يحددان آلية نقل البيانات عبر الإنترنت، إذ يشابه هذان البروتوكولان وسيلة التنقل التي تتخذها لتنظيم رحلتك من منزلك إلى المتجر وشراء حاجياتك، فقد تكون في مثالنا سيارة أو دراجة أو أيّ شيء مماثل. خادم أسماء النطاقات Domain Name System أو DNS اختصارًا: يمكن تشبيهه بدفتر عناوين لمواقع ويب، فعندما تكتب عنوان موقع ويب في متصفحك، يبحث خادم DNS عن عنوان بروتوكول الإنترنت IP address الموافق للموقع قبل إحضاره، إذ يحتاج المتصفح لمعرفة الخادم الذي يستضيف الموقع المطلوب ليرسل رسائل HTTP إلى الوجهة الصحيحة، ويشبه الأمر في مثالنا البحث عن عنوان المتجر. بروتوكول HTTP: وهو بروتوكول نقل النص التشعبي Hypertext Transfer Protocol ويُعَدّ بروتوكولًا تطبيقيًا يُوصِّف لغةً بين الخادم والعميل ليتواصلا من خلاله، أي الأمر مشابه أيضًا للغة المحكية التي تتواصل بها لطلب حاجياتك من المتجر. الملفات المكوّنة للموقع: يتألف الموقع من ملفات مختلفة بصورة مشابهة للبضائع المختلفة التي تحتاجها من المتجر، وتأتي هذه الملفات ضمن صنفين أساسيين: ملفات شيفرة Code files: تُبنى مواقع ويب بصورة أساسية من ملفات HTML و CSS وجافاسكربت، كما ستتعرف لاحقًا على ملفات أخرى تخدم تقانات مختلفة. ملفات مساعدة أو الأصول Assets: تشمل كل الأشياء التي يتكوّن منها الموقع مثل الصور والموسيقى والفيديو والمستندات النصية وغيرها. ما الذي يحدث عند طلب موقع ويب؟ إليك ما يحدث عندما تكتب عنوان موقع في شريط متصفحك، وهو أمر مماثل لانطلاقك نحو المتجر: يتوجه المتصفح إلى خادم DNS ويجد العنوان الحقيقي للخادم الذي يستضيف هذا الموقع، أي مثل إيجاد العنوان المفصّل للمتجر. يرسل المتصفح طلب HTTP إلى الخادم يسأله فيها إرسال نسخة عن موقع الويب إلى العميل، أي عندما تذهب إلى المتجر وتطلب حاجياتك، إذ تُرسل الطلبات وغيرها من البيانات بين العميل والخادم عبر اتصال الإنترنت من خلال بروتوكول TCP/IP. سيرسل الخادم إذا وافق على طلب العميل رسالةً تحمل العدد 200 وتعني تمامًا أنه بإمكانك الاطلاع على هذا الموقع، إذ يبدأ الخادم بعدها بإرسال ملفات الموقع إلى المتصفح على أساس سلسلة من القطع تُدعى رزم البيانات، أي عندما يبدأ المشرف على المتجر بإعطائك البضائع التي طلبتها ثم تنقلها إلى منزلك. يُجمِّع المتصفح حزم البيانات ضمن صفحة مكتملة ويعرضها لك مثل وصول حاجياتك كاملة إلى منزلك واستخدامها. الترتيب الذي يستخدمه المتصفح في تفسير الملفات عندما تطلب المتصفحات من الخوادم ملفات HTML، فستشير هذه الملفات من خلال العناصر <link> إلى ملفات CSS خارجية، ومن خلال العناصر <script> إلى ملفات جافاسكربت الخارجية، فمن المهم إذًا معرفة الترتيب الذي تفسر به المتصفحات ملفات موقع عندما تحمّله: يفسّر المتصفح ملف HTML أولًا، وبالتالي سيميّز العناصر <link> التي تشير إلى ملفات CSS الخارجية، كما يميّز العناصر <script> التي تشير إلى ملفات الشيفرة. يرسل المتصفح أثناء تفسير ملف HTML طلبات إلى الخوادم التي قد تستضيف ملفات CSS أو ملفات الشيفرة التي تشير إليها العناصر السابقة، ثم يفسّر هذه الملفات. يولّد المتصفح شجرة DOM مقيمة في الذاكرة انطلاقًا من ملف HTML الذي فسّره ومن ثم شجرة CSSOM بعد تفسير ملف CSS ثم يصرِّف وينفذ شيفرة جافاسكربت التي فسّرها. عندما يبني المتصفح شجرة DOM ويطبّق التنسيقات استنادًا إلى شجرة CSSOM وينفّذ شيفرة جافاسكربت، تُعرض الصفحة على الشاشة بعملية تُدعى الرسم paint ليراها المستخدِم ويكون قادرًا على التفاعل معها. توضيح عن خادم أسماء النطاقات DNS من الصعب تذكُّر عناوين ويب الحقيقية لأنها ليست عبارات نصية واضحة، وإنما هي أعداد خاصة تبدو مشابهةً للسلسلة 63.245.215.20، إذ تُدعى هذه السلسلة بعنوان بروتوكول الإنترنت IP address ويمثل موقعًا فريدًا على ويب، وطالما أنه من الصعب تذكر هذه العناوين، فقد اختُرعت خوادم أسماء النطاقات، وهي خوادم خاصة تربط عنوان موقع ويب الذي تدخله في شريط المتصفح مثل "hsoub.com" بعنوان بروتوكول الإنترنت الحقيقي له. يمكن أيضًا الوصول إلى مواقع ويب من خلال عناوين بروتوكول الإنترنت، لكن عليك أن تحصل أولًا على هذا العنوان عن طريق بعض الأدوات مثل IP Checker. توضيح عن رزم البيانات استُخدم المصطلح رزم packets لوصف هيئة البيانات التي يرسلها الخادم إلى العميل، فما الذي نعنيه بالرزم هنا؟ عندما تُرسل البيانات عبر الويب فإنها تُرسَل عبر آلاف الأجزاء الصغيرة وذلك لأسباب كثيرة، فقد تُهمل هذه البيانات أو تتشوه أثناء نقلها ومن الأسهل استبدال قطعة صغيرة إذا حدث ذلك، إضافةً إلى هذا الأمر، فقد توجّه الرزم إلى مسارات مختلفة، مما يجعل تبادلها أسرع ويسمح لعدد كبير من المستخدِمين تنزيل الموقع نفسه في الوقت عينه، بينما إذا أُرسِل الموقع دفعةً واحدةً، فلن يتمكن سوى مستخدِم واحد من تنزيل الموقع كل مرة، مما يجعل الويب غير فعالة وغير مسليَة. ترجمة -وبتصرف- للمقال How the web works. اقرأ أيضًا كيف تعمل شبكة الإنترنت كيفية التعامل مع الويب المدخل الشامل لتعلم تطوير الويب
  23. تُعَدّ جافاسكربت JavaScript لغة برمجة تزيد من القدرة التفاعلية لمواقع ويب، وتشاهد ذلك مثلًا في الألعاب وفي مظاهر استجابة الصفحات عند نقر الأزرار أو عند إدخال البيانات إلى النماذج والاستمارات الإلكترونية، أو من خلال التغيير الديناميكي لتنسيق الصفحة أو عبر الرسوم المتحركة وغيرها، إذ سيساعدك هذا المقال لتبدأ استخدام جافاسكربت ويعرّفك أكثر بإمكانيات هذه اللغة. تعرف على جافاسكربت تُعَدّ جافاسكربت لغة برمجة قوية تزيد من القدرة التفاعلية لمواقع ويب، وقد ابتكرها برنارد آيتش Brendan Eich، وهو مؤسس مشارك لمشروع موزيللا ومؤسسة موزيللا وشركة موزيللا، كما تُعَدّ جافاسكربت لغةً قريبةً من المبرمجين المبتدئين، إذ ستتمكن مع الممارسة والخبرة من إنشاء ألعاب ثنائية وثلاثية الألعاب وبناء تطبيقات عصرية مرتبطة بقواعد بيانات وغير ذلك الكثير، وصحيح أنّ هذه اللغة مدمجة مع المتصفحات، لكنها مرنة، فقد بنى المطورون الكثير من الأدوات انطلاقًا من جافاسكربت، مقدِّمين كمًا واسعًا من القدرات الوظيفية بأقل مجهود ممكن، ونذكر منها : واجهات برمجية للتطبيقات API مدمجة بالمتصفحات لتزويدها بوظائف إضافية مثل الإنشاء التلقائي لشيفرة HTML وضبط تنسيقات CSS أو التقاط وتعديل الفيديوهات المصورة عن طريق كاميرا ويب أو توليد رسوميات ثلاثية الأبعاد ومقاطع صوتية. واجهات برمجية لتطبيقات صممها طرف ثالث تسمح للمطورين دمج وظائف يقدمها مزوّدو محتوى مثل تويتر وفيس بوك ضمن مواقع أخرى. أطر عمل ومكتبات صممها طرف ثالث ويمكن تطبيقها على صفحات HTML لتسريع بناء المواقع والتطبيقات. لا يغطي مجال هذا المقال تفاصيل الاختلاف بين الأدوات التي بُنيت وتبنى باستخدام جافاسكربت واللغة نفسها، لذلك يمكن دائمًا البحث عن تفاصيل مثل هذه عبر الإنترنت، وسيقدِّم لك القسم التالي من المقال بعض مزايا جافاسكربت البنيوية، كما سيمنحك إمكانية تجريب بعض مزايا الواجهات البرمجية الخاصة بالمتصفحات أيضًا. كتابة أول برنامج في جافاسكربت تُعَدّ جافاسكربت من أكثر التقنيات الحديثة شعبيةً، وستُدخِل مواقعك مع تقدم مهارتك فيها أبعادًا جديدةً من القوة والإبداع، لكن التآلف مع هذه اللغة أصعب من الاعتياد على العمل مع HTML و CSS، فقد تبدأ بالقليل ثم تنمو قدراتك تدريجيًا، ولنبدأ بتفحّص الطريقة التي سنضيف فيها جافاسكربت إلى صفحتك لتنفيذ برنامج "!Hello world"، وهو برنامج معياري لتقديم لغة برمجة إلى المستخدِم لأوّل مرة. تنبيه: إذا لم تكن لسبب ما متابعًا للأفكار التي تحدثنا عنها في مقالات سابقة، فيمكنك تنزيل الشيفرة التجريبية للمثال واعتماده على أساس نقطة انطلاق. انتقل إلى المجلد الذي يضم موقعك التجريبي وانشئ ضمنه مجلدًا باسم scripts، ثم انشئ ضمن الأخير ملفًا نصيًا ثم احفظه بالاسم main.js. افتح الملف index.html واكتب قبل نهاية العنصر <body> سطر الشيفرة التالية: <script src="scripts/main.js"></script> ينفِّذ هذا العنصر ما ينفذه تمامًا العنصر <link> عندما أدرج ملف تنسيق CSS، إذ يطبِّق هذا العنصر شيفرة جافاسكربت الموجودة في الملف على عناصر HTML في الصفحة بالإضافة إلى تنسيقات CCS أو أيّ شيء آخر. أضف الشيفرة التالية إلى الملف main.js: const myHeading = document.querySelector('h1'); myHeading.textContent = 'Hello world!'; تأكد من حفظ التغيرات على الملفَين index.html وmain.js، ثم حمّل الملف index.html مجددًا في المتصفح الذي سيعرض صفحةً شبيهةً بالتالي: ملاحظة: يعود سبب وضع العنصر <script> في نهاية ملف HTML إلى طريقة القراءة المتسلسلة للشيفرة وفق ترتيب ظهور العناصر التي يتّبعها المتصفح. إذا حمّل المتصفح ملف جافاسكربت أولًا قبل عناصر HTML التي يُفترض أن يؤثر فيها، فقد تقع بعض المشاكل، لذلك من الأفضل تحميله بعد تحميل العناصر بوضع العنصر في نهاية صفحة HTML، فهذا سيحل المشكلة. ما الذي حدث؟ لقد تغيّر عنوان الصفحة إلى "!Hello world" باستخدام جافاسكربت، إذ نُفِّذت العملية عبر استدعاء الدالة ()querySelector التي التقطت مرجعًا إلى عنصر العنوان <h1> وخزّنته في المتغير myHeading، أي الأمر مشابه لما فعلناه باستخدام محددِّات CSS، فإذا أردت تطبيق شيء ما على عنصر، فلا بد من تحديده أولًا. بعد ذلك أُسندت إلى الخاصية textContent العائدة للمتغير myHeading والتي تمثل محتوى العنوان القيمة الجديدة "!Hello world". ملاحظة: تُعَدّ كلتا الميزتين المستخدَمين في هذا المثال جزءًا من الواجهة البرمجية لشجرة DOM التي تمتلك القدرة على التعامل مع مستند HTML. دورة تطوير التطبيقات باستخدام لغة JavaScript تعلم البرمجة بلغة جافا سكريبت انطلاقًا من أبسط المفاهيم وحتى بناء تطبيقات حقيقية. اشترك الآن أساسيات لغة جافاسكربت سنشرح لك فيما يأتي بعض الميزات البنيوية لجافاسكربت لتفهم طريقة عملها بصورة أفضل، ومن الجدير بالذكر أنّ هذه الميزات مشتركة بين كل لغات البرمجة، فإذا أتقنت هذه الأساسيات، فستكون قد وضعت حجر الأساس لكتابة شيفرات بلغات أخرى. تنبيه: حاول أن تُدخل أسطر الشيفرة التجريبية التي تتعلمها في هذا المقال ضمن طرفية جافاسكربت JavaScript console المضمنة داخل المتصفح، ولمزيد من المعلومات عن هذه الطرفية راجع مقال أدوات مطوري ويب المدمجة في المتصفحات. المتغيرات تُعدّ المتغيرات حاويات لتخزين القيم، إذ تبدأ القصة عندما تُصرِّح عن متحول باستخدام التعليمة var (مع أنها غير محبّذة، ستجد التفاصيل لاحقًا) أو التعليمة let يتبعها الاسم الذي تختاره للمتغير: let myVariable; تشير الفاصلة المنقوطة في نهاية السطر إلى نهاية الجملة البرمجية، وتحتاجها فقط عندما تريد الفصل بين العبارات البرمجية في السطر ذاته، لكن وضع فاصلة منقوطة في نهاية كل سطر هي عادة برمجية جيدة، وهنالك عدة قواعد أخرى عن وجوب استخدام الفاصلة المنقوطة وعدم وجوب ذلك، كما يمكنك تسمية المتغيِّر أيّ اسم تقريبًا مع بعض القيود، إذ يمكن الاطلاع على توثيق المتغيرات في موسوعة حسوب، وإذا لم تكن متأكدًا مما كتبت، فتستطيع استخدام بعض الخدمات على الإنترنت للتحقق من صحة تسمية المتغيرات، كما ينبغي الانتباه إلى أنّ جافاسكربت حساسة لحالة الأحرف، فالمتغير myVariable يختلف تمامًا عن myvariable، لذلك تحقق من مشاكل مثل هذه عندما تواجهك الأخطاء. يمكنك إسناد قيمة إلى المتغير بعد التصريح عنه: myVariable = 'Bob'; يمكنك تنفيذ خطوتَي التصريح والإسناد في سطر واحد: let myVariable = 'Bob'; استدع المتغير وحسب لتحصل على القيمة: myVariable; يمكن تغيير قيمة المتغير لاحقًا في مواضع أخرى في الشيفرة: let myVariable = 'Bob'; myVariable = 'Steve'; يمكن أن تحمل المتغيرات قيمًا من أنواع مختلفة: String: يمثل مجموعةً من محارف تمثل سلسلةً نصيةً، ولابد من وضع السلسلة بين إشارتَي إقتباس مفردتين كما يلي: let myVariable ='Bob'; Number: يمثل عددًا، ولا يُوضَع ضمن شارتي تنصيص. let myVariable =10; Boolean: ويمثل أحد القيمين المنطقيتين: صحيح true أو خاطئ false، وهاتين الكلمتين من الكلمات الخاصة المحجوزة في لغة جافاسكربت ولا حاجة لوضعهما بين إشارتي تنصيص: let myVariable = true; Array: يمثل مايُدعى بالمصفوفة، وهي بنية لتخزين عدة قيم تحت مرجع واحد: let myVariable = [1,'bob',10,'mad'] // يمكن الإشارة إلى كل عنصر من عناصر المصفوفة كما يلي myVariable[0]; // 1 تعرض أولى قيم المصفوفة وهي myVariable[3]; // 'mad' تعرض القيمة الأخيرة Object: قد يحمل أي نوع من القيم، فكل شيء في جافاسكربت هو كائن Object يمكن أن يُخزَّن في متغير: let myVariable = document.querySelector('h1'); // الشيفرة نفسها التي استخدمناها سابقًا لِمَ نحتاج المتغيِّرات إذًا؟ لأنها ضروية لتنفيذ كل ما له معنى في البرمجة، فالمتغيرات مخازن القيم، ولن تحصل على أية أفعال ديناميكية مثل تخصيص رسالة ترحيب أو تغيير الصورة المعروضة إذا لم تتغير القيم. التعليقات تُعَدّ التعليقات Comments مقتطفات نصيةً تُضاف إلى الشيفرة ويتجاهلها المتصفح، كما يمكن استخدام التعليقات في جافاسكربت الأسباب نفسها التي تستخدمها في CSS، ويشار إلى نص على أنه تعليق كما يلي: /* Everything in between is a comment. */ لكي يمتد التعليق على عدة أسطر، أو كما يلي: // This is a comment إذا كان التعليق على سطر واحد. العوامل يُعَدّ العامل Operator رمزًا رياضيًا يعطي نتيجةً استنادًا إلى قيمتين أو متغيرين، وإليك بعض العوامل الأبسط التي تُستخدَم في جافاسكربت مع بعض الأمثلة، وحاول تجريبها على طرفية جافاسكربت: الجمع: رمزه +، ويضيف عددين معًا أو يضم نصين إلى بعضهما: 9+6; //15 'hello' + 'world'; // helloworld الطرح والضرب والقسمة: رموزها بالترتيب -، *، /، وعملها مشابه تمامًا لعملها الرياضي: 4-5; //-1 3*3;//9 10/2;//5 الإسناد: رمزه =، ويسند قيمة إلى متغير: let myVar = 3; المساواة: رمزها ===، وتختبر تساوي قيمتين وتعيد نتيجةً منطقيةً؛ إما صحيح true أو خاطئ false : let myVar =3; myVar ===4; //fals تعيد النفي: رمزه !، ويعيد القيمة المنطقية المعاكسة لما يتقدمها، إذ تُغيِّر true إلى false وبالعكس: let myVar=3; !(myVar===4); //true تعيد عدم المساواة: رمزها ‎!==‎، وتختبر عدم تساوي قيمتين وتعيد النتيجة المنطقية المناسبة: let myVar=4; myVar==!3; //true تعيد هناك الكثير من العوامل الأخرى في جافاسكربت، لكننا سنكتفي الآن بما ذكرناه. ملاحظة: قد يقود دمج أنواع مختلفة من البيانات عند إجراء العمليات الحسابية إلى أخطاء، لذا فكن حذِرًا بالإشارة إلى متغيراتك لتحصل على النتيجة المتوقعة، فإذا نفَّذت العملية '35' +'25'، فستحصل على 2535 وهذا ما قد لا تتوقعه، لأن إشارات التنصيص المفردة تجعل ما داخلها نصوصًا لا أعدادًا، بينما إذا نفَّذت العملية على الصورة 35 + 25، فستحصل على نتيجة الجمع الصحيحة. العبارات الشرطية تُعَدّ العبارات الشرطية بُنًى تُستخدَم لاختبار تحقق شرط معيَن نتيجته true أو false، ومن أكثر الصيغ الشرطية استخدامًا هي العبارة if...else، وإليك مثالًا كما يلي: let iceCream = 'chocolate'; if(iceCream === 'chocolate') { alert('Yay, I love chocolate ice cream!'); } else { alert('Awwww, but chocolate is my favorite...'); } تختبر البنية الشرطية العبارة التي تقع ضمن (...)if، إذ تستخدِم البنية في الشيفرة السابقة عامل المساواة للموازنة بين قيمتي المتغّير iceCream والنص chocolate والتحقق من تساويهما، فإذا أعادت الموازنة القيمة true، فستُنفَّذ الكتلة الأولى من الشيفرة التي تلي تعليمة الشرط (...)if، وإلا فستُنفَّذ الكتلة الثانية التي تقع بعد العبارة else. الدوال تُعَدّ الدوال طريقةً لتنظيم الشيفرة التي ترغب في استخدامها مرارًا، إذ يمكنك أن تُعرِّف مثلًا مجموعةً من أسطر الشيفرة على أساس دالة يجري تنفيذها عندما تستدعيها باسمها في أي مكان من الشيفرة، إذ تملك هذه الطريقة فعاليةً كبيرةً في منع تكرار كتابة الشيفرة نفسها كلما احتجنا لها، ولقد رأينا في الشيفرات السابقة نماذجًا لدوال مثل: let myVariable = document.querySelector('h1');//هي دالة querySelector وكذلك: alert('hello!'); إنّ الدالتَين document.querySelector و alert مدمجتان مع المتصفح، فإذا رأيت شيئًا يشبه المتغير متبوعًا بقوسين ()، فهو دالة على الأغلب، كما تأخذ الدوال أشياء تُدعى وسائط arguments، وهي بيانات تحتاجها لتنفيذ وظائفها، إذ تُوضع الوسائط داخل قوسَي الدالة وتفصل بينها فاصلة ,. تعرض الدالة alert على سبيل المثال نافذةً منبثقةً ضمن المتصفح، فلا بد من تزويدها بنص على هيئة وسيط لكي تعرضه، كما يمكنك أيضًا تعريف دالة خاصة بك، إذ سننشئ في المثال التالي دالةً تأخذ وسيطَين عددين ثم تعيد ناتج جدائهما: function multiply(num1,num2) { let result = num1 * num2; return result; } حاول تنفيذ ذلك في الطرفية، ثم اختبر الدالة بتغيير قيم الوسيطَين كما يلي: multiply(4, 7); multiply(20, 20); multiply(0.5, 3); ملاحظة: تخبر التعليمة return في نهاية الدالة أن تعيد قيمةً هي نتيجة تنفيذ الدالة غالبًا -مثل result في المثال السابق- لكي تستطيع استخدامها، وهذا الأمر ضروري لأن المتغيرات التي تُعرَّف داخل الدالة لا يمكن استخدامها خارج الدالة وهذا ما يُعرَف بمجال رؤية المتغير variable scoop. الأحداث لابد من معالجة الأحداث التي تقع في الصفحة لتظهر تفاعلية الصفحة، فالأحداث events هي بنى برمجية تُنصِت إلى النشاطات التي تجري في المتصفح وتستجيب لها بتنفيذ شيفرة محددة، ومن أكثر الأحداث وضوحًا هو حدث النقر على الفأرة والذي يقع عندما تنقر بالفأرة على شيء ما ضمن الصفحة، ولشرح الفكرة، أدخِل الشيفرة التالية في طرفية جافاسكربت ثم انقر على الصفحة التي يعرضها المتصفح: document.querySelector('html').addEventListener('click', function() { alert('Ouch! Stop poking me!'); }); هناك طرق عدة لربط معالِج الحدث event handler بالعنصر، فقد اخترنا في الشيفرة السابقة العنصر <html>، ومن ثم استدعينا معه الدالة addEventListener()‎ مع تحديد اسم الحدث (في حالتنا 'click' النقر) المراد الإنصات له مع تحديد الدالة المراد تنفيذها عند وقوع ذلك الحدث. لاحظ الشيفرة التالية التي تشبه تمامًا الشيفرة السابقة في العمل: document.querySelector('html').onclick = function() { alert('Ouch! Stop poking me!'); } أسندنا فيها الخاصية onclick التي تمثل معالج حدث النقر (الذي يكون اسم الحدث مسبوقًا بكلمة on) إلى دالة دون اسم anonymous تضم الشيفرة التي نريد تنفيذها عندما يقع حدث النقر. أيضًا الشيفرة السابقة والتي تسبقها مماثل للشيفرة التالية: let myHTML = document.querySelector('html'); myHTML.addEventListener('click', function() { alert('Ouch! Stop poking me!'); }); لكنه أقصر. الدالة السابقة التي ممرناها إلى الدالة addEventListener()‎ تدعى دالة مجهولة لعدم امتلاكها اسمًا، وهنالك طريقة أخرى لكتابة دوال مجهولة تدعى الدوال السهمية التي تستعمل الصيغة ‎() =>‎ بدلًا من function ()‎، انظر مثلًا: document.querySelector('html').addEventListener('click', () => { alert('Ouch! Stop poking me!'); }); تطوير المثال التجريبي الذي نعمل عليه لنضِف بعض الميزات الجديدة إلى صفحة الويب التي نطوّرها باستخدام ما تعلمناه عن جافاسكربت. احذف بداية محتوى الملف main.js واحفظ الملف فارغًا كي لا تتعارض الشيفرات الجديدة مع تلك التي كتبناها سابقًا. تغيير للصورة سنتعلم استخدام جافاسكربت وميزات واجهة DOM البرمجية لتبديل الصورة مرةً أو مرتين عند النقر عليها. اختر صورةً جديدةً لاستخدامها والأفضل أن يكون لها قياس الصورة الأولى نفسه. احفظ الصورة في المجلد images باسم firefox2.png. أضف شيفرة جافاسكربت التالية إلى الملف main.js: let myImage = document.querySelector('img'); myImage.onclick = function() { let mySrc = myImage.getAttribute('src'); if(mySrc === 'images/firefox-icon.png') { myImage.setAttribute('src','images/firefox2.png'); } else { myImage.setAttribute('src','images/firefox-icon.png'); } } احفظ جميع التغيرات وحمّل الملف index.html في المتصفح وانقر على الصورة، إذ يجب أن تتغير الصورة المعروضة عند النقر. إليك ما حدث: لقد خزنت مرجعًا إلى العنصر <img> في المتغير myImage، ثم أسندت قيمة الخاصية onclick للمتغير والتي تمثل معالِج حدث النقر إلى دالة دون اسم لكي يُنفِّذ محتواها كلما نقرت على الصورة، وما تفعله الشيفرة هو استخلاص قيمة السمة src لعنصر الصورة واستخدم بنية شرطية للتحقق أن قيمتها تساوي مسار الصورة الأصلية، فإذا كانت كذلك، فستُغيِّر الشيفرة قيمة السمة src للعنصر <img> إلى مسار الصورة الثانية لكي يعرضها المتصفح، وإذا لم تكن كذلك، فهذا يعني أنّ قيمة السمة src قد تغيرت سابقًا وستعيدها الشيفرة إلى قيمتها الأصلية لتُعرض الصورة الأساسية وهكذا. إضافة رسالة ترحيب خاصة لنغيّر الآن عنوان الصفحة كي يعرض رسالة ترحيب خاصة بالمستخدِم عند زيارته للصورة وستبقى هذه الرسالة، ولإن غادر الزائر الصفحة وعاد مجددًا ستظهر الرسالة، لأننا سنخزنها باستخدام الواجهة البرمجية للتخزين على المتصفح، وسنقدم أيضًا خيارًا لتغيير المستخدِم، وبالتالي تغيير رسالة الترحيب. أضف السطر التالي في الملف index.html فقط قبل العنصر <script>: <button>Change user</button> أي أسفل الملف. ضع الشيفرة التالية في نهاية الملف main.js كما هي تمامًا، إذ تُخزِّن هذه الشيفرة مرجعًا إلى الزر الجديد ومرجعًا إلى العنوان داخل متغيرَين: let myButton = document.querySelector('button'); let myHeading = document.querySelector('h1'); أضف الدالة التالية لتخصيص رسالة الترحيب، إذ لن تفعل الدالة شيئًا بالطبع حتى اللحظة لكنها ستفعل قريبًا: function setUserName() { let myName = prompt('Please enter your name.'); localStorage.setItem('name', myName); myHeading.textContent = 'Mozilla is cool, ' + myName; } تحتوي الدالة ()setUserName على الدالة ()prompt التي تعرض مربع حوار على شاشة المتصفح بصورة مشابهة للدالة ()alert لكنها تنتظر من المستخدِم إدخال قيمة لكي تخزنها بعد أن ينقر الزر موافق OK. نطلب في هذه الحالة من المستخدِم إدخال اسمه ثم تستدعي الشيفرة الواجهة البرمجية localStorage التي تسمح بتخزين البيانات في ذاكرة المتصفح للوصول إليها لاحقًا، كما نستخدِم الدالة ()setItem لإنشاء وتخزين عنصر بيانات يُدعى name ثم إسناد قيمته إلى المتغير myName الذي يحوي القيمة التي يدخلها المستخدِم للاسم، ونضيف أخيرًا قيمة المتغير myName إلى محتوى العنوان ثم يُسند النص الناتح إلى الخاصية textContent ليظهر الاسم الذي أدخلناه على أساس جزء من العنوان. أضف الكتلة الشرطية if ... else التالية: if(!localStorage.getItem('name')) { setUserName(); } else { let storedName = localStorage.getItem('name'); myHeading.textContent = 'Mozilla is cool, ' + storedName; } يمكننا استدعاء هذه الشيفرة عند بداية تحميل الصفحة كونها شيفرة تهيئة للمحتوى، إذ يستخدِم السطر الأول منها عامل النفي المنطقي ! للتحقق من عدم وجود عنصر البيانات name ضمن مخازن الذاكرة LocalStorage، فإذا لم يجده، فسيستدعي الدالة ()setUserName لإنشاءه؛ أما إذا كان موجودًا -أي أنّ المستخدِم أدخله في أثناء زيارته الأولى-، فسنعيد قيمته باستخدام الدالة ()getItem ثم نضبط قيمة محتوى العنوان ليصبح النص الأصلي إضافة إلى اسم المستخدِم كما فعلنا ضمن الدالة ()setUserName. أضف معالِج الحدث onclick التالي إلى الزر لكي تُستدعى الدالة ()setUserName عند النقر عليه، وبالتالي سيتمكن المستخدِم من تغيير الاسم عند النقر على هذا الزر: myButton.onclick = function() { setUserName(); } ظهور القيمة null إذا نقرت زر إلغاء cancel بدل زر موافق ok أثناء ظهور مربع الحوار، فسيظهر لك عنوان الصفحة "Mozilla is cool, null"، ويعود السبب في ذلك إلى عدم إسناد قيمة إلى المتغير myName وبالتالي سيأخذ القيمة null، وهي قيمة خاصة في جافاسكربت تشير إلى غياب قيمة مطلوبة؛ أما إذا نقرت زر موافق ok دون إدخال اسم، فستكون النتيجة ",Mozilla is cool" وهذا أمر واضح، ولتفادي هذه المشاكل يمكنك التحقق من وجود الاسم فعلًا، لذلك سنعدِّل شيفرة الدالة ()setUserName كما يلي: function setUserName() { let myName = prompt('Please enter your name.'); if(!myName) { setUserName(); } else { localStorage.setItem('name', myName); myHeading.textContent = 'Mozilla is cool, ' + myName; } } يعني هذا إعادة استدعاء الدالة ()setUserName من جديد إذا لم يكن للمتغير قيمة؛ أما إذا كان له قيمة، فستُخزَّن ضمن localStorage وتُضاف إلى العنوان. خلاصة إذا اتبعت التوجيهات التي أشرنا إليها خلال اطلاعك على هذا المقال، فستبدو صفحة الويب التي نبنيها بالشكل التالي تقريبًا: إذا لم تجد عملك صحيحًا، فيمكنك دائمًا موازنة ما فعلته مع النسخة الجاهزة على جيت-هاب. ترجمة -وبتصرف- للمقال JavaScript basics. اقرأ أيضًا تعلم لغة جافا سكريبت من الصفر حتى الاحتراف توثيق لغة JavaScript العربي تعلم البرمجة ما هي جافاسكربت الدليل السريع إلى لغة البرمجة جافاسكريبت JavaScript سلسلة دليل تعلم جافاسكربت
  24. نتابع في هذا المقال ما بدأناه في ضبط إعدادات النظام بما يلائم حاجات المستخدم، وسنناقش المواضيع التي تختص بإدارة التجهيزات المثبتة، مثل التجهيزات الصوتية وشاشات العرض؛ كما نعرّج على مواضيع الإعدادات اللغوية والإقليمية وإعدادات الشمولية (الوصول السهل accessibility) التي تحسّتن من تجربة المستخدمين ذوي القدرات الخاصة، وذوي المشاكل البصرية والسمعية . إعدادات التجهيزات وتتضمن مجموعةً من الخيارات التي تتحكم بإعدادات بعض التجهيزات، مثل الصوت والطاقة والفأرة وغيرها. إعدادات الصوت للولوج إلى هذه اﻹعدادات، انقر على خيار "الصوت" في الشريط الجانبي لنافذة اﻹعدادات، وسيعرض لك التطبيق النافذة التالية: تضم الشاشة السابقة الأقسام التالية: شدة صوت نظام: يحدد مستوى الصوت في حاسوبك، ويمكنك ضبط نفس اﻹعداد من شريط المهام الرئيسي لسطح المكتب. تجدر اﻹشارة إلى وجود آلية لتعزيز الصوت، أي إيصاله إلى مستويات أعلى من 100% (يتعلق اﻷمر بالشريحة الصوتية المدمجة في نظامك وإمكاناتها)، وذلك بالنقر على زالقة خيار "تضخيم زائد over _ amplification". مستويات الصوت: يمكِّنك هذا الخيار من ضبط شدة الصوت لبعض التطبيقات مثل أصوات النظام التي يصدرها عند النقر على أيقونة، أو فتح وإغلاق نافذة، أو التنبيهات الصوتية أو لبعض المشغلات الصوتية (لاحظ وجود زالقة للتحكم بمستوى صوت تطبيق VLC؛ مما يدل على أن هذا المشغل الصوتي مثبّت على جهازك، ويعمل في لحظة التقاط صورة للشاشة). المخرج: عادةً ما يختار النظام تلقائيًا المنفذ الذي يخرج منه الصوت إن دعمت الشريحة الصوتية في حاسوبك أكثر من مخرج، وذلك عن طريق التقاط الإشارة ناتجة عن توصيلك مكبر الصوت أو السماعة في هذا المنفذ. يمكنك بالطبع اختيار أي منفذ لإخراج الصوت من قائمة "جهاز إخراج"، كما يتيح لك الزر "اختبار" المجاور للقائمة أن تختبر عمل هذا المخرج الصوتي. المدخل: ويتيح لك اختيار المنفذ الذي تدخل منه الإشارات الصوتية إلى حاسوبك لتسجيلها أو عرضها عندما تصل مايكروفون أو أي تجهيزة مشابهة بهذا المنفذ. صوت التنبيه: وذلك لاختيار نغمة التنبيه الصوتي أو نغمة الصوت المرافق لظهور تنبيه على الشاشة. إعدادات الطاقة تضم هذه اﻹعدادات بعض الخيارات التي تساعد على تخفيف استهلاك الطاقة قدر المستطاع سواءً أثناء العمل، أو عند التوقف عن العمل مؤقتًا؛ كما تتيح لك التحكم بما سيفعله حاسوبك عند الضغط على زر التشغيل. خيارات حفظ الطاقة تعتيم الشاشة blank screen: وهي الفترة الزمنية التي ينتظرها النظام بعد توقفك عن العمل لإخفاء شاشة سطح المكتب. يمكنك اختيار مدة تتراوح بين دقيقة و"أبدًًا" إن لم تكن تريد إخفاء سطح المكتب. واي فاي: تشغيل أو إطفاء محوّل الشبكة اللاسلكية لتوفير الطاقة إن لم تكن تستخدم الشبكة. بلوتوث: تشغيل أو إطفاء محوّل بلوتوث لتوفير الطاقة إن لم تكن تستخدمه. خيارات تعليق النظام والضغط على زر تشغيل الحاسب نعرض فيما يلي خيارات تعليق النظام والضغط على زر تشغيل الحاسب: التعليق التلقائي للنظام automatic suspend: وذلك لتعليق عمل النظام بعد فترة من الزمن إن لم تكن تستخدم الحاسوب. هذا الخيار معطّل افتراضيًا، لكن بإمكانك تفعيله ثم اختيار الفترة الزمنية التي تناسبك ما بين 15 دقيقة وساعتين. الضغط على زر التشغيل power button action: يمكنك أن تطفئ الجهاز، وهو الخيار اﻹفتراضي أو تعلق عمله أو أن يتجاهل الحاسوب أمر الضغط على زر التشغيل عند اختيار "لا تفعل شيئًا". إعدادات شاشة العرض تتيح لك هذه اﻹعدادات ضبط خواص جميع أجهزة العرض المتصلة مع حاسوبك. ضبط الشاشات الاتجاه: لضبط سياق العرض وفقًا للشاشة المستخدمة، فقد يكون العرض طوليًا أو عرضيًا. الميز أو دقة العرض: يختار النظام اﻹعدادت اﻷنسب تلقائيًا، لكن بإمكانك اختيار دقة العرض التي تراها مناسبة (وهي عدد البكسلات العرضية وعدد البكسلات الطولانية ونسبة الطول إلى العرض). ضبط اﻷبعاد الكسري fractional scaling: ويقصد به تكبير أو تصغير تفاصيل سطح المكتب باستخدام معاملات تكبير أو تصغير عشرية بدلًا من الصحيحة. لا تهتم كثيرًا للأمر إن لم تدرك تمامًا ما المقصود بذلك، لكن تفعيل هذا الخيار قد يزيد من دقة التفاصيل وملائمتها لشاشتك ككل، وقد يستهلك ذلك في المقابل قدرًا أكبر من الطاقة، كما قد يُخفض من حدة تمايز اﻷلوان. الشاشات الليلية للوصول إلى نافذة هذا الخيار، انقر على زر "اﻹضاءة الليلية" في شريط مهام النافذة. يغير هذا الخيار من شدة سطوع الشاشة ليلًا، ويجعل اﻷوان أكثر دفئًا (اقترابًا من اللون اﻷحمر)، مما يعطي راحةً أكبر للعين، ويخفف اﻹجهاد. يضبط النظام تلقائيًا الفترة الزمنية التي يحل فيها الظلام في منطقتك وفقًا للإعدادات المحلية له (عندما تحدد البلد وحزمة التوقيت)، كما يمكنك وضع جدول زمني لساعات اﻹضاءة الليلية بما يناسبك. يتيح لك النظام أيضًا خيار ضبط مقدار دفئ اﻷلوان لتحصل على اﻹضاءة التي تريحك. إعدادات أجهزة الدخل وتتيح ضبط بعض الميزات المتعلقة بالفأرة ولوحة المفاتيح والتطبيقات التي ستفتح تلقائيًا محتوى وسائط التخزين الخارجية. الفأرة ولوحة اللمس الزر اﻷساسي: انقر على الخيار "يسار" لتستخدم زر الفأرة الأيسر في تنفيذ اﻷوامر، أو "يمين" لتستخدم الزر الأيمن. وفي كلتا الحالتين يتحول الزر الآخر إلى زر قوائم (إظهار قائمة إن وجدت). سرعة الفأرة: عندما تحرك الزالقة إلى اليسار، فأنت تزيد بذلك من سرعة مؤشر الفأرة أثناء حركته على الشاشة وتبطئ حركة المؤشر إن حركت الزالقة يمينًا. اختصارات لوحة المفاتيح تعرض لك هذه النافذة جميع اختصارات لوحة المفاتيح مرتبة حسب طبيعة عملها، مثل اختصارات التنقل، أو اختصارات فتح وتشغيل تطبيقات وغيرها. تساعدك هذه الاختصارات في تنفيذ ما تريده بسرعة وتبقى مسألة اختيار الاختصارات التي تستخدمها في عملك أمرًا خاصًا بك وحدك. يقدم لك نظام التشغيل أوبونتو إمكانية تغييرالاختصار الافتراضي أو تعطيله أو إضافة اختصارات أخرى. مثال تطبيقي: يُعد تطبيق "المرقاب" شديد اﻷهمية في معرفة كل التطبيقات التي تعمل حاليًا وحجم الذاكرة الذي يستخدمه كلًا منها، باﻹضافة إلى مراقبة موارد النظام ومنظومة الملفات. سنعين اختصارًا لتشغيل هذا التطبيق: افتح التطبيق بالطريقة التقليدية: من قائمة التطبيقات، اختر "أدوات" ثم "مرقاب النظام". عندما يفتح التطبيق، ابحث عن العملية التي لها أيقونة مشابهة لأيقونة التطبيق، وستجد أن اسمها "gnome-system-monitor". احفظ هذا الاسم. أغلق التطبيق، ثم افتح اﻹعدادات، ومنها إلى خيار "اختصارات لوحة المفاتيح". انقر على الزر (+) في آخر القائمة. ليظهر لك مربع حوار يطلب من إدخال بعض المعلومات ضع في حقل "الاسم" أي اسم تريده لاختصارك، ثم ضع في حقل "اﻷمر" الاسم الذي حفظته في الخطوة الأولى كما هو تمامًا دون أية أخطاء. اضغط على زر حدد اختصارًا لتظهر لك النافذة التالية: انقر على الأزرار التي تريدها أن تشكل الاختصار معًا، مثل Ctrl + Alt + M، وسيكون الاختصار جاهزًا للاستخدام دومًا. التشغيل التلقائي لمحتوى الوسائط الخارجية انقر على خيار "الوسائط المنفصلة" وستظهر النافذة التالية: تقدم النافذة مجموعةً من الخيارات التي ينفذها النظام عند اكتشاف وجود وسيط تخزين خارجي يضم محتوى معين: صوتي، أو فيديو، أو صور، أو برمجيات. بالنقر على عنصر القائمة المجاور لكل نوع من المحتوى، ستظهر مجموعة من الخيارات في مقدمتها البرامج التي يزكّيها النظام لتشغيل هذا النوع من الوسائط، كما يعطيك حرية اختيار البرنامج الذي تريده إن كان مثبتًا على جهاز من خلال اختيار الأمر"تطبيق آخر". يمكنك أيضًا تجاهل الموضوع باختيار اﻷمر "لا تفعل شيئًا"، وإن أردت أن تقرر ما الذي ستفعله في الوقت المناسب، فاختراﻷمر "اسأل ما الذي يجب فعله"، أو بإمكانك فتح وسيط التخزين من خلال تطبيق "الملفات" باختيار اﻷمر "افتح المجلّد" بكل بساطة. إن لم يكن وسيط التخزين مدرجًا ضمن الوسائط المعروضة، فانقر على الخيار "وسائط أخرى"، وسيعرض لك مربع الحوار التالي: اختر نوع الوسيط، ثم اختر اﻹجراء المطلوب اتخاذه عند اكتشافه، وسيتذكر النظام ما اخترته. إن لم تشأ أن يسألك النظام عما يجب فعله عند اكتشاف وسيط تخزين جديد وأن لا يشغل تلقائيًا أية برامج، ففعّل الخيار "لا تسأل أو تبدأ البرامج عند إدخال الوسائط". اﻹعدادات اللغوية واﻹقليمية تتيح هذه اﻹعدادات ضبط الكثير من النقاط الهامة التي تتعلق بلغة نظام التشغيل ولغات لوحة المفاتيح والتنسيقات العامة كالتاريخ والوقت والعملة وغيرها. المنطقة واللغة عند النقر على خيار "المنطقة واللغة" في الشريط الجانبي لتطبيق "اﻹعدادات"، ستظهر نافذة تضم الخيارات التالية: اللغة: وتدل على لغة نظام التشغيل ولغة قوائمه ونوافذه. لتغييرها، انقر على هذا الخيار، ثم اختر إحدى اللغات المثبتة بالفعل على جهازك. ستتطلب العملية إعادة تشغيل النظام عند اﻹنتهاء. النسق: وتعرض تنسيق الوقت والتاريخ والعملة وجملة القياسات المستخدمة في البلد المحدد. لتغيير النسق، انقر على هذا الخيار، واختر نسق البلد الذي تنتمي إليه، ثم انقر على الزر "تمّ". تتطلب التغييرات إعادة تشغيل الحاسوب قبل أن تُطبق. مصادر اﻹدخال: وذلك لاختيار لوحات مفاتيح مخصصة لكتابة لغات معينة. لاحظ إمكانية الكتابة باللغة العربية واﻹنكليزية، لأن تخطيطات لوحة المفاتيح لكل منها مهيئة ومفعلة. إن أردت إضافة لغة إدخال جديدة، فانقر على زر (+) أسفل آخر تخطيط، وسيظهر لك مربع حوار لاختيار لغة جديدة. اختر اللغة، ثم انقر الزر "أضف". إدارة اللغات المثبتة manage installed languages: يفتح هذا الخيار تطبيق "دعم اللغات"، ويعرض اللغات المثبتة حسب اﻷفضلية. اللغة اﻷولى في القائمة هي لغة النظام الحالية، وتكوّن مثبتةً مع كامل اعتمادياتها ومكتباتها، لكن إن لم يجد النظام مقابلًا لتسمية أو زر أو خيار بهذه اللغة، فسيستخدم مقابلها من اللغة التالية في القائمة. عند اختيار ترتيب اللغات بالشكل الذي تريد، انقر على زر "طبّق على مستوى النظام"، وسيُنفذ النظام اﻷمر عند تسجيل الخروج، ثم تسجيله مجددًا. إن أردت أن تستخدم لغةً أخرى لتكون لغةً أساسيةً للنظام، أو رأيت أن دعم اللغة العربية غير مكتمل؛ فانقر على زر "ثبّت/احذف اللغات" الذي يتيح لك قائمةً بمجموعة اللغات التي يدعمها النظام كليًا أو جزئيًا. انقر على اللغة التي تريد تثبيتها، ثم انقر على الزر "طبّق". يبحث النظام عن اﻹعتماديات المثبتة فعلًا ويقرر ما إذا كانت هناك أية نواقص كي يستدعي عندها "محدّث البرمجيات" لتزيل وتثبيت ما يلزم تلقائيًا ودون تدخل منك. اﻹتاحة وشمولية الوصول يقصد بمصطلح اﻹتاحة أو الشمولية accessibility، تمكين أي مستخدم من العمل على نظام التشغيل بأفضل تجربة ممكنة إيًا كانت لغته أو إمكاناته الجسدية (سليم، أو لديه مشاكل بصرية، أو لديه مشاكل سمعية… إلخ). يتيح لك نظام أوبونتو بنسخته 20.04 طيفًا واسعًا من الخيارات التي تسهّل على ذوي القدرات الخاصة التعامل مع النظام، وسنلقي نظرةً سريعةً عليها في هذا القسم. انقر على خيار "اﻹتاحة" في الشريط الجانبي لنيافة تطبيق "اﻹعدادات" وستظهر لك الخيارات التالية: التباين العالي: يغيرّر النظام عند تفعيل هذا الخيار سمة سطح المكتب ليعرض اﻷيقونات والقوائم وأشرطة المهام والنوافذ باللونين اﻷسود واﻷبيض بتدرجات لونية ضئيلة لمساعد ضعيفي البصر على التمييز. نص كبير: تظهر جميع الكتابات بحجم كبير لتسهيل القراءة. حجم المؤشر: لاختيار حجم مؤشر الفأرة الظاهر على الشاشة بما يلائم المستخدم. التقريب: بالنقر على هذا الخيار ستظهر لك النافذة التالية: التكبير: ويحدد مقدار تكبير ما يُعرض على الشاشة ابتداءً من 1 وحتى 5 أضعاف. يتبع مؤشر الفأرة: تلحق المكبرة بمؤشر الفأرة لتكبير المنطقة التي يصل إليها. جزء من الشاشة: تبقى المكبرة في النصف العلوي أو السفلي أو اﻷيمن أو اﻷيسر من الشاشة أو تملؤها كلها. في النافذة الفرعية التالية "محاور الهدف"، يمكنك تفعيل ميزة ظهور محورين متقاطعين يتمركزان حول مؤشر الفأرة لتحديد موضعها مع إمكانية ضبط طول هذين المحورين وسماكتهما ولونهما. قارئ الشاشة: ينطق قارئ الشاشة المدمج مع النظام أي محتوى نصي يقع عليه مؤشر الفأرة، كما ينطق نوع العنصر الذي يحوي هذا المحتوى (زر، أو نافذة، أو رسالة) وموقعه. لا يُنصح به في النسخة العربية لأنه سيشوش المكفوف أكثر مما يساعده. هنالك خيارات أخرى متعددة يتيحها النظام للمستخدمين ذوي القدرات الخاصة وحتى العاديين أحيانًا، نترك للقارئ مهمة استكشافها والتعرف عليها كونها تشرح نفسها بنفسها غالبًا. خلاصة تعرفنا في المقالات الثلاثة التي تحمل عنوان "إعدادات أوبونتو" على طريقة ضبط الإعدادات العامة لنظام تشغيل أوبونتو من وجهة نظر المستخدم أو المستثمر العادي للنظام بما يحقق له الراحة و الفعالية في العمل وعرّجنا على مواضيع هامة تتعلق بإنشاء وضبط شبكات الاتصال بأنواعها وضبط إعدادات التطبيقات، من ثم إدارة التجهيزات. وهكذا سنكون قد خطونا خطوة بسيطة أخرى في عالم نظام التشغيل لينكس وتوزيعاته وأصبحنا جاهزين للانتقال إلى موضوع مهم جديد وهو تثبيت التطبيقات التي يحتاجها المستخدم عبر واجهات رسومية واضحة وسهلة الاستخدام. اقرأ أيضًا المقال السابق: إعدادات أوبونتو 20.04: التطبيقات وإدارة المستخدمين تعرّف على سطح مكتب أوبونتو 20.04 التعامل مع المجلدات والملفات في أوبونتو 20.04 تغيير اللغة في نظام لينكس أوبنتو إلى العربية
  25. استخدمنا في جزئية سابقة من هذه السلسلة صورتين مختلفتين هما "ubuntu" و "node" ونفّذنا بعض الأعمال يدويًا لتشغيل تطبيق "Hello, World". ستساعدنا الأدوات والأوامر التي تعلمناها سابقًا في هذه الجزئية من السلسلة، إذ نتعلم فيه بناء الصور وتهيئة بيئة عمل التطبيق. سنبدأ ببناء واجهة خلفية نمطية باستخدام Express/Node.js ثم نبني عليها مستخدمين خدمات أخرى مثل قواعد بيانات MongoDB. ملفات Dockerfile بإمكاننا إنشاء صورة جديدة تتضمن التطبيق "!Hello, World" بدلًا من تعديل الحاوية بنسخ ملفات جديدة إليها، وتساعدنا في ذلك أداة تُدعى Dockerfile، وهي ملفٌ نصي بسيط يحتوي كل التعليمات الخاصة بإنشاء صورة. سنبدأ إذًا بإنشاء مثال عن Dockerfile من تطبيق "!Hello, World". أنشئ مجلدًا جديدًا على جهازك ثم أنشئ ضمنه الملف "Dockerfile " إن لم تكن قد فعلت ذلك مسبقًا. ولنضع كذلك الملف "index.js" الذي يضم الشيفرة ('!console.log('Hello, World إلى جواره. ستبدو هيكلية المجلد على النحو التالي: ├── index.js └── Dockerfile سنخبر الصورة من خلال "Dockerfile" بثلاثة أمور: استخدم node:16 أساسًا للصورة. ضع الملف "index.js" ضمن الصورة، كي لا نُضطر إلى نسخه يديويًا إلى الحاوية. استخدم node لتنفيذ شيفرة الملف "index.js" عندما نشغّل الحاوية من الصورة. توضع هذه النقاط الثلاث ضمن ملف "Dockerfile" وأفضل مكان لإنشاء هذا الملف هو جذر المشروع، وسيبدو هذا الملف على النحو التالي: FROM node:16 WORKDIR /usr/src/app COPY ./index.js ./index.js CMD node index.js FROM: تخبر هذه التعليمة برنامج دوكر Docker أنّ أساس الصورة هو node:16. COPY: تنسخ هذه التعليمة الملف index.js من الجهاز المضيف إلى ملف بنفس الاسم ضمن الصورة. CMD: تخبر هذه التعليمة البرنامج ما يجب أن يحدث عند تنفيذ الأمر docker run. وهذه التعليمة هي تعليمة تنفيذ افتراضية يمكن استبدالها بالمعامل الذي يُعطي بعد اسم الصورة. اكتب الأمر docker run --help إن نسيت. WORKDIR: وضعت هذه التعليمة للتأكد من أننا لن نفعل شيئًا يتداخل مع محتوى الحاوية. إذ سيضمن أنّ كل التعليمات التي ستليه ستكون ضمن المجلد "usr/src/app/" الذي يُعد مجلد العمل في هذه الحالة. فإن لم يكن هذا المجلد موجودًا في الصورة، سيُنشأ تدريجيًا. لم نحدد مجلد عمل WORKDIR، فقد نجازف بتغيير ملفات هامة بطريق الخطأ. لو تحققت من الجذر / للصورة بتنفيذ الأمر docker run node:16 ls ستجد العديد من المجلدات والملفات في هذه الصورة. نستطيع الآن استخدام الأمر docker build لبناء الصورة بناءً على ملف Dockerfile، لكننا سنضيف الراية t- إلى هذا الأمر كي تساعدنا في إعادة تسمية الصورة: $ docker build -t fs-hello-world . [+] Building 3.9s (8/8) FINISHED ... ستكون نتيجة تنفيذ الأمر هي: "docker please build with tag fs-hello-world the Dockerfile in this directory" والتي تشير إلى بناء صورة بالاسم "fs-hello-world" بالاعتماد على ملف Dockerfile الموجود في المجلد. يمكنك الإشارة إلى أي ملف Dockerfile لكن في حالتنا البسيطة يكفي وضع . للإشارة إلى هذا الملف ضمن المجلد لهذا انتهى الأمر بالنقطة. يمكنك تشغيل الحاوية الآن باستخدام الأمر docker run fs-hello-world ويمكن نقل الصورة أو تنزيلها أو حذفها فهي في طبيعتها ملفات. إذ يمكنك تشكيل قائمة بالصور التي يضمها حاسوبك باستخدام الأمر docker image ls أو حذف الصورة ‍‍docker image rm. اطلع على بقية الأوامر المتاحة بتنفيذ أمر المساعدة docker image --help. صور أكثر فائدة ينبغي أن يكون نقل خادم Express إلى حاوية ببساطة نقل تطبيق "!Hello, World"، إذ يكمن الاختلاف الوحيد بين الحالتين في وجود ملفات أكثر في حالة الخادم، لكن ستغدو الأمور أبسط بوجود التعليمة COPY. لنحذف الآن الملف "index.js" وننشئ خادم Express باستخدام express-generator الذي يساعدنا على بناء هيكلية بسيطة للتطبيق. $ npx express-generator ... install dependencies: $ npm install run the app: $ DEBUG=playground:* npm start لنشغل التطبيق الآن كي نرى ما فعلنا، وانتبه أن أمر التشغيل قد يختلف في جهازك، فالمجلد في المثال السابق يُدعى "playground". $ npm install $ DEBUG=playground:* npm start playground:server Listening on port 3000 +0ms يمكنك الانتقال الآن إلى العنوان "http://localhost:3000" حيث يعمل التطبيق. إن ضم الملفات في حاويات هي عملية سهلة نوعًا ما بناءً على ما واجهناه حتى الآن: استخدم الأساس node. اضبط مجلد العمل كي لا يتداخل عملك مع بقية محتويات الصورة. انسخ كل ملفات المجلد إلى الصورة. ابدأ بتنفيذ الأمر DEBUG=playground:* npm start بعد الأمر CMD. لنضع ملف Dockerfile التالي في جذر المشروع: FROM node:16 WORKDIR /usr/src/app COPY . . CMD DEBUG=playground:* npm start سنبني الآن الصورة انطلاقًا من ملف باستخدام الأمر: docker build -t express-server . وسنشغلها باستخدام الأمر: docker run -p 3123:3000 express-server تبلّغ الراية p- دوكر بضرورة فتح منفذ الجهاز المضيف وتوجيهه إلى منفذ للحاوية ولهذا الأمر الصيغة التالية: p host-port:application-port- $ docker run -p 3123:3000 express-server > playground@0.0.0 start > node ./bin/www Tue, 29 Jun 2021 10:55:10 GMT playground:server Listening on port 3000 إن لم يفلح الأمر، تجاوز الفقرة التالية، فهناك تفسير لعدم نجاح الأمر حتى لو اتبعت الخطوات السابقة تمامًا. سيبدأ التطبيق عمله الآن، لهذا سنختبره بإرسال الطلب GET إلى العنوان "/http://localhost:3123". بالنسبة لإيقاف الخادم فهو أمر عصيبٌ حاليًا، لهذا افتح نافذة أخرى للطرفية ونفذ الأمر docker kill لإيقاف التطبيق؛ إذ يرسل هذا الأمر الإشارة SIGKILL إلى التطبيق ويجبره على الإنهاء. ستحتاج إلى اسم أو معرّف الحاوية ID مثل وسيط لتنفيذ الأمر السابق. وتجدر الإشارة أنه يكفي استخدام بداية المعرّف id عند تمريره وسيطًا، إذ سيعرف دوكر مباشرةً الحاوية المقصودة. $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 48096ca3ffec express-server "docker-entrypoint.s…" 9 seconds ago Up 6 seconds 0.0.0.0:3123->3000/tcp, :::3123->3000/tcp infallible_booth $ docker kill 48 48 لنعمل من الآن وصاعدًا على نفس المنفذ في كلا الجانبين p-. وهكذا لن تضطر إلى تذكُّر ما المنفذ الذي عليك اختياره. إصلاح المشاكل المحتملة الناتجة عن عملية النسخ واللصق لا بُد من تغيير بعض الخطوات لإنشاء ملف Dockerfile متقدم، وقد لا يعمل المثال الذي أوردناه سابقًا على الإطلاق، لأننا أهملنا خطوة هامة. عندما تنفِّذ الأمر npm install على حاسوبك، فقد يُثبّت مدير حزم Node بعض الاعتمادات التي تتعلق بنظام التشغيل أثناء تقدم التثبيت. وقد ننقل صدفةً أجزاءً غير وظيفية إلى الصورة عند استخدام التعليمة COPY، ويحدث ذلك بسهولة إن نسخنا المجلد "node_modules" إلى الصورة. من المهم جدًا إبقاء تلك النقاط في ذاكرتنا عند بناء الصورة، فمن الأفضل أن ننفذ معظم الأعمال مثل npm install أثناء عملية البناء ضمن الحاوية بدلًا من تنفيذها قبل البناء؛ إذ أن القاعدة الجوهرية هنا هي نسخ الملفات التي ستدفعها إلى غيت هب GitHub فقط، ولا ينبغي نسخ الاعتماديات ومتطلبات البناء كونها أشياء يمكن تثبيتها أثناء بناء الصورة. يمكنك استخدام الملف "dockerignore." لحل المشكلة، وهو ملفٌ شبيه بملف التجاهل "gitignore." لمنع نسخ الملفات غير المطلوبة إلى الصورة. يُوضع هذا الملف إلى جوار الملف Dockerfile، وإليك مثالًا عن محتوياته: .dockerignore .gitignore node_modules Dockerfile لكننا سنحتاج إضافةً إلى ملف "dockerignore." في حالتنا إلى تثبيت الاعتماديات خلال خطوة البناء، لهذا سيتغير ملف Dockerfile إلى الشكل: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm install CMD DEBUG=playground:* npm start قد يكون تنفيذ الأمر npm install خطرًا، لهذا يزوّدنا npm بأداة أفضل لتثبيت الاعتماديات وهو الأمر ci. تُلخّص الاختلافات بين ci و install على النحو التالي : قد يُحدّث install الملف "package-lock.json". قد يُثبِّت install نسخةً مختلفةً من الاعتمادية إن ظهرت المحارف "^" أو "~" في نسخة الاعتمادية. سيحذف ci المجلد "node_modules" قبل تثبيت أي شيء. سيتبع ci الملف "package-lock.json" ولا يبدّل أي ملف. باختصار: يقدم ci نسخًا يمكن الاعتماد عليها، بينما يُستخدم install عند تثبيت اعتماديات جديدة. طالما أننا لن نثبِّت أي شيء جديد في خطوة البناء، ولا نريد تغييرات فجائية في النسخ، سنستخدم الأمر ci: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci CMD DEBUG=playground:* npm start كما يمكننا تحسين الحالة أكثر باستخدام الأمر npm ci --only=production كي لا نهدر الوقت في تثبيت الاعتماديات. سيحذف ci المجلد "node_modules" كما أشرنا قبل قليل، وبالتالي لن نضطر إلى إنشاء الملف "dockerignore.". مع ذلك، يُعد هذا الملف أداةً رائعةً عندما تريد تحسين عملية البناء، وسنتحدث باختصار عن هذا الموضوع لاحقًا. ينبغي أن يعمل الملف من جديد، لهذا حاول تنفيذ الأمر التالي: docker build -t express-server . && docker run -p 3000:3000 express-server لاحظ كيف وصلنا هنا أمري باش bash باستخدام &&، وسنحصل تقريبًا على نفس النتيجة إذا نفذنا كلا الأمرين كلًّا على حدة؛ لكن عندما تربط أمرين باستخدام &&، فلن يُنفَّذ الأمر الآخر إذا فشل تنفيذ أحدهما. ضبطنا سابقًا متغير البيئة :DEBUG=playground من خلال الأمر CMD لتشغيل npm، كما يمكننا أيضًا استخدام التعليمة ENV في ملف Dockerfiles لضبط متغيرات البيئة، فلنفعل ذلك: FROM node:16 WORKDIR /usr/src/app COPY . . RUN npm ci ENV DEBUG=playground:* CMD npm start أفضل الممارسات المتعلقة باستخدام Dockerfiles عليك اتباع القاعدتين الجوهريّتين التاليتين عند إنشاء الصور: حاول أن تبني صورةً آمنة قدر المستطاع. حاول أن تُنشئ صورةً صغيرة قدر الإمكان. تُعد الصور الأصغر أكثر أمانًا لأن مجال الهجوم عليها محدود، كما يمكن نقلها بسرعة أكبر ضمن أنابيب النشر. وأخيرًا لا بُد من إصلاح آخر نقطة أهملناها وهي تشغيل التطبيق مثل جذر بدلًا من مستخدم بصلاحيات منخفضة: FROM node:16 WORKDIR /usr/src/app COPY --chown=node:node . . RUN npm ci ENV DEBUG=playground:* USER node CMD npm start التمرين 12.5: إنشاء حاوية لتطبيق Node يضم المستودع الذي نسخته في التمرين الأول تطبيق لائحة مهام todo-app. اطلع على الواجهة الخلفية "todo-app/todo-backend" للتطبيق واقرأ الملف "اقرأني README". لن نقترب حاليًا من الواجهة الأمامية "todo-frontend" الخطوة الأولى: وضع الواجهة الخلفية "todo-backend" ضمن حاوية بإنشاء الملف "todo-app/todo-backend/Dockerfile" ثم بناء الصورة. الخطوة الثانية: تشغيل الصورة على المنفذ الصحيح، والتأكد أن عدّاد الزيارات سيزداد عند استخدامه عبر المتصفح على العنوان "/http://localhost:3000" (أو على أي منفذ آخر قد تهيّئه). تلميح: شغّل التطبيق خارج الحاوية أولًا للتحقق منه قبل وضعه في الحاوية. استخدام الأداة docker-compose أنشأنا في جزئية سابقة من هذه السلسلة خادمًا وعلمنا أنه يعمل على المنفذ 3000 وشغّلناه باستخدام الأمر: docker build -t express-server . && docker run -p 3000:3000 express-server ويبدو أننا سنحتاج إلى سكربت لتذكر هذه التعليمات، لكن لحسن الحظ يقدّم دوكر لنا حلًا أفضل. تُعد الأداة Docker-compose من الأدوات الرائعة الأخرى التي تساعدك على إدارة الحاوية، لهذا سنبدأ استخدام هذه الأداة خلال رحلتنا في دراسة الحاويات، إذ ستساعد على توفير بعض الوقت عند تهيئة الحاوية. ثبّت الأداة Docker-compose ثم تأكد من عملها على النحو التالي: $ docker-compose -v docker-compose version 1.29.2, build 5becea4c سنحوّل الآن الأوامر السابقة إلى ملف yaml يمكن تخزينه في مستودع غيت Git. أنشئ الملف "docker-compose.yml" وضعه في جذر المشروع إلى جوار ملف Dockerfile، ثم ضع المحتوى التالي ضمنه: version: '3.8' # نسخة جديدة لا بد أن تعمل services: app: # اسم الخدمة وقد يكون أي شيء image: express-server # صرّح عن الصورة التي تريد استخدامها build: . # حدد مكان بناء الصورة إن لم تكن موجودة ports: # حدد المنفذ الذي يرتبط به التطبيق - 3000:3000 وضعنا شرحًا لكل سطر إلى جواره، لكن إذا أردت معرفة المواصفات الكاملة فعُد إلى التوثيق. سنتمكن الآن من استخدام الأمر docker-compose up في بناء وتشغيل التطبيق، وإن أردت إعادة بناء الصور، استخدم الأمر docker-compose up --build. بإمكانك أيضًا تشغيل التطبيق في الخلفية باستخدام الأمر docker-compose up -d (الراية d- لفصل التطبيق) وإيقافه بتنفيذ الأمر docker-compose down. يُصرِّح إنشاء الملفات بهذه الطريقة عمّا تريده بدلًا من ملفات السكربت التي عليك تنفيذها وفق ترتيبٍ محدد أو عددٍ محددٍ من المرات، وهذه ممارسةٌ جيدةٌ جدًا. التمرين 12.6: الأداة docker-compose أنشئ الملف "todo-app/todo-backend/docker-compose.yml" الذي يعمل مع تطبيق node من التمرين السابق. وعليك الانتباه إلى عدّاد الزيارات فهو الميزة الوحيدة التي ينبغي أن تعمل. استخدام الحاويات في مرحلة التطوير يمكن استخدام الحاويات أثناء تطوير التطبيقات بطرق متعددة لتسهيل عملك، ومن إحدى فوائدها تجاوز تثبيت وتهيئة الأدوات مرتين. قد لا يكون خيارك الأفضل أن تنقل كامل بيئة التطوير إلى الحاوية، لكن إذا أردت ذلك فهذا ممكن. سنعود إلى هذه الفكرة في آخر القسم، لكن حتى ذلك الوقت عليك تشغيل تطبيق node بنفسه خارج الحاويات. يستخدم التطبيق الذي تعرفنا عليه في التمارين السابقة MongoDB، لهذا دعونا نستخدم Docker Hub لإيجاد صورة MongoDB، إذ أنه المكان الافتراضي الذي نسحب الصور منه، كما يمكنك استخدام مسجلات أخرى أيضًا، لكن طالما أننا نتعمق في دوكر فهو خيارٌ جيد. يمكنك أن تجد من خلال بحث سريع الصورة المطلوبة على العنوان https://hub.docker.com/_/mongo. أنشئ ملف yaml يُدعى "todo-app/todo-backend/docker-compose.dev.yml" يحتوي ما يلي: version: '3.8' services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database يُوضَّح معنى أول متغيري بيئة معرفين في الشيفرة السابقة في صفحة Docker Hub: "تُنشِئ المتغيرات المُستخدمة على التوازي مستخدمًا جديدًا وتضبط كلمة المرور له. يُنشأ هذا المستخدم في قاعدة بيانات إدارة الاستيثاق ويعطى دور الجذر root، وهو دور المستخدم الأعلى superuser." يخبر متغير البيئة الأخير MONGO_INITDB_DATABASE قاعدة البيانات MongoDB أن تنشئ قاعدة بيانات بهذا الاسم. بإمكانك استخدام الراية f- لتخصيص ملف لتشغيل أمر Docker Compose مثل: docker-compose -f docker-compose.dev.yml up شغًل الآن MongoDB من خلال الأمر: docker-compose -f docker-compose.dev.yml up -d أما الراية d- فلتشغيل العملية في الخلفية. يمكنك متابعة سجلات الخرج بتنفيذ الأمر: docker-compose -f docker-compose.dev.yml logs -f وتُستخدم الراية f- للتأكد من متابعة السجلات. لا نحتاج حاليًا لتشغيل تطبيق Node ضمن الحاوية، فهذا أمرٌ ينطوي على قدر من التحدي، لكننا سنكتشف هذا الخيار في آخر قسم. نفِّذ الأمر القديم npm install على جهازك لإعداد تطبيق Node، ثم شغل التطبيق باستخدام متغيرات البيئة اللازمة. يمكنك تعديل الشيفرة لجعل متغيرات البيئة متغيرات افتراضية أو استخدم الملف env.. لا ضرر من وضع هذه المفاتيح على غيت هب لأنها تُستخدم ضمن بيئة التطوير المحلية، لذلك سأضعها هناك عن طريق الأمر npm run dev لمساعدتك في النسخ واللصق. $ MONGO_URL=mongodb://localhost:3456/the_database npm run dev لن يكون ذلك كافيًا، بل نحتاج إلى إنشاء مستخدم نستوثِق منه ضمن الحاوية، إذ سيقود الولوج إلى العنوان " http://localhost:3000/todos" إلى خطأ في الاستيثاق. [nodemon] 2.0.12 [nodemon] to restart at any time, enter `rs` [nodemon] watching path(s): *.* [nodemon] watching extensions: js,mjs,json [nodemon] starting `node ./bin/www` (node:37616) UnhandledPromiseRejectionWarning: MongoError: command find requires authentication at MessageStream.messageHandler (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/mongodb/lib/cmap/connection.js:272:20) at MessageStream.emit (events.js:314:20) ربط وتركيب وتهيئة قاعدة البيانات ستجد في صفحة MongoDB Docker Hub تحت عنوان " تهيئة نسخة جديدة Initializing a fresh instance" معلومات عن تنفيذ شيفرة لتهيئة قاعدة البيانات ومستخدم لها. في المشروع التجريبي ملف "todo-app/todo-backend/mongo/mongo-init.js" يضم المحتوى التالي: db.createUser({ user: 'the_username', pwd: 'the_password', roles: [ { role: 'dbOwner', db: 'the_database', }, ], }); db.createCollection('todos'); db.todos.insert({ text: 'Write code', done: true }); db.todos.insert({ text: 'Learn about containers', done: false }); يهيئ الملف قاعدة البيانات مع مستخدم وبعض المهام المخزّنة في القاعدة، وعلينا في الخطوة التالية نقلها إلى الحاوية وتشغيلها. من الممكن إنشاء صورة جديدة من mongo ثم نسخ الملف إلى الداخل، أو استخدام الأمر لتركيب الملف ضمن الحاوية، لكننا سنختار الطريقة الأخيرة. إن التركيب بالربط Bind mount هي عملية ربط ملف على حاسوبك المحلي بملف في الحاوية، ويمكننا تنفيذ ذلك باستخدام الراية v- مع الأمر بالصيغة التالية: v FILE-IN-HOST:FILE-IN-CONTAINER-. يُعرّف التركيب بالربط تحت المفتاح volumes في الملف "docker-compose"، أما صياغة التصريح فتكون على النحو التالي "مضيف ثم حاوية": mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js إن النتيجة هي أنّ الملف "mongo-init.js" في مجلد mongo على حاسوبك سيكون نفسه الملف "mongo-init.js" في المجلد "docker-entrypoint-initdb.d/" من الحاوية، وسيؤدي تعديل أحدهما إلى تعديل الآخر. لا حاجة لتغيير أي شيء أثناء التشغيل، وهذا هو مفتاح تطوير البرمجيات ضمن الحاويات. نفّذ الأمر التالي للتأكد من وجود كل شيء في مكانه: docker-compose -f docker-compose.dev.yml down --volumes ثم ابدأ لائحة جديدة بتنفيذ الأمر التالي لتهيئة قاعدة البيانات: docker-compose -f docker-compose.dev.yml up إذا واجهك خطأ على النحو التالي: mongo_database | failed to load: /docker-entrypoint-initdb.d/mongo-init.js mongo_database | exiting with code -3 فقد يكون لديك مشكلةً في إذن القراءة، وهذا أمرٌ قد يحدث عند التعامل مع الأقراص volumes. بإمكانك في حالتنا استخدام الأمر chmod a+r mongo-init.js الذي يمنح أيًا كان إمكانية قراءة الملف، لكن كن حذرًا عند استخدام التعليمة chmod لأن السماح بإذونات أكبر قد يقود إلى مشاكل أمنية، لهذا استخدم تلك التعليمة على الملف "mongo-init.js" الموجود على جهازك فقط. سيعمل تطبيق express الآن عبر متغيرات البيئة الصحيحة: $ MONGO_URL=mongodb://the_username:the_password@localhost:3456/the_database npm run dev لنتأكد أن الطلب إلى العنوان http://localhost:3000/todos سيعيد كل المهام الموجودة في قاعدة البيانات، فمن المفترض أن يعيد المهمتان اللتان هيأناهما، ولا بد من استخدام Postman لاختبار وظائف التطبيق الأساسية مثل إضافة وحذف مهام من قاعدة البيانات. البيانات المقيمة في الأقراص لا تُخزّن الحاويات بياناتنا افتراضيًا، فعندما تغلق حاوية قد تستطيع أو لا تستطيع استعادة البيانات. وبصورةٍ عامة هناك طريقتان مختلفتان لتخزين البيانات: التصريح عن مكان ضمن منظومة الملفات (عملية الربط بالتركيب bind mount). ترك الأمر لبرنامج دوكر كي يقرر تخزين البيانات (استخدام الأقراص volume) يُفضّل الخيار الأول عادةً في معظم الحالات التي تحتاج فيها حقًا إلى تفادي حذف البيانات، وسنرى الأسلوبين بالتطبيق العملي: services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - ./mongo_data:/data/db ستُنشئ الإعدادات السابقة مجلدًا يُدعى mongo_data ضمن منظومة الملفات في حاسوبك ثم تربطه بالحاوية بالاسم data/db/. أي أنّ البيانات الموجودة في المجلد data/db/ ستُخزّن خارج الحاوية لكن بإمكانها الوصول إليها. وتذكر إضافة المجلد إلى ملف التجاهل "gitignore.". يمكن تحقيق الأمر ذاته باستخدام أقراص التخزين المسماة: services: mongo: image: mongo ports: - 3456:27017 environment: MONGO_INITDB_ROOT_USERNAME: root MONGO_INITDB_ROOT_PASSWORD: example MONGO_INITDB_DATABASE: the_database volumes: - ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js - mongo_data:/data/db volumes: mongo_data: يُنشأ القرص الآن ويدار من قبل دوكر، وبإمكانك قبل تشغيل التطبيق استعراض الأقراص الموجودة بتنفيذ الأمر docker volume ls، أو فحصّها docker volume inspect، أو حذفها docker volume rm. ليس اختيار مكان تخزين البيانات محليًا في هذا الخيار أمرًا قليل الأهمية موازنةً بالخيار السابق. التمرين 12.7: كتابة القليل من الشيفرة للتعامل مع MongoDB نفترض في هذا التمرين أنك أنجزت جميع الإعدادات التي تحدثنا عنها سابقًا بعد التمرين 12.5. وسيبقى تشغيل التطبيق خارج الحاوية وستوضع قاعدة البيانات MongoDB فقط ضمن الحاوية. لا توجد طريق ملائمة حتى الآن للحصول على مهمة واحدة (GET* /todos/:id*) وتحديث مهمة واحدة (PUT* /todos/:id*). جد حلًا لهاتين المشكلتين. تنقيح المشاكل في الحاويات لا بُد من تعلم استعمال بعض الأدوات لتنقيح التطبيقات ضمن الحاويات، لأننا لا نستطيع استخدام الأمر console.log دائمًا. عندما تظهر الثغرات في شيفرتك، فلا بد وأن يعمل شيء ما في شيفرتك لتنطلق منه. وعمومًا هناك وضعان لتبدأ منهما: الأول هو تطبيق يعمل والثاني هو تطبيق لا يعمل، لهذا سنطلع على بعض الأدوات التي تساعد في تنقيح التطبيق في الحالة الثانية. يمكن أن تنتقل أثناء تطوير البرنامج في عملك خطوة خطوة لتتاكد طوال الوقت أن كل شيء يعمل كما تتوقع. لكن لا ينطبق هذا الأمر عند ضبط الإعدادات، فقد تخفق الإعدادات التي تكتبها حتى لحظة الإنتهاء منها؛ لهذا إذا كتبت ملف "docker-compose.yml" طويل أو ملف Dockerfile ولم يعمل، عليك التروي برهة والتفكير بالطرق المختلفة التي تتأكد منها أن شيء ما يعمل بالشكل المطلوب. لا تزال طريقة التشكيك بكل شيء واردة هنا، وكما قلنا في القسم الثالث: عليك أن تكون منظّما، وطالما أنّ المشكلة قد تكون في أي مكان، عليك التشكيك بكل شيء، وإزالة كل مصادر الخطأ واحدًا تلو الآخر. فالتوقف والتفكير بالمشكلة بدلًا من كتابة مزيدٍ من الإعدادات هي الطريقة الأنسب في الحصول على حل بسيط، كما أن البحث السريع باستخدام محركات البحث قد يساعدك في التقدم. الأمر exec يمكن استخدام الأمر exec للقفز مباشرةً إلى الحاوية وهي تعمل. لهذا سنهيّئ خادم ويب في الخلفية ونجري بعض الإعدادات كي نشغّله ليعرض الرسالة "!Hello, exec" ضمن المتصفح. سنستخدم الخادم Nginx القادر على تخديم الملفات الساكنة، ويدعم الملف "index.html" الذي يمثل الصفحة الافتراضية ويسمح لنا بتعديلها أو استبدالها: $ docker container run -d nginx الأسئلة المطروحة الآن هي: أين سنتوجه عبر المتصفح؟ هل يعمل الخادم بالفعل؟ لكن نعرف كيف نجيب على هذه الأسئلة وذلك باستعراض الحاويات التي تعمل: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 3f831a57b7cc nginx "/docker-entrypoint.…" About a minute ago Up About a minute 80/tcp keen_darwin لقد حصلنا على جواب السؤال الأول فعلًا، فيبدو أن الخادم ينصت إلى المنفذ 80. لهذا سنطفئ الخادم ثم نعيد تشغيله مستخدمين الراية p- للسماح للمتصفح بالوصول إليه: $ docker container stop keen_darwin $ docker container rm keen_darwin $ docker container run -d -p 8080:80 nginx لنلقي نظرةً على التطبيق بالانتقال إلى العنوان http://localhost:8080، وستجد أنّ التطبيق يعرض رسالة خاطئة، لهذا سنقفز إلى الحاوية مباشرةً لإصلاحها. أبقِ متصفحك مفتوحًا، فلا نريد إغلاق الحاوية عند إصلاحها، بل سنستخدم أوامر الطرفية ضمن الحاوية، ولا تنسى استخدام الراية it- لضمان قدرتك على التفاعل مع الحاوية: $ docker container ls CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 7edcb36aff08 nginx "/docker-entrypoint.…" About a minute ago Up About a minute 0.0.0.0:8080->80/tcp, :::8080->80/tcp wonderful_ramanujan $ docker exec -it wonderful_ramanujan bash root@7edcb36aff08:/# بعد أن قفزنا داخل الحاوية، لا بد من إيجاد الملف الخاطئ واستبداله، إذ يتضح لنا من خلال بحث سريع باستخدام محرك بحث أن الملف هو "usr/share/nginx/html/index.html/". لننتقل إلى المجلد ونحذف الملف: root@7edcb36aff08:/# cd /usr/share/nginx/html/ root@7edcb36aff08:/# rm index.html لو انتقلنا الآن إلى العنوان http://localhost:8080 فسنحصل على الصفحة 404 لأننا حذفنا الملف الصحيح، لهذا سنستبدله بملف آخر يضم المحتوى الصحيح: root@7edcb36aff08:/# echo "Hello, exec!" > index.html أعد تحميل الصفحة وسترى كيف يعرض المتصفح الرسالة الصحيحة. وهكذا نرى كيف نستفيد من الأمر exec في التفاعل مع الحاوية. ستُفقد كل التغييرات التي أجريتها عند حذف الحاوية، ولتحفظ هذه التغييرات لا بد من دفعها باستخدام الأمر commit. التمرين 12.8: واجهة سطر أوامر Mongo استخدم script لتسجيل ما تفعله، ثم احفظ الملف بالاسم "script-answers/exercise12_8.txt". حاول الولوج إلى قاعدة البيانات في التمرين السابق أثناء تشغيل MongoDB باستخدام سطر أوامر CLI. يمكنك تنفيذ الأمر باستخدام exec، ثم أضف بعد ذلك مهمة جديدة عبر سطر الأوامر CLI. شغّل سطر الأوامر وأنت ضمن الحاوية باستخدام الأمر mongo. يتطلب سطر أوامر mongo رايتي اسم مستخدم وكلمة مرور للاستيثاق على نحوٍ صحيح،. وستعمل الرايات u root -p example- جيدًا، وتكون القيم مأخوذةً من الملف docker-compose.dev.yml. الخطوة الأولى: شغّل MongoDB. الخطوة الثانية: استخدم الأمر exec للدخول إلى الحاوية. الخطوة الثالثة: افتح واجهة سطر أوامر mongo. عندما تصل إلى واجهة سطر أوامر mongo يمكنك أن تطلب منها عرض قواعد البيانات: > show dbs admin 0.000GB config 0.000GB local 0.000GB the_database 0.000GB للولوج إلى قاعدة البيانات الصحيحة: > use the_database ولإيجاد المجموعات: > show collections todos يمكنك الآن الوصول إلى البيانات الموجودة ضمن تلك المجموعات: > db.todos.find({}) { "_id" : ObjectId("611e54b688ddbb7e84d3c46b"), "text" : "Write code", "done" : true } { "_id" : ObjectId("611e54b688ddbb7e84d3c46c"), "text" : "Learn about containers", "done" : false } أضف مهمةً جديدة نصها: "Increase the number of tools in my toolbelt" مع ضبط حالتها على false. راجع التوثيق لتتعلم إضافة المدخلات إلى قاعدة البيانات، وتأكد من رؤية المهمة الجديدة في تطبيق Express وعند الاستعلام عنه عبر سطر أوامر Mongo CLI. قاعدة البيانات Redis Redis هي قاعدة بيانات من الشكل (مفتاح-قيمة) مما يجعلها قاعدة لحفظ البيانات بهيكلية أدنى من MongoDB مثلًا، فلن تجد فيها مجموعات أو جداول بل كميات من البيانات يمكن الحصول عليها وفقًا لقيم المفاتيح المرتبط بالبيانات (القيم). تعمل قاعدة البيانات Redis ضمن الذاكرة المؤقتة أي أنها لا تحتفظ بالبيانات دائمًا، ومن أفضل طرق الاستفادة منها هي استخدامها مثل مخزن مؤقت لتطبيق cache، إذ تُستخدم المخازن المؤقتة غالبًا لتخزين البيانات التي قد تكون بطيئة عند إحضارها وحفظها حتى تفقد صلاحيتها فيتوجب عليك إحضارها مجددًا بعد ذلك وتخزينها وهكذا. لا علاقة لقاعدة البيانات Redis بالحاويات لكن وطالما أننا قادرين على إضافة أي خدمة مصدرها طرف آخر إلى التطبيق، فلماذا لا نتعلم شيئًا جديدًا؟ التمارين 12.9 - 12.11 التمرين 12.9: إعداد Redis للمشروع هُيّئ خادم Express مسبقًا للتعامل مع Redis ويفتقد فقط إلى متغير البيئة REDIS_URL، إذ يستخدم التطبيق متغير البيئة للاتصال بقاعدة البيانات. اطلع على عمل Redis مع Docker Hub ثم أضفها إلى الملف "todo-app/todo-backend/docker-compose.dev.yml" من خلال تعريف خدمة جديدة بعد mogo. services: mongo: ... redis: ??? بما أن صفحة Docker Hub لا تضم جميع المعلومات الكافية، استخدم غوغل مثلًا في البحث. ستجد المنفذ الافتراضي للقاعدة بإجراء البحث الموضح في الصورة التالية: لا نعلم بعد إن كان الإعداد سيعمل ما لم نجرّب. لن يستخدم التطبيق Redis من تلقاء نفسه طبعًا، وهذا ما سنراه في التمرين التالي. حالما تهيئ Redis وتشغله، أعد تشغيل الواجهة الخلفية ومرر لها متغير البيئة ‍‍REDIS_URL بالصيغة redis://host:port. $ REDIS_URL=insert-redis-url-here MONGO_URL=mongodb://localhost:3456/the_database npm run dev يمكنك الآن اختبار الإعداد بإضافة السطر التالي: const redis = require('../redis') إلى مثال خادم Express في الملف "routes/index.js". إن لم يحدث شيء فقد طُبِّق الإعداد بصورةٍ صحيحة وإلا سينهار الخادم: events.js:291 throw er; // Unhandled 'error' event ^ Error: Redis connection to localhost:637 failed - connect ECONNREFUSED 127.0.0.1:6379 at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) Emitted 'error' event on RedisClient instance at: at RedisClient.on_error (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:342:14) at Socket.<anonymous> (/Users/mluukkai/opetus/docker-fs/container-app/express-app/node_modules/redis/index.js:223:14) at Socket.emit (events.js:314:20) at emitErrorNT (internal/streams/destroy.js:100:8) at emitErrorCloseNT (internal/streams/destroy.js:68:3) at processTicksAndRejections (internal/process/task_queues.js:80:21) { errno: -61, code: 'ECONNREFUSED', syscall: 'connect', address: '127.0.0.1', port: 6379 } [nodemon] app crashed - waiting for file changes before starting... التمرين 12.10 ثُبتت قاعدة البيانات https://www.npmjs.com/package/redis في المشروع مسبقًا وأضيفت إليه دالتين تعيدان وعودًا وهما getAsync و setAsync: تقبل الدالة setAsync قيمةً ومفتاحًا، ويُستخدم المفتاح لتخزين القيمة. تقبل الدالة getAsync مفتاحًا وتعيد قيمته المقابلة في الوعد promise. أنجز عدادًا للمهام يخزّن عدد المهام التي تُنشئها في قاعدة البيانات Redis: الخطوة الأولى: زد العداد بمقدار واحد في أي لحظة يُرسل فيها طلب لإضافة مهمة. الخطوة الثانية: أنشئ وصلة GET/statics ساكنة يمكنك طلب معلومات وصفية منها، وينبغي أن تُعاد بصيغة JSON على النحو التالي: { "added_todos": 0 } التمرين 12.11 استخدم script لتسجيل ما تفعله، ثم تحفظ الملف بالاسم "script-answers/exercise12_11.txt". إن لم يسلك التطبيق السلوك المطلوب، فقد يساعدك الولوج المباشر إلى قاعدة البيانات في الدلالة على المشاكل. لنلقي نظرةً إذًا على طريقة استخدام واجهة سطر أوامر Redis في الوصول إلى قاعدة البيانات: انتقل إلى حاوية redis باستخدام الأمر docker exec، ثم افتح الواجهة redis-cli. ابحث عن المفتاح الذي استخدمته باستخدام الأمر * KEYS. تحقق من قيمة المفتاح من خلال الأمر GET. راجع أوامر redis-cli للبحث عن الأمر المناسب لضبط قيمة العداد على 9001. تأكد من أن القيمة الجديدة ستعمل عند تحديث الصفحة http://localhost:3000/statistics. احذف المفتاح باستخدام واجهة سطر الأوامر وتأكد أن العداد سيعمل عند إضافة مهام جديدة. الذاكرة المقيمة وقاعدة البيانات Redis أشرنا سابقًا أنّ قاعدة البيانات Redis لا تحتفظ بالبيانات افتراضيًا، لكنه أمر يمكن حله بسهولة، إذ كل ما علينا فعله هو تشغيل Redis بأمر مختلف كما توضح صفحة Docker hub: services: redis: # أي شيء آخر command: ['redis-server', '--appendonly', 'yes'] # CMD تعديل volumes: # التصريح عن القرص - ./redis_data:/data ستقيم البيانات الآن في المجلد على الجهاز المُضيف، وتذكّر إضافة المجلد إلى الملف gitignore.. إمكانات أخرى لقاعدة البيانات Redis تقدم Redis عدّة ميزات إضافةً إلى عمليات إضافة وضبط وحذف المفاتيح، فهي تحدد مثلًا فترة صلاحية المفتاح وهذه ميزة شديدة الأهمية عند استعمالها مثل مخزن مؤقت. كما يمكن استخدام Redis في إنجاز نماذج نشر-اشتراك publish-subscribe -أواختصارًا PubSub-، وهي آلية تواصل غير متزامنة للتطبيقات الموزّعة. تعمل Redis في هذه الحالة مثل وسيط بين تطبيقين أو أكثر، ينشر بعضها رسائل بإرسالها إلى Redis وعند وصول هذه الرسائل تُبلغ Redis جميع الأطراف بأنها اشتركت بهذه الرسائل. التمرين 12.12 البيانات المقيمة و Redis تأكد أن البيانات لا تُخزّن افتراضيًا في قاعدة البيانات Redis بأن تكون قيمة العداد 0 بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down و docker-compose -f docker-compose.dev.yml up. أنشئ بعد ذلك قرصًا للبيانات عن طريق تعديل الملف "todo-app/todo-backend/docker-compose.dev.yml"، وتأكد من بقاء البيانات بعد تنفيذ الأمرين docker-compose -f docker-compose.dev.yml down وdocker-compose -f docker-compose.dev.yml up. ترجمة -وبتصرف- للفصل Building and Configuring Environments من سلسلة Deep Dive Into Modern Web Development اقرأ أيضًا المقال السابق: مدخل إلى الحاويات أبرز المفاهيم التي يجب عليك الإلمام بها عن الحاويات حاوية دوكر Docker ومخزن APCu في PHP
×
×
  • أضف...