Using Polymer with Closure Compiler - Part 2: Maximizing Renaming
Monday, June 20, 2016 | 5:40 AM
This is the second post in a 3-part series about using Polymer with Closure Compiler
UPDATE: goog.reflect.objectProperty
is now
available as part of the 20160619 compiler and library releases.
Closure Compiler's ADVANCED
mode property renaming and dead code elimination put it in a class all its own. In ADVANCED
mode, the compiler performs “whole world” optimizations. Polymer apps can take advantage of these optimizations
without losing functionality.
How Closure Compiler Renames Properties
Closure Compiler property renaming occurs in two primary ways. The first is quite straightforward: all properties
with the same name are renamed in the same way. This is ideal because it doesn't require any type information to
work.
All instances of .foo
are renamed .a
regardless of on which object they are defined.
However, if any property with the same name is found on any
object in the externs, the compiler cannot rename it with this strategy. The more extern properties included in your
compilation, the fewer properties can be renamed with this method.
The second method for property renaming was created to address the shortcomings in the first method. Here, the compiler uses type information to rename properties so that they are unique. This way, the first method can happily rename them as they no longer share the name of an extern property. This method is called type-based renaming and as its name suggests, it can only work with proper type information. It will decline to rename a property if it finds the same property on an object for which it cannot determine type information. The better type information provided, the better this method works.
Finally, for property renaming to work at all, properties must be consistently referenced. Properties accessed using
an array style bracket notation (such as foo['bar']
) are called quoted properties and they will never
be renamed.
Properties accessed using a dot notation (such as foo.bar
) are called dotted properties and may
be renamed. Your code can break if you access the same property using both methods - so make sure you choose one and
are consistent.
Renaming Polymer Properties
The Polymer library itself is considered an external library. A well maintained
externs file
for Polymer is hosted within the compiler repository (and distributed in the npm version).
Lifecycle methods (such as created
, ready
, attached
, etc) are externally defined and therefore not renameable. Also, as mentioned in part 1 of this series
, declared properties defined as part of Polymer's properties
object can never be renamed.
That leaves non-lifecycle standard properties as eligible for renaming - as long as they are not quoted. However, since Polymer's listeners and observers are specified as strings, that breaks the consistent access rule for properties and forces you to quote those properties. There are, however, other options.
Observers and Listeners
A Polymer element declares a property observer like:
Polymer({ is: 'foo-bar', properties: { foo: { type:String, observer: 'fooChanged_' } } /** @private */ 'fooChanged_': function(oldValue, newValue) {} });
In this case, our fooChanged_
method is a private implementation detail.
Renaming it would be ideal. However for that to be possible, we would
need to have access to the renamed name of fooChanged_
as a string.
Closure Library has a primitive that Closure Compiler understands to help in just this case:
goog.reflect.object.
By using goog.reflect.object
we can rename the keys of an object literal in the same way that our Polymer element is renamed. After renaming, we
can use goog.object.transpose
to swap the object keys and values enabling us to easily lookup the name of our now renamed property.
var FooBarElement = Polymer({ is: 'foo-bar', properties: { foo: { type: String, observer: FooBarRenamedProperties['fooChanged_'] } } /** @private */ fooChanged_: function(oldValue, newValue) {} }); var FooBarRenamedProperties = goog.object.transpose( goog.reflect.object(FooBarElement, { fooChanged_: 'fooChanged_' }) );
We can use the same technique to rename listener methods:
var FooBarElement = Polymer({ is: 'foo-bar', listeners: { 'tap': FooBarRenamedProperties['tapped_'] } /** @param {!Event} evt */ tapped_: function(evt) {} }); var FooBarRenamedProperties = goog.object.transpose( goog.reflect.object(FooBarElement, { tapped_: 'tapped_' }) );
Triggering Property Change Events
Polymer provides three different methods to indicate that a property has changed and data-binding expressions should
be re-evaluated: set
,
notifyPath
and notifySplices
.
All three have one unfortunate thing in common: they require us to specify the property name as a string. This
would also break the consistent access rule for properties and once again we need access to the renamed property as
a string. While the goog.object.transpose(goog.reflect.object(typeName, {}))
technique would also work for this case, it requires us to know the globally accessible type name of the object. In
this case, Closure Library has another primitive to help: goog.reflect.objectProperty
. This method is very new. As of this writing, goog.reflect.objectProperty
has yet to be released in either Closure Compiler or Closure Library (though it should be soon).
goog.reflect.objectProperty
allows us to call the notification methods with a renamed string.
Polymer({ is: 'foo-bar', baz:'Original Value', attached: function() { setTimeout((function() { this.baz = 'New Value'; this.notifyPath( goog.reflect.objectProperty('baz', this), this.baz); }).bind(this), 1000); } });
goog.reflect.objectProperty
simply returns the string name (first argument) in uncompiled mode. Its use comes solely as a Closure Compiler
primitive where the compiler replaces the entire call with simply a string of the renamed property.
Summary
By reserving Polymer's declared properties for cases where the special functionality offered is actually needed,
quite a bit of renaming can be obtained on an element. In addition, use of goog.reflect.object
and goog.reflect.objectProperty
allows us to rename properties which are required to be used with strings.
However now we find ourselves in a case where all this renaming has broken references in our template data-binding expressions. Time for Part 3: Renaming in Polymer Templates.
5 comments:
PeterStJ said...
I am wondering: how do we structure the polymer element definition in such a way as to allow the compiler to run smoothly as part of a build process.
The default way of creating polymer elements is to inline the script in the html file. I guess it would not be ideal for closure compiler. I have used the approach of linking the script via src of script tag and define everything in standard closure compatible JS. This has the disadvantage of having half the code compiled (via closure compiler) and other half is not compiled (polymer itself, but also the elements available freely like iron and paper elements etc).
I use closure library for some data structures, custom code written in the same style for logic in the app and only wrap it as polymer elements, polymer renamer for the templates but there it is also a bit unclear, should I run each template via the renamer separately or it is better to run the vulcanized version directly with the map from the build etc... I would be really great if those can be described (i.e. how the tools work together and in what succession, how can one combine the default recommended tools and closure ecosystem tools etc).
Thanks
June 20, 2016 at 2:27 PM
Chad Killingsworth said...
I talk about some of this in post 3. With use of Cripser, it's developer choice whether to use inline scripts or external references. My team is experimenting with non-trivial definitions being an external JS file to limit the length of a component HTML file, but that's for maintainability reasons - not technical build constraints.
As for mixing elements, I'm not using any of the publicly provided Polymer Elements (iron-pages, etc) in my compiler project - just the library myself. However, mixing those components in is going to be tricky unless they are fully compatible with the compiler's ADVANCED optimization mode. Template renaming is currently an all or nothing approach. It should be possible to treat some elements as external and some not, but I haven't personally tried that. I'd to start seeing the polymer element library be made compatible with the compiler natively.
Whether to use Closure-Library or not typically comes down to how widely I want to share the component. If it's internal only for a single app - then absolutely. If I'm going to be sharing the component among any projects not already using Closure-Library - I try to avoid it and use as few dependencies as possible.
June 20, 2016 at 5:04 PM
Anonymous said...
Instead of the code snippet where you use goog.object.transpose and goog.reflect.object, would this work?
var FooBarElement = Polymer({
is: 'foo-bar',
properties: {
foo: {
type: String,
observer: goog.reflect.objectProperty('fooChanged_', this),
}
}
/** @private */
fooChanged_: function(oldValue, newValue) {}
});
October 25, 2016 at 2:07 PM
Anonymous said...
Ah, no it wouldn't because 'this' is the wrong value at that spot.
October 25, 2016 at 2:15 PM
Chad Killingsworth said...
For this exact reason I started a discussion on overloading `goog.reflect.objectProperty` so that the 2nd argument could be a string literal. In that case it would be used as a named type.
October 26, 2016 at 3:01 AM
Post a Comment