Понимание прокси-серверов JavaScript путем изучения библиотеки при изменении

Прокси-серверы Javascript - это новое дополнение в ES6. Это мощная функция, которую можно использовать для элегантного решения различных задач. Мы собираемся исследовать и воссоздать небольшую служебную библиотеку от Синдре Сорхуса под названием on-change. Цель состоит в том, чтобы концептуально понять прокси-серверы JavaScript и в процессе создания чего-то, что укрепляет эти концепции.

Я старался, чтобы все было как можно проще, однако ожидается небольшое знакомство с языком JavaScript.

Так что же делать при смене? Это небольшая утилита, которая отслеживает изменения объекта или массива. Давайте посмотрим на пример кода, чтобы прояснить ситуацию -

Несколько замечаний -

  • onChange - это функция, которая принимает два параметра: объект для наблюдения и функцию, запускаемую при обнаружении изменения в указанном объекте. Он возвращает объект.
  • В строке 17, когда мы устанавливаем foo в true, вызывается функция logger.
  • В строке 20, когда мы устанавливаем объект, который глубоко вложен, даже в массивы, вызывается logger. То есть он работает рекурсивно, поэтому он даже обнаружит, изменим ли мы такое глубокое свойство, как watchedObject.a.b[0].c = true.

Итак, давайте посмотрим, как мы можем использовать прокси JavaScript для воссоздания этой утилиты! Но перед этим давайте изучим прокси.

Что такое прокси?

Рассмотрим этот короткий фрагмент кода -

const someObject = { prop1: 'Awesome' };
console.log(someObject.prop1);  // Awesome
console.log(someObject.prop2);  // undefined

Если мы сделаем someObject.prop1, мы получим Awesome. Но если мы сделаем someObject.prop2, мы получим undefined, потому что prop2 не существует на someObject.

Допустим, мы хотим возвращать значение по умолчанию при каждом обращении к несуществующему свойству. То есть someObject.prop2 должен давать Oops! This property does not exist вместо undefined. Как этого добиться, не изменяя и не добавляя новые свойства someObject ?

Добро пожаловать, прокси! Оксфордский словарь английского языка определяет прокси как право представлять кого-то еще. Именно это и есть прокси в JavaScript. Прокси-серверы являются частью ES6 и позволяют нам перехватывать операции (такие как установка значения или удаление свойства), выполняемые с объектами. При доступе к свойству объекта происходит следующее:

При использовании прокси все немного меняется.

Как видно из приведенной выше диаграммы, прокси-сервер находится между объектом и программой, опосредуя обмен значениями. Прокси-сервер может проверить объект на наличие ключа свойства, и, если он не существует, он также может отправить свой собственный ответ.

Показав, насколько хороши прокси, давайте посмотрим, как мы можем создать их на JavaScript. Но перед этим несколько терминов, с которыми вам следует ознакомиться -

  • target - объект, для которого мы будем создавать прокси.
  • ловушки - причудливый термин для операций, которые мы будем перехватывать. Например, доступ к свойству называется ловушкой get. Установка значения для свойства называется ловушкой set. Удаление свойства объекта называется ловушкой deleteProperty. Есть много ловушек. Вы можете увидеть их все здесь.
  • обработчик - объект, содержащий все ловушки вместе с их описаниями.

Создание прокси

Первое, что нам нужно, это объект, для которого мы создаем прокси. Пусть будет так -

const originalObject = { firstName: 'Arfat', lastName: 'Salman' };

Теперь нам нужно подумать, какие ловушки мы собираемся перехватить. Прямо сейчас мы собираемся перехватить get ловушку. Ловушка будет жить в обработчике. Итак, давайте создадим это.

const handler = {
  get(target, property, receiver) {
    console.log(`GET ${property}`);
    return target[property];
  }
};

Несколько замечаний -

  • handler - нормальный объект.
  • Ловушки - это функции (или методы), которые являются частью обработчика. Имена ловушек фиксированы и предопределены.
  • Ловушка get получает три параметра: цель, свойство, получатель.
  • target - это исходный объект, для которого мы создали прокси.
  • property - это имя свойства, к которому осуществляется доступ.
  • receiver - это либо прокси, либо объект, унаследованный от прокси.

Теперь нам нужно объединить handler и originalObject. Мы делаем это с помощью конструктора Proxy.

const proxiedObject = new Proxy(originalObject, handler);

Весь код должен выглядеть так -

Вы можете запустить его в консоли браузера или сохранить в файле и запустить с помощью узла (версия ≥ 7). Вот пример выполнения -

Теперь, если вы зарегистрируете свойство firstName объекта proxiedObject -

console.log(proxiedObject.firstName);
//=> GET firstName
//=> Arfat

Мы собираемся получить два журнала. Если вы получаете такие же результаты, как указано выше, это означает, что все было настроено правильно.

Теперь давайте изменим обработчик для обработки несуществующих свойств.

const newHandler = {
  get(target, property, receiver) {
    console.log(`GET ${property}`);
  if (property in target) {
      return target[property];
    }
return 'Oops! This property does not exist.';
  }
};

Сосредоточьтесь на жирных частях. Проверяем, существует ли свойство на цели. Если он существует, мы возвращаем его значение. В противном случае мы возвращаем Oops! This property does not exist..

Теперь, если вы это сделаете -

console.log(proxiedObject.thisPropertDoesNotExist);
// => GET thisPropertyDoesNotExist
// => Oops! This property does not exist.

вы не получите undefined, кроме настраиваемой строки ответа.

Следует отметить, что для операций, для которых не определены ловушки, они обычно передаются цели, как если бы прокси-сервер не существовал.

Воссоздание при изменении

Понимая, как работают прокси, мы собираемся воссоздать библиотеку при изменении. Как обсуждалось выше, onChange - это функция, которая принимает два параметра: объект для наблюдения и функция, которая будет выполняться при каждом изменении объекта. Давайте сделаем функцию, тогда -

const onChange = (objToWatch, onChangeFunction) => { };

Сейчас он ничего не делает.

Давайте еще раз сформулируем проблему: мы хотим запускать onChangeFunction всякий раз, когда objToWatch изменяется, то есть либо осуществляется доступ / извлечение свойства, либо добавляется новое свойство, либо свойство удаляется.

Кажется очевидным, что мы собираемся использовать прокси для перехвата операций с объектом. Итак, давайте вернем прокси в функции onChange с пустым обработчиком. Поскольку обработчик не указывает никаких прерываний, все операции прозрачно передаются целевому объекту, то есть objToWatch.

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {};
  return new Proxy(objToWatch, handler);
};

Давайте сосредоточимся на «при доступе / извлечении свойства», поскольку мы поняли get ловушку, указанную выше. Итак, если в ловушке get мы вызываем onChangeFunction перед возвратом значения свойства, мы сможем частично достичь того, что делает библиотека on-change. Давайте запрограммируем и посмотрим -

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction(); // Calling our function
      return target[property];
    }
  };
return new Proxy(objToWatch, handler);
};

Это кажется правильным. Давайте запустим его, прежде чем продолжить -

Итак, мы выполнили одну часть заявления. Теперь сосредоточимся на том, когда «добавляется новое свойство или свойство удаляется». Поскольку мы уже заложили основу, нам просто нужно добавить дополнительные ловушки, чтобы реализовать оставшуюся функциональность. Ловушка для установки свойства или изменения его значения - set. Добавим, что в handle -

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction();
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value, receiver) {
      onChangeFunction();
      return Reflect.set(target, property, value);
    }
  };
return new Proxy(objToWatch, handler);
};

set получает 4 параметра: дополнительный - это устанавливаемое значение. Мы используем Reflect, потому что он дает нам программный способ управления объектом. Это не сильно отличается от obj.name = 'Arfat' типа настройки свойства. Вы можете прочитать здесь, почему лучше использовать Reflect API. Подробнее про Reflect API можно прочитать здесь.

Поскольку мы используем Reflect API, я также заменю target[property] на эквивалентную функцию Reflect в get ловушке.

Аналогичным образом, если мы хотим перехватить удаление свойства, мы можем сделать это с помощью ловушки deleteProperty. Давайте добавим это и к обработчику -

const onChange = (objToWatch, onChangeFunction) => { 
  const handler = {
    get(target, property, receiver) {
      onChangeFunction();
      return Reflect.get(target, property, receiver);
    },
    set(target, property, value) {
      onChangeFunction();
      return Reflect.set(target, property, value);
    },
    deleteProperty(target, property) {
      onChangeFunction();
      return Reflect.deleteProperty(target, property);
    }
  };
return new Proxy(objToWatch, handler);
};

Отличная работа. Если вы сейчас запустите этот код -

const logger = () => console.log('I was called');
const obj = { a: 'a' };
const proxy = onChange(obj, logger);
console.log(proxy.a); // logger called here in get trap
proxy.b = 'b'; // logger called here as well in set trap
delete proxy.a; // logger called here in deleteProperty trap

Вы увидите I was called 3 раза. Это означает, что мы успешно воссоздали библиотеку при изменении.

Однако есть одна вещь, которую мы не учли. Если у вас есть вложенные объекты в массиве, они не будут запускать функцию регистратора. Например, если массив равен [1, 2, {a: false}] и вы установили array[2].a = true, функция регистратора не будет вызываться.

Эту ошибку легко исправить. Вместо того, чтобы возвращать значение в ловушке get, мы вернем еще Proxy значения если значение является объектом, чтобы цепочка прокси-серверов никогда не разрывалась на объектах.

Давайте добавим эту логику к ловушке get -

get(target, property, receiver) {
  onChangeFunction();
  const value = Reflect.get(target, property, receiver);
  if (typeof value === 'object') {
    return new Proxy(value, handler);
  }
  return value;
}

Теперь он будет работать даже с вложенными объектами внутри массивов и объектов.

Последние мысли

Некоторые вещи, которые при изменении делают иначе, чем наша реализация:

  • Он не вызывает onChangeFunction в ловушке get.
  • Вместо ловушки set, при изменении перехватывает defineProperty ловушку.

С пониманием прокси и знанием ловушек эти два были бы тривиальным дополнением / модификацией. Итак, я оставляю их, чтобы читатель мог добавить себя. Вы можете прочитать источник on-change здесь, для справки.

Еще одна проблема, которая беспокоит при изменении, заключается в следующем: если у вас есть массив и вы выполняете proxiedArray.sort() или любую другую функцию, которая сильно изменяет массив, функция logger будет выполняться несколько раз. Например, сортировка массива [2,3,4,5,6,7,1] запускает логгер 12 раз. Это может быть желаемая функциональность или нет. Это зависит от разработчика.

Есть еще одна ошибка в библиотеке при изменении. Если вы прочтете эту проблему, вы заметите, что ловушка get нарушает так называемый инвариант. Инварианты - это ограничения, накладываемые на прокси-объекты Proxy API. Эти ограничения запрещают выполнение незаконных операций с объектами, дескрипторы которых заданы определенным образом.

В проблеме также указано возможное решение. Вы можете прочитать ссылки ниже, чтобы получить более глубокое представление о прокси и инвариантах. И если вы никогда раньше не участвовали в разработке ПО с открытым исходным кодом, это может стать для вас отличным началом для выполнения запроса на перенос, исправляющего поведение. 🙂

Есть много других функций и предостережений прокси. Прочтите ссылки, чтобы лучше их понять.

Использованная литература -

Вам также могут понравиться еще несколько статей, которые я написал -

Лучшие расширения JavaScript VSCode для более быстрой разработки



Как НЕ реагировать: общие антипаттерны и ловушки в React



Я пишу о JavaScript, веб-разработке и информатике. Следуйте за мной за еженедельными статьями. Поделитесь этой статьей, если она вам нравится.

Свяжитесь со мной в @ Facebook @ Linkedin @ Twitter.

✉️ Подпишитесь на рассылку еженедельно Email Blast 🐦 Подпишитесь на CodeBurst на Twitter , просмотрите 🗺️ Дорожная карта веб-разработчиков на 2018 год и 🕸️ Изучите веб-разработку с полным стеком .