Build Chrome Extensions · Part 4 — Content Scripts
Inject JavaScript and CSS into web pages: match patterns, run_at timing, the isolated world, injecting your own UI, and the gotchas of touching someone else’s DOM. With an interactive injection lab.
Content script là phần duy nhất của extension có thể đọc và viết lại các trang người dùng xem. Trình chặn quảng cáo, tiêm dark-mode, theo dõi giá, kiểm tra ngữ pháp — đều sống ở đây. Đây là nơi extension trở nên kỳ diệu, và cũng là nơi nó có thể làm hỏng trang của người khác.
Chỉnh mẫu khớp, run_at, và loại tiêm bên dưới rồi xem script tiêm vào một trang giả:
1. Khai báo một content script
Cách đơn giản nhất là khai báo tĩnh trong manifest. Chrome tự tiêm nó vào mọi trang khớp:
{
"content_scripts": [
{
"matches": ["https://*.example.com/*"],
"js": ["content.js"],
"css": ["content.css"],
"run_at": "document_idle"
}
]
}
Không cần mục permissions cho các trang bạn liệt kê trong matches — liệt kê chính là lời xin phép. Các URL đó hiện trên màn hình đồng ý khi cài.
2. Mẫu khớp
Mẫu khớp là <scheme>://<host>/<path> với * là ký tự đại diện.
| Pattern | Khớp |
|---|---|
https://*.example.com/* | mọi subdomain + mọi path |
https://example.com/blog/* | chỉ phần /blog |
*://*/* | mọi trang http/https (phạm vi lớn, đồng ý đáng sợ) |
<all_urls> | đúng nghĩa mọi thứ |
Càng hẹp càng tốt. Mẫu rộng làm user và reviewer sợ, và làm chậm mọi lần tải trang. Dùng exclude_matches để loại trừ ngoại lệ.
3. run_at — script chạy khi nào
| Value | Kích hoạt khi | Dùng cho |
|---|---|---|
document_start | trước khi DOM dựng xong | tiêm CSS sớm để tránh nhấp nháy |
document_end | DOM đã phân tích, trước ảnh | đọc/sửa cấu trúc |
document_idle (default) | sau khi trang ổn định | đa số trường hợp — an toàn nhất |
Nếu bạn truy vấn DOM ở document_start, các phần tử bạn cần có thể chưa tồn tại. Mặc định document_idle trừ khi có lý do khác.
4. Thế giới cô lập
Đây là khái niệm làm ai cũng vấp. Content script dùng chung DOM của trang nhưng chạy trong scope JavaScript riêng.
// content.js (extension's isolated world)
window.__token = "abc"; // invisible to the page
document.title = "Hijacked"; // ✓ DOM is shared, this works
// the page's own script
console.log(window.__token); // undefined — different world
Vậy bạn có thể viết lại DOM, nhưng không thể đọc biến JS của trang hay gọi hàm của nó trực tiếp. Để bắc cầu vào thế giới riêng của trang bạn phải tiêm thẻ <script> vào DOM, nhưng đó là cửa thoát nâng cao — tránh trừ khi bắt buộc.
Chạy cả hai scope trong lab trên để thấy __secret trả về undefined ở phía trang.
5. Tiêm theo lập trình
Thay vì khai báo tĩnh, bạn có thể tiêm theo yêu cầu từ service worker bằng API scripting — tuyệt cho extension activeTab chỉ hành động khi user bấm:
// background.js — needs "scripting" permission
chrome.action.onClicked.addListener(async (tab) => {
await chrome.scripting.executeScript({
target: { tabId: tab.id },
func: () => { document.body.style.filter = "invert(1)"; },
});
});
Bạn có thể truyền func (được serialize và chạy trong trang) hoặc mảng files. Phần 10 sẽ nói kỹ về scripting.
6. Tiêm UI của riêng bạn
Muốn một widget nổi trên trang? Tạo phần tử và thêm vào — nhưng mọi thứ bạn thêm sẽ thừa hưởng CSS của trang. Hai cách phòng vệ:
- Khoanh vùng CSS bằng prefix/thuộc tính độc nhất để style của trang không lẫn vào.
- Dùng Shadow DOM để cô lập thật — lựa chọn sạch nhất:
const host = document.createElement("div");
const shadow = host.attachShadow({ mode: "open" });
shadow.innerHTML = `<style>.box{all:initial;font:14px sans-serif}</style>
<div class="box">Hi from my extension</div>`;
document.body.appendChild(host);
File tham chiếu từ trang (ảnh, font) phải liệt kê trong web_accessible_resources (Phần 2).
7. Bẫy thường gặp
- Trang có thể đổi dưới chân bạn. SPA viết lại DOM sau khi tải; dùng
MutationObserverthay vì chạy một lần. - Đừng làm bẩn global. Bạn là khách; giữ mọi thứ trong scope riêng.
- CSP có thể chặn thẻ
<script>bạn tiêm dù không chặn được JS thế giới cô lập của bạn. chrome.*hạn chế. Content script chỉ córuntime,storage,i18nvà vài cái — nhắn worker cho phần còn lại (Phần 6).
8. Bài tập
1. User vào https://news.example.com/article/42. "matches": ["https://*.example.com/*"] có tiêm không?
Lời giải
Có — *.example.com bao subdomain news và /* bao path. Thử trong lab.
2. Bạn đặt window.helper = ... trong content script nhưng code của trang in undefined. Vì sao?
Lời giải
Thế giới cô lập — content script và trang có scope JS riêng dù chung DOM.
3. Nút bạn tiêm trông hỏng ở vài site nhưng ổn ở site khác. Cách sửa bền nhất là gì?
Lời giải
Render UI trong Shadow DOM để CSS của trang chủ không lọt vào.
Nâng cao:trong lab, đổi run_at sang document_start và suy luận phần tử DOM nào đã/chưa tồn tại.
Điểm chính
- Content script là cách duy nhất đọc/sửa DOM của trang đang xem.
- Mẫu khớp giới hạn nơi chạy — giữ hẹp.
run_atđiều khiển thời điểm;document_idlelà mặc định an toàn.- Thế giới cô lập chung DOM nhưng không chung scope JS.
- Tiêm UI trong Shadow DOM để sống sót CSS của trang.
Tiếp theo
Phần 5 — Service worker nền: trái tim hướng-sự-kiện của MV3, vòng đời (cài, thức, rảnh, tắt), vì sao không có state thường trú, alarms vs timers, và giữ worker đáng tin.