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 scripts are the only part of your extension that can read and rewrite the pages a user visits {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}. Ad blockers, dark-mode injectors, price trackers, grammar checkers — all of them live here {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}. This is where an extension feels magical, and also where it can break someone else’s site {Đâ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}.
Tune the match pattern, run_at, and injection kind below and watch a script inject into a fake page {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. Declaring a content script {Khai báo một content script}
The simplest way is static declaration in the manifest {Cách đơn giản nhất là khai báo tĩnh trong manifest}. Chrome injects it automatically into every page that matches {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"
}
]
}
No permissions entry is needed for the pages you list in matches — listing them is the request {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}. Those URLs show up on the install consent screen {Các URL đó hiện trên màn hình đồng ý khi cài}.
2. Match patterns {Mẫu khớp}
A match pattern is <scheme>://<host>/<path> where * is a wildcard {Mẫu khớp là <scheme>://<host>/<path> với * là ký tự đại diện}.
| Pattern | Matches {Khớp} |
|---|---|
https://*.example.com/* | any subdomain + any path on example.com {mọi subdomain + mọi path} |
https://example.com/blog/* | only the /blog section {chỉ phần /blog} |
*://*/* | every http/https page (huge scope, scary consent) {mọi trang http/https (phạm vi lớn, đồng ý đáng sợ}) |
<all_urls> | literally everything {đúng nghĩa mọi thứ} |
Be as narrow as possible {Càng hẹp càng tốt}. Broad patterns frighten users and reviewers, and slow every page load {Mẫu rộng làm user và reviewer sợ, và làm chậm mọi lần tải trang}. Use exclude_matches to carve out exceptions {Dùng exclude_matches để loại trừ ngoại lệ}.
3. run_at — when the script runs {run_at — script chạy khi nào}
| Value | Fires when {Kích hoạt khi} | Use for {Dùng cho} |
|---|---|---|
document_start | before the DOM is built {trước khi DOM dựng xong} | injecting CSS early to avoid flash-of-unstyled-content {tiêm CSS sớm để tránh nhấp nháy} |
document_end | DOM parsed, before images/subresources {DOM đã phân tích, trước ảnh} | reading/modifying structure {đọc/sửa cấu trúc} |
document_idle (default) | after the page settles {sau khi trang ổn định} | most cases — safest {đa số trường hợp — an toàn nhất} |
If you query the DOM at document_start, the elements you want may not exist yet {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}. Default to document_idle unless you have a reason {Mặc định document_idle trừ khi có lý do khác}.
4. The isolated world {Thế giới cô lập}
This is the concept that trips everyone up {Đây là khái niệm làm ai cũng vấp}. A content script shares the page’s DOM but runs in its own JavaScript scope {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
So you can rewrite the DOM, but you cannot read the page’s JS variables or call its functions directly {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}. To bridge into the page’s own world you must inject a <script> tag into the DOM, but that’s an advanced escape hatch — avoid it unless required {Để 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}.
Run both scopes in the lab above to see __secret come back undefined on the page side {Chạy cả hai scope trong lab trên để thấy __secret trả về undefined ở phía trang}.
5. Programmatic injection {Tiêm theo lập trình}
Instead of static declaration, you can inject on demand from the service worker with the scripting API — great for activeTab extensions that only act when the user clicks {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)"; },
});
});
You can pass a func (serialized and run in the page) or a files array {Bạn có thể truyền func (được serialize và chạy trong trang) hoặc mảng files}. We cover scripting fully in Part 10 {Phần 10 sẽ nói kỹ về scripting}.
6. Injecting your own UI {Tiêm UI của riêng bạn}
Want a floating widget on the page? Create elements and append them — but everything you add inherits the page’s CSS {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}. Two defenses {Hai cách phòng vệ}:
- Scope your CSS with a unique prefix or attribute so the page’s styles don’t bleed in {Khoanh vùng CSS bằng prefix/thuộc tính độc nhất để style của trang không lẫn vào}.
- Use a Shadow DOM for true isolation — the cleanest option {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);
Files referenced from the page (images, fonts) must be listed under web_accessible_resources (Part 2) {File tham chiếu từ trang (ảnh, font) phải liệt kê trong web_accessible_resources (Phần 2)}.
7. Gotchas {Bẫy thường gặp}
- The page can change under you. SPAs rewrite the DOM after load; use a
MutationObserverinstead of running once {Trang có thể đổi dưới chân bạn. SPA viết lại DOM sau khi tải; dùngMutationObserverthay vì chạy một lần}. - Don’t pollute globals. You’re a guest; keep everything in your own scope {Đừng làm bẩn global. Bạn là khách; giữ mọi thứ trong scope riêng}.
- CSP can block injected
<script>tags even though it can’t block your isolated-world JS {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}. - Limited
chrome.*. Content scripts only getruntime,storage,i18nand a few others — message the worker for the rest (Part 6) {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. Exercises {Bài tập}
1. A user visits https://news.example.com/article/42. Will "matches": ["https://*.example.com/*"] inject? {User vào https://news.example.com/article/42. "matches": ["https://*.example.com/*"] có tiêm không?}
Solution {Lời giải}
Yes — *.example.com covers the news subdomain and /* covers the path {Có — *.example.com bao subdomain news và /* bao path}. Test it in the lab {Thử trong lab}.
2. You set window.helper = ... in your content script but the page’s own code logs undefined. Why? {Bạn đặt window.helper = ... trong content script nhưng code của trang in undefined. Vì sao?}
Solution {Lời giải}
Isolated world — the content script and page have separate JS scopes even though they share the DOM {Thế giới cô lập — content script và trang có scope JS riêng dù chung DOM}.
3. Your injected button looks broken on some sites but fine on others. What’s the most robust fix? {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ì?}
Solution {Lời giải}
Render your UI inside a Shadow DOM so the host page’s CSS can’t leak in {Render UI trong Shadow DOM để CSS của trang chủ không lọt vào}.
Stretch {Nâng cao}: in the lab, switch run_at to document_start and reason about which DOM elements would and wouldn’t exist yet {trong lab, đổi run_at sang document_start và suy luận phần tử DOM nào đã/chưa tồn tại}.
Key takeaways {Điểm chính}
- Content scripts are the only way to read/modify the visited page’s DOM {Content script là cách duy nhất đọc/sửa DOM của trang đang xem}.
- Match patterns scope where they run — keep them narrow {Mẫu khớp giới hạn nơi chạy — giữ hẹp}.
run_atcontrols timing;document_idleis the safe default {run_atđiều khiển thời điểm;document_idlelà mặc định an toàn}.- The isolated world shares DOM but not JS scope {Thế giới cô lập chung DOM nhưng không chung scope JS}.
- Inject UI inside a Shadow DOM to survive hostile page CSS {Tiêm UI trong Shadow DOM để sống sót CSS của trang}.
Next up {Tiếp theo}
Part 5 — The background service worker: the event-driven heart of MV3, the lifecycle (install, wake, idle, terminate), why there’s no persistent state, alarms vs timers, and keeping the worker reliable {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}.