开始之前稍微废话一下,如何解释框架?下面是汉语字典中的解释
框:镶在器物的外围有支撑作用或保护作用的东西,引申为约束,限制
架:用做支承的东西
可见在使用框架的时候,要遵守它的约束和限制,不能随心所欲。
目的和思路
回到正题,在前端项目中查询页面上的元素是常用的思路,尤其是在jQuery年代。时至今日,仍然会在很多angular项目中见到有人使用jQuery或zepto,不是说jQuery不好,而是不合适,这样做无异于在试图突破框架的约束和限制。
接下来我们使用查询元素,设置值的思路实现一个demo,这种方式在angular里应该是退而求其次的选择,demo只是为了说明思路的可行性,最重要的是体会如何正确设置static查询策略。
在此之前建议阅读一下Angular8的migration,另外最好对angular组件的生命周期有所了解。
场景假设
页面上的某些元素需要动态的添加和删除,显示的内容在页面上有一个数据源,其它元素需要根据这个数据源显示正确的数据,不管是动态组件还是普通组件。
测试代码
完整示例代码可以在这里找到。
app.component.ts 在这个组件里设置了一个checkbox,根据它的值决定是否在hello组件中显示问候语
@Component({
selector: 'my-app',
template: `
<label for="box">
AppCompnent:
<input type="checkbox" [(ngModel)]="checked" id="box">
</label>
<hello [showGreeting]="checked"></hello>
`,
styles: [`p {
font-family: Lato;
}`]
})
export class AppComponent {
checked = true;
}
hello.component.ts 主要需要实现的组件。除了destory外的所有组件生命周期的钩子函数在这里都实现了,在每个钩子函数运行时,我们都会打印当前组件内的所有查询结果,true表示可用,false不可用。
@Component({
selector: 'hello',
template: `
<h2 #title></h2>
<h4 *ngIf="showGreeting"><i #greeting></i></h4>
<app-input>
<div class="dynamic">
DynamicComponent:
<ng-template #container></ng-template>
<div>
`,
styles: [`
h4 { font-family: Lato; }
:host {display: block;border: 1px dashed #333; padding: 1em; margin-top: 1em;}
`]
})
export class HelloComponent {
@Input() showGreeting = false;
@ViewChild('title', { static: false }) title: ElementRef;
@ViewChild('greeting', { static: false }) greeting: ElementRef;
@ViewChild(InputComponent, { static: false }) appInput: InputComponent;
@ViewChild('container', { read: ViewContainerRef, static: false }) container: ViewContainerRef;
private logger: any[] = []; // 日志
private dynamicRef: ComponentRef<DynamicComponent>;
constructor(
private cfr: ComponentFactoryResolver,
) { }
// 日志函数
log(phash: string): void {
const target = {
phash,
title: !!this.title,
greeting: !!this.greeting,
container: !!this.container,
input: !!this.appInput
}
this.logger.push(target);
console.table(this.logger);
}
ngOnChanges(change: SimpleChanges) {
this.log('OnChange');
}
ngOnInit() {
this.log('OnInit');
}
ngDoCheck() {
this.log('DoCheck');
}
ngAfterContentInit() {
this.log('AfterContentInit');
}
ngAfterContentChecked() {
this.log('AfterContentChecked');
}
ngAfterViewInit() {
this.log('AfterViewInit');
}
ngAfterViewChecked() {
this.log('AfterViewChecked');
}
// 动态组件的渲染函数
render() {
const componentFactory: ComponentFactory<DynamicComponent> = this.cfr.resolveComponentFactory(DynamicComponent);
this.container.clear();
const componentRef = this.container.createComponent(componentFactory);
componentRef.instance.value = this.appInput.value;
this.dynamicRef = componentRef;
}
}
input.component.ts
@Component({
selector: 'app-input',
template: `
<label for="input">
InputComponent:
<input id="input" type="text" name="name" [(ngModel)]="value">
</label>
`,
styles: [`h1 { font-family: Lato; }`]
})
export class InputComponent {
value = "typescript";
}
dynamic.component.ts
@Component({
selector: 'app-dynamic',
template: `
<h3>Hi {{value}}</h3>
`,
styles: [`h3 { font-family: Lato; color: green; display: inline-block; }`]
})
export class DynamicComponent {
value = "";
}
接下来通过调整HelloComponent的代码,配合log先了解一下在不同情形,不同查询策略下所获取到的结果。
测试结果
以下标题中所指的true或false完整表述应该是 static:true/false。
全部false
官方建议大多数情况下应该设置 static: false,OK,看日志:
![77299feac10c56aa151ccae1840a6a68.png](https://i-blog.csdnimg.cn/blog_migrate/c78401cb933a5eb285e6ca248ec79b10.png)
AfterViewInit之后才能获取到结果。
title 的策略改为 true
这个查询不依赖于代码的运行时,属于‘静态’类型
![963e1c66040518cf49e458b423504324.png](https://i-blog.csdnimg.cn/blog_migrate/989b4e764c50e2bc43fffc255790d7f9.png)
可以看到都不用等到OnInit阶段,在OnChange时就已经可以获取到结果了。那么我们可以在这些阶段给它赋值,比如
ngOnInit() {
this.title.nativeElement.innerHTML = 'Hello';
this.log('OnInit');
}
greeting 的策略改为 true
这个查询被嵌套在ngIf中,依赖于代码的运行时,先看结果
![02db77dfd3a4e5f0a0c97847f60b2af3.png](https://i-blog.csdnimg.cn/blog_migrate/70eb670d031937c8fbcf2d09678d25d6.png)
所有阶段都无法获取到,可以点击checkbox试下
![a697a1a66288c3424dfc03146e1e6414.png](https://i-blog.csdnimg.cn/blog_migrate/4cc82a71b2878494955af9ca845bd006.jpeg)
在8版本之前你可能感到困惑,但现在一清二楚:
![d399aa8ab58376b1230ddb3e106f5f99.png](https://i-blog.csdnimg.cn/blog_migrate/9d3ca9679859f6cf6c27a3d1eba87068.png)
如果要通过查询设置模板中标签的值,首先 static 的值应该设成false,然后在对应的钩子函数中设置它,比如 AfterViewInit 或 AfterViewChecked中添加。
this.greeting.nativeElement.innerHTML = 'Hello World';
appInput 的策略改为 true
这个查询主要是为了说明那些需要在父组件中从子组件获取值的情形
![41ca23ac518c329b44d2d9f7681b5f49.png](https://i-blog.csdnimg.cn/blog_migrate/690d96056e95eabc4b17cc2a4d84ec4e.png)
此时,可以把appInput的value赋值给title元素
ngOnInit() {
this.title.nativeElement.innerHTML = this.appInput.value;
this.log('OnInit');
}
![99918730d8f3001daf211b4e4b5324d0.png](https://i-blog.csdnimg.cn/blog_migrate/3ddfd784bce2541896185e2545aa03d6.png)
container 的策略改为 true
这个组件是为了说明那些需要动态创建组件的情形
![cac9d17e8c4a2e6059ae6c78c9b3ded6.png](https://i-blog.csdnimg.cn/blog_migrate/ef238b174fe6982a7fe00d918f5e368c.png)
在OnInit的时候创建动态组件
ngOnInit() {
this.render();
this.log('OnInit');
}
![2b43dff2f18a2cd67da576a5440c3014.png](https://i-blog.csdnimg.cn/blog_migrate/ddd854d308a42feafa1f93233f2986ca.png)
log里显示,所有阶段都可以正确获取到container,上面是把render放在了OnInit阶段,如果放在AfterViewInit阶段呢?
ngAfterViewInit() {
this.log('AfterViewInit');
this.render();
}
不好意思,报错了:
![a795d26b26d4522f6b15879dad6f4749.png](https://i-blog.csdnimg.cn/blog_migrate/f7be8285bddeb928cdc6589c6dfc513f.png)
假如把 container 的查询策略重新改回 false,还是在 AfterViewInit 渲染,仍然会报上面的这个错误,官方解释:
![69f075fc775d4f799f841b0c228f8844.png](https://i-blog.csdnimg.cn/blog_migrate/21583557e73e9d5b45fc53d42b7ac7d8.png)
站在编译器的角度这话应该是这样说:同学,视图刚给你初始化完成怎么又变了?你不是产品经理吧?
所以这种情况下,只能使用 static:true 策略,然后在view初始化完成前的钩子函数中创建动态组件,一般情况下首选OnInit。
设置合适的策略
最后使用合适的策略来完成需求:
- 根据checkbox的值动态的显示问候语,也就是greeting。
- input的值发生变化后其它组件的值也相应的变化。
greeting只能设置false,同时要让它动态的根据InputCopmonent的值发生改变,如果在AfterViewInit中设置,则只能同步到初始状态的值,因为AfterViewInit只会发生一次,所以把它推后到AfterViewChecked阶段赋值。另外由于它依赖于程序运行时showGreeting的值,因此还需要进行一下判断。
title采用官方的建议就用false,也把它放在AfterViewChecked阶段,理由同greeting。
container只能设置true。
appInput也设置为true,因为需要在render中访问它的值,而render是在OnInit阶段就调用了。
动态组件的值要随appInut发生变化,需要对它重新赋值。如果赋值发生在AfterViewInit之后,势必会报ExpressionChangedAfterItHasBeenCheckedError,如果赋值写在OnChanges阶段,那么仅能在checkbox的值变化时才更新,这里最合适的是DoCheck阶段。
根据上面的分析添加如下代码:
ngDoCheck() {
this.dynamicRef.instance.value = this.appInput.value;
this.log('DoCheck');
}
ngAfterViewChecked() {
if (this.greeting) {
this.greeting.nativeElement.innerHTML = 'Hello ' + this.appInput.value;
}
this.title.nativeElement.innerHTML = this.appInput.value;
this.log('AfterViewChecked');
}
![5fb29202f54f3041f7f411f09a060c20.gif](https://i-blog.csdnimg.cn/blog_migrate/bf88d2f77514f5e5ef4d4b16f35fe840.gif)
![672003e9bc25bb789c2d80d12e1b3d5a.png](https://i-blog.csdnimg.cn/blog_migrate/e831e72d0b81fb7012ee202fa8ec4672.jpeg)