إطار عمل واجهة المستخدم في 5 دقائق


تساءلت منذ بعض الوقت عن سبب وجود العديد من أطر واجهة المستخدم للويب؟ لقد كنت في مجال تكنولوجيا المعلومات لفترة طويلة ولا أتذكر أن مكتبات واجهة المستخدم على الأنظمة الأساسية الأخرى قد ولدت وماتت بنفس السرعة كما في WEB. مكتبات لنظام تشغيل سطح المكتب ، مثل: MFC و Qt و WPF وما إلى ذلك. - كانت الوحوش التي تطورت على مر السنين وليس لديها عدد كبير من البدائل. كل شيء مختلف على الويب - يتم إصدار الأطر كل أسبوع تقريبًا ، ويغير القادة - لماذا يحدث هذا؟


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


لماذا هذه المقالة؟


في وقت ما على حبري ، كانت هناك سلسلة من المقالات - لكتابة X لـ 30 سطرًا من التعليمات البرمجية على js.


فكرت - هل من الممكن كتابة رد فعل في 30 سطرا؟ نعم ، بالنسبة لـ 30 سطرًا ، لم أنجح ، لكن النتيجة النهائية تتناسب تمامًا مع هذا الرقم.


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


في البداية ، أود أن أقول ما أعنيه بإطار واجهة المستخدم - لأن الكثيرين لديهم آراء مختلفة حول هذا الموضوع. على سبيل المثال ، يعتقد البعض أن Angular و Ember هو إطار عمل لواجهة المستخدم وأن React هي مجرد مكتبة ستسهل العمل مع جزء العرض من التطبيق.


نحدد إطار عمل واجهة المستخدم على النحو التالي: هذه مكتبة تساعد على إنشاء / تحديث / حذف صفحات أو عناصر صفحة فردية بهذا المعنى ، قد يتحول نطاق واسع إلى حد ما من الأغلفة عبر DOM API إلى إطار عمل واجهة المستخدم ، والسؤال الوحيد هو خيارات التجريد (API) التي توفرها هذه المكتبة للتلاعب في DOM وفعالية هذه التلاعبات


في الصياغة المقترحة - رد الفعل هو إطار عمل لواجهة المستخدم تمامًا.


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


نظرًا لأن شجرة المنزل الافتراضية هي كائن نص جافا عادي - من السهل جدًا التعامل معها - تغيير / مقارنة العقد ، بكلمة من السهل هنا أن أفهم أن رمز التجميع افتراضي ولكنه بسيط جدًا ويمكن إنشاؤه جزئيًا بواسطة معالج مسبق من لغة تعريفية بمستوى أعلى من JSX.


لنبدأ بـ JSX


هذا مثال على كود JSX


const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component 

نحن بحاجة إلى إنشاء مثل DOM الظاهري عند استدعاء وظيفة Component


 const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] } 

بالطبع ، لن نكتب هذا التحول يدويًا ، سنستخدم هذا المكون الإضافي ، المكون الإضافي قديم ، ولكنه بسيط بما يكفي لمساعدتنا على فهم كيفية عمل كل شيء. يستخدم تحويل jsx ، والذي يحول JSX على النحو التالي :


 jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])' 

لذلك ، كل ما نحتاجه هو تنفيذ مُنشئ vdom للعقد h ، وهي وظيفة ستنشئ بشكل متكرر عقد DOM افتراضية ، في حالة التفاعل ، تقوم وظيفة React.createElement بذلك. فيما يلي تنفيذ بدائي لمثل هذه الوظيفة


 export function h(type, props, ...stack) { const children = (stack || []).reduce(addChild, []) props = props || {} return typeof type === "string" ? { type, props, children } : type(props, children) } function addChild(acc, node) { if (Array.isArray(node)) { acc = node.reduce(addChild, acc) } else if (null == node || true === node || false === node) { } else { acc.push(typeof node === "number" ? node + "" : node) } return acc } 

بالطبع ، العودية تعقد الكود قليلاً هنا ، ولكن آمل أن يكون واضحًا ، الآن مع هذه الوظيفة يمكننا بناء vdom


 'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']} 

وهكذا بالنسبة للعقد من أي تداخل


رائع ، الآن تقوم دالة Component بإرجاع عقدة vdom.


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


ربما يمكنك كتابة هذا الرمز بشكل أسهل ، ولكن اتضح أنه لذلك أخذت الرمز من حزمة picodom كأساس


 export function patch(parent, oldNode, newNode) { return patchElement(parent, parent.children[0], oldNode, newNode) } function patchElement(parent, element, oldNode, node, isSVG, nextSibling) { if (oldNode == null) { element = parent.insertBefore(createElement(node, isSVG), element) } else if (node.type != oldNode.type) { const oldElement = element element = parent.insertBefore(createElement(node, isSVG), oldElement) removeElement(parent, oldElement, oldNode) } else { updateElement(element, oldNode.props, node.props) isSVG = isSVG || node.type === "svg" let childNodes = [] ; (element.childNodes || []).forEach(element => childNodes.push(element)) let oldNodeIdex = 0 if (node.children && node.children.length > 0) { for (var i = 0; i < node.children.length; i++) { if (oldNode.children && oldNodeIdex <= oldNode.children.length && (node.children[i].type && node.children[i].type === oldNode.children[oldNodeIdex].type || (!node.children[i].type && node.children[i] === oldNode.children[oldNodeIdex])) ) { patchElement(element, childNodes[oldNodeIdex], oldNode.children[oldNodeIdex], node.children[i], isSVG) oldNodeIdex++ } else { let newChild = element.insertBefore( createElement(node.children[i], isSVG), childNodes[oldNodeIdex] ) patchElement(element, newChild, {}, node.children[i], isSVG) } } } for (var i = oldNodeIdex; i < childNodes.length; i++) { removeElement(element, childNodes[i], oldNode.children ? oldNode.children[i] || {} : {}) } } return element } 

هذا التطبيق الساذج ، ليس مثاليًا بشكل رهيب ، ولا يأخذ في الاعتبار معرفات العناصر (المفتاح ، المعرف) - لتحديث العناصر الضرورية في القوائم بشكل صحيح ، ولكن في الحالات البدائية يعمل بشكل جيد.


تنفيذ createElement updateElement removeElement أنا لا أحضرها هنا ، من الجدير بالملاحظة ، يمكن لأي شخص مهتم مشاهدة المصدر هنا .


هناك التحذير الوحيد - عندما يتم تحديث خصائص value لعناصر input ، لا ينبغي أن تتم المقارنة مع vnode القديم ولكن مع سمة value في المنزل الحقيقي - سيمنع هذا العنصر النشط من تحديث هذه الخاصية (نظرًا لأنه تم تحديثه بالفعل هناك) ويمنع حدوث مشكلات في المؤشر والاختيار.


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


  1. كما هو الحال في React ، نحتاج إلى 3 معلمات لإنشاء التطبيق
    export function app(selector, view, initProps) {
    selector - محدد دوم الجذر الذي سيتم تحميل التطبيق فيه (افتراضيًا "body")
    عرض - وظيفة تقوم ببناء الجذر vnode
    initProps - خصائص التطبيق الأولية
  2. خذ العنصر الجذر في DOM
    const rootElement = document.querySelector(selector || 'body')
  3. نجمع vdom مع الخصائص الأولية
    let node = view(initProps)
  4. نقوم بتركيب vdom المستلمة في DOM حيث أن vdom القديم الذي نأخذه فارغًا
    patch(rootElement, null, node)
  5. نعيد وظيفة تحديث التطبيق بخصائص جديدة
    return props => patch(rootElement, node, (node = view(props)))

الإطار جاهز!


سيبدو "Hello world" في هذا الإطار كما يلي:


 import { h, app } from "../src/index" function view(state) { return ( <div> <h2>{`Hello ${state}`}</h2> <input value={state} oninput={e => render(e.target.value)} /> </div> ) } const render = app('body', view, 'world') 

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


بالطبع ، هناك الكثير من الأشياء في هذه المكتبة: أحداث دورة الحياة (على الرغم من أنه ليس من الصعب ربطها ، فنحن أنفسنا ندير إنشاء / تحديث / حذف العقد) ، تحديثات منفصلة للعقد الفرعية مثل this.setState (لهذا تحتاج إلى حفظ الروابط إلى عناصر DOM لكل منها عقدة vdom - سيؤدي هذا إلى تعقيد المنطق قليلاً) ، رمز patchElement هو غير مثالي بشكل رهيب ، لن يعمل بشكل جيد على عدد كبير من العناصر ، لا يتتبع العناصر بمعرف ، إلخ.


على أي حال ، تم تطوير المكتبة للأغراض التعليمية - لا تستخدمها في الإنتاج :)


PS: لقد استلهمت من مكتبة Hyperapp الرائعة لهذه المقالة ، وقد تم أخذ جزء من الرمز من هناك.


ترميز جيد!

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


All Articles