这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战
一、需求
当某个程序在后端运行的时候,前端经常要显示一个动态的进度条,或者 loading 的动画,或者百分比的数字
当然,更好的效果是给当前的用户展示出程序运行的具体细节,比如运行中记录的日志,或者实时的进程环节
比如
[12:01:59] 正在启动应用进程
[12:02:00] 进程启动成功,正在获取网络资源...
[12:02:01] 成功启动下载任务
[12:02:02] 下载中....
[12:02:03] 下载成功,开始解析网络资源
[12:02:04] 正在安装相关程序
[12:02:05] 安装成功,应用进程结束
当然,这是我瞎写的,只是为了展示我们想要实现的日志的样子,当这样的日志内容或者进程内容很多的时候,最好的办法是随着内容的增加,让页面自动向下滚动,方便用户查看最新的消息
二、分析
在这个过程中,一方面要不断的调用接口来获取最新的数据,一方面还要把数据渲染到页面中,同时让页面发生滚动
注意这里要让页面产生滚动的动画,有三个条件
- 不能每次接口都返回全量数据直接替换页面数据,这样会导致页面没有滚动的动画
- 页面的 css 样式要正确设置,确保内容只在父元素的可视区域内发生滚动而不是整个页面滚动
- 使用正确的 API 来完成滚动的效果
三、实现方法
- 第一个条件是很好的做的,我们可以使用
setInterval
var timer = setInertval(()=>{
getLogList().then(res=>{
randerView(res.data)
})
},2000)
不过这样做有个弊端,就是当用户网络不那么畅通,或者服务器比较拥挤的时候,已经调用的接口一直处于 pendding 的状态,后面的接口还在持续不断的调用,会让本就拥堵的服务雪上加霜
所以我更喜欢使用 setTimeout
async function getLogData(){
const logData = await getLogList()
randerView(logData.data)
setTimeout(getLogData, 2000)
}
getLogData()
-
第二个样式问题,只需要正确的给父元素添加固定的高度和
overflow-y: scroll
就可以了。 -
下面来说说第三个问题,如何让内容自动的向下滚动,因为这部分是比较重点的内容,我就单独放一个小节里来说
页面自动滚动的方案探讨
一般来说,如何让内容自动的向下滚动有两种方法来实现。
scrollIntoView
scrollIntoView 方法会滚动元素的父容器,使调用 scrollIntoView 的元素在父元素中可见。
语法如下:
element.scrollIntoView(); // 等同于element.scrollIntoView(true)
element.scrollIntoView(alignToTop); // Boolean型参数
element.scrollIntoView(scrollIntoViewOptions); // Object型参数
参数说明:
参数 | 类型 | 默认值 | 备注 |
---|---|---|---|
alignToTop | boolean | true | 该元素的顶端是否和其所在滚动区的可视区域的顶端对齐 |
scrollIntoViewOptions | object | {behavior: "auto", block: "start", inline: "nearest"} | behavior:定义滚动动画过度,可选auto 、smooth ;block:定义垂直方向的对齐,可选 start , center , end , 或 nearest ;inline:定义水平方向的对齐,可选 start , center , end , 或 nearest |
使用方法:
<template>
<div >
<strong>进程日志</strong>
<div style="max-height:120px; position:relative">
<div v-if="logs.length">
<p
class="logList-item"
v-for="(item, index) in logs"
:key="index"
:id="(logs.length === index+1)?'scollLog':''"
>{{ item }}</p>
</div>
<p v-else>暂无数据</p>
</div>
</div>
</template>
<script lang='ts'>
import { Component, Vue, Watch, Prop } from 'vue-property-decorator'
import { formatTime } from '@/utils'
@Component
export default class extends Vue {
@Prop() private LOGS: Array<object>;
private name: string = 'processLog';
private logs: Array<string> = [];
// getData 将父组件传递过来的日志转成`[12:01:59] 正在启动应用进程`这种格式
private getData () {
this.logs = this.LOGS
? this.LOGS.map(
(item: object): string =>
'[' + formatTime(item.updatedTime) + '] ' + item.content + '\n'
)
: []
}
@Watch('LOGS')
scrollLogs (newValue) {
this.getData()
// 在日志渲染之后,将最底下的日志滚动到其父元素可视区域内
this.$nextTick(() => {
if (newValue.length !== 0) {
(document.getElementById('scollLog') as HTMLElement)
.scrollIntoView({ behavior: 'smooth', block: 'nearest' })
}
})
}
mounted () {
this.getData()
}
}
</script>
<style lang="scss" scoped>
.logList-item {
padding: 8px 0;
margin: 0;
}
</style>
总结
scrollIntoView 这个方法的对 ios safari 和 IE 不是很友好,其他的浏览器没有什么问题
另外,这个方法对布局也没有什么要求,简单方便又易于理解,只需要针对最后一条渲染的日志调用即可
scrollTo
这个方法是老生常谈了,这个方法可把内容滚动到指定的坐标。
语法如下:
scrollTo(xpos,ypos)
参数说明:
参数 | 类型 | 默认值 | 备注 |
---|---|---|---|
xpos | number | - | 必需。要在窗口文档显示区左上角显示的文档的 x 坐标 |
ypos | number | - | 必需。要在窗口文档显示区左上角显示的文档的 y 坐标 |
使用方法:
<template>
<div class="console-wraper">
<div class="console_window" ref="consoleWindow">
<div id="console_output" ref="consoleOutput">
<-- 这里写日志信息,注意,一定要在父元素外再套一层-->
</div>
</div>
</div>
</template>
<script lang='ts'>
import { Component, Vue, Watch, Prop } from 'vue-property-decorator'
import { post } from '@/utils/http'
@Component
export default class extends Vue {
async getData() {
const res = await post(`/api/get/log`, { read: true })
this.formatData(res.data)
}
formatData(data) {
try {
if (data.length) {
data.forEach(item => {
this.printLine('[' + item.updateTime + '] ' + item.value)
})
} else {
this.printLine('暂无数据', 'input_line')
}
} catch (e) {
console.log('error')
}
}
printLine(s, type) {
if ((s = String(s))) {
let n = document.createElement('pre')
if (!type) type = 'output_line'
if (n.innerText !== undefined) {
// IE has to use innerText
n.innerText = s
} else {
// Firefox uses createTextNode
n.appendChild(document.createTextNode(s))
}
n.className = type
this.$refs.consoleOutput.appendChild(n)
// 添加完日志后,让父元素滚动到其自身的距离
this.$refs.consoleWindow.scrollTop = this.$refs.consoleWindow.scrollHeight
}
}
mounted () {
this.getData()
}
}
</script>
<style lang=scss scoped>
.console-wraper{
display: flex;
flex-direction: column;
height: 100%;
}
.console_window {
flex: 1;
background: #333;
overflow: auto;
.console_output {
white-space: pre-wrap;
word-wrap: break-word;
padding: 12px;
font-weight: bold;
}
/deep/ pre.input_line {
font-size: 14px;
color: yellow;
opacity: 0.8;
padding: 0 20px;
}
/deep/ pre.output_line {
color: #fff;
font-size: 13px;
white-space: pre-wrap;
word-wrap: break-word;
padding-left: 40px;
opacity: 0.7;
}
}
</style>
本文的内容到此就结束了,案例代码未经完整测试,有任何问题可以在评论区留言哦 :)