jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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ờ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.

PatternKhớ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

ValueKích hoạt khiDùng cho
document_starttrước khi DOM dựng xongtiêm CSS sớm để tránh nhấp nháy
document_endDOM đã 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ệ:

  1. Khoanh vùng CSS bằng prefix/thuộc tính độc nhất để style của trang không lẫn vào.
  2. 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 MutationObserver thay 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, i18n và 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/* 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_idle là 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.