Introduction
JavaScript proxies are an ES6 (aka ES2015) feature that lets you wrap an existing object and intercept any access to its attributes or methods, even if they do not exist.
var proxy = new Proxy({ name: "John Doe" }, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
return "Nope!";
}
}
});
console.log(proxy.age); // "Nope!"
console.log(proxy.name); // "John Doe"
console.log(proxy.title); // "Nope!"
As you can see the proxy intercepts (technically “traps”) the operation intended for the target object and allows you to perform your own behavior.
The syntax of a Proxy is really easy:
var p = new Proxy(target, handler);
target
is the Object that gets wrapped by the proxyhandler
is the placeholder object which contains traps
Notice: A complete list of traps is available on the MDN Page.
So how can we use Proxies to our own advantage?
Context
At work we are building a huge Single Page Application and we needed to integrate the Matomo (formerly Piwik) analytics platform for tracking our users.
In a standard website it is enough to include the JavaScript tracker for tracking page visits, but in a SPA it is necessary to explicitly call the required methods in order to track when the user is changing page or performing an additional action, for example:
Piwik.getTracker().trackPageView(document.title);
// or
Piwik.getTracker().trackLink("http://...");
In a SPA built with VueRouter it is possible to perform the trackPageView
automatically, just by attaching the method to the afterEach
hook
router.afterEach((to, from) => {
document.title = to.meta.title;
Piwik.getTracker().trackPageView(document.title);
});
The Problem
It is obvious that we don’t want to track anything with Piwik when the application is in development or in debug mode. However if we don’t include the Piwik tracker when developing the application we will get the error Uncaught ReferenceError: Piwik is not defined
. Neither it is feasible to scatter conditionals all across the application, just to check if Piwik
is defined in window
before calling methods on it, as it is used in many different parts of the application.
if (Piwik) Piwik.getTracker().trackPageView(document.title);
The Solution
So the first idea that came to my mind was to replace the Piwik object with a null object, an entity that could receive any method call or any attribute access without breaking the application. A Proxy is the perfect fit for this purpose.
The following is the code that we came up to. If the application is in production the regular Piwik tracker is injected in the page, otherwise the Piwik
object is replaced with a Proxy that traps every method call (logging it to the console) and returns the same instance of the Proxy, so that it is possible to chain methods on it.
@if(app()->environment("production") and ! config()->get("app.debug"))
<!-- Matomo -->
<script type="text/javascript">
var _paq = _paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function() {
var u="//domain.com/piwik/";
_paq.push(['setTrackerUrl', u+'piwik.php']);
_paq.push(['setSiteId', '<site id>']);
var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0];
g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s);
})();
</script>
@else
<script>
Piwik = new Proxy({}, {
get: function(target, name) {
return function (...args) {
console.log("[Piwik]: " + name + JSON.stringify(args));
return Piwik;
};
}
});
</script>
@endif
The result is wonderful. We get the benefit to check for free what get’s tracked by Piwik without moving away by the application:
[Piwik]: getTracker[]
[Piwik]: trackPageView["My awesome app"]
Warninng: You should be aware that Proxies are not available in IE11. In our case this is acceptable because they are used only in development and not in production.
References: