5 coisas que eu gostaria de saber quando comecei a usar o Angular

O Angular moderno é uma estrutura poderosa com muitos recursos, junto com a qual, à primeira vista, surgem conceitos e mecanismos complexos. Isso é especialmente perceptível para aqueles que acabaram de começar a trabalhar tanto no front-end em princípio quanto no Angular em particular.


Também enfrentei o mesmo problema quando cheguei a Tinkoff na posição de Junior Frontend Developer há cerca de dois anos e mergulhei no mundo da Angular. Portanto, ofereço-lhe uma pequena história sobre cinco coisas, cuja compreensão facilitaria muito meu trabalho a princípio.



Injeção de Dependência (DI)


No começo, entrei no componente e vi que havia alguns argumentos no construtor de classes. Fiz uma pequena análise do trabalho dos métodos de classe e ficou claro que essas são algumas dependências externas. Mas como eles entraram na aula? Onde o construtor foi chamado?


Sugiro imediatamente entender um exemplo, mas para isso precisamos de uma aula. Se no JavaScript "normal" o OOP estiver presente com certos "hacks", juntamente com o ES6, haverá uma sintaxe "real". Angular usa o TypeScript imediatamente, na qual a sintaxe é praticamente a mesma. Portanto, proponho usá-lo ainda mais.


Imagine que existe uma classe JokerService em nosso aplicativo que gerencia piadas. O método getJokes() retorna uma lista de piadas. Suponha que a usemos em três lugares. Como obter piadas em três lugares diferentes no código? Existem várias maneiras:


  1. Crie uma instância da classe em todos os lugares. Mas por que precisamos entupir a memória e criar tantos serviços idênticos? E se houver 100 lugares?
  2. Torne o método estático e recupere dados usando JokerService.getJokes ().
  3. Implemente um dos padrões de design. Se precisarmos que o serviço seja um para todo o aplicativo, esse será o Singleton. Mas, para isso, você precisa escrever uma nova lógica na classe.

Então, temos três opções bastante funcionais. O primeiro não nos convém - neste caso, é ineficaz. Não queremos criar cópias extras, pois elas serão completamente idênticas. Restam duas opções.


Vamos complicar a tarefa de entender qual método nos convém melhor. Suponha que, em terceiro lugar, precisamos, por algum motivo, criar nosso próprio serviço com determinados parâmetros. Esse pode ser um autor específico, a duração da piada, o idioma e muito mais. O que faremos então?


No caso do método estático, você terá que passar as configurações a cada chamada, pois a classe é comum a todos os lugares. Ou seja, em cada chamada para getJokes() passaremos todos os parâmetros exclusivos para esse local. Obviamente, é melhor passá-las ao criar a instância e depois chamar o método getJokes() .


Acontece que a segunda opção também não nos serve: fará sempre duplicar muito código em todos os lugares. Resta apenas o Singleton, que novamente precisará atualizar a lógica, mas com variações. Mas como entender qual opção precisamos?


Se você pensou que pode simplesmente criar um objeto e usar a chave para executar o serviço desejado, posso parabenizá-lo: você acabou de perceber como a Injeção de Dependência funciona em geral. Mas vamos um pouco mais fundo.


Para garantir que um mecanismo seja necessário para nos ajudar a obter as instâncias corretas, imagine que o JokerService precise de mais dois serviços, um dos quais é opcional e o segundo deve fornecer um resultado especial em um determinado local. Não é difícil.


Injeção de Dependência em Angular


Como diz a documentação , o DI é um padrão de design importante para um aplicativo. O Angular possui sua própria estrutura de dependência, usada no próprio Angular para aumentar a eficiência e a modularidade.


De um modo geral, a Injeção de Dependências é um mecanismo poderoso no qual uma classe recebe as dependências necessárias de algum lugar externo, em vez de criar instâncias por conta própria.


Deixe a sintaxe e os arquivos com a extensão html não confundirem você. Cada componente no Angular é um objeto JavaScript comum, uma instância de uma classe. Em termos gerais: quando você insere um componente em um modelo, uma instância da classe de componente é criada. Portanto, neste momento, você pode passar as dependências necessárias para o construtor. Agora considere um exemplo:


 @Component({ selector: 'jokes', template: './jokes.template.html', }) export class JokesComponent { private jokes: Observable<IJoke[]>; constructor(private jokerService: JokerService) { this.jokes = this.jokerService.getJokes(); } } 

No construtor de componentes, simplesmente indicamos que precisamos de um JokerService . Nós não criamos nós mesmos. Se houver mais cinco componentes que o usem, todos eles se referirão à mesma instância. Tudo isso nos permite economizar tempo, eliminar clichês e escrever aplicativos muito produtivos.


Fornecedores


E agora proponho lidar com o caso quando você precisar obter instâncias diferentes do serviço. Primeiro, dê uma olhada no próprio serviço:


 @Injectable({ providedIn: 'root', //   ,   «»  }) export class JokerService { getJokes(): Observable<IJoke[]> { //     } } 

Quando o serviço for um para todo o aplicativo, essa opção será suficiente. Mas e se tivermos, digamos, duas implementações do JokerService ? Ou é apenas por algum motivo que um componente específico precisa de sua própria instância de serviço? A resposta é simples: provider .


Por conveniência, chamarei provider provedor de provedor , e o processo de substituição de um valor em uma classe será verificado . Assim, podemos fornecer serviços de diferentes maneiras e em diferentes lugares. Vamos começar com o último. Existem três opções disponíveis:


  • Em todo o aplicativo - especifique provideIn: 'root' no próprio decorador de serviços.
  • No módulo - especifique o provedor no decorador de serviços como provideIn: JokesModule ou no decorador do módulo @NgModule providers: [JokerService] .
  • No componente - especifique o provedor no decorador do componente, como no módulo.

O local é escolhido dependendo de suas necessidades. Nós descobrimos o lugar, vamos para o mecanismo em si. Se simplesmente especificássemos o provideIn: root no serviço, isso seria equivalente à seguinte entrada no módulo:


 @NgModule({ // ...     providers: [{provide: JokerService, useClass: JokerService}], }) //   

Isso pode ser lido da seguinte maneira: "Se um JokerService solicitado, forneça uma instância da classe JokerService» A partir daqui, você pode obter uma instância específica de várias maneiras:


  • Por token - você precisa especificar um InjectionToken e obter um serviço nele. Observe que nos exemplos abaixo em provide você pode transmitir o mesmo token:


     const JOKER_SERVICE_TOKEN = new InjectionToken<string>('JokerService'); // ...     [{provide: JOKER_SERVICE_TOKEN, useClass: JokerService}]; 

  • Por classe - você pode substituir a classe. Por exemplo, solicitaremos o JokerService e forneceremos - JokerHappyService :


     [{provide: JokerService, useClass: JokerHappyService}]; 

  • Por valor - você pode retornar imediatamente a instância desejada:


     [{provide: JokerService, useValue: jokerService}]; 

  • Por fábrica - você pode substituir a classe por uma fábrica que criará a instância desejada quando for acessada:


     [{provide: JokerService, useFactory: jokerServiceFactory}]; 


Isso é tudo. Ou seja, para resolver o exemplo com uma instância especial, você pode usar qualquer um dos métodos acima. Escolha o mais adequado às suas necessidades.


A propósito, o DI trabalha não apenas para serviços, mas em geral para qualquer entidade que você obtém no construtor de componentes. Este é um mecanismo muito poderoso que deve ser usado em todo o seu potencial.


Um pequeno resumo


Para um entendimento completo, proponho considerar o mecanismo simplificado de injeção de dependência no Angular em etapas usando o exemplo de serviço:


  1. Ao inicializar o aplicativo, o serviço possui um token. Se não o especificamos especificamente no provedor, esse é o JokerService.
  2. Quando um serviço é solicitado em um componente, o mecanismo DI verifica se o token transferido existe.
  3. Se o token não existir, o DI emitirá um erro. No nosso caso, o token existe e o JokerService está localizado nele.
  4. Quando o componente é criado, uma instância do JokerService é passada ao construtor como argumento.

Detecção de alterações


Frequentemente ouvimos, como argumento para o uso de estruturas, algo como “A estrutura fará tudo por você - de maneira mais rápida e eficiente. Você não precisa pensar em nada. Apenas gerencie os dados. ” Talvez isso seja verdade com uma aplicação muito simples. Mas se você precisar trabalhar com as informações do usuário e operar constantemente com os dados, precisará saber como funciona o processo de detecção de alterações e renderização.


Em Angular, a Detecção de alterações é responsável por verificar as alterações. Como resultado de várias operações - alterando o valor de uma propriedade de classe, concluindo uma operação assíncrona, respondendo a uma solicitação HTTP e assim por diante - o processo de verificação é iniciado na árvore de componentes.


Como o principal objetivo do processo é entender como renderizar novamente um componente, a essência é verificar os dados usados ​​nos modelos. Se forem diferentes, o modelo será marcado como "alterado" e será redesenhado.


Zone.js


Compreender como o Angular controla as propriedades da classe e as operações síncronas é bastante simples. Mas como ele rastreia assíncrono? A biblioteca Zone.js, criada por um dos desenvolvedores Angular, é responsável por isso.


Aqui está o que é. Uma zona em si é um "contexto de execução", para ser franco, o local e o estado em que o código é executado. Após a conclusão da operação assíncrona, a função de retorno de chamada é executada na mesma zona em que foi registrada. Então a Angular descobre onde a alteração ocorreu e o que verificar.


O Zone.js substitui por suas implementações quase todas as funções e métodos assíncronos nativos. Portanto, ele pode rastrear quando o callback uma função assíncrona será chamado. Ou seja, o Zone informa ao Angular quando e onde iniciar o processo de validação de alterações.


Estratégias de detecção de alterações


Nós descobrimos como o Angular monitora um componente e executa a verificação de alterações. Agora imagine que você tenha um aplicativo enorme com dezenas de componentes. E para cada clique, toda operação assíncrona, toda solicitação executada com sucesso, uma verificação é iniciada em toda a árvore de componentes. Provavelmente, esse aplicativo terá sérios problemas de desempenho.


Os desenvolvedores angulares pensaram sobre isso e nos deram a oportunidade de estabelecer uma estratégia de detecção de alterações, cuja escolha correta pode aumentar significativamente a produtividade.


Há duas opções para escolher:


  • Padrão - como o nome sugere, esta é a estratégia padrão quando um CD é lançado para cada ação.
  • OnPush é uma estratégia na qual um CD é lançado em apenas alguns casos:
    • se o valor de @Input() mudou;
    • se um evento ocorreu dentro do componente ou de seus descendentes;
    • se a verificação foi iniciada manualmente;
    • se um novo evento chegar no Tubo assíncrono.

Com base na minha própria experiência em desenvolvimento no Angular, bem como na experiência de meus colegas, posso afirmar com certeza que é melhor sempre indicar a estratégia OnPush , a menos que o default realmente necessário. Isso lhe dará várias vantagens:


  • Uma compreensão clara de como o processo do CD funciona.
  • Trabalho puro com propriedades @Input() .
  • Ganho de desempenho.

Trabalhar com @Input()


Como outras estruturas populares, o Angular usa um fluxo de dados downstream. O componente aceita parâmetros de entrada marcados com o decorador @Input() . Considere um exemplo:


 interface IJoke { author: string; text: string; } @Component({ selector: 'joke', template: './joke.template.html', }) export class JokeComponent { @Input() joke: IJoke; } 

Suponha que exista um componente descrito acima que exiba o texto da piada e o autor. O problema com esta redação é que você pode alterar acidental ou especificamente o objeto transferido. Por exemplo, substitua o texto ou autor.


 setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.joke.author = name; } 

Percebo imediatamente que este é um mau exemplo, mas mostra claramente o que pode acontecer. Para se proteger contra esses erros, é necessário tornar os parâmetros de entrada somente leitura. Graças a isso, você entenderá como trabalhar com os dados corretamente e criar um CD. Com base nisso, a melhor maneira de escrever uma classe será algo como isto:


 @Component({ selector: 'joke', template: './joke.template.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class JokeComponent { @Input() readonly joke: IJoke; @Output() updateName = new EventEmitter<string>(); setAuthorNameOnly() { const name = this.joke.author.split(' ')[0]; this.updateName.emit(name); } } 

A abordagem descrita não é uma regra, mas apenas uma recomendação. Existem muitas situações em que essa abordagem será inconveniente e ineficaz. Com o tempo, você aprenderá a entender em que caso poderá recusar o método proposto de trabalhar com entradas.


Rxjs


É claro que posso estar errado, mas parece que o ReactiveX e a programação reativa em geral são uma nova tendência. Angular sucumbiu a essa tendência (ou talvez a tenha criado) e usa o RxJS por padrão. A lógica básica de toda a estrutura é executada nesta biblioteca, portanto, é muito importante entender os princípios da programação reativa.


Mas o que é RxJS? Ele combina três idéias que vou revelar em uma linguagem bastante simples, com algumas omissões:


  • O padrão "Observador" é uma entidade que produz eventos, e há um ouvinte que recebe informações sobre esses eventos.
  • O Padrão Iterador - permite obter acesso seqüencial aos elementos de um objeto sem revelar sua estrutura interna.
  • A programação funcional com coleções é um padrão no qual a lógica bate em componentes pequenos e muito simples, cada um dos quais resolve apenas um problema.

A combinação desses padrões nos permite descrever de maneira simples algoritmos complexos à primeira vista, por exemplo:


 private loadUnreadJokes() { this.showLoader(); //   fromEvent(document, 'load') .pipe( switchMap( () => this.http .get('/api/v1/jokes') //   .pipe(map((jokes: any[]) => jokes.filter(joke => joke.unread))), //   ), ) .subscribe( (jokes: any[]) => (this.jokes = jokes), //   error => { /*   */ }, () => this.hideLoader(), //       ); } 

Apenas 18 linhas com todo o belo recuo. Agora tente reescrever este exemplo no Vanilla ou pelo menos no jQuery. Quase 100% disso ocupará pelo menos o dobro de espaço e não será tão expressivo. Aqui você pode simplesmente seguir a linha com os olhos e ler o código como um livro.


Observável


O entendimento de que qualquer dado pode ser representado como um fluxo não vem imediatamente. Portanto, proponho passar para uma analogia simples. Imagine um fluxo é uma matriz de dados classificados por tempo. Por exemplo, nesta modalidade:


 const observable = []; let counter = 0; const intervalId = setInterval(() => { observable.push(counter++); }, 1000); setTimeout(() => { clearInterval(intervalId); }, 6000); 

Vamos considerar o último valor na matriz como relevante. A cada segundo, um número será adicionado à matriz. Como podemos descobrir em outro lugar no aplicativo que um elemento foi adicionado à matriz? Em uma situação normal, chamaríamos algum tipo de callback de callback e atualizaríamos o valor da matriz e, então, pegaríamos o último elemento.


Graças à programação reativa, não é necessário escrever apenas muita lógica nova, mas também pensar em atualizar as informações. Isso pode ser comparado a um ouvinte simples:


 document.addEventListener('click', event => {}); 

Você pode colocar muitos EventListener em todo o aplicativo, e eles funcionarão, a menos que, é claro, você cuide do oposto de propósito.


A programação reativa também funciona. Em um lugar, simplesmente criamos um fluxo de dados e periodicamente lançamos novos valores lá; em outro, assinamos esse fluxo e simplesmente ouvimos esses valores. Ou seja, sempre aprendemos sobre a atualização e podemos lidar com isso.


Agora vamos ver um exemplo real:


 export class JokesListComponent implements OnInit { jokes$: Observable<IJoke>; authors$ = new Subject<string[]>(); unread$ = new Subject<number>(); constructor(private jokerService: JokerService) {} ngOnInit() { //  ,    subscribe()    this.jokes$ = this.jokerService.getJokes(); this.jokes$.subscribe(jokes => { this.authors$.next(jokes.map(joke => joke.author)); this.unread$.next(jokes.filter(joke => joke.unread).length); }); } } 

Graças a essa lógica, ao alterar dados em jokes , atualizamos automaticamente os dados sobre o número de piadas não lidas e a lista de autores. Se você tiver mais alguns componentes, um dos quais coleta estatísticas sobre o número de piadas lidas por um autor e o segundo calcula a duração média das piadas, as vantagens se tornam óbvias.


Testbed


Mais cedo ou mais tarde, o desenvolvedor entende que, se o projeto não for MVP, será necessário escrever testes. E quanto mais testes forem escritos, mais clara e detalhada será a descrição, mais fácil, mais rápido e mais confiável será fazer alterações e implementar novas funcionalidades.


Angular provavelmente previu isso e nos deu uma poderosa ferramenta de teste. Muitos desenvolvedores, no início, tentam dominar algum tipo de tecnologia "desde o início", sem entrar na documentação. Fiz o mesmo, e foi por isso que percebi bastante tarde todos os recursos de teste disponíveis "prontos para uso".


Você pode testar qualquer coisa no Angular, mas se precisar instanciar e começar a chamar métodos para testar uma classe ou serviço regular, a situação com o componente é completamente diferente.


Como já descobrimos, graças às dependências de DI são retiradas do componente. Por um lado, isso complica um pouco todo o sistema, por outro, oferece grandes oportunidades para configurar testes e verificar muitos casos. Proponho entender o exemplo de um componente:


 @Component({ selector: 'app-joker', template: '<some-dependency></some-dependency>', styleUrls: ['./joker.component.less'], }) export class JokerComponent { constructor( private jokesService: JokesService, @Inject(PARTY_TOKEN) private partyService: PartyService, @Optional() private sleepService: SleepService, ) {} makeNewFriend(): IFriend { if (this.sleepService && this.sleepService.isSleeping) { this.sleepService.wakeUp(); } const joke = this.jokesService.generateNewJoke(); this.partyService.goToParty('Pacha'); this.partyService.toSay(joke.text); const laughingPeople = this.partyService.getPeopleByReaction('laughing'); const girl = laughingPeople.find(human => human.sex === 'female'); const friend = this.partyService.makeFriend(girl); return friend; } } 

Portanto, no exemplo atual, existem três serviços. Um é importado da maneira usual, um por token e outro serviço é opcional. Como configuramos o módulo de teste? Mostrarei imediatamente a exibição concluída:


 beforeEach(async(() => { TestBed.configureTestingModule({ imports: [SomeDependencyModule], declarations: [JokerComponent], //  ,    providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }).compileComponents(); fixture = TestBed.createComponent(JokerComponent); component = fixture.componentInstance; fixture.detectChanges(); //    ,     })); 

TestBed nos permite fazer uma simulação completa do módulo necessário. Você pode conectar a ele quaisquer serviços, substituir módulos, obter instâncias de classes de um componente e muito mais. Agora que já temos o módulo configurado, vamos para as possibilidades.


Dependências desnecessárias podem ser evitadas


Um aplicativo Angular consiste em módulos, que podem incluir outros módulos, serviços, diretrizes e muito mais. No teste, precisamos, de fato, recriar a operação do módulo. Se, em nosso exemplo, usarmos <some-dependency></some-dependency> no modelo, isso significa que devemos importar o SomeDependencyModule para o teste também. E se houver vícios lá? Portanto, eles também precisam ser importados.
Se o aplicativo for complexo, haverá muitas dessas dependências. A importação de todas as dependências levará ao fato de que em cada teste o aplicativo inteiro estará localizado e todos os métodos serão chamados. Talvez isso não nos convenha.


Há pelo menos uma maneira de se livrar das dependências necessárias - basta reescrever o modelo. Suponha que você tenha testes de captura de tela ou testes de integração e não há necessidade de testar a aparência do componente. Então basta verificar os métodos. Nesse caso, você pode escrever a configuração da seguinte maneira:


 TestBed.configureTestingModule({ declarations: [JokerComponent], providers: [{provide: PARTY_TOKEN, useClass: PartyService}], }) .overrideTemplate(JokerComponent, '') //   ,   .compileComponents(); 

Sim, isso não é para todos. Dentro do Tinkoff, concordamos em usar essa abordagem apenas nos casos em que não há necessidade de verificar a exibição do componente. Por exemplo, ao trabalhar apenas com dados ou se comunicar com a parte. Se houver necessidade de verificar como os dados são transferidos para componentes filho ou, por exemplo, como a entrada do usuário é processada, essa opção não funcionará. Se você tiver esse caso, vá para o próximo parágrafo.


Você pode molhar todas as dependências do construtor


Já nos familiarizamos com o Token de injeção , por isso proponho a começar imediatamente os negócios. No exemplo acima, eu já verifiquei o serviço de token no teste. Se você não está escrevendo um teste de integração, não faz sentido chamar métodos de um serviço real, apenas faça uma simulação.


ts-mockito , , . Angular « ».


 //    export class MockPartyService extends PartyService { meetFriend(): IFriend { return {} as IFriend; } goToParty() {} toSay(some: string) { console.log(some); } } // ... TestBed.configureTestingModule({ declarations: [JokerComponent, MockComponent], providers: [{provide: PARTY_TOKEN, useClass: MockPartyService}], //    }).compileComponents(); 

Isso é tudo. .



. , — , — . , :


  • .
  • — , . — .

— . , . — .


Sumário


Angular, . , , «».


, Angular - . HTTP-, , lazy-loading . Angular .

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


All Articles