How to reuse one Vue.js codebase across multiple apps

Thomas Holland
Dec 12, 2019 · 15 min read

This story is a follow-up on A domain-driven Vue.js Architecture, where we introduced an architecture that’s based on Business Domains rather than technical concepts like components or store. If you haven’t already, we recommend to read it first as we will build upon that foundation for this one.

Premise

A Vue.js app typically needs to do a couple of things, no matter what:

  • Implement a whole bunch of Business Logic

One of our current apps is a somewhat special case, in that the client we are developing for is sort of a middleman. We do not deliver one particular version of that app, but instead tailor it to the unique needs of several companies that in turn are our client’s clients. From now on, we’ll call them consumers for convenience’ sake. This specific scenario entails a number of other requirements in order to make it reusable across different consumers:

  1. Offer one core set of functionality

We’re basically talking about a white-label product with extra steps. Making all of this work well was an exciting challenge. We soon realized that developing for reusability increases the workload significantly, which proved to be especially true for the initial setup of the project’s whole infrastructure. But it was to be expected. The project’s roadmap envisages a prolonged ramp-up phase with diminishing low efforts for each additional consumer.

- Augustus. A couple of years BC.

This translates to make haste slowly, and we wholeheartedly agree on that.

1. The core

Remember the architecture diagram of our previous story?

Let’s have a look at it again:

The architecture diagram of our previous story on medium.
The architecture diagram of our previous story on medium.
The idea behind the core (2019, colorized)

You are looking at the essential idea behind the core’s architecture. Together with our client, we evaluated each business requirement and made sure to only include functionality in the core that qualifies as useful to every single consumer. This is the baseline for each specific evolution of our app. We proceeded to translate each requirement to a Business Domain in our architecture. In this example, those are Homepage, Profile and Settings.

A zoomed-in view of our architecture diagram’s UI layer in the Homepage slice.
A zoomed-in view of our architecture diagram’s UI layer in the Homepage slice.

If we zoom into the UI layer of the vertical slice Homepage, we’d traditionally see something like this. A Homepage.vue Single File Component that neatly colocates <template>, <script> and <style>. Later on, we’ll adjust this classical notion of a SFC slightly to our specific needs. Just keep in mind that right now, this is already a fully functional application and most of the time, you wouldn’t need to change a thing. What’s missing are means of individualization. Over the course of this story, we will enhance the architecture diagram so that it meets all of the above requirements.

2. Corporate Identity

There comes a point in time for most companies when the need arises to establish guidelines for their corporate identity which might or might not result in a design system of some sort. You can put an incredible amount of work into such guides and systems. You can go really deep into how your brand should sound like in which scenario, you can explain the specifics around accessibility or you can define your brand’s iconography. There’s a million things to consider. Karri Saarinen wrote a beautiful article on building a visual language for Airbnb, that only deals with the visual part of things.

Customizations

We needed to reduce the surface area of CI guides that we interact with. When you’re developing one thing but that one thing needs to be adaptable for a myriad of consumers, you simply cannot take each and every CI detail into account without breaking the idea of having one shared core. What that means in practice, is that we let each consumer choose a couple of things which they then have to provide:

  • Brand logos
An abbreviated example of the fundamental visual building blocks, represented as a JavaScript Object.

We do provide fallbacks for all of those in case a consumer does not want to configure everything. While this might not satisfy all the requirements a CI guide will call for, it does already give you a lot to work with.

This results in the need to almost over-communicate those technicalities to our client and for them to do the same with their consumers. When declaring reusability a first-class citizen, this impacts future product decisions. In this context, further customizations are of course possible— it’s simply a matter of money and time.

CSS-in-JS

The attentive reader might have noticed that defining CSS variables in JavaScript is pretty suggestive of a technology commonly coined as CSS-in-JS. And to no big surprise, it is indeed the solution we deemed most suitable for our needs.

CSS-in-JS triggers numerous heated debates. We won’t go into all that as it’s been written so much about already. If you want, you may jump into this rabbit hole, starting with an article about differing perspectives on CSS-in-JS by Chris Coyier that also points to further resources on that matter.

Vue.js actually provides its own CSS-in-JS solution in the form of Single File Components. The React community on the other hand came up with its own ideas around the problem. This is where we see fundamental differences between React and Vue come into play: Where Vue as a framework offers an all-in-one solution, React’s community was pretty much forced to develop the flourishing ecosystem that it is today by themselves since React itself is just the UI library. Two popular options that emerged are styled components and emotion, among others. And as it turns out, there is an emotion adapter for Vue as well which ultimately became our tool of choice.

Instead of going into too much detail on how and why to use it, you may want to understand styled components’ motivation, then have a look at this demo which should hopefully explain the gist of it:

CSS-in-JS example

Here, you can see the theme in action. Once provided at the app’s root level, you can access the fundamental visual building blocks of your application anywhere. Also, notice how styles are no longer bound to classes but instead manifest in the form of actual Vue components with speaking names — hence the name styled components.

The updated zoomed-in UI layer, now reflecting our changes introduced with CSS-in-JS.
The updated zoomed-in UI layer, now reflecting our changes introduced with CSS-in-JS.

Our zoomed-in UI layer of the Homepage slice now looks a bit different. We got rid of the <style> tag altogether and moved our styles inside of our actual JavaScript, defined in <script>. Notice that we did not move them out of the UI layer, only within. We’ll go into more detail around how we take advantage of this refactoring in the following chapters.

Me and others chimed into one of the current Vue RFCs regarding scoped style improvements, where we champion some of the advantages that libraries such as styled-components or emotion bring to the table. IMHO it would be incredible if such functionality would be baked into Vue itself. If you feel strongly about what’s to come to Vue, this is your chance to take part in that conversation.

3. Theming our app

As mentioned earlier, the following equation applies for this story: theme !== theme. Or in other words, CSS-in-JS libraries use the same naming conventions for their global themes as we do for our app’s themes, when in reality, there’s a big difference. When we talk about themes, we refer to all of our app’s CSS as well as distinct components that may differ between different themes. This will become clearer when we update our architecture diagram for the first time. For this, we introduce a new concept called Container. You should be familiar with the Core by now, which is put into its own container. Additionally, there’s now a new Theme container which accommodates any number of themes.

The architecture diagram, extended by themes.
The architecture diagram, extended by themes.
Architecture += Themes

Styles

In this first extension of our architecture diagram, styles moved from the UI layer of each functional slice into the UI layer of each theme. Hence, the core is left without any styles. The core’s sole function is to define what all the components are supposed to be doing and not how each component shall look like, apart from their semantic structure. In addition to the fundamentals like colors, spacings etc., we can now express unique stylings per theme with the full power and flexibility of CSS. The entire visual appearance is subject to reshape which allows each theme to be a completely different experience that goes beyond a simple paint job that solely changes brand colors — while guaranteeing that everything still just works™ since there remains exactly one truth to our components’ functionality, defined in the core.

What’s best is that consumers may now be presented a wide range of different visual concepts, alongside their brand colors and remaining visual identity in virtually no time with only little more effort than the flip of a switch. This is where consumers can choose a visual baseline on top of which we may fulfill possible individual wishes.

Assigning the right styles to the right component

This is where the in JS part of CSS-in-JS comes into play. Since emotion’s abstraction on top of CSS is in the form of plain ES6 exportable Vue.js components, you may do anything to your styles that modern JavaScript allows you to do. In particular, you may place them anywhere you want and are not bound to the <style> tag of your SFC.

On the flip side, this increases the distance between your component definition and its associated CSS. Collocation of all the relevant bits is one of the original selling points of components. When done properly, components turn into truly self-contained entities that handle rendering & styling, data requirements and corresponding business logic all by themselves. Our approach makes you lose some of those advantages in favor of reusability. Sometimes software development is about making tradeoffs, and in this case reusability trumps the convenience of collocation.

Now, how exactly does the marriage between the Core and any one of the themes take place? For that to happen, we make use of a code sharing concept called Higher Order Components (HOCs) as well as webpack’s require.context functionality.

HOCs are probably best explained with their mathematical equivalent. Imagine implementing two functions f: x => x and g: x => 2x. After a while, the requirements for your functions change: All functions’ results need to be increased by 1. Sure enough, you update your function definitions to f': x => x + 1and g': x => 2x + 1 respectively. Though, instead of doing the work manually for each function, you could have chosen to write a new function h: x => x + 1, and call it with your functions f and g: f': h(f(x)) => x + 1, g': h(g(x)) => 2x + 1. In case you had 1000 functions instead of just 2, implementing that new function h may just be the reason you were able to leave work early that day. As a matter of fact, you can apply the same principle to Vue components. You can write a function that takes a component, modifies it as appropriate and returns a new component with said modifications. This CodeSandbox illustrates the concept:

HOC example

This is a HOC withBox that adds a new component Box to a baseComponent, much like h adds + 1 to a function.

We are excited about the Composition API Vue@3 will bring. Higher Order Components, while being cool and all, carry their own disadvantages. They are kind of a foreign concept in the Vue community on top of that. In the example, what would happen if App registered its own Box component? We’re confident that the Composition API will allow for more elegant solutions to this sort of problem.


Regarding require.context, it’s first of all important to note that it is not part of the ECMAScript spec but something that comes along with webpack. Which means that this function is run during build-time, not runtime.

For our tests that aren’t aware of any webpack magic, we’re using a babel plugin.

require.context is great for recursively requireing a whole bunch of dependencies at once, matching an arbitrary RegExp, starting at a pre-defined root directory. In our case, we may define a context for our theme as follows:

This requires all files, starting from THEME_ROOT_PATH, that end with .styles.js. THEME_ROOT_PATH is a constant defined with webpack’s DefinePlugin.

This in turn accesses an environment variable THEME, defined with cross-env in our package.json, which is the single point in all of our code that lets us control the visual appearance for a consumer. It is actually just one flip of a switch.

Putting all of this together, we end up with a HOC withStyles that takes a Vue component MyComponent.vue, searches for its style implementation MyComponent.styles.js of theme THEME on the basis of MyComponent.vue’s name and finally extends its registered components by all the styled components found in its style implementation, much like the HOC example above. That’s why, in the example below, we use Wrapper and Box in addition to the regularly registered component Text.

MyComponent.vue, wrapped in withStyles.
Style implementation for MyComponent.vue

Further, we create a custom key styleInterface, which serves kind of a similar purpose as the local registration of components. It is an array of Strings, indicating what exact styled components are expected to be implemented by each theme. We use this in order to throw warnings during development in case a theme is missing some style implementations, as well as having an in-file overview of what components are at our disposal in the first place.

Extension Points

Different themes may intend functionality that differs slightly from the core’s implementation. Sometimes, you run into problems that CSS alone cannot solve. Take a display component for a couple of items, for example. Most themes might want to display items in a grid while another one assumes a slider that goes through them, highlighting one item at a time. A first naïve implementation could be to have a boolean prop useSlider, which toggles between a grid display and a slider display. Maybe a prop display would be even better: Just choose between a number of display modes! One problem with this is that inevitably you end up with ginormous components that do way too many things at once. Instead of asking: “How can I make this component do one more thing?”, the oftentimes better question is: “Should another component solve this thing?”.

Do one thing well.

Yeah. The irony is not lost on us here. The even more severe problem though, is that we are dealing with a defined core that holds all of our component definitions. It’s only the style implementations that may differ per theme. If you changed the suggested useSlider prop to true, this change would apply to all themes. All of a sudden, everyone would get the slider view instead of just that one theme.

To deal with this fixed state situation, we introduced a concept that we call Extension Points, a term originally introduced by UML. In our example case, we create two components GridView.vue and SliderView.vue. Assuming that the grid view is the default, we would go on and put that component into the core. SliderView.vue however would find its place in the respective theme’s UI Layer. Now, this is where the magic happens: each theme implements a file extensionPoints.js which assigns concrete components to a number of well-defined Extension Point Components, like this:

All other themes may decide to use GridView.vue instead. You can even go further and have the Core define defaults for each extension point, then merging it with a subset of overrides each theme may specify.

Now, all that’s left to do is to loop over the extensionPoints Object and register those components globally:

This makes them available everywhere in your app’s templates with their generic identifier, e.g. <ItemView-ExtensionPoint />. Notice how this elegantly circumnavigates the initial problem of having one fixed core that must not be modified.

Each extension point may in fact implement almost anything. Just be aware of a couple of gotchas:

  • To be fair: The more extension points, the more confusion arises. We carefully decide if it’s really necessary to create another one. It might just be that the existing functionality is good enough.

4. + 5. Extending the app & Modifying the core

Let’s update the architecture diagram one last time:

The architecture diagram, extended by themes and consumers.
The architecture diagram, extended by themes and consumers.
The architecture’s final form

A new container Consumer appears. Now, we simply apply all of this story’s ideas so far to our new container. That’s right. Consumers may also specify .styles.js files, as well as their own extension points. We end up with cascading containers. Consumer take precedence over Theme over Core. This makes us fulfill individual wishes on a consumer basis. Our 5th requirement to change parts of the core for any consumer without breaking or affecting the remaining parts is thus fulfilled.

On top of that, this is where we implement one-off solutions that only one specific consumer commissioned. Maybe Consumer 2 requests one of those universally loved live-chat integrations: “Hi, my name is Dave. If you’ve got any questions, you can always find me in the bottom right corner :)”. In that case, we would create a new Business Domain LiveChat with our layers UI, Model and Connector within the vertical consumer-slice Consumer 2.

Also, let’s update the scripts excerpt of our package.json:

Here we define a different build script for each consumer. In addition to the THEME variable, we specify another variableCONSUMER that essentially serves the same purpose, only for the Consumers container this time.

Final words

Reusability is kind of the final boss of software development. This story illustrates some of its ramifications: everything becomes more complex as soon as something needs to be reused many times. At the same time, we’re happy with our outcome. Without any structure and without any well-defined rules for our codebase, things would get out of hand pretty quickly.

All of this is of course rather specific to our project. We did our best to generalize our ideas as best as possible while still providing concrete examples. Hopefully, you found some ideas that inspire you for some of your projects.


Up next

The experience of working in this project does not quite resemble the experience of jumping into a project freshly bootstrapped with Vue CLI. The more complex thoughts flow into a project, the more crucial it becomes to document the main ideas. I guess that new hires could simply go through our series of blog posts. On the other hand, often there are many small technical details that are not particularly interesting to the broader public but still vital to the understanding of our code base. Stay tuned and follow our publication, where we will document our documentation process soon. Also, we’ll go into detail on how we structure our tests around this architecture and how to make sure to not accidentally introduce unwanted visual changes across all of our consumers.

Bauer + Kirch

Smart software applications, internet solutions and mobile apps for digital processes

Thanks to Alexander Zibert

Thomas Holland

Written by

Software Developer @bauer-kirch. Enthusiastic about the web™ and everything JavaScript.

Bauer + Kirch

Smart software applications, internet solutions and mobile apps for digital processes

More From Medium

More from Bauer + Kirch

More from Bauer + Kirch

More on Technology from Bauer + Kirch

More on Technology from Bauer + Kirch

How I improved Vue, and so can you

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade