KevinChen 发表于 2021-2-27 22:19:57

专题:服务端Node.js运行SpreadJS处理Excel的方案探讨(一)

本帖最后由 KevinChen 于 2021-2-28 02:41 编辑

在我们实际应用中,很可能会遇到这样的需求:批量绑定数据源并导出Excel、批量修改大量的Excel内容及样式、服务端批量打印以及生成PDF文档等。对于有这方面需求的用户,实际上我们已经提供了非常成熟的解决方案:GcExcel。GcExcel已经能够在完成以上需求场景的同时,在性能和稳定性上也达到了同类产品中一流的水平。的确,JavaScript确实是一门能力非常强大的语言,尤其是自2009年Node.js横空出世一来,JavaScript也已经被用在了更丰富的场景中,甚至业界流传着这样一句话:
Atwood's Law: Any application that can be written in JavaScript, will eventually be written in JavaScript.

任何可以用 JavaScript 来写的应用,最终都将用 JavaScript 来写。
但是,大家可能不知道的是,这句话还有下半句:
Reg Braithwaite: The strength of JavaScript is that you can do anything. The weakness is that you will.

JavaScript 的优点是可以写任何东西,缺点是你真的会用它去写这些东西。
我们的技术团队在跟国内外各行各业SpreadJS的用户交流中,发现曾有过很多的用户想用Node.js来实现在服务端运行SpreadJS。我们非常高兴能有这么多热爱前端技术的用户团队有这样的尝试和交流,我们也对Node.js与SpreadJS结合使用做了深入调研,甚至实现了一个场景Demo。但遗憾的是,即使用了一些hack技术,Node.js和SpreadJS结合使用的局限性仍然非常大,导致我们虽然可以拿来在Demo或者原型、玩具小项目中实现一些有趣的功能,但无论是开发和调试的难度,以及稳定性和性能都无法用于生产环境中。

本系列会就这个话题做深入探讨,以帮助大家在技术选型方面少走弯路。本系列计划做三篇,分别介绍如何在Node.js中运行SpreadJS,与GcExcel的性能对比,以及其它的技术局限性探讨。本文作为第一篇,先为大家介绍如何把Node.js与SpreadJS结合使用的问题。以下内容来自我的美国同事Kevin的博客(是的,叫Kevin的都是人才:P),后续两篇为版主自己撰写。
注意:SpreadJS不对Node.js环境做官方兼容性支持,本系列专题仅作技术方面的讨论验证!
注意:SpreadJS不对Node.js环境做官方兼容性支持,本系列专题仅作技术方面的讨论验证!
注意:SpreadJS不对Node.js环境做官方兼容性支持,本系列专题仅作技术方面的讨论验证!

第一篇:如何从Node.js应用程序生成Excel电子表格

在我们实际应用中,有时可能需要从Node.js应用程序生成Excel文件。您可能需要从数据库或Web服务获取数据,然后输出到Excel文件以进行进一步的报告或分析。 SpreadJS使这一切成为可能,这不需要服务器环境上预装任何Excel的依赖组件。 Node.js是流行的事件驱动的JavaScript Runtime,通常用于创建网络应用程序。它可以同时处理多个连接,并且不像其他大多数模型那样依赖线程。 在本教程中,我们将使用Spread.Sheets收集用户输入的信息,并将其自动导出到Excel文件——全部在您的Node.js应用程序中实现。
该项目的示例zip参考附件。
SpreadJS和Node.js入门首先,我们需要安装Node.js以及Mock-Browser,BufferJS和FileReader,如下链接所示:Installing Node.js viaPackage ManagerMock-BrowserBufferJSFileReader 我们将使用Visual Studio创建应用程序。打开Visual Studio后,使用JavaScript> Node.js>Blank Node.js控制台应用程序模板创建一个新应用程序。这将自动创建所需的文件并打开“ app.js”文件,这是我们将要更改的唯一文件。对于BufferJS库,您需要下载该软件包,然后通过导航到项目文件夹(一旦创建)并运行以下命令,将其手动安装到项目中:npm install 安装完成后,您可能需要打开项目的package.json文件并将其添加到“ dependencies”部分。文件内容应如下所示:{
"name": "spread-sheets-node-jsapp",
"version": "0.0.0",
"description": "SpreadSheetsNodeJSApp",
"main": "app.js",
"author": {
   "name": "admin"
},
"dependencies": {
   "FileReader": "^0.10.2",
   "bufferjs": "1.0.0",
   "mock-browser": "^0.92.14"
}
}
在此示例中,我们将使用Node.js的文件系统模块。我们可以将其加载到:var fs = require('fs')
为了将SpreadJS与Node.js结合使用,我们可以加载已安装的Mock-Browser:var mockBrowser =require('mock-browser').mocks.MockBrowser
在加载SpreadJS脚本之前,我们需要初始化模拟浏览器。初始化我们稍后在应用程序中可能需要使用的变量,尤其是“ window”变量:global.window =mockBrowser.createWindow()
global.document = window.document
global.navigator = window.navigator
global.HTMLCollection =window.HTMLCollection
global.getComputedStyle =window.getComputedStyle
初始化FileReader库:var fileReader = require('filereader');
global.FileReader = fileReader;
使用SpreadJS npm包SpreadJS Sheets和ExcelIO包需要被添加到项目中。您可以通过右键单击解决方案资源管理器的“ npm”部分并将它们添加到您的项目中,然后选择“安装新的NPM软件包”。您应该能够搜索“ GrapeCity”并安装以下2个软件包:@grapecity/spread-sheets
@grapectiy/spread-excelio
将SpreadJS npm软件包添加到项目后,正确的依赖关系将被写入package.json:{
"name": "spread-sheets-node-jsapp",
"version": "0.0.0",
"description": "SpreadSheetsNodeJSApp",
"main": "app.js",
"author": {
   "name": "admin"
},
"dependencies":{
   "@grapecity/spread-excelio": "^11.2.1",
   "@grapecity/spread-sheets": "^11.2.1",
   "FileReader": "^0.10.2",
   "bufferjs": "1.0.0",
   "mock-browser": "^0.92.14"
}
}
现在我们需要在app.js文件中引入它:var GC =require('@grapecity/spread-sheets')
var GCExcel =require('@grapecity/spread-excelio');
使用npm软件包时,还需要设置许可证密钥:GC.Spread.Sheets.LicenseKey ="<YOUR KEY HERE>"
在这个特定的应用程序中,我们将向用户显示他们正在使用哪个版本的SpreadJS。为此,我们可以引入package.json文件,然后引用依赖项以获取版本号:var packageJson =require('./package.json')
console.log('\n** Using Spreadjs Version"' + packageJson.dependencies["@grapecity/spread-sheets"] +'" **')
将Excel文件加载到您的Node.js应用程序中我们将加载现成的Excel模板文件,用来从用户那里获取数据。接下来,将数据放入文件中并导出。在这种情况下,文件是用户可以编辑的**。 首先初始化工作簿和ExcelIO变量:var wb = new GC.Spread.Sheets.Workbook();
var excelIO = new GCExcel.IO();
让我们在读取文件时将代码包装在try / catch块中。然后,我们可以初始化变量“ readline”——本质上是一个库,可让您读取用户输入到控制台的数据。接下来,我们将存储到一个JavaScript数组中,可用来轻松填写Excel文件:// Instantiate the spreadsheet and modifyit
console.log('\nManipulatingSpreadsheet\n---');
try {
   var file = fs.readFileSync('./content/billingInvoiceTemplate.xlsx');
   excelIO.open(file.buffer, (data) => {
       wb.fromJSON(data);
       const readline = require('readline');
       var invoice = {
            generalInfo: [],
            invoiceItems: [],
            companyDetails: []
       };
   });
} catch (e) {
   console.error("** Error manipulating spreadsheet **");
   console.error(e);
}
收集用户输入
上图显示了我们正在使用的Excel文件。我们要收集的第一个信息是一般**信息。我们可以在excelio.open调用中创建一个单独的函数,以在控制台中提示用户需要的每一项。我们可以创建一个单独的数组,将数据保存到每个输入之后,然后在我们拥有该节的所有输入之后。将其推送到我们创建的invoice.generalInfo数组中:fillGeneralInformation();
function fillGeneralInformation() {
   console.log("-----------------------\nFill in InvoiceDetails\n-----------------------")
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var generalInfoArray = [];
   rl.question('Invoice Number: ', (answer) => {
       generalInfoArray.push(answer);
       rl.question('Invoice Date (dd Month Year): ', (answer) => {
         generalInfoArray.push(answer);
            rl.question('Payment Due Date (ddMonth Year): ', (answer) => {
                generalInfoArray.push(answer);
                rl.question('Customer Name: ',(answer) => {
                   generalInfoArray.push(answer);
                  rl.question('CustomerCompany Name: ', (answer) => {
                     generalInfoArray.push(answer);
                        rl.question('Customer Street Address:', (answer) => {
                           generalInfoArray.push(answer);
                           rl.question('Customer City, State, Zip (<City>, <State Abbr><Zip>): ', (answer) => {
                              generalInfoArray.push(answer);
                               rl.question('Invoice Company Name: ', (answer) => {
                                 generalInfoArray.push(answer);
                                 rl.question('Invoice Street Address: ', (answer) => {
                                       generalInfoArray.push(answer);
                                       rl.question('Invoice City, State, Zip (<City>, <State Abbr><Zip>): ', (answer) => {
                                          generalInfoArray.push(answer);
                                           rl.close();
                                           invoice.generalInfo.push({
                                             "invoiceNumber": generalInfoArray,
                                             "invoiceDate": generalInfoArray,
                                             "paymentDueDate": generalInfoArray,
                                             "customerName": generalInfoArray,
                                             "customerCompanyName": generalInfoArray,
                                             "customerStreetAddress": generalInfoArray,
                                             "customerCityStateZip": generalInfoArray,
                                             "invoiceCompanyName": generalInfoArray,
                                             "invoiceStreetAddress": generalInfoArray,
                                             "invoiceCityStateZip": generalInfoArray,
                                          });
                                           console.log("General Invoice Information Stored");
                                           fillCompanyDetails();
                                        });
                                    });
                               });
                            });
                        });
                  });
                });
            });
       });
   });
}

在该函数中,我们称为“ fillCompanyDetails”,我们将收集有关公司的信息以填充到工作簿的第二张表中。该功能将与以前的功能非常相似:function fillCompanyDetails() {
   console.log("-----------------------\nFill in CompanyDetails\n-----------------------")
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var companyDetailsArray = []
   rl.question('Your Name: ', (answer) => {
       companyDetailsArray.push(answer);
       rl.question('Company Name: ', (answer) => {
            companyDetailsArray.push(answer);
            rl.question('Address Line 1: ',(answer) => {
               companyDetailsArray.push(answer);
                rl.question('Address Line 2: ',(answer) => {
                   companyDetailsArray.push(answer);
                  rl.question('Address Line3: ', (answer) => {
                     companyDetailsArray.push(answer);
                        rl.question('AddressLine 4: ', (answer) => {
                           companyDetailsArray.push(answer);
                           rl.question('Address Line 5: ', (answer) => {
                               companyDetailsArray.push(answer);
                               rl.question('Phone: ', (answer) => {
                                 companyDetailsArray.push(answer);
                                 rl.question('Facsimile: ', (answer) => {
                                       companyDetailsArray.push(answer);
                                        rl.question('Website: ', (answer)=> {
                                           companyDetailsArray.push(answer);
                                           rl.question('Email: ', (answer) => {
                                                companyDetailsArray.push(answer);
                                             rl.question('Currency Abbreviation: ', (answer) => {
                                                   companyDetailsArray.push(answer);
                                                    rl.question('Beneficiary: ',(answer) => {
                                                       companyDetailsArray.push(answer);
                                                       rl.question('Bank: ', (answer) => {
                                                            companyDetailsArray.push(answer);
                                                         rl.question('Bank Address: ', (answer) => {
                                                               companyDetailsArray.push(answer);
                                                               rl.question('Account Number: ', (answer) => {
                                                                   companyDetailsArray.push(answer);
                                                                  rl.question('RoutingNumber: ', (answer) => {
                                                                     companyDetailsArray.push(answer);
                                                                     rl.question('Make Checks Payable To: ', (answer) => {
                                                                           companyDetailsArray.push(answer);
                                                                            rl.close();
                                                                           invoice.companyDetails.push({
                                                                               "yourName": companyDetailsArray,
                                                                               "companyName": companyDetailsArray,
                                                                               "addressLine1": companyDetailsArray,
                                                                               "addressLine2": companyDetailsArray,
                                                                               "addressLine3": companyDetailsArray,
                                                                               "addressLine4": companyDetailsArray,
                                                                               "addressLine5": companyDetailsArray,
                                                                              "phone":companyDetailsArray,
                                                                               "facsimile": companyDetailsArray,
                                                                              "website":companyDetailsArray,
                                                                               "email": companyDetailsArray,
                                                                               "currencyAbbreviation":companyDetailsArray,
                                                                               "beneficiary": companyDetailsArray,
                                                                               "bank":companyDetailsArray,
                                                                               "bankAddress": companyDetailsArray,
                                                                               "accountNumber": companyDetailsArray,
                                                                               "routingNumber": companyDetailsArray,
                                                                               "payableTo": companyDetailsArray
                                                                           });
                                                                           console.log("Invoice Company Information Stored");
                                                                            console.log("-----------------------\nFillin Invoice Items\n-----------------------")
                                                                           fillInvoiceItemsInformation();
                                                                        });
                                                                   });
                                                               });
                                                         });
                                                       });
                                                   });
                                             });
                                          });
                                        });
                                    });
                              });
                            });
                        });
                  });
                });
            });
       });
   });
}




现在我们已经有了**的基本信息,我们可以集中精力收集单个**项目,这将在另一个名为“ fillInvoiceItemsInformation”的函数中进行。在每个项目之前,我们都会询问用户是否要添加一个项目。如果他们继续输入“ y”,那么我们将收集该项目的信息,然后再次询问直到他们键入“ n”:function fillInvoiceItemsInformation() {
   const rl = readline.createInterface({
       input: process.stdin,
       output: process.stdout
   });
   var invoiceItemArray = [];
   rl.question('Add item?(y/n): ', (answer) => {
       switch (answer) {
            case "y":
               console.log("-----------------------\nEnter ItemInformation\n-----------------------");
                rl.question('Quantity: ',(answer) => {
                   invoiceItemArray.push(answer);
                  rl.question('Details: ',(answer) => {
                     invoiceItemArray.push(answer);
                        rl.question('UnitPrice: ', (answer) => {
                           invoiceItemArray.push(answer);
                           invoice.invoiceItems.push({
                               "quantity":invoiceItemArray,
                               "details": invoiceItemArray,
                               "unitPrice": invoiceItemArray
                            });
                            console.log("ItemInformation Added");
                            rl.close();
                           fillInvoiceItemsInformation();
                        });
                  });
                });
                break;
            case "n":
               rl.close();
                return fillExcelFile();
                break;
            default:
                console.log("Incorrectoption, Please enter 'y' or 'n'.");
       }
   });
}

填入您的Excel文件收集所有必需的**信息后,我们可以填入到Excel文件中。有关帐单信息和公司设置,我们可以从JavaScript数组中手动​​设置单元格中的每个值:function fillExcelFile() {
   console.log("-----------------------\nFilling in Excelfile\n-----------------------");
   fillBillingInfo();
   fillCompanySetup();
}
function fillBillingInfo() {
   var sheet = wb.getSheet(0);
   sheet.getCell(0, 2).value(invoice.generalInfo.invoiceNumber);
   sheet.getCell(1, 1).value(invoice.generalInfo.invoiceDate);
   sheet.getCell(2, 2).value(invoice.generalInfo.paymentDueDate);
   sheet.getCell(3, 1).value(invoice.generalInfo.customerName);
   sheet.getCell(4, 1).value(invoice.generalInfo.customerCompanyName);
   sheet.getCell(5, 1).value(invoice.generalInfo.customerStreetAddress);
   sheet.getCell(6, 1).value(invoice.generalInfo.customerCityStateZip);
   sheet.getCell(3, 3).value(invoice.generalInfo.invoiceCompanyName);
   sheet.getCell(4, 3).value(invoice.generalInfo.invoiceStreetAddress);
   sheet.getCell(5, 3).value(invoice.generalInfo.invoiceCityStateZip);
}
function fillCompanySetup() {
   var sheet = wb.getSheet(1);
   sheet.getCell(2, 2).value(invoice.companyDetails.yourName);
   sheet.getCell(3, 2).value(invoice.companyDetails.companyName);
   sheet.getCell(4, 2).value(invoice.companyDetails.addressLine1);
   sheet.getCell(5, 2).value(invoice.companyDetails.addressLine2);
   sheet.getCell(6, 2).value(invoice.companyDetails.addressLine3);
   sheet.getCell(7, 2).value(invoice.companyDetails.addressLine4);
   sheet.getCell(8, 2).value(invoice.companyDetails.addressLine5);
   sheet.getCell(9, 2).value(invoice.companyDetails.phone);
   sheet.getCell(10, 2).value(invoice.companyDetails.facsimile);
   sheet.getCell(11, 2).value(invoice.companyDetails.website);
   sheet.getCell(12, 2).value(invoice.companyDetails.email);
   sheet.getCell(13, 2).value(invoice.companyDetails.currencyAbbreviation);
   sheet.getCell(14, 2).value(invoice.companyDetails.beneficiary);
   sheet.getCell(15, 2).value(invoice.companyDetails.bank);
   sheet.getCell(16, 2).value(invoice.companyDetails.bankAddress);
   sheet.getCell(17, 2).value(invoice.companyDetails.accountNumber);
   sheet.getCell(18, 2).value(invoice.companyDetails.routingNumber);
   sheet.getCell(19, 2).value(invoice.companyDetails.payableTo);
}

我们使用的模板具有针对**中项目的特定行数。用户添加的数量可能会超过最大值。在这种情况下,我们可以简单地在工作表中添加更多行。在设置数组中表单中的项目之前,我们将添加行:function fillInvoiceItems() {
   var sheet = wb.getSheet(0);
   var rowsToAdd = 0;
   if (invoice.invoiceItems.length > 15) {
       rowsToAdd = invoice.invoiceItems.length - 15;
       sheet.addRows(22, rowsToAdd);
   }
   var rowIndex = 8;
   if (invoice.invoiceItems.length >= 1) {
       for (var i = 0; i < invoice.invoiceItems.length; i++) {
            sheet.getCell(rowIndex,1).value(invoice.invoiceItems.quantity);
            sheet.getCell(rowIndex,2).value(invoice.invoiceItems.details);
            sheet.getCell(rowIndex,3).value(invoice.invoiceItems.unitPrice);
            rowIndex++;
       }
   }
}

将文档内容从Node.js导出到Excel文件在工作簿中填写完信息后,我们可以将工作簿导出到Excel文件中。为此,我们将使用excelio打开功能。在这种情况下,只需将日期输入文件名即可:function exportExcelFile() {
   excelIO.save(wb.toJSON(), (data) => {
       fs.appendFileSync('Invoice' + new Date().valueOf() + '.xlsx', newBuffer(data), function (err) {
            console.log(err);
       });
       console.log("Export success");
   }, (err) => {
       console.log(err);
   }, { useArrayBuffer: true });
}

您可以使用上述代码片段将工作簿导出到Excel文件。您完成的文件将如下所示:
以上就是第一篇的全部内容,接下来的内容请参考:专题:服务端Node.js运行SpreadJS处理Excel的方案探讨(二)

KevinChen 发表于 2021-2-27 22:25:59

完整的示例工程请参考附件。

KevinChen 发表于 2021-2-27 22:50:49

补充,本例会用到,以及可能用到的Excel和ssjson文件,见本层附件。

KevinChen 发表于 2021-2-27 23:30:13

注意,如果在运行时遇到“TypeError: Cannot read property 'font' of null”这样的错误时,需要先设置正确的授权,见代码中:
GC.Spread.Sheets.LicenseKey ="<YOUR KEY HERE>"
把授权的license字符串替换掉<YOUR KEY HERE>即可。
页: [1]
查看完整版本: 专题:服务端Node.js运行SpreadJS处理Excel的方案探讨(一)