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

Addy Osmani
Addy Osmani

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

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

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

Proxy API מכיל מתכנת proxy שמקבל אובייקט יעד ייעודי ואובייקט טיפול.

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

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

חטיפת מסירה

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

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"

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

פונקציית הטראפ יכולה, אם היא בוחרת, להטמיע פעולה באופן שרירותי (למשל, העברת הפעולה לאובייקט היעד). זה מה שקורה כברירת מחדל אם לא מציינים מלכודת. לדוגמה, הנה שרת 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 לאובייקטים פשוטים, אבל אפשר להשתמש בשרת 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, למרות שהיעדים הבסיסיים זהים. באופן דומה, אובייקט היעד שונה מכל אחד מהשרתים שלו:

// Continuing previous example

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

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

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

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

שרתי proxy כ-handlers

תרחיש לדוגמה נפוץ למטפלי 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

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

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

תוסף אובייקט

תרחיש לדוגמה נוסף לשימוש בשרתים proxy הוא הרחבה או הגדרה מחדש של הסמנטיקה של פעולות על אובייקטים. לדוגמה, יכול להיות שתרצו שהטיפול יתעד פעולות, יעדכן משקיפים, יזרוק חריגות במקום להחזיר ערכים לא מוגדרים או יפנה פעולות ליעדים שונים לאחסון. במקרים כאלה, שימוש בשרת 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 זהות לאלה של מתווכים.

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

באמצעות Reflect, אפשר לשפר את הדוגמה הקודמת של Superhero כדי לבצע יירוט שדה תקין במלכודות ה-get וה-set באופן הבא:

// 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 Proxies של Tagtree.

מילוי אובייקט.observe()‎

אנחנו אומרים שלום ל-Object.observe(), אבל עכשיו אפשר להשתמש ב-polyfills שלהם באמצעות שרתי proxy של ES2015. Simon Blackwell כתב לאחרונה תוסף ל-Object.observe() שמבוסס על שרת proxy, ששווה לבדוק. נוסף על כך, Erik Arvidsson כתב גרסה שלמה למדי כבר בשנת 2012.

תמיכה בדפדפנים

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

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

קריאה נוספת