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:
- 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?
- Torne o método estático e recupere dados usando JokerService.getJokes ().
- 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:
- Ao inicializar o aplicativo, o serviço possui um token. Se não o especificamos especificamente no provedor, esse é o JokerService.
- Quando um serviço é solicitado em um componente, o mecanismo DI verifica se o token transferido existe.
- Se o token não existir, o DI emitirá um erro. No nosso caso, o token existe e o JokerService está localizado nele.
- 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.
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 .