Já passou bastante tempo desde o lançamento do Angular atualizado. Atualmente, muitos projetos foram concluídos. Desde o "início", muitos desenvolvedores já passaram para o uso significativo dessa estrutura, seus recursos e aprenderam como contornar as armadilhas. Cada desenvolvedor e / ou equipe já formou seus próprios guias de estilo e práticas recomendadas ou usa outros. Mas, ao mesmo tempo, muitas vezes você precisa lidar com muito código Angular, que não usa muitos dos recursos dessa estrutura e / ou escrito no estilo do AngularJS.
Este artigo apresenta alguns dos recursos do uso da estrutura Angular, que, de acordo com a opinião modesta do autor, não são abordados adequadamente nos manuais ou não são usados pelos desenvolvedores. O artigo discute o uso de solicitações HTTP "Interceptores", o uso de Route Guards para limitar o acesso aos usuários. Algumas recomendações sobre o uso do RxJS e o gerenciamento do estado do aplicativo são fornecidas. Também são apresentadas algumas recomendações sobre o design do código do projeto, o que provavelmente tornará o código do projeto mais limpo e compreensível. O autor espera que este artigo seja útil não apenas para desenvolvedores que estão começando a se familiarizar com o Angular, mas também para desenvolvedores experientes.
Trabalhar com HTTP
A construção de qualquer aplicativo Web cliente é feita em torno de solicitações HTTP para o servidor. Esta parte discute alguns dos recursos da estrutura Angular para trabalhar com solicitações HTTP.
Usando interceptores
Em alguns casos, pode ser necessário modificar a solicitação antes que ela atinja o servidor. Ou você precisa alterar cada resposta. Começando com o Angular 4.3, um novo HttpClient foi lançado. Ele acrescentou a capacidade de interceptar uma solicitação usando interceptores (Sim, eles finalmente foram retornados apenas na versão 4.3 !, Esse era um dos recursos ausentes mais esperados dos AngularJs que não migraram para o Angular). Esse é um tipo de middleware entre o http-api e a solicitação real.
Um caso de uso comum pode ser autenticação. Para obter uma resposta do servidor, você geralmente precisa adicionar algum tipo de mecanismo de autenticação à solicitação. Esta tarefa usando interceptores é resolvida de maneira bem simples:
import { Injectable } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { HttpEvent, HttpInterceptor, HttpHandler, HttpRequest } from @angular/common/http"; @Injectable() export class JWTInterceptor implements HttpInterceptor { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { req = req.clone({ setHeaders: { authorization: localStorage.getItem("token") } }); return next.handle(req); } }
Como um aplicativo pode ter vários interceptores, eles são organizados em uma cadeia. O primeiro elemento é chamado pela própria estrutura Angular. Posteriormente, somos responsáveis por transmitir a solicitação ao próximo interceptador. Para fazer isso, chamamos o método handle do próximo elemento da cadeia assim que terminamos. Conectamos o interceptador:
import { BrowserModule } from "@angular/platform-browser"; import { NgModule } from "@angular/core"; import { AppComponent } from "./app.component"; import { HttpClientModule } from "@angular/common/http"; import { HTTP_INTERCEPTORS } from "@angular/common/http"; @NgModule({ declarations: [AppComponent], imports: [BrowserModule, HttpClientModule], providers: [ { provide: HTTP_INTERCEPTORS, useClass: JWTInterceptor, multi: true } ], bootstrap: [AppComponent] }) export class AppModule {}
Como você pode ver, a conexão e implementação de interceptores é bastante simples.
Rastreamento de progresso
Um dos recursos do HttpClient
é a capacidade de acompanhar o andamento de uma solicitação. Por exemplo, se você precisar fazer o download de um arquivo grande, provavelmente desejará informar o usuário sobre o andamento do download. Para obter progresso, você deve definir a propriedade reportProgress
do objeto HttpRequest
como true
. Um exemplo de serviço que implementa essa abordagem:
import { Observable } from "rxjs/Observable"; import { HttpClient } from "@angular/common/http"; import { Injectable } from "@angular/core"; import { HttpRequest } from "@angular/common/http"; import { Subject } from "rxjs/Subject"; import { HttpEventType } from "@angular/common/http"; import { HttpResponse } from "@angular/common/http"; @Injectable() export class FileUploadService { constructor(private http: HttpClient) {} public post(url: string, file: File): Observable<number> { var subject = new Subject<number>(); const req = new HttpRequest("POST", url, file, { reportProgress: true }); this.httpClient.request(req).subscribe(event => { if (event.type === HttpEventType.UploadProgress) { const percent = Math.round((100 * event.loaded) / event.total); subject.next(percent); } else if (event instanceof HttpResponse) { subject.complete(); } }); return subject.asObservable(); } }
O método post retorna um Observable
que representa o progresso do download. Tudo o que é necessário agora é exibir o progresso do carregamento no componente.
Encaminhamento Usando o Route Guard
O roteamento permite mapear solicitações de aplicativos para recursos específicos dentro do aplicativo. Muitas vezes, é necessário resolver o problema de limitar a visibilidade do caminho ao longo do qual determinados componentes estão localizados, dependendo de algumas condições. Nesses casos, o Angular possui um mecanismo de restrição de transição. Como exemplo, há um serviço que implementará o protetor de rota. Suponha que, em um aplicativo, a autenticação do usuário seja implementada usando o JWT. Uma versão simplificada do serviço que verifica se o usuário está autorizado pode ser representada como:
@Injectable() export class AuthService { constructor(public jwtHelper: JwtHelperService) {} public isAuthenticated(): boolean { const token = localStorage.getItem("token");
Para implementar o protetor de rota, você deve implementar a interface CanActivate
, que consiste em uma única função canActivate
.
@Injectable() export class AuthGuardService implements CanActivate { constructor(public auth: AuthService, public router: Router) {} canActivate(): boolean { if (!this.auth.isAuthenticated()) { this.router.navigate(["login"]); return false; } return true; } }
A implementação AuthGuardService
usa o AuthGuardService
descrito acima para verificar a autorização do usuário. O método canActivate
retorna um valor booleano que pode ser usado na condição de ativação da rota.
Agora podemos aplicar o Route Guard criado a qualquer rota ou caminho. Para fazer isso, ao declarar Routes
especificamos nosso serviço, que herda a interface CanActivate
, na seção canActivate
:
export const ROUTES: Routes = [ { path: "", component: HomeComponent }, { path: "profile", component: UserComponent, canActivate: [AuthGuardService] }, { path: "**", redirectTo: "" } ];
Nesse caso, a rota /profile
possui o valor de configuração opcional canActivate
. AuthGuard
descrito anteriormente é passado como argumento para essa propriedade canActivate
. Em seguida, o método canActivate
será chamado toda vez que alguém tentar acessar o caminho /profile
. Se o usuário estiver autorizado, ele obterá acesso ao caminho /profile
, caso contrário, ele será redirecionado para o caminho /login
.
Você deve estar ciente de que canActivate
ainda permite ativar o componente nesse caminho, mas não permite que você alterne para ele. Se você precisar proteger a ativação e o carregamento do componente, nesse caso, podemos usar canLoad
. CanLoad
implementação do CanLoad
pode ser feita por analogia.
Cooking RxJS
Angular é construído sobre o RxJS. O RxJS é uma biblioteca para trabalhar com fluxos de dados assíncronos e baseados em eventos usando sequências observáveis. RxJS é uma implementação JavaScript da API do ReactiveX. Na maioria das vezes, os erros que ocorrem ao trabalhar com esta biblioteca estão associados ao conhecimento superficial dos conceitos básicos de sua implementação.
Usando assíncrono em vez de se inscrever para eventos
Um grande número de desenvolvedores que recentemente passaram a usar a estrutura Angular usam a função de subscribe
do Observable
para receber e salvar dados no componente:
@Component({ selector: "my-component", template: ` <span>{{localData.name}} : {{localData.value}}</span>` }) export class MyComponent { localData; constructor(http: HttpClient) { http.get("api/data").subscribe(data => { this.localData = data; }); } }
Em vez disso, podemos assinar o modelo usando o canal assíncrono:
@Component({ selector: "my-component", template: ` <p>{{data.name | async}} : {{data.value | async}}</p>` }) export class MyComponent { data; constructor(http: HttpClient) { this.data = http.get("api/data"); } }
Ao assinar um modelo, evitamos vazamentos de memória porque o Angular cancela automaticamente a assinatura de Observable
quando um componente é interrompido. Nesse caso, para solicitações HTTP, o uso de pipe assíncrono praticamente não oferece nenhum benefício, exceto por uma coisa - o async cancelará a solicitação se os dados não forem mais necessários e não concluirá o processamento da solicitação.
Muitos recursos do Observables
não Observables
usados ao se inscrever manualmente. Observables
comportamento dos Observables
pode ser estendido repetindo (por exemplo, tente novamente em uma solicitação http), atualização com base no cronômetro ou pré-armazenamento em cache.
Use $
para indicar observáveis
O próximo parágrafo está relacionado ao design dos códigos-fonte do aplicativo e segue o parágrafo anterior. Para distinguir as variáveis Observable
das simples, muitas vezes você pode ouvir os conselhos para usar o sinal " $
" no nome de uma variável ou campo. Esse truque simples eliminará a confusão nas variáveis ao usar o assíncrono.
import { Component } from "@angular/core"; import { Observable } from "rxjs/Rx"; import { UserClient } from "../services/user.client"; import { User } from "../services/user"; @Component({ selector: "user-list", template: ` <ul class="user_list" *ngIf="(users$ | async).length"> <li class="user" *ngFor="let user of users$ | async"> {{ user.name }} - {{ user.birth_date }} </li> </ul>` }) export class UserList { public users$: Observable<User[]>; constructor(public userClient: UserClient) {} public ngOnInit() { this.users$ = this.client.getUsers(); } }
Quando cancelar a inscrição (cancelar a inscrição)
A pergunta mais comum que um desenvolvedor tem ao conhecer brevemente o Angular é quando você ainda precisa cancelar a assinatura e quando não. Para responder a essa pergunta, primeiro você precisa decidir que tipo de Observable
está sendo usado no momento. No Angular existem 2 tipos de Observable
- finito e infinito, alguns produzem um finito, outros, respectivamente, um número infinito de valores.
Http
Observable
é compacto e os ouvintes / ouvintes dos eventos DOM são infinitos.
Se a assinatura dos valores de um Observable
infinito Observable
feita manualmente (sem o uso de canal assíncrono), será necessário responder sem falhas. Se subscrevermos manualmente um Observable finito, não será necessário cancelar a inscrição, o RxJS cuidará disso. No caso de Observables
compactos Observables
podemos cancelar a assinatura se o Observable
tiver um tempo de execução maior que o necessário, por exemplo, uma solicitação HTTP múltipla.
Um exemplo de Observables
compactos:
export class SomeComponent { constructor(private http: HttpClient) { } ngOnInit() { Observable.timer(1000).subscribe(...); this.http.get("http://api.com").subscribe(...); } }
Exemplo de Observáveis Infinitos
export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.interval = Observable.interval(1000).subscribe(...); this.click = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.interval.unsubscribe(); this.click.unsubscribe(); } }
Abaixo, com mais detalhes, estão os casos em que você precisa cancelar a inscrição
- É necessário cancelar a inscrição no formulário e nos controles individuais nos quais você se inscreveu:
export class SomeComponent { ngOnInit() { this.form = new FormGroup({...}); this.valueChangesSubs = this.form.valueChanges.subscribe(...); this.statusChangesSubs = this.form.statusChanges.subscribe(...); } ngOnDestroy() { this.valueChangesSubs.unsubscribe(); this.statusChangesSubs.unsubscribe(); } }
- Roteador De acordo com a documentação, o Angular deve cancelar sua inscrição, mas isso não acontece . Portanto, para evitar mais problemas, escrevemos por nós mesmos:
export class SomeComponent { constructor(private route: ActivatedRoute, private router: Router) { } ngOnInit() { this.route.params.subscribe(..); this.route.queryParams.subscribe(...); this.route.fragment.subscribe(...); this.route.data.subscribe(...); this.route.url.subscribe(..); this.router.events.subscribe(...); } ngOnDestroy() {
- Sequências sem fim. Exemplos são sequências criadas usando
interva()
ou listeners de eventos (fromEvent())
:
export class SomeComponent { constructor(private element : ElementRef) { } interval: Subscription; click: Subscription; ngOnInit() { this.intervalSubs = Observable.interval(1000).subscribe(...); this.clickSubs = Observable.fromEvent(this.element.nativeElement, "click").subscribe(...); } ngOnDestroy() { this.intervalSubs.unsubscribe(); this.clickSubs.unsubscribe(); } }
takeUntil e takeWhile
Para simplificar o trabalho com Observables
infinitos no RxJS, existem duas funções convenientes - takeUntil
e takeWhile
. Eles executam a mesma ação - cancelando a inscrição no Observable
no final de alguma condição, a diferença está apenas nos valores aceitos. takeWhile
aceita um boolean
e takeUntil
um Subject
.
Exemplo de takeWhile
:
export class SomeComponent implements OnDestroy, OnInit { public user: User; private alive: boolean = true; public ngOnInit() { this.userService .authenticate(email, password) .takeWhile(() => this.alive) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.alive = false; } }
Nesse caso, quando o sinalizador alive
for alterado, o Observable
cancelará a inscrição. Neste exemplo, cancele a inscrição quando o componente for destruído.
Exemplo de takeUntil
:
export class SomeComponent implements OnDestroy, OnInit { public user: User; private unsubscribe: Subject<void> = new Subject(void); public ngOnInit() { this.userService.authenticate(email, password) .takeUntil(this.unsubscribe) .subscribe(user => { this.user = user; }); } public ngOnDestroy() { this.unsubscribe.next(); this.unsubscribe.complete(); } }
Nesse caso, para cancelar a inscrição em Observable
relatamos que o subject
pega o próximo valor e o completa.
O uso dessas funções evitará vazamentos e simplificará o trabalho com a desinscrição de dados. Qual função usar? A resposta a esta pergunta deve ser guiada por preferências pessoais e requisitos atuais.
Gerenciamento de estado em aplicativos angulares, @ ngrx / store
Com frequência, ao desenvolver aplicativos complexos, somos confrontados com a necessidade de armazenar estados e responder a suas alterações. Existem muitas bibliotecas para aplicativos desenvolvidos na estrutura ReactJs que permitem controlar o estado do aplicativo e responder a suas alterações - Flux, Redux, Redux-saga, etc. Para aplicativos Angular, existe um contêiner de estado baseado em RxJS inspirado no Redux - @ ngrx / store. O gerenciamento adequado do estado do aplicativo salvará o desenvolvedor de muitos problemas com a expansão adicional do aplicativo.
Por que Redux
O Redux se posiciona como um contêiner de estado previsível para aplicativos JavaScript. Redux é inspirado no Flux and Elm.
O Redux sugere pensar no aplicativo como um estado inicial modificável por uma sequência de ações, que pode ser uma boa abordagem para criar aplicativos da Web complexos.
O Redux não está associado a nenhuma estrutura específica e, embora tenha sido desenvolvido para o React, pode ser usado com Angular ou jQuery.
Os principais postulados do Redux:
- um repositório para todo o estado do aplicativo
- estado somente leitura
- as alterações são feitas por funções "puras", sujeitas aos seguintes requisitos:
- não deve fazer chamadas externas por uma rede ou banco de dados;
- retornar um valor que depende apenas dos parâmetros passados;
- argumentos são imutáveis, ou seja, funções não devem alterá-las;
- chamar uma função pura com os mesmos argumentos sempre retorna o mesmo resultado;
Um exemplo de uma função de gerenciamento de estado:
// counter.ts import { ActionReducer, Action } from "@ngrx/store"; export const INCREMENT = "INCREMENT"; export const DECREMENT = "DECREMENT"; export const RESET = "RESET"; export function counterReducer(state: number = 0, action: Action) { switch (action.type) { case INCREMENT: return state + 1; case DECREMENT: return state - 1; case RESET: return 0; default: return state; } }
O redutor é importado no módulo principal do aplicativo e, usando a função StoreModule.provideStore(reducers)
, disponibilizamos para o injetor Angular:
// app.module.ts import { NgModule } from "@angular/core"; import { StoreModule } from "@ngrx/store"; import { counterReducer } from "./counter"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ counter: counterReducer }) ] }) export class AppModule { }
Em seguida, o serviço Store
é introduzido nos componentes e serviços necessários. A função store.select () é usada para selecionar o estado “fatia”:
// app.component.ts ... interface AppState { counter: number; } @Component({ selector: "my-app", template: ` <button (click)="increment()">Increment</button> <div>Current Count: {{ counter | async }}</div> <button (click)="decrement()">Decrement</button> <button (click)="reset()">Reset Counter</button>` }) class AppComponent { counter: Observable<number>; constructor(private store: Store<AppState>) { this.counter = store.select("counter"); } increment() { this.store.dispatch({ type: INCREMENT }); } decrement() { this.store.dispatch({ type: DECREMENT }); } reset() { this.store.dispatch({ type: RESET }); } }
@ ngrx / loja de roteadores
Em alguns casos, é conveniente vincular o estado do aplicativo à rota atual do aplicativo. Nesses casos, o módulo @ ngrx / router-store existe. Para que o aplicativo use o router-store
para salvar o estado, basta conectar o routerReducer
e adicionar uma chamada ao RouterStoreModule.connectRoute
no módulo principal do aplicativo:
import { StoreModule } from "@ngrx/store"; import { routerReducer, RouterStoreModule } from "@ngrx/router-store"; @NgModule({ imports: [ BrowserModule, StoreModule.provideStore({ router: routerReducer }), RouterStoreModule.connectRouter() ], bootstrap: [AppComponent] }) export class AppModule { }
Agora adicione o RouterState
ao estado principal do aplicativo:
import { RouterState } from "@ngrx/router-store"; export interface AppState { ... router: RouterState; };
Além disso, podemos indicar o estado inicial do aplicativo ao declarar o armazenamento:
StoreModule.provideStore( { router: routerReducer }, { router: { path: window.location.pathname + window.location.search } } );
Ações suportadas:
import { go, replace, search, show, back, forward } from "@ngrx/router-store"; // store.dispatch(go(["/path", { routeParam: 1 }], { query: "string" })); // store.dispatch(replace(["/path"], { query: "string" })); // store.dispatch(show(["/path"], { query: "string" })); // store.dispatch(search({ query: "string" })); // store.dispatch(back()); // store.dispatch(forward());
UPD: O comentário sugeriu que essas ações não estarão disponíveis na nova versão @ngrx, para a nova versão https://github.com/ngrx/platform/blob/master/MIGRATION.md#ngrxrouter-store
O uso do contêiner de estado eliminará muitos problemas ao desenvolver aplicativos complexos. No entanto, é importante tornar o gerenciamento de estado o mais simples possível. Muitas vezes, é preciso lidar com aplicativos em que há aninhamento excessivo de estados, o que complica apenas o entendimento do aplicativo.
Organização do código
Livrar-se de expressões volumosas na import
Muitos desenvolvedores estão cientes de uma situação em que as expressões na import
bastante complicadas. Isso é especialmente visível em aplicativos grandes, onde existem muitas bibliotecas reutilizáveis.
import { SomeService } from "../../../core/subpackage1/subpackage2/some.service";
O que mais há de ruim nesse código? Caso você precise transferir nosso componente para outro diretório, as expressões na import
não serão válidas.
Nesse caso, o uso de aliases nos permitirá evitar expressões volumosas na import
e tornar nosso código muito mais limpo. Para preparar o projeto para usar aliases, você precisa adicionar as propriedades baseUrl e path em tsconfig.json
:
/ tsconfig.json { "compilerOptions": { ... "baseUrl": "src", "paths": { "@app/*": ["app/*"], "@env/*": ["environments/*"] } } }
Com essas alterações, é fácil gerenciar plug-ins:
import { Component, OnInit } from "@angular/core"; import { Observable } from "rxjs/Observable"; import { SomeService } from "@app/core"; import { environment } from "@env/environment"; import { LocalService } from "./local.service"; @Component({ }) export class ExampleComponent implements OnInit { constructor( private someService: SomeService, private localService: LocalService ) { } }
Neste exemplo, SomeService
importado diretamente de @app/core
vez de uma expressão volumosa (por exemplo, @app/core/some-package/some.service
). Isso é possível graças à reexportação de componentes públicos no arquivo principal index.ts
. É aconselhável criar um arquivo index.ts
para cada pacote no qual você precise reexportar todos os módulos públicos:
// index.ts export * from "./core.module"; export * from "./auth/auth.service"; export * from "./user/user.service"; export * from "./some-service/some.service";
Módulos principais, compartilhados e de recursos
Para um gerenciamento mais flexível dos componentes do aplicativo, muitas vezes é recomendado na literatura e em vários recursos da Internet para espalhar a visibilidade de seus componentes. Nesse caso, o gerenciamento dos componentes do aplicativo é simplificado. A seguinte separação é mais comumente usada: Módulos principais, compartilhados e de recursos.
Coremodule
O principal objetivo do CoreModule é descrever serviços que terão uma instância para todo o aplicativo (ou seja, implementar o padrão singleton). Isso geralmente inclui um serviço de autorização ou um serviço para obter informações do usuário. Exemplo do CoreModule:
import { NgModule, Optional, SkipSelf } from "@angular/core"; import { CommonModule } from "@angular/common"; import { HttpClientModule } from "@angular/common/http"; import { SomeSingletonService } from "./some-singleton/some-singleton.service"; @NgModule({ imports: [CommonModule, HttpClientModule], declarations: [], providers: [SomeSingletonService] }) export class CoreModule { constructor( @Optional() @SkipSelf() parentModule: CoreModule ) { if (parentModule) { throw new Error("CoreModule is already loaded. Import only in AppModule"); } } }
Módulo compartilhado
Este módulo descreve componentes simples. Esses componentes não importam ou injetam dependências de outros módulos em seus construtores. Eles devem receber todos os dados através dos atributos no modelo de componente. SharedModule
não depende do restante de nosso aplicativo.É também o local ideal para importar e reexportar componentes de material angular ou outras bibliotecas de interface do usuário.
import { NgModule } from "@angular/core"; import { CommonModule } from "@angular/common"; import { FormsModule } from "@angular/forms"; import { MdButtonModule } from "@angular/material"; import { SomeCustomComponent } from "./some-custom/some-custom.component"; @NgModule({ imports: [CommonModule, FormsModule, MdButtonModule], declarations: [SomeCustomComponent], exports: [ CommonModule, FormsModule, MdButtonModule, SomeCustomComponent ] }) export class SharedModule { }
Featuremodule
Aqui você pode repetir o guia de estilo Angular. Um FeatureModule separado é criado para cada função de aplicativo independente. O FeatureModule deve importar serviços apenas do CoreModule
. Se algum módulo precisar importar um serviço de outro módulo, é possível que esse serviço seja movido para o CoreModule
.
Em alguns casos, é necessário usar o serviço apenas por alguns módulos e não é necessário exportá-lo para o CoreModule
. Nesse caso, você pode criar um SharedModule
especial, que será usado apenas nesses módulos.
, — , - , , CoreModule
, SharedModule
.
, . , . , , .
Referências
- https://github.com/ngrx/store
- http://stepansuvorov.com/blog/2017/06/angular-rxjs-unsubscribe-or-not-unsubscribe/
- https://medium.com/@tomastrajan/6-best-practices-pro-tips-for-angular-cli-better-developer-experience-7b328bc9db81
- https://habr.com/post/336280/
- https://angular.io/docs