Защита объектов JS с помощью прокси
Задача охранника — защищать что-то или кого-то и перехватывать потенциальные взаимодействия с объектом, который он охраняет. Здесь мы поговорим об охраннике, чья работа — защищать объект JavaScript! И использование этого охранника бесплатно! Ну, давайте перейдем к делу.
Что такое прокси
Прокси JavaScript — это объект, который обертывает другой объект и определяет, как обрабатывать любую операцию над этим обернутым объектом (т. е. вызовы функций, получение/установку свойств и т. д.). Мы рассмотрим несколько примеров, которые сделают это немного понятнее.
const myProxy = new Proxy(target, handler);
Идея состоит в том, что когда вы создаете прокси, вы предоставляете два аргумента, оба объекта:
target
— объект, который охраняет прокси.handler
— объект, который перехватывает и потенциально переопределяет операции над файломtarget
. Вы можете увидеть эти переопределенные операции, также называемые в документации «ловушками».
Вот небольшой пример того, как это выглядит:
const foo = {
a: 1,
b: 2,
};
const bar = new Proxy(foo, {
// Intercept setter operation
set(target, property, value) {
if (target[property] === undefined) {
console.error(
`Property ${property} does not exist! Check the object again.`,
);
return false;
}
// If the property exists, allow the setter operation to go through
target[property] = value;
return true;
},
});
// This should console error! Property 'c' does not exist on Object 'foo'
bar.c = 3;
В приведенном выше фрагменте мы создаем прокси bar
, который оборачивает или «охраняет» объект foo
, перехватывая операцию «установки свойства» на foo
. В этом перехвате мы добавляем проверку, существует ли свойство, для которого установлено значение foo
, и, если нет, предотвращаем установку этого свойства foo
и добавляем console.error()
соответствующее свойство. Мы также возвращаем true
в конце, чтобы указать на успех, если мы можем установить свойство.
Вероятно, это вызывает некоторые естественные вопросы. Почему бы просто не выполнить проверку доступа к свойствам объекта в логике моего приложения? Зачем мне нужен посредник, контролирующий мой объект JS? Что ж, есть ряд случаев, когда вам может пригодиться этот удобный парень!
Случаи использования
Установка прокси на себя
Да, это звучит странно. Зачем объекту нужна собственная защита? Что ж, давайте возьмем простой пример объекта «пользователь» с рядом функций API, подключенных для POST
данных.
let user = {
writePost: (data) => {...},
deletePost: (data) => {...},
likePost: (data) => {...},
commentPost: (data) => {...},
// ... dozens more API actions
};
Представьте, что вы вызываете функции API для user
объекта десятки, а может быть, и сотни раз по всему приложению. И теперь возникают некоторые новые бизнес-требования, в которых вам нужно написать некоторую логику, которая проверяет, хранится ли идентификатор сеанса пользователя в localStorage
перед вызовом этих API, и если нет, перенаправляет пользователя на страницу входа.
Вы могли бы добавить проверки в каждую из user
функций API... но это может быть утомительно и скучно. Введите прокси! Обернув user
в прокси, нацеленный на самого себя, вы можете внедрить некоторую логику перед вызовом любой функции API, добавив логику в операцию getter
в обработчике.
let user = {
writePost: (data) => {...},
deletePost: (data) => {...},
likePost: (data) => {...},
commentPost: (data) => {...},
// ... dozens more API actions
};
user = new Proxy(user, {
get(target, property) {
if (typeof target[property] === "function") {
if (isUserInSession(target)) {
return function (...args) {
return target[property].apply(target, args);
};
} else {
// route to login page
}
}
return target[property];
}
});
В приведенном выше сценарии всякий раз, когда вызывается функция API, мы проверяем служебную функцию, чтобы узнать, находится ли пользователь в сеансе, и если да, то можем продолжить вызов функции API или направить пользователя к входу в систему.
Если оставить в стороне тот факт, что это решение может лучше подходить в качестве какого-то промежуточного программного обеспечения или проверки в другом месте, метод прокси сам по себе позволяет найти удобное место для внедрения какой-либо проверки или логики, прежде чем разрешить вызов некоторого API для объекта.
Более простая и понятная отладка
Мы все проводили время, выполняя некоторые варианты «пошаговой» отладки с помощью старой доброй серии символов console.log()
в нашем коде. Я до сих пор делаю это, в игре нет ничего постыдного.
obj.increment();
console.log("WHATS", obj);
obj.decrement();
console.log("GOING", obj);
obj.increment(); // Bug appears here
console.log("ON!!!", obj);
Конечно, вы можете использовать настоящий инструмент отладчика для пошагового выполнения кода, но я добавлю, что использование прокси-сервера для добавления отладки также является приемлемым и эффективным способом.
obj = new Proxy(obj, {
get(target, property) {
if (typeof target[property] === "function") {
console.log("State of obj: ", obj);
console.log(`Invoking ${property}()`);
return function (...args) {
return target[property].apply(target, args);
};
}
return target[property];
}
})
Теперь вам не обязательно иметь строку уникальных сообщений журнала консоли между вызовами API. Вы можете просто обернуть свой объект в прокси и позволить ему вести журнал консоли за вас. И вы можете расширить это до более надежной модели отладки для добавления проверок перед вызовами функций, получения значений свойств, установки значений свойств и многого другого. В этом случае прокси выступает скорее помощником объекта, чем охранником.
Мост между двумя объектами
Если взглянуть на более реальный вариант использования, то в недавней работе Rive возникла необходимость в среде выполнения JS/WASM. В этой библиотеке есть два объекта беспокойства:
- Контекст Canvas2D для операций рисования на
<canvas>
с помощьюcanvas.getContext('2d');
- Пользовательский объект рендеринга (
CanvasRenderer
) с API-интерфейсами, аналогичными API-интерфейсам контекста Canvas2D (т.е.transform()
.save()
,restore()
, и т. д.), а также некоторыми дополнительными пользовательскими API для рисования содержимого Rive. Этот объект рендеринга использует контекст Canvas2D для рисования на холсте.
Обычно пользователи этой библиотеки будут управлять всеми операциями, связанными с рисованием и холстом, через CanvasRenderer
, и этот объект будет использовать контекст Canvas2D за кулисами для выполнения операций. Однако CanvasRenderer
только он оборачивает/реализует подмножество API, существующих в контексте Canvas2D. Естественно, некоторые пользователи по той или иной причине хотели получить доступ к контексту Canvas2D.
Это оказалось непросто, поскольку некоторые основные API-интерфейсы рисования должны были проходить через CanvasRenderer
, а не непосредственно через контекст, так как же библиотека могла обеспечить лучшее из обоих миров?
Спойлер: Прокси!
Решение заключалось в том, чтобы предоставить пользователям возможность манипулировать прокси в качестве «контекста холста». Если прокси обнаружил, что вызов функции, запрошенный вызывающей стороной, существует в CanvasRenderer
, он соответствующим образом вызывал функцию оттуда. Если прокси обнаружил, что выполняемый вызов функции не существует в CanvasRenderer
, это должен был быть вызов, предназначенный для функции в базовом контексте Canvas2D, и, таким образом, прокси вместо этого вызывает вызов функции в контексте.
Вот фрагмент того, как это выглядело:
const newCanvasRenderer = new CanvasRenderer(canvas);
const c2dSource = newCanvasRenderer._ctx; // C2D Context
return new Proxy(newCanvasRenderer, {
get(target, property) {
if (typeof target[property] === "function") {
return function (...args) {
return target[property].apply(target, args);
};
} else if (typeof c2dSource[property] === "function") {
// Some functions on C2D are blocked, and adding this
// logic to a proxy helped centralize a place for that
if (c2dMethodBlockList.indexOf(property) > -1) {
throw new Error(
"RiveException: Method call to '" +
property +
"()' is not allowed, as the renderer cannot immediately pass through the return \
values of any canvas 2d context methods."
);
} else {
// Use the C2D context as needed
return function (...args) {
newCanvasRenderer._drawList.push(
c2dSource[property].bind(c2dSource, ...args)
);
};
}
}
return target[property];
},
// There are no properties to set on CanvasRenderer, so all
// property setting can go through the C2D context
set(target, property, value) {
if (property in c2dSource) {
c2dSource[property] = value;
return true;
}
},
});
Это решение послужило победой со стороны библиотеки в возможности использовать API-интерфейсы рендеринга, специфичные для Rive, а также преимуществом для пользователей в использовании общих API-интерфейсов Canvas2D для других действий, которые Rive не реализовала. Прокси как мост между двумя похожими объектами!
По прокси есть гораздо более ясная и четко определенная документация, с которой вам следует ознакомиться, чтобы узнать о дальнейшем использовании и предостережениях, но, надеюсь, вы начнете видеть некоторые преимущества и варианты использования этого невоспетого героя, защитника объектов JavaScript.