At the outset of this project, there was a lot of discussion about which front-end framework we’d be using. Why use something like Vue when React is clearly more popular?
Several factors pushed the needle toward Vue over React for our team. The first factor was Template syntax.
Vue templates look like traditional HTML with additional attributes — called Vue directives — sprinkled in.
In addition to to the template syntax, Vue supports single-file components which bundle familiar HTML, CSS and JS tags together in the same .vue file. Our team found that it was a convenient way to structure our components and keep our project organized.
Vue was also a great choice for our team because several team members had experience with it in smaller projects. Plus, while it is a less-popular project than React, it still has a vibrant community and an active core development team.
Getting Started with Webpacker
And since we’re using Rails as our backend for this app, we chose to use the official Webpacker add-on for Rails. This proved to be a delight to use! It made setting up our initial Vue single-page app a breeze. With a single command, Webpacker scaffolded a simple Vue component and wired up its Webpack config under the hood to compile the components:
One upside to this approach is that you can skip writing your own Webpack configuration by hand. But this level of abstraction is a double-edged sword.
An example: shortly into the project, we decided it would be useful to be able to share a set of Sass variables between Vue single-file components when defining per-component styles. The Webpack add-on sass-resources-loader aims to make this simple. But since Webpacker was using vue-loader inside the abstracted Webpack config, our new Sass loader wasn’t being applied in the proper order required to compile single-file Vue components.
Additionally, vue-loader loads its own set of Webpacker loaders which can be customized – but using only a string format instead of the object format required by sass-resources-loader. A workaround exists, but it results in a configuration override like this:
One thing our team learned through this entire process is that these plugins are under active development. Both Webpacker and vue-loader released fairly significant updates during our development period. Using the “latest and greatest” often means sacrificing stability and maturity in software.
Six engineers from our team worked closely together to build our internal app. This presented a few challenges, because none of us had really built a Vue single-page app before — and we had to coordinate who was going to do what.
To reduce confusion and to combat uncertainty, we focused on creating tiny, actionable tasks and assigning them to Github milestones. Each milestone represented a different chunk of functionality within the app. And as we progressed through each milestone, we discovered more tasks for that milestone.
As the back-end part of the app progressed, we gradually removed these stubs and replaced them with actual AJAX calls.
Some parts of collaboration can’t be planned in advance. For example: a few weeks into the project, we had a smattering of Vue files living in a single directory called components. This was fine because Vue isn’t opinionated about how you organize single-file components — but it started to feel a bit unwieldy.
These conversations happened organically through Slack over the course of several weeks. We eventually settled on a file structure that split pages and components into separate folders.
Recently, our team has been making an effort to pair program more frequently. Since most of our engineers are remote, this involves one person “driving” and sharing their screen while one or more people watch and share their thoughts.
These pairing moments also happen organically through Slack:
Our experience pairing on this project reinforced our decision to try to pair more often. We found it less daunting to learn a new front-end framework like Vue when we were able to walk through writing code with our teammates.
Linting and Testing
In addition to ESLint’s recommended settings, we added eslint-plugin-vue to our project so our Vue files could be linted, too. One rule in particular — order-in-components — was helpful because Vue instance definitions can become quite large, and searching for a specific property across several components was easier when each component was consistently-defined.
Another way we maintained code quality on a multi-person project was by writing tests. On the front-end, we wrote tests for Vue components using the official vue-test-utils library. While also a pretty new tool — it’s still in beta at the time of this post — it provided useful utilities to write tests for Vue components.
We found that a difficult part of front-end testing was stubbing the data our components normally fetched with AJAX. Our team experimented with the following three approaches to faking data for tests:
- Provide fake data for a component, and stub the initial fetcher method
The first approach was the simplest: stub the initial fetcher method, like fetchUsers() on the component, and set the fake data manually. While this worked, it limited our ability to write more complete integration tests: “How can we be sure that this component loads from start to finish, setting the given fake data, and renders the expected HTML?”.
That’s when we landed on our third approach: stubbing the HTTP requests used by JSORM. This proved to be a little more difficult than using a fake XHR and server through Sinon, since JSORM uses the native Fetch API.
This allowed us to write more complete integration tests, even if the creation of stubbed endpoints and fixtures meant more work at the beginning.
One part of the Vue project we weren’t able to lint or test was the order of attributes in each tag. It might sound silly that this is something we’d need to consider, but take this example:
Notice the v-if and v-else tags. Since the first element has the directive immediately after the opening of the tag, it’s pretty obvious that it will render conditionally. But on the second element, v-else is not the first attribute.
When reading through a large component, our team noticed that this made it difficult to determine the control flow of certain dynamic parts of a given component.
Engineer Winston Hearn made a comparison to Ruby’s ERB files which require an explicit pair of <% %> tags for control flow. These tags tend to jump out at you more than a standard HTML tag with a directive attached.
Drew Amato, another engineer, mentioned that he’s never seen lines blur as much — between "markup" and "code" — as with a Vue project.
And engineer Becca Barton noted that it would be very helpful to quickly glance at any file and be able to see which pieces are just plain HTML and which are modified by Vue directives.
To solve this issue, we made a “handshake agreement” that we would always make the control flow Vue directives — v-if, v-else and v-show — the first attributes in the tag. This rule helped clean up our templates and made our code more readable:
Under the Hood: JSON-API Suite
Our team decided to adhere to the JSON-API spec when building out the server-side API for this project. Principal engineer Steve McKinney discovered a really great toolset built for Rails projects called JSON-API Suite.
JSON-API Suite presents the concepts of resources and serializers which are tied to each ActiveRecord model you’d like available to your API.
Our front-end engineers quickly discovered that interacting with server requests and responses formatted for the JSON-API spec was a bit of a chore: the data attributes are nested in a child object, and relationships are split into a series of sibling data objects.
Using JSORM changed our world. Querying and persisting objects felt very similar to how we might do it on the server in Rails. The only weird part about this was having to declare model and relationship behavior on both the server and on the front-end.
We had success adhering to the JSON-API spec using the JSON-API Suite, and it sounds like other companies like Netflix are experimenting with new approaches to model serialization with the fast_jsonapi gem.
What did we learn?
- Vue is really lightweight, and it’s a popular tool with a lot of third-party libraries and plugins. This makes implementing certain tools — like routing and state management — a breeze. However, since Vue is so lightweight, you might find yourself writing extra functionality for a single-page app you wouldn’t have expected to — like dynamic page titles and meta descriptions.
- Single-file components are really great. Our team of engineers felt that Vue’s single-file component system was intuitive, and it was easy to set up a minimum-viable product with very little experience. Additionally, grouping functionality, markup and styles in a single felt more natural than splitting up a component’s functionality with a separate template file, like you might find in Glimmer. Finally, we enjoyed writing markup that was similar to HTML instead of ramping up to JSX markup we might have used had we chosen React as our framework.
- Vue isn’t opinionated, so you have to be. Our team had a lot of discussions surrounding file structure and rules of thumb for writing components consistently. Since Vue doesn’t dictate these things out of the box, it was important for our team to be opinionated about our approach in order to reduce chaos.
Our team is excited to share the experience we had with Vue and Rails with the rest of the product team. Share your thoughts or experiences with me at @jplhomer.
This project was a joint effort between members of the Revenue Platforms team: product manager Briar Cromartie, designer Matt Sullivan, engineers Drew Amato, Becca Barton, Messay Bekele, Josh Larson, Winston Hearn, Steve McKinney, and Guillermo Esteves, and QA team Nate Edwards and Andrew Geesler.
Special thanks to Becca Barton for thoughtful edits and contributions to this post.
Our team found the following resources to be useful, especially for a first-time Vue project: