Extensões Extensíveis em JavaScript

Olá Habr!

Chamamos sua atenção para a tão esperada cópia adicional do livro " Expressive JavaScript ", que acaba de chegar da gráfica.


Para aqueles que não estão familiarizados com o trabalho do autor do livro (por toda a natureza enciclopédica, os iniciantes também vão gostar) - sugerimos que você se familiarize com o artigo do blog dele; O artigo descreve idéias sobre como organizar extensões em JavaScript.

Atualmente, tornou-se moda estruturar grandes sistemas na forma de muitos pacotes separados. A ideia principal subjacente a essa abordagem é que é melhor não limitar as pessoas a um recurso específico (proposto por você) implementando um recurso, mas fornecê-lo como um pacote separado que uma pessoa pode baixar junto com o pacote básico do sistema.
Para fazer isso, em termos gerais, você precisará ...

  • A capacidade de não baixar os recursos que você não precisa é especialmente útil nos sistemas clientes.
  • A capacidade de substituir por outra implementação uma parte da funcionalidade que não atende aos seus objetivos. Portanto, a carga nos módulos do kernel também é reduzida - não é necessário cobrir todos os casos práticos possíveis com a ajuda deles.
  • Verificando as interfaces do kernel em condições reais - implementando recursos básicos na parte superior da interface voltada para o lado do cliente, você é obrigado a tornar essa interface pelo menos tão poderosa que pode suportar esses recursos. Assim, você pode ter certeza de que será possível criar coisas semelhantes escritas por desenvolvedores de terceiros.
  • Isolamento entre partes do sistema. Os participantes do projeto podem simplesmente procurar o pacote no qual estão interessados. Os pacotes podem ser versionados, marcados como indesejados ou substituídos sem afetar o código principal.

Essa abordagem geralmente está associada ao aumento da complexidade. Para facilitar a inicialização dos usuários, você pode fornecer a eles um pacote wrapper no qual “tudo está incluído”, mas mais cedo ou mais tarde eles provavelmente terão que se livrar desse shell e instalar e configurar pacotes auxiliares específicos, que às vezes é mais difícil do que mudar para outro recurso na biblioteca monolítica.

Neste artigo, tentaremos discutir opções para mecanismos de expansão que suportam “extensibilidade em larga escala” e permitem criar novos pontos de extensão onde isso não foi fornecido.

Extensibilidade


O que queremos de um sistema extensível? Antes de tudo, é claro, ele deve ter a capacidade de expandir seus próprios recursos usando código externo.

Mas isso não é suficiente. Deixe-me divagar sobre um problema estúpido que encontrei uma vez. Estou desenvolvendo um editor de código. Em uma das versões anteriores deste editor, você pode definir o estilo para uma linha específica no código do cliente. Foi ótimo - layout de linha seletivo.

Exceto no caso em que as tentativas de alterar a aparência da linha são feitas imediatamente a partir de duas seções do código - e essas tentativas começam a ocorrer. A segunda extensão aplicada à linha substitui o estilo da primeira extensão. Ou, quando o primeiro código tenta remover o design criado em algum lugar na parte posterior do código, ele substitui o estilo introduzido pelo segundo fragmento de código.

Conseguimos encontrar uma solução, fornecendo a capacidade de adicionar (e excluir) a extensão e não defini-la, para que as duas extensões pudessem interagir com a mesma linha sem sabotar o trabalho uma da outra.

Em um sentido mais geral, é necessário garantir que as extensões possam ser combinadas, mesmo que elas não tenham consciência da existência uma da outra - sem causar conflitos entre elas.

Para fazer isso, você precisa garantir que qualquer número de atores possa afetar cada ponto de expansão. Como exatamente vários efeitos serão processados ​​depende da situação. Aqui estão algumas abordagens que você pode achar úteis:

  • Todos eles entram em vigor. Por exemplo, ao adicionar uma classe CSS a um elemento ou ao exibir um widget, esses dois recursos são adicionados ao mesmo tempo. Geralmente, eles ainda precisam ser classificados de alguma maneira: os widgets devem ser exibidos em uma sequência previsível e bem definida.
  • Eles se alinham na forma de um transportador. Um exemplo é um manipulador que pode filtrar as alterações adicionadas a um documento antes de serem feitas. Cada alteração é primeiro alimentada a um manipulador, que, por sua vez, pode alterá-la adicionalmente. Encomendar neste caso não é crítico, mas pode fazer a diferença.
  • Você pode aplicar uma abordagem de primeiro a chegar, primeiro a servir aos manipuladores de eventos. Cada manipulador tem a chance de servir o evento até que um deles diga que já lidou com ele, após o que os manipuladores que estão na fila atrás dele não são mais interrogados.
  • Também acontece que você realmente precisa escolher um valor específico - por exemplo, determine o valor de um parâmetro de configuração específico. Pode ser aconselhável usar um determinado operador (por exemplo, lógico e, lógico ou, mínimo ou máximo) para limitar o número de valores de entrada para uma posição. Por exemplo, um editor pode alternar para o modo somente leitura se alguma extensão solicitar. Você pode definir o valor máximo do documento ou o número mínimo de valores relatados para esta opção.

Em muitos desses casos, a ordem é importante. Isso significa que a prioridade dos efeitos aplicados deve ser controlável e previsível.

É nesta frente que os sistemas de extensão imperativos baseados no uso de efeitos colaterais geralmente não conseguem lidar. Por exemplo, a operação addEventListener executada pelo modelo DOM do navegador faz com que os manipuladores de eventos sejam chamados exatamente na ordem em que foram registrados. Isso é normal se todas as chamadas forem controladas por um único sistema ou se a ordem das operações não for realmente importante, no entanto, quando você precisar lidar com muitos fragmentos de software que adicionam manipuladores de forma independente, pode ser muito difícil prever quais delas serão chamadas em primeiro lugar.

Abordagem simples


Para dar um exemplo simples: primeiro apliquei uma estratégia modular ao ProseMirror, um sistema para editar rich text. O núcleo deste sistema em si é, em princípio, inútil - ele se baseia inteiramente em pacotes adicionais que descrevem a estrutura dos documentos, chaves de ligação, liderando o histórico de cancelamentos. Embora seja um pouco difícil usar esse sistema, ele foi adotado em produtos que exigem design de texto personalizado, o que não está disponível nos editores clássicos.

O mecanismo de extensão usado no ProseMirror é relativamente direto. Ao criar o editor, o código do cliente indica uma única matriz de objetos conectados. Cada um desses plugins pode de alguma forma afetar o trabalho do editor, por exemplo, adicionar partes de dados de status ou manipular eventos de interface.

Todos esses aspectos foram projetados para trabalhar com uma matriz de valores de configuração usando as estratégias descritas na seção anterior. Por exemplo, ao especificar várias atribuições de chave, a ordem na qual as instâncias de plug-in de mapa de teclas são especificadas determina sua prioridade. O primeiro mapa de teclas que sabe como lidar com isso recebe uma chave específica no processamento.

Geralmente esse mecanismo é bastante poderoso e é usado ativamente. No entanto, em um certo estágio, fica complicado e desconfortável trabalhar com ele.

  • Se o plug-in tiver muitos efeitos, você poderá esperar que, nessa ordem, eles sejam aplicados a outros plug-ins ou precisará dividi-los em plug-ins menores para poder organizá-los corretamente.
  • Em geral, a organização de plug-ins se torna muito sensível, pois o usuário final nem sempre entende quais plug-ins podem afetar a operação de outros plug-ins se eles tiverem uma prioridade mais alta. Todos os erros geralmente aparecem apenas em tempo de execução, ao usar funcionalidades específicas - portanto, eles são fáceis de perder.
  • Plug-ins baseados em outros plug-ins devem documentar esse fato - e espera-se que os usuários não esqueçam de ativar suas dependências (na ordem correta).

O CodeMirror na versão 6 é um editor reescrito com o mesmo nome . Na sexta versão, tento desenvolver uma abordagem modular. Isso requer um sistema de extensão mais expressivo. Vejamos alguns dos desafios associados ao design de um sistema desse tipo.

Encomendar


É fácil projetar um sistema que forneça controle completo sobre a ordem das extensões. Mas é muito difícil projetar um sistema desse tipo, que ao mesmo tempo seja agradável de usar e permita combinar o código de extensões independentes sem intervenção manual extensa e completa.

Quando se trata de pedidos, ele aplica para aplicar valores de prioridade. Um exemplo semelhante é a propriedade CSS z-index , que permite definir um número que indica a profundidade do item na pilha.

Como as folhas de estilo às vezes têm valores ridiculamente grandes z-index , é óbvio que essa maneira de indicar prioridade é problemática. Um módulo específico individualmente “não sabe” quais valores de prioridade indicam outros módulos. As opções são apenas pontos em um intervalo de número indefinido. Você pode especificar valores proibitivamente altos (ou valores profundamente negativos), esperando chegar ao fim desta escala, mas todo o resto é um jogo de adivinhação.

Essa situação pode ser melhorada um pouco, definindo um conjunto limitado de categorias de prioridades claramente definidas, para que as extensões possam ser classificadas pelo "nível" aproximado de sua prioridade. Mas você ainda precisa de alguma forma romper os laços dentro dessas categorias.

Agrupamento e desduplicação


Como mencionei acima, uma vez que você comece a confiar seriamente em extensões, poderão surgir situações em que algumas extensões usarão outras. O gerenciamento mútuo de dependências não é bem dimensionado; portanto, seria bom se você pudesse puxar um grupo de extensões de uma só vez.

No entanto, não apenas isso, neste caso, o problema de ordenar agravará ainda mais; outro problema surgirá. Muitas outras extensões podem depender de uma extensão específica de uma só vez e, se você as representar como valores, poderá ocorrer a situação com vários downloads da mesma extensão. Em alguns casos, por exemplo, ao atribuir chaves ou manipular manipuladores de eventos, isso é normal. Em outros, por exemplo, ao rastrear o histórico de cancelamentos ou ao trabalhar com uma biblioteca de dicas de ferramentas, essa abordagem seria um desperdício de recursos com o risco de quebrar alguma coisa.

Portanto, permitindo a composição de extensões, somos forçados a mudar para o sistema de extensão, parte da complexidade associada ao gerenciamento de dependências. Você precisa reconhecer as extensões que não devem ser duplicadas e baixar apenas uma instância de cada uma delas.

No entanto, como na maioria dos casos as extensões podem ser configuradas e todas as instâncias de uma extensão específica serão um pouco diferentes uma da outra, não podemos apenas pegar uma instância da extensão e usá-la - teremos que combiná-las de alguma maneira significativa (ou relatar um erro, quando isso não for possível).

Projeto


Aqui, descreverei o que foi feito no CodeMirror 6. Proponho este exemplo como uma solução, e não como a única solução verdadeira. É possível que esse sistema se desenvolva ainda mais à medida que a biblioteca se estabiliza.

O principal primitivo nessa abordagem é chamado comportamento. Comportamentos são apenas aquelas coisas que você pode expandir indicando valores. Como exemplo, considere o comportamento de um campo de estado, onde, com a ajuda de extensões, você pode adicionar novos campos, fornecendo uma descrição de cada campo. Ou o comportamento de um manipulador de eventos baseado em navegador, no qual você pode adicionar seus próprios manipuladores usando extensões.

Do ponto de vista do comportamento do consumidor, esses comportamentos configurados em uma instância específica do editor fornecem uma sequência ordenada de valores, em que os valores com maior prioridade vêm em primeiro lugar. Cada comportamento tem um tipo e os valores para ele devem corresponder a esse tipo.

Um comportamento é representado como um valor usado para declarar uma instância de um comportamento e para se referir aos valores que o comportamento pode ter. Por exemplo, uma extensão que define o plano de fundo de um número de linha pode definir um comportamento que permite que outro código adicione novos marcadores nesse plano de fundo.

Uma extensão é um valor que pode ser usado ao configurar o editor. Uma matriz de extensões é relatada durante a inicialização. Cada extensão é permitida em zero ou mais comportamentos.

O tipo mais simples de extensão é uma instância de comportamento. Ao definir um valor para esse comportamento, obtemos em resposta o valor da extensão que implementa esse comportamento.
Uma sequência de extensões também pode ser agrupada em uma única extensão. Por exemplo, na configuração do editor para uma determinada linguagem de programação, várias outras extensões podem ser acessadas, em particular uma gramática para analisar e destacar a linguagem, informações sobre como recuar e também uma fonte de informações sobre a conclusão que complementa de forma inteligente o código nessa linguagem. Então você obtém uma extensão de idioma, que simplesmente coleta todas as extensões necessárias que fornecem o valor cumulativo.

Ao descrever uma versão simples desse sistema, poderíamos parar com isso e simplesmente ajustar as extensões aninhadas em uma única matriz de extensões para comportamentos. Em seguida, eles podem ser agrupados por tipo de comportamento e obter sequências ordenadas de valores de comportamento.

No entanto, ainda não descobrimos a deduplicação e precisamos de um controle mais completo sobre os pedidos.

Os valores do terceiro tipo incluem extensões exclusivas ; esse é o mecanismo para garantir a desduplicação. As extensões que você não deseja instanciar duas vezes no mesmo editor são exatamente isso. Para definir essa extensão, um tipo de especificação é especificado, ou seja, o tipo de valor de configuração esperado pelo construtor de extensão e uma função de instanciação que pega uma matriz desses valores de especificação e retorna a extensão.
Extensões únicas complicam o processo de resolver uma coleção de extensões em um conjunto de comportamentos. Enquanto houver extensões exclusivas no conjunto alinhado de extensões, o mecanismo de resolução deve selecionar o tipo de extensão exclusiva, coletar todas as suas instâncias, chamar a função de instanciação com seus valores de especificação e substituí-los pelo resultado (em uma instância).

(Há mais um problema - eles devem ser resolvidos em uma determinada ordem. Se você ativar primeiro a extensão exclusiva X, mas a extensão Y for resolvida para outro X, isso será um erro, pois todas as instâncias de X devem ser combinadas. Como instanciar as extensões é uma operação pura, o sistema, confrontado com ele, executa por tentativa e erro, reiniciando o processo - e registrando as informações esclarecidas.)
Finalmente, vamos falar sobre prioridade. A abordagem básica nesse caso é manter a ordem na qual as extensões foram relatadas. As extensões compostas são alinhadas e incorporadas nessa ordem exatamente na posição em que se encontram pela primeira vez. O resultado da resolução de uma extensão exclusiva também é inserido no local em que ocorre pela primeira vez.

Mas as extensões podem atribuir algumas de suas subextensões a uma categoria com uma prioridade diferente. O sistema determina os tipos dessas categorias: reversão (entra em vigor depois que outras coisas acontecem), por padrão, expande (uma prioridade mais alta que a maioria) e redefine (talvez deva estar localizado na parte superior). A ordem real é realizada primeiro por categoria e depois pela posição inicial.

Portanto, uma extensão com uma atribuição de chave de baixa prioridade e um manipulador de eventos com uma prioridade normal podem nos fornecer uma extensão composta criada com base em uma extensão com uma atribuição de chave (nesse caso, você não precisa saber quais comportamentos estão incluídos nela, com a reversão de prioridade mais uma instância de comportamento manipulador de eventos.

A principal conquista parece ser que adquirimos a capacidade de combinar extensões, independentemente do que é feito dentro de cada uma delas. Nas extensões que modelamos até agora, entre as quais: dois sistemas de análise com o mesmo comportamento sintático, destaque de sintaxe, serviço de indentação inteligente, histórico de cancelamentos, histórico de números de linhas, fechamento automático de parênteses, atribuição de teclas e seleção múltipla - tudo funciona bem.

Para usar esse sistema, você realmente precisa dominar vários conceitos novos, e é definitivamente mais complicado que os sistemas imperativos tradicionais aceitos na comunidade JavaScript (chame um método para adicionar / remover um efeito). No entanto, a capacidade de vincular corretamente extensões parece justificar esses custos.

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


All Articles