UI-Framework in 5 Minuten


Vor einiger Zeit habe ich mich gefragt, warum es so viele UI-Frameworks für das Web gibt. Ich bin schon lange in der IT tätig und kann mich nicht erinnern, dass UI-Bibliotheken auf anderen Plattformen mit der gleichen Geschwindigkeit wie in WEB geboren wurden und starben. Bibliotheken für Desktop-Betriebssysteme wie MFC, Qt, WPF usw. - waren Monster, die sich im Laufe der Jahre entwickelten und nicht viele Alternativen hatten. Im Web ist alles anders - Frameworks werden fast jede Woche veröffentlicht, Führungskräfte wechseln - warum passiert das?


Ich denke, der Hauptgrund ist, dass die Komplexität beim Schreiben von UI-Bibliotheken stark abgenommen hat. Ja, um eine Bibliothek zu schreiben, die viele verwenden werden - es erfordert immer noch viel Zeit und Fachwissen, aber um einen Prototyp zu schreiben - der, wenn er in eine praktische API eingebunden ist, einsatzbereit ist - dauert es sehr wenig Zeit. Wenn Sie daran interessiert sind, wie dies getan werden kann, lesen Sie weiter.


Warum dieser Artikel?


Zu einer Zeit auf Habré gab es eine Reihe von Artikeln - um X für 30 Codezeilen auf js zu schreiben.


Ich dachte - ist es möglich, eine Reaktion in 30 Zeilen zu schreiben? Ja, für 30 Zeilen war ich nicht erfolgreich, aber das Endergebnis entspricht dieser Zahl.


Im Allgemeinen ist der Zweck des Artikels rein pädagogisch. Dies kann zu einem etwas tieferen Verständnis des Prinzips des UI-Frameworks beitragen, das auf dem virtuellen Haus basiert. In diesem Artikel möchte ich zeigen, wie einfach es ist, ein anderes UI-Framework basierend auf einem virtuellen Zuhause zu erstellen.


Am Anfang möchte ich sagen, was ich mit dem UI-Framework meine - weil viele unterschiedliche Meinungen dazu haben. Einige glauben beispielsweise, dass Angular und Ember ein UI-Framework sind und React nur eine Bibliothek, die die Arbeit mit dem Ansichtsteil der Anwendung erleichtert.


Wir definieren das UI-Framework wie folgt: Dies ist eine Bibliothek, die beim Erstellen / Aktualisieren / Löschen von Seiten oder einzelnen Seitenelementen in diesem Sinne hilft. Eine ziemlich große Anzahl von Wrappern über die DOM-API kann sich als UI-Framework herausstellen. Die einzige Frage sind die Abstraktionsoptionen (API), die diese Bibliothek zum Bearbeiten des DOM bereitstellt und in der Wirksamkeit dieser Manipulationen


In der vorgeschlagenen Formulierung ist - React ein ziemliches UI-Framework.


Nun, mal sehen, wie Sie Ihre Reaktion mit Blackjack und mehr schreiben. React verwendet bekanntermaßen das Konzept eines virtuellen Hauses. In vereinfachter Form besteht es darin, dass die Knoten des realen DOM in strikter Übereinstimmung mit den Knoten des zuvor erstellten virtuellen DOM-Baums erstellt werden. Eine direkte Manipulation des realen DOM ist nicht erwünscht. Wenn Sie Änderungen am realen DOM vornehmen müssen, die Änderungen am virtuellen DOM vorgenommen werden, wird die neue Version des virtuellen DOM mit der alten Version verglichen, die Änderungen werden gesammelt, die auf das reale DOM angewendet werden müssen, und sie werden so angewendet, dass die Interaktion mit dem realen DOM minimiert wird DOM - das macht die Anwendung optimaler.


Da der virtuelle Hausbaum ein gewöhnliches Java-Skriptobjekt ist - es ist ziemlich einfach zu manipulieren - seine Knoten zu ändern / zu vergleichen, verstehe ich hier, dass der Assembler-Code virtuell, aber recht einfach ist und teilweise von einem Präprozessor aus einer deklarativen Sprache einer höheren JSX-Ebene generiert werden kann.


Beginnen wir mit JSX


Dies ist ein Beispiel für JSX-Code


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

Wir müssen ein solches virtuelles DOM erstellen, wenn wir die Component aufrufen


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

Natürlich werden wir diese Transformation nicht manuell schreiben, wir werden dieses Plugin verwenden , das Plugin ist veraltet, aber es ist einfach genug, um zu verstehen, wie alles funktioniert. Es verwendet jsx-transform , das JSX wie folgt konvertiert:


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

Alles, was wir tun müssen, ist den vdom-Konstruktor der h-Knoten zu implementieren, eine Funktion, die rekursiv virtuelle DOM-Knoten erstellt. Im Falle einer Reaktion führt die React.createElement-Funktion dies aus. Nachfolgend finden Sie eine primitive Implementierung einer solchen Funktion


 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 } 

Natürlich verkompliziert die Rekursion den Code hier ein wenig, aber ich hoffe, es ist klar, jetzt können wir mit dieser Funktion vdom erstellen


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

und so für Knoten einer Verschachtelung


Großartig, jetzt gibt unsere Komponentenfunktion den vdom-Knoten zurück.


Jetzt wird der Teil sein, wir müssen eine patch Funktion schreiben, die das Root-DOM-Element der Anwendung, das alte Vdom, das neue Vdom verwendet und die Knoten des realen DOM gemäß dem neuen Vdom aktualisiert.


Vielleicht können Sie diesen Code einfacher schreiben, aber es stellte sich heraus, dass ich den Code aus dem Picodom-Paket als Grundlage genommen habe


 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 } 

Diese naive Implementierung, die schrecklich nicht optimal ist, berücksichtigt nicht die Bezeichner der Elemente (Schlüssel, ID) - um die erforderlichen Elemente in den Listen korrekt zu aktualisieren, funktioniert sie jedoch in primitiven Fällen einwandfrei.


Die Implementierung der createElement updateElement removeElement ich bringe sie nicht hierher, es ist bemerkenswert, jeder, der interessiert ist, kann die Quelle hier sehen .


Es gibt die einzige Einschränkung: Wenn die Werteigenschaften für input aktualisiert werden, sollte der Vergleich nicht mit dem alten vnode, sondern mit dem value Attribut im realen Haus durchgeführt werden. Dadurch wird verhindert, dass das aktive Element diese Eigenschaft aktualisiert (da es dort bereits aktualisiert wurde), und es werden Probleme mit dem Cursor vermieden und Auswahl.


Nun, das ist alles, jetzt müssen wir nur noch diese Teile zusammenfügen und das UI-Framework schreiben
Wir halten uns innerhalb von 5 Zeilen .


  1. Wie in React benötigen wir zum Erstellen der Anwendung 3 Parameter
    export function app(selector, view, initProps) {
    Selektor - Root-Dom-Selektor, in dem die Anwendung gemountet wird (standardmäßig 'body')
    view - eine Funktion, die den Root-VNode erstellt
    initProps - Eigenschaften der ersten Anwendung
  2. Nehmen Sie das Stammelement in das DOM
    const rootElement = document.querySelector(selector || 'body')
  3. Wir sammeln vdom mit anfänglichen Eigenschaften
    let node = view(initProps)
  4. Wir mounten das empfangene Vdom im DOM als das alte Vdom, das wir für null halten
    patch(rootElement, null, node)
  5. Wir geben die Anwendungsaktualisierungsfunktion mit neuen Eigenschaften zurück
    return props => patch(rootElement, node, (node = view(props)))

Framework ist fertig!


'Hallo Welt' in diesem Framework sieht folgendermaßen aus:


 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') 

Diese Bibliothek unterstützt wie React das Zusammensetzen von Komponenten sowie das Hinzufügen und Entfernen von Komponenten zur Laufzeit, sodass sie als UI-Framework betrachtet werden kann. Einen etwas komplexeren Anwendungsfall finden Sie hier ToDo-Beispiel .


Natürlich gibt es in dieser Bibliothek viele Dinge: Lebenszyklusereignisse (obwohl es nicht schwierig ist, sie zu befestigen, verwalten wir selbst das Erstellen / Aktualisieren / Löschen von Knoten), separate Aktualisierungen von untergeordneten Knoten wie this.setState (dazu müssen Sie für jedes Element Links zu DOM-Elementen speichern vdom-Knoten - dies wird die Logik etwas komplizieren), PatchElement-Code ist schrecklich nicht optimal, funktioniert bei einer großen Anzahl von Elementen nicht gut, verfolgt keine Elemente mit einem Bezeichner usw.


In jedem Fall wurde die Bibliothek für Bildungszwecke entwickelt - verwenden Sie sie nicht in der Produktion :)


PS: Ich wurde von der großartigen Hyperapp- Bibliothek für diesen Artikel inspiriert, ein Teil des Codes wurde von dort übernommen.


Gute Codierung!

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


All Articles