Hi! Welcome to my first post of 2018! If you are a new reader, please consider subscribing to my blog feed and leaving me a comment below! A few weeks back I blogged about my first experience using Vue.js and Vuex: "An Example of Vuex and State Management for Vue.js". One of things I mentioned in that post was how my mental model for Vuex was Angular's Provider feature. It's definitely not the best mental model, but it's how I'm thinking about it for now. One of the biggest differences that I can see is that in Vuex you get the ability to automatically update your components with new data. Everything is kept in sync with little to no work on your part. That's a big difference compared to Providers, but I'm not an Angular expert and that may be something natively supported.
For this post, I wanted to look at using Vuex to wrap an API provider. Basically using the store as an abstraction over a remote API. For my demo I decided to use the Goodreads API. This is an OK API. The docs are good, but the actual implementation leaves a bit to be desired. One of the biggest flaws is not supporting JSON well, but I got around that. For my demo I decided to support 2 basic operations:
- Search for Books: Given a term, find the first ten books that match it.
- Search for Related Books: Given a book, ask the API for books related to it.
That second operation isn't actually a feature of the API. Instead, it is returned in one of the book detail APIs. However, I simply used Apache OpenWhisk (and IBM Cloud Functions) to build my own wrappers. I also converted the ugly XML into proper JSON results. This isn't a serverless post so I won't go into the details, but you can find both actions up on my GitHub repo: https://github.com/cfjedimaster/Serverless-Examples/tree/master/goodreads.
I began by creating the demo without Vuex at all. Let's begin by looking at the HTML. My idea for the layout was a simple vertical list of results on the left side and related books displayed to the right.
<div id="app">
<input v-model="search" type="search">
<input type="button" @click="doSearch" value="Search">
<br clear="left">
<div v-if="searching"><i>Searching...</i></div>
<div class="allResults">
<div v-if="books.length" >
<h2>Results</h2>
<p><i>Click the cover to find similar books...</i></p>
<div v-for="book in books" class="bookResult">
<img :src="book.image_url" class="bookResult" @click="findSimilar(book)">
{{book.title}}
</div>
</div>
<div v-if="relatedBooks.length">
<h2>Books Related to {{ selectedBook.title }}</h2>
<div v-for="book in relatedBooks" class="bookResult">
<img :src="book.image_url" class="bookResult" @click="findSimilar(book)">
{{book.title}}
</div>
</div>
</div>
</div>
Nothing too fancy there. You can see a few conditionals to handle displaying loading conditions, lists of books, etc. Now let's look at the JavaScript:
const app = new Vue({
el:'#app',
data:{
search:'',
books:[],
relatedBooks:[],
searching:false,
selectedBook:null
},
methods:{
doSearch() {
if(this.search === '') return;
this.searching = true;
this.books = [];
this.relatedBooks = [];
console.log('search for '+this.search);
fetch(`https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/goodreads/search.json?search=${encodeURIComponent(this.search)}`)
.then(res=>res.json())
.then(res => {
this.searching = false;
this.books = res.result;
});
},
findSimilar(book) {
this.selectedBook = book;
this.relatedBooks = [];
console.log('find books similar to '+book.id);
fetch(`https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/goodreads/findSimilar.json?id=${encodeURIComponent(book.id)}`)
.then(res=>res.json())
.then(res => {
this.relatedBooks = res.result;
});
}
}
})
Again, not too complex. Make note of the data
section and methods
. My code handles updating various different data variables based on the current action. This isn't too bad at all I think. You can view the complete source, and run it as a demo, in the embed below. Note that I do not use a loading widget for loading related books and the APIs have been a bit slow today. Give it a minute when you click.
See the Pen Vue and GoodReads API by Raymond Camden (@cfjedimaster) on CodePen.
Alright, so how can I modify this to use Vuex? Let me share the updated JavaScript (the markup didn't change at all, which is kick butt) and then I'll walk through the differences.
const api = new Vuex.Store({
state:{
books:[],
relatedBooks:[],
searching:false
},
mutations:{
books(state,books) {
state.books = books;
},
relatedBooks(state,books){
state.relatedBooks = books;
},
searching(state,bool) {
state.searching = bool;
},
clearBooks(state) {
state.books = [];
},
clearRelatedBooks(state) {
state.relatedBooks = [];
}
},
actions:{
searchBooks(context,term){
context.commit('searching',true);
context.commit('clearBooks');
context.commit('clearRelatedBooks');
context.searching = true;
fetch(`https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/goodreads/search.json?search=${encodeURIComponent(term)}`)
.then(res=>res.json())
.then(res => {
context.commit('books',res.result);
context.commit('searching',false);
});
},
relatedBooks(context,book) {
context.commit('clearRelatedBooks');
console.log('find books similar to '+book.id);
fetch(`https://openwhisk.ng.bluemix.net/api/v1/web/rcamden%40us.ibm.com_My%20Space/goodreads/findSimilar.json?id=${encodeURIComponent(book.id)}`)
.then(res=>res.json())
.then(res => {
context.commit('relatedBooks',res.result);
});
}
}
});
const app = new Vue({
el:'#app',
store:api,
data:{
search:'',
selectedBook:null
},
computed:{
books() {
return api.state.books;
},
relatedBooks() {
return api.state.relatedBooks;
},
searching() {
return api.state.searching;
}
},
methods:{
doSearch() {
if(this.search === '') return;
api.dispatch('searchBooks',this.search);
},
findSimilar(book) {
this.selectedBook = book;
api.dispatch('relatedBooks',book);
}
}
})
Ok, do me a favor and start from the bottom. Right away you can see that the main application code is quite a bit simpler. I've updated my data to use computed
for stuff that the store is handling. (Note that I've called my store 'api'.) Now the books, related books, and searching state, all come from Vuex. My methods also are much simpler as they ask the store to do the heavy lifting.
The store itself consists of it's state values, mutations, and actions. Actions are how you handle asynch actions on a store and you can see the Ajax calls have been moved in there. The actions use commit
calls to tell the store to save new values. In general, it should all be pretty obvious what's going on, but definitely ask me a question in the comments below.
So what's the net result? The Vuex version actually has twice the code as the original version. However - just looking at it, it feels like a much better architected version of the code. I like the Vue app is basically "hands off" in terms of how the API works and I like how my Vuex store is nicely organized by itself. I could easily use this store in other Vue apps whereas the previous version everything was mixed in together.
You can view the full source code, and demo, below:
See the Pen Vue and GoodReads API (with Vuex) by Raymond Camden (@cfjedimaster) on CodePen.
Let me know what you think in the comments below, and as always, I invite Vue experts to come in and give me suggestions on how I could improve this!
Archived Comments
Hello Ray. I'm new to Vue.js and have been following all of your Vue.js posts. I've never used Openwhisk/IBM or anything 'serverless'. Am I correct in assuming that your Openwhisk/IBM setup is handling OAuth with the Goodreads API and the parsing of the XML to JSON? Does it do anything else? How would your example change if you weren't using Openwhisk/IBM? Thanks, CV
Hey Che! First off, let me say that I have a large number of OpenWhisk/Serverless blog posts you could read if you want to learn more. Just click on "OpenWhisk" in the tags above to see them. If you click the "About" link on top, I've also got a link to my *free* ebook on OpenWhisk.
Now specifically to your question - Goodreads supports a key based system for API calls when you do not need to know about the current user - so that's all I did, use a key. The key is "hidden" in the serverless code so you can't get it if you view source in the client-side app. Of course, my API is 100% open so you can still "abuse" my key, but I've only exposed the two things I need.
You could bypass OpenWhisk and just hit their API directly too if you want - they support CORS so it would work.
Thanks! So, do you primarily use OpenWhisk as a scalable "microservice" that passes on the heavy lifting and the obfuscation of specific snippets of code you wish to execute or to build/host entire apps? Or both? :) I ask b/c I've always thought that it was a best practice to limit the number of external http calls (JS/CSS/API) when possible.
Well, serverless is a big concept and has *many* uses. My user here as basically an API "proxy" (convert XML to JSON, return slimmer results, hide a key) is *one* use of serverless.
Your last question about best practices doesn't really involve serverless at all. Whether or not I was hitting GoodReads directly or OpenWhisk, I'm still making network calls. Yes, it *does* matter how many network calls you make of course.
I suppose, technically, my user of a proxy here doubles the network calls. My client is making the same amount of calls, but there are more involved since OpenWhisk is calling out to GoodReads. However, I think the benefits outweigh that. I could also add my own caching at the OpenWhisk level. I could even completely rip out GoodReads and replace it with Amazon services too, and my front end would never know.
Thanks! The more I read about OpenWhisk, the more interested I become. Keep up the Vue.js posts!
Ray thanks for all of the great posts on Openwhisk as well as Vue.js. I have found them quite helpful. I also downloaded your 'Developing Serverless Applications' book which was a great intro and got me up to speed quickly. I appreciate your efforts.
Thank you!