Vue3$View-VueRouter

71 阅读4分钟

Vue3$View-VueRouter

0. Slot and Router

Slot

如果我们想在组件的某个地方插入某些内容,我们可以通过插槽 slot 来实现:

  • where:<slot></slot> 定义的地方 (provider)
  • what:组件标签内部的内容 (consumer)

Router

如果我们想在组件的某个地方插入某些内容,我们也可以通过路由 vue-router 来实现:

  • where:<RouterView> 定义的地方 (consumer)
  • what:路由对应的组件 (provider)
  • when:路由改变的时候 / 调用 router 方法的时候 (consumer)

P. 安装

单独安装:

npm install vue-router@4

脚手架安装:

npm create vue@latest

1. 配置路由器

路由器参数

const router = createRouter({
  history: createWebHistory(), // createWebHashHistory() | createMemoryHistory()
  routes,
  linkActiveClass: 'border-indigo-500', 
  linkExactActiveClass: 'border-indigo-700',
  scrollBehavior (to, from, savedPosition) {
    if (savedPosition) {
      return savedPosition
    } else {
      return { top: 0 }
    }
  }
})

路由器参数介绍

history: createWebHistory(), // createWebHashHistory() | createMemoryHistory() // 需要后端配置
// 只考虑 path 和 params。
// 考虑 alias,不考虑 redirect。
linkActiveClass: 'border-indigo-500',
linkExactActiveClass: 'border-indigo-700',
scrollBehavior (to, from, savedPosition) {
	// return desired position, if {} or falsy value => no scrolling
	  
	// always scroll to top
	return { top: 0 }

    // always scroll 10px above the element #main
	return {
		// could also be
		// el: document.getElementById('main'),
		el: '#main',
		// 10px above the element
		top: 10,
	}

	// savedPosition
	if (savedPosition) {
		return savedPosition
	} else {
		return { top: 0 }
	}

	// to anchor
	if (to.hash) {
		return {
			el: to.hash,
			behavior: 'smooth'
		}
	}

	// delay (for animation...)
	return new Promise((resolve, reject) => {
		setTimeout(() => {
			resolve({ left: 0, top: 0 })
		}, 500)
	})
}

注册路由器

const app = createApp(App)
app.use(router)

处理导航故障

导航失败的原因:

  • 导航守卫 return false
  • 导航守卫重定向
  • 新的导航守卫替换了之前的
  • 已经在所导航的位置
  • 导航守卫 throw Error

检测导航故障

router.push() 返回的是 Promise:

  • 导航被阻止:Navigation Failure (带有额外属性的 Error
  • 正常:falsy(一般是 undefined
const navigationResult = await router.push('/my-profile')

if (navigationResult) {
  // 导航被阻止
} else {
  // 导航成功 (包括重新导航的情况)
  this.isMenuOpen = false
}
全局导航故障
router.afterEach((to, from, failure) => {
  if (failure) {
    sendToAnalytics(to, from, failure)
  }
})

鉴别导航故障

故障分为3类(throw error 不在其中,通过 router.onError() 处理):

  • aborted:在导航守卫中 return false
  • cancelled:去了新的导航
  • duplicated:导航被阻止,已经在目标位置了
import { isNavigationFailure, NavigationFailureType } from 'vue-router'

// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')

if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
  // 给用户显示一个小通知
  showToast('You have unsaved changes, discard and leave anyway?')
}

导航故障的属性

// 正在尝试访问 admin 页面
router.push('/admin').then(failure => {
  if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
    failure.to.path // '/admin'
    failure.from.path // '/'
  }
})

检测重定向

导航守卫中返回一个新的位置

await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
  // redirectedFrom 是解析出的路由地址,就像导航守卫中的 to 和 from
}

2. 配置路由

路由参数

const route = {
	path: '/users/:id?',
	name: 'user',
	component: User,
	meta: {
		requiresAuth: true
	},
	redirect: '/customers/:id', // alias
	strict: true, // sensitive
	props: true, // boolean | object | function
	beforeEnter(to, from){},
	children: [{}]
}

路由参数介绍

path: '/users/:id?',
// $route.params.id
// id 改变是组件不变,可 watch / onBeforeRouteUpdate
// :id, :id?, :id*, :id+, id(\\d)?
// /:pathMatch(.*)* => match all, under `route.params.pathMatch`
// /user-:afteruser(.*) => match all starting with 'user-', under `route.params.afterUser`
name: 'user',
// 不能重复,否则:No match found for location with path...
// 可以和 param 一起使用, 和 path 不同
component: User,
// 静态 / 动态 () => import()
// webpack:  [named chunks](https://webpack.js.org/guides/code-splitting/#dynamic-imports)
	// () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
// vite:  [rollupOptions](https://vitejs.dev/config/#build-rollupoptions)
// components: { name: component, name2: cmp2 }
meta: {
	requiresAuth: true
},
// route.meta 会包含从父到子的并集,以子优先
// 官网中的例子有问题,下面两个并不同。如果子 true 父 false,第一个是 true,第二个是 false
// to.matched.some(record => record.meta.requiresAuth)
// if (to.meta.requiresAuth && !auth.isLoggedIn())
redirect: '/customers/:id', // alias
// redirect: Navigation Guards only apply to its target
// redirect: path can be relative
// alias: String | Array<String>
strict: true, // sensitive
// route / router level
props: true, // boolean | object | function
// Boolean Mode: component should define a prop whose name is the same with param
// Boolean with named views: props: { default: true, sidebar: false }
// Object Mode: static values
// Function Mode: props: route => ({ query: route.query.q })
// Via RouterView see RouterView
beforeEnter(to, from){},
children: [{}]
/* path:
	if not starting with '/' => relative
	can be '' // reload will show '' child component
*/
// component: can ignore (4.1+)

Routes' Matching Syntax

  1. :userId => ([^/]+)
  2. /:orderId(\\d+), /:productName // orderId: only numbers, productName: anything else
  3. Repeatable params:
    1. /:chapters+: matches /one, /one/two, /one/two/three, etc.
    2. /:chapters*: matches /, /one, /one/two, /one/two/three, etc.
    3. /:chapters(\\d+)+: matches /1, /1/2, etc.
// given { path: '/:chapters*', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// produces /
router.resolve({ name: 'chapters', params: { chapters: ['a', 'b'] } }).href
// produces /a/b

// given { path: '/:chapters+', name: 'chapters' },
router.resolve({ name: 'chapters', params: { chapters: [] } }).href
// throws an Error because `chapters` is empty

Match all with pathMatch

// Match all with pathMatch
router.push({
  name: 'NotFound',
  // preserve current path and remove the first char to avoid the target URL starting with `//`
  params: { pathMatch: route.path.substring(1).split('/') },
  // preserve existing query and hash if any
  query: route.query,
  hash: route.hash,
})

vite rollupOptions for dynamic imports

// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      // https://rollupjs.org/guide/en/#outputmanualchunks
      output: {
        manualChunks: {
          'group-user': [
            './src/UserDetails',
            './src/UserDashboard',
            './src/UserProfileEdit',
          ],
        },
      },
    },
  },
})

动态路由

有时候我们需要动态路由。虽然说是动态路由,也需要提前准备好。我们之后可以通过配置来筛选这些配置好的路由。

添加路由

// router.addRoute()
router.addRoute({ path: '/about', component: About })
// 需要手动跳转
router.replace(router.currentRoute.value.fullPath)
// 在导航守卫中不使用 router.replace,而是直接返回要跳转的地址
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })

删除路由

  1. 替换路由:通过添加相同名称的路由,会替换之前的路由:
router.addRoute({ path: '/about', name: 'about', component: About })
// this will remove the previously added route because they have
// the same name and names are unique across all routes
router.addRoute({ path: '/other', name: 'about', component: Other })
  1. 调用添加路由返回的函数:
const removeRoute = router.addRoute(routeRecord)
removeRoute() // removes the route if it exists
  1. router.removeRoute(name)
router.addRoute({ path: '/about', name: 'about', component: About })
// remove the route
router.removeRoute('about')

查看已存在的路由

  • router.hasRoute()
  • router.getRoutes()

3. 导航守卫 Navigation Guards

路由守卫分为两类三种:

  • 全局路由守卫
  • 单个路由守卫
    • 路由独享守卫
    • 组件内守卫

3.1 Global Guards

1. Global Before Guards

Global before guards are called in creation order, whenever a navigation is triggered.

Guards may be resolved asynchronously, and the navigation is considered pending before all hooks have been resolved.

return value:

  • false: cancel
  • route location
  • throw error: router.onError() callbacks will be called
const router = createRouter({ ... })

router.beforeEach((to, from) => {
  // ...
  // explicitly return false to cancel the navigation
  return false
})

2. Global Resolve Guards

Global resolve guards triggers on every navigation, but are called right before the navigation is confirmed, after all in-component guards and async route components are resolved.

router.beforeResolve(async to => {
  if (to.meta.requiresCamera) {
    try {
      await askForCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        // ... handle the error and then cancel the navigation
        return false
      } else {
        // unexpected error, cancel the navigation and pass the error to the global handler
        throw error
      }
    }
  }
})

3. Global After Hooks

router.afterEach((to, from) => {
  sendToAnalytics(to.fullPath)
})
router.afterEach((to, from, failure) => {
  if (!failure) sendToAnalytics(to.fullPath)
})

Global injections within guards

// main.ts
const app = createApp(App)
app.provide('global', 'hello injections')

// router.ts or main.ts
router.beforeEach((to, from) => {
  const global = inject('global') // 'hello injections'
  // a pinia store
  const userStore = useAuthStore()
  // ...
})

3.2 Per-Route Guards

beforeEnter guards only trigger when entering the route, navigating from a different route. beforeEnter: Function | Array<Function>

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: (to, from) => {
      // reject the navigation
      return false
    },
  },
]
function removeQueryParams(to) {
  if (Object.keys(to.query).length)
    return { path: to.path, query: {}, hash: to.hash }
}

function removeHash(to) {
  if (to.hash) return { path: to.path, query: to.query, hash: '' }
}

const routes = [
  {
    path: '/users/:id',
    component: UserDetails,
    beforeEnter: [removeQueryParams, removeHash],
  },
  {
    path: '/about',
    component: UserDetails,
    beforeEnter: [removeQueryParams],
  },
]

3.3 In-Component Guards

Options API:

  • beforeRouteEnter
  • beforeRouteUpdate
  • beforeRouteLeave

Composition API: can be used by components in <RouterView/>

  • onBeforeRouteUpdate
  • onBeforeRouteLeave
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
import { ref } from 'vue'

const userData = ref()
// same as beforeRouteUpdate option but with no access to `this`
onBeforeRouteUpdate(async (to, from) => {
  // only fetch the user if the id changed as maybe only the query or the hash changed
  if (to.params.id !== from.params.id) {
    userData.value = await fetchUser(to.params.id)
  }
})

// same as beforeRouteLeave option but with no access to `this`
onBeforeRouteLeave((to, from) => {
  const answer = window.confirm(
    'Do you really want to leave? you have unsaved changes!'
  )
  // cancel the navigation and stay on the same page
  if (!answer) return false
})
</script>

3.x The Full Navigation Resolution Flow

  1. Navigation triggered.
  2. Call beforeRouteLeave guards in deactivated components.
  3. Call global beforeEach guards.
  4. Call beforeRouteUpdate guards in reused components.
  5. Call beforeEnter in route configs.
  6. Resolve async route components.
  7. Call beforeRouteEnter in activated components.
  8. Call global beforeResolve guards.
  9. Navigation is confirmed.
  10. Call global afterEach hooks.
  11. DOM updates triggered.
  12. Call callbacks passed to next in beforeRouteEnter guards with instantiated instances.

X. 使用路由

引入

import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()

0. 使用路由的步骤:

  1. 触发路由变化
    1. 使用 <RouterLink> 标签
    2. 使用 router.push() / router.replace() / router.go()
  2. 显示对应的组件:<RouterView>

1.1 RouterLink

<RouterLink
  activeClass="border-indigo-500"
  exactActiveClass="border-indigo-700"
  to="/"
>

1.2 编程式路由导航 router

router.push() / router.replace() 返回 Promise。

router.push('/users/eduardo')

router.push({ path: '/users/eduardo' })

// named route with params to let the router build the url
router.push({ name: 'user', params: { username: 'eduardo' } })
// params will be ignored with path
// params value should be String / Nubmer / Array<String | Nubmer>
// optional params: use '' or null to remove

// /register?plan=private
router.push({ path: '/register', query: { plan: 'private' } })

// /about#team
router.push({ path: '/about', hash: '#team' })
router.push({ path: '/home', replace: true })
// equivalent to
router.replace({ path: '/home' })

router.go()

// router.forward()
router.go(1)

// router.back()
router.go(-1)

// go forward by 3 records
router.go(3)

// fails silently if there aren't that many records
router.go(-100)
router.go(100)

2. <RouterView>

2.0 基本使用

<RouterView />

2.1 Named Views

<router-view class="view left-sidebar" name="LeftSidebar" />
<router-view class="view main-content" /> <!-- default -->
<router-view class="view right-sidebar" name="RightSidebar" />
{
  path: '/',
  components: {
	default: Home,
	// they match the `name` attribute on `<router-view>`
	LeftSidebar,
	RightSidebar,
  },
},

2.2 RouterView slot

<router-view v-slot="{ Component }">
  <component :is="Component" />
</router-view>
KeepAlive & Transition
<router-view v-slot="{ Component, route }">
  <transition :name="route.meta.transition || 'fade'"> <!-- 在 meta 里设置 -->
    <keep-alive>
      <component :is="Component" />
    </keep-alive>
  </transition>
</router-view>
router.afterEach((to, from) => {
  const toDepth = to.path.split('/').length
  const fromDepth = from.path.split('/').length
  to.meta.transition = toDepth < fromDepth ? 'slide-right' : 'slide-left'
})
Passing props and slots
<router-view v-slot="{ Component }">
  <component :is="Component" some-prop="a value">
    <p>Some slotted content</p>
  </component>
</router-view>
Template refs
<router-view v-slot="{ Component }">
  <component :is="Component" ref="mainContent" />
</router-view>

Links

VueRouter