问题
使用node-xlsx处理excel一次最多能处理30M的文件,所以来个80M的话就要手动拆成3个文件,这看起来太蠢了,需要换一个能处理大excel文件的库。将界面优化成这样子:
思路
经过测试,js最受欢迎的库js-xlsx也不能处理80M的excel文件,所以可能需要使用另外一种语言的库,如果使用另外一种语言,需要考虑怎么选语言,上手难度,开发成本,还有就是是否能嵌入到electron的桌面应用里,如果能嵌入,该怎么进行数据通信等等问题。
- 在electron里支持用node开辟一个子进程去运行其他语言打包出来的可执行文件,这样子就可以实现electron里跑其他语言写的程序
- 经过了解,python和go都比较容易上手,而且python有xlsxWriter库,go有excelize库都可以处理超大的excel文件,都是解析出来一个数组,后面处理数组就行。 因为python是解释型语言,打包成可执行文件时会连解释器一起打包,所以就算只写了几行代码,打包出来的包也会是几百兆,go是编译型语言,编译打包出来的包比较小,嵌入electron以后比较轻量,所以选用了go语言
- 因为还不熟悉新语言语法,所以做一个tcp的socket通信,将go进程解析出来的数组传到node进程,处理数组生成文件部分就可以用之前写好的node部分逻辑进行处理了
- 之前node进程里只处理了生成文件,如果需要用go的excelize库生成excel格式的话还需要将放置生成文件的文件夹路径返回给go进程,go再批量处理excel文件格式
go部分
主要流程
- 创建一个tcp的socket
func createServer() {
// 建立socket,监听端口 第一步:绑定端口
netListen, err := net.Listen("tcp", "127.0.0.1:9800")
CheckError(err)
// defer延迟关闭改资源,以免引起内存泄漏
defer netListen.Close()
Log("Waiting for clients")
for {
conn, err := netListen.Accept() // 第二步:获取连接
if err != nil {
continue // 出错退出当前一次循环
}
Log(conn.RemoteAddr().String(), " tcp connect success")
// 这句代码的前面加上一个 go,就可以让服务器并发处理不同的Client发来的请求
go handleConnection(conn) // 使用goroutine来处理用户的请求
}
}
func handleConnection(conn net.Conn) {
buffer := make([]byte, 2048)
for { // 无限循环
n, err := conn.Read(buffer) // 第三步:读取从该端口传来的内容
words := "ok" // 向链接中写数据
conn.Write([]byte(words))
if err != nil {
Log(conn.RemoteAddr().String(), " connection error: ", err)
return // 出错后返回
}
tcpData := string(buffer[:n]) // 接收到的数据
}
}
- 接收待处理excel文件的绝对路径,返回解析出来的数组给node进程处理并生成拆分好的excel文件
- 接收放置输出文件的文件夹绝对路径,批量处理里面的excel文件格式
遇到的坑
- go一些库比较新的版本需要在https://pkg.go.dev/搜,在github里直接搜名字可能搜不到想要的版本链接,比如excelize这个库,直接在github搜是找不到v2的链接的
- 静态语言写起来比动态语言麻烦挺多,走到哪一步都是需要确定类型的
- 在和node进程通信的时候要适当地加time.Sleep(),不然调用conn.Write()的时候会使本来应该两次传递的消息变成了一次
node部分
主要流程
- 在electron的主进程里开辟一个子进程来运行go程序
const isWin = /^win/.test(process.platform)
console.log(process.platform)
const path = require('path')
let pyProc = null
const createPyProc = () => {
let port = '4242'
let script = path.resolve(__dirname, 'go', isWin ? 'testGo.exe' : 'testGo')
if (process.env.NODE_ENV === 'production') {
script = path.join(process.resourcesPath, 'app.asar.unpacked/go', isWin ? 'testGo.exe' : 'testGo')
}
console.log(script)
pyProc = require('child_process').execFile(script, [port]) // 开辟一个子进程运行go打包出来的可执行程序
if (pyProc != null) {
console.log('child process success')
}
}
const exitPyProc = () => {
pyProc.kill()
pyProc = null
}
app.on('ready', createPyProc)
app.on('will-quit', exitPyProc)
- 在界面点击开始处理后,开始与go进程进行tcp通信
async () => {
let tcpData = ''
const client = net.connect({ port: 9800 })
client.write(JSON.stringify(this.sourcePathList))
client.on('data', async data => {
tcpData += data.toString()
if (tcpData.startsWith('[')) {
// 第一个通信会回抛数组
if (tcpData.endsWith('数据传输结束标记')) {
data = JSON.parse(tcpData.replace(/数据传输结束标记$/, ''))
for (let i = 0; i < data.length; i++) { // 遍历循环用node处理每个表的数据,拆分并生成文件
await splitXlsx(
data[i],
i + 1,
data.length,
this.folderPath,
this.log,
this.sourcePathList.length
).catch(e => {
this.log.error = e
this.isLoading = false
})
}
tcpData = ''
// 处理完以后,传文件夹过去给go批量生成xlsx格式
client.write(this.folderPath)
}
} else {
// 之后的通信都是回抛处理状态
if (tcpData.startsWith('处理中断')) {
this.log.error = tcpData
this.isLoading = false
} else {
this.log.text = tcpData
if (this.log.text.startsWith('处理结束')) {
this.isLoading = false
}
}
tcpData = ''
}
})
}
- 待处理文件绝对路径传给go进程,go进程解析出一个大数组后传回给node进程处理拆分数组并循环生成新的excel文件,最终将输出文件所在文件夹绝对路径传给go进程,go进程批量处理excel文件的样式格式和打印格式
遇到的坑
- 因为需要运行在win和mac系统,所以需要根据系统环境来选择go的可执行程序文件
- 在打包electron安装包的时候,需要将go的可执行程序文件打包进去,我用的是electron-builder打包,需要在package.json里增加打包配置项
这样子就相当于直接复制这些不需打包的静态文件去到安装包里的app.asar.unpacked文件夹里面,在生产环境取文件路径时可以这样取"build": { // ... "extraResources": [ { "from": "src/main/go", // go文件夹里有go的可执行程序文件 "to": "app.asar.unpacked/go" } ], // ... }
win/mac系统都是可以这样配置的if (process.env.NODE_ENV === 'production') { script = path.join(process.resourcesPath, 'app.asar.unpacked/go', isWin ? 'testGo.exe' : 'testGo') }
- 由于go是解析完全部80M的文件,解析出来的数组超级大,在tcp中传数据缓冲区会不够用,所以数据变成分块传输,这样node进程就无法确定数据传输完毕的时机,需要在go进程传输的数据上加上结束标识,这样在node进程就需要自己拼接分块传输的数据,并且识别结束标识。
- 在excel中的时间戳和js计算出来的时间戳是不一样的
两者的换算公式如下:
js时间戳转excel时间戳
excel时间戳转js时间戳const excelTimeNum = (Number(new Date()) / 1000 + 8 * 3600) / 86400 + 70 * 365 + 19
const jsTimeNum = new Date(((excelTimeNum - 19 - 70 * 365) * 86400 - 8 * 3600) * 1000)