問題:VueRouter の beforeEach の next() で再描画時に、元の画面のデータが残ってしまう。
下記の通り Vue のエラー時の routing 処理を実装したいと思い、下記の通り実装を進めました。
- Vue-Cliの Typescript プロジェクトを作成
- src/router.ts に routing 処理を VueRouter を使い実装する。
- dashboard にアクセスした際に、認証の有無を確認する処理を router.beforeEach において実装する。
- vuex store に実装した
isAuthenticated:boolean
が false だった場合、AuthenticationError を throw する。 - AuthenticationError を catch した際は、vuex store に実装した、setApplicationMessage 処理を呼び出して Errorメッセージを描画する。
しかしVueRouter の beforeEach の next() で再描画時に、元の画面のデータが残ってしまうという問題が発生しました。
その解決までにいろいろと調べたところを記載します。
その時のソースは下記の通りです。
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from '@/views/Login.vue';
import Dashboard from '@/views/Dashboard.vue';
import ServerError from '@/views/ServerError.vue';
import store from '@/store/store'
Vue.use(VueRouter);
const routes = [
{path: '/', component: Login},
{path: '/dashboard', component: Dashboard, meta:{ requiresAuth: true }},
{path: '/ServerError',component: ServerError},
];
const router:VueRouter = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next)=>{
try
{
if(to.matched.some(record => record.meta.requiresAuth)
&& ! (store.state.isAuthenticated))
{
console.log('認証が必要です');
throw new AuthorizationError('認証が必要です');
}
store.commit('setApplicationMessage', '');
next();
}
catch(error)
{
let path = '';
if (error instanceof AuthorizationError)
{
//ログイン画面に遷移
path = '/';
store.commit('setApplicationMessage', error.message);
}
store.commit('setRedirecting', true);
path ? next(path)
: next();
}
});
export default router;
原因:Vuejs のリソースの再利用
Vue.js アプリケーションのシステム的に、VueRouterは同じリソースの描画をなるべく行わないようにするらしく、VueRouter の beforeEach でroutingの制御をした時 たまたま意図しないデータの保持が行われてしまうようでした。
解決法:エラー時の再描画前に、必ずダミーのパスへの遷移を行い、パラメータをリセットする。
ApplicationError発生時にだけ、必ずメッセージの再描画するために、エラー時の再描画前に、必ずダミーのパスへの遷移を行い、パラメータをリセットすることで解決しました。
src/router.ts
import Vue from 'vue';
import VueRouter from 'vue-router';
import Login from '@/views/Login.vue';
import Dashboard from '@/views/Dashboard.vue';
import {Profile} from '@/store/profile';
import store from '@/store/store';
import ProfileState from '@/store/states/profile-state';
import AuthorizationError from '@/errors/AuthorizationError';
import ServerError from '@/views/ServerError.vue';
import ErrorHandler from '@/errors/error-handler';
Vue.use(VueRouter);
const routes = [
{
path: '/',
component: Login
},
{
path: '/dashboard',
component: Dashboard,
meta: { requiresAuth: true }
},
{
path: '/ServerError',
component: ServerError
},
];
const router:VueRouter = new VueRouter({
mode: 'history',
routes
});
router.beforeEach((to, from, next)=>{
const redirecting:boolean = store.getters.redirecting;
console.log('redirecting:'+redirecting)
redirecting
? store.commit('setRedirecting',false)
: store.commit('setApplicationMessage','');
try
{
if(to.matched.some(record => record.meta.requiresAuth)
&& ! (((Profile.state) as ProfileState).authenticated)) {
console.log('認証が必要です');
throw new AuthorizationError('認証が必要です');
}
if(redirecting)
{
next();
return;
}
store.commit('reload');
next();
}
catch(error)
{
next('__nowhere__')
const path = ErrorHandler.handleAndGetPathOnError(error);
store.commit('setRedirecting', true);
path ? next(path)
: next();
}
});
export default router;
また、
画面の reload 処理を実装する。
App.vue のテンプレートの divの :key を更新することで、App.vue の再描画を強制します。
https://michaelnthiessen.com/force-re-renderredirect中を検知するグローバル変数を vuex store に作成して処理の重複を制御する
beforeEach 処理で redirect中だったら vuex seRedirectでredirect中をoffする。普通のアクセスなら、setApplicationMessageを呼び出し、メッセージを空にする。
this.$store.subscribe で、setApplicationMessage 処理の実行を監視して、App.vueのリロード処理を行う
(こちらの記事を参考にさせて頂きました
https://dev.to/viniciuskneves/watch-for-vuex-state-changes-2mgj
)
等の修正も併せて行いました。
src/store/store.ts
import Vue from 'vue';
import Vuex, { StoreOptions } from 'vuex';
import { Profile } from './profile';
import RootState from './states/root-state';
Vue.use(Vuex);
const store:StoreOptions<RootState> = {
state:{
__applicationMessage__:'',
__redirecting__: false,
__reloadKey__: 0
},
getters:{
getReloadKey(state:RootState):number
{
return state.__reloadKey__;
},
redirecting(state:RootState):boolean{
return state.__redirecting__;
},
getApplicationMessage(state:RootState):string {
const messsage = state.__applicationMessage__;
return messsage;
},
},
mutations:{
setApplicationMessage(state:RootState,message:string)
{
state.__applicationMessage__ = message;
},
setRedirecting(state:RootState,occuring:boolean):void
{
state.__redirecting__ = occuring;
},
reload(state:RootState)
{
state.__reloadKey__++;
}
},
modules: {
Profile
}
};
export default new Vuex.Store<RootState>(store);
src/App.vue
<template>
<div id="app" :key='reloadKey'>
<ApplicationMessage
v-if="show"
:message="getApplicationMessage()"
></ApplicationMessage>
{{ this.username }}
<div class="links">
<router-link to="/dashboard">Dashboard</router-link>
<router-link to="/">Login</router-link>
<router-link to="/ServerError">ServerError</router-link>
</div>
<img alt="Vue logo" src="./assets/logo.png">
<router-view/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import Component from 'vue-class-component';
import { State, Action, Getter } from 'vuex-class';
import UserInterface from '@/models/user';
import VueRouter from "vue-router";
import ApplicationMessage from '@/components/ApplicationMessage.vue';
@Component({
components:{
ApplicationMessage
},
})
export default class App extends Vue {
private get user():UserInterface{
return this.$store.state.Profile.user;
}
@Getter('getApplicationMessage') applicationMessage!:string;
@Getter('getReloadKey') reloadKey!:number;
private username:string = this.user.name;
private getApplicationMessage():string
{
return this.applicationMessage;
}
mounted():void {
this.$store.subscribe((mutation, state) => {
switch(mutation.type) {
case 'setApplicationMessage':
this.show = false;
const newValue = mutation.payload;
if(newValue === '')
{
this.$store.commit('reload');
return;
}
this.show = true;
this.$store.commit('reload');
this.$store.commit('setRedirecting',false)
break;
}
});
}
private show:boolean= false;
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.links *{
padding: 0.5rem;
}
</style>
参照
認証機能に関する VueRouter の routing については下記の記事に詳しく書いて頂いておりましたので参考にさせて頂きました。
https://qiita.com/takatama/items/05e9fbc7199cde4caf60
Vue と Vuex による認証処理の実装については、下記の記事に詳しく書いて頂いておりましたので参考にさせて頂きました。
Vue.js で簡単なログイン画面 (トークン認証) を作ってみた
https://qiita.com/sunadorinekop/items/f3486da415d3024c7ed4
TypeScriptのError処理の実装については下記の記事に詳しく記載して頂いておりましたので参考にさせて頂きました。
TypeScriptを利用した場合の例外の基本設計
https://qiita.com/kenju/items/b0554846a44d369cba7b
Vuex での Error 動作の作成については、下記の記事を参考にさせて頂きました。
https://www.hypertextcandy.com/vue-laravel-tutorial-error-handling
Vuex 内部で Axiosを使っている事例として、下記の記事を参考にさせて頂きました。
https://qiita.com/kai_kou/items/c4e449964df59d5a5fb0
JTWToken認証処理については下記の記事を参考にさせて頂きました。
https://www.webopixel.net/php/1444.html
SPAにおける全体の例外設計に関しましては下記の記事を参考にさせて頂きました。
https://www.altus5.co.jp/blog/angular/2019/03/30/angular-error-hadling-design/
ソース
以上です。