使用场景
在项目中开发中,遇到这样的需求:需要用户通过input上传文件后,按行读取文件内容并使用。
首先想到的解决方案也很简单,就是直接读取文件所有内容,再通过换行符分割内容输出一个行内容的数组,再遍历数组进行行数据使用。在文件较小的情况下,不失为一个好办法,不过文件比较大时,读取文件耗时就会增加,会影响使用体验。
那通过js能否直接按行读取文件呢。通过调研发现js并没有提供按行读取的接口。不过提供了流式读取文件的方式,ReadableStream是js提供用于读取流的接口。在拿到用户上传文件的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>