How to minimize data traffic in vuejs - vue.js

At work, we think about using Vuejs with Vuex for our frontend. I have already used it for private projects.
One of our big questions is the traffic. Our platform needs a lot of data, sometimes in large packages. Some of them are really big and it would be more expensive to load them more than once.
I've seen many examples of vue with vuex, but for me it looked like all the samples would request the same (already loaded) data every time they paged.
My real question is: Is there a way in vuejs or general to avoid or solve this problem? The only helpful thing I have found so far was this.

As far as I know, you can use this library https://github.com/kuitos/axios-extensions
An example here
import Axios from 'Axios';
import { throttleAdapterEnhancer } from 'axios-extensions';
const http = axios.create({
baseURL: '/',
headers: { 'Cache-Control': 'no-cache' },
adapter: throttleAdapterEnhancer(axios.defaults.adapter, { threshold: 2 * 1000 })
});
http.get('/users'); // make real http request
http.get('/users'); // responsed from the cache
http.get('/users'); // responsed from the cache
setTimeout(() => {
http.get('/users'); // after 2s, the real request makes again
}, 2 * 1000)
As you can see, you can create an adaptor and custom what you want. For example here, you keep the cache for only 2 seconds. So the first request to /users is a real one. The second and thirst request are cache, and the last one is after the two seconds, so it's a real one.
Can you be more specific about the context of how you will keep the cache. When do you need to reload the cache, after how many times, after which event, ...?

The strategy I use is to store the value on the Vuex state.
I write all my request in Vuex actions. In every action, I check if the data already exists on the Vuex state. If it does, I simply bypass the request and return the data from the state (saving requests from being called multiple times). If it doesn't exist, I'll make the request, then store the result on the Vuex state, and return the value.
Vuex Action:
getLists({ state, commit }) {
return new Promise((resolve, reject) => {
if (state.isSetLists === false) {
getListsFromServer((error, data) => {
if (error) {
reject(error);
} else {
console.log('call to getLists successful:', data);
commit('setLists', data.lists);
resolve(data.lists);
}
});
} else {
resolve(state.lists);
}
});
},
Then, the setLists mutation handles it like so:
setLists(state, lists) {
state.isSetLists = true;
state.lists = lists;
},
This way, the user can page around all they want, and only ever call each request once.

Related

How to use a private API key with Nuxt (on the client)?

Problem Solved
If you're struggling with the same issue, look at the accepted answer which is one way to achieve it by using serverMiddleware
I'm using an API which required a private key. I've stored the key inside a .env file, and called it in the nuxt configuration file, like this :
privateRuntimeConfig: {
secretKey: process.env.MY_SECRET_KEY
},
My API call is done inside the asyncData() hook on my index page. It works fine when i load this page, or reload it, but everytime i use the navigation to come back to this page, i end up with an error (I use a buffer to convert my API key to base64)
First argument must be a string, Buffer, ArrayBuffer, Array, or array-like object.
After some research and debugging, i found out that my private key wasn't available at the time, and the "secret" value used in my api call was "undefined".
The thing I don't get is why is this working on initial load / reload but not on page navigation ? And is there a way to fix it without using a backend ? (SSR for SEO and the ability to use private keys without exposing them are the main reasons why i used Nuxt for my project)
Here is my code :
async asyncData({ $content, store, $config }) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
const stocks = store.getters['products/getProducts']
return { stocks }
},
Update
Looking at the #nuxtjs/snipcart module's key key and since it's a buildModules, you can totally put it there since it will be available only during the build (on Node.js only)!
For more info, Snipcart do have a lot of blog posts, this one based on Nuxt may help clearing things up: https://www.storyblok.com/tp/how-to-build-a-shop-with-nuxt-storyblok-and-snipcart
You do have your key initially because you're reaching the server when you enter the page or hard refresh it.
If you navigate after the hydration, it will be a client side navigation so you will not be able to have access to the private key. At the end, if your key is really private (nowadays, some API provide keys that can be exposed), you'll need to work around it in some ways.
Looking at Snipcart: https://docs.snipcart.com/v3/api-reference/authentication, it clearly states that the key should be available in
Appear in your compiled front-end assets (HTML, JavaScript)
Meanwhile, if you need to make another call to your backend (trying to access something else than products), you'll need to make a second call.
With Nuxt2, you cannot reach for the backend each time as of right now since you will stay in an SPA context (Nuxt is a server then client Vue app basically). But you could write down the token into a cookie or even better, use a backend as a proxy to hide this specific key (or even a serverless function).
Some more info can be found on my other answer here: https://stackoverflow.com/a/69575243/8816585
Thanks #kissu for your (very) quick answer :)
So, based on what you said and your other answer on the subject, i've made a server Middleware in Nuxt in my server folder.
server/snipcart.js
const bodyParser = require('body-parser')
const axios = require('axios')
const app = require('express')()
app.use(bodyParser.json())
app.all('/getProducts', (request, response) => {
const url = 'https://app.snipcart.com/api/products'
const secret = Buffer.from(process.env.SNIPCART_SECRET).toString('base64')
const config = {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
}
axios
.get(url, config)
.then(res => {
const products = {}
res.data.items.forEach(
item => {
const productId = item.userDefinedId.replace(/-/g, '')
const stocks = {}
item.variants.forEach(
variant => {
const size = variant.variation[0].option
const stock = variant.stock
stocks[size] = stock
}
)
products[productId] = stocks
}
)
response.json(products)
})
.catch( err => response.json(err) )
})
module.exports = app
Correct me if i'm wrong, but I think that's basically the same as using a server as a proxy right ? Based on Nuxt lifecycle hooks, the serverMiddleware one is only run on the server, so my API key shouldn't be exposed to the client ? (I still need to do some refactoring to clean the code, but at least it's working) (https://nuxtjs.org/docs/concepts/nuxt-lifecycle/#server & https://nuxtjs.org/docs/configuration-glossary/configuration-servermiddleware/)
nuxt.config.js
serverMiddleware: [
{ path: "/server", handler: "~/server/snipcart.js" }
]
index.vue (where my snipcart API call was previously made, i guess now I should move this call directly from the product card component where the data is needed) :
async asyncData({ $content, store, $axios }) {
await $axios
.get('/server/getProducts')
.then(res => store.commit('products/addProducts', res.data))
.catch(err => console.log(err))
const stocks = store.getters['products/getProducts']
return {stocks, masterplanProducts }
},
PS : Snipcart does provide a public API key, but the use is very limited. In order to access the remaining stock for each product, i have to use the private key (which allows for some other operations, like removing products / accessing orders and such)
UPDATE :
It's not working when the website is fists accessed from any other page than the one one where the API call is, since the store won't have any data from the API call)
Okay, now I feel dumb. I found a way to make it work. I guess taking the time to explain my problem helped me understand how to solve it.
For those who encounter a similar issue, i fixed it by wrapping my API call with a If statement.
if ($config.secretKey) {
const secret = Buffer.from($config.secretKey).toString('base64')
const request = await fetch('https://app.snipcart.com/api/products', {
headers: {
'Authorization': `Basic ${secret}`,
'Accept': 'application/json'
}
})
const result = await request.json()
store.commit('products/addProducts', result)
}
const stocks = store.getters['products/getProducts']
This way, i can just skip the API call and access values from my vuex store.

How to use Nuxt $auth inside an axios plugin (How to add Token to all axios requests)

Im looking to use $auth inside my Nuxt project, specially inside an axios plugin.
Here is my code:
plugins/api.js
export default function ({ $axios }, inject) {
const api = $axios.create({
headers: {
common: {
Accept: 'text/plain, */*',
},
},
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Now the problem comes when I try to use $auth from #nuxtjs/auth-next package.
As stated in the docs:
This module globally injects $auth instance, meaning that you can
access it anywhere using this.$auth. For plugins, asyncData, fetch,
nuxtServerInit and Middleware, you can access it from context.$auth.
I tried the following:
This results in $auth being undefined
export default function ({ $axios, $auth }, inject) {
This one was near
export default function ({ $axios, app }, inject) {
console.log(app) //This one logs $auth in the object logged
console.log(app.$auth) // I don't understand why but this one returns undefined
My main goal here is to make use of this.$auth.strategy.token.get()and pass it (if the token exists of course) to the headers of every request made using this.$api
I have been looking for similar questions and answers but none has helped me to solve this, I could just add the token every time I write this.$api but that would increase the code unnecessarily.
Thanks in advance to all the people for your time and help.
EDIT:
Okay, now I made a test. and the next code is actually logging the $auth object correctly, it seems some time is needed to make it work but now Im afraid that using setTimeout could cause an error because I can't know exactly how much time is needed for $auth to be available.
export default function ({ $axios, app }, inject) {
setTimeout(() => {
console.log('After timeout', app.$auth)
}, 50)
EDIT 2:
So now I have made more tests, and using 0 milliseconds instead of 50 works too, so I will use setTimeout with 0 milliseconds for now, I hope anyone find a better solution or explain why $auth is not available before using setTimeout so I can decide what to do with my code.
EDIT 3:
After trying to wrap all my previous code inside setTimeout I noticed that the code fails, so that isn't a solution.
I have found a solution so I will post it so that every person that could have the same problem in the future can solve it.
It turns out that I could easily solve it using interceptors.
export default function ({ $axios, app }, inject) {
// At this point app.$auth is undefined. (Unless you use setTimeout but that is not a solution)
//Create axios instance
const api = $axios.create({
headers: {
common: {
Accept: 'application/json', //accept json
},
},
})
// Here is the magic, onRequest is an interceptor, so every request made will go trough this, and then we try to access app.$auth inside it, it is defined
api.onRequest((config) => {
// Here we check if user is logged in
if (app.$auth.loggedIn) {
// If the user is logged in we can now get the token, we get something like `Bearer yourTokenJ9F0JFODJ` but we only need the string without the word **Bearer**, So we split the string using the space as a separator and we access the second position of the array **[1]**
const token = app.$auth.strategy.token.get().split(' ')[1]
api.setToken(token, 'Bearer') // Here we specify the token and now it works!!
}
})
// Set baseURL to something different
api.setBaseURL('http://localhost:4100/')
// Inject to context as $api
inject('api', api)
}
Also Nuxt Auth itself has provided a solution for this issue:
https://auth.nuxtjs.org/recipes/extend/

Duplicated requests to same url with fetch using vue and webpack

I have weird results displayed in the web console:
fetch() is sending duplicated requests to the same url.
I thought it could be something related to fetch(), but also noticed that on reload of the app (quasar, based on webpack) also the requests to the http://localhost:8080/sockjs-node/info are duplicated.
By contrast, I noticed that requests handled by jQuery are NOT duplicated and works fine.
I cannot say if it is an error due to webpack configuration, fetch or they way I am using it i Vue components.
E.g. This article points out possible causes https://insights.daffodilsw.com/blog/how-to-avoid-duplicate-api-requests but in my case it is not due to user interaction : requests are triggered at time of relaunching the app (webpack), and particularly the stack trace shows that the requests are fired at time of creating the components, just multiple times.
Example of how I am using fetch():
// component -
methods : {
search(input) {
return new Promise(resolve => { // a new promise is request multiple times, in spite in created() it is called just once
var _base = 'myEndpoint/api'
const url = `${_base}fuzzyautocomplete?q=${encodeURI(input)}`
if (input.length < 3) {
return resolve([])
}
fetch(url) // so promises are actually different, and duplicated requests are fired by fetch
.then(response => response.json())
.then(data => {
console.log(data)
// resolve(data.query.search)
resolve(data)
})
})
},
....
// and it should be called just once at time of creation
created() {
this.search('init first query !!');
}
Could you advise?

Nuxt JS - reuse async call (axios get) within mounted?

If I am rendering a page with Nuxt, Vue, and Axios - is there a way to reuse the asyncData request (or data)?. For example, if I render a response, and the user takes an action on the page to filter, sort, etc. the data, can I reuse the same data to render again - or do I need to make a new call in mounted?
export default {
asyncData ({ env, params, error }) {
return axios.get(`${env.cockpit.apiUrl}/collections/get/cat_ruleset?token=${env.cockpit.apiToken}&simple=1&sort[ruleid]=1`)
.then((res) => {
return { catrules: res.data }
})
.catch((e) => {
error({ statusCode: 404, message: 'Post not found' })
})
},
mounted() {
},
methods: {
}
}
Of course you can reuse it. The simplest way would be to store the result somewhere (anywhere, really, but your store would be a good storage candidate) and change your method to:
asyncData ({ env, params, error }) {
return X ? Promise.resolve(X) : axios.get(...)
}
... where X is the stored result of your previous call.
But you don't have to.
Because, by default, the browser will do it for you. Unless you specifically disable the caching for your call, the browser will assume making the same call to the server will yield the same result if you do it within the number of seconds set in max-age of Cache-control.
Basically, the browser returns the previous result from cache without making a call to the server, so the optimization you're after is already performed by the browser itself unless you specifically disable it.
You can easily spot which calls were served from cache and which from server by looking in the Network tab of DevTools in Chrome. The ones from cache will have (memory cache) in the Size column:
... and will have a value of 0ms in Time column.
If you want control over when to call the server and when to serve a cached result — most browsers have a limit on max-age (see link above) — you could (and should) store the result of your previous call and not rely at all on the browser cache (basically the internal check inside the method, which I suggested at the top).
This would enable you to avoid making a call long time after the cache max-age has passed, because you already have the data, should you choose to do so.

Using vue router BeforeRouteEnter method to wait for http request to complete

Hi I'm trying to make it so that when a user opens a page it won't open until the data from the server is successfully retrieved so that it won't appear after 0.5s or so after the user enters.
To do this I read that I need to use BeforeRouteEnter but I'm having trouble finding information on how to properly use this, especially with waiting for my REST API to complete its request.
Here's the method I want to wait to complete before routing to my new component:
async getThread() {
const response = await postsService.fetchOneThread({
id: this.blockId,
topic: this.topicId,
thread: this.postId
});
this.thread = response.data;
}
so once this.thread = response.data only then do I want the page to display.
An important thing to note is that I am also passing through URL parameters to get the data which is the topic/black/post ID.
Here is my getUrlParam method also
url() {
let x = this.$route.params.topic.split('-');
this.topicId = x[0];
let y = this.$route.params.id.split('-');
this.blockId = y[0];
let post = this.$route.params.thread.split('-');
this.postId = post[1];
this.getThread();
}
Thanks
You need to move getThread inside beforeRouteEnter
beforeRouteEnter: (to, from, next) => {
postsService.fetchOneThread({
id: this.blockId,
topic: this.topicId,
thread: this.postId
}).then( response => {
//store the data somewhere accessible
next()
})
},
A few notes:
I don't think beforeRouteEnter can be async, so I'm using then to get the response
the component is not yet ready, so you can't access it yet, you need to save the information some other place so it can be read by the component. I'd suggest using Vuex for this.
If you decide to use Vuex than you need to add a mutation and call it from the promise's callback.
store.commit('ADD_THREAD', response.data)