UI framework en 5 minutos


Hace alg煤n tiempo, me preguntaba por qu茅 hay tantos marcos de interfaz de usuario para la web. He estado en TI durante mucho tiempo y no recuerdo que las bibliotecas de interfaz de usuario en otras plataformas nacieron y murieron a la misma velocidad que en WEB. Bibliotecas para SO de escritorio, tales como: MFC, Qt, WPF, etc. - eran monstruos que se desarrollaron a lo largo de los a帽os y no ten铆an una gran cantidad de alternativas. Todo es diferente en la Web: los marcos se lanzan casi todas las semanas, los l铆deres cambian.


Creo que la raz贸n principal es que la complejidad de escribir bibliotecas de interfaz de usuario ha disminuido considerablemente. S铆, para escribir una biblioteca que muchos usar谩n, a煤n requiere mucho tiempo y experiencia, pero para escribir un prototipo, que, cuando est谩 envuelto en una API conveniente, estar谩 listo para usar, toma muy poco tiempo. Si est谩 interesado en c贸mo se puede hacer esto, siga leyendo.


驴Por qu茅 este art铆culo?


Hubo un tiempo en Habr茅 que hab铆a una serie de art铆culos: escribir X para 30 l铆neas de c贸digo en js.


Pens茅: 驴es posible escribir una reacci贸n en 30 l铆neas? S铆, durante 30 l铆neas no tuve 茅xito, pero el resultado final es bastante acorde con esta cifra.


En general, el prop贸sito del art铆culo es puramente educativo. Puede ayudar a comprender un poco m谩s el principio del marco de UI basado en la casa virtual. En este art铆culo quiero mostrar cu谩n simple es hacer otro marco de interfaz de usuario basado en un hogar virtual.


Al principio quiero decir lo que quiero decir con el marco de la interfaz de usuario, porque muchos tienen opiniones diferentes sobre esto. Por ejemplo, algunos creen que Angular and Ember es un marco de interfaz de usuario y React es solo una biblioteca que facilitar谩 el trabajo con la parte de vista de la aplicaci贸n.


Definimos el marco de la interfaz de usuario de la siguiente manera: esta es una biblioteca que ayuda a crear / actualizar / eliminar p谩ginas o elementos de p谩gina individuales en este sentido, una gama bastante amplia de envoltorios sobre la API DOM puede resultar ser el marco de la interfaz de usuario, la 煤nica pregunta son las opciones de abstracci贸n (API) que esta biblioteca proporciona para manipular el DOM y en la efectividad de estas manipulaciones


En la redacci贸n propuesta: React es un marco de interfaz de usuario bastante.


Bueno, veamos c贸mo escribir su React con blackjack y m谩s. Se sabe que React usa el concepto de un hogar virtual. En una forma simplificada, consiste en el hecho de que los nodos del DOM real se construyen en estricta conformidad con los nodos del 谩rbol DOM virtual construido previamente. La manipulaci贸n directa del DOM real no es bienvenida, si necesita realizar cambios en el DOM real, los cambios se realizan en el DOM virtual, luego se compara la nueva versi贸n del DOM virtual con la anterior, se recopilan los cambios que deben aplicarse al DOM real y se aplican de tal manera que se minimice la interacci贸n con el DOM real. DOM: lo que hace que la aplicaci贸n sea m谩s 贸ptima.


Dado que el 谩rbol de la casa virtual es un objeto ordinario de Java-Script, es bastante f谩cil manipularlo, cambiar / comparar sus nodos, por la palabra es f谩cil aqu铆, entiendo que el c贸digo de ensamblaje es virtual pero bastante simple y puede ser generado parcialmente por un preprocesador a partir de un lenguaje declarativo de un nivel superior JSX.


Comencemos con JSX


Este es un ejemplo de c贸digo JSX


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

necesitamos hacer un DOM virtual al llamar a la funci贸n Component


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

Por supuesto, no escribiremos esta transformaci贸n manualmente, usaremos este complemento , el complemento est谩 desactualizado, pero es lo suficientemente simple como para ayudarnos a entender c贸mo funciona todo. Utiliza jsx-transform , que convierte JSX de esta manera:


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

entonces, todo lo que tenemos que hacer es implementar el constructor vdom de los nodos h, una funci贸n que crear谩 recursivamente nodos DOM virtuales, en el caso de una reacci贸n, la funci贸n React.createElement hace esto. A continuaci贸n se muestra una implementaci贸n primitiva de dicha funci贸n.


 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 } 

Por supuesto, la recursi贸n complica un poco el c贸digo aqu铆, pero espero que est茅 claro, ahora con esta funci贸n podemos construir vdom


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

y as铆 para los nodos de cualquier anidamiento


Genial, ahora nuestra funci贸n Componente devuelve el nodo vdom.


Ahora, la parte ser谩 que necesitamos escribir una funci贸n de patch que tome el elemento DOM ra铆z de la aplicaci贸n, el viejo vdom, el nuevo vdom y actualice los nodos del DOM real de acuerdo con el nuevo vdom.


Tal vez puedas escribir este c贸digo m谩s f谩cilmente, pero result贸 que tom茅 el c贸digo del paquete picodom como base


 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 } 

Esta implementaci贸n ingenua, es terriblemente no 贸ptima, no tiene en cuenta los identificadores de los elementos (clave, id) para actualizar correctamente los elementos necesarios en las listas, pero en casos primitivos funciona bien.


La implementaci贸n de las createElement updateElement removeElement no la traigo aqu铆, es notable, cualquiera que est茅 interesado puede ver la fuente aqu铆 .


Existe la 煤nica advertencia: cuando se actualizan las propiedades de value para input elementos de input , la comparaci贸n no debe hacerse con el antiguo vnode sino con el atributo de value en la casa real: esto evitar谩 que el elemento activo actualice esta propiedad (ya que ya est谩 actualizada all铆) y evitar谩 problemas con el cursor y selecci贸n.


Bueno, eso es todo, ahora solo tenemos que juntar estas piezas y escribir el marco de la interfaz de usuario
Nos mantenemos dentro de 5 l铆neas .


  1. Como en React, para construir la aplicaci贸n necesitamos 3 par谩metros
    export function app(selector, view, initProps) {
    selector - selector dom de ra铆z en el que se montar谩 la aplicaci贸n (por defecto 'cuerpo')
    view - una funci贸n que construye el vnode ra铆z
    initProps: propiedades de la aplicaci贸n inicial
  2. Tome el elemento ra铆z en el DOM
    const rootElement = document.querySelector(selector || 'body')
  3. Recolectamos vdom con propiedades iniciales
    let node = view(initProps)
  4. Montamos el vdom recibido en el DOM como el viejo vdom que tomamos nulo
    patch(rootElement, null, node)
  5. Devolvemos la funci贸n de actualizaci贸n de la aplicaci贸n con nuevas propiedades.
    return props => patch(rootElement, node, (node = view(props)))

隆El marco est谩 listo!


'Hello world' en este Framework se ver谩 as铆:


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

Esta biblioteca, como React, admite la composici贸n de componentes, agregando y eliminando componentes en tiempo de ejecuci贸n, por lo que puede considerarse un marco de interfaz de usuario . Un caso de uso un poco m谩s complejo se puede encontrar aqu铆 Ejemplo de tarea pendiente .


Por supuesto, hay muchas cosas en esta biblioteca: eventos del ciclo de vida (aunque no es dif铆cil sujetarlos, nosotros mismos gestionamos la creaci贸n / actualizaci贸n / eliminaci贸n de nodos), actualizaciones separadas de nodos secundarios como este.setState (para esto necesita guardar enlaces a elementos DOM para cada uno vdom node: esto complicar谩 un poco la l贸gica), el c贸digo patchElement es terriblemente no 贸ptimo, no funcionar谩 bien en una gran cantidad de elementos, no rastrea elementos con un identificador, etc.


En cualquier caso, la biblioteca se desarroll贸 con fines educativos; no la use en producci贸n :)


PD: Me inspir贸 la magn铆fica biblioteca Hyperapp para este art铆culo, parte del c贸digo fue tomado de all铆.


Buena codificaci贸n!

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


All Articles