Il y a quelque temps, je me demandais pourquoi il y avait tant de cadres d'interface utilisateur pour le Web? Je travaille dans l'informatique depuis longtemps et je ne me souviens pas que les bibliothèques d'interface utilisateur sur d'autres plates-formes sont nées et mortes à la même vitesse que sur le WEB. Bibliothèques pour OS de bureau, telles que: MFC, Qt, WPF, etc. - étaient des monstres qui se sont développés au fil des ans et n'avaient pas un grand nombre d'alternatives. Tout est différent sur le Web - les frameworks sont publiés presque chaque semaine, les dirigeants changent - pourquoi cela se produit-il?
Je pense que la principale raison est que la complexité de l'écriture des bibliothèques d'interface utilisateur a fortement diminué. Oui, pour écrire une bibliothèque que beaucoup utiliseront - cela prend encore beaucoup de temps et d'expertise, mais pour écrire un prototype - qui, une fois emballé dans une API pratique - sera prêt à l'emploi - cela prend très peu de temps. Si vous souhaitez savoir comment procéder, lisez la suite.
Pourquoi cet article?
À une époque sur Habré, il y avait une série d'articles - pour écrire X pour 30 lignes de code sur js.
J'ai pensé - est-il possible d'écrire une réaction en 30 lignes? Oui, pour 30 lignes je n'ai pas réussi, mais le résultat final est assez proportionné à ce chiffre.
En général, le but de l'article est purement éducatif. Cela peut aider à comprendre un peu plus profondément le principe du cadre d'interface utilisateur basé sur la maison virtuelle. Dans cet article, je veux montrer à quel point il est simple de créer un autre cadre d'interface utilisateur basé sur une maison virtuelle.
Au début, je veux dire ce que je veux dire par le cadre de l'interface utilisateur - parce que beaucoup ont des opinions différentes à ce sujet. Par exemple, certains pensent qu'Angular and Ember est un cadre d'interface utilisateur et React n'est qu'une bibliothèque qui facilitera le travail avec la partie vue de l'application.
Nous définissons le cadre de l'interface utilisateur comme suit: il s'agit d'une bibliothèque qui aide à créer / mettre à jour / supprimer des pages ou des éléments de page individuels dans ce sens, une gamme assez large de wrappers sur l'API DOM peut se révéler être le cadre de l'interface utilisateur, la seule question est les options d'abstraction (API) que cette bibliothèque fournit pour manipuler le DOM et dans l'efficacité de ces manipulations
Dans le libellé proposé - React est tout à fait un cadre d'interface utilisateur.
Eh bien, voyons comment écrire votre React avec le blackjack et plus encore. React est connu pour utiliser le concept d'une maison virtuelle. Sous une forme simplifiée, elle consiste dans le fait que les nœuds du DOM réel sont construits en stricte conformité avec les nœuds de l'arbre DOM virtuel précédemment construit. La manipulation directe du DOM réel n'est pas la bienvenue, si vous devez apporter des modifications au DOM réel, les modifications sont apportées au DOM virtuel, puis la nouvelle version du DOM virtuel est comparée à l'ancienne, les modifications sont collectées qui doivent être appliquées au DOM réel et elles sont appliquées de telle manière que l'interaction avec le DOM réel soit minimisée DOM - ce qui rend l'application plus optimale.
Étant donné que l'arbre de la maison virtuelle est un objet de script java ordinaire - il est assez facile de le manipuler - changez / comparez ses nœuds, par le mot c'est facile ici, je comprends que le code d'assemblage est virtuel mais assez simple et peut être partiellement généré par un préprocesseur à partir d'un langage déclaratif d'un JSX de niveau supérieur.
Commençons par JSX
Ceci est un exemple de code JSX
const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component
nous devons créer un tel DOM virtuel lors de l'appel de la fonction Component
const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] }
Bien sûr, nous n'écrirons pas cette transformation manuellement, nous utiliserons ce plugin , le plugin est obsolète, mais il est assez simple pour nous aider à comprendre comment tout fonctionne. Il utilise jsx-transform , qui convertit JSX comme ceci:
jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])'
ainsi, tout ce que nous devons faire est d'implémenter le constructeur vdom des nœuds h, une fonction qui créera récursivement des nœuds DOM virtuels, dans le cas d'une réaction, la fonction React.createElement le fait. Voici une implémentation primitive d'une telle fonction
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 }
Bien sûr, la récursion complique un peu le code ici, mais j'espère que c'est clair, maintenant avec cette fonction, nous pouvons construire vdom
'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}
et ainsi pour les nœuds de toute imbrication
Génial, maintenant notre fonction Component retourne le nœud vdom.
Maintenant, la partie
sera, nous devons écrire une fonction de patch
qui prend l'élément DOM racine de l'application, l'ancien vdom, le nouveau vdom et met à jour les nœuds du vrai DOM en fonction du nouveau vdom.
Peut-être que vous pouvez écrire ce code plus facilement, mais il s'est avéré que j'ai pris le code du package picodom comme 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 }
Cette implémentation naïve, elle n'est terriblement pas optimale, ne prend pas en compte les identifiants des éléments (clé, id) - pour mettre correctement à jour les éléments nécessaires dans les listes, mais dans les cas primitifs cela fonctionne très bien.
L'implémentation des createElement updateElement removeElement
je ne l'apporte pas ici, c'est notable, toute personne intéressée peut voir la source ici .
Il y a la seule mise en garde - lorsque les propriétés de value
pour input
éléments d' input
sont mises à jour, la comparaison ne doit pas être effectuée avec l'ancien vnode mais avec l'attribut value
dans la vraie maison - cela empêchera l'élément actif de mettre à jour cette propriété (car il est déjà mis à jour là-bas) et évitera des problèmes avec le curseur et sélection.
Eh bien, c'est tout maintenant, il nous suffit de rassembler ces éléments et d'écrire le cadre de l'interface utilisateur
Nous restons dans les 5 lignes .
- Comme dans React, pour construire l'application, nous avons besoin de 3 paramètres
export function app(selector, view, initProps) {
selector - root dom selector dans lequel l'application sera montée (par défaut 'body')
view - une fonction qui construit le vnode racine
initProps - propriétés initiales de l'application - Prenez l'élément racine dans le DOM
const rootElement = document.querySelector(selector || 'body')
- Nous collectons vdom avec les propriétés initiales
let node = view(initProps)
- Nous montons le vdom reçu dans le DOM comme l'ancien vdom que nous prenons nul
patch(rootElement, null, node)
- Nous retournons la fonction de mise à jour de l'application avec de nouvelles propriétés
return props => patch(rootElement, node, (node = view(props)))
Le framework est prêt!
'Hello world' sur ce Framework ressemblera à ceci:
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')
Cette bibliothèque, comme React, prend en charge la composition de composants, l'ajout et la suppression de composants au moment de l'exécution, afin qu'elle puisse être considérée comme une
interface utilisateur à
. Un cas d'utilisation légèrement plus complexe peut être trouvé ici, par exemple ToDo .
Bien sûr, il y a beaucoup de choses dans cette bibliothèque: les événements du cycle de vie (bien qu'il ne soit pas difficile de les attacher, nous gérons nous-mêmes la création / mise à jour / suppression des nœuds), des mises à jour séparées des nœuds enfants comme this.setState (pour cela, vous devez enregistrer des liens vers des éléments DOM pour chacun nœud vdom - cela compliquera un peu la logique), le code patchElement est terriblement non optimal, ne fonctionnera pas bien sur un grand nombre d'éléments, ne suit pas les éléments avec un identifiant, etc.
Dans tous les cas, la bibliothèque a été développée à des fins pédagogiques - ne l'utilisez pas en production :)
PS: J'ai été inspiré par la magnifique bibliothèque Hyperapp pour cet article, une partie du code a été prise à partir de là.
Bon codage!