Przedstawiamy serwery proxy ES2015

Addy Osmani
Addy Osmani

Pośrednicy ES2015 (w Chrome 49 i nowszych wersjach) udostępniają JavaScriptowi interfejs pośredniczący API, który umożliwia przechwytywanie lub przechwytywanie wszystkich operacji na obiekcie docelowym i modyfikowanie jego działania.

Serwery proxy mają wiele zastosowań, w tym:

  • Przejęcie
  • Wirtualizacja obiektów
  • Zarządzanie zasobami
  • profilowanie lub rejestrowanie danych na potrzeby debugowania;
  • Zabezpieczenia i kontrola dostępu
  • Umowy dotyczące korzystania z obiektów

Interfejs Proxy API zawiera konstruktor Proxy, który przyjmuje wyznaczony obiekt docelowy i obiekt modułu obsługi.

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

Zachowanie serwera proxy jest kontrolowane przez obsługę, która może modyfikować pierwotne zachowanie obiektu docelowego na wiele przydatnych sposobów. Obsługa zawiera opcjonalne metody pułapki (np..get(), .set(), .apply()), które są wywoływane, gdy na serwerze proxy wykonywana jest odpowiednia operacja.

Przejęcie

Zacznijmy od pobrania zwykłego obiektu i dodania do niego pośredniczącego oprogramowania pośredniczącego za pomocą interfejsu Proxy API. Pamiętaj, że pierwszy parametr przekazany do konstruktora to target (obiekt, który jest proxy) i drugi to handler (samo proxy). Tutaj możemy dodać uchwyty dla naszych metod getter i setter lub innych zachowań.

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

Po uruchomieniu tego kodu w Chrome 49 otrzymujemy:

get was called for: power  
"Flight"

Jak widać w praktyce, wywołanie metody get lub set właściwości obiektu zastępczego prawidłowo wywołało wywołanie na poziomie meta odpowiedniej pułapki w modułu obsługi. Operacje obsługi obejmują odczyt właściwości, przypisanie właściwości i zastosowanie funkcji. Wszystkie są przekazywane do odpowiedniej pułapki.

Funkcja pułapki może dowolnie stosować operacje (np.przekazywać operacje do obiektu docelowego). Tak się właśnie dzieje domyślnie, jeśli nie zostanie określona pułapka. Oto przykład serwera proxy przekierowującego, który nie wykonuje żadnych operacji:

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

Właśnie omawialiśmy proxyowanie prostych obiektów, ale równie łatwo możemy utworzyć proxy obiektu funkcji, jeśli funkcja jest naszym celem. Tym razem użyjemy pułapki 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

Identyfikowanie serwerów proxy

Identyfikator zastępczy można obserwować za pomocą operatorów równości JavaScript (=====). Jak wiadomo, po zastosowaniu do 2 obiektów operatory te porównują tożsamość obiektów. Następujący przykład pokazuje to zachowanie. Porównywanie 2 różnych serwerów proxy zwraca wartość false, mimo że docelowe wartości są takie same. Podobnie obiekt docelowy różni się od wszystkich swoich obiektów zastępczych:

// Continuing previous example

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

W idealnej sytuacji nie powinno być możliwe odróżnienie obiektu pośredniczącego od obiektu niebędącego pośredniczącym, aby wdrożenie obiektu pośredniczącego nie miało wpływu na działanie aplikacji. Dlatego interfejs Proxy API nie zawiera sposobu na sprawdzenie, czy obiekt jest pośredniczącym, ani nie zapewnia pułapek dla wszystkich operacji na obiektach.

Przypadki użycia

Jak już wspomnieliśmy, proxy mają wiele zastosowań. Wiele z tych funkcji, np. kontrola dostępu i profilowanie, zalicza się do uniwersalnych opakowań: obiektów zastępczych, które otaczają inne obiekty w tym samym „przestrzeni” adresów. Wspomniano też wirtualizację. Obiekty wirtualne to obiekty zastępcze, które emulują inne obiekty bez konieczności znajdowania się w tej samej przestrzeni adresowej. Przykłady obejmują obiekty zdalne (które emulują obiekty w innych przestrzeniach) i przejrzyste przyszłe wartości (które emulują wyniki, które nie zostały jeszcze obliczone).

Serwery proxy jako moduły obsługi

Typowym zastosowaniem modułów pośredniczących jest przeprowadzanie weryfikacji lub kontroli kontroli dostępu przed wykonaniem operacji na opakowanym obiekcie. Operacja jest przekierowywana tylko wtedy, gdy sprawdzanie zakończy się powodzeniem. Przykład weryfikacji poniżej pokazuje to:

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

Bardziej złożone przykłady tego wzorca mogą uwzględniać wszystkie operacje, które mogą przechwycić moduły obsługi. Można sobie wyobrazić, że w ramach implementacji trzeba powielać wzór sprawdzania dostępu i przekazywać operację w każdej pułapce.

Może to być trudne do uproszczenia, ponieważ każda opcja może wymagać innego przekierowania. W idealnym scenariuszu, jeśli wszystkie operacje mogłyby być równomiernie kierowane przez jedną pułapkę, moduł obsługi musiałby przeprowadzić weryfikację tylko raz w tej jednej pułapce. Możesz to zrobić, implementując sam moduł obsługi serwera proxy jako serwer proxy. Temat ten wykracza niestety poza zakres tego artykułu.

Rozszerzenie obiektu

Innym typowym zastosowaniem proxy jest rozszerzanie lub redefiniowanie semantyki operacji na obiektach. Możesz na przykład chcieć, aby w obiekcie-obsługi prowadzono rejestr operacji, wysyłano powiadomienia do obserwatorów, wyrzucano wyjątki zamiast zwracania wartości undefined lub przekierowywano operacje do różnych celów do przechowywania. W takich przypadkach użycie obiektu zastępczego może prowadzić do bardzo różnych wyników niż użycie obiektu docelowego.

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

Kontrola dostępu

Kontrola dostępu to kolejny przykład zastosowania serwerów proxy. Zamiast przekazywania obiektu docelowego do kodu, któremu nie ufamy, można przekazać jego element zastępczy zawinięty w rodzaj osłony ochronnej. Gdy aplikacja uzna, że nieufny kod wykonał określone zadanie, może cofnąć odwołanie, co spowoduje odłączenie serwera proxy od celu. Membrana rozszerzy to odłączenie rekurencyjnie do wszystkich obiektów dostępnych z pierwotnie zdefiniowanego celu.

Korzystanie z odbicia w przypadku serwerów proxy

Reflect to nowy wbudowany obiekt, który udostępnia metody przechwytywania operacji JavaScript. Jest on bardzo przydatny do pracy z serwerami proxy. W istocie metody odbicia są takie same jak metody przekaźników.

Języki o typach statycznych, takie jak Python czy C#, od dawna oferują interfejs API do odbicia lustrzanego, ale JavaScript, jako język dynamiczny, nie potrzebował go. Można argumentować, że ES5 ma już sporo funkcji odbicia, takich jak Array.isArray() lub Object.getOwnPropertyDescriptor(), które można uznać za odbicie w innych językach. ES2015 wprowadza interfejs Reflection API, który będzie zawierać przyszłe metody z tej kategorii, co ułatwi ich analizowanie. To ma sens, ponieważ obiekt ma być podstawowym prototypem, a nie zbiorem metod odbicia.

Za pomocą funkcji odzwierciedlenia możemy ulepszyć nasz wcześniejszy przykład z bohaterem superboskim, aby umożliwić przechwytywanie pól w pułapkach get i set w ten sposób:

// 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

Dane wyjściowe:

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

Innym przykładem jest sytuacja, w której użytkownik chce:

  • Owiń definicję zastępnika w ramach niestandardowego konstruktora, aby uniknąć ręcznego tworzenia nowego zastępnika za każdym razem, gdy chcesz pracować z konkretną logiką.

  • Dodaj możliwość „zapisania” zmian, ale tylko wtedy, gdy dane zostały faktycznie zmodyfikowane (hipotetycznie, ponieważ operacja zapisu jest bardzo kosztowna).

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();

Więcej przykładów użycia interfejsu Reflect API znajdziesz w artykule ES6 Proxies autorstwa Tagtree.

Wypełnianie obiektu za pomocą funkcji Object.observe()

Chociaż pożegnaliśmy się z wersją Object.observe(), teraz można ją wypełnić za pomocą proxy ES2015. Simon Blackwell napisał niedawno przełącznik Object.observe() oparty na pośredniku, który warto sprawdzić. W 2012 r. Erik Arvidsson napisał też kompletną specyfikację.

Obsługa przeglądarek

Proxy ES2015 są obsługiwane w Chrome 49, Operze, Microsoft Edge i Firefox. W przypadku Safari sygnały dotyczące tej funkcji są mieszane, ale jesteśmy optymistyczni. Aplikacja Reflect jest dostępna w przeglądarkach Chrome, Opera i Firefox, a w Microsoft Edge jest w fazie rozwoju.

Google udostępniło ograniczoną wersję polyfill dla proxy. Można go używać tylko w przypadku uniwersalnych obudówek, ponieważ może on zawierać tylko usługi proxy znane w momencie tworzenia proxy.

Więcej informacji