-
المساهمات
189 -
تاريخ الانضمام
-
تاريخ آخر زيارة
نوع المحتوى
ريادة الأعمال
البرمجة
التصميم
DevOps
التسويق والمبيعات
العمل الحر
البرامج والتطبيقات
آخر التحديثات
قصص نجاح
أسئلة وأجوبة
كتب
دورات
كل منشورات العضو Ola Abbas
-
أضفنا في مقال تقسيم تطبيق Svelte إلى مكونات من هذه السلسلة مزيدًا من الميزات إلى قائمة المهام وبدأنا بتنظيم تطبيقنا ضمن مكونات، وسنضيف في هذا المقال الميزات النهائية لتطبيقنا مع استكمال تقسيمه إلى مكونات، وسنتعلم كيفية التعامل مع مشاكل التفاعل المتعلقة بتحديث الكائنات والمصفوفات، كما سنتعرّف على حل بعض مشاكل تركيز سهولة الوصول أو الشمولية أي سهولة وصول كل المستخدِمين خصوصًا من يملك بعض الإعاقات وغير ذلك. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: تعلّم بعض تقنيات Svelte المتقدمة التي تتضمن حل مشاكل التفاعل ومشاكل سهولة الوصول لمستخدِمي لوحة المفاتيح المتعلقة بدورة حياة المكونات وغير ذلك. سنركز على بعض مشاكل سهولة الوصول التي تتضمن إدارة التركيز، إذ سنستخدِم بعض التقنيات للوصول إلى عُقد نموذج DOM وتنفيذ توابع مثل التابعَين focus() و select()، كما سنرى كيفية التصريح عن تنظيف مستمعي الأحداث على عناصر DOM، كما سنتعلّم بعض الأمور عن دورة حياة المكونات لفهم متى تُثبَّت عُقد DOM ومتى تُفصَل من نموذج DOM وكيف يمكننا الوصول إليها، كما سنتعرف على الموجه action الذي سيسمح بتوسيع وظائف عناصر HTML بطريقة قابلة لإعادة الاستخدام والتصريح. أخيرًا، سنتعلم المزيد عن المكونات، فقد رأينا سابقًا كيف يمكن للمكونات مشاركة البيانات باستخدام الخاصيات Props والتواصل مع المكونات الآباء باستخدام الأحداث وربط البيانات ثنائي الاتجاه، وسنرى الآن كيف يمكن للمكونات الوصول إلى التوابع والمتغيرات. سنطوّر المكونات الجديدة التالية خلال هذا المقال: MoreActions: يعرض الزرين "تحديد الكل Check All" و"حذف المهام المكتملة Remove Completed" ويصدر الأحداث المقابلة المطلوبة للتعامل مع وظائفهما. NewTodo: يعرض حقل الإدخال <input> وزر "الإضافة Add" لإضافة مهمة جديدة. TodosStatus: عرض عنوان الحالة "x out of y items completed" التي تمثِّل المهام المكتملة. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/05-advanced-concepts أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من svelte.dev. المكون MoreActions سنعالج الآن الزرين "تحديد الكل Check All" و"حذف المهام المكتملة Remove Completed"، لذا لننشئ مكونًا يكون مسؤولًا عن عرض الأزرار وإصدار الأحداث المقابلة. أولًا، أنشئ ملفًا جديدًا بالاسم components/MoreActions.svelte. ثانيًا، سنرسل الحدث checkAll عند النقر على الزر الأول للإشارة إلى أنه يجب تحديد أو إلغاء تحديد جميع المهام، كما سنرسل الحدث removeCompleted عند النقر على الزر الثاني للإشارة إلى أنه يجب حذف جميع المهام المكتملة، لذا ضَع المحتوى التالي في الملف MoreActions.svelte: <script> import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() let completed = true const checkAll = () => { dispatch('checkAll', completed) completed = !completed } const removeCompleted = () => dispatch('removeCompleted') </script> <div class="btn-group"> <button type="button" class="btn btn__primary" on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" on:click={removeCompleted}>Remove completed</button> </div> ضمّنا المتغير completed للتبديل بين تحديد جميع المهام وإلغاء تحديدها. ثالثًا، سنستورد المكوِّن MoreActions مرةً أخرى في Todos.svelte وسننشئ دالتين للتعامل مع الأحداث الصادرة من المكوِّن MoreActions، لذا أضف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الموجودة مسبقًا: import MoreActions from './MoreActions.svelte' رابعًا، أضف بعد ذلك الدوال الموضَّحة في نهاية القسم <script>: const checkAllTodos = (completed) => todos.forEach((t) => t.completed = completed) const removeCompletedTodos = () => todos = todos.filter((t) => !t.completed) خامسًا، انتقل الآن إلى الجزء السفلي من شيفرة HTML الخاصة بـ Todos.svelte واستبدل العنصر <div> الذي له الصنف btn-group والذي نسخناه إلى MoreActions.svelte باستدعاء المكوِّن MoreActions كما يلي: <!-- MoreActions --> <MoreActions on:checkAll={e => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} /> سادسًا، لنعد إلى التطبيق ونجربه، إذ ستجد أنّ زر "حذف المهام المكتملة Remove Completed" يعمل بصورة جيدة، ولكن يفشل الزر "تحديد الكل Check All" أو "إلغاء تحديد الكل Uncheck All". اكتشاف التفاعل: تحديث الكائنات والمصفوفات يمكننا تسجيل المصفوفة todos من الدالة checkAllTodos() إلى الطرفية لمعرفة ما يحدث. أولًا، عدّل الدالة checkAllTodos() إلى ما يلي: const checkAllTodos = (completed) => { todos.forEach((t) => t.completed = completed); console.log('todos', todos); } ثانيًا، ارجع إلى متصفحك وافتح طرفية أدوات التطوير DevTools وانقر على زر تحديد الكل أو إلغاء تحديد الكل عدة مرات. ستلاحظ أنّ المصفوفة تُحدَّث بنجاح في كل مرة تضغط فيها على الزر، إذ تُبدَّل الخاصيات completed الخاصة بالكائنات todo بين القيمتين true و false، ولكن إطار Svelte ليس على علم بذلك، وهذا يعني أنه لن تكون تعليمة التفاعل مثل التعليمة $: console.log('todos', todos) مفيدةً جدًا في هذه الحالة، لذلك يجب فهم كيفية عمل التفاعل في إطار Svelte عند تحديث المصفوفات والكائنات. تستخدِم العديد من أطر عمل الويب تقنية نموذج DOM الافتراضي لتحديث الصفحة، إذ يُعَدّ DOM الافتراضي نسخةً في الذاكرة لمحتويات صفحة الويب، كما يحدّث إطار العمل هذا التمثيل الافتراضي الذي تجري مزامنته بعد ذلك مع نموذج DOM الحقيقي، وهذا أسرع بكثير من التحديث المباشر لنموذج DOM ويسمح لإطار العمل بتطبيق العديد من تقنيات التحسين، إذ تعيد هذه الأطر تشغيل كل شيفرة جافاسكربت افتراضيًا في كل تغيير لنموذج DOM الافتراضي، وتطبّق توابعًا مختلفةً لتخزين العمليات الحسابية باهظة الثمن مؤقتًا ولتحسين التنفيذ. لا يستخدِم إطار Svelte تمثيل نموذج DOM الافتراضي، وإنما يحلّل الشيفرة وينشئ شجرةً اعتماديةً، ثم ينشئ شيفرة جافاسكربت المطلوبة لتحديث أجزاء نموذج DOM التي تحتاج إلى تحديث فقط، إذ تنشئ هذه التقنية شيفرة جافاسكربت مثالية بأقل قدر من عمليات المعالجة إلى حد ما ولكن لذلك لا يخلو من بعض القيود. يتعذر على إطار Svelte في بعض الأحيان اكتشاف التغييرات التي تطرأ على المتغيرات المُراقَبة، وتذكَّر أنه يمكنك إخبار إطار Svelte بتغيّر متغير ما من خلال إسناد قيمة جديدة إليه، كما يجب أن يظهر اسم المتغير المُحدَّث على الجانب الأيسر من هذا الإسناد كما يلي على سبيل المثال: const foo = obj.foo foo.bar = 'baz' لن يحدِّث إطار عمل Svelte مراجع الكائن obj.foo.bar إلّا إذا تتبّعتها باستخدام الإسناد obj = obj، إذ لا يمكن لإطار عمل Svelte تتبّع مراجع الكائنات، لذلك يجب إخباره صراحةً أنّ الكائن obj تغير باستخدام الإسناد. ملاحظة: إذا كان foo متغيرًا من المستوى الأعلى، فيمكنك بسهولة إخبار إطار Svelte بتحديث الكائن obj عندما يتغير المتغير foo باستخدام تعليمة التفاعل التالية: $: foo, obj = obj، وبالتالي يُعرَّف foo على أنه اعتمادية، وكلما تغير، سيعمل إطار عمل Svelte على تشغيل عملية الإسناد obj = obj. إذا شغلت ما يلي في الدالة checkAllTodos(): todos.forEach((t) => t.completed = completed); لن يلحظ إطار Svelte تغيّر المصفوفة todos لأنه لا يعرف أننا نعدّلها عند تحديث المتغير t ضمن التابع forEach()، ويُعَدّ ذلك منطقيًا، إذ سيعرف إطار Svelte عمل التابع forEach() الداخلي إذا حدث عكس ذلك، لذا سيُطبَّق الأمر نفسه بالنسبة لأيّ تابع مرتبط بكائن أو مصفوفة، لكن هناك تقنيات مختلفة يمكننا تطبيقها لحل هذه المشكلة، وتتضمن جميعها إسناد قيمة جديدة للمتغير المُراقَب. يمكننا إخبار إطار عمل Svelte بتحديث المتغير باستخدام إسناد ذاتي كما يلي: const checkAllTodos = (completed) => { todos.forEach((t) => t.completed = completed); todos = todos; } تحل هذه الطريقة المشكلة، إذ سيرفع إطار Svelte رايةً تعبِّر عن تغيير المصفوفة todos ويزيل الإسناد الذاتي الذي يراه زائدًا، كما يمكن أن تبدو هذه الطريقة غريبةً، ولكنها تُعَدّ جيدةً ومختصَرةً. يمكننا الوصول أيضًا إلى المصفوفة todos باستخدام الفهرس كما يلي: const checkAllTodos = (completed) => { todos.forEach((t, i) => todos[i].completed = completed); } تعمل الإسنادات إلى خاصيات المصفوفات والكائنات مثل obj.foo += 1 أو array[i] = x بالطريقة نفسها للإسنادات إلى القيم نفسها، فإذا حلّل إطار عمل Svelte هذه الشيفرة، فيمكنه اكتشاف أنّ المصفوفة todos تُعدَّل. يوجد حل آخر هو إسناد مصفوفة جديدة إلى المصفوفة todos، إذ تحتوي هذه المصفوفة الجديدة على نسخة من جميع المهام مع تحديث الخاصية completed وفقًا لذلك كما يلي: const checkAllTodos = (completed) => { todos = todos.map((t) => ({ ...t, completed })); } نستخدِم في هذه الحالة التابع map() الذي يعيد مصفوفةً جديدةً مع نتائج تنفيذ الدالة المتوفرة لكل عنصر، إذ تعيد الدالة نسخةً من كل مهمة باستخدام صيغة الانتشار Spread Syntax وتعيد كتابة خاصية القيمة completed وفقًا لذلك، وتتمثل فائدة هذا الحل في إعادة مصفوفة جديدة مع كائنات جديدة وتجنب تغيير المصفوفة todos الأصلية. ملاحظة: يتيح إطار Svelte تحديد خيارات مختلفة تؤثر على كيفية عمل المصرِّف Compiler، إذ يخبر الخيار <svelte:options immutable={true}/> المصرِّف بأنك تتعهد بعدم تغيير أيّ كائنات، مما يتيح له بأن يكون أقل تحفظًا بشأن التحقق من تغيير القيم وإنشاء شيفرة أبسط وأكثر فعالية. تتضمن كل هذه الحلول إسنادًا يكون فيه المتغير المحدَّث في الجانب الأيسر من المساواة، وستسمح جميعها لإطار Svelte بملاحظة تعديل المصفوفة todos، لذا اختر أحد هذه الحلول وحدّث الدالة checkAllTodos() كما هو مطلوب، ويجب الآن أن تكون قادرًا على تحديد جميع مهامك وإلغاء تحديدها دفعةً واحدةً. الانتهاء من المكون MoreActions سنضيف أحد تفاصيل إمكانية الاستخدام إلى مكوننا، إذ سنعطّل الأزرار في حالة عدم وجود مهام لمعالجتها من خلال استخدام المصفوفة todos بوصفها خاصيةً وضبط الخاصية disabled لكل زر وفقًا لذلك. أولًا، عدّل المكوِّن MoreActions.svelte كما يلي: <script> import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() export let todos let completed = true const checkAll = () => { dispatch('checkAll', completed) completed = !completed } const removeCompleted = () => dispatch('removeCompleted') $: completedTodos = todos.filter(t => t.completed).length </script> <div class="btn-group"> <button type="button" class="btn btn__primary" disabled={todos.length === 0} on:click={checkAll}>{completed ? 'Check' : 'Uncheck'} all</button> <button type="button" class="btn btn__primary" disabled={completedTodos === 0} on:click={removeCompleted}>Remove completed</button> </div> صرّحنا عن متغير التفاعل completedTodos لتفعيل أو تعطيل زر "إزالة المهام المكتملة Remove Completed". لا تنسى تمرير الخاصية إلى المكوِّن MoreActions من المكوِّن Todos.svelte حيث يُستدعَى المكوِّن كما يلي: <MoreActions {todos} on:checkAll={(e) => checkAllTodos(e.detail)} on:removeCompleted={removeCompletedTodos} /> التعامل مع نموذج DOM: التركيز على التفاصيل أكملنا جميع الوظائف المطلوبة للتطبيق، وسنركِّز على بعض ميزات سهولة الوصول Accessibility التي ستحسّن إمكانية استخدام تطبيقنا لكل من مستخدمِي لوحة المفاتيح فقط وقارئات الشاشة، كما يواجه تطبيقنا حاليًا مشكلتين متعلقتين بسهولة وصول استخدام لوحة المفاتيح وتتضمن إدارة التركيز، لذا لنلقِ نظرةً على هذه المشاكل. استكشاف مشاكل سهولة الوصول لمستخدمي لوحة المفاتيح في تطبيقنا سيكتشف مستخدِمو لوحة المفاتيح حاليًا أنّ تدفق التركيز في تطبيقنا لا يمكن التنبؤ به أو غير مترابط، فإذا نقرت على حقل الإدخال في الجزء العلوي من تطبيقنا، فسترى حدًّا سميكًا ومتقطعًا حول هذا الحقل، إذ يُعَد هذا الحدّ المؤشر المرئي على أنّ المتصفح يركِّز حاليًا على هذا العنصر. إذا كنت من مستخدمِي الفأرة، فيمكن أن تتخطى هذه الإشارة المرئية، ولكن إذا أردت العمل باستخدام لوحة المفاتيح فقط، فمعرفة عنصر التحكم المُركَّز عليه أمرٌ بالغ الأهمية، إذ يخبرنا هذا التركيز أيّ عنصر تحكم سيتلقى ضغطات المفاتيح، فإذا ضغطت على مفتاح Tab بصورة متكررة، فسترى مؤشر التركيز المتقطع يتنقل بين جميع العناصر القابلة للتركيز على الصفحة، وإذا نقلت التركيز إلى زر "التعديل Edit" وضغطتَ على مفتاح Enter، فسيختفي التركيز فجأة دون إمكانية تحديد عنصر التحكم الذي سيتلقى ضغطات المفاتيح. إذا ضغطت على مفتاح Escape أو Enter، فلن يحدث شيء؛ أما إذا نقرت على زر "الإلغاء Cancel" أو "الحفظ Save"، فسيختفي التركيز مرةً أخرى، كما سيكون هذا السلوك محيرًا بالنسبة لمستخدِم يعمل باستخدام لوحة المفاتيح. كما نود إضافة بعض ميزات إمكانية الاستخدام مثل تعطيل زر "الحفظ Save" عندما تكون الحقول المطلوبة فارغةً، أو التركيز على بعض عناصر HTML أو التحديد التلقائي للمحتويات عند التركيز على حقل إدخال النص، كما يجب الوصول برمجيًا إلى عقد نموذج DOM لتشغيل دوال مثل الدالتين focus() و select() بهدف تطبيق جميع هذه الميزات، ويجب استخدام التابعين addEventListener() و removeEventListener() لتشغيل مهام محددة عندما يتلقى عنصر التحكم التركيز. تكمن المشكلة في أنّ جميع عقد نموذج DOM ينشئها إطار عمل Svelte ديناميكيًا في وقت التشغيل، لذا علينا الانتظار حتى إنشائها وإضافتها إلى نموذج DOM لاستخدامها، إذ يجب التعرف على دورة حياة المكونات لفهم متى يمكننا الوصول إليها. إنشاء المكون NewTodo أنشئ ملف مكوِّن جديد وعدّل الشيفرة لإصدار الحدث addTodo من خلال تمرير اسم المهمة الجديدة مع التفاصيل الإضافية كما يلي: أولًا، أنشئ ملفًا جديدًا بالاسم components/NewTodo.svelte. ضع بعد ذلك المحتويات التالية ضمن هذا الملف: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let name = ''; const addTodo = () => { dispatch('addTodo', name); name = ''; } const onCancel = () => name = ''; </script> <form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg">What needs to be done?</label> </h2> <input bind:value={name} type="text" id="todo-0" autoComplete="off" class="input input__lg" /> <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button> </form> ربطنا هنا العنصر <input> بالمتغير name باستخدام bind:value={name} وعطّلنا زر "الإضافة Add" عندما يكون حقل الإدخال فارغًا -أي لا يحتوي على محتوى نصي- باستخدام disabled={!name}، كما عالجنا استخدام مفتاح Escape باستخدام on:keydown={(e) => e.key === 'Escape' && onCancel()}، إذ نشغّل التابع onCancel() الذي يمسح المتغير name في كل مرة نضغط فيها على مفتاح Escape. يجب الآن استيراد import المكوِّن NewTodo واستخدامه ضمن المكوِّن Todos وتحديث الدالة addTodo() للحصول على اسم المهمة الجديد، لذا أضف تعليمة الاستيراد التالية بعد تعليمات الاستيراد الأخرى الموجودة ضمن Todos.svelte: import NewTodo from './NewTodo.svelte' عدّل الدالة addTodo() بعد ذلك كما يلي: function addTodo(name) { todos = [...todos, { id: newTodoId, name, completed: false }] } تتلقى الدالة addTodo() الآن اسم المهمة الجديدة مباشرةً، لذلك لم نَعُد بحاجة المتغير newTodoName لإعطائه قيمة، إذ سيهتم المكوِّن NewTodo بذلك. ملاحظة: تُعَدّ الصيغة { name } اختصارًا للصيغة { name: name }، إذ يأتي هذا الاختصار من لغة جافاسكربت وليس له علاقة بإطار Svelte مع توفير بعض الإلهام للاختصارات الخاصة بإطار Svelte. أخيرًا، استبدل شيفرة HTML الخاصة بنموذج NewTodo باستدعاء المكوِّن NewTodo كما يلي: <!-- NewTodo --> <NewTodo on:addTodo={(e) => addTodo(e.detail)} /> التعامل مع عقد نموذج DOM باستخدام الموجه bind:this={dom_node} نريد الآن أن يعود التركيز إلى العنصر <input> الخاص بالمكون NewTodo في كل مرة يُضغَط فيها على زر "الإضافة Add"، لذا سنحتاج مرجعًا إلى عقدة نموذج DOM الخاصة بحقل الإدخال، إذ يوفر إطار عمل Svelte طريقةً لذلك باستخدام الموجِّه bind:this={dom_node}، كما يسند إطار Svelte مرجع عقدة DOM إلى متغير محدد بمجرد تثبيت المكوِّن وإنشاء عقدة DOM. لننشئ المتغير nameEl ونربطه بحقل الإدخال باستخدام bind:this={nameEl}، ثم سنستدعي التابع nameEl.focus() ضمن الدالة addTodo() لإعادة التركيز إلى العنصر <input> مرةً أخرى بعد إضافة المهام الجديدة، وسنطبّق الشيء نفسه عندما يضغط المستخدِم على مفتاح Escape باستخدام الدالة onCancel(). عدّل محتويات المكوِّن NewTodo.svelte كما يلي: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); let name = ''; let nameEl; // مرجع إلى عقدة حقل الإدخال name في نموذج DOM const addTodo = () => { dispatch('addTodo', name); name = ''; nameEl.focus(); // ركّز على حقل الإدخال name } const onCancel = () => { name = ''; nameEl.focus(); // ركّز على حقل الإدخال name } </script> <form on:submit|preventDefault={addTodo} on:keydown={(e) => e.key === 'Escape' && onCancel()}> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg">What needs to be done?</label> </h2> <input bind:value={name} bind:this={nameEl} type="text" id="todo-0" autoComplete="off" class="input input__lg" /> <button type="submit" disabled={!name} class="btn btn__primary btn__lg">Add</button> </form> جرِّب التطبيق واكتب اسم مهمة جديدة في حقل الإدخال <input> واضغط على المفتاح tab للتركيز على زر "الإضافة Add"، ثم اضغط على مفتاح Enter أو Escape لترى كيف يستعيد حقل الإدخال التركيز. التركيز التلقائي على حقل الإدخال الميزة التالية التي سنضيفها إلى المكوِّن NewTodo هي الخاصية autofocus التي ستسمح بتحديد أننا نريد التركيز على حقل الإدخال <input> في صفحة التحميل. محاولتنا الأولى هي كما يلي: لنحاول إضافة الخاصية autofocus واستدعاء التابع nameEl.focus() في كتلة القسم <script>، لذا عدِّل الجزء الأول من القسم <script> الخاص بالمكوِّن NewTodo.svelte (الأسطر الأربعة الأولى) لتبدو كما يلي: <script> import { createEventDispatcher } from 'svelte'; const dispatch = createEventDispatcher(); export let autofocus = false; let name = ''; let nameEl; // مرجع إلى عقدة حقل الإدخال name في نموذج DOM if (autofocus) nameEl.focus(); عُد الآن إلى المكوِّن Todos ومرّر الخاصية autofocus إلى استدعاء المكوِّن <NewTodo> كما يلي: <!-- NewTodo --> <NewTodo autofocus on:addTodo={(e) => addTodo(e.detail)} /> إذا جربت تطبيقك، فسترى أنّ الصفحة فارغة حاليًا، وسترى في طرفية أدوات تطوير الويب خطأً بالشكل: TypeError: nameEl is undefined. دورة حياة المكون والدالة onMount() يشغّل إطار Svelte شيفرة التهيئة -أي قسم <script> الخاص بالمكون- عند إنشاء نسخة من هذا المكون، ولكن تكون جميع العقد التي يتألف منها المكوِّن غير مرتبطة بنموذج DOM في تلك اللحظة، وهي في الحقيقة غير موجودة أصلًا، كما يمكن أن تتساءل عن كيفية معرفة وقت إنشاء المكوِّن فعليًا وتثبيته على نموذج DOM، والإجابة هي أنه لكل مكوِّن دورة حياة تبدأ عند إنشائه وتنتهي عند تدميره، وهناك عدد من الدوال التي تسمح بتشغيل الشيفرة في اللحظات المهمة خلال دورة الحياة هذه. الدالة التي ستستخدِمها بكثرة هي الدالة onMount() والتي تتيح تشغيل دالة رد نداء Callback بمجرد تثبيت المكوِّن على نموذج DOM، لذا لنجربها ونرى ما سيحدث للمتغير nameEl. أضِف أولًا السطر التالي في بداية القسم <script> الخاص بالمكوِّن NewTodo.svelte: import { onMount } from 'svelte'; وأضِف الأسطر التالية في نهايته: console.log('initializing:', nameEl); onMount( () => { console.log('mounted:', nameEl); }) احذف الآن السطر if (autofocus) nameEl.focus() لتجنب الخطأ الذي رأيناه سابقًا. سيعمل التطبيق الآن مرةً أخرى، وسترى ما يلي في الطرفية: initializing: undefined mounted: <input id="todo-0" class="input input__lg" type="text" autocomplete="off"> يكون المتغير nameEl غير مُعرَّف أثناء تهيئة المكوِّن، وهو أمر منطقي لأن عقدة حقل الإدخال <input> غير موجودة حتى الآن، لذا أسند إطار عمل Svelte مرجع العقدة <input> في نموذج DOM إلى المتغير nameEl بفضل الموجّه bind:this={nameEl} بعد تثبيت المكوِّن. يمكنك تشغيل وظيفة التركيز التلقائي من خلال استبدال كتلة onMount() ضمن console.log() السابقة بما يلي: onMount(() => autofocus && nameEl.focus()); // سنشغّل التابع nameEl.focus() إذا كانت قيمة autofocus هي true انتقل إلى تطبيقك مرةً أخرى وسترى الآن تركيز حقل الإدخال <input> على صفحة التحميل. انتظار تحديث نموذج DOM باستخدام الدالة tick() سنهتم الآن بتفاصيل إدارة تركيز المكون Todo، إذ نريد أولًا أن يستلم تعديل حقل الإدخال <input> الخاص بالمكوِّن Todo التركيز عند الدخول في وضع التعديل من خلال الضغط على زر "التعديل Edit"، كما سننشئ المتغير nameEl ضمن المكوِّن Todo.svelte وسنستدعي التابع nameEl.focus() بعد ضبط المتغير editing على القيمة true. أولًا، افتح الملف components/Todo.svelte وأضِف التصريح عن المتغير nameEl التالي بعد التصريح عن المتغيرين editing و name مباشرةً: let nameEl; // مرجع إلى عقدة حقل الإدخال name في نموذج DOM ثانيًا، عدّل الدالة onEdit() كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل nameEl.focus(); // ضبط التركيز على حقل الإدخال name } أخيرًا، اربط المتغير nameEl بحقل الإدخال <input> من خلال تعديله كما يلي: <input bind:value={name} bind:this={nameEl} type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> ولكن ستحصل على خطأ بالشكل: "TypeError: nameEl is undefined" في الطرفية عند الضغط على زر تعديل المهمة. لا يحدّث إطار عمل Svelte نموذج DOM مباشرةً عند تحديث حالة المكوِّن، وإنما ينتظر حتى المهمة السريعة microtask التالية لمعرفة ما إذا كانت هناك أيّ تغييرات أخرى يجب تطبيقها بما في ذلك التغييرات في المكونات الأخرى، مما يؤدي إلى تجنب العمل غير الضروري ويسمح للمتصفح بتجميع الأشياء بطريقة أكثر فعالية. لا يكون تعديل حقل الإدخال <input> مرئيًا في هذه الحالة لأنه غير موجود في نموذج DOM عندما يكون للمتغير editing القيمة false، لذا اضبط editing = true في الدالة onEdit() وحاول بعد ذلك مباشرةً الوصول إلى المتغير nameEl ونفّذ التابع nameEl.focus()، ولكن المشكلة هنا هي أنّ إطار عمل Svelte لم يحدّث نموذج DOM بعد. تتمثل إحدى طرق حل هذه المشكلة في استخدام التابع setTimeout() لتأخير استدعاء التابع nameEl.focus() حتى دورة الأحداث التالية وإعطاء إطار عمل Svelte الفرصة لتحديث نموذج DOM كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل setTimeout(() => nameEl.focus(), 0); // استدعاء غير متزامن لضبط التركيز على حقل الإدخال name } الحل السابق جيد، ولكنه غير مرتب إلى حد ما، إذ يوفر إطار Svelte طريقةً أفضل للتعامل مع هذه الحالات، حيث تعيد الدالة tick() وعدًا Promise يُحَل بمجرد تطبيق أيّ تغييرات على حالة مُعلَّقة في نموذج DOM، أو مباشرةً إذا لم تكن هناك تغييرات على حالة مُعلَّقة. استورد أولًا tick في بداية القسم <script> مع تعليمات الاستيراد الموجودة مسبقًا كما يلي: import { tick } from 'svelte' استدعِ بعد ذلك الدالة tick() مع المعامِل await من دالة غير متزامنة، وعدّل الدالة onEdit() كما يلي: async function onEdit() { editing = true; // الدخول في وضع التعديل await tick(); nameEl.focus(); } إذا جربت التطبيق الآن، فسترى أنّ كل شيء يعمل كما هو متوقع. إضافة وظائف إلى عناصر HTML باستخدام الموجه use:action نريد بعد ذلك أن يحدّد حقل الإدخال <input> كل النص عند التركيز عليه، كما نريد تطوير ذلك بطريقة يمكن إعادة استخدامه بسهولة على أيّ عنصر <input> في HTML وتطبيقه بطريقة تصريحية، وسنستخدِم هذا المتطلب بوصفه سببًا لإظهار ميزة قوية جدًا يوفرها إطار Svelte لإضافة وظائف لعناصر HTML العادية، وهذه الميزة هي الإجراءات actions. يمكنك تحديد نص عقدة حقل إدخال في نموذج DOM من خلال استدعاء التابع select()، إذ يجب استخدام مستمع أحداث لاستدعاء هذه الدالة كلما انتقل التركيز إلى هذه العقدة كما يلي: node.addEventListener('focus', event => node.select()) كما يجب استدعاء الدالة removeEventListener() عند تدمير العقدة لتجنب تسرّب الذاكرة Memory Leak. ملاحظة: كل ما سبق هو مجرد وظيفة قياسية من واجهة WebAPI دون وجود شيء خاص بإطار عمل Svelte. يمكن تحقيق كل ذلك في المكوِّن Todo كلما أضفنا أو أزلنا عنصر <input> من نموذج DOM، ولكن يجب أن نكون حريصين جدًا على إضافة مستمع الأحداث بعد إضافة العقدة إلى نموذج DOM وإزالة المستمع قبل إزالة العقدة من نموذج DOM. كما أنّ هذا الحل لن يكون قابلًا لإعادة الاستخدام بصورة كبيرة، وهنا يأتي دور إجراءات إطار Svelte التي تسمح بتشغيل دالة كلما أُضيف عنصر إلى نموذج DOM وبعد إزالته من نموذج DOM. سنعرّف دالة بالاسم selectOnFocus() تأخذ عقدة على أساس معامل لها، وستضيف هذه الدالة مستمع أحداث إلى تلك العقدة بحيث يُحدَّد النص كلما انتقل التركيز إليها، ثم ستعيد كائنًا مع الخاصية destroy التي سينفذها إطار Svelte بعد إزالة العقدة من نموذج DOM، وسنزيل هنا المستمع للتأكّد من أننا لا نترك أيّ تسرّب للذاكرة خلفنا. أولًا، لننشئ الدالة selectOnFocus()، لذا أضف ما يلي إلى أسفل القسم <script> الخاص بالمكوِّن Todo.svelte: function selectOnFocus(node) { if (node && typeof node.select === 'function' ) { // تأكّد من أن العقدة مُعرَّفة ولديها التابع select() const onFocus = event => node.select(); // معالج الحدث node.addEventListener('focus', onFocus); // استدعِ التابع onFocus() عندما ينتقل التركيز إلى العقدة return { destroy: () => node.removeEventListener('focus', onFocus) // سيُنفَّذ هذا السطر عند إزالة العقدة من نموذج DOM } } } يجب الآن إعلام حقل الإدخال <input> بأن يستخدِم هذه الدالة من خلال الموجّه use:action كما يلي: <input use:selectOnFocus /> نطلب باستخدام هذا الموجّه من إطار Svelte تشغيل هذه الدالة وتمرير عقدة نموذج DOM الخاصة بحقل الإدخال <input> بوصفها معاملًا لها بمجرد تثبيت المكوِّن على نموذج DOM، وسيكون مسؤولًا عن تنفيذ الدالة destroy عند إزالة المكوِّن من نموذج DOM، وبالتالي يهتم Svelte بدورة حياة المكوِّن باستخدام الموجّه use، وسيكون العنصر <input> في حالتنا كما يلي: عدّل أول زوج تسمية أو عنوان/حقل إدخال label/input للمكوِّن ضمن قالب التعديل على النحو التالي: <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-{todo.id}" autocomplete="off" class="todo-text" /> انتقل إلى تطبيقك واضغط على زر تعديل المهام ثم اضغط على المفتاح Tab لإبعاد التركيز عن العنصر <input>، ثم انقر عليه وسترى تحديد نص حقل الإدخال بالكامل. جعل الإجراء قابلا لإعادة الاستخدام لنجعل الآن هذه الدالة قابلة لإعادة الاستخدام بين المكونات، إذ تُعَدّ الدالة selectOnFocus() مجرد دالة لا تعتمد على المكوِّن Todo.svelte، لذا يمكننا وضعها في ملف واستخدامها من هناك. أولًا، أنشئ ملفًا جديدًا بالاسم actions.js ضمن المجلد src. ثانيًا، ضع فيه المحتوى التالي: export function selectOnFocus(node) { if (node && typeof node.select === 'function' ) { // تأكّد من أن العقدة مُعرَّفة ولديها التابع select() const onFocus = event => node.select(); // معالج الحدث node.addEventListener('focus', onFocus); // يُستدعى عند التركيز على القعدة return { destroy: () => node.removeEventListener('focus', onFocus) // سيُنفَّذ هذا السطر عند إزالة العقدة من نموذج DOM } } } استورده من داخل المكوِّن Todo.svelte من خلال إضافة تعليمة الاستيراد التالية: import { selectOnFocus } from '../actions.js' احذف تعريف الدالة selectOnFocus() من المكوِّن Todo.svelte، لأننا لم نعُد بحاجة إليها هناك. إعادة استخدام الإجراء لنستخدم الإجراء في المكوِّن NewTodo.svelte لإثبات إمكانية إعادة استخدامه. أولًا، استورد الدالة selectOnFocus() من الملف actions.js في الملف NewTodo.svelte كما يلي: import { selectOnFocus } from '../actions.js'; ثانيًا، أضف الموجّه use:selectOnFocus إلى العنصر <input> كما يلي: <input bind:value={name} bind:this={nameEl} use:selectOnFocus type="text" id="todo-0" autocomplete="off" class="input input__lg" /> يمكننا إضافة وظائف لعناصر HTML العادية بطريقة قابلة لإعادة الاستخدام وتصريحية باستخدام بضعة أسطر من الشيفرة البرمجية، إذ يتطلب الأمر استيرادًا import وموجّهًا قصيرًا مثل الموجّه use:selectOnFocus الذي يوضِّح الغرض منه، ويمكننا تحقيق ذلك دون الحاجة إلى إنشاء عنصر مُغلِّف مخصَّص مثل TextInput أو MyInput أو ما شابه ذلك، كما يمكنك إضافة العديد من موجّهات use:action إلى عنصر ما. كما أنه ليس علينا أن نعاني باستخدام الدوال onMount() أو onDestroy() أو tick()، إذ يهتم الموجّه use بدورة حياة المكوِّن. تحسينات الإجراءات الأخرى كان علينا في القسم السابق أثناء العمل مع مكونات Todo التعاملَ مع الدوال bind:this و tick() و async للتركيز على حقل الإدخال <input> بمجرد إضافته إلى نموذج DOM. يمكننا تطبيق ذلك باستخدام الإجراءات كما يلي: const focusOnInit = (node) => node && typeof node.focus === 'function' && node.focus(); يجب بعد ذلك إضافة موجّه use: آخر في شيفرة HTML كما يلي: <input bind:value={name} use:selectOnFocus use:focusOnInit /> يمكن الآن أن تكون الدالة onEdit() أبسط كما يلي: function onEdit() { editing = true; // الدخول في وضع التعديل } لنعُد إلى المكوِّن Todo.svelte ونركّز على زر "التعديل Edit" بعد أن يضغط المستخدِم على زر "الحفظ Save" أو "الإلغاء Cancel". يمكننا محاولة إعادة استخدام الإجراء focusOnInit مرة أخرى من خلال إضافة الموجّه use:focusOnInit إلى زر "التعديل Edit"، لكننا سندخِل بذلك زلة برمجية، إذ سينتقل التركيز عند إضافة مهمة جديدة إلى زر "التعديل Edit" الخاص بالمهمة التي أُضيفت مؤخرًا بسبب تشغيل الإجراء focusOnInit عند إنشاء المكوِّن. لا نريد ذلك، وإنما نريد أن يستلم زر "التعديل Edit" التركيز فقط عندما يضغط المستخدِم على زر "الحفظ Save" أو "الإلغاء Cancel". لذا ارجع إلى الملف Todo.svelte، إذ سننشئ أولًا رايةً بالاسم editButtonPressed ونهيّئها بالقيمة false، لذا أضف ما يلي بعد تعريفات المتغيرات الأخرى: let editButtonPressed = false; // تتبّع إذا ضُغِط على زر التعديل للتركيز عليه بعد الإلغاء أو الحفظ سنعدّل بعد ذلك وظيفة زر "التعديل Edit" لحفظ هذه الراية وإنشاء إجرائها الخاص، لذا عدّل الدالة onEdit() كما يلي: function onEdit() { editButtonPressed = true; // سيؤدي ضغط المستخدم على زر التعديل إلى عودة التركيز إليه editing = true; // الدخول في وضع التعديل } أضِف بعد ذلك تعريف الدالة focusEditButton() التالي: const focusEditButton = (node) => editButtonPressed && node.focus(); أخيرًا، استخدم الموجّه use:focusEditButton مع زر "التعديل Edit" كما يلي: <button type="button" class="btn" on:click={onEdit} use:focusEditButton> Edit<span class="visually-hidden"> {todo.name}</span> </button> جرّب تطبيقك مرةً أخرى، إذ يُنفَّذ الإجراء focusEditButton في هذه المرحلة في كل مرة يُضاف فيها زر "التعديل Edit" إلى نموذج DOM، ولكنه سيعطي التركيز فقط للزر إذا كانت قيمة الراية editButtonPressed هي true. ملاحظة: لم نتعمق كثيرًا في الإجراءات هنا، إذ يمكن أن تحتوي الإجراءات على معامِلات تفاعلية، ويتيح إطار Svelte اكتشاف متى يتغير أيّ من هذه المعامِلات لنتمكن من إضافة وظائف تتكامل جيدًا مع نظام التفاعل في إطار Svelte، كما تُعَدّ الإجراءات مفيدةً للتكامل بسلاسة مع المكتبات الخارجية. ربط المكونات: الوصول إلى توابع ومتغيرات المكون باستخدام الموجه bind:this={component} توجد مشكلة أخرى وهي أنه يتلاشى التركيز عندما يضغط المستخدِم على زر "الحذف Delete"، إذ تتضمن الميزة الأخيرة التي سنشرحها في هذا المقال ضبط التركيز على عنوان الحالة بعد حذف مهمة. اخترنا التركيز على عنوان الحالة بسبب حذف العنصر الذي جرى التركيز عليه، لذلك لا يوجد عنصر آخر واضح لتلقي التركيز، إذ يُعَدّ عنوان الحالة قريبًا من قائمة المهام، وهو طريقة مرئية لمعرفة إزالة المهمة بالإضافة إلى توضيح ما حدث لمستخدِمي قارئ الشاشة. أولًا، أنشئ ملفًا جديدًا بالاسم components/TodosStatus.svelte. ثانيًا، أضِف إليه المحتويات التالية: <script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; </script> <h2 id="list-heading"> {completedTodos} out of {totalTodos} items completed </h2> ثالثًا، استورد هذا الملف في بداية المكوِّن Todos.svelte من خلال إضافة تعليمة الاستيراد import التالية بعد تعليمات الاستيراد الأخرى: import TodosStatus from './TodosStatus.svelte'; رابعًا، استبدل عنوان الحالة <h2> ضمن الملف Todos.svelte باستدعاء المكوِّن TodosStatus من خلال تمرير todos إليه بوصفها خاصيةً كما يلي: <TodosStatus {todos} /> خامسًا، أزِل المتغيرين totalTodos و completedTodos من المكوِّن Todos.svelte، إذ ما عليك سوى إزالة السطرين $: totalTodos = ... و$: completedTodos = ... وإزالة المرجع إلى المتغير totalTodos عندما نحسب newTodoId واستخدم todos.length بدلًا من ذلك، أي استبدل الكتلة التي تبدأ بالسطر let newTodoId بما يلي: $: newTodoId = todos.length ? Math.max(...todos.map(t => t.id)) + 1 : 1; يعمل كل شيء كما هو متوقع، واستخرجنا للتو آخر جزء من شيفرة HTML إلى مكوِّنه الخاص. يجب الآن إيجاد طريقة للتركيز على تسمية الحالة <h2> بعد إزالة المهمة، إذ رأينا حتى الآن كيفية إرسال المعلومات إلى مكوِّن باستخدام الخاصيات Props، وكيف يمكن للمكوِّن التواصل مع المكوِّن الأب عن طريق إصدار أحداث أو استخدام ربط البيانات ثنائي الاتجاه، إذ يمكن للمكوِّن الابن الحصول على مرجع إلى العقدة <h2> باستخدام الموجّه bind:this={dom_node} ويمكن للمكونات الخارجية الوصول إليه باستخدام ربط البيانات ثنائي الاتجاه، لكن سيؤدي ذلك إلى كسر تغليف المكوِّن، لذلك نحن بحاجة إلى المكوِّن TodosStatus للوصول إلى تابع يمكن للمكوِّن الابن استدعاؤه للتركيز علي، إذ تُعَدّ حاجة المكوِّن لإمكانية وصول المستخدِم لبعض السلوك أو المعلومات أمرًا شائعًا جدًا، لذا لنرى كيفية تحقيق ذلك في إطار عمل Svelte. رأينا سابقًا أن إطار عمل Svelte يستخدِم التعليمة export let varname = ... للتصريح عن الخاصيات، ولكن إذا صدّرتَ ثابتًا const أو صنفًا class أودالةً function بدلًا من استخدام let لوحدها، فستكون للقراءة فقط خارج المكوِّن، وتُعَدّ تعابير الدوال خاصيات صالحةً. تُعَدّ التصريحات الثلاثة الأولى في المثال التالي خاصيات، والتصريحات الأخرى هي عبارة عن قيم مُصدَّرة: <script> export let bar = "optional default initial value"; // خاصية export let baz = undefined; // خاصية export let format = n => n.toFixed(2); // خاصية // these are readonly export const thisIs = "readonly"; // تصدير للقراءة فقط export function greet(name) { // تصدير للقراءة فقط alert(`hello ${name}!`); } export const greet = (name) => alert(`hello ${name}!`); // تصدير للقراءة فقط </script> لننشئ تابعًا بالاسم focus() يركّز على العنوان <h2>، لذا سنحتاج إلى المتغير headingEl للاحتفاظ بالمرجع إلى عقدة DOM، ويجب ربطه بالعنصر <h2> باستخدام الموجّه bind:this={headingEl}، إذ سيشغّل تابع التركيز فقط headingEl.focus(). أولًا، عدّل محتويات المكوِّن TodosStatus.svelte كما يلي: <script> export let todos; $: totalTodos = todos.length; $: completedTodos = todos.filter((todo) => todo.completed).length; let headingEl; export function focus() { // shorter version: export const focus = () => headingEl.focus() headingEl.focus(); } </script> <h2 id="list-heading" bind:this={headingEl} tabindex="-1"> {completedTodos} out of {totalTodos} items completed </h2> لاحظ أننا أضفنا السمة tabindex إلى العنوان <h2> للسماح للعنصر بتلقي التركيز برمجيًا، إذ يعطينا استخدام الموجِّه bind:this={headingEl} مرجعًا إلى عقدة DOM في المتغير headingEl كما رأينا سابقًا، كما نستخدم بعد ذلك التعليمة export function focus() لإمكانية الوصول إلى دالة تركّز على العنوان <h2>، كما يمكنك ربط نسخ المكوِّن باستخدام الموجِّه bind:this={component} مثل ربط عناصر DOM باستخدام الموجّه bind:this={dom_node}، لذا تحصل على مرجع لعقدة DOM عند استخدام الموجِّه bind:this مع عنصر HTML، وتحصل على مرجع إلى نسخة من هذا المكوِّن عندما تفعل ذلك مع مكوِّن Svelte. ثانيًا، سننشئ أولًا المتغير todosStatus في Todos.svelte للربط بنسخة من المكوِّن Todos.svelte، لذا أضف السطر التالي بعد تعليمات الاستيراد الموجودة مسبقًا: let todosStatus; // مرجع إلى نسخة من المكون TodosStatus ثالثًا، أضِف بعد ذلك الموجّه bind:this={todosStatus} إلى الاستدعاء كما يلي: <!-- TodosStatus --> <TodosStatus bind:this={todosStatus} {todos} /> رابعًا، يمكننا الآن استدعاء التابع focus() المُصدَّر من التابع removeTodo() كما يلي: function removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id); todosStatus.focus(); // ركّز على عنوان الحالة } خامسًا، ارجع إلى تطبيقك، فإذا حذفت أيّ مهمة الآن، فسينتقل التركيز إلى عنوان الحالة، وهذا مفيد لتسليط الضوء على التغيير في عدد المهام لكل من المستخدِمين المبصرين ومستخدِمي قارئات الشاشة. ملاحظة: يمكن أن تتساءل عن سبب حاجتنا للتصريح عن متغير جديد لربط المكون بالرغم من أنه يمكننا فقط استدعاء التابع TodosStatus.focus()، إذ يمكن أن يكون لديك العديد من نسخ المكوِّن TodosStatus النشطة، لذلك تحتاج لطريقة للرجوع إلى كل نسخة معينة، وبالتالي يجب تحديد متغير لربط كل نسخة محددة به. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/06-stores أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/06-stores تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من موقع svelte.dev. الخلاصة انتهينا في هذا المقال من إضافة جميع الوظائف المطلوبة إلى تطبيقنا، بالإضافة إلى اهتمامنا بعدد من مشاكل سهولة الوصول وسهولة الاستخدام، وانتهينا من تقسيم تطبيقنا إلى مكونات يمكن إدارتها مع إعطاء كل منها مسؤولية فريدة، كما رأينا بعض تقنيات إطار عمل Svelte المتقدمة مثل: التعامل مع اكتشاف التفاعل عند تحديث العناصر والمصفوفات. العمل مع عقد DOM باستخدام الموجّه bind:this={dom_node} (ربط عناصر DOM). استخدام الدالة onMount() الخاصة بدورة حياة المكوِّن. إجبار إطار عمل Svelte على حل تغييرات الحالة المُعلَّقة باستخدام الدالة tick(). إضافة وظائف لعناصر HTML بطريقة تصريحية وقابلة لإعادة الاستخدام باستخدام الموجّه use:action. الوصول إلى توابع المكونات باستخدام الموجّه bind:this={component} (ربط المكونات). سنرى في المقال التالي كيفية استخدام المخازن Stores للتواصل بين المكونات وإضافة الحركة إلى المكونات. ترجمة -وبتصرُّف- للمقال Advanced Svelte: Reactivity, lifecycle, accessibility. اقرأ أيضًا بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte التعامل مع المتغيرات والخاصيات في إطار عمل Svelte تقسيم تطبيق Svelte إلى مكونات
-
بدأنا في مقال التعامل مع المتغيرات والخاصيات بتطوير تطبيق قائمة المهام، والهدف الأساسي من هذا المقال هو تعلّم كيفية تقسيم تطبيقنا إلى مكونات يمكن إدارتها ومشاركة المعلومات فيما بينها. سنقسّم تطبيقنا إلى مكونات، ثم سنضيف مزيدًا من الوظائف للسماح للمستخدمين بتحديث المكونات الحالية. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: تعلم كيفية تقسيم تطبيقنا إلى مكونات ومشاركة المعلومات فيما بينها. يمكنك متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/04-componentizing-our-app أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، وإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL. تقسيم التطبيق إلى مكونات يتكون التطبيق في إطار عمل Svelte من مكوِّن واحد أو من مكونات متعددة، ويُعَدّ المكوِّن كتلةً من الشيفرة البرمجية القابلة لإعادة الاستخدام والمستقلة ذاتيًا والتي تغلّف شيفرة HTML و CSS وجافاسكربت المرتبطة مع بعضها البعض والمكتوبة في ملف .svelte، كما يمكن أن تكون المكونات كبيرةً أو صغيرةً، لكنها تكون عادةً محدَّدةً بوضوح، فالمكونات الأكثر فاعليةً هي المكونات التي تخدم غرضًا واحدًا واضحًا. من فوائد تحديد المكونات هو قابلية هذه المكونات للموازنة مع أفضل الممارسات العامة بهدف تنظيم شيفرتك البرمجية ضمن أجزاء يمكن إدارتها، مما يساعدك على فهم كيفية ارتباطها ببعضها البعض ويعزِّز إعادة الاستخدام ويجعل شيفرتك البرمجية أسهل للتفكير بها وصيانتها وتوسيعها. لا توجد قواعد صارمة لتقسيم المكونات، لذا يفضِّل بعض الأشخاص اتباع نهج بسيط يتمثل بالنظر إلى شيفرة HTML ثم رسم مربعات حول كل مكوِّن ومكوِّن فرعي يبدو أنّ له شيفرته الخاصة، في حين يطبق أشخاص آخرون الأساليب نفسها المُستخدَمة لتحديد ما إذا كان يجب إنشاء دالة أو كائن جديد، وأحد هذه الأساليب هو مبدأ المسؤولية الفردية، أي يجب أن يطبّق المكون شيئًا واحدًا فقط بصورة مثالية، ثم يمكننا تقسيمه إلى مكونات فرعية أصغر إذا لزم الأمر، كما يجب أن يكمل هذا النهجان بعضهما البعض لمساعدتك على تحديد كيفية تنظيم مكوناتك بطريقة أفضل. سنقسم تطبيقنا إلى المكونات التالية: Alert.svelte: مربع إشعارات عام لإرسال الإجراءات التي حدثت. NewTodo.svelte: حقل إدخال النص والزر الذي يسمح بإدخال عنصر مهام جديد. FilterButton.svelte: أزرار "كل المهام All" و"المهام النشطة Active" و"المهام المكتملة Completed" التي تسمح بتطبيق المرشّحات Filters على عناصر المهام المعروضة. TodosStatus.svelte: العنوان الذي يعرض العبارة "x out of y items completed" التي تمثّل عدد المهام المكتملة. Todo.svelte: عنصر مهام مفرد، إذ سيُعرَض كل عنصر مهمة مرئي في نسخة منفصلة من هذا المكوِّن. MoreActions.svelte: الزرّان "تحديد الكل Check All" و"احذف المهام المكتملة Remove Completed" الموجودان أسفل واجهة المستخدِم، ويسمحان بتنفيذ مجموعة إجراءات على عناصر المهام. سنركز في هذا المقال على إنشاء المكونين FilterButton و Todo وسنشرح المكونات الأخرى في المقالات القادمة. ملاحظة: سنتعلم أيضًا في عملية إنشاء أول مكونين تقنيات مختلفة لتواصل المكونات مع بعضها بعضًا، وإيجابيات وسلبيات كل من هذه التقنيات. استخراج مكون الترشيح سننشئ أولًا المكون FilterButton.svelte باتباع الخطوات التالية: أولًا، أنشئ ملفًا جديدًا components/FilterButton.svelte. ثانيًا، سنصرّح عن الخاصية filter في هذا الملف ثم سننسخ شيفرة HTML المتعلقة به من الملف Todos.svelte، لذا أضِف المحتوى التالي إلى هذا الملف: <script> export let filter = 'all' </script> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={()=> filter = 'all'} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={()=> filter = 'active'} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={()=> filter = 'completed'} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> ثالثًا، ارجع إلى المكوِّن Todos.svelte، حيث نريد الاستفادة من المكوِّن FilterButton، إذ يجب استيراده أولًا، لذا أضِف السطر التالي قبل القسم <script> في المكوِّن Todos.svelte: import FilterButton from './FilterButton.svelte' رابعًا، استبدل الآن العنصر <div> الذي يملك اسم الصنف filters باستدعاء المكون FilterButton الذي يأخذ المرشح الحالي بوصفه خاصيةً كما يلي: <FilterButton {filter} /> ملاحظة: تذكَّر أنه إذا تطابق اسم سمة في لغة HTML مع اسم المتغير، فيمكن استبدالهما بالشكل {variable}، لذا يمكننا استبدال <FilterButton filter={filter} /> بالشكل <FilterButton {filter} />. لنجرب التطبيق الآن، حيث ستلاحظ أنه إذا نقرت على أزرار الترشيح، فستُحدَّد هذه الأزرار وسيُحدَّث التنسيق بطريقة مناسبة، ولكن لدينا مشكلة وهي عدم ترشيح المهام، وسبب هذه المشكلة هو انتقال المتغير filter من المكوِّن Todos إلى المكوِّن FilterButton عبر الخاصية، ولكن لا تنتقل التغييرات التي تحدث في المكوِّن FilterButton مرةً أخرى إلى المكوِّن الأب، إذ يكون ارتباط البيانات أحادي الاتجاه افتراضيًا. مشاركة البيانات بين المكونات: تمرير المعالج بوصفه خاصية تتمثل إحدى طرق السماح للمكونات الأبناء بإعلام المكونات الآباء بأيّ تغييرات في تمرير المعالج بوصفه خاصيةً Prop، حيث سينفّذ المكوِّن الابن المعالج، ويمرّر المعلومات المطلوبة بوصفها معاملًا وسيعدّل المعالج حالة المكوِّن الأب، كما سيتلقّى المكوِّن FilterButton في حالتنا المعالج onclick من المكوِّن الأب، فإذا نقر المستخدِم على أيّ زر ترشيح، فسيستدعي المكونُ الابن المعالجَ onclick ويمرّر المرشّح المحدد على أساس معامل إلى المكوِّن الأب. سنصرّح فقط عن الخاصية onclick التي تُسنَد إلى معالِج وهمي لمنع الأخطاء كما يلي: export let onclick = (clicked) => {} وسنصرّح عن التعليمة التفاعلية $: onclick(filter) لاستدعاء المعالِج onclick كلما جرى تحديث المتغير filter. أولًا، يجب أن يبدو القسم <script> الخاص بالمكوِّن FilterButton كما يلي: <script> export let filter = 'all' export let onclick = (clicked) => {} $: onclick(filter) </script> إذا استدعينا المكوِّن FilterButton ضمن المكوِّن Todos.svelte الآن، فيجب تحديد المعالج، لذا عدّله إلى ما يلي: <FilterButton {filter} onclick={ (clicked) => filter = clicked }/> إذا نقرت على أيّ زر ترشيح، فسنعدّل المتغير filter باستخدام المرشّح الجديد وسيعمل المكوِّن FilterButton مرةً أخرى. طريقة أسهل لربط البيانات ثنائي الاتجاه باستخدام الموجه bind أدركنا في المثال السابق أنّ المكوِّن FilterButton لم يعمل، لأنّ حالة التطبيق تنتقل من المكوِّن الأب إلى المكوِّن الابن من خلال الخاصية filter، ولكنها لا ترجع مرةً أخرى من المكوِّن الابن إلى المكوِّن الأب، لذلك أضفنا الخاصية onclick للسماح للمكوِّن الابن بإرسال قيمة الخاصية filter الجديدة إلى المكوِّن الأب. يعمل التطبيق جيدًا، ولكن يوفر إطار عمل Svelte طريقةً سهلةً ومباشرةً لتحقيق ربط البيانات ثنائي الاتجاه، إذ تتدفق البيانات عادةً من المكوِّن الأب إلى المكوِّن الابن باستخدام الخاصيات، وإذا أردنا أن تتدفق في الاتجاه الآخر من المكوِّن الابن إلى المكوِّن الأب، فيمكننا استخدام الموجّه bind:. سنخبر إطار عمل Svelte باستخدام الموجّه bind أنّ أيّ تغييرات تجرَى على الخاصية filter في المكوِّن FilterButton يجب أن تنتشر إلى المكوِّن الأب Todos، أي أننا سنربط قيمة المتغير filter في المكوِّن الأب بقيمته في المكوِّن الابن. أولًا، عدّل استدعاء المكوِّن FilterButton في Todos.svelte كما يلي: <FilterButton bind:filter={filter} /> يوفِّر إطار عمل Svelte اختصارًا، إذ تعادل التعليمةُ bind:value={value} التعليمةَ bind:value، لذلك يمكنك في المثال السابق كتابة <FilterButton bind:filter /> فقط. ثانيًا، يمكن للمكوِّن الابن الآن تعديل قيمة المتغير filter الخاص بالمكون الأب، لذلك لم نعد بحاجة إلى الخاصية onclick، لذا عدّل القسم <script> الخاص بالمكوِّن FilterButton كما يلي: <script> export let filter = 'all' </script> ثالثًا، جرب تطبيقك مرةً أخرى، وستظل ترى أن المرشّحات تعمل بصورة صحيحة. إنشاء المكون Todo سننشئ الآن المكون Todo لتغليف كل مهمة بما في ذلك مربع الاختيار وشيفرة التعديل لتتمكّن من تعديل مهمة موجودة مسبقًا، وسيتلقى المكوِّن Todo الكائن todo بوصفه خاصيةً، لذا لنصرّح عن الخاصية todo ولننقل الشيفرة البرمجية من المكوِّن Todos، كما سنستبدل حاليًا استدعاء removeTodo باستدعاء alert وسنضيف هذه الوظيفة مرةً أخرى في وقت لاحق. أنشئ ملف مكوِّن جديد components/Todo.svelte، وضَع بعد ذلك المحتويات التالية ضمن هذا الملف: <script> export let todo </script> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={() => todo.completed = !todo.completed} checked={todo.completed} /> <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">{todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={() => alert('not implemented')}> Delete <span class="visually-hidden">{todo.name}</span> </button> </div> </div> يجب الآن استيراد المكوِّن Todo إلى Todos.svelte، لذا انتقل إلى هذا الملف الآن وأضف تعليمة الاستيراد import التالية بعد تعليمة الاستيراد الموجودة مسبقًا: import Todo from './Todo.svelte' يجب بعد ذلك تحديث كتلة {#each} لتضمين المكوِّن <Todo> لكل مهمة بدلًا من الشيفرة المنقولة إلى Todo.svelte، ويجب تمرير كائن todo الحالي إلى المكوِّن بوصفه خاصيةً، لذا عدّل كتلة {#each} ضمن المكوِّن Todos.svelte كما يلي: <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} /> </li> {:else} <li>Nothing to do here!</li> {/each} </ul> تُعرَض قائمة المهام على الصفحة، ويجب أن تعمل مربعات الاختيار (حاول تحديد أو إلغاء تحديد مربعات الاختيار، ثم لاحظ أنّ المرشحات لا تزال تعمل كما هو متوقع)، ولكن لن يُحدَّث عنوان الحالة "x out of y items completed" وفقًا لذلك لأن المكوِّن Todo يتلقى المهام باستخدام الخاصية، لكنه لا يرسل أيّ معلومات إلى المكوِّن الأب، وسنصلح ذلك لاحقًا. مشاركة البيانات بين المكونات: نمط الخاصيات للأسفل Props-down والأحداث للأعلى Events-up يُعَد الموجّه bind واضحًا جدًا ويسمح بمشاركة البيانات بين المكوِّن الأب والمكوِّن الابن، ولكن يمكن أن يكون تتبّع جميع القيم المرتبطة ببعضها بعضًا أمرًا صعبًا عندما ينمو تطبيقك بصورة أكبر وأكثر تعقيدًا، لذا يمكنك استخدام نهج مختلف هو نمط الاتصال "props-down, events-up". يعتمد هذا النمط على المكونات الأبناء التي تتلقى البيانات من آبائها عبر الخاصيات والمكونات الآباء لتحديث حالتها من خلال معالجة الأحداث التي تطلقها المكونات الأبناء، لذا تتدفق الخاصيات للأسفل Flow Down من المكوِّن الأب إلى المكوِّن الابن وتنتشر Bubble Up الأحداث للأعلى من المكوِّن الابن إلى المكوِّن الأب، إذ ينشئ هذا النمط تدفقًا أسهل ثنائي الاتجاه للمعلومات. لنلقِ نظرةً على كيفية إصدار أحداثنا لإعادة تطبيق وظيفة زر "الحذف Delete" المفقودة، إذ يمكن إنشاء أحداث مخصصة من خلال استخدام الأداة createEventDispatcher التي تعيد الدالة dispatch() التي تسمح بإصدار أحداث مخصصة، فإذا أرسلتَ حدثًا، فيجب تمرير اسم الحدث وكائن اختياري به معلومات إضافية تريد تمريرها إلى كل مستمع، كما ستكون هذه البيانات الإضافية متاحةً في الخاصية detail لكائن الحدث. ملاحظة: تشترك الأحداث المخصصة في إطار عمل Svelte بواجهة برمجة التطبيقات نفسها التي تستخدِمها أحداث DOM العادية، كما يمكنك نشر حدث إلى المكوِّن الأب عن طريق تحديد on:event بدونّ أي معالج. سنعدّل المكون Todo لإصدار الحدث remove عبر تمرير المهمة المحذوفة بوصفها معلومات إضافية. أضِف أولًا الأسطر التالية إلى الجزء العلوي من القسم <script> للمكوِّن Todo: import { createEventDispatcher } from 'svelte' const dispatch = createEventDispatcher() عدّل الآن زر "الحذف Delete" في قسم شيفرة HTML بالملف نفسه ليبدو كما يلي: <button type="button" class="btn btn__danger" on:click={() => dispatch('remove', todo)}> Delete <span class="visually-hidden">{todo.name}</span> </button> نصدر الحدث remove من خلال استخدام dispatch('remove', todo) ونمرِّر المهام todo المحذوفة بوصفها بيانات إضافية، إذ سيُستدعى المعالج باستخدام كائن الحدث المتوفر مع البيانات الإضافية المتوفرة في الخاصية event.detail. يجب الآن الاستماع إلى هذا الحدث من داخل الملف Todos.svelte والتصرف وفقًا لذلك، لذا ارجع إلى هذا الملف وعدّل استدعاء المكوِّن <Todo> كما يلي: <Todo {todo} on:remove={e => removeTodo(e.detail)} /> يتلقى معالجنا المعامِل e (كائن الحدث) الذي يحتفظ بالمهام المحذوفة في الخاصية detail. إذا حاولت تجربة تطبيقك مرةً أخرى الآن، فسترى أنّ وظيفة الحذف تعود للعمل، وبذلك نجح حدثنا المخصّص كما توقعنا، كما يرسل مستمع الحدث remove تغيّر البيانات إلى المكوِّن الأب، لذلك سيُحدَّث عنوان الحالة "x out of y items completed" بصورة مناسبة عند حذف المهام. سنهتم الآن بالحدث update بحيث يمكن إعلام المكوِّن الأب بأيّ مهام مُعدَّلة. تحديث المهام لا يزال يتعين علينا تنفيذ الوظيفة للسماح بتعديل المهام الحالية، إذ يجب تضمين وضع التعديل في المكوِّن Todo، كما سنعرض حقل الإدخال <input> عند الدخول في وضع التعديل للسماح بتعديل اسم المهمة الحالي مع زرين لتأكيد التغييرات أو إلغائها. معالجة الأحداث أولًا، سنحتاج متغيرًا واحدًا لتتبّع ما إذا كنا في وضع التعديل أم في وضع آخر لتخزين اسم المهمة المُعدَّلة، لذا أضِف تعريفات المتغيرات التالية في الجزء السفلي من القسم <script> للمكوِّن Todo: let editing = false // تتبّع نمط التعديل let name = todo.name // تخزين اسم المهمة المُعدَّلة يجب أن نقرِّر ما هي الأحداث التي سيصدرها المكوِّن Todo كما يلي: يمكننا إصدار أحداث مختلفة لتبديل الحالة وتعديل الاسم مثل updateTodoStatus و updateTodoName. أو يمكننا اتباع نهج أعم وإصدار حدث update واحد لكلتا العمليتين. سنتخذ النهج الثاني لنتمكن من إظهار طريقة مختلفة، إذ تتمثل ميزة هذا النهج في أنه يمكننا لاحقًا إضافة المزيد من الحقول إلى المهام مع إمكانية معالجة جميع التحديثات باستخدام الحدث نفسه، فلننشئ الدالة update() التي ستتلقى التغييرات وتصدر حدث تحديث مع المهام المُعدَّلة، لذا أضف ما يلي مرةً أخرى إلى الجزء السفلي من القسم <script>: function update(updatedTodo) { todo = { ...todo, ...updatedTodo } // تطبيق تعديلات على المهمة dispatch('update', todo) // إصدار حدث التحديث } استخدمنا صيغة الانتشار Spread Syntax لإعادة المهمة الأصلية مع التعديلات المُطبَّقة عليها. سننشئ بعد ذلك دوالًا مختلفةً للتعامل مع كل إجراء للمستخدِم، إذ يمكن للمستخدِم حفظ التغييرات أو إلغائها عندما تكون المهمة في وضع التعديل، ويمكن للمستخدِم حذف المهمة أو تعديلها أو تبديل حالتها بين الحالة المكتملة والنشطة عندما لا تكون في وضع التعديل، لذا أضف مجموعة الدوال التالية بعد آخر دالة للتعامل مع هذه الإجراءات: function onCancel() { name = todo.name // إعادة المتغير name إلى قيمته الأولية editing = false // والخروج من وضع التعديل } function onSave() { update({ name: name }) // تحديث اسم المهمة editing = false // والخروج من وضع التعديل } function onRemove() { dispatch('remove', todo) // إصدار حدث الحذف } function onEdit() { editing = true // الدخول في وضع التعديل } function onToggle() { update({ completed: !todo.completed}) // تحديث حالة المهمة } تحديث ملف شيفرة HTML يجب الآن تحديث شيفرة HTML الخاصة بالمكون Todo لاستدعاء الدوال السابقة عند اتخاذ الإجراءات المناسبة، إذ يمكنك التعامل مع وضع التعديل من خلال استخدام المتغير editing الذي له قيمة منطقية، فإذا كانت قيمة هذا المتغير true، فيجب أن يُعرَض حقل الإدخال <input> لتعديل اسم المهمة وزرَّي "الإلغاء Cancel" و"الحفظ Save"؛ أما إذا لم تكن في وضع التعديل، فسيُعرَض مربع الاختيار واسم المهمة وأزرار تعديل المهام وحذفها. يمكن تحقيق ذلك من خلال استخدام كتلة if التي تصيّر شيفرة HTML شرطيًا، ولكن ضع في الحسبان أنها لن تظهِر أو تخفي شيفرة HTML بناءً على شرط معيّن، وإنما ستضيف وتزيل عناصر نموذج DOM ديناميكيًا اعتمادًا على هذا الشرط. إذا كانت قيمة المتغير editing هي true مثلًا، فسيعرض إطار عمل Svelte نموذج التحديث؛ أما إذا كانت قيمته false، فسيزيله من نموذج DOM وسيضيف مربع الاختيار، لذا سيكون تعيين قيمة المتغير editing كافيًا لعرض عناصر HTML الصحيحة بفضل خاصية التفاعل في إطار عمل Svelte. ستكون كتلة if كما يلي: <div class="stack-small"> {#if editing} <!-- markup for editing to-do: label, input text, Cancel and Save Button --> {:else} <!-- markup for displaying to-do: checkbox, label, Edit and Delete Button --> {/if} </div> يمثِّل الجزء {:else} أو النصف السفلي من كتلة if قسم عدم التعديل، كما سيكون مشابهًا جدًا للقسم الموجود في المكوِّن Todos، ولكن الاختلاف الوحيد بينهما هو أننا نستدعي الدوال onToggle() و onEdit() و onRemove() اعتمادًا على إجراء المستخدِم. {:else} <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={onToggle} checked={todo.completed} > <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn" on:click={onEdit}> Edit<span class="visually-hidden"> {todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={onRemove}> Delete<span class="visually-hidden"> {todo.name}</span> </button> </div> {/if} </div> تجدر الإشارة إلى ما يلي: ننفّذ الدالة onEdit() التي تضبط المتغير editing على القيمة true عندما يضغط المستخدِم على زر "التعديل Edit". نستدعي الدالة onToggle() التي تنفذ الدالة update() من خلال تمرير كائن مع قيمة completed الجديدة بوصفه معامِلًا عندما ينقر المستخدِم على مربع الاختيار. تصدِر الدالة update() الحدث update من خلال تمرير نسخة من المهمة الأصلية مع التغييرات المطبَّقة بوصفها معلومات إضافية. أخيرًا، تصدِر الدالة onRemove() الحدث remove من خلال تمرير المهمة todo المراد حذفها بوصفها بيانات إضافية. ستحتوي واجهة المستخدِم الخاصة بالتعديل -أي النصف العلوي- على حقل الإدخال <input> وزرين لإلغاء التغييرات أو حفظها كما يلي: <div class="stack-small"> {#if editing} <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}> <div class="form-group"> <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" on:click={onCancel} type="button"> Cancel<span class="visually-hidden">renaming {todo.name}</span> </button> <button class="btn btn__primary todo-edit" type="submit" disabled={!name}> Save<span class="visually-hidden">new name for {todo.name}</span> </button> </div> </form> {:else} [...] إذا ضغط المستخدِم على زر "التعديل Edit"، فسيُضبَط المتغير editing على القيمة true وسيزيل إطار عمل Svelte شيفرة HTML في الجزء {:else} من نموذج DOM وسيستبدله بشيفرة HTML الموجودة في القسم {#if}. ستكون الخاصية value الخاصة بالعنصر <input> مرتبطةً بالمتغير name، وستستدعي أزرار إلغاء التغييرات وحفظها الدالتين onCancel() و onSave() على التوالي كما يلي، وقد أضفنا هاتين الدالتين سابقًا: إذا استُدعيت الدالة onCancel()، فستُعاد الخاصية name إلى قيمتها الأصلية عند تمريرها بوصفها خاصيةً Prop وسنخرج من وضع التعديل عن طريق ضبط المتغير editing على القيمة false. إذا استُدعيت الدالة onSave()، فسنشغّل الدالة update() من خلال تمرير الخاصية name المُعدَّلة، وسنخرج من وضع التعديل. كما نعطّل زر "الحفظ Save" عندما يكون حقل الإدخال <input> فارغًا باستخدام السمة disabled={!name}، كما نسمح للمستخدِم بإلغاء التعديل باستخدام المفتاح Escape كما يلي: on:keydown={e => e.key === 'Escape' && onCancel()}. كما نستخدِم الخاصية todo.id لإنشاء معرّفات فريدة لعناصر التحكم بحقل الإدخال والتسميات Labels الجديدة. تبدو شيفرة HTML المعدَّلة الكاملة للمكون Todo كما يلي: <div class="stack-small"> {#if editing} <!-- markup for editing todo: label, input text, Cancel and Save Button --> <form on:submit|preventDefault={onSave} class="stack-small" on:keydown={e => e.key === 'Escape' && onCancel()}> <div class="form-group"> <label for="todo-{todo.id}" class="todo-label">New name for '{todo.name}'</label> <input bind:value={name} type="text" id="todo-{todo.id}" autoComplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" on:click={onCancel} type="button"> Cancel<span class="visually-hidden">renaming {todo.name}</span> </button> <button class="btn btn__primary todo-edit" type="submit" disabled={!name}> Save<span class="visually-hidden">new name for {todo.name}</span> </button> </div> </form> {:else} <!-- markup for displaying todo: checkbox, label, Edit and Delete Button --> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" on:click={onToggle} checked={todo.completed} > <label for="todo-{todo.id}" class="todo-label">{todo.name}</label> </div> <div class="btn-group"> <button type="button" class="btn" on:click={onEdit}> Edit<span class="visually-hidden"> {todo.name}</span> </button> <button type="button" class="btn btn__danger" on:click={onRemove}> Delete<span class="visually-hidden"> {todo.name}</span> </button> </div> {/if} </div> ملاحظة: يمكننا أيضًا تقسيم هذا المكوِّن إلى مكوِّنين مختلفين أحدهما لتعديل المهام والآخر لعرضها، إذ يتلخص الأمر في مدى شعورك بالراحة في التعامل مع هذا المستوى من التعقيد باستخدام مكوِّن واحد، لذا يجب التفكير فيما إذا كان تقسيمه سيمكّنك أكثر من إعادة استخدام هذا المكوِّن في سياق مختلف. يجب معالجة الحدث update من المكوِّن Todos لتشغيل وظيفة التحديث، لذا أضف معالِج الأحداث التالي في القسم <script>: function updateTodo(todo) { const i = todos.findIndex(t => t.id === todo.id) todos[i] = { ...todos[i], ...todo } } نجد المهمة todo باستخدام معرِّفها id في مصفوفة المهام todos ونحدّث محتواها باستخدام صيغة الانتشار، وقد كان بإمكاننا أيضًا استخدام todos[i] = todo في هذه الحالة، ولكن هذا التطبيق أفضل، مما يسمح للمكوِّن Todo بإعادة الأجزاء المُعدَّلة فقط من المهام. يجب بعد ذلك الاستماع إلى الحدث update في استدعاء المكون <Todo>، وتشغيل الدالة updateTodo() عند حدوث ذلك لتغيير المتغير name والحالة completed، لذا عدّل استدعاء المكوِّن <Todo> كما يلي: {#each filterTodos(filter, todos) as todo (todo.id)} <li class="todo"> <Todo {todo} on:update={e => updateTodo(e.detail)} on:remove={e => removeTodo(e.detail)} /> </li> جرب تطبيقك مرةً أخرى وسترى أنه يمكنك حذف وإضافة وتعديل وإلغاء تعديل وتبديل حالة اكتمال المهام، وسيُعدَّل عنوان الحالة "x out of y items completed" بطريقة مناسبة عند اكتمال المهام. يُعَدّ تطبيق نمط "props-down, events-up" في إطار عمل Svelte سهلًا، ولكن يمكن أن يكون الموجّه bind اختيارًا جيدًا للمكونات البسيطة، وسيتيح لك إطار Svelte الاختيار. ملاحظة: يوفِّر إطار Svelte آليات أكثر تقدمًا لمشاركة المعلومات بين المكونات، وهي واجهة Context API والمخازن Stores، إذ توفِّر Context API آليةً للمكونات وأحفادها للتواصل مع بعضها البعض دون تمرير البيانات والدوال بوصفها خاصيات، أو إرسال الكثير من الأحداث، في حين تتيح المخازن Stores مشاركة البيانات التفاعلية بين المكونات غير المرتبطة بطريقة هرمية. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/05-advanced-concepts أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/05-advanced-concepts تذكر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة أضفنا جميع الوظائف المطلوبة لتطبيقنا، إذ يمكننا عرض المهام وإضافتها وتعديلها وحذفها وتمييزها على أنها مكتملة وترشيحها حسب الحالة، كما غطينا في هذا المقال المواضيع التالية: استخراج وظائف مكوِّن جديد. تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام معالج يُستقبَل بوصفه خاصيةً. تمرير المعلومات من المكوِّن الابن إلى المكوِّن الأب باستخدام الموجّه bind. عرض كتل شيفرة HTML المشروطة باستخدام كتلة if. تطبيق نمط الاتصال "props-down, events-up". إنشاء الأحداث المخصصة والاستماع إليها. سنواصل في المقال التالي من جزئية svelte من هذه السلسلة تقسيم تطبيقنا إلى مكونات ونتعرف على بعض التقنيات المتقدمة للعمل مع نموذج DOM. ترجمة -وبتصرُّف- للمقال Componentizing our Svelte app. اقرأ أيضًا التعامل مع المتغيرات والخاصيات في إطار عمل Svelte إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب
-
يتحدّث الكاتب في هذا المقال عن تجربته الشخصية في الانتقال من الأعمال الخدمية إلى العمل مع المنتجات وإطلاق منتج جديد. لماذا أردت العمل مع المنتجات؟ تلقيتُ -يقول الكاتب- عرضًا للانضمام إلى فريق سوق Tradalaxy في الشهر العاشر من سنة 2018، وعرفتُ مباشرةً أنه المكان المناسب لي، إذ لدي خبرة كبيرة في إطلاق أنواع مختلفة من الأسواق وهو شيء وجدته محفزًا وجذابًا دائمًا. كانت فرصةً أخرى للعودة إلى العمل مع إدارة المنتجات التي اشتقتُ إليها نوعًا ما، حيث كان آخر أعمالي فيها في سنة 2014 عندما عملت على تطبيق منصة إنترنت الأشياء للسيارات بما في ذلك خدمة الويب وتطبيقات الهاتف المحمول والعتاد، وكانت أول تجربة لي في العمل مع شركة ناشئة. شغلت قبل الانضمام إلى شركة Tradalaxy مناصبًا على مستوى المديرين التنفيذيين في شركات خدمات وعملت مع فرق من مختلف المتخصصين من التنفيذيين إلى مديري المبيعات والمصممين والمطورين. كان عملائي من جميع أنحاء العالم وساعدتهم على جعل أعمالهم مربحة مثل المشاريع التقنية المالية وإنترنت العملاء والأسواق وما إلى ذلك، وكان كل شيء على ما يرام لولا فكرة واحدة، وهي أنني لا أنتمي إلى هذا العالم. حصلت بعدها على فرصة للمساعدة في إنشاء المنتجات، حيث كانت لدي خبرة في الاستشارات وتطوير مشاريع العملاء، وأدركت أنني أفكر بصوت عالٍ، فقد أردت حقًا العودة لكوني مبدعًا أو مبتكرًا مرةً أخرى بدلًا من أكون منفّذًا فقط، وهنا بدت العودة إلى إدارة المنتجات خيارًا جيدًا، إذ يجب أن تستغل فرصتك في حال توفرها. كذلك، أتاح لي الانتقال إلى العمل مع أحد المنتجات الابتعاد عن الحاجة المستمرة للمساومة الموجودة في الشركات الخدمية، إذ لن يسمح النقص في الوقت بالانغماس في التفاصيل بصورة كاملة. كل مشروع للعميل لديه نقطة بداية ونهاية، فهو مثلث متساوي الأضلاع كما تنص معايير PMI، حيث تمثل الأضلاع الثلاثة لهذا المثلث الميزانية والوقت ونطاق العمل. تعزّز المواعيد النهائية تلك الفكرة، حيث تُعَد المواعيد النهائية عنصرًا ضروريًا في أي نوع من أنواع العمل، فالمشاريع التي بدون مواعيد نهائية عديمة الجدوى، إذ لن تُنجَز بدون هذه القيود الضرورية، لكن المواعيد النهائية تخلق مواقفًا تكون فيها الحلول المُقدَّمة (سواءً كانت متعلقةً بالتصميم أو البناء أو المشاكل التقنية) عبارةً عن تسويات. لا تُعَد هذه الحلول شاملةً كما تحب أن تكون، إذ تُعَد القدرة على تتبّع ما إذا كانت هذه الحلول تساعد الأعمال أم لا الاستثناء وليست القاعدة، لأن سير العمل الطبيعي يتعلق أكثر بالوفاء بالتزامك تجاه العميل لإنشاء منتج X مثلًا في الإطار الزمني Y ضمن نطاق Z. يوجد نموذج فريق مُخصَّص في الأعمال الخدمية، والذي (وإن كان جزئيًا وليس دائمًا) يسمح للفريق بالعمل من منظور طويل الأمد ومراقبة فعالية الحلول المُقدَّمة، فقد أكسبني العمل مع المنتجات القدرة الدائمة على مراقبة فعالية الحلول، وهو ما أقدره حقًا. من جهة أخرى، جذبتني احتمالية العمل مع مؤشرات الوحدات الاقتصادية التي لا يكشف عنها العملاء في الأعمال الخدمية، حيث كانت لدي رغبة في التعمق في احتياجات المستخدمين النهائيين والعمل مع مهامهم وتحدياتهم وتوقعاتهم والتحدث الفعلي معهم والتعاطف قدر الإمكان لفهم ما يزعجهم والسبب في ذلك؛ فقد رغبتُ في تجربة طرق إطلاق منتج لحساب النقطة التي يصل عندها إلى نقطة التعادل بلا ربح أو خسارة مثل نماذج أرباح التصميم وخيارات خارطة الطريق الافتراضية. سمحت لي هذه الوظيفة بالعمل عن قرب مع الفريق بكل أدواره المختلفة، وصقلتُ هذه المهارة في مناصبي الإدارية السابقة وأردتُ تطويرها وتطبيقها بصورة أكبر، فنصيب الأسد من العمل على المنتجات يأخذه التواصل الذي له الأولوية الأكبر. ويتطلب التواصل بناء وتعديل العمليات والإجراءات المثلى للأدوار المختلفة في الفريق، وقد أفادتني تجربتي السابقة في عملي كوكيل. يتضمن دوري في شركة Tradalaxy تطوير المنتجات وإدارتها. يُعَد تعريفي لمدير المنتجات مشابهًا لتعريف شركة Intercom الذي ينص على أن دور مدير المنتجات هو مزيج من تجربة المستخدم (تجربة التفاعل بالمعنى الواسع) والحلول التقنية والأعمال. يعرف مدير المنتجات ما المشكلة ولمَن ولماذا صُمِّم المنتج لحلها، ويجب أن يفهم كيفية قياس الأداء وأين سيكون المنتج بعد فترة من الوقت، ولا أتفق مع فكرة أن مدير المنتجات يُعَد مديرًا تنفيذيًا صغيرًا. لقد جرى إضفاء الطابع العاطفي على مديري المنتجات بطريقة مفرطة في وسائل الإعلام، وهناك نقص معين في فهم المسؤولية الواقعة على عاتق الرئيس التنفيذي بما في ذلك الجانب المالي. يُعَد مدير المنتجات -بالنسبة لي- وسيلة اتصال رئيسية بين المنطقتين الداخلية (مع الفريق) والخارجية (السوق وأصحاب المصلحة). ويكون مدير المنتجات مدافعًا عمّا يريده المستخدم، حيث يربط هذه المتطلبات مع الفريق ويقدّم حلولًا فعالة وأنيقة. مدير المنتجات هو اختصاص رائع يجمع بين العديد من الكفاءات في وقت واحد، حيث تتّحد مهاراته من خلال التواصل والقدرة على توصيل رسالة المنتج. العلامة التجارية ونموذجها يُعَد المنتج الجيد منتجًا له علامة تجارية رائعة، ويُرجَّح أن يشتري الناس المنتجات التي يحبونها ويفهمونها أو يرتبطون بها. لكن هناك رأي مثير للجدل إلى حدٍ ما مفاده أن المنتج ليس بأهمية العلامة التجارية، لذلك كانت إحدى مهامي الأولى في شركة Tradalaxy هي تطوير نموذج للعلامة التجارية. يجب أولًا فهم مَن نحن ومَن هو جمهورنا وكيفية التواصل، وعندها نطور هويتنا البصرية ونطبّقها من خلال قنوات محددة. لقد تضمّن تطوير نموذج العلامة التجارية التواصلَ مع الشركات المحلية لفهم احتياجاتهم وتحليل منافسينا، وإن كان ذلك بطريقة غير مباشرة. وقد اكتشفنا من خلال هذه العملية أن أفضل وصف للعلامة التجارية لشركة Tradalaxy هو أنها دليل في بحر التجارة الدولية والصادرات والقوانين، لذا أصبح النجم الذي يرشد البحارة هو الرمز والعلامة البصرية الرئيسية للشركة. أثبت هذا النموذج للعلامة التجارية أنه مثالي نظرًا للاهتمامات التجارية التي حددناها لجمهورنا المستهدف. تتمثل إحدى الاحتياجات المهيمنة لشركات التصدير في تلقي معلومات معقدة وغير واضحة حول التجارة والاقتصاد والظواهر المرتبطة بها بعبارات بسيطة وسهلة الفهم. يعجبني مفهوم البساطة الذي يحدده جيفري كلوغر Jeffrey Kluger: "شرح الأشياء المعقدة بلغة بسيطة"، وتتمثل مهمة منصة Tradalaxy في جعل التجارة الدولية أسهل. قيمة تجربة المستخدم UX لإدارة المنتجات تسمح أدوات تجربة المستخدم UX بالإدارة الفعالة لعرض قيمة المنتج أو جوهره، حيث يمكنك استخدام تجربة المستخدم لمساعدة المستخدم النهائي على رؤية قيمة المنتج وفهمه وتوظيف المنتج لإنجاز واجباته والاستمرار في استخدامه ومشاركته مع الآخرين، ويرتبط ذلك بالبحث عن جمهورك من العملاء. يُعَد تطوير العملاء والمقابلات المتعمقة ومجموعات التركيز بعض الأساليب التي سنستخدمها، ويمكن أن تنجح مكالمة زووم Zoom قصيرة واستبيان موجز واجتماع ودي على فنجان من القهوة، بالرغم من أن هذه الأساليب يمكن ألّا تبدو أنيقةً (أو لا تحتوي على مصطلح رائع يثني عليه الآخرون على الإنترنت). لقد استمتعتُ بتطبيق هذه الأساليب لأنها تعطينا فهمًا أعمق لاحتياجات عملائنا، إذ يأتي سؤال لمَن هذا المنتج في المرتبة الأولى، ثم تليه كيفية إنشائه. وتتعلق مسألة الكيفية بملاءمة المنتج ووضوحه، إذ لا يمكن أن يكون المنتج المربك مناسبًا للاستخدام المنتظم. أعتقد أن الشركات التي تبخل على تجربة المستخدم ستخسر أمام الشركات التي تعطي الأولوية لتجربة المستخدم. تتعلق تجربة المستخدم بإعطاء نقطة تركيز عاطفية في 0.02 ثانية أثناء تفكير المستخدم في منتجك، فالفوز بمعركة جذب الانتباه وجعل المنتج بديهيًا وجذابًا هو طموح أداة تجربة المستخدم، كما تتمحور تجربة المستخدم حول راحة المستخدم وجلب عمليات الشراء المتكررة والاحتفاظ بالعملاء أو معايير AARRR الأخرى، وتتعلق بتجميع مجموعة من المكونات من منظور الأعمال، مثل القوائم المنسدلة وخطوات الإعداد ومعلومات صفحة المنتج ضمن تنسيق واضح وموجز. يجب أن تبسّط تجربة المستخدم الأشياء، فسبب وجود البساطة هو إحساس المستخدم بالراحة عند التفاعل مع منتجك من خلال واجهته وقائمته البريدية والمحتوى النصي، حيث يتعلق العمل على واجهة المنتج بتوفير تجربة مستخدم مريحة تؤمن الأمان والراحة ويمكن فهمها والتنبؤ بها، وهذه هي إحدى القواعد الأولى للخدمات والمبيعات التي علّمتُها للفرق التي عملت معها. من جهة أخرى، استثمرنا بصورة كبيرة في تطوير استراتيجية اتصال لتشكيل صوتنا الذي يمثلنا بالإضافة إلى نموذج علامتنا التجارية وهويتنا المتناسقة والمميزة وتطوير عملائنا مع أبحاث السوق، فقد أنشأنا صورًا لعملائنا والتي بدورها شكلت الأساس لتصميم الواجهة وقوائم الاختبار، ثم حددنا الأشخاص الذين هم عملاؤنا فعليًا، ولكننا لا نعرفهم بعد. كذلك، جمّعنا الكلمات الرئيسية الدلالية لبناء دليل للمحتوى والعلاقات العامة PR والتسويق عبر شبكات التواصل الاجتماعي SMM، ولولا هذا النهج الشمولي، لكانت النتائج مجردة وغير واقعية. كلما زاد البحث، زادت أهمية نتائجك، ولكن الأبحاث المختصة تتطلب جهودًا متفانيةً عوضًا عن الميزانيات الضخمة، إذ لا تُعَد متابعة الاتجاهات الرائجة أو إنفاق كميات هائلة من الموارد أمرًا ضروريًا. لا تقتصر مسؤولياتي بصفتي مدير منتجات على المنتج والعلامة التجارية وتجربة المستخدم، فلدي مسؤوليات أيضًا في تصميم وتطبيق سير العمل الأمثل. أعتقد أن الخبرة الإدارية تمثل رصيدًا ضخمًا في تطوير المنتج وعمليات البناء في فرق هذا المنتج، ولدي هذه الميزة لحسن حظي، فقد كانت خبرتي السابقة في الأعمال الخدمية مفيدة جدًا. كذلك، يتعلق المنتج بالخدمة وبالقيمة المضافة التي يوفرها للمستخدم. وتعني القيمة الصفرية أن المنتج يفتقد شيئًا ما بصورة واضحة، لذا أشعر بالراحة في توجيه المنتج نحو حالة موجهة نحو تحقيق القيمة، ولدي حرية اتخاذ القرارات بناءً على رؤيتي التي تعكس ما يحتاجه المستخدمون، وأنا أتحمّل مسؤوليتها. أخيرًا، أود أن أكرر أهمية الاستثمار في البحث، فبدونه لا يمكنك بناء منتج يحتاجه العملاء فعليًا، ويمثل ذلك العلامة الرئيسية للنجاح. اعتمِد على المحترفين في حل المشاكل التي ليس لديك خبرة فيها، ولا بأس في ألّا تعرف شيئًا ما، ولكن يجب أن يعرف كل محترف أين ينتهي مجال اختصاصه. لا تتجاوز نطاق خبرتك بالتوصيات والقرارات، ولكن اعمل جيدًا ضمنها. ترجمة -وبتصرُّف- للمقال Maker vs Doer: How I Returned To Product Management) لصاحبه Taras Zherebetskyy. اقرأ أيضًا كيف تدخل إلى مجال إدارة المنتجات وتنجح فيه؟ كيفية الدخول في مجال إدارة المنتجات وإتقانه دليل إرشادي للدخول في مجال إدارة المنتجات مجموعة مصادر مهمة تساعد على دخول مجال إدارة المنتج
-
يمكننا الآن البدء في تطوير الميزات المطلوبة لتطبيق قائمة المهام في إطار عمل Svelte بعد أن أصبح التوصيف والتنسيق جاهزًا، إذ سنستخدِم في هذا المقال المتغيرات والخاصيات Props لجعل تطبيقنا ديناميكيًا، مما يسمح بإضافة وحذف المهام ووضع علامة على المهام المكتملة وترشيحها حسب الحالة. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: تعلّم وتطبيق بعض مفاهيم Svelte الأساسية مثل إنشاء المكونات وتمرير البيانات باستخدام الخاصيات وتصيير Render تعابير جافاسكربت في شيفرة HTML وتعديل حالة المكونات وتكرارها عبر القوائم. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/03-adding-dynamic-behavior أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، وإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام الأداة REPL من هنا. التعامل مع المهام يعرِض المكوِّن Todos.svelte حاليًا شيفرة HTML ثابتةً لا تتغير، إذًا لنبدأ في جعله أكثر ديناميكيةً، إذ سنأخذ معلومات المهام من شيفرة HTML ونخزنها في المصفوفة todos، كما سننشئ متغيرين لتتبّع العدد الإجمالي للمهام والمهام المكتملة، إذ ستُمثَّل حالة المكوِّن من خلال هذه المتغيرات الثلاثة ذات المستوى الأعلى. أولًا، أنشئ قسم <script> قبل المكوِّن src/components/Todos.svelte وضَع فيه المحتوى التالي: <script> let todos = [ { id: 1, name: "Create a Svelte starter app", completed: true }, { id: 2, name: "Create your first component", completed: true }, { id: 3, name: "Complete the rest of the tutorial", completed: false } ]; let totalTodos = todos.length; let completedTodos = todos.filter((todo) => todo.completed).length; </script> لنبدأ بعد ذلك بإظهار رسالة الحالة، لذا ابحث عن العنوان <h2> الذي له المعرِّف id بالقيمة list-heading واستبدل العدد الثابت للمهام النشطة والمكتملة بتعابير ديناميكية كما يلي: <h2 id="list-heading">{completedTodos} out of {totalTodos} items completed</h2> انتقل إلى التطبيق وسترى الرسالة "2 out of 3 items completed" كما كانت سابقًا، ولكن تأتي المعلومات هذه المرة من المصفوفة todos. أخيرًا، يمكن إثبات ذلك من خلال الانتقال إلى تلك المصفوفة ثم محاولة تغيير بعض قيم الخاصية المكتملة completed لكائن المهمة، ويمكنك إضافة كائن مهمة جديد أيضًا، ولاحظ كيف تُحدَّث الأعداد في الرسالة بطريقة مناسبة. إنشاء مهام من بيانات يدخلها المستخدم تُعَدّ عناصر المهام المعروضة ثابتةً حاليًا، ونريد تكرار كل عنصر في المصفوفة todos وتصيير Render شيفرة HTML لكل مهمة. ليس لدى لغة HTML طريقة للتعبير عن المنطق مثل التعابير الشرطية والحلقات، ولكن إطار عمل Svelte يمكنه ذلك من خلال استخدام الموجّه {#each...} للتكرار عبر المصفوفة todos. يتضمن المعامِل الثاني -إذا كان موجودًا- فهرس العنصر الحالي، كما يمكن توفير تعبير مفتاحي يحدد كل عنصر بطريقة فريدة، وسيستخدِمه إطار Svelte لمعرفة الاختلاف في القائمة عند تغيير البيانات بدلًا من إضافة العناصر أو إزالتها في النهاية، ويُعَدّ تحديد عنصر مفتاحي دائمًا من الممارسات الجيدة، كما يمكن توفير كتلة :else التي ستُصيَّر عندما تكون القائمة فارغةً. أولًا، استبدل العنصر <ul> الحالي بالإصدار المبسط التالي لتفهم كيفية العمل: <ul> {#each todos as todo, index (todo.id)} <li> <input type="checkbox" checked={todo.completed}/> {index}. {todo.name} (id: {todo.id}) </li> {:else} Nothing to do here! {/each} </ul> ثانيًا، ارجع إلى التطبيق وسترى شيئًا يشبه ما يلي: رأينا الآن أنّّ كل شيء يعمل جيدًا، فلننشئ عنصر مهمة مكتملة مع كل حلقة للموجّه {#each}، ونضمّن فيها المعلومات من المصفوفة todos مثل id وname وcompleted واستبدل كتلة <ul> الحالية بما يلي: <!-- Todos --> <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each todos as todo (todo.id)} <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-{todo.id}" checked={todo.completed}/> <label for="todo-{todo.id}" class="todo-label"> {todo.name} </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">{todo.name}</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">{todo.name}</span> </button> </div> </div> </li> {:else} <li>Nothing to do here!</li> {/each} </ul> لاحظ كيفية استخدام الأقواس المعقوصة لتضمين تعابير جافاسكربت ضمن سمات HTML كما فعلنا مع السمتين checked و id لمربع الاختيار. حوّلنا بذلك شيفرة HTML الثابتة إلى قالب ديناميكي جاهز لعرض المهام من حالة المكوِّن. التعامل مع الخاصيات لا يُعَدّ المكوِّن Todos مفيدًا جدًا بوجود قائمة مهام ثابتة، إذ يمكن تحويل المكون إلى محرّر مهام للأغراض العامة من خلال السماح لأب هذا المكون بالمرور على قائمة المهام لتعديلها، مما يسمح بحفظها في خدمة ويب أو في التخزين المحلي واستعادتها لاحقًا لتحديثها، لذلك لنحوّل المصفوفة إلى خاصية prop. أولًا، استبدل كتلة let todos = ... الموجودة مسبقًا في Todos.svelte بالتعليمة التالية: export let todos = [] يمكن أن يبدو هذا غريبًا بعض الشيء في البداية، فهذه ليست الطريقة التي تعمل بها تعليمة export في وحدات جافاسكربت، وإنما هي الطريقة التي يوسّع بها إطار عمل Svelte شيفرة جافاسكربت من خلال استخدام صيغة صالحة لهدف جديد. يستخدِم إطار عمل Svelte في حالتنا الكلمة export لتمييز التصريح عن متغير بوصفه خاصية Property أو Prop، مما يعني أنه يصبح في متناول مستخدِمي المكوِّن، كما يمكنك تحديد قيمة أولية افتراضية للخاصية التي تُستخدَم إذا لم يحدد مستخدِم المكون الخاصية الخاصة بالمكوِّن أو إذا كانت قيمتها الأولية غير محدَّدة عند إنشاء نسخة من المكوِّن، لذا نخبر إطار Svelte من خلال التعليمة export let todos = [] أنّ المكوِّن Todos.svelte سيقبل السمة todos والتي ستُهيَّأ إلى مصفوفة فارغة عند حذفها. ثانيًا، ألقِ نظرةً على التطبيق وسترى الرسالة "Nothing to do here!" لأننا لا نمرّر حاليًا أيّ قيمة إليه من المكوِّن App.svelte، لذلك سيستخدِم القيمة الافتراضية. ثالثًا، لننقل مصفوفة مهامنا todos إلى المكوِّن App.svelte ولنمرّرها إلى المكوِّن Todos.svelte بوصفها خاصيةً، لذا عدِّل المكوِّن src/App.svelte كما يلي: <script> import Todos from "./components/Todos.svelte"; let todos = [ { id: 1, name: "Create a Svelte starter app", completed: true }, { id: 2, name: "Create your first component", completed: true }, { id: 3, name: "Complete the rest of the tutorial", completed: false } ]; </script> <Todos todos={todos} /> أخيرًا، يسمح إطار عمل Svelte بتحديد المتغير بوصفه اختصارًا عندما يكون للسمة والمتغير الاسم نفسه، ويمكننا بذلك إعادة كتابة السطر الأخير كما يلي: <Todos {todos} /> يجب أن تُصيَّر المهام كما كانت سابقًا باستثناء أننا نمرّرها الآن من المكوِّن App.svelte. تبديل وإزالة المهام سنضيف الآن بعض الوظائف لتبديل حالة المهمة، إذ يحتوي إطار Svelte على الموجّه on:eventname للاستماع إلى أحداث DOM، لذا أضِف معالجًا إلى الحدث on:click الخاص بمربع الاختيار لتبديل القيمة المكتملة. أولًا، عدّل العنصر <input type="checkbox"> في المكوِّن src/components/Todos.svelte كما يلي: <input type="checkbox" id="todo-{todo.id}" on:click={() => todo.completed = !todo.completed} checked={todo.completed} /> ثانيًا، سنضيف دالة لإزالة مهمة من المصفوفة todos، لذا أضف الدالة removeTodo() في الجزء السفلي من القسم <script> في المكوِّن Todos.svelte كما يلي: function removeTodo(todo) { todos = todos.filter((t) => t.id !== todo.id) } سنستدعي بعد ذلك هذه الدالة باستخدام زر "الحذف Delete"، لذا عدّلها باستخدام الحدث click كما يلي: <button type="button" class="btn btn__danger" on:click={() => removeTodo(todo)} > Delete <span class="visually-hidden">{todo.name}</span> </button> يُعَدّ تمرير نتيجة تنفيذ دالة بوصفها معالجًا بدلًا من تمرير الدالة من الأخطاء الشائعة جدًا في المعالجات في إطار Svelte، فإذا حدّدت on:click={removeTodo(todo)} مثلًا، فستُنفَّذ الدالة removeTodo(todo) وستُمرَّر النتيجة بوصفها معالِجًا، لذا يجب تحديد on:click={() => removeTodo(todo)} على أساس معالج، فإذا لم تأخذ الدالة removeTodo() أيّ معامِل، فيمكنك استخدام on:event={removeTodo}، ولكن لا يمكنك استخدام on:event={removeTodo()}، كما أنّ ليس هذا الشكل صيغةً خاصةً من Svelte، وإنما استخدمنا دوال جافاسكربت السهمية Arrow Functions العادية. يمكننا الآن حذف المهام، إذ تُزال المهام ذات الصلة من المصفوفة todos عند الضغط على زر حذف عنصر المهمة، وتُحدَّث واجهة المستخدِم لعدم إظهاره لاحقًا، كما يمكننا الآن تحديد مربعات الاختيار، وستُحدَّث الحالة المكتملة للمهام ذات الصلة في المصفوفة todos، لكن لا يُحدَّث العنوان "x out of y items completed". المهام التفاعلية يعرِف إطار Svelte كيفية تحديث واجهة المستخدِم في كل مرة تُعدَّل فيها قيمة متغير المستوى الأعلى للمكوِّن، حيث تُعدَّل في تطبيقنا قيمة المصفوفة todos مباشرةً في كل مرة تُبدَّل أو تُحذَف فيها المهام المطلوبة، وبالتالي سيحدّث إطار Svelte نموذج DOM تلقائيًا. لا ينطبق الأمر نفسه على المتغيرين totalTodos وcompletedTodos، إذ يُسنَد إليهما قيمة عند إنشاء نسخة من المكوِّن وينفَّذ السكربت في الشيفرة التالية، ولكن لا تُعدَّل قيمتهما بعد ذلك: let totalTodos = todos.length let completedTodos = todos.filter((todo) => todo.completed).length يمكننا إعادة حسابهما بعد تبديل المهام وإزالتها، ولكن هناك طريقة أسهل لذلك، حيث نخبر إطار Svelte بأننا نريد أن يكون المتغيران totalTodos و completedTodos تفاعليين من خلال جعلهما مسبوقين بالرمز :$، إذ سينشئ إطار Svelte الشيفرة لتحديثهما تلقائيًا كلما تغيرت البيانات التي يعتمدان عليها. ملاحظة: يستخدِم إطار Svelte صيغة تعليمة تسمية جافاسكربت :$ لتمييز التعليمات التفاعلية مثل الكلمة export المستخدَمة للتصريح عن الخاصيات، إذ يُعَد هذا المثال مثالًا آخرًا يستفيد فيه إطار Svelte من صيغة جافاسكربت صالحة مع إعطائها هدفًا جديدًا، وهو في هذه الحالة "إعادة تشغيل هذه الشيفرة كلما تغيرت أيّ من القيم المشار إليها". عدِّل تعريف المتغيرين totalTodos وcompletedTodos ضمن الملف src/components/Todos.svelte لتبدو كما يلي: $: totalTodos = todos.length $: completedTodos = todos.filter((todo) => todo.completed).length إذا فحصت تطبيقك الآن، فسترى تحديث أرقام العناوين عند اكتمال المهام أو حذفها. يحلّل مصرِّف Svelte الشيفرة لإنشاء شجرة اعتماديات، ثم ينشئ شيفرة جافاسكربت لإعادة تقييم كل تعليمة تفاعلية كلما حُدِّثت إحدى اعتمادياتها، كما تُطبَّق التفاعلية في Svelte بطريقة خفيفة الوزن وفعالة دون استخدام المستمعِين Listeners أو التوابع الجالبة Getters أو الضابطة Setters أو أيّ آلية معقدة أخرى. إضافة مهام جديدة يجب الآن إضافة بعض الوظائف لإضافة مهام جديدة. أولًا، سننشئ متغيرًا للاحتفاظ بنص المهام الجديدة، لذا أضف التصريح التالي إلى القسم <script> في الملف Todos.svelte: let newTodoName = '' سنستخدِم الآن هذه القيمة في العنصر <input> لإضافة مهام جديدة، وسنحتاج ربط المتغير newTodoName بدخل todo-0، بحيث تبقى قيمة المتغير newTodoName متزامنةً مع الخاصية value الخاصة بالعنصر <input> كما يلي: <input value={newTodoName} on:keydown={(e) => newTodoName = e.target.value} /> كلما تغيرت قيمة المتغير newTodoName، فسينتقل هذا التغيير إلى السمة value الخاصة بحقل الإدخال، وكلما ضُغِط على مفتاح في حقل الإدخال، فسنحدّث محتويات المتغير newTodoName، إذ يُعَدّ ذلك تطبيقًا يدويًا لربط البيانات ثنائي الاتجاه لحقل الإدخال، لكننا لسنا بحاجة لهذه الآلية، إذ يوفِّر إطار Svelte طريقةً أسهل لربط أيّ خاصية بمتغير باستخدام الموجّه bind:property كما يلي: <input bind:value={newTodoName} /> إذًا لنعدّل حقل الإدخال todo-0 كما يلي: <input bind:value={newTodoName} type="text" id="todo-0" autocomplete="off" class="input input__lg" /> يمكن اختبار نجاح هذه الطريقة من خلال إضافة تعليمة تفاعلية لتسجيل محتويات المتغير newTodoName، لذا أضف مقتطف الشيفرة التالي في نهاية القسم <script>: $: console.log('newTodoName: ', newTodoName) ملاحظة: لا تقتصر التعليمات التفاعلية على التصريح عن المتغيرات، إذ يمكنك وضع أيّ تعليمة جافاسكربت بعد الرمز :$. ارجع الآن إلى المضيف المحلي localhost:5042 واضغط على الاختصار Ctrl + Shift + K لفتح طرفية المتصفح واكتب شيئًا ما في حقل الإدخال، ويجب أن ترى إدخالاتك مسجلةً، كما يمكنك الآن حذف التابع console.log() التفاعلي إذا رغبت في ذلك. سننشئ بعد ذلك دالةً لإضافة مهمة جديدة وهي الدالة addTodo() التي ستدفع كائن todo جديد إلى المصفوفة todos، لذا أضف ما يلي إلى الجزء السفلي من كتلة <script> ضمن الملف src/components/Todos.svelte: function addTodo() { todos.push({ id: 999, name: newTodoName, completed: false }) newTodoName = '' } ملاحظة: سنسنِد حاليًا المعرِّف id نفسه لكل مهمة، ولكن لا تقلق إذ سنصلح ذلك لاحقًا. نريد الآن تحديث ملف HTML لاستدعاء الدالة addTodo() كلما أُرسِل النموذج، لذا عدّل وسم فتح النموذج NewTodo كما يلي: <form on:submit|preventDefault={addTodo}> يدعم الموجّه on:eventname إضافة مُعدِّلات إلى حدث DOM باستخدام المحرف |، حيث يخبر المُعدِّل preventDefault إطار Svelte بإنشاء شيفرة لاستدعاء التابع event.preventDefault() قبل تشغيل المعالج. إذا حاولت إضافة مهام جديدة، فستُضاف هذه المهام الجديدة إلى المصفوفة todos، ولكن لن تُحدَّث واجهة المستخدِم، وتذكَّر أنه في إطار Svelte يبدأ التفاعل باستخدام الإسنادات، وهذا يعني تنفيذ الدالة addTodo() وإضافة عنصر إلى المصفوفة todos، ولكن لن يكتشف إطار Svelte أن تابع الدفع قد عدّل المصفوفة، وبذلك لن يحدّث مهام العنصر <ul>، كما ستؤدي إضافة todos = todos إلى نهاية الدالة addTodo() إلى حل هذه المشكلة، ولكن يبدو تضمين ذلك في نهاية الدالة أمرًا غريبًا، لذلك سنأخذ التابع push() مع استخدام صيغة الانتشار Spread Syntax لتحقيق النتيجة نفسها، إذ سنسند قيمة إلى المصفوفة todos تساوي المصفوفة todos بالإضافة إلى الكائن الجديد. ملاحظة: المصفوفة Array لديها العديد من العمليات المتغيرة مثل push() و pop() و splice() و shift() و unshift() و reverse() و sort() التي يمكن أن يتسبب استخدامها في حدوث آثار جانبية وأخطاء يصعب تتبعها، لذا نتجنب تغيّر المصفوفة باستخدام صيغة الانتشار بدلًا من التابع push()، ويُعَدّ ذلك من الممارسات جيدة. عدّل الدالة addTodo() كما يلي: function addTodo() { todos = [...todos, { id: 999, name: newTodoName, completed: false }] newTodoName = '' } إعطاء كل مهمة معرفا فريدا إذا حاولت إضافة مهام جديدة في تطبيقك الآن، فستتمكن من إضافة مهام جديدة وستظهر في واجهة المستخدِم أيضًا، ولكن إذا جربته مرةً ثانية، فلن يعمل، وستتلقى رسالة تقول "Error: Cannot have duplicate keys in a keyed each"، أي نحتاج إلى معرِّفات فريدة لمهامنا. لنصرّح أولًا عن المتغير newTodoId يُحسَب من عدد المهام زائد 1، ولنجعله تفاعليًا، لذا أضِف مقتطف الشيفرة التالي إلى القسم <script>: let newTodoId $: { if (totalTodos === 0) { newTodoId = 1; } else { newTodoId = Math.max(...todos.map((t) => t.id)) + 1; } } ملاحظة: لا تقتصر التعليمات التفاعلية على سطر واحد One-liners، كما يمكن استخدام التعليمة التفاعلية الآتية: $: newTodoId = totalTodos ? Math.max(...todos.map(t => t.id)) + 1 : 1 والتي تُعَدّ مفيدةً أيضًا، ولكنه أقل قابليةً للقراءة. يحلّل المصرِّف التعليمة التفاعلية بأكملها، ويكتشف أنها تعتمد على المتغير totalTodos والمصفوفة todos. لذا كلما عُدِّل أيّ منهما، فسيُعاد تقييم هذه الشيفرة وتحديث newTodoId وفقًا لذلك، ولنستخدِم ذلك في الدالة addTodo()، ولنعدّلها كما يلي: function addTodo() { todos = [...todos, { id: newTodoId, name: newTodoName, completed: false }] newTodoName = '' } ترشيح المهام حسب الحالة لنطبّق الآن القدرة على ترشيح مهامنا حسب الحالة، لذا سننشئ متغيرًا للاحتفاظ بالمرشِّح الحالي، ودالة مساعدة ستعيد المهام المُرشَّحة. أولًا، أضف ما يلي في الجزء السفلي من القسم <script>: let filter = 'all' const filterTodos = (filter, todos) => filter === 'active' ? todos.filter((t) => !t.completed) : filter === 'completed' ? todos.filter((t) => t.completed) : todos نستخدِم المتغير filter للتحكم في مرشّح جميع المهام all أو المهام النشطة active أو المكتملة completed، إذ سيؤدي إسناد إحدى هذه القيم إلى المتغير filter إلى تفعيل المرشح وتحديث قائمة المهام، إذ ستتلقى الدالة filterTodos() المرشِّح الحالي وقائمة المهام وستعيد مصفوفةً جديدةً من المهام المُرشَّحة وفقًا لذلك. لنحدّث بعد ذلك شيفرة HTML الخاصة بزر الترشيح لجعله ديناميكيًا ولنحدّث المرشِّح الحالي عندما يضغط المستخدِّم على أحد أزرار الترشيح كما يلي: <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" class:btn__primary={filter === 'all'} aria-pressed={filter === 'all'} on:click={()=> filter = 'all'} > <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'active'} aria-pressed={filter === 'active'} on:click={()=> filter = 'active'} > <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" class:btn__primary={filter === 'completed'} aria-pressed={filter === 'completed'} on:click={()=> filter = 'completed'} > <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> سنعرِض المرشِّح الحالي من خلال تطبيق الصنف btn__primary على زر ترشيح المهام النشطة، ويمكنك تطبيق أصناف تنسيق CSS شرطيًا على عنصر من خلال استخدام الموجِّه class:name={value}، فإذا قُيِّمت عبارة القيمة على أنها صحيحة، فسيُطبَّق اسم الصنف، كما يمكنك إضافة العديد من هذه الموجّهات بشروط مختلفة إلى العنصر نفسه، لذلك إذا كانت التعليمة class:btn__primary={filter === 'all'}، فسيطبّق إطار Svelte الصنف btn__primary إذا كان المرشح يساوي جميع المهام all. ملاحظة: يوفِّر إطار العمل Svelte اختصارًا يتيح لنا إمكانية اختصار <div class:active={active}> إلى <div class:active> عندما يتطابق الصنف Class مع اسم المتغير. يحدث شيء مشابه مع aria-pressed={filter === 'all'} عند تقييم تعبير جافاسكربت الممرَّر بين الأقواس المعقوصة إلى قيمة صحيحة، حيث ستُضاف السمة aria-pressed إلى الزر، وبالتالي سنحدِّث متغير filter باستخدام class:btn__primary={filter === 'all'} كلما نقرنا على الزر. يجب الآن استخدام الدالة المساعدة في حلقة {#each} كما يلي: ... <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> {#each filterTodos(filter, todos) as todo (todo.id)} ... يكتشف إطار Svelte بعد تحليل شيفرتنا أنّ الدالة filterTodos() تعتمد على المتغيرين filter و todos، وكلما تغيرت أيّ من هذه الاعتماديات، فسيُحدَّث نموذج DOM وفقًا لذلك، لذا كلما تغيَّر المتغيران filter وtodos، فسيُعاد تقييم الدالة filterTodos() وستُحدَّث العناصر الموجودة ضمن الحلقة. ملاحظة: يمكن أن تكون التفاعلية خادعةً في بعض الأحيان، إذ يتعرّف إطار Svelte على المتغير filter بوصفه اعتماديةً لأننا نشير إليه في التعبيرfilterTodos(filter, todo)، إذ يُعَدّ المتغير filter متغيرًا من المستوى الأعلى، لذلك يمكن إزالته من معامِلات الدالة المساعدة واستدعائه بالشكل: filterTodos(todo)، كما يمكن أن ينجح هذا الأمر، ولكن ليس لدى إطار Svelte الآن طريقةً لمعرفة أنّ {#each filterTodos(todos)... } يعتمد على المتغير filter، ولن تُحدَّث قائمة المهام المُرشَّحة عندما يتغير المرشّح، وتذكَّر دائمًا أنّ إطار Svelte يحلِّل الشيفرة لاكتشاف الاعتماديات، لذلك يُفضَّل أن تكون صريحًا بشأنه وألّا تعتمد على رؤية متغيرات المستوى الأعلى، كما يُعَدّ جعل الشيفرة واضحةً وصريحةً بشأن المعلومات التي تستخدِمها من الممارسات الجيدة. يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/04-componentizing-our-app أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/04-componentizing-our-app تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة طبّقنا في هذا المقال معظم الوظائف المطلوبة، إذ يمكن لتطبيقنا عرض وإضافة وحذف المهام وتبديل حالتها المكتملة وإظهار عدد المهام المكتملة وتطبيق المرشحات، حيث غطينا المواضيع التالية: إنشاء واستخدام المكونات. تحويل شيفرة HTML الثابتة إلى قالب حي. تضمين تعابير جافاسكربت في شيفرة HTML. التكرار على القوائم باستخدام الموجّه {#each}. تمرير المعلومات بين المكونات باستخدام الخاصيات. الاستماع إلى أحداث DOM. التصريح عن التعليمات التفاعلية. تنقيح الأخطاء الأساسي باستخدام التابع console.log() والتعليمات التفاعلية. ربط خاصيات HTML بالموجّه bind:property. بدء التفاعل باستخدام الإسنادات. استخدام العبارات التفاعلية لترشيح البيانات. التعريف الصريح عن الاعتماديات التفاعلية. سنضيف مزيدًا من الوظائف التي ستسمح للمستخدِمين بتعديل المهام في المقال التالي. ترجمة -وبتصرُّف- للمقال Dynamic behavior in Svelte: working with variables and props. اقرأ أيضًا بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستعمال إطار عمل Svelte
-
يمكننا الآن البدء في إنشاء تطبيقنا مثل تطبيق قائمة المهام بعد أن فهمنا الأمور الأساسية في إطار عمل Svelte في المقال السابق، إذ سنلقي في هذا المقال نظرةً على الوظائف المطلوبة لتطبيقنا أولًا ثم سننشئ المكوِّن Todos.svelte وسنضع شيفرة HTML وشيفرة التنسيق الثابتة في مكانها، وبالتالي سيصبح كل شيء جاهزًا لبدء تطوير ميزات تطبيق قائمة المهام التي سننتقل إليها في المقالات اللاحقة. نريد أن يتمكن المستخدِمون من تصفح المهام وإضافتها وحذفها ووضع علامة عليها بوصفها مكتملةً، كما سنلقي نظرةً على بعض المفاهيم الأكثر تقدمًا. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML و CSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، وستحتاج طرفية مثبَّت عليها node وnpm لتصريف وبناء تطبيقك. الهدف: معرفة كيفية إنشاء مكوِّن Svelte وتصييره في مكوِّن آخر وتمرير البيانات إليه باستخدام الخاصيات Props وحفظ حالته. يمكن متابعة كتابة شيفرتك معنا، لذلك انسخ أولًا مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من تشغيل الأمر التالي: cd mdn-svelte-tutorial/02-starting-our-todo-app أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/02-starting-our-todo-app تذكَّر تشغيل الأمر التالي لبدء تشغيل تطبيقك في وضع التطوير: npm install && npm run dev فإذا أردت متابعتنا فابدأ بكتابة الشيفرة باستخدام أداة REPL. ميزات تطبيق قائمة المهام سيبدو تطبيق قائمة المهام كما يلي بمجرد أن يصبح جاهزًا: سيتمكّن المستخدِم من تطبيق الأمور التالية باستخدام واجهة المستخدِم: تصفح المهام. وضع علامة على المهام المكتملة أو تعليقها دون حذفها. إزالة المهام. إضافة مهام جديدة. ترشيح المهام حسب الحالة: جميع المهام أو المهام النشطة أو المهام المكتملة. تعديل المهام. وضع علامة على جميع المهام بوصفها نشطةً أو مكتملةً. إزالة جميع المهام المكتملة. إنشاء المكون الأول لننشئ المكوِّن Todos.svelte الذي سيحتوي على قائمة المهام. أولًا، أنشئ مجلدًا جديدًا بالاسم src/components. ملاحظة: يمكنك وضع مكوناتك في أيّ مكان ضمن المجلد src، ولكن المجلد components هو اصطلاح معروف يجب اتباعه، مما يسمح لك بالعثور على مكوناتك بسهولة. ثانيًا، أنشئ ملفًا بالاسم src/components/Todos.svelte بحيث يحوي ما يلي: <h1>Svelte To-Do list</h1> ثالثًا، عدّل العنصر title في الملف public/index.html ليحتوي على النص "Svelte To-do list" كما يلي: <title>Svelte To-Do list</title> رابعًا، افتح الملف src/App.svelte واستبدل محتوياته بما يلي: <script> import Todos from './components/Todos.svelte' </script> <Todos /> سيصدر إطار عمل Svelte في وضع التطوير تحذيرًا في طرفية المتصفح عند تحديد خاصية غير موجودة في المكوِّن مثل تحديد الخاصية name عند إنشاء نسخة من المكون App ضمن الملف src/main.js، إذ لا تُستخدَم هذه الخاصية ضمن المكوِّن App، كما يجب أن تعطيك الطرفية حاليًا رسالة مثل الرسالة " was created with unknown prop 'name'"، لكن يمكنك حل هذه المشكلة من خلال إزالة الخاصية name من src/main.js ويجب أن يبدو الآن كما يلي: import App from './App.svelte' const app = new App({ target: document.body }) export default app إذا تحققت من عنوان URL لخادم الاختبار، فسترى تصيير المكوِّن Todos.svelte كما يلي: إضافة شيفرة HTML الثابتة سنبدأ أولًا بتمثيل شيفرة HTML لتطبيقنا لتتمكّن من رؤية الشكل الذي سيبدو عليه، لذا انسخ والصق ما يلي في ملف المكوِّن Todos.svelte ليحل محل المحتوى الموجود مسبقًا: <!-- Todos.svelte --> <div class="todoapp stack-large"> <!-- NewTodo --> <form> <h2 class="label-wrapper"> <label for="todo-0" class="label__lg"> What needs to be done? </label> </h2> <input type="text" id="todo-0" autocomplete="off" class="input input__lg" /> <button type="submit" disabled="" class="btn btn__primary btn__lg"> Add </button> </form> <!-- Filter --> <div class="filters btn-group stack-exception"> <button class="btn toggle-btn" aria-pressed="true"> <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" aria-pressed="false"> <span class="visually-hidden">Show</span> <span>Active</span> <span class="visually-hidden">tasks</span> </button> <button class="btn toggle-btn" aria-pressed="false"> <span class="visually-hidden">Show</span> <span>Completed</span> <span class="visually-hidden">tasks</span> </button> </div> <!-- TodosStatus --> <h2 id="list-heading">2 out of 3 items completed</h2> <!-- Todos --> <ul role="list" class="todo-list stack-large" aria-labelledby="list-heading"> <!-- todo-1 (editing mode) --> <li class="todo"> <div class="stack-small"> <form class="stack-small"> <div class="form-group"> <label for="todo-1" class="todo-label"> New name for 'Create a Svelte starter app' </label> <input type="text" id="todo-1" autocomplete="off" class="todo-text" /> </div> <div class="btn-group"> <button class="btn todo-cancel" type="button"> Cancel <span class="visually-hidden">renaming Create a Svelte starter app</span> </button> <button class="btn btn__primary todo-edit" type="submit"> Save <span class="visually-hidden">new name for Create a Svelte starter app</span> </button> </div> </form> </div> </li> <!-- todo-2 --> <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-2" checked/> <label for="todo-2" class="todo-label"> Create your first component </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">Create your first component</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">Create your first component</span> </button> </div> </div> </li> <!-- todo-3 --> <li class="todo"> <div class="stack-small"> <div class="c-cb"> <input type="checkbox" id="todo-3" /> <label for="todo-3" class="todo-label"> Complete the rest of the tutorial </label> </div> <div class="btn-group"> <button type="button" class="btn"> Edit <span class="visually-hidden">Complete the rest of the tutorial</span> </button> <button type="button" class="btn btn__danger"> Delete <span class="visually-hidden">Complete the rest of the tutorial</span> </button> </div> </div> </li> </ul> <hr /> <!-- MoreActions --> <div class="btn-group"> <button type="button" class="btn btn__primary">Check all</button> <button type="button" class="btn btn__primary">Remove completed</button> </div> </div> تحقّق من الخرج المُصيَّر مرةً أخرى، وسترى شيئًا يشبه ما يلي: يُعَدّ تنسيق شيفرة HTML السابق ليس جيدًا كما أنه غير مفيد وظيفيًا، ولكن لنلقِ نظرةً على الشيفرة ونرى مدى ارتباطها بالميزات التي نرغب بها: تسمية أو عنوان Label ومربع نص لإدخال مهام جديدة. ثلاثة أزرار لترشيح المهام حسب حالتها. تسمية label توضّح العدد الإجمالي للمهام والمهام المكتملة. قائمة غير مرتبة تحتوي على عنصر قائمة لكل مهمة. يحتوي عنصر القائمة عند تعديل المهمة على حقل إدخال وزرَين لإلغاء التعديلات أو حفظها. إذا لم تكن المهمة قيد التعديل، فهناك مربع اختيار لضبط حالة المهمة المكتملة وزرَين لتعديل المهمة أو حذفها. يوجد زران لتحديد أو إلغاء تحديد جميع المهام وإزالة المهام المكتملة. سنعمل في المقالات اللاحقة على تشغيل جميع هذه الميزات. ميزات سهولة الوصول Accessibility لقائمة المهام لاحظ وجود بعض السمات غير المعتادة مثل: <button class="btn toggle-btn" aria-pressed="true"> <span class="visually-hidden">Show</span> <span>All</span> <span class="visually-hidden">tasks</span> </button> تخبر السمة aria-pressed التقنيات المساعدة مثل قارئات الشاشة أنّ الزر يمكن أن يكون في إحدى الحالتين: pressed أو unpressed مثل القول بأن الزر في وضع التشغيل أو الإيقاف، ويعني ضبط القيمة true أنّ الزر مضغوط افتراضيًا. ليس للصنف visually-hidden أيّ تأثير حتى الآن، لأننا لم نضمّن أيّ ملف CSS، وسيُخفَى أيّ عنصر موجود في هذا الصنف عن المستخدِمين المبصرين وسيظل متاحًا لمستخدِمي قارئات الشاشة بمجرد أن نضع التنسيق في مكانه، لأن هذه الكلمات لا يحتاجها المستخدِمون المبصرون، وإنما تُستخدَم لتقديم مزيد من المعلومات حول ما يفعله الزر لمستخدِمي قارئات الشاشة الذين ليس لديهم القدرة البصرية لمساعدتهم. كما يمكنك العثور على عنصر <ul> التالي: <ul role="list" className="todo-list stack-large" aria-labelledby="list-heading"> تساعد السمة role التقنيات المساعدة في توضيح نوع القيمة الدلالية للعنصر أو ما هو الغرض منه، إذ يُعامَل العنصر <ul> بوصفه قائمةً افتراضيًا، ولكن ستؤدي التنسيقات التي نريد إضافتها إلى تعطيل هذه الوظيفة، ولكن سيعيد هذا الدور معنى القائمة إلى العنصر <ul>. تخبر السمة aria-labelledby التقنيات المساعدة بأننا نتعامل مع العنصر <h2> مع معرِّف id عنوان القائمة list-heading بوصفه التسمية التي تشرح الغرض من القائمة الموجودة تحتها، إذ يعطي هذا الارتباط القائمةَ سياقًا مفيدًا، مما يساعد مستخدِمي قارئات الشاشة على فهم الغرض منها بصورة أفضل. دعم إطار عمل Svelte لسهولة الوصول يركّز إطار Svelte على سهولة الوصول أو الشمولية Accessibility، والهدف هو تشجيع المطورين على كتابة المزيد من الشيفرة البرمجية الشاملة افتراضيًا، وبما أنّ إطار Svelte يُعَدّ مصرِّفًا، فيمكنه تحليل قوالب HTML بطريقة ساكنة لتوفير تحذيرات متعلقة بسهولة الوصول عند تصريف المكونات. لا يُعَدّ عملية تطبيق مبادئ سهولة الوصول -التي تُختصَر إلى a11y- أمرًا سهلًا دائمًا، ولكن سيساعدك إطار Svelte من خلال تحذيرك إذا كتبت شيفرة HTML لا تراعي تلك المبادئ، فإذا أضفنا العنصر <img> مثلًا إلى المكوِّن todos.svelte بدون الخاصية alt المقابلة له كما يلي: <h1>Svelte To-Do list</h1> <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A" /> فسيعطي المصرِّف التحذير التالي: (!) Plugin svelte: A11y: <img> element should have an alt attribute src/components/Todos.svelte 1: <h1>Svelte To-Do list</h1> 2: 3: <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A"> ^ created public/build/bundle.js in 220ms [2020-07-15 04:07:43] waiting for changes… كما يمكن لمحرر الشيفرة عرض هذا التحذير حتى قبل استدعاء المصرِّف كما يلي: يمكنك إخبار إطار عمل Svelte بتجاهل هذا التحذير للكتلة التالية من شيفرة HTML بتعليق يبدأ بعبارة svelte-ignore كما يلي: <!-- svelte-ignore a11y-missing-attribute --> <img height="32" width="88" src="https://www.w3.org/WAI/wcag2A"> ملاحظة: يمكنك باستخدام المحرّر VSCode إضافة تعليق التجاهل هذا تلقائيًا بالنقر على الرابط "Quick fix…" أو بالضغط على الاختصار Ctrl + .. إذا أردت تعطيل هذا التحذير، فيمكنك إضافة المعالج onwarn إلى الملف rollup.config.js ضمن إعداد الإضافة Svelte كما يلي: plugins: [ svelte({ dev: !production, css: css => { css.write('public/build/bundle.css'); }, // Warnings are normally passed straight to Rollup. You can // optionally handle them here, for example to squelch // warnings with a particular code onwarn: (warning, handler) => { // e.g. I don't care about screen readers -> please DON'T DO THIS!!! if (warning.code === 'a11y-missing-attribute') return; // let Rollup handle all other warnings normally handler(warning); } }), ... ] تُنفَّذ هذه التحذيرات في المصرِّف نفسه حسب التصميم وليس على أساس إضافة يمكن أن تختار إضافتها إلى مشروعك، كما تكمن الفكرة في التحقق من وجود مشاكل سهولة الوصول a11y في الشيفرة افتراضيًا والسماح بإلغاء تحذيرات معينة. ملاحظة: لا يجب تعطيل هذه التحذيرات إلا إذا كانت لديك أسباب وجيهة لذلك مثل تعطيلها أثناء إنشاء نموذج أولي prototype سريع، إذ يجب أن تجعل صفحاتك قابلةً للوصول إلى أوسع قاعدة ممكنة من المستخدِمين. قواعد الشمولية التي تحقق منها إطار عمل Svelte مأخوذة من الإضافة eslint-plugin-jsx-a11y، وهي إضافة من ESLint توفِّر فحوصات ساكنة للعديد من قواعد سهولة الوصول على عناصر JSX، كما يهدف إطار Svelte إلى تنفيذ كل من هذه القواعد في مصرِّفه، وقد نُقِل معظمها إلى Svelte فعليًا، بالإضافة إلى أنه يمكنك على GitHub معرفة فحوصات الشمولية التي لا تزال مفقودة، ويمكنك التحقق من معنى كل قاعدة من خلال النقر على رابطها الخاص. تنسيق التطبيق لنجعل قائمة المهام تبدو أفضل قليلًا، لذا استبدل محتويات الملف public/global.css بما يلي: /* RESETS */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; outline-offset: 0; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; -webkit-appearance: none; } button::-moz-focus-inner { border: 0; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { overflow: visible; } input[type="text"] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*END RESETS*/ /* GLOBAL STYLES */ .form-group > input[type="text"] { display: inline-block; margin-top: 0.4rem; } .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn.toggle-btn { border-width: 1px; border-color: #d3d3d3; } .btn.toggle-btn[aria-pressed="true"] { text-decoration: underline; border-color: #4d4d4d; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__filter { border-color: lightgrey; } .btn__primary { color: #fff; background-color: #000; } .btn__primary:disabled { color: darkgrey; background-color:#565656; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 49%; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } .visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*="stack"] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } .stack-exception { margin-top: 1.2rem; } /* END GLOBAL STYLES */ .todoapp { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { .todoapp { padding: 4rem; } } .todoapp > * { max-width: 50rem; margin-left: auto; margin-right: auto; } .todoapp > form { max-width: 100%; } .todoapp > h1 { display: block; max-width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } .label__lg { line-height: 1.01567; font-weight: 300; padding: 0.8rem; margin-bottom: 1rem; text-align: center; } .input__lg { padding: 2rem; border: 2px solid #000; } .input__lg:focus { border-color: #4d4d4d; box-shadow: inset 0 0 0 2px; } [class*="__lg"] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*="__lg"]:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*="__lg"] { font-size: 2.4rem; } } .filters { width: 100%; margin: unset auto; } /* Todo item styles */ .todo { display: flex; flex-direction: row; flex-wrap: wrap; } .todo > * { flex: 0 0 100%; } .todo-text { width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } .todo-text:focus { box-shadow: inset 0 0 0 2px; } /* CHECKBOX STYLES */ .c-cb { box-sizing: border-box; font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 44px; padding-left: 40px; clear: left; } .c-cb > label::before, .c-cb > input[type="checkbox"] { box-sizing: border-box; top: -2px; left: -2px; width: 44px; height: 44px; } .c-cb > input[type="checkbox"] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; margin: 0; opacity: 0; } .c-cb > label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .c-cb > label::before { content: ""; position: absolute; border: 2px solid currentColor; background: transparent; } .c-cb > input[type="checkbox"]:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .c-cb > label::after { box-sizing: content-box; content: ""; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .c-cb > input[type="checkbox"]:checked + label::after { opacity: 1; } يبدو كل شيء الآن أفضل كما يلي: يمكنك الوصول إلى نسختك من مستودعنا على النحو التالي لمعرفة حالة الشيفرة، كما يجب أن تكون في نهاية هذا المقال: cd mdn-svelte-tutorial/03-adding-dynamic-behavior أو يمكنك تنزيل محتوى المجلد مباشرةً باستخدام الأمر التالي: npx degit opensas/mdn-svelte-tutorial/03-adding-dynamic-behavior تذكَّر تشغيل الأمر npm install && npm run dev لبدء تشغيل تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL. الخلاصة بدأ تطبيق قائمة المهام في التبلور مع تطبيق شيفرة HTML وشيفرة التنسيق CSS، وأصبح كل شيء جاهزًا لنتمكّن من التركيز على الميزات التي يجب تطبيقها. ترجمة -وبتصرُّف- للمقال Starting our Svelte to-do list app. اقرأ أيضًا المقال السابق: بدء استخدام إطار العمل Svelte لبناء تطبيقات ويب إنشاء تطبيق قائمة مهام باستخدام React إنشاء تطبيق Todo List بسيط باستخدام Laravel 5 - الجزء الأول
-
لا توجد وصفة مثالية لتطوير منتج ناجح، ولكن يُعَد امتلاك فكرةٍ مجرد بداية تساعدك على اكتشاف ملاءمة السوق وخصائص المنتج. يكون تطبيق هذه الفكرة عمليةً صعبة يمكن أن تخفق بسهولة، حيث يمتد التطوير على مدى فترة زمنية طويلة مع ألوف العوامل لتحويل الفكرة الأولية. يتواجد مدير المنتجات في مركز كل ذلك، فمهمة مدير المنتجات هي إبقاء الجميع في حالة تزامن أثناء تطور فكرة المنتج، بحيث تفهم جميع الفرق المنتجَ بالطريقة نفسها. يمكن تحقيق ذلك بمساعدة وثيقة استراتيجية تسمى خارطة طريق المنتج Product Roadmap. سنحدد في هذا المقال دور مدير المنتجات في تطوير المنتج وخريطة طريق المنتج وميزاتها الرئيسية، وسنتعرّف على أنواع خرائط الطريق الشائعة وأمثلة عنها وبعض النصائح حول إنشائها. من هو مدير المنتجات؟ سنقدّم لك أولًا مخططًا سريعًا لمهام مدير المنتجات الذي يبني خرائط الطريق لفهم خصائصها، حيث تكون المهام الرئيسية لمدير المنتجات هي: تحليل السوق والمنافسين. التواصل مع العملاء. تطوير رؤية المنتج وتخطيطه واستراتيجيته. تقدير العمل وتحديد أولوياته. إنشاء خارطة طريق المنتج. مشاركة خرائط الطريق عبر المؤسسة. الالتزام بخارطة طريق المنتج. يتمثل الجزء الأساسي من وظيفة مدير المنتجات في اكتشاف ما سيكون عليه المنتج وإخبار الجميع عنه من خلال خارطة طريق المنتج. الفرق بين مدير المنتجات ومالك المنتج لا بد أنك تتساءل عن الفرق بين مدير المنتجات Product Manager -أو PM اختصارًا- ومالك المنتج Product Owner -أو PO اختصارًا، إذ يتعامل كلاهما مع تفاصيل المنتج. يكون مالك المنتج PO مسؤولًا عن قيمة المنتج وأعماله المتراكمة وقصص المستخدمين، والشيء الرئيسي الذي يجب تذكره هو أنه لا يوجد دور لمالك المنتج PO خارج مشروع سكروم Scrum. وفي المقابل، يُعَد مدير المنتجات مهنةً، ويمكن أن يكون مدير المنتجات مالكًا للمنتج ضمن فريق سكروم. يقدّم مدير المنتجات PM خارطة طريق للمنتج مع الرؤية والاستراتيجية، بينما يعمل مالك المنتج PO على أعمال المنتج المتراكمة ويحدد المتطلبات التجارية والتقنية. ما هي خارطة طريق المنتج؟ خارطة طريق المنتج هي وثيقة استراتيجية عالية المستوى ترسم المراحل العامة لتطوير المنتج، والغرض الرئيسي منها هو ربط رؤية المنتج بأهداف أعمال الشركة. مثال عن خارطة طريق المنتج. المصدر: Roadmunk تنشأ خارطة طريق المنتج بوصفها نتيجةً للتخطيط الاستراتيجي، وتوثّق كلًا من الاستراتيجية التنفيذية وأهداف المنتج العامة. تتضمن خارطة طريق المنتج الاستراتيجية النقاط الرئيسية التالية: رؤية المنتج: تمثل ما تريد أن يصبح منتجك عليه في المستقبل. الاستراتيجية: خطة تنفيذ توضح بالتفصيل ما ستفعله شركتك لتحقيق الرؤية. الهدف: هدف محدد زمنيًا يمكن قياسه بمقياس معين. المبادرة: موضوعات واسعة توحّد الميزات التي يجب تطبيقها لتحقيق الهدف. الميزة: جزء فعلي من منتج يكون إما جزءًا من وظيفة أو تطبيقًا تابعًا لجهة خارجية. الأطر الزمنية: المواعيد أو الفترات الزمنية لهدف أو ميزة معينة يجب الانتهاء منها، حيث تقترح خارطة طريق المنتج مواعيدًا زمنية تقريبية كقاعدة عامة. علامات الحالة: تُستخدَم لتتبّع تقدّم العمل. المقاييس: المساعدة في قياس الأهداف الموجَّهة بالبيانات مثل معدل النفور Churn Rate أو حركة الزوار الطبيعية Organic Traffic. مخطط عناصر تخطيط المنتج يمكن أن يختلف عدد الفرق المشاركة في خرائط الطريق اعتمادًا على المنتج الذي تطوره والمنهجيات التي تمارسها، حيث سيكون لديك في أغلب الأحيان فريق الهندسة وتجربة المستخدم UX والمبيعات والتسويق وفريق الدعم والفريق التشغيلي والمصممين وفريق الاختبار، وتمثل هذه الفرق الأشخاص الذين سيعملون على المنتج الفعلي. يجب أن تكون خارطة طريق أي منتج واضحةً وسهلة الفهم، مما يساعد مدير المنتجات في توجيه جميع الفرق خلال عملية التطوير بما يتماشى مع احتياجات العملاء وأهداف العمل. لذا تكون خارطة طريق المنتج مفيدة وقابلة للتطبيق عندما تلبي بالمتطلبات التالية: تنقل استراتيجية تطوير المنتج. تُظهِر رؤية المنتج. تتطور وتتغير حسب المنتج ومتطلبات السوق. تعطي الأولوية لوحدات التطوير عالية المستوى. تعمل بوصفها أداة اتصال بين جميع الأشخاص المعنيين. تحدد الأطر الزمنية طويلة الأمد. تحدد الأهداف الدقيقة وتربطها بأهداف العمل. يبني مدير المنتجات في بعض الأحيان خرائط طريق متعددة من أنواع مختلفة لتقديم المعلومات إلى أصحاب المصلحة الداخليين والخارجيين. كيف تنشئ خارطة طريق المنتج؟ يُعَد التمسك بالممارسات العامة والمُجرَّبة جيدًا أمرًا منطقيًا عند التفكير في عملية متعددة الخطوات مثل إعداد خارطة طريق. إذًا إليك بعض النصائح العملية لإنشائها. صغ استراتيجيتك ورؤيتك للمنتج تحدَّث إلى أصحاب المصلحة الداخليين والخارجيين وإلى عملائك، وانظر إلى السوق ومنافسيك. حدد شخصيات عملائك، وأنصِت إلى ما يقوله مندوبو المبيعات لك، وتحدث إلى العملاء أنفسهم، ووفّر معلومات تمثل صوت العميل لفريق الإنتاج والإدارة. ستكون لديك بيانات الإدخال المطلوبة لبدء العمل على خارطة طريقك بمجرد ملاءمة الرؤية مع جميع أصحاب المصلحة والمشاركين. حدد جمهورك لا تُعَد خارطة طريق المنتج خطةً ذات حجم واحد يناسب الجميع، إذ سيكون الجمهور الذي يجب عليك تقديم خارطة الطريق إليه عاملًا مُحدَّدًا مسبقًا لشكل خارطة الطريق ونوعها والمحتويات التي يجب تضمينها فيها. يُعَد تحديد نوع خارطة الطريق أمرًا معقدًا، وسنفصّل ذلك في القسم التالي. اختر تنسيقا مناسبا يؤثر التنسيق على اختيارك للمحتويات، حيث يمكن أن يكون التنسيق الذي تختاره أكثر ملاءمةً لجمهور معين، فمثلًا، لا يكون التنسيق المستند إلى الميزات مناسبًا لقسم التسويق أو الإدارة، ولكن يُوصَى به لفريقك الهندسي. سيقترح التنسيق المختار عناصر المعلومات الضرورية التي يجب تمييزها والمواضيع أو الأهداف التي يجب تحديدها حسب الأولوية في الجدول الزمني. اختر المقاييس ولائمها مع الميزات الفعلية ستساعدك المقاييس على رؤية صورة أوسع وقياس تقدمك، ويمكنك اختيار المقاييس الموجهة لاحتياجات العملاء أو احتياجات العمل اعتمادًا على الغرض من خارطة الطريق. يمكنك تحليل السوق ومنافسيك أو اللجوء إلى محلل المجال كمصدر للمقاييس ذات الصلة. استخدم أدوات محددة لخارطة الطريق يمكن أن يكون استخدام أداة مثل إكسل Excel لبناء خارطة طريق عمليةً صعبة، إذ ستحصل على عرض تقديمي ثابت يصعب تحديثه إلى حدٍ ما. تتيح لك أدوات خرائط الطريق المستندة إلى السحابة تسريع العملية والحفاظ على خارطة الطريق مُحدَّثةً عندما تتغير الأولويات. إليك بعض الأدوات التي يمكنك استخدامها: OpenProject: هي أداة خارطة طريق مجانية تتيح لك إنشاء مشاريع غير محدودة ضمن ملف تعريف مستخدم واحد، وهي برنامج إدارة منتجات مفتوح المصدر مصمَّم لتلبية احتياجات فرق سكروم/أجايل Agile/Scrum. Roadmap Planner: هي أداة أخرى مفتوحة المصدر لإدارة المنتجات لنظام لينكس Linux. تُعَد الأداة ProductPlan الأكثر شيوعًا من بين هذه الأدوات، حيث تستخدمه كبرى الشركات الرقمية مثل ويندوز Windows وأدوبي Adobe. تشارك الأداة ProductPlan عددًا كبيرًا من قوالب خرائط الطريق لأغراض مختلفة، ويمكنك استيراد عناصر من نظام جيرا Jira أو جداول Spreadsheets أو VSTS، مما يجعل عملية التخطيط أسهل بكثير. Aha!: هي شركة عملاقة أخرى تستخدمها شركات شاترستوك Shutterstock ولينكد إن LinkedIn وديل Dell، حيث تمثل قائمة متكاملة مع التطبيقات الأخرى. Roadmunk: من أفضل تطبيقات إدارة المنتجات التي تلبي جميع المعايير اللازمة مع أسعار مناسبة. أداة أنا من حسوب: وهي أداة عربية يمكن استخدامها لإدارة مشاريعك وفريق عملك عن بعد، حيث يمكنك باستخدامها بناء خرائط ذاتية حسب ميولات كل شخص ومتطلبات المشروع. حافظ على معلومات محدثة وعالية المستوى يجب التركيز على توفير الرؤية العامة والاستراتيجية دون التركيز على الأساليب للحفاظ على وظائف خارطة الطريق الاستراتيجية. تُعَد عناصر معلوماتك الثانوية قيّمةً، ولكن خارطة طريقك هي وثيقة استراتيجية يجب أن تكون واضحة وسهلة الفهم. لذا يجب تجنّب الإفراط في التفاصيل أو تضمين الكثير من المعلومات غير الضرورية. يجب التفاعل بديناميكية مع تغييرات خارطة طريق المنتج، حيث سيجلب تقدّم منتجك ميزات وأهدافًا جديدة، لذا يجب تحديث خارطة طريق المنتج باستمرار لتتبعها ونقل المعلومات إلى بقية أصحاب المصلحة، مما يعني تطورًا تدريجيًا مع المنتج. أنواع جمهور خارطة طريق المنتج لخارطة الطريق -كما المنتج- جمهورها المُستهدَف. ترتبط مجموعات مختلفة من الأشخاص بالمنتج، لذلك يجب التواصل مع تلك المجموعات باستخدام معلومات مختلفة ودقيقة. سيخبرك عامل الجمهور بنوع المحتوى المراد تضمينه وشكله ومدى تفصيله، لذا يمكنك -بصفتك مدير منتجات- إما إنشاء خرائط طريق متعددة لكل مجموعة من الأشخاص أو إنشاء مستند استراتيجي واحد للجميع (وهي حالة نادرة). لنلقِ نظرةً على أنواع الجمهور الذي يمكن إنشاء خارطة طريقك من أجله. أنواع الجمهور في خرائط الطريق يمكن أن يكون جمهور خارطة طريق المنتج داخليًا مثل فريقك والمديرين التنفيذيين، أو خارجيًا مثل العملاء والمستثمرين. تنقل خرائط الطريق الداخلية المعلومات التي يطلبها كل قسم، لذلك يجب اقتراح البيانات المناسبة للأشخاص المناسبين. وتكون خرائط الطريق الداخلية مُخصَّصةً للمديرين التنفيذيين وفريق الإنتاج والمبيعات. تتطلّب المجموعة التنفيذية نظرةً أكثر استراتيجيةً على البيانات، لذلك يجب أن تركّز خارطة الطريق على الرؤية والأهداف الاستراتيجية والجداول الزمنية وأرقام السوق وغير ذلك، بينما يركز فريق الإنتاج على الجوانب التخطيطية والمواعيد النهائية وتفاصيل التطبيق التقنية، حيث يجب أن تنقل خارطة الطريق معلومات منخفضة المستوى بناءً على أجزاء المنتج أو مواضيعه أو ميزاته الفعلية لتحقيق قيمة حقيقية لفريق الإنتاج. يهتم فريق المبيعات بمجموعة ميزات المنتج وفوائده للعملاء، لذا يجب أن يركز هذا النوع من خرائط الطريق على قيمة المنتج، إذ يُعَد التنسيق المستند إلى موضوعٍ ما هو الأنسب، حيث يمكن للمواضيع إظهار الهدف الذي تحققه كل ميزة بيانيًا. تحتوي خرائط الطريق الخارجية على تنسيق يشبه العرض التقديمي لأنها لا تشارك أي معلومات محددة حول العمليات الداخلية. يجب أن تكون خرائط الطريق الخارجية سهلة الفهم وواضحة بصريًا وتشارك أكبر قدر من المعلومات حول الفوائد التي تعود على العملاء. لا تحتوي خرائط الطريق التي تُشارَك مع الجمهور على أي مواعيد نهائية في أغلب الأحيان، بل تقدّم أطرًا زمنية تقريبية وتتالٍ من إصدارات الميزات. أنواع خارطة طريق المنتج وأمثلة عنها تختلف خرائط طريق المنتج من مشروع لآخر، وذلك لأنها يمكن أن تكون مصممةً لنقل أنواع مختلفة من البيانات أو تتبع منطق مختلف، وبذلك سيختلف شكلها وبنيتها. صنّف بريان لاولي Brian Lawley في كتابه "Expert Product Development" خرائط الطريق للأغراض العامة إلى الأنواع التالية: خارطة طريق الاستراتيجية والسوق: تتعامل مع التفاصيل عالية المستوى وحالة السوق. خارطة الطريق ذات الرؤية: تحدد رؤية المنتج. خارطة الطريق التقنية: مختلفة تمامًا عن النوعين السابقين، وهي خارطة طريق تقنية منخفضة المستوى لفريق الإنتاج. خارطة الطريق التقنية الخاصة بالمنتج: مزيج من التقنيات أو الميزات الفعلية المُخطَّطة للمنتج أو لمجموعة المنتجات. خارطة طريق المنصة: موجَّهة إلى المنتجات الرقمية متعددة المنصات. خارطة طريق المنتج الداخلية والخارجية: مرتبطة بأنواع مختلفة من الجمهور. لكن تنوع يكون خرائط الطريق في العالم الحقيقي يكون أوسع بكثير بين مستخدمي أجايل Agile والشركات التقنية الرقمية. لنلقِ نظرة الآن على بعض الأنواع الشائعة من خرائط الطريق مع وضع الجمهور بوصفه عاملًا أساسيًا في الحسبان. تصف خارطة الطريق الآن-التالي-لاحقًا Now-Next-Later المهام/الفترات الزمنية السريعة Sprints/الميزات بطريقة مرتبة حسب الأولوية، وهي نسخة مبسطة من أعمال المنتج المتراكمة التي تصنّف عناصر المعلومات أفقيًا وشاقوليًا. تعرض هذه الخارطة ما سيصدر الآن وما سيُعَد تاليًا وما سيصدر لاحقًا، والغرض منها هو إظهار الأولويات بأبسط طريقة ممكنة. خارطة طريق الآن-التالي-لاحقًا. المصدر: Scrum تساعد خارطة طريق المنتج swimlane في توضيح تفاصيل المشروع الأساسية، مع تبيان تقسيمات الأعمال أقسام المشروع، موضحةً المسؤول عن كل عملية مع المرحلة التي وصل إليها بالتنفيذ والنسبة المئوية لمعدل اكتمال العمل لديه. مثال عن خارطة طريق المنتج swimlane. المصدر: Roadmunk تساعد خارطة Business Development Roadmap في إبقاء جميع المعلومات مُجمَّعة ومُفسَّرة بوضوح، إذ تحدد الأهداف سبب وجود كل ميزة، ويمكن تحديد الهدف بكلمات بسيطة مثل "زيادة مشاركة المستخدمين" أو "تسريع عملية التسجيل". يمكنك الحفاظ على خارطة طريق عالية المستوى وعلى استراتيجيتك ورؤيتك سهلة الفهم من خلال تنظيم المعلومات حول الأهداف. مثال عن خارطة الطريق المستندة إلى الموضوع من موقع Roadmunk تستخدم خارطة الطريق المستندة إلى الميزات Feature Roadmap الميزات بوصفها نقطة مركزية لخارطة الطريق، مما يجعلها مُفصَّلة جدًا، ولكن لها بعض العيوب هي: لا تُعَد الميزة وحدًة مستقرةً بالنظر إلى السوق المتغير، إذ تتسبب الابتكارات التقنية واحتياجات العملاء في تغيير مجموعة ميزاتك في كثير من الأحيان. لا يوفر التنسيق المستند إلى الميزات تفاصيلًا عالية المستوى، مما يؤدي إلى تشويش الرؤية العامة للمنتج ويجعل فهم خارطة الطريق والحفاظ عليها أمرًا صعبًا بصورة أكبر. مثال عن خارطة الطريق المستندة إلى الميزات. المصدر: Roadmunk خارطة طريق الإستراتيجية Strategy Roadmap هي خارطة طريق للأغراض العامة، ويمكن أن تتضمن أي نوع من المعلومات وتكون مناسبةً لكل من الجمهور الداخلي والخارجي؛ كما تُعَد مخططًا عالي المستوى لمعلومات المنتج العامة المرتبطة بجانب معين اعتمادًا على الغرض منه. مثال عن خارطة طريق الاستراتيجية. المصدر: blog.aha.io خرائط الطريق التقنية Technology Roadmap أو خرائط طريق تقانة المعلومات IT Roadmaps هي وثائق ذات مستوى منخفض تُنشَأ عادةً لدعم خارطة طريق الاستراتيجية الرئيسية، وتُستخدَم للفرق الداخلية لصياغة المتطلبات التقنية. تحدد خرائط الطريق التقنية استخدامَ تقنية معينة، وتساعد في تخصيص الموارد التي تعتمد عليها. مثال عن خارطة طريق تقنية من موقع Roadmunk تُعَد خارطة طريق الإصدار Release Roadmap مثالًا عن خارطة طريق خارجية مُقدَّمة للعملاء. يمثل هذا النوع الإصدارات الرئيسية لوظائف التطبيق للاستخدام العام، لذلك لا تحتاج إلى كثير من التفاصيل التقنية أو العملية. خارطة طريق جدول الإصدار الزمني لأصحاب المصلحة الخارجيين من موقع Roadmunk خارطة طريق السوق Market Roadmap هي مستند يمكن استخدامه عند التخطيط لإطلاق المنتج عبر أسواق متعددة، وهي مُطوَّرة لتمكين قسم التسويق وأصحاب المصلحة الداخليين من تخطيط استراتيجية التسويق لمنتج واحد أو منتجات متعددة. يمكن أن تكون خرائط طريق السوق خرائط الطريق الأكثر ديناميكيةً، حيث يجب عليها التقاط التغيّرات السريعة في السوق، ويمكن أن يتسبب المنافس أو التقدم التقني في تحولات كبيرة تتطلب تعديل الاستراتيجية. تتضمن خارطة طريق السوق ثلاثة أو أربعة عناصر، فنادرًا ما توزع الشركات منتجاتها على عدد كبير من الأسواق. قوالب خارطة طريق المنتج قد لا يكون الفهم الأساسي لنوع خارطة طريق المنتج كافيًا إذا أردت إنشاء خارطة طريقك، ولكن يمكن استخدام جميع الأمثلة السابقة كمرجع لك. سنوفر الآن بعض القوالب التي يمكن استخدامها أو مشاركتها مع جمهورك، وأبسط مثال هو قالب جدول البيانات التالي الذي يمثل خارطة طريق مستندة إلى الموضوع: 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; } Q1 2018 المحتوى التسويق الرقمي العلامة التجارية الفعاليات والعلاقات العامة المدير البحث في المجتمعات المستهدفة كلمات البحث المفتاحية تحديد عرض القيمة المُقترَح مشاركات العروض التقديمية نشر الوظائف إعداد القنوات الاجتماعية إعداد لوحات التحكم بناء صفحات الهبوط استراتيجية العلاقات الإعلامية تقييم المكدس بناء تقويم المحتوى الزمني نشرة أخبار الإطلاق Q2 2018 إطلاق المدونة تحسين إعلانات AdWords إنشاء دليل التنسيق مؤتمر الخطة في تموز/يوليو توظيف مدير تسويق رقمي زيادة حركة الزوار الطبيعية تحسين الكلمات المفتاحية إنشاء شعار العلامة التجارية حجز قاعة لسلسلة المناسبات الصيفية إعداد الميزانية تكامل الشبكات الاجتماعية إرسال دعوات للمتحدثين تطبيق النشر المشترك على منصات متعددة Q3 2018 جمع منشورات الزوار التكامل مع خدمة MailChimp إعادة تصميم الموقع تنظيم سوق رقمي من الدرجة الأولى توظيف مدير العلامة التجارية تحسين الروابط الخلفية اختبار أ/ب للخصم البريدي إنشاء دليل العلامة التجارية توظيف مدير المحتوى تقليل معدل التخلي عن عربة التسوق توسيع قسم التوظيف إعداد الميزانية يقدّم موفرو برامج إنشاء خرائط الطريق قوالبًا مجانية يمكن استخدامها أثناء الفترات التجريبية أو تنزيلها مثل: قوالب Roadmunk. قوالب Aha!: متاحة بعد تسجيل الدخول فقط. قوالب ProductPlan. قوالب Miro: متوفرة بعد تسجيل الدخول فقط. قوالب Venngage. بينما إذا أردت قالبًا يمكن تنزيله، فيمكنك استخدام أحد القوالب الآتية، حيث يُعَد تحديث ملفات خرائط الطريق المُضمَّنة أمرًا صعبًا وتتطلب تسجيلًا، ولكن يمكن مشاركتها مجانًا: قوالب Office Timeline القابلة للتنزيل. قوالب TemplateLAB القابلة للتنزيل. قوالب UseFyi القابلة للتنزيل. المستندات الداعمة لخارطة الطريق لا يزال يتعين على خارطة طريق المنتج بأي شكل من أشكالها أن تحافظ على نقاط المعلومات الأساسية العامة. يمكن تجنب إضافة معلومات منخفضة المستوى إلى المستند من خلال استخدام أداتين أخريين تدعمان خارطة الطريق الاستراتيجية، فعلى سبيل المثال، تُعَد أعمال المنتج المتراكمة Product Backlog أداةً من نموذج سكروم Scrum مع قائمة بالمتطلبات والميزات عالية المستوى، حيث ينشئها مالكو المنتجات وتتكون من قصص المستخدمين. وتُعَد أعمال المنتج المتراكمة هي قائمة مهام تحدد تطوير المنتج على المستوى التخطيطي. خطة الإصدار Release Plan هي وثيقة تحدد مواعيد إصدار صارمة، حيث يحدد مديرو المنتجات الأطر الزمنية بين الإصدارات. تظهِر خارطة طريق المنتج تعاقب إصدارات المنتج، بينما تقدم خطة الإصدار مواعيدًا أدق لميزة معينة يجب إصدارها، كما يمكن مطابقة الفترات الزمنية السريعة في عملية التطوير مع ميزات معينة أو إصلاحات الأخطاء. هل خارطة طريق المنتج ضرورية؟ لا بد أنك الآن تتساءل عمّا إذا كان بناء خارطة طريق للمنتج أمرًا ضروريًا. حسنًا، يتطلب إنشاء أي مستند بمفرده كثيرًا من الجهد، وسيقضي مدير المنتجات وقتًا في جمع بيانات الإدخال من أصحاب المصلحة وفريق المنتجات، لكن خرائط الطريق تمنح -التي يُحافَظ عليها وعلى تنسيقها بصورة صحيحة- فرقَك وصولًا سهلًا إلى المعلومات الاستراتيجية، وبالتالي تُعَد أداةً مفيدة. إن ساعدتك خارطة الطريق في تحقيق أهدافك الإنتاجية، فاستخدمها؛ لكن إذا استغرق الأمر وقتًا أطول في البناء والتوزيع أو تطلّب تعديل خارطة الطريق تحديث مستندات متعددة في كل مرة، فيمكنك الاستغناء عنها. إذا كان تحديد أولويات المهام وتحديد مواعيد نهائية صارمة أمرًا صعبًا، فهناك تقنيات أكثر ملاءمة في إدارة المنتجات مثل تخطيط قصة المستخدم، وضع في بالك تقنيات تحديد أولويات التراكم العامة المستخدمة في أجايل، لأن المنتجات هي عبارة عن أعمال متراكمة بطريقة أو بأخرى. ترجمة -وبتصرُّف- للمقال Product Roadmap: Key Features, Types, Building Tips, and Roadmap Examples. اقرأ أيضًا مدخل مبسط إلى عالم إدارة المنتجات كيفية الدخول في مجال إدارة المنتجات وإتقانه مجموعة مصادر مهمة تساعد على دخول مجال إدارة المنتج مقارنة بين مدير المنتج ومدير المشروع
-
سنقدّم في هذا المقال مقدمةً سريعةً عن إطار عمل Svelte، إذ سنرى كيفية عمله وما يميزه عن باقي أطر العمل وأدواته، ثم سنتعلم كيفية إعداد بيئة التطوير وإنشاء تطبيق ويب بسيط وفهم بنية المشروع ومعرفة كيفية تشغيله محليًا وإنشائه للإنتاج. المتطلبات الأساسية: يوصَى على الأقل بأن تكون على دراية بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة باستخدام سطر الأوامر أو الطرفية، إذ يُعَد إطار العمل Svelte مصرِّفًا Compiler ينشئ شيفرة جافاسكربت مُصغَّرةً ومُحسَّنةً من شيفرتنا البرمجية، لذا ستحتاج إلى طرفية مثبَّت عليها node و npm لتصريف وبناء تطبيقك. الهدف: إعداد بيئة تطوير Svelte محلية وإنشاء وبناء تطبيق بسيط وفهم أساسيات كيفية عمله. إطار عمل Svelte: طريقة جديدة لبناء واجهات المستخدم يوفِّر إطار العمل Svelte نهجًا مختلفًا لبناء تطبيقات الويب عن بعض أطر العمل الأخرى التي تحدثنا عنها في هذه السلسلة تعلم تطوير الويب مثل Ember أو Vue.js، إذ تطبّق أطر العمل مثل React أو Vue.js الجزء الأكبر من عملها في متصفح المستخدِم أثناء تشغيل التطبيق، بينما ينقل إطار Svelte العمل إلى خطوة التصريف التي لا تحدث إلا عند بناء تطبيقك، مما ينتج عنه شيفرة مُحسَّنة باستخدام لغة جافاسكربت الصرفة Vanilla JavaScript، كما ينتج عن هذا النهج حزم تطبيقات أصغر وأداء أفضل، بالإضافة إلى تجربة مطور أسهل للأشخاص الذين لديهم خبرة محدودة في النظام البيئي المجتمعي للأدوات الحديثة. يلتزم إطار عمل Svelte بنموذج تطوير الويب الكلاسيكي باستخدام اللغات HTML و CSS و JS مع إضافة بعض الامتدادات إلى HTML وجافاسكربت، إذ يمكن القول أنه لديه مفاهيم وأدوات أقل للتعلم من خيارات أطر العمل الأخرى، لكن تتمثل عيوب Svelte الرئيسية الحالية في أنه إطار عمل جديد، وبالتالي فإن نظامه البيئي محدود أكثر من الأطر الأقدم من حيث الأدوات والدعم والإضافات وأنماط الاستخدام الواضحة وما إلى ذلك، كما أنّ هناك فرص عمل أقل متعلقة به، لذلك يجب أن تكون مزاياه كافيةً للاهتمام باستخدامه. ملاحظة: أضاف إطار Svelte مؤخرًا دعم لغة TypeScript الرسمي، وهو أحد أكثر الميزات المطلوبة. حالات الاستخدام يمكن استخدام إطار عمل Svelte لتطوير أجزاء صغيرة من واجهة أو تطبيقات كاملة، إذ يمكنك إما البدء من نقطة الصفر والسماح لإطار عمل Svelte بتشغيل واجهة المستخدِم أو يمكنك دمجه مع تطبيق موجود مسبقًا، كما يُعَدّ إطار Svelte مناسبًا لمعالجة المواقف التالية: تطبيقات الويب المخصصة للأجهزة ذات الإمكانات المنخفضة: تتميز التطبيقات المُنشَأة باستخدام إطار عمل Svelte بأحجام حزم أصغر، وهي مثالية للأجهزة ذات اتصالات الشبكة البطيئة وقوة المعالجة المحدودة، إذ يؤدي استخدام شيفرة برمجية أقل إلى استخدام كيلوبايتات أقل لتنزيلها وتحليلها وتنفيذها والاستمرار في التنقل ضمن الذاكرة بسلاسة. الصفحات التفاعلية جدًا أو ذات المؤثرات البصرية المعقدة: إذا أردت بناء مؤثرات البيانات البصرية التي تحتاج لعرض عدد كبير من عناصر نموذج DOM، فستضمن مكاسب الأداء التي تأتي من إطار عمل بدون تكاليف تشغيل إضافية أن تكون تفاعلات المستخدِم ذات استجابة سريعة. تأهيل الأشخاص ذوي المعرفة الأساسية بتطوير الويب: يتمتع إطار Svelte بمنحنى تعليمي سطحي، إذ يمكن لمطوري الويب الذين لديهم معرفة أساسية بلغات HTML و CSS وجافاسكربت استيعاب تفاصيل إطار Svelte بسهولة في وقت قصير والبدء في إنشاء تطبيقات الويب. يساعد إطار عمل Sapper الذي يعتمد على إطار عمل Svelte في تطوير تطبيقات ذات ميزات متقدمة مثل التصيير من طرف الخادم Server-side Rendering وتقسيم الشيفرة والتوجيه المستند إلى الملفات والدعم دون الاتصال بالإنترنت، وهناك إطار عمل Svelte Native الذي يتيح بناء تطبيقات هاتف محمول أصيلة Native. كيفية عمل إطار عمل Svelte يمكن لإطار عمل Svelte توسيع لغات HTML و CSS وجافاسكربت نظرًا لكونه مصرِّفًا، مما يؤدي إلى إنشاء شيفرة جافاسكربت مثالية دون أيّ تكاليف تشغيل إضافية، إذ يوسّع إطار عمل Svelte تقنيات الويب الصرفة بالطرق التالية: يوسّع لغة HTML عن طريق السماح بتعابير جافاسكربت في شيفرة التوصيف وتوفير الموجّهات لاستخدام الشروط والحلقات بطريقة تشبه لغة Handlebars. يوسّع لغة CSS عن طريق إضافة آلية تحديد نطاق، مما يسمح لكل مكوِّن بتحديد تنسيقه الخاص دون التعرض لخطر التعارض مع تنسيق المكونات الأخرى. يوسّع لغة جافاسكربت من خلال إعادة تفسير موجّهات محددة للغة لتحقيق تفاعل حقيقي وتسهيل إدارة حالة المكوِّن. يتدخل المصرِّف فقط في مواقف محددة للغاية وفي سياق مكونات Svelte، كما تُعَدّ الامتدادات في لغة جافاسكربت قليلةً وتُنتَقى بعناية بهدف عدم تغيير صيغة جافاسكربت أو إبعاد المطورين، إذ ستعمل باستخدام لغة جافاسكربت الصرفة Vanilla JavaScript في أغلب الأحيان. الخطوات الأولى لاستخدام إطار Svelte لا يمكنك إضافة الوسم <script src="svelte.js"> إلى صفحتك واستيرادها إلى تطبيقك فقط، إذ سيتعين عليك إعداد بيئة التطوير للسماح للمصرِّف بتطبيق عمله. المتطلبات يجب تثبيت Node.js للعمل مع إطار عمل Svelte، إذ يوصَى باستخدام إصدار الدعم طويل الأمد LTS، كما يتضمن Node مدير الحزم npm ومشغّل الحزم npx. لاحظ أنه يمكنك استخدام مدير الحزم Yarn بدلًا من npm، لكننا سنفترض أنك تستخدِم npm في هذا المقال، ويمكنك مراجعة مقال أساسيات إدارة الحزم لمزيد من المعلومات حول npm وyarn. إذا استخدَمت نظام ويندوز، فيجب عليك تثبيت بعض البرامج لمنحك التكافؤ مع طرفية نظامَي يونكس Unix أو ماك macOS من أجل استخدام أوامر الطرفية المذكورة في هذا المقال، إذ يُعَدّ كل من Gitbash الذي يأتي على أساس جزء من مجموعة أدوات git لنظام ويندوز أو نظام ويندوز الفرعي لنظام لينكس -WSL اختصارًا- مناسبين، كما يُعَدّ برنامج Cmder بديلًا آخر جيدًا وكاملًا، ويمكنك مراجعة مقال سطر الأوامر للحصول على مزيد من المعلومات حول هذه الأوامر وأوامر الطرفية. إنشاء تطبيق Svelte الأول أسهل طريقة لإنشاء قالب تطبيق بسيط هي مجرد تنزيل قالب تطبيق البدء من خلال زيارة صفحة sveltejs/template على GitHub أو يمكنك تجنب الاضطرار إلى تنزيله وفك ضغطه واستخدام أداة degit فقط. أنشئ قالب تطبيق البدء وشغّل أوامر الطرفية التالية: npx degit sveltejs/template moz-todo-svelte cd moz-todo-svelte npm install npm run dev ملاحظة: تتيح degit تنزيل أحدث إصدار من محتويات مستودع Git وفك ضغطه، وهذا أسرع بكثير من استخدام git clone لأنه لن ينزّل كل محفوظات المستودع أو ينشئ نسخةً محليةً كاملةً. سيصرّف إطار عمل Svelte التطبيق ويبنيه بعد تشغيل الأمر npm run dev، كما سيشغّل خادمًا محليًا على المضيف المحلي localhost:8080، إذ يراقب Svelte تحديثات الملفات، ويعيد تلقائيًا تصريف وتحديث التطبيق نيابةً عنك عند إجراء تغييرات على الملفات المصدرية، ثم سيعرض متصفحك شيئًا يشبه ما يلي: بنية التطبيق يأتي قالب البدء بالبنية التالية: moz-todo-svelte ├── README.md ├── package.json ├── package-lock.json ├── rollup.config.js ├── .gitignore ├── node_modules ├── public │ ├── favicon.png │ ├── index.html │ ├── global.css │ └── build │ ├── bundle.css │ ├── bundle.js │ └── bundle.js.map ├── scripts │ └── setupTypeScript.js └── src ├── App.svelte └── main.js يتكون من المحتويات التالية: الملفان package.json و package-lock.json: يحتويان على معلومات حول المشروع التي يستخدمها Node.js ومدير الحزم npm لإبقاء المشروع منظمًا، ولا تحتاج إلى فهم هذين الملفين على الإطلاق، لكن إذا أردت معرفة المزيد عنهما، فاطلع على مقال أساسيات إدارة الحزم. node_modules: هو المكان الذي تحفظ فيه Node اعتماديات المشروع، ولن تُرسَل هذه الاعتماديات إلى مرحلة الإنتاج، وإنما ستُستخدَم فقط لأغراض التطوير. .gitignore: يحدِّد git الملفات أو المجلدات التي يجب تجاهلها من المشروع، وهذا مفيد إذا قررت تضمين تطبيقك في مستودع git. rollup.config.js: يستخدِم إطار عمل Svelte مجمّع الوحدات rollup.js، كما يوضّح ملف الإعداد كيفية تجميع وبناء تطبيقك، وإذا فضلت استخدام أداة Webpack، فيمكنك إنشاء مشروعك باستخدام الأمر npx degit sveltejs/template-webpack svelte-app بدلًا من ذلك. scripts: يحتوي على سكربتات الإعداد المطلوبة، ويجب أن يحتوي حاليًا على الملف setupTypeScript.js فقط. setupTypeScript.js: يضبط هذا السكربت دعم لغة TypeScript في إطارعمل Svelte. src: هذا المجلد هو المكان الذي توجد فيه شيفرة تطبيقك البرمجية، أي حيث ستنشئ شيفرة تطبيقك. App.svelte: هو مكوّن المستوى الأعلى لتطبيقك، إذ يصيّر حتى الآن الرسالة "Hello World!". main.js: نقطة الدخول إلى التطبيق. ينشئ نسخةً من المكون App ويربطها بجسم صفحة html. public: يحتوي هذا المجلد على جميع الملفات التي ستُنشَر في مرحلة الإنتاج. favicon.png: الرمز المفضل لتطبيقك، وهو شعار Svelte حاليًا. index.html: الصفحة الرئيسية لتطبيقك، وهي في البداية مجرد صفحة HTML5 فارغة تحمّل ملفات CSS وحزم JS التي ينشئها إطار عمل Svelte. global.css: يحتوي هذا الملف على تنسيقات غير محددة النطاق، وهو ملف CSS الذي سيُطبَّق على التطبيق بأكمله. build: يحتوي هذا المجلد على شيفرة CSS وجافاسكربت الناتجة. bundle.css: ملف CSS الذي أنشأه إطار عمل Svelte من التنسيقات المُعرَّفة لكل مكوّن. bundle.js: ملف جافاسكربت المُصرَّف من كل شيفرة جافسكربت المصدرية. مكون Svelte الأول المكونات هي اللبنات الأساسية لتطبيقات Svelte وتُكتَب في الملفات ذات اللاحقة .svelte باستخدام مجموعة شاملة من شيفرة HTML، كما تُعَدّ جميع الأقسام الثلاثة <script> و <style> والتوصيف Markup أقسامًا اختياريةً ويمكن أن تظهر بأيّ ترتيب تريده. <script> // ضع شيفرتك البرمجية هنا </script> <style> /* ضع تنسيقاتك هنا */ </style> <-- ضع التوصيف (أي عناصر HTML) هنا --!> لنلقِ نظرةً على الملف src/App.svelte المرفق مع قالب البداية، حيث يجب أن ترى شيئًا يشبه ما يلي: <script> export let name; </script> <main> <h1>Hello {name}!</h1> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> <style> main { text-align: center; padding: 1em; max-width: 240px; margin: 0 auto; } h1 { color: #ff3e00; text-transform: uppercase; font-size: 4em; font-weight: 100; } @media (min-width: 640px) { main { max-width: none; } } </style> القسم <script> تحتوي كتلة <script> على شيفرة جافاسكربت التي تُشغَّل عند إنشاء نسخة من المكوِّن، إذ تكون المتغيرات المصرَّح عنها أو المستوردة في المستوى الأعلى مرئيةً من شيفرة توصيف المكوِّن، وتُعَدّ متغيرات المستوى الأعلى الطريقة التي يتعامل بها إطار عمل Svelte مع حالة المكوِّن وتكون تفاعليةً افتراضيًا، إذ سنشرح بالتفصيل ما يعنيه ذلك لاحقًا. <script> export let name; </script> يستخدِم إطار العمل Svelte الكلمة export للتصريح عن المتغير بوصفه خاصيةً Prop، مما يعني أنه يصبح بإمكان مستخدِمي المكوِّن -مثل المكونات الأخرى- الوصول إليه، ويمثل هذا المثال توسعة إطار عمل Svelte لصيغة لغة جافاسكربت لجعلها أكثر فائدةً مع بقائها مألوفة. قسم التوصيف يمكنك إدراج أيّ شيفرة HTML تريدها في قسم التوصيف، كما يمكنك إدراج تعبير جافاسكربت صالح ضمن أقواس معقوصة مفردة {}، وسنضمّن في حالتنا قيمة الخاصية name بعد النص Hello مباشرةً. <main> <h1>Hello {name}!</h1> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> كما يدعم إطار عمل Svelte وسومًا مثل {#if...} و {#each...} و {#await...} التي تتيح لك تصييرًا مشروطًا لجزء من شيفرة التوصيف والتكرار على قائمة من العناصر والعمل بقيم غير متزامنة. القسم <style> إذا كانت لديك خبرة في العمل مع لغة CSS، فيجب أن يكون جزء الشيفرة التالية مفهومًا: <style> main { text-align: center; padding: 1em; max-width: 240px; margin: 0 auto; } h1 { color: #ff3e00; text-transform: uppercase; font-size: 4em; font-weight: 100; } @media (min-width: 640px) { main { max-width: none; } } </style> سنطبِّق تنسيقًا على العنصر <h1>، لذلك لا بدّ أنك تتساءل عمّا سيحدث للمكونات الأخرى التي تحتوي على عناصر <h1> ضمنها. يُحدَّد في إطار عمل Svelte نطاق شيفرة CSS ضمن كتلة <style> الخاصة بمكوِّن ما لهذا المكوِّن فقط من خلال إضافة صنف Class إلى العناصر المحدَّدة، ولا يضاف هذا الصنف عشوائيًا، وإنما يعتمد على قيمة مُعمَّاة Hash خاصة بتنسيق هذا المكوِّن. يمكنك رؤية جميع هذه الأمور عمليًا من خلال فتح المضيف المحلي localhost:5042 في تبويب متصفح جديد، ثم الضغط بزر الفأرة الأيمن أو الضغط على مفتاح Ctrl على العنوان HELLO WORLD! وتحديد الخيار فحص Inspect: يغيِّر إطار العمل Svelte عند تصريف التطبيق تعريف تنسيق العنصر h1 إلى h1.svelte-1tky8bj، ثم يعدِّل كل عنصر <h1> في المكوِّن إلى الشكل <h1 class="svelte-1tky8bj"> بحيث يُطبَّق التنسيق على العنصر الخاص بالمكوِّن المحدَّد فقط. ملاحظة: يمكنك تغيير هذا السلوك وتطبيق التنسيق على محدد Selector بطريقة عامة باستخدام المعدِّل :global(...). إجراء بعض التغييرات يمكنك تعديل المكوِّن App.svelte مثل تعديل العنصر <h1> في السطر رقم 6 من المكوِّن App.svelte بحيث يكون كما يلي: <h1>Hello {name} from MDN!</h1> يؤدي حفظ التعديلات إلى حفظ التطبيق المُشغَّل على المضيف المحلي localhost:5042 تلقائيًا. التفاعل في إطار العمل Svelte يعني التفاعل Reactivity في سياق إطار عمل واجهة المستخدِم أنّ إطار العمل يمكنه تلقائيًا تحديث نموذج DOM عند تعديل حالة أيّ مكون، إذ يُشغَّل التفاعل في إطار عمل Svelte عن طريق إسناد قيمة جديدة لأيّ متغير في المستوى الأعلى ضمن أحد المكونات، فيمكننا مثلًا تضمين دالة toggleName() في المكوِّن App وزر لتشغيلها. عدّل القسمين <script> والتوصيف كما يلي: <script> export let name; function toggleName() { if (name === 'world') { name = 'svelte' } else { name = 'world' } } </script> <main> <h1>Hello {name}!</h1> <button on:click={toggleName}>Toggle name</button> <p>Visit the <a href="https://svelte.dev/tutorial">Svelte tutorial</a> to learn how to build Svelte apps.</p> </main> ينفّذ إطار العمل Svelte الدالة toggleName() عند النقر على الزر، مما يؤدي إلى تحديث قيمة المتغير name. يُحدَّث عنوان Label العنصر <h1> تلقائيًا، إذ ينشئ إطار Svelte شيفرة جافاسكربت لتحديث نموذج DOM كلما تغيرت قيمة المتغير name دون استخدام نموذج DOM الافتراضي أو أيّ آلية توافق معقدة أخرى، ولاحظ استخدام : في on:click التي تُعَدّ صيغة Svelte للاستماع إلى أحداث DOM. فحص main.js: نقطة الدخول إلى التطبيق افتح الملف src/main.js حيث يُستورَد ويُستخدَم المكوِّن App، إذ يُعَدّ هذا الملف نقطة الدخول لتطبيقنا، ويبدو في البداية كما يلي: import App from './App.svelte'; const app = new App({ target: document.body, props: { name: 'world' } }); export default app; يبدأ الملف main.js باستيراد مكوِّن Svelte الذي سنستخدِمه، ثم ينشئ نسخةً منه في السطر رقم 3، ويمرّر كائنًا له الخاصيات التالية: target: عنصر DOM الذي نريد تصيير المكوِّن ضمنه، وهو العنصر <body> في هذه الحالة. props: القيم المراد إسنادها لكل خاصية للمكوِّن App. نظرة إلى خلفية إطار Svelte لا بدّ أنك تتساءل عن كيفية تمكّن إطار Svelte من جعل كل هذه الملفات تعمل مع بعضها البعض بطريقة صحيحة، إذ يعالِج مصرّف Svelte القسم <style> لكل مكوِّن ويصرّفه في الملف public/build/bundle.css، ويصرّف قسمَي التوصيف و<script> لكل مكوِّن ويخزن النتيجة في الملف public/build/bundle.js، كما يضيف الشيفرة البرمجية في الملف src/main.js للإشارة إلى ميزات كل مكوِّن يتضمن الملف public/index.html الملفين bundle.css و bundle.js: <!DOCTYPE html> <html lang="en"> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1'> <title>Svelte app</title> <link rel='icon' type='image/png' href='/favicon.png'> <link rel='stylesheet' href='/global.css'> <link rel='stylesheet' href='/build/bundle.css'> <script defer src='/build/bundle.js'></script> </head> <body> </body> </html> حجم الإصدار المصغر من الملف bundle.js أكثر بقليل من 3 كيلوبايت، والذي يتضمن "وقت تشغيل Svelte" -أي 300 سطر فقط من شيفرة جافاسكربت- والمكوِّن المُصرَّف App.svelte، كما يُعَدّ الملف bundle.js ملف جافاسكربت الوحيد الذي يشير إليه الملف index.html، ولا توجد مكتبات أخرى مُحمَّلة في صفحة الويب. تُعَدّ هذه المساحة أصغر بكثير من الحزم المُصرَّفة في أطر عمل أخرى، وضَع في الحسبان أنه لا يقتصر الأمر على حجم الملفات التي يجب تنزيلها في حالة تجميع الشيفرة البرمجية، والتي هي شيفرة قابلة للتنفيذ يجب تحليلها وتنفيذها والاحتفاظ بها في الذاكرة، لذلك يحدث ذلك فرقًا حقًا خاصةً في الأجهزة ذات الإمكانات المنخفضة أو التطبيقات ذات الاستخدام الكبير لوحدة المعالجة المركزية. متابعة هذه السلسلة من المقالات سنبني في هذه السلسلة من المقالات تطبيق ويب كامل، لذلك وفّرنا مستودع GitHub مع مجلد يحتوي على شيفرة التطبيق البرمجية الكاملة، كما يمكنك قراءة المحتوى فقط للحصول على فهم جيد لميزات Svelte، ولكن ستحصل على أقصى استفادة من هذا السلسلة من المقالات إذا اتبعت شيفرة التطبيق معنا، كما سنوفر مستودع GitHub مع مجلد يحتوي على شيفرة التطبيق البرمجية كما هي في بداية كل مقال لتسهيل متابعته. يوفِّر إطار Svelte أداة REPL على الإنترنت، وهي أداة لتطبيقات Svelte ذات الشيفرة الحية المباشرة على الويب دون الحاجة إلى تثبيت أيّ شيء على جهازك، ولنتحدث الآن عن كيفية استخدام هذه الأدوات. استخدام Git يُعَدّ Git أكثر أنظمة التحكم في الإصدارات شيوعًا مع GitHub، وهو موقع يوفِّر استضافةً لمستودعاتك والعديد من الأدوات للعمل بها، وسنستخدِم GitHub لتتمكّن من تنزيل الشيفرة البرمجية بسهولة. يجب تنفيذ الأمرالتالي بعد تثبيت Git لنسخ المستودع: git clone https://github.com/opensas/mdn-svelte-tutorial.git يمكنك إدخال الأمر cd في المجلد المقابل وبدء تشغيل التطبيق في وضع التطوير dev لترى ما يجب أن تكون عليه حالة التطبيق الحالية كما يلي: cd 02-starting-our-todo-app npm install npm run dev ملاحظة: إذا أردت تنزيل الملفات فقط دون نسخ مستودع Git، فيمكنك استخدام الأداة degit في الأمر npx degit opensas/mdn-svelte-tutorial، كما يمكنك تنزيل مجلد محدد باستخدام الأمر npx degit opensas/mdn-svelte-tutorial/01-getting-started، ولن تنشئ الأداة degit مستودع git محلي، وإنما ستنزّل ملفات المجلد المحدَّد فقط. استخدام أداة REPL في إطار عمل Svelte تُعَدّ أداة REPL (أي حلقة قراءة-تقييم-طباعة read–eval–print Loop) بيئةً تفاعليةً تسمح بإدخال الأوامر والاطلاع على النتائج مباشرةً، وتوفِّر العديد من لغات البرمجة أداة REPL، كما تُعَدّ حلقة REPL في إطار Svelte أداةً عبر الإنترنت تتيح إنشاء تطبيقات كاملة وحفظها عبر الإنترنت ومشاركتها مع الآخرين، إذ تُعَدّ أسهل طريقة لبدء العمل باستخدام Svelte من أيّ جهاز دون الحاجة إلى تثبيت أيّ شيء، كما يستخدمها مجتمع Svelte على نطاق واسع، لذا إذا أردت مشاركة فكرة أو طلب المساعدة أو الإبلاغ عن مشكلة، فيمكنك إنشاء نسخة REPL توضِّح المشكلة. لنلقِ نظرةً سريعةً على أداة REPL في إطار عمل Svelte وكيفية استخدامها، حيث تبدو كما يلي: افتح المتصفح وانتقل إلى أداة REPL. لنتعرف على محتوياتها: سترى شيفرة مكوناتك على الجانب الأيسر من الشاشة، وسترى على اليمين خرج تنفيذ تطبيقك. يتيح لك الشريط الموجود أعلى الشيفرة إنشاء ملفات .svelte و .js وإعادة تنظيمها، كما يمكنك إنشاء ملف ضمن مجلد من خلال تحديد اسم المسار الكامل components/MyComponent.svelte ثم سيُنشَأ المجلد تلقائيًا. يوجد عنوان أداة REPL فوق هذا الشريط. يمكنك الضغط عليه لتعديله. يوجد على الجانب الأيمن ثلاث تبويبات هي: يعرض تبويب "النتيجة Result" خرج التطبيق، ويوفِّر طرفيةً Console في الأسفل. يتيح تبويب "JS output" فحص شيفرة جافاسكربت التي أنشأها إطار عمل Svelte ويضبط خيارات المصرّف Compiler. يعرض تبويب '"CSS Output" شيفرة CSS التي أنشأها إطار عمل Svelte. ستجد شريط أدوات فوق التبويبات، حيث يتيح شريط الأدوات الدخول إلى وضع ملء الشاشة وتنزيل تطبيقك، فإذا سجّلتَ الدخول باستخدام حساب GitHub، فستتمكّن من نسخ التطبيق وحفظه، وستتمكن من رؤية جميع أدوات REPL المحفوظة من خلال النقر على اسم مستخدِم حسابك على GitHub وتحديد التطبيقات المحفوظة. كلما عدّلت أيّ ملف على REPL، فسيعيد إطار عمل Svelte تصريف التطبيق وتحديث تبويب النتيجة، كما يمكنك مشاركة تطبيقك من خلال مشاركة عنوان URL مثل رابط REPL الخاص بتشغيل تطبيقنا الكامل. ملاحظة: لاحظ كيف يمكنك تحديد إصدار Svelte في عنوان URL، إذ يكون ذلك مفيدًا عند الإبلاغ عن المشاكل المتعلقة بإصدار معيّن من إطار Svelte. ملاحظة: لا يمكن لأداة REPL حاليًا التعامل مع أسماء المجلدات بطريقة صحيحة، فإذا أردت متابعة الخطوات التي نطبّقها، فأنشئ جميع مكوناتك ضمن المجلد الجذر، فإذا رأيت مسارًا في الشيفرة import Todos from './components/Todos.svelte'، فضَع مكانه عنوان URL مسطح Flat مثل import Todos from './Todos.svelte'. يمكنك نسخ مستودع github -إذا لم تفعل ذلك مسبقًا- باستخدام الأمر التالي: git clone https://github.com/opensas/mdn-svelte-tutorial.git ثم يمكنك الوصول إلى حالة التطبيق الحالية من خلال تشغيل الأمر التالي: cd mdn-svelte-tutorial/01-getting-started أو يمكنك تنزيل محتوى المجلد مباشرةً كما يلي: npx degit opensas/mdn-svelte-tutorial/01-getting-started تذكَّر تشغيل الأمر npm install && npm run dev لبدء تطبيقك في وضع التطوير، فإذا أردت متابعتنا، فابدأ بكتابة الشيفرة باستخدام الأداة REPL من هنا. الخلاصة ألقينا في هذا المقال النظرة الأولية إلى إطار عمل Svelte بما في ذلك كيفية تثبيته محليًا وإنشاء تطبيق بدء بسيط وكيفية عمل الأساسيات، كما سنبدأ في المقال التالي ببناء أول تطبيق وهو تطبيق قائمة المهام، ولنلخص بعض الأشياء التي تعلمناها في Svelte وهي: تعريف سكربت وتنسيق وتوصيف كل مكون في ملف .svelte واحد. يُصرَّح عن خاصيات المكونات باستخدام الكلمة export. يمكن استخدام مكونات Svelte فقط عن طريق استيراد ملف .svelte المقابل. يجب تحديد نطاق تنسيق المكونات، مما يمنعها من التضارب مع بعضها البعض. يمكنك تضمين أيّ تعبير جافاسكربت في قسم التوصيف Markup بوضع هذا التعبير بين قوسين معقوصين. تشكّل متغيرات المستوى الأعلى للمكوِّن حالته. يمكن إطلاق التفاعل عن طريق إسناد قيمة جديدة لمتغير المستوى الأعلى. ترجمة -وبتصرُّف- للمقال Getting started with Svelte. اقرأ أيضًا استخدام أطر العمل في برمجة تطبيقات الويب: فلاسك نموذجا مقدمة في بناء تطبيقات الويب باستخدام إطار العمل Angular وقاعدة بيانات Firestore أساسيات بناء التطبيقات في إطار العمل Laravel 5 البدء مع إطار العمل جانغو لإنشاء تطبيق ويب.
-
ستعرف في هذا المقال ما إذا كان مجال إدارة المنتجات مناسبًا لك وكيفية الانتقال إليه والمهارات التي ستحتاجها لتكون مدير منتجات ناجح. يُعَد دور مدير المنتجات Product Manager -أو PM اختصارًا- هو الدور الأكثر جاذبيةً ضمن الفرق التقنية حاليًا، إذ يكون مديرو المنتجات أقرب إلى مركز العمل، ولديهم قدر متباين من التأثير على القرارات الرئيسية، كما يتطورون في أغلب الأحيان لتأسيس شركاتهم الخاصة. إذًا ليس مستغربًا أن تبدأ إدارة المنتجات في التربّع على قمة قوائم أفضل وأشهر الوظائف الواعدة في الولايات المتحدة (وليس في المجال التقني فقط). قررتُ -يقول الكاتب- قبل سبع سنوات بعد الانضمام إلى شركة Airbnb أن أنفّذ هذه القفزة النوعية بنفسي، وانتقلت من الهندسة إلى المنتجات، وأصبحت أحد أوائل مديري المنتجات في شركة Airbnb. ساعدت منذ ذلك الحين عددًا من الأشخاص داخل شركة Airbnb وخارجها على الانتقال إلى المنتجات من وظائف أخرى، مثل الوظائف المتعلقة بالعمليات وعلوم البيانات والشؤون المالية. سأقدّم في هذا المقال خلاصة كل ما تعلمته وما أوصي به عند إجراء مثل هذا الانتقال، وسأوضّح كيفية التأكد من أن دور مدير المنتجات مناسب لك، إلى جانب كيفية تولي دورك الأول بصفتك مدير منتجات والمهارات التي ستحتاجها من أجل النجاح فيه. ابدأ بالسبب: تأكد من أن دور مدير المنتجات مناسب لك لم أخطط مطلقًا لأن أصبح مدير منتجات، ولكنني لا أستطيع الآن تخيّل نفسي أشغل أيّ دور آخر ضمن مؤسسة. يمكن أن يكون هذا الدور غير مجدٍ ومرهقًا للأعصاب ومتعبًا، ولكنك ستشعر وكأنك وُلدت لتكون مدير منتجات عند النجاح، لذا يُفضَّل إجراء تقييم صادق لما يدفعك لتكون مدير منتجات، وهل هذا الدور مناسب لك حقًا قبل البدء به. يمكنك أن تكون مدير منتجات إذا تحقّق لديك ما يلي: حل مشاكل الآخرين (لكل من مستخدميك وفريقك). توجيه الأعمال للنمو. العمل بالقرب من مجموعة متنوعة من الأشخاص. تطوير استراتيجية. إنهاء جميع مهامك. قيادة فريق (من خلال تأثيرك وليس سلطتك). التواصل في كثير من الأحيان وعلى نطاق واسع. اتخاذ القرارات. خلق تجارب مدهشة للناس. أن تكون منظمًا ومُوجَّهًا بالتفاصيل ومستعدًا. لا يمكنك أن تصبح مدير منتجات إذا كان لديك ما يلي: التقدير. أن تستخدم طريقتك فقط. أن تكون منعزلًا. أن تكون دائمًا على حق. تصميم أو بناء الأشياء بنفسك. الجميع معجب بك. حالات التركيز الكامل. تجنب الاجتماعات. تجنب البريد الإلكتروني. تجنب الناس. لست بحاجة إلى تتوفّر جميع هذه العناصر فيك، ولكن إذا شعرت أنك تمتلك الصفات الجوهرية فقط، فاستمر في القراءة. التخطيط لكيفية الانتقال: المسارات الأربعة الأكثر شيوعا للوصول إلى دور مدير المنتجات سنتعرّف الآن على كيفية حصول معظم الأشخاص على أول فرصة لهم في مجال إدارة المنتجات بناءً على تجربتي الشخصية والتجارب العديدة التي اطلعتُ عليها: الانتقال الداخلي في شركة كبيرة: يُعَد المسار الأسهل والأسرع، ولكنه يتطلب تحقيق ثلاثة أشياء هي: عملية نقل داخلي من نوع ما، مع وجود فرصة لإظهار المهارات التي سأوضحها لاحقًا، والأهم من ذلك هو وجود مدير المنتجات الداخلي الذي سيكون بطل انتقالك (مديرك الجديد مثلًا). وإذا تحقق شرطان من هذه الشروط الثلاثة، فابحث عن طريقة لتحقيق الشرط الثالث، وإلّا اتبع أحد المسارات الأخرى. العثور على وظيفة مدير منتجات PM مبتدئ في شركة كبيرة: يُحتمَل أن يكون المسار الأكثر شيوعًا، ولكنه يقتصر على الشركات التي لديها برامج مدير المنتجات المساعد APM أو برامج التدريب. يُعَد هذا المسار طريقًا شائعًا بصورة متزايدة لخريجي ماجستير إدارة الأعمال، بالرغم من أن ماجستير إدارة الأعمال ليس ضروريًا لتصبح مدير منتجات. وإن لم يكن لديك ماجستير في إدارة الأعمال، فابحث في برامج تعليمية لذلك، وإلّا ابحث عن طرق لممارسة المهارات التي سأوضحها لاحقًا في وظيفتك الحالية، فمفتاح هذا المسار هو إثبات أنك ذكي ويمكن قيادتك ولديك نقاط قوة فطرية من هذه المهارات. الانضمام إلى شركة ناشئة عند الحاجة الملحة: مفتاح هذا المسار هو وجود اتصالات مع مؤسسي شركة ناشئة، وإظهار الكثير من النشاط، وتحقيق النجاح عندما تتاح لك الفرصة. ابحث عن وظائف في الشركات الناشئة، وابحث عن طريقة لمقابلة المؤسسين، مع التركيز على تطوير المهارات التي سنوضحها لاحقًا. يُعَد امتلاك عقلية نمو قوية أمرًا أساسيًا، إذ يجب عليك تعلم كيفية تطبيق تلك المهمة بسرعة. يمكنك في هذا المسار إما البدء بصفتك مدير منتجاب مؤسس أو الانتقال إليه بعد أداء العمل لفترة من الوقت كما دخل رئيس المنتجات الأصلي في شركة Airbnb إلى مجال إدارة المنتجات. البدء بشركتك الخاصة: يُعَد هذا المسار إلى حدٍ بعيد المسار الأكثر الذي يتطلب عملًا، ولا يُخطَّط له إلّا نادرًا، لكنه مع ذلك ينجح، وهذه هي الطريقة التي دخلتُ بها شخصيًا إلى إدارة المنتجات. يصبح المديرون التنفيذيون مديرو منتجات بعد الاستحواذ لأنهما وظيفتان متشابهتان جدًا، ويمكن للمؤسسين بدلًا من ذلك تولي دور مدير المنتجات في شركتهم الخاصة مع نمو الشركة. لا أوصي بإعطاء الأولوية لهذا المسار لتصبح مدير منتجات، ولكن هذا شيء يجب مراعاته عند بدء شركتك. قد يبدو هذا التحول الوظيفي غامضًا وعشوائيًا في أغلب الأحيان، إلا أنه يجب أن يمنحك الراحة، وذلك لأن كل مدير منتجات يعمل حاليًا قد مر بهذا التحوّل بشكل أو بآخر. وإذا كنت لا تزال متشككًا في إمكانية تحقيق ذلك، يمكنك الاطلاع على رأي بعض مدراء المنتجات من خلال خبرتهم في الأمر في مقالات ما الذي تعلمته في أول عام لي كمدير منتجات، وهل ينبغي على المصممين إدارة المنتجات؟ تمتلك فكرة منتج وترغب بإطلاقه؟ تعلم إدارة تطوير المنتجات مع أكاديمية حسوب بدءًا من دراسة السوق وتحليل المنافسين وحتى إطلاق منتج مميز وناجح اشترك الآن تطوير المهارات الأساسية السبع لتكون مدير منتجات يختلف دور إدارة المنتجات باختلاف الشركة والفريق، ولكن -من واقع خبرتي- فإن المهارات السبعة الآتية (مرتبة حسب الأهمية) هي الأهم لتبنيها في وقت مبكر من حياتك المهنية في منصب مدير المنتجات. سأضع -يقول الكاتب- نظرة ًعامةً موجزةً عن كل مهارة، مع تضمين مجموعة من الاقتراحات الملموسة حول كيفية تطوير هذه المهارة، بالإضافة إلى روابط إلى كتاباتي المفضلة حول كل موضوع من خبراء المجال. لا تقلق بشأن هذه المهارات بل ضاعف نقاط قوتك وسد ثغراتك. 1. أخذ أي مشكلة والقدرة على تطوير استراتيجية لحلها تتمثل وظيفة مدير المنتجات في تنسيق موارد فريقه لدفع قيمة الأعمال، حيث سيواجه فريقك مشاكلًا مثل نمو اعتماد منتج وتقليل حدوث فقدان في الخدمة وزيادة تحويل التدفق، وستكون مسؤوليتك هي توجيه فريقك لتقليل المشكلة. وتُعَد الاستراتيجية المقترنة برؤية واضحة طريقَك للانتقال من مشكلة إلى حل. إليك بعض التوصيات لتطوير تفكيرك الاستراتيجي: اطلب من أفضل مديري المنتجات الذين تعرفهم أن يتحدثوا إليك عن الرؤية والاستراتيجية التي طوروها لمبادرة منتج حالي (ثق بي سيكونون متحمسين للمشاركة). تعامل مع مشكلة تواجهها شركتك الحالية أو صديقك وتوصّل إلى إطار عمل يقسم هذه المشكلة إلى أجزاء قابلة للحل. تعرّف على الفرق بين الرؤية والرسالة والاستراتيجية. اقرأ عن الاستراتيجية والتحليل الاستراتيجي. اقرأ عن الإدارة الاستراتيجية وكيفية تحقيق الميزة التنافسية. اقرأ عن كيفية اختيار مدير المنتجات لمقاييس أداء صحيحة. اقرأ عن المنتج وتصنيفه ودورة حياته. اقرأ عن كيفية إيجاد المشاكل وحلها لتتعرّف على طرق حل المشاكل. اقرأ حول استراتيجية العمل العكسية في أمازون وتدرّب عليها. اقرأ عن مفارقات إدارة المنتجات: الاستراتيجية الجيدة والرؤية السيئة. 2. تنفيذ وإنهاء جميع المهام لمدير المنتجات الذي لا يجيد أيّ شيء آخر غير التنفيذ قيمةٌ كبيرة للفريق، بينما يكون الفريق الذي لديه مدير منتجات لا يستطيع تنفيذ أيّ شيء أفضل حالًا بدونه. يتضمن ذلك من الناحية التخطيطية أشياءً مثل بناء خارطة طريق Roadmap يتماشى معها كل فرد في فريقك، وتحديد المواعيد النهائية والوفاء بها، وإلغاء المعوقات بحزم. يُفضَّل أن يمارس مديرو المنتجات الجدد هذه المهارة مباشرةً. إليك بعض التوصيات لتطوير مهاراتك في التنفيذ: ستتعلم كيفية التنفيذ من خلال العمل فقط. ابحث عن مدير منتجات لطيف واطلب تولي مهام إدارة المنتجات لأحد مشاريعه (نصيحة مهمة: لا تطلق على مدير المنتجات اسم مدير مشروع أبدًا). انتبه إلى الأشخاص من حولك الذين يجيدون القيادة وكيف يديرون الاجتماعات ويعالجون المشاكل عند ظهورها والأنظمة التي يستخدمونها للحفاظ على توافق فريقهم. تعرّف على مبدأ الأهداف والنتائج الرئيسية OKR وتحديدها وتأكد من الاطلاع على كيفية تحديد التي تضع بها أهدافك وتتابعها مع نظام الأهداف والنتائج الرئيسية. اسأل نفسك دائمًا "كيف سأحرز تقدمًا في منتجي اليوم؟" كما يقترح ساشين رخي Sachin Rekhi. اقرأ عن كيفية تطوير المنتج. اقرأ مقال عن وظيفة مدير المنتجات. اقرأ عن بعض النصائح المفيدة للدخول في مجال إدارة المنتجات. 3. التواصل يكتب المهندسون الشيفرة البرمجية وينتج المصممون التصميمات، أما مديرو المنتجات فهم يتواصلون، فكل ما تفعله بوصفك مدير منتجات يمكن تحقيقه من خلال الكتابة والتحدث والاجتماعات، كما يقول بوز Boz: "التواصل هو الوظيفة". لا يمكنك أبدًا أن تكون جيدًا للغاية في هذا الأمر، ويُعَد الإفراط في التواصل أمرًا صعبًا. إليك بعض التوصيات لتنمية مهاراتك في التواصل: رسائل البريد الإلكتروني: أجبر نفسك على النظر إلى بريدك الإلكتروني مرةً واحدةً على الأقل قبل إرساله، فهناك دائمًا شيء يمكنك قصه أو توضيحه. اطلع على استراتيجية البريد الإلكتروني بالأسلوب الدقيق المُستخدَم لدى العسكريين التي أفضّلها. اعلم أن الإفراط في التواصل أمر صعب جدًا. المستندات: اطلب دائمًا ملاحظات من شخص واحد على الأقل قبل مشاركة المستند على نطاق واسع، وركّز على التنسيق النظيف والمتناسق، وأغلق التعليقات قبل مشاركتها مع المديرين التنفيذيين. اجعل المستندات سهلة للاطلاع عليها، واستمر في دفع نفسك لتتعلم الكتابة بصورة أفضل. الاجتماعات: ضمّن الهدف الأساسي للاجتماع في دعوتك مع جدول الأعمال بصورة مثالية. وإذا حضرت اجتماعًا لا تشعر أنه مثمر، فاستدعِ الموظفين لاجتماع آخر. ادعُ أقل عدد ممكن من الأشخاص، واخرج من الاجتماع بعناصر عمل واضحة، وتابع عبر البريد الإلكتروني مع عناصر العمل والمالكين. العروض التقديمية: هل أنت متأكد من أنك بحاجة إلى تقديم عرض تقديمي مقابل إرسال بريد إلكتروني؟ تأكد من أن جمهورك يعرف الهدف من العرض التقديمي، فهل تبحث عن قرار أو ملاحظات عامة أم مجرد مشاركة للمعلومات؟ ليس الأمر واضحًا كما تعتقد. احصل على ملاحظات على عرضك التقديمي مما يمكّنك من توفير وجهات نظر وطرق تفكير جديدة. اجعل عرضك التقديمي قصيرًا، فلا أحد يرغب في أن يستمر العرض التقديمي لفترة أطول. سرد القصص: هي مهارة تعريفية ستجعلك أفضل في كل ما سبق، ويُعَد إطار عمل SCR أداةً ممتازةً لإعداد عرضك التقديمي. 4. القيادة من خلال التأثير يتحمل مدير المنتجات جميع المسؤوليات بدون أيّ سلطة فعلية، فإدارة المنتجات صعبة وغير عادية إلى حد ما، ولكنها مرضية جدًا عند النجاح بها. لذا يجب أن تكون قادرًا على بناء الثقة مع زملائك في الفريق واتخاذ القرارات وإعطاء صوت للجميع، والحفاظ على الروح المعنوية بغض النظر عمّا يحدث للنجاح في هذا المجال. سرعان ما يصبح أفضل مديرو المنتجات قادة الفريق الفعليين، ليس بسبب أيّ سلطة فعلية، بل لأنهم يساعدون كل فرد في الفريق على تنفيذ أفضل عمل في حياتهم. إليك بعض التوصيات لتطوير مهاراتك في قيادة المنتجات: راقب مديري المنتجات من حولك عن كثب، وتعلّم كيف ينجزون الأمور بينما لا يديرون أيّ شخص في الفريق فعليًا. ابحث عن مشروع صغير يمكنك قيادته، واعمل بصورة مثالية مع مدرب مدير منتجات. عزّز الثقافة ضمن أيّ فريق أنت فيه من خلال إنشاء طقوس وعادات ممتعة والتخطيط للنزهات وإنشاء هوية دائمة لفريقك. اقرأ عن مهارة إدارة المنتجات الأكثر تقديرًا وهي التأثير على الموظفين بدون سلطة مباشرة. اقرأ عن سمات مديري المنتجات وعن أهم الدروس التي تحتاجها لتصبح مدير منتجات ناجح. اقرأ عن الأمور التي تجعل مدير منتجات ناجحًا. اقرأ مقال الدليل الواضح لمدير منتجات ناجح. اقرأ عن الأدوات التي يحتاجها كل مدير منتجات ليكون ناجحًا. اقرأ عن مفهوم القيادة والفرق بين كل من القائد والمدير. 5. اتخاذ القرارات المبنية على البيانات تلجأ الفرق عمومًا إلى مدير المنتجات لمساعدتهم على اتخاذ القرارات، لذا يجب أن تتوقع اتخاذ عشرات القرارات نيابة عن الفريق يوميًا. أفضل صديق لك في اتخاذ القرارات هو مجموعة واضحة من المبادئ التي جرى الاتفاق عليها سابقًا والبيانات الثابتة (الكمية والنوعية). وكلما قلت الآراء التي يجب عليك الاعتماد عليها، زادت الحقائق التي تكون تحت تصرفك وأصبحت حياتك أسهل. إليك بعض التوصيات لتطوير مهارات اتخاذ القرار: ادرس كيف تتخذ الشركات الناجحة قرارات من خلال التجريب مثل شركات Airbnb وأوبر Uber ونتفليكس Netflix وبنترست Pinterest، وابحث عن طريقة لبدء تجربة أو اثنتين حيث تعمل حاليًا. راقب القادة الناجحين من حولك كيف يتخذون القرارات، وما مدى سرعة اتخاذهم لها، وما الذي يسألون عنه قبل اتخاذ القرار، وكيف يحوّلون تفكيرهم. امتلك دائمًا وجهة نظر في القرار المُتخَذ (وجهة نظرك الخاصة)، ولكن كن مستعدًا لتغيير رأيك في ضوء المعلومات الجديدة. اقرأ عن كيفية معالجة الدماغ للمعلومات لاتخاذ القرارات: النظام التأملي والنظام الانفعالي. اقرأ عن كيفية اتخاذ القرارات الإدارية. اقرأ عن كيفية تحسين جودة اتخاذ القرار. اقرأ عن أثر الحدس والبيانات على إدارة المنتجات. تأكد دائمًا أنّ مهمتك ليست أن تجعل كل عميل محتمل سعيدًا. لا بأس في أن تقول لا. 6. بناء منتجات رائعة وامتلاك ذوق رفيع ستبني منتجًا لأشخاص آخرين في النهاية، لذا سترغب في اكتساب بعض الخبرة، حيث تلعب جميع المهارات الأخرى المذكورة سابقًا دورًا في هذا الأمر، ولكن هناك بعض المهارات الفريدة التي يجب تطويرها بما في ذلك بناء حدس بما يجعل المنتج رائعًا وكيفية إيجاد التوازن بين الفن والعلم وأفضل طريقة للعمل مع التخصصات الأخرى. إليك بعض التوصيات لتطوير إحساسك بالمنتجات: اعثر على طريقة لبناء منتج بنفسك أو مع الأصدقاء، وانقله إلى العالم الخارجي. ابحث عن مشكلة صغيرة وحاول حلها لشخص ما (أو لنفسك). لا توجد طريقة لتعلم شيء ما أفضل من أن تطبّقه بنفسك. تعلّم أن تلاحظ ما الذي يجعلك تحب المنتج وما لا يعجبك فيه. ضع في بالك تدوين الملاحظات حول ما يجعل المنتجات جيدة وما يجعلها سيئة. تعرّف على التفكير التصميمي. اقرأ عن كيفية تطوير منتج جديد خطوةً بخطوة. تعلّم كيفية التعامل مع أفراد فريقك من مصممين ومهندسين. اقرأ عن انعكاسات الحدس ولبيانات على عملية ونتائج إدارة المنتجات. أتقن الانضباط في مجال إدارة المنتجات. اقرأ عن أفضل إطارات العمل التي يجب أن يعرفها مديرو المنتجات. 7. كن مستعدا دائما إحدى أكثر السمات التي جرى التقليل من شأنها ولكنها الأكثر قيمةً بالنسبة لمدير المنتجات هي ما أسميه هالة "سأتولى أمر هذا الشيء". يجب أن يشعر الجميع أنه إذا أخذت شيئًا ما، فستنجزه بصورة جيدة. المفتاح لبناء هذه الهالة هو أن تصبح مُوجَّهًا نحو التفاصيل بصورة متزايدة وأن تكون أكثر استعدادًا من أي شخص آخر وأن يكون لديك مرتبة أعلى ممّن حولك. كنت شخصيًا سيئًا للغاية في هذا الأمر في البداية، وشهدت الكثير من النمو في مسيرتي المهنية بمجرد أن أعطيت الأولوية لهذه المهارات. إليك بعض التوصيات لتطوير هالة "سأتولى أمر هذا الشيء": لا تحضر أيّ اجتماع دون قضاء بضع دقائق على الأقل في التحضير وتجميع أفكارك. احتفظ بمستوٍ مرتفع لكل ما تفعله. أعد قراءة التوصيات الواردة في البند رقم 3 (التواصل). ابنِ "حاستك السادسة"، وانظر للأمام وحول الزوايا، وخطط للأمور الطارئة التي يمكن أن تحدث وكيفية الاستعداد لها. ابحث عن أشخاص منظمين جيدًا من حولك وتعلم منهم عمليًا ماذا يفعلون للبقاء على علم بكافة الأمور. اقرأ عن دور مدير المنتجات ومسؤولياته وتعرّف على أهم مهارات مدير المنتجات. تعلّم كيف تدير وقتك بصفتك مدير منتجات. مهارات يجب بناؤها بمرور الوقت لمواصلة التفوق الرؤية: حدد رؤيةً لفريقك وبيّنها بوضوح، واجعل فريقك وأصحاب المصلحة يشتركون فيها. حس الأعمال: افهم ما يحرك الأعمال، وساعد فريقك وشركتك على بناء الأشياء الصحيحة بالترتيب الصحيح. الهوس بالتأثير: اربط كل ما تفعله بالتأثير الذي سيحدِثه على عملك وعملائك. عقلية النمو: انظر إلى نفسك وإلى مَن حولك على أنهم دائمو التطور وقادرون على التحسّن. الخلاصة يكون الطريق طويلًا وغير مُتنبَّأ به لتصبح مدير منتجات في أغلب الأحيان، ولكنه من أكثر الأشياء إثارة للاهتمام. يمكنك اختصار وظيفة مدير المنتجات في أربع كلمات: "اكتشف ما هو التالي". إذًا ما الذي يأتي لاحقًا في رحلتك إلى إدارة المنتجات؟ حسنًا، نصيحتي هي البدء في تطوير وإظهار المهارات التي حددناها سابقًا. اقرأ وعالج وضع ما تعلّمته موضع التنفيذ بالطريقة الممكنة، إذ يُعَد الإبداع جزءًا من كونك مدير منتجات، ومهمتك هي أن تكون مستعدًا قدر الإمكان عندما تسمح الفرصة، وأثناء ذلك اسلك مسارًا أو اثنين من المسارات السابقة التي اقترحتها. يمكن أن يستغرق ذلك سنة أو سنتين أو ثلاثة، فإن لم تصل إلى أيّ مكان، فجرّب مسارًا آخر. قابل مديري منتجات، واستمع إلى ملاحظاتهم ونفّذها. إذا اعتقدتَ حقًا أن هذا هو الدور مناسب لك، فابحث عن طريقة للوصول إليه. ترجمة -وبتصرُّف- للمقال How To Get Into Product Management (And Thrive) لصاحبه Lenny Rachitsky. اقرأ أيضًا كيفية الدخول في مجال إدارة المنتجات وإتقانه دليل إرشادي للدخول في مجال إدارة المنتجات مجموعة مصادر مهمة تساعد على دخول مجال إدارة المنتج
-
تقع مسؤولية اختيار الحل الصحيح من خلال تقييم كل جانب من جوانب المشكلة وحلولها الممكنة على عاتق مديري المنتجات، وتكون التحديات الأكثر شيوعًا التي يواجهها مديرو المنتجات سببها المقايضات Trade-Offs، حيث يمكنك تعلم كيفية التعامل مع هذه المقايضات من خلال وضع بعض الأشياء التي سنوضحها لاحقًا في الحسبان مع وجود عملية صنع قرار منظّمة. يحتاج مديرو المنتجات عادةً إلى تقييم المقايضات التي تغطي ثلاثة مجالات هي: المجال التقني ومجال الأعمال ومجالات التصميم، حيث ينتهي الأمر بالقرارات التي تفضّل جانبًا ما إلى المساومة على الجانب الآخر في أغلب الأحيان. لنلقِ نظرةً على بعض الأشياء التي يجب وضعها في الحسبان أثناء التعامل مع المقايضات قبل الاطلاع على نهج فعّال للتعامل معها في إدارة المنتجات. بعض الأشياء التي يجب أن يتذكرها مدير المنتجات حول المقايضات بما أن عملية صنع القرار فوضوية وغير منظمة بسبب المقايضات، فلا تريد استخدام طريقة التفكير الخاطئة التي تجعلها غير فعالة وتستغرق وقتًا طويلًا. سنوضح فيما يلي بعض الأشياء التي يجب وضعها في الحسبان والتي ستحسّن عملية اتخاذ القرار. لا تسع إلى الكمال لن تكون قادرًا على إيجاد حل قابل للتطبيق لمشكلة في مجالٍ مثل إدارة المنتجات إذا أنصتَ باستمرار إلى الكمال في عقلك، إذ يكمن فهم حل المشكلة في الفروق الدقيقة في المقايضة التي ستساعدك على تقييم المنهجيات بصورة أفضل واتخاذ قرارات فعالة. تخلص من الانحياز التأكيدي آخر شيء تريده لتغيير قراراتك بوصفك مدير منتجات هو تأكيد تحيزك لنهج معين لمشكلة ما، إذ يضمن الحصولُ على مزيد من المدخلات من أصحاب المصلحة لحل مشاكل المنتج الاستراتيجية والاستفادة من اتخاذ القرارات المبنية على الحقائق اتباعَ الحل الصحيح لمشكلتك. تذكّر إبقاء النموذجين العقليين السابقين بعيدَين عن ذهنك عند اتخاذ قرارات بشأن المنتج. لنلقِ الآن نظرةً على نهجٍ للتعامل مع المقايضات غير المنحازة والمُوجَّهة بالحقائق والتي يمكن استخدامها لاتخاذ قرارات مُوجَّهة نحو تحقيق الهدف. نهج مضمون للتعامل مع المقايضات في إدارة المنتجات تتضمن معظم تحديات إدارة المنتجات جلب العديد من أصحاب المصلحة حول مشاكل استراتيجية للمساعدة في التوصل إلى اتفاق على الحل الأمثل. هناك بعض الخطوات التي سنوضحها فيما يلي ويمكن اتباعها لاتخاذ قرارات تستند إلى البيانات وتتماشى مع الأهداف. التفكير يبدأ العثور على الحل الصحيح للمشاكل الاستراتيجية التي تظهر في إدارة المنتجات باتباع منهجيات مختلفة للحل، ويكمن الحل الأمثل في الفروق الدقيقة لاختلاف الأفكار المتعددة. يُعَد العصف الذهني Brainstorming أحد أكثر الطرق فعاليةً لهذا الاختلاف، حيث يمكنك إجراء جلسات عصف ذهني مع الفرق ذات الصلة للعثور على الطرق المختلفة التي يمكنك اتباعها لحل المشكلة. حدّد أيضًا تكاليف الفرص المختلفة التي ستُضمَّن في كل طريقة من طرق الحل. سنوضّح فيما يلي بعض الأشياء التي يجب أن تضعها في بالك لضمان أن يكون اختلاف الأفكار طبيعيًا وفعالًا: حدد مساحة مشكلتك مع أصحاب المصلحة وحدد أهداف الحل. ادعُ جميع أصحاب المصلحة أو الأشخاص الآخرين ذوي الصلة إلى مساحة المشكلة، إذ يمكن أن يكون لديهم معرفة في إيجاد حلول للمشكلة. هيّئ بيئةً مريحةً لإجراء مناقشة فعالة تتجاوز الألقاب والمناصب. استمع إلى كل فكرة عن الحل وراجعها، حتى إن أتت من شخص متعدد الوظائف وليست ذات صلة مباشرة بمساحة المشكلة. احرص على اتفاق جميع أصحاب المصلحة على مساحة المشكلة والأهداف. ضع قائمةً بأفضل الأفكار غير المُرشَّحة مع قائمة المقايضات التي تتضمنها. يجب بمجرد أن يكون لديك تباين من الأفكار للحل أن تقيّم كل فكرة من هذه الأفكار من خلال تكاليف الفرص التي تتضمنها وإيجاد الحل الأمثل. التقارب للوصول إلى الحل لن يكون أيّ نهج في قائمة الطرق لحل المشكلة بعد الوصول إلى تباين من الأفكار مثاليًا، وستصبح عندها إدارة المنتجات أمرًا معقدًا، لذلك يجب أن تفهم ذلك وتتصالح معه بصفتك مبتدئًا يتطلع إلى أن يصبح مدير منتجات. ستجد في أغلب الأحيان حلولًا تبدو فعالةً بالقدر نفسه، ولكن مع مقايضات مختلفة، مما يصعّب تقييمها. هناك ثلاث طرق للتعامل مع عملية صنع القرار هي: التصويت: هو وسيلة فعالة لاختيار الحل الصحيح، لأن كل صاحب مصلحة يمثل مصالحه الخاصة ويأخذ في الحسبان المقايضات المرتبطة بوظيفته، وبذلك يجري اختيار المنهجيات المناسبة وفقًا لأقصى عدد من أصحاب المصلحة لمزيد من التقييم. المصفوفات التي تقابل التأثير: تُعَد إدارة الوقت والموارد التي تتوافق مع اكتمال الهدف أمرًا بالغ الأهمية في إدارة المنتجات. يمكن لمديري المنتجات تبسيط عملية اتخاذ القرار والتركيز على الحلول التي تمثل قيمة أكبر مع أقل الجهود والموارد المطلوبة من خلال إنشاء مصفوفة التأثير مقابل الجهد ومصفوفة التأثير مقابل الموارد لكل من الحلول المقترحة. الترجيح: قد يكون الترجيح عملية تستغرق وقتًا طويلًا ولكنها الطريقة الأكثر فعاليةً للتعامل مع المقايضات. ابدأ بتحديد أهدافك وأعطِ كل فكرة نقاطًا على أساس تحقيق تلك الأهداف. استخدم الترجيح لإعطاء كل نهج لأهداف مختلفة نقاطًا واختر النهج الذي يمتلك أكبر مجموع من النقاط، وخير مثال على ذلك يمكن أن يكون المقايضات المتضمنة في إنشاء منصات حسب الطلب. يمكن أن يكون الشكل والشعور في تتبّع الطلبات هو المشكلة الحقيقية بالنسبة لكثير من أصحاب المصلحة، بينما سيركّز الكثير من أصحاب المصلحة الآخرين ومعظمهم يتبعون منهجية الصيانة الإنتاجية الشاملة TPM على أشياء مثل التقدير الدقيق لتخصيصات الوقت المقدَّرة ETA وقضايا الشمولية Accessibility. يُعَد حل المشاكل والتفكير الاستراتيجي أحد المسؤوليات الأساسية لمدير المنتجات، لكن يمكن أن تصعّب المقايضات أو تكاليف الفرصة البديلة Opportunity Costs المتضمنة، العثور على الحلول الصحيحة لمشكلة ما. يمكن لمديري المنتجات جعل عملية صنع القرار سريعة وبسيطة وقائمة على الحقائق من خلال القضاء على أيّ نزعة للكمال وللانحياز التأكيدي من العملية. ترجمة -وبتصرُّف- للمقال Mastering Trade-Offs for Effective Product Management لصاحبه Abhishek Kumar. اقرأ أيضًا مستقبل إدارة المنتجات في التطوير بدون شيفرة برمجية إدارة المنتجات: المراحل الرئيسية ودور مدير المنتجات وظيفة مدير المنتج وما عليك فعله لتحصل عليها تأهيل مدير المنتجات: ما يجب فعله وما يجب تجنبه في الأسابيع الأولى
-
سنتعرّف في هذا المقال على ثلاثة من أهم المفاهيم التي تنظم العمليات وتعالجها في معمارية الحواسيب الحديثة وهي الجدولة Scheduling والصدَفة Shell والإشارات Signals. الجدولة Scheduling يحتوي النظام المُشغَّل على مئات أو حتى أُلوف العمليات، ويُطلَق على جزء النواة Kernel الذي يتعقّب جميع هذه العمليات اسم المجدوِل Scheduler لأنه يجدول أيّ عملية يجب تشغيلها لاحقًا. تُعَدّ خوارزميات الجدولة كثيرةً ومتنوعةً، إذ يكون لمعظم المستخدِمين أهداف مختلفة تتعلق بما يريدون تنفيذه من حواسيبهم، وهذا يؤثّر على قرارات الجدولة، فأنت تريد مثلًا التأكد من منح التطبيقات الرسومية في حاسوبك المكتبي متسعًا من الوقت للتشغيل حتى إذا استغرقت عمليات النظام وقتًا أطول قليلًا، مما سيؤدي إلى زيادة الاستجابة التي يشعر بها المستخدِم، وبالتالي سيكون لأفعالهم استجابات فورية، في حين يمكن أن ترغب في إعطاء الأولوية لتطبيق خادم الويب إذا عملتَ على خادم. ينشئ الناس دائمًا خوارزميات جديدةً، كما يمكنك إنشاء خوارزمياتك الخاصة بسهولة إلى حد ما، ولكن هناك عدد من المكونات المختلفة للجدولة. الجدولة ذات الأولوية Preemptive والجدولة التعاونية Co-operative يمكن أن تنقسم استراتيجيات الجدولة إلى فئتين: الجدولة التعاونية Co-operative Scheduling: هي المكان الذي تتخلى فيه العملية المُشغَّلة حاليًا طواعيةً عن التنفيذ للسماح بتشغيل عملية أخرى، والعيب في هذه الاستراتيجية هو أنّ العملية يمكنها اتخاذ قرار بعدم التخلي عن التنفيذ بسبب خطأ تسبَّب في شكل من أشكال الحلقة اللانهائية مثلًا، وبالتالي لا يمكن تشغيل أيّ شيء آخر. الجدولة الاستباقية Preemptive Scheduling: هي المكان الذي تُقاطَع فيه العملية لإيقافها للسماح بتشغيل عملية أخرى، إذ تحصل كل عملية على شريحة زمنية Time-slice لتعمل فيها، كما سيُعاد ضبط عدّاد الوقت عند كل عملية تبديل سياق Context Switching وستُشغَّل العملية ثم تُقاطَع عند انتهاء الشريحة الزمنية، فتبديل السياق Context Switching هو العملية التي تطبّقها النواة للتبديل من عملية إلى أخرى، في حين يتعامل العتاد مع المقاطعة على أنها مستقلة عن العملية المُشغَّلة، وبالتالي سيعود التحكم إلى نظام التشغيل عند حدوث المقاطعة، كما يمكن أن يقرِّر المجدوِل العملية التالية التي ستُشغَّل، وهذا هو نوع الجدولة الذي تستخدمه جميع أنظمة التشغيل الحديثة. الوقت الفعلي Realtime تحتاج بعض العمليات إلى معرفة المدة التي ستستغرقها شريحتها الزمنية والمدة التي المُستغرَقة قبل أن تحصل على شريحة زمنية أخرى لتعمل، ولنفترض أنه لديك نظامًا يشغّل جهاز القلب والرئتين، إذ لا تريد أن تتأخر النبضة التالية لأنّ شيئًا آخر قرّر العمل في النظام. تقدّم أنظمة الوقت الفعلي الصارمة Hard Realtime ضمانات حول جدولة القرارات مثل الحد الأقصى لمقدار الوقت الذي ستُقاطَع فيه العملية قبل تشغيلها مرةً أخرى، إذ تُستخدَم غالبًا في التطبيقات الحرجة مثل التطبيقات الطبية والعسكرية وتطبيقات الطائرات، في حين لا تكون الضمانات في أنظمة الوقت الفعلي غير الصارمة Soft Realtime صارمةً ولكن يمكن التنبؤ بسلوك النظام العام. يمكن استخدام نظام لينكس على أساس نظام وقت فعلي غير صارم، إذ يُستخدَم في الأنظمة التي تتعامل مع الصوت والفيديو، وإذا أردتَ تسجيل بث صوتي، فلا بد أنك لا تريد مقاطعتك لفترات طويلة من الوقت لأنك ستفقد البيانات الصوتية التي لا يمكن استرجاعها. القيمة اللطيفة تسنِد أنظمة يونيكس لكل عملية قيمةً لطيفة Nice Value، إذ ينظر المجدوِل إلى هذه القيمة ويمكن أن يعطي الأولوية لتلك العمليات التي تتمتع بأعلى قيمة لطيفة. مجدول لينكس خضع مجدول لينكس ولا يزال يخضع للعديد من التغييرات، إذ يحاول المطورون الجدد تحسين سلوكه، ويُعرَف المجدول الحالي باسم المجدول O(1) الذي يشير إلى الخاصية التي تمثل أنّ المجدول سيختار العملية التالية لتشغيلها في فترة زمنية ثابتة بغض النظر عن عدد العمليات التي يجب عليه الاختيار من بينها. تُعَدّ صيغة Big-O طريقةً لوصف الوقت الذي تستغرقه الخوارزمية للتشغيل بالنظر إلى الدخل المتزايد، فإذا استغرقت الخوارزمية ضعف الوقت للتشغيل مع ضعف الدخل، فهذا يؤدي إلى التزايد خطيًا، وإذا استغرقت خوارزمية أخرى أربعة أضعاف الوقت للتشغيل مع ضعف الدخل، فهذا يؤدي إلى تزايد أسي؛ أما إذا استغرق الأمر الوقت نفسه مهما كان مقدار الدخل، فستُشغَّل الخوارزمية في وقت ثابت، ويمكنك رؤية أنه كلما كانت الخوارزمية تنمو بصورة أبطأ مع مزيد من الدخل، كان ذلك أفضل. استخدمت مجدولات لينكس السابقة مفهوم الجودة Goodness لتحديد العملية التالية لتشغيلها، إذ يُحتفَظ بجميع المهام المُحتمَلة في رتل تشغيل Run Queue، وهو قائمة مترابطة من العمليات التي تعرِف النواة أنها في حالة قابلية للتشغيل، أي لا تنتظر نشاطًا من القرص الصلب أو ليست في حالة سكون. تبرز مشكلة أنه يجب حساب مدى جودة كل عملية قابلة للتشغيل بحيث تفوز العملية التي تتمتع بأعلى جودة لتكون العملية التالية التي يجب تشغيلها، إذ سيستغرق الأمر وقتًا أطول بكثير لمزيد من المهام لتحديد العمليات التالية التي ستشغَّل. المجدول O(1) يستخدِم المجدول O(1) بنية رتل التشغيل الموضح في الشكل السابق، كما يحتوي رتل التشغيل على عدد من الحزم Buckets مرتبةً حسب الأولوية وخارطة نقطية Bitmap تشير إلى الحزم التي تحتوي على عمليات متاحة، إذ يُعَدّ البحث عن العملية التالية لتشغيلها بمثابة قراءة الخارطة النقطية للعثور على حزمة العمليات الأولى، ثم اختيار العملية الأولى من رتل الحزم. يحتفظ المجدول ببنيتَين هما مصفوفة العمليات النشطة Active التي يمكن تشغيلها ومصفوفة العمليات منتهية الصلاحية Expired التي استخدمت شريحتها الزمنية بالكامل، كما يمكن تبديل هاتين البنيتَين ببساطة من خلال تعديل المؤشرات عندما يكون لجميع العمليات بعض الوقت من وحدة المعالجة المركزية. لكن الجزء المهم هو كيفية تحديد المكان الذي يجب أن تذهب إليه العملية في رتل التشغيل، فمن الأشياء التي يجب أخذها في الحسبان هو المستوى اللطيف Nice Level، وتقارب المعالج Processor Affinity أو الحفاظ على العمليات مرتبطة بالمعالج الذي تُشغَّل عليه لأن نقل العملية إلى وحدة معالجة مركزية أخرى في نظام SMP يمكن أن يكون عمليةً مكلفةً، بالإضافة إلى دعم أفضل لتحديد البرامج التفاعلية مثل تطبيقات واجهة المستخدم الرسومية التي يمكن أن تقضي الكثير من الوقت في حالة سكون في انتظار الدخل من المستخدِم، ولكن يريد المستخدِم استجابةً سريعةً عندما يتفاعل معها. الصدفة Shell تُعَدّ الصدَفة في نظام يونيكس الواجهة المعيارية لمعالجة العمليات على نظامك، ولكن تحتوي أنظمة لينكس الحديثة على واجهة مستخدِم رسومية وتوفّر صدفةً عبر تطبيق طرفية Terminal أو ما شابه ذلك، كما تتمثل مهمة الصدَفة الأساسية في مساعدة المستخدِم على التعامل مع بدء العمليات المُشغَّلة في النظام وإيقافها والتحكم فيها. إذا كتبتَ أمرًا في موجّه أوامر الصدفة، فسيؤدي ذلك إلى تطبيق الاستدعاء fork على نسخة منه وتطبيق الاستدعاء exec على الأمر الذي حددته، ثم تنتظِر الصدَفة بعد ذلك افتراضيًا حتى ينتهي تشغيل هذه العملية قبل العودة إلى موجّه الأوامر لبدء العملية بأكملها مرةً أخرى. كما تسمح لك الصدَفة بتشغيل وظيفة ما في الخلفية Background من خلال وضع & بعد اسم الأمر للإشارة إلى وجوب تفرع الصدَفة وتنفيذ الأمر دون الانتظار حتى يكتمل الأمر قبل أن تُظهِر لك موجّه الأوامر مرةً أخرى، في حين تعمل العملية الجديدة في الخلفية مع جهوزية الصدَفة في انتظار بدء عملية جديدة إذا رغبت في ذلك، لكن يمكنك إخبار الصدَفة بتنفيذ عملية ما في الأمام Foreground، مما يعني أننا نريد انتظار انتهاء العملية فعلًا. الإشارات Signals تتطلب العمليات المُشغَّلة في النظام طريقةً لإخبارنا بالأحداث التي تؤثر عليها، إذ توجد بنية تحتية في نظام يونيكس بين النواة Kernel والعمليات تسمّى الإشارات Signals التي تسمح للعملية بتلقي إشعار بالأحداث المهمة بالنسبة لها. تستدعي النواة معالِجًا Handler يجب أن تسجّله العملية مع النواة للتعامل مع الإشارة المُرسَلة إلى عملية ما، والمعالج هو دالة مصمّمة في الشيفرة البرمجية التي كُتِبت لمعالجة المقاطعة، كما تُرسَل الإشارة في أغلب الأحيان من النواة نفسها، ولكن يمكن أن ترسِل إحدى العمليات إشارةً إلى عملية أخرى، وهذا يمثِّل أحد أشكال التواصل بين العمليات Interprocess Communication. يُستدعَى معالج الإشارة بصورة غير متزامنة، إذ يُقاطَع البرنامج المشغَّل حاليًا عمّا يفعله لمعالجة حدث الإشارة، كما تُعَدّ المقاطعة أحد أنواع الإشارات التي تُحدَّد في ترويسات النظام بالاسم SIGINT، إذ تُسلَّم إلى العملية عند الضغط على الاختصار ctrl-c. تستخدِم العملية استدعاء نظام read لقراءة الدخل من لوحة المفاتيح، إذ ستراقب النواة مجرى الدخل بحثًا عن محارف خاصة، لكن ستنتقل إلى وضع معالجة الإشارة في حالة ظهور الاختصار ctrl-c، وستبحث النواة لمعرفة ما إذا سجّلت العملية معالجًا لهذه المقاطعة، فإذا كان الأمر كذلك، فسيُمرَّر التنفيذ إلى تلك الدالة التي ستعالج المقاطعة، وإذا لم تسجّل العملية معالجًا لهذه الإشارة، فستتخذ النواة بعض الإجراءات الافتراضية، ويكون الإجراء الافتراضي هو إنهاء العملية باستخدام ctrl-c. يمكن أن تختار العملية تجاهل بعض الإشارات ولكن لا تسمح بتجاهل الإشارات الأخرى، فالإشارة SIGKILL مثلًا هي الإشارة المرسَلة عندما يجب إنهاء العملية، حيث سترى النواة أن العملية أرسلت هذه الإشارة وتنهي تشغيل العملية دون طرح أيّ أسئلة، كما لا يمكن للعملية الطلب من النواة تجاهل هذه الإشارة، إذ تكون النواة حريصةً للغاية بشأن العملية المسموح لها بإرسال هذه الإشارة إلى عملية أخرى، فلا يجوز لك إرسالها إلا إلى العمليات التي تمتلكها إلا إذا كنت المستخدِم الجذر. لا بد أنك رأيت الأمر kill -9 الذي يأتي من تطبيق الإشارة SIGKILL، إذ تُعرَّف الإشارة SIGKILL على أنها 0x9، لذا ستتوقف العملية المحددة مباشرةً عند تحديدها على أساس وسيط لبرنامج kill، ونظرًا لأنه لا يمكن للعملية اختيار تجاهل هذه الإشارة أو معالجتها، فسيُنظَر إلى هذه الإشارة على أنها الملاذ الأخير، إذ لن يكون لدى البرنامج فرصةً للتنظيف أو الإنهاء بصورة نظيفة. يُفضَّل إرسال الإشارة SIGTERM -للإنهاء Terminate- إلى العملية أولًا، فإذا تعطلت أو لم تنتهي، فيمكنك اللجوء إلى الإشارة SIGKILL، كما تثبّت معظم البرامج معالجًا للإشارة SIGHUP، أي تعليق Hangup الطرفيات وأجهزة المودِم التسلسلية، إذ سيعيد هذا المعالج تحميل البرنامج لالتقاط التغييرات في ملف الإعداد أو ما شابه ذلك. إذا سبق لك وبرمجتَ على نظام يونيكس، فستكون على دراية بأخطاء التقطيع segmentation faults عندما تحاول القراءة أو الكتابة في ذاكرة غير مخصَّصة لك، فإذا لاحظت النواة أنك تحاول الوصول إلى ذاكرة ليست مخصَّصة لك، فسترسل لك إشارة خطأ تقطيع segmentation fault signal، ولن تمتلك العملية معالجًا مثبَّتًا لهذه الإشارة، وبالتالي فإنّ الإجراء الافتراضي هو إنهاء البرنامج وتعطيل برنامجك، كما يمكن أن يثبّت البرنامج معالجًا لأخطاء التقطيع في بعض الحالات المحدودة. لكن يمكنك التساؤل عمّا يحدث بعد تلقي الإشارة، إذ سيُعاد التحكم إلى العملية التي تستأنف عملها من حيث توقفت بمجرد انتهاء معالج الإشارة من عمله، ويقدّم البرنامج البسيط التالي تشغيل بعض الإشارات: $ cat signal.c #include <stdio.h> #include <unistd.h> #include <signal.h> void sigint_handler(int signum) { printf("got SIGINT\n"); } int main(void) { signal(SIGINT, sigint_handler); printf("pid is %d\n", getpid()); while (1) sleep(1); } $ gcc -Wall -o signal signal.c $ ./signal pid is 2859 got SIGINT # press ctrl-c # press ctrl-z [1]+ Stopped ./signal $ kill -SIGINT 2859 $ fg ./signal got SIGINT Quit # press ctrl-\ $ يعرّف البرنامج البسيط السابق معالجًا للإشارة SIGINT التي تُرسَل عندما يضغط المستخدِم على الاختصار ctrl-c، إذ تُعرَّف جميع إشارات النظام في مكتبة signal.h بما في ذلك الدالة signal التي تسمح لنا بتسجيل دالة المعالجة. يبقى البرنامج ضمن حلقة لا تفعل شيئًا حتى يتوقف، وحاول الضغط على الاختصار ctrl-c عند بدء البرنامج لإنهائه، إذ يُستدعَى المعالج ونحصل على الخرج المتوقَّع بدلًا من اتخاذ الإجراء الافتراضي، ثم نضغط بعد ذلك على الاختصار ctrl-z الذي يرسل الإشارة SIGSTOP التي تضع العملية افتراضيًا في وضع السكون، أي أنها لم تُوضَع في رتل تشغيل المجدول وبالتالي تُعَدّ خاملةً في النظام. نستخدم برنامج kill لإرسال الإشارة نفسها من نافذة طرفية أخرى، إذ يمكن تطبيق ذلك فعليًا باستخدام استدعاء النظام kill الذي يأخذ إشارة ومعرّف PID لإرسالها، ويُعَدّ اسم هذه الدالة خاطئًا بعض الشيء، إذ لا تقتل جميعُ الإشارات العمليةَ فعليًا، ولكن تُستخدَم الدالة signal لتسجيل المعالج Handler، كما توضَع الإشارة في رتل خاص بهذه العملية عند توقفها، وبالتالي تأخذ النواة ملاحظةً بالإشارة وتسلّمها في الوقت المناسب. ننبّه العملية بعد ذلك باستخدام الأمر fg الذي يرسل الإشارة SIGCONT إلى العملية، مما يؤدي إلى تنشيط العملية افتراضيًا، كما تدرك النواة وضع العملية في رتل التشغيل وتمنحها وقتًا من وحدة المعالجة المركزية مرةً أخرى، إذ نرى في هذه المرحلة تسليم الإشارة الموجودة في رتل التشغيل. نحاول أخيرًا الضغط على الاختصار ctrl-\ الذي يرسل الإشارة SIGQUIT -أي إلغاء- إلى العملية، ويأتي خرج الإلغاء Quit من استخدام مزيد من الإشارات بالرغم من إلغاء العملية، وإذا كان لدى الأب عملية ابن ميتة أو منتهية، فسيحصل على الإشارة SIGCHLD، إذ تُعَدّ الصدَفةُ أنها العملية الأب في هذه الحالة، أي أنها ستحصل على الإشارة. تذكّر أنّ العملية الزومبي Zombie التي يجب حصادها باستخدام الاستدعاء wait للحصول على الشيفرة المُعادة من العملية الابن، ولكن هناك شيء آخر يمنحه الابن للأب وهو رقم الإشارة التي أدّت إلى موت الابن، وهكذا تعرف الصدَفة أنّ العملية الابن قد ماتت أو انتهت بسبب الإشارة SIGABRT وتطبع معلومات أخرى للمستخدِم على أساس خدمة إعلامية، إذ تحدُث العملية نفسها لطباعة خطأ التقطيع Segmentation Fault عندما تموت العملية الابن بسبب الإشارة SIGSEGV. يمكنك رؤية استخدام حوالي خمس إشارات مختلفة للتواصل بين العمليات والنواة والحفاظ على سير الأمور حتى في برنامج بسيط، وهناك العديد من الإشارات الأخرى، لكننا استخدمنا في هذا المثال الإشارات الأكثر شيوعًا، إذ تحتوي معظمها على دوال نظام تعرّفها النواة، ولكن هناك بعض الإشارات المحجوزة للمستخدِمين لاستخدامها لأغراضهم الخاصة في برامجهم SIGUSR. ترجمة -وبتصرُّف- للأقسام Context Switching و Scheduling و The Shell و Signals من الفصل The Process من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: الذاكرة الوهمية والذاكرة الحقيقية في معمارية الحاسوب المقال السابق: تسلسل العمليات الهرمي واستدعاءات النظام Fork و Exec في نظام تشغيل الحاسوب معمارية الشبكة الحاسوبية وشبكة الإنترنت (Network Architecture) دور نظام التشغيل وتنظيمه في معمارية الحاسوب تعرف على وحدة المعالجة المركزية وعملياتها في معمارية الحاسوب
-
يشرح الكاتب في هذا المقال تجربته في عالم سريع النمو من التطوير بدون شيفرة برمجية وتأثيره على على مستقبل إدارة المنتجات. يجلب عدم وجود شيفرة برمجية تطويرًا لجمهور جديد. المصدر: glazestock.com لصاحبها Rudyitas كنتُ -يقول الكاتب- مدير منتجات لمدة 10 سنوات حتى الآن، وذلك إما كجزء من فريق أو أنشأت فرقًا جديدة كاملة، أو كمؤسس لشركة ناشئة صنعت منتجًا. كان مفهوم إدارة المنتجات مفهومًا جديدًا إلى حد ما بالعودة إلى عام 2009 في المملكة المتحدة على الأقل وخاصةً في المنظمات الكبرى، فقد اعتقد مديرو المنتجات أنهم يواجهون صعوبةً اليوم في شرح ما يفعلونه للأصدقاء والزملاء، وقد كان ذلك أسوأ بكثير في ذلك الوقت. لكن جاءت نقطة التحول الكبيرة بالنسبة لي مع إصدار كتاب Lean Startup لصاحبه إريك رييس Eric Ries في عام 2011، وبالتالي كان لدينا طريقة أو حركة لاتباعها وتأييدها، ثم تغير كل شيء. تُعَد حلقة الاستجابة المتمثلة في البناء-القياس-التعلم من المكونات الأساسية لمنهجية كتاب Lean Startup. وتتمثل الخطوة الأولى في اكتشاف المشكلة التي يجب حلها، ثم تطوير منتج الحد الأدنى Minimum Viable Product -أو MVP اختصارًا- لبدء عملية التعلم في أسرع وقت ممكن. يمكن للشركة الناشئة بعد إنشاء منتج الحد الأدنى MVP العمل على ضبط العملية التي تشمل القياس والتعلم ويجب أن تتضمن مقاييسًا قابلة للتنفيذ يمكن أن توضح السبب والنتيجة. الاعتدال في تقليل الهدر لا يزال نهج كتاب Lean Startup يُستخدَم على نطاق واسع بوصفه الأسلوب الواقعي لبناء المنتجات والشركات الناشئة، ولكن الشيء الذي لم يتغير هو طبيعة تكوين الفريق، والأشخاص الذين تحتاجهم لإنشاء منتج ما. لقد أُنشِئت جميع فرق المنتجات تقريبًا بحيث تكون مكونةً من مدير المنتجات Product Manager -أو PM اختصارًا- ومدير المشروع أو محلل الأعمال والمصمم وفريق التطوير. لا يزال منتج الحد الأدنى MVP يتطلب هذا الفريق متعدد الوظائف من الأشخاص ومدة بضعة أشهر في أحسن الأحوال أو فترة أطول بكثير في الواقع لجعل هذا المنتج في متناول العملاء، ونعني بمنتج الحد الأدنى MVP منتجًا وظيفيًا وليس صفحة هبوط أو نموذجًا أوليًا تفاعليًا. أحب أن أكون جزءًا من إنشاء منتجات جديدة وإيجاد حلول جديدة للمشاكل وللمشاريع الجديدة، فهذا ما يناسبني، ولكن كنت دائمًا محبطًا بعض الشيء بسبب الوقت والمال اللذين يتطلبهما إنجاز المهام. يُعَد إنفاق ما يزيد عن 50 ألف جنيه إسترليني ومدة 6 أشهر للحصول على منتج الحد الأدنى المُفترَض بمثابة حكم بالإعدام، أو على الأقل جهدًا كبيرًا بالنسبة للشركات الناشئة في المراحل الأولى، خاصةً تلك الشركات التي ينتهي بها الأمر باستخدام الوكالات لبناء منتجاتها. سترتفع التكاليف الشاملة قريبًا حتى لو كنت تقنيًا أو كنت محظوظًا بما يكفي ليكون لديك تقني كمؤسس مشارك. ستتعلم بسرعة بصفتك شخصًا منتجًا لديه عقلية تجريبية أن الأخطاء هي ببساطة طريقة أخرى لتنفيذ الأشياء، لذا يجب أن يكون لديك ذهن متفتح وأن تهتم بما يمكن أن يحل مشكلة العميل على أفضل وجه، وما الذي سيؤدي المهمة التي يحاولون إنجازها. يمكنني تعلم البرمجة بدلًا من الاعتماد على فريق كامل لإنشاء منتج الحد الأدنى MVP، أو بالأحرى اعتدتُ أن أكون قادرًا على ذلك. لقد صنعتُ "لعبة حاسوبية" عندما كان عمري 11 عامًا لحاسوب كومودور 64 (نسخة هوبر) وبعت شرائطًا منها في المدرسة، ولكنك تشعر مع كتابة شيفرة برمجية أنك متخصص في مجال واحد فقط، وحتى هذا يتطلب الكثير من الوقت والالتزام لمجرد الحصول على الأساسيات، كما أنني لست مصممًا محترفًا ولا بأي شكل من الأشكال، ولكنني أعرف المبادئ الأساسية فقط، فقد أنشأت نماذجًا أوليةً تفاعليةً وهي جيدة للحصول على بعض الملاحظات الأولية، ولكنها ليست منتجًا مناسبًا في النهاية. لحسن الحظ، هناك شيء ما يحدث الآن في عالم تطوير المنتجات استجابة لدعواتي ودعوات العديد من الأشخاص المحبطين حول العالم لتحسين جميع هذه الأمور، حيث سنتحدث عن ذلك الآن. الدخول في حركة التطوير بدون شيفرة برمجية صادفتُ أداة Webflow منذ حوالي 4 سنوات، حيث كانت ولا زالت منافسًا لووردبريس Wordpress، واستخدمتُها لإنشاء مواقع الويب دون الحاجة إلى تعلم كتابة شيفرة برمجية. ولتوفير بعض المال عند الاضطرار إلى توظيف شخص آخر، وقد تطورَت كثيرًا منذ ذلك الحين وتُعَد أداةً مهمة، لكنها لا تزال في النهاية مجرد أداة للواجهة الأمامية، إذ ستحتاج إلى قاعدة بيانات خلفية وسير عمل لإنشاء منتج وظيفي كامل. إذا أردت مثلًا إنشاء نسخة من موقع Medium مع خطة عضوية وأسعار متدرجة وما إلى ذلك، فلا يمكنك استخدام Webflow فقط، إذ توجد العديد من مواقع الويب ومنصات أنظمة إدارة المحتوى CMS المتاحة. أمضيتُ الأشهر الثلاثة الماضية في هذا العالم الجديد الشجاع بلا شيفرة برمجية دون هدف محدد، حيث جربت إنشاء سوق وإنشاء تطبيقات داخلية للمساعدة في جمع البيانات والمؤثرات المرئية وإنشاء رد آلي صوتي. لا يزال هناك منحنى تعليمي كبير قبل أن تبدأ في توقع أنه يمكنك طرح تطبيق لائق ينفّذ كل ما تحتاجه بسرعة، فكل منصة لها حيلها الخاصة، ويستغرق الأمر بعض الوقت لمعرفة ما هي قادرة على فعله. سيكون الأمر مختلفًا لكل مشروع، لذا يُعَد الوصول إلى النقطة التي تعرف فيها فطريًا طريقةً لتطبيق ما تريد تحقيقه هو المكان الذي تكمن فيه المهارة الحقيقية. وهنا أدين بالفضل إلى الأسطورة بين توسل Ben Tossell من منصة Makerpad الذي يُعَد مصدر مصدر إلهام حقيقي في هذا المجال. الخبر السار هنا هو وجود أدوات للتطوير بدون شيفرة برمجية فعليًا، فلا حصر للاحتمالات. كذلك، تظهر الابتكارات الجديدة التي لا تحتوي على شيفرة برمجية بصورة أسرع مما يمكن أن يقوله إطار عمل روبي أون ريلز Ruby On Rails، كما صعدت المنتجات التي بدون شيفرة برمجية إلى المراتب الأولى على موقع برودكت هنت ProductHunt. أركز حاليًا في هذه المرحلة من رحلتي على إنشاء منتجات الحد الأدنى MVP. أعتقد أنه الوقت المثالي لإنشاء منتجات MVP السريعة التي تنفّذ ما نحتاجه بالنظر إلى ما هو متاح الآن من حيث الأدوات التي لا تحتوي على شيفرة برمجية وما تقدمه. يمكنك إنشاء تطبيقات الويب أو التطبيقات الأصيلة Native والواجهة الخلفية وإدارة سير العمل وبدء الأحداث ودمج المدفوعات وغير ذلك الكثير؛ لكنني متأكد من أن أي شيء سيكون ممكنًا في القريب العاجل، فأيّ شيء يمكن تنفيذه مع شيفرة برمجية سيُنفَّذ بدونها. تأثير التطوير بدون شيفرة برمجية على مستقبل المنتجات والتصميم والبرمجة لا بد أن يكون لحركة التطوير بدون شيفرة برمجية -مثل أيّ ثورة جيدة- نصيبها من النقاد والمتشككين، ويمكن أن تفترض أن المبرمجين هم الكارهون الأكبر، ولكنني لم أشهد الكثير منهم في الواقع، والسبب هو أن البرمجة بمعناها النقي لا يمكن أبدًا أن تندثر، إذ لا بد من شخص يبرمج جميع المنصات والأدوات التي ستبتكر عالمًا دون شيفرة برمجية. يرغب المطورون في تبني عدم وجود شيفرة برمجية وتعلم المهارات بأنفسهم، وينطبق الشيء نفسه على المصممين، إذ أننا ننتقل إلى عصر جديد من صنع الأشياء. لذا يمتلك مديرو المنتجات الآن فرصةً مهمةً للغاية في أن يكونوا قادرين على الجمع بين طرقهم المُجرَّبة والمُختبَرة لتحديد المنتجات والميزات التي يجب إنشاؤها مع إنشائها فعليًا، وستنخفض أوقات التسليم والتكاليف، وستظهر وظائف جديدة، وسأكون سعيدًا في ذلك كله. ترجمة -وبتصرُّف- للمقال The Future of Product Management is No-Code Development لصاحبه Martin Slaney. اقرأ أيضًا الطريق إلى منصب مدير المنتجات دليل مدير المنتجات لاختيار مقاييس أداء صحيحة تأهيل مدير المنتجات: ما يجب فعله وما يجب تجنبه في الأسابيع الأولى وظيفة مدير المنتج وما عليك فعله لتحصل عليها
-
يمكن لنظام التشغيل تشغيلُ العديد من العمليات في الوقت نفسه، إلّا أنه يبدأ بتشغيل عملية واحدة مباشرةً تُدعَى بالعملية الأولية init -اختصارًا للكلمة Initial- التي لا تُعَدّ عمليةً خاصةً باستثناء أنً معرِّف العملية PID الخاص بها هو 0 دائمًا وستبقى مُشغَّلةً دائمًا. تُعَدّ جميع العمليات الأخرى أبناءً Children لهذه العملية الأولية، فللعمليات شجرة عائلة مثل أيّ شجرة أخرى، إذ يكون لكل عملية أبًا Parent ويمكن أن يكون لها العديد من الأشقاء Siblings التي تُعَدّ عمليات أنشأها الأب نفسه. يُستخدَم المصطلح "تولّد Spawn" عند الحديث عن العمليات الآباء التي تنشئ العمليات الأبناء مثل القول بأن "عملية ولّدت ابنًا"، كما يمكن أن تنشئ العمليات الأبناء مزيدًا من الأبناء وهكذا، وإليك مثال عن تنفيذ الأمر pstree الذي يعرض العمليات المُشغَّلة مثل شجرة: init-+-apmd |-atd |-cron ... |-dhclient |-firefox-bin-+-firefox-bin---2*[firefox-bin] | |-java_vm---java_vm---13*[java_vm] | `-swf_play يمكن إنشاء عمليات جديدة باستخدام واجهتين متعلقتين ببعضهما هما fork و exec. استدعاءات Fork إذا وصلتَ إلى مفترق طرق، فسيكون لديك خياران لتختار من بينهما وسيؤثر هذا القرار على مستقبلك، كما تصل البرامج الحاسوبية إلى مفترق طرق عندما تضغط على استدعاء النظام fork()، إذ سيُنشئ نظام التشغيل عمليةً جديدةً مماثلةً للعملية الأب، إذ ستُنسَخ جميع الحالات التي تحدّثنا عنها سابقًا بما في ذلك الملفات المفتوحة وحالة المسجّل وجميع عمليات تخصيص الذاكرة التي تتضمن شيفرة البرنامج. تُعَدّ القيمة المُعادة من استدعاء النظام الطريقةَ الوحيدة التي يمكن للعملية من خلالها تحديد ما إذا كانت العملية موجودةً مسبقًا أم عملية جديدة، إذ ستكون القيمة المُعادة إلى العملية الأب هي معرّف عملية الابن Process ID أو PID اختصارًا، في حين سيحصل الابن على القيمة المُعادة 0، وعندها نقول أن العملية متفرعة forked مع وجود علاقة أب-ابن. استدعاءات Exec يوفّر التفريع Forking طريقةً للعملية الحالية بأن تبدأ عملية جديدة، فإذا لم تكن العملية الجديدة جزءًا من برنامج العملية الأب كما هو الحال في الصدَفة Shell، إذ يجب أن يشغِّل المستخدِم أمرًا في عملية جديدة ليس لها علاقة بالصدَفة، فيجب تشغيل استدعاء النظام exec الذي سيبدّل بمحتويات العملية المُشغَّلة حاليًا معلومات من برنامج ثنائي. العملية التي تتبعها الصدَفة عند إطلاق برنامج جديد هي fork أولًا، مما يؤدي إلى إنشاء عملية جديدة، ثم تنفيذ الاستدعاء exec، أي تحميل البرنامج الثنائي الذي يُفترَض تشغيله في الذاكرة وتنفيذه. كيفية تعامل لينكس مع fork و exec سنشرح كيفية تعامل نظام التشغيل لينكس مع عملية النسخ fork وعملية الاستدعاء exec. النسخ يُنفَّذ الاستدعاء fork باستخدام استدعاء النظام clone في النواة، إذ توفّر واجهات clone بفعالية مستوًى من التجريد لكيفية إنشاء نواة لينكس للعمليات، كما يتيح الاستدعاء clone تحديد أجزاء العملية الجديدة المنسوخة في العملية الجديدة والأجزاء المشتركة بين العمليتين صراحةً، وقد يبدو هذا غريبًا بعض الشيء في البداية، لكنه يسمح لنا بسهولة بتطبيق الخيوط Threads باستخدام واجهة واحدة بسيطة جدًا. الخيوط Threads ينسخ الاستدعاء fork جميع السمات التي ذكرناها سابقًا. تخيّل نسخ كل شيء للعملية الجديدة باستثناء الذاكرة، فهذا يعني اشتراك الأب والابن في الذاكرة نفسها التي تتضمن شيفرة البرنامج والبيانات. يُسمَّى الابن الهجين السابق بالخيط، كما تحتوي الخيوط على عدد من المزايا بالموازنة مع المكان الذي يُستخدَم فيه الاستدعاء fork ومنها ما يلي: لا يمكن للعمليات المنفصلة أن ترى ذاكرة بعضها بعضًا، وإنما يمكنها التواصل مع بعضها بعضًا عبر استدعاءات النظام الأخرى فقط، لكن مع ذلك تشترك الخيوط في الذاكرة نفسها، لذا سيكون لديك ميزة العمليات المتعددة مع الاضطرار إلى استخدام استدعاءات النظام للتواصل فيما بينها، وتكمن مشكلة ذلك في إمكانية تداخل الخيوط بسهولة مع بعضها بعضًا، إذ يمكن أن يزيد أحد الخيوط متغيرًا، في حين يمكن أن ينقصه خيط آخر بدون إعلام الخيط الأول، وتسمى هذه الأنواع من المشاكل بمشاكل التزامن وهي كثيرة ومتنوعة، ولكن يمكن حل هذه المشكلة باستخدام مكتبات مجال المستخدِم التي تساعد المبرمجين على العمل مع الخيوط بصورة صحيحة، وتسمى أكثر الخيوط شيوعًا بخيوط POSIX أو كما يشار إليها pthreads بصورة شائعة. يُعَدّ التبديل بين العمليات مكلفًا جدًا، ومن أكثر الأمور تكلفةً هو تعقّب الذاكرة التي تستخدِمها كل عملية، ويمكن تجنّب ذلك من خلال مشاركة الذاكرة التي تزيد الأداء بصورة ملحوظة. هناك العديد من الطرق المختلفة لتطبيق الخيوط، إذ يمكن أن يطبِّق مجال المستخدِم الخيوط ضمن عملية دون أن تكون لدى النواة أيّ فكرة عن ذلك، إذ تظهر جميع الخيوط للنواة كأنها تعمل في عملية واحدة، ويُعَدّ ذلك دون المستوى الأمثل لأنّ النواة تحجب معلومات عمّا يجري تشغيله في النظام؛ أما مهمة النواة فهي التأكد من استخدام موارد النظام بأفضل طريقة ممكنة، وإذا كان ما تعتقده النواة هو وجود عملية واحدة تشغّل خيوط عمليات متعددة، فيمكن أن تتخذ قرارات دون المستوى الأمثل. الطريقة الأخرى هي أن تكون للنواة معرفة كاملة بالخيط، إذ يمكن إنشاء ذلك في نظام لينكس من خلال جعل جميع العمليات قادرة على مشاركة الموارد عبر استدعاء النظام clone، ولا يزال كل خيط يحتوي على موارد مرتبطة بالنواة، لذلك يمكن أن تأخذها النواة في حساباتها عند إجراء عمليات تخصيص الموارد. تحتوي أنظمة التشغيل الأخرى على طريقة هجينة من الطريقتين السابقتين، إذ يمكن تحديد بعض الخيوط للتشغيل في مجال المستخدِم فقط -أي مخفيّة عن النواة- ويمكن أن يكون البعض الآخر من الخيوط عمليةً خفيفة الوزن، ويُعَدّ ذلك مؤشرًا مشابهًا للنواة على أن العمليات هي جزء من مجموعة خيوط. النسخ عند الكتابة يُعَدّ نسخ ذاكرة عملية كاملة إلى عملية أخرى عند استخدام الاستدعاء fork عمليةً مكلفةً كما ذكرنا سابقًا، ويمكن تحسين ذلك باستخدام ما يُسمَّى بالنسخ عند الكتابة Copy On Write، إذ يمكن مشاركة الذاكرة فعليًا بدلًا من نسخها بين العمليتين عند استدعاء fork، فإذا كانت العمليات ستقرأ الذاكرة فقط، فلن يكون نسخ البيانات ضروريًا، لكن يجب أن تكون النسخة خاصةً وغير مشتركة عندما تكتب عملية ما في ذاكرتها. يعمل النسخ عند الكتابة -كما يوحي اسمه- على تحسين ذلك من خلال النسخ من الذاكرة فقط عندما تُكتَب النسخة فيها، وللنسخ عند الكتابة فائدة كبيرة للاستدعاء exec الذي سيكتب البرنامج الجديد في الذاكرة، لذا سيضيّع نسخ الذاكرة الكثير من الوقت، وبالتالي ستوفّر عملية النسخ عند الكتابة علينا النسخ فعليًا. العملية الأولية Init Process ناقشنا سابقًا الهدف العام للعملية الأولية وسنفهم الآن كيفية عملها، إذ تبدأ النواةُ العمليةَ الأولية init عند بدء التشغيل وتفرّع وتنفّذ هذه العملية بعد ذلك سكربتات بدء تشغيل الأنظمة التي تفرّع وتنفّذ مزيدًا من البرامج، وينتهي بها الأمر في النهاية إلى تفريع عملية تسجيل الدخول. الوظيفة الأخرى للعملية init هي الحصاد Reaping، إذ سترغب العملية الأب في التحقق من الشيفرة المُعادة للتأكد من إنهاء العملية الابن بصورة صحيحة أم لا عندما تستدعي العملية استدعاء الإنهاء exit باستخدام الشيفرة المُعادة، لكن تُعَدّ شيفرة الإنهاء جزءًا من العملية التي استدعت exit، لذا يُقال أنّ هذه العملية ميتة Dead مثل أنها لا تعمل، ولكنها لا تزال بحاجة إلى البقاء حتى جمع الشيفرة المُعادة، وتسمى العملية في هذه الحالة شبه ميتة أو زومبي Zombie. تبقى العملية في حالة زومبي حتى تجمع العملية الأب الشيفرة المُعادة من الاستدعاء wait، ولكن إذا انتهت العملية الأب قبل جمع الشيفرة المُعادة، فستبقى عملية الزومبي موجودةً وتنتظر بلا هدف لإعطاء حالتها لعمليةٍ ما، ثم ستُنسَب العملية الابن التي تكون في حالة زومبي إلى العملية الأولية التي تمتلك معالجًا خاصًا يحصد القيمة المُعادة، وستكون العملية حرةً في النهاية ويمكن إزالة واصفها من جدول عمليات النواة. مثال عملية شبه ميتة إليك مثال عن عملية شبه ميتة أو عملية زومبي كما يقال: $ cat zombie.c #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; printf("parent : %d\n", getpid()); pid = fork(); if (pid == 0) { printf("child : %d\n", getpid()); sleep(2); printf("child exit\n"); exit(1); } /* في الأب */ while (1) { sleep(1); } } $ ps ax | grep [z]ombie 16168 pts/9 S 0:00 ./zombie 16169 pts/9 Z 0:00 [zombie] <defunct> أنشأنا في المثال السابق عملية زومبي، إذ ستكون العملية الأب في حالة سكون Sleep إلى الأبد، في حين ستنتهي العملية الابن بعد بضع ثوان، ويمكنك رؤية نتائج تشغيل البرنامج بعد الشيفرة البرمجية. تكون العملية الأب (16168) في الحالة S للإشارة إلى أنها في حالة سكون وتكون العملية الابن في الحالة Z للإشارة إلى أنها في حالة زومبي، في حين يخبرنا خرج الأمر ps أن العملية أصبحت زومبي أو defunct في وصف العملية. ملاحظة: الأقواس المربعة حول الحرف "z" في الكلمة "zombie" هي خدعة صغيرة لتزيل عمليات الأمر grep نفسها من خرج الأمر ps، إذ يفسّر الأمر grep كل شيء بين الأقواس المربعة على أنه صنف محرفي Character Class، أي يبحث عن تطابق واحد فقط بين المحارف الموجودة بين القوسين والنص، ولكن بما أن اسم العملية سيكون "grep [z]ombie" مع الأقواس، فلن يكون هناك تطابق. ترجمة -وبتصرُّف- للقسمين Process Hierarchy و Fork and Exec من الفصل The Process من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: أهم المفاهيم التي تنظم العمليات وتعالجها في معمارية الحاسوب الحديثة المقال السابق: العمليات وعناصرها في نظام تشغيل الحاسوب استدعاءات النظام والصلاحيات في نظام التشغيل دور نظام التشغيل وتنظيمه في معمارية الحاسوب العمليات (Processes) في أنظمة التشغيل أنظمة التشغيل للمبرمجين
-
جميعنا على دراية بنظام التشغيل الحديث الذي يدير العديد من المهام في وقت واحد أو ما يسمى بتعدد المهام Multitasking، حيث تُعَدّ العملية حزمةً من العناصر التي تحتفظ بها النواة لتعقّب جميع المهام التي تكون قيد التشغيل. عناصر العملية معرف العملية يضبط نظام التشغيل معرّف العملية Process ID -أو PID اختصارًا- ويكون فريدًا لكل عملية مُشغَّلة. الذاكرة سنتعلم كيف تحصل عملية ما على ذاكرتها لاحقًا، وتُعَدّ الذاكرة أحد الأجزاء الأساسية لكيفية عمل نظام التشغيل، ولكن سنكتفي حاليًا بمعرفة أنّ كل عملية لها قسمها الخاص من الذاكرة. تُخزَّن شيفرة البرنامج في هذه الذاكرة مع المتغيرات وأيّ عمليات تخزين أخرى مخصَّصة، ويمكن مشاركة أجزاء من الذاكرة بين العمليات، إذ تسمَّى بالذاكرة المشتركة Shared Memory، كما يمكن أن تراها بالاسم System Five Shared Memory -أو SysV SHM اختصارًا- بعد التطبيق الأصلي في نظام تشغيل أقدم. مفهوم مهم آخر يمكن أن تستخدِمه العملية هو مفهوم ربط ملف موجود في القرص الصلب مع الذاكرة أو ما يُسمى mmaping، إذ يبدو الملف كما لو كان أيّ نوع آخر من الذاكرة RAM بدلًا من الاضطرار إلى فتح الملف واستخدام أوامر مثل read() و write()، كما تمتلك مناطق mmaped أذونات يجب تعقّبها مثل القراءة والكتابة والتنفيذ، فمهمّة نظام التشغيل هي الحفاظ على الأمن والاستقرار، لذلك يجب التحقق مما إذا كانت العملية تحاول الكتابة في منطقة للقراءة فقط وإعادة خطأ بذلك. الشيفرة والبيانات يمكن تقسيم العملية بصورة أكبر إلى قسمين هما الشيفرة Code والبيانات Data، إذ يجب الاحتفاظ بشيفرة البرنامج وبياناته بصورة منفصلة لأنها تتطلب أذونات مختلفة من نظام التشغيل، ويسهّل هذا المقال بينهما مشاركة الشيفرة كما سنرى لاحقًا، كما يجب أن يعطي نظام التشغيل إذنًا لشيفرة البرنامج للتمكّن من قراءتها وتنفيذها دون الكتابة فيها، في حين تتطلب البيانات (المتغيرات) أذونات القراءة والكتابة ولا ينبغي أن تكون قابلةً للتنفيذ، ولكن لا تدعم جميع المعماريات ذلك، مما أدى إلى مجموعة واسعة من مشاكل الأمان في العديد منها. المكدس Stack يُعَدّ المكدس جزءًا مهمًا آخر من العملية وهو منطقة من الذاكرة وجزء من قسم البيانات في العملية، ويشارك في تنفيذ أيّ برنامج، كما يُعَدّ بنية بيانات عامة تعمل مثل مجموعة الأطباق، إذ يمكنك دفع push عنصر أو وضع طبق أعلى كومة من الأطباق بحيث يصبح العنصر العلوي، أو يمكنك سحب pop عنصر أو سحب طبق وظهور الطبق السابق. المكدسات أساسية لاستدعاءات الدوال، إذ تحصل على إطار مكدس stack frame جديد في كل مرة تُستدعَى فيها الدالة، وهي منطقة من الذاكرة تحتوي على الأقل على العنوان الذي يجب الرجوع إليه عند الانتهاء ووسائط دخل الدالة وفضاء المتغيرات المحلية. تنمو المكدسات عادةً إلى الأسفل، إذ يبدأ المكدس عند عنوان مرتفع في الذاكرة وينخفض تدريجيًا، في حين تحتوي بعض المعماريات مثل PA-RISC من HP على مكدسات تنمو للأعلى، وتوجد في بعض المعماريات الأخرى مثل IA64 مناطق تخزين أخرى (مخزن داعم للمسجّل) تنمو من الأسفل باتجاه المكدس. يعطي وجود مكدس العديد من الميزات للدوال ومنها ما يلي: أولًا، لكل دالة نسختها الخاصة من وسائط الدخل، إذ يُخصَّص إطار مكدس جديد لكل دالة مع وسائطها في منطقة جديدة من الذاكرة، + وبالتالي لا يمكن أن ترى الدوال الأخرى المتغير المُعرَّف في دالة ما، في حين تُخزَّن المتغيرات العامة التي يمكن أن تراها أيّ دالة في منطقة منفصلة من ذاكرة البيانات، مما يسهّل الاستدعاءات العودية Recursive Calls، وهذا يعني أنّ الدالة حرة في استدعاء نفسها مرةً أخرى، إذ سيُنشَأ إطار مكدس جديد لجميع متغيراتها المحلية. ثانيًا، يحتوي كل إطار على عنوان للعودة إليه، وتسمح لغة C فقط بإعادة قيمة واحدة من الدالة، لذلك تُعاد هذه القيمة إلى دالة الاستدعاء في مسجّل محدد بدلًا من المكدس. ثالثًا، يحتوي كل إطار على مرجع للإطار الذي يسبقه، لذا يمكن لمنقّح الأخطاء العودة للخلف متتبّعًا المؤشرات ليصل إلى أعلى المكدس، ويمكن أن ينتج متعقّب مكدسات Stack Trace الذي يُظهِر لك جميع الدوال التي جرى استدعاؤها حتى الوصول إلى هذه الدالة، ويُعَدّ ذلك مفيدًا لتنقيح الأخطاء، كما يمكنك رؤية كيف تتناسب الطريقة التي تعمل بها الدوال مع طبيعة المكدس، إذ يمكن لأيّ دالة استدعاءُ أيّ دالة أخرى، وبالتالي تصبح الدالة الأعلى بحيث تُوضَع في أعلى المكدس، وستعيد هذه الدالة في النهاية النتيجة إلى الدالة التي استدعتها، أي تخرج من المكدس. رابعًا، تجعل المكدسات استدعاء الدوال أبطأ، لأنه يجب نقل القيم خارج المسجلات إلى الذاكرة، في حين تسمح بعض المعماريات الحاسوبية بتمرير الوسائط في المسجلات مباشرةً، ولكن يجب تدوير المسجلات للحفاظ على دلالات حصول كل دالة على نسخة فريدة من كل وسيط. خامسًا، لا بد أنك سمعت بمصطلح طفحان المكدس Stack Overflow الذي يُعَدّ طريقةً شائعةً لاختراق النظام من خلال تمرير قيم وهمية، فإذا كنت مبرمجًا تقبل بالإدخال العشوائي في متغير المكدس مثل القراءة من لوحة المفاتيح أو عبر الشبكة، فيجب عليك تحديد حجم هذه البيانات صراحةً، إذ يؤدي السماح بأيّ كمية من البيانات دون تحديد إلى الكتابة فوق الذاكرة، مما يؤدي إلى حدوث عطل، ولكن أدرك بعض الأشخاص أنهم إذا كتبوا في ذاكرة كافية فقط لوضع قيمة محددة في جزء العنوان المُعاد من إطار المكدس، فيمكنهم إعادتها في البيانات التي أرسلوها للتو عند اكتمال الدالة بدلًا من الإعادة إلى المكان الصحيح الذي استدعاها، فإذا احتوت هذه البيانات على شيفرة ثنائية قابلة للتنفيذ وتخترق النظام مثل تشغيل طرفية للمستخدِم مع صلاحيات الجذر، فهذا يعني تعرّض حاسوبك للاختراق. يحدث ذلك بسبب نمو المكدس للأسفل، ولكن تُقرَأ البيانات للأعلى أي من العنوان الأدنى إلى العناوين الأعلى. هناك عدة طرق للتغلب على هذه المشكلة، إذ يجب عليك التأكد -بصفتك مبرمجًا- من أنك تتحقق دائمًا من كمية البيانات التي تتلقاها في متغير، ويمكن أن يساعد نظام التشغيل في تجنّب ذلك نيابةً عن المبرمج من خلال التأكد من تمييز المكدس على أنه غير قابل للتنفيذ، وبالتالي لن يشغّل المعالج أيّ شيفرة برمجية، حتى إذا حاول مستخدِم سيئ تمرير شيفرة برمجية إلى برنامجك، وتدعم المعماريات وأنظمة التشغيل الحديثة هذه الوظيفة. سادسًا، يدير المصرّف Compiler المكدسات، فهو المسؤول عن إنشاء شيفرة البرنامج، ويبدو المكدس بالنسبة لنظام التشغيل مثل أيّ منطقة أخرى من الذاكرة الخاصة بالعملية. يعرِّف العتاد المسجِّل بوصفه مؤشر المكدس Stack Pointer بهدف تعقّب نمو المكدس الحالي، إذ يستخدِم المصرّف -أو المبرمج عند الكتابة باستخدام لغة التجميع- هذا المسجِّل لتعقّب الجزء العلوي الحالي من المكدس، وإليك مثال عن مؤشر المكدس: $ cat sp.c void function(void) { int i = 100; int j = 200; int k = 300; } $ gcc -fomit-frame-pointer -S sp.c $ cat sp.s .file "sp.c" .text .globl function .type function, @function function: subl $16, %esp movl $100, 4(%esp) movl $200, 8(%esp) movl $300, 12(%esp) addl $16, %esp ret .size function, .-function .ident "GCC: (GNU) 4.0.2 20050806 (prerelease) (Debian 4.0.1-4)" .section .note.GNU-stack,"",@progbits عرضنا في المثال السابق دالةً بسيطةً تخصّص ثلاثة متغيرات على المكدس، إذ توضّح شيفرة فك التجميع السابقة استخدام مؤشر المكدس في معمارية x86. أولًا، نخصّص مساحةً على المكدس لمتغيراتنا المحلية، كما تنمو المكدسات للأسفل، لذلك يجب أن نطرح من القيمة الموجودة في مؤشر المكدس. تُعَدّ القيمة 16 قيمةً كبيرةً بما يكفي للاحتفاظ بمتغيراتنا المحلية، ولكن يمكن ألّا تكون بالحجم المطلوب للحفاظ على محاذاة المكدس في الذاكرة ضمن الحدود التي يتطلبها المصرّف، إذ نحتاج مثلًا 12 بايت فقط وليس 16 مع 3 قيم من النوع int المكوَّن من 4 بايتات، وننقل بعد ذلك القيم إلى ذاكرة المكدس التي تستخدِمها الدالة الحقيقية. أخيرًا، نسحب القيم من المكدس قبل العودة إلى الدالة الأصلية من خلال تحريك مؤشر المكدس إلى حيث كان قبل أن نبدأ. ملاحظة: لاحظ أننا استخدمنا رايةً خاصةً في مصرّف gcc، وهذه الراية هي -fomit-frame-pointer التي تحدِّد أنه لا ينبغي استخدام مسجل إضافي للاحتفاظ بمؤشر إلى بداية إطار المكدس، إذ يساعد وجود هذا المؤشر منقّحات الأخطاء للانتقال للأعلى عبر إطارات المكدس، ولكنه يجعل المسجِّل متاحًا بصورة أقل للتطبيقات الأخرى. الكومة Heap الكومة Heap هي مساحة من الذاكرة تديرها العملية لتخصيص الذاكرة السريع، وتُستخدَم مع المتغيرات التي لا تكون متطلبات ذاكرتها معروفةً في وقت التصريف، ويُعرَف الجزء السفلي من الكومة بالاسم brk وهو استدعاء النظام الذي يعدّل الكومة، كما يمكن للعملية أن تطلب من النواة تخصيص مزيد من الذاكرة لاستخدامها باستخدام الاستدعاء brk لتوسيع المنطقة إلى الأسفل. يدير استدعاء مكتبة malloc الكومةَ، وهذا يجعل إدارة الكومة أمرًا سهلًا للمبرمج من خلال السماح له بتخصيص وتحرير كومة ذاكرة باستخدام الاستدعاء free، كما يمكن أن يستخدِم الاستدعاء malloc أنظمةً مثل مخصّص الأصدقاء Buddy Allocator لإدارة كومة ذاكرة المستخدِم، ويمكن أن يكون الاستدعاء malloc ذكيًا فيما يتعلق بعملية التخصيص ويمكنه استخدام عمليات ربط مجهولة Anonymous mmaps لذاكرة عملية إضافية، وهو المكان الذي يربط منطقة من ذاكرة RAM الخاصة بالنظام مباشرةً بدلًا من ربط ملف مع ذاكرة العملية، إذ يمكن أن يكون ذلك أكثر كفاءةً، كما أنه ليس مألوفًا أن يكون لأيّ برنامج حديث سبب لاستدعاء brk مباشرة نظرًا لتعقيد إدارة الذاكرة بصورة صحيحة. تخطيط الذاكرة تمتلك العملية مناطق أصغر من الذاكرة المخصَّصة لها ويكون لكل منها غرض محدد. ذكرنا في الشكل السابق كيفية وضع العملية في الذاكرة باستخدام النواة، إذ تحتفظ النواة لنفسها ببعض الذاكرة في الجزء العلوي من العملية، ويمكن مشاركة هذه الذاكرة فعليًا بين جميع العمليات باستخدام الذاكرة الوهمية، كما يوجد أسفل ذلك مساحة للملفات والمكتبات المربوطة mmaped، ثم يوجد المكدس وتحته الكومة، في حين توجد في الجزء السفلي صورة البرنامج كما جرى تحميلها من الملف القابل للتنفيذ على القرص الصلب، وسنلقي نظرةً على عملية تحميل هذه البيانات في مقالات لاحقة. واصفات الملف File Descriptors تعرّفنا سابقًا على الملفات الافتراضية المعطاة لكل عملية وهي stdin و stdout و stderr، إذ يكون لهذه الملفات دائمًا رقم واصف الملف نفسه (0 و 1 و 2 على التوالي)، وبالتالي تحتفظ النواة بواصفات الملفات بصورة فردية لكل عملية. تمتلك واصفات الملفات أذونات أيضًا، إذ يمكن أن تتمكّن من القراءة من ملف ولكن لا يمكنك الكتابة فيه مثلًا، إذ يحتفظ نظام التشغيل عند فتح الملف بسجل أذونات العمليات لهذا الملف في واصف الملف ولا يسمح للعملية بفعل أيّ شيء لا ينبغي فعله. المسجلات Registers يطبّق المعالج عمليات بسيطة على القيم الموجودة في المسجِّلات، وتُقرَأ هذه القيم وتُكتَب في الذاكرة، فلكل عملية منطقة مخصَّصة لها في الذاكرة التي تتعقّبها النواة، لذا يجب تعقّب المسجلات، فإذا حان الوقت لتتخلّى العملية المُشغَّلة عن المعالج لتشغيل عملية أخرى، فيجب حفظ حالتها الحالية، كما يجب أن نكون قادرِين على استعادة هذه الحالة عند منح العملية مزيدًا من الوقت للتشغيل على وحدة المعالجة المركزية، لذا يجب على نظام التشغيل تخزين نسخة من مسجِّلات وحدة المعالجة المركزية في الذاكرة. ينسخ نظام التشغيل قيم المسجّل مرةً أخرى من الذاكرة إلى مسجلات وحدة المعالجة المركزية عندما يحين وقت تشغيل العملية مرةً أخرى وستعود العملية للعمل من حيث توقفت. حالة النواة يجب أن تتعقّب النواة عددًا من العناصر لكل عملية والتي سنوضّحها فيما يلي. حالة العملية يجب على نظام التشغيل تعقّب حالة العملية، فإذا كانت العملية قيد التشغيل حاليًا، فيجب أن تكون بحالة تشغيل Running، لكن إذا طلبت العملية قراءة ملف من القرص الصلب، فسنعلَم من تسلسل الذواكر الهرمي أنّ ذلك يمكن أن يستغرق وقتًا طويلًا، إذ يجب على العملية التخلي عن تنفيذها الحالي للسماح بتشغيل عملية أخرى، ولكن لا يجب أن تسمح النواة بتشغيل العملية مرةً أخرى حتى تصبح البيانات من القرص الصلب متاحةً في الذاكرة، وبالتالي يمكن تحديد العملية على أنها في حالة انتظار القرص الصلب Disk Wait حتى تصبح البيانات جاهزةً. الأولوية Priority تُعَدّ بعض العمليات أكثر أهميةً من غيرها وتحظى بأولوية أعلى. الإحصائيات يمكن للنواة الاحتفاظ بإحصائيات حول سلوك كل عملية والتي يمكن أن تساعدها في اتخاذ قرارات حول كيفية تصرف العملية مثل معرفة ما إذا كانت العملية تقرأ من القرص الصلب في أغلب الأحيان أم أنها تنفّذ عمليات مكثفةً في وحدة المعالجة المركزية بمعدّل أعلى. ترجمة -وبتصرُّف- للقسمين What is a process? و Elements of a process من الفصل The Process من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: تسلسل العمليات الهرمي واستدعاءات النظام Fork و Exec في نظام تشغيل الحاسوب المقال السابق: استدعاءات النظام والصلاحيات في نظام التشغيل دور نظام التشغيل وتنظيمه في معمارية الحاسوب العمليات (Processes) في أنظمة التشغيل أنظمة التشغيل للمبرمجين
-
كثيرًا ما يتساءل الناس عن مدير المنتجات ودوره في عملية إنشاء المنتج الذي يتم تطويره في عدة مراحل للوصول إلى شكله النهائي. حسنًا، يمكن القول أنك ستحتاج أولًا إلى فكرة عن مظهر المنتج الذي سيبدو عليه في النهاية، ثم تتبع ذلك عملية طويلة لإنشائه، والتي تستغرق كثيرًا من الوقت والجهد، وفريقًا من المحترفين، وقائدًا لهم؛ كما يجب على الشركة لتحويل أيّ فكرة إلى منتج مربح أن تمر بعدة مراحل لوضع رؤية وتحديد استراتيجية وتطوير منتج وبيعه للأشخاص المناسبين، وسنوضّح في هذا المقال تفاصيل إدارة المنتجات ومراحلها الرئيسية ومسؤوليات مدير المنتجات في هذه العملية. مفهوم إدارة المنتجات إدارة المنتجات هي عملية تركز على جلب منتج جديد إلى السوق أو تطوير منتج موجود مسبقًا، حيث تبدأ هذه العملية بفكرة عن منتج سيتفاعل معه العميل، وتنتهي بتقييم نجاح هذا المنتج. يوحد هذا النوع من الإدارة الأعمالَ وتطوير المنتجات والتسويق والمبيعات مع بعضها البعض، وتشير الدراسات إلى أن الإدارة الفعالة للمنتجات يمكن أن تزيد الربح بنسبة 34.2%، مما يثبت أهمية تطبيقها. يُعَد إنشاء وتوثيق استراتيجية المنتج أحد الأنشطة الهامة لإدارة المنتجات، وهي عملية واسعة النطاق ومهمة جدًا. يقود مدير المنتجات عملية إدارتها، إذ لا يجب الخلط بين دوره ودور مدير المشروع، فمدير المشروع مسؤول عن جزء واحد من دورة حياة المنتج وهو تطوير المنتج، بينما تكون مسؤولية مدير المنتجات هي قيادة المنتج من بذرة فكرته حتى إطلاقه مع التركيز على الميزات وقيمة الأعمال والعميل. من هو مدير المنتجات؟ مدير المنتجات هو الشخص الذي ينشئ رؤيةً داخليةً وخارجيةً للمنتج ويقود تطوير المنتج من البداية، ويحدد احتياجات العملاء، ويعمل مع أصحاب المصلحة والفرق على إنشاء المنتج المطلوب، ويتحمل مسؤولية نجاح المنتج بصورة عامة. يحدّد مارتي كاجان Marty Cagan -مؤلف كتاب Inspired: How to Create Products Customers Love- هدف مدير المنتجات بالطريقة التالية: "اكتشاف منتج قيّم وقابل للاستخدام والتطبيق". لذا يجب أن يكون هذا المدير على دراية بثلاثة مجالات رئيسية هي: الأعمال والتقنيات وتجربة المستخدم. سنوضح فيما يلي المسؤوليات الأساسية لمدير المنتجات: تحديد الفرص: أول شيء يفعله مدير المنتجات هو رؤية الفرصة لتطوير منتج جديد ناجح أو تحسين منتج حالي وإضافة الميزات الضرورية إليه. يجب أن يعرف هذا المدير هنا الاتجاهات الدارجة الحالية وأن يكون لديه فهم عميق بالسوق لاتخاذ القرارات الصحيحة عندما تقرر الشركة كيفية بناء منتج أو تحسينه، وهو مسؤول عن نتيجة إطلاق المنتج أيضًا. تطوير رؤية واستراتيجية المنتج: يجب على مدير المنتجات تحديد مهمة المشروع طويلة الأمد وبناء خطة واضحة وواقعية لكيفية الوصول إلى النتيجة المرجوة. أظهر استطلاع حديث أن النشاط الرئيسي لمعظم مدراءàà المنتجات (84% منهم) هو وضع استراتيجية المنتج، ويتبع ذلك صياغة خارطة طريق واضحة والإشراف على استكمالها. أن يدير الفريق وأصحاب المصلحة: يجب على مدير المنتجات التأكد من أن جميع أعضاء الفريق يعملون بصورة متناغمة لتحقيق الهدف الرئيسي. تتمثل إحدى أهم وظائف مدير المنتجات في توصيل المتطلبات بوضوح إلى فريق التطوير وتنظيم عملية التطوير بأكثر الطرق كفاءة، كما يجب عليه التفاوض مع أصحاب المصلحة وتحقيق التوازن بين متطلباتهم وتوقعاتهم. الأنشطة التسويقية: يُعَد التسويق أحد العوامل الرئيسية التي تساهم في نجاح المنتج، لذلك يتعاون مديرو المنتجات مع مديري تسويق المنتجات. يتضمن ذلك إجراء بحث في السوق ومراقبة اتجاهات الصناعة الدارجة الحالية وجمع ملاحظات العملاء وتحليلها وتحديد الأسعار وتطوير استراتيجية التسويق. التحسين المستمر للمنتج: يبدو للوهلة الأولى أن مدير المنتجات يؤدي مهامًا إداريةً فقط بدلًا من تطبيق شيء ما، ولكن ذلك غير صحيح، إذ يعمل مدراء المنتجات باستمرار على تحسين المنتج الحالي واختباره وتحليل البيانات وإدارة العيوب. يجب على مدير المنتجات اتخاذ القرار النهائي بشأن الشكل الذي يجب أن يكون عليه المنتج النهائي واستراتيجية تطويره وإطلاقه. وبالرغم من عدم وجود مجموعة واحدة من مؤشرات الأداء الرئيسية KPIs والمسؤوليات لمديري المنتجات، إلّا أنها تتضمن عادةً تحقيق الدخل المادي وتفاعل المستخدم ومستوى رضاه، ويمكن أن تختلف مؤشرات الأداء الرئيسية باختلاف الشركة والصناعة. يركز بعض مديري المنتجات بصورة أساسية على التطوير وكتابة المواصفات والإشراف على تقدّم التطوير، بينما يُظهِر البعض الآخر مزيدًا من التركيز على التسويق والمبيعات من خلال تطوير خطة التسويق وتدريب فريق المبيعات. أنشطة مديري المنتجات. المصدر: TheProductManager دورة حياة إدارة المنتجات ودور مدير المنتجات تتتالى الإجراءات في الإدارة الفعلية للمنتجات من الإجراءات الاستراتيجية إلى التخطيطية. تتضمن العملية الكاملة لهذه الإدارة ما يلي: تطوير الرؤية. فهم العميل. تطوير الاستراتيجية. تطوير المنتج. التسويق والمبيعات. تتبّع المقاييس. يمكن أن تتضمن كل مرحلة من هذه المراحل أنشطةً داخليةً Inbound وخارجيةً Outbound. وبطبيعة الحال، لا يطبّق مدير المنتجات جميع الأنشطة، ولكنه يشرف على تنفيذها، إذ تركز الأنشطة الداخلية على تطوير المنتج وتشمل تحديد الرؤية والاستراتيجية وتطوير المنتج والاختبار والإطلاق، بينما تكون الأنشطة الخارجية مُوجَّهة نحو تسويق المنتج ومبيعاته، ويتضمن ذلك العلامة التجارية والمبيعات وتحليل ملاحظات العملاء. تطوير مدير المنتجات للرؤية تُعَد الرؤية جزءًا مهمًا من إدارة المنتج. إذا أردنا مقارنة إدارة المنتج بطريقٍ ما، فإن الرؤية هي لافتة الطريق ووجهته، حيث تحدد الرؤية منتجك النهائي وتظهِر الاتجاه نحو تحقيقه. لا تُعَد الرؤية استراتيجيةً لتطوير المنتج بعد، ولكنها المكان الذي يبدأ فيه تطوير الاستراتيجية بإدارة الأفكار عندما يناقش الفريق منتجًا جديدًا. يمكن التعبير عن الرؤية خلال عملية العصف الذهني أو يمكن أن تستند إلى تراكم من الأفكار. يحدّد مدير المنتجات عند تطوير الرؤية أهداف المنتج ويعرّف المواصفات. تجيب رؤية المنتج ذات التعريف الجيد على الأسئلة التالية: ما هي شخصية مستخدم المنتج؟ ما هي المشاكل التي سيحلّها المنتج؟ كيف يمكننا قياس نجاح المنتج؟ يقترح جيفري موور Geoffrey Moore في كتابه "Crossing the Chasm" استخدام النموذج التالي لتعريف رؤية المنتج: نموذج بيان رؤية المنتج. المصدر: ProdPad يوصي جيفري مور بإبقاء الرؤية قصيرة، فعلى حد تعبيره: "إن لم تتمكن من اختبار رؤية المنتج عبر عرض موجز أو ما يسمى بحديث المصعد Elevator Pitch، فلن تُعَد هذه الرؤية جاهزةً بعد". تتمثل رؤية أمازون Amazon مثلًا في "أن تكون الشركة الأكثر تركيزًا على العملاء على الأرض، حيث يمكن للعملاء العثور على أي شيء يرغبون في شرائه عبر الإنترنت واكتشافه، وتسعى لتزويد عملائها بأقل الأسعار الممكنة". البحث في السوق وفهم العملاء أبحاث السوق هي عملية جمع المعلومات وتحليل السوق وعملائها الحاليين أو المُحتمَلين، وتشمل موازنة المنتجات المماثلة الموجودة مسبقًا ودراسة المنافسة وتحديد مجموعات العملاء المستهدَفة. تُعَد معرفة عميلك الأساسَ لإنشاء منتج ناجح، حيث يتوقع 76% من المستهلكين أن تفهم الشركات احتياجاتهم، وقد سجّلت 84% من الشركات التي ركّزت على تحسين تجربة العملاء زيادةً في الإيرادات. يتعاون مدير المنتجات مع مدير تسويق المنتجات لإجراء العديد من الأبحاث للحصول على فهم عميق لمستهلكي المنتج المحتملين، وتتضمن هذه العملية عدة وجهات نظر هي: يعني إنشاء شخصيات المستخدم وصف شخصيات خيالية تمثّل أنواع المستخدمين التي يمكن أن يكون لها اهتمام بالمنتج المستقبلي، أو بعبارة أخرى يمكن تمثيلها باستخدام صورة لعميلك المثالي. يمكن أن تتضمن شخصيات المستخدم معلومات مثل العمر والجنس ومستوى التعليم ومتوسط الدخل والأهداف الحياتية والمشاكل الشائعة وعادات الإنفاق وغير ذلك. مثال عن شخصية مستخدم. المصدر: CleverTap يُعَد تحديد احتياجات العملاء الطريقة الوحيدة لإنشاء وتقديم المنتج المطلوب، حيث يمكن تصنيف العملاء ضمن مجموعات وفقًا لاحتياجاتهم الأربعة الرئيسية وهي: السعر والجودة والاختيار والراحة، بالإضافة إلى تحديد مشاكل وضرورة منتج معين. تتضمن دراسة سلوك العملاء ضرورة فهم نفسية ودوافع العملاء المستهدفين، ويتضمن ذلك معرفة كيف يفكر العملاء ويختارون بين البدائل المختلفة، وكيف يجرون الأبحاث ويتأثرون بمحيطهم ويتفاعلون مع حملات التسويق وغير ذلك. يمكن أن تجري الشركة أبحاث السوق (بحث أولي)، أو يمكن أن تُؤخَذ هذه الأبحاث من مصدر خارجي (بحث ثانوي). يتضمن البحث الثانوي البيانات المُنتَجة مسبقًا، والتي يمكن العثور عليها في قواعد البيانات الإحصائية والمجلات والمصادر عبر الإنترنت وغير ذلك، بينما يمكن تكييف البحث الأولي مع احتياجات الشركة، ويمكن أن يكون نوعيًا أو كميًا. يركّز البحث النوعي على تحديد المشاكل والقضايا ذات الصلة، ويتضمن ذلك المقابلات الشخصية والاستطلاعات الجماعية ومجموعات التركيز. يعتمد بحث السوق الكمي على جمع البيانات والتحليل الإحصائي، ويسمح لمدير المنتجات بالوصول إلى جمهور أكبر وجمع معلومات عامة، بينما يوفر البحث النوعي نظرةً إلى مشكلةٍ ما وتحديد الرغبات والاحتياجات والعقبات المحتملة. أنواع أبحاث السوق. المصدر: Product coalition تُعَد أبحاث السوق مهمةً لتطوير المنتجات الجديدة، سواءً في مرحلة التنفيذ أو في مرحلة التسويق والمبيعات. يمكن للشركة بمساعدة أبحاث السوق فهم ما يريده العملاء وتطوير استراتيجية تسمح بإنتاج منتج ناجح. تطوير مدير المنتجات للاستراتيجية يجب بمجرد أن تكون لديك الرؤية ومعرفة السوق وفهم احتياجات العملاء صياغةُ استراتيجية منتج محددة وفقًا لذلك. تحدد الرؤية أهداف المنتج، بينما تصِف الاستراتيجية طريقةً لتحقيقها، وتضع المعالم الرئيسية لها، حيث يجب أن تكون هذه الاستراتيجية خطةً واضحةً وواقعيةً للفريق الذي يعمل على المنتج. تحدد استراتيجية المنتج الفعالة ميزات المنتج الرئيسية والمستخدمين واحتياجاتهم ومؤشرات الأداء الرئيسية التي يجب أن يلبيها المنتج. عناصر الاستراتيجية. المصدر: Romanpichler تُوثَّق استراتيجية المنتج على شكل خارطة طريق Roadmap مكتوبة تسمح للفريق بالتحكم بالعمل في جميع المراحل. وتُعَد خارطة الطريق أداةً توفر إطار عمل للفريق مع جدول زمني وإجراءات محددة، وتوضح الرؤية والأهداف وحالة تطوير المنتج الحالية. تُعَد خارطة الطريق الجيدة واضحةً وتعمل بوصفها دليلًا مرئيًا لجميع أعضاء الفريق، ويجب أن توضّح خارطةُ الطريق الحالةَ الحالية للأشياء والخطوات التالية بغض النظر عن بنيتها. هناك قوالب مختلفة لخارطة الطريق وتعتمد تنسيقاتها على عدد المنتجات (خارطة طريق منتج واحد أو منتجات متعدد) وجوانب تطوير المنتج (موجهة نحو تحقيق الهدف أو الميزة)، ولكن يجب أن تجمّع أي خارطة طريق العناصر حسب تسلسل تطبيقها، كما يمكن أن تكون خرائط الطريق داخليةً أو خارجية. خارطة طريق قائمة على الموضوع. المصدر: Prodpad تُستخدَم خارطة الطريق الداخلية على مستوى الشركة، وتظهِر الرؤية والأهداف قصيرة وطويلة الأمد والعمليات المتصلة. يمكن للفرق التي تعمل في مراحل مختلفة من تطوير المنتج تتبّع الجدول الزمني والبقاء على دراية بالإجراءات القادمة، حيث يستخدم مدير المنتجات والمدير التنفيذي خارطة طريق داخلية للتحكم في التقدم. تكون خارطة طريق المنتج الخارجية أقل تعقيدًا وتُنشَأ لأصحاب المصلحة أو المساهمين والعملاء المحتملين والحاليين والمستثمرين وغيرهم. خارطة الطريق العامة المؤقتة. المصدر: Trello يُعَد تحديد الأولويات مسؤوليةً مهمةً لمدير المنتجات في مرحلة إعداد خارطة الطريق، حيث يجب أن تتراوح الأهداف والغايات والأنشطة من الأكثر أهميةً إلى الأقل أهميةً. يجب على مدير المنتجات توصيل الاستراتيجية إلى فريق المنتجات وأصحاب المصلحة عندما تكون جاهزة، ويجب أن يركّز على العملاء وأصحاب المصلحة في الوقت نفسه، فبالرغم من أن العميل يجب أن يكون دائمًا أولويةَ مدير المنتجات، إلا أن الحفاظ على علاقات عمل فعالة مع أصحاب المصلحة أمرٌ مهم أيضًا. لأصحاب المصلحة تأثير كبير على تطوير المنتج والقدرة على تخفيض الميزانية أو تغيير الجدول الزمني، ويمكنهم اقتراح تطبيق ميزات المنتج التي يجدونها ضرورية ومهمة ولكنها غير مجدية تمامًا للمستخدمين، لذا تتمثل مهمة مدير المنتجات في توصيل الاستراتيجية لأصحاب المصلحة لضمان الفهم المشترك للرؤية. عمل مدير المنتجات على التنفيذ والاختبار يعمل فريق المنتجات على المنتج نفسه أثناء مرحلة التنفيذ، حيث يبنون منتجًا جديدًا أو يضيفون ميزات جديدة إلى منتج موجود مسبقًا. المراحل الرئيسية في هذه المرحلة هي تطوير المنتج والاختبار الداخلي والخارجي وتطبيق نتائج الملاحظات. يتحكم مدير المنتجات طوال مرحلة التنفيذ في تطبيق خارطة الطريق ويشارك في الأنشطة المصاحبة لها. يبدأ تطوير المنتج بتحديد المواصفات التقنية وإنشاء نماذج أولية وتصميم نموذج محاكي Mockup، حيث يغطّي فريق تجربة المستخدم UX هذه الأنشطة ويمكن أن يشارك مدير المنتجات في كتابة المواصفات التقنية. الهدف الرئيسي لمدير المنتجات هو تحديد ما يريده المستخدمون وإيصال هذه المعلومات إلى فريق التطوير ومدير المشروع، لذا يجري مجموعات تركيز ومقابلات شخصية مع العملاء المحتملين، حيث تسمح نتائج هذه الأنشطة لمدير المنتجات بتحديد أولويات الميزات الضرورية وغير الضرورية. يكتب مدير المنتجات المستندات المتعلقة بالمنتج مثل مستند متطلبات المنتج Product Requirement Document -أو PRD اختصارًا- ومستند المواصفات الوظيفية Functional Specifications Document -أو FSD اختصارًا. تتمثل إحدى المسؤوليات الأساسية لمدير المنتجات في تحديد منتج الحد الأدنى Minimum Viable Product -أوMVP اختصارًا- والتأكد من أنه يخدم الغرض منه. يضبط مدير المنتجات آليةً لجمع الملاحظات عند إصدار منتج الحد الأدنى MVP، ويجمع الملاحظات ويعدّل متطلبات المنتج بناءً على مدخلات المستخدم. أفاد 60% من مديري المنتجات بأن أفضل أفكارهم جاءت مباشرةً من ملاحظات العملاء. يُعَد اختبار أ/ب A/B Testing أحد أكثر تقنيات التقييم شيوعًا، والهدف الرئيسي لهذا الاختبار هو اختيار ميزات المنتج الأكثر فائدةً للعملاء أو تفعيل مشاركة أكبر للعملاء. يحدد مدير المنتجات سيناريوهات الاختبار بالتعاون مع متخصص تجربة المستخدم UX، ويتتبّع النتائج ويوصِل التغييرات إلى مدير المشروع و/أو فريق التطوير. يطوّر مديرُ المنتجات في بعض الأحيان لإجراء اختبارات ناجحة علاقةً مع العملاء المحتملين للتأكد من أنهم صادقون بشأن قابلية استخدام المنتج، ويجب أثناء الاختبار تحليل استجابة المستخدمين وملاحظات العملاء. يجب على مدير المنتجات نقل النتائج إلى مدير المشروع عندما تكون جاهزة، وذلك حتى يتمكّن المطورون من إعداد البرنامج للإطلاق أو إدخال تغييرات على المنتج الحالي. دور مدير المنتجات في التسويق والمبيعات حان الوقت لدخول المنتج إلى السوق بمجرد اكتماله، حيث يجب في هذه المرحلة الانتهاء من خطط التسويق والإطلاق وتدريب فرق المبيعات على بدء التوزيع. الجوانب الثلاثة المهمة لإطلاق منتج ناجح هي: بناء وعي العملاء بمساعدة الحملات التسويقية والأنشطة الترويجية المختلفة. تحديد استراتيجية التسعير على أساس قيمة المنتج والمنافسة في السوق. اختيار توقيت الإصدار الأكثر فاعليةً، مع مراعاة جاهزية العملاء وأداء المنتجات الحالية الأخرى وعمليات إطلاق المنافسين وغير ذلك. تتضمن استراتيجية التسويق الكاملة الكثير من أنشطة ما قبل الإطلاق التي تهدف إلى خلق ضجة حول منتجك حتى قبل ظهوره في السوق، وتشمل هذه الأنشطة الإعلان عبر قنوات الوسائط المختلفة وهدايا ما قبل الإطلاق وإنشاء محتوًى عالي الجودة ومُحسَّن باستخدام السيو SEO وإلخ، ويجب أن تركّز جميعها على المجموعة المستهدفة من العملاء المُحدَّدة مسبقًا خلال أبحاث السوق السابقة. يقدّم مدير المنتجات خلال كامل هذه العملية خطة تشغيل تهدف إلى تتبع نمو المنتج في السوق، حيث سنتحدث عن هذه العملية والمقاييس المحددة في القسم التالي. يمكن أن يكون لمدير المنتجات المزيد من المسؤوليات في هذه المرحلة في الشركات الناشئة والشركات الصغيرة التي ليس لديها منصب منفصل لمدير تسويق المنتجات، ويمكن في هذه الحالة أن يشارك مدير المنتجات في العمليات التالية: كتابة الأعمال وحالات الاستخدام. تشكيل خطة إطلاق المنتج ونماذج التوزيع. تحديد السوق المستهدف. تحديد استراتيجية التسعير. إعداد دعم المبيعات والأدوات المطلوبة. يمكن توزيع هذه الأنشطة بين المديرين التنفيذيين من فرق المنتجات والمبيعات والتسويق. تتبع مقاييس المنتج يراقب مدير المنتجات بعد إطلاق المنتج تقدّمه ويحلل البيانات لفهم نجاحه. ويمكن تنظيم مقاييس إدارة المنتجات ومؤشرات الأداء الرئيسية في عدة مجموعات رئيسية هي: المقاييس المالية لتحديد الإيرادات، مثل الإيرادات المتكررة الشهرية التي توضح الإيرادات المتعلقة بالمنتج في شهر واحد. المقاييس التي تمثل تفاعل المستخدم، مثل مدة الجلسة التي تقيس مدة استخدام المنتج. المقاييس التي توضح اهتمام المستخدم، أي معدل الاحتفاظ الذي يحسب عدد المستهلكين الذين بقوا مخلصين للشركة بعد فترة زمنية معينة. المقاييس التي تقيس شعبية المنتج مثل عدد الجلسات لكل مستخدم التي توضح عدد مرات استخدام الموقع. المقاييس التي تُظهِر رضا المستخدم، مثل صافي درجة الترويج الذي يحدد عدد العملاء الذين يُحتمَل أن يوصوا بالمنتج. لا يكفي بالتأكيد مجرد اختيار المقاييس لمتابعة وجمع المعلومات، فما يهم هو مزيد من التحليل والرؤى القيّمة التي يمكن الحصول عليها من البيانات للتأثير لاحقًا في صنع القرار. ستظهِر نتائج هذا التحليل لفريق الإدارة مدى جودة أداء المنتج وما إذا كانت هناك أي تغييرات ضرورية، سواءً كانت إضافة ميزات جديدة أو تعديل استراتيجية المبيعات أو تحديث حملة التسويق. أدوار مدير المنتجات في فريق المنتجات يمكن أن يختلف دور مدير المنتجات بصورة كبيرة، وذلك اعتمادًا على حجم الشركة ومرحلة نضجها، حيث يمكن استبدال هذا المنصب في شركة ناشئة صغيرة بمدير المشروع أو مالك المنتج الذي نناقشه لاحقًا، بينما يمكن في الشركات الصغيرة أن يكون مدير المنتجات متعدد المهام والمهارات مع مجموعة واسعة من المسؤوليات، بما في ذلك التسويق والتسعير وحتى المبيعات. لكن تُحدَّد الأدوار في شركة أكبر وأنضج بصورة أوضح ويكون لها نطاق وظيفي أضيق، وتنشأ مع نمو الأعمال والبدء في تطوير منتجات متعددة الحاجةُ إلى مدير منتجات رئيسي للإشراف على مجموعة المنتجات بأكملها. يُعَد مدير المنتجات جزءًا من فريق المنتجات الذي يتكون من عدة أدوار، بما في ذلك الأدوار الموجودة على مستوى الإدارة، فهناك عادةً ثلاثة أدوار هي: مدير المنتجات ومدير المشروع ومدير تسويق المنتجات؛ كما يمكن أن يتأثر تطوير المنتج بأصحاب المصلحة ومحلل الأعمال، وهو الشخص الذي يترجم طلبات أعمال أصحاب المصلحة إلى مهام تطوير للفريق التقني. لكل مدير مسؤولياته الخاصة التي تقتصر على مجال اهتمامه، فدور مدير المنتجات أوسع بكثير ويتضمن أنشطةً على كل المستويات. لنحدّد النطاق الوظيفي للمناصب الأخرى لفهم دور هذا النوع من المدراء أكثر. مديرو المشاريع ومديرو المنتجات ينظم مدير المشروع العملية الداخلية لتطوير المنتج مع التأكد من أن المشروع يتبع جدولًا زمنيًا ويتناسب مع الميزانية. يتتبّع هذا الشخص التقدم وينسّق جميع الموارد الداخلية وأعضاء الفريق (المهندسين والمصممين) لتسليم المنتج في الوقت المحدد. بينما تكون مسؤوليات مدير المنتجات عالية المستوى، حيث أنه يحدد الرؤية الشاملة، ويطور الاستراتيجية، ويحدد المتطلبات وأولوياتها مع مدير المشروع، ويتبع هذه الرؤية والاستراتيجية لتلبية المتطلبات المحددة مسبقًا من خلال إسناد المهام وتخطيط الجداول الزمنية وتخصيص موارد المشروع، وبالتالي يكون دور مدير المنتجات أكثر استراتيجية، بينما يكون دور مدير المشروع تخطيطيًا بدرجة أكبر. يتعاون مديرو المنتجات بصورة وثيقة مع الأقسام الأخرى، مثل التسويق والمبيعات، بينما لا يفعل مديرو المشاريع ذلك، ويضعون معظم تركيزهم على العمل مع فريق التطوير؛ لذا لا تزال هذه الأدوار مكملةً لبعضها البعض، ولها عدد من الوظائف المتداخلة كونها متميزة بصورة واضحة لأنها مسؤولة عن جوانب مختلفة من تطوير المنتج، فالمسؤوليات المشتركة مع مدير المشروع هي: العمل على توثيق المشروع. التحكم في عملية التطوير. التواصل مع أصحاب المصلحة والعملاء. إعلام العملاء و/أو أصحاب المصلحة بمراحل العمل. مديرو تسويق المنتجات ومديرو المنتجات مديرو تسويق المنتجات مسؤولون عن تسويق المنتج وعلامته التجارية وتحديد موقعه، ويجرون أبحاث السوق والتعبئة وتدريب فريق المبيعات وتخطيط الأنشطة والفعاليات الترويجية، ويكونون عادةً مسؤولين عمّا يلي: تحديد شخصية المستخدم والتعرف على العملاء. إنشاء استراتيجية تسويق المنتج. إيصال قيمة المنتج إلى السوق. تطوير أدوات مبيعات المنتج. تُعَد وظائف مديري المنتجات أوسع بكثير لأنهم يتحملون المسؤولية النهائية عن إنشاء المنتج، مع وجود التسويق جزءًا من هذه المسؤولية، فهم -كما ذكرنا سابقًا- يعملون مع مدير تسويق المنتجات لخلق فهم واضح للعملاء المحتملين. تسمح طرق البحث المختلفة -مثل المقابلات والاستطلاعات ومجموعات التركيز وغيرها- بتحديد النقاط التي يشتكي منها العملاء والمشاكل الرئيسية التي يجب استهدافها وإنشاء شخصيات المستخدم وتوقع سلوك العميل، ثم تُستخدَم جميع هذه المعلومات المهمة لتطوير ميزات المنتج المطلوبة وتحسين تجربة المستخدم للوصول إلى الجمهور المناسب. تمثل المسؤوليات التالية بعض المسؤوليات التي يتشارك بها مدير المنتجات مع مدير تسويق المنتجات: التسعير. جمع ملاحظات العملاء. إجراء أبحاث السوق. تطوير أدوات المبيعات. تحليل بيانات المبيعات. لكن يعتمد نطاق المسؤوليات على حجم الشركة، فقد أظهر البحث مثلًا أن 69% من مديري المنتجات الذين يعملون في شركات أصغر -أقل من 1000 شخص- مسؤولون عن أبحاث المستخدمين. أصحاب المصلحة ومديرو المنتجات أصحاب المصلحة هم الأشخاص الذين لديهم اهتمام بالمنتج النهائي، ويمكنهم التأثير على عملية إدارة المنتج وتطويره والمشاركة في صنع القرار. يمكن أن يكون أصحاب المصلحة في مجال إدارة المنتجات عملاءً أو مستثمرين أو حتى مطورين ومستخدمين للمنتج، أو يمكنهم أن يمثّلوا جميع هذه الأدوار مجتمعة. من مسؤوليات أصحاب المصلحة ما يلي: تقديم ملاحظات على أفكار المنتج. وصف المتطلبات بالتفصيل. المساهمة بميزات جديدة لتطوير المنتج. الموافقة على ميزات المنتج أو رفضها. التأثير في صنع القرار والجدول الزمني. تحديد المخاطر والمشاكل المحتملة في إدارة المنتج. توفير الموارد اللازمة لتطوير المنتج. بما أنه توجد العديد من المجموعات المختلفة من أصحاب المصلحة من المستثمرين إلى المستخدمين النهائيين ويمكنهم جميعًا التأثير على النتيجة النهائية لتطوير المنتج، فيجب على مديري المنتجات التواصل والعمل معهم جميعًا. تسمى هذه العملية إدارة أصحاب المصلحة، وتتضمن التنقل وإدارة مطالب أصحاب المصلحة، ويجب تحديد أصحاب المصلحة وأولوياتهم من خلال اهتماماتهم وتأثيرهم، وبالتالي يمكن لمديري المنتجات إما إبقاؤهم على اطلاع بدورة حياة المنتج أو إشراكهم في العملية بنشاط. يُعَد الحصول على دعم أصحاب المصلحة أمرًا حيويًا لعملية تطوير سلسِة وناجحة للمنتج، لذلك يجب على مديري المنتجات تشجيع علاقات العمل القوية المبنية على الثقة والتعاون. مالك المنتج ومدير المنتجات يُستخدَم هذان المصطلحان بالتبادل في أغلب الأحيان، ولكن هناك فرق بينهما، حيث يأتي مفهوم مالك المنتج من إطار عمل سكروم Scrum الذي يُعَد إطار عمل أجايل Agile لتطوير حلول للمشاكل المعقدة، إذ يُعَد مالك المنتج وفقًا لدليل سكروم "مسؤولًا عن زيادة قيمة المنتج الناتجة عن عمل فريق سكروم". يعمل مالكو المنتج داخليًا، ويشاركون بعمق في العملية التقنية، ويتعاونون بصفة وثيقة مع الفرق التقنية، ويحددون التكرارات، ويضعون معايير القبول، ويقودون الأعمال المتراكمة، ويقبلون القصص ويتأكدون من أنها جاهزة؛ ولكنهم يعملون مع مدراء المنتجات في تحديد خطط الإصدارات وتعريف الميزات وإدارة العيوب، لذا يكون دور مالك المنتج تخطيطيًا ويركّز على المهام قصيرة الأمد أكثر من دور مدير المنتجات. يمكن أن يبدو منصب مالك المنتج أكثر تشابهًا مع منصب مدير المشروع، حيث يشرف كلاهما على فرق التطوير، ولكن مالك المنتج أكثر توجهًا نحو التفاصيل ولا يتواجد إلا بصفته جزءًا من فرق سكروم. يكون مدير المشروع ضروريًا لتنسيق فرق متعددة تعمل في مشاريع معقدة أو خطرة وإدارة الوثائق ولتتبّع تقدم الفريق في بعض الأحيان. كيف تصبح مدير منتجات جيد؟ لا يمتلك مدير المنتجات شهادةً في إدارة المنتجات في أغلب الأحيان، حيث يكون شخصًا لديه خلفية في التسويق أو تصميم تجربة المستخدم أو هندسة البرمجيات، ويكون عادةً شخصًا أصبح خبيرًا في مجالٍ ما، ثم اكتسب خبرةً في تخصصات أخرى. وبطبيعة ليس العنصر الرئيسي الخبرة في حد ذاتها، ولكن المعرفة بالمجال، فكلما زادت معرفتك بسوق معين وعملائه، فستتمكّن من قيادة منتجك إلى النجاح بصورة أفضل. مع ذلك، الأمر لا يخلو من ضرورة التعرف على أساسيات هذا المجال وأخذ معارفه النظرية اللازمة للعمل قبل الانتقال الفعلي لهذا المنصب، لأن الانتقال دون دراية جيدة أو كافية، لن يجعل منك مديرًا ناجحًا للمنتجات في نهاية المطاف؛ وعليه، يُنصح بحضور دورات تخص هذا المجال لتساعدك على فهم أساسيات المنصب والأدوار التي يجب القيام بها، مع إجراء تطبيقات عملية تدريبية بمعطيات حقيقية للدخول في المجال وتسهيل طريقك فيه. وبهذا الصدد، يمكنك يمكن اقتراح دورة إدارة تطوير المنتجات المقدمة من أكاديمية حسوب بلغة عربية، والتي لن تكون بحاجة عند حضورها إلى أي خبرة مسبقة، حيث ستتعلم فيها الأسس النظرية الخاصة بهذا المجال مطروحة من قبل خبراء فعليين فيه، كما ستعمل على تطبيق مشاريع حقيقية تدعم رحلة تعلمك، لتكون في النهاية قادرًا على تسليم مشروعك تخرجك التطبيقي الواقعي حتى تتحصل في النهاية على شهادة معتمدة بإدارة تطوير المنتجات تخولك لدخول مجال إدارة المنتجات والنجاح فيه. سنوضّح فيما يلي بعض التوصيات للأشخاص من خلفيات مختلفة، والتي ستساعدك على سد الفجوة والانتقال إلى دور مدير المنتجات: إذا كنت تقنيًا: يُعَد مدير المنتجات منصبًا قياديًا، لذا كن قياديًا وشارك في قرارات مصيرية حول منتج ما، واقترح ميزات جديدة ووسائلًا تطبيقها، مع دعم أفكارك بالبحث باستخدام مجموعات التركيز، وابدأ مشروعك الجانبي أو الناشئ. ليس من الضروري أن تنجح في تقديم ممارسة قيّمة للمهارات المتعلقة بمنصبك أو في فهم مؤشرات الأداء الرئيسية، كما يمكن أن يمثل أيّ مشروعٍ دراسةَ حالة لإظهار موظفيك المستقبليين أو مديرك الحالي. إذا كنت قادمًا من مجال التسويق: يمكن أن تتطابق أنشطة مديري المنتجات ومديري التسويق في أغلب الأحيان، ولكن لاحظ الاختلاف الكبير بينهما، حيث يشارك مدير المنتجات بصورة كبيرة في تطوير المنتج. ستكون أهدافك الرئيسية هي تعلّم فهم سير عمل التطوير والتقنيات والتواصل الناجح مع الفريق الهندسي. لا يُتوقَّع من محترفي التسويق في العديد من المشاريع المشاركة مباشرةً في الجانب التقني، لذا يجب عليك بدء المحادثات وتطبيق المهارات التي تملكها فعليًا. وإن عرفتَ المشاكل التي يواجهها العملاء كل يوم، فيمكنك تقديم حلولك وتقدير مقدار الوقت والجهد الهندسي المُستغرَق لحلها. إذا كنت مصممًا: ستضطر إلى اكتساب المهارات التقنية والتسويقية اللازمة، ويُحتمَل أن تواجه تغييرًا ملحوظًا في جدولك اليومي وتنوعًا في المهام وإيقاع العمل العام. تشير سولين يو Suelyn Yu مصممة تجربة المستخدم UX التي تحولت إلى مديرة منتجات، إلى أنه كان لديها جدول زمني خاص بالمبدعين عندما كانت مصممة، فمعظم أعمالها كانت غير مجدولة وكانت حرة في التخطيط لمهامها بصورة مستقلة. إذا كنت في وضع مماثل، فاغتنم هذه الفرصة لفهم القرارات الكامنة وراء التغييرات التي يُطلَب منك إنشاؤها. اطرح أسئلة واطلب الوصول إلى ملاحظات العملاء ومقابلات المستخدمين إن لم تكن لديك فعليًا. يحتاج مديرو المنتجات إلى مجموعة واسعة من المهارات المتنوعة لأداء مهامهم بنجاح، حيث يجب أن يكونوا أذكياء في مجال الأعمال، وأن تكون لديهم معرفة تقنية وخلفية في التصميم والتسويق، ولكن لا يدرك الجميع أهمية المهارات الشخصية والذكاء العاطفي لهذا المنصب. مجموعة مهارات مدراء المنتجات. المصدر: Productplan يرتبط الجزء الأكبر من مسؤوليات مدير المنتجات بالتواصل المتمثل في تنسيق فريق التطوير وإجراء مقابلات مع العملاء وإعلام المديرين التنفيذيين والتواصل مع أصحاب المصلحة وغير ذلك، لذا تُعَد مهارات إدارة العلاقات الممتازة ضروريةً لهذا الدور. يجب أن يكون مديرو المنتجات قادرين على إلهام الناس وحل النزاعات التي لا مفر منها والتوازن بين اهتمامات ومطالب جميع أصحاب المصلحة، مما يحفز الجميع ويرضيهم. بعض السمات الشخصية التي تصنع أفضل مديري المنتجات هي: التعاطف: لفهم العملاء وأعضاء الفريق بصورة أفضل. الوعي الذاتي: ليبقوا موضوعيين وتجنب إشراك مصالحهم الخاصة في العمل. الإدارة الذاتية: ليكونوا منضبطين ومنظمين مع قدرتهم على تنظيم الآخرين. تحمل الإجهاد: لإدارة المشاعر والبقاء هادئين تحت الضغط المستمر. يُتوقَّع أن يتمتع مدير المنتج "بالذكاء والقدرة القوية على حل المشاكل"، كما هو مذكور في مقال "كيف توظف مدير منتجات" بقلم كين نورتون Ken Norton، ويؤكد أنه يفضل مدير المنتجات الذكي عديم الخبرة على مدير المنتجات متوسط الذكاء مع سنوات الخبرة الكثيرة؛ كما ذكر المهارات التقنية والحدس القوي والإبداع والمهارات القيادية والقدرة على توجيه وجهات نظر متعددة بوصفها أهم الخصائص. الخاتمة يُطلَق أكثر من 30000 منتج جديد كل عام ويفشل 85% منها وفقًا للإحصاءات. هناك العديد من الأسباب المختلفة لذلك، ولكن أهمها هو أن الكثير من المنتجات ليست مُعدَّة للسوق تمامًا، حيث يؤدي إهمال أحد جوانب تطوير المنتج والتركيز المفرط على الجانب الآخر إلى خسائر مالية، ولكن يمكن تجنب مثل هذه العواقب وزيادة فرص نجاح المنتج في السوق من خلال إدارته السليمة. ترجمة -وبتصرُّف- للمقال Product Management: Main Stages and Product Manager Role. اقرأ أيضًا مدخل مبسط إلى عالم إدارة المنتجات وظيفة مدير المنتج وما عليك فعله لتحصل عليها
-
استدعاءات النظام system calls هي كيفية تفاعل برامج مجال المستخدِم Userspace مع نواة النظام Kernel، إذ سنشرح فيما يلي المبدأ العام لكيفية عمل هذه الاستدعاءات، وسنتعرّف على الصلاحيات في نظام التشغيل للوصول إلى الموارد. أرقام استدعاءات النظام لكل استدعاء نظام رقم يعرفه مجال المستخدِم والنواة، إذ يعرِف كلاهما أنّ رقم استدعاء النظام 10 هو الاستدعاء open() ورقم استدعاء النظام 11 هو الاستدعاء read() على سبيل المثال. تُعَدّ واجهة التطبيق الثنائية Application Binary Interface -أو ABI اختصارًا- مشابهةً جدًا لواجهة برمجة التطبيقات API، ولكنها مُخصَّصة للعتاد بدلًا من أن تكون خاصةً بالبرمجيات، إذ ستحدّد واجهة برمجة التطبيقات API المسجّل Register الذي يجب إدخال رقم استدعاء النظام فيه لتتمكّن النواة من العثور عليه عندما يُطلب منها إجراء استدعاء النظام. الوسائط Arguments لا تكون استدعاءات النظام جيدةً بدون الوسائط، فالاستدعاء open() مثلًا يحتاج إلى إعلام النواة بالضبط بالملف الذي يجب فتحه، وستحدّد واجهة ABI أيًا من وسائط المسجّلات التي يجب وضعها لاستدعاء النظام. المصيدة Trap يجب أن تكون هناك طريقة ما للاتصال بالنواة التي نريد إجراء استدعاء نظام إليها، إذ تعرِّف جميع المعماريات الحاسوبية تعليمةً تسمى عادةً break أو شيئًا آخر مشابه يشير إلى العتاد الذي نريد إجراء استدعاء النظام إليه، وستخبر هذه التعليمة العتاد بتعديل مؤشر التعليمة ليؤشّر إلى معالج استدعاءات النظام الخاص بالنواة، إذ يخبر نظام التشغيل العتاد بمكان وجود معالج استدعاء النظام عندما يضبط نفسه، لذلك يفقد مجال المستخدِم السيطرة على البرنامج وتمريره إلى النواة بمجرد أن يستدعي التعليمة break. يُعَدّ ما تبقى من هذه العملية بسيطًا إلى حد ما، إذ تبحث النواة في المسجل المُعرَّف مسبقًا عن رقم استدعاء النظام وتبحث عنه في جدول لمعرفة الدالة التي يجب أن تستدعيها، وتُستدعَى هذه الدالة وتنفّذ ما يجب تنفيذه، ثم تضع القيمة المُعادة في مسجل آخر تعرّفه الواجهة ABI بوصفه مسجّل إعادة Return. تتمثل الخطوة الأخيرة في أن تنفّذ النواة تعليمات قفز إلى برنامج مجال المستخدِم لتتمكّن من المتابعة من حيث توقفت، ويحصل برنامج مجال المستخدِم على البيانات التي يحتاجها من مسجِّل الإعادة ثم يكمل عمله، كما يمكن أن تصبح تفاصيل هذه العملية خطيرة للغاية، إلّا أنّ هذا كله يتعلق باستدعاء النظام. مكتبة libc يمكنك تنفيذ كل ما سبق يدويًا لكل استدعاء نظام، لكن تنفّذ مكتبات النظام معظم العمل نيابةً عنك عادةً، والمكتبة القياسية التي تتعامل مع استدعاءات النظام على أنظمة يونيكس هي مكتبة libc. تحليل استدعاء النظام بما أنّ مكتبات النظام تجعل الأنظمة تستدعي نيابة عنك، فيجب تطبيق اختراق منخفض المستوى لتوضيح كيفية عمل استدعاءات النظام، وسنوضح كيفية عمل أبسط استدعاء نظام getpid() الذي لا يأخذ أيّ وسيط ويعيد معرّف البرنامج أو العملية التي تكون قيد التشغيل حاليًا. #include <stdio.h> /* خاصة باستدعاء النظام syscall() */ #include <sys/syscall.h> #include <unistd.h> /* أرقام استدعاءات النظام */ #include <asm/unistd.h> void function(void) { int pid; pid = __syscall(__NR_getpid); } نبدأ بكتابة برنامج صغير بلغة C لتوضيح آلية عمل استدعاءات النظام، وأول شيء يجب ملاحظته هو وجود الوسيط syscall الذي توفّره مكتبات النظام لإجراء استدعاءات النظام مباشرةً، إذ يوفِّر هذا الوسيط طريقةً سهلةً للمبرمجين لإجراء استدعاءات النظام مباشرةً دون الحاجة إلى معرفة إجراءات لغة التجميع الدقيقة لإجراء الاستدعاء على عتادهم. نستخدِم الدالة getpid() لأنّ استخدام اسم دالة رمزي في شيفرتك البرمجية أوضح وتعمل الدالة getpid() بطرق مختلفة جدًا على أنظمة مختلفة، إذ يمكن تخزين الاستدعاء getpid() في الذاكرة المخبئية في نظام لينكس مثلًا، لذا إذا جرى تشغيله مرتين، فلن تتحمل مكتبة النظام عقوبة الاضطرار إلى إجراء استدعاء نظام بالكامل للعثور على المعلومات نفسها مرةً أخرى. تُعرَّف أرقام استدعاءات النظام في الملف asm/unistd.h من مصدر النواة في نظام لينكس، وبما أنّ هذا الملف موجود في المجلد الفرعي asm، فسيختلف ذلك لكل معمارية يعمل عليها نظام لينكس، كما تُعطَى أرقام استدعاءات النظام اسمًا #define يتكون من __NR_، وبالتالي يمكنك رؤية أنّ شيفرتك البرمجية ستجري استدعاء النظام getpid ويخزّن القيمة في المعرِّف pid. سنلقي نظرةً على كيفية تطبيق العديد من المعماريات لهذه الشيفرة البرمجية وسنطّلع على الشيفرة البرمجية الحقيقية التي يمكن أن تكون خطيرةً ولكن يجب الالتزام بها، فهذه هي بالضبط الطريقة التي يعمل بها نظامك. معمارية PowerPC يُعَدّ نظام PowerPC معماريةَ RISC شائعة في حواسيب Apple القديمة، وهو جوهر أجهزة أحدث إصدار من Xbox مثلًا، وفيما يلي مثال عن استدعاء نظام PowerPC: /* يتلِف استدعاءُ النظام المسجّلاتِ نفسها لاستدعاء الدالة في نظام powerpc، * باستثناء المسجّل LR الذي يحتاجه التسلسل "sc; bnslr" * والمسجّل CR حيث يُتلَف المسجل CR0.SO فقط الذي يشير إلى * حالة إعادة خطأ. */ #define __syscall_nr(nr, type, name, args...) \ unsigned long __sc_ret, __sc_err; \ { \ register unsigned long __sc_0 __asm__ ("r0"); \ register unsigned long __sc_3 __asm__ ("r3"); \ register unsigned long __sc_4 __asm__ ("r4"); \ register unsigned long __sc_5 __asm__ ("r5"); \ register unsigned long __sc_6 __asm__ ("r6"); \ register unsigned long __sc_7 __asm__ ("r7"); \ \ __sc_loadargs_##nr(name, args); \ __asm__ __volatile__ \ ("sc \n\t" \ "mfcr %0 " \ : "=&r" (__sc_0), \ "=&r" (__sc_3), "=&r" (__sc_4), \ "=&r" (__sc_5), "=&r" (__sc_6), \ "=&r" (__sc_7) \ : __sc_asm_input_##nr \ : "cr0", "ctr", "memory", \ "r8", "r9", "r10","r11", "r12"); \ __sc_ret = __sc_3; \ __sc_err = __sc_0; \ } \ if (__sc_err & 0x10000000) \ { \ errno = __sc_ret; \ __sc_ret = -1; \ } \ return (type) __sc_ret #define __sc_loadargs_0(name, dummy...) \ __sc_0 = __NR_##name #define __sc_loadargs_1(name, arg1) \ __sc_loadargs_0(name); \ __sc_3 = (unsigned long) (arg1) #define __sc_loadargs_2(name, arg1, arg2) \ __sc_loadargs_1(name, arg1); \ __sc_4 = (unsigned long) (arg2) #define __sc_loadargs_3(name, arg1, arg2, arg3) \ __sc_loadargs_2(name, arg1, arg2); \ __sc_5 = (unsigned long) (arg3) #define __sc_loadargs_4(name, arg1, arg2, arg3, arg4) \ __sc_loadargs_3(name, arg1, arg2, arg3); \ __sc_6 = (unsigned long) (arg4) #define __sc_loadargs_5(name, arg1, arg2, arg3, arg4, arg5) \ __sc_loadargs_4(name, arg1, arg2, arg3, arg4); \ __sc_7 = (unsigned long) (arg5) #define __sc_asm_input_0 "0" (__sc_0) #define __sc_asm_input_1 __sc_asm_input_0, "1" (__sc_3) #define __sc_asm_input_2 __sc_asm_input_1, "2" (__sc_4) #define __sc_asm_input_3 __sc_asm_input_2, "3" (__sc_5) #define __sc_asm_input_4 __sc_asm_input_3, "4" (__sc_6) #define __sc_asm_input_5 __sc_asm_input_4, "5" (__sc_7) #define _syscall0(type,name) \ type name(void) \ { \ __syscall_nr(0, type, name); \ } #define _syscall1(type,name,type1,arg1) \ type name(type1 arg1) \ { \ __syscall_nr(1, type, name, arg1); \ } #define _syscall2(type,name,type1,arg1,type2,arg2) \ type name(type1 arg1, type2 arg2) \ { \ __syscall_nr(2, type, name, arg1, arg2); \ } #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1, type2 arg2, type3 arg3) \ { \ __syscall_nr(3, type, name, arg1, arg2, arg3); \ } #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \ type name(type1 arg1, type2 arg2, type3 arg3, type4 arg4) \ { \ __syscall_nr(4, type, name, arg1, arg2, arg3, arg4); \ } #define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5) \ type name(type1 arg1, type2 arg2, type3 arg3, type4 arg4, type5 arg5) \ { \ __syscall_nr(5, type, name, arg1, arg2, arg3, arg4, arg5); \ } يوضِّح جزء الشيفرة البرمجية السابق من ملف ترويسة النواة asm/unistd.h كيف يمكننا تطبيق استدعاءات النظام على نظام PowerPC، ويمكن أن يبدو الأمر معقدًا للغاية، ولكن لنشرحه خطوةً خطوة. انتقل أولًا إلى نهاية المثال إلى تعريف وحدات الماكرو _syscallN، إذ يمكنك رؤية أنّ هناك العديد من وحدات الماكرو ويأخذ كل منها وسيطًا آخر تدريجيًا، وسنركّز على أبسط إصدار وهو _syscall0 للبدء به والذي لا يتطلب سوى وسيطَين هما نوع القيمة المُعادة لاستدعاء النظام مثل int أو char واسم استدعاء النظام، إذ يكون مع الاستدعاء getpid بالصورة _syscall0(int,getpid). سنبدأ الآن بتفكيك الماكرو __syscall_nr الذي لا يختلف عما كان عليه سابقًا، إذ سنأخذ عدد الوسائط على أنه المعامِل الأول ثم النوع والاسم والوسائط الفعلية، فالخطوة الأولى هي التصريح عن بعض الأسماء للمسجّلات، إذ يشير الاسم __sc_0 إلى المسجّل r0 أي المسجّل 0، ويستخدِم المصرِّف Compiler المسجلات بالطريقة التي يريدها، لذلك يجب أن نعطيه قيودًا حتى لا يقرّر استخدام المسجّل الذي نحتاجه بطريقة مخصصة. سنستدعي بعد ذلك sc_loadargs إلى جانب المعامِل ## الذي يُعَدّ أمر لصق يُستبدَل بالمتغير nr، وسنوسّعه إلى __sc_loadargs_0(name, args);، ويمكننا رؤية __sc_loadargs الذي يضبط __sc_0 ليكون رقم استدعاء النظام، ولاحظ معامِل اللصق مرةً أخرى مع البادئة __NR_ واسم المتغير الذي يشير إلى مسجّل معيّن، لذا تُستخدَم هذه الشيفرة البرمجية السابقة ذات المظهر الصعب لوضع رقم استدعاء النظام في المسجّل 0، كما يمكنك باتباع الشيفرة البرمجية السابقة رؤية أن وحدات الماكرو الأخرى ستضع وسائط استدعاء النظام في المسجّل r3 عبر المسجّل r7، ويمكنك فقط الحصول على 5 وسائط على أساس حد أقصى لاستدعاء النظام. سنعالج الآن القسم __asm__، إذ لدينا هنا ما يسمّى بالتجميع المُضمَّن Inline Assembly لأنها شيفرة تجميع مختلطة مع الشيفرة المصدرية، وهذه الصيغة معقدة بعض الشيء، لذلك سنشير إلى الأجزاء المهمة منها فقط، كما عليك تجاهل البت __volatile__ حاليًا والذي يخبر المصرّف أنّ هذه الشيفرة البرمجية لا يمكن التنبؤ بها، لذا لا تحاول التعامل معها بذكاء، كما أنّ كل الأشياء الموجودة بعد النقطتين هي طريقة للتواصل مع المصرّف حول ما يفعله التجميع المضمَّن لمسجلات وحدة المعالجة المركزية، ويجب أن يعرِف المصرّف ذلك حتى لا يحاول استخدام أيّ من هذه المسجلات بطرق يمكنها التسبب في حدوث عطل. لكن الجزء المهم هو وجود تعليمتَي التجميع في الوسيط الأول، إذ ينفِّذ الاستدعاء sc كل العمل، وهذا كل ما عليك تطبيقه لإجراء استدعاء نظام، وبالتالي يحدث ما يلي عند إجراء استدعاء النظام، إذ يعرف المعالِج المُقاطَع أنه يجب عليه نقل التحكم إلى جزء معيّن من إعداد الشيفرة البرمجية في وقت بدء تشغيل النظام لمعالجة المقاطعات، كما توجد هناك العديد من المقاطعات، وتُعَدّ استدعاءات النظام إحداها، وتبحث هذه الشيفرة البرمجية بعد ذلك في المسجل 0 للعثور على رقم استدعاء النظام، ثم تبحث في جدول لإيجاد الدالة الصحيحة للانتقال إليها لمعالجة استدعاء النظام، وتتلقى هذه الدالة وسائطها من المسجل 3 إلى المسجل 7. يعود التحكم إلى التعليمة التالية بعد sc وهي في هذه الحالة تعليمات سور الذاكرة Memory Fence بمجرد تشغيل معالج استدعاء النظام واكتماله، كما تتأكد تعليمات سور الذاكرة من أن كل شيء ملتزم بالذاكرة، حيث تضمن هذه التعليمة أنّ كل ما نعتقد أنه مكتوب في الذاكرة قد حدث فعليًا دون المرور عبر خط أنابيب Pipeline في مكان ما. انتهينا تقريبًا، ولكن الشيء الوحيد المتبقي هو إعادة القيمة من استدعاء النظام، إذ نرى ضبط القيمة __sc_ret من المسجل r3 وضبط القيمة __sc_err من المسجل r0، فالقيمة الأولى هي القيمة المُعادة، والأخرى هي قيمة الخطأ، إذ يمكن أن تفشل استدعاءات النظام مثل أيّ دالة أخرى، لكن تكمن المشكلة في أنّ استدعاء النظام يمكن أن يعيد أيّ قيمة ممكنة، إذ لا يمكننا أن نقول أن القيمة السالبة تشير إلى الفشل، لأنها يمكن أن تكون مقبولةً لبعض استدعاءات النظام، لذا تضمن دالة استدعاء النظام أنّ نتيجتها في المسجل r3 وأنّ أيّ رمز خطأ موجود في المسجل r0 قبل إعادة النتيجة. يجب التحقق من رمز الخطأ للتأكد من ضبط البِتّ العلوي الذي من شأنه الإشارة إلى عدد سالب، فإذا كان الأمر كذلك، فسنضبط قيمة المتغير errno العام على هذه القيمة وهي المتغير القياسي للحصول على معلومات الخطأ عند فشل الاستدعاء، كما سنضبط القيمة المُعادة على -1، وسنعيد النتيجة مباشرةً في حالة تلقّي نتيجة صالحة، لذا يجب على دالة الاستدعاء التحقق من أنّ القيمة المعادة ليست -1، فإذا كان الأمر كذلك، فيمكنها التحقق من المتغير errno للعثور على سبب فشل الاستدعاء، وهذا هو استدعاء نظام كامل على نظام PowerPC. استدعاءات نظام x86 إليك الواجهة المطبَّقة لمعالج x86: /* توجد أرقام الأخطاء المرئية للمستخدِم ضمن المجال من -1 إلى -124: راجع <asm-i386/errno.h> */ #define __syscall_return(type, res) \ do { \ if ((unsigned long)(res) >= (unsigned long)(-125)) { \ errno = -(res); \ res = -1; \ } \ return (type) (res); \ } while (0) /* _foo يجب أن تكون __foo، بينما __NR_bar يمكن أن تكون _NR_bar */ #define _syscall0(type,name) \ type name(void) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name)); \ __syscall_return(type,__res); } #define _syscall1(type,name,type1,arg1) \ type name(type1 arg1) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1))); \ __syscall_return(type,__res); } #define _syscall2(type,name,type1,arg1,type2,arg2) \ type name(type1 arg1,type2 arg2) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2))); \ __syscall_return(type,__res); } #define _syscall3(type,name,type1,arg1,type2,arg2,type3,arg3) \ type name(type1 arg1,type2 arg2,type3 arg3) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3))); \ __syscall_return(type,__res); \ } #define _syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4) \ type name (type1 arg1, type2 arg2, type3 arg3, type4 arg4) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3)),"S" ((long)(arg4))); \ __syscall_return(type,__res); \ } #define _syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \ type5,arg5) \ type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5))); \ __syscall_return(type,__res); \ } #define _syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4, \ type5,arg5,type6,arg6) \ type name (type1 arg1,type2 arg2,type3 arg3,type4 arg4,type5 arg5,type6 arg6) \ { \ long __res; \ __asm__ volatile ("push %%ebp ; movl %%eax,%%ebp ; movl %1,%%eax ; int $0x80 ; pop %%ebp" \ : "=a" (__res) \ : "i" (__NR_##name),"b" ((long)(arg1)),"c" ((long)(arg2)), \ "d" ((long)(arg3)),"S" ((long)(arg4)),"D" ((long)(arg5)), \ "0" ((long)(arg6))); \ __syscall_return(type,__res); \ } تختلف معمارية x86 كثيرًا عن PowerPC التي تحدّثنا عنها سابقًا، إذ يُصنَّف x86 على أنه معالِج من النوع CISC على عكس PowerPC الذي يُعَدذ من النوع RISC، ولديه مسجلات أقل بكثير. اطّلع على أبسط ماكرو _syscall0 الذي يستدعي تعليمة من النوع int والقيمة 0x80، إذ تعمل هذه التعليمة على جعل وحدة المعالجة المركزية ترفع المقاطعة 0x80 التي ستنتقل إلى الشيفرة البرمجية التي تعالج استدعاءات النظام في النواة. لنفحص الآن كيفية تمرير الوسائط باستخدام وحدات الماكرو الأطول، ولاحظ كيف أنّ نظام PowerPC قد طبّق تتال وانسياب cascade من وحدات الماكرو من خلال إضافة وسيط واحد في كل مرة، كما يحتوي هذا التطبيق على شيفرة برمجية منسوخة ولكن اتباعه أسهل قليلًا. تستند أسماء المسجّلات في معمارية x86 إلى الأحرف عوضًا عن أسماء المسجّلات الرقمية في PowerPC، إذ يمكننا رؤية من الماكرو عديم الوسائط أنّ المسجّل A يُحمَّل فقط، وبالتالي يمكننا القول أنّ رقم استدعاء النظام متوقَّع وجوده في المسجّل EAX، كما يمكنك رؤية أسماء المسجلات المختصرة في وسائط استدعاء __asm__ عندما نبدأ بتحميل المسجلات في وحدات الماكرو الأخرى. لاحظ الماكرو __syscall6 الذي يأخذ 6 وسائط، إذ تعمل التعليمتان push و pop مع المكدس في x86، بحيث تدفع إحداهما قيمةً إلى أعلى المكدس في الذاكرة وتسحب الأخرى القيمة من المكدس في الذاكرة، كما يجب تخزين قيمة المسجل ebp في الذاكرة ووضع الوسيط في التعليمة mov وإجراء استدعاء للنظام، ثم إعادة القيمة الأصلية إلى المسجل ebp في حالة وجود ستة مسجلات، كما يمكنك هنا رؤية عيوب عدم وجود مسجلات كافية، إذ يُعَدّ التخزين في الذاكرة باهظ الثمن، لذا كلما تمكنت من تجنّبها، كان ذلك أفضل. لاحظ عدم وجود تعليمات سور الذاكرة التي رأيناها سابقًا مع PowerPC لأنّ معمارية x86 تضمن أن يكون تأثير جميع التعليمات مرئيًا عند اكتمالها، مما يسهّل البرمجة على المصرّف والمبرمج ولكنه يقلل من المرونة. يوجد أيضًا اختلاف في القيمة المُعادة، فقد كان لدينا مسجّلَين مع قيم مُعادة من النواة في معمارية PowerPC، إحداهما هي القيمة والأخرى هي رمز الخطأ، في حين لدينا قيمة مُعادة واحدة في معمارية x86 تُمرَّر إلى الماكرو __syscall_return الذي يغيّر نوع القيمة المُعادة إلى النوع unsigned long ويوازنها مع مجال من القيم السالبة تعتمد على المعمارية والنواة، حيث تمثّل هذه القيم رموز الخطأ. لاحظ أنّ قيمة رمز الخطأ errno موجبة، مما يؤدي إلى إلغاء النتيجة السالبة من النواة، لكن يعني هذا أنّ استدعاءات النظام لا يمكنها إعادة قيم سالبة صغيرة، إذ لا يمكن تمييزها عن رموز الخطأ، كما تضيف بعض استدعاءات النظام التي لديها هذا المتطلب مثل الاستدعاء getpriority() إزاحةً إلى القيمة المُعادة لإجبارها بأن تكون دائمًا موجبة، فالأمر متروك لمجال المستخدِم لإدراك ذلك وطرح هذه القيمة الثابتة للحصول على القيمة الحقيقية. الصلاحيات يُعَدّ تطبيق الأمان أحد مهام نظام التشغيل الرئيسية بهدف عدم السماح لتطبيق أو مستخدِم بالتضارب مع أيّ تطبيق آخر يعمل في النظام، وهذا يعني أن التطبيقات يجب ألّا تكون قادرةً على الكتابة في ذاكرة أو ملفات التطبيقات الأخرى، ويجب أن تصل فقط إلى الموارد وفق سياسة النظام. لكن يكون لأحد التطبيقات عند تشغليها استخدام حصري للمعالج، إذ سنرى كيف يعمل ذلك عندما نتعرّف على العمليات في المقال التالي، ويمكن التأكد من وصول التطبيق إلى الذاكرة التي يمتلكها فقط باستخدام نظام الذاكرة الوهمية Virtual Memory، إذ يُعَدّ العتاد مسؤولًا عن تطبيق هذه القواعد. تُعَدّ واجهة استدعاء النظام بوابة التطبيق للوصول إلى موارد النظام، إذ يمكن للنواة فرض قواعد حول نوع الوصول الذي يمكن توفيره من خلال إجبار التطبيق على طلب الموارد من خلال استخدام استدعاء نظام إلى النواة، فإذا أجرى أحد التطبيقات استدعاء النظام open() لفتح ملف على القرص الصلب مثلًا، فسيتحقق من أذونات المستخدِم المقابلة لأذونات الملف ثم سيسمح بوصوله أو يرفضه. مستويات الصلاحيات تُعَدّ حماية العتاد مجموعةً من الحلقات متحدة المركز حول مجموعة أساسية من العمليات. مستويات الصلاحيات في معمارية x86 توجد التعليمات ذات الحماية الأكبر في الحلقة الداخلية، وهي التعليمات التي يجب السماح للنواة فقط باستدعائها مثل التعليمة HLT المُستخدَمة لإيقاف المعالج، إذ يجب ألّا يُسمَح بأن يشغّلها تطبيق مستخدِم، لأن ذلك سيوقف الحاسوب بأكمله عن العمل، لكن يجب أن تكون النواة قادرةً على استدعاء هذه التعليمة عند إيقاف تشغيل الحاسوب بطريقة نظامية، إذ يرفع العتاد استثناءً عندما يستدعي تطبيق ما هذه التعليمة، ويتضمّن هذا الاستثناء القفز إلى معالج محدد في نظام التشغيل مشابه لمعالج استدعاء النظام، كما يُحتمَل أن ينهي نظام التشغيل بعد ذلك البرنامج ويعطي المستخدِم بعض الأخطاء حول كيفية تعطل التطبيق. يمكن لكل حلقة داخلية الوصول إلى أيّ تعليمات تحميها حلقة خارجية، ولكن لا يمكنها الوصول إلى تعليمة تحميها حلقة داخلية، كما لا تحتوي جميع المعماريات على مستويات متعددة من الحلقات كما في الشكل السابق، ولكن سيوفر معظمها على الأقل مستوى النواة Kernel ومستوى المستخدِم User. نموذج الحماية 386 يحتوي نموذج الحماية 386 على أربع حلقات بالرغم من أن معظم أنظمة التشغيل مثل لينكس وويندوز تستخدِم حلقتَين فقط للحفاظ على التوافق مع المعماريات الأخرى التي تسمح الآن بأكبر عدد من مستويات الحماية المنفصلة، كما يحتفظ النموذج 386 بالصلاحيات من خلال أن يكون لكل جزء من شيفرة التطبيق البرمجية المُشغَّلة في النظام واصف صغير يسمى واصف الشيفرة البرمجية Code Descriptor الذي يصِف مستوى صلاحياتها. تقفز شيفرة التطبيق عند تشغليها سريعًا إلى الشيفرة البرمجية الموجودة خارج المنطقة التي يصِفها واصفُ شيفرة التطبيق مع التحقق من مستوى صلاحيات الهدف، فإذا كانت الصلاحيات أعلى من صلاحيات الشيفرة المُشغَّلة حاليًا، فلن يسمح العتاد بهذه القفزة وسيتعطل التطبيق. رفع مستوى الصلاحيات يمكن أن ترفع التطبيقات مستوى صلاحياتها فقط من خلال استدعاءات محددة تسمح بذلك مثل التعليمات الخاصة بتنفيذ استدعاء النظام، إذ يشار إليها عادةً باسم بوابة الاستدعاءات Call Gate لأنها تعمل مثل بوابة حقيقية تسمح بمدخل صغير عبر جدار غير قابل للاختراق. رأينا كيف يوقِف العتاد التطبيق الذي يكون قيد التشغيل ويسلّم التحكم إلى النواة عند استدعاء هذه التعليمات، ويجب أن تعمل النواة بوصفها حارسًا للبوابة للتأكد من عدم دخول أيّ شيء غير مرغوب به من البوابة، إذ يجب التحقق من وسائط استدعاء النظام بعناية للتأكد من أنه لن ينخدع بفعل شيء لا ينبغي أن يفعله، وبالتالي حدوث خطأ أمني. تعمل النواة في الحلقة الداخلية، لذا فهي تمتلك الأذونات اللازمة لإجراء أيّ عملية تريدها، ثم ستعيد التحكم في النهاية إلى التطبيق الذي سيعمل مرةً أخرى بمستوى صلاحيات أقل. استدعاءات النظام السريعة تتمثل إحدى مشاكل المصائد كما هو موضح سابقًا في أنها باهظة الثمن بالنسبة للمعالج لتطبيقها، فهناك الكثير من الحالات التي يجب حفظها قبل تبديل السياق، وقد أدركت المعالجات الحديثة هذا الحِمل وتسعى جاهدة لتقليله. يتطلب فهم آلية بوابة الاستدعاءات الموضحة سابقًا التدقيق في مخطط التقطيع المبتكر والمعقد الذي يستخدمه المعالج، وقد كان السبب الأصلي لتطبيق التقطيع هو القدرة على استخدام أكثر من 16 بِتًا متوفرًا في المسجل لعنوان ما كما هو موضح في الشكل التالي: تقطيع العنونة Segmentation Addressing في معمارية x86: يؤدي التقطيع إلى توسيع مساحة عناوين المعالج من خلال تقسيمه إلى أجزاء. يحتفظ المعالج بمسجلات مقاطع خاصة، ويمكن تحديد العناوين من خلال مسجّل المقطع والإزاحة. تُضاف قيمة مسجل المقطع إلى جزء الإزاحة للعثور على العنوان النهائي. بقي مخطط التقطيع كما هو ولكن بتنسيق مختلف عندما انتقلت معمارية x86 إلى مسجّلات بحجم 32 بِتًا، إذ يُسمَح للمقاطع بأن تكون بأيّ حجم بدلًا من استخدام أحجام ثابتة، ويجب أن يتعقّب المعالج كل هذه المقاطع المختلفة وأحجامها، وهو ما يفعله باستخدام الواصفات Descriptors. تكون واصفات المقاطع المتاحة للجميع محفوظةً في جدول الواصفات العام Global Descriptor Table أو GDT اختصارًا، كما تحتوي كل عملية على عدد من المسجلات التي توشّر إلى مدخلات في جدول GDT، وهذه المدخلات هي المقاطع التي يمكن للعملية الوصول إليها، كما توجد جداول واصفات محلية، وتتفاعل جميعها مع مقاطع حالة المهمات، لكنها ليست مهمةً حاليًا. مقاطع x86: لاحظ كيف يمر الاستدعاء البعيد عبر بوابة الاستدعاءات التي توجّهه إلى مقطع من الشيفرة يعمل على مستوى الحلقة الأدنى. الطريقة الوحيدة لتعديل محدّد مقطع الشيفرة -المستخدَم ضمنيًا لجميع عناوين الشيفرة- هي استخدام آلية الاستدعاء، حيث تضمن آلية بوابة الاستدعاءات اختيار واصف مقطع جديد، مما يؤدي إلى تغيير مستويات الحماية التي يجب عليك الانتقال إليها عبر نقطة دخول معروفة. يضبط نظام التشغيل مسجلات المقطع بوصفها جزءًا من حالة العملية، لذا يعرِف عتاد المعالِج مقاطع الذاكرة التي يمكن للعملية المُشغَّلة الوصول إليها، كما يمكنه فرض الحماية لضمان عدم وصول العملية لأيّ شيء لا يُفترَض أن تصل إليه، فإذا خرجت العملية خارج حدودها المفروضة، فستتلقّى خطأ تقطيع Segmentation Fault يعرفه معظم المبرمجين. إذا احتاج تشغيل الشيفرة البرمجية إلى إجراء استدعاءات إلى شيفرة موجودة في مقطع آخر، فستطبّق معمارية x86 ذلك كما في الحلقات Rings، إذ تكون الحلقة 0 هي الحلقة ذات الإذن الأعلى والحلقة 3 هي الأدنى، ويمكن للحلقات الداخلية الوصول إلى الحلقات الخارجية ولكن ليس العكس. إذا أرادت شيفرة الحلقة 3 القفز إلى شيفرة الحلقة 0، فستعدّل محدّد مقطع الشيفرة الخاص بها ليؤشّر إلى مقطع مختلف، لذلك يجب أن تستخدِم تعليمة استدعاءات بعيدة خاصة بحيث يتأكد العتاد من مرورها عبر بوابة الاستدعاءات، ولا توجد طريقة أخرى للعملية المُشغَّلة لاختيار واصف مقطع شيفرة جديد، وسيبدأ المعالج بعد ذلك في تنفيذ الشيفرة البرمجية عند الإزاحة المعروفة في مقطع الحلقة 0، وهذا هو سبب الحفاظ على السلامة مثل عدم قراءة الشيفرة البرمجية العشوائية والضارة وتنفيذها، كما سيبحث المهاجمون دائمًا عن طرق لجعل شيفرتك البرمجية تفعل شيئًا لا تريده. يسمح ذلك بتسلسل هرمي كامل للمقاطع والأذونات، ولاحظ أنّ استدعاء المقطع العرضي يشبه استدعاء النظام، فإذا سبق لك أن شاهدت لغة تجميع لينكس x86، فالطريقة القياسية لإجراء استدعاء النظام هي باستخدام int 0x80 التي ترفع المقاطعة 0x80، إذ توقِف المقاطعة المعالج وتنتقل إلى بوابة المقاطعات التي تعمل بعد ذلك بطريقة بوابة الاستدعاءات نفسها بحيث تغيّر مستوى الصلاحيات وتعيدك إلى منطقة أخرى من الشيفرة البرمجية. مشكلة هذا المخطط أنه بطيء، إذ يتطلب الأمر الكثير من الجهد لتطبيق كل هذا الفحص، ويجب حفظ العديد من المسجلات للوصول إلى الشيفرة الجديدة، كما يجب استعادة كل شيء مرةً أخرى في طريق العودة. لا يُستخدَم نظام الحلقات ذو المستويات الأربعة في تقطيع نظام x86 الحديث بفضل الذاكرة الوهمية، والشيء الوحيد الذي يحدث فعليًا مع تبديل التقطيع هو استدعاءات النظام التي تتحوّل من الوضع 3 -أي مجال المستخدِم- إلى الوضع 0 وتقفز إلى شيفرة معالج استدعاء النظام في النواة. يوفّر المعالِج تعليمات استدعاء نظام فائقة السرعة تسمى sysenter (و sysexit للعودة)، إذ تسرّع هذه التعليمات العملية برمتها عبر الاستدعاء int 0x80 من خلال إزالة الطبيعة العامة للاستدعاء البعيد، أي إمكانية الانتقال إلى أيّ مقطع في أيّ مستوى حلقة، وتقييد الاستدعاء للانتقال فقط إلى شيفرة الحلقة 0 في مقطع معيّن مع الإزاحة كما هي مخزّنة في المسجلات. بما أننا استبدلنا هذه الطبيعة العامة بالكثير من المعلومات المعروفة مسبقًا، فيمكن تسريع العملية، وبالتالي سنحصل على استدعاء النظام السريع الذي ذكرناه سابقًا، والشيء الآخر الذي يجب ملاحظته هو أنّ الحالة لا تُحفَظ عندما ينتقل التحكم إلى النواة، إذ يجب أن تكون النواة حريصةً على عدم تدمير الحالة، ولكن يعني هذا أنها حرة في حفظ الحالة الصغيرة كما هو مطلوب لتنفيذ المهمة، لذلك يمكن أن تكون أكثر فاعليةً، إذ تتعلق هذه الفكرة بمعمارية RISC، وتوضّح كيفية تلاشي الخط بين معالجات RISC و CISC. هناك طرق أخرى للتواصل مع النواة مثل ioctl وأنظمة الملفات مثل proc و sysfs و debugfs وغير ذلك. ترجمة -وبتصرُّف- للقسمين System Calls و Privileges من الفصل The Operating System من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: العمليات وعناصرها في نظام تشغيل الحاسوب المقال السابق: دور نظام التشغيل وتنظيمه في معمارية الحاسوب العمليات (Processes) في أنظمة التشغيل أنظمة التشغيل للمبرمجين
-
يدعم نظام التشغيل العملية الكاملة للحواسيب الحديثة، فهو عنصر أساسي في معمارية الحواسيب، لذا سنتعرّف في هذا المقال على دوره وكيفية تنظيمه. تجريد العتاد تتمثل العملية الأساسية لنظام التشغيل Operating System -أو OS اختصارًا- في تجريد Abstraction العتاد للمبرمج والمستخدِم، إذ يوفّر نظام التشغيل واجهات عامة للخدمات التي يقدمها العتاد الأساسي، كما يجب على المبرمجين معرفة تفاصيل العتاد الأساسي الأكثر خصوصيةً لتشغيل أيّ شيء في عالم خال من أنظمة التشغيل، إذ لن تعمل برامجهم على عتاد آخر حتى عند وجود اختلافات طفيفة في هذا العتاد. تعدد المهام Multitasking نتوقع من الحواسيب الحديثة تنفيذ العديد من الأشياء المختلفة في وقت واحد، لذا يجب التحكيم بين جميع البرامج المختلفة التي تعمل على النظام، ويُعَدً ذلك وظيفة أنظمة التشغيل التي تسمح بحدوث ذلك بسلاسة، فنظام التشغيل مسؤول عن إدارة الموارد داخل النظام، إذ تتنافس المهام المتعددة على موارده أثناء تشغيله بما في ذلك وقت المعالج والذاكرة والقرص الصلب ودخل المستخدِم، كما تتمثل وظيفته في التحكيم في وصول المهام المتعددة لهذه الموارد بطريقة منظمة. لا بد أنك مررت بفشل حاسوبك وتعطله مثل ظهور شاشة الموت الزرقاء Blue Screen of Death الشهيرة بسبب التنافس على هذه الموارد. الواجهات الموحدة Standardised Interfaces يرغب المبرمجون في كتابة برامج تعمل على أكبر عدد ممكن من المنصات العتادية، ويمكن ذلك من خلال دعم نظام التشغيل للواجهات الموحَّدة المعيارية، فإذا كانت دالة فتح ملف مثلًا على أحد الأنظمة open() وكانت open_file() و openf() على نظام آخر، فسيواجه المبرمجون مشكلةً مزدوجةً تتمثل في الاضطرار إلى تذكّر ما يفعله كل نظام مع عدم عمل البرامج على أنظمة متعددة. تُعَدّ واجهة نظام التشغيل المتنقلة Portable Operating System Interface -أو POSIX اختصارًا- معيارًا مهمًا للغاية تطبّقه أنظمة تشغيل من نوع يونيكس UNIX، كما يملك نظام مايكروسوفت ويندوز معايير مشابهة، ويأتي حرف X في POSIX من نظام يونيكس Unix الذي نشأ منه المعيار، وهو اليوم الإصدار رقم 3 من مواصفات يونيكس الواحدة Single UNIX Specification Version 3 أو ISO/IEC 9945:2002 نفسه، كما أنه معيار مجاني ومتاح على الإنترنت. كانت مواصفات يونيكس الواحدة ومعايير POSIX كيانات منفصلةً سابقًا، وقد أصدر اتحاد يسمّى المجموعة المفتوحة Open Group مواصفات يونيكس الواحدة، وكان متاحًا مجانًا وفقًا لمتطلبات هذا الاتحاد، وأحدث إصدار هو الإصدار الثالث من مواصفات يونيكس الواحدة، كما أُصدِرت معايير IEEE POSIX بوصفها معايير بالشكل [رقم المراجعة، رقم الإصدار].IEEE Std 1003، ولم تكن متاحةً مجانًا، وأحدث إصدار منها هو IEEE 1003.1-2001 وهو مكافئ للإصدار الثالث من مواصفات يونيكس الواحدة. دُمِج هذان المعياران المنفصلان فيما يُعرف باسم الإصدار الثالث من مواصفات يونيكس الواحدة، ووحّدته منظمة ISO بالاسم ISO/IEC 9945:2002 في بداية عام 2002، لذا عندما يتحدث الناس عن معيار POSIX أو SUS3 أو ISO/IEC 9945:2002، فإنهم يعنون الشيء نفسه. الأمن يُعَدّ الأمن مهمًا جدًا في الأنظمة متعددة المستخدِمين، إذ يكون نظام التشغيل -بصفته المتحكّم في الوصول إلى النظام- مسؤولًا عن ضمان أنّ الأشخاص الذين لديهم الأذونات الصحيحة فقط يمكنهم الوصول إلى الموارد، فإذا امتلك مستخدِم أحد الملفات مثلًا، فلا ينبغي السماح لمستخدِم آخر بفتحه وقراءته، ولكن هناك حاجة لوجود آليات لمشاركة هذا الملف بأمان بين المستخدِمين إذا أرادوا ذلك. أنظمة التشغيل هي برامج كبيرة ومعقدة تحتوي على مشكلات أمنية في أغلب الأحيان، إذ يستفيد الفيروس أو الدودة الفيروسية من هذه الأخطاء غالبًا للوصول إلى الموارد التي لا ينبغي السماح لها بالوصول إليها مثل الملفات أو اتصال الشبكة، لذا يجب عليك تثبيت حزم التصحيح أو التحديثات التي يوفرها مصنّع نظام التشغيل لمحاربتها. الأداء يوفِّر نظام التشغيل العديد من الخدمات للحاسوب، لذلك يُعَدّ أداؤه أمرًا بالغ الأهمية، إذ تعمل أجزاء كثيرة من نظام التشغيل بصورة متكررة، كما يمكن أن تؤدي زيادة عدد دورات المعالج إلى انخفاض كبير في أداء النظام، ويحتاج نظام التشغيل لاستغلال ميزات العتاد الأساسي للتأكد من الحصول على أفضل أداء ممكن لإجراء العمليات، وبالتالي يجب على مبرمجي الأنظمة فهم التفاصيل الدقيقة للمعمارية التي يبنون نظام التشغيل من أجلها. تكون مهمة مبرمجي الأنظمة في كثير من الحالات هي تحديد سياسات النظام، إذ تؤدي الآثار الجانبية لجعل جزء من نظام التشغيل يعمل بصورة أسرع إلى جعل جزء آخر يعمل بصورة أبطأ أو أقل كفاءة، لذا يجب على مبرمجي الأنظمة فهم كل هذه المقايضات عند بناء نظام التشغيل. تنظيم نظام التشغيل يُعَدّ نظام التشغيل منظمًا تقريبًا كما في الصورة التالية: تنظيم النواة Kernel: تُشغَّل عمليات النواة مباشرةً في مجال المستخدِم Userspace، وتتواصل النواة مباشرةً مع العتاد Hardware وعبر المشغّلات Drivers. النواة Kernel تُعَدّ النواة نظام تشغيل، وتجرّد المشغّلات Drivers العتاد للنواة كما تجرّد النواة العتاد لبرامج المستخدِم، حيث يوجد العديد من أنواع بطاقات الرسوم المختلفة على سبيل المثال، ولكل منها ميزات مختلفة قليلًا عن بعضها البعض، ولكن طالما أن النواة تصدّر واجهة برمجة تطبيقات API، فيمكن للأشخاص الذين لديهم إذن الوصول إلى مواصفات العتاد كتابةُ برامج للمشغّلات لتطبيق هذه الواجهة، ويمكن للنواة باستخدام هذه الطريقة الوصول إلى أنواع مختلفة من العتاد. تُوصَف النواة بأن لها صلاحيات Privileged، فللعتاد أدوار مهمة يؤديها لتشغيل مهام متعددة والحفاظ على أمان النظام، ولكن لا تُطبَّق هذه القواعد على النواة، كما يجب أن تتعامل النواة مع البرامج التي تتعطل، فوظيفة أنظمة التشغيل هي فقط تنظيم العمل والتحكيم بين العديد من البرامج التي تعمل على النظام نفسه، وليس هناك ما يضمن أنها ستتصرف لحل المشاكل، ولكن سيصبح النظام بأكمله عديم الفائدة في حالة تعطل أي جزء داخلي من نظام التشغيل، كما يمكن أن تستغل عمليات المستخدِم مشاكل الأمان لترفع مستواها إلى مستوى صلاحيات النواة، وبالتالي يمكنها الوصول إلى أيّ جزء من النظام. النواة الأحادية Monolithic والنواة الدقيقة Microkernel أحد الأمور الجدلية التي تُطرَح غالبًا حول أنظمة التشغيل هو ما إذا كانت النواة أحادية Monolithic أو نواة دقيقة Microkernel. تُعَدّ النواة الأحادية الأكثر شيوعًا كما هو الحال في معظم أنظمة يونيكس الشائعة مثل لينكس، إذ تكون النواة في هذا النموذج ذات صلاحيات كبيرة، وتحتوي على مشغّلات العتاد ومتحكمات الوصول إلى نظام الملفات وفحص الأذونات والخدمات مثل نظام ملفات الشبكة Network File System -أو NFS اختصارًا. تتمتع النواة دائمًا بصلاحيات، لذلك إذا تعطل أيّ جزء منها، فيُحتمَل أن يتوقف النظام بأكمله، وإذا كان لدى مشغّل خطأ برمجي ما bug، فيمكنه الكتابة في أيّ ذاكرة في النظام دون أيّ مشاكل، مما يؤدي في النهاية إلى تعطل النظام. تحاول معمارية النواة الدقيقة تقليل هذا الاحتمال من خلال جعل الجزء الذي يمتلك الصلاحيات من النواة صغيرًا قدر الإمكان. هذا يعني أن معظم النظام يعمل كبرامج دون صلاحيات، مما يحد من الضرر الذي يمكن أن يسبّبه أيّ مكونٍ معطَّل، فمثلًا يمكن تشغيل مشغّلات العتاد في عمليات منفصلة، وبالتالي إذا تعطّل أحد هذه المشغّلات، فلن يتمكّن من الكتابة في أيّ ذاكرة غير تلك المخصصة له. تبدو معمارية النواة الدقيقة جيدةً، ولكنها ستؤدي إلى المشكلتين التاليتين: انخفاض الأداء، إذ يمكن أن يؤدي التواصل بين العديد من المكونات المختلفة إلى تقليل الأداء. يُعَدّ تطبيقها أصعب قليلًا على المبرمجين. تأتي هذه المشاكل بسبب تطبيق معظم الأنوية الدقيقة باستخدام نظام قائم على تمرير الرسائل Message Passing بهدف الحفاظ على الفصل بين المكونات، ويشار إلى هذا النظام عادةً باسم التواصل بين العمليات Inter-process Communication أو IPC اختصارًا. يحدث التواصل بين المكونات باستخدام رسائل منفصلة يجب تجميعها ضمن حزم وإرسالها إلى المكوِّن الآخر وتفكيكها وتشغيلها وإعادة تجميعها وإعادة إرسالها ثم تفكيكها مرةً أخرى للحصول على النتيجة، وهذه خطوات كثيرة لطلبٍ بسيط إلى حد ما من مكون خارجي، ويمكن أن تجعل أحد الطلبات المكون الآخر يُجري طلبات أكثر لمكونات أكثر، وستتفاقم المشكلة. كانت تطبيقات تمرير الرسائل البطيئة مسؤولة إلى حد كبير عن الأداء الضعيف لأنظمة النواة الدقيقة القديمة، وكانت مفاهيم تمرير الرسائل أصعب قليلًا على المبرمجين، ولم تكن الحماية المُحسَّنة من تشغيل المكونات بصورة منفصلة كافيةً للتغلب على هذه العقبات في أنظمة النواة الدقيقة القديمة، لذا أصبحت قديمة الطراز، في حين تكون الاستدعاءات بين المكونات استدعاءات وظيفيةً بسيطةً في النواة الأحادية كما هو معتاد لدى جميع المبرمجين. لا توجد إجابة محددة حول أفضل تنظيم، وقد بدأت العديد من المناقشات في الأوساط الأكاديمية وغير الأكاديمية حول ذلك، لذا نأمل أن تكون قادرًا على اتخاذ قرار بنفسك عندما تتعلم المزيد عن أنظمة التشغيل. الوحدات Modules تطبّق نواة لينكس نظام الوحدات، حيث يمكن تحميل المشغّلات في النواة المشغَّلة مباشرةً كما هو مطلوب، وهذا أمر جيد لأنّ المشغّلات التي تشكّل جزءًا كبيرًا من شيفرة نظام التشغيل لا تُحمَّل للأجهزة غير الموجودة في النظام، كما يمكن لأيّ شخص يريد أن يصنع أكثر نواة عامة ممكنة -أي تعمل على العديد من الأجهزة المختلفة مثل RedHat أو Debian- تضمينَ معظم المشغّلات بوصفها وحدات تُحمَّل فقط إذا احتوى النظام الذي يعمل عليه على العتاد المتاح، لكن تُحمَّل الوحدات مباشرةً في النواة ذات الصلاحيات وتعمل على مستوى الصلاحيات نفسه لبقية أجزاء النواة، لذلك لا يزال يُعَدّ النظام نواةً أحاديةَ. الافتراضية Virtualisation يرتبط مفهوم العتاد الوهمي أو الافتراضي ارتباطًا وثيقًا بالنواة، إذ تُعَدّ الحواسيب الحديثة قوية جدًا، ولا يُفضَّل استخدامها على أساس نظام واحد كامل، وإنما تقسيم الحاسوب الحقيقي الواحد إلى آلات افتراضية منفصلة virtual machines، إذ تبحث كلٌّ من هذه الآلات الافتراضية عن جميع الأهداف والأغراض بوصفها آلة منفصلة تمامًا بالرغم من أنها فيزيائيًا موجودة في المكان نفسه. بعض طرق تطبيق الافتراضية المختلفة يمكن تنظيم الافتراضية بعدة طرق مختلفة، إذ يمكن تشغيل مراقب آلة افتراضية Virtual Machine Monitor صغير مباشرةً على العتاد وتوفير واجهة لأنظمة تشغيل المضيف التي تعمل في الأعلى، ويُطلَق على مراقب الآلة الافتراضية VMM اسم المشرف Hypervisor من الكلمة Supervisor. يشترك المشرف في كثير الأمور مع النواة الدقيقة، كما يسعيان ليكوّنا طبقات صغيرةً لتقديم العتاد بطريقة آمنة عن الطبقات التي تعلوها، ويمكن ألّا يكون لدى نظام التشغيل الموجود في الطبقة العليا أيّ فكرة عن وجود المشرف Hypervisor على الإطلاق، إذ يقدم هذا المشرف ما يبدو أنه نظام كامل، ويعترض العمليات بين نظام التشغيل المضيف والعتاد ويقدّم مجموعةً فرعيةً من موارد النظام لكل منها. يُستخدَم المشرف غالبًا على الأجهزة الكبيرة التي تحتوي على العديد من وحدات المعالجة المركزية والكثير من ذواكر RAM لتطبيق عملية التجزيء Partitioning، وهذا يعني أنه يمكن تقسيم الجهاز إلى أجهزة افتراضية أصغر، كما يمكنك تخصيص المزيد من الموارد لتشغيل الأنظمة حسب المتطلبات، ويُعَدّ المشرفون الموجودون على العديد من أجهزة IBM الكبيرة معقدةً للغاية مع ملايين الأسطر من الشيفرة البرمجية مع توفير العديد من خدمات إدارة النظام. الخيار الآخر هو جعل نظام التشغيل على دراية بالمشرف الأساسي وطلب موارد النظام عبره، إذ يشار إلى ذلك في بعض الأحيان باسم شبه الوهمية Paravirtualisation نظرًا لطبيعته غير المكتملة، وهو مشابه للطريقة التي تعمل بها الإصدارات الأولى من نظام Xen الذي يُعَدّ حلًا وسطًا، إذ توفّر هذه الطريقة أداءً أفضل لأن نظام التشغيل يطلب صراحةً موارد النظام من المشرف عند الحاجة بدلًا من أن يطبّق المشرف الأمور آليًا. أخيرًا، يمكن أن تصادف موقفًا حيث يقدّم التطبيق الذي يعمل على نظام التشغيل الحالي نظامًا وهميًا يتضمن وحدة معالجة مركزية وذاكرةً ونظام BIOS وقرص صلب وغير ذلك، ويمكن تشغيل نظام تشغيل عادي عليه، إذ يحوّل التطبيق الطلبات إلى العتاد ثم إلى العتاد الأساسي عبر نظام التشغيل الحالي، وهذا مشابه لكيفية عمل برنامج VMWare. تتطلب هذه الطريقة تكلفةً أكبر، إذ يتعين على عملية التطبيق محاكاة نظام بأكمله وتحويل كل شيء إلى طلبات من نظام التشغيل الأساسي، ولكنها تتيح محاكاةً معماريةً مختلفةً تمامًا، إذ يمكنك ترجمة التعليمات آليًا من نوع معالج إلى آخر كما يفعل نظام روزيتا Rosetta مع برمجيات Apple التي انتقلت من معالج PowerPC إلى المعالجات القائمة على إنتل Intel. يُعَدّ الأداء مصدر قلق كبير عند استخدام أيّ من تقنيات الوهمية، إذ يجب أن تمر العمليات -التي كانت تُعَدّ سابقًا عمليات سريعةً ومباشرةً على العتاد- عبر طبقات التجريد. ناقشت شركة إنتل Intel دعم العتاد للوهمية لتكون موجودةً في أحدث معالجاتها، إذ تعمل هذه التوسعات من خلال رفع استثناء خاص للعمليات التي يمكن أن تتطلب تدخّل مراقب الآلة الافتراضية، وبالتالي فإن المعالج يشبه المعالج غير الافتراضي الخاص بالتطبيق الذي يعمل عليه، ولكن يمكن استدعاء مراقب الآلة الافتراضية عندما يقدّم هذا التطبيق طلبات للحصول على موارد يمكن مشاركتها بين أنظمة تشغيل المضيف الأخرى. يوفّر ذلك أداءً فائقًا لأنّ مراقب الآلة الافتراضية لا يحتاج إلى مراقبة كل عملية لمعرفة ما إذا كانت آمنةً، ولكن يمكنه الانتظار حتى يُعلِم المعالج بحدوث شيء غير آمن. القنوات السرية Covert Channels إذا لم يكن تقسيم النظام ساكنًا وإنما آليًا، فهناك مشكلة أمنية محتملة متضمنة في النظام ويُعَدّ هذا عيبًا أمنيًا يتعلق بالآلات الافتراضية. تُخصَّص الموارد لأنظمة التشغيل التي تعمل في الطبقة العليا حسب الحاجة في النظام الآلي، وبالتالي إذا كان أحد هذه الأنظمة ينفّذ عمليات مكثفةً لوحدة المعالجة المركزية بينما ينتظر النظام الآخر وصول البيانات من الأقراص الصلبة، فستُمنَح المهمة الأولى مزيدًا من طاقة وحدة المعالجة المركزية، في حين سيحصل كل منهما على 50% من طاقة وحدة المعالجة المركزية في النظام الساكن، وسيُهدَر الجزء غير المستخدَم. يفتح التخصيص الآلي قناة اتصال بين نظامَي التشغيل التي تكون كافيةً للتواصل في نظام ثنائي في أيّ مكان يمكن الإشارة فيه إلى تلك الحالتين، لكن تخيل أنّ كلا النظامين آمنان جدًا، ولا ينبغي أن تكون أيّ معلومات قادرةً على المرور بينهما على الإطلاق، كما يمكن أن يتآمر شخصان لديها إذن وصول لتمرير المعلومات فيما بينهما من خلال كتابة برنامجين يحاولان أخذ كميات كبيرة من الموارد في الوقت نفسه. إذا أخذ أحدهما مساحةً كبيرةً من الذاكرة، فسيكون هناك قدر أقل من المساحة المتاحة للآخر؛ أما إذا تعقّبا الحد الأقصى من التخصيصات، فيمكن نقل القليل من المعلومات فقط، ولنفترض أنهما اتفقا على التحقق في كل ثانية مما إذا كان بإمكانهما تخصيص هذا القدر الكبير من الذاكرة، فإذا كان الطرف الهدف قادرًا على ذلك، فستُعَدّ هذه الحالة 0 ثنائيًا، وإذا لم يستطع ذلك -أيّ أن الجهاز الآخر يحتوي على كل الذاكرة-، فستُعَدّ هذه الحالة 1 ثنائيًا، كما أنه ليس معدل البيانات المُقدَّر ببت واحد في الثانية مذهلًا، ولكن هذا يدل على وجود تدفق للمعلومات. يسمى ذلك بالقناة السرية Covert Channel، وهذا يظهِر أنّ الأمور ليست بهذا البساطة على مبرمج الأنظمة بالرغم من وجود أمثلة عن انتهاكات أمنية في مثل هذه الآليات. مجال المستخدم نسمي المكان الذي يشغِّل فيه المستخدِم البرامج باسم مجال المستخدِم Userspace، إذ يعمل كل برنامج في مجال مستخدِم، ويتواصل مع النواة عبر استدعاءات النظام التي سنوضحّها في المقال القادم، كما لا يتمتع مجال المستخدِم بصلاحيات Unprivileged، إذ يمكن لبرامج المستخدِم تطبيق مجموعة محدودة فقط من الأشياء، ويجب ألّا تكون قادرةً على تعطيل البرامج الأخرى حتى إذا تعطلت هي نفسها. ترجمة -وبتصرُّف- للقسمين Operating System Organisation و The role of the operating system من الفصل The Operating System من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: استدعاءات النظام والصلاحيات في نظام التشغيل المقال السابق: أنظمة المعالجات في معمارية الحاسوب المدخل الشامل لتعلم علوم الحاسوب اختيار العتاد والبرامج في العالم الرقمي النسخة العربية الكاملة من كتاب أنظمة التشغيل للمبرمجين
-
نَمت قوة الحوسبة بوتيرة سريعة دون ظهور أيّ علامات على التباطؤ كما توقّع قانون مور Moore، فليس مألوفًا أن تحتوي أيّ خوادم عالية الجودة على وحدة معالجة مركزية واحدة فقط مع إمكانية تحقيق ذلك باستخدام عدد من الأساليب المختلفة. المعالجة المتعددة المتماثلة Symmetric Multi-Processing تُعَدّ المعالجة المتعددة المتماثلة Symmetric Multi-Processing -أو SMP اختصارًا- الإعداد الأكثر شيوعًا حاليًا لتضمين وحدات المعالجة المركزية CPU المتعددة في نظام واحد، ويشير المصطلح متماثل Symmetric إلى حقيقة أنّ جميع وحدات المعالجة المركزية في النظام هي نفسها من حيث المعمارية وسرعة الساعة مثلًا، كما توجد في نظام SMP معالجات متعددة تشترك في جميع موارد النظام الأخرى مثل الذاكرة والقرص الصلب وغير ذلك. ترابط الذواكر المخبئية Cache Coherency تعمل وحدات المعالجة المركزية في النظام بصورة مستقلة عن بعضها بعضًا، فلكل منها مجموعته الخاصة من المسجلات وعدّاد البرنامج وغير ذلك، ولكن يوجد مكوِّن واحد يتطلب تزامنًا صارمًا بالرغم من تشغيل وحدات المعالجة المركزية بصورة منفصلة عن بعضها بعضًا، وهذا المكوِّن هو الذاكرة المخبئية Cache الخاصة بوحدة المعالجة المركزية. تذكّر أنّ الذاكرة المخبئية هي مساحة صغيرة من الذاكرة يمكن الوصول إليها بسرعة وتعكس القيم المخزّنة في ذاكرة النظام الرئيسية، فإذا عدّلت إحدى وحدات المعالجة المركزية البيانات في الذاكرة الرئيسية وكان لدى وحدة معالجة مركزية أخرى نسخة قديمة من تلك الذاكرة في ذاكرتها المخبئية، فلن يكون النظام في حالة متناسقة، ولاحظ أنّ هذه المشكلة تحدث عندما تكتب المعالجات في الذاكرة فقط، إذ ستكون البيانات متناسقةً إذا كانت القيمة للقراءة فقط. يستخدِم نظام SMP عملية التنصت Snooping لتنسيق الحفاظ على ترابط الذواكر المخبئية على جميع المعالجات، إذ يُعَدّ التنصت العمليةَ التي يستمع فيها المعالج إلى ناقل تتصل به جميع المعالجات لمعرفة أحداث الذاكرة المخبئية، ثم يحدّث الذاكرة المخبئية وفقًا لذلك. يمكن تحقيق ذلك باستخدام بروتوكول واحد هو بروتوكول MOESI الذي يرمز إلى الكلمات مُعدَّل Modified ومالك Owner وحصري Exclusive ومشارَك Shared وغير صالح Invalid التي تمثّل الحالة التي يمكن أن يكون فيها خط الذاكرة المخبئية على معالج في النظام، كما توجد بروتوكولات أخرى لذلك، ولكن تشترك جميعها في مفاهيم متشابهة، وسنوضح فيما يلي بروتوكول MOESI. إذا طلب المعالج قراءة خط ذاكرة مخبئية من الذاكرة الرئيسية، فيجب عليه أولًا التنصت على جميع المعالجات الأخرى في النظام لمعرفة ما إذا كانت تعرف حاليًا أيّ شيء عن تلك المنطقة من الذاكرة مثل تخزينها في الذاكرة المخبئية، فإذا لم تكن موجودةً في أيّ عملية أخرى، فيمكن للمعالج تحميل الذاكرة في الذاكرة المخبئية وتمييزها على أنها حصرية Exclusive، ويغير الحالة إلى معدَّلة Modified عند الكتابة في الذاكرة المخبئية. تلعب هنا تفاصيل الذاكرة المخبئية دورًا أساسيًا، إذ ستعيد بعض الذواكر المخبئية مباشرةً كتابة الذاكرة المخبئية المعدَّلة إلى ذاكرة النظام المعروفة باسم الذاكرة المخبئية من النوع Write-through، لأن عمليات الكتابة تنتقل إلى الذاكرة الرئيسية، في حين لن تفعل ذلك الذواكر المخبئية الأخرى، بل ستترك القيمة المُعدَّلة في الذاكرة المخبئية فقط حتى التخلص منها عندما تمتلئ الذاكرة المخبئية مثلًا. الحالة الأخرى هي المكان الذي يطبّق فيه المعالج عملية التنصت ويكتشف أن القيمة موجودة في ذاكرة مخبئية خاصة بمعالجات أخرى، فإذا كانت هذه القيمة مميَّزةً بوصفها مُعدَّلةً Modified، فسينسخ المعالج البيانات في ذاكرته المخبئية ويميّزها على أنها مشتركة Shared، كما سيرسل رسالةً إلى المعالج الآخر الذي حصلنا على البيانات منه لتمييز خط ذاكرته المخبئية بوصفه المالك Owner، لنفترض الآن أن معالجًا ثالثًا في النظام يريد استخدام تلك الذاكرة أيضًا، فسيتنصت ويبحث عن نسخة مشتركة ونسخة مالكة، وبالتالي سيأخذ قيمته من قيمة المالك. تقرأ جميع المعالجات الأخرى القيمة فقط، ولكن يبقى خط الذاكرة المخبئية مشتركًا في النظام، فإذا احتاج معالج ما تحديث القيمة، فإنه يرسل رسالة إلغاء صلاحية Invalidate عبر النظام، كما يجب على أيّ معالج له هذا الخط الخاص بالذاكرة المخبئية تمييزه بوصفه غير صالح Invalid لأنه لم يَعُد يعكس القيمة الحقيقية، ويميز المعالج خط الذاكرة المخبئية بوصفه معدَّلًا في ذاكرته المخبئية عندما يرسل رسالة إلغاء الصلاحية وستميّزه المعالجات الأخرى على أنه غير صالح. لاحظ أنه إذا كان خط الذاكرة المخبئية حصريًا، فسيعلم المعالج أنه لا يوجد معالج آخر يعتمد عليه، لذا يمكنه تجنّب إرسال رسالة إلغاء صلاحية، وتبدأ بعدها العملية من جديد، وبالتالي يتحمل أيُّ معالج له القيمة المعدّلة مسؤوليةَ كتابة القيمة الحقيقية مرةً أخرى إلى الذاكرة RAM عند التخلص منها من الذاكرة المخبئية، ولاحظ أنّ هذا البروتوكول يضمن تناسق خط الذاكرة المخبئية بين المعالجات. هناك العديد من المشاكل في هذا النظام عند زيادة عدد المعالجات، إذ يمكن التحكم في تكلفة التحقق من وجود معالج آخر يحتوي على خط ذاكرة مخبئية (التنصت على عملية القراءة) أو إلغاء صلاحية البيانات في كل معالج آخر (إلغاء عملية التنصت) عند استخدام عدد قليل من المعالجات، ولكن تزداد حركة النواقل مع زيادة عدد المعالجات وهذا هو السبب في أن أنظمة SMP تصل إلى حوالي 8 معالجات فقط. يعطي وجود جميع المعالجات في الناقل نفسه مشاكل فيزيائية أيضًا، إذ تسمح خصائص الأسلاك الفيزيائية بوضعها على مسافات معينة من بعضها بعضًا وتسمح بأن يكون لها أطوال معينة فقط، وتبدأ سرعة الضوء في أن تصبح أحد الجوانب التي يجب مراعاتها في المدة التي تستغرقها الرسائل للتنقل في النظام مع المعالجات التي تعمل بسرعة مقدَّرةً بالجيجاهيرتز. لاحظ أنّ برمجيات النظام ليس لها أيّ جزء من هذه العملية، بالرغم من أنّ المبرمجين يجب أن يكونوا على دراية بما يطبّقه العتاد استجابةً للبرمجيات التي يصمّمونها لزيادة الأداء إلى الحد الأقصى. حصرية الذاكرة المخبئية في أنظمة SMP شرحنا في مقال سابق الذواكر المخبئية الشاملة Inclusive والحصرية Exclusive، إذ تكون الذواكر المخبئية L1 شاملةً، أي أن جميع البيانات الموجودة في الذاكرة المخبئية L1 موجودة في الذاكرة المخبئية L2، وتعني الذاكرة المخبئية L1 الشاملة أنّ الذاكرة المخبئية L2 يجب أن تتنصت حركة مرور الذاكرة للحفاظ على ترابطها في نظام متعدد المعالجات، إذ ستضمن L1 عكس أيّ تغييرات في الذاكرة L2، مما يقلل من تعقيد ذاكرة L1 ويفصله عن عملية التنصت، وبالتالي سيسمح لها بأن تكون أسرع. تحتوي معظم المعالجات الحديثة المتطورة مثل المعالجات التي ليست مدمَجة على سياسة كتابة الذاكرة المخبئية L1 من النوع Write-through وسياسة الكتابة من النوع Write-back في الذواكر المخبئية ذات المستوى الأدنى، وهناك عدة أسباب لذلك، فبما أنّ ذواكر L2 المخبئية في هذا الصنف من المعالجات تكون حصريةً تقريبًا على الشريحة وسريعةً جدًا عمومًا، فليست العقوبات المفروضة على كتابة الذاكرة المخبئية L1 من النوع Write-through الأمر الرئيسي، كما يمكن أن تتسبّب مجمّعات البيانات المكتوبة التي لا يُحتمَل قراءتها في المستقبل في تلوث مورد L1 المحدود لأن أحجام L1 صغيرة. ليس هناك داع للقلق بشأن الكتابة في L1 من النوع Write-through إذا احتوت على بيانات متسخة معلَّقة، وبالتالي يمكن أن تمرّر منطق الترابط الإضافي إلى ذاكرة L2 التي لديها دور أكبر تلعبه في ترابط الذاكرة المخبئية. تقنية خيوط المعالجة الفائقة Hyperthreading يمكن أن يقضي المعالج الحديث كثيرًا من وقته في انتظار أجهزة أبطأ بكثير في تسلسل الذواكر الهرمي لتقديم البيانات للمعالجة، وبالتالي فإن إستراتيجيات الحفاظ على خط أنابيب المعالج ممتلئًا لها أهمية قصوى، وتتمثل إحدى الإستراتيجيات في تضمين عدد كاف من المسجلات ومنطق الحالة بحيث يمكن معالجة مجريين من التعليمات في الوقت نفسه، مما يجعل وحدة معالجة مركزية واحدة تبحث عن جميع النوايا والأهداف المطلوبة كأنها وحدتان CPU. تحتوي كل وحدة معالجة مركزية على مسجلاتها الخاصة، ولكن يجب عليها مشاركة منطق المعالج الأساسي والذاكرة المخبئية وحيز النطاق التراسلي للإدخال والإخراج من وحدة المعالجة المركزية إلى الذاكرة، لذا يمكن أن يحافظ مجريان من التعليمات على المنطق الأساسي للمعالج أكثر انشغالًا، ولكن لن تكون زيادة الأداء كبيرةً بسبب وجود وحدتَي CPU منفصلتين فيزيائيًا، ويكون تحسين الأداء أقل من 20%، ولكن يمكن أن يكون أفضل أو أسوأ كثيرًا اعتمادًا على الحِمل. الأنوية المتعددة Multi Core أصبح وضع معالِجَين أو أكثر في الحزمة الفيزيائية نفسها ممكنًا مع زيادة القدرة على احتواء مزيد من الترانزستورات على شريحة واحدة، ولكن المعالجات الأكثر شيوعًا هي المعالجات ثنائية النواة، إذ توجد نواتان للمعالج على الشريحة نفسها، وتُعَدّ هذه الأنوية -على عكس تقنية خيوط المعالجة الفائقة Hyperthreading- معالجات كاملةً، وبالتالي تبدو على أنها معالجات منفصلة فيزيائيًا مثل نظام SMP. تحتوي المعالجات على ذاكرة L1 المخبئية الخاصة بها، ولكن يجب عليها مشاركة الناقل المتصل بالذاكرة الرئيسية وأجهزة أخرى، وبالتالي لن يكون الأداء جيدًا مثل نظام SMP الكامل، ولكنه أفضل بكثير من نظام خيوط المعالجة الفائقة، ويمكن لكل نواة تطبيق تقنية خيوط المعالجة الفائقة لتحسين إضافي. تتمتع المعالجات متعددة الأنوية ببعض المزايا التي لا تتعلق بالأداء، كما أنّ للناقلات الفيزيائية الخارجية بين المعالجات حدود فيزيائية، ولكن يمكن حل بعض هذه المشاكل من خلال احتواء المعالجات على قطعة السيليكون نفسها بحيث تكون قريبةً جدًا من بعضها بعضًا. تُعَدّ متطلبات الطاقة للمعالجات متعددة الأنوية أقل بكثير من المعالجات المنفصلة عن بعضها بعضًا، وهذا يعني أن هناك حاجة أقل لتبريد الحرارة والتي يمكن أن تكون ميزةً كبيرةً في تطبيقات مراكز البيانات حيث تُجمَّع الحواسيب مع وجود حاجة كبيرة للتبريد، كما يجعل وجود الأنوية في الحزمة الفيزيائية نفسها المعالجةَ المتعددة عمليةً في التطبيقات التي لن تكون فيها كذلك مثل الحواسيب المحمولة، كما يُعَدّ إنتاج شريحة واحدة بدلًا من شريحتين أرخص بكثير. العناقيد Clusters تتطلب العديد من التطبيقات أنظمةً أكبر بكثير من عدد المعالجات التي يمكن لنظام SMP التوسع إليها، وتُعَدّ العناقيد Clusters إحدى الطرق لتوسيع النظام أكثر، وهي عدد من الحواسيب التي لديها بعض القدرة على التواصل مع بعضها بعضًا، كما لا تعرف الأنظمة بعضها بعضًا على مستوى العتاد، إذ تُترَك مهمة ربط هذه الحواسيب للبرمجيات. تسمح البرمجيات مثل MPI للمبرمجين بكتابة برامجهم ثم وضع أجزاء منها على حواسيب أخرى في النظام مثل تمثيل حلقة تُنفَّذ عدة آلاف من المرات وتطبّق إجراءً مستقلًا، أي لا يوجد تكرار للحلقة يؤثر على أيّ تكرار آخر، ويمكن للبرمجيات جعل كل حاسوب يشغّل 250 حلقة لكل منها مع وجود أربعة حواسيب في العنقود. يختلف الترابط بين الحواسيب، إذ يمكن أن يكون بطيئًا مثل روابط شبكة الإنترنت أو سريعًا مثل الناقلات المخصَّصة والخاصة مثل روابط إنفيني باند Infiniband، ومهما كان هذا الترابط، فسيبقى في المستوى الأخفض من تسلسل الذواكر الهرمي وسيكون أبطأ بكثير من الذاكرة RAM، وبالتالي لن يقدّم العنقود أداءً جيدًا في الموقف الذي تتطلب فيه كل وحدة معالجة مركزية الوصول إلى البيانات المُخزَّنة في الذاكرة RAM الخاصة بحاسوب آخر، إذ ستحتاج البرمجيات في كل مرة أن تطلب نسخةً من البيانات من الحاسوب الآخر، وتنسخها عبر الرابط البطيء إلى الذاكرة RAM المحلية قبل أن يتمكن المعالج من إنجاز أيّ عمل. لا تتطلب العديد من التطبيقات هذا النسخ المستمر بين الحواسيب، وأحد الأمثلة الشائعة عن ذلك هو SETI@Home، إذ تُحلَّل البيانات التي جرى جمعها من هوائي راديو بحثًا عن علامات على وجود كائن فضائي، ويمكن توزيع كل حاسوب لبضع دقائق للحصول على البيانات لتحليلها ويعطي تقريرًا ملخصًا لما وجده، إذ يُعَدّ SETI@Home عنقودًا مخصَّصًا وكبيرًا جدًا. يوجد تطبيق آخر هو تطبيق تصيير الصور Rendering of Images الذي يُستخدَم خاصةً للتأثيرات الخاصة في الأفلام، إذ يُسلَّم كل حاسوب إطارًا واحدًا من الفيلم يحتوي على نماذج إطارات شبكية وخامات Textures ومصادر إضاءة يجب دمجها أو تصييرها في التأثيرات الخاصة المذهلة التي نحصل عليها، كما يُعَدّ كل إطار ساكنًا، لذلك لا يحتاج الحاسوب بمجرد حصوله على الدخل الأولي لمزيد من الاتصال حتى يصبح الإطار النهائي جاهزًا لإرساله ودمجه في الحركة، فقد كان لفيلم سيد الخواتم مثلًا تأثيرات خاصة مصيَّرة على عنقود ضخم يعمل بنظام لينكس. الوصول غير الموحد للذاكرة Non-Uniform Memory Access يُعَدّ الوصول غير الموحد للذاكرة Non-Uniform Memory Access -أو NUMA اختصارًا- عكس نظام العناقيد السابق تقريبًا، ولكنه -كما هو الحال في نظام العنقود- يتكون من عقد فردية مرتبطة ببعضها بعضًا، إلا أنّ الارتباط بين العقد شديد التخصص ومكلف، ولا يمتلك العتاد أيّ معرفة بالربط بين العقد في نظام العنقود، في حين لا تمتلك البرمجيات في نظام NUMA معرفةً جيدةً أو تمتلك معرقةً أقل حول تخطيط النظام، إذ يطبّق العتاد كل العمل لربط العقد مع بعضها بعضًا. يأتي مصطلح الوصول غير الموحّد إلى الذاكرة من حقيقة أن الذاكرة RAM ليست محلية بالنسبة لوحدة المعالجة المركزية، وبالتالي يمكن أن هناك حاجة لأن تصل عقدة على بعد مسافة ما إلى البيانات، إذ يستغرق ذلك وقتًا أطول على النقيض من معالج واحد أو نظام SMP حيث يمكن الوصول إلى الذاكرة RAM مباشرةً، ويستغرق ذلك دائمًا وقتًا ثابتًا أو موحّدًا. تخطيط نظام NUMA يُعَدّ تقليل المسافة بين العقد أمرًا بالغ الأهمية مع وجود العديد من العقد التي تتواصل مع بعضها في النظام، إذ يُفضَّل أن يكون لكل عقدة رابط مباشر بكل عقدة أخرى لأنه يقلّل المسافة التي تحتاجها أيّة عقدة للعثور على البيانات، لكن لا يُعَدّ ذلك موقفًا عمليًا عندما ينمو عدد العقد إلى المئات والآلاف كما هو الحال مع الحواسيب العملاقة الكبيرة، فالأساس في هذا النمو هو مجموعة مؤلفة من عقدتين تتواصلان مع بعضهما بعضًا ثم ستنمو إلى n!/2*(n-2)!. تُستخدَم التخطيطات البديلة لمقايضة المسافة بين العقد مع الوصلات المطلوبة بهدف التقليل من هذا النمو الأسي، فأحد هذه التخطيطات الشائعة في معماريات NUMA الحديثة هو المكعب الفائق Hypercube الذي يحتوي على تعريف رياضي صارم، ويكون المكعب الفائق هو نظير رباعي الأبعاد للمكعب الذي هو نظير ثلاثي الأبعاد للمربع. مثال عن المكعب الفائق Hypercube الذي يوفر مقايضةً جيدةً بين المسافة بين العقد وعدد الوصلات المطلوب. يمكننا أن نرى في الشكل السابق أن المكعب الخارجي يحتوي على 8 عقد، والحد الأقصى لعدد المسارات المطلوبة لأي عقدة للتواصل مع عقدة أخرى هو 3، فإذا وضعنا مكعبًا آخر داخل هذا المكعب، فسيكون لدينا ضعف عدد المعالجات ولكن زادت التكلفة القصوى للمسار إلى 4، مما يعني نمو تكلفة المسار القصوى خطيًا فقط عند نمو عدد المعالجات بمقدار 2n. ترابط الذاكرة المخبئية Cache Coherency لا يزال الحفاظ على ترابط الذاكرة المخبئية في نظام NUMA ممكنًا، إذ يشار إلى ذلك باسم نظام NUMA مع ترابط الذاكرة المخبئية Cache Coherent NUMA System أو ccNUMA اختصارًا، ولا يتوسّع المخطط القائم على البث الإذاعي المُستخدَم للحفاظ على ترابط ذاكرة المعالج المخبئية في نظام SMP إلى مئات أو حتى آلاف المعالجات في نظام NUMA كبير. يشار إلى أحد المخططات الشائعة لترابط الذاكرة المخبئية في نظام NUMA باسم النموذج المستند إلى الدليل Directory Based Model الذي تتصل فيه المعالجات الموجودة في النظام بعتاد دليل الذاكرة المخبئية، إذ يحافظ عتاد الدليل على صورة متناسقة لكل معالج، كما يخفي هذا التجريد عمل نظام NUMA عن المعالج. يحتفظ المخطط المستند إلى الدليل لصاحبيه Censier و Feautrier بدليل مركزي، إذ تحتوي كل كتلة ذاكرة على بِت راية يُعرَف بالبِت الصالح Valid Bit لكل معالج وبِت واحد يُسمَّى بالبِت المتسخ Dirty Bit، ويضبط الدليل البِت الصالح للمعالج الذي يقرأ الذاكرة إلى ذاكرته المخبئية. إذا أراد المعالج الكتابة إلى خط الذاكرة المخبئية، فيجب أن يضبِط الدليل البِتَّ المتسخ لكتلة الذاكرة من خلال إرسال رسالة إلغاء صلاحية إلى تلك المعالجات التي تستخدِم خط الذاكرة المخبئية والمعالجات التي جرى ضبط رايتها فقط بهدف تجنب حركة مرور البث broadcast traffic. يجب بعد ذلك أن يحاول أيّ معالج آخر قراءة كتلة الذاكرة، وسيجد الدليل ضبط البِت المتسخ، كما يجب أن يحصل الدليل على خط الذاكرة المخبئية المُحدَّث من المعالج مع البِت الصالح المضبوط حاليًا، ويعيد كتابة البيانات المتسخة إلى الذاكرة الرئيسية ثم إعادة هذه البيانات إلى المعالج المطلوب، مما يؤدي إلى ضبط البِت الصالح للمعالج الطالب في هذه العملية، ولاحظ أنّ هذا الأمر واضح للمعالج الطالب ويمكن أن يحتاج الدليل الحصول على تلك البيانات من مكان قريب جدًا أو من مكان بعيد جدًا. لا يمكن أن يتوسع المخطط المؤلَّف من آلاف المعالجات التي تتصل بدليل واحد بصورة جيدة، إذ تتضمن توسّعات المخطط وجود تسلسل هرمي من الدلائل التي تتواصل فيما بينها باستخدام بروتوكول منفصل، كما يمكن أن تستخدِم الدلائل شبكة اتصالات ذات أغراض أعم للتواصل فيما بينها بدلًا من ناقل وحدة المعالجة المركزية، مما يسمح بالتوسع إلى أنظمة أكبر بكثير. تطبيقات NUMA تُعَدّ أنظمة NUMA الأنسب لأنواع المشاكل التي تتطلب قدرًا كبيرًا من التفاعل بين المعالج والذاكرة، فمن المصطلحات الشائعة في محاكاة الطقس مثلًا هو تقسيم البيئة إلى صناديق صغيرة تستجيب بطرق مختلفة، بحيث تعكس المحيطات والأرض أو تخزن كميات مختلفة من الحرارة مثلًا، ويجب تغذية الاختلافات الصغيرة لمعرفة النتيجة الإجمالية أثناء تشغيل عمليات المحاكاة. يؤثر كل صندوق على الصناديق المحيطة، إذ يعني وجود الشمس أكثر قليلًا مثلًا أنّ صندوقًا معينًا ينشر مزيدًا من الحرارة مما يؤثر على الصناديق المجاورة له، ولكن سيكون هناك الكثير من الاتصالات على عكس إطارات الصور الفردية في عملية التصيير Rendering التي لا تؤثر على بعضها، كما يمكن أن تحدث عمليةً مماثلةً إذا أردت تصميم نموذج لحادث سيارة، حيث سيُطوى كل صندوق صغير من السيارة التي تحاكيها بطريقة ما وسيمتص قدرًا من الطاقة. ليس للبرمجيات معرفة مباشرة بأن النظام الأساسي هو نظام NUMA، ولكن يجب أن يتوخّى المبرمجون الحذر عند البرمجة لهذا النظام للحصول على أفضل أداء، وسيؤدي الاحتفاظ بالذاكرة بالقرب من المعالج الذي سيستخدِمها إلى أفضل أداء، ولكن يجب أن يستخدِم المبرمجون تقنيات مثل التشخيص Profiling لتحليل مسارات الشيفرة البرمجية المتّبَعة والعواقب التي تسببها الشيفرة البرمجية للنظام لاستخراج أفضل أداء. ترتيب الذاكرة وقفلها تجلب الذاكرة المخبئية متعددة المستويات والمعمارية متعددة المعالجات الفائقة بعض المشاكل المتعلقة بكيفية رؤية المبرمج لشيفرة المعالج البرمجية التي تكون قيد التشغيل. لنفترض أنّ شيفرة البرنامج البرمجية تعمل على معالجَين في الوقت نفسه، وأنّ كلا المعالجين يشتركان بفعالية في منطقة واحدة كبيرة من الذاكرة، فإذا أصدر أحد المعالجَين تعليمات تخزين لوضع قيمة مسجّل في الذاكرة، فلا بد أنك تتساءل عن الوقت الذي يمكن فيه التأكد من أن المعالج الآخر يحمّل تلك الذاكرة التي سيرى قيمتها الصحيحة. يمكن للنظام في أبسط الحالات أن يضمن أنه في حالة تنفيذ أحد البرامج لتعليمات التخزين، وبالتالي سترى أيّ تعليمات تحميل لاحقة هذه القيمة، إذ يُشار إلى ذلك باسم ترتيب الذاكرة الصارم Strict Memory Ordering، لأن القواعد لا تسمح بأيّ مجال للحركة، كما يجب أن تدرك أنّ هذا النوع من الأشياء يُعَدّ عائقًا خطيرًا أمام أداء النظام. لا يُطلَب من ترتيب الذاكرة أن يكون صارمًا جدًا في كثير من الأحيان، إذ يمكن للمبرمج تحديد النقاط التي يحتاجها للتأكد من رؤية جميع العمليات المُعلَّقة بطريقة عامة، ولكن يمكن أن يكون هناك العديد من التعليمات من بين هذه النقاط حيث لا تكون الدلالات Semantics مهمة، ولنفترض الموقف التالي مثلًا الذي يمثل ترتيب الذاكرة: typedef struct { int a; int b; } a_struct; /* * مرّر مؤشرًا لتخصيصه بوصفه بنيةً جديدةً */ void get_struct(a_struct *new_struct) { void *p = malloc(sizeof(a_struct)); /* لا نهتم بترتيب التعليمتين التاليتين * اللتين ستُنفَّذان في النهاية */ p->a = 100; p->b = 150; /* .لكن يجب أن تُنفَّذا قبل التعليمة التالية * p وإلّا فسيتمكن معالج آخر ينظر إلى قيمة * .من أن يجدها تؤشّر إلى بنية قيمها غير مملوءة */ new_struct = p; } لدينا في هذا المثال عمليتَي تخزين يمكن تطبيقهما بأيّ ترتيب معيّن بما يناسب المعالج، ولكن يجب في الحالة الأخيرة تحديث المؤشر فقط بمجرد التأكد من اكتمال عمليتَي التخزين السابقتين، وإلّا فيمكن أن ينظر معالج آخر إلى قيمة p ويتبع المؤشر إلى الذاكرة ويحمّلها ويحصل على قيمة غير صحيحة تمامًا، لذا يجب أن تحتوي عمليات التحميل والتخزين على دلالات تصف سلوكها. توصَف دلالات الذاكرة من حيث الأسوار Fences التي تحدّد كيفية إعادة ترتيب عمليات التحميل والتخزين، كما يمكن افتراضيًا إعادة طلب عملية التحميل أو التخزين في أيّ مكان، ويشبه اكتساب الدلالات Acquire Semantics السورَ الذي يسمح فقط لعمليات التحميل والتخزين بالتحرك للأسفل عبره، أي يمكنك ضمان أنّ أيّ عملية تحميل أو تخزين لاحقة سترى القيمة -لأنه لا يمكن نقلها فوقها- عند اكتمال هذا التحميل أو التخزين. يُعَدّ تحرير الدلالات Release Semantics عكس ذلك، أي يسمح السور بأي عملية تحميل أو تخزين أن تكتمل قبله -أي التحرك للأعلى-، ولكن لا يوجد شيء قبلها للتحرك للأسفل. وبالتالي يمكنك تخزين أيّ عملية تحميل أو تخزين سابقة مكتملة عند معالجة التحميل أو التخزين باستخدام تحرير الدلالات. رسم توضيحي يمثّل عمليات إعادة الترتيب الصالحة للعمليات باستخدام اكتساب الدلالات وتحريرها سور الذاكرة الكامل full memory fence هو مزيج من اكتساب الدلالات وتحريرها، حيث لا يمكن إعادة ترتيب عمليات التحميل أو التخزين في أيّ اتجاه حول عملية التحميل أو التخزين الحالية، كما يستخدِم نموذج الذاكرة الأكثر صرامة سور ذاكرة كامل لكل عملية، في حين سيترك النموذج الأضعف كل عملية تحميل وتخزين على أساس تعليمات عادية قابلة لإعادة الترتيب. المعالجات ونماذج الذاكرة تطبّق المعالجات المختلفة نماذج ذاكرة مختلفة، إذ يحتوي معالج x86 ومعالج AMD64 على نموذج ذاكرة صارم تمامًا، حيث تحتوي جميع عمليات التخزين على تحرير دلالات، أي يجب أن ترى أيّة عملية تحميل أو تخزين لاحقة نتيجةَ عملية التخزين، ولكن جميع عمليات التحميل لها دلالات عادية، كما تعطي بادئة القفل سورًا للذاكرة، في حين يسمح المعالج إيتانيوم Itanium لجميع عمليات التحميل والتخزين بأن تكون عاديةً ما لم يُجرَى إخباره صراحةً بغير ذلك. القفل ليست معرفة متطلبات ترتيب الذاكرة لكل معمارية عمليةً ومناسبةً لجميع المبرمجين وسيجعل ذلك نقل البرامج وتنقيحها عبر أنواع المعالجات المختلفة أمرًا صعبًا، إذ يستخدِم المبرمجون مستوًى أعلى من التجريد يسمى القفل Locking للسماح بالتشغيل المتزامن للبرامج عندما يكون هناك وحدات معالجة مركزية متعددة، كما لا يمكن لأيّ معالج آخر الحصول على القفل حتى يُحرَّر عندما يحصل برنامج ما عليه لجزء من شيفرة برمجية، كما يجب أن يحاول المعالج أخذ القفل قبل أيّ أجزاء مهمة من الشيفرة البرمجية، فإذا لم يستطع الحصول عليه، فلن يستمر في عمله. يمكنك رؤية كيف أنّ ذلك مقيَّد بتسمية دلالات ترتيب الذاكرة الموضَّحة سابقًا، كما نريد التأكد من أنه لن يُعاد طلب أيّ عمليات يجب أن يحميها القفل قبل الحصول عليه، وهذه هي الطريقة التي تعمل بها عملية اكتساب الدلالات، في حين يجب التأكد من أنّ كل عملية طبّقناها أثناء احتفاظنا بالقفل مكتملة عندما نحرره مثل مثال تحديث المؤشر الموضَّح سابقًا، وهذا ما يسمى بتحرير الدلالات. هناك العديد من المكتبات البرمجية المتاحة التي تسمح للمبرمجين بعدم القلق بشأن تفاصيل دلالات الذاكرة واستخدام المستوى الأعلى من تجريد القفل lock() وإلغاء القفل unlock(). صعوبات الأقفال تجعل أنظمة القفل البرمجة أكثر تعقيدًا، إذ يمكنها أن تؤدي إلى تعطيل البرامج، ولنفترض أنّ معالجًا ما يحتفظ بقفل على بعض البيانات، وينتظر قفلًا على بيانات أخرى حاليًا، فإذا انتظر معالج آخر البيانات التي يحتفظ بها المعالج الأول وكان قبل ذلك وقبل دخوله في حالة قفل يحتفظ ببيانات يريدها المعالج الأول ذاك لفك قفله، فسنواجه حالة تعطل تام، بحيث ينتظر كل معالج المعالج الآخر ولا يمكن لأيّ منهما الاستمرار بدون قفل المعالج الآخر. ينشأ هذا الموقف بسبب حالة التسابق Race Condition في أغلب الأحيان التي تُعَدّ إحدى أصعب الأخطاء التي يمكن تعقّبها، فإذا كان هناك معالِجان يعتمدان على عمليات تحدث بترتيب معيّن في الوقت، فهناك دائمًا احتمال حدوث حالة تسابق، كما يمكن أن تصطدم أشعة جاما المنبعثة من نجم متفجر في مجرة أخرى بأحد المعالجات، مما يؤدي إلى الخروج عن ترتيب العمليات، ثم ستحدث حالة تعطل تام كما رأينا سابقًا، لذا يجب ضمان ترتيب البرامج باستخدام الدلالات وليس عبر الاعتماد على سلوكيات محددة لمرة واحدة. يوجد وضع مماثل يسمى المنع Livelock وهو عكس التعطل Deadlock، إذ يمكن أن تكون إحدى الاستراتيجيات لتجنب التعطل أن يكون لديك قفل مؤدب Polite يرفض إعطاء القفل لكل مَن يطلبه، وقد يتسبب هذا القفل المؤدّب في جعل خيطين Threads يمنحان بعضهما القفل باستمرار دون الحاجة إلى أخذ القفل لفترة كافية لإنجاز العمل المهم والانتهاء من القفل، إذ يمكن أن يكون هناك وضع مشابه في الحياة الواقعية لشخصين يلتقيان عند الباب في الوقت نفسه، ويقول كلاهما: "لا، أنت أولًا، أنا أصر على ذلك" دون المرور عبر الباب نهائيًا. استراتيجيات القفل هناك العديد من الاستراتيجيات المختلفة لتطبيق سلوك الأقفال، إذ يُشار إلى القفل البسيط الذي يحتوي ببساطة على حالتين -مقفل Locked أو غير مقفل Unlocked- على أنه كائن مزامنة Mutex، وهو اختصار للاستبعاد المتبادل Mutual Exclusion الذي يعني أنه إذا كان لدى شخص ما قفلًا، فلا يمكن لشخص آخر الحصول عليه، وهناك عدد من الطرق لتطبيق قفل كائن المزامنة، إذ لدينا في أبسط الحالات ما يسمى بالقفل الدوار Spinlock إذ يبقى المعالج ضمن حلقة في انتظار أخذ القفل مثل طفل صغير يطلب من والديه شيئًا ويقول "هل يمكنني الحصول عليه الآن؟" باستمرار. تكمن مشكلة هذه الاستراتيجية في أنها تضيع الوقت، إذ لا ينفّذ المعالج أيّ عملٍ مفيد بينما يكون متوقفًا ويطلب القفل باستمرار، وقد يكون ذلك مناسبًا للأقفال التي يُحتمَل أن تُقفَل لفترة قصيرة جدًا من الوقت فقط، ولكن يمكن أن يكون مقدار الوقت الذي يستغرقه القفل أطول بكثير في كثير من الحالات. الاستراتيجية الأخرى هي السكون Sleep، حيث إذا لم يتمكن المعالج من الحصول على القفل، فسينفّذ بعض الأعمال الأخرى في انتظار إشعار بأن القفل متاح للاستخدام، وسنرى في المقالات القادمة كيف يمكن لنظام التشغيل تبديل العمليات وإعطاء المعالج مزيدًا من العمل لتنفيذه. يُعَدّ كائن المزامنة حالةً خاصةً من متغير تقييد الوصول Semaphore الذي اخترعه عالم الحاسوب الهولندي ديكسترا Dijkstra، إذ يمكن ضبط متغير تقييد الوصول Semaphore لحساب عدد مرات الوصول إلى الموارد في حالة توفر العديد منها، في حين يكون لديك كائن المزامنة Mutex في الحالة التي يكون فيها عدد الموارد يساوي واحدًا فقط. لكن لا تزال أنظمة القفل هذه تواجه بعض المشاكل، إذ يرغب معظم الأشخاص في قراءة البيانات التي تُحدَّث في حالات نادرة فقط. يمكن أن يؤدي وجود جميع المعالجات التي ترغب في قراءة البيانات فقط التي تتطلب قفلًا إلى تنازع القفل حيث يُنجَز القليل من العمل لأن الجميع ينتظر الحصول على القفل نفسه لبعض البيانات. ترجمة -وبتصرُّف- للقسم Small to big systems من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: دور نظام التشغيل وتنظيمه في معمارية الحاسوب المقال السابق: الأجهزة الطرفية Peripherals ونواقلها Buses في معمارية الحاسوب وحدة المعالجة المركزية المدخل الشامل لتعلم علوم الحاسوب اختيار العتاد والبرامج في العالم الرقمي
-
الأجهزة الطرفية peripherals هي مجموعة الأجهزة الخارجية التي تتصل بحاسوبك، ويجب أن يكون للمعالج طريقة ما للتواصل مع هذه الأجهزة الطرفية لجعلها مفيدة، وتسمى قناة الاتصال بين المعالج والأجهزة الطرفية بالناقل Bus. المفاهيم الخاصة بنواقل الأجهزة الطرفية يتطلب الجهاز عمليات إدخال وإخراج ليكون مفيدًا، ويوجد هناك عدد من المفاهيم الشائعة المطلوبة للتواصل المفيد مع الأجهزة الطرفية التي سنستعرضها فيما يلي. المقاطعات Interrupts تسمح المقاطعة للجهاز بمقاطعة المعالج حرفيًا بما تعنيه الكلمة للإشارة إلى بعض المعلومات، فمثلًا تُنشَأ مقاطعة لتسليم حدث الضغط على مفتاح إلى نظام التشغيل عند الضغط عليه، إذ تسنِد تركيبة من نظام التشغيل وبيوس BIOS مقاطعةً لكل جهاز. ترتبط الأجهزة عمومًا بمتحكم المقاطعة القابل للبرمجة Programmable Interrupt Controller أو PIC اختصارًا، وهو شريحة منفصلة تُعَدّ جزءًا من اللوحة الأم التي تخزّن معلومات المقاطعة مؤقتًا وتنقلها إلى المعالج الرئيسي، كما يحتوي كل جهاز على خط مقاطعة فيزيائي بينه وبين أحد خطوط PIC التي يوفرها النظام، فإذا أراد الجهاز مقاطعة المعالج، فسيعدّل الجهد على هذا الخط. هناك وصف واسع جدًا لدور متحكم PIC وهو أنه يتلقى هذه المقاطعة ويحولها إلى رسالة ليستخدمها المعالج الرئيسي، كما يختلف هذا الإجراء حسب المعمارية، ولكن المبدأ العام هو أن يضبط نظام التشغيل جدول واصف المقاطعات Interrupt Descriptor Table الذي تربط فيه كل مقاطعة محتمَلة بعنوان شيفرة برمجية للانتقال إليها عند تلقي المقاطعة كما هو موضح في الشكل الآتي. كتابة معالج المقاطعة Interrupt Handler هو عمل مطور برنامج تشغيل الجهاز بالتزامن مع نظام التشغيل. نظرة عامة على معالجة المقاطعة: يرفع الجهاز المقاطعة إلى متحكم المقاطعة، حيث تمرِّر هذه المقاطعة المعلومات إلى المعالج. ينظر المعالج إلى جدول واصف مقاطعاته الذي يملؤه نظام التشغيل للعثور على الشيفرة البرمجية التي تعالج الخطأ. تقسّم معظم المشغّلات معالجة المقاطعات إلى نصفين سفلي وعلوي، إذ يتعرف النصف السفلي على المقاطعة ويضع الإجراءات في رتل للمعالجة ويعيد المعالج إلى ما كان يفعله سابقًا بسرعة، في حين سيُشغَّل النصف العلوي لاحقًا عندما تكون وحدة المعالجة المركزية متاحةً، وسينفّذ المعالجة الإضافية، كما يؤدي ذلك إلى وقف المقاطعة التي تعطل وحدة المعالجة المركزية بأكملها. حفظ الحالة بما أنّ المقاطعة يمكن أن تحدث في أيّ وقت، فيجب أن تتمكن من العودة إلى العملية الجارية عند الانتهاء من معالجة المقاطعة، كما أنّ مهمة نظام التشغيل هي التأكد من أنه يحفظ أيّ حالة State عند الدخول إلى معالج المقاطعة، أي يسجلها ويستعيدها عند العودة من معالج المقاطعة، وتكون بذلك المقاطعة واضحةً تمامًا في كل ما يحدث في ذلك الوقت بغض النظر عن الوقت الضائع. المقاطعات Interrupts والمصائد Traps والاستثناءات Exceptions ترتبط المقاطعة عمومًا بحدث خارجي من جهاز فيزيائي، ولكن تُعَدّ الآلية نفسها مفيدةً للتعامل مع عمليات النظام الداخلية، فإذا اكتشف المعالج مثلًا حالات مثل الوصول إلى ذاكرة غير صالحة أو محاولة القسمة على صفر أو تعليمات غير صالحة، فيمكنه داخليًا رفع استثناء ليعالجه نظام التشغيل، كما تُستخدَم هذه الآلية ليلتقط نظام التشغيل استدعاءات النظام ولتطبيق الذاكرة الوهمية virtual memory، في حين تبقى مبادئ مقاطعة الشيفرة البرمجية المُشغَّلة بطريقة غير متزامنة كما هي بالرغم من إنشائها داخليًا وليس من مصدر خارجي. أنواع المقاطعات هناك طريقتان رئيسيتان لإصدار إشارات إلى المقاطعات على الخط هما المستوى level والحافة edge المُنبَّهة، إذ تحدّد المقاطعات ذات المستوى المُنبَّه جهد خط المقاطعة الذي يُحتفَظ به مرتفعًا للإشارة إلى وجود مقاطعة معلَّقة، في يحن تكتشف المقاطعات ذات الحافة المُنبَّهة الانتقالات في الناقل عندما ينتقل جهد الخط من منخفض إلى مرتفع، ويكتشف متحكم المقاطعة PIC نبضة الموجة المربعة باستخدام المقاطعة ذات الحافة المنبَّهة عند إصدار الإشارة ورفع المقاطعة. يظهر الفرق عندما تشترك الأجهزة في خط مقاطعة، إذ سيكون خط المقاطعة مرتفعًا في نظام المقاطعة ذي المستوى المنبَّه حتى معالجة جميع الأجهزة التي رفعت المقاطعة وإلغاء تأكيد مقاطعتها، كما تشير النبضة الموجودة على الخط إلى متحكم المقاطعة PIC الذي تنشئه المقاطعة في نظام المقاطعة ذي الحافة المنبَّهة، وستصدر هذه النبضة إشارةً إلى نظام التشغيل لمعالجة المقاطعة في حالة ظهور نبضات أخرى على الخط المؤكَّد مسبقًا من جهاز آخر. تكمن مشكلة المقاطعات ذات المستوى المنبَّه في أنها يمكن أن تتطلب قدرًا كبيرًا من الوقت لمعالجة مقاطعة أحد الأجهزة، إذ يظل خط المقاطعة مرتفعًا أثناء هذا الوقت ولا يمكن تحديد ما إذا تسبّب أيّ جهاز آخر في حدوث مقاطعة على الخط، وهذا يعني أنه يمكن أن يكون هناك زمن تأخير كبير وغير متوقع في خدمة المقاطعات. يمكن ملاحظة المقاطعة طويلة الأمد ووضعها في رتل انتظار في المقاطعات ذات الحافة المُنبَّهة، ولكن لا يزال بإمكان الأجهزة الأخرى التي تشترك في الخط الانتقال -وبالتالي رفع المقاطعات- أثناء حدوث ذلك، ويؤدي ذلك إلى حدوث مشاكل جديدة، إذ يمكن تفويت أحد المقاطعات في حالة مقاطعة جهازين في الوقت نفسه أو يمكن أن يؤدي التشويش البيئي أو غيره إلى حدوث مقاطعة زائفة يجب تجاهلها. المقاطعات غير القابلة للتقنع أو الإخفاء Non-maskable Interrupts يجب أن يكون النظام قادرًا على إخفاء المقاطعات أو منعها في أوقات معينة، ويمكن وضع المقاطعات لتكون قيد الانتظار، لكن هناك صنف معيّن من المقاطعات يسمى المقاطعات غير القابلة للتقنّع أو الإخفاء Non-maskable Interrupts أو NMI اختصارًا، إذ تُعَدّ هذه المقاطعات استثناءً من هذه القاعدة مثل مقاطعة إعادة الضبط reset. يمكن أن تكون مقاطعات NMI مفيدةً لتطبيق أشياء مثل مراقبة النظام، حيث تُرفَع مقاطعة NMI دوريًا وتضبِط بعض الرايات التي يجب أن يقرّ بها نظام التشغيل، فإذا لم يظهر هذا الإقرار قبل مقاطعة NMI الدورية التالية، فيمكن عَدّ النظام أنه لا يحرز أيّ تقدم، كما يمكن استخدام مقاطعات NMI لتشخيص Profiling النظام، إذ يمكن رفع مقاطعات NMI الدورية واستخدامها لتقييم الشيفرة البرمجية التي يعمل بها المعالج حاليًا، مما يؤدي بمرور الوقت إلى إنشاء ملف تعريف للشيفرة البرمجية التي تعمل والحصول على رؤية مفيدة للغاية حول أداء النظام. فضاء الإدخال والإخراج IO يجب أن يتصل المعالج بالجهاز الطرفي عبر عمليات الإدخال والإخراج IO، ويُطلَق على الشكل الأكثر شيوعًا من عمليات IO عمليات الإدخال والإخراج المرتبطة بالذاكرة Memory Mapped IO، إذ ترتبط المسجلات الموجودة على الجهاز مع الذاكرة، وما عليك سوى القراءة أو الكتابة في عنوان محدد من الذاكرة للتواصل مع الجهاز. الوصول المباشر للذاكرة DMA بما أن سرعة الأجهزة أقل بكثير من سرعة المعالجات، فيجب أن يكون هناك طريقة ما لتجنب انتظار وحدة المعالجة المركزية للبيانات من الأجهزة. يُعَدّ الوصول المباشر للذاكرة Direct Memory Access -أو DMA اختصارًا- طريقةً لنقل البيانات مباشرةً بين الجهاز الطرفي وذاكرة RAM الخاصة بالنظام، ويمكن لمشغّل الجهاز إعداده لإجراء نقل باستخدام طريقة الوصول DMA من خلال إعطائه منطقةً من ذاكرة RAM لوضع بياناته فيها، ثم يمكنه بدء نقل DMA والسماح لوحدة المعالجة المركزية بمواصلة تنفيذ المهام الأخرى. سيرفع الجهاز المقاطعة بعد الانتهاء ويرسل لمشغّل الجهاز إشارةً باكتمال النقل، ثم ستكون البيانات القادمة من الجهاز مثل ملف من قرص صلب أو إطارات من بطاقة التقاط الفيديو موجودةً في الذاكرة وجاهزةً للاستخدام. نواقل أخرى تصل نواقل أخرى بين ناقل PCI والأجهزة الخارجية مثل ناقل USB الذي سنتعرف عليه فيما يلي. USB يُعَدّ جهاز USB من وجهة نظر نظام التشغيل أنه مجموعة من نقاط النهاية المجمَّعة معًا في واجهة ما، إذ يمكن أن تكون نقطة النهاية إما نقطة إدخال أو إخراج، بحيث تنقل نقطة النهاية البيانات باتجاه واحد فقط، كما يمكن أن تحتوي نقاط النهاية على عدد من الأنواع المختلفة هي: نقاط نهاية خاصة بعمليات التحكم Control End-points: مخصصة لإعداد الجهاز وغير ذلك. نقاط نهاية خاصة بالمقاطعات Interrupt End-points: تُستخدَم لنقل كميات صغيرة من البيانات، ولديها أولوية عليا. نقاط النهاية المجمَّعة Bulk End-points: تنقل كميات كبيرة من البيانات ولكنها لا تحصل على قيود زمنية مضمونة. عمليات النقل المتزامنة Isochronous Transfers: هي عمليات نقل ذات أولوية عالية في الوقت الحقيقي، ولكن إذا جرى تفويتها، فلن يعاد تجربتها، وتُستخدَم لبيانات البث مثل الفيديو أو الصوت حيث لا توجد فائدة من إرسال البيانات مرةً أخرى. يمكن أن يكون هناك العديد من الواجهات المكونة من نقاط نهاية متعددة، وتُجمَّع الواجهات ضمن إعدادات Configurations، ولكن معظم الأجهزة لها إعداد واحد فقط. نظرة عامة على متحكم UCHI (مأخوذة من توثيق إنتل Intel) يوضح الشكل السابق نظرة عامة على واجهة متحكم المضيف العامة Universal Host Controller Interface أو UHCI اختصارًا، إذ ويوفر نظرةً عامةً حول كيفية نقل بيانات USB خارج النظام عن طريق مجموعة من العتاد والبرمجيات، كما تضبط البرمجيات قالب بيانات بتنسيق محدد لمتحكم المضيف لقراءته وإرساله عبر ناقل USB. يحتوي المتحكم بدءًا من أعلى يسار الشكل السابق على مسجل إطارات مع عدّاد يُزاد دوريًا في كل ميلي ثانية، إذ تُستخدَم هذه القيمة للفهرسة ضمن قائمة إطارات تنشئها البرمجيات، ويؤشّر كل إدخال في هذا الجدول إلى رتل واصفات النقل Transfer Descriptors، كما تضبط البرمجيات هذه البيانات في الذاكرة ويقرؤها المتحكم المضيف الذي يُعَدّ شريحةً منفصلةً تشغّل ناقل USB، ويجب أن تجدول البرمجيات أرتال العمل بحيث يُمنَح 90% من وقت الإطار للبيانات المتزامنة ويُمنَح 10% المتبقية لبيانات المقاطعة والتحكم والبيانات المُجمَّعة. تعني الطريقة التي تُربَط بها البيانات أنّ واصفات النقل للبيانات المتزامنة ترتبط بمؤشر إطار معيّن واحد فقط -أي فترة زمنية معينة واحدة فقط- ثم ستُهمَل، لكن تُوضَع جميع بيانات المقاطعة والتحكم والبيانات المُجمَّعة ضمن رتل انتظار بعد البيانات المتزامنة، وبالتالي إذا لم تُرسَل في إطار واحد -أو فترة زمنية واحدة- فسيجري ذلك في المرة التالية. تتواصل طبقات USB عبر كتل طلبات USB أو URB اختصارًا، إذ تحتوي كتل URB على معلومات حول نقطة النهاية التي يرتبط بها هذا الطلب والبيانات وأي معلومات أو سمات ذات صلة ودالة رد نداء call-back function تُستدعَى عند اكتمال كتلة URB، كما ترسِل مشغّلات USB كتل URB بتنسيق ثابت إلى مركز USB الذي يديرها بالتنسيق مع متحكم مضيف USB على النحو الوارد أعلاه، وتُرسَل بياناتك إلى جهاز USB عبر مركز USB، ثم تُشغَّل دالة رد النداء. ترجمة -وبتصرُّف- للقسم Peripherals and buses من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: أنظمة المعالجات في معمارية الحاسوب المقال السابق: نظرة عميقة على تسلسل الذواكر الهرمي والذاكرة المخبئية في معمارية الحاسوب المدخل الشامل لتعلم علوم الحاسوب فهم عملية التخبئة (Caching) في معمارية الحاسوب
-
يمكن لوحدة المعالجة المركزية جلب التعليمات والبيانات مباشرةً من الذاكرة المخبئية Cache Memory الموجودة على شريحة المعالج فقط، لذا يجب تحميل الذاكرة المخبئية من ذاكرة النظام الرئيسية، أي ذاكرة الوصول العشوائي Random Access Memory -أو RAM اختصارًا-، ولكن تحتفظ الذاكرة RAM بمحتوياتها فقط عند الوصل بمصدر طاقة، لذلك يجب تخزينها على مساحة تخزين دائمة وغير متطايرة. تسلسل الذواكر الهرمي نطلق على طبقات الذواكر التالية اسم تسلسل الذواكر الهرمي Memory Hierarchy: 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; } السرعة الذاكرة الوصف الأسرع الذاكرة المخبئية Cache الذاكرة المخبئية هي ذاكرة مضمَّنة في وحدة المعالجة المركزية، وهي ذاكرة سريعة جدًا وتستغرق دورة واحدة فقط للوصول إليها، ولكن هناك حد لحجمها لأنها مُدمَجة مباشرةً في وحدة المعالجة المركزية، كما توجد هناك عدة مستويات فرعية من الذاكرة المخبئية تسمى L1 و L2 و L3 بسرعات متزايدة قليلًا عن بعضها البعض. الذاكرة RAM يجب أن تأتي جميع التعليمات وعناوين التخزين الخاصة بالمعالج من الذاكرة RAM، وتستغرق وحدة المعالجة المركزية بعض الوقت للوصول إلى الذاكرة RAM يسمى زمن التأخير Latency بالرغم من أنها ذاكرة سريعة جدًا، كما تُخزَّن الذاكرة RAM في شرائح منفصلة ومخصصة متصلة باللوحة الأم، مما يعني أنها أكبر بكثير من الذاكرة المخبئية. الأبطأ القرص الصلب Disk جميعنا على دراية بالبرامج التي تصلنا على قرص مرن floppy disk أو قرص مضغوط، ونعلم كيفية حفظ ملفاتنا على القرص الصلب، ونعلم الوقت الطويل الذي يمكن أن يستغرقه البرنامج للتحميل من القرص الصلب، إذ يعني وجود آليات فيزيائية مثل الأقراص الدوارة والرؤوس المتحركة أن الأقراص الصلبة هي أبطأ وسيلة من وسائل التخزين، ولكنها أكبرها حجمًا. النقطة المهمة التي يجب معرفتها حول تسلسل الذواكر الهرمي هي المقايضات بين السرعة والحجم على حساب بعضهما البعض، فكلما كانت الذاكرة أسرع، كان حجمها أصغر. سبب فعالية الذواكر المخبئية هو أنّ شيفرة الحاسوب البرمجية تعرض شكلَين من أشكال المحلية Locality هما: تشير المحلية المكانية Spatial Locality إلى احتمالية الوصول إلى البيانات الموجودة ضمن الكتل مع بعضها بعضًا. تشير المحلية الزمانية Temporal Locality إلى أن البيانات المستخدَمة مؤخرًا يُحتمَل أن تُستخدَم مرة أخرى قريبًا. يعني ذلك أنه يمكن الاستفادة من تنفيذ أكبر قدر ممكن من عمليات الوصول السريعة إلى الذاكرة أي المحلية الزمانية وتخزين كتل صغيرة من المعلومات ذات الصلة أي المحلية المكانية. الذاكرة المخبئية تُعَدّ الذاكرة المخبئية أحد أهم عناصر معمارية وحدة المعالجة المركزية، إذ يجب على المطورين فهم كيفية عمل الذاكرة المخبئية في أنظمتهم لكتابة شيفرة برمجية فعالة، كما تُعَدّ نسخةً سريعةً جدًا من ذاكرة النظام الرئيسية الأبطأ، وهي أصغر بكثير من الذواكر الرئيسية لأنها مضمنة داخل شريحة المعالج جنبًا إلى جنب مع المسجلات ومنطق المعالج، وهناك حدود اقتصادية ومادية لأقصى حجم لها. تجد الشركات المصنعة مزيدًا من الطرق لحشر مزيد من الترانزستورات على الشريحة، مما يؤدي إلى زيادة أحجام الذواكر المخبئية بصورة كبيرة، ولكن يُقدَّر حجم حتى أكبر الذواكر المخبئية بعشرات الميجابايتات بعكس حجم الذاكرة الرئيسية المقدَّر بالجيجابايتات أو حجم القرص الصلب المقدَّر بالتيرابايتات. تتكون الذاكرة المخبئية من قطع صغيرة تعكس محتوى أجزاء من الذاكرة الرئيسية، إذ يُطلَق على حجم هذه القطع بحجم الخط Line Size، ويساوي تقريبًا 32 أو 64 بايتًا، ومن الشائع التحدث عن حجم الخط أو خط الذاكرة المخبئية عند الحديث عن الذاكرة المخبئية، والذي يشير إلى قطعة واحدة تعكس محتوى قطعة من الذاكرة الرئيسية، كما يمكن للذاكرة المخبئية فقط تحميل وتخزين الذاكرة بأحجام مضاعفة من خط الذاكرة المخبئية. تحتوي الذواكر المخبئية على تسلسلها الهرمي الخاص، ويطلق عليه عادةً L1 و L2 و L3، إذ تُعَدّ الذاكرة المخبئية L1 هي الأسرع والأصغر و L2 أكبر وأبطأ منها و L3 هي الأكبر والأبطأ، كما تُقسَم الذاكرة المخبئية L1 إلى ذواكر مخبئية خاصة بالتعليمات وأخرى بالبيانات، وتُعرف باسم معمارية هارفارد Harvard Architecture بعد أن قدمها حاسوب Harvard Mark-1 القائم على المُرحّلات Relay. تساعد الذواكر المخبئية المقسمة على تقليل الاختناقات في خطوط الأنابيب، حيث تشير مراحل خط الأنابيب السابقة إلى تعليمات الذاكرة المخبئية وتشير المراحل اللاحقة إلى بيانات الذاكرة المخبئية، كما يسمح توفير ذاكرة مخبئية منفصلة للتعليمات بإجراء تطبيقات بديلة تستفيد من طبيعة مجرى التعليمات بغض النظر عن فائدة تقليل التنازع على مورد مشترك، إذ تكون الذاكرة المخبئية الخاصة بالتعليمات للقراءة فقط، أي لا تحتاج إلى ميزات باهظة الثمن على الشريحة مثل تعدد المنافذ، ولا تحتاج إلى التعامل مع عمليات قراءة الكتل الفرعية لأن مجرى التعليمات يستخدِم عمومًا عمليات وصول ذات أحجام أكثر انتظامًا. ترابط الذاكرة المخبئية: يمكن أن يجد خط ذاكرة مخبئية معيّن مكانًا صالحًا في أحد الإدخالات المظللة. يطلب المعالج باستمرار من الذاكرة المخبئية أثناء التشغيل العادي التحققَ من تخزين عنوان معيّن في الذاكرة المخبئية، لذلك تحتاج الذاكرة المخبئية لطريقة ما لمعرفة ما إذا كان لديها خط صالح أم لا، فإذا أمكن تخزين عنوان معيّن في أيّ مكان ضمن الذاكرة المخبئية، فيجب البحث في كل خط من الذاكرة المخبئية في كل مرة يُنشَأ فيها مرجع لتحديد وصول صحيح أو خاطئ، كما يمكن الاستمرار في البحث السريع من خلال إجرائه على التوازي في عتاد الذاكرة المخبئية، ولكن يكون البحث في كل إدخال مكلفًا للغاية بحيث يتعذر تطبيقه في ذاكرة مخبئية ذات حجم معقول، لذا يمكن جعل الذاكرة المخبئية أبسط من خلال فرض قيود على مكان وجود عنوان معيّن. يُعَدّ ذلك مقايضةً، فالذاكرة المخبئية أصغر بكثير من ذاكرة النظام، لذا يجب أن تحمل بعض العناوين أسماء بديلة Alias للعناوين الأخرى، فإذا جرى تحديث عنوانَين يحملان أسماء بديلةً لبعضهما البعض باستمرار، فسيقال أنهما يتنازعان على خط الذاكرة المخبئية، كما يمكننا تصنيف الذواكر المخبئية إلى ثلاثة أنواع عامة كما هو موضح في الشكل السابق وهي: الذواكر المخبئية المربوطة مباشرةً Direct mapped Caches التي تسمح لخط الذاكرة المخبئية بالتواجد فقط في إدخال واحد في الذاكرة المخبئية، ويُعَدّ ذلك أبسط تطبيق في العتاد، ولكن -كما هو موضح في الشكل السابق- لا توجد إمكانية لتجنب استخدام الأسماء البديلة لأن العنوانَين المظلَّلين يجب عليهما التشارك في خط الذاكرة المخبئية نفسه. الذواكر المخبئية الترابطية بالكامل Fully Associative Caches التي تسمح بوجود خط الذاكرة المخبئية في أيّ إدخال منها، مما يؤدي إلى تجنّب مشكلة الأسماء البديلة، لأن أيّ إدخال يكون متاحًا للاستخدام، لكن يُعَدّ تطبيق ذلك في العتاد مكلفًا للغاية لأنه يجب البحث عن كل موقع محتمَل في الوقت نفسه لتحديد ما إذا كانت القيمة موجودةً في الذاكرة المخبئية. الذواكر المخبئية التجميعية Set Associative Caches التي تُعَدّ عبارةً عن مزيج من الذواكر المخبئية المربوطة مباشرةً والذواكر المخبئية الترابطية بالكامل، وتسمح بوجود قيمة معينة للذاكرة المخبئية في بعض المجموعات الفرعية من الخطوط الموجودة ضمن هذه الذاكرة المخبئية، كما تُقسَم الذاكرة المخبئية إلى مناطق تسمَّى طرقًا Ways، ويمكن وجود عنوان معيّن في أيّ طريق، وبالتالي ستسمح الذاكرة المخبئية التجميعية المؤلفة من مجموعة من الطرق عددها n لخط الذاكرة المخبئية بالتواجد ضمن مجموعة الإدخالات التي عددها يساوي باقي قسمة مجموعة الكتل الإجمالية ذات الحجم المحدد على n، ويظهِر الشكل السابق عينةً من ذاكرة تجميعية مؤلفة من 8 عناصر و 4 طرق، إذ يكون للعنوانَين أربعة مواقع محتملة، مما يعني أنه يجب البحث عن نصف الذاكرة المخبئية فقط في كل عملية بحث، وكلما زاد عدد الطرق، زادت المواقع الممكنة ونقصت الأسماء البديلة، مما يؤدي إلى أداء أفضل. يجب أن يتخلص المعالِج من الخط بمجرد امتلاء الذاكرة المخبئية لإفساح المجال لخط جديد، وهناك العديد من الخوارزميات التي يمكن للمعالج من خلالها اختيار الخط الذي سيتخلص منه مثل خوارزمية الأقل استخدامًا مؤخرًا Least Recently Used -أو LRU اختصارًا- والتي تُعَدّ خوارزميةً يجري فيها التخلص من أقدم خط غير مستخدَم لإفساح المجال للخط الجديد. ليس هناك داع لضمان التوافق مع الذاكرة الرئيسية عندما تكون البيانات للقراءة فقط من الذاكرة المخبئية، لكن يحتاج المعالج لاتخاذ بعض القرارات حول كيفية تحديث الذاكرة الرئيسية الأساسية عندما يبدأ في الكتابة في خطوط الذاكرة المخبئية، إذ ستكتب طريقة التخزين الخاصة بالذاكرة المخبئية التي تُسمَّى Write-through Cache التغييرات مباشرةً في ذاكرة النظام الرئيسية عندما يحدّث المعالج الذاكرة المخبئية، ويُعَدّ ذلك أبطأ لأن عملية الكتابة في الذاكرة الرئيسية أبطأ، في حين تؤخر طريقة التخزين الخاصة بالذاكرة المخبئية التي تُسمَّى Write-back Cache كتابةَ التغييرات على الذاكرة RAM حتى الضرورة القصوى، والميزة الواضحة لذلك هي أنّ الوصول إلى الذاكرة الرئيسية مطلوب عند كتابة إدخالات الذاكرة المخبئية. يُشار إلى خطوط الذاكرة المخبئية المكتوبة دون وضعها في الذاكرة على أنها متسخة Dirty، فعيبها هو أنه يمكن أن يتطلب الأمر وصولَين إلى الذاكرة أحدهما لكتابة بيانات الذاكرة الرئيسية المتسخة والآخر لتحميل البيانات الجديدة عند التخلص من إدخال معيّن من الذاكرة المخبئية. إذا كان الإدخال موجودًا في كل من الذاكرة المخبئية ذات المستوى الأعلى والمستوى الأدنى في الوقت نفسه، فإننا نسمّي الذاكرة المخبئية ذات المستوى الأعلى بالشاملة Inclusive. بينما إذا أزالت الذاكرة المخبئية ذات المستوى الأعلى التي تحتوي على خط معيّن إمكانيةَ احتواء ذاكرة مخبئية ذات مستوى أقل على هذا الخط، فإننا نقول أنها حصرية Exclusive وسنناقش ذلك لاحقًا. عنونة الذاكرة المخبئية لم نناقش حتى الآن كيف تقرر الذاكرة المخبئية ما إذا كان عنوان معيّن موجودًا في الذاكرة المخبئية أم لا، إذ يجب أن تحتفظ الذواكر المخبئية بمجلد للبيانات الموجودة حاليًا في خطوط الذاكرة المخبئية، ويمكن وضع مجلد وبيانات الذاكرة المخبئية على المعالج معًا، ولكن يمكن أن يكونا منفصلَين أيضًا كما في حالة المعالج POWER5 الذي يحتوي على مجلد ذاكرة L3 على المعالج، ولكن يتطلب الوصول إلى البيانات اجتياز ناقل L3 للوصول إلى ذاكرة خارجية ليست على المعالج، ويمكن أن يسهّل هذا الترتيب معالجة عمليات الوصول الصحيحة أو الخاطئة بصورة أسرع دون التكاليف الأخرى للاحتفاظ بالذاكرة المخبئية بالكامل على المعالج. وسوم الذاكرة المخبئية Cache Tags: يجب التحقق من الوسوم على التوازي للحفاظ على وقت الاستجابة منخفضًا، إذ يتطلب المزيدُ من بتات الوسوم (أي ارتباطات مجموعات أقل) عتادًا أكثر تعقيدًا لتحقيق ذلك. بينما تعني ارتباطاتُ المجموعات الأكثر وسومًا أقل، ولكن يحتاج المعالج الآن إلى عتاد لمضاعفة خرج العديد من المجموعات التي يمكن أن تضيف زمن تأخير أيضًا. يمكن تحديد ما إذا كان العنوان موجودًا في الذاكرة المخبئية بسرعة من خلال فصله إلى ثلاثة أجزاء هي الوسم Tag والفهرس Index والإزاحة Offset. تعتمد بتات الإزاحة على حجم خط الذاكرة المخبئية، إذ يمكن استخدام خط بحجم 32 بايت مثلًا آخر 5 بتات أي 25 من العنوان بوصفه إزاحةً في الخط، ويُعَدّ الفهرس خط ذاكرة مخبئية معيّن يمكن أن يتواجد فيه الإدخال، فلنفترض أنه لدينا ذاكرة مخبئية تحتوي على 256 إدخالًا مثلًا، فإذا كانت هذه الذاكرة هي ذاكرة مخبئية مربوطة مباشرةً، فيمكن أن تكون البيانات موجودة في خط واحد محتمَل فقط، لذا تصف 8 بتات التالية (28) بعد الإزاحة الخط المراد التحقق منه بين 0 و 255. لنفترض الآن أنّ الذاكرة المخبئية المكونة من 256 عنصرًا مقسمة إلى طريقين، وهذا يعني أنّ هناك مجموعتين مؤلفتين من 128 خط، ويمكن أن يقع العنوان المحدد في أيّ من هاتين المجموعتين، وبالتالي فإن المطلوب هو 7 بتات فقط على أساس فهرس للإزاحة في الطرق المؤلفة من 128 إدخالًا، كما نخفّض عدد البتات المطلوبة على أساس فهرس لأن كل طريق يصبح أصغر عندما نزيد عدد الطرق بالنسبة إلى حجم ذاكرة مخبئية معيّن. لا يزال مجلد الذاكرة المخبئية بحاجة إلى التحقق مما إذا كان العنوان المخزن في الذاكرة المخبئية هو العنوان الذي يريده، وبالتالي فإن البتات المتبقية من العنوان هي بتات الوسوم التي يتحقق مجلد الذاكرة المخبئية منها مقابل بتات وسم العنوان الواردة لتحديد ما إذا كان هناك عملية وصول صحيحة أم لا، وهذه العلاقة موضحة في الصورة السابقة. إذا كان هناك طرق متعددة، فيجب إجراء هذا التحقق على التوازي في كل طريق، ثم تُمرَر النتيجة بعد ذلك إلى معدد إرسال Multiplexor ينتج عنه نتيجة وصول صحيحة hit أو خاطئة miss، وكلما كانت الذاكرة المخبئية أكثر ارتباطًا، قل عدد البتات المطلوبة للفهرس وزاد عدد البتات المطلوبة للوسم، حتى الوصول إلى أقصى حد للذاكرة المخبئية الترابطية بالكامل حيث لا تُستخدَم بتات كبتات للفهرس، كما تُعَدّ المطابقة على التوازي لبتات الوسوم مكونًا باهظًا لتصميم الذاكرة المخبئية وهي عمومًا العامل المحدّد لعدد الخطوط -أي حجمها- التي يمكن أن تنمو إليها الذاكرة المخبئية. ترجمة -وبتصرُّف- للقسم Memory من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: الأجهزة الطرفية Peripherals ونواقلها Buses في معمارية الحاسوب المقال السابق: تعرف على وحدة المعالجة المركزية وعملياتها في معمارية الحاسوب الذاكرة وأنواعها فهم عملية التخبئة (Caching) في معمارية الحاسوب المدخل الشامل لتعلم علوم الحاسوب
-
تنفّذ وحدة المعالجة المركزية التعليمات على القيم الموجودة في المسجّلات Registers، إذ يوضّح المثال الآتي أولًا ضبط R1 على القيمة 100 وتحميل القيمة من موقع الذاكرة 0x100 إلى R2 وجمع القيمتين، ثم وضع النتيجة في R3، وأخيرًا تخزين القيمة الجديدة 110 في R4. يتكون الحاسوب من وحدة معالجة مركزية Central Processing Unit -أو CPU اختصارًا- متصلة بالذاكرة، إذ توضّح الصورة السابقة المبدأ العام لجميع عمليات الحاسوب، كما تنفّذ وحدة المعالجة المركزية التعليمات المقروءة من الذاكرة، وهناك نوعان من هذه التعليمات هما: التعليمات التي تحمّل القيم من الذاكرة إلى المسجلات وتخزّن القيم من المسجلات إلى الذاكرة. التعليمات التي تُشغَّل على القيم المخزَّنة في المسجّلات مثل جمع أو طرح أو ضرب أو قسمة قيمتين موجودتين في مسجلين، أو إجراء العمليات الثنائية and و or و xor وغيرها، أو إجراء عمليات حسابية أخرى، مثل الجذر التربيعي و sin و cos و tan وغيرها. لذا نجمع في مثالنا ببساطة العدد 100 مع قيمة مُخزَّنة في الذاكرة ونخزّن النتيجة الجديدة في الذاكرة. التفريع Branching يُعَدّ التفريع عمليةً مهمةً لوحدة المعالجة المركزية، وذلك بغض النظر عن عمليتي التحميل أو التخزين، إذ تحتفظ وحدة المعالجة المركزية داخليًا بسجل للتعليمة التالية التي ستنفَّذ في مؤشر التعليمات Instruction Pointer، بحيث يُزاد هذا المؤشر ليؤشّر إلى التعليمة التالية تسلسليًا، إذ ستتحقق التعليمة الفرعية مما إذا كان لمسجل معيّن القيمة صفر، أو تتحقق من وجود من ضبط راية flag ما. فإذا كان الأمر كذلك، فسيُعدَّل المؤشر ليؤشّر إلى عنوان مختلف، وبالتالي ستكون التعليمة التالية للتنفيذ من جزء مختلف من البرنامج، وهذه هي الطريقة التي تعمل بها الحلقات وتعليمات القرار. يمكن مثلًا تنفيذ التعليمة if (x==0) من خلال إيجاد ناتج تطبيق عملية or على اثنين من المسجلات، أحدهما يحمل القيمة x والآخر يحمل القيمة صفر، فإذا كانت النتيجة صفرًا، فستكون المقارنة صحيحة، أي أنّ جميع بتات x أصفار ويجب تنفيذ جسم التعليمة، وإلّا فستتجاوز التعليمة الفرعية هذه الشيفرة. الدورات جميعنا على دراية بسرعة الحاسوب المعطاة بالميجاهرتز أو الجيجاهرتز التي تقابل ملايين أو آلاف الملايين من الدورات في الثانية، ويسمى ذلك بسرعة الساعة Clock Speed لأنها السرعة التي تنبض بها ساعة الحاسوب الداخلية، إذ تُستخدَم النبضات ضمن المعالج لإبقائه متزامنًا داخليًا، ويمكن البدء بعملية أخرى في كل لحظة أو نبضة. جلب التعليمة وفك تشفيرها وتنفيذها وتخزين نتيجتها يتكون تنفيذ تعليمة واحدة من دورة معينة من الأحداث، وهي الجلب وفك التشفير والتنفيذ والتخزين، إذ يجب على وحدة المعالجة المركزية تطبيق الخطوات التالية لتنفيذ تعليمة add السابقة مثلًا: الجلب Fetch: الحصول على التعليمات من الذاكرة إلى المعالج. فك التشفير Decode: فك تشفير ما يجب أن تفعله داخليًا، أي الجمع في هذه الحالة. التنفيذ Execute: أخذ القيم من المسجلات وجمعها. التخزين Store: تخزين النتيجة في مسجل آخر، كما يمكن رؤية مصطلح انتهاء Retiring التعليمة. نظرة داخلية إلى وحدة المعالجة المركزية تحتوي وحدة المعالجة المركزية داخليًا على العديد من المكونات الفرعية المختلفة التي تطبّق كلًا من الخطوات المذكورة سابقًا، كما يمكن أن تحدث جميعها بصورة مستقلة عن بعضها البعض، وهي مشابهة لخط الإنتاج في المصانع، حيث توجد العديد من المحطات ولكل خطوة مَهمة معينة لأدائها، ثم يمكنه تمرير النتائج إلى المحطة التالية وأخذ مدخلات جديدة للعمل عليها. تتكون وحدة المعالجة المركزية من العديد من المكونات الفرعية المختلفة، وتطبّق كل منها مهمةً مُخصَّصةً. توضِّح الصورة السابقة مخططًا بسيطًا لبعض الأجزاء الرئيسية لوحدة المعالجة المركزية الحديثة، حيث يمكنك رؤية التعليمات تأتي ثم يفك المعالج تشفيرها؛ كما تحتوي وحدة المعالجة المركزية على نوعين رئيسيين من المسجّلات، هما مسجلات العمليات الحسابية الخاصة بالأعداد الصحيحة ومسجلات العمليات الحسابية الخاصة بالأعداد العشرية. تُعَدّ الأعداد العشرية Floating Point طريقةً لتمثيل الأعداد ذات المنزلة العشرية بصيغة ثنائية، ويجري التعامل معها بطريقة مختلفة ضمن وحدة المعالجة المركزية، كما تُعَدّ المسجلات MMX (توسع الوسائط المتعددة Multimedia Extension) و SSE (مجرى بيانات متعددة لتعليمة مفردة Streaming Single Instruction Multiple Data) أو Altivec مسجلات مماثلة للمسجلات الخاصة الأعداد العشرية. يُعَدّ ملف المسجلات Register File اسمًا يجمع جميع المسجلات الموجودة ضمن وحدة المعالجة المركزية، وتوجد ضمنه أجزاء وحدة المعالجة المركزية التي تنفّذ كل العمل، إذ تحمّل المعالجات أو تخزّن قيمةً في مسجل أو من مسجل إلى الذاكرة، أو تنفّذ بعض العمليات على القيم الموجودة في المسجلات كما قلنا سابقًا. تُعَدّ وحدة الحساب والمنطق Arithmetic Logic Unit -أو ALU اختصارًا- قلب عمليات وحدة المعالجة المركزية، إذ تأخذ القيم من المسجلات وتنفّذ أيًا من العمليات المتعددة التي تستطيع وحدة المعالجة المركزية تنفيذها، كما تحتوي جميع المعالجات الحديثة على عدد من وحدات ALU، بحيث يمكن لكل منها العمل بصورة مستقلة، وتحتوي المعالجات مثل المعالج بنتيوم Pentium على وحدات ALU سريعة ووحدات ALU بطيئة، إذ تكون الوحدات السريعة أصغر حجمًا، لذا يمكنك استخدام المزيد منها على وحدة المعالجة المركزية، ولكن يمكنك تنفيذ العمليات الأكثر شيوعًا فقط؛ أما وحدات ALU البطيئة، فيمكنها تنفيذ جميع العمليات ولكنها تكون أكبر حجمًا. تعالِج وحدة إنشاء العناوين Address Generation Unit -أو AGU اختصارًا- التواصل مع الذاكرة المخبئية Cache Memory والذاكرة الرئيسية لجلب القيم إلى المسجلات لكي تعمل وحدة ALU، ثم استعادة القيم من المسجلات إلى الذاكرة الرئيسية، كما تحتوي مسجلات الأعداد العشرية على المفاهيم نفسها، ولكنها تستخدِم مصطلحات مختلفةً قليلًا لمكوناتها. استخدام خط الأنابيب تُعَدّ عملية وحدة ALU التي تجمع قيم المسجلات منفصلةً تمامًا عن عملية وحدة AGU التي تكتب القيم في الذاكرة، إذ لا يوجد سبب يمنع وحدة المعالجة المركزية من تطبيق هاتين العمليتين معًا في وقت واحد، كما توجد عدة وحدات ALU في النظام والتي يمكن أن تعمل كل منها على تعليمات منفصلة. يمكن لوحدة المعالجة المركزية تنفيذ بعض عمليات الأعداد العشرية باستخدام منطق الأعداد العشرية أثناء تشغيل تعليمات الأعداد الصحيحة أيضًا، إذ تسمى هذه العملية باستخدام خط الأنابيب Pipelining، ويشار إلى المعالج الذي يمكنه تطبيق هذه العملية بأن له معمارية عددية فائقة Superscalar Architecture، إذ تُعَدّ جميع المعالجات الحديثة معالجات عدديةً فائقةً، ويحتوي أيّ معالج حديث على أكثر من أربع مراحل يمكنه استخدامها ضمن خط أنابيب، وكلما زاد عدد المراحل التي يمكن تنفيذها في الوقت نفسه، زاد عمق خط الأنابيب. يمكن تشبيه خط الأنابيب بأنبوب مملوء بكرات زجاجية، باستثناء أن هذه الكرات هي تعليمات وحدة المعالجة المركزية، إذ ستضع الكرات الزجاجية في نهاية واحدة، بحيث تضعها واحدةً تلو الأخرى -أي كرة لكل نبضة ساعة- حتى تملأ الأنبوب، وستنتقل كل كرة زجاجية -أو تعليمة- تدفعها للداخل إلى الموضع التالي بمجرد أن يمتلئ الأنبوب مع سقوط كرة في النهاية التي تمثّل النتيجة. تؤدي التعليمات الفرعية إلى إحداث فوضى في هذا النموذج، إذ يمكن أن تتسبب أو لا تتسبب في بدء التنفيذ من مكان مختلف، فإذا أردت استخدام خط الأنابيب، فسيتعين عليك تخمين الاتجاه الذي ستتجه فيه التعليمة الفرعية حتى تعرف التعليمات التي يجب إحضارها إلى خط الأنابيب، فإذا خمّنت وحدة المعالجة المركزية ذلك بصورة صحيحة، فسيسير كل شيء على ما يرام، إذ تستخدِم المعالجات مثل معالج بنتيوم ذاكرة تخزين مؤقت Trace Cache لتعقب مسار التعليمات الفرعية، حيث يمكن في كثير من الأحيان أن تخمّن الطريق الذي ستذهب إليه التعليمة الفرعية من خلال تذكر نتائجها السابقة، فإذا تذّكرتَ نتيجة التعليمة الفرعية الأخيرة في حلقة تتكرر 100 مرة مثلًا، فستكون على صواب 99 مرة، لأن المرة الأخيرة فقط ستستمر في البرنامج فعليًا؛ بينما إذا جرى تخمين المعالج بطريقة غير صحيحة، فهذا يعني أنّ المعالج قد أهدر كثيرًا من الوقت ويجب عليه مسح خط الأنابيب والبدء من جديد. يشار إلى هذه العملية عادةً باسم تفريغ خط الأنابيب Pipeline Flush وهي مماثلة للحاجة إلى التوقف وإفراغ كل الكرات من الأنبوب، كما تتكوّن عملية تخمين التعليمة الفرعية Branch Prediction من تفريغ خط الأنابيب وأخذ التخمين أو عدم الأخذ به وفتحات تأخير التعليمة الفرعية branch delay slots. إعادة الترتيب إذا كانت وحدة المعالجة المركزية هي الأنبوب، فسنكون لك الحرية في إعادة ترتيب الكرات ضمنه طالما أنها تخرج من نهايته بالترتيب نفسه الذي وضعتَها فيه، إذ نسمي ذلك بترتيب البرنامج Program Order لأنه ترتيب التعليمات المُعطَى في البرنامج الحاسوبي، كما يمكنك الاطلاع على المثال التالي الذي يمثل إعادة ترتيب المخزن المؤقت Buffer: 1: r3 = r1 * r2 2: r4 = r2 + r3 3: r7 = r5 * r6 4: r8 = r1 + r7 افترض مجرى التعليمات الموضح سابقًا، إذ يجب على التعليمة 2 انتظار اكتمال التعليمة 1 قبل أن تبدأ، وهذا يعني أنّ خط الأنابيب يجب عليه التوقف أثناء انتظار القيمة المراد حسابها، كما تعتمد التعليمتان 3 و 4 على قيمة r7، ولكن التعليمتان 2 و 3 لا تعتمدان على بعضهما البعض أبدًا، وهذا يعني أنهما يعملان في مسجلات منفصلة تمامًا، فإذا بدّلنا بين التعليمتين 2 و 3، فسنحصل على ترتيب أفضل لخط الأنابيب، إذ يمكن أن ينفّذ المعالج عملًا مفيدًا بدلًا من انتظار اكتمال خط الأنابيب للحصول على نتيجة التعليمة السابقة. يمكن أن تتطلب التعليمات بعض الأمان حول كيفية ترتيب العمليات عند كتابة شيفرة منخفضة المستوى، إذ نطلق على هذا المتطلب دلالات الذاكرة Memory Semantics، فإذا أردت اكتساب الدلالات Acquire Semantics، فهذا يعني أنه يجب عليك التأكد من إكمال نتائج جميع التعليمات السابقة للتعليمة الحالية، وإذا أردت تحرير الدلالات Release Semantics، فهذا يعني أنّ جميع التعليمات بعد هذه التعليمة يجب أن ترى النتيجة الحالية. توجد دلالات أخرى أكثر صرامة وهي حاجز الذاكرة Memory Barrier أو سور الذاكرة Memory Fence الذي يتطلب أن تكون العمليات مرتبطةً بالذاكرة قبل المتابعة، كما يضمن المعالج هذه الدلالات في بعض المعماريات، بينما يجب أن تحددها بصورة صريحة في المعماريات الأخرى، ولا يحتاج معظم المبرمجين إلى القلق بشأنها على الرغم من أنك قد تصادفها. معمارية CISC ومعمارية RISC يمكن تقسيم معماريات الحاسوب إلى معمارية حاسوب مجموعة التعليمات المعقدة Complex Instruction Set Computer -أو CISC اختصارًا- ومعمارية حاسوب مجموعة التعليمات المُخفَّضة Reduced Instruction Set Computer أو RISC اختصارًا. لاحظ أننا في المثال الأول من مقالنا حمّلنا القيم صراحةً في المسجلات وأجرينا عملية الجمع، ثم خزّنا القيمة الناتجة المحفوظة في مسجل آخر في الذاكرة، إذ يُعَدّ ذلك مثالًا عن نهج RISC للحوسبة الذي يشمل تنفيذ العمليات على القيم الموجودة في المسجلات وتحميل القيم وتخزينها بصورة صريحة من الذاكرة وإليها، كما يمكن أن يكون نهج CISC مجرد تعليمات مفردة تأخذ قيمًا من الذاكرة وتنفذ عملية الجمع داخليًا ثم تكتب النتيجة، وهذا يعني أنّ التعليمات يمكن أن تستغرق عدة دورات، ولكن كلا النهجين يحققان في النهاية الهدف نفسه. تُعَدّ جميع المعماريات الحديثة معماريات RISC حتى معمارية إنتل بنتيوم Intel Pentium الأكثر شيوعًا والتي تهدم التعليمات داخليًا إلى تعليمات فرعية بأسلوب RISC داخل الشريحة قبل التنفيذ، بالرغم من وجود مجموعة تعليمات مصنَّفة على أنها CISC، وهناك عدة أسباب لذلك وهي: تجعل معمارية RISC البرمجة بلغة التجميع Assembly أكثر تعقيدًا، نظرًا لأن جميع المبرمجين تقريبًا يستخدِمون لغات عالية المستوى ويتركون العمل الشاق لإنتاج شيفرة التجميع للمصرّف Compiler، وبالتالي ستتفوق المزايا الأخرى على هذا العيب. بما أنّ التعليمات الموجودة في معالج RISC أبسط، فهناك مساحة أكبر ضمن شريحة المسجلات، إذ تُعَدّ المسجلات أسرع أنواع الذواكر كما نعلم من تسلسل الذواكر الهرمي، ويجب في النهاية تنفيذ جميع التعليمات على القيم المحفوظة في المسجلات، لذا ستؤدي زيادة عدد المسجلات إلى أداء أعلى عند تكافؤ جميع الأشياء الأخرى. بما أنّ جميع التعليمات تُنفَّذ في الوقت نفسه، فسيكون استخدام خطوط الأنابيب ممكنًا، وكما نعلم أنّ استخدام خط الأنابيب يتطلب تدفقات من التعليمات باستمرار إلى المعالج، لذلك إذا استغرقت بعض التعليمات وقتًا طويلًا جدًا دون أن تتطلب التعليمات الأخرى ذلك، فسيصبح خط الأنابيب معقدًا ليكون فعّالًا. معمارية EPIC يُعَدّ معالج إيتانيوم Itanium مثالًا على معمارية معدَّلة تسمى الحوسبة الصريحة للتعليمات الفرعية Explicitly Parallel Instruction Computing. ناقشنا سابقًا كيف أنّ المعالجات الفائقة لها خطوط أنابيب بها العديد من التعليمات في الوقت نفسه ضمن أجزاء مختلفة من المعالج، إذ يمكن تحقيق ذلك من خلال إعطاء التعليمات للمعالج بالترتيب الذي يمكن أن يحقق أفضل استفادة من العناصر المتاحة في وحدة المعالجة المركزية، وقد كان تنظيم مجرى التعليمات الواردة تقليديًا مهمة العتاد، إذ يصدر البرنامج التعليمات بطريقة تسلسلية، ويجب أن ينظر المعالج إلى الأمام ويحاول اتخاذ قرارات حول كيفية تنظيم التعليمات الواردة. الفكرة وراء معمارية EPIC هي أنّ هناك مزيد من المعلومات المتاحة على مستويات أعلى والتي يمكن أن تجعل هذه القرارات أفضل مما يفعله المعالج، ويؤدي تحليل مجرًى من تعليمات لغة التجميع -كما تفعل المعالجات الحالية- إلى فقدان الكثير من المعلومات التي قدّمها المبرمج في الشيفرة البرمجية الأصلية. فكر في الأمر على أنه الفرق بين دراسة مسرحية لشكسبير وقراءة نسخة ملاحظات الجرف Cliff's Notes من المسرحية نفسها، فكلاهما يمنحك النتيجة نفسها، ولكن النسخة الأصلية تحتوي على جميع أنواع المعلومات الإضافية التي تحدد المشهد وتعطيك فهمًا جيدًا للشخصيات، وبالتالي يمكن نقل منطق ترتيب التعليمات من المعالج إلى المصرّف، وهذا يعني أنّ مطوِّري المصرّفات يجب أن يكونوا أذكى في محاولة العثور على أفضل ترتيب للشيفرة البرمجية للمعالج، كما يجب تبسيط المعالج كثيرًا، إذ نُقِل الكثير من عمله إلى المصرِّف. يوجد مصطلح آخر غالبًا ما يُستخدَم مع معمارية EPIC وهو عالم التعليمات الطويلة جدًا Very Long Instruction World -أو VLIW اختصارًا-، إذ تُوسَّع كل تعليمة للمعالج لإخباره بالمكان الذي يجب أن ينفّذ فيه التعليمة في وحداته الداخلية، وتكمن مشكلة هذا الأسلوب في أنّ الشيفرة البرمجية تعتمد كليًا على طراز المعالج الذي صُرِّفت الشيفرة البرمجية من أجله، كما تُجري الشركات دائمًا مراجعات على العتاد، وتجعل العملاء يعيدون تصريف تطبيقاتهم في كل مرة، مما جعل صيانة مجموعة من الشيفرات البرمجية الثنائية المختلفة أمرًا غير عملي. تحل معمارية EPIC هذه المشكلة بطريقة علوم الحاسوب المعتادة من خلال إضافة طبقة من التجريد، كما تنشئ معمارية EPIC عرضًا مبسطًا مع بعض الوحدات الأساسية مثل الذاكرة ومسجّلات الأعداد الصحيحة والعشرية بدلًا من التحديد الصريح للجزء الدقيق من المعالج الذي يجب أن تنفّذ التعليمات عليه. ترجمة -وبتصرُّف- للقسم The CPU من الفصل Computer Architecture من كتاب Computer Science from the Bottom Up لصاحبه Ian Wienand. اقرأ أيضًا المقال التالي: نظرة عميقة على تسلسل الذواكر الهرمي والذاكرة المخبئية في معمارية الحاسوب المقال السابق: تمثيل الأنواع والأعداد في الأنظمة الحاسوبية وحدة المعالجة المركزية المدخل الشامل لتعلم علوم الحاسوب
-
اقتربنا من نهاية سلسلة إطار العمل Vue.js، إذ يجب الآن تعلّم كيفية الوصول إلى العناصر وإدارتها وتعديلها مثل إدارة التركيز أو كيف يمكننا تحسين الشمولية أو إمكانية الوصول لمستخدمي لوحة المفاتيح في تطبيقنا، إذ سنتعرّف على كيفية استخدام خاصيات ref في إطار العمل Vue للتعامل مع إدارة التركيز التي تُعَدّ ميزةً متقدمةً تتيح الوصول المباشر إلى عقد DOM الأساسية أسفل نموذج DOM الافتراضي أو الوصول المباشر من أحد المكوّنات إلى بنية DOM الداخلية الخاصة بمكوِّن ابن، كما سنوفِّر مزيدًا من الموارد لتعلّم إطار عمل Vue للاستزادة منها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّتٌ عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية التعامل مع إدارة التركيز باستخدام خاصيات ref في إطار العمل Vue. مشكلة إدارة التركيز لدينا وظيفة تعديل تعمل بصورة جيدة في تطبيقنا، إلا أننا لم نقدّم تجربةً رائعةً للمستخدِمين الذين لا يستخدِمون الفأرة، فإذا فعّل المستخدِم زر "التعديل Edit"، فإننا نزيل هذا الزر من نموذج DOM دون نقل تركيز المستخدِم إلى أيّ مكان آخر، لذلك سيختفي التركيز، ويمكن أن يكون هذا مربكًا لمستخدِمي لوحة المفاتيح والمستخدِمين غير المبصرين. طبقّ ما يلي لفهم ما يحدث حاليًا: أعد تحميل صفحتك ثم اضغط على مفتاح Tab، ويجب أن تشاهد إطارًا يمثل التركيز حول حقل الإدخال لإضافة عناصر مهام جديدة. اضغط على مفتاح Tab مرةً أخرى، ويجب أن ينتقل التركيز إلى زر "الإضافة Add". اضغط على مفتاح Tab مرةً أخرى وسينتقل التركيز إلى مربع الاختيار الأول، ثم يجب أن ينتقل التركيز إلى زر "التعديل Edit" الأول. فعّل زر "التعديل Edit" بالضغط على مفتاح Enter، وبالتالي سيُستبدَل مربع الاختيار بمكوِّن التعديل، ولكن سيختفي إطار التركيز. يمكن أن يكون هذا السلوك مربكًا، ويختلف ما يحدث عند الضغط على مفتاح Tab مرةً أخرى تبعًا للمتصفح الذي تستخدِمه، فإذا حفظت التعديل أو ألغيته، فسيختفي التركيز مرةً أخرى عندما تعود إلى العرض الذي لا يتضمن تعديلًا. يمكن منح المستخدِمين تجربةً أفضل من خلال إضافة شيفرة برمجية للتحكّم في التركيز بحيث يُضبَط على حقل التعديل عند عرض نموذج التعديل، وسنعيد وضع التركيز على زر "التعديل Edit" عندما يلغي المستخدِم تعديله أو يحفظه، وبالتالي يجب فهم المزيد حول كيفية عمل إطار Vue داخليًا بهدف ضبط التركيز. نموذج DOM الافتراضي وخاصيات ref يستخدِم إطار العمل Vue مثل بعض أطر العمل الأخرى نموذج DOM الافتراضي Virtual DOM -أو VDOM اختصارًا- لإدارة العناصر، وهذا يعني أنّ إطار Vue يحتفظ في الذاكرة بتمثيلٍ لجميع العقد في تطبيقنا، كما يمكن إجراء أيّ تحديثات أولًا على العقد الموجودة في الذاكرة، ثم تُزامَن جميع التغييرات التي يجب إجراؤها على العقد الفعلية على الصفحة دفعةً واحدةً. بما أن قراءة وكتابة عقد DOM الفعلية تكون مستهلكة أكثر للأداء من العقد الافتراضية في أغلب الأحيان، فسيؤدي ذلك إلى أداء أفضل، ولكنه يعني أيضًا أنه لا يجب تعديل عناصر HTML مباشرةً من خلال واجهات برمجة تطبيقات المتصفح الأصيلة Native مثل Document.getElementById عند استخدام أطر العمل، لأنه سيؤدي إلى عدم مزامنة نموذج VDOM و DOM الفعلي، فإذا كنت بحاجة إلى الوصول إلى عقد DOM الأساسية مثل حالة ضبط التركيز، فيمكنك استخدام خاصيات ref في إطار Vue، كما يمكنك بالنسبة لمكّونات Vue المخصَّصة استخدام خاصيات ref للوصول مباشرةً إلى البنية الداخلية لمكوّن ابن، ولكن يجب تطبيق ذلك بحذر لأنه قد يصعّب فهم الشيفرة البرمجية. يمكن استخدام خاصيات ref في أحد المكونات من خلال إضافة السمة ref إلى العنصر الذي تريد الوصول إليه مع معرِّف من نوع سلسلة نصية لقيمة هذه السمة، إذ يجب أن تكون السمة ref فريدةً في المكون، ولا ينبغي أن يكون هناك عنصران يصيّّران في الوقت نفسه ولهما السمة ref نفسها. إضافة الخاصية ref إلى التطبيق لنضِف السمة ref إلى زر "التعديل Edit" في المكوِّن ToDoItem.vue كما يلي: <button type="button" class="btn" ref="editButton" @click="toggleToItemEditForm"> Edit <span class="visually-hidden">{{label}}</span> </button> يمكن الوصول إلى القيمة المرتبطة بالسمة ref من خلال استخدام الخاصية $refs المتوفِّرة في نسخة المكوِّن، لذا أضِف التابع console.log() إلى التابع toggleToItemEditForm() كما يلي لمعرفة قيمة السمة ref عند النقر على زر "التعديل Edit": toggleToItemEditForm() { console.log(this.$refs.editButton); this.isEditing = true; } إذا فعّلتَ زر "التعديل Edit" الآن، فيُفترَض أن ترى عنصر الزر <button> في HTML مشارًا إليه في طرفيتك. التابع $nextTick() في إطار العمل Vue يجب الآن التركيز على زر "التعديل Edit" عندما يحفظ المستخدِم التعديل أو يلغيه، لذلك يجب التعامل مع التركيز باستخدام التابعَين itemEdited() و editCancelled() في المكوِّن ToDoItem. أنشئ تابعًا جديدًا لا يأخذ أيّ وسيط بالاسم focusOnEditButton() وأسند السمة ref إلى متغير، ثم استدعِ التابع focus() على هذه السمة. focusOnEditButton() { const editButtonRef = this.$refs.editButton; editButtonRef.focus(); } أضِف بعد ذلك استدعاءً إلى this.focusOnEditButton() في نهاية التابعَين itemEdited() و editCancelled() كما يلي: itemEdited(newItemName) { this.$emit("item-edited", newItemName); this.isEditing = false; this.focusOnEditButton(); }, editCancelled() { this.isEditing = false; this.focusOnEditButton(); }, جرّب تعديل عنصر مهمة ثم حفظ التعديل أو إلغاءه باستخدام لوحة المفاتيح وستلاحظ عدم ضبط التركيز، لذلك لا تزال لدينا مشكلة ويجب حلها، فإذا فتحتَ طرفيتك، فسترى ظهور الخطأ الذي يبدو غريبًا: "can't access property "focus", editButtonRef is undefined" إذ جرى تعريف المرجع ref الخاص بالزر عند تفعيل زر "التعديل Edit"، ولكنه ليس كذلك الآن، وتذكّر أننا لم نعُد نصيّر قسم المكوّن الذي يحتوي على زر "التعديل Edit" عند تغيير قيمة isEditing إلى true، وبالتالي لا يوجد عنصر لربط المرجع به، لذلك يصبح غير مُعرَّف. لكننا نضبط قيمة isEditing على false قبل محاولة الوصول إلى المرجع، فهل يجب أن يعرض الموجّه v-if الزر الآن؟ حسنًا، يجب الآن استخدام نموذج DOM الافتراضي، وبما أنّ Vue يحاول تحسين وإصلاح التغييرات، فلن يحدّث نموذج DOM مباشرةً عند ضبط قيمة isEditing على false، لذلك فإنّ زر "التعديل Edit" لم يُصيَّر بعد عند استدعاء التابع focusOnEdit()، لذا يجب الانتظار حتى يجتاز إطار Vue دورة تحديث نموذج DOM التالية، إذ تحتوي مكونات Vue على تابع خاص يسمَّى $nextTick() الذي يقبل دالة رد نداء Callback Function تنفّذ بعد تحديث نموذج DOM. يمكننا تغليف جسم الدالة الحالية ضمن استدعاء الدالة $nextTick() لأنه يجب استدعاء التابع focusOnEditButton() بعد تحديث نموذج DOM. focusOnEditButton() { this.$nextTick(() => { const editButtonRef = this.$refs.editButton; editButtonRef.focus(); }); } إذا فعّلتَ زر "التعديل Edit" ثم ألغيت التغييرات أو حفظتها باستخدام لوحة المفاتيح، فيجب إعادة التركيز إلى زر "التعديل Edit". توابع دورة حياة إطار عمل Vue تتمثَّل الخطوة التالية في نقل التركيز إلى العنصر <input> في نموذج التعديل عند النقر على زر "التعديل Edit"، ولكن لا يمكننا فقط وضع التركيز ضمن معالِج حدث النقر على زر "التعديل Edit" لأن نموذج التعديل موجود في مكوِّن مختلف عن هذا الزر، لذا يمكننا إزالة المكوِّن ToDoItemEditForm ونعيد تحميله كلما نقرنا على زر "التعديل Edit" للتعامل مع هذا الأمر. تجتاز مكوّنات Vue سلسلةً من الأحداث تُعرَف باسم دورة الحياة Lifecycle التي تمتد من قبل إنشاء العناصر وإضافتها إلى نموذج VDOM أي تثبيتها، حتى إزالتها من نموذج VDOM أي تدميرها. يتيح إطار عمل Vue تشغيل توابع في مراحل مختلفة من دورة الحياة باستخدام توابع دورة الحياة Lifecycle Methods المفيدة لأشياء متعددة مثل جلب البيانات، إذ يمكن أن تحتاج إلى الحصول على بياناتك قبل تصيير مكوِّنك أو بعد تغيير الخاصيات، وتمثِّل القائمة التالية قائمة توابع دورة الحياة حسب الترتيب الذي تُطلَق فيه: beforeCreate(): يُشغَّل قبل إنشاء نسخة من مكوِّنك، وليست البيانات والأحداث متاحةً بعد. created(): يُشغَّل بعد تهيئة مكوِّنك ولكن قبل إضافته إلى نموذج VDOM، ويُعَدّ المكان الذي يحدث فيه جلب البيانات. beforeMount(): يُشغَّل بعد تصريف Compile قالبك وقبل تصيير مكوِّنك إلى نموذج DOM الفعلي. mounted(): يُشغَّل بعد تثبيت مكوِّنك في نموذج DOM، ويمكن هنا الوصول إلى المراجع refs. beforeUpdate(): يُشغَّل كلما تغيرت البيانات في مكوِّنك وقبل تصيير التغييرات إلى نموذج DOM. updated(): يُشغَّل كلما تغيرت البيانات في مكوِّنك وبعد تصيير التغييرات إلى نموذج DOM. beforeDestroy(): يُشغَّل قبل إزالة أحد المكوّنات من نموذج DOM. destroyed(): يُشغَّل بعد إزالة أحد المكونات من نموذج DOM. activated(): يُستخدَم فقط في المكونات المغلَّفة بوسم keep-alive الخاص، ويعمل بعد تفعيل المكون. deactivated(): يُستخدَم فقط في المكونات المغلَّفة بوسم keep-alive الخاص، ويعمل بعد إلغاء تفعيل المكون. ملاحظة: يمكنك الاطلاع على مخطط رائع يشرح وقت حدوث هذه التوابع في توثيق Vue. لنستخدِم الآن تابعًا من توابع دورة الحياة لبدء التركيز عند تثبيت المكوِّن ToDoItemEditForm، لذا أضِف ref="labelInput" إلى العنصر <input> في المكوِّن ToDoItemEditForm.vue كما يلي: <input :id="id" ref="labelInput" type="text" autocomplete="off" v-model.lazy.trim="newName" /> أضف بعد ذلك الخاصية mounted() إلى كائن المكوِّن مباشرةً، إذ لا ينبغي وضعها ضمن الخاصية methods، وإنما يجب وضعها في مستوى تسلسل props و data() و methods الهرمي نفسه، فتوابع دورة الحياة هي توابع خاصة بمفردها ولا توضَع مع التوابع التي يعرّفها المستخدِم ولا تأخذ أيّ مدخلات، ولاحظ أنه لا يمكنك استخدام دالة سهمية هنا لأننا نحتاج الوصول إلى this للوصول إلى المرجع labelInput. mounted() { } أسنِد المرجع labelInput إلى متغير ضمن التابع mounted()، ثم استدعِ الدالة focus() الخاصة بالمرجع، ولست مضطرًا إلى استخدام $nextTick هنا لأن المكوِّن مُضاف بالفعل إلى نموذج DOM عند استدعاء التابع mounted(). mounted() { const labelInputRef = this.$refs.labelInput; labelInputRef.focus(); } إذا فعّلتَ زر "التعديل Edit" الآن باستخدام لوحة المفاتيح، فيجب أن ينتقل التركيز إلى تعديل العنصر <input> مباشرةً. التعامل مع التركيز عند حذف عناصر المهام إذا نقرت على زر "التعديل Edit"، فسيكون نقل التركيز إلى مربع نص تعديل الاسم والعودة إلى زر "التعديل Edit" عند الإلغاء أو الحفظ من نموذج التعديل أمرًا منطقيًا، لكن ليس لدينا موقع واضح لنقل التركيز إليه عند حذف عنصر، ونحتاج إلى طريقة لتزويد مستخدِمي التقنيات المساعدة بالمعلومات التي تؤكّد حذف عنصر. نتعقّب فعليًا عدد العناصر في عنوان القائمة أي العنصر <h2> في المكوِّن App.vue، وهو مرتبط بقائمة عناصر المهام، مما يجعله مكانًا مناسبًا لنقل التركيز إليه عند حذف عقدة. يجب أولًا إضافة مرجع إلى عنوان القائمة ثم إضافة السمة tabindex="-1" إليه، مما يجعل العنصر قابلًا للتركيز برمجيًا، أي يمكن التركيز عليه باستخدام شيفرة جافاسكربت إن لم يكن كذلك افتراضيًا. عدِّل العنصر <h2> في المكوِّن App.vue كما يلي: <h2 id="list-summary" ref="listSummary" tabindex="-1">{{listSummary}}</h2> ملاحظة: تُعَدّ السمة tabindex أداةً قويةً للتعامل مع بعض مشاكل الشمولية Accessibility، ولكن يجب استخدامها بحذر، إذ يمكن أن يتسبّب الإفراط في استخدام السمة tabindex="-1" في حدوث مشاكل لجميع أنواع المستخدِمين، لذلك استخدمها فقط في المكان الذي تحتاجها فيه فقط، كما يجب عدم استخدام السمة tabindex > = 0، إذ يمكن أن تسبب مشاكل للمستخدِمين لأنها يمكن أن تؤدي إلى عدم التطابق مع ترتيب انتقال التركيز باستخدام مفتاح Tab في نموذج DOM، و / أو إضافة عناصر غير تفاعلية إلى هذا الترتيب، كما يكون ذلك مربكًا للمستخدِمين خاصةً أولئك الذين يستخدِمون قارئات الشاشة والتقنيات المساعدة الأخرى. أصبح لدينا مرجع ref وأعلَمنا المتصفحات أنه يمكننا التركيز برمجيًا على العنصر <h2>، ويجب الآن التركيز عليه. استخدم المرجع listSummary في نهاية التابع deleteToDo() لضبط التركيز على العنصر <h2>، وبما أنّ العنصر <h2> يُصيَّر دائمًا في التطبيق، فليس هناك داع للقلق بشأن استخدام $nextTick مع توابع دورة الحياة للتعامل مع التركيز عليه. deleteToDo(toDoId) { const itemIndex = this.ToDoItems.findIndex(item => item.id === toDoId); this.ToDoItems.splice(itemIndex, 1); this.$refs.listSummary.focus(); } إذا حذفتَ عنصرًا من قائمتك الآن، فيجب نقل التركيز إلى عنوان القائمة الذي يجب أن يوفِّر تجربة تركيز جيدة لجميع المستخِدمين. تهانينا، انتهينا الآن من إنشاء تطبيقنا باستخدام إطار العمل Vue، وسنتعرّف الآن على بعض الموارد الإضافية لتعلم إطار العمل Vue. ملاحظة: إذا كنت بحاجة إلى التحقق من شيفرتك مقابل نسختنا، فيمكنك العثور على نسخة نهائية من شيفرة تطبيق Vue في مستودع todo-vue، كما يمكنك الحصول على إصدار حي مباشر قيد التشغيل. موارد إضافية لتعلم إطار العمل Vue سنختتم الآن هذا القسم من سلسلة تعلم تطوير الويب الذي يشمل إطار العمل Vue من خلال إعطائك قائمة بالموارد التي يمكنك استخدامها في مسيرة تعلمك، بالإضافة إلى بعض النصائح المفيدة الأخرى. من الموارد التي يجب أن تتطلع عليها لمعرفة المزيد عن إطار العمل Vue: كتاب أساسيات إطار العمل Vue .js من أكاديمية حسوب الذي يشرح مفهوم إطار العمل Vue. مقالات عن Vue: مقالات عربية من أكاديمية حسوب عن إطار العمل Vue.js. قسم الأسئلة البرمجية: إن أردت الحصول على مساعدة من مبرمجين عرب. منتدى البرمجة العربية: المنتدى البرمجي العربي للحصول على مساعدة في إطار العمل Vue أو أي شيء متعلق بالبرمجة. توثيق Vue: يحتوي موقع Vue الرسمي على توثيق شامل بما في ذلك الأمثلة والكتب والموارد المرجعية. مستودع Vue Github: شيفرة Vue البرمجية نفسها حيث يمكنك فيه الإبلاغ عن المشاكل والمساهمة مباشرةً في شيفرة Vue الأساسية، كما يمكن أن تساعدك دراسة شيفرة Vue البرمجية على فهم كيفية عمل إطار العمل وكتابة شيفرة أفضل. توثيق Vue CLI: يحتوي على معلومات حول تخصيص وتوسيع الخرج الذي تنشئه باستخدام واجهة سطر الأوامر CLI. NuxtJS: هو إطار عمل من Vue من طرف الخادم مع بعض الآراء المعمارية التي يمكن أن تكون مفيدةً لإنشاء تطبيقات قابلة للصيانة، حتى إن لم تستخدِم أيًا من ميزات التصيير من طرف الخادم Server Side Rendering التي يوفرها، كما يوفِّر توثيقًا مفصلًا حول استخدام NuxtJS. بناء ونشر تطبيق Vue توفِّر واجهة CLI في Vue أدوات لإعداد تطبيقنا للنشر على الويب كما يلي: إذا كان خادمك المحلي لا يزال قيد التشغيل، فيمكن إنهاؤه بالضغط على الاختصار Ctrl + C في الطرفية. شغّل الأمر npm run build أو الأمر yarn build في الطرفية. مما يؤدي إلى إنشاء مجلد dist جديد يحتوي على جميع ملفات الإنتاج الجاهزة. يمكنك نشر موقعك على الويب من خلال نسخ محتويات هذا المجلد إلى بيئة استضافتك. ملاحظة: يتضمن توثيق Vue CLI دليلًا حول كيفية نشر تطبيقك على العديد من أنظمة الاستضافة الشائعة. إصدار Vue 3 يُعَدّ Vue 3 إصدارًا رئيسيًا من Vue مع الكثير من التغييرات الرئيسية، وقد دخل هذا الإصدار مرحلة الإصدار التجريبي النشط في شهر 4 من عام 2020، كما يُعَدّ أكبر تغيير فيه هو واجهة Composition API الجديدة التي تعمل بوصفها بديلًا لواجهة API الحالية القائمة على الخاصيات، وتُستخدَم دالة setup() واحدة مع المكوِّن في هذه الواجهة الجديدة، حيث يتوفر فقط ما تعيده من هذه الدالة في عناصر القوالب <template>. كما يجب أن تكون واضحًا بشأن الخاصيات التفاعلية عند استخدام هذه الواجهة، إذ يعالج إطار العمل Vue ذلك نيابةً عنك باستخدام الواجهة Options API، مما يجعل واجهة برمجة التطبيقات الجديدة حالة استخدام أكثر تقدمًا. كما توجد بعض التغييرات الأخرى، بما في ذلك التغيير في كيفية تهيئة التطبيقات في Vue، ويمكنك الاطلاع عليها في توثيق Vue.js الرسمي. ترجمة -وبتصرّف- للمقالَين Focus management with Vue refs وVue resources. اقرأ أيضًا المقال السابق: العرض الشرطي في إطار العمل Vue.js إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js استخدام Vue.js للتعامل مع DOM
-
حان الوقت الآن لإضافة الوظائف التي تمكّننا من تعديل عناصر المهام الموجودة مسبقًا، لذلك سنستفيد من إمكانات التصيير الشرطي Conditional Rendering في إطار العمل Vue مثل v-if و v-else للسماح بالتبديل بين عرض عناصر المهام الموجودة مسبقًا وعرض التعديل حيث يمكنك تعديل عناوين أو تسميات labels عناصر المهام، كما سنتعرّف على إضافة وظيفة لحذف عناصر المهام. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية استخدام التصيير الشرطي في إطار العمل Vue. إنشاء مكون التعديل يمكننا البدء بإنشاء مكوِّن منفصل للتعامل مع وظيفة التعديل، لذا أنشئ ملفًا جديدًا بالاسم ToDoItemEditForm.vue في المجلد components وانسخ الشيفرة التالية في هذا الملف: <template> <form class="stack-small" @submit.prevent="onSubmit"> <div> <label class="edit-label">Edit Name for "{{label}}"</label> <input :id="id" type="text" autocomplete="off" v-model.lazy.trim="newLabel" /> </div> <div class="btn-group"> <button type="button" class="btn" @click="onCancel"> Cancel <span class="visually-hidden">editing {{label}}</span> </button> <button type="submit" class="btn btn__primary"> Save <span class="visually-hidden">edit for {{label}}</span> </button> </div> </form> </template> <script> export default { props: { label: { type: String, required: true }, id: { type: String, required: true } }, data() { return { newLabel: this.label }; }, methods: { onSubmit() { if (this.newLabel && this.newLabel !== this.label) { this.$emit("item-edited", this.newLabel); } }, onCancel() { this.$emit("edit-cancelled"); } } }; </script> <style scoped> .edit-label { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; color: #0b0c0c; display: block; margin-bottom: 5px; } input { display: inline-block; margin-top: 0.4rem; width: 100%; min-height: 4.4rem; padding: 0.4rem 0.8rem; border: 2px solid #565656; } form { display: flex; flex-direction: row; flex-wrap: wrap; } form > * { flex: 0 0 100%; } </style> ملاحظة: تصفَّح الشيفرة السابقة ثم اقرأ الوصف التالي للتأكد من فهمك لكل شيء يطبّقه المكوِّن قبل المضي قدمًا، إذ تُعَدّ هذه الطريقة مفيدةً للمساعدة في تعزيز ما تعلمته حتى الآن. تضبط الشيفرة السابقة أساس وظيفة التعديل، إذ ننشئ نموذجًا يحتوي على حقل إدخال <input> لتعديل اسم المهمة، ويوجد زر "حفظ Save" وزر "إلغاء Cancel": إذا نقرت على زر "الحفظ Save"، فسيصدِر المكوِّن التسمية الجديدة باستخدام الحدث item-edited. إذا نقرت على زر "الإلغاء Cancel"، فسيشير المكوِّن إلى ذلك عن طريق إصدار الحدث edit-cancelled. تعديل المكون ToDoItem يجب إجراء بعض التعديلات على المكوِّن ToDoItem قبل التمكّن من إضافة المكوِّن ToDoItemEditForm إلى تطبيقنا، إذ يجب إضافة متغير لتعقّب تعديل العنصر، وزر لتبديل هذا المتغير، كما سنضيف زر حذف Delete لأن الحذف وثيق الصلة بالتعديل، أي عدّل قالب المكون ToDoItem كما يلي: <template> <div class="stack-small"> <div class="custom-checkbox"> <input type="checkbox" class="checkbox" :id="id" :checked="isDone" @change="$emit('checkbox-changed')" /> <label :for="id" class="checkbox-label">{{label}}</label> </div> <div class="btn-group"> <button type="button" class="btn" @click="toggleToItemEditForm"> Edit <span class="visually-hidden">{{label}}</span> </button> <button type="button" class="btn btn__danger" @click="deleteToDo"> Delete <span class="visually-hidden">{{label}}</span> </button> </div> </div> </template> أضفنا عنصر <div> الذي يغلّف القالب بأكمله لأغراض التنسيق، وأضفنا زرَّي "تعديل Edit" و "حذف Delete": إذا نقرت على زر "التعديل Edit"، فسيبدّل عرض مكوِّن ToDoItemEditForm لنتمكن من استخدامه لتعديل عنصر المهام باستخدام دالة معالج حدث تسمى toggleToItemEditForm() التي ستضبط الراية isEditing على القيمة true، لكن يجب أولًا تعريف هذه الدالة ضمن الخاصية data(). إذا نقرت على زر "الحذف Delete"، فسيُحذَف عنصر المهام باستخدام دالة معالج حدث تسمى deleteToDo()، إذ سنصدِر في هذا المعالج الحدث item-deleted إلى المكوِّن الأب، مما يؤدي إلى تحديث القائمة. لنعرِّف الآن معالجات النقرات والراية isEditing، لذا أضِف الخاصية isEditing بعد الخاصية isDone كما يلي: data() { return { isDone: this.done, isEditing: false }; } أضِف الآن توابعك ضمن الخاصية methods وبعد الخاصية data() مباشرةً: methods: { deleteToDo() { this.$emit('item-deleted'); }, toggleToItemEditForm() { this.isEditing = true; } } عرض المكونات شرطيا باستخدام v-if و v-else لدينا الآن الراية isEditing التي يمكننا استخدامها للإشارة إلى أن العنصر مُعدَّل أم لا، فإذا كانت للراية isEditing القيمة true، فيجب استخدام هذه الراية لعرض المكوِّن ToDoItemEditForm بدلًا من مربع الاختيار، إذ سنستخدِم الموجِّه v-if في إطار العمل Vue. لن يصيِّر الموجِّه v-if كتلةً ما إلا إذا كانت القيمة المُمرَّرة إليه true، وهذا مشابه لكيفية عمل التعليمة if في لغة جافاسكربت، إذ يحتوي الموجّه v-if على موجّهات مقابلة هي v-else-if و v-else لتوفير ما يعادلها في لغة جافاسكربت مثل else if و else ضمن قوالب Vue. يجب أن تكون كتل v-else و v-else-if الشقيق الأول لكتل v-if و v-else-if، وإلا فلن يتعرَّف Vue عليها، كما يمكنك استخدام الموجّه v-if مع الوسم <template> إذا كنت بحاجة إلى تصيير قالب كامل شرطيًا. أخيرًا، يمكنك استخدام الموجّهَين v-if و v-else مع جذر المكوِّن لعرض إحدى الكتل فقط أو الكتلة الأخرى، لأنّ Vue لن يصيّر سوى كتلةً واحدةً من هذه الكتل في كل مرة، لذا سنطبّق ذلك في تطبيقنا مما يسمح باستبدال الشيفرة التي تعرض عنصر المهمة في نموذج التعديل. أضِف v-if="!isEditing" إلى عنصر <div> الجذر في المكون ToDoItem كما يلي: <div class="stack-small" v-if="!isEditing"> أضف بعد ذلك السطر التالي بعد وسم إغلاق <div>: <to-do-item-edit-form v-else :id="id" :label="label"></to-do-item-edit-form> كما يجب استيراد المكوِّن ToDoItemEditForm وتسجيله لنتمكّن من استخدامه ضمن هذا القالب، لذا أضف السطر التالي قبل العنصر <script>: import ToDoItemEditForm from "./ToDoItemEditForm"; أضِف الخاصية components قبل الخاصية props ضمن كائن المكوِّن: components: { ToDoItemEditForm }, إذا انتقلتَ الآن إلى تطبيقك ونقرت على زر "تعديل Edit" عنصر المهام، فيجب أن ترى مربع الاختيار مستبدَلًا بنموذج التعديل. لكن لا توجد طريقة حاليًا للعودة، لذلك يجب إضافة مزيد من معالجات الأحداث إلى المكوِّن. الرجوع من وضع التعديل يجب أولًا إضافة التابع itemEdited() إلى الخاصية methods في المكوِّن ToDoItem، إذ يأخذ هذا التابع عنوان label العنصر الجديد بوصفه وسيطًا ويرسل الحدث itemEdited إلى المكوِّن الأب ويضبط isEditing على القيمة false، لذا أضِف هذا التابع الآن بعد التوابع الحالية كما يلي: itemEdited(newLabel) { this.$emit('item-edited', newLabel); this.isEditing = false; } سنحتاج بعد ذلك إلى التابع editCancelled()، ولا يأخذ هذا التابع أي وسائط ويعمل فقط على ضبط isEditing مرةً أخرى على القيمة false، لذا أضِف هذا التابع بعد التابع السابق: editCancelled() { this.isEditing = false; } أخيرًا، سنضيف معالِجات الأحداث للأحداث الصادرة من المكوِّن ToDoItemEditForm، وسنربط التوابع المناسبة لكل حدث، لذا عدِّل الاستدعاء <to-do-item-edit-form> ليبدو كما يلي: <to-do-item-edit-form v-else :id="id" :label="label" @item-edited="itemEdited" @edit-cancelled="editCancelled"> </to-do-item-edit-form> تعديل وحذف عناصر المهام يمكننا الآن التبديل بين نموذج التعديل ومربع الاختيار، ولكن لم نعالِج تحديث المصفوفة ToDoItems مرةً أخرى في المكوِّن App.vue، ويمكن إصلاح ذلك من خلال الاستماع إلى الحدث item-edited وتحديث القائمة وفقًا لذلك، كما يجب التعامل مع حدث الحذف لنتمكّن من حذف عناصر المهام. أضف التوابع الجديدة التالية إلى كائن مكوِّن App.vue بعد التوابع الموجودة مسبقًا ضمن الخاصية methods: deleteToDo(toDoId) { const itemIndex = this.ToDoItems.findIndex(item => item.id === toDoId); this.ToDoItems.splice(itemIndex, 1); }, editToDo(toDoId, newLabel) { const toDoToEdit = this.ToDoItems.find(item => item.id === toDoId); toDoToEdit.label = newLabel; } سنضيف بعد ذلك مستمعي الأحداث للحدثين item-deleted و item-edited، بحيث: يجب تمرير item.id إلى التابع بالنسبة للحدث item-deleted. يجب تمرير item.id والمتغير الخاص $event بالنسبة للحدث item-edited، إذ يُعَدّ المتغير $event هو متغير خاص بإطار العمل Vue ويُستخدَم لتمرير بيانات الحدث إلى التوابع، فإذا استخدمتَ أحداث HTML الأصيلة Native مثل الحدث click، فسيمرّر هذا المتغير كائن الحدث الأصيل إلى تابعك. عدّل الاستدعاء <to-do-item></to-do-item> ضمن قالب App.vue ليبدو كما يلي: <to-do-item :label="item.label" :done="item.done" :id="item.id" @checkbox-changed="updateDoneStatus(item.id)" @item-deleted="deleteToDo(item.id)" @item-edited="editToDo(item.id, $event)"> </to-do-item> يجب أن تكون الآن قادرًا على تعديل العناصر وحذفها من القائمة. إصلاح خطأ باستخدام الحالة isDone يبدو كل شيء رائعًا حتى الآن، ولكننا أحدثنا خطأً عن طريق إضافة وظيفة التعديل، لذا جرّب تنفيذ ما يلي: تحديد أو إلغاء تحديد أحد مربعات اختيار المهام. الضغط على زر "التعديل Edit" لعنصر المهام نفسه. إلغاء التعديل بالضغط على زر "الإلغاء Cancel". لاحظ حالة مربع الاختيار بعد الضغط على زر الإلغاء، فلم ينسَ التطبيق حالة مربع الاختيار فقط، وإنما أحدث خللًا أيضًا في حالة الاكتمال done لعنصر المهام المقابل، فإذا حاولت تحديده أو إلغاء تحديده مرةً أخرى، فسيتغير عدد المهام المكتملة بعكس ما تتوقعه لأنّ الخاصية isDone ضمن data تُعطَى القيمة this.done عند تحميل المكوِّن، ويمكن إصلاح ذلك عن طريق تحويل عنصر بيانات isDone إلى الخاصية computed، إذ سنستخدِم ميزةً أخرى للخاصيات computed هي أنها تحافظ على التفاعل، مما يعني أنّ حالتها تُحفَظ عندما يتغير القالب. أزِل السطر التالي من الخاصية data(): isDone: this.done, أضِف الكتلة التالية بعد كتلة data() { }: computed: { isDone() { return this.done; } }, ستجد أنّ المشكلة قد حُلَّت الآن عند الحفظ وإعادة التحميل، إذ سيجري الاحتفاظ بحالة مربع الاختيار عند التبديل بين قوالب عناصر المهام. فهم تشابك الأحداث يُعَدّ تشابكُ الأحداث القياسية والمخصَّصة التي استخدمناها لتحفيز التفاعل في تطبيقنا أحد أكثر الأجزاء المربكة، إذ يمكننا فهم ذلك بصورة أفضل من خلال كتابة مخطط تدفقي أو وصف أو رسم توضيحي لمكان إصدار الأحداث ومكان الاستماع إليها وما يحدث نتيجة إطلاقها. فمثلًا في الملف App.vue: يستمع العنصر <to-do-form> إلى: الحدث todo-added الصادر عن التابع onSubmit() ضمن المكوِّن ToDoForm عند إرسال النموذج، والنتيجة هي استدعاء التابع addToDo() لإضافة عنصر مهمة جديد إلى المصفوفة ToDoItems. يستمع العنصر <to-do-item> إلى: الحدث checkbox-changed الصادر عن مربع الاختيار في العنصر <input> ضمن المكوِّن ToDoItem عند تحديده أو إلغاء تحديده، والنتيجة هي استدعاء التابع updateDoneStatus() لتحديث حالة الاكتمال done لعنصر المهمة المقابل. الحدث item-deleted الصادر عن التابع deleteToDo() ضمن المكوِّن ToDoItem عند الضغط على زر "الحذف Delete"، والنتيجة هي استدعاء التابع deleteToDo() لحذف عنصر المهمة المقابل. الحدث item-edited الصادر عن التابع itemEdited() ضمن المكوِّن ToDoItem عند الاستماع بنجاح إلى الحدث item-edited باستخدام التابع onSubmit() ضمن المكوِّن ToDoItemEditForm، إذ يمثّل ذلك سلسلةً من حدثَي item-edit مختلفين، والنتيجة هي استدعاء التابع editToDo() لتحديث عنوان label عنصر المهمة المقابل. في الملف ToDoForm.vue: يستمع العنصر <form> إلى الحدث submit، والنتيجة هي استدعاء التابع onSubmit() الذي يتحقَّق من أنّ العنوان label الجديد ليس فارغًا، ثم يصدر الحدث todo-added الذي يمكن الاستماع إليه لاحقًا ضمن الملف App.vue كما ذكرنا سابقًا، ثم يمسح عنوان العنصر <input> الجديد. في الملف ToDoItem.vue: يستمع عنصر مربع الاختيار checkbox في العنصر <input> إلى الحدث change، والنتيجة هي إصدار الحدث checkbox-changed عند تحديد أو إلغاء تحديد مربع الاختيار الذي يمكن الاستماع إليه لاحقًا في المكوِّن App.vue كما ذكرنا سابقًا. يستمع زر <button> "التعديل Edit" إلى الحدث click، والنتيجة هي استدعاء التابع toggleToItemEditForm() الذي قيمة this.isEditing مبدَّلة إلى true والتي تعرض بدورها قالب تعديل عنصر المهام عند إعادة التصيير. يستمع زر <button> "الحذف Delete" إلى الحدث click، والنتيجة هي استدعاء التابع deleteToDo() الذي يصدر الحدث item-deleted، والذي يمكن الاستماع إليه لاحقًا ضمن المكوِّن App.vue كما ذكرنا سابقًا. يستمع المكوِّن <to-do-item-edit-form> إلى: الحدث item-edited الصادر عن التابع onSubmit() ضمن المكوِّن ToDoItemEditForm عند إرسال النموذج بنجاح، والنتيجة هي استدعاء التابع itemEdited() الذي يصدر الحدث item-edited الذي يمكن الاستماع إليه لاحقًا في المكوِّن App.vue كما ذكرنا سابقًا، كما يعيد ضبط this.isEditing على القيمة false، وبالتالي لن يظهر نموذج التعديل عند إعادة التصيير. الحدث edit-cancelled الصادر عن التابع onCancel() ضمن المكوِّن ToDoItemEditForm عند النقر على زر "الإلغاء Cancel"، والنتيجة هي استدعاء التابع editCancelled() الذي يعيد ضبط this.isEditing على القيمة false، وبالتالي لن يظهر نموذج التعديل عند إعادة التصيير. في الملف ToDoItemEditForm.vue: يستمع العنصر <form> إلى الحدث submit، والنتيجة هي استدعاء التابع onSubmit() الذي يتحقق مما إذا كانت قيمة العنوان الجديدة غير فارغة وليست مماثلةً للقيمة القديمة، فإذا كان الأمر كذلك، فسيصدر الحدث item-edited الذي يمكن الاستماع إليه لاحقًا ضمن المكوِّن ToDoItem.vue كما ذكرنا سابقًا. يستمع زر <button> "الإلغاء Cancel" إلى الحدث click، والنتيجة هي استدعاء التابع onCancel() الذي يصدر الحدث edit-cancelled الذي يمكن الاستماع إليه لاحقًا في المكون ToDoItem.vue كما ذكرنا سابقًا. الخلاصة أصبح لدينا الآن وظائف التعديل والحذف في تطبيقنا، وبالتالي سنقترب من نهاية سلسلة Vue، إذ يجب الآن تعلّم كيفية إدارة التركيز أو كيف يمكننا تحسين الشمولية لمستخدمي لوحة المفاتيح في تطبيقنا. ترجمة -وبتصرّف- للمقال Vue conditional rendering: editing existing todos. اقرأ أيضًا المقال السابق: إضافة تنسيق للمكونات واستعمال الخاصية computed في تطبيق Vue.js عرض مكونات Vue.js استخدام Vue.js للاتصال بالإنترنت
-
لقد حان الوقت أخيرًا لجعل تطبيقنا يبدو أجمل قليلًا، إذ سنتعرّف في هذا المقال على الطرق المختلفة لتنسيق مكونات إطار العمل Vue باستخدام لغة CSS، كما سنضيف عدّادًا يعرض عدد عناصر المهام المكتملة باستخدام ميزة في إطار عمل Vue تسمّى الخاصية computed، إذ تعمل هذه الخاصية بصورة مشابهة للتوابع، ولكن يُعاد تشغيلها فقط عندما تتغير إحدى اعتمادياتها. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكونات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، كما ستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكونات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: التعرف على مكونات التنسيق وتعلّم كيفية استخدام خاصيات computed في Vue. يجب إضافة شيفرة CSS الأساسية إلى تطبيقنا لجعله يبدو أفضل قبل إضافة مزيد من الميزات المتقدمة إليه، إذ يمتلك إطار العمل Vue ثلاث طرق شائعة لتنسيق التطبيقات: ملفات CSS الخارجية. التنسيقات العام في مكونات الملف المفرد، أي الملفات ذات اللاحقة .vue. التنسيقات على مستوى المكونات في مكونات الملف المفرد. سنستخدِم مزيجًا من الطرق الثلاثة لإضفاء مظهر وتنسيق أفضل على تطبيقنا للتعرف على هذه الطرق. التنسيق باستخدام ملفات CSS الخارجية يمكنك تضمين ملفات CSS الخارجية وتطبيقها بطريقة عامة على تطبيقك، لذا أنشئ ملفًا بالاسم reset.css في المجلد src/assets، إذ تُعالَج الملفات الموجودة في هذا المجلد باستخدام Webpack، وبالتالي يمكننا استخدام معالجات CSS المسبقَة مثل SCSS أو معالجات ملحقَة مثل PostCSS. لن نستخدِم في هذا المقال مثل هذه الأدوات، ولكن يجب معرفة أنه ستُعالَج الشيفرة تلقائيًا عند تضمينها في المجلد assets. أضِف المحتويات التالية في الملف reset.css: /*reset.css*/ /* إعادة الضبط */ *, *::before, *::after { box-sizing: border-box; } *:focus { outline: 3px dashed #228bec; } html { font: 62.5% / 1.15 sans-serif; } h1, h2 { margin-bottom: 0; } ul { list-style: none; padding: 0; } button { border: none; margin: 0; padding: 0; width: auto; overflow: visible; background: transparent; color: inherit; font: inherit; line-height: normal; -webkit-font-smoothing: inherit; -moz-osx-font-smoothing: inherit; -webkit-appearance: none; } button::-moz-focus-inner { border: 0; } button, input, optgroup, select, textarea { font-family: inherit; font-size: 100%; line-height: 1.15; margin: 0; } button, input { /* 1 */ overflow: visible; } input[type="text"] { border-radius: 0; } body { width: 100%; max-width: 68rem; margin: 0 auto; font: 1.6rem/1.25 "Helvetica Neue", Helvetica, Arial, sans-serif; background-color: #f5f5f5; color: #4d4d4d; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } @media screen and (min-width: 620px) { body { font-size: 1.9rem; line-height: 1.31579; } } /*نهاية إعادة الضبط*/ استورد بعد ذلك الملف reset.css في الملف src/main.js كما يلي: import './assets/reset.css'; سيؤدي ذلك إلى نقل الملف أثناء خطوة البناء وإضافته تلقائيًا إلى موقعنا. يجب تطبيق التنسيقات المُعاد ضبطها على التطبيق الآن، إذ تُظهر الصور التالية مظهر التطبيق قبل وبعد تطبيق إعادة الضبط. قبل إعادة ضبط التنسيق: بعد إعادة ضبط التنسيق: تشمل التغييرات الملحوظة إزالة نقط تعداد للقائمة وتغييرات لون الخلفية والتغييرات في الزر الأساسي وتنسيقات حقل الإدخال. إضافة التنسيقات العامة إلى مكونات الملف المفرد أعدنا ضبط شيفرة CSS لتكون موحدةًَ في المتصفحات ويجب الآن تخصيص التنسيقات، فهناك بعض التنسيقات التي نريد تطبيقها على مستوى المكونات في تطبيقنا من خلال إضافة التنسيقات إلى وسوم <style> في الملف App.vue بدلًا من إضافتها مباشرةً إلى ملف التنسيقات reset.css. توجد مسبقًا بعض التنسيقات في الملف App.vue، فلنحذفها ونستبدلها بالتنسيقات التالية التي تضيف تنسيقًا إلى الأزرار وحقول الإدخال وتخصّص العنصر #app وأبناءه، لذا عدِّل العنصر <style> في الملف App.vue بحيث يبدو كما يلي: <style> /* التنسيقات العامة */ .btn { padding: 0.8rem 1rem 0.7rem; border: 0.2rem solid #4d4d4d; cursor: pointer; text-transform: capitalize; } .btn__danger { color: #fff; background-color: #ca3c3c; border-color: #bd2130; } .btn__filter { border-color: lightgrey; } .btn__danger:focus { outline-color: #c82333; } .btn__primary { color: #fff; background-color: #000; } .btn-group { display: flex; justify-content: space-between; } .btn-group > * { flex: 1 1 auto; } .btn-group > * + * { margin-left: 0.8rem; } .label-wrapper { margin: 0; flex: 0 0 100%; text-align: center; } [class*="__lg"] { display: inline-block; width: 100%; font-size: 1.9rem; } [class*="__lg"]:not(:last-child) { margin-bottom: 1rem; } @media screen and (min-width: 620px) { [class*="__lg"] { font-size: 2.4rem; } } .visually-hidden { position: absolute; height: 1px; width: 1px; overflow: hidden; clip: rect(1px 1px 1px 1px); clip: rect(1px, 1px, 1px, 1px); clip-path: rect(1px, 1px, 1px, 1px); white-space: nowrap; } [class*="stack"] > * { margin-top: 0; margin-bottom: 0; } .stack-small > * + * { margin-top: 1.25rem; } .stack-large > * + * { margin-top: 2.5rem; } @media screen and (min-width: 550px) { .stack-small > * + * { margin-top: 1.4rem; } .stack-large > * + * { margin-top: 2.8rem; } } /* نهاية التنسيقات العامة */ #app { background: #fff; margin: 2rem 0 4rem 0; padding: 1rem; padding-top: 0; position: relative; box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1); } @media screen and (min-width: 550px) { #app { padding: 4rem; } } #app > * { max-width: 50rem; margin-left: auto; margin-right: auto; } #app > form { max-width: 100%; } #app h1 { display: block; min-width: 100%; width: 100%; text-align: center; margin: 0; margin-bottom: 1rem; } </style> إذا اختبرت التطبيق، فسترى أنّ قائمة المهام موجودة ضمن قسم خاص به الآن مع بعض التنسيقات الأفضل لعناصر المهام كما يلي: إضافة أصناف CSS في إطار العمل Vue سنطبّق أصناف CSS على عنصر الزر <button> في المكوِّن ToDoForm، وبما أنّ قوالب Vue تستخدِم لغة HTML، فسنستخدِم الطريقة نفسها عن طريق إضافة السمة class="" إلى العنصر، لذا أضِف السمة class="btn btn__primary btn__lg" إلى العنصر <button> في نموذجك: <button type="submit" class="btn btn__primary btn__lg"> Add </button> هناك تغيير آخر يمكن إجراؤه، فبما أنّ النموذج يشير إلى قسم معيّن من الصفحة، فيمكن استخدام العنصر <h2>، ولكن يشير العنصر label إلى الغرض من النموذج مسبقًا، لذلك يجب تجنب التكرار من خلال تغليف العنصر label ضمن العنصر <h2>، كما يمكننا إضافة بعض تنسيقات CSS العامة الأخرى، إذ سنضيف الصنف input__lg إلى العنصر <input>، لذا عدِّل قالب المكوّن ToDoForm بحيث يبدو كما يلي: <template> <form @submit.prevent="onSubmit"> <h2 class="label-wrapper"> <label for="new-todo-input" class="label__lg"> What needs to be done? </label> </h2> <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" v-model.lazy.trim="label" class="input__lg" /> <button type="submit" class="btn btn__primary btn__lg"> Add </button> </form> </template> كما سنضيف الصنف stack-large إلى الوسم <ul> في الملف App.vue كما يلي، مما يساعد في تحسين التباعد بين عناصر المهام: <ul aria-labelledby="list-summary" class="stack-large"> إضافة التنسيقات ذات النطاق المحدد نريد الآن تنسيق المكوِِّن ToDoItem، إذ يمكننا إضافة العنصر <style> ضمنه لتكون تعريفات التنسيقات قريبةً من المكوِّن، لكن إذا غيرت هذه التنسيقات أشياءً خارج هذا المكوِّن، فسيكون تعقّب التنسيقات المسؤولة عن ذلك وإصلاح المشكلة أمرًا صعبًا. سنستخدِم السمة scoped حيث يرتبط محدِّد سمة HTML الفريدة data مع جميع التنسيقات، مما يمنعها من التعارض على المستوى العام، كما يمكنك استخدام معدِّل scoped من خلال إنشاء العنصر <style> ضمن المكوِّن ToDoItem.vue، ثم منحه السمة scoped كما يلي: <style scoped> </style> انسخ بعد ذلك شيفرة CSS التالية والصقها في العنصر <style> الذي أنشأناه للتو: .custom-checkbox > .checkbox-label { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; font-size: 1rem; line-height: 1.25; color: #0b0c0c; display: block; margin-bottom: 5px; } .custom-checkbox > .checkbox { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; font-weight: 400; font-size: 16px; font-size: 1rem; line-height: 1.25; box-sizing: border-box; width: 100%; height: 40px; height: 2.5rem; margin-top: 0; padding: 5px; border: 2px solid #0b0c0c; border-radius: 0; -webkit-appearance: none; -moz-appearance: none; appearance: none; } .custom-checkbox > input:focus { outline: 3px dashed #fd0; outline-offset: 0; box-shadow: inset 0 0 0 2px; } .custom-checkbox { font-family: Arial, sans-serif; -webkit-font-smoothing: antialiased; font-weight: 400; font-size: 1.6rem; line-height: 1.25; display: block; position: relative; min-height: 40px; margin-bottom: 10px; padding-left: 40px; clear: left; } .custom-checkbox > input[type="checkbox"] { -webkit-font-smoothing: antialiased; cursor: pointer; position: absolute; z-index: 1; top: -2px; left: -2px; width: 44px; height: 44px; margin: 0; opacity: 0; } .custom-checkbox > .checkbox-label { font-size: inherit; font-family: inherit; line-height: inherit; display: inline-block; margin-bottom: 0; padding: 8px 15px 5px; cursor: pointer; touch-action: manipulation; } .custom-checkbox > label::before { content: ""; box-sizing: border-box; position: absolute; top: 0; left: 0; width: 40px; height: 40px; border: 2px solid currentColor; background: transparent; } .custom-checkbox > input[type="checkbox"]:focus + label::before { border-width: 4px; outline: 3px dashed #228bec; } .custom-checkbox > label::after { box-sizing: content-box; content: ""; position: absolute; top: 11px; left: 9px; width: 18px; height: 7px; transform: rotate(-45deg); border: solid; border-width: 0 0 5px 5px; border-top-color: transparent; opacity: 0; background: transparent; } .custom-checkbox > input[type="checkbox"]:checked + label::after { opacity: 1; } @media only screen and (min-width: 40rem) { label, input, .custom-checkbox { font-size: 19px; font-size: 1.9rem; line-height: 1.31579; } } يجب الآن إضافة أصناف CSS إلى القالب لربط التنسيقات، لذا انتقل إلى العنصر <div> الجذر ثم أضِف الصنف custom-checkbox، وأضف الصنف checkbox إلى العنصر <input> ثم أضف الصنف إلى العنصر <label>، وبالتالي سيحتوي التطبيق على مربعات اختيار مخصَّصة، كما يجب أن يبدو تطبيقك الآن كما يلي: انتهينا من تنسيق تطبيقنا، وسنعود الآن إلى إضافة مزيد من الوظائف إليه، أي استخدام الخاصية computed لإضافة عدد عناصر المهام المكتملة إلى تطبيقنا. استخدام الخاصية computed في إطار العمل Vue نريد إضافة عدّاد counter يلخّص عدد العناصر في قائمة المهام، مما يساعد التقنيات المساعدة في معرفة عدد المهام المكتملة، فإذا كان لدينا عنصران مكتملان من أصل خمسة في قائمة المهام، فسنرى العبارة "2 items completed out of 5" كما يلي: <h2>{{ToDoItems.filter(item => item.done).length}} out of {{ToDoItems.length}} items completed</h2> سيُعاد حساب هذا العدد في كل عملية تصيير، ويمكن أن يكون ذلك غير مهم بالنسبة إلى تطبيق صغير مثل تطبيقنا، ولكنه سيتسبب في مشكلة خطيرة في الأداء بالنسبة للتطبيقات الأكبر حجمًا أو عندما يكون التعبير أكثر تعقيدًا، لذا يكون الحل الأفضل هو استخدام خاصيات computed في إطار العمل Vue، إذ تعمل هذه الخاصيات بصورة مشابهة للتوابع، ولكن يُعاد تشغيلها فقط عندما تتغير إحدى اعتمادياتها، إذ سيُعاد تشغيلها في حالتنا عندما تتغير المصفوفة ToDoItems فقط. يمكن إنشاء الخاصية computed من خلال إضافتها إلى كائن المكوِّن مثل الخاصية methods التي استخدمناها سابقًا. إضافة عداد أضِف الشيفرة البرمجية التالية إلى كائن المكوِّن App بعد الخاصية methods، إذ يأخذ التابع listSummary() عدد عناصر ToDoItems المنتهية، ويعيد سلسلةً نصيةً تمثّل ذلك. computed: { listSummary() { const numberFinishedItems = this.ToDoItems.filter(item =>item.done).length return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed` } } يمكننا الآن إضافة {{listSummary}} مباشرةً إلى نموذجنا، إذ سنضيفه ضمن العنصر <h2> قبل العنصر <ul> مباشرةً، كما سنضيف السمة id والسمة aria-labelledby لتكون محتويات العنصر <h2> عنوانًا label للعنصر <ul>، لذا أضِف العنصر <h2> وعدّل العنصر <ul> ضمن قالب المكون App كما يلي: <h2 id="list-summary">{{listSummary}}</h2> <ul aria-labelledby="list-summary" class="stack-large"> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> يجب أن تشاهد الآن ملخص القائمة في تطبيقك مع تحديث عدد العناصر الإجمالي كلما أضفت مزيدًا من عناصر المهام، ولكن إذا حاولت تحديد بعض العناصر ثم إلغاء تحديدها، فسيظهر خطأ لأننا لم نتعقّب بعد البيانات "المكتملة" فعليًا، لذلك لن يتغير عدد العناصر المكتملة. تعقب تغيرات عدد العناصر المكتملة يمكننا استخدام الأحداث لالتقاط تحديث مربع الاختيار وإدارة القائمة وفقًا لذلك، كما يمكننا ربط معالج الحدث @change مع كل مربع اختيار بدلًا من استخدام الموجِّه v-model لأننا لا نعتمد على ضغط الزر لبدء التغيير. عدّل العنصر <input> في المكون ToDoItem.vue ليبدو كما يلي، كما يمكننا تضمين $emit() لأنّ كل ما يجب فعله هو إرسال أن مربع الاختيار محدَّد فقط: <input type="checkbox" class="checkbox" :id="id" :checked="isDone" @change="$emit('checkbox-changed')" /> أضف تابعًا جديدًا بالاسم updateDoneStatus() في المكوِّن App.vue بعد التابع addToDo()، إذ يجب أن يأخذ هذا التابع معامِلًا واحدًا هو معرّف عنصر المهمة، كما نريد العثور على العنصر ذي المعرّف id المطابق وتحديث حالته done لتكون معاكسةً لحالته الحالية: updateDoneStatus(toDoId) { const toDoToUpdate = this.ToDoItems.find(item => item.id === toDoId) toDoToUpdate.done = !toDoToUpdate.done } يجب تشغيل هذا التابع كلما أصدر المكوِّن ToDoItem الحدث checkbox-changed، وتمرير معرّفه item.id بوصفه معاملًا، لذا عدّل الاستدعاء <to-do-item></to-do-item> كما يلي: <to-do-item :label="item.label" :done="item.done" :id="item.id" @checkbox-changed="updateDoneStatus(item.id)"> </to-do-item> إذا اختبرت العنصر ToDoItem الآن، فيجب أن تشاهد تحديث العدّاد الملخِّص بطريقة صحيحة. الخلاصة تعرّفنا في هذا المقال على تنسيق مكوّنات إطار العمل Vue باستخدام لغة CSS، واستخدمنا الخاصية computed لإضافة ميزة مفيدة إلى تطبيقنا وهي حساب عدد عناصر المهام المكتملة، وسنتعرّف في المقال التالي على التصيير أو العرض الشرطي Conditional Rendering وكيفية استخدامه لإظهار نموذج التعديل عندما نريد تعديل عناصر المهام الحالية. ترجمة -وبتصرّف- للمقالين Styling Vue components with CSS وUsing Vue computed properties. اقرأ أيضًا المقال السابق: إنشاء المكونات في تطبيق Vue.js عرض مكونات Vue.js شرح مفهوم الأحداث والتوابع والنماذج في Vue.js
-
لدينا الآن عينة من البيانات وحلقة تأخذ كل جزء من هذه البيانات وتصيّره أو تعرضه Render ضمن المكوِّن ToDoItem في تطبيقنا، كما نريد السماح لمستخدِمينا بإدخال عناصر مهامهم في التطبيق، لذلك سنحتاج إلى نص إدخال <input> وحدث يُطلَق عند إرسال البيانات وتابع لإطلاق الإرسال بهدف إضافة البيانات وتصيير القائمة ونموذج للتحكم في البيانات، وهذا هو موضوعنا في هذا المقال. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية التعامل مع الاستمارات Forms في Vue والأحداث Events والنماذج Models والتوابع Methods المرتبطة بها. إنشاء استمارة مهام جديدة لدينا الآن تطبيق يعرض قائمةً بعناصر المهام، ولكن لا يمكننا تعديل قائمة العناصر دون تغيير شيفرتنا البرمجية يدويًا، إذًا لنصلح ذلك ولْننشئ مكوِّنًا جديدًا يسمح لنا بإضافة عنصر مهام جديد. أنشئ ملفًا جديدًا يسمى ToDoForm.vue في مجلد المكوّنات، ثم أضِف عنصر <template> فارغًا ووسم <script> كما يلي: <template></template> <script> export default {}; </script> أضِف استمارة HTML الذي يتيح إدخال عنصر مهام جديد وإرساله إلى التطبيق، كما نحتاج إلى العنصر <form> مع العناصر <label> و<input> و<button>، لذا عدِّل قالبك ليبدو كما يلي: <template> <form> <label for="new-todo-input"> What needs to be done? </label> <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" /> <button type="submit"> Add </button> </form> </template> لدينا الآن مكوِّن استمارة يمكننا من خلاله إدخال عنوان عنصر مهام جديد والذي سيصبح عنوانًا أو تسمية label للمكوِّن ToDoItem المقابل عند تصييره لاحقًا، ولنحمِّل هذا المكوِّن في تطبيقنا، لذا ارجع إلى الملف App.vue وأضف تعليمة الاستيراد import التالية بعد تعليمة الاستيراد السابقة مباشرةً ضمن العنصر <script>: import ToDoForm from './components/ToDoForm'; يجب تسجيل المكوِّن الجديد في المكون App من خلال تعديل الخاصية components الخاصة بكائن المكوِّن بحيث تبدو كما يلي: components: { ToDoItem, ToDoForm } صيّر المكوِّن ToDoForm ضمن تطبيقك من خلال إضافة العنصر <to-do-form /> إلى العنصر <template> ضمن المكوِّن App كما يلي: <template> <div id="app"> <h1>My To-Do List</h1> <to-do-form></to-do-form> <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> </div> </template> إذا شغّلت موقعك الآن، فيجب أن ترى الاستمارة الجديدة كما يلي: إذا ملأت الاستمارة ونقرت على زر "الإضافة Add"، فستسرسل الصفحة الاستمارة مرةً أخرى إلى الخادم، ولكننا لا نريد ذلك، وإنما نريد تشغيل تابع على الحدث submit الذي سيضيف المهام الجديدة إلى قائمة بيانات ToDoItem المُعرَّفة ضمن المكوِّن App، لذلك يجب إضافة تابع إلى نسخة المكوِّن. إنشاء تابع وربطه بحدث باستخدام الموجه v-on يمكن إتاحة تابع للمكوِّن ToDoForm من خلال إضافته إلى كائن المكوِّن ضمن الخاصية methods التي تشبه الخاصيات data() و props وما إلى ذلك، إذ تحتوي الخاصية methods على أيّ تابع قد نحتاجه لاستدعاء مكوننا. كما تُشغَّل جميع التوابع عند استدعاء هذه الخاصية، لذلك لا يُعَدّ استخدامها لعرض المعلومات ضمن القالب أمرًا جيدًا، وبالتالي يجب استخدام الخاصية computed -التي سنتحدث عنها لاحقًا- لعرض البيانات الناتجة عن العمليات الحسابية. يجب إضافة التابع onSubmit() إلى الخاصية methods ضمن كائن المكوِّن ToDoForm، إذ سنستخدِم هذا التابع للتعامل مع إجراء الإرسال، لذا أضف هذا التابع كما يلي: export default { methods: { onSubmit() { console.log('form submitted') } } } يجب بعد ذلك ربط التابع بمعالج حدث الإرسال submit للعنصر <form>، ويشبه ذلك إلى حد كبير كيفية استخدام إطار العمل Vue لصيغة v-bind بهدف ربط السمات، إذ يمتلك Vue موجِّهًا خاصًا لمعالجة الأحداث وهو v-on الذي يعمل باستخدام الصيغة v-on:event="method"، كما توجد صيغة مختصرة هي @event="method"، إذ سنستخدِم الصيغة المختصرة في هذا المقال، وبالتالي أضف معالج حدث الإرسال submit إلى العنصر <form> كما يلي: <form @submit="onSubmit"> إذا شغّلنا معالج حدث الإرسال، فلا يزال التطبيق يرسل البيانات إلى الخادم مما يتسبب في تحديث الصفحة، وبما أننا نطبّق كل عمليات المعالجة على العميل، فلا يوجد خادم ليتعامل مع إعادة الإرسال وسنفقد جميع الحالات المحلية عند تحديث الصفحة، إذ يمكن منع المتصفح من الإرسال إلى الخادم من خلال إيقاف إجراء الحدث الافتراضي أثناء انتشاره للأعلى Bubbling Up في الصفحة باستخدام التابع Event.preventDefault() في لغة جافاسكربت الصرفة Vanilla JavaScript مثلًا. كما يحتوي إطار عمل Vue على صيغة خاصة تُسمَّى معدِّلات الأحداث Event Modifiers التي يمكنها معالجة هذا الأمر مباشرةً في قالبنا، في حين تُضاف المُعدِّلات إلى نهاية الحدث مع نقطة بالصورة @event.modifier، ونوضِّح فيما يلي قائمةً بمعدِّلات الأحداث: .stop: يوقِف انتشار الحدث ويكافئ التابع Event.stopPropagation() في أحداث جافاسكربت العادية. .prevent: يمنع سلوك الحدث الافتراضي ويكافئ التابع Event.preventDefault(). .self: يؤدي إلى تشغيل المعالج فقط إذا أُرسِل الحدث من هذا العنصر المُحدَّد. {.key}: يؤدي إلى تشغيل معالج الأحداث باستخدام مفتاح محدَّد فقط، ويحتوي موقع MDN على قائمة بقيم المفاتيح الصالحة، ولكن يجب تحويل المفاتيح متعددة الكلمات إلى حالة الأحرف التي تسمى نمط أسياخ الشواء Kebab Case مثل page-down. .native: يستمع إلى الحدث الأصيل Native في عنصر الجذر أو الغلاف الخارجي Outer-most Wrapping لمكوِّنك. .once: يستمع إلى الحدث حتى تشغيله لمرة واحدة فقط لا أكثر. .left: يشغِّل المعالج باستخدام حدث زر الفأرة الأيسر فقط. .right: يشغِّل المعالج باستخدام حدث زر الفأرة الأيمن فقط. .middle: يشغِّل المعالج باستخدام حدث زر الفأرة الأوسط فقط. .passive: يكافئ استخدام المعامِل { passive: true } عند إنشاء مستمع حدث في لغة جافاسكربت الصرفة Vanilla JavaScript باستخدام التابع addEventListener(). سنستخدِم في حالتنا المعالِج .prevent لإيقاف إجراء الإرسال الافتراضي للمتصفح، لذا أضِف .prevent إلى معالج الإرسال @submit في قالبك كما يلي: <form @submit.prevent="onSubmit"> إذا حاولت إرسال الاستمارة الآن، فستلاحظ عدم إعادة تحميل الصفحة، وإذا فتحت الطرفية، فيمكنك رؤية نتائج التابع console.log() التي أضفناها ضمن التابع onSubmit(). ربط البيانات مع الدخل باستخدام الموجه v-model نحتاج الآن إلى طريقة للحصول على القيمة الموجودة في حقل الإدخال <input> من الاستمارة لنتمكّن من إضافة عنصر المهام الجديد إلى قائمة بيانات ToDoItems، إذ يكون أول شيء نحتاجه هو الخاصية data في استمارتنا لتعقّب قيمة المهمة. أضِف التابع data() إلى كائن مكون ToDoForm الذي يعيد الحقل label، إذ يمكننا ضبط قيمة الحقل label الأولية بوصفها سلسلة نصية فارغة، ويجب أن يبدو كائن المكوِّن الآن كما يلي: export default { methods: { onSubmit() { console.log("form submitted"); } }, data() { return { label: "" }; } }; نحتاج الآن إلى طريقة ما لربط قيمة حقل الإدخال <input> ذي المعرِّف new-todo-input بالحقل label، إذ يمتلك إطار العمل Vue موجِّهًا خاصًا لذلك وهو v-model الذي يرتبط مع خاصية البيانات التي ضبطتها عليه ويبقيها متزامنةً مع حقل الإدخال <input>، في حين يعمل الموجّه v-model مع جميع أنواع حقول الإدخال المختلفة بما في ذلك مربعات الاختيار Checkboxes وأزرار الانتقاء Radios وحقول الاختيار Select Inputs، كما يُستخدَم هذا الموجِّه من خلال إضافة سمة بالصورة v-model="variable" إلى العنصر <input> كما يلي: <input type="text" id="new-todo-input" name="new-todo" autocomplete="off" v-model="label" /> ملاحظة: يمكنك مزامنة البيانات مع قيم العنصر <input> باستخدام تركيبة من الأحداث مع سمات v-bind وهذا ما يفعله الموجّه v-model، كما تختلف تركيبة الأحداث والسمات وفقًا لنوع حقل الإدخال وستتطلب شيفرةً برمجيةً أكبر من مجرد استخدام صيغة v-model المختصرة. لنختبر استخدام الموجِّه v-model عن طريق تسجيل قيمة البيانات المقدَّمة في التابع onSubmit()، إذ يمكن الوصول إلى سمات البيانات في المكوّنات باستخدام الكلمة this، وبالتالي يمكن الوصول إلى الحقل label بالصورة this.label، لذا عدّل التابع onSubmit() ليبدو كما يلي: methods: { onSubmit() { console.log('Label value: ', this.label); } }, عد الآن إلى تطبيقك المُشغَّل، وأضِف نصًا في الحقل <input> ثم انقر على زر "الإضافة Add"، إذ يجب أن ترى القيمة التي أدخلتها مسجلةً في الطرفية كما يلي على سبيل المثال: Label value: My value تغيير سلوك الموجه v-model باستخدام المعدلات يمكننا إضافة معدِّلات Modifiers لتغيير سلوك الموجِّه v-model بطريقة مماثلة لمعدِّلات الأحداث، ومن أبزر هذه المعدِّلات: .trim: يزيل المسافة الفارغة الموجودة قبل نص الإدخال أو بعده، ويمكننا إضافة هذا المُعدِّل إلى تعليمة v-model بالصورة v-model.trim="label" .lazy: يتغير هذا المعدِّل عندما يتزامن v-model مع قيمة نص حقل الإدخال، كما يمكن مزامنة الموجِّه v-model عن طريق تحديث المتغير الذي يستخدم الأحداث، بينما تحدُث هذه المزامنة مع نص حقل الإدخال باستخدام الحدث input، ويعني ذلك أنّ إطار عمل Vue يزامن البيانات بعد كل ضغطة مفتاح، في حين يتسبب المعدِّل .lazy في أن يستخدِم الموجّه v-model الحدث change بدلًا من ذلك، وهذا يعني أنّ Vue لن يزامن البيانات إلّا عندما يفقد حقل الإدخال التركيز أو عند إرسال الاستمارة، وهذا منطقي أكثر لأننا نحتاج إلى البيانات النهائية فقط. ملاحظة: يمكن استخدام المعدِّلَين .trim و.lazy مع بعضهما البعض من خلال استخدامهما بالشكل v-model.lazy.trim="label". عدّل السمة v-model إلى سلسلة lazy و trim كما هو موضَّح أعلاه، ثم اختبر تطبيقك مرةً أخرى، وجرّب إرسال قيمة بمسافة فارغة في كل نهاية مثلًا. تمرير البيانات إلى العناصر الآباء مع الأحداث المخصصة يجب الآن تمرير عنصر المهام الذي أنشأناه إلى المكوِّن App من خلال جعل المكوِّن ToDoForm يصدر حدثًا مخصَّصًا يمرِّر البيانات، وجعل المكون App يستمع إلى هذا الحدث، وتعمل هذه الطريقة بصورة مشابهة جدًا للأحداث الأصيلة مع عناصر HTML، إذ يمكن للمكوِّن الابن إصدار حدث يمكن الاستماع إليه باستخدام الموجِّه v-on. لنضِف الحدث todo-added إلى الحدث onSubmit الخاص بالمكوِّن ToDoForm، كما يمكن إصدار الأحداث المخصَّصة بالصورة this.$emit("event-name")، ويجدر بالذكر أنّ معالجات الأحداث حساسة لحالة الأحرف ولا يمكن أن تتضمن مسافات، وتُحوَّل قوالب Vue إلى أحرف صغيرة، مما يعني أنها لا تستطيع الاستماع إلى الأحداث المُسمَّاة بأحرف كبيرة. ضَع ما يلي بدلًا من التابع console.log() الموجود في التابع onSubmit(): this.$emit("todo-added"); ارجع بعد ذلك إلى المكوِّن App.vue وأضِف الخاصية methods إلى كائن المكوِّن الذي يحتوي على التابع addToDo() كما هو موضّح أدناه، إذ يمكن لهذا التابع فقط إظهار العبارة To-do added على الطرفية حاليًا. export default { name: 'app', components: { ToDoItem, ToDoForm }, data() { return { ToDoItems: [ { id:uniqueId('todo-'), label: 'Learn Vue', done: false }, { id:uniqueId('todo-'), label: 'Create a Vue project with the CLI', done: true }, { id:uniqueId('todo-'), label: 'Have fun', done: true }, { id:uniqueId('todo-'), label: 'Create a to-do list', done: false } ] }; }, methods: { addToDo() { console.log('To-do added'); } } }; أضِف بعد ذلك مستمع حدث إلى الحدث todo-added في العنصر <to-do-form></to-do-form>، إذ يستدعي هذا المستمع التابع addToDo() عند إطلاق الحدث، وسيبدو المستمع بالصورة @todo-added="addToDo" باستخدام الاختصار @: <to-do-form @todo-added="addToDo"></to-do-form> يجب أن ترى سجل الطرفية من التابع addToDo() عند إرسال المكوِّن ToDoForm، ويُعَدّ ذلك أمرًا جيدًا، لكننا ما زلنا لا نعيد أيّ بيانات إلى المكوِّن App.vue، ويمكن ذلك عن طريق إعادة وسائط إضافية إلى الدالة this.$emit() في المكوِّن ToDoForm، إذ نريد في حالتنا تمرير بيانات العنصر label مع الحدث عند إطلاقه من خلال تضمين البيانات التي نريد تمريرها بوصفها معاملًا آخرًا في التابع $emit() بالصورة this.$emit("todo-added", this.label)، وهذا مشابه لكيفية تضمين أحداث جافاسكربت الأصيلة للبيانات باستثناء أنّ أحداث Vue المخصَّصة التي لا تتضمن أيّ كائن حدث افتراضيًا، وبالتالي سيتطابق الحدث المنطلق مع أيّ كائن ترسله مباشرةً، كما سيكون كائن الحدث في حالتنا سلسلةً نصيةً فقط، لذا عدِّل التابع onSubmit() كما يلي: onSubmit() { this.$emit('todo-added', this.label) } يمكن التقاط هذه البيانات ضمن المكوِّن App.vue من خلال إضافة معامِل إلى التابع addToDo() الذي يتضمن عنصر label خاص بعنصر المهام الجديد، لذا ارجع إلى المكوِّن App.vue وعدّله ليبدو كما يلي: methods: { addToDo(toDoLabel) { console.log('To-do added:', toDoLabel); } } إذا اختبرت استمارتك مرةً أخرى، فسترى أنّ أيّ نص تدخله مسجَّل في الطرفية عند الإرسال، كما يمرِّر Vue تلقائيًا الوسائط بعد أن يكون اسم الحدث في this.$emit() هو معالِج الأحداث خاصتك. إضافة المهام الجديدة إلى بياناتنا يجب الآن إضافة عنصر يمثِّل بيانات المكوِّن ToDoForm التي أصبحت متوفرةً في المكوِّن App.vue في المصفوفة ToDoItems، ويمكن ذلك عن طريق دفع كائن عنصر مهام جديد إلى المصفوفة التي تحتوي على البيانات الجديدة. عدِّل التابع addToDo() كما يلي: addToDo(toDoLabel) { this.ToDoItems.push({id:uniqueId('todo-'), label: toDoLabel, done: false}); } اختبر الاستمارة مرةً أخرى، إذ يُفترَض أن ترى عناصر مهام جديدة تُلحَق بنهاية القائمة، وإذا أرسلتَ الاستمارة وحقل الإدخال فارغ، فستُضاف عناصر المهام التي لا تحتوي على نص إلى القائمة، ويمكن إصلاح ذلك من خلال منع تشغيل الحدث todo-added عندما يكون الاسم فارغًا، وبما أنّ الاسم قد أزيل باستخدام الموجِّه .trim، فيجب اختبار السلسلة النصية الفارغة فقط، لذا ارجع إلى المكوِّن ToDoForm وعدِّل التابع onSubmit() كما يلي، وإذا كانت قيمة الحقل label فارغةً، فيجب عدم إصدار الحدث todo-added. onSubmit() { if(this.label === "") { return; } this.$emit('todo-added', this.label); } جرّب استمارتك مرةً أخرى، إذ لن تتمكّن الآن من إضافة عناصر فارغة إلى قائمة المهام. استخدام الموجه v-model لتحديث قيمة حقل الإدخال لا يزال العنصر <input> يحتوي على القيمة القديمة بعد الإرسال، ويمكن إصلاح ذلك لأننا نستخدم الموجّه v-model لربط البيانات بالعنصر <input> في المكوِّن ToDoForm، فإذا ضبطنا معامِل الاسم name ليكون سلسلة نصية فارغة، فسيُحدَّث حقل الإدخال. عدِّل التابع onSubmit() الخاص بالمكوِّن ToDoForm كما يلي: onSubmit() { if(this.label === "") { return; } this.$emit('todo-added', this.label); this.label = ""; } إذا نقرتَ الآن على زر "الإضافة Add"، فسيمسح حقل إدخال المهمة الجديدة "new-todo-input" نفسه. الخلاصة يمكننا الآن إضافة عناصر المهام إلى استمارتنا، وبالتالي بدأ تطبيقنا الآن يصبح تفاعليًا، ولكننا تجاهلنا شكله وتنسيقه تمامًا، وسنركز في المقال التالي على إصلاح ذلك، كما سنتعرف على الطرق المختلفة التي يوفرها إطار العمل Vue لمكوّنات التنسيق. ترجمة -وبتصرّف- للمقال Adding a new todo form: Vue events, methods, and models. اقرأ أيضًا عرض مكونات Vue.js إنشاء المكونات في تطبيق Vue.js الموجهات الشرطية والتكرارية في Vue.js
-
لدينا حتى الآن مكوِّن يعمل بنجاح، ونحن الآن جاهزون لإضافة عدة مكوّنات ToDoItem إلى تطبيقنا، إذ سنتعلّم في هذا المقال كيفية إضافة مجموعة من بيانات عناصر المهام إلى المكوِّن App.vue التي سنكرّرها ونعرضها ضمن المكونات ToDoItem باستخدام الموجّه v-for. المتطلبات الأساسية: الإلمام بأساسيات لغات HTML وCSS وجافاسكربت JavaScript، ومعرفة استخدام سطر الأوامر أو الطرفية، إذ تُكتَب مكوِّنات Vue بوصفها مجموعةً من كائنات جافاسكربت التي تدير بيانات التطبيق وصيغة القوالب المستنِدة إلى لغة HTML المرتبطة مع بنية DOM الأساسية، وستحتاج إلى طرفية مثبَّت عليها node و npm لتثبيت Vue ولاستخدام بعض ميزاته الأكثر تقدمًا مثل مكوّنات الملف المفرد Single File Components أو دوال التصيير Render. الهدف: تعلّم كيفية تكرار مصفوفة من البيانات وتصييرها أو عرضها وإخراجها في مكوّنات متعددة. عرض القوائم باستخدام الموجه v-for يجب أن نكون قادرين على تصيير عناصر مهام متعددة باستخدام الموجِّه v-for في Vue لكي تكون قائمة المهام فعّالةً، إذ يسمح هذا الموجِّه المبني مسبقًا بتضمين حلقة ضمن قالبنا مع تكرار تصيير ميزة القالب لكل عنصر في المصفوفة، إذ سنستخدِمه للتكرار ضمن مصفوفة من عناصر المهام وعرضها في تطبيقنا ضمن مكوّنات ToDoItem منفصلة. إضافة بعض البيانات لعرضها يجب أولًا الحصول على مصفوفة من عناصر المهام من خلال إضافة الخاصية data إلى كائن المكوّن App.vue، وتحتوي هذه الخاصية على الحقل ToDoItems الذي تكون قيمته مصفوفةً من عناصر المهام، كما سنضيف لاحقًا آليةً لإضافة عناصر مهام جديدة، ولكن يمكننا البدء ببعض عناصر المهام المقلِّدة Mock Items، إذ سيُمثَّل كل عنصر من عناصر المهام بكائن له الخاصيتان name و done. أضِف بعض نماذج عناصر المهام كما يلي، وبالتالي تكون لديك بعض البيانات المتاحة للتصيير باستخدام الموجّه v-for: export default { name: 'app', components: { ToDoItem }, data() { return { ToDoItems: [ { label: 'Learn Vue', done: false }, { label: 'Create a Vue project with the CLI', done: true }, { label: 'Have fun', done: true }, { label: 'Create a to-do list', done: false } ] }; } }; يمكننا الآن بعد أن أصبح لدينا قائمة بالعناصر، استخدام الموجِّه v-for لعرض هذه العناصر، إذ تُطبَّق الموجَّهات Directives على العناصر مثل السمات الأخرى، كما يمكنك في حالة الموجِّه v-for استخدام صيغة خاصة مشابهة لحلقة for...in في جافاسكربت هي v-for="item in items"، إذ تُعَدّ items هي المصفوفة التي نريد تكرار عناصرها؛ أما item، فهو مرجع للعنصر الحالي في المصفوفة. يرتبط الموجِّه v-for بالعنصر الذي تريد تكراره، ويصيّر ذلك العنصر وأبناؤه، كما نريد في حالتنا عرض العنصر <li> لكل عنصر مهمةً ضمن المصفوفة ToDoItems، ثم نريد تمرير البيانات من كل عنصر مهمة إلى المكوِّن ToDoItem. السمة key هناك جزء آخر من الصيغة المُستخدَمة مع الموجِّه v-for يجب معرفته وهو السمة key، إذ يحاول Vue تحسين تصيير العناصر في القائمة من خلال تصحيح عناصر القائمة حتى لا يعيد إنشاءها في كل مرة تتغير فيها القائمة، لذلك يحتاج إلى "مفتاح key" فريد للعنصر نفسه الذي يرتبط مع الموجِّه v-for للتأكد من أنه يعيد استخدام عناصر القائمة بطريقة مناسبة. يجب أن تكون قيم السمات key سلسلةً نصيةً أو قيمًا عدديةً للتأكد من أنّ Vue يمكنه الموازنة بين هذه السمات بدقة، كما يمكن أن يكون استخدام حقل الاسم name أمرًا رائعًا، ولكن يتحكّم به إدخال المستخدِم، مما يعني أنه لا يمكننا ضمان أن تكون الأسماء فريدةً، كما يمكننا استخدام التابع lodash.uniqueid() كما فعلنا في المقال السابق. استورد أولًا lodash.uniqueid إلى المكوِّن App بالطريقة نفسها التي استخدمناها مع المكوِّن ToDoItem كما يلي: import uniqueId from 'lodash.uniqueid'; أضِف بعد ذلك حقل المعرّف id لكل عنصر في المصفوفة ToDoItems، وأسند القيمة uniqueId('todo-') لكل معرِّف، إذ يجب أن تبدو محتويات العنصر <script> في App.vue الآن كما يلي: import ToDoItem from './components/ToDoItem.vue'; import uniqueId from 'lodash.uniqueid' export default { name: 'app', components: { ToDoItem }, data() { return { ToDoItems: [ { id: uniqueId('todo-'), label: 'Learn Vue', done: false }, { id: uniqueId('todo-'), label: 'Create a Vue project with the CLI', done: true }, { id: uniqueId('todo-'), label: 'Have fun', done: true }, { id: uniqueId('todo-'), label: 'Create a to-do list', done: false } ] }; } }; أضِف الموجِّه v-for والسمة key إلى العنصر <li> في قالب App.vue كما يلي: <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item label="My ToDo Item" :done="true"></to-do-item> </li> </ul> إذا أجرينا هذا التغيير، فيمكن لكل تعبير جافاسكربت بين وسوم <li> الوصول إلى قيمة item وإلى سمات المكوِّن الأخرى، وهذا يعني أنه يمكننا تمرير حقول كائنات العنصر إلى المكوِّن ToDoItem وتذكّر استخدام صيغة v-bind، ويُعَدّ ذلك مفيدًا حقًا لأننا نريد أن تعرض عناصر المهام خاصيات label بوصفها تسميةً أو عنوانًا لها وليس عنوانًا ثابتًا "My Todo Item"، كما نريد أن تعكس حالة التحديد الخاصيات done بحيث لا تُضبَط دائمًا على القيمة done="false". عدِّل السمة label="My ToDo Item" إلى :label="item.label" والسمة :done="false" إلى :done="item.done" كما يلي: <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done"></to-do-item> </li> </ul> إذا نظرت إلى تطبيقك المُشغَّل الآن، فسيُظهِر عناصر المهام بأسمائها الصحيحة، وإذا فحصت الشيفرة البرمجية، فسترى أنّ جميع المدخلات لها معرِّفات فريدة مأخوذة من الكائن في المكوِّن App. إجراء بعض التعديلات يمكنك إجراء عملية إعادة البناء Refactoring هنا، إذ يمكننا تحويل المعرِّف إلى خاصية بدلًا من إنشاء معرِّفات لمربعات الاختيار ضمن المكوِّن ToDoItem، وبالرعم من أنّ ذلك لا يُعَدّ ضروريًا، إلّا أنه يسهّل علينا إدارته نظرًا لأنه يجب إنشاء معرِّف فريد لكل عنصر مهمة على أية حال. أضف خاصية id الجديدة إلى المكون ToDoItem. اجعل هذه الخاصية إجباريةً واجعل نوعها String. احذف الحقل id من السمة data لمنع تعارض الأسماء. لم نَعُد نستخدِم التابع uniqueId، لذلك يجب إزالة السطر التالي، وإلا فسيعطي تطبيقك خطأً. import uniqueId from 'lodash.uniqueid'; يجب أن تبدو محتويات العنصر <script> في المكوِّن ToDoItem كما يلي: export default { props: { label: {required: true, type: String}, done: {default: false, type: Boolean}, id: {required: true, type: String} }, data() { return { isDone : this.done, } }, } مرّر item.id الآن في المكون App.vue بوصفه خاصيةً إلى المكوِّن ToDoItem، إذ يجب أن يبدو قالب App.vue الآن كما يلي: <template> <div id="app"> <h1>My To-Do List</h1> <ul> <li v-for="item in ToDoItems" :key="item.id"> <to-do-item :label="item.label" :done="item.done" :id="item.id"></to-do-item> </li> </ul> </div> </template> إذا نظرت إلى موقعك المُصيَّر الآن، فيجب أن يبدو كما هو، ولكن تعني عملية إعادة البناء أن معرّفنا id مأخوذ من البيانات الموجودة ضمن App.vue وهو مُمرَّر إلى المكون ToDoItem بوصفه خاصية Prop مثل أي شيء آخر، لذلك أصبحت الأمور الآن منطقيةً ومتناسقةً أكثر. الخلاصة لدينا الآن عينة من البيانات وحلقة تأخذ كل جزء من هذه البيانات وتصيّره ضمن المكوِّن ToDoItem في تطبيقنا، إذ يجب الآن السماح لمستخدمينا بإدخال عناصر مهامهم في التطبيق، ولذلك سنحتاج إلى نص حقل الإدخال <input> وحدث يُطلَق عند إرسال البيانات وتابع للإطلاق بهدف إضافة البيانات وتصيير القائمة بالإضافة إلى نموذج للتحكم في البيانات، وهذا هو موضوعنا في المقال التالي. ترجمة -وبتصرّف- للمقال Rendering a list of Vue components. اقرأ أيضًا المقال السابق: إنشاء المكونات في تطبيق Vue.js مدخل إلى التعامل مع المكونات في Vue.js النسخة الكاملة لكتاب أساسيات إطار العمل Vue.js