定义 Vue-Router 路由时访问 Vuex 状态

我有以下的 Vuex 商店(main.js) :

import Vue from 'vue'
import Vuex from 'vuex'


Vue.use(Vuex)


//init store
const store = new Vuex.Store({
state: {
globalError: '',
user: {
authenticated: false
}
},
mutations: {
setGlobalError (state, error) {
state.globalError = error
}
}
})


//init app
const app = new Vue({
router: Router,
store,
template: '<app></app>',
components: { App }
}).$mount('#app')

我还为 Vue 路由器(outes.js)定义了以下路由:

import Vue from 'vue'
import VueRouter from 'vue-router'


Vue.use(VueRouter)


//define routes
const routes = [
{ path: '/home', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/secret', name: 'Secret', component: SecretPage, meta: { requiresLogin: true }
]

我试图让它,如果 Vuex 存储的 user对象,它的 authenticated属性设置为 false,是有路由器重定向用户到登录页面。

我有这个:

Router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresLogin) && ???) {
// set Vuex state's globalError, then redirect
next("/Login")
} else {
next()
}
})

问题是我不知道如何从 beforeEach函数内部访问 Vuex 存储的 user对象。

我知道我可以使用 BeforeRouteEnter在组件内部使用路由器保护逻辑,但是这会使每个组件混乱不堪。我想在路由器层集中定义它。

117004 次浏览

As suggested here, what you can do is to export your store from the file it is in and import it in the routes.js. It will be something like following:

You have one store.js:

import Vuex from 'vuex'


//init store
const store = new Vuex.Store({
state: {
globalError: '',
user: {
authenticated: false
}
},
mutations: {
setGlobalError (state, error) {
state.globalError = error
}
}
})


export default store

Now in routes.js, you can have:

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from ./store.js


Vue.use(VueRouter)


//define routes
const routes = [
{ path: '/home', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/secret', name: 'Secret', component: SecretPage, meta: { requiresLogin: true }
]


Router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresLogin) && ???) {
// You can use store variable here to access globalError or commit mutation
next("/Login")
} else {
next()
}
})

In main.js also you can import store:

import Vue from 'vue'
import Vuex from 'vuex'


Vue.use(Vuex)


import store from './store.js'


//init app
const app = new Vue({
router: Router,
store,
template: '<app></app>',
components: { App }
}).$mount('#app')

I ended up moving the store out of main.js and into store/index.js, and importing it into the router.js file:

import store from './store'


//routes


const routes = [
{ path: '/home', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/secret', name: 'Secret', component: SecretPage, meta: { requiresLogin: true }
]


//guard clause
Router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresLogin) && store.state.user.authenticated == false) {
store.commit("setGlobalError", "You need to log in before you can perform this action.")
next("/Login")
} else {
next()
}
})

This is how i would to it.

In App.vue, I will keep a watcher on cookie that stores authentication details. ( Obviously I would store a token containing authentication details as cookie after authentication )

Now whenever this cookie becomes empty, I will route the user to /login page. Logging out deletes the cookie. Now if user hit back after logging out, now since the cookie doesnot exist, ( which requires user to be logged in ), user will be routed to login page.

Managing your location state separate from the rest of your application state can make things like this harder than they maybe need to be. After dealing with similar problems in both Redux and Vuex, I started managing my location state inside my Vuex store, using a router module. You might want to think about using that approach.

In your specific case, you could watch for when the location changes within the Vuex store itself, and dispatch the appropriate "redirect" action, like this:

dispatch("router/push", {path: "/login"})

It's easier than you might think to manage the location state as a Vuex module. You can use mine as a starting point if you want to try it out:

https://github.com/geekytime/vuex-router

I found that the store was not available to me in router.js when using the guard router.beforeEach, however by changing the guard to router.beforeResolve, then the store was available.

I also found that by awaiting the import of the store in the guard router.beforeEach, I was then able to successfully use router.beforeEach. I provide an example of that below the router.beforeResolve code.

So to keep my example simular to the OP's question the following is how it would have worked for me. I am using vue-router 3.0.2 and vuex 3.1.0.

import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store';  //or use a full path to ./store


Vue.use(VueRouter)


//define routes
const routes = [
{ path: '/home', name: 'Home', component: Home },
{ path: '/login', name: 'Login', component: Login },
{ path: '/secret', name: 'Secret', component: SecretPage, meta: { requiresLogin: true }
]


const router = new VueRouter({
routes  //ES6
})


router.beforeResolve((to, from, next) => {
const user = store.state.user.user;  //store with namespaced  modules
if (to.matched.some(record => record.meta.requiresLogin) && user.isLoggedIn) {
next() //proceed to the route
} else next("/login")  //redirect to login


})


export default router;

I also found that I could get router.beforeEach to work by await-ing the loading of the store in the beforeEach guard.

router.beforeEach(async (to, from, next) => {
const store = await import('@/store');  //await the store
const user = store.state.user.user;  //store with namespaced modules
if (to.matched.some(record => record.meta.requiresLogin) && user.isLoggedIn) {
....  //and continue as above
});

Importing the store as @Saurabh suggested works. However IMHO it brings a certain workaround smell into your code.

It works, because the Vuex store is a singleton. Importing it it creates a hard linked dependency between your component, the routers and the store. At the very least it makes it harder to unit test. There is a reason why vue-router is decoupled and works like this and it may pay off to follow its suggested pattern and to keep the router decoupled from the actual store instance.

Looking at the source of vue-router it becomes apparent that there is a more elegant way to access the store from the router, e.g. in the beforeRouteEnter guard:

beforeRouteEnter: (to, from, next) => {
next(vm => {
// access any getter/action here via vm.$store
// avoid importing the store singleton and thus creating hard dependencies
})
}

Edit on 10. Sept 2020 (thanks @Andi for pointing that out)

Using the beforeRouteEnter guard is then up to the concrete case. Off the bat I see the following options:

  1. Declare the guard in a mixin and selectively use it in the components that need it, instead of filtering needed components in a global guard
  2. Declare the guard in a global mixin (beware of declaration peculiarities, e.g. needs to be declared after Vue.use(VueRouter);: here and here)

I found vuex-router-sync to be the easiest solution. From their site:

It adds a route module into the store, which contains the state representing the current route

You may use router.app to access the root Vue instance the router was injected into, then access store regularly via router.app.$store.

const router = new Router({
routes,
})


router.beforeEach((to, from, next) => {
// access store via `router.app.$store` here.
if (router.app.$store.getters('user')) next();
else next({ name: 'login' });
})

Here is the API Reference.

Vue 3

The router.app is removed in Vue 3, but you can still add it when using the router as explained in the migration guide:

app.use(router)
router.app = app

in main.js you can do:

router.$store = store

and then in index.js under router:

await router.$store.dispatch("abcd")