js-xlsx
没有需求就没有方法
最近接到一个新需求,需求乍一看很简单,就是在价格数据表格页面增加一个数据导出功能。然而在开发过程中,遇到了这儿样那样的问题。
后端同事说他那边只支持返回 json 数据,但是对于太大的数据量他那边只能限制导出的时候进行限量。这就很尴尬,前端如何将 json 数据成功导出为用户使用的 excel 文件?
项目使用的是 iVew UI 库。库中只提供了导出格式为 .csv
格式。这就很尴尬,需要自己实现。
废话不多说,来到今天的主题 js-xlsx 实现纯前端导出 excel 表格
js-xlsx
js-xlsx 是一个纯 JavaScript 实现的,能运行在所有 JavaScript 环境中,包括浏览器,NodeJs 等的 excel 库,能够读取和写入 excel 表格。
兼容性如下图:
下面开始介绍 js-xlsx 的简单使用方式。
XLSX
使用之前,先安装它:
复制 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script
lang="javascript"
src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"
></script>
</head>
<body>
<script>
console.log(XLSX)
</script>
</body>
</html>
通过打印 XLSX
看看它主要有哪些属性
write
用来把数据写入并生成 xlsx 文件的 API
直接跳转实现例子
读取 Excel 数据
为了解析数据,第一步就是读取文件,这里有几个不同的地方
复制 const workBook = XLSX.readFile('test.xlsx')
因为 Excel 数据表与 HTML Table 结构是相应的,所以也可以从 Table 里获取数据
复制 // 给table标签定义id属性
const workBook = XLSX.utils.table_to_book(document.getElementById('table'))
复制 // 表格文件地址
let url = '/save/test.xlsx'
let req = new XMLHttpRequest()
req.open('GET', url, true)
req.responseType = 'arraybuffer'
req.onload = (s) => {
let data = new Uint8Array(req.response)
let workBook = XLSX.read(data, { type: 'array' })
console.log(workBook)
}
req.send()
左边为输出内容,右边为表格数据
复制 <div id="drop" style="width: 200px;height: 200px; background-color: aqua;">
将文件拖到此处
</div>
<div id="excelView"></div>
<script>
const drop = document.getElementById('drop')
//拖进
drop.addEventListener(
'dragenter',
function(e) {
e.preventDefault()
},
false
)
//拖离
drop.addEventListener(
'dragleave',
function(e) {
dragleaveHandler(e)
},
false
)
//拖来拖去 , 一定要注意dragover事件一定要清除默认事件
//不然会无法触发后面的drop事件
drop.addEventListener(
'dragover',
function(e) {
e.preventDefault()
},
false
)
//扔
drop.addEventListener(
'drop',
function(e) {
dropHandler(e)
},
false
)
console.log(drop)
const excelView = document.getElementById('excelView')
drop.addEventListener('drop', function(e) {
console.log(11)
// e.stopPropagation();
e.preventDefault()
let files = e.dataTransfer.files
console.log(files)
let reader = new FileReader()
reader.readAsArrayBuffer(files[0])
reader.onload = function(event) {
let data = event.target.result
let wb = XLSX.read(data, { type: 'array' })
var wsName = wb.SheetNames[0]
var ws = wb.Sheets[wsName]
// 渲染
excelView.innerHTML = XLSX.utils.sheet_to_html(ws)
}
})
</script>
HTML5 拖拽上传文件请点击 查看
js-xlsx 的解析数据 API
XLSX.read(data, read_opts)
XLSX.readFile(filename, read_opts)
workBook
workBook
就是读取 Excel 数据后的 json 对象,里面记录着 Excel 的数据信息
Directory
是 Excel 文件的描述对象
SheetNames
里存储着每个 Sheet 的名字
Sheets
里面存储的是每个 Sheet 的数据
每个 Sheet 对象里都根据坐标存储 Excel 的二维数据,其中 !ref
代表这个表的范围
对应的是 Excel 表数据
其中 Strings 是一个数组,他按照从左到右,从上到下的顺序来把二维 的数据降到一维
复制 const arr = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
const cat = (...args) => arg.reduce((acc, cur) => [...acc, ...cur], [])
// 将二维数组降到一维
cat(...arr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
单元格对象
在下图中 A1 就是一个单元格对象,它代表着 Excel 二维表里的 A1 位置的单元格对象。
单元格类型:b 布尔值,n 数字,e 错误,s 字符串,d 日期
单元格超链接对象(.Target 持有链接,.Tooltip 是工具提示)
导出数据到文件
对于写入,第一步是生成输出数据。对于生成输出数据也分为几种。
复制 const XLSX = require('xlsx')
XLSX.writeFile(workBook, 'out.xlsx')
因为 Excel 是一个表格结构,因此可以轻易输出 HTML Table
复制 const workSheet = workBook.Sheets[workBook.SheetNames[0]]
const container = document.getElementById('table')
container.innerHTML = XLSX.utils.sheet_to_html(workSheet)
复制 // bookType can be 'xlsx' or 'xlsm' or 'xlsb'
let wOpts = { bookType: 'xlsx', bookSST: false, type: 'binary' }
let wBout = XLSX.write(workBook, wOpts)
/**
* 字符串转字符流
*/
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Unit8Array(buf)
for (let i = 0; i != s.length; ++i) {
view[i] = s.charCodeAt(i) & 0xff
}
return buf
}
// saveAs调用在本地计算机上下载文件
saveAs(
new Blob([s2ab(wBout)], { type: 'application/octet-stream' }),
'test.xlsx'
)
复制 // 通过 FormData 上传到服务器
let wOpts = { bookType: 'xlsx', bookSST: false, type: 'base64' }
let wBout = XLSX.write(workbook, wOpts)
let req = new XMLHttpRequest()
req.open('POST', '/upload', true)
let formData = new FormData()
formData.append('file', 'test.xlsx')
formData.append('data', wBout)
req.send(formData)
js-xlsx
写入数据 API
XLSX.write(wb, write_opts)
XLSX.writeFIle(wb, filename, write_opts)
XLSX.writeFileAsync(filename,wb,o,cb)
工具函数
XLSX.utils
是一个工具 API,一些函数用于把数据导出各种格式
sheet_to_*
函数接受一个工作表和一个可选的选项对象
*_to_sheet
函数接受一个数据对象和一个可选选项对象
XLSX.utils.sheet_to_formulae
XLSX.utils.book_append_sheet(wb, ws, “test”)
向 workbook 添加一个 ws,名称为 test。
XLSX.utils.table_to_sheet
XLSX.utils.sheet_to_formulae
XLSX.utils.encode_row/decode_row
XLSX.utils.encode_col/decode_col
XLSX.utils.encode_cell/decode_cell
XLSX.utils.encode_range/decode_range
数据自定义
下面介绍几种方式来定义数据对象
阵列数组
复制 // 构造一个二维表
const ws = XLSX.utils.aoa_to_sheet(['我的名字是Lambda'.split('')])
/**
{
A1: {v: "我", t: "s"}
B1: {v: "的", t: "s"}
C1: {v: "名", t: "s"}
D1: {v: "字", t: "s"}
E1: {v: "是", t: "s"}
F1: {v: "L", t: "s"}
G1: {v: "a", t: "s"}
H1: {v: "m", t: "s"}
I1: {v: "b", t: "s"}
J1: {v: "d", t: "s"}
K1: {v: "a", t: "s"}
!ref: "A1:K1"
}
*/
对象数组
复制 const ws = XLSX.utils.json_to_sheet(
[
{ S: 1, h: 2, e: 3, e_1: 4, t: 5, J: 6, S_1: 7 }, // 第一行
{ S: 2, h: 3, e: 4, e_1: 5, t: 6, J: 7, S_1: 8 } // 第二行
],
{
header: ['S', 'h', 'e', 'e_1', 't', 'J', 'S_1'] // 表头
}
)
HTML Table
复制 <table id="sheet">
<tr>
<td>S</td>
<td>h</td>
<td>e</td>
<td>e</td>
<td>t</td>
<td>J</td>
<td>S</td>
</tr>
<tr>
<td>1</td>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
</tr>
<tr>
<td>2</td>
<td>3</td>
<td>4</td>
<td>5</td>
<td>6</td>
<td>7</td>
<td>8</td>
</tr>
</table>
<script>
const tbl = document.getElementById('sheet')
const wb = XLSX.utils.table_to_book(tbl)
// 之后需要将新的数据对象添加到workBook里
let ws_name = 'Sheet'
let ws = XLSX.utils.aoa_to_sheet([
['S', 'h', 'e', 'e', 't', 'J', 'S'],
[1, 2, 3, 4, 5]
])
wb.SheetNames.push(ws_name)
wb.Sheets[ws_name] = ws
</script>
生成 xlsx 文件
js-xlsx 提供一个默认的 workBook
对象(XLSX.utils.book_new)
复制 const ws = XLSX.utils.json_to_sheet(
[
{ name: 'Lambda', age: 21, sex: '男' }, // 第一行
{ name: 'Lambda1', age: 21, sex: '男' }, // 第一行
{ name: 'Lambda2', age: 21, sex: '男' } // 第一行
],
{
header: ['name', 'age', 'sex'] // 表头
}
)
var tmpWB = {
SheetNames: ['sheet'], //保存的表标题
Sheets: {
sheet: Object.assign(
{},
ws, //内容
{}
)
}
}
tmpDown = new Blob(
[
s2ab(
XLSX.write(
tmpWB,
{
// bookType can be 'xlsx' or 'xlsm' or 'xlsb'
bookType: type == undefined ? 'xlsx' : type,
bookSST: false, // 是否生成Shared String Table,官方解释是,如果开启生成速度会下降,但在低版本IOS设备上有更好的兼容性
type: 'binary'
} //这里的数据是用来定义导出的格式类型
)
)
],
{
type: ''
}
) //创建二进制对象写入转换好的字节流
const outFile = document.createElement('a')
var href = URL.createObjectURL(tmpDown) // 创建对象超链接
outFile.download = '下载名称.xlsx' // 下载名称
outFile.style.display = 'none'
outFile.href = href // 绑定a标签
document.body.appendChild(outFile)
outFile.click() // 模拟点击实现下载
setTimeout(function() {
// 延时释放
URL.revokeObjectURL(tmpDown) // 用URL.revokeObjectURL()来释放这个object URL
document.body.removeChild(outFile)
}, 100)
// 字符串转字符流
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i != s.length; ++i) view[i] = s.charCodeAt(i) & 0xff
return buf
}
兄弟们,实现导出了!!
合并单元格功能
合并表格的数据在 Sheets Object 里的 !merges 属性里,它是一个数组,数组的顺序是按照二维表顺序(从左到右,从上到下)排列。
复制 ;[
{
s: { c: 1, r: 1 }, // 第 2 列第 2 行开始,也就是 B2
e: { c: 1, r: 3 } // 第 2 列第 4 行结束,也就是 B4
}
]
其中 s 是 start 的简写,e 是 end 的简写,c 是 column 的简写,r 是 row 的简写(这种简写方式主要是为了节约内存,想想 Excel 表里可能有上万个单元格,如果用全称实在是太浪费内存)。
复制 const ws = XLSX.utils.json_to_sheet(
[
{ name: 'Lambda', age: 21, sex: '男' }, // 第一行
{ name: 'Lambda1', age: 21, sex: '男' }, // 第一行
{ name: 'Lambda2', age: 21, sex: '男' } // 第一行
],
{
header: ['name', 'age', 'sex'] // 表头
}
)
ws['!merges'] = [
{
s: { c: 1, r: 1 }, // 第 2 列第 2 行开始,也就是 B2
e: { c: 1, r: 3 } // 第 2 列第 4 行结束,也就是 B4
}
]
项目实现
导入
复制 <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<input type="file" onchange="importFIle(this)" />
<div id="demo"></div>
<script>
/*
FileReader共有4种读取方法:
1.readAsArrayBuffer(file):将文件读取为ArrayBuffer。
2.readAsBinaryString(file):将文件读取为二进制字符串
3.readAsDataURL(file):将文件读取为Data URL
4.readAsText(file, [encoding]):将文件读取为文本,encoding缺省值为'UTF-8'
*/
var wb //读取完成的数据
var rABS = false //是否将文件读取为二进制字符串
function importFIle(obj) {
//导入
if (!obj.files) {
return
}
var f = obj.files[0]
var reader = new FileReader()
reader.onload = function(e) {
var data = e.target.result
if (rABS) {
wb = XLSX.read(btoa(changeData(data)), {
//手动转化
type: 'base64'
})
} else {
wb = XLSX.read(data, {
type: 'binary'
})
}
//wb.SheetNames[0]是获取Sheets中第一个Sheet的名字
//wb.Sheets[Sheet名]获取第一个Sheet的数据
const showData = JSON.stringify(
XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]])
)
// 这里可以根据需要添加转换条件
document.getElementById('demo').innerHTML = showData
}
if (rABS) {
reader.readAsArrayBuffer(f)
} else {
reader.readAsBinaryString(f)
}
}
function changeData(data) {
//文件流转BinaryString
var o = '',
l = 0,
w = 10240
for (; l < data.byteLength / w; ++l)
o += String.fromCharCode.apply(
null,
new Uint8Array(data.slice(l * w, l * w + w))
)
o += String.fromCharCode.apply(null, new Uint8Array(data.slice(l * w)))
return o
}
</script>
导出
复制 <script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
<button onclick="exportData()">导出</button>
<script>
let isExport = false
/**
* 点击导出按钮,请求数据
*/
function exportData() {
// 自定义文件名
const fileName = setFileName(),
// 正在导出
isExport = true
// 请求数据
this.$store
.dispatch('')
.then((res) => {
if (res.code == 200) {
const resData = res.data
let data = [{}]
for (let k in res.data[0]) {
switch (k) {
// json 数据属性名
case 'id':
data[0][k] = '编号'
break
}
}
data = data.concat(resData)
downloadExl(data, fileName)
isExport = false
} else {
isExport = false
}
})
.catch((err) => {
if (err) {
isExport = false
}
})
}
/**
* 导出到Excel
* @param {*} json 导出内容
* @param {*} fileName 文件名称
* @param {*} type 文件类型
*/
function downloadExl(json, fileName, type) {
let keyMap = [] // 获取键
for (let k in json[0]) {
keyMap.push(k)
}
let tmpData = [] // 用来保存转换好的json
json
.map((v, i) =>
keyMap.map((k, j) =>
Object.assign(
{},
{
v: v[k],
position:
(j > 25 ? this.getCharCol(j) : String.fromCharCode(65 + j)) +
(i + 1)
}
)
)
)
.reduce((prev, next) => prev.concat(next))
.forEach(function(v) {
tmpData[v.position] = {
v: v.v
}
})
let outputPos = Object.keys(tmpData) // 设置区域,比如表格从A1到D10
let tmpWB = {
SheetNames: ['sheet'], // 保存的表标题
Sheets: {
sheet: Object.assign(
{},
tmpData, // 内容
{
'!ref': outputPos[0] + ':' + outputPos[outputPos.length - 1] // 设置填充区域
}
)
}
}
let tmpDown = new Blob(
[
this.s2ab(
XLSX.write(
tmpWB,
{
bookType: type === undefined ? 'xlsx' : type,
bookSST: false,
type: 'binary'
} // 这里的数据是用来定义导出的格式类型
)
)
],
{
type: ''
}
) // 创建二进制对象写入转换好的字节流
const outFile = document.createElement('a')
var href = URL.createObjectURL(tmpDown) // 创建对象超链接
outFile.download = fileName + '.xlsx' // 下载名称
outFile.style.display = 'none'
outFile.href = href // 绑定a标签
document.body.appendChild(outFile)
outFile.click() // 模拟点击实现下载
setTimeout(function() {
// 延时释放
URL.revokeObjectURL(tmpDown) // 用URL.revokeObjectURL()来释放这个object URL
document.body.removeChild(outFile)
}, 100)
}
/**
* 字符串转字符流
* @param {String} s
*/
function s2ab(s) {
var buf = new ArrayBuffer(s.length)
var view = new Uint8Array(buf)
for (var i = 0; i !== s.length; ++i) {
view[i] = s.charCodeAt(i) & 0xff
}
return buf
}
/**
* 将指定的自然数转换为26进制表示。映射关系:[0-25] -> [A-Z]。
* @param {Number} n
*/
function getCharCol(n) {
let s = ''
let m = 0
while (n > 0) {
m = (n % 26) + 1
s = String.fromCharCode(m + 64) + s
n = (n - m) / 26
}
return s
}
// 设置文件名 eg: 2020-02-02
function setFileName() {
let str = ''
const date = new Date()
str += `${date.getFullYear()}-${
date.getMonth() + 1 > 10 ? date.getMonth() + 1 : '0' + date.getMonth()
}-${date.getDate() > 10 ? date.getDate() : '0' + date.getDate()}`
return str
}
</script>