נעים להכיר: שרתי proxy של ES2015

אדי אוסמאני
אדי אוסמאני

שרתי proxy של ES2015 (ב-Chrome 49 ואילך) מספקים JavaScript עם ממשק API של אינטראקציה, ומאפשר לנו ללכוד או ליירט את כל הפעולות באובייקט יעד ולשנות את האופן שבו היעד הזה פועל.

לשרתי proxy יש הרבה שימושים, כולל:

  • חטיפת מסירה
  • וירטואליזציה של אובייקטים
  • ניהול המשאבים
  • יצירת פרופילים או רישום ביומן לצורך ניפוי באגים
  • אבטחה ובקרת גישה
  • חוזים לשימוש באובייקטים

ממשק ה-API של Proxy מכיל בנאי Proxy שלוקח אובייקט יעד מוגדר ואובייקט handler.

var target = { /* some properties */ };
var handler = { /* trap functions */ };
var proxy = new Proxy(target, handler);

ההתנהגות של שרת proxy נשלטת על ידי ה-handler, שיכול לשנות את ההתנהגות המקורית של אובייקט target בכמה דרכים שימושיות. המטפל מכיל שיטות trap אופציונליות (למשל .get(), .set(), .apply()) שנקראות כאשר הפעולה המתאימה מתבצעת בשרת ה-proxy.

חטיפת מסירה

נתחיל באובייקט פשוט והוספה של תווכה ליירוט באמצעות ממשק Proxy API. חשוב לזכור, הפרמטר הראשון שמועבר לבונה הוא היעד (האובייקט שעובר דרך שרת proxy) והפרמטר השני הוא הגורם המטפל (שרת ה-proxy עצמו). כאן אנחנו יכולים להוסיף קטעי 'תוכן מושך' ('hooks) לחומרים שלנו למתקני למידה, למתכנתים או להתנהגות אחרת.

var target = {};

var superhero = new Proxy(target, {
    get: function(target, name, receiver) {
        console.log('get was called for:', name);
        return target[name];
    }
});

superhero.power = 'Flight';
console.log(superhero.power);

כדי שנריץ את הקוד שלמעלה ב-Chrome 49, תקבלו את הדברים הבאים:

get was called for: power  
"Flight"

כפי שניתן לראות בפועל, ביצוע get או set properties של המאפיינים שלנו באובייקט של שרת ה-proxy גרם להפעלת קריאה ברמת המטא ל-trap המתאימה ב-handler. פעולות של handler כוללות קריאה של מאפיינים, הקצאת נכסים והחלת פונקציות, וכולן מועברות ל-trap המתאים.

פונקציית trap יכולה, אם היא בוחרת, ליישם פעולה באופן שרירותי (למשל, העברת הפעולה לאובייקט היעד). זה אכן מה שקורה כברירת מחדל אם לא מצוינת מלכודת. לדוגמה, כאן יש שרת proxy להעברה ללא תפעול (no-op) המבצע את הפעולה הבאה:

var target = {};

var proxy = new Proxy(target, {});
    // operation forwarded to the target
proxy.paul = 'irish';
// 'irish'. The operation has been  forwarded
console.log(target.paul);

עד עכשיו בדקנו אובייקטים פשוטים בשרת proxy, אבל באותה מידה אנחנו יכולים להעביר אובייקט של פונקציה, כאשר פונקציה היא היעד שלנו. הפעם נשתמש בטראפ handler.apply():

// Proxying a function object
function sum(a, b) {
    return a + b;
}

var handler = {
    apply: function(target, thisArg, argumentsList) {
        console.log(`Calculate sum: ${argumentsList}`);
        return target.apply(thisArg, argumentsList);
    }
};

var proxy = new Proxy(sum, handler);
proxy(1, 2);
// Calculate sum: 1, 2
// 3

זיהוי שרתי proxy

אפשר לזהות את שרת ה-proxy באמצעות האופרטורים לשוויון ב-JavaScript (== ו-===). כפי שאנחנו יודעים, כשמחילים אותם על שני אובייקטים, האופרטורים האלה משווים בין זהויות של אובייקטים. הדוגמה הבאה ממחישה את ההתנהגות הזו. השוואה בין שני שרתי proxy נפרדים מחזירה FALSE למרות שהיעדים הבסיסיים זהים. בעור דומה, אובייקט היעד שונה מכל אחד משרתי ה-proxy שלו:

// Continuing previous example

var proxy2 = new Proxy (sum, handler);
(proxy==proxy2); // false
(proxy==sum); // false

במצב אידיאלי, לא אמורה להיות אפשרות להבחין בין שרת proxy לאובייקט שאינו שרת proxy, כך שהוספת שרת proxy לא תשפיע באמת על התוצאה של האפליקציה. זו אחת הסיבות לכך ש-Proxy API לא כולל דרך לבדוק אם אובייקט הוא שרת proxy ולא מספק מלכודות לכל הפעולות באובייקטים.

תרחישים לדוגמה

כפי שצוין, לשרתי proxy יש מגוון רחב של תרחישים לדוגמה. הרבה מהתחומים שלמעלה, כמו בקרת גישה ופרופיל, נמצאים תחת רכיבי wrapper: שרתי proxy שעוטפים אובייקטים אחרים באותו 'מרחב כתובת'. הוזכר גם וירטואליזציה. אובייקטים וירטואליים הם שרתי proxy שמחקים אובייקטים אחרים בלי שהאובייקטים האלה צריכים להיות באותו מרחב כתובות. לדוגמה: אובייקטים מרוחקים (המדמים אובייקטים במרחבים אחרים) ועתידים שקופים (אמולציה של תוצאות שעדיין לא מחושבות).

שרתי Proxy כמו מטפלים

תרחיש נפוץ למדי לגבי מטפלים בשרת proxy הוא ביצוע בדיקות של אימות או בקרת גישה לפני ביצוע פעולה על אובייקט עטוף. הפעולה מועברת רק אם הבדיקה מצליחה. דוגמת האימות הבאה ממחישה זאת:

var validator = {
    set: function(obj, prop, value) {
    if (prop === 'yearOfBirth') {
        if (!Number.isInteger(value)) {
        throw new TypeError('The yearOfBirth is not an integer');
        }

        if (value > 3000) {
        throw new RangeError('The yearOfBirth seems invalid');
        }
    }

    // The default behavior to store the value
    obj[prop] = value;
    }
};

var person = new Proxy({}, validator);

person.yearOfBirth = 1986;
console.log(person.yearOfBirth); // 1986
person.yearOfBirth = 'eighties'; // Throws an exception
person.yearOfBirth = 3030; // Throws an exception

דוגמאות מורכבות יותר של הדפוס הזה עשויות להביא בחשבון את כל הפעולות השונות שגורמי ה-handler של הפעולות השונות יכולים ליירט. אפשר לדמיין שהטמעה מסוימת צריכה לשכפל את דפוס בדיקת הגישה ולהעביר את הפעולה בכל מלכודת.

קשה לפשט את התהליך הזה, כי יכול להיות שיהיה צורך להעביר כל פעולה בצורה שונה. בתרחיש מושלם, אם ניתן יהיה לנתב באופן אחיד את כל הפעולות דרך מלכודת אחת, ה-handler יצטרך לבצע את בדיקת האימות רק פעם אחת בלוקאל היחיד. כדי לעשות זאת, יש ליישם את ה-handler של שרת ה-proxy עצמו כשרת proxy. לצערנו, האפשרות הזו לא נכללת בהיקף הנושאים במאמר הזה.

תוסף אובייקטים

תרחיש נפוץ נוסף לדוגמה בשרתי proxy הוא הרחבה או הגדרה מחדש של הסמנטיקה של פעולות באובייקטים. לדוגמה: יכול להיות שתרצו ש-handler ירשום פעולות ביומן, יודיע לתצפית, יזרוק חריגות במקום להחזיר פעולות לא מוגדרות או יפנה מחדש פעולות ליעדים שונים לצורך אחסון. במקרים כאלה, שימוש בשרת proxy עשוי להוביל לתוצאה שונה מאוד משימוש באובייקט היעד.

function extend(sup,base) {

    var descriptor = Object.getOwnPropertyDescriptor(base.prototype,"constructor");

    base.prototype = Object.create(sup.prototype);

    var handler = {
    construct: function(target, args) {
        var obj = Object.create(base.prototype);
        this.apply(target,obj, args);
        return obj;
    },

    apply: function(target, that, args) {
        sup.apply(that,args);
        base.apply(that,args);
    }
    };

    var proxy = new Proxy(base, handler);
    descriptor.value = proxy;
    Object.defineProperty(base.prototype, "constructor", descriptor);
    return proxy;
}

var Vehicle = function(name){
    this.name = name;
};

var Car = extend(Vehicle, function(name, year) {
    this.year = year;
});

Car.prototype.style = "Saloon";

var Tesla = new Car("Model S", 2016);

console.log(Tesla.style); // "Saloon"
console.log(Tesla.name); // "Model S"
console.log(Tesla.year);  // 2016

בקרת גישה

עוד תרחיש טוב לדוגמה עם שרתי proxy הוא בקרת גישה. במקום להעביר אובייקט יעד לקטע קוד לא מהימן, אפשר להעביר את שרת ה-proxy שלו עטוף במעין ממברנה מגנה. לאחר שהאפליקציה קובעת שהקוד הלא מהימן השלים משימה מסוימת, היא יכולה לבטל את קובץ העזר שמנתק את שרת ה-proxy מהיעד. הממברנה תרחיב את ההפרדה הזו באופן רקורסיבי לכל האובייקטים שניתן להגיע אליהם מהיעד המקורי שהוגדר.

שימוש בהשתקפות עם שרתי proxy

Reflect הוא אובייקט מובנה חדש שמספק שיטות לפעולות JavaScript ניתנות ליירוט. הוא שימושי מאוד לעבודה עם שרתי proxy. למעשה, שיטות Reflect זהות לשיטות של רכיבי handler של שרת proxy.

שפות בהקלדה סטטית כמו Python או C# מציעות כבר זמן רב ממשק API לשקף, אבל ב-JavaScript לא נדרשה בדיוק שפה דינמית. אפשר לטעון של-ES5 כבר יש לא מעט תכונות השתקפות, כמו Array.isArray() או Object.getOwnPropertyDescriptor() שנחשבות השתקפות בשפות אחרות. ES2015 מציג ממשק API של Reflection שיאחסן שיטות עתידיות לקטגוריה הזו, כך שיהיה קל יותר לחשוב עליהן. יש בכך היגיון, מכיוון ש-Object נועד לשמש כאב-טיפוס בסיסי ולא כקטגוריה של שיטות השתקפות.

באמצעות Reflect, אנחנו יכולים לשפר את הדוגמה הקודמת של גיבורי העל שלנו ליירוט תקין של השדה לצורך השיגור והצבה של מלכודות באופן הבא:

// Field interception with Proxy and the Reflect API

var pioneer = new Proxy({}, {
    get: function(target, name, receiver) {
        console.log(`get called for field: ${name}`);
        return Reflect.get(target, name, receiver);
    },

    set: function(target, name, value, receiver) {
        console.log(`set called for field: ${name} and value: ${value}`);
        return Reflect.set(target, name, value, receiver);
    }
});

pioneer.firstName = 'Grace';
pioneer.secondName = 'Hopper';
// Grace
pioneer.firstName

אילו פלטים:

set called for field: firstName and value: Grace
set called for field: secondName and value: Hopper
get called for field: firstName

דוגמה נוספת היא מצב שבו כדאי:

  • כוללים הגדרה של שרת proxy בתוך בונה מותאם אישית כדי להימנע מיצירה ידנית של שרת proxy חדש בכל פעם שאנחנו רוצים לעבוד עם לוגיקה ספציפית.

  • הוסיפו את היכולת 'לשמור' שינויים, אבל רק אם הנתונים שונו בפועל (באופן היפותטי בגלל שפעולת השמירה הייתה יקרה מאוד).

function Customer() {

    var proxy = new Proxy({
    save: function(){
        if (!this.dirty){
        return console.log('Not saving, object still clean');
        }
        console.log('Trying an expensive saving operation: ', this.changedProperties);
    },

    }, {

    set: function(target, name, value, receiver) {
        target.dirty = true;
        target.changedProperties = target.changedProperties || [];

        if(target.changedProperties.indexOf(name) == -1){
        target.changedProperties.push(name);
        }
        return Reflect.set(target, name, value, receiver);
    }

    });

    return proxy;
}


var customer = new Customer();

customer.name = 'seth';
customer.surname = 'thompson';
// Trying an expensive saving operation:  ["name", "surname"]
customer.save();

דוגמאות נוספות ל-Reflect API זמין במאמר ES6 שרתי proxy של Tagtree.

Polyfilling Object.observe()

למרות שאנחנו נפרדים להתראות מ-Object.observe(), עכשיו אפשר למלא אותם באמצעות שרתי proxy של ES2015. סיימון בלאקוול כתב לאחרונה shim באמצעות Object.observe() שמבוסס על שרת proxy, וכדאי לבדוק אותו. אריק ארבידסון כתב גם גרסה שלמה ומפרטת למדי, בשנת 2012.

תמיכת דפדפן

שרתי proxy של ES2015 נתמכים ב-Chrome 49, Opera, Microsoft Edge ו-Firefox. ל-Safari היו אותות ציבוריים שונים לגבי התכונה אך אנחנו נשארים אופטימיים. Reflect נמצא ב-Chrome, ב-Opera וב-Firefox, ונמצא בפיתוח של Microsoft Edge.

Google השיקה Polyfill מוגבל עבור שרת Proxy. אפשר להשתמש באפשרות הזו רק ל-wrappers גנריים, כי הם יכולים להשתמש רק במאפייני proxy שידועים בזמן יצירת שרת ה-Proxy.

קריאה נוספת