![8a9f29aa5f043c6a440cef3b1f947218.png](https://i-blog.csdnimg.cn/blog_migrate/8f35b9b151fa24edcf6ccafc2f01fafe.jpeg)
接上篇,关于请求并处理分页数据的纯逻辑部分,我们已经通过服务实现。现在需要关注的是页面交互部分:怎样实现用户滚动到列表底部时,自动加载下一页数据呢?我们首先想到的就是指令。
@Directive({
selector: '[appScrollLoading]',
})
export class ScrollLoadingDirective implements OnInit, OnDestroy {
loadingDom: HTMLElement;
loadingDomRef: ComponentRef<any>;
loadingState$: Subject<state>;
end$ = new Subject()
constructor(
private el: ElementRef,
private cfr: ComponentFactoryResolver,
private appRef: ApplicationRef,
private injector: Injector,
@Optional() @SkipSelf() private listData: ListDataService<unknown>
) {
this.loadingState$ = this.listData?.loadingState$
}
}
在这里我需要明确一下,我们编写的指令,跟上一节我们写的服务是高度关联的(见上述代码)。也就是说,我们需要将PagingDataService服务注入到指令中来。毕竟要请求下一页数据,不可避免地要调用服务中nextPage方法,并且指令还得知道http请求何时开始,何时结束,这样才能在页面底部展示相应的文字提示,而这点也必须通过访问服务的loadingState$对象来实现。
这也是我为什么会强调,我们要在渲染分页数据的组件里注册此服务。毕竟我们的指令也是用在该组件上的。这样一来,服务对于组件以及组件内部使用的指令来说就是单例的,而且不会影响到其他同样需要加载分页数据的组件,因为在别的组件那里,PagingDataService又会单独生成一个服务实例。
这也是angular依赖注入的强大之处,由于注入器树的设计,再配合rxjs的响应式,我们想实现一个局部的状态管理方案真的是轻而易举。简直是香格里拉的香!
理论部分讲完,我们来分析具体需求。首先,页面滚动是一个scroll事件,毫无疑问,我们需要在指令中监听该事件,并且等到页面滚动到底部时,再执行对应的方法。下面是代码:
bindScrollEvent() {
fromEvent(this.hostEl,'scroll')
.pipe(
debounceTime(250),
takeUntil(this.end$)
)
.subscribe( _ => {
const topIns = this.hostEl.scrollTop
const bottomIns = this.hostEl.scrollHeight - topIns - this.hostEl.offsetHeight
if(bottomIns < 15) {
this.listData.nextPage()
}
})
}
我们定义了一个bindScrollEvent方法,并再ngOninit钩子内直接调用。可以注意到,我并没有用angular内置的hostListener的事件监听,而用了rxjs内置的dom事件捕获函数。因为rxjs的fromEvent可以直接将事件转化为流,这样我们就能通过操作符debounceTime实现滚动事件的防抖功能。这样比angular自带的方法可方便多了(同时不要忘记需要new一个Subject用来取消事件流的订阅)。
此外,还用到了angular的ElementRef来获取指令的宿主元素:
get hostEl() {
return this.el.nativeElement
}
通过监听宿主元素hostEl的滚动事件,我们计算出作为父容器的hostEl的底部与n内部整个文档内容底部的距离,在小于15px时,我们就认为列表即将滚动至容器底部(提醒:作为父容器的hostEl必须是可滚动的,这个需要我们自己把css属性设置好)。这时候,我们便可调用pagingDataService的nextPage()方法。pagingDataService调用该方法后,拿到下一页数据并发布出去,然后被组件订阅到,最后就会自然而然地渲染到页面上。是不是很舒爽?
做到这里,其实该指令的任务基本完成了。当然,还剩一个小任务,就是加载时需要在页面底部给一个文字提示。尤其是网络慢时,用户滚动到底部,看到“正在加载更多”文字提示,对用户的反馈就很友好。
回顾上一节我们知道,pagingDataService有一个Observable对象loadingState$,现在终于派上用场,我们只需要在指令内部监听他就可以了。
listenLoading() {
this.pagingDataService.loadingState$
.pipe(
distinctUntilChanged(),
takeUntil(this.end$)
)
.subscribe(state => {
switch(state) {
default:
case 'success':
this.loadingDomRef.instance.text = '';
break;
case 'end':
this.loadingDomRef.instance.text = '- 已经到底啦 -';
break;
case 'error':
this.loadingDomRef.instance.text = '加载失败';
break;
case 'pagePending':
this.loadingDomRef.instance.text = '加载中...';
break;
}
})
}
可以看到,无非就是在不同的加载状态给不同的文字内容(end表示没有更多数据)。当然,你肯定会问这个loadingDomRef是个什么鬼。这里,我们用到了动态组件,此动态组件的模板内容就是一段文本,没有其他:
@Component({
template:`
<div class="tip-container">
<span>{{text}}</span>
</div>
`,
styles:[`
.tip-container{
margin-top: 15px;
text-align: center;
padding: 5px 0;
min-height: 15px;
color: #999;
}
`]
})
class LoadingBox {
text: string ;
}
至于指令怎么将该组件动态插入到宿主元素,代码如下:
insertLoadingDom() {
const factory = this.cfr.resolveComponentFactory(LoadingBox)
this.loadingDom = document.createElement('loading')
this.loadingDomRef = factory.create(this.injector,[],this.loadingDom)
// 在第一次加载数据成功后,再将动态组件插入
this.loadingState$
.pipe(
filter( state => ['success','end'].indexOf(state) > -1),
take(1)
).subscribe( _ => {
this.appRef.attachView(this.loadingDomRef.hostView)
this.hostEl.appendChild(this.loadingDom)
})
}
其中crf为angular内部带有组件工厂函数的服务,可以调用resolveComponentFactory方法传入一个组件类,返回一个组件工厂对象,该对象调用create方法,又会返回刚才传入的组件类的组件实例。拿到这个组件实例,我们就能把他插入到指令的宿主元素中了。
具体的操作可以参看angular官网关于动态组件和自定义元素这两节,讲得其实比较详细了。这两块内容对于实现比较复杂的交互功能来讲还是很重要的。