前端实现日志的自动滚动

3,629 阅读3分钟

这是我参与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] 安装成功,应用进程结束

当然,这是我瞎写的,只是为了展示我们想要实现的日志的样子,当这样的日志内容或者进程内容很多的时候,最好的办法是随着内容的增加,让页面自动向下滚动,方便用户查看最新的消息

二、分析

在这个过程中,一方面要不断的调用接口来获取最新的数据,一方面还要把数据渲染到页面中,同时让页面发生滚动

注意这里要让页面产生滚动的动画,有三个条件

  1. 不能每次接口都返回全量数据直接替换页面数据,这样会导致页面没有滚动的动画
  2. 页面的 css 样式要正确设置,确保内容只在父元素的可视区域内发生滚动而不是整个页面滚动
  3. 使用正确的 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型参数

参数说明:
参数类型默认值备注
alignToTopbooleantrue该元素的顶端是否和其所在滚动区的可视区域的顶端对齐
scrollIntoViewOptionsobject{behavior: "auto", block: "start", inline: "nearest"}behavior:定义滚动动画过度,可选autosmooth
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)

参数说明:
参数类型默认值备注
xposnumber-必需。要在窗口文档显示区左上角显示的文档的 x 坐标
yposnumber-必需。要在窗口文档显示区左上角显示的文档的 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>

本文的内容到此就结束了,案例代码未经完整测试,有任何问题可以在评论区留言哦 :)