分针网——每日分享:nodejs导出excel实战

本文转载:http://www.f-z.cn/id/268

我们都知道nodejs的内存由于v8内存分配机制的原因十分有限

64位系统也只能占1.4G左右, 因此当我们要生成或者读取大文件的时候内存的吃紧会给我们造成极大的困扰, 遇到这样的情况Node给了我们一个很好的解决方法 stream

简单的了解一下流

流是数据的集合 —— 就像数组或字符串一样。区别在于流中的数据可能不会立刻就全部可用,并且你无需一次性地把这些数据全部放入内存。这使得流在操作大量数据或是数据从外部来源逐段发送过来的时候变得非常有用

管道与流的结合在Linux中运用得非常多, 也使得linux能够通过 pipe 组合多个命令实现复杂的功能, 其实通俗一点来说, stream 就是把我们需要一口气吃下的东西分成多次按量的吃下去, 避免一口气吃撑, 在吃的过程中, 我们甚至可以边 ”吃“ 边 ”排“(transform), 使我们的身体能够保持一个均衡低负荷的状态, 同时, 也就是保证node进程内存不会太吃紧的条件.

nodejs中的流

因为先天的原因, 流在node中通常被我们用来处理大文件, 甚至说在nodejs的各类模块中均采用了 stream

在server中的 http request 是可读流(Readable Stream), http response是一个可写流(Writable Stream), 这也就是为什么我们通常在request中读取client传回的值, 而通过response写入数据返回

fs则是一个可读可写的流(Duplex Stream), 我们可以对一个文件进行写入和读取的操作以上这些

模块只是nodejs中流应用的一小部分场景, 在其官方API中流的类型和解释则是

Readable - streams from which data can be read (for example fs.createReadStream()).

Writable - streams to which data can be written (for example fs.createWriteStream()).

Duplex - streams that are both Readable and Writable (for example net.Socket).

Transform - Duplex streams that can modify or transform the data as it is written and read (for example zlib.createDeflate()).

没错也就是四种类型, 其中transform继承自duplex

流的典型例子

在nodejs中读取和写入大文件通常是流应用得最广泛、最重要的场景之一, 这也是写这篇博客的原因之一, 因此以一个简单的文件读取的例子作为我们了解流的小Demo

读取一个大小为160M的文件, 采用fs.readFile()方法

const fs = require(‘fs‘)

const server = require(‘http‘).createServer()

server.on(‘request‘, (req, res) => {

fs.readFile(‘./Demo.txt‘, (err, result)=>{

if(err){

throw err

}

res.end(result)

})

})

server.listen(3000)

启动这样一个node进程, 内存占用约为8m, 当我们执行请求 curl localhost:3000时, 会发现内存暴增到160m, 差不多是我们读取的文件的大小

采用流读取的方式

const fs = require(‘fs‘)

const server = require(‘http‘).createServer()

server.on(‘request‘, (req, res) => {

let data = fs.createReadStream(‘./Demo.txt‘)

data.pipe(res)

})

server.listen(3000)

可以看见内存基本稳定在11m, 这证明了采用读取流方式在内存上给我们带来了极大的优化, 当然这只是一个小Demo, 我们可以尝试着去读取1G 甚至超过2G的文件, fs.readFile()的方式可能就会突破内存的限制而导致进程crash掉, 假如在生产环境中, 请求多并发相对较高的环境下, 这种方式是行不通的

通过流的方式导出Excel文件

背景

需求是希望能够将项目下的所有分组以一个项目excel文件包含多个分组sheet导出

如果是导出csv文件, 我们完全可以通过流的方式导出, 但是在excel中, 由于文件类型的限制, 我们很难把excel通过流的方式直接导出, 最终选择了exceljs

编码格式

Node.js支持 ascii 、utf8、base64、binary 编码方式,不支持 utf-8 + BOM(字节顺序标记) 格式, 而微软给utf8加了BOM头(在windows下不管是utf8还是utf16(Unicode)都有BOM, utf16自带BOM头), 因此excel会出现中文乱码, 因此我们需要在文件头加上三个标识字节, 由于utf8对应的BOM是 EF BB BF 传送门, 因此这么实现: res.write(Buffer.from(‘\xEF\xBB\xBF‘, ‘binary‘))

既然是导出excel文件, 那文件内容(‘Content-Type’)是什么, 最终在StackOverflow上找到了答案, 传送门StackOverflow, 文件类型有专属的Office套件: ‘Content-Type‘: ‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet‘

实现

使用exceljs最重要的原因是支持伪流

The interface to the streaming workbook and worksheet is almost the same as the document versions with a few minor practical differences:

Once a worksheet is added to a workbook, it cannot be removed.

Once a row is committed, it is no longer accessible since it will have been dropped from the worksheet.

unMergeCells() is not supported.

从文档的说明来看, exceljs是支持将每一个sheet写入excel document中, 然后立马pipe出去, 这是相对可行的一个方案, 但是当每个sheet数据量过大怎么办? 同样会导致我们内存的暴增, 导致进程的crash, 而源码也确实是这样实现了exceljs Stream, 所以这是一个伪流, 它并没有解决我们的根本问题, 但是很大程度的舒缓了我们遇到的问题, 问题的根本原因还是在excel中我们需要区别每一个sheet, 导致我们不能持续的进行读写, 还是需要先把这部分的数据先读取到内存中进行写入。 这也导致我们在业务代码中需要添加一些数据量上的限制针对每一个sheet

部分代码, Demo.coffee

res.set(‘Content-Type‘,‘application/vnd.openxmlformats-officedocument.spreadsheetml.sheet‘)

res.setHeader(‘Content-Disposition‘, filename)

res.set(‘Set-Cookie‘, ‘fileDownload=true; path=/‘)

excel = require(‘exceljs‘)

options = {

stream: res,

useStyles: true,

useSharedStrings: true

}

workbook = new excel.stream.xlsx.WorkbookWriter(options)

worksheet = workbook.addWorksheet(tasklist.title)

headers = [

{ header: ‘csv.content‘, width: 15 }

{ header: ‘csv.ancestor‘, width: 10 }

{ header: ‘csv.note‘, width: 20 }

{ header: ‘csv.priority‘, width: 10 }

{ header: ‘csv.executor‘, width: 10 }

{ header: ‘csv.startDate‘, width: 20 }

{ header: ‘csv.dueDate‘, width: 20 }

{ header: ‘csv.creator‘, width: 20 }

{ header: ‘csv.created‘, width: 20 }

{ header: ‘csv.isDone‘, width: 10 }

{ header: ‘csv.accomplished‘, width: 20 }

{ header: ‘csv.tasklist‘, width: 10 }

{ header: ‘csv.stage‘, width: 10 }

{ header: ‘csv.delayDays‘, width: 10 }

{ header: ‘csv.delayed‘, width: 10 }

{ header: ‘csv.totaltime‘, width: 10 }

{ header: ‘csv.usedtime‘, width: 10 }

{ header: ‘csv.tag‘, width: 10 }

]

// 生成标题头

worksheet.columns = headers

rows = [

[5,‘Bob‘,new Date()], // row by array

{id:6, name: ‘Barbara‘, dob: new Date()}

]

worksheet.addRows rows

worksheet.commit()

workbook.commit()

总结

导出excel仅仅是stream一个小小的体现和应用, 真正要掌握它的整个实现背景和场景应用还是需要我们去实践以及查看源码的实现, 这里仅仅是一个读、写的单向实现, 可以去尝试transform从一个文件读取数据进行相关处理之后写入一个新的文件, 这样更能感受它给我们带来的性能上优化和简便, 以上

时间: 2024-10-10 21:35:18

分针网——每日分享:nodejs导出excel实战的相关文章

分针网—每日分享: 根据屏幕大小,加载不同大小的图片

引言 今天要介绍的东西,很简单,但是对于前端响应式的时候是个很重要的知识: 我们在用bootstrap这类前端框架时, 虽然页面局部通过media query实现了,页面始终无滚动条,响应式页面. 但是,bootstrap里面的img-responsive类只是通过设置图片100%, 并没有真正的实现在手机上和电脑端加载不同大小的图片. 代码其实很简单 <!DOCTYPE html> <html lang="en"> <head> <meta 

分针网—每日分享: 怎么轻松学习JavaScript

js给初学者的印象总是那么的"杂而乱",相信很多初学者都在找轻松学习js的途径. 我试着总结自己学习多年js的经验,希望能给后来的学习者探索出一条"轻松学习js之路". js给人那种感觉的原因多半是因为它如下的特点: A:本身知识很抽象.晦涩难懂,如:闭包.内置对象.DOM. B:本身内容很多,如函数库,对象库就一大堆. C:混合多种编程思想. 它里面不但牵涉面向过程编程思想,又有面向对象编程思想,同时,它的面向对象还和别的编程语言(如:C++,JAVA,PHP)不

分针网—每日分享:说一说 React 和 Redux 你知道或者不知道的一些事情

本文介绍一下自己在使用React和Redux过程中的一些思考,主要面向初学者. 1. 为什么要有redux 传统前端开发中,把模板和功能逻辑分开作为一种最佳实践,React采用了不同的思路,通过组件把模板和逻辑组合在一起.但是React也并不是一个完整的组件化框架,其组件化只是主要集中在展示层面,如果要构建复杂的应用,在React component中放置太多的逻辑代码,不仅组件化的初衷复用性会降低,从代码维护的角度看也不合理. Flux是Facebook提出的一种前端架构模式,通过Flux的数

分针网——每日分享: jquery选择器的用法

jQuery选择器是jQuery库的一大特色,用这些选择器不但可以省去繁琐的JavaScript 书写方式,还可以节省时间和效率,正是有这些jQuery选择器,才让我们更容易的操作JavaScript的dom. 1. 基本选择器 ·#id 根据给定的ID匹配一个元素.例如:$("#id") ·element 根据给定的元素名匹配所有元素.例如:$("div") ·.class 根据给定的类匹配元素.例如:$(".style1"); ·* 匹配所有

分针网—每日分享:js刷新页面方法大全

如何实现刷新当前页面呢?借助js你将无所不能. 1,reload 方法,该方法强迫浏览器刷新当前页面. 语法:location.reload([bForceGet]) 参数: bForceGet, 可选参数, 默认为 false,从客户端缓存里取当前页.true, 则以 GET 方式,从服务端取最新的页面, 相当于客户端点击 F5("刷新") 2,replace 方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,你不能通过"前进

分针网——每日分享:登录之后,在其他页面怎么判断是否已经登录

本文转载:http://www.f-z.cn/id/261 一.背景介绍 登录功能,是前端经常要完成的需求之一. 一个 网站有很多的操作是必须要用户登陆才能进行操作的 那么如何进行登录判断? 需要用到什么样的属性或者方法? 有什么地方的细节需要注意? 以上这些,都是本次小课堂要讲解的重点! 二.知识剖析 如果想要实现登陆判断,就要有一个判断的依据. 首先,这个依据在我们访问网站的过程中不会失效, 其次,这个依据要能存储一定的信息,以提供必要的判断, 同时满足这两个条件有Storage和cooki

分针网——每日分享:10个程序员常用的代码简写技术

更多文章:www.f-z.cn 今天小编我给大家整理了一份10个程序员常用的代码简写技术,看懂一种是入门,全懂就是大神,你能知道几个呢? 1.三元操作符 当想写if...else语句时,使用三元操作符来代替. const x = 20;let answer;if (x > 10) { 简写: const answer = x > 10 ? 'is greater' : 'is lesser'; 也可以嵌套if语句: const big = x > 10 ? " greater

分针网—每日分享:mongoose对查询结果进行排序

http://www.fenzhen.cc/id/177 前面的学习已经可以刷出数据了,不过通过循环取出来后,发现并不是按照想象中的按先后顺序列出来的,而是按照数据在数据库中存放的顺序刷出来的. 如图,MongoDB存储数据并没有按照我添加数据的先后顺序,而是按照了author这个字段来的,因此前台刷出的数据也是安装author来的. 那么如何让前台通过时间来排序呢? 解决方法如下: MyModel.find(condition, fields, {sort: [['_id', -1]]}, c

分针网—每日分享:H5 页面高级字体应用实践

前端开发 背景 最近在开发一个 H5 活动页快速搭建平台,可以通过拖拽编辑图片,文字等元素组件,快速搭建出一个移动端的活动页面,基本交互和成品效果类似 PPT 软件.这类活动大量在微信等平台上传播,其中会包含各种动画和特效,而各类高级艺术字体(如:方正兰亭黑,方正彩云,方正大草,方正剑体等)的应用也非常广泛. 之前用户只能通过 ps 等软件将文字转化为图片再贴到平台上使用.使用成本很高,修改,调试都非常不便,而且图片占用的资源也比较多,为了降低用户的使用成本,基于一站式搭建的理念,我们需要将高级