Przedstawiamy serwery proxy ES2015

Addy Osmani
Addy Osmani

Serwery proxy ES2015 (w Chrome 49 i nowsze) udostępniają JavaScript z interfejsem API intercession, co umożliwia przechwytywanie lub przechwytywanie wszystkich operacji na obiekcie docelowym oraz modyfikowanie jego działania.

Mają one wiele zastosowań, do których należą:

  • Przejęcie
  • Wirtualizacja obiektów
  • Zarządzanie zasobami
  • Profilowanie lub logowanie na potrzeby debugowania
  • Zabezpieczenia i kontrola dostępu
  • Umowy na korzystanie z obiektów

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

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

Działanie serwera proxy jest kontrolowane przez moduł obsługi, który może zmieniać pierwotne działanie obiektu target na wiele przydatnych sposobów. Moduł obsługi zawiera opcjonalne metody pułapek (np..get(), .set(), .apply()) wywoływane, gdy na serwerze proxy jest wykonywana odpowiednia operacja.

Przejęcie

Zacznijmy od prostego obiektu i dodania do niego oprogramowania pośredniczącego do przechwytywania za pomocą interfejsu Proxy API. Pamiętaj, że pierwszy parametr przekazywany do konstruktora jest obiektem docelowym (obiektem przesyłanym przez serwer proxy), a drugim jest modułem obsługi (sam serwer proxy). Tutaj możesz dodać haki dla metod pobierających, setek i innych działań.

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, prawidłowe pobranie właściwości lub ustawienie właściwości w obiekcie serwera proxy spowodowało wywołanie odpowiedniego poziomu obsługi w module obsługi. Operacje obsługi obejmują odczyty właściwości, przypisanie właściwości i stosowanie funkcji – wszystkie te działania są przekazywane do odpowiedniej pułapki.

Funkcja trap może, jeśli zechce, wdrożyć operację dowolnie (np.przekazać ją do obiektu docelowego). Dzieje się tak domyślnie, jeśli pułapka nie zostanie określona. Oto przykład takiego serwera proxy do przekazywania dalej, który spełnia to zadanie:

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

Przyjrzeliśmy się tylko zwykłym obiektom przez serwer proxy, ale równie łatwo możemy utworzyć serwer proxy dla obiektu funkcji, tam gdzie funkcja jest 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

Tożsamość serwera proxy można zaobserwować przy użyciu operatorów równości JavaScript (== i ===). Jak wiemy, stosowane do 2 obiektów operatory te porównują tożsamości obiektów. Następny przykład ilustruje takie działanie. Porównanie 2 różnych serwerów proxy zwraca wartość „fałsz”, mimo że podstawowe elementy docelowe są takie same. Podobny obiekt docelowy różni się od któregokolwiek z jego serwerów proxy:

// Continuing previous example

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

Najlepiej byłoby, gdyby można było odróżnić serwer proxy od obiektu bez niego, tak by utworzenie serwera proxy nie miało wpływu na działanie aplikacji. To jeden z powodów, dla których interfejs Proxy API nie umożliwia sprawdzenia, czy obiekt jest serwerem proxy, ani nie zapewnia pułapek na wszystkie operacje na obiektach.

Przypadki użycia

Jak już wspomnieliśmy, serwery proxy mają wiele zastosowań. Wiele z powyższych elementów, np. kontrola dostępu i profilowanie, należy do ogólnych opakowań: serwerów proxy, które opakowują inne obiekty w tej samej „przestrzeni” adresu. Wspomniano też o wirtualizacji. Obiekty wirtualne to serwery proxy, które emulują inne obiekty bez konieczności ich przechowywania w tej samej przestrzeni adresowej. Mogą to być na przykład obiekty zdalne (które emulują obiekty w innych przestrzeniach) i przezroczyste obiekty przyszłe (emulujące wyniki, które nie zostały jeszcze obliczone).

Serwery proxy jako moduły obsługi

Częstym przypadkiem użycia modułów obsługi serwera proxy jest sprawdzanie poprawności lub kontroli dostępu przed wykonaniem operacji na opakowanym obiekcie. Jeśli operacja się powiedzie, operacja zostanie przekierowana. Widać to w poniższym przykładzie weryfikacji:

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 różne działania, które mogą przechwycić moduły obsługi serwera proxy. Można sobie wyobrazić, że implementacja wymaga zduplikowania wzorca sprawdzania dostępu i przekazywania operacji w każdej pułapce.

Takie podejście może być trudne do wyodrębnienia, ponieważ każda operacja wymaga osobnego przekazywania. Jeśli w idealnym scenariuszu wszystkie operacje można jednolicie przekierować przez tylko 1 pułapkę, moduł obsługi powinien przeprowadzić weryfikację poprawności tylko raz w ramach pojedynczej pułapki. Możesz to zrobić, implementując moduł obsługi serwera proxy jako serwer proxy. Ten artykuł nie mieści się w zakresie tego artykułu.

Rozszerzenie obiektu

Innym częstym przypadkiem użycia serwerów proxy jest rozszerzanie lub ponowne definiowanie semantyki operacji na obiektach. Możesz na przykład potrzebować, aby moduł obsługi logował operacje, powiadamiał obserwatorów, zgłaszał wyjątki zamiast zwracać niezdefiniowane lub przekierowywał operacje do innych miejsc docelowych w celu przechowywania danych. W takich przypadkach użycie serwera proxy może przynieść całkiem inny wynik niż przy użyciu 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 dobry przypadek użycia serwerów proxy. Zamiast przekazywać obiekt docelowy do fragmentu niezaufanego kodu, można przekazać serwer proxy owinięty w pewną błonę ochronną. Gdy aplikacja uzna, że niezaufany kod wykonał określone zadanie, może unieważnić odwołanie, co spowoduje odłączenie serwera proxy od celu. Membrana będzie rozciągać to odłączenie rekurencyjnie na wszystkie obiekty osiągalne ze zdefiniowanego pierwotnie miejsca docelowego.

Korzystanie z funkcji odbicia w przypadku serwerów proxy

Reflect to nowy wbudowany obiekt, który udostępnia metody przechwytywania operacji JavaScriptu, które są bardzo przydatne podczas pracy z serwerami proxy. W rzeczywistości metody odzwierciedlenia są takie same jak w przypadku modułów obsługi proxy.

Języki ze statyczną pisownią, takie jak Python czy C#, od dawna oferują interfejs API odbicia, ale JavaScript w ogóle nie potrzebuje języka dynamicznego. Można stwierdzić, że ES5 ma już sporo funkcji związanych z odbiorem, np. Array.isArray() i Object.getOwnPropertyDescriptor(), które w innych językach można by uznać za odbicie. W programie ES2015 wprowadziliśmy interfejs Reflection API, który będzie przechowywać w przyszłości metody dla tej kategorii, co ułatwi ich radzenie sobie. Ma to sens, ponieważ obiekt typu Object ma być podstawowym prototypem, a nie zasobnikiem metod odbicia.

Wykorzystując funkcję Reflect, możemy udoskonalić nasz przykład z superbohaterami w zakresie prawidłowego przechwytywania piłki i ustawiania pułapek 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

Wyniki:

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

Innym przykładem może być:

  • Spakuj definicję serwera proxy do konstruktora niestandardowego, aby uniknąć ręcznego tworzenia nowego serwera proxy za każdym razem, gdy chcesz pracować z określoną logiką.

  • Dodanie możliwości „zapisania” zmian, ale tylko wtedy, gdy dane zostały rzeczywiście zmodyfikowane (hipotetycznie ze względu na bardzo kosztowne działanie zapisu).

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 interfejsu Reflect API znajdziesz na stronie ES6 Proxies firmy Tagtree.

Polyfilling Object.observe()

Żegnamy się z usługą Object.observe(), ale teraz można wypełnić je kodowaniem ES2015 za pomocą serwerów proxy ES2015. Simon Blackwell napisał ostatnio opartą na serwerze proxy funkcję Object.observe() shim, którą warto wypróbować. Erik Arvidsson napisał również dość pełną specyfikację wersję, aż do roku 2012.

Obsługiwane przeglądarki

Serwery proxy ES2015 są obsługiwane w przeglądarkach Chrome 49, Opera, Microsoft Edge i Firefox. Safari miało niejednolite sygnały dotyczące tej funkcji, ale jesteśmy optymistycznie nastawieni. Funkcja Reflect jest obecnie dostępna w przeglądarkach Chrome, Opera i Firefox, a wraz z nią pracujemy nad Microsoft Edge.

Wprowadziliśmy ograniczoną obsługę kodu polyfill dla serwerów proxy. Tej opcji można używać tylko w przypadku kodów ogólnych, ponieważ mogą one być używane tylko w przypadku właściwości serwera proxy znanych w momencie tworzenia tego serwera.

Więcej informacji