最近我們公司接到一個客戶的需求,要求為正在開發的專案加個功能。專案的前端使用的是React,客戶想新增具備Excel 匯入/匯出功能的電子試算表模組。
經過幾個小時的原型構建後,技術團隊確認所有客戶需求檔案中描述的功能都已經實現了,並且原型可以在截止日期前做好演示準備。但是,在跟產品組再次討論客戶需求時,我們發現之前對有關電子試算表的部分理解可能存在偏差。
客戶的具體需求點僅僅提到支援雙擊填報、具備邊框設定、背景色設定和刪除行列等功能,但這部分需求描述不是很明確,而且最後提到「像Excel的類似體驗」,我們之前忽略了這句話背後的資訊量。經過與客戶的業務需求方的直接溝通,可以確認終端使用者就是想直接在網頁端操作Excel,並且直接把編輯完成的表格以Excel的格式下載到本地。
你可以看到在 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
行動端範例(可掃碼體驗):http://demo.grapecity.com.cn/spreadjs/mobilesample/