浏览器实现按行读取文件

309 阅读1分钟

使用场景

在项目中开发中,遇到这样的需求:需要用户通过input上传文件后,按行读取文件内容并使用。

首先想到的解决方案也很简单,就是直接读取文件所有内容,再通过换行符分割内容输出一个行内容的数组,再遍历数组进行行数据使用。在文件较小的情况下,不失为一个好办法,不过文件比较大时,读取文件耗时就会增加,会影响使用体验。

那通过js能否直接按行读取文件呢。通过调研发现js并没有提供按行读取的接口。不过提供了流式读取文件的方式,ReadableStreamjs提供用于读取流的接口。在拿到用户上传文件的file对象后,可以用file.stream()获取ReadableStream实例对象,再对ReadableStream对象进行流式读取操作。不过此时的流式读取并不是按行读取,在拿到流数据后,将数据解码为字符串,再通过换行符分割内容,就可以达到按行读取文件的效果了。

示例代码如下:

// 从file对象中创建文件流
const stream = file.stream()

// 获取文件流的reader对象
const reader = stream.getReader()

const decoder = new TextDecoder('utf-8')

// reader.read()会流式读取内容,done为true时代表读取结束
let { done, value } = await reader.read()

// 读取出的内容
let buffer = ''

while (!done) {
  buffer += decoder.decode(value, { stream: true })
  
  // 将读取出的内容按换行符分割
  let lines = buffer.split('\n')

  // 保留最后一行未处理的部分
  buffer = lines.pop()

  for (const line of lines) {
    console.log(`Line: ${line}\n`)
    
    // 文件较大时,一直循环会导致无法处理其他逻辑,所以这里可以做个一延时,每行读取结束后去做其他事情
    await sleep(10)
  }

  ;({ value, done } = await reader.read())
}

sleep函数:

// 延时函数
function sleep(time = 1000) {
    let timeouter
    let _resolve
    const promise = new Promise((resolve) => {
      _resolve = resolve
      timeouter = setTimeout(() => {
        resolve(true)
      }, time)
    })

    // 取消延时
    promise.cancel = () => {
      clearTimeout(timeouter)
      _resolve(false)
    }

    return promise
}

通过上面的代码,就可以模拟实现对文件的按行读取了。

html完整示例:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Stream File Line by Line</title>
  </head>
  <body>
    <input type="file" id="fileInput" />
    <button id="btn">停止</button>
    <pre id="fileContent"></pre>

    <script>
      let paused = false

      document.getElementById('btn').addEventListener('click', function () {
        paused = !paused
        if (paused) {
          this.textContent = '开始'
        } else {
          this.textContent = '停止'
        }
      })

      function sleep(time = 1000) {
        let timeouter
        let _resolve
        const promise = new Promise((resolve) => {
          _resolve = resolve
          timeouter = setTimeout(() => {
            resolve(true)
          }, time)
        })

        promise.cancel = () => {
          clearTimeout(timeouter)
          _resolve(false)
        }

        return promise
      }

      document.getElementById('fileInput').addEventListener('change', async function (event) {
        const file = event.target.files[0]
        console.log('file', file)
        if (!file) return

        const output = document.getElementById('fileContent')
        output.textContent = '' // 清空之前的内容
        console.time('readFile')

        // 创建文件流
        const stream = file.stream()
        const reader = stream.getReader()
        const decoder = new TextDecoder('utf-8')

        let { done, value } = await reader.read()
        console.log('chunk', value)

        let buffer = ''

        while (!done) {
          buffer += decoder.decode(value, { stream: true })
          let lines = buffer.split('\n')

          // 保留最后一行未处理的部分
          buffer = lines.pop()

          for (const line of lines) {
            output.textContent += `Line: ${line}\n`
            console.log(`Line: ${line}\n`)
            await sleep(10)
            if (paused) return
          }

          ;({ value, done } = await reader.read())
        }

        // 处理最后剩余的内容
        if (buffer.length > 0) {
          console.log(`Line: ${buffer}\n`)
          output.textContent += `Line: ${buffer}\n`
        }
        console.log('END')
        console.timeEnd('readFile')
      })
    </script>
  </body>
</html>