tl;dr - I wrote about a few patterns I’ve found myself using/rewriting repeatedly on Vue projects. Use at your own risk.
I’ve worked on a few Vue frontend projects now and while I can’t say that any of the projects are paragons of good frontend engineering (tm), I have found myself implementing a few patterns over and over from project to project, which (charitably interpreted) is a sign that I’ve foudn something that worked so well I wanted to use it twice. I’ve detailed them here in this blog post to hopefully solicit some feedback and maybe see what people think and if they might be useful. The suggestions here range from super obvious to niche and possibly wrong, proceed with caution.
Before we get into it, please note that the code examples are not really copy-pastable, they’re just examples of what the approach looks like. I didn’t make a code repo to go with this post that contains guaranteed-to-compile code, just some sketches of what this code looks like. My goal in writing this is to inspire people to atleast think of simpler solutions to common problems you run into on day ~2 of smaller Vue projects.
This is more of a general tip (and the likely the most defensible thing in this post), but if you’re going to write Vue frontend apps, you should try and make yourself aware of the pre-built component libraries that are out there for getting started quickly. One of the great things about the component-composition approach to frontends is that regardless of how you structure data, or route or other things, you should be able to use components across projects. Take this composability one step further and you should be able to use components across companies/organizations.
Just about every component library has a bunch of component libraries available for it, here are some I’ve found for Vue:
You can find even more awesome vue stuff on the aptly named vuejs/awesome-vue github repository.
One of the first obvious problems after you have yoru components set up is actually getting data to show. These days people seem to most often jump to the flux pattern and it’s cohorts redux and vuex, I prefer to first start with the conceptually simpler POJOs that perform AJAX requests. These POJOs can also be made Vue instances relatively simply if you want them to be able to make use of more features that Vue itself offers.
I’d love to use more principled data management libraries like Ember Data or Backbone here, but they just don’t strike me as easy to integrate with anything other than their intended usecases. They aren’t general data-management tools, they’re more like very specific to their intended usecase (if I’m wrong about this I’d love to know, please reach out).
Either way here’s a basic example of a service, with the class
syntax sugar of ES6 (implemented by Typescript, which you should be using):
class UserService {
// Get a user
getUsers(): Promise<User[]> {
// ... actual code that probably uses fetch/axios
}
// Add a user
addUser(u: User): Promise<User> {
// ... actual code that probably uses fetch/axios
}
}
export UserService;
As mentioned I recently have been kicking the tires/finding more places to use axios/axios
in my projects – I personally like the slight edge of ergonomics it often provides over using raw github/fetch
. As far as the actual component goes, things don’t change very much if you decide to use a Vue instance, which is what I very often (so I can take advantage of Vue’s observables and other features). Here’s an excerpt from some code I’ve written relatively recently:
const UserService = new Vue({
data: function() {
const state = {};
try {
state = JSON.parse(StateService.getKVState(LS_USER_SERVICE_STATE));
} finally {
state = _.merge(DEFAULT_STATE, state);
}
return {
state,
router: null,
};
},
created: function() {
this.hasValidSession();
},
computed: {
currentUserEmail: function() { return this.currentUser ? this.currentUser.email : ""; },
currentUserId: function() { return this.currentUser ? this.currentUser.id : ""; },
isAdmin: function() { return this.currentUser ? this.currentUser.role === Constants.ROLES.ADMINISTRATOR : false; },
isUser: function() { return this.currentUser ? this.currentUser.role === Constants.ROLES.USER : false ; },
currentUserPermissions: function() { return this.currentUser ? this.currentUser.permission : null; },
},
methods: {
setRouter(r) { this.router = r; }
// Save local state -- possibly to local storage or other client-side persistence.
saveState: function(): void {
StateService.setKVState(LS_USER_SERVICE_STATE, JSON.stringify(this.state));
},
// ... lots more code
};
export UserService;
Whether you choose to use a Vue instance or not when you use the UserService
from other code fragments, you should be talking to the same same UserService
(if that even matters for your usecase), and you can centralize access/use of the functionality that the UserService
provides. Centralization of this functionality means that you can start implementing things like caching and adaptors and model validation in one consistent place, shielding the rest of your application.
So what’s that StateService
? Glad you asked! It’s a usage of this simple pattern to wrap around browser-side localstorage.
The LocalStorage API is an excellent browser-side feature for persisting things like preferences (that aren’t important enough to get stored on the backend for example), or even session tokens (gulp, you shouldn’t do this because XSS attacks leave you vulnerable though). I often abstract my local storage access to a KV store type service:
class LocalStorageKVService {
get(k: string): Promise {
if (!k) { return Promise.reject(...); }
const raw = localStorage.getItem(k)
return Promise.resolve(JSON.parse(raw));
}
put(k: string, v: any): Promise<void> {
if (!k || !v) { return Promise.reject(...); }
const json = JSON.stringify(v);
return Promise.resolve(localStorage.setItem(k, json));
}
}
Then I can use this LocalStorageService
as a dependency of something like a CurrentUserPreferencesService
, and abstract away the storage medium a little bit if I choose, starting with a super specific one like LocalStorageKVService
and use Typescript interfaces to make things more generic if necessary.
The idea of data staleness is probably the next big important consideration that comes up. There’s a basic contention between getting the freshest data and being able to make snappy UIs – if you want to get the absolute freshest data from the server every time you do a simple read, your user going to spend a lot of time looking at asynchronous loading icons/animations. Ember data has a really slick but somewhat involved generic implementation of caching and getting things by ids and what not but we can get a good percentage of the efficiency gains by writing a small in-service caching layer, and making sure we allow for caching busting at the method level. Here’s an example that I’ve written recently:
const DocumentsService = new Vue({
data() {
this.memoizedFetchByUuid = memoize(this.fetchByUuid);
return {};
},
methods: {
/**
* Fetch a document by UUID
*
* @param {string} uuid
* @returns {Promise<Document>}
*/
fetchByUuid: function(uuid: string): Promise<Document> {
if (!uuid) { return Promise.reject(new Error("UUID not provided")); }
return UserSvc
.getAuthenticatedAxios()
.then(axios => axios.get(`${Constants.URLS.API_BASE_URL}/${Constants.URLS.documents.getByUuid(uuid)}`))
.then(Util.errorOnStatusOtherThan([200]))
.then(Util.unwrapTransportSuccess)
.then(Util.unwrapServerSuccess)
.then(Util.convertToModel(Document))
.then(res => {
// Invalide the cache ~5s - request time after it's done
setTimeout(() => this.memoizedFetchByUuid.cache.delete(uuid), Constants.DEFAULT_GET_BY_UUID_CACHE_MS);
return res;
});
},
/**
* Get a document by uuid, possibly from cache or directly
*
* @param {string} uuid
* @param {GetOptions} opts
* @returns {Promise<Document>}
*/
getByUuid: function(uuid: string, opts?: GetOptions): Promise<Document> {
if (!uuid) { return Promise.reject(new Error("UUID not provided")); }
// Immediately invalidate cache if noCache is specified & entry is in the cache
if (opts && opts.noCache) {
this.memoizedFetchByUuid.cache.delete(uuid);
}
return this
.memoizedFetchByUuid(uuid)
.catch(() => this.fetchByUuid(uuid));
},
// ... more code
};
export DocumentsService;
Again, this is a pretty simple way to implement a super simple cache for values, and gets you 80% of the way there with relatively simple code. The differentiation between getByUuid
and fetchByUuid
might seem excessive, but I find it’s important to be explicit with names when something is expected to do some sort of long-running operation – callers that want to ensure the fetch is done can just call fetchByUuid
directly without too much trouble. This is another spot where a Vue mixin can go a long way to avoid repetition but I’ve foudn most of the time I didn’t even need it as the project was small enough.
For this section I’m going to assume that you’re using vue-router
– if you’re not you’ll have to try and adopt this basic solution to your own router. The idea for this is pretty simple, just mark the routes that users can access unauthenticated explicitly. On some projects I marked the routes that required auth, but this can get pretty tediuos with applications gated by login screens. Another approach would be to check if any route in the hierarchy is auth-required and assuming all children are – which of course means you need to structure all auth-required routes under some route.
Here’s an example of a simple implementation as far as the route definitions are concerned (in a file called routes.js
that gets imported and fed to vue-router
):
const routes = [
{name:"login", path: "/", component: LoginPage}, // <-- login can't require authentcation
{name:"home", path: "/home", component: HomePage, meta: {auth: true} }, // <----- the home page required authentication
{
name:"users-view",
path: "/users/:uuid",
component: UsersViewPage,
meta: {auth: true}, // <--- this route is going to require authentication
props: r => ({uuid: r.params.uuid}),
},
// ... lots more routes
};
export routes;
As you can see, in this example it might actually be better to specify which pages don’t need auth (since most do). I could also have been a bit more descriptive with the meta property, maybe something like requiresAuth
. This approach is also pretty trivially extendable if you define some sort of AuthRequirements
type that can express what kind of auth is required (does the user need to have a certain role? permission list?) – you can also just have it be one or more functions that return promises or whatever, go crazy.
Somewhere along the way you’ll need to implement the guard on vue-router
that actually checks the permissions/authentication status:
// Setup route guards
router.beforeEach((to, from, next) => {
// Redirect to login
if (to.name !== "login" && to.meta && to.meta.auth && !UserService.isLoggedIn()) {
next({name: "login"});
return;
}
next();
});
There’s lots of ways you could extend this and do more crazy things but this is a decent start that covers most of the simple usecases. This solution should also play nice with Vue Router’s lazy loading support (which combines with vue’s async components feature) .
This is a bit more of a server-side issue, but the thing about client-side rendered apps is that you need to know whether the user is currently logged in yourself relatively frequently, and at the very least at initial load of the application so you can know whether to put them on the login page or an authenticated “home” page. I accomplish this by ensuring a /me
route exists on the backend, and that I hit that route somewhere early in application initialization and use it to determine whether the user is logged out and re-initialize whatever needs intiializing.
Here’s an example of what that looks like for me:
// ... other code
/**
* Fetch the current user information from the backend
*
* @returns {User}
*/
fetchCurrentUser: function(): Promise<User> {
return this.getAuthenticatedAxios()
.then(axios => axios.get(`${Constants.URLS.API_BASE_URL}/${Constants.URLS.ME}`))
.then(Util.errorOnStatusOtherThan([200]))
.then(_.partialRight(_.get, "data"))
.then(this.setSession)
.catch(e => {
this.resetCredentials(e);
});
},
// Check if the user is currenty logged in
// we know a user is logged in if the long term state contains a token
isLoggedIn: function() {
return !_.isNull(this.state.token) && !_.isUndefined(this.state.token) ;
},
// ... other code
As you might have guessed, these functions are part of the UserService
. One of the reasons I like using axios is the concept of axios instances that can be passed around. The above code segment mentions the getAuthenticatedAxios
, check out it’s definition:
// Obtain an authenticated axios instance that can be used to perform requests
getAuthenticatedAxios(): Promise<Axios> {
const headers = {};
if (this.isLoggedIn()) {
Object.assign(headers, {'Authorization': `Bearer ${this.state.token}`});
}
return Promise.resolve(new Axios({ baseURL: Constants.API_BASE_URL, headers }));
}
In this example I’m using basic Authorization
header style authentication in this application – and the session is persisted client-side. This isn’t a particularly safe way to do this – the OWASP guide recommends proper secure cookies, but it’s a decent for quick prototyping, as this kind of authentication can be used consistently across all clients (mobile apps, etc). It’s also fairly easy to move whatever token you’re passing in the Authorization
tag into a secure cookie, and just detect when browser-based clients come in. Of course, you can’t trust any data you get from the client on the server side (telling you that it’s a browser or not), but for a client looking to try and compromise your system limiting themselves to dealing with secure cookies (when they could just connect directly) doesn’t seem like a smart move, and of course if your client’s browser is compromised you’re back where you started anyway.
Security considerations aside, this is a pretty convenient way to enable other services to get access to the current user credentials in order to make authenticated requests. The AvatarService
can depend on the UserService
and call the UserService.getAuthenticatedAxios()
to get an instance that’s already got everything it needs set.
Following the idea of building services on top of each other, I’ve found that it’s best to be explicit in the places where I use the services – this means extending this pattern by having services and Vue components that depend on a specific service take it as a dependency, with a default to an imported version:
import UserService from "@app/services/user";
export {
props: {
userSvc: {type: UserService, default: () => UserService}
}
// ... rest of the component which uses `this.userSvc` when necssary ...
}
There’s also some good Typescript synergy here, because you can use it’s duck-typed interface system to actually define the methods you expect the service you want to support! So a UserLoginBox
component might want some service (POJO) that has the doLogin(username: string, password: string): Promise<User>
method signature. This approach also helps testing at both the integration and E2E level since you can pass known-good/trivial/mocked implementations of services and use that to write tests.
Typescript offers a bit more flexibility here because we can specify the shape of the service we want, without specifying even a very specific class. The usual constrained polymorphism gains apply. You may need to use plugins like vue-class-component
to get full support for the power that Typescript provides (I assume this will go away over time as Vue gets even better Typescript integration).
This is a bit of a compound suggestion – you’d need to be using something like vue-router
with a component library like ElementUI that has a NavMenu component. You can straightforwardly write a function from NavMenu
entries (however they’re structured) to the current active route by using a numbering scheme like the one employed by Element UI.
Here’s an small excerpt of a LHSNav
component I’ve written that wraps Element UI’s NavMenu
:
<template>
<nav id="lhs-nav" class="rel-pos full-width centered-text">
<img id="lhs-nav-logo" class="xs-margin-top" :src="logoURL"></img>
<div class="abs-pos top right xs-margin-right cursor-pointer" @click="$emit('toggle-compression')">
<icon icon="expand" v-if="isCompressed"></icon>
<icon icon="compress" v-else></icon>
</div>
<el-menu
:collapse="isCompressed"
:default-active="currentRouteIndex"
class="full-width"
:class="{collapsed: isCompressed}"
background-color="rgba(0,0,0,0)"
text-color="#fff"
active-text-color="#ffd04b">
<router-link class="no-txt-decoration" :to="{name: 'home'}">
<el-menu-item index="1"> <!--- the link to home page, note the index -->
<icon icon="home"></icon>
<span>γγΌγ </span>
</el-menu-item>
</router-link>
<el-submenu popper-class="test" index="2">
<template slot="title">
<icon icon="users"></icon>
<span>Users</span>
</template>
<router-link class="no-txt-decoration" :to="{name: users-list'}">
<el-menu-item index="2-1">
<icon icon="list"></icon> <!--- the link to the user's listing, note the index -->
Listing
</el-menu-item>
</router-link>
<!-- ... more template ... -->
Here are the corresponding routes that are serviced by these links:
const routes = [
// ... other routes ....
{name:"home", path: "/home", component: HomePage, meta: {auth: true, lhsNavIndex: "1"} },
// /users
{name:"users-list", path: "/users", component: UsersListPage, meta: {auth: true, lhsNavIndex: "2-1"}},
// ... other routes ....
As you might have noticed, the index
attributes on the el-menu-item
s match the routes! The function that supports finding the current active route, inside of the LHSNav
wrapper component looks like this:
const component = Vue.extend({
props: {
isCompressed: Boolean,
},
data() {
return {
logoURL
};
},
computed: {
currentRouteIndex: function() { return this.$route.meta.lhsNavIndex; }
},
methods: {
doLogout: function() {
UserService.logout(this.$router);
}
}
});
Vue.component("lhs-nav", component);
export default component;
Most of this should be pretty self-explanatory, but the the relevant bit here is the currentRouteIndex
computed variable – I’ve got a direct and easy to determine link between the current route and which index is correct in the menu machinery. meta.lhsNavIndex
provides a simple (but manual and somewhat tedious to update) mapping, but sitting right in the current route itself, which is accessible through vue-router
’s component $route
injection. The solution here is basically just tacks on the ElementUI-specific information to the routes themselves. This could be tied even more tightly to Vue itself by just using some sort of concatenation of canonical route names home.whatever.whatever
and deriving things that way but I liked how simple this approach was.
The overarching point here is that storing meta data on routes is a powerful and relatively simple way to get some nice functionality.
Very often I find that I want to trigger some fucntionality in child compnents of a current component. “data down events up” is a saying/paradigm bandied around the component library space a lot, and while I agree with it I don’t particularly derive great pleasure from being dogmatic about it. What I mean when I say that is that I find it perfectly acceptable to write components that receive and react to messages (which you could interpret loosely as “events”). I personally solve this by just giving them an eventBus
(or msgBus
if you prefer).
Here’s an example of a users table component which retains information on how to retrieve the backend data that it displays:
const UsersTable = Vue.extend({
mixins: [ EntityListingDataMixin(FETCH_FN) ],
props: {
restrictTo: String,
paginated: {type: Boolean, default: true}, // for EntityListingDataMixin, specity whether pagination is disabled in the response
userSvc: {default: () => UserSvc},
cmdBus: {default: () => new Vue()},
},
});
Vue.component("users-table", component);
export default component;
There’s a lot happening here:
EntityListingDataMixin
takes a fetching function and does the rest of the work setting up the component .restrictTo
and paginated
are decent examplesuserSvc
is used by this component, so it’s in therecmdBus
is used to enable triggering of this component from the outsideIt’s the last point that I want to talk about – the usecase here is when there’s something like a button/link on the parent component (let’s say the page itself) that needs to make the table reload or something else. It’s possible to easily unify the code-paths for the reload/change of the UsersTable
here by converting the external (in-parent) button/link click into a do-search
event being emitted over the cmdBus
.
It’s important to note that you can solve this usecase a bunch of different ways, for example you could have the search preferences managed at the data store level (in the flux pattern) or some sort of upper container that functional-reactively delivers results (and ensure both the button and the link can manipulate this container), but I’ve found in little projects that the small scale event/message-passing paradigm was easy to reason about with less spooky action far away. Do what works for you.
This is another pattern that can be farmed out to a Vue mixin if you want to cut down on duplication, I could have easily had a CmdBusMixin
(or even EventBusMixin
right next to EntityListingDataMixin
) – this could also be solved by providing a eventBusSvc
and listening to that.
Hopefully some of these patterns were interesting – they might not be appropriate for your big enterprise (tm) large-scale project, but they’re so simple they’re almost always where I start (and half the time never migrate off of) for smaller projects. As always, if I’m completely wrong (and you know why), I’d love to know about it! Shoot me an email and let me know.