前言
之前因为工作需要对geojson进行数据处理,在操作的过程中我就发现,自己手动去下载和读取文件就很麻烦。因此我就想在这里探讨一下如何在前端实现文件自动读取和下载。
一、文件读取
读取文件功能的技术点其实主要有两个:一是<input type="file">
元素 ; 二是 FileReader类型。前者的作用是在浏览器中实现"文件选择"这样一个交互动作;后者的作用则是读取和解析所选择的文件。
1.文件选择器元素
<input type="file">
元素的作用是允许用户从他们的设备中选择一个或多个文件。选择后,这些文件可以通过JS代码和文件API进行操作。
1.1 基本示例
HTML
<input type="file" >

点击之后就会打开如下的窗口:

1.2 获取文件
点击<input type="file">
元素可以打开一个窗口选择文件,那么所选择的文件我们如何拿到呢?
这个问题其实又可以拆解成两个问题,什么时候拿?和 在哪儿拿?
首先回答什么时候拿的问题,当<input type="file">
元素上传一个文件时会触发change事件,因此在onchange事件处理程序中去拿,就可以保证此时的文件是存在的,是可以拿的到的。
再回答在哪儿拿的问题,所选择的文件被保存在 <input>
元素的 files
属性上,其属性值是一个FileList,FileList是一个类数组结构,其中的每个元素都是File对象,我们可以通过方括号或item()
读取其中的元素。
所以这时我们就可以尝试去获取一个文件了:
HTML
<!DOCTYPE html>
<html lang="en">
<body>
<input type="file" onchange="readFile(event)" />
<script>
function readFile(event) {
const file = event.target.files[0]
console.log(file);
}
</script>
</body>
</html>

上面打印的就是拿到的文件,它是一个File
对象,后续我们还要通过 FileReader
去读取他。
1.3 附加属性
<input type="file">
元素还有一些附加属性,这些属性可以帮助我们拓展文件读取功能。
首先是multiple
属性,它可以允许<input>
选择多个文件。
HTML
<!DOCTYPE html>
<html lang="en">
<body>
<input type="file" multiple onchange="readFile(event)" />
<script>
function readFile(event) {
const file = event.target.files[0]
console.log(event.target.files);
}
</script>
</body>
</html>
添加了multiple
属性后,我们在选择窗口中就可以选择多个文件了

此时,flies
属性中的文件也就有多个

另一个重要的附加属性是accept
,它可以指定文件input接收的文件类型,从而实现"限制可接收的文件类型"的功能。accept
属性值是一个字符串,这个字符串是一个以逗号为分隔的唯一文件类型说明符列表。
那么这个所谓的"唯一文件类型说明符" 该怎么写呢?它其实是包括以下的这些形式:
- 一个以英文句号(".")开头的合法的不区分大小写的文件名扩展名。例如:.jpg、.pdf 或 .doc。
- 一个不带扩展名的 MIME 类型字符串。
- 字符串 audio/*,表示"任何音频文件"。
- 字符串 video/*,表示"任何视频文件"。
- 字符串 image/*,表示"任何图片文件"。
因此,假如我们的文件选择器只需要读取图片,包括标准的图片格式和 PDF 文件,那么大概是这样的:
HTML
<input type="file" accept="image/*,.pdf" />
此时所打开的选择窗口中就只剩下我们所限定类型的文件了。

2.使用FileReader解析文件
通过文件选择器我们可以在项目中拿到文件,但此时的文件是 File
类型的,这种状态的文件我们无法直接处理和使用。这时就需要使用FileReader
解析文件。
2.1基本使用
FileReader
使用分为三步:
第一步:实例化一个文件读取器
JavaScript
const reader = new FileReader()
第二步:调用文件读取器的某个读取方法,开始读取文件
JavaScript
reader.readAsText(file ,'UTF-8') //这个方法可以将文件读取为字符串
第三步: 侦听读取器的onload
事件或者onloadend
, 当文件读取完毕后,会触发这两个事件。在事件处理程序中通过读取器的result
属性可以拿到读取的结果
JavaScript
reader.onload = function(){
this.result //读取的结果
}
读取一个txt文件
此时我们就可以尝试去读取一个文件。首先创建如下的txt文件。

可以读取出txt文件中的内容

HTML
<!DOCTYPE html>
<html lang="en">
<body>
<input type="file" onchange="readFile(event)" />
<script>
function readFile(event) {
const file = event.target.files[0]
const reader = new FileReader()
reader.readAsText(file ,'UTF-8')
reader.onload = function () {
console.log('读取的结果', this.result)
}
}
</script>
</body>
</html>
2.2 读取方法介绍
FileReader
的读取方法可以帮助我们将Bobl、file
文件转换为其它的格式。常用的读取方法有三个:
readAsArrayBuffer
,将文件读取为ArrayBuffer
数据对象readAsDataURL
,将文件读取为一个URLreadAsText
,将文件读取为字符串
readAsArrayBuffer
方法所转化的结果是一个ArrayBuffer
对象是一个用于存储二进制数据的内存,对于
ArrayBuffer
我还不是很理解,也不知道在什么情况下使用,等以后研究明白了再做介绍。readAsText
在我写的读取文件的工具中就使用了这个方法,让它帮助我读取geojson文件,目前感觉这个方法是最实用的。readAsDataURL
方法我看到有的文章中用它来读取图片,下面我就来尝试一下。
读取图片并显示
HTML
<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<input type="file" accept="image/*" onchange="readImg(event)" />
<br />
<img src="" alt="请选择一个图片" width="800" />
<script>
function readImg(event) {
const file = event.target.files[0]
const reader = new FileReader()
reader.readAsDataURL(file)
reader.onloadend = function () {
console.log('读取的结果', this.result)
document.images[0].src = this.result
}
}
</script>
</body>
</html>

二、文件下载
文件下载也分为两步,第一步是创建一个<a>
元素(当<a>
元素设置了download
属性后,浏览器就会将链接的 URL 视为下载资源);第二步是将我们的数据(或文件)转换为URL 。
1.文件下载链接
<a>
元素的download
属性可以将连接变为文件下载链接。download
属性的值为一个字符串,这个字符串就是fileName
即下载文件的文件名。
HTML
<a href="" download="xxx.json">
2.文件转URL
由于我是需要将js中的数据下载为json文件,所以这里就讨论一下 "如何将js数据转换为url"。
2.1 FileReader方案
首先将数据转换为Bolb
对象,Blob
对象接收两个参数:
Array
,一个可迭代对象,包含ArrayBuffer
、TypedArray
、DataView
、Blob
、字符串或者任意这些元素的混合。Options
,一个对象,其中可以指定以下属性。
-
type
,将会被存储到blob
中的数据的 MIME 类型;
JavaScript
data = JSON.stringify(data)
const blob = new Blob([data], { type: 'Application/json' })
然后再通过FileReader
对象的readAsDataURL
方法转换为URL
JavaScript
const reader = new FileReader()
reader.onload = function () {
a.href = this.result //读取到的URL
a.click()
}
reader.readAsDataURL(blob)
2.2 URL.createObjectURL方案
也可以直接使用URL.createObjectURL
方法将 blob
对象转换为URL。(为了获得最佳性能和内存使用状况,建议在URL对象使用完毕后,通过URL.revokeObjectURL
方法将其释放)
JavaScript
// 创建blob对象
data = JSON.stringify(data)
const blob = new Blob([data], { type: 'Application/json' })
// 将blob转换为URL对象
const url = URL.createObjectURL(blob)
// 设置url并点击下载
a.href = url
a.click()
// 释放URL对象
URL.revokeObjectURL(url)
三、实现一个先文件读取后文件下载的小工具
为了方便工作中的数据处理,我编写了一个小工具,它可以读取geojson文件,然后处理其中的数据,最后将数据下载为json文件。
1.读取文件函数
JavaScript
function readFile() {
return new Promise((resolve) => {
const fileElement = document.createElement('input')
fileElement.type = 'file'
fileElement.multiple = true
fileElement.accept = ".json,.geojson"
fileElement.onchange = (event) => {
// 获取文件
let fileArr = Array.from(event.target.files)
Promise.all(
fileArr.map(
(file) =>
new Promise((success) => {
// 实例化一个文件读取器对象
const reader = new FileReader()
// 让读取器开始读取
reader.readAsText(file)
// 当文件读取完成后处理数据
reader.onload = (e) => {
const data = JSON.parse(e.target.result)
success(data)
}
})
)
).then((res) => {
resolve(res)
})
}
fileElement.click()
})
}
读取文件基本还是按照之前介绍的思路进行实现的,这里重点介绍一下我在实现这个readFile
函数时遇到一个难点。
为了提高函数的泛用性,我希望可以将读取到的文件数据return出去。为什么return会成为一个问题能?因为实际上这个函数中的文件读取是一组复杂的异步任务。

于是我使用期约来处理这里的异步任务:
JavaScript
function readFile() {
return new Promise((resolve) => {
// 第一个Promise用于处理文件选择器的change事件
// ......
//Promise.all()用于实现多个文件的读取操作并行,保证能够拿到所有文件的数据
Promise.all(
fileArr.map(
(file) =>
new Promise((success) => {
// ......
// 第二个Promise用于处理FileReader的load事件
success(data)
})
)
).then((res) => {
resolve(res)
})
})
}
2.下载文件函数
JavaScript
//下载文件
function download(files) {
if (!files) return alert('请先读取一个文件')
files.forEach((el) => {
const a = document.createElement('a')
a.download = el.name || getNowTime()
const fileData = JSON.stringify(el.data)
const blob = new Blob([fileData], { type: 'Application/json' })
a.href = URL.createObjectURL(blob)
a.click()
})
alert('文件下载成功')
}
下载文件的函数就很简单了,这里我也就不过多介绍了。
3.完整代码
index.html
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/@turf/turf/turf.min.js"></script>
<script src="./util/util.js"></script>
<script src="./util/File.js"></script>
<script src="./main/handle.js"></script>
</head>
<body>
<button id="read">读取</button>
<button id="handle">处理数据</button>
<button id="download">下载</button>
<script>
let files
document.getElementById('read').addEventListener('click', () => {
readFile().then((data) => {
console.log(data)
files = data
alert('文件读取成功')
})
})
document.getElementById('handle').addEventListener('click', () => {
handleData(files).then((res) => {
console.log(res)
files = res
alert('数据处理成功')
})
})
document.getElementById('download').addEventListener('click', () => {
download(files)
})
</script>
</body>
</html>
File.js
HTML
function readFile() {
return new Promise((resolve) => {
const fileElement = document.createElement('input')
fileElement.type = 'file'
fileElement.multiple = true
fileElement.accept = ".json,.geojson"
fileElement.onchange = (event) => {
// 获取文件
let fileArr = Array.from(event.target.files)
Promise.all(
fileArr.map(
(file) =>
new Promise((success) => {
// 实例化一个文件读取器对象
const reader = new FileReader()
// 让读取器开始读取
reader.readAsText(file)
// 当文件读取完成后处理数据
reader.onload = (e) => {
const data = JSON.parse(e.target.result)
success(data)
}
})
)
).then((res) => {
resolve(res)
})
}
fileElement.click()
})
}
//下载文件
function download(files) {
if (!files) return alert('请先读取一个文件')
files.forEach((el) => {
const a = document.createElement('a')
a.download = el.name || getNowTime()
const fileData = JSON.stringify(el.data)
const blob = new Blob([fileData], { type: 'Application/json' })
a.href = URL.createObjectURL(blob)
a.click()
})
alert('文件下载成功')
}
// 获取当前时间
function getNowTime() {
var date = new Date() //时间戳为10位需*1000,时间戳为13位的话不需乘1000
var Y = date.getFullYear() + '-'
var M =
(date.getMonth() + 1 < 10
? '0' + (date.getMonth() + 1)
: date.getMonth() + 1) + '-'
var D = (date.getDate() < 10 ? '0' + date.getDate() : date.getDate()) + ' '
var h = (date.getHours() < 10 ? '0' + date.getHours() : date.getHours()) + ':'
var m =
(date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()) + ':'
var s = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds()
return Y + M + D + h + m + s
}