問題
使用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)