Phát triển ứng dụng Desktop nhanh hơn: Hướng dẫn về Electron, React và TypeScript

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Cầu nối giữa Web và Desktop

Bạn không còn cần hàng năm kinh nghiệm C++ hay C# để xây dựng một ứng dụng desktop đẳng cấp thế giới. Các ứng dụng như Discord, Slack và VS Code đã chứng minh rằng công nghệ web có thể thống trị desktop. Bằng cách kết hợp Electron.js với ReactTypeScript, bạn có thể tận dụng hệ sinh thái thư viện khổng lồ trong khi vẫn đảm bảo tính an toàn kiểu dữ liệu (type safety) cần thiết cho các phần mềm phức tạp.

Sau khi triển khai nhiều công cụ thực tế với bộ công nghệ này, tôi nhận thấy đây là “điểm ngọt” (sweet spot) cho tốc độ phát triển. Độ ổn định rất ấn tượng nếu bạn tôn trọng ranh giới giữa giao diện người dùng (UI) và hệ thống. Hãy bỏ qua lý thuyết suông và bắt đầu xây dựng một cấu trúc sẵn sàng cho môi trường production.

Từ con số 0 đến Hello World trong 5 phút

Cấu hình thủ công thường dẫn đến sự ức chế. Đối với quy trình làm việc hiện đại, tôi khuyên dùng electron-vite. Nó mang lại tính năng Hot Module Replacement (HMR) cực nhanh — thường làm mới trong chưa đầy 100ms — và tự động xử lý việc biên dịch TypeScript cho bạn.

Mở terminal và chạy lệnh sau:

npm create @electron-vite/project@latest my-electron-app -- --template react-ts

Di chuyển vào thư mục dự án và khởi động môi trường phát triển:

cd my-electron-app
npm install
npm run dev

Một cửa sổ sẽ hiện lên ngay lập tức. Hãy chú ý cấu trúc thư mục: main, renderer, và preload. Đây không chỉ là cách tổ chức; đó là một yêu cầu về bảo mật. Việc tách biệt các lớp này giúp ngăn chặn lỗ hổng ở giao diện người dùng chiếm quyền kiểm soát hoàn toàn máy tính của người dùng.

Cơ chế hoạt động bên dưới

Để xây dựng bất cứ thứ gì quan trọng, bạn phải hiểu mô hình đa tiến trình của Electron. Nó hoạt động giống như một cụm các chương trình hơn là một ứng dụng đơn lẻ.

1. Tiến trình chính (Main Process)

Đây là tiến trình “đầu não”. Nó chạy trong Node.js, quản lý vòng đời của ứng dụng và nắm giữ chìa khóa truy cập vào hệ điều hành. Nó có thể kích hoạt các hộp thoại hệ thống, quản lý khay hệ thống (system tray) và tương tác trực tiếp với hệ thống tệp.

2. Tiến trình kết xuất (Renderer Process)

Về cơ bản, đây là một tab Chrome chuyên dụng. Mã React của bạn nằm ở đây. Theo mặc định, nó được chạy trong sandbox. Nó không thể đọc tệp cục bộ hoặc thực thi các lệnh shell. Hạn chế này giúp bảo vệ người dùng của bạn.

3. Kịch bản Preload (Preload Script)

Hãy coi đây là một khoang đệm bảo mật. Nó cho phép tiến trình Main và Renderer giao tiếp với nhau mà không làm lộ các hàm nội bộ nguy hiểm của Node.js ra frontend. Bạn sẽ sử dụng contextBridge để truyền các hàm cụ thể và an toàn vào các component React.

Dưới đây là một thiết lập sạch cho src/preload/index.ts:

import { contextBridge, ipcRenderer } from 'electron'

contextBridge.exposeInMainWorld('electronAPI', {
  saveFile: (content: string) => ipcRenderer.invoke('dialog:saveFile', content),
  onUpdateStatus: (callback: (status: string) => void) => 
    // Lắng nghe sự kiện thay đổi trạng thái
    ipcRenderer.on('status-changed', (_event, value) => callback(value))
})

IPC trong thực tế: Lưu tệp

Giao tiếp liên tiến trình (IPC) là trái tim của việc phát triển Electron. Hãy cùng triển khai tính năng cho phép người dùng lưu một tệp văn bản vào ổ đĩa cục bộ.

Bước 1: Xử lý ở tiến trình Main

Trong src/main/index.ts, chúng ta lắng nghe yêu cầu và mở hộp thoại lưu tệp của hệ thống:

import { ipcMain, dialog } from 'electron'
import fs from 'fs'

ipcMain.handle('dialog:saveFile', async (event, content: string) => {
  const { filePath } = await dialog.showSaveDialog({
    title: 'Lưu công việc của bạn',
    defaultPath: 'note.txt'
  })

  if (filePath) {
    fs.writeFileSync(filePath, content, 'utf-8')
    return true
  }
  return false
})

Bước 2: Triển khai trong React

Đầu tiên, mở rộng interface Window để TypeScript không báo lỗi. Tạo tệp src/renderer/src/env.d.ts:

interface Window {
  electronAPI: {
    saveFile: (content: string) => Promise<boolean>
  }
}

Bây giờ, hãy kích hoạt việc lưu từ component React của bạn:

import { useState } from 'react'

function App() {
  const [text, setText] = useState('')

  const handleSave = async () => {
    const success = await window.electronAPI.saveFile(text)
    if (success) alert('Lưu tệp thành công!')
  }

  return (
    <div className="p-4">
      <textarea 
        className="w-full border" 
        onChange={(e) => setText(e.target.value)} 
      />
      <button onClick={handleSave}>Lưu vào Desktop</button>
    </div>
  )
}

Đóng gói để phân phối

Một ứng dụng tuyệt vời sẽ vô dụng nếu bạn không thể phân phối nó. Tôi sử dụng electron-builder vì nó đơn giản hóa việc ký mã (code signing) và tự động cập nhật đầy phức tạp. Nó có thể tạo ra các tệp .exe, .dmg, và .AppImage từ một cấu hình duy nhất.

Cấu hình package.json của bạn sẽ trông như thế này:

"build": {
  "appId": "com.yourname.myapp",
  "productName": "My Electron App",
  "win": { "target": ["nsis"] },
  "mac": { "target": ["dmg"] }
}

Chạy npm run build:win hoặc npm run build:mac để tạo bộ cài đặt. Quá trình này thường mất khoảng 1-2 phút tùy thuộc vào tài nguyên của bạn.

Lời khuyên “xương máu” cho lập trình viên

  • Chú ý dung lượng: Một ứng dụng Electron “Hello World” bắt đầu ở mức khoảng 80MB. Điều này là do bạn đang đóng gói kèm theo một bản sao đầy đủ của Chromium. Hãy giữ cho node_modules gọn nhẹ để tránh làm ứng dụng phình to hơn.
  • Trải nghiệm Native UX: Thanh cuộn web và việc bôi đen văn bản là những dấu hiệu cho thấy ứng dụng không phải là native. Hãy sử dụng user-select: none; trong CSS cho các thành phần giao diện. Vô hiệu hóa việc kéo ảnh bằng -webkit-user-drag: none; để mang lại cảm giác như một ứng dụng desktop thực thụ.
  • Bảo mật là không thể thỏa hiệp: Không bao giờ thiết lập nodeIntegration: true. Đó là một lỗ hổng bảo mật cực lớn. Luôn sử dụng kịch bản preload để làm cầu nối.
  • Bất đồng bộ mọi thứ: Đừng bao giờ sử dụng các lời gọi hệ thống tệp đồng bộ (fs.readFileSync) trong tiến trình Main nếu có thể tránh được. Nó sẽ làm đóng băng toàn bộ giao diện người dùng của bạn.

Electron là một framework cực kỳ đáng giá. Nó cho phép bạn tiếp cận người dùng trên desktop bằng chính những kỹ năng web bạn đã có. Hãy bắt đầu với một công cụ nhỏ, nắm vững giao tiếp IPC, và bạn sẽ sẵn sàng để xuất bản phần mềm chuyên nghiệp trong nháy mắt.

Share: