O autor do artigo, a primeira parte da tradução que publicamos, diz que ele trabalha em um aplicativo Angular em larga escala no
Trade Me há cerca de dois anos. Nos últimos anos, a equipe de desenvolvimento de aplicativos aprimorou constantemente o projeto, tanto em termos de qualidade quanto de desempenho do código.
Esta série de materiais se concentrará nas abordagens de desenvolvimento usadas pela equipe do Trade Me, que são expressas na forma de mais de duas dúzias de recomendações sobre tecnologias como Angular, TypeScript, RxJS e @ ngrx / store. Além disso, será dada alguma atenção às técnicas de programação universal que visam tornar o código do aplicativo mais limpo e preciso.
1. Sobre trackBy
Usando
ngFor
para percorrer matrizes em modelos, use essa construção com a função
trackBy
, que retorna um identificador exclusivo para cada elemento.
▍ Explicações
Quando a matriz muda, Angular renderiza novamente a árvore DOM inteira. Mas se você usar
trackBy
, o sistema saberá qual elemento foi alterado e fará alterações no DOM que se aplicam apenas a esse elemento específico. Detalhes sobre isso podem ser encontrados
aqui .
▍Para
<li *ngFor="let item of items;">{{ item }}</li>
▍Depois
// <li *ngFor="let item of items; trackBy: trackByFn">{{ item }}</li> // trackByFn(index, item) { return item.id; // id, }
2. Palavras-chave const e let
Se você declarar uma variável cujo valor não planeja alterar, use a palavra-chave
const
.
▍ Explicações
O uso apropriado das palavras-chave
let
e
const
esclarece a intenção em relação ao uso de entidades declaradas usando-as. Além disso, essa abordagem facilita o reconhecimento de problemas causados pela substituição acidental de valores constantes. Nessa situação, um erro de compilação é lançado. Além disso, melhora a legibilidade do código.
▍Para
let car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) { myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) { yourCar = `${youCar}s`; }
▍Depois
// car , car const car = 'ludicrous car'; let myCar = `My ${car}`; let yourCar = `Your ${car}; if (iHaveMoreThanOneCar) { myCar = `${myCar}s`; } if (youHaveMoreThanOneCar) { yourCar = `${youCar}s`; }
3. Operadores de transporte
Ao trabalhar com o RxJS, use operadores em pipeline.
▍ Explicações
Os operadores transmitidos suportam o algoritmo de agitação de árvore, ou seja, quando são importados, apenas o código planejado para execução será incluído no projeto. Isso também facilita a identificação de instruções não utilizadas nos arquivos.
Observe que esta recomendação é relevante para a versão Angular 5.5 e superior.
▍Para
import 'rxjs/add/operator/map'; import 'rxjs/add/operator/take'; iAmAnObservable .map(value => value.item) .take(1);
▍Depois
import { map, take } from 'rxjs/operators'; iAmAnObservable .pipe( map(value => value.item), take(1) );
4. Isolamento de correções de API
Nem todas as APIs são completamente estáveis e sem erros. Portanto, às vezes é necessário introduzir alguma lógica no código destinado a corrigir problemas de API. Em vez de colocar essa lógica em componentes onde as APIs corrigidas são usadas, seria melhor isolá-la em algum lugar, por exemplo, em um serviço e referir-se ao serviço correspondente, em vez da API problemática do componente.
▍ Explicações
A abordagem proposta permite manter as correções "mais próximas" da API, ou seja, o mais próximo possível do código a partir do qual as solicitações de rede são feitas. Como resultado, a quantidade de código do aplicativo que interage com APIs problemáticas é reduzida. Além disso, verifica-se que todas as correções estão em um único local, como resultado, será mais fácil trabalhar com elas. Se você precisar corrigir erros na API, será muito mais fácil fazer isso em um único arquivo do que espalhar essas correções pelo aplicativo. Isso facilita não apenas a criação de correções, mas também a busca pelo código apropriado no projeto e seu suporte.
Além disso, você pode criar suas próprias tags, como
API_FIX
(que se assemelha à tag
TODO
), e sinalizar correções com elas. Isso facilita a localização dessas correções.
5. Assinatura no modelo
Evite assinar observáveis de componentes. Em vez disso, assine-os em modelos.
▍ Explicações
Os operadores assíncronos em pipeline cancelam a assinatura automaticamente, o que simplifica o código, eliminando a necessidade de gerenciamento manual de assinaturas. Além disso, reduz o risco de o desenvolvedor esquecer de cancelar a assinatura do componente, o que pode levar a vazamentos de memória. É possível reduzir a probabilidade de vazamentos de memória usando regras linter destinadas a identificar objetos observáveis dos quais eles não cancelaram a inscrição.
Além disso, a aplicação dessa abordagem leva ao fato de que os componentes deixam de ser componentes com estado, o que pode levar a erros quando os dados são alterados fora da assinatura.
▍Para
// <p>{{ textToDisplay }}</p> // iAmAnObservable .pipe( map(value => value.item), takeUntil(this._destroyed$) ) .subscribe(item => this.textToDisplay = item);
▍Depois
// <p>{{ textToDisplay$ | async }}</p> // this.textToDisplay$ = iAmAnObservable .pipe( map(value => value.item) );
6. Removendo assinaturas
Ao assinar objetos monitorados, sempre verifique se as assinaturas foram excluídas corretamente usando operadores como
take
,
takeUntil
e assim por diante.
▍ Explicações
Se você não cancelar a inscrição do objeto observado, isso causará vazamentos de memória, pois o fluxo do objeto observado permanecerá aberto, o que é possível mesmo após a destruição do componente ou após o usuário ir para outra página do aplicativo.
Melhor ainda seria criar uma regra de linter para detectar objetos observados com uma assinatura válida.
▍Para
iAmAnObservable .pipe( map(value => value.item) ) .subscribe(item => this.textToDisplay = item);
▍Depois
Use o operador
takeUntil
se desejar observar as alterações de um objeto até que outro objeto observado gere um determinado valor:
private destroyed$ = new Subject(); public ngOnInit (): void { iAmAnObservable .pipe( map(value => value.item)
Usar algo assim é um padrão usado para controlar a remoção de assinaturas de muitos objetos observados em um componente.
Use
take
se precisar apenas do primeiro valor retornado pelo objeto observado:
iAmAnObservable .pipe( map(value => value.item), take(1), takeUntil(this._destroyed$) ) .subscribe(item => this.textToDisplay = item);
Observe que aqui usamos
takeUntil
com
take
. Isso é feito para evitar vazamentos de memória causados pelo fato de a assinatura não levar à obtenção do valor até a destruição do componente. Se a função
takeUntil
não fosse usada
takeUntil
, a assinatura existiria até o primeiro valor ser recebido, mas, como o componente já teria sido destruído, esse valor nunca teria sido recebido, o que levaria a um vazamento de memória.
7. Utilizando operadores adequados
Usando operadores de suavização com objetos observáveis, aplique os que correspondem aos recursos do problema que está sendo resolvido.
- Use
switchMap
quando precisar ignorar a ação agendada anterior quando uma nova ação chegar. - Use o
mergeMap
caso precise processar todas as ações despachadas em paralelo. - Use
concatMap
quando as ações precisarem ser processadas uma após a outra, na ordem em que são recebidas. - Use o
exhaustMap
em situações em que, no processo de processamento de ações recebidas anteriormente, é necessário ignorar novas.
Detalhes sobre isso podem ser encontrados
aqui .
▍ Explicações
Usar, se possível, um operador, em vez de obter o mesmo efeito combinando vários operadores em uma cadeia, reduz a quantidade de código do aplicativo que precisa ser enviado ao usuário. O uso de um operador selecionado incorretamente pode levar ao comportamento incorreto do programa, pois diferentes operadores processam os objetos observados de maneira diferente.
8. Carregamento preguiçoso
Em seguida, sempre que possível, tente organizar o carregamento lento dos módulos da aplicação Angular. Essa técnica se resume ao fato de que algo carrega apenas se for usado. Por exemplo, um componente é carregado apenas quando precisa ser exibido.
▍ Explicações
O carregamento lento reduz o tamanho dos materiais do aplicativo que o usuário precisa baixar. Isso pode melhorar a velocidade de download do aplicativo devido ao fato de os módulos não utilizados não serem transferidos do servidor para os clientes.
▍Para
// app.routing.ts { path: 'not-lazy-loaded', component: NotLazyLoadedComponent }
▍Depois
// app.routing.ts { path: 'lazy-load', loadChildren: 'lazy-load.module#LazyLoadModule' } // lazy-load.module.ts import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; import { LazyLoadComponent } from './lazy-load.component'; @NgModule({ imports: [ CommonModule, RouterModule.forChild([ { path: '', component: LazyLoadComponent } ]) ], declarations: [ LazyLoadComponent ] }) export class LazyModule {}
9. Sobre assinaturas em outras assinaturas
Às vezes, para executar alguma ação, você pode precisar de dados de vários objetos observáveis. Em tal situação, evite criar assinaturas para esses objetos dentro dos blocos de
subscribe
de outros objetos observáveis. Em vez disso, use operadores adequados para encadear comandos. Entre esses operadores, pode-se notar
withLatestFrom
e
combineLatest
. Considere os exemplos e depois comente-os.
▍Para
firstObservable$.pipe( take(1) ) .subscribe(firstValue => { secondObservable$.pipe( take(1) ) .subscribe(secondValue => { console.log(`Combined values are: ${firstValue} & ${secondValue}`); }); });
▍Depois
firstObservable$.pipe( withLatestFrom(secondObservable$), first() ) .subscribe(([firstValue, secondValue]) => { console.log(`Combined values are: ${firstValue} & ${secondValue}`); });
▍ Explicações
Se falamos de legibilidade, da complexidade do código ou dos sinais de código incorreto, quando o programa não usa os recursos do RxJS ao máximo, isso significa que o desenvolvedor não está familiarizado com a API do RxJS. Se tocarmos no tópico desempenho, acontece que, se o objeto observado precisar de algum tempo para inicializar, ele
firstObservable
, o sistema aguardará a conclusão da operação e somente depois disso o trabalho com o segundo objeto observado será iniciado. Se esses objetos forem solicitações de rede, será semelhante à execução síncrona de solicitações.
10. Sobre digitação
Sempre tente declarar variáveis ou constantes com um tipo diferente de
any
outro.
▍ Explicações
Se uma variável ou constante for declarada no TypeScript sem especificar um tipo, o tipo será inferido com base no valor atribuído a ela. Isso pode levar a problemas. Considere um exemplo clássico de comportamento do sistema em uma situação semelhante:
const x = 1; const y = 'a'; const z = x + y; console.log(`Value of z is: ${z}` // Value of z is 1a
Supõe-se que
y
seja um número aqui, mas nosso programa não o conhece, portanto exibe algo que parece errado, mas não produz nenhuma mensagem de erro. Problemas semelhantes podem ser evitados atribuindo tipos apropriados a variáveis e constantes.
Reescrevemos o exemplo acima:
const x: number = 1; const y: number = 'a'; const z: number = x + y; // : Type '"a"' is not assignable to type 'number'. const y:number
Isso ajuda a evitar erros de tipo de dados.
Outra vantagem de uma abordagem sistemática da digitação é que ela simplifica a refatoração e reduz a probabilidade de erros durante esse processo.
Considere um exemplo:
public ngOnInit (): void { let myFlashObject = { name: 'My cool name', age: 'My cool age', loc: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: any): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`); console.log(`Location: ${myObject.loc}`); } // Name: My cool name Age: My cool age Location: My cool location
Suponha que
myFlashObject
alterar, no
myFlashObject
, o nome da propriedade
loc
para
location
e cometi um erro
myFlashObject
editar o código:
public ngOnInit (): void { let myFlashObject = { name: 'My cool name', age: 'My cool age', location: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: any): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`); console.log(`Location: ${myObject.loc}`); } // Name: My cool name Age: My cool age Location: undefined
Se a digitação não for usada ao criar o objeto
myFlashObject
, no nosso caso, o sistema assume que o valor da propriedade
loc
de
myFlashObject
é
undefined
. Ela não acha que
loc
possa ser um nome de propriedade inválido.
Se a digitação for usada ao descrever o objeto
myFlashObject
, em uma situação semelhante, veremos, ao compilar o código, uma maravilhosa mensagem de erro:
type FlashObject = { name: string, age: string, location: string } public ngOnInit (): void { let myFlashObject: FlashObject = { name: 'My cool name', age: 'My cool age', // Type '{ name: string; age: string; loc: string; }' is not assignable to type 'FlashObjectType'. Object literal may only specify known properties, and 'loc' does not exist in type 'FlashObjectType'. loc: 'My cool location' } this.processObject(myFlashObject); } public processObject(myObject: FlashObject): void { console.log(`Name: ${myObject.name}`); console.log(`Age: ${myObject.age}`) // Property 'loc' does not exist on type 'FlashObjectType'. console.log(`Location: ${myObject.loc}`); }
Se você estiver iniciando o trabalho em um novo projeto, será útil definir, no arquivo
tsconfig.json
, a opção
strict:true
para ativar a verificação estrita de tipo.
11. Sobre o uso de linter
Tslint tem várias regras padrão, como
nenhum ,
nenhum número mágico ,
nenhum console . O linter pode ser personalizado editando o arquivo
tslint.json
para organizar a verificação de código de acordo com certas regras.
▍ Explicações
Usar um linter para verificar o código significa que, se algo aparecer no código que é proibido pelas regras, você receberá uma mensagem de erro. Isso contribui para a uniformidade do código do projeto, melhora sua legibilidade. Veja outras regras tslint aqui.
Note-se que algumas regras incluem meios de corrigir o que é considerado inadmissível de acordo com elas. Se necessário, você pode criar suas próprias regras. Se você estiver interessado, dê uma olhada
neste material, que discute a criação de regras personalizadas para o linter usando o
TSQuery .
▍Para
public ngOnInit (): void { console.log('I am a naughty console log message'); console.warn('I am a naughty console warning message'); console.error('I am a naughty console error message'); }
▍Depois
// tslint.json { "rules": { ....... "no-console": [ true, "log", // console.log "warn" // console.warn ] } } // ..component.ts public ngOnInit (): void { console.log('I am a naughty console log message'); console.warn('I am a naughty console warning message'); console.error('I am a naughty console error message'); } // . console.log and console.warn console.error, Calls to 'console.log' are not allowed. Calls to 'console.warn' are not allowed.
Sumário
Hoje revisamos 11 recomendações que, esperamos, serão úteis para os desenvolvedores Angular. Da próxima vez, aguarde mais 11 dicas.
Caros leitores! Você usa a estrutura Angular para desenvolver projetos da web?
