用Nextjs寫一個在線電子表格編輯器,已開源!
前幾天和大家分享了我精心研發的開源可視化打印平臺 flowmix/print,最近有粉絲說想實現一個在線電子表格的案例,由于之前寫過類似的應用,所以今天和大家分享一下使用js實現一個在線電子表格的案例,方便大家學習參考。
Demo演示
圖片
我也寫了配套的電子表格管理頁面,具體UI如下:
圖片
當然電子表格管理頁面主要是純前端實現,主要包括個人表格管理和表格模版模塊,大家可以對接真實的數據。
圖片
如果大家想學習代碼,可以在【趣談前端】公眾號回復“表格源碼”。
技術實現
圖片
目前我實現的"在線電子表格"是一個基于Web的電子表格應用,提供類似Excel的功能,支持數據編輯、公式計算、表格樣式設置、多工作表管理等功能。該項目采用現代主流前端技術棧構建,具有響應式設計和良好的用戶體驗。
技術棧如下:
1. 核心框架與庫
- Next.js: 基于React的全棧框架,提供服務端渲染、路由管理和API功能
- React: 用于構建用戶界面的JavaScript庫
- TypeScript: 為JavaScript添加靜態類型檢查,提高代碼質量和開發效率
- Luckysheet: 開源的JavaScript電子表格庫,提供核心的電子表格功能
- Tailwind CSS: 實用優先的CSS框架,用于快速構建自定義界面
- Lucide React: 提供現代化圖標集的React組件庫
2. UI組件
- shadcn/ui: 基于Radix UI的高質量React組件集合
- Radix UI: 無樣式、可訪問性優先的UI組件庫
3. 工具與輔助庫
- next-themes: 提供深色模式支持
- React Router DOM: 用于客戶端路由管理(在React版本中使用)
核心功能實現
1. 電子表格引擎集成
項目核心是對Luckysheet的集成與擴展。LuckysheetEditor 組件封裝了Luckysheet的功能,并添加了額外的數據管理、事件處理和UI交互層。代碼實現類似如下:
// 初始化Luckysheet
window.luckysheet.create({
container: "luckysheet",
title: currentTitle,
data: initialData,
index: activeSheetIndex,
lang: "zh",
// 其他配置...
});
更詳細的封裝大家可以參考我的源代碼實現。
2. 數據變更監聽與保存
目前我的方案是實現了多層次的數據變更監聽機制,確保用戶的編輯操作能被準確捕獲并標記為未保存狀態:
- 方法重寫: 重寫Luckysheet的setCellValue方法,在原始功能基礎上添加變更監聽,代碼類似如下:
window.luckysheet.setCellValue = function(...args) {
const result = originalSetCellValue.apply(this, args);
setHasUnsavedChanges(true);
return result;
};
- 事件監聽: 監聽Luckysheet的各種事件,如工作表添加、刪除、單元格更新等, 類似代碼如下:
document.addEventListener("luckysheet.deleteSheet", () => {
setHasUnsavedChanges(true);
});
- DOM變更觀察: 使用 MutationObserver 監聽DOM變化,捕獲可能的數據變更,實現的代碼類似如下:
const observer = new MutationObserver((mutations) => {
// 防抖處理
if (dataChangeTimerRef.current) {
clearTimeout(dataChangeTimerRef.current);
}
dataChangeTimerRef.current = setTimeout(() => {
// 檢查是否是真正的數據變化
// ...
}, 1000);
});
自動保存機制:我之前也實現了一個,大家也可以應用到在線表格項目中,代碼類似如下:
useEffect(() => {
if (autoSaveInterval > 0 && luckysheetInitialized.current) {
autoSaveTimerRef.current = setInterval(() => {
if (hasUnsavedChanges) {
handleSave(true).then(success => {
// 處理保存結果
});
}
}, autoSaveInterval);
}
return () => {
if (autoSaveTimerRef.current) {
clearInterval(autoSaveTimerRef.current);
}
};
}, [autoSaveInterval, handleSave, hasUnsavedChanges]);