Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。它可以用在 react、angular、vue 等專案中, 但與 react 配合使用更加方便一些。
Redux 原理圖如下,可以看到store倉庫是Redux的核心,通過維護一個store倉庫管理 state。state 是唯讀的,唯一改變 state 的方法就是元件觸發 Action。通過編寫Reducers 函數,它會接收先前的 state 和 action,並返回新的 state。
Redux的核心理念就是如何根據這些 action 物件來更新 state,強制使用 action 來描述所有變化帶來的好處是你可以清晰地知道應用中到底發生了什麼。如果一些東西改變了,你可以知道為什麼變化,action 就是描述發生了什麼的指示器。
來看一下Redux在大屏展示中具體的使用場景:
下面的截圖是一個產品開發中非常常見的大屏展示介面範例。核心的資料來源為一組銷售資料,上方三個儀表板以及下方的表格元件共用同一個資料來源,實現了資料明細顯示以及各維度的資料統計。
從圖上來看,似乎已經具備了大屏展示的資料顯示和統計功能,但是展示的資料是沒有辦法被編輯和修改的。此時,你可能會收到來自客戶的靈魂拷問:
「展示功能已經不錯了,但是表格資料可以實時編輯更新嗎?」
圖中的銷售明細資料是用html表格直接顯示的,如果要實現編輯,通常的做法是,我們挑選一個前端表格元件,實現編輯的功能。
文末可下載文章程式碼檔案。
將表格新增到你的 React 應用程式
我們要用電子試算表替換這個html表格,修改component資料夾中的SalesTable.js,替換其中的table。
<SpreadSheets hostClass={config.hostClass} workbookInitialized={workbookInit} valueChanged={(e,info) => handleValueChanged(e,info)}>
<Worksheet name={config.sheetName} dataSource={tableData} autoGenerateColumns={config.autoGenerateColumns} >
<Column width={50} dataField='id' headerText="編號"></Column>
<Column width={200} dataField='client' headerText="客戶"></Column>
<Column width={320} dataField='description' headerText="描述"></Column>
<Column width={100} dataField='value' headerText="銷售額" formatter={config.priceFormatter} resizable="resizable"></Column>
<Column width={100} dataField='itemCount' headerText="數量"></Column>
<Column width={100} dataField='soldBy' headerText="銷售人員"></Column>
<Column width={100} dataField='country' headerText="國家"></Column>
</Worksheet>
</SpreadSheets>
其中,SpreadSheets元素建立了一個電子試算表並定義瞭如何顯示資料列。dataSource 屬性定義了繫結的資料來源,Column 中的dataField 屬性告訴該列應該繫結底層資料集的哪個屬性。
接下來是js程式碼部分,
import '@grapecity/spread-sheets-react';
import "@grapecity/spread-sheets/styles/gc.spread.sheets.excel2016colorful.css";
import { SpreadSheets, Worksheet, Column } from '@grapecity/spread-sheets-react';
export const SalesTable = ({ tableData, valueChangedCallback,} ) => {
const config = {
sheetName: 'Sales Data',
hostClass: ' spreadsheet',
autoGenerateColumns: false,
width: 200,
visible: true,
resizable: true,
priceFormatter: '$ #.00',
chartKey: 1
}
function handleValueChanged(e, obj) {
valueChangedCallback(obj.sheet.getDataSource());
}
handleValueChanged.bind(this);
const [_spread, setSpread] = useState({});
function workbookInit(spread) {
setSpread(spread)
}
}
只需很少的程式碼即可完成。config中的幾個資料屬性。是繫結到電子試算表中的元件的設定選項。workbookInit 方法是在初始化工作表時呼叫的回撥。handleValueChanged是在表格資料發生變化後的回撥
重新執行,即可顯示電子試算表資料:
現在我們用一個完整的電子試算表替換了原來的html table,此時可以對錶格中的資料做任意的修改編輯,但是在編輯後上方的銷售統計結果並不會實時更新,接下來我們就用Redux來建立一個store倉庫用來儲存銷售資料,以實現資料的共用和實時更新。
1.引入相關庫
"@reduxjs/toolkit": "^1.9.1",
"react-redux": "^7.2.0",
"redux": "^4.0.5"
2.通過createSlice建立切片
新建一個js檔案,寫入下面的程式碼,通過Redux 提供createSlice方法,我們建立了一個切片,初始化了state,在其中加入了銷售明細資料作為recentSales。為reducers新增了兩個方法updatesales和importSales,用於在銷售明細資料更新或者匯入這兩種情況時,來同步recentSales。
import { createSlice } from '@reduxjs/toolkit';
import { recentSalesdata } from "../data/data";
const initialState = {
recentSales: JSON.parse(JSON.stringify(recentSalesdata)),
status: 'idle',
};
export const salesSlice = createSlice({
name: 'recentSales',
initialState,
reducers: {
importSales: (state,action) => {
state.recentSales=JSON.parse(JSON.stringify(action.payload));
},
updatesales: (state,action) => {
let sales=state.recentSales;
let arr=sales.map(function(o){return o.id});
console.log(arr);
action.payload.forEach((newsale)=>{
if(arr.indexOf(newsale.id)>=0){
state.recentSales[arr.indexOf(newsale.id)]=JSON.parse(JSON.stringify(newsale));
}
else{
console.log("add");
state.recentSales.push(JSON.parse(JSON.stringify(newsale)));
}
});
},
},
});
export const { updatesales,importSales} = salesSlice.actions;
export const recentSales = (state) => state.recentSales.recentSales;
export default salesSlice.reducer;
3.建立store
新增store.js檔案並加入下面的程式碼,這裡建立的store中加入了剛剛建立的切片器。
import { configureStore } from '@reduxjs/toolkit';
import recentSalesReducer from '../store/salesSlice';
export const store = configureStore({
reducer: {
recentSales: recentSalesReducer,
},
});
4.在component元件中使用store
在Dashboard.js中,import下面的程式碼。
import { useSelector, useDispatch } from 'react-redux';
import {
updatesales,importSales,
recentSales
} from '../store/salesSlice';
然後在建立的Dashboard方法體中,再加入下面的程式碼,其中react-redux 提供的:
const sales = useSelector(recentSales);
const dispatch = useDispatch();
function handleValueChanged(tableData) {
dispatch(updatesales(tableData));
}
function handleFileImported(newSales) {
dispatch(importSales(newSales));
}
對大屏展示面板加入redux做了上述改造後,就達到了銷售資料編輯後,資料統計結果同步更新的效果:
動圖中可以看到上面三個儀表板顯示的內容也同步進行了更新。原因是表格被編輯後,我們同步更新了state中的recentSales。
好了,現在我們已經有了一個可以隨著資料變化而實時更新的增強型儀表板。客戶的需求順利完成,但是在演示時,你很可能又會聽到客戶說出的下面的需求:
「能支援Excel資料的匯入匯出嗎?」
如果您已經開發軟體很長時間,您可能不止一次地從最終客戶或者產品經理那裡聽到過這個靈魂拷問。對於非技術人群來說,覺得要求 Excel 匯入/匯出/展示是一個非常正常且容易實現的需求。
但實際上,這個問題常常讓前端開發人員感到束手無策。處理 Excel 檔案需要大量工作。即使使用第三方的grid元件,也很難支援匯入一個複雜的Excel表格作為資料。
這個問題通過表格可以變得簡單,匯入和匯入都可以直接實現。這也是我們在開始時使用將電子試算表作為表格明細資料顯示和編輯控制元件的原因。下面我們為應用加入Excel匯入匯出功能
將 Excel 匯入匯出功能新增到工作表很容易。首先,在介面上新增相關的檔案輸入框和按鈕。把它放在電子試算表面板的底部,在 SpreadSheets 結束標記之後新增。
<div className="dashboardRow">
{/* EXPORT TO EXCEL */}
<button className="btn btn-primary dashboardButton"
onClick={exportSheet}>Export to Excel</button>
{/* IMPORT FROM EXCEL */}
<div>
<b>Import Excel File:</b>
<div>
<input type="file" className="fileSelect"
onChange={(e) => fileChange(e)} />
</div>
</div>
</div>
接下來新增點選時觸發的 exportSheet方法,首先新增並匯入下面的包,其中@grapecity/spread-excelio是SpreadJS中用於匯入匯出Excel的包。
import { IO } from "@grapecity/spread-excelio";
import { saveAs } from 'file-saver';
然後將匯出方法 exportSheet 新增到元件中:
function exportSheet() {
const spread = _spread;
const fileName = "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, fileName);
}, function (e) {
alert(e);
});
}
執行測試點選按鈕,即可直接獲取到匯出的Excel檔案。
需要注意的是,我們設定了兩個序列化選項:includeBindingSource 和 columnHeadersAsFrozenRows。以確保繫結到工作表的資料被正確匯出,且工作表包含列標題,
我們繼續來新增匯入的方法,剛剛建立檔案輸入框,我們來處理它的onChange事件,建立一個fileChange方法
function fileChange(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);
spread.getSheet(0).setDataSource(newSalesData,false);
fileImportedCallback(newSalesData);
});
}
}
選擇檔案後,使用ExcelIO 匯入它。獲取其中的json資料。傳入自定義的函數extractSheetData,從中提取需要的資料,然後設定給SpreadJS作為電子試算表資料來源,另外傳給fileImportedCallback方法,這個函數中會呼叫dispatch(importSales(newSales)); 來同步更新了state中的recentSales。
extractSheetData 函數可以在 src/util.util.js 檔案中找到,用於 解析Excel中的資料。extractSheetData函數假定匯入工作表中的資料與原始資料集具有相同的列。如果有人上傳的電子試算表不符合此要求,將無法解析。這個應該是大多數客戶可以接受的限制。資料不符時,也可以嘗試給客戶一個提示資訊。
Excel匯入匯出效果
最終的專案可以參考下面的附件
https://gcdn.grapecity.com.cn/forum.php?mod=attachment&aid=MjUzNTE4fGU5MTk4OGQxfDE2NzM0MTYxMjd8NjI2NzZ8OTk3MTg%3D
React、Redux 和 電子試算表的配合使用讓這個應用的增強開發變的非常方便。藉助 Redux提供的可預測化的狀態管理和互動式電子試算表,可以在很短內建立複雜的企業 JavaScript 應用程式。