MAM: montagem de front-end sem dor

Olá, meu nome é Dmitry Karlovsky e eu ... adoro o MAM. MAM domina o módulo gnóstico M , poupando-me a maior parte da rotina.


Módulo Agnóstico Típico


Um módulo agnóstico , diferente do tradicional, não é um arquivo com uma fonte, mas um diretório dentro do qual pode haver fontes em várias linguagens: lógica do programa em JS / TS , testes para ele em TS / JS , composição de componentes em view.tree , styles em CSS , localização no locale=*.json do locale=*.json , imagens, etc., etc. Se desejado, não é difícil fixar o suporte de qualquer outro idioma. Por exemplo, Stylus para escrever estilos ou HTML para descrever modelos.


As dependências entre os módulos são rastreadas automaticamente, analisando a fonte. Se o módulo estiver ativado, ele será ativado como um todo - cada código-fonte do módulo é transposto e cai no pacote correspondente: scripts - separadamente, estilos - separadamente, testes - separadamente. Para plataformas diferentes - seus pacotes configuráveis: para um nó - próprio, para um navegador - próprio.


Automação total, falta de configuração e clichê, tamanhos mínimos de pacotes, bombeamento automático de dependências, desenvolvimento de centenas de bibliotecas e aplicativos alienados em uma única base de código, sem dor e sofrimento. Uau, que vício! Retire as crianças grávidas e nervosas dos monitores e seja bem-vindo ao submarino!


Filosofia


O MAM é um experimento ousado para alterar radicalmente a forma como o código é organizado e o processo de trabalho com ele. Aqui estão os princípios básicos:


Convenções em vez de configuração. Acordos razoáveis, simples e universais permitem automatizar toda a rotina, mantendo a conveniência e a uniformidade entre diferentes projetos.


A infraestrutura é separada, o código é separado. Uma situação não é incomum quando você precisa desenvolver dezenas ou mesmo centenas de bibliotecas e aplicativos. Não implante a infraestrutura de montagem, desenvolvimento, implantação etc. para cada um deles. Basta perguntar uma vez e depois rebitar aplicativos como tortas.


Não pague pelo que você não usa. Você usa algum tipo de módulo - ele está incluído no pacote com todas as suas dependências. Não use - não liga. Quanto menores os módulos, maior a granularidade e menos código desnecessário no pacote.


Código redundante mínimo. Dividir o código em módulos deve ser tão simples quanto escrever todo o código em um único arquivo. Caso contrário, o desenvolvedor terá preguiça de dividir módulos grandes em pequenos.


Nenhuma versão conflita. Existe apenas uma versão - a atual. Não há necessidade de gastar recursos no suporte a versões antigas, se você pode gastá-los na atualização da última.


Mantenha um dedo no pulso. O feedback mais rápido sobre incompatibilidades não permitirá que o código fique ruim.


A maneira mais fácil é a mais certa. Se o caminho certo exigir um esforço adicional, certifique-se de que ninguém os procure.


Importação / Exportação


Abrimos o primeiro projeto lançado usando um sistema de módulo moderno: um módulo tem menos de 300 linhas, 30 das quais são importadas.


Mas ainda são flores: para uma função de 9 linhas, são necessárias 8 importações.


E o meu favorito: nem uma única linha de código útil. 20 linhas de valores de deslocamento do monte de módulos em um, para que você possa importar posteriormente de um módulo, e não de vinte.


Tudo isso é um boilerplate, o que leva ao fato de que os desenvolvedores são preguiçosos demais para alocar pequenos pedaços de código em módulos separados, preferindo módulos grandes a pequenos. E mesmo que eles não sejam preguiçosos, resultam muitos códigos para importação de módulos pequenos ou módulos especiais que importam muitos módulos para si mesmos e os exportam em massa.


Tudo isso leva à baixa granularidade do código e ao aumento do tamanho dos pacotes configuráveis ​​com o código não utilizado, o que é uma sorte o suficiente para estar próximo do que é usado. Para JS, eles estão tentando resolver esse problema complicando o pipeline de montagem adicionando o chamado "tremor de árvore", que corta o excesso do que você importou. Isso diminui a velocidade da montagem, mas nem tudo é cortado.


Idéia: e se não importarmos, apenas pegar e usar, e o próprio colecionador descobrir o que precisa ser importado?


IDEs modernos podem gerar importações automaticamente para as entidades que você usa. Se o IDE puder fazer isso, o que impede o coletor de fazer isso? Basta ter uma convenção simples sobre a nomeação e localização dos arquivos, o que seria conveniente para o usuário e compreensível para a máquina. Há muito tempo o PHP tem uma convenção padrão: PSR-4 . O MAM apresenta o mesmo para arquivos .ts e .jam.js.: os nomes que começam com $ são o nome completo de alguma entidade global cujo código é carregado no caminho obtido no FQN, substituindo delimitadores por barras. Um exemplo simples de dois módulos:


meu / alert / alert.ts


 const $my_alert = alert // FQN    

meu / app / app.ts


 $my_alert( 'Hello!' ) // ,   /my/alert/ 

Um módulo inteiro de uma linha - o que poderia ser mais simples? O resultado não demorou a chegar: a simplicidade de criar e usar módulos leva a minimizar seu tamanho. Como resultado, para maximizar a granularidade. E como uma cereja - minimizando o tamanho dos pacotes sem tremer as árvores.


Um bom exemplo é a família de módulos de validação JSON / mol / data . Se você usar a função $mol_data_integer em algum lugar do seu código, os módulos /mol/data/integer e /mol/data/number , dos quais $mol_data_integer depende, serão incluídos no pacote. Mas, por exemplo, o coletor /mol/data/email nem lê do disco, pois ninguém depende disso.


Arrumando uma bagunça


Desde que começamos a chutar o Angular, não vamos parar. Onde você acha que procura a applyStyles função applyStyles ? Você não vai adivinhar /packages/core/src/render3/styling_next/bindings.ts . A capacidade de colocar qualquer coisa em qualquer lugar leva ao fato de que em cada projeto observamos um sistema único de localização de arquivos, geralmente não passível de lógica. E se o IDE é freqüentemente salvo pelo “salto para a definição”, a visualização do código no github ou a revisão da solicitação de recebimento são privadas dessa oportunidade.


Ideia: E se os nomes das entidades corresponderem estritamente à sua localização?


Para colocar o código no arquivo /angular/packages/core/src/render3/stylingNext/bindings.ts , na arquitetura do MAM, você precisará nomear a entidade $angular_packages_core_src_render3_stylingNext_applyStyles , mas é claro que ninguém agirá, porque há muito mais no nome. Mas os nomes no código que eu quero ver são curtos e concisos, para que o desenvolvedor tente excluir todos os desnecessários do nome, deixando apenas o importante: $angular_render3_applyStyles . E ele estará localizado de acordo em /angular/render3/applyStyles/applyStyles.ts .


Observe como o MAM usa os pontos fracos dos desenvolvedores para alcançar o resultado desejado: cada entidade recebe um nome curto e exclusivo globalmente que pode ser usado em qualquer contexto. Por exemplo, nas mensagens de confirmações, esses nomes permitem capturar de forma rápida e precisa o que eles estão falando:


 73ebc45e517ffcc3dcce53f5b39b6d06fc95cae1 $mol_vector: range expanding support 3a843b2cb77be19688324eeb72bd090d350a6cc3 $mol_data: allowed transformations 24576f087133a18e0c9f31e0d61052265fd8a31a $mol_data_record: support recursion 

Ou, digamos que você queira encontrar todas as menções do módulo $ mol_fiber na Internet - tornando-o mais fácil do que nunca, graças ao FQN.


Dependências cíclicas


Vamos escrever 7 linhas de código simples em um arquivo:


 export class Foo { get bar() { return new Bar(); } } export class Bar extends Foo {} console.log(new Foo().bar); 

Apesar da dependência cíclica, ele funciona corretamente. Dividimos em 3 arquivos:


my / foo.js


 import { Bar } from './bar.js'; export class Foo { get bar() { return new Bar(); } } 

my / bar.js


 import { Foo } from './foo.js'; export class Bar extends Foo {} 

my / app.js


 import { Foo } from './foo.js'; console.log(new Foo().bar); 

Ops, ReferenceError: Cannot access 'Foo' before initialization . Que tipo de bobagem? Para corrigir isso, nosso app.js precisa saber que o foo.js depende do bar.js Portanto, primeiro precisamos importar bar.js , que importa foo.js Após o qual já podemos importar o foo.js sem erros:


my / app.js


 import './bar.js'; import { Foo } from './foo.js'; console.log(new Foo().bar); 

Os navegadores, o NodeJS, o Webpack, o Parcel - todos eles funcionam tortamente com dependências circulares. E bem, eles simplesmente os proibiriam - seria possível complicar imediatamente o código para que não haja loops. Mas eles podem funcionar bem, e então bam, e dar um erro incompreensível.


Ideia: E se, durante a montagem, colarmos os arquivos na ordem correta, como se todo o código estivesse originalmente escrito em um arquivo?


Vamos dividir o código usando os princípios do MAM:


meu / foo / foo.ts


 class $my_foo { get bar() { return new $my_bar(); } } 

meu / bar / bar.ts


 class $my_bar extends $my_foo {} 

meu / app / app.ts


 console.log(new $my_foo().bar); 

Todas as mesmas 7 linhas de código originalmente. E eles simplesmente trabalham sem xamanismo adicional. O problema é que o colecionador entende que a dependência do my/bar no my/foo mais rigorosa do que do my/foo no my/bar . Isso significa que você deve incluir esses módulos no pacote nesta ordem: my/foo , my/bar , my/app .


Como o colecionador entende isso? Agora, a heurística é simples - pelo número de indentação na linha em que a dependência é detectada. Observe que uma dependência mais forte em nosso exemplo tem recuo zero e uma fraca tem recuo duplo.


Línguas diferentes


Aconteceu que, para coisas diferentes, temos idiomas diferentes para essas coisas diferentes afiadas. Os mais comuns são: JS, TS, CSS, HTML, SVG, SCSS, Menos, Stylus. Cada um possui seu próprio sistema de módulos, que não interage com outros idiomas de forma alguma. Escusado será dizer que, cerca de 100500 tipos de idiomas mais específicos. Como resultado, para conectar um componente, você deve conectar separadamente seus scripts, estilos separados, registrar modelos separadamente, configurar separadamente a implantação dos arquivos estáticos necessários, etc., etc.


Graças aos carregadores, o Webpack está tentando resolver esse problema. Mas ele tem um ponto de entrada é um script que já conecta arquivos em outros idiomas. E se não precisarmos de um script? Por exemplo, temos um módulo com belos estilos de chapas e queremos que elas tenham as mesmas cores no tema claro e outros no escuro:


 .dark-theme table { background: black; } .light-theme table { background: white; } 

Além disso, se dependemos do tópico, um script deve ser carregado para instalar o tópico desejado, dependendo da hora do dia. Ou seja, o CSS realmente depende do JS.


Idéia: e se um sistema modular não depender de idiomas?


Como no MAM o sistema modular é separado dos idiomas, as dependências podem ser cruzadas. O CSS pode depender do JS, que pode depender do TS, que pode depender de outro JS. Isso é alcançado devido ao fato de que as dependências de origem são detectadas nos módulos, e os módulos são totalmente conectados e podem conter códigos de origem em qualquer idioma. No caso do exemplo de temas, fica assim:


/my/table/table.css


 /* ,   /my/theme */ [my_theme="dark"] table { background: black; } [my_theme="light"] table { background: white; } 

/my/theme/theme.js


 document.documentElement.setAttribute( 'my_theme' , ( new Date().getHours() + 15 ) % 24 < 12 ? 'light' : 'dark' , ) 

A propósito, usando essa técnica, você pode implementar seu Modernizr , mas sem 300 verificações necessárias, porque apenas as verificações das quais o CSS realmente depende serão incluídas no pacote.


Muitas bibliotecas


Normalmente, o ponto de entrada para a construção de um pacote configurável é algum tipo de arquivo. No caso do Webpack, este é JS. Se você desenvolver muitas bibliotecas e aplicativos alienáveis, precisará de muitos pacotes. E para cada pacote, você precisa criar um ponto de entrada separado. No caso do Parcel, o ponto de entrada é HTML, que para aplicativos terá que ser criado de qualquer maneira. Mas, para as bibliotecas, isso não é muito adequado.


Idéia: E se algum módulo puder ser montado em um pacote independente sem preparação preliminar?


Vamos montar a versão mais recente do construtor de projetos $ mol_build MAM:


 mam mol/build 

Agora execute esse coletor e deixe-o se reunir novamente para garantir que ele ainda possa se reunir:


 node mol/build/-/node.js mol/build 

Embora não, vamos pedir que ele execute testes junto com o assembly:


 node mol/build/-/node.test.js mol/build 

E se tudo correu bem, publique o resultado no NPM:


 npm publish mol/build/- 

Como você pode ver, ao montar o módulo, um subdiretório é criado com o nome - e todos os artefatos de montagem são colocados lá. Vamos examinar os arquivos que você pode encontrar lá:


  • web.dep.json - todas as informações sobre o gráfico de dependência
  • web.js - pacote de scripts do navegador
  • web.js.map - mapas para ele
  • web.esm.js - está na forma de um módulo es
  • web.esm.js.map - e mapas para ele
  • web.test.js - pacote de teste
  • web.test.js.map - e para testes de sorsmap
  • web.d.ts - pacote com tipos de tudo o que está no pacote de scripts
  • web.css - pacote com estilos
  • web.css.map - e mapas de classificação para ele
  • web.test.html - ponto de entrada para executar testes de desempenho em um navegador
  • web.view.tree - declarações de todos os componentes incluídos no pacote view.tree
  • web.locale=*.json - pacotes com textos localizados, cada pacote possui seu próprio pacote
  • package.json - permite publicar imediatamente o módulo montado no NPM
  • node.dep.json - todas as informações sobre o gráfico de dependência
  • node.js - pacote de scripts do nó
  • node.js.map - sorsmaps para ele
  • node.esm.js - está na forma de um módulo es
  • node.esm.js.map - e sorsmaps para ele
  • node.test.js - o mesmo pacote, mas também com testes
  • node.test.js.map - e sorsmaps para ele
  • node.d.ts - pacote com tipos de tudo o que está no pacote de scripts
  • node.view.tree - declarações de todos os componentes incluídos no pacote view.tree
  • node.locale=*.json - pacotes node.locale=*.json com textos localizados, cada pacote node.locale=*.json possui seu próprio pacote node.locale=*.json

A estática é simplesmente copiada junto com os caminhos. Como exemplo, considere um aplicativo que exibe seus próprios códigos-fonte . Suas fontes estão aqui:


  • /mol/app/quine/quine.view.tree
  • /mol/app/quine/quine.view.ts
  • /mol/app/quine/index.html
  • /mol/app/quine/quine.locale=ru.json

Infelizmente, no caso geral, o coletor não pode saber que precisaremos desses arquivos em tempo de execução. Mas podemos dizer isso colocando um arquivo especial por perto:


/mol/app/quine/quine.meta.tree


 deploy \/mol/app/quine/quine.view.tree deploy \/mol/app/quine/quine.view.ts deploy \/mol/app/quine/index.html deploy \/mol/app/quine/quine.locale=ru.json 

Como resultado do assembly /mol/app/quine , eles serão copiados das seguintes maneiras:


  • /mol/app/quine/-/mol/app/quine/quine.view.tree
  • /mol/app/quine/-/mol/app/quine/quine.view.ts
  • /mol/app/quine/-/mol/app/quine/index.html
  • /mol/app/quine/-/mol/app/quine/quine.locale=ru.json

Agora, o diretório /mol/app/quine/- pode ser definido em qualquer hospedagem estática e o aplicativo estará totalmente funcional.


Plataformas alvo


JS pode ser executado no cliente e no servidor. E como é legal quando você pode escrever um código e ele funcionará em qualquer lugar. No entanto, às vezes a implementação da mesma coisa no cliente e no servidor é fundamentalmente diferente. E eu quero, por exemplo, uma implementação a ser usada para um nó e outra para um navegador.


Idéia: e se o objetivo do arquivo estiver refletido em seu nome?


O MAM usa um sistema de tags nos nomes dos arquivos. Por exemplo, o módulo $mol_state_arg fornece acesso a parâmetros de aplicativos definidos pelo usuário. No navegador, esses parâmetros são definidos através da barra de endereço. E no nó, através de argumentos de linha de comando. $mol_sate_arg abstrai o restante do aplicativo dessas nuances implementando ambas as opções com uma única interface, colocando-as em arquivos:


  • / mol / estado / arg / arg. web .ts - implementação para navegadores
  • / mol / estado / arg / arg. node .ts - implementação para um nó

As fontes não identificadas com essas tags são incluídas, independentemente da plataforma de destino.


Uma situação semelhante é observada nos testes - eles querem ser armazenados ao lado do restante das fontes, mas não desejam ser incluídos no pacote configurável que vai para o usuário final. Portanto, os testes também são marcados com uma tag separada:


  • / mol / estado / arg / arg. test .ts - testes de módulo, eles se encaixam no pacote de teste

Tags podem ser paramétricas. Por exemplo, com cada módulo, podem aparecer textos em vários idiomas e devem ser incluídos nos pacotes de idiomas correspondentes. Um arquivo de texto é um dicionário JSON comum nomeado com o código do idioma no nome:


  • / mol / app / life / life. locale = ru .json - textos para o idioma russo
  • / mol / app / life / life. locale = jp .json - textos para japonês

Por fim, e se quisermos colocar arquivos por perto, mas desejar que o coletor os ignore e não os inclua automaticamente no pacote? Basta adicionar no início do nome qualquer caractere não alfanumérico. Por exemplo:


  • / hyoo / brinquedos / . git - começa com um ponto, então o coletor ignorará esse diretório

Versionamento


O Google lançou o AngularJS pela primeira vez e o publicou no NPM como angular . Então ele criou uma estrutura completamente nova com um nome semelhante - Angular e a publicou com o mesmo nome, mas já a versão 2. Agora, esses dois fogos de artifício estão se desenvolvendo de forma independente. Somente uma alteração de quebra de API ocorre entre as principais versões. E o outro - entre o menor . E como é impossível colocar duas versões da mesma dependência no mesmo nível, não se pode falar de uma transição suave, quando duas versões da biblioteca coexistem simultaneamente por algum tempo no aplicativo.


Parece que a equipe da Angular já entrou em todos os possíveis sorteios. E mais uma coisa: o código da estrutura é dividido em vários módulos grandes. No início, eles os fizeram uma versão independente, mas muito rapidamente até eles mesmos começaram a ficar confusos sobre quais versões dos módulos são compatíveis entre si, sem falar nos desenvolvedores comuns. Portanto, o Angular mudou para o versionamento de ponta a ponta , onde a versão principal do módulo pode ser alterada mesmo sem nenhuma alteração no código. O suporte a várias versões de vários módulos é um grande problema, tanto para os mantenedores quanto para o ecossistema como um todo. Afinal, muitos recursos de todos os membros da comunidade são gastos para garantir a compatibilidade com módulos já obsoletos.


A bela idéia do Semantic Versioning se transforma em realidade dura - você nunca sabe se algo vai quebrar quando você altera a versão secundária ou mesmo a versão do patch . Portanto, em muitos projetos, uma versão específica da dependência é corrigida. No entanto, essa correção não afeta dependências transitivas, que podem ser atraídas para a versão mais recente ao instalar a partir do zero, mas podem permanecer as mesmas se já estiverem instaladas. Essa confusão leva ao fato de que você nunca pode confiar em uma versão fixa e precisa verificar regularmente a compatibilidade com as versões atuais de dependências (pelo menos transitivas).


Mas e os arquivos de bloqueio ? Se você estiver desenvolvendo uma biblioteca instalada por meio de dependências, o arquivo de bloqueio não o ajudará, porque será ignorado pelo gerenciador de pacotes. Para a aplicação final, o arquivo de bloqueio fornecerá a chamada "reprodutibilidade de montagens". Mas sejamos honestos. Quantas vezes você precisa criar o aplicativo final a partir da mesma fonte? Exatamente uma vez. Recebendo a saída, independentemente de qualquer NPM, o artefato de montagem: um binário executável, contêiner de docker ou apenas um archive com todo o código necessário para executá-lo. Espero que você não npm install no prod?


Alguns acham o uso de arquivos de bloqueio para que o servidor de CI monte exatamente o que o desenvolvedor comprometeu. Mas espere, o próprio desenvolvedor pode simplesmente montá-lo em sua máquina local. , , , . Continuous Integration , , , , - . CI , .


, , . , Angular@4 ( 3). , , " " " ". Angular@4 , Angular@5. Angular@6, . Angular TypeScript . . , 2 , … , business value , , , , .


, , , , 2 . : , — , — . 3 React, 5 jQuery, 7 lodash.


: — ?


. - . , . , . , . , . , . , , . : issue, , workaround, pull request, , . , , . . .


, . , , . . . : , , -. - — . , , - . , , , NPM . , . .


, ? — . mobx , mobx2 API . — , : , . mobx mobx2 , API. API, .


. — . , :


 var pages_count = $mol_atom2_sync( ()=> $lib_pdfjs.getDocument( uri ).promise ).document().numPages 

mol_atom2_sync lib_pdfjs , :


 npm install mol_atom2_sync@2.1 lib_pdfjs@5.6 

, , — , . ? — , *.meta.tree , :


/.meta.tree


 pack node git \https://github.com/nin-jin/pms-node.git pack mol git \https://github.com/eigenmethod/mol.git pack lib git \https://github.com/eigenmethod/mam-lib.git 

. .


NPM


MAM — NPM . , — . , , NPM .


NPM , $node. , - -:


/my/app/app.ts


 $node.portastic.find({ min : 8080 , max : 8100 , retrieve : 1 }).then( ( ports : number[] ) => { $node.express().listen( ports[0] ) }) 

, . - lib NPM . , NPM- pdfjs-dist :


/lib/pdfjs/pdfjs.ts


 namespace $ { export let $lib_pdfjs : typeof import( 'pdfjs-dist' ) = require( 'pdfjs-dist/build/pdf.min.js' ) $lib_pdfjs.disableRange = true $lib_pdfjs.GlobalWorkerOptions.workerSrc = '-/node_modules/pdfjs-dist/build/pdf.worker.min.js' } 

/lib/pdfjs/pdfjs.meta.tree


 deploy \/node_modules/pdfjs-dist/build/pdf.worker.min.js 

, .



. create-react-app angular-cli , . , , eject . . , , .


: ?


MAM . .


MAM MAM , :


 git clone https://github.com/eigenmethod/mam.git ./mam && cd mam npm install npm start 

8080 . , — MAM.


( — acme ) ( — hello home ):


/acme/acme.meta.tree


 pack hello git \https://github.com/acme/hello.git pack home git \https://github.com/acme/home.git 

npm start :


 npm start acme/hello acme/home 

. — . , , . — : https://t.me/mam_mol

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


All Articles