有开发大型应用的同学对Angular路由懒加载一定不陌生。Angular在框架层面支持路由懒加载,用户首次访问应用,没有访问到的应用路由不会立刻加载,提升应用的初始加载速度。
懒加载tab组件,也是同样的概念,用户没有访问到的tab内容不加载。在复杂应用中可以切分不同功能为不同的tab,并且在切换tab时候不造成销毁,从而保留用户对不同tab的操作,提升使用体验。在各个手机app中都能看到,比如知乎,b站,淘宝(tab抽象为底部导航)。
目前无论是zorro
还是material
的tab组件都是一次性渲染所有内容(material支持ng-template的懒加载),在大多数情况下并没有问题,但是遇到特别多的tab内容(比如每个tab里都有很多异步请求,很多渲染逻辑),一次性加载可能造成性能问题。
实现
Tab组件结构
可以想象大体的tab组件使用写法应该是:
<app-tab>
<app-tab-pane>...</app-tab-pane>
<app-tab-pane>...</app-tab-pane>
</app-tab>
从结构上来说没有什么问题,app-tab
包裹整个tab组件,app-tab-pane
为各个tab的内容组件。但是这样写,angular会整体接管所有视图的渲染,app-tab-pane
里的内容都会直接渲染执行,无法做到懒加载。要控制视图的渲染,需要将app-tab-pane
通过结构指令来实现。
<app-tab>
<div *appTabPane></div>
<div *appTabPane></div>
</app-tab>
具体懒加载需求
- 在未切换到某一个
pane
时,该pane底下的内容不渲染 - 渲染过的
pane
不销毁
<div *appTabPane> <!-- <- 视图容器的位置 -->
<div>I am a template which will not be rendered until createEmbeddedView</div> <!-- <- 模板 -->
<!-- call ViewContainerRef.insert. the view will insert here. -->
</div>
结构指令内部可以通过两个抽象来控制视图的渲染ViewContainerRef
, TemplateRef
。ViewContainerRef
视图的容器,TemplateRef
模板(即未被渲染的视图)。渲染之后会得到ViewRef
视图引用。pane
指令在初次渲染时,需要将TemplateRef
渲染得到ViewRef
,并放到视图容器中,在后续的渲染操作中都复用初次得到的视图引用即可。关于ViewContainerRef
, TemplateRef
更多的理解可参考https://segmentfault.com/a/1190000008672478
constructor(
private template: TemplateRef<TabPaneDirective>,
private container: ViewContainerRef,
) { }
通过注入的方式可以获得ViewContainerRef
和TemplateRef
。
show() {
// 如果存在视图引用,则复用。不存在则渲染模板,并保留视图引用
if (this.viewRef) {
this.attachView();
} else {
this.createView();
}
}
hide() {
this.container.detach();
this.active = false
}
createView() {
this.viewRef = this.container.createEmbeddedView(this.template);
}
attachView() {
this.container.insert(this.viewRef);
}
this.template
为TemplateRef
,是appTabPane
内部的模板视图,通过this.container.createEmbeddedView(this.template)
可将模板渲染得到视图并挂载的视图容器中。而视图容器的insert
和detach
方法用于处理视图容器插入或者删除已有的视图。通过保留viewRef
引用,我们达到了懒加载并且不销毁的功能。
其他pane组件属性
@Input('appTabPane') name: string;
active: boolean;
name
为pane的名称,active
为当前pane
的显示/隐藏状态。
Tab父组件
<div class="head">
<div
class="head-item"
*ngFor="let pane of panes"
[class.active]="pane.active"
(click)="selectPane(pane)"
>
{{pane.name}}
</div>
</div>
<div class="body">
<ng-content></ng-content>
</div>
通过ng-content
直接插入pane
的内容,ngFor
显示pane
名称。最简化的组件设计并且满足组件需求。
测试代码
代码地址: github
<app-tab>
<app-tab-example-timer *appTabPane="'tab1'"></app-tab-example-timer>
<app-tab-example-timer *appTabPane="'tab2'">
<input type="text">
</app-tab-example-timer>
</app-tab>
运行得到tab1
和tab2
两个pane, 只有当点击到相应的tab之后,timer才会启动,并且你在input里输入的东西也不会随着切换pane而被销毁。