HomePhabricator
Creating a vue.js based skin with server side rendering
An inspiring experiment

For WMF staff's inspiration week, I decided to take a step back from my work building out a new skin architecture and a redesign of Vector and put myself into the shoes of a skin developer to see if the changes my team had made life easier. As a secondary objective, I was interested in how a MediaWiki skin could be written in Vue.js and what the challenges were to get there.

What I built

I decided to build a new skin called Alexandria named after the Great Library. The design was modeled on an open-source project and website I volunteer for called OpenLibrary.org.

I began by creating a skin that was JavaScript only.

I generated most of my boilerplate using the skins.wmflabs.org tool. I tweaked it so it gave me all the client-side tooling I needed.

Once I had that, I added a few more advanced features to my skin. In particular, I created a PHP class that extended the core SkinMustache class to allow me to extend the data given by core.

I wanted to be able to render my skin in JavaScript, so I needed to pass the data in PHP to the client. To do this, I added a template data value that represented a stringified JSON of the entire data that would be passed to the template like so:
https://github.com/jdlrobson/Alexandria/blob/master/SkinAlexandria.php#L29

The skin template rendered this JSON into a data attribute.

<div id="ol-app" data-json="{{data-json}}"></div>

This rendered a blank screen that was readable by JavaScript. While not a useful skin, from here, I was able to begin using Vue.js to parse that data attribute and pass it through Vue components to render the skin. https://github.com/jdlrobson/Alexandria/blob/master/resources/skin.js#L20

From here, I was up and running. I had a skin made from a Vue.js component that said hello world!

Building a skin with Vue.

This really was a breeze.

I made use of the [[ http://github.com/wikimedia/wvui | wvui library ]]for existing standard components, such as TypeaheadSearch and Button by including the wvui and vue libraries in my main skin module.

Keen to test out the work Roan and others did to request ES6 only modules, I decided to allow myself the luxury of writing code in ES6 and excluding older browsers.

https://github.com/jdlrobson/Alexandria/blob/master/skin.json#L86

When components didn’t exist in the wvui library, I made them and thinking in terms of components led to well scoped CSS. Aside from components inside the wvui library, I ended up creating components such as App.vue, AppArticle.vue, AppBanner.vue, AppFooter.vue, AppHeader.vue, DropdownMenu.vue, FooterMenu.vue, Portlet.vue, TypeaheadSearch.vue.

One thing that was frustrating as I created/renamed these components is I had to update these in the skin.json manifest. It was unintuitive and I often forgot to do it, which made development a little more tedious. I captured this in a Phabricator ticket, as I think it’s something that could be much better in the developer experience: https://phabricator.wikimedia.org/T283388.

While wvui and Vue.js were on npm, Since I was relying on some styles inside MediaWiki-core, I couldn’t use Vite or Parcel.js without lots of scaffolding, so I decided to develop without hot reloading. This slowed me down a lot as I was doing a lot of page refreshing.

Using the OpenLibrary project I copied across the CSS I needed. The resulting CSS was much better organized and scoped than the original project as I was constantly thinking about reuse.

Styling article content

To generate articles I used the MobileFrontend content provider ( https://www.mediawiki.org/wiki/Extension:MobileFrontend#Testing_with_articles_on_a_foreign_wiki_(live_data)) to generate articles to test on. I found myself running into a few issues with that and a few CSS rules that were in Minerva that should have been in MobileFrontend for toggling and ended up submitting patches to deal with that: https://gerrit.wikimedia.org/r/c/mediawiki/extensions/MobileFrontend/+/693503, https://gerrit.wikimedia.org/r/c/mediawiki/extensions/MobileFrontend/+/696644

Styles for thumbnails and table of contents are provided by core. Both of these styles didn’t fit in with the aesthetics of my design, so I ended up adding override CSS. I would have preferred to have not spent any time styling these elements so raised Phabricator tickets lest I forget to revisit our defaults https://phabricator.wikimedia.org/T283836 and https://phabricator.wikimedia.org/T283396 .

I wanted to do a lot with the content - such as move all images and infobox to the left, but after wrestling with inline styles, CSS grid and I gave up. It would be great if the parser marked up articles in a way that lent itself better to a grid system, but sadly it doesn’t. I didn’t know what tasks to raise here, as I didn’t think about it too deeply, but I want to acknowledge that this was a point of pain.

Server side rendering

I then turned my attention to server-side rendering. MediaWiki currently doesn’t support server-side rendering of Vue components. (https://phabricator.wikimedia.org/T272878)

To server-side render Vue components, a Node.js service is advised which PHP can request HTML from. Because I’m a little crazy and didn’t want to set up such a service, for now, I explored the differences between Mustache and Vue.js templates. I built a Node script that imported Vue components, found the template tag, and then traversed the DOM of that template rewriting it node by node recursively to a Mustache equivalent. Constraining myself to the minimal work possible I managed to create https://github.com/jdlrobson/Alexandria/blob/master/ssr.js

This mostly worked but of course, I ran into a few problems.

This couldn’t load the general components in the wvui library. For these, I had to define fallback templates such as https://github.com/jdlrobson/Alexandria/blob/master/resources/TypeaheadSearch.vue. For the search widget, I ended up rendering a form fallback that looked nothing like the JavaScript Vue version, but that was fine. I made sure the script threw an error so that I’d never load it accidentally in my JavaScript application.

I have a bad habit of giving Vue component props the same name as attributes. I had to stop this. A parameter id became menuId for example. This allowed me to avoid too much complexity in my Mustache template generator, to know when I was dealing with an attribute or a template property.

With for loops, it was easier to parse v-for=”a in list than it was to parse something like v-for=”a in data.list” so I made sure that was a constraint in the Vue templates I was writing.

I decided against computed properties as these involved JavaScript so those were not supported in my proof of concept.

Now I was generating a template via a build step, my skin was working with JavaScript loading, however, loading styles for the new experience became my next problem. I had included all my styles in a Vue template, so now needed them out. I extended my build script to generate a stylesheet as well, but later backtracked on that and pulled the styles out of the Vue components. I opened https://phabricator.wikimedia.org/T283882 to discuss best practices for that.

API-driven frontend!

Now I had a skin rendering in Vue via JavaScript. It would be silly not to play to its strengths and make it a single-page application, loading content from JS. Unfortunately, there was no Skin API, only APIs for generating content and various things can vary on a page such as JavaScript/CSS loaded, mw.config values and even items in menu.. eek!!

A while back I made https://github.com/jdlrobson/mediawiki-skins-skinjson to help with skin development. It allows you to see a JSON representation of the data that a skin template can render. I repurposed this to allow me to use it as an API in my app. I made use of this. Pages rendered via API calls and JavaScript with ease. Wire up was very small: https://github.com/jdlrobson/Alexandria/blob/master/resources/App.vue#L252 Maybe something for the #product-infrastructure-team-backlog ?

This allowed me to load articles, however, I ran into technical debt. Most MediaWiki extensions expect to be run on page load. Some work, because of the use of mw.hook. For event handlers bound to the body tag using a proxy pattern, things just worked. We clearly need to use that pattern more if we ever want to go down this route.

E.g

$(‘body’).on(‘click’, ‘.uls-button’, loadULS );

https://skins-demo.wmflabs.org/wiki/Alexandria?useskin=alexandria demonstrates article loading using the Parsoid API

Reflection

  • Converting Vue templates to Mustache is possible with constraints
  • There are a few kinks in ResourceLoader that need to be worked out
  • API-driven skins are possible if we're willing to put in the effort across our codebases to build the APIs and rethink how our existing features load.
Written by Jdlrobson on May 27 2021, 9:54 PM.
User
Projects
None
Subscribers
dev.kadirselcuk, AnneT, Quiddity
Tokens
"Love" token, awarded by AnneT."Love" token, awarded by Mholloway."Love" token, awarded by Quiddity.

Event Timeline

Great post: we're doing to need to do more of this "eating our own dog food" in the process of modernizing different parts of our ecosystem, to ensure that all those parts can work together and that we're not negatively affecting developer experience anywhere within that ecosystem. Bonus points for nonchalantly whipping up an SSR implementation. Thanks for sharing!