Mục lục bài viết10 mục
- 1.Chuẩn bị môi trường
- 2.Các bước thực hiện
- 2.1Bước 1: Xây dựng mô hình dữ liệu (Data Model)
- 2.2Bước 2: Tích hợp I/O .xlsx (đọc tệp)
- 2.3Bước 3: Xây dựng Grid UI (hiển thị lưới)
- 2.4Bước 4: Xây dựng Engine công thức
- 2.5Bước 5: Tích hợp chèn ảnh
- 2.6Bước 6: Áp dụng định dạng cơ bản
- 2.7Bước 7: Xuất PDF / In ấn
- 2.8Bước 8: Tích hợp ghi .xlsx (lưu tệp)
Chuẩn bị môi trường
Stack công nghệ chính (đã kiểm tra không xung đột):
- Node.js 18+ với npm/yarn (package manager)
- TypeScript 4.9+ (optional nhưng khuyến nghị cho type safety)
- React 18+ hoặc framework web-first tương đương (Next.js 13+ nếu muốn PWA sau)
- Trình duyệt hiện đại (Chrome/Edge/Firefox phiên bản mới nhất)
Cài đặt ban đầu:
Tạo dự án mới với Create React App hoặc Vite:
npm create vite@latest my-spreadsheet-app -- --template react
cd my-spreadsheet-app
npm install
Cài đặt các thư viện core (đã check tương thích):
Mở terminal tại thư mục dự án, cài các gói sau theo thứ tự:
- Đọc/ghi .xlsx:
npm install xlsx exceljs jszip
xlsx(SheetJS): phân tích nhanh, tương thích tốtexceljs: hỗ trợ đầy đủ OOXML, dễ cập nhật styling và ảnhjszip: xử lý ZIP archive (nội bộ, dependencies của exceljs)
- Lưới hiển thị:
npm install react-windowhoặcnpm install handsontable react-handsontable
react-window: ảo hóa DOM, hiệu suất cao, tối thiểu hóa- HOẶC
handsontable: lưới bảng tính đầy đủ, hỗ trợ công thức, merge, định dạng sẵn (xung đột với react-window — chọn 1 trong 2) - [KHÔNG CHẮC: cần xác nhận handsontable license cho dự án; nếu commercial, có thể cần payment]
- Engine công thức:
npm install hyperformulahoặc tự xây (option nhẹ)
hyperformula: 400+ hàm, dependency graph, hoạt động tốt- HOẶC tự xây parser đơn giản (xem bước 4 dưới)
- [CẦN NGUỒN: tài liệu HyperFormula API cho xác nhận hỗ trợ các 10 hàm]
- Định dạng và styling ô:
npm install csstype(nếu cần type CSS)
- Hầu hết grid library đã có API styling ô sẵn
- Xuất PDF:
npm install jspdf html2canvashoặcnpm install pdfkit
jspdf + html2canvas: export từ DOM, đơn giảnpdfkit: render PDF từ dữ liệu, nặng hơn
- Chèn ảnh: Hỗ trợ sẵn trong grid library hoặc xây bằng
<input type="file">+ canvas API
exceljshỗ trợ addImage() cho .xlsx
- Utilities:
npm install clsx(CSS class binding, tùy chọn)
Phiên bản khuyến nghị (không xung đột):
"react": "^18.2.0"
"react-window": "^1.8.10" (hoặc handsontable ^14+)
"exceljs": "^4.3.0"
"xlsx": "^0.18.5"
"hyperformula": "^2.6.0" (hoặc không dùng nếu tự xây engine)
"jspdf": "^2.5.0"
"html2canvas": "^1.4.1"
Các bước thực hiện
Bước 1: Xây dựng mô hình dữ liệu (Data Model)
Làm gì: Định nghĩa cấu trúc dữ liệu để lưu trữ workbook, worksheet, ô (cell), công thức, định dạng, merge, ảnh.
Thư viện/công cụ: Không dùng thư viện, viết code TypeScript thuần (hoặc JavaScript)
Đầu vào: Yêu cầu chức năng (danh sách ô, công thức, định dạng)
Đầu ra: Một set của interface/class như:
Workbook(chứa worksheets, metadata)Worksheet(chứa mảng ô, merges, images)Cell(value, formula, format, dataType)CellFormat(font, fill, alignment, border, numberFormat)Merge(top-left, bottom-right)Image(data URL, position, size)
Lưu ý xung đột: Nếu dùng handsontable, có thể dùng định dạng dữ liệu của nó sẵn; nếu dùng react-window, phải định nghĩa rõ. Khuyến nghị: xây dựng model độc lập để tránh phụ thuộc quá chặt vào grid library.
Tệp tham khảo tạo:
src/types/Spreadsheet.ts: định nghĩa interfacesrc/models/Workbook.ts: class quản lý workbook
Bước 2: Tích hợp I/O .xlsx (đọc tệp)
Làm gì: Tải tệp .xlsx từ máy người dùng, phân tích (parse) thành mô hình dữ liệu, hiển thị lên grid.
Thư viện: exceljs (đủ đầy đủ) hoặc xlsx (nhanh hơn, nhưng cần xử lý styling riêng)
Đầu vào: Tệp .xlsx từ <input type="file">
Đầu ra: Đối tượng Workbook trong mô hình dữ liệu, sẵn sàng hiển thị
Các bước con:
- Tạo file input với
<input type="file" accept=".xlsx"> - Dùng FileReader để đọc file thành Buffer/Blob
- Dùng
new ExcelJS.Workbook().xlsx.load(buffer)để parse - Duyệt qua worksheets, ô, merge, style, images và đổi về mô hình dữ liệu riêng
- Lưu vào state React
Lưu ý xung đột:
exceljsnặng hơnxlsx(file ~200KB vs ~30KB), nhưng hỗ trợ đầy đủ OOXML. Nếu muốn nhẹ, dùngxlsx+ xử lý styling bằng tay.- SharedStrings có thể lớn → lưu ý memory khi load file lớn (>10MB).
- [KHÔNG CHẮC: cần xác nhận hiệu năng exceljs với file >50MB]
Tệp tham khảo tạo:
src/services/ExcelLoader.ts: hàm loadExcel(file) → Workbook
Bước 3: Xây dựng Grid UI (hiển thị lưới)
Làm gì: Render lưới với hàng, cột, ô có thể click, edit, merge, style như Excel.
Thư viện: Chọn 1 trong 2:
- Option A (Khuyến nghị cho MVP):
handsontable+react-handsontable— hỗ trợ tất cả tính năng (merge, format, formula) sẵn - Option B (Nhẹ hơn):
react-window+ xây UI ô thủ công — hoàn toàn kiểm soát, nhưng phải code nhiều
Nếu chọn Handsontable:
- Cài:
npm install handsontable react-handsontable - Tạo component grid wrapper để kết nối dữ liệu mô hình vào Handsontable
- Cấu hình columns, rows từ Workbook
- Bật chế độ edit, merge, formatting từ options Handsontable
- Bind sự kiện (afterChange, afterMerge, etc.) để cập nhật mô hình dữ liệu
- [CẦN NGUỒN: tài liệu Handsontable API cho merge, formula, styling]
Nếu chọn React-window:
- Cài:
npm install react-window - Xây Grid component hiển thị row/column virtualization
- Mỗi ô là component nhỏ (Cell) với click/double-click để edit
- Ghi đè mô hình dữ liệu khi user edit
- Xử lý merge bằng CSS (colspan, rowspan) hoặc custom rendering
Đầu vào: Mô hình Workbook từ Bước 2
Đầu ra: Grid UI tương tác, hiển thị dữ liệu ô, cho phép click/edit/merge
Lưu ý xung đột:
- Handsontable và react-window không tương thích — chọn 1
- Nếu dùng Handsontable, styles được quản lý bởi Handsontable; nếu dùng react-window, phải quản lý bằng inline CSS hoặc className
- Memory: Handsontable có thể chậm với >100k ô; react-window xử lý tốt hơn nhưng phải code nhiều
Tệp tham khảo tạo:
src/components/SpreadsheetGrid.tsx: wrapper Handsontable hoặc custom gridsrc/components/Cell.tsx(nếu dùng react-window): component ô đơnsrc/hooks/useGridState.ts: quản lý state grid (selection, edit mode, etc.)
Bước 4: Xây dựng Engine công thức
Làm gì: Parser công thức từ chuỗi (ví dụ =SUM(A1:A10)), phân tích tham chiếu ô, tính toán kết quả.
Thư viện: Chọn 1 trong 2:
- Option A (Đủ đầy đủ):
hyperformula— 400+ hàm, dependency graph, tối ưu tốt - Option B (Tối thiểu, tự xây): Parser AST đơn giản, hỗ trợ 10 hàm cốt lõi (SUM, AVERAGE, IF, MIN, MAX, COUNT, ROUND, CONCAT, TODAY, NOW)
Nếu chọn HyperFormula:
- Cài:
npm install hyperformula - Khởi tạo HyperFormula instance với ngôn ngữ công thức (en-US mặc định)
- Khi user enter công thức, gọi
hf.setCellContents()để đăng ký công thức - HyperFormula tự tính toán dependency và recalculate tự động
- Bind sự kiện
valuesChangedđể cập nhật UI grid khi có ô phụ thuộc thay đổi - [CẦN NGUỒN: tài liệu HyperFormula initializationConfig và API setCellContents]
Nếu chọn Parser tự xây:
- Cấu trúc:
- Tokenizer: cắt chuỗi công thức thành tokens (hàm, (), số, tham chiếu ô, etc.)
- AST Parser: xây cây biểu thức từ tokens theo quy tắc precedence
- Evaluator: duyệt cây, resolve tham chiếu ô, gọi hàm, trả kết quả
- Dependency tracker: xây đồ thị phụ thuộc để recalc hiệu quả
- Hỗ trợ các hàm:
- Số học: SUM, AVERAGE, MIN, MAX, COUNT
- Logic: IF
- Text: CONCAT
- Math: ROUND
- Ngày/thời: TODAY, NOW, DATE
- Lưu ý: Cần xử lý tham chiếu ô (A1, $A$1, A1:B10, Sheet1!A1)
Đầu vào: Công thức dạng chuỗi từ cell editor; mô hình Workbook với giá trị ô
Đầu ra: Giá trị tính toán (number, string, boolean, error), trả về grid để hiển thị
Lưu ý xung đột:
- HyperFormula có API riêng, cần wrapper để tích hợp với mô hình dữ liệu
- Parser tự xây dễ kiểm soát nhưng phức tạp; HyperFormula tiện hơn nhưng thư viện nặng (~800KB)
- Nếu dùng Handsontable, nó có integration với HyperFormula sẵn (Handsontable Pro)
Tệp tham khảo tạo:
src/services/FormulaEngine.ts: wrapper enginesrc/engine/Parser.ts(nếu tự xây): tokenize + parse ASTsrc/engine/Evaluator.ts(nếu tự xây): tính toán + dependency
Bước 5: Tích hợp chèn ảnh
Làm gì: Cho phép user tải ảnh từ máy, hiển thị trong lưới (trong ô hoặc overlay), lưu vào mô hình dữ liệu, cập nhật khi ghi .xlsx.
Thư viện:
- Grid library hỗ trợ sẵn (Handsontable có hỗ trợ), hoặc xây bằng
<input type="file">+ canvas API - Lưu ảnh: canvas.toDataURL() → data URL (inline) hoặc file reference
Các bước con:
- Tạo button "Insert Image" hoặc drag-drop zone
- Dùng FileReader để đọc file ảnh thành data URL
- Lưu vào Cell.image (URL, width, height) hoặc riêng trong Worksheet.images
- Render ảnh trong cell hoặc overlay
- Cho phép resize/move bằng mouse (dùng CSS transform hoặc canvas)
- Khi ghi .xlsx, dùng
workbook.addImage()của exceljs để nhúng ảnh
Đầu vào: File ảnh PNG/JPEG từ user
Đầu ra: Ảnh hiển thị trong cell; data lưu trong mô hình; ghi vào .xlsx
Lưu ý xung đột:
- Data URL có thể lớn (base64); nên compress ảnh hoặc lưu reference thay vì inline
- Handsontable có thể không hỗ trợ đầy đủ drag-drop ảnh; có thể cần custom renderer
- exceljs sử dụng anchor
twoCellAnchor(tọa độ EMU); cần chuyển đổi pixel → EMU (1 EMU = 1/914400 inch) - [KHÔNG CHẮC: cần xác nhận hỗ trợ ảnh của Handsontable vs exceljs]
Tệp tham khảo tạo:
src/components/ImageInsert.tsx: UI button/drag-dropsrc/services/ImageManager.ts: xử lý ảnh (resize, compress, data URL)src/components/ImageRenderer.tsx: hiển thị ảnh trong cell
Bước 6: Áp dụng định dạng cơ bản
Làm gì: Hỗ trợ user định dạng ô (font, màu, căn lề, border, number format, merge).
Thư viện: Handsontable hoặc grid library tích hợp; hoặc CSS + state React
Các tính năng:
- Font: tên font, size, bold, italic, underline, màu
- Cell fill: màu nền ô
- Alignment: căn lề (left, center, right), vertical (top, middle, bottom)
- Border: kiểu, màu
- Number format: số thập phân, percent, ngày/thời (vi-VN hoặc en-US)
- Merge cells: gộp ô
- Wrap text: xuống dòng trong ô
Các bước con:
- Tạo toolbar (buttons/dropdown) cho các tùy chọn format
- Khi user chọn ô, tìm format của ô và highlight trong toolbar
- Khi user click nút format, cập nhật Cell.format trong mô hình
- Grid render lại với CSS style từ format
- Khi ghi .xlsx, dùng exceljs style API để lưu format
Đầu vào: Tùy chọn format từ toolbar; ô được chọn
Đầu ra: Ô hiển thị với format; dữ liệu format lưu trong mô hình
Lưu ý xung đột:
- exceljs style format và CSS style không giống nhau; cần wrapper để chuyển đổi
- Merge trong exceljs cần cập nhật lúc ghi; trong grid cần custom render để ẩn ô merged
- Number format: Excel dùng built-in format code (0, 0.00, mm/dd/yyyy); cần parser format string
Tệp tham khảo tạo:
src/components/FormatToolbar.tsx: UI buttonssrc/services/FormatService.ts: chuyển đổi format giữa model và CSSsrc/types/Format.ts: định nghĩa CellFormat interface
Bước 7: Xuất PDF / In ấn
Làm gì: Cho phép user in bảng tính hoặc xuất PDF.
Thư viện: jspdf + html2canvas (đơn giản, từ DOM) hoặc pdfkit (từ dữ liệu)
Option A (khuyến nghị cho MVP): jspdf + html2canvas
- Cài:
npm install jspdf html2canvas - Render grid HTML (hoặc canvas)
- Dùng
html2canvasđể convert DOM → canvas - Dùng
jsPDFđể tạo PDF từ canvas - Định cỡ giấy (A4, etc.), margin, orientation (portrait/landscape)
Option B (Nâng cao): pdfkit + custom render
- Cài:
npm install pdfkit - Render PDF từ dữ liệu (không qua DOM)
- Cần iterate qua ô, vẽ cell + border + text + ảnh lên PDF
- Phức tạp hơn nhưng kết quả đẹp hơn
Các bước con (Option A):
- Tạo button "Export PDF"
- Khi click, ẩn UI (toolbar, etc.), để lại grid
- Dùng
html2canvas(gridElement)để capture canvas - Tạo PDF:
const pdf = new jsPDF() - Thêm canvas vào PDF:
pdf.addImage(canvas, ...) - Handle pagination (nếu grid lớn)
- Save:
pdf.save('spreadsheet.pdf')
Đầu vào: Grid element DOM hoặc dữ liệu workbook
Đầu ra: File PDF tải về máy người dùng
Lưu ý xung đột:
- html2canvas có thể không capture toàn bộ grid nếu overflow; cần adjust scroll height trước
- PDF size có thể lớn nếu grid lớn + ảnh cao độ phân giải
- Pagination cần manual logic (chia trang)
- Font trong PDF: jsPDF hỗ trợ limited fonts; cần embed font nếu cần Unicode (vi-VN)
- [CẦN NGUỒN: tài liệu jsPDF + html2canvas cho ví dụ grid]
Tệp tham khảo tạo:
src/services/PDFExport.ts: hàm exportPDF(gridElement) → PDFsrc/components/ExportButton.tsx: UI button
Bước 8: Tích hợp ghi .xlsx (lưu tệp)
Làm gì: Cho phép user lưu lại các thay đổi vào .xlsx (round-trip: đọc → chỉnh sửa → ghi).
Thư viện: exceljs (đầy đủ hỗ trợ ghi styling, image, formula)
Các bước con:
- Tạo button "Save" hoặc auto-save
- Khi click, từ mô hình Workbook, xây dựng ExcelJS workbook:
- Iterate qua worksheets, ô
- Set cell value, formula (nếu có)
- Apply format từ Cell.format
- Add merge (từ Worksheet.merges)
- Add ảnh (từ Worksheet.images) với anchor
- Dùng
workbook.xlsx.writeBuffer()để tạo Buffer - Trigger download: tạo Blob, URL.createObjectURL, click
<a>tag - Xóa Object URL sau
Đầu vào: Mô hình Workbook (có dữ liệu + format + ảnh + formula)
Đầu ra: File .xlsx tải về máy người dùng
Lưu ý xung đolt:
- Formula: nếu dùng HyperFormula, lưu công thức từ engine; nếu tự xây, lưu chuỗi formula từ Cell.formula
- Image anchor: exceljs dùng twoCellAnchor với EMU (1 EMU = 1/914400 inch); cần convert