Build Chrome Extensions · Part 11 — Pro Tooling & Build (Vite + CRXJS + TS + React)
Why plain files stop scaling, and how to set up a modern extension project with Vite, CRXJS, TypeScript and React — HMR, bundling, npm packages, and a typed manifest. With an interactive build-pipeline visualizer.
Everything so far used plain .js files you load directly {Mọi thứ đến giờ dùng file .js thuần bạn tải trực tiếp}. That’s perfect for learning, but it stops scaling the moment you want TypeScript, React, npm packages, or fast iteration {Tuyệt để học, nhưng ngừng mở rộng ngay khi bạn muốn TypeScript, React, npm package, hay lặp nhanh}. This part sets up the professional toolchain {Phần này thiết lập bộ công cụ chuyên nghiệp}.
Run a build and start dev mode below to see how source becomes a loadable dist/ {Chạy build và bật dev mode bên dưới để xem source thành dist/ tải được}:
1. Why a build step? {Vì sao cần bước build?}
Recall the MV3 CSP from Part 9: no remote code, no inline scripts {Nhớ CSP của MV3 từ Phần 9: không code từ xa, không script inline}. So to use an npm package you must bundle it into a local file {Vậy để dùng npm package bạn phải đóng gói nó vào file cục bộ}. A build step also gives you TypeScript, JSX, minification, and hot reload {Bước build còn cho bạn TypeScript, JSX, minify, và hot reload}.
CRXJS is a Vite plugin built specifically for extensions {CRXJS là plugin Vite làm riêng cho extension}. It understands manifest.json, bundles every entry point, rewrites paths, and gives you HMR for the popup/options and auto-reload for content scripts {Nó hiểu manifest.json, đóng gói mọi entry point, viết lại đường dẫn, và cho bạn HMR cho popup/options và auto-reload cho content script}.
2. Scaffolding the project {Dựng project}
npm create vite@latest my-extension -- --template react-ts
cd my-extension
npm install
npm install -D @crxjs/vite-plugin@beta
Versions move fast — check the CRXJS docs for the current install command and Vite compatibility {Phiên bản đổi nhanh — xem tài liệu CRXJS cho lệnh cài hiện tại và tương thích Vite}.
3. The manifest as typed config {Manifest như config có kiểu}
Instead of a hand-edited JSON, define the manifest in TypeScript and get autocompletion and type-checking {Thay vì JSON sửa tay, định nghĩa manifest trong TypeScript và có autocomplete cùng kiểm tra kiểu}:
// manifest.config.ts
import { defineManifest } from "@crxjs/vite-plugin";
export default defineManifest({
manifest_version: 3,
name: "My Extension",
version: "1.0.0",
action: { default_popup: "src/popup/index.html" },
background: { service_worker: "src/background.ts", type: "module" },
content_scripts: [
{ matches: ["https://*/*"], js: ["src/content.ts"] },
],
permissions: ["storage", "activeTab"],
});
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.config";
export default defineConfig({
plugins: [react(), crx({ manifest })],
});
Point entry points at your source files; CRXJS emits the final hashed paths into dist/manifest.json {Trỏ entry point vào file source; CRXJS phát đường dẫn băm cuối cùng vào dist/manifest.json}.
4. The dev loop {Vòng lặp dev}
npm run dev # starts Vite, writes dist/ and watches
Load dist/ once as an unpacked extension (Part 1) {Tải dist/ một lần như extension chưa đóng gói (Phần 1)}. Now {Bây giờ}:
- Edit the popup/options → HMR updates instantly, state preserved {Sửa popup/options → HMR cập nhật tức thì, giữ state}.
- Edit a content script → CRXJS auto-reloads the affected tabs {Sửa content script → CRXJS tự tải lại các tab liên quan}.
- Edit the service worker → it reloads automatically {Sửa service worker → nó tự tải lại}.
No more manual “reload extension” clicks for most changes {Không còn click “reload extension” thủ công cho đa số thay đổi}. Trigger the “Save an edit” button in the visualizer to see the HMR path light up {Bấm nút “Save an edit” trong trình trực quan để xem đường HMR sáng lên}.
5. Typing the chrome.* APIs {Gắn kiểu cho chrome.*}
Install the official types so chrome.* is fully typed {Cài types chính thức để chrome.* có kiểu đầy đủ}:
npm install -D @types/chrome
Now chrome.storage.local.get returns typed results, and you catch mistakes at compile time {Giờ chrome.storage.local.get trả kết quả có kiểu, và bạn bắt lỗi lúc biên dịch}. Combine with the typed storage wrapper from Part 7 for end-to-end safety {Kết hợp với wrapper storage có kiểu từ Phần 7 để an toàn đầu-cuối}.
6. A sane project structure {Cấu trúc project hợp lý}
my-extension/
├─ manifest.config.ts # typed manifest
├─ vite.config.ts
├─ src/
│ ├─ background.ts # service worker
│ ├─ content.ts # content script
│ ├─ popup/ # React popup
│ │ ├─ index.html
│ │ └─ Popup.tsx
│ ├─ options/ # React options page
│ ├─ lib/storage.ts # typed storage wrapper (Part 7)
│ └─ lib/messages.ts # shared message types
└─ dist/ # build output — this is what you load/zip
Share message-shape types and storage helpers in lib/ so every context agrees on the same contracts {Chia sẻ kiểu hình-dạng-tin-nhắn và helper storage trong lib/ để mọi ngữ cảnh đồng thuận cùng hợp đồng}.
7. Building for production {Build cho production}
npm run build # minified, tree-shaken dist/
The output dist/ is exactly what you zip and upload to the Chrome Web Store (Part 12) {dist/ xuất ra chính là thứ bạn nén và tải lên Chrome Web Store (Phần 12)}. CRXJS handles code-splitting and emits the production manifest.json automatically {CRXJS lo code-splitting và phát manifest.json production tự động}.
8. Exercises {Bài tập}
1. You import { z } from "zod" in your popup and it works after bundling, but a <script src="https://cdn.../zod"> did not. Why is bundling required? {Bạn import { z } from "zod" trong popup và nó chạy sau khi bundle, nhưng <script src="https://cdn.../zod"> thì không. Vì sao cần bundle?}
Solution {Lời giải}
MV3’s CSP forbids remote code; the bundler inlines the package into a local file you ship {CSP của MV3 cấm code từ xa; bundler nội tuyến package vào file cục bộ bạn ship}.
2. Which entry points should manifest.config.ts reference — src/ or dist/? {manifest.config.ts nên tham chiếu entry point nào — src/ hay dist/?}
Solution {Lời giải}
src/ files. CRXJS transforms them and writes the correct hashed dist/ paths into the emitted manifest.json {File src/. CRXJS chuyển đổi chúng và ghi đường dẫn dist/ băm đúng vào manifest.json phát ra}.
3. After npm run build, what exactly do you upload to the Chrome Web Store? {Sau npm run build, bạn tải chính xác cái gì lên Chrome Web Store?}
Solution {Lời giải}
A zip of the dist/ folder — the built, minified output, not the source {Một zip của thư mục dist/ — kết quả đã build, minify, không phải source}.
Stretch {Nâng cao}: in the visualizer, run dev mode then “Save an edit” and note only the popup hot-reloads while the rest of dist/ stays put — that’s HMR {trong trình trực quan, chạy dev mode rồi “Save an edit” và để ý chỉ popup hot-reload trong khi phần còn lại của dist/ giữ nguyên — đó là HMR}.
Key takeaways {Điểm chính}
- A build step is required to use npm packages under MV3’s CSP {Bước build là bắt buộc để dùng npm package dưới CSP của MV3}.
- Vite + CRXJS bundles entry points, rewrites manifest paths, and gives HMR/auto-reload {Vite + CRXJS đóng gói entry point, viết lại đường dẫn manifest, và cho HMR/auto-reload}.
- Define the manifest as typed config; add
@types/chrome{Định nghĩa manifest như config có kiểu; thêm@types/chrome}. - Share message types and storage helpers in
lib/{Chia sẻ kiểu tin nhắn và helper storage tronglib/}. - You ship the built
dist/, never the source {Bạn shipdist/đã build, không bao giờ source}.
Next up {Tiếp theo}
Part 12 — Publish, auto-update, cross-browser & capstone: packaging and uploading to the Chrome Web Store, the review process, versioning and auto-update, porting to Firefox/Edge, and a capstone extension that ties the whole series together {Phần 12 — Phát hành, tự cập nhật, đa trình duyệt & capstone: đóng gói và tải lên Chrome Web Store, quy trình duyệt, đánh phiên bản và tự cập nhật, port sang Firefox/Edge, và một extension capstone gắn cả series lại}.