Hướng dẫn thực hiện: Xây dựng ứng dụng bảng tính tương tự Excel với chức năng cơ bản

Mục lục bài viết10 mục

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ự:

  1. Đọc/ghi .xlsx: npm install xlsx exceljs jszip
  • xlsx (SheetJS): phân tích nhanh, tương thích tốt
  • exceljs: hỗ trợ đầy đủ OOXML, dễ cập nhật styling và ảnh
  • jszip: xử lý ZIP archive (nội bộ, dependencies của exceljs)
  1. Lưới hiển thị: npm install react-window hoặc npm 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]
  1. Engine công thức: npm install hyperformula hoặ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]
  1. Đị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
  1. Xuất PDF: npm install jspdf html2canvas hoặc npm install pdfkit
  • jspdf + html2canvas: export từ DOM, đơn giản
  • pdfkit: render PDF từ dữ liệu, nặng hơn
  1. Chèn ảnh: Hỗ trợ sẵn trong grid library hoặc xây bằng <input type="file"> + canvas API
  • exceljs hỗ trợ addImage() cho .xlsx
  1. 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 interface
  • src/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:

  1. Tạo file input với <input type="file" accept=".xlsx">
  2. Dùng FileReader để đọc file thành Buffer/Blob
  3. Dùng new ExcelJS.Workbook().xlsx.load(buffer) để parse
  4. Duyệt qua worksheets, ô, merge, style, images và đổi về mô hình dữ liệu riêng
  5. Lưu vào state React

Lưu ý xung đột:

  • exceljs nặng hơn xlsx (file ~200KB vs ~30KB), nhưng hỗ trợ đầy đủ OOXML. Nếu muốn nhẹ, dùng xlsx + 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 grid
  • src/components/Cell.tsx (nếu dùng react-window): component ô đơn
  • src/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:
  1. Tokenizer: cắt chuỗi công thức thành tokens (hàm, (), số, tham chiếu ô, etc.)
  2. AST Parser: xây cây biểu thức từ tokens theo quy tắc precedence
  3. Evaluator: duyệt cây, resolve tham chiếu ô, gọi hàm, trả kết quả
  4. 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 engine
  • src/engine/Parser.ts (nếu tự xây): tokenize + parse AST
  • src/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:

  1. Tạo button "Insert Image" hoặc drag-drop zone
  2. Dùng FileReader để đọc file ảnh thành data URL
  3. Lưu vào Cell.image (URL, width, height) hoặc riêng trong Worksheet.images
  4. Render ảnh trong cell hoặc overlay
  5. Cho phép resize/move bằng mouse (dùng CSS transform hoặc canvas)
  6. 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-drop
  • src/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:

  1. Tạo toolbar (buttons/dropdown) cho các tùy chọn format
  2. Khi user chọn ô, tìm format của ô và highlight trong toolbar
  3. Khi user click nút format, cập nhật Cell.format trong mô hình
  4. Grid render lại với CSS style từ format
  5. 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 buttons
  • src/services/FormatService.ts: chuyển đổi format giữa model và CSS
  • src/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):

  1. Tạo button "Export PDF"
  2. Khi click, ẩn UI (toolbar, etc.), để lại grid
  3. Dùng html2canvas(gridElement) để capture canvas
  4. Tạo PDF: const pdf = new jsPDF()
  5. Thêm canvas vào PDF: pdf.addImage(canvas, ...)
  6. Handle pagination (nếu grid lớn)
  7. 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) → PDF
  • src/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:

  1. Tạo button "Save" hoặc auto-save
  2. 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
  1. Dùng workbook.xlsx.writeBuffer() để tạo Buffer
  2. Trigger download: tạo Blob, URL.createObjectURL, click <a> tag
  3. 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