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
appendeprependmé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 mape, 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 textexceto 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.
[ad_2]