Locker Service, Lightning Components and JavaScript Libraries
Introduction
As I’ve previously blogged, the Summer 17 release of Salesforce allows you to turn the locker service on or off based on the API version of a component. This is clearly an awesome feature, but there is a gotcha which I came across this week while working with the Lightning Testing Service Pilot.
I have a JavaScript library containing functionality that needs to work as a singleton (so a single instance that all Lightning Components have access to). This library is a static resource that is loaded via the <ltng:require /> standard component.
One window to rule them all?
In JavaScript world there is a single Window object, representing the browser’s window, which all global objects, functions and variables are attached to. When my JavaScript library is loaded, an immediately invoked function expression executes and attaches an my library object to the Window.
In Lightning Components world, with the locker service enabled, the single Window object changes somewhat. Instead of a Window, your components see a SecureWindow object which is shared among all components in the same namespace. This SecureWindow is isolated from the real Window for security reasons. In practice, this means that if you mix locker and non-locker Lightning Components on the same page, there are two different window concepts which know nothing about each other.
Example code
The example here is a lightning application that attaches a String to the Window object when it initialises and includes two components that each attempt to access this variable from the window, one at ApI 40 and one at Api 39. The app also attempts to access the variable, just to show that it is correctly attached.
App
<aura:application > <aura:handler name="init" value="{!this}" action="{!c.doInit}" /> <button onclick="{!c.showFromWindow}">Show from in App</button> <br/> <c:NonLockerWindow /> <br/> <c:LockerWindow /> <br/> </aura:application>
Controller
({ doInit : function(component, event, helper) { window.testValue="From the Window"; }, showFromWindow : function(component, event, helper) { alert('From window = ' + window.testValue); } })
Locker Component
<aura:component > <button onclick="{!c.showFromWindow}">Show from Locker Component</button> </aura:component>
Controller
({ showFromWindow : function(component, event, helper) { alert('From window = ' + window.testValue); } })
Non Locker Component
<aura:component > <button onclick="{!c.showFromWindow}">Show from non-Locker Window</button> </aura:component>
Controller
({ showFromWindow : function(component, event, helper) { alert('From window = ' + window.testValue); } })
Executing the example
If I set the API version of the app to 40, this enables the locker service. Click the three buttons in turns shows the following alerts:
From App
As expected, the variable is defined.
Non-Locker Component
As the application is running with the locker service enabled, the variable is attached to a secure window. The non-locker service component cannot access this so the variable is undefined.
Locker Component
As this component is also executing the with the locker service enabled, it has access to the secure window for it’s namespace. As the namespace between the app and this component is the same, the variable is available.
Changing the app to API version 39 allows the non-locker component to access the variable from the regular JavaScript window, while the locker component doesn’t have access as the variable is not defined on the secure window.
So what?
This had a couple of effects on my code if I mix Api versions to that my contains a combination of locker and non-locker components:
- I can’t rely on the library being made available by the containing component or app. Thus I have to ensure that every component loads the static resource. This is best practice anyway, so not that big a deal
- I don’t have a singleton library any more. While this might not sound like a big deal, given that I can load it into whatever window variant I have, it means that if I change something in that library from one of my components, it only affects the version attached to the window variant that my component currently have access. For example, if I set a flag to indicate debug mode is enabled, only those components with access to the specific window variant will pick this up. I suspect I’ll solve this by having two headless components that manage the singletons, one with API 40 and one with API < 40, and send an event to each of these to carry out the same action.
Related Posts
- Locker Service in Summer 17
- The Hurt Locker
- LockerService and Lightning Container Component: Securely Using Third-Party Libraries in Lightning Components (Developer Relations Post)
- Introducing the Locker Service for Lightning Components (Developer Relations announcement)