VueJS get AJAX data before routing / mounting componenents - vue.js

I want to initialize some base data once before having VueJS does any routing / mounting.
Now I found out about the Global Navigation Guard router.beforeEach. But It triggers not only on the initial load (page load), but every route that is triggered. Now I could put in some sort of if-statement to have the code run only once, but that's not my preferred way of solving this:
// pseudo:
router.beforeEach((to, from, next) => {
if (state.initialized === false) {
await store.dispatch(init)
}
// the rest of my routing guard logic....
})
I'd prefer not having the if-statement run everytime (knowing it's only going to be true once, and false forever after).
Is there an official way to have (ajax) code run only once, AFTER vue is initialized (so I have access to vuex state, etc.), BEFORE any routing has started.

You can easily perform an asynchronous task before mounting your root Vue instance.
For example
// main.js
import Vue from 'vue'
import store from 'path/to/your/store'
import router from 'path/to/your/router'
import App from './App.vue'
store.dispatch('init').then(() => {
new Vue({
store,
router,
render: h => h(App)
}).$mount('#app')
})
It would be a good idea to show something in index.html while that's loading, eg
<div id="app">
<!-- just an example, this will be replaced when Vue mounts -->
<p>Loading...</p>
</div>

Related

Vue Vite + CosmicJS

I'm building a blog SPA website as a hobby, and trying to figure out what would be the best way to get the smallest latency to acquire posts from a database.
So far I've tried Wordpress, but its API at least at the very first initial request every time, with API cache enabled, takes about a second (800-1100ms) to load only a handful posts - 6 to be precise each with a picture and about 2-300 words, and this is only for testing.
So I'm looking around for other possible solutions to make the request faster but stay free of charge and came across Cosmic JS.
I installed the cosmicjs module, but getting all sorts of errors as I try to initiate the requests, based on their documentation which looks like the following:
<script>
const Cosmic = require('cosmicjs')
const api = Cosmic()
// Set these values, found in Bucket > Settings after logging in at https://app.cosmicjs.com/login
const bucket = api.bucket({
slug: "YOUR_BUCKET_SLUG",
read_key: "YOUR_BUCKET_READ_KEY"
})
</script>
First, you can't use require in Vite, so I've changed
this
const Cosmic = require('cosmicjs')
to this
import Cosmic from "cosmicjs"
But I'm still getting error:
ReferenceError: process is not defined
at node_modules/cosmicjs/dist/helpers/constants.js (cosmicjs.js?v=2a84de6d:1367:19)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at node_modules/cosmicjs/dist/main.js (cosmicjs.js?v=2a84de6d:1387:21)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at node_modules/cosmicjs/dist/index.js (cosmicjs.js?v=2a84de6d:3359:23)
at __require2 (chunk-NKHIPFFU.js?v=2a84de6d:15:50)
at cosmicjs.js?v=2a84de6d:3371:16ű
Can't figure out what to do next to even make this work, currently my code looks like this for the cosmic request part:
import Cosmic from "cosmicjs"
const api = Cosmic();
const bucket = api.bucket({
slug: "NOT-GOING-TO-SHOW-SORRY-AND-THX",
read_key: "NOT-GOING-TO-SHOW-SORRY-AND-THX",
});
const data = await bucket.objects
.find({
type: "posts", // Object Type slug
})
.props("title,slug,metadata") // response properties
.limit(10); // number of Objects to be returned
console.log(data)
Any idea might be a good help, thanks in advance
Figured it out:
So anyone trying to use in Vite ANY node module that has process as a function in any part in that module, should do the following
In your vite.config.ts or vite.config.js add the following
export default defineConfig({
// ...
define: {
'process.env': process.env
}
})
And instead of require
const Cosmic = require('cosmicjs')
always use import
import Cosmic from "cosmicjs"
Besides that, everything works the same as in other API requests, so eg. in my case, I'm API requesting posts from my cosmic js Bucket
<script setup lang="ts">
import { ref } from "vue";
import { onMounted } from "vue";
import moment from "moment";
import Cosmic from "cosmicjs"
const api = Cosmic();
const posts = ref([] as any);
const isLoading = ref(false);
const bucket = api.bucket({
slug: "c27229f0-9018-11ed-b853-65fa50acc7e7",
read_key: "G71yMNlvizQCtrvVyp9Z1AecQp8a4RTr5dl9uEGi6nst9FNQIW",
});
async function fetchData() {
isLoading.value = true
const data = await bucket.objects.find({
type: 'posts'
}).props('slug,title,content,metadata') // Limit the API response data by props
posts.value = data.objects
isLoading.value = false
console.log(posts)
}
onMounted(async () => {
fetchData();
});
</script>
and the iterate through them in my template
<template>
<ul v-if="!isLoading" class="blog-posts-ul" v-for="post in posts" :key="post.slug">
<div class="posts-card">
<a
><router-link
:to="/blog/ + post.slug"
key="post.id"
class="posts-permalink"
>
</router-link
></a>
<img
v-if="post.metadata.image != null"
class="post.metadata.hero"
:src="post.metadata.image.imgix_url"
:alt="post.title"
/>
<img v-else src="#/assets/logos/favicon-big.png" />
<div class="posts-date">
<p>
{{ moment(post.date).fromNow() + " " + "ago" }}
</p>
</div>
<div class="posts-text">
<h1 class="posts-title">{{ post.title }}</h1>
<p v-html="post.excerpt" class="posts-excerpt"></p>
</div>
</div>
</ul>
</template>
As a last sidenote, this works flawlessly compared to Wordpress API requests, I was using Wordpress for my backend CMS and even with API cache plugin enabled, it took around 800-1100ms to load the posts, now that time shrank to about 30ms for the API request of the text based data, and extra 30-40ms for each image (thumbnails for the posts).
As a request from a commenter, I'm including some description as to why Vite needs the change of vite.config.js
Basically, other vue instances do include by default the process.env in their configurations, Vite doesn't cause its generally a slower method for acquiring data - based on what I read, haven't tested this performance difference myself.
Vite is used instead of simply using Vue CLI, because its supposed to be even a bit faster then Vue CLI.
But, this is a small change.
"Why it is needed at all? I don't even see in your code above
calling any function named process"
Because it is this cosmicjs that is using it. I'm using a service that has a free tier, named Cosmic, and in their documentation it's advised to use their own node_module named comsicjs to fetch your data (posts, thumbnails etc etc).
This node module is what is using process, so when you are
importing it as I am as you can see above in my code:
import Cosmic from "cosmicjs"
And this is why you need to change your Vite's configuration file, so it can use process.
TLDR:
If you get an error in your Vite project with "process is not defined", that could be because a module you installed is using process, so simply add the following code to your vite.config.ts or vite.config.js
export default defineConfig({
....,
define: {
'process.env': process.env
}
})

Vue 3 access to app level provided instances from vue-router

I want to write some complicated guard logics in vue-router in Vue 3 to protect entering some routes according to store and my other provided modules. For example, I want to check if user profile info is present or not:
router.afterEach((to, from) => {
console.log('store: ', useStore());
const puex = usePuex();
puex.isReady().then(() => {
const me = puex.me.compute();
watch(me, (...params) => console.log('router: ', ...params));
});
});
In the above code, useStore and usePuex both try to inject store and puex instances from Vue app which are provided while being used in main.js bootstrap. But both use functions return undefined and I guess that the inject in this scope searches a different place where app-level provided instances do not exist.
So how can I inject them in the router file, or in other words how can I get store and puex instance using useStore and usePuex here?
I have found a way according this question but I still don't know if it is the best available solution. I can export the app instance from main.js file and then use app.$store and app.$puex instead. Although it works, I still think about a better solution to inject the store and puex instance using use functions (inject).
You still can add the navigation guards after that your app has mounted in main.js/ts, the code would look like:
// main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
const vm = createApp(App)
.use(puex)
.use(router)
.mount('#app');
router.afterEach((to, from) => {
const me = vm.puex.me.compute();
watch(me, (...params) => console.log('router: ', ...params));
});
You still can export that vm, to import it in the router file and use it the same way, but I really find somehow confusing, as main.js/ts is already importing the router file.

How to integrate inertiaJS with quasar framework?

I would like to integrate intertiaJS into my Quasar app so that I can communicate with my Laravel backend. My problem now is that the general stuff is taken over by the Quasar CLI, which is good in principle, but in this case it takes away my entry point as described at https://inertiajs.com/client-side-setup:
import { createApp, h } from 'vue'
import { App, plugin } from '#inertiajs/inertia-vue3'
const el = document.getElementById('app')
createApp({
render: () => h(App, {
initialPage: JSON.parse(el.dataset.page),
resolveComponent: name => require(`./Pages/${name}`).default,
})
}).use(plugin).mount(el)
My thought is that I could use a boot file like the offered in Quasar (https://quasar.dev/quasar-cli/boot-files), but I have to admit that I don't have the right approach.
When I look at the app.js that is automatically generated, I see that nothing special happens in the rendering:
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* DO NOT EDIT.
*
* You are probably looking on adding startup/initialization code.
* Use "quasar new boot <name>" and add it there.
* One boot file per concern. Then reference the file(s) in quasar.conf.js > boot:
* boot: ['file', ...] // do not add ".js" extension to it.
*
* Boot files are your "main.js"
**/
import Vue from 'vue'
import './import-quasar.js'
import App from 'app/src/App.vue'
import createStore from 'app/src/store/index'
import createRouter from 'app/src/router/index'
export default async function () {
// create store and router instances
const store = typeof createStore === 'function'
? await createStore({Vue})
: createStore
const router = typeof createRouter === 'function'
? await createRouter({Vue, store})
: createRouter
// make router instance available in store
store.$router = router
// Create the app instantiation Object.
// Here we inject the router, store to all child components,
// making them available everywhere as `this.$router` and `this.$store`.
const app = {
router,
store,
render: h => h(App)
}
app.el = '#q-app'
// expose the app, the router and the store.
// note we are not mounting the app here, since bootstrapping will be
// different depending on whether we are in a browser or on the server.
return {
app,
store,
router
}
}
I.e. in principle I should be able to link in without it causing any conflict situations. The question is, how would that look?
I have to link into the rendering afterwards and overwrite it as described in the code example. I would like to stay with the Quasar Cli, because it is very useful and the situation described here is the only exception.
p7
the boot files is the right place to inject and initialize your own dependencies or just configure some startup code for your application.
I have not had the opportunity to use the library you mention, but I detail a little how you could implement
create your boot file
import { plugin } from '#inertiajs/inertia-vue';
export default async({ app, Vue }) => {
Vue.use(plugin);
}
until there you have 50%. On the other hand, you cannot do a mixin to the main instance but you could do it for each page, however I recommend that you make a component part to which you add the data you need and make a mixin of the library you need
<template>
<div />
</template>
<script>
import { App } from '#inertiajs/inertia-vue';
export default {
mixins: [App],
props: ['initialPage', 'resolveComponent'],
}
</script>
In order to do this, modify according to how the library you use works.

How to force vue to wait for async <script> to run before mounting components

I have an async js script which is loaded at the top of index.html in my vue project. This script exposes several functions into the window object which I would like to be able to call. I would like to be able to call these functions in the mounted() lifecycle hook, but the async function appears to complete only after mounted has finished. Is there a way I can force the vue instance to wait for all <script> to complete before mounting the root component?
According to this issue in Github https://github.com/vuejs/vue/issues/7209 it seems that the async hooks in Vue lifecycle mounted() created() etc, is only for the ability to call async functions. but the lifecycle itself is synchronous.
Here are some ideas to manage this problem:
You can wrap your component with v-if and render it just as soon as your data is ready.
If you render a component by router - you can use the lifecycle of Vue router. and call your async function before enter to Vue page. do it like this:
export default {
beforeRouteEnter (to, from, next) {
// called before the route that renders this component is confirmed.
next()
}
}
this next() function calls the first Vue lifecycle hook...
full tutorial: https://router.vuejs.org/guide/advanced/navigation-guards.html#in-component-guards
If anyone is interested, here is how I solved my problem:
I modified the js file that the <script> was referencing to set global.initComplete=false initially, and when the script was complete, I marked it as true.
For main.js file, I used:
import Vue from 'vue'
import App from './App.vue'
function init() {
let vm = await new Vue({
render: h => h(App),
})
if(!global.initComplete) {
setTimeout(init, 50)
} else {
vm.$mount('#app')
}
}
init()

Creating a single instance of a class within a Vue application

I'm new to Vue and I'm struggling to wrap my head around how to implement what seems to me like a good case for a global variable or singleton.
The background is that I'm using Azure AD B2C for authentication with the MSAL library. MSAL requires a single instance of the Msal.UserAgentApplication to be declared and then shared through the application.
What I'm struggling with is how to declare that instance somewhere central and then access it from each component including the router.
At the moment I've got a class which is similar to this example: https://github.com/sunilbandla/vue-msal-sample/blob/master/src/services/auth.service.js and when I want to use the methods I'm doing:
var authService = new AuthService();
authService.Login();
Unfortunately this creates a new instance of MSAL each time the class is instantiated which in turn caused my users to end up stuck in an authentication loop.
Any help would be greatly appreciated.
Many thanks.
Following on from the answer below by Teddy I've amended my main.js as follows:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import './registerServiceWorker'
import AuthService from './services/AuthService';
Vue.config.productionTip = false
Vue.prototype.$authService = new AuthService();
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app');
And my register.vue component as follows:
<template>
<div class="about">
<h1>This is the register page, it should redirect off to B2C</h1>
</div>
</template>
<script>
import router from '#/router.js'
export default {
created(){
this.$authService.isAuthenticated().then(
function(result){
if(result){
router.push('/');
}
else{
authService.register();
}
});
}
}
</script>
The component is saying that this.$authService is undefined so it's obviously not reading the prototype.
It feels like I'm missing something really fundamental in Vue at this point.
You can just add it as a Vue instance property. It will be there for all Vue components.
Set it up in main.js like this:
Vue.prototype.$authService = new AuthService();
You can later access it in any Vue component. For example:
this.$authService.Login();
Refer:
https://v2.vuejs.org/v2/cookbook/adding-instance-properties.html
Edit:
You have to use this.$router.push and this.$authService.register inside the isAuthenticated callback. If "this" refers to something else in that block, store var self=this; before the callback starts, or use fat arrow syntax.
<script>
//No import as router is available in 'this'
export default {
created(){
var self=this; //For use inside the callback
this.$authService.isAuthenticated().then(
function(result){
if(result){
self.$router.push('/');
}
else{
self.$authService.register();
}
});
}
}
</script>
Edit 2:
Maybe you can create the instance (singleton) itself in a file called AuthServiceInst.js. Then you can import it in both main.js and router.js.
New file AuthServiceInst.js:
import AuthService from './AuthService.js'
export const authService = new AuthService();
main.js:
import {authService} from './AuthServiceInst.js'
Vue.prototype.$authService = authService;
router.js:
import {authService} from './AuthServiceInst.js'
//Now you can use authService
In Vue 3, to declare global instances you need to use app.config.globalProperties. This is a replacement of Vue 2's Vue.prototype which is no longer present in Vue 3. As with anything global, this should be used sparingly.
// main.js
const app = createApp(App)
.use(router)
.use(store)
.use(vuetify)
app.config.globalProperties.msg = 'hello world'
app.mount('#app')
This makes msg available inside any component template in the application, and also on this of any component instance:
export default {
mounted() {
console.log(this.msg) // 'hello world'
}
}
Source: Docs