Algum tempo atrás, eu me perguntava por que existem tantas estruturas de interface do usuário para a web? Estou na TI há muito tempo e não lembro que as bibliotecas de interface do usuário em outras plataformas nasceram e morreram na mesma velocidade que na WEB. Bibliotecas para sistemas operacionais de desktop, como: MFC, Qt, WPF etc. - eram monstros que se desenvolveram ao longo dos anos e não tinham um grande número de alternativas. Tudo é diferente na Web - os frameworks são lançados quase toda semana, os líderes mudam - por que isso está acontecendo?
Eu acho que a principal razão é que a complexidade de escrever bibliotecas da interface do usuário diminuiu bastante. Sim, escrever uma biblioteca que muitos usarão - ainda requer tempo e experiência consideráveis, mas escrever um protótipo - que, quando envolvido em uma API conveniente - estará pronto para uso - leva muito pouco tempo. Se você estiver interessado em como isso pode ser feito, continue a ler.
Por que este artigo?
Certa vez, em Habré, havia uma série de artigos - escrever X para 30 linhas de código em js.
Eu pensei - é possível escrever uma reação em 30 linhas? Sim, em 30 linhas não tive sucesso, mas o resultado final é bastante proporcional a esse número.
Em geral, o objetivo do artigo é puramente educacional. Isso pode ajudar a entender um pouco mais profundamente o princípio da estrutura da interface do usuário com base na casa virtual. Neste artigo, quero mostrar como é simples criar outro framework de interface do usuário com base em uma casa virtual.
No começo, quero dizer o que quero dizer com a estrutura da interface do usuário - porque muitos têm opiniões diferentes sobre isso. Por exemplo, alguns acreditam que Angular e Ember são uma estrutura de interface do usuário e o React é apenas uma biblioteca que facilitará o trabalho com a parte de exibição do aplicativo.
Definimos a estrutura da interface do usuário da seguinte maneira: esta é uma biblioteca que ajuda a criar / atualizar / excluir páginas ou elementos de página individuais, nesse sentido, uma variedade bastante ampla de invólucros sobre a API do DOM pode vir a ser a estrutura da interface do usuário, a única questão são as opções de abstração (API) que essa biblioteca fornece para manipular o DOM e na eficácia dessas manipulações
Na redação proposta - React é uma estrutura de interface do usuário.
Bem, vamos ver como escrever seu React com blackjack e muito mais. O React é conhecido por usar o conceito de uma casa virtual. De uma forma simplificada, consiste no fato de que os nós do DOM real são construídos em estrita conformidade com os nós da árvore do DOM virtual construída anteriormente. A manipulação direta do DOM real não é bem-vinda; se você precisar fazer alterações no DOM real, as alterações no DOM virtual, a nova versão do DOM virtual será comparada com a antiga, as alterações são coletadas que precisam ser aplicadas ao DOM real e são aplicadas de maneira que a interação com o DOM real seja minimizada DOM - que torna o aplicativo mais ideal.
Como a árvore da casa virtual é um objeto java-script comum - é muito fácil manipulá-lo - altere / compare seus nós, pela palavra é fácil aqui, eu entendo que o código do assembly é virtual, mas bastante simples e pode ser parcialmente gerado por um pré-processador a partir de uma linguagem declarativa de um JSX de nível superior.
Vamos começar com JSX
Este é um exemplo de código JSX
const Component = () => ( <div className="main"> <input /> <button onClick={() => console.log('yo')}> Submit </button> </div> ) export default Component
precisamos criar um DOM virtual ao chamar a função Component
const vdom = { type: 'div', props: { className: 'main' }, children: [ { type: 'input' }, { type: 'button', props: { onClick: () => console.log('yo') }, children: ['Submit'] } ] }
Obviamente, não escreveremos essa transformação manualmente, usaremos esse plug-in , o plug-in está desatualizado, mas é simples o suficiente para nos ajudar a entender como tudo funciona. Ele usa jsx-transform , que converte JSX assim:
jsx.fromString('<h1>Hello World</h1>', { factory: 'h' }); // => 'h("h1", null, ["Hello World"])'
portanto, tudo o que precisamos fazer é implementar o construtor vdom dos nós h, uma função que criará recursivamente nós DOM virtuais, no caso de uma reação, a função React.createElement faz isso. Abaixo está uma implementação primitiva dessa função
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 }
Claro, a recursão complica um pouco o código aqui, mas espero que esteja claro, agora com essa função podemos construir vdom
'h("h1", null, ["Hello World"])' => { type: 'h1', props:null, children:['Hello World']}
e assim para nós de qualquer aninhamento
Ótimo, agora nossa função Component retorna o nó vdom.
Agora, a parte
será: precisamos escrever uma função de patch
que pegue o elemento DOM raiz do aplicativo, o antigo vdom, o novo vdom e atualize os nós do DOM real, de acordo com o novo vdom.
Talvez você possa escrever esse código com mais facilidade, mas acabou assim, eu peguei o código do pacote 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 }
Essa implementação ingênua, por ser terrivelmente inadequada, não leva em consideração os identificadores dos elementos (chave, id) - para atualizar corretamente os elementos necessários nas listas, mas em casos primitivos funciona bem.
A implementação das createElement updateElement removeElement
não a trago para cá, é notável que qualquer pessoa interessada pode ver a fonte aqui .
Existe a única ressalva - quando as propriedades de value
dos elementos de input
são atualizadas, a comparação não deve ser feita com o vnode antigo, mas com o atributo value
na casa real - isso impedirá que o elemento ativo atualize essa propriedade (já que ela já está atualizada lá) e evite problemas com o cursor e seleção.
Bem, agora só precisamos juntar essas peças e escrever o UI Framework
Mantemos dentro de 5 linhas .
- Como no React, para criar o aplicativo, precisamos de 3 parâmetros
export function app(selector, view, initProps) {
seletor - seletor de raiz no qual o aplicativo será montado (por padrão 'corpo')
view - uma função que constrói o vnode raiz
initProps - propriedades iniciais do aplicativo - Pegue o elemento raiz no DOM
const rootElement = document.querySelector(selector || 'body')
- Coletamos vdom com propriedades iniciais
let node = view(initProps)
- Montamos o vdom recebido no DOM como o antigo vdom que tomamos nulo
patch(rootElement, null, node)
- Retornamos a função de atualização do aplicativo com novas propriedades
return props => patch(rootElement, node, (node = view(props)))
Framework está pronto!
'Olá, mundo' neste Framework, terá a seguinte aparência:
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')
Essa biblioteca, como React, oferece suporte à composição de componentes, adicionando e removendo componentes em tempo de execução, para que possa ser considerada uma estrutura de interface do usuário
. Um caso de uso um pouco mais complexo pode ser encontrado aqui, exemplo ToDo .
Obviamente, existem muitas coisas nesta biblioteca: eventos do ciclo de vida (embora não seja difícil prendê-los, nós mesmos gerenciamos a criação / atualização / exclusão de nós), atualizações separadas de nós filhos como this.setState (para isso, você precisa salvar links para elementos DOM para cada nó vdom - isso complicará um pouco a lógica), o código patchElement é terrivelmente não ideal, não funcionará bem em um grande número de elementos, não rastreia elementos com um identificador etc.
De qualquer forma, a biblioteca foi desenvolvida para fins educacionais - não a use na produção :)
PS: Eu fui inspirado pela magnífica biblioteca Hyperapp deste artigo, parte do código foi tirada de lá.
Boa codificação!