Using Vue in a non-SPA
I’ve spoken to a number of other developers about using Vue in applications that’s we’re single page applications but just standard MVC applications. Most of the time I get asked, “How would you do that?” So I finally sat down to explain how I do that.
I made a video first, because I seem to think better in talking it out with a demo, so you can catch that here.
This application is a Laravel application, but it’s pretty standard setup for anything that you would be using in any kind of MVC or modern MVC framework, including Ruby on Rails or Django. I think a lot of what I am doing here is pretty easily applicable to something else, but you’ll have to figure out some of the specificson your own. I’ll try and use generic terms so that you can more easily transfer from one to the other.
The application that I’ve done this work on is a live site called Downtown, TX. This is an application I built for the state of Texas’s historical commission and it lets them manage historical properties in Texas. It’s a cool site if you want to check it out.
Before getting into Vue when I first created the site, I used a component library called Riot.js. Riot was nice because it let you build single components that you could then embed in any webpage that you wanted to without any kind of compile step or big Webpack setup. However, I’ve really gotten into Vue and I like the way Vue creates and manages their components.
And so I’ve migrated a number of the original Riot components over to Vue, but Vue needs a little bit extra help. Vue components cannot just be put onto a webpage and have them run. They need cross compiled into a JavaScript version that a webpage can actually run. To do that, we’ve got some extra set up to do.
Vue Components
The first thing realize is, I write all of my components as single file components.
<template>
<div class="document-preview">
...
</div>
</template>
<script>
import { handleDelete } from '../../js/utils.js';
export default {
name: 'document-preview',
props: {
...
},
data() {
return {
...
};
},
methods: {
...
},
created() {
...
},
};
</script>
Vue components aren’t JavaScript right out of the gate. It’s something that needs to be compiled into JavaScript, but there are compilers out there that will do this for us. So I write all of these components as just .vue
components.
They have props
, data
, methods
and lifecycle methods just like any other Vue components. So there’s nothing special here that is outside of what you would normally build for any Vue component, whether that’s in a single page application or not. If you do happen to build some components in a single page application, and you want to use them on a webpage, you can do that too. These are just standard Vue components.
You can also import other JavaScript libraries or utilities just as you normally would. In fact, there are a number of methods and exports that I was using in the Riot components that I just imported into the Vue components and used as is.
This is pretty standard JavaScript, so you won’t need to do anything special at this stage. You’re going to write your Vue components, just like you would any Vue component and you can unit test them just like you would any Vue component.
Loader Scripts
Where we need to starting doing something a little bit different is in a Loader Script. I need a piece of JavaScript that I can use to load this component onto the page and I do that with what I’m calling a loader script.
import DocumentPreview from '../components/DocumentPreview.vue';
Vue.config.productionTip = false;
new Vue({
components: { DocumentPreview },
}).$mount('[data-vue-app=document-preview-component]');
Within my JavaScript assets folder where I keep all the JavaScript that needs cross compiled, I have a folder called loaders
. Every component will get its own loader script that can be used to mount the component onto a webpage.
The first thing it does is import the component. Then I can set whatever configuration I want for Vue and finally I’m creating a new Vue object giving it the components that I want it to load and then telling it to mount onto any tag that has an attribute of data-vue-app
where the value is document-preview-component
. Each component get its own little tag like this.
So if this is included on a page and I have an HTML element with this attribute on it, this loader will find it and mount the imported component onto that element. This is the key to how these components work. Instead of a main.js
that is loading an App
component that includes every other Vue component you are using, these loaders load the components as they are need in the application.
This is pretty simple. All of these loaders look just like this. They pull in a component and they mount it on the page.
Just a quick note on the
new Vue
syntax, I set this up before Vue 3 came out so this is using Vue 2 specifically. If you want to use Vue 3, this would be a little different.
Template Partial
Next is to include these elements in a partial that I can use in the application’s templates:
<div data-vue-app="document-preview-component">
<document-preview
data-url="{{ $data_url }}"
for="{{ $for }}"
reorder-url="{{ route('document.reorder' )}}"
csrf-token="{{ csrf_token() }}"
delete-url="{{ route('documents.delete_session_document', ['']) }}"
/>
</div>
@push('scripts')
<script src="{{ mix('js/document-preview.js') }}"></script>
@endpush
I’m using Laravel, so this is a Blade partial, but the basic building blocks are here. I have a div with the data-vue-app
attribute on it that contains the component code. That component has attributes that link to the component’s props
and are filled in with data from the MVC back-end, and finally there is a script tag to include the loader from the asset pipeline (the mix
part).
So the partial is used to link the back-end data to the component’s props
and then includes the component’s compiled code through the loader that I set up earlier. Fairly straightforward!
Putting it all together
Three pieces here need to be built for each component. First the component, which is just a normal Vue component, then a loader JavaScript that includes the component and then mounts that component on the page, and then a partial, which is what your templates are going to use to include this component on a whatever page you want to include it on.
The partial is the piece that is actually a part of the MVC framework that you’re using. The other two are JavaScript pieces.
Why do we need the loader if the component is already JavaScript? Because the Vue component needs to be compiled and that needs to go through your asset pipeline (or whatever it might be called in your MVC framework). In Laravel, this is called Mix and it will compile any Vue component, ES6 JavaScript, TypeScript, Sass or other cross compile source code and drop it into the public folders for the browser to use. It looks something like this:
mix
/**
* Vue components loaders go here
*/
.js('resources/assets/vue/loaders/image-gallery.js', 'public/js')
.js('resources/assets/vue/loaders/existing-image-preview.js', 'public/js')
.js('resources/assets/vue/loaders/multi-document-upload.js', 'public/js')
.js('resources/assets/vue/loaders/document-preview.js', 'public/js')
.js('resources/assets/vue/loaders/multi-image-upload.js', 'public/js')
...
All I need to do is tell this to compile the loader. And then the loader includes the component and the component also gets compiled. And any other pieces that get pulled in by the component will get compiled as well. The loader then becomes the one piece that ties all of the JavaScript parts of this together and that’s why it’s important here.
Communication
Another question is can these components talk to each other? And they actually can. You could set up a Vuex store that you then can include on the loader, pass to the component, and then all the components that include that same store would all be sharing an instance of it and any data you put in there, all the components would have access to it. Even though they’re in their own little Vue context, they’re still within a shared JavaScript context. If you have your Vuex store set up as a const
and you’re exporting it from a JavaScript file and you import it into your components, they will all have access to that same Vuex store. I’m also using a universal service bus that I can emit events on and listen to within the components. That way I actually have my Vue components talking to some of the legacy Riot components and could also talk to some other piece of JavaScript and they’re all using that same bus. Not only does that allow my Vue components to talk to other Vue components, but they can also talk to my Riot components and vice-versa. That made the transition to Vue very easy to do to because that kept everything seperate and encapsulated. This is the value of separated and isolated components when you build these applications.
Hopefully this is helpful enough for you to do this in your own application, if you need to. It’s worked great for me and allows me to progressively migrate more and more of my JavaScript code into isolated, encapsulated, and, yes, even tested Vue components.