最近我們公司接到一個客戶的需求,要求爲正在開發的項目加個功能。項目的前端使用的是React,客戶想添加具備Excel 導入/導出功能的電子表格模塊。
經過幾個小時的原型構建後,技術團隊確認所有客戶需求文檔中描述的功能都已經實現了,並且原型可以在截止日期前做好演示準備。但是,在跟產品組再次討論客戶需求時,我們發現之前對有關電子表格的部分理解可能存在偏差。
客戶的具體需求點僅僅提到支持雙擊填報、具備邊框設置、背景色設置和刪除行列等功能,但這部分需求描述不是很明確,而且最後提到“像Excel的類似體驗”,我們之前忽略了這句話背後的信息量。經過與客戶的業務需求方的直接溝通,可以確認終端用戶就是想直接在網頁端操作Excel,並且直接把編輯完成的表格以Excel的格式下載到本地。
如何把前端表格添加到你的React應用中
你可以看到在 StackBlitz 上實時運行的靜態表格應用程序,並且可以在此處找到演示源。
如果你想要已經添加了 SpreadJS 的成熟應用程序,請下載此示例。
完成後,打開終端,導航到克隆存儲庫的目錄,然後運行:
> npm install
現在你將看到更新後的應用程序正在運行。
Step 1: 原生HTML表格
該應用程序的前端基於 ReactJS 構建,並由使用 JSX 語法、JavaScript 和 HTML 代碼組合創建的組件構成。該應用程序是使用功能組件的語法創建的。這種方法使我們可以避免編寫類,這會使組件更加複雜和難以閱讀。
儀表板位於 JSX 組件層次結構的頂部。它呈現 HTML 內容並維護應用程序狀態,源自具有虛擬 JSON 銷售數據的文件。
每個子組件負責呈現其內容。由於只有 Dashboard 保存應用程序狀態,因此它通過 props 將數據向下傳遞給每個子組件。
Import React, { useState } from ‘react’;
import { NavBar } from ‘./NavBar’
import { TotalSales } from ‘./TotalSales’
import { SalesByCountry } from ‘./SalesByCountry’
import { SalesByPerson } from ‘./SalesByPerson’
import { SalesTable } from ‘./SalesTable’
import { groupBySum } from “../util/util”;
import { recentSales } from “../data/data”;
export const Dashboard = () => {
const sales = recentSales;
function totalSales() {
const items = sales;
const total = items.reduce(
(acc, sale) => (acc += sale.value),
0
);
return parseInt(total);
};
function chartData() {
const items = sales;
const groups = groupBySum(items, “country”, “value”);
return groups;
};
function personSales() {
const items = sales;
const groups = groupBySum(items, “soldBy”, “value”);
return groups;
};
function salesTableData() {
return sales;
};
return (
<div style={{ backgroundColor: ‘#ddd’ }}>
<NavBar title=”Awesome Dashboard” />
<div className=”container”>
<div className=”row”>
<TotalSales total={totalSales()}/>
<SalesByCountry salesData={chartData()}/>
<SalesByPerson salesData={personSales()}/>
<SalesTable tableData={salesTableData()}/>
</div>
</div>
</div>
);
}
Step 2: 替換爲SpreadJS表格
在編寫任何代碼行之前,我們必須首先安裝 GrapeCity 的 Spread.Sheets Wrapper Components for React。只需停止應用程序,然後運行以下兩個命令:
> npm install @grapecity/spread-sheets-react
> npm start
在使用 SpreadJS 之前,你必須修改 SalesTable.js 文件以聲明 GrapeCity 組件的導入。這些導入將允許訪問 SpreadSheets、Worksheet 和 SpreadJS 庫的 Column 對象。
Import React from ‘react’;
import { TablePanel } from “./TablePanel”;
// SpreadJS imports
import ‘@grapecity/spread-sheets-react’;
/* eslint-disable */
import “@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css”;
import { SpreadSheets, Worksheet, Column } from ‘@grapecity/spread-sheets-react’;
此外,如果沒有一些基本設置,SpreadJS 工作表將無法正常工作,因此讓我們創建一個配置對象來保存工作表參數。
Export const SalesTable = ({ tableData } ) => {
const config = {
sheetName: ‘Sales Data’,
hostClass: ‘ spreadsheet’,
autoGenerateColumns: false,
width: 200,
visible: true,
resizable: true,
priceFormatter: ‘$ #.00’,
chartKey: 1
}
首先,我們必須消除在 SalesTable 組件中呈現靜態面板的 JSX 代碼:
return (
<TablePanel title=”Recent Sales”>
<table className=”table”>
<thead>
<tr>
<th>Client</th>
<th>Description</th>
<th>Value</th>
<th>Quantity</th>
</tr>
</thead>
<tbody>
{tableData.map((sale) =>
(<tr key={sale.id}>
<td>{sale.client}</td>
<td>{sale.description}</td>
<td>${sale.value}</td>
<td>{sale.itemCount}</td>
</tr>))}
</tbody>
</table>
</TablePanel>
);
通過消除這個代碼塊,我們最終只得到了 TablePanel,這是我們在每個組件中使用的通用 UI 包裝器。
Return (
<TablePanel title=”Recent Sales”>
</TablePanel>
);
此時,我們現在可以在 TablePanel 中插入 SpreadJS SpreadSheets 組件。請注意,SpreadSheets 組件可能包含一個或多個工作表,就像 Excel 工作簿可能包含一個或多個工作表一樣。
Return (
<TablePanel key={config.chartKey} title=”Recent Sales”>
<SpreadSheets hostClass={config.hostClass}>
<Worksheet name={config.sheetName} dataSource={tableData} autoGenerateColumns={config.autoGenerateColumns}>
<Column width={50} dataField=’id’ headerText=”ID”></Column>
<Column width={200} dataField=’client’ headerText=”Client”></Column>
<Column width={320} dataField=’description’ headerText=”Description”></Column>
<Column width={100} dataField=’value’ headerText=”Value” formatter={config.priceFormatter} resizable=”resizable”></Column>
<Column width={100} dataField=’itemCount’ headerText=”Quantity”></Column>
<Column width={100} dataField=’soldBy’ headerText=”Sold By”></Column>
<Column width={100} dataField=’country’ headerText=”Country”></Column>
</Worksheet>
</SpreadSheets>
</TablePanel>
);
作爲畫龍點睛的一筆,我們將以下這些行添加到 App.css 文件中以修復電子表格的尺寸,以便該組件佔據底部面板的整個寬度和銷售儀表板頁面的適當高度。
/*SpreadJS Spreadsheet Styling*/
.container.spreadsheet {
width: 100% !important;
height: 400px !important;
border: 1px solid lightgray !important;
padding-right: 0;
padding-left: 0;
}
而且……瞧!這爲我們提供了下面令人驚歎的電子表格:
請注意,SpreadJS 工作表如何爲我們提供與 Excel 電子表格相同的外觀。
在 Worksheet 組件中,我們可以看到 Column 組件,它定義了每一列的特徵,例如寬度、綁定字段和標題文本。我們還在銷售價值列中添加了貨幣格式。
與舊的靜態表一樣,新的 SpreadJS 電子表格組件從儀表板傳遞的道具接收數據。如你所見,電子表格允許你直接更改值,就像在 Excel 電子表格中一樣。但是,正如你對 React 應用程序所期望的那樣,這些更改不會自動反映在其他組件中。爲什麼呢?
從儀表板接收數據後,SpreadJS 工作表開始使用副本,而不是儀表板組件中聲明的銷售數據。事件和函數應該處理任何數據修改以相應地更新應用程序的狀態。
對於下一個任務,你必須使應用程序反映對所有 Dashboard 組件上的 SpreadJS 工作表所做的更改。
Step 3: SpreadJS實現響應式數據綁定
目前,在 Dashboard.js 文件中聲明的銷售常量負責維護應用程序的狀態。
Const sales = recentSales;
正如我們所看到的,這種結構意味着靜態數據,阻止了我們希望實現的動態更新。因此,我們將用稱爲鉤子的賦值替換那行代碼。在 React 中,鉤子具有簡化的語法,可以同時提供狀態值和處理函數的聲明。
Const[sales, setSales] = new useState(recentSales);
上面的代碼行顯示了 JavaScript 數組解構語法。 useState 函數用於聲明銷售常量,它保存狀態數據,以及 setSales,它引用僅在一行中更改銷售數組的函數。
但是,我們的應用程序中還不存在這個 useState 函數。我們需要從 Dashboard.js 組件文件開頭的 React 包中導入它:
import React, { useState } from ‘react’;
現在,我們準備在必要時更新 sales 數組的狀態。
我們希望將對工作表所做的更改傳播到儀表板的其餘部分。因此,我們必須訂閱一個事件來檢測對 Worksheet 組件單元格所做的更改,並在 SalesTable.js 文件中實現相應的事件處理。
我們將此事件處理程序稱爲handleValueChanged。
<SpreadSheets hostClass={config.hostClass} valueChanged={handleValueChanged}>
我們仍然需要實現一個同名的函數。在其中,我們獲取工作表的已更改數據源數組,並將該數組傳遞給名爲 valueChangeCallback 的函數。
Function handleValueChanged(e, obj) {
valueChangedCallback(obj.sheet.getDataSource());
}
handleValueChanged.bind(this);
然後將 valueChangedCallback 函數從 Dashboard 傳遞到 SalesTable 組件:
<SalesTable tableData={salesTableData()}
valueChangedCallback={handleValueChanged}/>
現在,你必須將此回調函數作爲參數傳遞給 SalesTable 組件:
export const SalesTable = ({ tableData, valueChangedCallback } ) => {
對工作表中單元格的任何更改都會觸發回調函數,該函數會執行 Dashboard 組件中的 handleValueChanged 函數。下面的handleValueChanged 函數必須在Dashboard 組件中創建。它調用 setSales 函數,該函數更新組件的狀態。因此,更改會傳播到應用程序的其他組件。
Function handleValueChanged(tableData) {
setSales(tableData.slice(0));
}
你可以通過編輯一些銷售額值並查看儀表板頂部的銷售額變化來嘗試此操作:
看起來比爾的銷售業績不錯!
Step 4: 實現導入導出Excel
到目前爲止,我們已經瞭解瞭如何用 SpreadJS 電子表格替換靜態銷售表。我們還學習瞭如何通過 React 的鉤子和回調在應用程序組件上傳播數據更新。我們設法用很少的代碼提供了這些功能。你的應用程序看起來已經很棒了,並且你相信它將給你未來的客戶留下深刻印象。但在此之前,讓我們錦上添花。
你已經知道你的企業用戶在日常生活中經常使用 Excel。相同的用戶將開始在 React 和 SpreadJS 之上使用你的全新應用程序。但在某些時候,他們會錯過 Excel 和你出色的儀表板之間的集成。
如果你只能將電子表格數據導出到 Excel 並將數據從 Excel 導入到 SpreadJS,則該應用程序將更加強大。你如何實現這些功能?
讓我們再次停止應用程序並安裝 GrapeCity 的 Spread.Sheets 客戶端 Excel IO 包以及文件保護程序包:
> npm install @grapecity/spread-excelio
> npm install file-saver
> npm start
要將數據從我們的應用程序導出到 Excel 文件(擴展名爲 .xlsx),我們必須修改 SalesTable 組件,聲明 Excel IO 和文件保護程序組件的導入。
Import { IO } from “@grapecity/spread-excelio”;
import { saveAs } from ‘file-saver’;
接下來,我們將更改 SalesTable.js 文件的 JSX 代碼,以添加一個按鈕以將 SpreadJS 工作表數據導出到本地文件。單擊該按鈕將觸發一個名爲 exportSheet 的事件處理程序。
{/* EXPORT TO EXCEL */}
<div className=”dashboardRow”>
<button className=”btn btn-primary dashboardButton”
onClick={exportSheet}>Export to Excel</button>
</div>
</TablePanel>
反過來,exportSheet 函數會將工作表中的數據保存到名爲 SalesData.xslx 的文件中。該函數首先將 Spread 對象中的數據序列化爲 JSON 格式,然後通過 Excel IO 對象將其轉換爲 Excel 格式。
Function exportSheet() {
const spread = _spread;
const ilename = “SalesData.xlsx”;
const sheet = spread.getSheet(0);
const excelIO = new IO();
const json = JSON.stringify(spread.toJSON({
includeBindingSource: true,
columnHeadersAsFrozenRows: true,
}));
excelIO.save(json, (blob) => {
saveAs(blob, ilename);
}, function € {
al€(
});
}
請注意上述函數如何需要一個展開對象,該對象必須與我們在 SalesTable 組件中使用的 SpreadJS 工作表的實例相同。一旦定義了 SpreadSheet 對象,上面清單中的 getSheet(0) 調用就會檢索電子表格數組中的第一個工作表:
const sheet = spread.getSheet(0);
但是我們如何以編程方式獲取電子表格的實例呢?
一旦電子表格對象被初始化,SpreadJS 庫就會觸發一個名爲 workbookInitialized 的事件。我們必須處理它並將實例存儲爲 SalesTable 組件的狀態。讓我們首先使用 useState 鉤子爲電子表格實例聲明一個狀態常量:
const [_spread, setSpread] = useState({});
我們需要將 useState 函數導入到 SalesTable.js 組件文件開頭的 React 聲明中:
import React, { useState }‘from ’react';
現在我們可以聲明一個函數來處理 workbookInit 事件……
function workbookInit(sprea
setSpread(spread)
}
...然後將 workbookInit 事件綁定到我們剛剛創建的函數:
<SpreadSheets hostClass={config.hostClass} workbookInitialized={workbookInit} valueChanged={handleValueChanged}>
現在,“導出到 Excel”按鈕將如下所示:
現在我們來演示如何實現 Excel 數據導入。這個過程是導出的逆過程,所以讓我們從 XLSX 文件開始。
此功能的訪問點是另一個按鈕,我們需要將其添加到 SalesTable 組件的 JSX 代碼的末尾。請注意,這裏我們使用不同的按鈕類型:“文件”類型的輸入元素,它產生一個選擇文件的按鈕。當文件被選中時,onChange 事件觸發 fileChangeevent 處理程序:
<div clas”Name="dashbo”rd>
{/* EXPORT TO EXCE}
<button clas”Name="btn btn-primary dashboard”utton"
onClick={exportSheet}>Export to Excel</bu>
{/* IMPORT FROM EXCE}
<div>
<b>Import Excel File:</b>
<div>
<input”type”"file" clas”Name="file”elect"
onCh€e={(e) => f€Change(e)} />
</div>
</div>
</div>
反過來,fileChange 函數將使用 Excel IO 對象將文件導入工作表對象。在函數結束時,會觸發一個 fileImportedCallback 事件,將數據帶到 Dashboard 組件中:
functio€hange(e) {
if (_spread) {
const fileDom = e.target || e.srcElement;
const excelIO = new IO();
const spread = _spread;
const deserializationOptions = {
frozenRowsAsColumnHeaders: true
};
excelIO.open(fileDom.files[0], (data) => {
const newSalesData = extractSheetData(data);
fileImportedCallback(newSalesData });
}
}
但是這個回調需要聲明爲 SalesTable 組件的參數:
export const SalesTable = ({ tableData, valueChangedCallback,
fileImportedCallback } ) => {
此外,我們必須通過從 util.js 文件中導入來爲 SalesTable 組件提供 extractSheetData 函數:
import { extractSh“etData } from "”./util/util.js";
我們需要爲 Dashboard 組件上的保存文件實現事件處理程序。這個函數唯一要做的就是使用來自 SpreadJS 工作表的數據更新儀表板的狀態。
function handleFileImportewSales) {
setSales(newSales.slice(0));
}
<SalesTable tableData={saleleData()}
valueChangedCallback={handleValueChanged}
fileImportedCallback={handleFileImported}/>
只需幾個簡單的步驟,我們就可以將帶有靜態數據的無聊應用程序變成以具有 Excel 導入和導出功能的電子表格爲中心的響應式應用程序。最後,你查看客戶的請求並驗證你的應用程序是否滿足所有要求!
我們可以擴展這些想法併爲我們的應用程序探索其他令人興奮的功能。例如,我們可以自動、靜默地保存工作表數據,從而在需要時保留更改日誌和回滾錯誤到表中。
此外,你可以將表格數據與遠程數據庫同步。或者你可以實現一個保存按鈕,通過 Web 服務方法將表數據複製到外部系統。
更多純前端表格在線demo示例 :https://demo.grapecity.com.cn/spreadjs/gc-sjs-samples/index.html
純前端表格應用場景:https://www.grapecity.com.cn/developer/spreadjs#scenarios
移動端示例(可掃碼體驗):http://demo.grapecity.com.cn/spreadjs/mobilesample/