Mở rộng Browser Extension: Hướng dẫn triển khai Production với TypeScript và Manifest V3

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

Sự mong manh của quy trình phát triển Extension kiểu cũ

Sáu tháng trước, tôi tiếp nhận một extension cũ được xây dựng trên Manifest V2 và JavaScript thuần (vanilla JS). Mã nguồn lúc đó là một mớ hỗn độn các biến toàn cục và cơ chế truyền tin nhắn (message passing) lỏng lệo. Mỗi khi chúng tôi đẩy một bản cập nhật, khoảng 3% trong số 50.000 người dùng báo cáo lỗi sập ứng dụng ngầm. Nếu không có tính năng an toàn kiểu (type safety), chỉ một lỗi chính tả nhỏ trong dữ liệu tin nhắn (message payload) cũng đủ làm đứt gãy liên kết giữa background script và content script. Những lỗi này thường lọt qua khâu kiểm thử QA thủ công và chỉ lộ ra khi bản cập nhật đã lên Chrome Web Store.

Khi Google bắt đầu lộ trình ngừng hỗ trợ Manifest V2, chúng tôi đã chọn viết lại toàn bộ thay vì chắp vá. Chúng tôi cần một hệ thống có thể xử lý tính chất tạm thời (ephemeral) của Manifest V3 (MV3) Service Workers, đồng thời mang lại trải nghiệm lập trình hiện đại. Chúng tôi muốn ngừng dựa dẫm vào “lập trình dựa trên hy vọng” và hướng tới một môi trường ổn định, an toàn về kiểudễ dàng mở rộng.

Tại sao Manifest V3 và JavaScript thường thất bại trong môi trường Production

Việc chuyển sang Manifest V3 đã thay thế các background page duy trì liên tục bằng Service Workers. Không giống như các background page cũ luôn hoạt động chừng nào trình duyệt còn mở, Service Worker hoạt động rất nghiêm ngặt. Chúng thường tự tắt sau 30 giây không hoạt động để tiết kiệm bộ nhớ hệ thống. Thay đổi này tạo ra một sự khó khăn lớn cho việc quản lý trạng thái (state management).

Trong môi trường JavaScript tiêu chuẩn, có ba vấn đề thường làm hỏng tính ổn định của bản production:

  • Lỗi sai biệt kiểu dữ liệu ngầm (Silent Type Mismatches): Gửi dữ liệu qua chrome.runtime.sendMessage không nhận được bất kỳ phản hồi nào từ IDE. Nếu background script của bạn mong đợi userId nhưng content script lại gửi user_id, extension sẽ hỏng mà không có thông báo lỗi. Điều này chiếm gần 90% các lỗi giao tiếp của chúng tôi.
  • Trạng thái ngủ của Service Worker: Các lập trình viên thường quên rằng các biến toàn cục trong Service Worker sẽ bị xóa sạch khi nó ở trạng thái ngủ. Nếu bạn không có cách cấu trúc để khởi tạo lại trạng thái từ bộ nhớ (storage), extension sẽ ngừng hoạt động ngay khi trình duyệt quyết định thu hồi bộ nhớ.
  • Cấu hình cồng kềnh: Việc quản lý nhiều điểm đầu vào (entry point) cho popup, trang tùy chọn và content script trong một tập lệnh build thủ công là một công thức dẫn đến sự kiệt sức khi bảo trì.

Đánh giá các chiến lược phát triển

Chúng tôi đã so sánh ba quy trình làm việc phổ biến trước khi chốt bộ công cụ (stack) hiện tại.

1. Cách làm “Cổ điển” (Vanilla JS + Không có Build Step)

Bạn viết mã JS thuần và liên kết trực tiếp trong manifest. Cách này nhanh đối với các dự án sở thích cuối tuần. Tuy nhiên, nó thiếu khả năng import module và kiểm tra kiểu dữ liệu. Một khi dự án vượt quá 1.000 dòng code, việc bảo trì sẽ trở thành một cơn ác mộng.

2. Cấu hình Webpack hoặc Rollup thủ công

Thiết lập này cho phép sử dụng TypeScript và nén mã nguồn (minification). Nhưng việc cấu hình Webpack để theo dõi manifest.json đồng thời xử lý HMR (Hot Module Replacement) cho các content script là một việc cực kỳ tẻ nhạt. Bạn thường mất nhiều thời gian để sửa quy trình build hơn là viết tính năng.

3. Stack hiện đại: TypeScript + Vite + CRXJS

Vite rất nhanh, thường khởi động trong chưa đầy 300ms. Plugin CRXJS coi manifest.json của bạn là điểm đầu vào thực tế. Nó tự động phát hiện các script được tham chiếu trong manifest và đóng gói chúng. Kể từ khi chuyển sang stack này, thời gian build của chúng tôi đã giảm 70% và chúng tôi không gặp bất kỳ lỗi đường dẫn nào liên quan đến manifest nữa.

Kiến trúc chuẩn Production

Một extension mạnh mẽ cần xử lý việc truyền tin nhắn an toàn về kiểu và duy trì trạng thái dữ liệu. Đây là thiết lập chúng tôi sử dụng cho các bản build production.

Khởi tạo dự án

Bắt đầu bằng cách khởi tạo một dự án Vite. Khuyên dùng PNPM để giải quyết các phụ thuộc nhanh hơn.

pnpm create vite my-extension --template react-ts
cd my-extension
pnpm install @crxjs/vite-plugin@beta -D

Định nghĩa Manifest chặt chẽ

Trong MV3, file manifest.json phải rõ ràng. Bằng cách sử dụng TypeScript để định nghĩa manifest, bạn sẽ bắt được các lỗi schema trước khi thử tải extension vào Chrome.

// manifest.config.ts
import { defineManifest } from '@crxjs/vite-plugin'

export default defineManifest({
  manifest_version: 3,
  name: 'Extension Chuẩn Production',
  version: '1.0.0',
  action: { default_popup: 'index.html' },
  background: {
    service_worker: 'src/background/index.ts',
    type: 'module',
  },
  content_scripts: [
    {
      matches: ['https://*.google.com/*'],
      js: ['src/content/index.ts'],
    },
  ],
  permissions: ['storage', 'tabs'],
})

Truyền tin nhắn an toàn về kiểu

Để loại bỏ các lỗi giao tiếp, hãy tạo một file chứa các kiểu dữ liệu dùng chung. Cả background và content script đều nên import các định nghĩa này. Điều này đảm bảo dữ liệu tin nhắn của bạn luôn đồng bộ.

// src/types/messaging.ts
export type ActionType = 'FETCH_DATA' | 'UPDATE_UI';

export interface ExtensionMessage {
  type: ActionType;
  payload?: Record<string, unknown>;
}

export const sendMessage = (message: ExtensionMessage) => {
  return chrome.runtime.sendMessage(message);
}

Xử lý Service Worker tạm thời

Hãy mặc định rằng background script của bạn sẽ tắt sau mỗi 30 giây. Thay vì dùng biến toàn cục, hãy sử dụng chrome.storage.local để duy trì trạng thái. Nếu bạn đang thực hiện một tác vụ bất đồng bộ, bạn phải trả về true trong trình lắng nghe tin nhắn để giữ cho kênh kết nối luôn mở.

// src/background/index.ts
chrome.runtime.onMessage.addListener((message: ExtensionMessage, sender, sendResponse) => {
  if (message.type === 'FETCH_DATA') {
    // Trả về true để báo cho Chrome rằng chúng ta sẽ phản hồi bất đồng bộ
    performAsyncAction().then(data => sendResponse({ success: true, data }));
    return true; 
  }
});

Debug ở ba nơi khác nhau

Debug một extension khó hơn debug một website. Bạn phải theo dõi ba môi trường riêng biệt:

  1. Popup: Nhấp chuột phải vào biểu tượng extension và chọn “Inspect” (Kiểm tra).
  2. Content Scripts: Mở DevTools tiêu chuẩn trên trang web cụ thể nơi script của bạn chạy.
  3. Background Worker: Truy cập chrome://extensions và nhấp vào liên kết “service worker” dưới thẻ thông tin extension của bạn.

Nếu bạn thấy lỗi “Could not establish connection”, điều đó thường có nghĩa là content script của bạn chưa được tải. TypeScript giúp phát hiện những vấn đề này lúc build, nhưng bạn vẫn cần kiểm tra xem một tab đã tải xong hoàn toàn chưa trước khi gửi tin nhắn.

Lời kết

Chuyển sang quy trình ưu tiên TypeScript đã thay đổi chu kỳ phát triển của chúng tôi. Việc cấu hình ban đầu mất khoảng mười phút, nhưng nó tiết kiệm hàng chục giờ debug các lỗi sập lúc runtime. Bằng cách đối xử với extension như một ứng dụng web hiện đại—với quy trình build hoàn chỉnh và các kiểu dữ liệu chặt chẽ—bạn sẽ xây dựng được một công cụ luôn ổn định ngay cả khi các API trình duyệt thay đổi.

Share: