Router 3:路由确认和页面数据是两件事
Router 把 URL 匹配到组件,再把匹配结果送进 router-view。Router 3 使用 new VueRouter({ mode, routes }) 创建实例,插件通过 Vue.use(VueRouter) 接入 Vue 2。组件中的 this.$route 是当前路由信息,this.$router 是发起 push/replace 等命令的路由器;不要把“一个路由快照”和“整个导航器”混成同一个对象。
先把参数变化接到实际组件
在本章实验目录保存 router.html。默认 hash 模式把前端地址放在 # 后,刷新时服务器仍请求同一份 HTML;history 模式使用普通路径,需要服务器回退到应用入口。下面不要求任何后端配置。
<!doctype html>
<meta charset="utf-8">
<div id="app">
<router-link to="/items/A">A</router-link>
<router-link to="/items/B">B</router-link>
<router-view></router-view>
</div>
<script src="./node_modules/vue/dist/vue.js"></script>
<script src="./node_modules/vue-router/dist/vue-router.js"></script>
<script>
Vue.use(VueRouter)
const Item = {
props: ['id'],
data() { return { visits: 0 } },
watch: {
id: { immediate: true, handler() { this.visits++ } }
},
created() { console.log('created item') },
template: '<p>{{ id }} / 参数处理 {{ visits }} 次</p>'
}
const router = new VueRouter({
mode: 'hash',
routes: [
{ path: '/', redirect: '/items/A' },
{ path: '/items/:id', component: Item, props: true }
]
})
new Vue({ el: '#app', router })
</script>:id 是动态路径段,props:true 把参数作为组件 prop,避免组件内到处硬编码 $route.params。首次进入 A,immediate 侦听器执行一次,显示 A / 1;点击 B,Router 复用同一个 Item 实例,id 变化,显示 B / 2,但 created 不再运行。因此仅在 created 中取一次参数的页面会停留在旧数据。
如果页面需要在参数变化时决定是否允许离开当前条目,可使用组件 beforeRouteUpdate(to, from, next)。此时实例已存在,能访问 this。若采用 watch 拉数据,beforeRouteUpdate 就不要再重复发同一请求;前者负责状态变化后的工作,后者负责导航决策。beforeRouteEnter 则发生在新实例创建前,不能直接使用 this,需要时通过 next(vm => ...) 在确认后访问实例。
Router 3 的守卫必须把路径交给 next
下面独立保存 navigation.cjs。abstract 模式只维护内存历史,适合在 Node 观察守卫和失败;它不是浏览器部署模式。
const Vue = require('vue')
const VueRouter = require('vue-router')
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'abstract',
routes: [
{ path: '/home', component: {} },
{ path: '/private', component: {}, meta: { requiresAuth: true } }
]
})
let signedIn = false
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth) && !signedIn) {
next(false)
return
}
next()
})
async function main() {
await router.push('/home')
try { await router.push('/private') }
catch (failure) {
console.log(VueRouter.isNavigationFailure(
failure, VueRouter.NavigationFailureType.aborted
)) // true
}
console.log(router.currentRoute.path) // /home
signedIn = true
await router.push('/private')
console.log(router.currentRoute.path) // /private
try { await router.push('/private') }
catch (failure) {
console.log(VueRouter.isNavigationFailure(
failure, VueRouter.NavigationFailureType.duplicated
)) // true
}
}
main().catch(console.error)next() 放行,next(false) 取消,next(location) 重定向,next(error) 报告错误。Router 3 不把普通 return false 当作 next(false);漏调 next 会让该导航一直等。这里 return 的用途是在取消后结束函数,避免继续执行第二个 next。不能只数源码中写了几个 next,而要检查每条运行分支恰好完成一次决策。
守卫检查 to.matched 中所有匹配记录,是因为权限条件可能写在父布局上。路由守卫改善前端导航体验,服务端仍要独立验证真正的权限。若异步检查登录状态,成功、失败、取消都要完成 next;例如 try 成功后 next(),catch 中 next(error),不能让接口失败挂住导航。
Router 3.6 的程序化 push 在受阻时可能拒绝 Promise:aborted 表示被取消,duplicated 表示重复位置,cancelled 表示新导航打断旧导航,redirected 表示守卫重定向。真实脚本错误也可能拒绝,所以不能把 router.push(...).catch(() => {}) 作为统一修复。只忽略业务明确接受的失败类型,其余应显示或上报。Router 4 对已知导航失败的 Promise 结果契约不同,迁移时要改调用方判断,而不只是替换初始化函数。
加载页面数据时分别处理失败和取消
导航到 B 被确认,不代表 B 的请求已经完成。若选择“先进入再加载”,页面要明确区分 loading、success、error,并使 A 的迟到响应失效。可将第二篇的请求身份机制放进 id 的侦听器:每次参数任务开始增加 revision,成功/失败写入前比较;beforeDestroy 时取消与失效。复用实例没有销毁,因此只在 beforeDestroy 清理会漏掉 A → B 这条路径。
若选择“进入前加载”,可以把任务放入 beforeRouteEnter/beforeRouteUpdate,但要明白它阻塞确认过程:当前页面仍在,加载失败必须通过 next(error) 或明确取消结束导航。对于取消后的请求,还需让任务失效,避免被别的代码接收。两种策略可以按页面需要选择,不能同一数据在守卫和 watch 各取一次。
亲眼看到新导航中断旧导航
下面另存 cancel-navigation.cjs。它手动保留 /slow 的 next,在 /fast 完成后才放行旧导航,验证“旧守卫后来同意了”不等于“旧页面最终获胜”。
const Vue = require('vue')
const VueRouter = require('vue-router')
Vue.use(VueRouter)
const router = new VueRouter({
mode: 'abstract',
routes: ['/home', '/slow', '/fast'].map(path => ({ path, component: {} }))
})
let release
router.beforeEach((to, from, next) => {
if (to.path === '/slow') release = next
else next()
})
async function main() {
await router.push('/home')
const slow = router.push('/slow').catch(error => error)
await router.push('/fast')
release()
const failure = await slow
console.log(VueRouter.isNavigationFailure(
failure, VueRouter.NavigationFailureType.cancelled
), router.currentRoute.path) // true /fast
}
main().catch(console.error)练习:离开保护与重复导航
在 navigation.cjs 中增加 dirty 状态:只有从 /private 离开且 dirty=true 时才取消。依次进入 private、设 dirty=true、尝试 home、设 dirty=false、再次 home,要求路径依次为 private、private、private、private、home。然后重复 home,应分类为 duplicated,不能显示成“尚未保存”。
再解释 router.html 的 B 参数更新为什么不执行 created;若每个条目必须拥有全新草稿,可给 router-view 使用 :key="$route.params.id",比较重建与原实例复用的差别。重新挂载有成本,也会丢掉旧局部状态,应先确定草稿保存契约。