ddraper

Deja-Vue

Blog Post created by ddraper on Jan 16, 2017

Introduction

If you've been following my personal blogs then you'll know that I've been experimenting with some of the currently popular web development frameworks (Vue.js, React, Aurelia and Angular). Each comes with their own advantages and disadvantages but my personal preference (and everybody is entitled to their own preference!) was for Vue.js.

 

There are lots of reasons to like Vue.js (many of which are summarized described here) but one of the things that I found really useful was that you could very easily write it in plain ol' ES5 JavaScript.

 

An Argument Against Transpilation

Now don't get me wrong... I love writing in ES6 and I get positively giddy when writing in TypeScript (although that might just be down to my flagrant abuse of the "any" keyword) but ultimately the code you write in ES6 and TypeScript is going to get transpiled down into ES5 and that can mean that the code ends up both larger and in some cases actually less efficient which will actually result in poor performance.

 

Bundling and Tree Shaking

The other interesting thing about these new frameworks is that they all offer lovely development environments with hot-reloading which can really enhance development speed.... however, when you come to deploy your code you still need to bundle it up and will want to tree-shake and minify (or uglify) the code - and you have to do this each time you want to deployment your code.

 

The idea of tree-shaking is that you remove all the code that isn't required on the page in order that the page load is faster to boost performance. Surf has actually been providing this capability via Aikau since March 2013. The fundamental differences being that it is done in Java (and not via Node.js) and that it does this efficiently at production time through aggressive caching. 

 

  • Yes, you can load System.js into the browser and transpile and asynchronously load from the client but the performance will be poor.
  • Yes, it is true that that there is some initial overhead to Surf/Aikau performing dependency analysis on the first page load this then disappears for all subsequent page loads through aggressive caching.
  • Yes, it's also true that Node.js will do the necessary string processing required for dependency analysis must faster than Java, there isn't (to my knowledge at least) any Node.js middleware that is offering this capability.

 

Re-usability

The other aspect of web application development that has always interested me is in re-use. It's really easy to write some JavaScript component that is re-usable as a "leaf node" and you can probably provide some configuration options to customize its appearance or behaviour.

 

We should all be implementing components following the "single responsibility principle" that will typically result in a nested component hierarchy. This is perfectly fine where you own that entire hierarchy because you control all the components and are the only one using them. But what if you want to share your components for others to use? And what if they want to customize those components?

 

Let's say you provide component A which contains component B which contains component C. Someone else wants to replace C with D, so they have to customize B to create D instead of C (which results in component E) and then customize component A to instantiate E instead of B. So the end goal was A contains B contains D, but the result was F contains E contains D.

 

That's 3 extra components that you now have create and maintain instead of 1.

 

Transclusion, et al

Some of the frameworks provide a solution to this that is variously called "transclusion" (Angular), "content projection" (Aurelia) and "content distribution" (Vue.js) but I've found that these only really work to a single level of nesting and aren't the easiest things to implement. 

 

The reason that this is a hard problem boils down to dependency management. A component will need to declare the sub-components that it can contain in order to be able to render them.

 

Aikau has always provided a solution to this problem by allowing developers to compose pages of widgets in a JSON model that is analysed by Surf to establish all of the dependencies prior to rendering. Widget references in the model can easily be swapped out, removed or reconfigured without needing to update any other widgets in the model.

 

Unfortunately one of the main criticisms levelled at Aikau was that it's hard to find people with the skills despite the fact that all you really need is some basic web development skills and an understanding of 3 very simple concepts (as described here).

 

So this got me to thinking...

 

An Idea...

We already have a great tree-shaking and module loading solution in Surf (albeit one that is desperately uncool) that provides an excellent basis for providing re-usable components (as ably demonstrated by Aikau for over 100 releases).... what if we could marry that existing infrastructure with some cool new JS framework?

 

So that's what I've done.

 

I've added a couple of widgets into Aikau that allow you to easily build Vue.js components that can be composed together in the traditional Aikau page models. This means that if you can get your head around some trivial boilerplate code then you can create re-usable and Vue.js components.

 

The Solution...

So the boiler plate code looks like this:

define(["dojo/_base/declare",
        "aikau/vue/Base",
        function(declare, Base) {
  
   return declare([Base], {

      getComponentElement: function() {
         return "";
      },

      getComponent: function getComponent() {
         return {
         };
      }
   });
});

 

There are two functions that need to be implemented:

  • getComponentElement: returns a string with the element custom component element name
  • getComponent: returns a Vue.js component object

 

For example, I implemented the same simple application as I had previously done in standalone Vue.js (and React, Aurelia and Angular!) which resulted in 4 components including a toolbar component containing forward and back pagination buttons that looks like this: 

define(["dojo/_base/declare",
        "aikau/vue/Base",
        "dojo/text!./templates/Toolbar.html"],
        function(declare, Base, template) {
  
   return declare([Base], {

      getComponentElement: function() {
         return "toolbar";
      },

      getComponent: function aikau_vue_Toolbar__getComponent() {
         return {

            template: template,

            props: ["list"],

            methods: {
               back: function() {
                  var changeEvent = new CustomEvent("pageBack", {
                     bubbles: true
                  });
                  this.$el.dispatchEvent(changeEvent);
               },

               forward: function() {
                  var changeEvent = new CustomEvent("pageForward", {
                     bubbles: true
                  });
                  this.$el.dispatchEvent(changeEvent);
               }
            }
         };
      }
   });
});

 

These components can be composed in an Aikau page model like this:

model.jsonModel = {
   widgets: [
      {
         name: "aikau/vue/Bootstrap",
         config: {
            widgets: [
               {
                  name: "aikau/vue/List",
                  config: {
                     widgets: [
                        {
                           name: "aikau/vue/Breadcrumb",
                           config: {
                              "v-bind:relativePath": "relativePath"
                           }
                        },
                        {
                           name: "aikau/vue/Toolbar",
                           config: {
                              "v-bind:list": "list"
                           }
                        },
                        {
                           name: "aikau/vue/ListView",
                           config: {
                              "v-bind:list": "list"
                           }
                        }
                     ]
                  }
               }
            ]
         }
      }
   ]
};

 

How it Works

The solution is pretty straightforward. The "aikau/vue/Base" module (that all the Vue.js component widgets must extend) iterates over any child widgets and registers a local component for them. The child component element (the value returned from getComponentElement) is written into the template and the properties defined in the widget config are output as attributes of that component.

 

If you're familiar with Vue.js then you'll note the use of the "v-bind:" prefix that is used in this case to create a dynamic binding between the "list" value in the List widget and the Toolbar and ListView widgets. This means that when "list" is updated in the List component the Toolbar and ListView components can reactively update to reflect the changes.

 

The List component template contains a single property "${widgets_slot}" that is swapped out with the locally registered child components, so that...

<div @navigate="navigate" 
     @setRelativePath="setRelativePath"
     @pageBack="pageBack"
     @pageForward="pageForward">

   ${widgets_slot}
</div>

...becomes...

<div @navigate="navigate" 
     @setRelativePath="setRelativePath"
     @pageBack="pageBack"
     @pageForward="pageForward">

   <breadcrumb :relativePath="relativePath"></breadcrumb>
   <toolbar :list="list"></toolbar>
   <list-view :list="list"></list-view>
</div>

...when the child components are rendered.

 

Limitations

The only limitation that I've found with this approach is that I wasn't able to take advantage of the Vue.js event model and needed to fall back to native custom events, but the only downside I could see to this was that you wouldn't be able to benefit from the excellent debugging tools provided by the Vue.s developer extension for Chrome.

 

Why?

So if you're creating a Vue.js application (or indeed an application using any other frameworks) then you probably won't want to jump through these hoops. But if you want to provide an application that you want people to be able to customize (like Alfresco Share) then you need to allow developers to make small (or large) changes to deeply nested components without copy/pasting code or extending every component in the hierarchy of the only one they actually need to change.

 

Even in this very simple 4 component example it means that you could swap out the ListView component without needing to change the parent component. The ListView component itself could be re-written to be composed from a library of metadata renderering components that could be selected from to render views specific to the data displayed.

 

This approach also allows composition of components that haven't been written with respect to each other - once again something that we have been able to successfully benefit from using Aikau at Alfresco.

 

What Next?

I strongly doubt that this implementation is going to be used anywhere unless there is any kind of major demand for it from customers, our partners or the community. It currently exists on an unmerged branch of Aikau but could easily be integrated into the main code base and released for use.

 

Could This Approach be Used with Framework {X}?

The main reason why I picked Vue.js for this experiment takes us back to the original point way back at the beginning of this post. Vue.js can very happily be run without transpilation in ES5 code. This same approach could theoretically be applied to React but to get the best out of it you'd want to use JSX and that would require a transpilation step that Surf currently doesn't provide. 

 

It would obviously be possible to transpile the component code as part of the build process for Aikau but that would introduce its own set of problems as Surf/Aikau doesn't provide source maps which would create a barrier to effective debugging.

 

Conclusion

If you've reached the end of this very long post then thanks for your perseverance! I'd really appreciate any comments and thoughts that you might have on this - especially on whether you think the use case being addressed is even a valid one.

Outcomes