تصميم وبناء تطبيق ويب تقدمي بدون إطار (الجزء الثاني)
نشرت: 2022-03-10كان سبب وجود هذه المغامرة هو دفع مؤلفك المتواضع قليلاً في تخصصات التصميم المرئي وترميز JavaScript. لم تكن وظيفة التطبيق الذي قررت إنشاءه مختلفة عن تطبيق "المهام المطلوب تنفيذها". من المهم التأكيد على أن هذا لم يكن تمرينًا على التفكير الأصلي. كانت الوجهة أقل أهمية بكثير من الرحلة.
تريد أن تعرف كيف انتهى التطبيق؟ قم بتوجيه متصفح هاتفك إلى https://io.benfrain.com.
فيما يلي ملخص لما سنقوم بتغطيته في هذا المقال:
- إعداد المشروع ولماذا اخترت Gulp كأداة بناء ؛
- أنماط تصميم التطبيق وما تعنيه في الممارسة ؛
- كيفية تخزين وتصور حالة التطبيق ؛
- كيف تم تحديد نطاق CSS للمكونات ؛
- ما هي التفاصيل الدقيقة لواجهة المستخدم / UX التي تم استخدامها لجعل الأشياء أكثر "شبيهة بالتطبيقات" ؛
- كيف تغير التحويل من خلال التكرار.
لنبدأ بأدوات البناء.
أدوات البناء
من أجل الحصول على الأدوات الأساسية الخاصة بي من TypeScipt و PostCSS وتشغيلها وإنشاء تجربة تطوير لائقة ، سأحتاج إلى نظام بناء.
في وظيفتي اليومية ، على مدار السنوات الخمس الماضية أو نحو ذلك ، كنت أقوم ببناء نماذج أولية للواجهة في HTML / CSS وبدرجة أقل ، JavaScript. حتى وقت قريب ، كنت أستخدم Gulp مع أي عدد من المكونات الإضافية بشكل حصري تقريبًا لتحقيق احتياجات البناء المتواضعة إلى حد ما.
عادةً ما أحتاج إلى معالجة CSS ، وتحويل JavaScript أو TypeScript إلى JavaScript مدعوم على نطاق أوسع ، وفي بعض الأحيان ، تنفيذ المهام ذات الصلة مثل تقليل إخراج الكود وتحسين الأصول. لقد سمح لي استخدام Gulp دائمًا بحل هذه المشكلات مع الثقة بالنفس.
بالنسبة لأولئك غير المألوفين ، يتيح لك Gulp كتابة JavaScript للقيام "بشيء ما" للملفات الموجودة على نظام الملفات المحلي لديك. لاستخدام Gulp ، عادة ما يكون لديك ملف واحد (يسمى gulpfile.js
) في جذر مشروعك. يتيح لك ملف JavaScript هذا تحديد المهام كوظائف. يمكنك إضافة "مكونات إضافية" لجهات خارجية ، والتي تعد في الأساس وظائف JavaScript إضافية ، والتي تتعامل مع مهام محددة.
مثال على مهمة Gulp
مثال على مهمة Gulp قد تستخدم مكونًا إضافيًا لتسخير PostCSS للمعالجة إلى CSS عندما تقوم بتغيير ورقة أنماط التأليف (gulp-postcss). أو تجميع ملفات TypeScript إلى Vanilla JavaScript (gulp-typescript) أثناء حفظها. فيما يلي مثال بسيط لكيفية كتابة مهمة في Gulp. تستخدم هذه المهمة المكون الإضافي "del" gulp لحذف جميع الملفات في مجلد يسمى "build":
var del = require("del"); gulp.task("clean", function() { return del(["build/**/*"]); });
يقوم الطلب بتعيين del
plugin require
متغير. ثم يتم استدعاء طريقة gulp.task
. نقوم بتسمية المهمة بسلسلة كمعامل أول ("نظيف") ثم نقوم بتشغيل وظيفة ، والتي في هذه الحالة تستخدم طريقة "del" لحذف المجلد الذي تم تمريره إليها كوسيطة. رموز علامة النجمة هناك أنماط "glob" والتي تشير بشكل أساسي إلى "أي ملف في أي مجلد" لمجلد الإنشاء.
يمكن أن تصبح مهام Gulp أكثر تعقيدًا ولكن في جوهرها ، هذه هي آليات كيفية التعامل مع الأشياء. الحقيقة ، مع Gulp ، لا تحتاج إلى أن تكون معالج JavaScript لتتمكن من تدبر الأمر ؛ مهارات النسخ واللصق من الصف الثالث هي كل ما تحتاجه.
لقد تمسكت بـ Gulp كأداة إنشاء / عداء مهام افتراضية طوال هذه السنوات بسياسة "إذا لم يتم كسرها ؛ لا تحاول إصلاحه.
ومع ذلك ، كنت قلقة من أن أعلق في طرقي. إنه فخ سهل الوقوع فيه. أولاً ، تبدأ في قضاء العطلة في نفس المكان كل عام ، ثم ترفض تبني أي اتجاهات أزياء جديدة قبل أن ترفض في النهاية وبقوة تجربة أي أدوات بناء جديدة.
لقد سمعت الكثير من الأحاديث على الإنترنت حول "Webpack" واعتقدت أنه من واجبي تجربة مشروع باستخدام الخبز المحمص الجديد لمطور الواجهة الأمامية Cool-kids.
حزمة الويب
أتذكر بوضوح تخطي إلى موقع webpack.js.org باهتمام شديد. بدأ الشرح الأول لماهية Webpack وما يفعله على هذا النحو:
import bar from './bar';
يقول ما؟ على حد تعبير الدكتور Evil ، "ارميني بعظمة frickin 'هنا ، Scott".
أعلم أنه من الصعب التعامل مع الأمر ولكني شعرت بالاشمئزاز من أي تفسيرات تتعلق بالترميز تذكر "foo" أو "bar" أو "baz". هذا بالإضافة إلى الافتقار التام لوصف موجز لما كان عليه Webpack في الواقع جعلني أشك في أنه ربما لم يكن مناسبًا لي.
بالتعمق أكثر في وثائق Webpack ، تم تقديم تفسير أقل غموضًا ، "في جوهره ، webpack عبارة عن مجمّع وحدة ثابتة لتطبيقات JavaScript الحديثة".
أمم. وحدة تجميع ثابتة. هل هذا ما أردت؟ لم أكن مقتنعا. قرأت ولكن كلما قرأت أكثر ، كنت أقل وضوحًا. في ذلك الوقت ، فقدت مفاهيم مثل الرسوم البيانية التبعية ، وإعادة تحميل الوحدة النمطية الساخنة ، ونقاط الدخول بشكل أساسي.
بعد أمسيات من البحث في Webpack ، تخلت عن أي فكرة لاستخدامه.
أنا متأكد من أنه في الموقف الصحيح والأيدي الأكثر خبرة ، فإن Webpack قوي للغاية ومناسب ولكن يبدو أنه مبالغة كاملة في تلبية احتياجاتي المتواضعة. بدا تجميع الوحدات ، اهتزاز الشجرة ، وإعادة تحميل الوحدة الساخنة رائعًا ؛ لم أكن مقتنعًا أنني بحاجة إليهم من أجل "تطبيقي" الصغير.
لذا ، عد إلى Gulp إذن.
فيما يتعلق بموضوع عدم تغيير الأشياء من أجل التغيير ، هناك قطعة تقنية أخرى أردت تقييمها وهي الغزل عبر NPM لإدارة تبعيات المشروع. حتى تلك النقطة ، كنت دائمًا أستخدم NPM وكان يوصف Yarn كبديل أفضل وأسرع. ليس لدي الكثير لأقوله عن الغزل بخلاف ما إذا كنت تستخدم حاليًا NPM وكل شيء على ما يرام ، فلن تحتاج إلى عناء تجربة الغزل.
إحدى الأدوات التي وصلت متأخرة جدًا بالنسبة لي لتقييم هذا التطبيق هي Parceljs. مع وجود تكوين صفري و BrowserSync مثل إعادة تحميل المتصفح ، فقد وجدت منذ ذلك الحين فائدة كبيرة فيه! بالإضافة إلى ذلك ، في دفاع Webpack ، قيل لي أن الإصدار 4 وما بعده من Webpack لا يتطلب ملف تكوين. وفقًا للروايات المتناقلة ، في استطلاع حديث أجريته على Twitter ، من بين 87 مشاركًا ، اختار أكثر من نصفهم Webpack بدلاً من Gulp أو Parcel أو Grunt.
لقد بدأت ملف Gulp الخاص بي بوظائف أساسية للبدء والتشغيل.
ستراقب المهمة "الافتراضية" مجلدات "المصدر" الخاصة بأوراق الأنماط وملفات TypeScript وتجميعها في مجلد build
جنبًا إلى جنب مع HTML الأساسي وخرائط المصدر المرتبطة.
حصلت على BrowserSync يعمل مع Gulp أيضًا. قد لا أعرف ماذا أفعل بملف تكوين Webpack ولكن هذا لا يعني أنني كنت نوعًا من الحيوانات. إن الاضطرار إلى تحديث المتصفح يدويًا أثناء التكرار باستخدام HTML / CSS هو soooo 2010 ويمنحك BrowserSync تلك التعليقات القصيرة وحلقة التكرار المفيدة جدًا لتشفير الواجهة الأمامية.
هنا ملف gulp الأساسي اعتبارًا من 11.6.2017
يمكنك أن ترى كيف قمت بتعديل ملف Gulpfile بالقرب من نهاية الشحن ، مضيفًا تصغيرًا باستخدام ugilify:
هيكل المشروع
نتيجة لاختياراتي التقنية ، كانت بعض عناصر تنظيم الكود للتطبيق تحدد نفسها. gulpfile.js
في جذر المشروع ، node_modules
(حيث يخزن Gulp كود البرنامج المساعد) ، preCSS
أنماط التأليف ، ومجلد ts
لملفات TypeScript ، ومجلد build
للكود المترجم.
كانت الفكرة أن يكون لديك index.html
يحتوي على "غلاف" التطبيق ، بما في ذلك أي بنية HTML غير ديناميكية ثم روابط إلى الأنماط وملف JavaScript الذي من شأنه أن يجعل التطبيق يعمل. على القرص ، سيبدو مثل هذا:
build/ node_modules/ preCSS/ img/ partials/ styles.css ts/ .gitignore gulpfile.js index.html package.json tsconfig.json
إن تكوين BrowserSync للنظر في مجلد build
هذا يعني أنه يمكنني توجيه المستعرض الخاص بي إلى localhost:3000
وكان كل شيء جيدًا.
مع وجود نظام بناء أساسي في مكانه ، واستقر تنظيم الملفات وبعض التصميمات الأساسية للبدء بها ، فقد نفد علف التسويف الذي يمكنني استخدامه بشكل شرعي لمنعني من بناء الشيء بالفعل!
كتابة طلب
كان هذا هو مبدأ كيفية عمل التطبيق. سيكون هناك مخزن للبيانات. عند تحميل JavaScript ، سيتم تحميل تلك البيانات ، وتكرار الحلقات عبر كل مشغل في البيانات ، وإنشاء HTML المطلوب لتمثيل كل لاعب على شكل صف في التخطيط ووضعهم في قسم الإدخال / الإخراج المناسب. ثم تنقل التفاعلات من المستخدم اللاعب من حالة إلى أخرى. بسيط.
عندما يتعلق الأمر بكتابة التطبيق فعليًا ، كان التحديان المفاهيميان الكبيران اللذان يجب فهمهما هما:
- كيفية تمثيل البيانات لتطبيق بطريقة يمكن توسيعها ومعالجتها بسهولة ؛
- كيفية جعل واجهة المستخدم تتفاعل عند تغيير البيانات من إدخال المستخدم.
يعد تدوين الكائن من أبسط الطرق لتمثيل بنية البيانات في JavaScript. هذه الجملة تقرأ القليل من علوم الكمبيوتر- ذ. ببساطة أكثر ، "كائن" في لغة JavaScript هي طريقة سهلة لتخزين البيانات.
ضع في اعتبارك كائن JavaScript هذا المعين لمتغير يسمى ioState
(لحالة In / Out):
var ioState = { Count: 0, // Running total of how many players RosterCount: 0; // Total number of possible players ToolsExposed: false, // Whether the UI for the tools is showing Players: [], // A holder for the players }
إذا كنت لا تعرف JavaScript جيدًا ، يمكنك على الأقل فهم ما يحدث: كل سطر داخل الأقواس المتعرجة هو خاصية (أو "مفتاح" في لغة JavaScript) وزوج قيم. يمكنك ضبط كل أنواع الأشياء على مفتاح JavaScript. على سبيل المثال ، وظائف أو صفائف بيانات أخرى أو كائنات متداخلة. هذا مثال:
var testObject = { testFunction: function() { return "sausages"; }, testArray: [3,7,9], nestedtObject { key1: "value1", key2: 2, } }
والنتيجة النهائية هي أنه باستخدام هذا النوع من بنية البيانات ، يمكنك الحصول على أي من مفاتيح الكائن وتعيينها. على سبيل المثال ، إذا أردنا ضبط عدد كائن ioState على 7:
ioState.Count = 7;
إذا أردنا تعيين جزء من النص على تلك القيمة ، فإن التدوين يعمل على النحو التالي:
aTextNode.textContent = ioState.Count;
يمكنك أن ترى أن الحصول على القيم وتعيين القيم لكائن الحالة أمر بسيط في جانب JavaScript للأشياء. ومع ذلك ، فإن عكس تلك التغييرات في واجهة المستخدم أقل من ذلك. هذا هو المجال الرئيسي حيث تسعى الأطر والمكتبات إلى تجريد الألم.
بشكل عام ، عندما يتعلق الأمر بالتعامل مع تحديث واجهة المستخدم بناءً على الحالة ، فمن الأفضل تجنب الاستعلام عن DOM ، حيث يعتبر هذا عمومًا نهجًا دون المستوى الأمثل.
ضع في اعتبارك واجهة In / Out. يعرض عادةً قائمة باللاعبين المحتملين للعبة. يتم سردها عموديًا ، واحدة تحت الأخرى ، أسفل الصفحة.
ربما يتم تمثيل كل لاعب في DOM مع label
تغلف input
مربع الاختيار. بهذه الطريقة ، سيؤدي النقر فوق لاعب إلى تبديل اللاعب إلى "In" بموجب التسمية التي تجعل الإدخال "محددًا".
لتحديث واجهتنا ، قد يكون لدينا "مستمع" لكل عنصر إدخال في JavaScript. عند النقر أو التغيير ، تستعلم الوظيفة عن DOM وتحسب عدد إدخالات لاعبنا التي تم التحقق منها. على أساس هذا العدد ، سنقوم بعد ذلك بتحديث شيء آخر في DOM لنبين للمستخدم عدد اللاعبين الذين يتم فحصهم.
دعونا ننظر في تكلفة تلك العملية الأساسية. نحن نستمع إلى عقد DOM متعددة للنقر / التحقق من الإدخال ، ثم نستعلم عن DOM لمعرفة عدد أنواع DOM المعينة التي تم التحقق منها ، ثم نكتب شيئًا ما في DOM لإظهار المستخدم ، وواجهة المستخدم ، وعدد اللاعبين لقد عدنا للتو.
سيكون البديل هو الاحتفاظ بحالة التطبيق ككائن JavaScript في الذاكرة. يمكن لنقرة زر / إدخال في DOM أن تقوم فقط بتحديث كائن JavaScript ، وبعد ذلك ، بناءً على هذا التغيير في كائن JavaScript ، قم بإجراء تحديث لمرة واحدة لجميع تغييرات الواجهة المطلوبة. يمكننا تخطي الاستعلام عن DOM لحساب المشغلات لأن كائن JavaScript سيحتوي بالفعل على تلك المعلومات.
وبالتالي. بدا استخدام بنية كائن JavaScript للحالة بسيطًا ولكنه مرن بدرجة كافية لتغليف حالة التطبيق في أي وقت. بدت نظرية كيفية إدارة ذلك سليمة بدرجة كافية أيضًا - يجب أن يكون هذا هو ما تدور حوله عبارات مثل "تدفق البيانات في اتجاه واحد"؟ ومع ذلك ، فإن الحيلة الحقيقية الأولى ستكون في إنشاء بعض التعليمات البرمجية التي من شأنها تحديث واجهة المستخدم تلقائيًا بناءً على أي تغييرات تطرأ على تلك البيانات.
والخبر السار هو أن الأشخاص الأكثر ذكاءً مني قد اكتشفوا هذه الأشياء بالفعل ( الحمد لله! ). لقد كان الناس يتقنون أساليب مواجهة هذا النوع من التحدي منذ فجر التطبيقات. هذه الفئة من المشاكل هي الخبز والزبدة في "أنماط التصميم". بدا لي "نمط تصميم" اللقب مقصورًا على فئة معينة في البداية ، ولكن بعد البحث قليلاً ، بدأ كل شيء يبدو أقل من علوم الكمبيوتر وأكثر منطقية.
أنماط التصميم
يعد نمط التصميم ، في قاموس علوم الكمبيوتر ، طريقة محددة ومثبتة مسبقًا لحل أحد التحديات التقنية الشائعة. فكر في أنماط التصميم كمكافئ ترميز لوصفة طبخ.
ربما كان الكتاب الأكثر شهرة حول أنماط التصميم هو "أنماط التصميم: عناصر البرامج القابلة لإعادة الاستخدام الموجهة للكائنات" من عام 1994. على الرغم من أن هذا يتعامل مع C ++ و smalltalk ، فإن المفاهيم قابلة للتحويل. بالنسبة لجافا سكريبت ، تغطي "أنماط تصميم جافا سكريبت التعليمية" من Addy Osman أرضًا مماثلة. يمكنك أيضًا قراءتها عبر الإنترنت مجانًا هنا.
نمط المراقب
عادةً ما يتم تقسيم أنماط التصميم إلى ثلاث مجموعات: إبداعية وتركيبية وسلوكية. كنت أبحث عن شيء سلوكي يساعد في التعامل مع توصيل التغييرات حول الأجزاء المختلفة للتطبيق.
في الآونة الأخيرة ، رأيت وقراءة عمقًا رائعًا حقًا حول تنفيذ التفاعلية داخل أحد التطبيقات بواسطة Gregg Pollack. يوجد هنا منشور مدونة وفيديو لتستمتع به.
عند قراءة الوصف الافتتاحي لنمط "المراقب" في Learning JavaScript Design Patterns
، كنت متأكدًا تمامًا من أنه كان النمط بالنسبة لي. يتم وصفها على هذا النحو:
المراقب هو نمط تصميم حيث يحتفظ الكائن (المعروف باسم الموضوع) بقائمة من الكائنات اعتمادًا عليه (المراقبون) ، وإخطارهم تلقائيًا بأي تغييرات على الحالة.
عندما يحتاج موضوع ما إلى إخطار المراقبين بشأن حدث مثير للاهتمام ، فإنه يبث إخطارًا إلى المراقبين (والذي يمكن أن يتضمن بيانات محددة تتعلق بموضوع الإخطار).
كان مفتاح حماستي هو أن هذا يبدو أنه يقدم طريقة ما لتحديث الأشياء نفسها عند الحاجة.
لنفترض أن المستخدم نقر على لاعب يُدعى "بيتي" ليختار أنه "دخل" في اللعبة. قد يلزم حدوث بعض الأشياء في واجهة المستخدم:
- أضف 1 إلى عدد اللعب
- قم بإزالة Betty من مجموعة اللاعبين "Out"
- أضف Betty إلى مجموعة اللاعبين "In"
سيحتاج التطبيق أيضًا إلى تحديث البيانات التي تمثل واجهة المستخدم. ما كنت حريصًا جدًا على تجنبه هو:
playerName.addEventListener("click", playerToggle); function playerToggle() { if (inPlayers.includes(e.target.textContent)) { setPlayerOut(e.target.textContent); decrementPlayerCount(); } else { setPlayerIn(e.target.textContent); incrementPlayerCount(); } }
كان الهدف هو الحصول على تدفق بيانات أنيق يقوم بتحديث ما هو مطلوب في DOM عندما يتم تغيير البيانات المركزية وإذا تم تغييرها.
باستخدام نمط Observer ، كان من الممكن إرسال تحديثات إلى الحالة وبالتالي واجهة المستخدم بإيجاز تام. فيما يلي مثال على الوظيفة الفعلية المستخدمة لإضافة لاعب جديد إلى القائمة:
function itemAdd(itemString: string) { let currentDataSet = getCurrentDataSet(); var newPerson = new makePerson(itemString); io.items[currentDataSet].EventData.splice(0, 0, newPerson); io.notify({ items: io.items }); }
الجزء المتعلق بنمط المراقب هو طريقة io.notify
. نظرًا لأن هذا يوضح لنا تعديل جزء items
من حالة التطبيق ، دعني أوضح لك المراقب الذي استمع إلى التغييرات التي تم إجراؤها على "العناصر":
io.addObserver({ props: ["items"], callback: function renderItems() { // Code that updates anything to do with items... } });
لدينا طريقة إعلام تقوم بإجراء تغييرات على البيانات ثم المراقبون لتلك البيانات التي تستجيب عندما يتم تحديث الخصائص التي يهتمون بها.
باستخدام هذا النهج ، يمكن أن يكون للتطبيق مراقبون يراقبون التغييرات في أي خاصية للبيانات وتشغيل وظيفة كلما حدث تغيير.
إذا كنت مهتمًا بنمط المراقب الذي اخترته ، فسأصفه بشكل كامل هنا.
كان هناك الآن نهج لتحديث واجهة المستخدم بشكل فعال على أساس الحالة. خوخي. ومع ذلك ، لا يزال هذا يترك لي مع مسألتين صارختين.
كان أحدهما هو كيفية تخزين الحالة عبر عمليات إعادة تحميل الصفحة / الجلسات وحقيقة أنه على الرغم من عمل واجهة المستخدم ، إلا أنها لم تكن "تشبه التطبيق" بشكل مرئي. على سبيل المثال ، إذا تم الضغط على أحد الأزرار ، فستتغير واجهة المستخدم على الفور على الشاشة. لم يكن الأمر مقنعًا بشكل خاص.
دعونا نتعامل مع جانب التخزين للأشياء أولاً.
الدولة المنقذة
ركز اهتمامي الأساسي من جانب التطوير بالدخول إلى هذا على فهم كيفية إنشاء واجهات التطبيق وجعلها تفاعلية مع JavaScript. كانت كيفية تخزين البيانات واستردادها من الخادم أو معالجة مصادقة المستخدم وتسجيلات الدخول "خارج النطاق".
لذلك ، بدلاً من الاتصال بخدمة ويب لاحتياجات تخزين البيانات ، اخترت الاحتفاظ بجميع البيانات على العميل. هناك عدد من طرق منصة الويب لتخزين البيانات على العميل. اخترت localStorage
.
واجهة برمجة التطبيقات للتخزين المحلي بسيطة للغاية. يمكنك تعيين والحصول على بيانات مثل هذا:
// Set something localStorage.setItem("yourKey", "yourValue"); // Get something localStorage.getItem("yourKey");
يحتوي LocalStorage على طريقة setItem
التي تقوم بتمرير سلسلتين إليها. الأول هو اسم المفتاح الذي تريد تخزين البيانات به ، والسلسلة الثانية هي السلسلة الفعلية التي تريد تخزينها. تأخذ طريقة getItem
سلسلة نصية كوسيطة ترجع إليك كل ما تم تخزينه تحت هذا المفتاح في localStorage. جميل وبسيط.
ومع ذلك ، من بين أسباب عدم استخدام localStorage حقيقة أنه يجب حفظ كل شيء كـ "سلسلة". هذا يعني أنه لا يمكنك تخزين شيء مثل مصفوفة أو كائن بشكل مباشر. على سبيل المثال ، حاول تشغيل هذه الأوامر في وحدة تحكم المستعرض لديك:
// Set something localStorage.setItem("myArray", [1, 2, 3, 4]); // Get something localStorage.getItem("myArray"); // Logs "1,2,3,4"
على الرغم من أننا حاولنا تعيين قيمة "myArray" كمصفوفة ؛ عندما استرجعناها ، تم تخزينها كسلسلة (لاحظ علامات الاقتباس حول "1،2،3،4").
يمكنك بالتأكيد تخزين العناصر والمصفوفات باستخدام localStorage ولكن عليك أن تضع في اعتبارك أنها بحاجة إلى التحويل ذهابًا وإيابًا من السلاسل.
لذلك ، من أجل كتابة بيانات الحالة في localStorage ، تمت كتابتها إلى سلسلة باستخدام طريقة JSON.stringify()
مثل هذا:
const storage = window.localStorage; storage.setItem("players", JSON.stringify(io.items));
عندما احتاجت البيانات إلى الاسترداد من localStorage ، تم إعادة السلسلة إلى بيانات قابلة للاستخدام باستخدام طريقة JSON.parse()
مثل هذا:
const players = JSON.parse(storage.getItem("players"));
يعني استخدام localStorage
أن كل شيء كان على العميل وهذا يعني عدم وجود خدمات طرف ثالث أو مخاوف تتعلق بتخزين البيانات.
كانت البيانات الآن مستمرة في التحديث والجلسات - رائع! كانت الأخبار السيئة هي أن localStorage لا ينجو من قيام المستخدم بإفراغ بيانات متصفحه. عندما يقوم شخص ما بذلك ، ستفقد جميع بيانات In / Out الخاصة به. هذا عيب خطير.
ليس من الصعب إدراك أن "localStorage" ربما لا يكون الحل الأفضل للتطبيقات "المناسبة". إلى جانب مشكلة السلسلة المذكورة أعلاه ، فهي أيضًا بطيئة بالنسبة للعمل الجاد لأنها تمنع "الخيط الرئيسي". البدائل قادمة ، مثل KV Storage ولكن في الوقت الحالي ، قم بتدوين ملاحظة ذهنية لتحذير استخدامها بناءً على الملاءمة.
على الرغم من هشاشة حفظ البيانات محليًا على جهاز مستخدم ، تمت مقاومة الربط بخدمة أو قاعدة بيانات. بدلاً من ذلك ، تم حل المشكلة من خلال تقديم خيار "تحميل / حفظ". سيسمح هذا لأي مستخدم لـ In / Out بحفظ بياناته كملف JSON يمكن تحميله مرة أخرى في التطبيق إذا لزم الأمر.
لقد نجح هذا بشكل جيد على Android ولكن بشكل أقل أناقة لنظام iOS. على جهاز iPhone ، أدى ذلك إلى تدفق نصوص على الشاشة مثل هذا:
كما يمكنك أن تتخيل ، لم أكن وحدي في توبيخ Apple عبر WebKit حول هذا النقص. كان الخطأ ذي الصلة هنا.
في وقت كتابة هذا التقرير ، يحتوي هذا الخطأ على حل وتصحيح ولكنه لم يشق طريقه بعد إلى iOS Safari. يُزعم أن iOS13 يعمل على إصلاحه ولكنه في الإصدار التجريبي كما أكتب.
لذلك ، بالنسبة إلى الحد الأدنى من المنتج القابل للتطبيق ، كان هذا هو التخزين. حان الوقت الآن لمحاولة جعل الأشياء أكثر "شبيهة بالتطبيقات"!
تطبيق I-Ness
تبين بعد العديد من المناقشات مع العديد من الأشخاص ، أن تحديد ما تعنيه كلمة "تطبيق مثل" أمر صعب للغاية.
في النهاية ، استقرت على أن "يشبه التطبيق" مرادف للبراعة المرئية التي عادة ما تكون مفقودة من الويب. عندما أفكر في التطبيقات التي تشعر بالرضا عن استخدامها ، فإنها تتميز جميعها بالحركة. ليس بلا مبرر ، ولكن الحركة تضيف إلى قصة أفعالك. قد تكون انتقالات الصفحة بين الشاشات ، الطريقة التي تظهر بها القوائم. من الصعب وصفها بالكلمات ولكن معظمنا يعرفها عندما نراها.
كان الجزء الأول من الذوق البصري المطلوب هو نقل أسماء اللاعبين لأعلى أو لأسفل من "In" إلى "Out" والعكس صحيح عند تحديده. كان جعل اللاعب ينتقل على الفور من قسم إلى آخر أمرًا بسيطًا ولكنه بالتأكيد ليس "مثل التطبيق". من المأمول أن تؤكد الرسوم المتحركة عند النقر على اسم اللاعب نتيجة هذا التفاعل - انتقال اللاعب من فئة إلى أخرى.
مثل العديد من هذه الأنواع من التفاعلات المرئية ، فإن بساطتها الواضحة تدحض التعقيد الذي ينطوي عليه فعلاً جعلها تعمل بشكل جيد.
استغرق الأمر بعض التكرارات للحصول على الحركة بشكل صحيح ولكن المنطق الأساسي كان كما يلي:
- بمجرد النقر على "لاعب" ، التقط مكان وجود ذلك اللاعب ، هندسيًا ، على الصفحة ؛
- قم بقياس المسافة التي يحتاجها اللاعب للانتقال إلى الجزء العلوي من المنطقة إذا كان صاعدًا ("In") ومدى بُعد القاع ، إذا كان ينزل ("للخارج") ؛
- في حالة الصعود ، يجب ترك مساحة مساوية لارتفاع صف اللاعب بينما يتحرك اللاعب لأعلى ويجب أن ينهار اللاعبون أعلاه لأسفل بنفس معدل الوقت الذي يستغرقه اللاعب للسفر لأعلى للهبوط في الفضاء تم إخلاؤه من قبل اللاعبين الموجودين (إن وجد) الذين ينزلون ؛
- إذا كان اللاعب يخرج ويتحرك لأسفل ، فإن كل شيء آخر يحتاج إلى الانتقال لأعلى إلى المساحة المتبقية واللاعب يجب أن ينتهي به الأمر دون أي لاعب "خارج" حالي.
تفو! لقد كان الأمر أصعب مما كنت أعتقده في اللغة الإنجليزية - لا تهتم بجافا سكريبت!
كانت هناك تعقيدات إضافية للنظر فيها وتجربتها مثل سرعات الانتقال. في البداية ، لم يكن واضحًا ما إذا كانت سرعة الحركة الثابتة (على سبيل المثال 20 بكسل لكل 20 مللي ثانية) ، أو المدة الثابتة للحركة (على سبيل المثال 0.2 ثانية) ستبدو أفضل. كان الأول أكثر تعقيدًا بعض الشيء حيث كانت السرعة بحاجة إلى أن يتم حسابها "أثناء الطيران" بناءً على المسافة التي يحتاجها اللاعب للسفر - مسافة أكبر تتطلب فترة انتقال أطول.
ومع ذلك ، اتضح أن فترة الانتقال الثابتة لم تكن مجرد أبسط في الكود ؛ لقد أحدثت تأثيرًا أكثر ملاءمة. كان الاختلاف طفيفًا ولكن هذه هي نوع الخيارات التي لا يمكنك تحديدها إلا بعد رؤية كلا الخيارين.
في كثير من الأحيان أثناء محاولة تثبيت هذا التأثير ، كان هناك خلل بصري يلفت الأنظار ولكن كان من المستحيل التفكيك في الوقت الفعلي. لقد وجدت أن أفضل عملية تصحيح أخطاء كانت إنشاء تسجيل QuickTime للرسوم المتحركة ثم المرور عبرها في إطار في كل مرة. لقد كشف هذا دائمًا عن المشكلة بشكل أسرع من أي تصحيح أخطاء يعتمد على الكود.
بالنظر إلى الكود الآن ، يمكنني أن أقدر أنه في شيء ما يتجاوز تطبيقي المتواضع ، من شبه المؤكد أنه يمكن كتابة هذه الوظيفة بشكل أكثر فعالية. نظرًا لأن التطبيق سيعرف عدد المشغلات ويعرف الارتفاع الثابت للشرائح ، يجب أن يكون من الممكن تمامًا إجراء جميع حسابات المسافة في JavaScript وحدها ، دون قراءة DOM.
لا يعني ذلك أن ما تم شحنه لا يعمل ، إنه ليس نوع حل الكود الذي ستعرضه على الإنترنت. اه انتظر.
كانت تفاعلات "التطبيقات المشابهة" الأخرى أسهل بكثير في التنفيذ. بدلاً من القوائم التي تدخل وتخرج ببساطة بشيء بسيط مثل تبديل خاصية العرض ، تم اكتساب الكثير من الأميال من خلال تعريضها ببساطة بمزيد من البراعة. كان لا يزال يتم تشغيله ببساطة ولكن CSS كانت تقوم بكل الرفع الثقيل:
.io-EventLoader { position: absolute; top: 100%; margin-top: 5px; z-index: 100; width: 100%; opacity: 0; transition: all 0.2s; pointer-events: none; transform: translateY(-10px); [data-evswitcher-showing="true"] & { opacity: 1; pointer-events: auto; transform: none; } }
هناك عندما تم تبديل السمة data-evswitcher-showing="true"
على عنصر أصلي ، فإن القائمة تتلاشى وتتحول مرة أخرى إلى موضعها الافتراضي ويتم إعادة تمكين أحداث المؤشر حتى يمكن أن تتلقى القائمة نقرات.
منهجية ورقة نمط ECSS
ستلاحظ في هذا الرمز السابق أنه من وجهة نظر التأليف ، يتم دمج تجاوزات CSS داخل محدد رئيسي. هذه هي الطريقة التي أفضّل بها دائمًا كتابة أوراق أنماط واجهة المستخدم ؛ مصدر واحد للحقيقة لكل محدد وأي تجاوزات لذلك المحدد مغلف ضمن مجموعة واحدة من الأقواس. إنه نمط يتطلب استخدام معالج CSS (Sass ، PostCSS ، LESS ، Stylus ، وآخرون) ولكني أشعر أن الطريقة الإيجابية الوحيدة للاستفادة من وظائف التداخل.
لقد عززت هذا النهج في كتابي ، Enduring CSS وعلى الرغم من وجود عدد كبير من الأساليب الأكثر انخراطًا المتاحة لكتابة CSS لعناصر الواجهة ، فقد خدمني ECSS وفرق التطوير الكبيرة التي أعمل معها جيدًا منذ أن تم توثيق النهج لأول مرة. مرة أخرى في عام 2014! أثبتت فعاليتها في هذه الحالة.
Partialling The TypeScript
حتى بدون معالج CSS أو لغة مجموعة شاملة مثل Sass ، فإن CSS لديها القدرة على استيراد واحد أو أكثر من ملفات CSS إلى ملف آخر باستخدام توجيه الاستيراد:
@import "other-file.css";
عند البدء بجافا سكريبت فوجئت بعدم وجود مكافئ. عندما تصبح ملفات التعليمات البرمجية أطول من شاشة أو عالية جدًا ، يبدو دائمًا أن تقسيمها إلى أجزاء أصغر سيكون مفيدًا.
ومن المزايا الأخرى لاستخدام TypeScript هو أنه يحتوي على طريقة بسيطة وجميلة لتقسيم التعليمات البرمجية إلى ملفات واستيرادها عند الحاجة.
كانت هذه الإمكانية قديمة لوحدات JavaScript الأصلية وكانت ميزة راحة كبيرة. عندما تم تجميع TypeScript ، تم تجميعه مرة أخرى في ملف JavaScript واحد. كان هذا يعني أنه كان من الممكن تقسيم رمز التطبيق بسهولة إلى ملفات جزئية يمكن إدارتها للتأليف والاستيراد ثم إلى الملف الرئيسي بسهولة. بدا الجزء العلوي من inout.ts
الرئيسي كما يلي:
/// <reference path="defaultData.ts" /> /// <reference path="splitTeams.ts" /> /// <reference path="deleteOrPaidClickMask.ts" /> /// <reference path="repositionSlat.ts" /> /// <reference path="createSlats.ts" /> /// <reference path="utils.ts" /> /// <reference path="countIn.ts" /> /// <reference path="loadFile.ts" /> /// <reference path="saveText.ts" /> /// <reference path="observerPattern.ts" /> /// <reference path="onBoard.ts" />
ساعدت مهمة التدبير المنزلي والتنظيم البسيطة هذه بشكل كبير.
أحداث متعددة
في البداية ، شعرت أنه من وجهة نظر وظيفية ، فإن حدثًا واحدًا ، مثل "كرة القدم في ليلة الثلاثاء" سيكون كافياً. في هذا السيناريو ، إذا قمت بتحميل In / Out up ، فأنت قد أضفت / أزلت أو نقلت اللاعبين إلى الداخل أو الخارج وكان هذا هو الحال. لم يكن هناك فكرة عن أحداث متعددة.
سرعان ما قررت أن (حتى لو كنت أذهب إلى الحد الأدنى من المنتجات القابلة للتطبيق) من شأنه أن يجعل تجربة محدودة للغاية. ماذا لو نظم شخص ما مباراتين في أيام مختلفة بقائمة مختلفة من اللاعبين؟ من المؤكد أن In / Out يمكن / يجب أن يلبي هذه الحاجة؟ لم يستغرق الأمر وقتًا طويلاً لإعادة تشكيل البيانات لجعل ذلك ممكنًا وتعديل الطرق اللازمة للتحميل في مجموعة مختلفة.
في البداية ، بدت مجموعة البيانات الافتراضية كما يلي:
var defaultData = [ { name: "Daz", paid: false, marked: false, team: "", in: false }, { name: "Carl", paid: false, marked: false, team: "", in: false }, { name: "Big Dave", paid: false, marked: false, team: "", in: false }, { name: "Nick", paid: false, marked: false, team: "", in: false } ];
مصفوفة تحتوي على عنصر لكل لاعب.
بعد احتساب أحداث متعددة في الاعتبار ، تم تعديله ليبدو كما يلي:
var defaultDataV2 = [ { EventName: "Tuesday Night Footy", Selected: true, EventData: [ { name: "Jack", marked: false, team: "", in: false }, { name: "Carl", marked: false, team: "", in: false }, { name: "Big Dave", marked: false, team: "", in: false }, { name: "Nick", marked: false, team: "", in: false }, { name: "Red Boots", marked: false, team: "", in: false }, { name: "Gaz", marked: false, team: "", in: false }, { name: "Angry Martin", marked: false, team: "", in: false } ] }, { EventName: "Friday PM Bank Job", Selected: false, EventData: [ { name: "Mr Pink", marked: false, team: "", in: false }, { name: "Mr Blonde", marked: false, team: "", in: false }, { name: "Mr White", marked: false, team: "", in: false }, { name: "Mr Brown", marked: false, team: "", in: false } ] }, { EventName: "WWII Ladies Baseball", Selected: false, EventData: [ { name: "C Dottie Hinson", marked: false, team: "", in: false }, { name: "P Kit Keller", marked: false, team: "", in: false }, { name: "Mae Mordabito", marked: false, team: "", in: false } ] } ];
كانت البيانات الجديدة عبارة عن مصفوفة تحتوي على كائن لكل حدث. ثم في كل حدث كانت خاصية EventData
كانت عبارة عن مصفوفة بها كائنات لاعب كما كان من قبل.
استغرق الأمر وقتًا أطول بكثير لإعادة النظر في كيفية تعامل الواجهة بشكل أفضل مع هذه الإمكانية الجديدة.
منذ البداية ، كان التصميم دائمًا معقمًا جدًا. بالنظر إلى أنه كان من المفترض أيضًا أن يكون تمرينًا في التصميم ، لم أشعر أنني كنت شجاعًا بما فيه الكفاية. لذلك تمت إضافة المزيد من الذوق البصري ، بدءًا من الرأس. هذا ما سخرت منه في Sketch:
لم تكن ستفوز بجوائز لكنها كانت بالتأكيد مثيرة للإعجاب أكثر من حيث بدأت.
بصرف النظر عن الجوانب الجمالية ، لم يكن الأمر كذلك إلا بعد أن أشار إليها شخص آخر ، حيث أقدر أن رمز زائد كبير في العنوان كان محيرًا للغاية. يعتقد معظم الناس أنها كانت وسيلة لإضافة حدث آخر. في الواقع ، تحولت إلى وضع "إضافة لاعب" مع انتقال خيالي يتيح لك كتابة اسم اللاعب في نفس المكان الذي كان فيه اسم الحدث حاليًا.
كان هذا مثالًا آخر حيث كانت العيون الجديدة لا تقدر بثمن. لقد كان أيضًا درسًا مهمًا في التخلي عنه. الحقيقة الصادقة هي أنني كنت متمسكًا بانتقال وضع الإدخال في الرأس لأنني شعرت أنه رائع وذكي. ومع ذلك ، فإن الحقيقة هي أنها لا تخدم التصميم وبالتالي التطبيق ككل.
تم تغيير هذا في النسخة الحية. بدلاً من ذلك ، يتعامل العنوان فقط مع الأحداث - وهو سيناريو أكثر شيوعًا. وفي الوقت نفسه ، تتم إضافة اللاعبين من قائمة فرعية. يمنح هذا التطبيق تسلسلًا هرميًا أكثر قابلية للفهم.
الدرس الآخر المستفاد هنا هو أنه كلما كان ذلك ممكنًا ، من المفيد جدًا الحصول على تعليقات صريحة من الزملاء. إذا كانوا أشخاصًا جيدين وصادقين ، فلن يسمحوا لك بتمرير نفسك!
ملخص: كود بلدي ينتن
حق. حتى الآن ، قطعة فنية عادية بأثر رجعي ؛ هذه الأشياء هي عشرة بنس على المتوسط! تسير الصيغة على هذا النحو: يشرح المطور بالتفصيل كيفية تحطيم جميع العقبات التي تحول دون إطلاق برنامج مضبوط بدقة في الإنترنت ثم إجراء مقابلة في Google أو الحصول على وظيفة مكتسبة في مكان ما. ومع ذلك ، فإن حقيقة الأمر هي أنني كنت أول مرة في malarkey لبناء التطبيقات ، لذا تم شحن الكود في النهاية باعتباره التطبيق "النهائي" الذي ينتقل إلى السماء!
على سبيل المثال ، نجح تطبيق نمط المراقب المستخدم بشكل جيد للغاية. لقد كنت منظمًا ومنهجيًا في البداية ، لكن هذا النهج "ذهب جنوبًا" حيث أصبحت أكثر يأسًا لإنهاء الأمور. مثل اتباع نظام غذائي متسلسل ، تسللت العادات القديمة المألوفة مرة أخرى وانخفضت جودة الشفرة لاحقًا.
Looking now at the code shipped, it is a less than ideal hodge-bodge of clean observer pattern and bog-standard event listeners calling functions. In the main inout.ts
file there are over 20 querySelector
method calls; hardly a poster child for modern application development!
I was pretty sore about this at the time, especially as at the outset I was aware this was a trap I didn't want to fall into. However, in the months that have since passed, I've become more philosophical about it.
The final post in this series reflects on finding the balance between silvery-towered code idealism and getting things shipped. It also covers the most important lessons learned during this process and my future aspirations for application development.