vue-router基本使用
-
配置路由文件
// 路由配置文件router/index.js // 导入vue和vue-router import Vue from 'vue' import VueRouter from 'vue-router' // 导入首页视图文件 import Index from '../views/Index.vue' // 注册路由插件 // Vue.use()用来注册插件,会调用传入对象的install方法 Vue.use(VueRouter) // 定义路由规则,即URL路径与视图的映射关系 const routes = [ { path: '/', name: 'Index', // 展示首页 component: Index }, { path: '/blog', name: 'Blog', // 路由层面的代码分割,为这个路径生成一个独立的代码块 // 当该路径被访问时实现懒加载功能 conponent: () => import(/* webpackChunkName: "blog" */'../views/Blog.vue') } ... ] // 创建路由对象router,传入路由规则 const router = new VueRouter({ // 默认模式是hash模式,不需要显示配置 routes }) // 导出路由对象 export default router
-
入口文件中导入路由对象,传给Vue构造函数实例化
-
$router用来控制路由,$route作为$router的数据映射仓库使用
import Vue from 'vue' // 导入根组件 import App from './App.vue' // 导入路由对象 import router from './router' // 生产环境不出现提示信息 Vue.config.productionTip = false // 实例化Vue,挂载应用 new Vue({ // 传入router对象,将会给Vue实例注入$route和$router两个属性 router, render: h => h(App) }).$mount('#app')
-
-
在视图文件中,创建路由渲染的占位组件
// App.vue <template> <div id="app"> // 路由跳转,相当于<a>标签 <router-link to="/">Index</router-link> <router-link to="/blog">Blog</router-link> // 路由渲染的占位组件,当匹配到某个路径时,渲染对应的组件 <router-view /> </div> </template> <script> // ... </script> <style> // ... </style>
动态路由传参
const routes = [
{
// 动态路由中id作为参数,接收从URL传来的数据
path: '/detail/:id',
name: 'Detail',
// 开启props,会把URL中的参数传递给组件,组件通过props来接收
props: true,
component: () => import(/* webpackChunkName: "detail" */'../views/Detail.vue')
}
]
组件代码
// Detai.vue
<template>
// 方式一:通过当前路由规则获取路由参数
<div>{{ $route.params.id }}</div>
// 方式二:通过props接收路由参数
<div>{{ id }}</div>
</template>
<script>
export default {
name: 'Detail',
// props声明:接收参数id,这与第二种方式接收路由参数相对应
props: ['id']
}
</script>
推荐使用props来接收动态路由参数。第一种通过$route.params
来接收路由参数,使得组件与路由强耦合,不利于组件的灵活使用与维护。
路由中的查询字符串
使用$route.query来获取到查询字符串对象。
嵌套路由
当需要有两个组件嵌套时,可以使用嵌套路由来同时展示。例如,layout.vue组件负责header和footer部分,而中间content部分则嵌套其他组件,根据路由的不同,content嵌套不同的组件。因此,content部分可以使用<router-view >
来加载不同的组件。
- 路径的匹配是由嵌套组件的路径组合来决定的。
- 当路径匹配时,嵌套组件都会加载,将组件渲染到
<router-view >
// 路由配置文件
const routes = [
...,
{
path: '/',
component: Layout,
// 嵌套路由配置,children里的path可以是相对路径,也可以是绝对路径
children: [
{
name: 'index',
// 当路径为/时,匹配到Index组件
path: '',
component: Index
},
{
name: 'detail',
// 当路径是/detail/:id时,匹配到Detail组件
path: 'detail/:id',
// @是src的别名
component: () => import('@/views/Detail.vue')
}
]
}
]
编程式导航
调用$router的导航方法,如push、replace、go等等。
这些方法可以接收location对象和回调函数:(location, onComplete?, onAbort) => {}
-
location可以是表示路径的字符串,也可以是更具体的对象
// location对象 { // 在routes中配置的组件名称 name, // 路径字符串,同时提供path和params时,params不生效,同时path必须是完整的带参数的路径 path, // 动态路由参数 params: { key: value }, // 查询字符串 query: { key: value } }
-
go方法只需要一个数值型参数即可。
hash与history模式
表现形式
- hash:基于锚点,路径中带#
- history:正常的路径,但需要服务器配合
触发事件
- hash:hashchange事件
- history:popstate事件监听前进后退操作,pushState和replaceState方法
- 调用history.pushState()或history.replaceState()不会触发popstate事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退按钮(或者在Javascript代码中调用history.back()或者history.forward()方法)。
History模式
单页应用中通过JavaScript来访问(pushState之类的方法不会发送请求)没有问题,但如果通过浏览器地址栏来访问时,浏览器会发送请求给服务器,则会造成404情况。
因此,服务器出了静态资源外,都返回单页应用的index.html。
配置404页面路由
// 路由配置文件
const routes = [
...,
// 当以上所有路径都不匹配时,是一个未匹配路径,则用*捕获该路径,显示404页面
{
path: '*',
name: '404',
component: () => import('../views/404.vue')
}
]
const router = new VueRouter({
mode: 'history',
routes
})
History模式下,node.js服务器的配置
// app.js,express框架开发的服务器
const path = require('path')
// 导入处理history模式的中间件模块
const history = require('connect-history-api-fallback')
const express = require('express')
const app = express()
// 处理单页应用在history模式下的请求
app.use(history())
app.use(express.static(path.join(__dirname, 'web')))
app.listen(3000, () => {
console.log('监听port:3000');
})
nginx服务器配置
修改nginx配置文件
server {
location / {
root html;
index index.html index.htm
// 这里要开启history模式对应的配置
try_files $uri $uri/ /index.html
}
}
模拟Vue Router实现
URL变化的几种方式
- 手动修改URL:浏览器会发送请求,服务端返回index.html,然后重新加载运行脚本,这样就会将当前URL对应的组件渲染出来。
- 通过链接元素<a>修改URL:
- 在hash模式下触发hashchange事件
- 在history模式下没有对应的监控URL变化事件,只能拦截<a>元素的点击事件阻止跳转
e.preventDefault()
,使用history对象的pushState或replaceState方法,然后手动修改路由实例中表示当前路径的data.current,从而触发重新渲染。
- 通过浏览器的前进后退或history.forward()或history.back()方法修改URL(这些行为通常的请求会使用缓存资源,所以不会和服务端通信),则会触发popstate事件,在该事件内修改路由实例中表示当前路径的data.current,触发渲染。
思路
将URL的变化更新到路由实例的data.current上,由于current属性是响应式的,所以一旦current属性更新,则会引起重新渲染,将current对应的组件渲染到页面。
hash模式
- URL中#后的内容作为路由地址。
- 通过location.url改变地址栏
- #后的内容更改不会发送请求,但会更新到历史记录
- 监听hashchange事件
- 根据当前路由地址找到对应组件,重新渲染
history模式
- 通过history.pushState()或history.replaceState()方法改变地址栏
- 不会发送请求
- 更新到历史记录
- 不触发popstate事件
- 不触发hashchange事件,即使hash真的有改变
- 监听popstate事件
- 浏览器行为会触发popstate事件,如点击后退或前进按钮
- 调用history.forward()或history.back()才会触发该事件。
- 根据当前路由地址找到对应组件,重新渲染
核心代码实现
- 根据Vue-Router的使用情况来确定如何实现
// 导入Vue-Router, 说明Vue-Router需要默认导出一个成员
import VueRouter from 'vue-router'
// Vue.use()方法注册VueRouter插件
// 插件可以是一个函数,也可以是一个对象
Vue.use(VueRouter)
// 创建路由对象,说明默认导出的是一个类
const router = new VueRouter({
// 注册routes规则表
routes: []
})
new Vue({
// 创建Vue实例时,传入router实例
router,
render: h => h(App)
}).$mount('#app')
Vue.use()
接收函数的时候,会直接调用该函数。如果接收的是对象,则会调用对象的install()方法
实现过程
-
先给出类图,即类中的成员表
- 中间的是属性,下面的是方法。+表示公共成员,_表示私有成员
options
:记录构造函数传入的选项对象routeMap
:记录路由地址与组件的映射关系,将routes定义的路由规则表解析后存储到routeMap中。data
:data.current
定义当前的路由地址,data对象的目的是需要一个响应式的对象,当路由地址发生变化时,组件需要自动更新。使用Vue.observable( object )
方法即可。Constructor(Options): VueRouter
install(Vue): void
:Vue的插件机制调用的init(): void
:调用下面的三个方法initEvent(): void
:注册popState事件createRouteMap(): void
:初始化routeMapinitComponents(Vue): void
:创建<router-link />
和<router-view />
这两个组件
- 中间的是属性,下面的是方法。+表示公共成员,_表示私有成员
-
第一步,实现_install方法
let _Vue = null // 声明一个变量来接收Vue,因为后面要用到Vue来做很多事 // 导出一个类,包含一个静态的install方法 export default class VueRouter { // install()接收Vue构造函数和可选的options static install (Vue, options) { // 判断当前插件是否已经安装,用静态属性来存储安装状态最合适 if (VueRouter.install.installed) { return } VueRouter.install.installed = true // 2. 把Vue构造函数记录到变量中,因为VueRouter还需要利用Vue的其他方法做些事,如Vue.component()来创建<router-link />和<router-view />组件 _Vue = Vue // 3. 将实现挂载时创建的Vue实例传入的VueRouter实例注入到所有Vue实例上,这样每个Vue实例(组件本来就是Vue实例)的$router属性指向该VueRouter实例 // 让所有实例都共享的引用,毫无疑问应该定义在原型上,所以这里使用_Vue.prototype.$router // $router的值应该是创建Vue实例时传入的options.router,即实例可以用vm.$options.router拿到该值 // 但是如何获取到挂载时创建的Vue实例呢? // 使用全局混入(一般写插件用,业务代码不推荐),使得接下来的所有Vue实例化时都会使用混入的自定义逻辑,比如这里自定义beforeCreate钩子函数 // 所有的生命周期钩子自动绑定 this 上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 _Vue.mixin({ beforeCreate () { // 该方法会在每个Vue实例化(包括组件)都执行一次,但实际上我们只要执行一次就够了,因为是挂载到原型上的 // 生命周期钩子的this指向实例 if (this.$options.router) { // 这里的含义是,传入选项包含router时,才执行原型挂载,否则不执行,比如组件通常不传入router选项 _Vue.prototype.$router = this.$options.router // 在Vue实例化时才调用router的初始化方法 this.$options.router.init() } } }) }
- 首先需要判断插件是否安装
- 其次,保存Vue构造函数到一个变量中
- 最后,将router实例注入到每个Vue实例中,这里使用了全局混入的方法来达到目的。
-
实现构造函数contructor
constructor (options) { // options属性来保存传入的选项对象 this.options = options // routeMap来保存路径与组件的映射关系 this.routeMap = {} // 这里的data属性由于是用Vue.observable()创建,被Vue加入了响应式系统,从而data的更新会导致视图的更新 // data.current的默认值应该根据当前浏览器路径来判断,而不是首页 this.data = _Vue.observable({ // current属性来保存当前浏览器地址栏的路径 current: window.location.pathname }) }
-
创建createMap方法,遍历routes规则数组,将path与组件映射关系存入routeMap属性
// 把options.routes转换到this.routeMap,键为path,值为组件 createMap () { this.options.routes.forEach(route => { this.routeMap[route.path] = route.component }) }
-
创建initComponent方法,用于创建全局组件
<router-link />
和<router-view />
initComponents (Vue) { const self = this // 全局组件 Vue.component('router-link', { props: { to: String }, render (h) { return h('a', { attrs: { href: this.to }, // 给<a>添加点击事件,阻止浏览器跳转 on: { click: this.clickHandle } }, [this.$slots.default]) }, methods: { clickHandle (e) { // 更新浏览器地址栏和历史记录 history.pushState({}, '', this.to) // 更新当前router的data.current this.$router.data.current = this.to // 阻止浏览器跳转 e.preventDefault() } } }) Vue.component('router-view', { render (h) { const component = self.routeMap[self.data.current] // 异步组件也可以直接传入h()中渲染 return h(component) } }) }
-
创建initEvents方法,用于监听popstate事件,更新data.current
initEvents () { window.addEventListener('popstate', () => { this.data.current = window.location.pathname }) }
-
创建init方法,统一调用初始化要用到的方法
init () { this.createMap() this.initComponents(_Vue) this.initEvents() }
路由匹配的原理
Vue-Router内部使用了 path-to-regexp库,将路由表中的path字符串转换为正则表达式,然后用正则表达式来匹配URL中的pathname,得到匹配信息与路径参数的值
path-to-regexp的使用
安装path-to-regexp,下面是基本使用的示例代码
const pathToRegexp = require('path-to-regexp').pathToRegexp
// pathToRegexp(path, keys, options),options选项对象可以不传
// keys数组用来记录匹配过程中得到的捕获组的key
const keys = []
// 返回一个正则表达式/^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i
let re = pathToRegexp('/foo/:bar', keys)
/* keys的值,每个对象都记录了原始字符串中的路径参数
[
{
name: 'bar',
prefix: '/',
suffix: '',
pattern: '[^\\/#\\?]+?',
modifier: ''
}
]
*/
console.log(keys)
/* re.exec('/foo/baz')的执行结果
[
'/foo/baz',
'baz',
index: 0,
input: '/foo/randal',
groups: undefined
]
*/
console.log(re.exec('/foo/baz'))
const match = re.exec('/foo/baz')
// url来记录当前被匹配的字符串, values则记录捕获到的值
const [url, ...values] = match
// 将路径参数与捕获到的值存储到params对象中
const params = keys.reduce((cache, key, index) => {
cache[key.name] = values[index]
return cache
}, {})
// { bar: 'baz' }
console.log(params)