前言
近年来做的很多后台管理系统对文件这方面要求比较高,尤其是Excel这种非常实用的工具,
公司的架构是前端用三大框架(Angular React Vue),用nodejs做中间层,调用后端的SOA服务封装出应用层的接口,
在开发过程中生成文件很多时候被列为是应用层的功能,所以由前端去实现。。。(苦逼的前端)
开发过程中我们做了很多方面的尝试和改进,这里做个总结,汇总一下做过的文件处理
1.前端传入数据由node生成文件,再进行下载
举个栗子,之前的angular项目中我自己封装了一个downloadUtil,代码如下
angular.module('core').factory('downloadUtil', ['$http', function ($http) {
return {
/**
* 下载excel的方法
* @param downloadList 下载内容列表
* @param downloadName Excel名
* @param flag 用于导出阻塞的对象(一定要是Object)
* @param fieldName 导出阻塞对象中的字段名(字符类型) 如'downloadingExcel' 和第三个参数一起使用(flag[fieldName])
*/
downloadExcel: function (downloadList, downloadName, flag, fieldName) {
$http({
url:'/api/product/download',
method: "POST",
data: {'data': {list: downloadList, orderNo: downloadName}}
}).error(function () {
flag[fieldName] = false;
}).then(function (resp) {
flag[fieldName] = false;
window.location = '/api/product/downloadFile/' + resp.data;
},function (response) {
});
},
/**
* 下载csv的方法
* @param downloadList 下载内容列表
* @param downloadName CSV名
* @param flag 用于导出阻塞的对象(一定要是Object)
* @param fieldName 导出阻塞对象中的字段名(字符类型) 如'downloadingExcel' 和第三个参数一起使用(flag[fieldName])
*/
downloadCSV: function (downloadList, downloadName, flag, fieldName) {
$http({
url:'/api/product/downloadCSV',
method: "POST",
data: {'data': {list: downloadList, orderNo: downloadName}}
}).error(function () {
flag[fieldName] = false;
}).then(function (resp) {
flag[fieldName] = false;
window.location = '/api/product/downloadFile/' + resp.data;
},function (response) {
});
},
/**
* @param downloadFileName 下载改版,获取到的excel名
*/
downloadFile: function (downloadFileName, flag, fieldName) {
flag[fieldName] = false;
window.location = '/api/product/downloadFile/' + downloadFileName;
}
};
}]);
node端(express)生成excel依赖node-xlsx这个包,先处理好数据,通过node-xlsx这个包的方法转成一个buffer, 将buffer转成文件写入到指定的路径中,最后返回生成的文件名
var xlsx = require('node-xlsx');
var _ = require('lodash');
/**
* 下载excel
* @param req
* @param res
* @param data
*/
exports.handleDownloadFile = function (req, res, next) {
var user = req.user;
var data = req.body.data;
var keyWordList = constant.keyWordList;
if(req.body.data.keyWordList && req.body.data.keyWordList.length){
keyWordList = req.body.data.keyWordList;
}
var title1List = [];
var title2List = [];
var dataList = [];
var tempList = [];
for(var i in data.list[0]) {
var findOne = _.find(keyWordList, {key: i});
if(findOne) {
title1List.push(findOne.key);
title2List.push(findOne.value);
}
}
dataList.push(title1List);
dataList.push(title2List);
_.each(data.list, function (item) {
tempList = [];
_.find(title1List, function (subItem) {
for(var i in item) {
if(subItem === i) {
tempList.push(item[i]);
}
}
});
dataList.push(tempList);
});
var buffer = xlsx.build([{name: "mySheetName", data: dataList}]);
var name = data.orderNo + '_' + moment(new Date()).format('YYYYMMDDHHmm') + '.xlsx';
fs.writeFileSync('./public/output/' + encodeURI(name)||'data', buffer, 'binary');
res.json(encodeURI(name));
};
将文件名返回前端后根据文件名去获取对应的excel文件,需要设置对应的响应头,传输完毕后删除在node生成的excel文件
/**
* 下载excel
* @param req
* @param res
* @param data
*/
exports.downloadExcelFile = function (req, res, data) {
var fileName = encodeURI(req.params.fileName);
var fileToSend = fs.readFileSync('./public/output/' + fileName);
res.setHeader('Content-Type', 'application/vnd.openxmlformats');
res.setHeader("Content-Disposition", "attachment; filename=" + fileName);
res.send(fileToSend);
fs.unlinkSync('./public/output/' + fileName);
};
2.在nodejs用流生成好文件再返回给前端
var Excel = require('exceljs');
/*
**使用流生成excel data: 数据 fileOption: 其他参数(如文件名)
*/
exports.generatedExcelFileUsingStream = function(data, fileOptions){
return new Promise(function (resolve) {
let baseDir = 'app/public/tmp/';
let name = encodeURI(fileOptions.fileName || 'Excel') + moment(new Date()).format('YYYYMMDDHHmm') + '.xlsx';
let path = baseDir + name;
let writeStream = fs.createWriteStream(path);
let options = {
stream: writeStream,
filename: path
};
let workbook = new Excel.stream.xlsx.WorkbookWriter(options);
//生成第一张sheet
let worksheet = workbook.addWorksheet(fileOptions.fileName || 'Excel');
fillWorksheet(worksheet, fileOptions.columns, data).then(resp => {
workbook.commit().then(function () {
console.log('使用流生产excel完成!');
resolve(name);
});
});
});
};
exports.generatedCSVFileUsingStream = function (data, fileOptions) {
return new Promise(function (resolve) {
let baseDir = 'app/public/tmp/';
let CSVName = encodeURI(fileOptions.fileName) + '_' + moment(new Date()).format('YYYYMMDDHHmm') + '.csv';
let filePath = baseDir + CSVName;
let writeStream = fs.createWriteStream(filePath);
let workbook = new Excel.Workbook(fileOptions.fileName);
let worksheet = workbook.addWorksheet(fileOptions.fileName);
if (fileOptions.columns && fileOptions.columns.length) {
fileOptions.columns[0].header = '\ufeff' + fileOptions.columns[0].header;
}
fillWorksheet(worksheet, fileOptions.columns, data).then(resp => {
workbook.csv.write(writeStream, {sheetName: fileOptions.fileName}).then(function () {
resolve(CSVName);
});
writeStream.on('finish', function () {
// resolve(CSVName);
});
});
});
};
/**
* 填充工作表 包括设置列属性(不含英文头)和填充数据
* @param worksheet 工作表对象
* @param columnKey 列属性数组 数组元素为字符串 ['salesOrderNo']
* @param list 所有的数据
*/
function fillWorksheet(worksheet, columnsOptions, list) {
return new Promise((resolve, reject) => {
worksheet.columns = columnsOptions;
let j = 0;
for(; j < list.length; j++) {
let obj = {};
for(let i = 1; i <= worksheet.columns.length; i++) {
obj[worksheet.getColumn(i).key] = list[j][worksheet.getColumn(i).key];
}
worksheet.addRow(obj).commit();
}
resolve({endIndex: j, obj: list[j - 1]});
worksheet.commit();
});
}
使用方法
let options = {
columns: [
{ key: 'couponNo', header: '券号' },
{ key: 'orderAmount', header: '吊牌金额' },
{ key: 'orderItemQuantity', header: '订单件数' },
{ key: 'orderDiscount', header: '订单折扣' },
{ key: 'unionId', header: 'unionId' },
],
fileName: '券列表',
};
let {result, optionsResult} = mapDownloadData(resp, options)
let downloadResp = await commonApi.generatedExcelFileUsingStream(result.items || [], optionsResult);
3.由第三方生成文件返回
nodejs不适合做CPU密集型的行为,但是业务上会需要一些需要很复杂很大的报表的情况,这时候就不适合前端来做,可以由第三方例如大数据方面提供生成好的文件,由前端去下载,按我接触的项目来讲,例如大数据提供的是一个内网的文件路径,为了兼容外网也可以下载,所以想到了再应用上加一层代理,走公网传输
以做过的angular项目为例
// 返回文件下载url的情况
$http({
url: '/dc/xx/xxx',
method: "POST",
headers: {"Content-Type": "text/plain; "},
data: param
}).success(function (resp) {
if (resp && resp.code === 200 && resp.data && _.endsWith(resp.data, 'xlsx')) {
// window.location = resp.data;
var arrUrl = resp.data.split("//");
if (arrUrl.length > 1) {
var start = arrUrl[1].indexOf("/");
var relUrl = arrUrl[1].substring(start);//stop省略,截取从start开始到结尾的所有字符
window.location = '/file' + relUrl;
}
}
$scope.downloadFlag.isDownloadingDailyExcel = false;
}).then(function (res) {
$scope.downloadFlag.isDownloadingDailyExcel = false;
}).catch(function (error) {
$scope.downloadFlag.isDownloadingDailyExcel = false;
});
nginx配置
location /dc/ {
rewrite ^/dc/(.*)$ /$1 break;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_pass http://xx.x.xx.xxx:xx;
}
location /file/ {
rewrite ^/file/(.*)$ /$1 break;
proxy_pass http://xx.x.xx.xxx;
proxy_connect_timeout 600s;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
4.后端返回一个blob或者流的情况
前端接收好数据,然后直接生成文件,文件大小未知,直到所有数据传输完才知道大小,这种方式的话用户不会看到下载的过程,用户等待时间会稍微长一点,交互也相对不好做,但是中间少了生成临时文件的耗时
以做过的react项目为例
import axios from 'axios';
// 创建实例时设置配置的默认值
var instance = axios.create({
baseURL: ''
});
instance.post('/center-data/data/xx/xx/export', downloadParams, {
headers: {
'content-type': 'application/json',
},
'withCredentials': true,
responseType: 'blob', //--设置请求数据格式
}).then(res => {
if (res && res.data) {
// let blob = new Blob([res.data], { type: 'application/vnd.ms-excel;charset=utf-8' });
let timeStamp = moment().format('YYYY-MM-DD HH:mm');
this.downFile(res.data, `列表${timeStamp}.xlsx`);
}
this.setState({isDownloadingCouponProxy: false});
}).catch(err => {
this.setState({isDownloadingCouponProxy: false});
});
中间要设置一层代理,这里就不做展示了