DevUI是一支兼具设计视角和工程视角的团队,服务于华为云 DevCloud平台和华为内部数个中后台系统,服务于设计师和前端工程师。
官方网站: devui.design
Ng组件库: ng-devui(欢迎Star)
引言
Angular 框架作为前端三大框架之一,有着其独到优点,可用于创建高效、复杂、精致的单页面应用。
本文介绍了Angular开发过程中推荐的十八个最佳实践及示例,用于开发过程中参考运用。
1. trackBy
What
当使用*ngFor指令在html中对数组进行陈列时,添加trackBy()函数,目的是为每个item指定一个独立的id
Why
一般情况下,当数组内有变更时,Angular将会对整个DOM树加以重新渲染。如果加上trackBy方法,Angular将会知道具体的变更元素,并针对性地对此特定元素进行DOM刷新,提升页面渲染性能
详细内容 ->NetanelBasal
Example
【Before】
{{ item }}
【After】
// in the template
{{ item }}
// in the component
trackByFn(index, item) {
return item.id; // unique id corresponding to the item
}
2. const vs let
What
声明常量时,使用const而不是let
Why
a. 使赋值意图更加明确
b. 若常量被重赋值,编译将直接报错,避免潜在风险
c. 增加代码可读性
Example
【Before】
let car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car};
if (iHaveMoreThanOneCar) {
myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
yourCar = `${youCar}s`;
}
【After】
// 变量car不会被重赋值,所以用const声明
const car = 'ludicrous car';
let myCar = `My ${car}`;
let yourCar = `Your ${car}`;
if (iHaveMoreThanOneCar) {
myCar = `${myCar}s`;
}
if (youHaveMoreThanOneCar) {
yourCar = `${youCar}s`;
}
3. pipeable 操作符
What
使用RxJs算子时,使用pipeable操作符号 ->拓展阅读
Why
a. 可被摇树优化: import的代码中,只有需要被执行的才会被引入
b. 容易定位到代码中未使用的算子
注: 需要RxJs版本在5.5及以上
Example
【Before】
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
iAmAnObservable
.map(value => value.item)
.take(1)
【After】
import { map, take } from 'rxjs/operators';
iAmAnObservable
.pipe(
map(value => value.item),
take(1)
)
4. 隔离API攻击
What
不是所有的API都是安全的 -> 很多情况下需要添加额外的代码逻辑去为API打补丁
相较于将这些逻辑放在component中,更好的做法是封装到一个独立的地方:比如封装到service中,再在其他地方引用
Why
a. 隔离攻击,使得攻击更靠近于原有的请求所在地
b. 减少用于处理攻击打补丁的代码
c. 将这些攻击封装在同一个地方,更容易发现
d. 当要解决bug的时候, 只需要到同一个文件内去搜寻,更容易定位
注: 也可以打个自有标签,比如API_FIX
,类似于TODO
标签,用于标记API修复
5. 模板的订阅
What
最好在html订阅变化,而不是在ts中
Why
a.async
管道能自动取消订阅:通过减少手动订阅管理能够简化代码
b. 减少在ts中忘记取消订阅,造成内存泄露的风险(这种风险也可以通过lint规则检测来避免)
c. 减少由于在订阅之外数据发生变更,进而引入bug的情况
Example
【Before】
// template
{{ textToDisplay }}
// component
iAmAnObservable
.pipe(
map(value => value.item),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item
【After】
// template
{{ textToDisplay$ | async }}
// component
this.textToDisplay$ = iAmAnObservable
.pipe(
map(value => value.item)
)
6. 订阅清理
What
如果订阅了observable,记得通过take
,takeUntil
等操作符妥善取消订阅
Why
a. 如果不取消订阅,可能导致哪怕组件被销毁或者用户去到了其他页面了,但观察observable流始终保持进而造成内存泄露
b. 更好的做法是:通过lint规则检测来避免
Example
【Before】
iAmAnObservable
.pipe(
map(value => value.item)
)
.subscribe(item => this.textToDisplay = item);
【After】
private _destroyed$ = new Subject();
public ngOnInit (): void {
iAmAnObservable
.pipe(
map(value => value.item)
// 被销毁前希望一直监听
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
}
public ngOnDestroy (): void {
this._destroyed$.next();
this._destroyed$.complete();
}
如果你只想要第一个值,那么就使用一个take(1)
iAmAnObservable
.pipe(
map(value => value.item),
take(1),
takeUntil(this._destroyed$)
)
.subscribe(item => this.textToDisplay = item);
注: 此处takeUntil与take在此处同时被使用,目的是防止在组件被销毁前一直没有收到值,导致内存泄露。
(如果没有takeUntil,那么在获取到第一个值之前,这个订阅将持续存在,
而组件在被销毁后,由于不可能接收到第一个值,就会造成内存泄露)
7. 使用合适的操作符
What
选取合适的合并操作符
switchMap: 当你想要用新接收的值替换前面的旧值
mergeMap: 当你希望同时所有接收到的值进行操作
concatMap: 当你希望对接收到的值轮番处理
exhaustMap: 当还在处理前一个接收到的值时,取消处理后来值
Why
a. 相较于链式使用多个操作符,使用一个合适的操作符实现相同的目的有助于有效减少代码量
b. 不恰当地使用操作符可能导致预料外的行为,因为不同的操作符所实现的效果是不同的
8. 懒加载
What
如果条件允许的话,尝试在angular应用中懒加载模块。
懒加载是指仅在需要的情况下才加载模块内容
Why
a. 有效减少需要加载的应用体积
b. 通过避免加载不需要的模块,能够有效提升启动性能
Example
【Before】
{ path: 'not-lazy-loaded', component: NotLazyLoadedComponent }
【After】
// app.routing.ts
{
path: 'lazy-load',
loadChildren: () => import(lazy-load.module).then(m => m.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
]
})
9. 避免嵌套订阅
What
一定情况下,可能需要从多个observable中获取数据去达到特定目的。
在这种情况下,避免在订阅块内嵌套订阅。
更好的方法是使用合适的链式操作符。
比如:withLatestFrom
,combineLatest
等
Why
代码异味/可读性/复杂度: 没有完全使用RxJs,表明开发者对RxJs的API浅层使用不熟悉
代码表现: 如果是冷observable,将会持续订阅第一个observable直到其complete,然后才是启动第二个observable的工作。
假如其中有网络请求,那么表现就会为瀑布流式的
Example
【Before】
firstObservable$.pipe(take(1))
.subscribe(firstValue => {
secondObservable$.pipe(
take(1)
)
.subscribe(secondValue => {
console.log(Combined values are: ${firstValue} & ${secondValue});
});
});
【After】
firstObservable$.pipe(
withLatestFrom(secondObservable$),
first()
)
.subscribe(([firstValue, secondValue]) => {
console.log(Combined values are: ${firstValue} &${secondValue});
});
10. 避免使用any,明确定义类型
What
声明变量或常量时,为其指定具体类型而不是简单使用any
Why
a. 当在TS中声明未指定类型的变量或者厂里,其类型将会由赋予的值推论得出,这容易引起意料之外的问题
一个经典的例子如下:
Example
【Before】
const x = 1;
const y = 'a';
const z = x + y;
console.log(Value of z is: ${z}
// 输出
Value of z is 1a
如果原来的预期输入是y也是个数字类型,那么就会导致意料之外的问题。
【After】
这些问题可以通过为声明变量指定一个恰当的类型来避免:
const x: number = 1;
const y: number = 'a';
const z: number = x + y;
// 这个输入将会导致编译报错抛出
Type '"a"' is not assignable to type 'number'.
const y:number
通过上述方法,可以避免由于类型缺失导致的bug
b. 指定类型的另一个好处是可以使得重构更简单,更安全
Example
【Before】
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
假如希望重命名myFlashObjec
t中的loc
属性名为location
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
当未对myFlashObject
指定类型时,看起来方法loc
属性在myFlashObject
中不存在而不是属性取值错误导致的上述结果
【After】
当对myFlashObject
增加了类型定义,我们将获取到一个更加清晰的编译报错问题如下
type FlashObject = {
name: string,
age: string,
location: string
}
public ngOnInit (): void {
let myFlashObject: FlashObject = {
name: 'My cool name',
age: 'My cool age',
// Compilation error
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})
// Compilation error
Property 'loc' does not exist on type 'FlashObjectType'.
console.log(Location: ${myObject.loc});
}
如果你正在开启一个全新的工程,推荐在tsconfig.json
文件中设定strict:true
方式,将严格模式打开,开启所有的严格类型检查选项
11. 使用lint规则
What
lint规则由多个预置的选项比如no-any
,no-magic-numbers
,no-consle
等,你可以在你的tslint.json
文件中去开启特定的校验规则
Why
使用lint规则意味着,在某个地方有不应当产生发生的行为出现时,你将会得到较为清晰的报错
这将会提高你应用代码的一致性以及可读性
一些lint规则甚至有特定的fix解法用于解决此lint任务
如果你希望去定义自己的lint规则,你也可以去撰写
使用TSQuery去编写自己的lint规则的教程链接
一个经典的例子如下:
Example
【Before】
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');
}
// 输出
并不会报错,而是在控制台中打印如下信息:
I am a naughty console message
I am a naughty console warning message
I am a naughty console error message
【After】
// tslint.json
{
"rules": {
.......
"no-console": [
true,
"log", // no console.log allowed
"warn" // no console.warn allowed
]
}
}
// ..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');
}
// Output
lint在console.log及log.warn语句处报错,console.error并不会报错,因为lint规则中未配置
Calls to 'console.log' are not allowed.
Calls to 'console.warn' are not allowed.
12. 精简,可重用的组件
What
将组件中可重用的代码片段抽取出来成为一个新的组件
让组件尽可能地“dumb”,从而能够在更多的场景中复用
编写“dumb”组件的意思是,其中没有隐含特别的逻辑,操作只是简单地依赖于提供给它的输入输出
作为一个通用的规则,在组件树中的最子节点的组件将会是其中最“dumb”的一个
Why
可重用的组件将会降低代码重复率,进而使其更易于维护及变更
dumb组件更加简单,因此存在bug的可能性也更低。dumb组件使得你去仔细思考如何抽取通用组件API,并且帮助你识别出混杂的问题
13. 组件只处理展示逻辑
What
避免将除了展示逻辑外的业务逻辑封装进组件,确保组件只用于处理展示逻辑
Why
a. 组件是为控制视图及展示目的而设计的,任何业务逻辑都应封装到自己合适的方法或者service内部,业务逻辑应与组件逻辑分离
b. 业务逻辑如果被抽取到一个service内部,通常更适用于使用单元测试,而且可以被其他需要相同的业务逻辑的组件重用
14. 避免长方法
What
长方法通常说明他们已经包含了太多的任务,尝试使用单一职责原则
一个方法应该作为整体去完成一件事情,如果其中有多个操作,那么我们可以抽取这些方法,形成独立的函数,使得他们独自负责各自职责,再去调用他们
Why
a. 长方法难以阅读、理解以及维护。他们容易产生bug,因为改变其中一部分很可能影响方法内的其他逻辑。这也使得代码重构更加难以进行
b. 方法可以用圈复杂度衡量,有一些TSLint方法用于检测圈复杂度,你可以在你的项目中去使用,避免bug以及检测代码可用性
15. Dry
What
Dry = Do not Repeat Yourself
保证在代码仓库中没有重复拷贝的代码,抽取重复代码,并且在需要使用的地方引用即可
Why
a. 在多个地方用重复代码意味着,如果我们想要改变代码逻辑,我们需要在多个地方修改,降低了代码的可维护性
使得对代码逻辑进行变更变得很困难而且测试过程很漫长
b. 抽取重复代码到一个地方,意味着只需要修改一处代码以及单次测试
c. 同时更少的代码意味着更快的速度
16. 增加缓存
What
发起API请求得到的响应通常并没有经常变化,在这类场景里,可以通过增加缓存机制并且储存获取的值
当同样的API请求再发起的时候,确认cache中是否已经有值,若有,则可以直接使用,否则发起请求并缓存。
如果这些值会变化但变化不频繁,那么可以引入一个缓存时间,用于决策是否使用缓存或者去重新调用
Why
具有缓存机制意味着可以避免不必要的API调用,通过避免重复调用有助于提高应用响应速度,不再需要等待网络返回,而且我们不需要重复地下载同样的信息
17. 避免模板中的逻辑
What
如果在HTML中需要增加任何逻辑,哪怕只是简单的&&,最好都将其抽取到组件内
Why
模板中的逻辑难以单元测试,当切换模板代码的时候容易导致代码问题
【Before】
// template
Status: Developer
// component
public ngOnInit (): void {
this.role = 'developer';
}
【After】
Status: Developer
// component
public ngOnInit (): void {
this.role = 'developer';
this.showDeveloperStatus = true;
}
18. 安全地声明string类型
What
如果有一些string变量只有一些特定的值,相比于声明为string类型,更好的方式是将其声明为一个可能的值集合类型
Why
通过为变量提供恰当的声明有助于避免bug:当编写代码超出预期时可以在编译阶段被发现,而不是等运行了才发现
【Before】
private myStringValue: string;
if (itShouldHaveFirstValue) {
myStringValue = 'First';
} else {
myStringValue = 'Second'
}
【After】
private myStringValue: 'First' | 'Second';
if (itShouldHaveFirstValue) {
myStringValue = 'First';
} else {
myStringValue = 'Other'
}
// This will give the below error
Type '"Other"' is not assignable to type '"First" | "Second"'
(property) AppComponent.myValue: "First" | "Second"
加入我们
我们是DevUI团队,欢迎来这里和我们一起打造优雅高效的人机设计/研发体系。招聘邮箱:[email protected]。
本文版权归原作者所有,仅用于学习与交流;
如需转载译文,烦请按下方注明出处信息,谢谢!
原文链接:Best practices for a clean and performant Angular application
作者: Vamsi Vempati
译者: DevUI 弘一