Já se maravilhou com a magia do React? Já se perguntou como o Dojo faz isso? Já teve curiosidade sobre a ginástica do jQuery? Neste tutorial, vamos nos esgueirar pelos bastidores e tentar construir uma versão supersimples do jQuery.
Usamos bibliotecas JavaScript quase todos os dias. Seja para implementar um algoritmo, fornecer uma abstração sobre uma API ou manipular o DOM, as bibliotecas executam muitas funções na maioria dos sites modernos.
Estamos envolvendo os elementos em um objeto porque queremos ser capazes de criar métodos para o objeto.
Neste tutorial, vamos fazer uma tentativa (decididamente superficial) de construir uma dessas bibliotecas do zero. Trabalharemos na criação de uma biblioteca para manipulação de DOM, como jQuery. Sim, vai ser divertido, mas antes que você fique muito animado, deixe-me esclarecer alguns pontos:
- Esta não será uma biblioteca completamente completa. Oh, temos um conjunto sólido de métodos para escrever, mas não é jQuery. Faremos o suficiente para lhe dar uma boa noção do tipo de problemas que você encontrará ao construir bibliotecas.
- Não estamos buscando compatibilidade completa do navegador aqui. O que estamos escrevendo hoje deve funcionar no Chrome, Firefox e Safari, mas pode não funcionar em navegadores mais antigos, como o IE.
- Não vamos cobrir todos os usos possíveis de nossa biblioteca. Por exemplo, nosso
append
eprepend
métodos só funcionarão se você passar a eles uma instância de nossa biblioteca; eles não funcionarão com nós DOM brutos ou listas de nós.
1. Criando o Boilerplate da Biblioteca
Vamos começar com o próprio módulo. Usaremos os Módulos ECMAScript (ESM), uma forma moderna de importar e exportar código na web.
export class Dome { constructor(selector) { } }
Como você pode ver, estamos exportando uma classe chamada Dome
. Esta será a parte principal da biblioteca e representará um elemento ou uma matriz de elementos.
2. Obtendo elementos e criando instâncias de cúpula
o Dome
construtor terá um parâmetro, mas pode ser uma série de coisas. Se for uma string, assumiremos que é um seletor CSS; mas também podemos pegar um único DOM Node, ou um NodeList
.
constructor(selector) { let els; if (typeof selector === "string") { els = document.querySelectorAll(selector); } else if (selector.length) { els = selector; } else { els = [selector]; } this.elements = els this.length = els.length; }
Estamos usando document.querySelectorAll
para simplificar a descoberta de elementos. Se selector
não é uma string, vamos verificar se há length
propriedade. Se existir, saberemos que temos um NodeList
; caso contrário, temos um único elemento e o colocaremos em um array. Em seguida, definimos this.elements
aos elementos e this.length
ao número de elementos.
3. Adicionando alguns utilitários
As primeiras funções que vamos escrever são funções utilitárias simples. Uma vez que nossa Dome
objetos podem envolver mais de um elemento DOM, vamos precisar fazer um loop sobre cada elemento em praticamente todos os métodos; então, esses utilitários serão úteis.
Vamos começar com um map
função:
map(callback) { // put this inside the Dome class as a method let results = []; for (let i = 0; i < this.length; i++) { results.push(callback.call(this, this.elements[i], i)); } return results; }
Claro, o map
função recebe um único parâmetro, uma função de retorno de chamada. Faremos um loop sobre os itens na matriz, coletando o que for retornado do retorno de chamada no results
variedade. Observe como estamos chamando essa função de retorno de chamada:
callback.call(this, this.elements[i], i));
Fazendo desta forma, a função será chamada no contexto do nosso Dome
instância, e receberá dois parâmetros: o elemento atual e o número do índice.
Também queremos um forEach
função. Isso é realmente muito simples:
forEach(callback) { return this.elements.forEach(callback) }
Desde NodeLists
e Arrays
venha com o forEach
por padrão, podemos simplesmente encaminhar a chamada para o this.elements
.
Mais um: mapOne
. É fácil ver o que essa função faz, mas a verdadeira questão é: por que precisamos dela? Isso requer um pouco do que você poderia chamar de “filosofia da biblioteca”.
Um pequeno desvio filosófico
Em primeiro lugar, o DOM pode ser bastante difícil para um iniciante; é uma desculpa muito pobre para uma API.
Se construir uma biblioteca fosse apenas escrever o código, não seria um trabalho muito difícil. Mas enquanto eu trabalhava neste projeto, descobri que a parte mais difícil era decidir como certos métodos deveriam funcionar.
Em breve, vamos construir um text
método que retorna o texto de nossos elementos selecionados. Se nosso Dome
objeto envolve vários nós DOM (new Dome("li")
, por exemplo), o que isso deve retornar? Se você fizer algo semelhante em jQuery ($("li").text()
), você obterá uma única string com o texto de todos os elementos concatenados. Isso é útil? Acho que não, mas não tenho certeza de qual seria um valor de retorno melhor.
Para este projeto, retornarei o texto de vários elementos como um array, a menos que haja apenas um item no array; então retornaremos apenas a string de texto, não um array com um único item. Acho que na maioria das vezes você receberá o texto de um único elemento, então otimizamos para esse caso. No entanto, se você estiver recebendo o texto de vários elementos, retornaremos algo com o qual você possa trabalhar.
Voltar para Codificação
Então o mapOne
o método simplesmente será executado map
e, em seguida, retorne a matriz ou o único item que estava na matriz. Se você ainda não tem certeza de como isso é útil, fique por aqui: você verá!
mapOne(callback) { const m = this.map(callback); return m.length > 1 ? m : m[0]; };
4. Trabalhando com texto e HTML
Em seguida, vamos adicionar que text
método. Assim como o jQuery, podemos passar uma string e definir o texto do elemento ou não usar parâmetros para recuperar o texto.
text(text) { if (typeof text !== "undefined") { return this.forEach(function (el) { el.innerText = text; }); } else { return this.mapOne(function (el) { return el.innerText; }); } }
Como você pode esperar, precisamos verificar um valor em text
para ver se estamos configurando ou recebendo. Observe que apenas if (text)
não funcionaria, porque uma string vazia é um valor falso.
Se estivermos configurando, faremos um forEach
sobre os elementos e definir seus innerText
propriedade para o text
. Se estivermos recebendo, retornaremos os elementos' innerText
propriedade. Observe nosso uso do mapOne
método: se estivermos trabalhando com vários elementos, isso retornará um array; caso contrário, será apenas a string.
o html
método fará praticamente a mesma coisa que text
exceto que ele usará o innerHTML
propriedade, em vez de innerText
.
html(html) { if (typeof html !== "undefined") { this.forEach(function (el) { el.innerHTML = html; }); return this; } else { return this.mapOne(function (el) { return el.innerHTML; }); } }
Como eu disse: quase idêntico.
5. Manipulando classes
Em seguida, queremos poder adicionar e remover classes; então vamos escrever o addClass
e removeClass
métodos.
Nosso addClass
O método receberá uma string ou um array de nomes de classes. Essencialmente, estamos apenas usando o classList.add
método em cada elemento. Quando uma string é passada, somente aquela classe é adicionada, e quando um array é passado, nós iteramos através do array e adicionamos todas as classes contidas.
addClass(classes) { return this.forEach(function (el) { if (typeof classes !== "string") { for (const elClass of classes) { el.classList.add(elClass); } } else { el.classList.add(classes); } }); }
Bem direto, hein?
Agora, que tal remover classes? Para fazer isso, você faz quase exatamente a mesma coisa, apenas com classList.remove
.
6. Ajustando Atributos
Agora, queremos um attr
função. Isso vai ser fácil, porque é praticamente idêntico ao nosso text
ou html
métodos. Assim como esses métodos, poderemos obter e definir atributos: pegaremos um nome de atributo e um valor para definir, e apenas um nome de atributo para obter.
attr(attr, val) { if (typeof val !== "undefined") { return this.forEach(function (el) { el.setAttribute(attr, val); }); } else { return this.mapOne(function (el) { return el.getAttribute(attr); }); } }
Se o val
tem um valor, vamos percorrer os elementos e definir o atributo selecionado com esse valor, usando o elemento setAttribute
método. Caso contrário, usaremos mapOne
para retornar esse atributo através do getAttribute
método.
7. Criando elementos
Devemos ser capazes de criar novos elementos, como qualquer boa biblioteca. Claro, isso não seria bom como um método em um Dome
instância, então vamos criá-lo fora do Dome
classe
export function create(tagName,attrs) { }
Como você pode ver, tomaremos dois parâmetros: o nome do elemento e um objeto de atributos. A maioria dos atributos pode ser aplicada através do nosso attr
método, mas dois terão tratamento especial. Nós vamos usar o addClass
método para o className
propriedade, e o text
método para o text
propriedade. Claro, precisaremos criar o elemento e o Dome
objeto primeiro. Aqui está tudo isso em ação:
export function create(tagName, attrs) { let el = new Dome([document.createElement(tagName)]); if (attrs) { for (let key in attrs) { if (attrs.hasOwnProperty(key)) { el.attr(key, attrs[key]); } } } return el; }
Como você pode ver, criamos o elemento e o enviamos diretamente para um novo Dome
objeto. Em seguida, tratamos dos atributos. Claro, terminamos devolvendo o novo Dome
objeto.
Mas agora que estamos criando novos elementos, vamos querer inseri-los no DOM, certo?
8. Anexando e Pré-Anexando Elementos
A seguir, escreveremos append
e prepend
métodos, Agora, essas são realmente funções um pouco complicadas de escrever, principalmente por causa dos vários casos de uso. Aqui está o que queremos ser capazes de fazer:
dome1.append(dome2); dome1.prepend(dome2);
Os casos de uso são os seguintes: podemos querer acrescentar ou preceder
- um novo elemento para um ou mais elementos existentes.
- vários novos elementos para um ou mais elementos existentes.
- um elemento existente para um ou mais elementos existentes.
- vários elementos existentes para um ou mais elementos existentes.
Nota: estou usando “new” para significar elementos que ainda não estão no DOM; elementos existentes já estão no DOM.
Vamos passar por isso agora:
append(els) { }
Nós esperamos que els
parâmetro para ser um Dome
objeto. Uma biblioteca DOM completa aceitaria isso como um nó ou uma lista de nós, mas não faremos isso. Temos que fazer um loop sobre cada um de nossos elementos e, dentro disso, fazemos um loop sobre cada um dos elementos que queremos anexar.
Se estamos anexando o els
para mais de um elemento, precisamos cloná-los. No entanto, não queremos clonar os nós na primeira vez que forem anexados, apenas nas vezes subsequentes. Então vamos fazer isso:
if (i > 0) { childEl = childEl.cloneNode(true); }
Este i
vem do exterior forEach
loop: é o índice do elemento pai atual. Se não estivermos anexando ao primeiro elemento pai, clonaremos o nó. Dessa forma, o nó real irá para o primeiro nó pai e todos os outros pais receberão uma cópia. Isso funciona bem, porque o Dome
objeto que foi passado como argumento terá apenas os nós originais (não clonados). Portanto, se estivermos apenas anexando um único elemento a um único elemento, todos os nós envolvidos farão parte de seus respectivos Dome
objetos.
Por fim, anexaremos o elemento:
parEl.appendChild(childEl);
Então, ao todo, é isso que temos:
append(els) { return this.forEach(function (parEl, i) { els.forEach(function (childEl) { if (i > 0) { childEl = childEl.cloneNode(true); } parEl.appendChild(childEl); }); }); }
o prepend
Método
Queremos cobrir os mesmos casos para o prepend
método, então o método é bastante semelhante:
preprend(els) { return this.forEach(function (parEl, i) { for (var j = els.length - 1; j > -1; j--) { childEl = i > 0 ? els[j].cloneNode(true) : els[j]; parEl.insertBefore(childEl, parEl.firstChild); } }); }
A diferença ao preceder é que, se você preceder sequencialmente uma lista de elementos a outro elemento, eles terminarão na ordem inversa. Já que não podemos forEach
para trás, estou percorrendo o loop para trás com um for
ciclo. Novamente, clonaremos o nó se este não for o primeiro pai ao qual estamos anexando.
9. Removendo nós
Para nosso último método de manipulação de nós, queremos remover nós do DOM. Fácil, realmente:
remove() { return this.forEach(function (el) { return el.parentNode.removeChild(el); }); }
Basta iterar pelos nós e chamar o removeChild
método em cada elemento parentNode
. A beleza aqui (tudo graças ao DOM) é que isso Dome
objeto ainda funcionará bem; podemos usar qualquer método que quisermos, incluindo anexar ou prefixar de volta ao DOM. Legal, hein?
10. Trabalhando com eventos
Por último, mas certamente não menos importante, vamos escrever algumas funções para manipuladores de eventos.
Confira o método e depois discutiremos:
on(evt, fn) { return this.forEach(function (el) { el.addEventListener(evt, fn, false); }); }
Isso é bastante simples. Nós apenas percorremos os elementos e usamos addEventListener
em cada.
o off
função, que desconecta manipuladores de eventos, é praticamente idêntica:
off(evt, fn) { return this.forEach(function (el) { el.removeEventListener(evt, fn, false); }); }
11. Usando a Biblioteca
Para usar o Dome, basta colocar nele um script e import
isto.
import {Dome, create} from "./dome.js"
A partir daí, você pode usá-lo assim:
new Dome("li") ...
Certifique-se de que o script no qual você está importando é um Módulo ES.
É isso!
Espero que você experimente nossa pequena biblioteca e talvez até a estenda um pouco. Como mencionei anteriormente, eu tenho no Github. Sinta-se à vontade para fazer um fork, brincar e enviar um pull request.
Deixe-me esclarecer novamente: o objetivo deste tutorial não é sugerir que você deve sempre escrever suas próprias bibliotecas.
Existem equipes dedicadas de pessoas trabalhando juntas para tornar as bibliotecas grandes e estabelecidas as melhores possíveis. O objetivo aqui era dar uma pequena espiada no que poderia acontecer dentro de uma biblioteca; Espero que você tenha pego algumas dicas aqui.
Eu realmente recomendo que você pesquise dentro de algumas de suas bibliotecas favoritas. Você descobrirá que eles não são tão enigmáticos quanto você poderia ter pensado, e você provavelmente aprenderá muito. Aqui estão alguns ótimos lugares para começar:
Este post foi atualizado com contribuições de Jacob Jackson. Jacob é desenvolvedor web, redator técnico, freelancer e colaborador de código aberto.